changeset 141:a508a69c068a

Remove a lot of Results from functions. Many functions are documented to only return failing Results when given improper inputs or when there is a memory allocation failure (which can be verified by looking at the source). In cases where we know our input is correct, we don't need to check for memory allocation errors for the same reason that Rust doesn't do so when you, e.g., create a new Vec.
author Paul Fisher <paul@pfish.zone>
date Sat, 05 Jul 2025 17:16:56 -0400
parents add7228adb2f
children 5c1e315c18ff
files src/environ.rs src/handle.rs src/lib.rs src/libpam/conversation.rs src/libpam/environ.rs src/libpam/handle.rs src/libpam/memory.rs src/libpam/mod.rs src/libpam/module.rs src/libpam/question.rs
diffstat 10 files changed, 458 insertions(+), 396 deletions(-) [+]
line wrap: on
line diff
--- a/src/environ.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/environ.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -3,7 +3,6 @@
 //! PAM modules can set environment variables to apply to a user session.
 //! This module manages the interface for doing all of that.
 
-use crate::constants::Result;
 use std::ffi::{OsStr, OsString};
 
 /// A key/value map for environment variables, as [`OsString`]s.
@@ -15,19 +14,15 @@
     fn get(&self, key: impl AsRef<OsStr>) -> Option<OsString>;
 
     /// Iterates over the key/value pairs of the environment.
-    fn iter(&self) -> Result<impl Iterator<Item = (OsString, OsString)>>;
+    fn iter(&self) -> impl Iterator<Item = (OsString, OsString)>;
 }
 
 pub trait EnvironMapMut<'a>: EnvironMap<'a> {
     /// Sets the environment variable with the given key,
     /// returning the old one if present.
-    fn insert(
-        &mut self,
-        key: impl AsRef<OsStr>,
-        val: impl AsRef<OsStr>,
-    ) -> Result<Option<OsString>>;
+    fn insert(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> Option<OsString>;
 
     /// Removes the environment variable from the map, returning its old value
     /// if present.
-    fn remove(&mut self, key: impl AsRef<OsStr>) -> Result<Option<OsString>>;
+    fn remove(&mut self, key: impl AsRef<OsStr>) -> Option<OsString>;
 }
--- a/src/handle.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/handle.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -317,8 +317,7 @@
     /// Retrieves the authentication token from the user.
     ///
     /// This should only be used by *authentication* and *password-change*
-    /// PAM modules. This is an extension provided by
-    /// both Linux-PAM and OpenPAM.
+    /// PAM modules.
     ///
     /// # References
     ///
@@ -340,6 +339,11 @@
     #[doc = manbsd!(3 pam_get_authtok)]
     fn authtok(&mut self, prompt: Option<&str>) -> Result<String>;
 
+    /// Retrieves the user's old authentication token when changing passwords.
+    ///
+    ///
+    fn old_authtok(&mut self, prompt: Option<&str>) -> Result<String>;
+
     trait_item!(
         /// Gets the user's authentication token (e.g., password).
         ///
@@ -356,44 +360,12 @@
     trait_item!(
         /// Gets the user's old authentication token when changing passwords.
         ///
+        /// This is normally set automatically by PAM when calling
+        /// [`old_authtok`](Self::old_authtok), but can be set explicitly.
+        ///
         /// This should only ever be called by *password-change* PAM modules.
         get = old_authtok_item,
         item = "PAM_OLDAUTHTOK",
         see = PamShared::set_old_authtok_item
     );
-
-    /*
-    TODO: Re-enable this at some point.
-        /// Gets some pointer, identified by `key`, that has been set previously
-        /// using [`set_data`](Self::set_data).
-        ///
-        /// The data, if present, is still owned by the current PAM session.
-        ///
-        /// See the [`pam_get_data` manual page][man]
-        /// or [`pam_get_data` in the Module Writer's Guide][mwg].
-        ///
-        /// # Safety
-        ///
-        /// The data stored under the provided key must be of type `T`,
-        /// otherwise you'll get back a completely invalid `&T`
-        /// and further behavior is undefined.
-        ///
-        /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_data.3.html
-        /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_data
-        unsafe fn get_data<T>(&mut self, key: &str) -> Result<Option<&T>>;
-
-        /// Stores a pointer that can be retrieved later with [`get_data`](Self::get_data).
-        ///
-        /// This data is accessible to this module and other PAM modules
-        /// (using the provided `key`), but is *not* accessible to the application.
-        /// The PAM session takes ownership of the data, and it will be dropped
-        /// when the session ends.
-        ///
-        /// See the [`pam_set_data` manual page][man]
-        /// or [`pam_set_data` in the Module Writer's Guide][mwg].
-        ///
-        /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_data.3.html
-        /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_data
-        fn set_data<T>(&mut self, key: &str, data: Box<T>) -> Result<()>;
-     */
 }
--- a/src/lib.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/lib.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -40,7 +40,7 @@
 
 #[cfg(feature = "link")]
 #[doc(inline)]
-pub use crate::libpam::{LibPamHandle, OwnedLibPamHandle};
+pub use crate::libpam::{OwnedLibPamHandle, RawPamHandle};
 #[doc(inline)]
 pub use crate::{
     constants::{ErrorCode, Flags, Result},
--- a/src/libpam/conversation.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/libpam/conversation.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -5,38 +5,35 @@
 use crate::libpam::question::Question;
 use crate::ErrorCode;
 use crate::Result;
-use libpam_sys::AppData;
+use libpam_sys::{AppData, ConversationCallback};
 use libpam_sys_helpers::memory::PtrPtrVec;
 use std::ffi::c_int;
 use std::iter;
-use std::marker::PhantomData;
 use std::ptr::NonNull;
 use std::result::Result as StdResult;
 
 /// The type used by PAM to call back into a conversation.
+///
+/// This has the same structure as a [`libpam_sys::pam_conv`].
+#[derive(Debug)]
 #[repr(C)]
-pub struct LibPamConversation<'a> {
-    pam_conv: libpam_sys::pam_conv,
-    /// Marker to associate the lifetime of this with the conversation
-    /// that was passed in.
-    pub life: PhantomData<&'a mut ()>,
+pub struct OwnedConversation<C: Conversation> {
+    callback: ConversationCallback,
+    conv: Box<C>,
 }
 
-impl LibPamConversation<'_> {
-    pub fn wrap<C: Conversation>(conv: &C) -> Self {
+impl<C: Conversation> OwnedConversation<C> {
+    pub fn new(conv: C) -> Self {
         Self {
-            pam_conv: libpam_sys::pam_conv {
-                conv: Self::wrapper_callback::<C>,
-                appdata_ptr: (conv as *const C).cast_mut().cast(),
-            },
-            life: PhantomData,
+            callback: Self::wrapper_callback,
+            conv: Box::new(conv),
         }
     }
 
     /// Passed as the conversation function into PAM for an owned handle.
     ///
     /// PAM calls this, we compute answers, then send them back.
-    unsafe extern "C" fn wrapper_callback<C: Conversation>(
+    unsafe extern "C" fn wrapper_callback(
         count: c_int,
         questions: *const *const libpam_sys::pam_message,
         answers: *mut *mut libpam_sys::pam_response,
@@ -71,7 +68,10 @@
     }
 }
 
-impl Conversation for LibPamConversation<'_> {
+/// A conversation owned by a PAM handle and lent to us.
+pub struct PamConv(libpam_sys::pam_conv);
+
+impl Conversation for PamConv {
     fn communicate(&self, messages: &[Exchange]) {
         let internal = || {
             let questions: Result<_> = messages.iter().map(Question::try_from).collect();
@@ -79,11 +79,11 @@
             let mut response_pointer = std::ptr::null_mut();
             // SAFETY: We're calling into PAM with valid everything.
             let result = unsafe {
-                (self.pam_conv.conv)(
+                (self.0.conv)(
                     messages.len() as c_int,
                     questions.as_ptr(),
                     &mut response_pointer,
-                    self.pam_conv.appdata_ptr,
+                    self.0.appdata_ptr,
                 )
             };
             ErrorCode::result_from(result)?;
--- a/src/libpam/environ.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/libpam/environ.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -1,34 +1,33 @@
-use crate::constants::{ErrorCode, Result};
 use crate::environ::{EnvironMap, EnvironMapMut};
-use crate::libpam::memory::CHeapString;
-use crate::libpam::{memory, LibPamHandle};
+use crate::libpam::memory::{CHeapBox, CHeapString};
+use crate::libpam::{memory, RawPamHandle};
 use std::ffi::{c_char, CStr, CString, OsStr, OsString};
 use std::marker::PhantomData;
 use std::os::unix::ffi::{OsStrExt, OsStringExt};
 use std::ptr;
 use std::ptr::NonNull;
 
-pub struct LibPamEnviron<'a> {
-    source: &'a LibPamHandle,
-}
-
-pub struct LibPamEnvironMut<'a> {
-    source: &'a mut LibPamHandle,
-}
-
-impl LibPamHandle {
+impl RawPamHandle {
     fn environ_get(&self, key: &OsStr) -> Option<OsString> {
         let key = CString::new(key.as_bytes()).ok()?;
         // SAFETY: We are a valid handle and are calling with a good key.
-        unsafe { copy_env(libpam_sys::pam_getenv(self.0.as_ref(), key.as_ptr())) }
+        let src = unsafe { libpam_sys::pam_getenv(self.raw_ref(), key.as_ptr()) };
+        let val = match NonNull::new(src) {
+            None => return None,
+            Some(ptr) => ptr.as_ptr(),
+        };
+        // SAFETY: We were just returned this string from PAM.
+        // We have to trust it.
+        let c_str = unsafe { CStr::from_ptr(val) };
+        Some(OsString::from_vec(c_str.to_bytes().to_vec()))
     }
 
-    fn environ_set(&mut self, key: &OsStr, value: Option<&OsStr>) -> Result<Option<OsString>> {
+    fn environ_set(&mut self, key: &OsStr, value: Option<&OsStr>) -> Option<OsString> {
         let old = self.environ_get(key);
         if old.is_none() && value.is_none() {
             // pam_putenv returns an error if we try to remove a non-existent
             // environment variable, so just avoid that entirely.
-            return Ok(None);
+            return None;
         }
         let total_len = key.len() + value.map(OsStr::len).unwrap_or_default() + 2;
         let mut result = Vec::with_capacity(total_len);
@@ -37,43 +36,41 @@
             result.push(b'=');
             result.extend(value.as_bytes());
         }
-        let put = CString::new(result).map_err(|_| ErrorCode::ConversationError)?;
+        let put = CString::new(result).unwrap();
         // SAFETY: This is a valid handle and a valid environment string.
-        ErrorCode::result_from(unsafe { libpam_sys::pam_putenv(self.0.as_mut(), put.as_ptr()) })?;
-        Ok(old)
+        // pam_putenv is only ever going to
+        let _ = unsafe { libpam_sys::pam_putenv(self.raw_mut(), put.as_ptr()) };
+        old
     }
 
-    fn environ_iter(&self) -> Result<impl Iterator<Item = (OsString, OsString)>> {
+    fn environ_iter(&self) -> impl Iterator<Item = (OsString, OsString)> {
         // SAFETY: This is a valid PAM handle. It will return valid data.
         unsafe {
-            NonNull::new(libpam_sys::pam_getenvlist(self.0.as_ref()))
+            NonNull::new(libpam_sys::pam_getenvlist(self.raw_ref()))
                 .map(|ptr| EnvList::from_ptr(ptr.cast()))
-                .ok_or(ErrorCode::BufferError)
+                .unwrap_or_else(EnvList::empty)
         }
     }
 }
 
-/// Copies the data of the given C string pointer to an OsString,
-/// or None if src is null.
-unsafe fn copy_env(src: *const c_char) -> Option<OsString> {
-    let val = match NonNull::new(src.cast_mut()) {
-        None => return None,
-        Some(ptr) => ptr.as_ptr(),
-    };
-    // SAFETY: We were just returned this string from PAM.
-    // We have to trust it.
-    let c_str = unsafe { CStr::from_ptr(val) };
-    Some(OsString::from_vec(c_str.to_bytes().to_vec()))
+/// A view to the environment stored in a PAM handle.
+pub struct LibPamEnviron<'a> {
+    source: &'a RawPamHandle,
+}
+
+/// A mutable view to the environment stored in a PAM handle.
+pub struct LibPamEnvironMut<'a> {
+    source: &'a mut RawPamHandle,
 }
 
 impl<'a> LibPamEnviron<'a> {
-    pub fn new(source: &'a LibPamHandle) -> Self {
+    pub fn new(source: &'a RawPamHandle) -> Self {
         Self { source }
     }
 }
 
 impl<'a> LibPamEnvironMut<'a> {
-    pub fn new(source: &'a mut LibPamHandle) -> Self {
+    pub fn new(source: &'a mut RawPamHandle) -> Self {
         Self { source }
     }
 }
@@ -83,7 +80,7 @@
         self.source.environ_get(key.as_ref())
     }
 
-    fn iter(&self) -> Result<impl Iterator<Item = (OsString, OsString)>> {
+    fn iter(&self) -> impl Iterator<Item = (OsString, OsString)> {
         self.source.environ_iter()
     }
 }
@@ -93,21 +90,17 @@
         self.source.environ_get(key.as_ref())
     }
 
-    fn iter(&self) -> Result<impl Iterator<Item = (OsString, OsString)>> {
+    fn iter(&self) -> impl Iterator<Item = (OsString, OsString)> {
         self.source.environ_iter()
     }
 }
 
 impl EnvironMapMut<'_> for LibPamEnvironMut<'_> {
-    fn insert(
-        &mut self,
-        key: impl AsRef<OsStr>,
-        val: impl AsRef<OsStr>,
-    ) -> Result<Option<OsString>> {
+    fn insert(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> Option<OsString> {
         self.source.environ_set(key.as_ref(), Some(val.as_ref()))
     }
 
-    fn remove(&mut self, key: impl AsRef<OsStr>) -> Result<Option<OsString>> {
+    fn remove(&mut self, key: impl AsRef<OsStr>) -> Option<OsString> {
         self.source.environ_set(key.as_ref(), None)
     }
 }
@@ -120,10 +113,19 @@
     start: NonNull<Option<EnvVar>>,
     /// The environment variable we're about to iterate into.
     current: NonNull<Option<EnvVar>>,
-    _owner: PhantomData<&'a LibPamHandle>,
+    _owner: PhantomData<&'a RawPamHandle>,
 }
 
 impl EnvList<'_> {
+    fn empty() -> Self {
+        let none: CHeapBox<Option<EnvVar>> = CHeapBox::new(None);
+        let ptr = CHeapBox::into_ptr(none);
+        Self {
+            start: ptr,
+            current: ptr,
+            _owner: PhantomData,
+        }
+    }
     unsafe fn from_ptr(ptr: NonNull<*mut c_char>) -> Self {
         Self {
             start: ptr.cast(),
--- a/src/libpam/handle.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/libpam/handle.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -1,4 +1,4 @@
-use super::conversation::LibPamConversation;
+use super::conversation::{OwnedConversation, PamConv};
 use crate::constants::{ErrorCode, Result};
 use crate::conv::Exchange;
 use crate::environ::EnvironMapMut;
@@ -10,32 +10,18 @@
     guide, linklist, stdlinks, Conversation, EnvironMap, Flags, PamHandleApplication,
     PamHandleModule,
 };
+use libpam_sys_helpers::constants;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::cell::Cell;
 use std::ffi::{c_char, c_int, CString};
-
+use std::mem::ManuallyDrop;
 use std::ptr;
 use std::ptr::NonNull;
 
-/// Owner for a PAM handle.
-pub struct LibPamHandle(pub NonNull<libpam_sys::pam_handle>);
-
-impl AsRef<libpam_sys::pam_handle> for LibPamHandle {
-    fn as_ref(&self) -> &libpam_sys::pam_handle {
-        unsafe { self.0.as_ref() }
-    }
-}
-
-impl AsMut<libpam_sys::pam_handle> for LibPamHandle {
-    fn as_mut(&mut self) -> &mut libpam_sys::pam_handle {
-        unsafe { self.0.as_mut() }
-    }
-}
-
 /// An owned PAM handle.
-pub struct OwnedLibPamHandle<'a> {
+pub struct OwnedLibPamHandle<C: Conversation> {
     /// The handle itself.
-    handle: LibPamHandle,
+    handle: ManuallyDrop<RawPamHandle>,
     /// The last return value from the handle.
     last_return: Cell<Result<()>>,
     /// If set, the Conversation that this PAM handle owns.
@@ -45,7 +31,7 @@
     /// 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: Option<Box<LibPamConversation<'a>>>,
+    conversation: Box<OwnedConversation<C>>,
 }
 
 #[derive(Debug, PartialEq)]
@@ -68,12 +54,12 @@
         self
     }
     /// Builds a PAM handle and starts the transaction.
-    pub fn build(self, conv: &impl Conversation) -> Result<OwnedLibPamHandle> {
+    pub fn build(self, conv: impl Conversation) -> Result<OwnedLibPamHandle<impl Conversation>> {
         OwnedLibPamHandle::start(self.service_name, self.username, conv)
     }
 }
 
-impl OwnedLibPamHandle<'_> {
+impl<C: Conversation> OwnedLibPamHandle<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
@@ -92,12 +78,8 @@
         }
     }
 
-    fn start(
-        service_name: String,
-        username: Option<String>,
-        conversation: &impl Conversation,
-    ) -> Result<Self> {
-        let conv = Box::new(LibPamConversation::wrap(conversation));
+    fn start(service_name: String, username: Option<String>, conversation: C) -> Result<Self> {
+        let conv = Box::new(OwnedConversation::new(conversation));
         let service_cstr = CString::new(service_name).map_err(|_| ErrorCode::ConversationError)?;
         let username_cstr = memory::prompt_ptr(memory::option_cstr(username.as_deref())?.as_ref());
 
@@ -108,7 +90,7 @@
             libpam_sys::pam_start(
                 service_cstr.as_ptr(),
                 username_cstr,
-                (conv.as_ref() as *const LibPamConversation)
+                (conv.as_ref() as *const OwnedConversation<C>)
                     .cast_mut()
                     .cast(),
                 &mut handle,
@@ -117,35 +99,37 @@
         ErrorCode::result_from(result)?;
         let handle = NonNull::new(handle).ok_or(ErrorCode::BufferError)?;
         Ok(Self {
-            handle: LibPamHandle(handle),
+            handle: ManuallyDrop::new(RawPamHandle(handle)),
             last_return: Cell::new(Ok(())),
-            conversation: Some(conv),
+            conversation: conv,
         })
     }
+
+    /// "Quietly" 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 end_quiet(self) {}
 }
 
-impl PamHandleApplication for OwnedLibPamHandle<'_> {
-    fn authenticate(&mut self, flags: Flags) -> Result<()> {
-        let ret =
-            unsafe { libpam_sys::pam_authenticate(self.handle.as_mut(), flags.bits() as c_int) };
-        let result = ErrorCode::result_from(ret);
-        self.last_return.set(result);
-        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()) })
+        }
+    };
+}
 
-    fn account_management(&mut self, flags: Flags) -> Result<()> {
-        let ret = unsafe { libpam_sys::pam_acct_mgmt(self.handle.as_mut(), flags.bits() as c_int) };
-        let result = ErrorCode::result_from(ret);
-        self.last_return.set(result);
-        result
-    }
-
-    fn change_authtok(&mut self, flags: Flags) -> Result<()> {
-        let ret = unsafe { libpam_sys::pam_chauthtok(self.handle.as_mut(), flags.bits() as c_int) };
-        let result = ErrorCode::result_from(ret);
-        self.last_return.set(result);
-        result
-    }
+impl PamHandleApplication for RawPamHandle {
+    wrap!(fn authenticate { pam_authenticate });
+    wrap!(fn account_management { pam_acct_mgmt });
+    wrap!(fn change_authtok { pam_chauthtok });
 }
 
 // TODO: pam_authenticate - app
@@ -158,7 +142,7 @@
 //       pam_getenv - shared
 //       pam_getenvlist - shared
 
-impl Drop for OwnedLibPamHandle<'_> {
+impl<C: Conversation> Drop for OwnedLibPamHandle<C> {
     /// Closes the PAM session on an owned PAM handle.
     ///
     /// This internally calls `pam_end` with the appropriate error code.
@@ -171,199 +155,13 @@
     fn drop(&mut self) {
         unsafe {
             libpam_sys::pam_end(
-                self.handle.as_mut(),
+                self.handle.raw_mut(),
                 ErrorCode::result_to_c(self.last_return.get()),
             );
         }
     }
 }
 
-/// Macro to implement getting/setting a CStr-based item.
-macro_rules! cstr_item {
-    (get = $getter:ident, item = $item_type:path) => {
-        fn $getter(&self) -> Result<Option<String>> {
-            unsafe { self.get_cstr_item($item_type) }
-        }
-    };
-    (set = $setter:ident, item = $item_type:path) => {
-        fn $setter(&mut self, value: Option<&str>) -> Result<()> {
-            unsafe { self.set_cstr_item($item_type, value) }
-        }
-    };
-}
-
-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 = "linux-pam")]
-        {
-            _ = 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<&str>) -> Result<String> {
-        let prompt = memory::option_cstr(prompt)?;
-        let mut output: *const c_char = ptr::null();
-        let ret = unsafe {
-            libpam_sys::pam_get_user(
-                self.as_mut(),
-                &mut output,
-                memory::prompt_ptr(prompt.as_ref()),
-            )
-        };
-        ErrorCode::result_from(ret)?;
-        unsafe { memory::copy_pam_string(output) }
-            .transpose()
-            .unwrap_or(Err(ErrorCode::ConversationError))
-    }
-
-    fn environ(&self) -> impl EnvironMap {
-        LibPamEnviron::new(self)
-    }
-
-    fn environ_mut(&mut self) -> impl EnvironMapMut {
-        LibPamEnvironMut::new(self)
-    }
-
-    cstr_item!(get = user_item, item = ItemType::User);
-    cstr_item!(set = set_user_item, item = ItemType::User);
-    cstr_item!(get = service, item = ItemType::Service);
-    cstr_item!(set = set_service, item = ItemType::Service);
-    cstr_item!(get = user_prompt, item = ItemType::UserPrompt);
-    cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt);
-    cstr_item!(get = tty_name, item = ItemType::Tty);
-    cstr_item!(set = set_tty_name, item = ItemType::Tty);
-    cstr_item!(get = remote_user, item = ItemType::RemoteUser);
-    cstr_item!(set = set_remote_user, item = ItemType::RemoteUser);
-    cstr_item!(get = remote_host, item = ItemType::RemoteHost);
-    cstr_item!(set = set_remote_host, item = ItemType::RemoteHost);
-    cstr_item!(set = set_authtok_item, item = ItemType::AuthTok);
-    cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok);
-}
-
-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 PamHandleModule for LibPamHandle {
-    #[cfg(any(pam_impl = "LinuxPam", pam_impl = "OpenPam"))]
-    fn authtok(&mut self, prompt: Option<&str>) -> Result<String> {
-        let prompt = memory::option_cstr(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.as_mut(),
-                ItemType::AuthTok.into(),
-                &mut output,
-                memory::prompt_ptr(prompt.as_ref()),
-            )
-        };
-        ErrorCode::result_from(res)?;
-        // SAFETY: We got this string from PAM.
-        unsafe { memory::copy_pam_string(output) }
-            .transpose()
-            .unwrap_or(Err(ErrorCode::ConversationError))
-    }
-
-    #[cfg(not(any(pam_impl = "LinuxPam", pam_impl = "OpenPam")))]
-    fn authtok(&mut self, prompt: Option<&str>) -> Result<String> {
-        Err(ErrorCode::ConversationError)
-    }
-
-    cstr_item!(get = authtok_item, item = ItemType::AuthTok);
-    cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok);
-}
-
-/// 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) {
-    unsafe {
-        let _data: Box<T> = Box::from_raw(c_data.cast());
-    }
-}
-
-impl LibPamHandle {
-    /// Gets a C string item.
-    ///
-    /// # Safety
-    ///
-    /// You better be requesting an item which is a C string.
-    unsafe fn get_cstr_item(&self, item_type: ItemType) -> Result<Option<String>> {
-        let mut output = ptr::null();
-        let ret =
-            unsafe { libpam_sys::pam_get_item(self.as_ref(), item_type as c_int, &mut output) };
-        ErrorCode::result_from(ret)?;
-        memory::copy_pam_string(output.cast())
-    }
-
-    /// Sets a C string item.
-    ///
-    /// # Safety
-    ///
-    /// You better be setting an item which is a C string.
-    unsafe fn set_cstr_item(&mut self, item_type: ItemType, data: Option<&str>) -> Result<()> {
-        let data_str = memory::option_cstr(data)?;
-        let ret = unsafe {
-            libpam_sys::pam_set_item(
-                self.as_mut(),
-                item_type as c_int,
-                memory::prompt_ptr(data_str.as_ref()).cast(),
-            )
-        };
-        ErrorCode::result_from(ret)
-    }
-
-    /// Gets the `PAM_CONV` item from the handle.
-    fn conversation_item(&self) -> Result<&mut LibPamConversation<'_>> {
-        let output: *mut LibPamConversation = ptr::null_mut();
-        let result = unsafe {
-            libpam_sys::pam_get_item(
-                self.as_ref(),
-                ItemType::Conversation.into(),
-                &mut output.cast_const().cast(),
-            )
-        };
-        ErrorCode::result_from(result)?;
-        // SAFETY: We got this result from PAM, and we're checking if it's null.
-        unsafe { output.as_mut() }.ok_or(ErrorCode::ConversationError)
-    }
-}
-
 macro_rules! delegate {
     // First have the kind that save the result after delegation.
     (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => {
@@ -405,7 +203,7 @@
     result.as_ref().map(drop).map_err(|&e| e)
 }
 
-impl PamShared for OwnedLibPamHandle<'_> {
+impl<C: Conversation> PamShared for OwnedLibPamHandle<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);
@@ -420,6 +218,290 @@
     delegate!(set = set_old_authtok_item);
 }
 
+/// Macro to implement getting/setting a CStr-based item.
+macro_rules! cstr_item {
+    (get = $getter:ident, item = $item_type:path) => {
+        fn $getter(&self) -> Result<Option<String>> {
+            unsafe { self.get_cstr_item($item_type) }
+        }
+    };
+    (set = $setter:ident, item = $item_type:path) => {
+        fn $setter(&mut self, value: Option<&str>) -> Result<()> {
+            unsafe { self.set_cstr_item($item_type, value) }
+        }
+    };
+}
+
+/// 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 [`OwnedLibPamHandle`], 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.
+pub struct RawPamHandle(NonNull<libpam_sys::pam_handle>);
+
+impl RawPamHandle {
+    /// 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_quiet(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 RawPamHandle {
+    fn drop(&mut self) {
+        unsafe { libpam_sys::pam_end(self.0.as_mut(), 0) };
+    }
+}
+
+impl PamShared for RawPamHandle {
+    #[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 = "linux-pam")]
+        {
+            _ = 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<&str>) -> Result<String> {
+        let prompt = memory::option_cstr(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_ref()),
+            )
+        };
+        ErrorCode::result_from(ret)?;
+        unsafe { memory::copy_pam_string(output) }
+            .transpose()
+            .unwrap_or(Err(ErrorCode::ConversationError))
+    }
+
+    fn environ(&self) -> impl EnvironMap {
+        LibPamEnviron::new(self)
+    }
+
+    fn environ_mut(&mut self) -> impl EnvironMapMut {
+        LibPamEnvironMut::new(self)
+    }
+
+    cstr_item!(get = user_item, item = ItemType::User);
+    cstr_item!(set = set_user_item, item = ItemType::User);
+    cstr_item!(get = service, item = ItemType::Service);
+    cstr_item!(set = set_service, item = ItemType::Service);
+    cstr_item!(get = user_prompt, item = ItemType::UserPrompt);
+    cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt);
+    cstr_item!(get = tty_name, item = ItemType::Tty);
+    cstr_item!(set = set_tty_name, item = ItemType::Tty);
+    cstr_item!(get = remote_user, item = ItemType::RemoteUser);
+    cstr_item!(set = set_remote_user, item = ItemType::RemoteUser);
+    cstr_item!(get = remote_host, item = ItemType::RemoteHost);
+    cstr_item!(set = set_remote_host, item = ItemType::RemoteHost);
+    cstr_item!(set = set_authtok_item, item = ItemType::AuthTok);
+    cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok);
+}
+
+impl Conversation for RawPamHandle {
+    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 PamHandleModule for RawPamHandle {
+    fn authtok(&mut self, prompt: Option<&str>) -> Result<String> {
+        self.get_authtok(prompt, ItemType::AuthTok)
+    }
+
+    fn old_authtok(&mut self, prompt: Option<&str>) -> Result<String> {
+        self.get_authtok(prompt, ItemType::OldAuthTok)
+    }
+
+    cstr_item!(get = authtok_item, item = ItemType::AuthTok);
+    cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok);
+}
+
+/// 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) {
+    unsafe {
+        let _data: Box<T> = Box::from_raw(c_data.cast());
+    }
+}
+
+// Implementations of internal functions.
+impl RawPamHandle {
+    #[cfg(any(pam_impl = "LinuxPam", pam_impl = "OpenPam"))]
+    fn get_authtok(&mut self, prompt: Option<&str>, item_type: ItemType) -> Result<String> {
+        let prompt = memory::option_cstr(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_ref()),
+            )
+        };
+        ErrorCode::result_from(res)?;
+        // SAFETY: We got this string from PAM.
+        unsafe { memory::copy_pam_string(output) }
+            .transpose()
+            .unwrap_or(Err(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 a C string item.
+    ///
+    /// # Safety
+    ///
+    /// You better be requesting an item which is a C string.
+    unsafe fn get_cstr_item(&self, item_type: ItemType) -> Result<Option<String>> {
+        let mut output = ptr::null();
+        let ret =
+            unsafe { libpam_sys::pam_get_item(self.raw_ref(), item_type as c_int, &mut output) };
+        ErrorCode::result_from(ret)?;
+        memory::copy_pam_string(output.cast())
+    }
+
+    /// Sets a C string item.
+    ///
+    /// # Safety
+    ///
+    /// You better be setting an item which is a C string.
+    unsafe fn set_cstr_item(&mut self, item_type: ItemType, data: Option<&str>) -> Result<()> {
+        let data_str = memory::option_cstr(data)?;
+        let ret = unsafe {
+            libpam_sys::pam_set_item(
+                self.raw_mut(),
+                item_type as c_int,
+                memory::prompt_ptr(data_str.as_ref()).cast(),
+            )
+        };
+        ErrorCode::result_from(ret)
+    }
+
+    /// 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)]
@@ -427,29 +509,33 @@
 #[non_exhaustive] // because C could give us anything!
 pub enum ItemType {
     /// The PAM service name.
-    Service = 1,
+    Service = constants::PAM_SERVICE,
     /// The user's login name.
-    User = 2,
+    User = constants::PAM_USER,
     /// The TTY name.
-    Tty = 3,
+    Tty = constants::PAM_TTY,
     /// The remote host (if applicable).
-    RemoteHost = 4,
+    RemoteHost = constants::PAM_RHOST,
     /// The conversation struct (not a CStr-based item).
-    Conversation = 5,
+    Conversation = constants::PAM_CONV,
     /// The authentication token (password).
-    AuthTok = 6,
+    AuthTok = constants::PAM_AUTHTOK,
     /// The old authentication token (when changing passwords).
-    OldAuthTok = 7,
+    OldAuthTok = constants::PAM_OLDAUTHTOK,
     /// The remote user's name.
-    RemoteUser = 8,
+    RemoteUser = constants::PAM_RUSER,
     /// The prompt shown when requesting a username.
-    UserPrompt = 9,
+    UserPrompt = constants::PAM_USER_PROMPT,
+    #[cfg(feature = "linux-pam-ext")]
     /// App-supplied function to override failure delays.
-    FailDelay = 10,
+    FailDelay = constants::PAM_FAIL_DELAY,
+    #[cfg(feature = "linux-pam-ext")]
     /// X display name.
-    XDisplay = 11,
+    XDisplay = constants::PAM_XDISPLAY,
+    #[cfg(feature = "linux-pam-ext")]
     /// X server authentication data.
-    XAuthData = 12,
+    XAuthData = constants::PAM_XAUTHDATA,
+    #[cfg(feature = "linux-pam-ext")]
     /// The type of `pam_get_authtok`.
-    AuthTokType = 13,
+    AuthTokType = constants::PAM_AUTHTOK_TYPE,
 }
--- a/src/libpam/memory.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/libpam/memory.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -57,11 +57,11 @@
 #[allow(clippy::wrong_self_convention)]
 impl<T> CHeapBox<T> {
     /// Creates a new CHeapBox holding the given data.
-    pub fn new(value: T) -> Result<Self> {
+    pub fn new(value: T) -> Self {
         let memory = calloc(1);
         unsafe { ptr::write(memory.as_ptr(), value) }
         // SAFETY: We literally just allocated this.
-        Ok(Self(memory))
+        Self(memory)
     }
 
     /// Takes ownership of the given pointer.
@@ -95,7 +95,8 @@
     ///
     /// # Safety
     ///
-    /// The different type has to be compatible in size/alignment and drop behavior.
+    /// The other type has to have the same size and alignment and
+    /// have compatible drop behavior with respect to other resources.
     pub unsafe fn cast<R>(this: Self) -> CHeapBox<R> {
         mem::transmute(this)
     }
@@ -103,7 +104,7 @@
 
 impl<T: Default> Default for CHeapBox<T> {
     fn default() -> Self {
-        Self::new(Default::default()).expect("allocation should not fail")
+        Self::new(Default::default())
     }
 }
 
@@ -274,10 +275,10 @@
             }
         }
 
-        let mut dropbox = CHeapBox::new(Dropper(&drop_count)).unwrap();
+        let mut dropbox = CHeapBox::new(Dropper(&drop_count));
         _ = dropbox;
         // ensure the old value is dropped when the new one is assigned.
-        dropbox = CHeapBox::new(Dropper(&drop_count)).unwrap();
+        dropbox = CHeapBox::new(Dropper(&drop_count));
         assert_eq!(1, drop_count.get());
         *dropbox = Dropper(&drop_count);
         assert_eq!(2, drop_count.get());
--- a/src/libpam/mod.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/libpam/mod.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -15,4 +15,4 @@
 mod question;
 
 #[doc(inline)]
-pub use handle::{LibPamHandle, OwnedLibPamHandle};
+pub use handle::{OwnedLibPamHandle, RawPamHandle};
--- a/src/libpam/module.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/libpam/module.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -42,7 +42,7 @@
     ($ident:ident) => {
         mod _pam_hooks_scope {
             use std::ffi::{c_char, c_int, c_void, CStr};
-            use $crate::{ErrorCode, Flags, LibPamHandle, PamModule};
+            use $crate::{ErrorCode, Flags, PamModule, RawPamHandle};
 
             #[no_mangle]
             extern "C" fn pam_sm_acct_mgmt(
@@ -51,7 +51,7 @@
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                if let Some(handle) = unsafe { pamh.cast::<RawPamHandle>().as_mut() } {
                     let args = extract_argv(argc, argv);
                     ErrorCode::result_to_c(super::$ident::account_management(handle, args, flags))
                 } else {
@@ -66,7 +66,7 @@
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                if let Some(handle) = unsafe { pamh.cast::<RawPamHandle>().as_mut() } {
                     let args = extract_argv(argc, argv);
                     ErrorCode::result_to_c(super::$ident::authenticate(handle, args, flags))
                 } else {
@@ -81,7 +81,7 @@
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                if let Some(handle) = unsafe { pamh.cast::<RawPamHandle>().as_mut() } {
                     let args = extract_argv(argc, argv);
                     ErrorCode::result_to_c(super::$ident::change_authtok(handle, args, flags))
                 } else {
@@ -96,7 +96,7 @@
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                if let Some(handle) = unsafe { pamh.cast::<RawPamHandle>().as_mut() } {
                     let args = extract_argv(argc, argv);
                     ErrorCode::result_to_c(super::$ident::close_session(handle, args, flags))
                 } else {
@@ -112,7 +112,7 @@
                 argv: *const *const c_char,
             ) -> c_int {
                 let args = extract_argv(argc, argv);
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                if let Some(handle) = unsafe { pamh.cast::<RawPamHandle>().as_mut() } {
                     ErrorCode::result_to_c(super::$ident::open_session(handle, args, flags))
                 } else {
                     ErrorCode::Ignore as c_int
@@ -127,7 +127,7 @@
                 argv: *const *const c_char,
             ) -> c_int {
                 let args = extract_argv(argc, argv);
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                if let Some(handle) = unsafe { pamh.cast::<RawPamHandle>().as_mut() } {
                     ErrorCode::result_to_c(super::$ident::set_credentials(handle, args, flags))
                 } else {
                     ErrorCode::Ignore as c_int
--- a/src/libpam/question.rs	Sat Jul 05 17:11:33 2025 -0400
+++ b/src/libpam/question.rs	Sat Jul 05 17:16:56 2025 -0400
@@ -1,13 +1,11 @@
 //! Data and types dealing with PAM messages.
 
-#[cfg(feature = "linux-pam-ext")]
-use crate::conv::{BinaryQAndA, RadioQAndA};
-use libpam_sys_helpers::memory::{BinaryPayload, TooBigError};
 use crate::conv::{ErrorMsg, Exchange, InfoMsg, MaskedQAndA, QAndA};
 use crate::libpam::conversation::OwnedExchange;
-use crate::libpam::memory::{CHeapBox, CHeapPayload, CHeapString};
+use crate::libpam::memory;
 use crate::ErrorCode;
 use crate::Result;
+use libpam_sys_helpers::memory as pammem;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::ffi::{c_int, c_void, CStr};
 use std::ptr::NonNull;
@@ -84,7 +82,7 @@
     unsafe fn binary_data(&self) -> (&[u8], u8) {
         self.data
             .as_ref()
-            .map(|data| BinaryPayload::contents(data.as_ptr().cast()))
+            .map(|data| pammem::BinaryPayload::contents(data.as_ptr().cast()))
             .unwrap_or_default()
     }
 }
@@ -94,11 +92,11 @@
     fn try_from(msg: &Exchange) -> Result<Self> {
         let alloc = |style, text| -> Result<_> {
             Ok((style, unsafe {
-                CHeapBox::cast(CHeapString::new(text)?.into_box())
+                memory::CHeapBox::cast(memory::CHeapString::new(text)?.into_box())
             }))
         };
         // We will only allocate heap data if we have a valid input.
-        let (style, data): (_, CHeapBox<c_void>) = match *msg {
+        let (style, data): (_, memory::CHeapBox<c_void>) = match *msg {
             Exchange::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
             Exchange::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
             Exchange::Error(p) => alloc(Style::ErrorMsg, p.question()),
@@ -108,9 +106,11 @@
             #[cfg(feature = "linux-pam-ext")]
             Exchange::BinaryPrompt(p) => {
                 let (data, typ) = p.question();
-                let payload = CHeapPayload::new(data, typ)?.into_inner();
-                Ok((Style::BinaryPrompt, unsafe { CHeapBox::cast(payload) }))
-            },
+                let payload = memory::CHeapPayload::new(data, typ)?.into_inner();
+                Ok((Style::BinaryPrompt, unsafe {
+                    memory::CHeapBox::cast(payload)
+                }))
+            }
             #[cfg(not(feature = "linux-pam-ext"))]
             Exchange::RadioPrompt(_) | Exchange::BinaryPrompt(_) => {
                 Err(ErrorCode::ConversationError)
@@ -118,7 +118,7 @@
         }?;
         Ok(Self {
             style: style.into(),
-            data: Some(CHeapBox::into_ptr(data)),
+            data: Some(memory::CHeapBox::into_ptr(data)),
         })
     }
 }
@@ -136,21 +136,22 @@
                     Style::BinaryPrompt => self
                         .data
                         .as_mut()
-                        .map(|p| BinaryPayload::zero(p.as_ptr().cast())),
+                        .map(|p| pammem::BinaryPayload::zero(p.as_ptr().cast())),
                     #[cfg(feature = "linux-pam-ext")]
                     Style::RadioType => self
                         .data
                         .as_mut()
-                        .map(|p| CHeapString::zero(p.cast())),
+                        .map(|p| memory::CHeapString::zero(p.cast())),
                     Style::TextInfo
                     | Style::ErrorMsg
                     | Style::PromptEchoOff
-                    | Style::PromptEchoOn => {
-                        self.data.as_mut().map(|p| CHeapString::zero(p.cast()))
-                    }
+                    | Style::PromptEchoOn => self
+                        .data
+                        .as_mut()
+                        .map(|p| memory::CHeapString::zero(p.cast())),
                 };
             };
-            let _ = self.data.map(|p| CHeapBox::from_ptr(p));
+            let _ = self.data.map(|p| memory::CHeapBox::from_ptr(p));
         }
     }
 }
@@ -173,9 +174,13 @@
                 Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)),
                 Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)),
                 #[cfg(feature = "linux-pam-ext")]
-                Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)),
+                Style::RadioType => {
+                    Self::RadioPrompt(crate::conv::RadioQAndA::new(question.string_data()?))
+                }
                 #[cfg(feature = "linux-pam-ext")]
-                Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())),
+                Style::BinaryPrompt => {
+                    Self::BinaryPrompt(crate::conv::BinaryQAndA::new(question.binary_data()))
+                }
             }
         };
         Ok(prompt)
@@ -183,8 +188,8 @@
 }
 
 #[cfg(feature = "linux-pam-ext")]
-impl From<TooBigError> for ErrorCode {
-    fn from(_: TooBigError) -> Self {
+impl From<pammem::TooBigError> for ErrorCode {
+    fn from(_: pammem::TooBigError) -> Self {
         ErrorCode::BufferError
     }
 }
@@ -225,6 +230,7 @@
     #[test]
     #[cfg(feature = "linux-pam-ext")]
     fn linux_extensions() {
+        use crate::conv::{BinaryQAndA, RadioQAndA};
         assert_matches!(
             (Exchange::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)),
             BinaryQAndA::new((&[5, 4, 3, 2, 1], 66))