changeset 153:3036f2e6a022

Add module-specific data support. This adds support for a safe form of `pam_get_data` and `pam_set_data`, where data is (as best as humanly possible) type-safe and restricted to only the module where it was created.
author Paul Fisher <paul@pfish.zone>
date Tue, 08 Jul 2025 00:31:54 -0400
parents 4d11b2e7da83
children f71bfffb6de1
files libpam-sys/src/lib.rs src/handle.rs src/libpam/handle.rs
diffstat 3 files changed, 228 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
--- a/libpam-sys/src/lib.rs	Mon Jul 07 19:05:31 2025 -0400
+++ b/libpam-sys/src/lib.rs	Tue Jul 08 00:31:54 2025 -0400
@@ -139,7 +139,7 @@
 
     /// Gets module-specific data. PAM still owns the data.
     pub fn pam_get_data(
-        pamh: *mut pam_handle,
+        pamh: *const pam_handle,
         module_data_name: *const c_char,
         data: *mut *const c_void,
     ) -> c_int;
--- a/src/handle.rs	Mon Jul 07 19:05:31 2025 -0400
+++ b/src/handle.rs	Tue Jul 08 00:31:54 2025 -0400
@@ -235,6 +235,153 @@
     #[doc = stdlinks!(3 pam_get_authtok)]
     fn old_authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString>;
 
+    /// Gets an item of module-specific data stored over the transaction.
+    ///
+    /// This gives you a reference to the data that was earlier set with
+    /// [`Self::set_module_data`]. If not present, you get `None`.
+    ///
+    /// Data is in a module-specific, type-specific namespace.
+    ///
+    /// ```
+    /// # use nonstick::ModuleClient;
+    /// # use std::path::PathBuf;
+    /// # fn test(client: &impl ModuleClient) {
+    /// // These two can coexist and do not overlap.
+    /// let str_data: Option<&String> = client.get_module_data("the_key");
+    /// let num_data: Option<&u64> = client.get_module_data("the_key");
+    /// // ...
+    /// let nothing_data: Option<&PathBuf> = client.get_module_data("this does not exist");
+    /// # }
+    /// ```
+    ///
+    /// # References
+    ///
+    #[doc = linklist!(pam_get_data: mwg, _std)]
+    ///
+    #[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_get_data")]
+    #[doc = stdlinks!(3 pam_get_data)]
+
+    fn get_module_data<T: 'static>(&self, key: &str) -> Option<&T>;
+
+    /// Sets module-specific data.
+    ///
+    /// A PAM module may need to store data across multiple invocations within
+    /// the same PAM transaction. For instance, a module that stores credentials
+    /// would need to know where those credentials were stored in order to
+    /// update or destroy them later. Also see [`Self::get_module_data`].
+    ///
+    /// Module-specific data gives a module a way to store such data.
+    /// Data are stored in a module-specific, type-specific namespace.
+    ///
+    /// PAM takes ownership of the data passed in. See the **Cleanup** section
+    /// below for details on how cleanup is handled.
+    ///
+    /// # Examples
+    ///
+    /// Each type of data is in a separate namespace:
+    ///
+    /// ```
+    /// # use nonstick::{ModuleClient, Result};
+    /// # fn test(client: &mut impl ModuleClient) -> Result<()> {
+    /// client.set_module_data("count", 999i32)?;
+    ///
+    /// let count_int: Option<&i32> = client.get_module_data("count");
+    /// // count_int = Some(&999i32)
+    /// let count_string: Option<&String> = client.get_module_data("count");
+    /// // count_string = None
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// Data persist across invocations of the same module:
+    ///
+    /// ```
+    /// # use nonstick::{ModuleClient, Result};
+    /// // In a pam_authenticate call, this function is called:
+    /// fn authenticate(client: &mut impl ModuleClient) -> Result<()> {
+    ///     client.set_module_data::<u64>("TOKEN_ID", 0x0fa1afe10000beef)?;
+    ///     Ok(())
+    /// }
+    ///
+    /// // Later, in a pam_session_start call:
+    /// fn start_session(client: &mut impl ModuleClient) -> Result<()> {
+    ///     match client.get_module_data::<u64>("TOKEN_ID") {
+    ///         Some(&tid) => {
+    ///             // This will execute and tid will be 0x0fa1afe10000beef.
+    ///         },
+    ///         None => { /* This will not execute. */ },
+    ///     }
+    ///     Ok(())
+    /// }
+    /// ```
+    ///
+    /// Each module has its own set of data:
+    ///
+    /// ```
+    /// # use nonstick::{ModuleClient, Result};
+    /// // This function is called somewhere in pam_module_a.so.
+    /// fn in_pam_module_a(client: &mut impl ModuleClient) -> Result<()> {
+    ///     client.set_module_data("value", String::from("pam_module_a data"))?;
+    ///     Ok(())
+    /// }
+    ///
+    /// // This function is called later in pam_module_b.so.
+    /// fn in_pam_module_b(client: &mut impl ModuleClient) -> Result<()> {
+    ///     match client.get_module_data::<String>("value") {
+    ///         Some(value) => {
+    ///             // This will match, because pam_module_a's data
+    ///             // is completely unrelated to pam_module_b's data.
+    ///         },
+    ///         None => {
+    ///             // This branch will execute.
+    ///         },
+    ///     }
+    ///     // ...
+    /// #    Ok(())
+    /// }
+    /// ```
+    ///
+    /// # Cleanup
+    ///
+    /// PAM modules should be careful about cleaning up data outside their own
+    /// address space, because PAM applications may `fork()`:
+    ///
+    /// ```plain
+    /// ┃ let tx = start_pam_transaction();
+    /// ┃
+    /// ┃ tx.authenticate();
+    /// ┃ │ // PAM calls into your module where you set data:
+    /// ┃ │ handle.set_module_data("key", the_data);
+    /// ┃
+    /// ┃ fork();
+    /// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+    /// Parent process             Child process
+    /// ┃ wait(child);             ┃ setuid(user_to_login);
+    /// ┃ ┆                        ┃ // ... do other stuff ...
+    /// ┃ ┆                        ┃ drop(tx);
+    /// ┃ ┆                        ┃ │ // PAM cleans up your data.
+    /// ┃ ┆                        ┃ │ drop(the_data);
+    /// ┃ ┆                        ┗ exec(user's shell)
+    /// ┃ ┆                          ┃ // user does stuff over their session
+    /// ┃ ┆                          ┃ // ...
+    /// ┃ ┆                          X
+    /// ┃
+    /// ┃ drop(tx);
+    /// ┃ │ // Parent PAM cleans up your data.
+    /// ┃ │ drop(the_data);  // Called again, but in this process instead!
+    /// ```
+    ///
+    /// While LibPAM offers a way to customize the action taken on cleanup,
+    /// we do not (yet) offer this.
+    ///
+    /// # References
+    ///
+    #[doc = linklist!(pam_set_data: mwg, _std)]
+    ///
+    #[doc = guide!(mwg: "mwg-expected-by-module-item.html#mwg-pam_set_data")]
+    #[doc = stdlinks!(3 pam_set_data)]
+    fn set_module_data<T: 'static>(&mut self, key: &str, data: T) -> Result<()>;
+
     getter!(
         /// Gets the user's authentication token (e.g., password).
         ///
--- a/src/libpam/handle.rs	Mon Jul 07 19:05:31 2025 -0400
+++ b/src/libpam/handle.rs	Tue Jul 08 00:31:54 2025 -0400
@@ -1,5 +1,5 @@
 use super::conversation::{OwnedConversation, PamConv};
-use crate::_doc::{guide, linklist, stdlinks};
+use crate::_doc::{guide, linklist, man7, stdlinks};
 use crate::constants::{ErrorCode, Result};
 use crate::conv::Exchange;
 use crate::environ::EnvironMapMut;
@@ -12,8 +12,9 @@
 use crate::{Conversation, EnvironMap, Flags, ModuleClient, Transaction};
 use libpam_sys_consts::constants;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
+use std::any::TypeId;
 use std::cell::Cell;
-use std::ffi::{c_char, c_int, CString, OsStr, OsString};
+use std::ffi::{c_char, c_int, c_void, CString, OsStr, OsString};
 use std::mem::ManuallyDrop;
 use std::os::unix::ffi::OsStrExt;
 use std::ptr;
@@ -107,17 +108,37 @@
         })
     }
 
-    /// "Quietly" closes the PAM session on an owned PAM handle.
+    #[cfg_attr(
+        pam_impl = "LinuxPam",
+        doc = "Ends the PAM transaction \"quietly\" (on Linux-PAM only)."
+    )]
+    #[cfg_attr(
+        not(pam_impl = "LinuxPam"),
+        doc = "Exactly equivalent to `drop(self)` (except on Linux-PAM)."
+    )]
     ///
-    /// This internally calls `pam_end` with the appropriate error code.
+    /// On Linux-PAM, this is equivalent to passing the `PAM_DATA_SILENT` flag
+    /// to [`pam_end` on Linux-PAM][man7], which signals that data cleanup
+    /// should "not treat the call too seriously" \[sic].
+    ///
+    /// On other platforms, this is no different than letting the transaction
+    /// end on its own.
     ///
-    /// # References
-    #[doc = linklist!(pam_end: adg, _std)]
-    ///
-    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
-    #[doc = stdlinks!(3 pam_end)]
+    #[doc = man7!(3 pam_end)]
+    pub fn end_silent(self) {
+        #[cfg(pam_impl = "LinuxPam")]
+        {
+            let mut me = ManuallyDrop::new(self);
+            me.end_internal(libpam_sys::PAM_DATA_SILENT);
+        }
+        // If it's not LinuxPam, we just drop normally.
+    }
 
-    fn end_quiet(self) {}
+    /// Internal "end" function, which binary-ORs the status with `or_with`.
+    fn end_internal(&mut self, or_with: i32) {
+        let result = ErrorCode::result_to_c(self.last_return.get()) | or_with;
+        unsafe { libpam_sys::pam_end(self.handle.raw_mut(), result) };
+    }
 }
 
 macro_rules! wrap {
@@ -150,12 +171,7 @@
     #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
     #[doc = stdlinks!(3 pam_end)]
     fn drop(&mut self) {
-        unsafe {
-            libpam_sys::pam_end(
-                self.handle.raw_mut(),
-                ErrorCode::result_to_c(self.last_return.get()),
-            );
-        }
+        self.end_internal(0)
     }
 }
 
@@ -273,7 +289,7 @@
     ///
     #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
     #[doc = stdlinks!(3 pam_end)]
-    pub fn end_quiet(self, result: Result<()>) {
+    pub fn end_silent(self, result: Result<()>) {
         let mut me = ManuallyDrop::new(self);
         let result = ErrorCode::result_to_c(result);
         #[cfg(pam_impl = "LinuxPam")]
@@ -390,6 +406,37 @@
         self.get_authtok(prompt, ItemType::OldAuthTok)
     }
 
+    fn get_module_data<T: 'static>(&self, key: &str) -> Option<&T> {
+        // It's technically unsafe to do this, but we assume that other modules
+        // aren't going to go out of their way to find the key we've used
+        // and corrupt its value's data.
+        let full_key = module_data_key::<T>(key);
+        let mut ptr: *const c_void = ptr::null();
+        unsafe {
+            ErrorCode::result_from(libpam_sys::pam_get_data(
+                self.raw_ref(),
+                full_key.as_ptr(),
+                &mut ptr,
+            ))
+            .ok()?;
+
+            (ptr as *const T).as_ref()
+        }
+    }
+
+    fn set_module_data<T: 'static>(&mut self, key: &str, data: T) -> Result<()> {
+        let full_key = module_data_key::<T>(key);
+        let data = Box::new(data);
+        ErrorCode::result_from(unsafe {
+            libpam_sys::pam_set_data(
+                self.raw_mut(),
+                full_key.as_ptr(),
+                Box::into_raw(data).cast(),
+                drop_module_data::<T>,
+            )
+        })
+    }
+
     fn authtok_item(&self) -> Result<Option<OsString>> {
         unsafe { items::get_cstr_item(self, ItemType::AuthTok) }
     }
@@ -398,13 +445,27 @@
     }
 }
 
+/// Constructs a type-specific, module-specific key for this data.
+fn module_data_key<T: 'static>(key: &str) -> CString {
+    // The type ID is unique per-type.
+    let tid = TypeId::of::<T>();
+    // The `set_data_cleanup` function lives statically inside each PAM module,
+    // so its address will be different between `pam_a.so` and `pam_b.so`,
+    // even if both modules .so files are byte-for-byte identical.
+    let cleanup_addr = drop_module_data::<T> as usize;
+    // Then, by adding the key,
+    let key = format!("{key:?}::{tid:?}::{cleanup_addr:016x}");
+    CString::new(key).expect("null bytes somehow got into a debug string?")
+}
+
 /// Function called at the end of a PAM session that is called to clean up
 /// a value previously provided to PAM in a `pam_set_data` call.
 ///
 /// You should never call this yourself.
-extern "C" fn set_data_cleanup<T>(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) {
+extern "C" fn drop_module_data<T>(_: *mut libpam_sys::pam_handle, c_data: *mut c_void, _: c_int) {
     unsafe {
-        let _data: Box<T> = Box::from_raw(c_data.cast());
+        // Adopt the pointer into a Box and immediately drop it.
+        let _: Box<T> = Box::from_raw(c_data.cast());
     }
 }