# HG changeset patch # User Paul Fisher # Date 1751949114 14400 # Node ID 3036f2e6a0227a15c52df1b9a27661e61194e329 # Parent 4d11b2e7da83a9e8dac07d3c4e3a1ab960e1a551 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. diff -r 4d11b2e7da83 -r 3036f2e6a022 libpam-sys/src/lib.rs --- 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; diff -r 4d11b2e7da83 -r 3036f2e6a022 src/handle.rs --- 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; + /// 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(&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::("TOKEN_ID", 0x0fa1afe10000beef)?; + /// Ok(()) + /// } + /// + /// // Later, in a pam_session_start call: + /// fn start_session(client: &mut impl ModuleClient) -> Result<()> { + /// match client.get_module_data::("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::("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(&mut self, key: &str, data: T) -> Result<()>; + getter!( /// Gets the user's authentication token (e.g., password). /// diff -r 4d11b2e7da83 -r 3036f2e6a022 src/libpam/handle.rs --- 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(&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::(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(&mut self, key: &str, data: T) -> Result<()> { + let full_key = module_data_key::(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::, + ) + }) + } + fn authtok_item(&self) -> Result> { 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(key: &str) -> CString { + // The type ID is unique per-type. + let tid = TypeId::of::(); + // 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:: 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(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) { +extern "C" fn drop_module_data(_: *mut libpam_sys::pam_handle, c_data: *mut c_void, _: c_int) { unsafe { - let _data: Box = Box::from_raw(c_data.cast()); + // Adopt the pointer into a Box and immediately drop it. + let _: Box = Box::from_raw(c_data.cast()); } }