Mercurial > crates > nonstick
view src/libpam/handle.rs @ 154:f71bfffb6de1
fix remaining AppDatas in libpam-sys
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 08 Jul 2025 00:42:09 -0400 |
parents | 3036f2e6a022 |
children | ab8020566cd9 |
line wrap: on
line source
use super::conversation::{OwnedConversation, PamConv}; use crate::_doc::{guide, linklist, man7, stdlinks}; use crate::constants::{ErrorCode, Result}; use crate::conv::Exchange; use crate::environ::EnvironMapMut; use crate::handle::PamShared; use crate::items::{Items, ItemsMut}; use crate::libpam::environ::{LibPamEnviron, LibPamEnvironMut}; use crate::libpam::items::{LibPamItems, LibPamItemsMut}; use crate::libpam::{items, memory}; use crate::logging::{Level, Location}; 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, c_void, CString, OsStr, OsString}; use std::mem::ManuallyDrop; use std::os::unix::ffi::OsStrExt; use std::ptr; use std::ptr::NonNull; /// An owned PAM handle. pub struct LibPamTransaction<C: Conversation> { /// The handle itself. handle: ManuallyDrop<LibPamHandle>, /// The last return value from the handle. last_return: Cell<Result<()>>, /// If set, the Conversation that this PAM handle owns. /// /// We have to hold on to this because the PAM specification doesn't /// actually say what the PAM library should do with a passed-in /// conversation. Linux-PAM copies the contents of the `pam_conv` struct /// that you pass in to `pam_start`, but OpenPAM uses the pointer itself, /// so you have to keep it in one place. conversation: Box<OwnedConversation<C>>, } #[derive(Debug, PartialEq)] pub struct TransactionBuilder { service_name: OsString, username: Option<OsString>, } impl TransactionBuilder { /// Updates the service name. pub fn service_name(mut self, service_name: OsString) -> Self { self.service_name = service_name; self } /// Sets the username. Setting this will avoid the need for an extra /// round trip through the conversation and may otherwise improve /// the login experience. pub fn username(mut self, username: OsString) -> Self { self.username = Some(username); self } /// Builds a PAM handle and starts the transaction. pub fn build(self, conv: impl Conversation) -> Result<LibPamTransaction<impl Conversation>> { LibPamTransaction::start(self.service_name, self.username, conv) } } impl<C: Conversation> LibPamTransaction<C> { /// Creates a builder to start a PAM transaction for the given service. /// /// The service name is what controls the steps and checks PAM goes through /// when authenticating a user. This corresponds to the configuration file /// named <code>/etc/pam.d/<var>service_name</var></code>. /// /// # References #[doc = linklist!(pam_start: adg, _std)] /// #[doc = stdlinks!(3 pam_start)] #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_start")] pub fn build_with_service(service_name: OsString) -> TransactionBuilder { TransactionBuilder { service_name, username: None, } } fn start(service_name: OsString, username: Option<OsString>, conversation: C) -> Result<Self> { let conv = Box::new(OwnedConversation::new(conversation)); let service_cstr = CString::new(service_name.as_bytes()).expect("null is forbidden"); let username_cstr = memory::option_cstr_os(username.as_deref()); let username_cstr = memory::prompt_ptr(username_cstr.as_deref()); let mut handle: *mut libpam_sys::pam_handle = ptr::null_mut(); // SAFETY: We've set everything up properly to call `pam_start`. // The returned value will be a valid pointer provided the result is OK. let result = unsafe { libpam_sys::pam_start( service_cstr.as_ptr(), username_cstr, (conv.as_ref() as *const OwnedConversation<C>) .cast_mut() .cast(), &mut handle, ) }; ErrorCode::result_from(result)?; let handle = NonNull::new(handle).ok_or(ErrorCode::BufferError)?; Ok(Self { handle: ManuallyDrop::new(LibPamHandle(handle)), last_return: Cell::new(Ok(())), conversation: conv, }) } #[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)." )] /// /// 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. /// #[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. } /// 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 { (fn $name:ident { $pam_func:ident }) => { fn $name(&mut self, flags: Flags) -> Result<()> { ErrorCode::result_from(unsafe { libpam_sys::$pam_func(self.0.as_mut(), flags.bits()) }) } }; } impl Transaction for LibPamHandle { wrap!(fn authenticate { pam_authenticate }); wrap!(fn account_management { pam_acct_mgmt }); wrap!(fn change_authtok { pam_chauthtok }); } // TODO: pam_setcred - app // pam_open_session - app // pam_close_session - app // pam_set/get_data - module impl<C: Conversation> Drop for LibPamTransaction<C> { /// Closes the PAM session on an owned PAM handle. /// /// This internally calls `pam_end` with the appropriate error code. /// /// # References #[doc = linklist!(pam_end: adg, _std)] /// #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")] #[doc = stdlinks!(3 pam_end)] fn drop(&mut self) { self.end_internal(0) } } macro_rules! delegate { // First have the kind that save the result after delegation. (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => { fn $meth(&self $(, $param: $typ)*) -> Result<$ret> { let result = self.handle.$meth($($param),*); self.last_return.set(split(&result)); result } }; (fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => { fn $meth(&mut self $(, $param: $typ)*) -> Result<$ret> { let result = self.handle.$meth($($param),*); self.last_return.set(split(&result)); result } }; // Then have the kind that are just raw delegates (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> $ret:ty) => { fn $meth(&self $(, $param: $typ)*) -> $ret { self.handle.$meth($($param),*) } }; (fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> $ret:ty) => { fn $meth(&mut self $(, $param: $typ)*) -> $ret { self.handle.$meth($($param),*) } }; // Then have item getters / setters (get = $get:ident$(, set = $set:ident)?) => { delegate!(fn $get(&self) -> Result<Option<OsString>>); $(delegate!(set = $set);)? }; (set = $set:ident) => { delegate!(fn $set(&mut self, value: Option<&OsStr>) -> Result<()>); }; } fn split<T>(result: &Result<T>) -> Result<()> { result.as_ref().map(drop).map_err(|&e| e) } impl<C: Conversation> Transaction for LibPamTransaction<C> { delegate!(fn authenticate(&mut self, flags: Flags) -> Result<()>); delegate!(fn account_management(&mut self, flags: Flags) -> Result<()>); delegate!(fn change_authtok(&mut self, flags: Flags) -> Result<()>); } impl<C: Conversation> PamShared for LibPamTransaction<C> { delegate!(fn log(&self, level: Level, location: Location<'_>, entry: &str) -> ()); delegate!(fn environ(&self) -> impl EnvironMap); delegate!(fn environ_mut(&mut self) -> impl EnvironMapMut); delegate!(fn username(&mut self, prompt: Option<&OsStr>) -> Result<OsString>); delegate!(fn items(&self) -> impl Items); delegate!(fn items_mut(&mut self) -> impl ItemsMut); } /// An owned variation of a basic PAM handle. /// /// This is the most basic version of a wrapped PAM handle. It's mostly used /// as the inside of the [`LibPamTransaction`], but can also be used to "adopt" /// a PAM handle created by another library. /// /// If [`Self::end`] is not called, this will always call `pam_end` reporting /// successful completion. #[repr(transparent)] pub struct LibPamHandle(NonNull<libpam_sys::pam_handle>); impl LibPamHandle { /// Takes ownership of the pointer to the given PAM handle. /// /// **Do not use this just to get a reference to a PAM handle.** /// /// # Safety /// /// - The pointer must point to a valid PAM handle. /// - The conversation associated with the handle must remain valid /// for as long as the handle is open. pub unsafe fn from_ptr(handle: NonNull<libpam_sys::pam_handle>) -> Self { Self(handle) } /// Ends the transaction, reporting `error_code` to cleanup callbacks. /// /// # References #[doc = linklist!(pam_end: adg, _std)] /// #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")] #[doc = stdlinks!(3 pam_end)] pub fn end(self, result: Result<()>) { let mut me = ManuallyDrop::new(self); unsafe { libpam_sys::pam_end(me.raw_mut(), ErrorCode::result_to_c(result)) }; } #[cfg_attr( not(pam_impl = "LinuxPam"), doc = "Exactly equivalent to [`Self::end`], except on Linux-PAM." )] #[cfg_attr( pam_impl = "LinuxPam", doc = "Ends the transaction \"quietly\", reporting `error_code` to cleanup callbacks." )] /// /// On Linux-PAM only, this sets the /// [`PAM_DATA_SILENT`](libpam_sys::PAM_DATA_SILENT) flag on the flags /// passed to the cleanup callbacks. This conventionally means that this /// `pam_end` call is occurring on a forked process, and that a session /// may still be open on the parent process, and modules "should not treat /// the call too seriously". /// /// # References #[doc = linklist!(pam_end: adg, _std)] /// #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")] #[doc = stdlinks!(3 pam_end)] pub fn end_silent(self, result: Result<()>) { let mut me = ManuallyDrop::new(self); let result = ErrorCode::result_to_c(result); #[cfg(pam_impl = "LinuxPam")] let result = result | libpam_sys::PAM_DATA_SILENT; unsafe { libpam_sys::pam_end(me.raw_mut(), result); } } /// Consumes this and gives you back the raw PAM handle. pub fn into_inner(self) -> NonNull<libpam_sys::pam_handle> { let me = ManuallyDrop::new(self); me.0 } /// Gets a reference to the inner PAM handle. pub fn raw_ref(&self) -> &libpam_sys::pam_handle { unsafe { self.0.as_ref() } } /// Gets a mutable reference to the inner PAM handle. pub fn raw_mut(&mut self) -> &mut libpam_sys::pam_handle { unsafe { self.0.as_mut() } } } impl Drop for LibPamHandle { fn drop(&mut self) { unsafe { libpam_sys::pam_end(self.0.as_mut(), 0) }; } } impl PamShared for LibPamHandle { #[cfg(any())] fn log(&self, level: Level, loc: Location<'_>, entry: &str) { let entry = match CString::new(entry).or_else(|_| CString::new(dbg!(entry))) { Ok(cstr) => cstr, _ => return, }; #[cfg(pam_impl = "LinuxPam")] { _ = loc; // SAFETY: We're calling this function with a known value. unsafe { libpam_sys::pam_syslog(self, level as c_int, "%s\0".as_ptr().cast(), entry.as_ptr()) } } #[cfg(pam_impl = "OpenPam")] { let func = CString::new(loc.function).unwrap_or(CString::default()); // SAFETY: We're calling this function with a known value. unsafe { libpam_sys::_openpam_log( level as c_int, func.as_ptr(), "%s\0".as_ptr().cast(), entry.as_ptr(), ) } } } fn log(&self, _level: Level, _loc: Location<'_>, _entry: &str) {} fn username(&mut self, prompt: Option<&OsStr>) -> Result<OsString> { let prompt = memory::option_cstr_os(prompt); let mut output: *const c_char = ptr::null(); let ret = unsafe { libpam_sys::pam_get_user( self.raw_mut(), &mut output, memory::prompt_ptr(prompt.as_deref()), ) }; ErrorCode::result_from(ret)?; Ok(unsafe { memory::copy_pam_string(output).ok_or(ErrorCode::ConversationError)? }) } fn environ(&self) -> impl EnvironMap { LibPamEnviron::new(self) } fn environ_mut(&mut self) -> impl EnvironMapMut { LibPamEnvironMut::new(self) } fn items(&self) -> impl Items { LibPamItems(self) } fn items_mut(&mut self) -> impl ItemsMut { LibPamItemsMut(self) } } impl Conversation for LibPamHandle { fn communicate(&self, messages: &[Exchange]) { match self.conversation_item() { Ok(conv) => conv.communicate(messages), Err(e) => { for msg in messages { msg.set_error(e) } } } } } impl ModuleClient for LibPamHandle { fn authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString> { self.get_authtok(prompt, ItemType::AuthTok) } fn old_authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString> { 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) } } fn old_authtok_item(&self) -> Result<Option<OsString>> { unsafe { items::get_cstr_item(self, ItemType::OldAuthTok) } } } /// 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 drop_module_data<T>(_: *mut libpam_sys::pam_handle, c_data: *mut c_void, _: c_int) { unsafe { // Adopt the pointer into a Box and immediately drop it. let _: Box<T> = Box::from_raw(c_data.cast()); } } // Implementations of internal functions. impl LibPamHandle { #[cfg(any(pam_impl = "LinuxPam", pam_impl = "OpenPam"))] fn get_authtok(&mut self, prompt: Option<&OsStr>, item_type: ItemType) -> Result<OsString> { let prompt = memory::option_cstr_os(prompt); let mut output: *const c_char = ptr::null_mut(); // SAFETY: We're calling this with known-good values. let res = unsafe { libpam_sys::pam_get_authtok( self.raw_mut(), item_type.into(), &mut output, memory::prompt_ptr(prompt.as_deref()), ) }; ErrorCode::result_from(res)?; // SAFETY: We got this string from PAM. unsafe { memory::copy_pam_string(output) }.ok_or(ErrorCode::ConversationError) } #[cfg(not(any(pam_impl = "LinuxPam", pam_impl = "OpenPam")))] fn get_authtok(&mut self, prompt: Option<&str>, item_type: ItemType) -> Result<String> { Err(ErrorCode::ConversationError) } /// Gets the `PAM_CONV` item from the handle. fn conversation_item(&self) -> Result<&PamConv> { let output: *const PamConv = ptr::null_mut(); let result = unsafe { libpam_sys::pam_get_item( self.raw_ref(), ItemType::Conversation.into(), &mut output.cast(), ) }; ErrorCode::result_from(result)?; // SAFETY: We got this result from PAM, and we're checking if it's null. unsafe { output.as_ref() }.ok_or(ErrorCode::ConversationError) } } /// Identifies what is being gotten or set with `pam_get_item` /// or `pam_set_item`. #[derive(TryFromPrimitive, IntoPrimitive)] #[repr(i32)] #[non_exhaustive] // because C could give us anything! pub enum ItemType { /// The PAM service name. Service = constants::PAM_SERVICE, /// The user's login name. User = constants::PAM_USER, /// The TTY name. Tty = constants::PAM_TTY, /// The remote host (if applicable). RemoteHost = constants::PAM_RHOST, /// The conversation struct (not a CStr-based item). Conversation = constants::PAM_CONV, /// The authentication token (password). AuthTok = constants::PAM_AUTHTOK, /// The old authentication token (when changing passwords). OldAuthTok = constants::PAM_OLDAUTHTOK, /// The remote user's name. RemoteUser = constants::PAM_RUSER, /// The prompt shown when requesting a username. UserPrompt = constants::PAM_USER_PROMPT, #[cfg(feature = "linux-pam-ext")] /// App-supplied function to override failure delays. FailDelay = constants::PAM_FAIL_DELAY, #[cfg(feature = "linux-pam-ext")] /// X display name. XDisplay = constants::PAM_XDISPLAY, #[cfg(feature = "linux-pam-ext")] /// X server authentication data. XAuthData = constants::PAM_XAUTHDATA, #[cfg(feature = "linux-pam-ext")] /// The type of `pam_get_authtok`. AuthTokType = constants::PAM_AUTHTOK_TYPE, }