changeset 80:5aa1a010f1e8

Start using PAM headers; improve owned/borrowed distinction. - Uses bindgen to generate bindings (only if needed). - Gets the story together on owned vs. borrowed handles. - Reduces number of mutable borrows in handle operation (since `PamHandle` is neither `Send` nor `Sync`, we never have to worry about thread safety. - Improves a bunch of macros so we don't have our own special syntax for docs. - Implement question indirection for standard XSSO PAM implementations.
author Paul Fisher <paul@pfish.zone>
date Tue, 10 Jun 2025 01:09:30 -0400
parents 2128123b9406
children a8f4718fed5d
files Cargo.toml build.rs src/constants.rs src/handle.rs src/libpam/answer.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/memory.rs src/libpam/mod.rs src/libpam/pam_ffi.rs src/libpam/question.rs src/module.rs
diffstat 12 files changed, 721 insertions(+), 568 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.toml	Sun Jun 08 04:21:58 2025 -0400
+++ b/Cargo.toml	Tue Jun 10 01:09:30 2025 -0400
@@ -13,11 +13,17 @@
 default = ["link"]
 # Enable this to actually link against your system's PAM library.
 link = []
+# Enable this to get access to Linux-PAM extensions.
+linux-pam-extensions = []
+# Enable this to get access to OpenPAM features not available in Linux-PAM.
+openpam = []
 
 [dependencies]
 bitflags = "2.9.0"
 libc = "0.2.97"
-num-derive = "0.4.2"
-num-traits = "0.2.19"
+num_enum = "0.7.3"
 secure-string = "0.3.0"
 thiserror = "2.0.12"
+
+[build-dependencies]
+bindgen = "0.72.0"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/build.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -0,0 +1,58 @@
+use bindgen::MacroTypeVariation;
+use std::env;
+use std::path::PathBuf;
+
+fn main() {
+    if cfg!(feature = "link") {
+        println!("cargo::rustc-check-cfg=cfg(pam_impl, values(\"linux-pam\",\"openpam\"))");
+        let common_builder = bindgen::Builder::default()
+            .merge_extern_blocks(true)
+            .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+            .blocklist_type("pam_handle")
+            .blocklist_type("pam_conv")
+            .allowlist_var(".*")
+            .allowlist_function("pam_start")
+            .allowlist_function("pam_[gs]et_item")
+            .allowlist_function("pam_get_user")
+            .allowlist_function("pam_get_authtok")
+            .allowlist_function("pam_end")
+            .dynamic_link_require_all(true)
+            .default_macro_constant_type(MacroTypeVariation::Signed);
+
+        let linux_builder = common_builder.clone().header_contents(
+            "linux-pam.h",
+            r#"
+                #include <security/_pam_types.h>
+                #include <security/pam_appl.h>
+                #include <security/pam_ext.h>
+                #include <security/pam_modules.h>
+                "#,
+        );
+        let openpam_builder = common_builder.clone().header_contents(
+            "openpam.h",
+            r#"
+                #include <security/openpam.h>
+                #include <security/pam_appl.h>
+                #include <security/pam_constants.h>
+                #include <security/pam_types.h>
+                "#,
+        );
+
+        let (pam_impl, bindings) = {
+            let bb = linux_builder.generate();
+            bb.as_ref().unwrap();
+            if let Ok(bindings) = bb {
+                ("linux-pam", bindings)
+            } else if let Ok(bindings) = openpam_builder.generate() {
+                ("openpam", bindings)
+            } else {
+                panic!("unrecognized PAM implementation")
+            }
+        };
+        println!("cargo::rustc-cfg=pam_impl={pam_impl:?}");
+        let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+        bindings
+            .write_to_file(out_path.join("bindings.rs"))
+            .unwrap();
+    }
+}
--- a/src/constants.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/constants.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -1,13 +1,79 @@
 //! Constants and enum values from the PAM library.
 
+#[cfg(feature = "link")]
+use crate::libpam::pam_ffi;
 use bitflags::bitflags;
-use libc::{c_int, c_uint};
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-use std::any;
-use std::marker::PhantomData;
+use libc::c_int;
+use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::result::Result as StdResult;
 
+/// Arbitrary values for PAM constants when not linking against system PAM.
+///
+/// **The values of these constants are deliberately selected _not_ to match
+/// any PAM implementations. Applications should always use the symbolic value
+/// and not a magic number.**
+#[cfg(not(feature = "link"))]
+mod ffi {
+    macro_rules! define {
+        ($(#[$attr:meta])* $($name:ident = $value:expr),+) => {
+            define!(
+                @meta { $(#[$attr])* }
+                $(pub const $name: i32 = $value;)+
+            );
+        };
+        (@meta $m:tt $($i:item)+) => { define!(@expand $($m $i)+); };
+        (@expand $({ $(#[$m:meta])* } $i:item)+) => {$($(#[$m])* $i)+};
+    }
+    const fn bit(n: i8) -> i32 {
+        1 << n
+    }
+    define!(
+        PAM_SILENT = bit(13),
+        PAM_DISALLOW_NULL_AUTHTOK = bit(14),
+        PAM_ESTABLISH_CRED = bit(15),
+        PAM_DELETE_CRED = bit(16),
+        PAM_REINITIALIZE_CRED = bit(17),
+        PAM_REFRESH_CRED = bit(18),
+        PAM_CHANGE_EXPIRED_AUTHTOK = bit(19),
+        PAM_PRELIM_CHECK = bit(20),
+        PAM_UPDATE_AUTHTOK = bit(21)
+    );
+
+    define!(
+        PAM_ABORT = 513,
+        PAM_ACCT_EXPIRED = 514,
+        PAM_AUTHINFO_UNAVAIL = 515,
+        PAM_AUTHTOK_DISABLE_AGING = 516,
+        PAM_AUTHTOK_ERR = 517,
+        PAM_AUTHTOK_EXPIRED = 518,
+        PAM_AUTHTOK_LOCK_BUSY = 519,
+        PAM_AUTHTOK_RECOVERY_ERR = 520,
+        PAM_AUTH_ERR = 521,
+        PAM_BAD_ITEM = 522,
+        PAM_BUF_ERR = 533,
+        PAM_CONV_AGAIN = 534,
+        PAM_CONV_ERR = 535,
+        PAM_CRED_ERR = 536,
+        PAM_CRED_EXPIRED = 537,
+        PAM_CRED_INSUFFICIENT = 538,
+        PAM_CRED_UNAVAIL = 539,
+        PAM_IGNORE = 540,
+        PAM_INCOMPLETE = 541,
+        PAM_MAXTRIES = 542,
+        PAM_MODULE_UNKNOWN = 543,
+        PAM_NEW_AUTHTOK_REQD = 544,
+        PAM_NO_MODULE_DATA = 545,
+        PAM_OPEN_ERR = 546,
+        PAM_PERM_DENIED = 547,
+        PAM_SERVICE_ERR = 548,
+        PAM_SESSION_ERR = 549,
+        PAM_SYMBOL_ERR = 550,
+        PAM_SYSTEM_ERR = 551,
+        PAM_TRY_AGAIN = 552,
+        PAM_USER_UNKNOWN = 553
+    );
+}
+
 bitflags! {
     /// The available PAM flags.
     ///
@@ -15,26 +81,26 @@
     /// See `/usr/include/security/pam_modules.h` for more details.
     #[derive(Debug, PartialEq)]
     #[repr(transparent)]
-    pub struct Flags: c_uint {
+    pub struct Flags: c_int {
         /// The module should not generate any messages.
-        const SILENT = 0x8000;
+        const SILENT = pam_ffi::PAM_SILENT;
 
         /// The module should return [ErrorCode::AuthError]
         /// if the user has an empty authentication token
         /// rather than immediately accepting them.
-        const DISALLOW_NULL_AUTHTOK = 0x0001;
+        const DISALLOW_NULL_AUTHTOK = pam_ffi::PAM_DISALLOW_NULL_AUTHTOK;
 
         // Flag used for `set_credentials`.
 
         /// Set user credentials for an authentication service.
-        const ESTABLISH_CREDENTIALS = 0x0002;
+        const ESTABLISH_CREDENTIALS = pam_ffi::PAM_ESTABLISH_CRED;
         /// Delete user credentials associated with
         /// an authentication service.
-        const DELETE_CREDENTIALS = 0x0004;
+        const DELETE_CREDENTIALS = pam_ffi::PAM_DELETE_CRED;
         /// Reinitialize user credentials.
-        const REINITIALIZE_CREDENTIALS = 0x0008;
+        const REINITIALIZE_CREDENTIALS = pam_ffi::PAM_REINITIALIZE_CRED;
         /// Extend the lifetime of user credentials.
-        const REFRESH_CREDENTIALS = 0x0010;
+        const REFRESH_CREDENTIALS = pam_ffi::PAM_REFRESH_CRED;
 
         // Flags used for password changing.
 
@@ -43,7 +109,7 @@
         /// the password service should update all passwords.
         ///
         /// This flag is only used by `change_authtok`.
-        const CHANGE_EXPIRED_AUTHTOK = 0x0020;
+        const CHANGE_EXPIRED_AUTHTOK = pam_ffi::PAM_CHANGE_EXPIRED_AUTHTOK;
 
         /// This is a preliminary check for password changing.
         /// The password should not be changed.
@@ -52,7 +118,7 @@
         /// Applications may not use this flag.
         ///
         /// This flag is only used by `change_authtok`.
-        const PRELIMINARY_CHECK = 0x4000;
+        const PRELIMINARY_CHECK = pam_ffi::PAM_PRELIM_CHECK;
         /// The password should actuallyPR be updated.
         /// This and [Self::PRELIMINARY_CHECK] are mutually exclusive.
         ///
@@ -60,7 +126,7 @@
         /// Applications may not use this flag.
         ///
         /// This flag is only used by `change_authtok`.
-        const UPDATE_AUTHTOK = 0x2000;
+        const UPDATE_AUTHTOK = pam_ffi::PAM_UPDATE_AUTHTOK;
     }
 }
 
@@ -70,71 +136,72 @@
 /// For more detailed information, see
 /// `/usr/include/security/_pam_types.h`.
 #[allow(non_camel_case_types, dead_code)]
-#[derive(Copy, Clone, Debug, PartialEq, thiserror::Error, FromPrimitive)]
+#[derive(Copy, Clone, Debug, PartialEq, thiserror::Error, TryFromPrimitive, IntoPrimitive)]
 #[non_exhaustive] // C might give us anything!
+#[repr(i32)]
 pub enum ErrorCode {
     #[error("dlopen() failure when dynamically loading a service module")]
-    OpenError = 1,
+    OpenError = pam_ffi::PAM_OPEN_ERR,
     #[error("symbol not found")]
-    SymbolError = 2,
+    SymbolError = pam_ffi::PAM_SYMBOL_ERR,
     #[error("error in service module")]
-    ServiceError = 3,
+    ServiceError = pam_ffi::PAM_SERVICE_ERR,
     #[error("system error")]
-    SystemError = 4,
+    SystemError = pam_ffi::PAM_SYSTEM_ERR,
     #[error("memory buffer error")]
-    BufferError = 5,
+    BufferError = pam_ffi::PAM_BUF_ERR,
     #[error("permission denied")]
-    PermissionDenied = 6,
+    PermissionDenied = pam_ffi::PAM_PERM_DENIED,
     #[error("authentication failure")]
-    AuthenticationError = 7,
+    AuthenticationError = pam_ffi::PAM_AUTH_ERR,
     #[error("cannot access authentication data due to insufficient credentials")]
-    CredentialsInsufficient = 8,
+    CredentialsInsufficient = pam_ffi::PAM_CRED_INSUFFICIENT,
     #[error("underlying authentication service cannot retrieve authentication information")]
-    AuthInfoUnavailable = 9,
+    AuthInfoUnavailable = pam_ffi::PAM_AUTHINFO_UNAVAIL,
     #[error("user not known to the underlying authentication module")]
-    UserUnknown = 10,
+    UserUnknown = pam_ffi::PAM_USER_UNKNOWN,
     #[error("retry limit reached; do not attempt further")]
-    MaxTries = 11,
+    MaxTries = pam_ffi::PAM_MAXTRIES,
     #[error("new authentication token required")]
-    NewAuthTokRequired = 12,
+    NewAuthTokRequired = pam_ffi::PAM_NEW_AUTHTOK_REQD,
     #[error("user account has expired")]
-    AccountExpired = 13,
+    AccountExpired = pam_ffi::PAM_ACCT_EXPIRED,
     #[error("cannot make/remove an entry for the specified session")]
-    SessionError = 14,
+    SessionError = pam_ffi::PAM_SESSION_ERR,
     #[error("underlying authentication service cannot retrieve user credentials")]
-    CredentialsUnavailable = 15,
+    CredentialsUnavailable = pam_ffi::PAM_CRED_UNAVAIL,
     #[error("user credentials expired")]
-    CredentialsExpired = 16,
+    CredentialsExpired = pam_ffi::PAM_CRED_EXPIRED,
     #[error("failure setting user credentials")]
-    CredentialsError = 17,
+    CredentialsError = pam_ffi::PAM_CRED_ERR,
     #[error("no module-specific data is present")]
-    NoModuleData = 18,
+    NoModuleData = pam_ffi::PAM_NO_MODULE_DATA,
     #[error("conversation error")]
-    ConversationError = 19,
+    ConversationError = pam_ffi::PAM_CONV_ERR,
     #[error("authentication token manipulation error")]
-    AuthTokError = 20,
+    AuthTokError = pam_ffi::PAM_AUTHTOK_ERR,
     #[error("authentication information cannot be recovered")]
-    AuthTokRecoveryError = 21,
+    AuthTokRecoveryError = pam_ffi::PAM_AUTHTOK_RECOVERY_ERR,
     #[error("authentication token lock busy")]
-    AuthTokLockBusy = 22,
+    AuthTokLockBusy = pam_ffi::PAM_AUTHTOK_LOCK_BUSY,
     #[error("authentication token aging disabled")]
-    AuthTokDisableAging = 23,
+    AuthTokDisableAging = pam_ffi::PAM_AUTHTOK_DISABLE_AGING,
     #[error("preliminary password check failed")]
-    TryAgain = 24,
+    TryAgain = pam_ffi::PAM_TRY_AGAIN,
     #[error("ignore underlying account module, regardless of control flag")]
-    Ignore = 25,
+    Ignore = pam_ffi::PAM_IGNORE,
     #[error("critical error; this module should fail now")]
-    Abort = 26,
+    Abort = pam_ffi::PAM_ABORT,
     #[error("authentication token has expired")]
-    AuthTokExpired = 27,
+    AuthTokExpired = pam_ffi::PAM_AUTHTOK_EXPIRED,
     #[error("module is not known")]
-    ModuleUnknown = 28,
+    ModuleUnknown = pam_ffi::PAM_MODULE_UNKNOWN,
     #[error("bad item passed to pam_[whatever]_item")]
-    BadItem = 29,
+    BadItem = pam_ffi::PAM_BAD_ITEM,
     #[error("conversation function is event-driven and data is not available yet")]
-    ConversationAgain = 30,
+    ConversationAgain = pam_ffi::PAM_CONV_AGAIN,
     #[error("call this function again to complete authentication stack")]
-    Incomplete = 31,
+    Incomplete = pam_ffi::PAM_INCOMPLETE,
 }
 
 /// A PAM-specific Result type with an [ErrorCode] error.
@@ -159,37 +226,6 @@
     }
 }
 
-impl TryFrom<c_int> for ErrorCode {
-    type Error = InvalidEnum<Self>;
-
-    fn try_from(value: c_int) -> StdResult<Self, Self::Error> {
-        Self::from_i32(value).ok_or(value.into())
-    }
-}
-
-impl From<ErrorCode> for c_int {
-    fn from(val: ErrorCode) -> Self {
-        val as Self
-    }
-}
-
-/// Error returned when attempting to coerce an invalid C integer into an enum.
-#[derive(Debug, PartialEq, thiserror::Error)]
-#[error("{0} is not a valid {type}", type = any::type_name::<T>())]
-pub struct InvalidEnum<T>(c_int, PhantomData<T>);
-
-impl<T> From<InvalidEnum<T>> for c_int {
-    fn from(value: InvalidEnum<T>) -> Self {
-        value.0
-    }
-}
-
-impl<T> From<c_int> for InvalidEnum<T> {
-    fn from(value: c_int) -> Self {
-        Self(value, PhantomData)
-    }
-}
-
 /// Returned when text that should not have any `\0` bytes in it does.
 /// Analogous to [`std::ffi::NulError`], but the data it was created from
 /// is borrowed.
@@ -199,13 +235,15 @@
 
     #[test]
     fn test_enums() {
-        assert_eq!(Ok(ErrorCode::ServiceError), 3.try_into());
-        assert_eq!(Err(InvalidEnum::from(999)), ErrorCode::try_from(999));
         assert_eq!(Ok(()), ErrorCode::result_from(0));
-        assert_eq!(Err(ErrorCode::Abort), ErrorCode::result_from(26));
+        assert_eq!(
+            pam_ffi::PAM_BAD_ITEM,
+            ErrorCode::result_to_c::<()>(Err(ErrorCode::BadItem))
+        );
+        assert_eq!(
+            Err(ErrorCode::Abort),
+            ErrorCode::result_from(pam_ffi::PAM_ABORT)
+        );
         assert_eq!(Err(ErrorCode::SystemError), ErrorCode::result_from(423));
-        assert!(InvalidEnum::<ErrorCode>(33, PhantomData)
-            .to_string()
-            .starts_with("33 is not a valid "));
     }
 }
--- a/src/handle.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/handle.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -1,64 +1,52 @@
 //! The wrapper types and traits for handles into the PAM library.
+
 use crate::constants::Result;
 use crate::conv::Conversation;
 
 macro_rules! trait_item {
-    (get = $getter:ident, item = $item:literal $(, see = $see:path)? $(, $($doc:literal)*)?) => {
-        $(
-            $(#[doc = $doc])*
-            #[doc = ""]
-        )?
+    ($(#[$md:meta])* get = $getter:ident, item = $item:literal $(, see = $see:path)?) => {
+        $(#[$md])*
+        #[doc = ""]
         #[doc = concat!("Gets the `", $item, "` of the PAM handle.")]
         $(
             #[doc = concat!("See [`", stringify!($see), "`].")]
         )?
-        #[doc = ""]
-        #[doc = "Returns a reference to the item's value, owned by PAM."]
-        #[doc = "The item is assumed to be valid UTF-8 text."]
-        #[doc = "If it is not, `ConversationError` is returned."]
-        #[doc = ""]
-        #[doc = "See the [`pam_get_item`][man] manual page,"]
-        #[doc = "[`pam_get_item` in the Module Writers' Guide][mwg], or"]
-        #[doc = "[`pam_get_item` in the Application Developers' Guide][adg]."]
-        #[doc = ""]
-        #[doc = "[man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html"]
-        #[doc = "[adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_get_item"]
-        #[doc = "[mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item"]
-        fn $getter(&mut self) -> Result<Option<&str>>;
+        ///
+        /// Returns a reference to the item's value, owned by PAM.
+        /// The item is assumed to be valid UTF-8 text.
+        /// If it is not, `ConversationError` is returned.
+        ///
+        /// See the [`pam_get_item`][man] manual page,
+        /// [`pam_get_item` in the Module Writers' Guide][mwg], or
+        /// [`pam_get_item` in the Application Developers' Guide][adg].
+        ///
+        /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html
+        /// [adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_get_item
+        /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item
+        fn $getter(&self) -> Result<Option<&str>>;
     };
-    (set = $setter:ident, item = $item:literal $(, see = $see:path)? $(, $($doc:literal)*)?) => {
-        $(
-            $(#[doc = $doc])*
-            #[doc = ""]
-        )?
+    ($(#[$md:meta])* set = $setter:ident, item = $item:literal $(, see = $see:path)?) => {
+        $(#[$md])*
         #[doc = concat!("Sets the `", $item, "` from the PAM handle.")]
         $(
             #[doc = concat!("See [`", stringify!($see), "`].")]
         )?
-        #[doc = ""]
-        #[doc = "Sets the item's value. PAM copies the string's contents."]
-        #[doc = "If the string contains a null byte, this will return "]
-        #[doc = "a `ConversationError`."]
-        #[doc = ""]
-        #[doc = "See the [`pam_set_item`][man] manual page,"]
-        #[doc = "[`pam_set_item` in the Module Writers' Guide][mwg], or"]
-        #[doc = "[`pam_set_item` in the Application Developers' Guide][adg]."]
-        #[doc = ""]
-        #[doc = "[man]: https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html"]
-        #[doc = "[adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_set_item"]
-        #[doc = "[mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_item"]
+        ///
+        /// Sets the item's value. PAM copies the string's contents.
+        /// If the string contains a null byte, this will return
+        /// a `ConversationError`.
+        ///
+        /// See the [`pam_set_item`][man] manual page,
+        /// [`pam_set_item` in the Module Writers' Guide][mwg], or
+        /// [`pam_set_item` in the Application Developers' Guide][adg].
+        ///
+        /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html
+        /// [adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_set_item
+        /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_item
         fn $setter(&mut self, value: Option<&str>) -> Result<()>;
     };
 }
 
-/// All-in-one trait for what you should expect from PAM as an application.
-pub trait PamHandleApplication: PamApplicationOnly + PamShared {}
-impl<T> PamHandleApplication for T where T: PamApplicationOnly + PamShared {}
-
-/// All-in-one trait for what you should expect from PAM as a module.
-pub trait PamHandleModule: PamModuleOnly + PamShared {}
-impl<T> PamHandleModule for T where T: PamModuleOnly + PamShared {}
-
 /// Functionality for both PAM applications and PAM modules.
 ///
 /// This base trait includes features of a PAM handle that are available
@@ -100,130 +88,130 @@
     fn get_user(&mut self, prompt: Option<&str>) -> Result<&str>;
 
     trait_item!(
+        /// The identity of the user for whom service is being requested.
+        ///
+        /// Unlike [`get_user`](Self::get_user), this will simply get
+        /// the current state of the user item, and not request the username.
+        /// While PAM usually sets this automatically in the `get_user` call,
+        /// it may be changed by a module during the PAM transaction.
+        /// Applications should check it after each step of the PAM process.
         get = user_item,
         item = "PAM_USER",
-        see = Self::get_user,
-        "The identity of the user for whom service is being requested."
-        ""
-        "Unlike [`get_user`](Self::get_user), this will simply get"
-        "the current state of the user item, and not request the username. "
-        "While PAM usually sets this automatically in the `get_user` call, "
-        "it may be changed by a module during the PAM transaction. "
-        "Applications should check it after each step of the PAM process."
+        see = Self::get_user
     );
     trait_item!(
+        /// Sets the identity of the logging-in user.
+        ///
+        /// Usually this will be set during the course of
+        /// a [`get_user`](Self::get_user) call, but you may set it manually
+        /// or change it during the PAM process.
         set = set_user_item,
         item = "PAM_USER",
-        see = Self::user_item,
-        "Sets the identity of the logging-in user."
-        ""
-        "Usually this will be set during the course of "
-        "a [`get_user`](Self::get_user) call, but you may set it manually "
-        "or change it during the PAM process."
+        see = Self::user_item
     );
 
     trait_item!(
+        /// The service name, which identifies the PAM stack which is used
+        /// to perform authentication.
         get = service,
-        item = "PAM_SERVICE",
-        "The service name, which identifies the PAM stack which is used "
-        "to perform authentication."
+        item = "PAM_SERVICE"
     );
     trait_item!(
+        /// The service name, which identifies the PAM stack which is used
+        /// to perform authentication. It's probably a bad idea to change this.
         set = set_service,
         item = "PAM_SERVICE",
-        see = Self::service,
-        "The service name, which identifies the PAM stack which is used "
-        "to perform authentication. It's probably a bad idea to change this."
+        see = Self::service
     );
 
     trait_item!(
+        /// The string used to prompt for a user's name.
+        /// By default, this is a localized version of `login: `.
         get = user_prompt,
-        item = "PAM_USER_PROMPT",
-        "The string used to prompt for a user's name."
-        "By default, this is a localized version of `login: `."
+        item = "PAM_USER_PROMPT"
     );
     trait_item!(
+        /// Sets the string used to prompt for a user's name.
         set = set_user_prompt,
         item = "PAM_USER_PROMPT",
-        see = Self::user_prompt,
-        "Sets the string used to prompt for a user's name."
+        see = Self::user_prompt
     );
 
     trait_item!(
+        /// "The terminal name prefixed by /dev/ for device files."
+        ///
+        /// This is the terminal the user is logging in on.
+        /// Very old applications may use this instead of `PAM_XDISPLAY`.
         get = tty_name,
-        item = "PAM_TTY",
-        "\"The terminal name prefixed by /dev/ for device files.\""
-        ""
-        "This is the terminal the user is logging in on."
-        "Very old applications may use this instead of `PAM_XDISPLAY`."
+        item = "PAM_TTY"
     );
     trait_item!(
+        /// Sets the terminal name.
+        ///
+        /// (TODO: See if libpam sets this itself or if the application does.)
         set = set_tty_name,
         item = "PAM_TTY",
-        see = Self::tty_name,
-        "Sets the terminal name."
-        ""
-        "(TODO: See if libpam sets this itself or if the application does.)"
+        see = Self::tty_name
     );
 
     trait_item!(
+        /// If set, the identity of the remote user logging in.
+        ///
+        /// This is only as trustworthy as the application calling PAM.
+        /// Also see [`remote_host`](Self::remote_host).
         get = remote_user,
-        item = "PAM_RUSER",
-        "If set, the identity of the remote user logging in."
-        ""
-        "This is only as trustworthy as the application calling PAM."
-        "Also see [`remote_host`](Self::remote_host)."
+        item = "PAM_RUSER"
     );
     trait_item!(
+        /// Sets the identity of the remote user logging in.
+        ///
+        /// This is usually set by the application before making calls
+        /// into a PAM session. (TODO: check this!)
         set = set_remote_user,
-        item = "PAM_RUSER",
-        "Sets the identity of the remote user logging in."
-        ""
-        "This is usually set by the application before making calls "
-        "into a PAM session. (TODO: check this!)"
+        item = "PAM_RUSER"
     );
 
     trait_item!(
+        /// If set, the remote location where the user is coming from.
+        ///
+        /// This is only as trustworthy as the application calling PAM.
+        /// This can be combined with [`Self::remote_user`] to identify
+        /// the account the user is attempting to log in from,
+        /// with `remote_user@remote_host`.
+        ///
+        /// If unset, "it is unclear where the authentication request
+        /// is originating from."
         get = remote_host,
-        item = "PAM_RHOST",
-        "If set, the remote location where the user is coming from."
-        ""
-        "This is only as trustworthy as the application calling PAM. "
-        "This can be combined with [`Self::remote_user`] to identify "
-        "the account the user is attempting to log in from, "
-        "with `remote_user@remote_host`."
-        ""
-        "If unset, \"it is unclear where the authentication request "
-        "is originating from.\""
+        item = "PAM_RHOST"
     );
     trait_item!(
+        /// Sets the location where the user is coming from.
+        ///
+        /// This is usually set by the application before making calls
+        /// into a PAM session. (TODO: check this!)
         set = set_remote_host,
         item = "PAM_RHOST",
-        see = Self::remote_host,
-        "Sets the location where the user is coming from."
-        ""
-        "This is usually set by the application before making calls "
-        "into a PAM session. (TODO: check this!)"
+        see = Self::remote_host
     );
 
     trait_item!(
+        /// Gets the user's authentication token (e.g., password).
+        ///
+        /// This is usually set automatically when
+        /// [`get_authtok`](PamHandleModule::get_authtok) is called,
+        /// but can be manually set.
         set = set_authtok_item,
         item = "PAM_AUTHTOK",
-        see = PamModuleOnly::authtok_item,
-        "Gets the user's authentication token (e.g., password)."
-        ""
-        "This is usually set automatically when "
-        "[`get_authtok`](PamModuleOnly::get_authtok) is called, "
-        "but can be manually set."
+        see = PamHandleModule::authtok_item
     );
 
     trait_item!(
+        /// Sets the user's "old authentication token" when changing passwords.
+        //
+        /// This is usually set automatically by PAM.
         set = set_old_authtok_item,
         item = "PAM_OLDAUTHTOK",
-        see = PamModuleOnly::old_authtok_item,
-        "Sets the user's \"old authentication token\" when changing passwords."
-        ""
-        "This is usually set automatically by PAM."
+        see = PamHandleModule::old_authtok_item
     );
 }
 
@@ -234,29 +222,8 @@
 ///
 /// Like [`PamShared`], this is intended to allow creating mock implementations
 /// of PAM for testing PAM applications.
-pub trait PamApplicationOnly {
-    /// Closes the PAM session on an owned PAM handle.
-    ///
-    /// This should be called with the result of the application's last call
-    /// into PAM services. Since this is only applicable to *owned* PAM handles,
-    /// a PAM module should never call this (and it will never be handed
-    /// an owned `PamHandle` that it can `close`).
-    ///
-    /// See the [`pam_end` manual page][man] for more information.
-    ///
-    /// ```no_run
-    /// # use nonstick::handle::PamApplicationOnly;
-    /// # use std::error::Error;
-    /// # fn _doc(handle: impl PamApplicationOnly, auth_result: nonstick::Result<()>) -> Result<(), Box<dyn Error>> {
-    /// // Earlier: authentication was performed and the result was stored
-    /// // into auth_result.
-    /// handle.close(auth_result)?;
-    /// # Ok(())
-    /// # }
-    /// ```
-    ///
-    /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html
-    fn close(self, status: Result<()>) -> Result<()>;
+pub trait PamHandleApplication: PamShared {
+    // reserved!
 }
 
 /// Functionality of a PAM handle that can be expected by a PAM module.
@@ -266,7 +233,7 @@
 ///
 /// Like [`PamShared`], this is intended to allow creating mock implementations
 /// of PAM for testing PAM modules.
-pub trait PamModuleOnly: Conversation {
+pub trait PamHandleModule: Conversation + PamShared {
     /// Retrieves the authentication token from the user.
     ///
     /// This should only be used by *authentication* and *password-change*
@@ -278,8 +245,8 @@
     /// # Example
     ///
     /// ```no_run
-    /// # use nonstick::handle::PamModuleOnly;
-    /// # fn _doc(handle: &mut impl PamModuleOnly) -> Result<(), Box<dyn std::error::Error>> {
+    /// # use nonstick::handle::PamHandleModule;
+    /// # fn _doc(handle: &mut impl PamHandleModule) -> Result<(), Box<dyn std::error::Error>> {
     /// // Get the user's password using the default prompt.
     /// let pass = handle.get_authtok(None)?;
     /// // Get the user's password using a custom prompt.
@@ -293,25 +260,25 @@
     fn get_authtok(&mut self, prompt: Option<&str>) -> Result<&str>;
 
     trait_item!(
+        /// Gets the user's authentication token (e.g., password).
+        ///
+        /// This is normally set automatically by PAM when calling
+        /// [`get_authtok`](Self::get_authtok), but can be set explicitly.
+        ///
+        /// Like `get_authtok`, this should only ever be called
+        /// by *authentication* and *password-change* PAM modules.
         get = authtok_item,
         item = "PAM_AUTHTOK",
-        see = Self::get_authtok,
-        "Gets the user's authentication token (e.g., password)."
-        ""
-        "This is normally set automatically by PAM when calling "
-        "[`get_authtok`](Self::get_authtok), but can be set explicitly."
-        ""
-        "Like `get_authtok`, this should only ever be called "
-        "by *authentication* and *password-change* PAM modules."
+        see = Self::get_authtok
     );
 
     trait_item!(
+        /// Gets the user's old authentication token when changing passwords.
+        ///
+        /// This should only ever be called by *password-change* PAM modules.
         get = old_authtok_item,
         item = "PAM_OLDAUTHTOK",
-        see = PamShared::set_old_authtok_item,
-        "Gets the user's old authentication token when changing passwords."
-        ""
-        "This should only ever be called by *password-change* PAM modules."
+        see = PamShared::set_old_authtok_item
     );
 
     /*
--- a/src/libpam/answer.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/libpam/answer.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -2,9 +2,10 @@
 
 use crate::libpam::conversation::OwnedMessage;
 use crate::libpam::memory;
-use crate::libpam::memory::{CBinaryData, Immovable};
+use crate::libpam::memory::CBinaryData;
+pub use crate::libpam::pam_ffi::Answer;
 use crate::{ErrorCode, Result};
-use std::ffi::{c_int, c_void, CStr};
+use std::ffi::CStr;
 use std::ops::{Deref, DerefMut};
 use std::{iter, mem, ptr, slice};
 
@@ -195,23 +196,6 @@
     }
 }
 
-/// Generic version of answer data.
-///
-/// This has the same structure as [`BinaryAnswer`]
-/// and [`TextAnswer`].
-#[repr(C)]
-#[derive(Debug)]
-pub struct Answer {
-    /// Pointer to the data returned in an answer.
-    /// For most answers, this will be a [`CStr`], but for answers to
-    /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`]
-    /// (a Linux-PAM extension).
-    data: *mut c_void,
-    /// Unused.
-    return_code: c_int,
-    _marker: Immovable,
-}
-
 impl Answer {
     /// Frees the contents of this answer.
     ///
--- a/src/libpam/conversation.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/libpam/conversation.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -3,50 +3,15 @@
 };
 use crate::libpam::answer::{Answer, Answers, BinaryAnswer, TextAnswer};
 use crate::libpam::memory::Immovable;
-use crate::libpam::question::{Indirect, Questions};
+use crate::libpam::pam_ffi::AppData;
+pub use crate::libpam::pam_ffi::LibPamConversation;
+use crate::libpam::question::{Indirect, IndirectTrait, Question, Questions};
 use crate::ErrorCode;
 use crate::Result;
 use std::ffi::c_int;
 use std::iter;
 use std::marker::PhantomData;
 
-/// An opaque structure that is passed through PAM in a conversation.
-#[repr(C)]
-pub struct AppData {
-    _data: (),
-    _marker: Immovable,
-}
-
-/// The callback that PAM uses to get information in a conversation.
-///
-/// - `num_msg` is the number of messages in the `pam_message` array.
-/// - `messages` is a pointer to the messages being sent to the user.
-///   For details about its structure, see the documentation of
-///   [`OwnedMessages`](super::OwnedMessages).
-/// - `responses` is a pointer to an array of [`Answer`]s,
-///   which PAM sets in response to a module's request.
-///   This is an array of structs, not an array of pointers to a struct.
-///   There should always be exactly as many `responses` as `num_msg`.
-/// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed.
-pub type ConversationCallback = unsafe extern "C" fn(
-    num_msg: c_int,
-    messages: *const Indirect,
-    responses: *mut *mut Answer,
-    appdata: *mut AppData,
-) -> c_int;
-
-/// The type used by PAM to call back into a conversation.
-#[repr(C)]
-pub struct LibPamConversation<'a> {
-    /// The function that is called to get information from the user.
-    callback: ConversationCallback,
-    /// The pointer that will be passed as the last parameter
-    /// to the conversation callback.
-    appdata: *mut AppData,
-    life: PhantomData<&'a mut ()>,
-    _marker: Immovable,
-}
-
 impl LibPamConversation<'_> {
     fn wrap<C: Conversation>(conv: &mut C) -> Self {
         Self {
@@ -59,7 +24,7 @@
 
     unsafe extern "C" fn wrapper_callback<C: Conversation>(
         count: c_int,
-        questions: *const Indirect,
+        questions: *const *const Question,
         answers: *mut *mut Answer,
         me: *mut AppData,
     ) -> c_int {
@@ -69,7 +34,7 @@
                 .cast::<C>()
                 .as_mut()
                 .ok_or(ErrorCode::ConversationError)?;
-            let indirect = questions.as_ref().ok_or(ErrorCode::ConversationError)?;
+            let indirect = Indirect::borrow_ptr(questions).ok_or(ErrorCode::ConversationError)?;
             let answers_ptr = answers.as_mut().ok_or(ErrorCode::ConversationError)?;
 
             // Build our owned list of Q&As from the questions we've been asked
--- a/src/libpam/handle.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/libpam/handle.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -1,110 +1,66 @@
 use super::conversation::LibPamConversation;
-use crate::constants::{ErrorCode, InvalidEnum, Result};
+use crate::constants::{ErrorCode, Result};
 use crate::conv::Message;
-use crate::handle::{PamApplicationOnly, PamModuleOnly, PamShared};
-use crate::libpam::memory;
-use crate::libpam::memory::Immovable;
-use crate::Conversation;
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
+use crate::handle::PamShared;
+pub use crate::libpam::pam_ffi::LibPamHandle;
+use crate::libpam::{memory, pam_ffi};
+use crate::{Conversation, PamHandleModule};
+use num_enum::{IntoPrimitive, TryFromPrimitive};
+use std::cell::Cell;
 use std::ffi::{c_char, c_int};
 use std::ops::{Deref, DerefMut};
-use std::result::Result as StdResult;
-use std::{mem, ptr};
-
-/// An owned PAM handle.
-#[repr(transparent)]
-pub struct OwnedLibPamHandle(*mut LibPamHandle);
-
-/// An opaque structure that a PAM handle points to.
-#[repr(C)]
-pub struct LibPamHandle {
-    _data: (),
-    _marker: Immovable,
-}
-
-impl LibPamHandle {
-    /// Gets a C string item.
-    ///
-    /// # Safety
-    ///
-    /// You better be requesting an item which is a C string.
-    unsafe fn get_cstr_item(&mut self, item_type: ItemType) -> Result<Option<&str>> {
-        let mut output = ptr::null();
-        let ret = unsafe { super::pam_get_item(self, item_type as c_int, &mut output) };
-        ErrorCode::result_from(ret)?;
-        memory::wrap_string(output.cast())
-    }
+use std::ptr;
 
-    /// 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 {
-            super::pam_set_item(
-                self,
-                item_type as c_int,
-                memory::prompt_ptr(data_str.as_ref()).cast(),
-            )
-        };
-        ErrorCode::result_from(ret)
-    }
+struct HandleWrap(*mut LibPamHandle);
 
-    /// Gets the `PAM_CONV` item from the handle.
-    fn conversation_item(&mut self) -> Result<&mut LibPamConversation<'_>> {
-        let output: *mut LibPamConversation = ptr::null_mut();
-        let result = unsafe {
-            super::pam_get_item(
-                self,
-                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)
-    }
-}
-
-impl PamApplicationOnly for OwnedLibPamHandle {
-    fn close(self, status: Result<()>) -> Result<()> {
-        let ret = unsafe { super::pam_end(self.0, ErrorCode::result_to_c(status)) };
-        // Forget rather than dropping, since dropping also calls pam_end.
-        mem::forget(self);
-        ErrorCode::result_from(ret)
-    }
-}
-
-impl Deref for OwnedLibPamHandle {
+impl Deref for HandleWrap {
     type Target = LibPamHandle;
     fn deref(&self) -> &Self::Target {
         unsafe { &*self.0 }
     }
 }
 
-impl DerefMut for OwnedLibPamHandle {
+impl DerefMut for HandleWrap {
     fn deref_mut(&mut self) -> &mut Self::Target {
         unsafe { &mut *self.0 }
     }
 }
 
+/// An owned PAM handle.
+pub struct OwnedLibPamHandle {
+    handle: HandleWrap,
+    last_return: Cell<Result<()>>,
+}
+
+// TODO: pam_authenticate - app
+//       pam_setcred - app
+//       pam_acct_mgmt - app
+//       pam_chauthtok - app
+//       pam_open_session - app
+//       pam_close_session - app
+//       pam_putenv - shared
+//       pam_getenv - shared
+//       pam_getenvlist - shared
+
 impl Drop for OwnedLibPamHandle {
-    /// Ends the PAM session with a zero error code.
-    /// You probably want to call [`close`](Self::close) instead of
-    /// letting this drop by itself.
+    /// Closes the PAM session on an owned PAM handle.
+    ///
+    /// See the [`pam_end` manual page][man] for more information.
+    ///
+    /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html
     fn drop(&mut self) {
         unsafe {
-            super::pam_end(self.0, 0);
+            pam_ffi::pam_end(
+                self.handle.0,
+                ErrorCode::result_to_c(self.last_return.get()),
+            );
         }
     }
 }
 
 macro_rules! cstr_item {
     (get = $getter:ident, item = $item_type:path) => {
-        fn $getter(&mut self) -> Result<Option<&str>> {
+        fn $getter(&self) -> Result<Option<&str>> {
             unsafe { self.get_cstr_item($item_type) }
         }
     };
@@ -119,8 +75,9 @@
     fn get_user(&mut self, prompt: Option<&str>) -> Result<&str> {
         let prompt = memory::option_cstr(prompt)?;
         let mut output: *const c_char = ptr::null();
-        let ret =
-            unsafe { super::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref())) };
+        let ret = unsafe {
+            pam_ffi::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref()))
+        };
         ErrorCode::result_from(ret)?;
         unsafe { memory::wrap_string(output) }
             .transpose()
@@ -156,13 +113,13 @@
     }
 }
 
-impl PamModuleOnly for LibPamHandle {
+impl PamHandleModule for LibPamHandle {
     fn get_authtok(&mut self, prompt: Option<&str>) -> Result<&str> {
         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 {
-            super::pam_get_authtok(
+            pam_ffi::pam_get_authtok(
                 self,
                 ItemType::AuthTok.into(),
                 &mut output,
@@ -190,9 +147,95 @@
     }
 }
 
+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<&str>> {
+        let mut output = ptr::null();
+        let ret = unsafe { pam_ffi::pam_get_item(self, item_type as c_int, &mut output) };
+        ErrorCode::result_from(ret)?;
+        memory::wrap_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 {
+            pam_ffi::pam_set_item(
+                self,
+                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(&mut self) -> Result<&mut LibPamConversation<'_>> {
+        let output: *mut LibPamConversation = ptr::null_mut();
+        let result = unsafe {
+            pam_ffi::pam_get_item(
+                self,
+                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 {
+    (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
+        }
+    };
+    (get = $get:ident$(, set = $set:ident)?) => {
+        delegate!(fn $get(&self) -> Result<Option<&str>>);
+        $(delegate!(set = $set);)?
+    };
+    (set = $set:ident) => {
+        delegate!(fn $set(&mut self, value: Option<&str>) -> Result<()>);
+    };
+}
+
+fn split<T>(result: &Result<T>) -> Result<()> {
+    result.as_ref().map(drop).map_err(|&e| e)
+}
+
+impl PamShared for OwnedLibPamHandle {
+    delegate!(fn get_user(&mut self, prompt: Option<&str>) -> Result<&str>);
+    delegate!(get = user_item, set = set_user_item);
+    delegate!(get = service, set = set_service);
+    delegate!(get = user_prompt, set = set_user_prompt);
+    delegate!(get = tty_name, set = set_tty_name);
+    delegate!(get = remote_user, set = set_remote_user);
+    delegate!(get = remote_host, set = set_remote_host);
+    delegate!(set = set_authtok_item);
+    delegate!(set = set_old_authtok_item);
+}
+
 /// Identifies what is being gotten or set with `pam_get_item`
 /// or `pam_set_item`.
-#[derive(FromPrimitive)]
+#[derive(TryFromPrimitive, IntoPrimitive)]
 #[repr(i32)]
 #[non_exhaustive] // because C could give us anything!
 pub enum ItemType {
@@ -223,16 +266,3 @@
     /// The type of `pam_get_authtok`.
     AuthTokType = 13,
 }
-
-impl TryFrom<c_int> for ItemType {
-    type Error = InvalidEnum<Self>;
-    fn try_from(value: c_int) -> StdResult<Self, Self::Error> {
-        Self::from_i32(value).ok_or(value.into())
-    }
-}
-
-impl From<ItemType> for c_int {
-    fn from(val: ItemType) -> Self {
-        val as Self
-    }
-}
--- a/src/libpam/memory.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/libpam/memory.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -25,7 +25,7 @@
 
 /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`].
 #[repr(C)]
-#[derive(Debug)]
+#[derive(Debug, Default)]
 pub struct Immovable(pub PhantomData<(*mut u8, PhantomPinned)>);
 
 /// Safely converts a `&str` option to a `CString` option.
--- a/src/libpam/mod.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/libpam/mod.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -13,52 +13,8 @@
 mod handle;
 mod memory;
 mod module;
+pub(crate) mod pam_ffi;
 mod question;
 
+#[doc(inline)]
 pub use handle::{LibPamHandle, OwnedLibPamHandle};
-use std::ffi::{c_char, c_int, c_void};
-
-#[link(name = "pam")]
-extern "C" {
-    fn pam_get_data(
-        pamh: *mut LibPamHandle,
-        module_data_name: *const c_char,
-        data: &mut *const c_void,
-    ) -> c_int;
-
-    fn pam_set_data(
-        pamh: *mut LibPamHandle,
-        module_data_name: *const c_char,
-        data: *const c_void,
-        cleanup: extern "C" fn(pamh: *const c_void, data: *mut c_void, error_status: c_int),
-    ) -> c_int;
-
-    fn pam_get_item(pamh: *mut LibPamHandle, item_type: c_int, item: &mut *const c_void) -> c_int;
-
-    fn pam_set_item(pamh: *mut LibPamHandle, item_type: c_int, item: *const c_void) -> c_int;
-
-    fn pam_get_user(
-        pamh: *mut LibPamHandle,
-        user: &mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-    fn pam_get_authtok(
-        pamh: *mut LibPamHandle,
-        item_type: c_int,
-        data: &mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-    fn pam_end(pamh: *mut LibPamHandle, status: c_int) -> c_int;
-
-    // TODO: pam_authenticate - app
-    //       pam_setcred - app
-    //       pam_acct_mgmt - app
-    //       pam_chauthtok - app
-    //       pam_open_session - app
-    //       pam_close_session - app
-    //       pam_putenv - shared
-    //       pam_getenv - shared
-    //       pam_getenvlist - shared
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/pam_ffi.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -0,0 +1,115 @@
+//! The types that are directly represented in PAM function signatures.
+
+#![allow(non_camel_case_types)]
+
+use crate::libpam::memory::Immovable;
+use std::ffi::{c_int, c_void};
+use std::marker::PhantomData;
+use num_enum::{IntoPrimitive, TryFromPrimitive};
+
+/// An opaque structure that a PAM handle points to.
+#[repr(C)]
+pub struct LibPamHandle {
+    _data: (),
+    _marker: Immovable,
+}
+
+/// An opaque structure that is passed through PAM in a conversation.
+#[repr(C)]
+pub struct AppData {
+    _data: (),
+    _marker: Immovable,
+}
+
+/// Generic version of answer data.
+///
+/// This has the same structure as [`BinaryAnswer`](crate::libpam::answer::BinaryAnswer)
+/// and [`TextAnswer`](crate::libpam::answer::TextAnswer).
+#[repr(C)]
+#[derive(Debug)]
+pub struct Answer {
+    /// Pointer to the data returned in an answer.
+    /// For most answers, this will be a [`CStr`](std::ffi::CStr),
+    /// but for [`BinaryQAndA`](crate::conv::BinaryQAndA)s (a Linux-PAM extension),
+    /// this will be [`CBinaryData`](crate::libpam::memory::CBinaryData)
+    pub data: *mut c_void,
+    /// Unused.
+    return_code: c_int,
+    _marker: Immovable,
+}
+
+/// The C enum values for messages shown to the user.
+#[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
+#[repr(i32)]
+pub enum Style {
+    /// Requests information from the user; will be masked when typing.
+    PromptEchoOff = 1,
+    /// Requests information from the user; will not be masked.
+    PromptEchoOn = 2,
+    /// An error message.
+    ErrorMsg = 3,
+    /// An informational message.
+    TextInfo = 4,
+    /// Yes/No/Maybe conditionals. A Linux-PAM extension.
+    RadioType = 5,
+    /// For server–client non-human interaction.
+    ///
+    /// NOT part of the X/Open PAM specification.
+    /// A Linux-PAM extension.
+    BinaryPrompt = 7,
+}
+
+/// A question sent by PAM or a module to an application.
+///
+/// PAM refers to this as a "message", but we call it a question
+/// to avoid confusion with [`Message`](crate::Message).
+///
+/// This question, and its internal data, is owned by its creator
+/// (either the module or PAM itself).
+#[repr(C)]
+pub struct Question {
+    /// The style of message to request.
+    pub style: c_int,
+    /// A description of the data requested.
+    ///
+    /// For most requests, this will be an owned [`CStr`](std::ffi::CStr), but for requests
+    /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`]
+    /// (a Linux-PAM extension).
+    pub data: *mut c_void,
+    pub _marker: Immovable,
+}
+
+/// The callback that PAM uses to get information in a conversation.
+///
+/// - `num_msg` is the number of messages in the `pam_message` array.
+/// - `questions` is a pointer to the [`Question`]s being sent to the user.
+///   For information about its structure,
+///   see [`GenericQuestions`](super::question::GenericQuestions).
+/// - `answers` is a pointer to an array of [`Answer`]s,
+///   which PAM sets in response to a module's request.
+///   This is an array of structs, not an array of pointers to a struct.
+///   There should always be exactly as many `answers` as `num_msg`.
+/// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed.
+pub type ConversationCallback = unsafe extern "C" fn(
+    num_msg: c_int,
+    questions: *const *const Question,
+    answers: *mut *mut Answer,
+    appdata: *mut AppData,
+) -> c_int;
+
+/// The type used by PAM to call back into a conversation.
+#[repr(C)]
+pub struct LibPamConversation<'a> {
+    /// The function that is called to get information from the user.
+    pub callback: ConversationCallback,
+    /// The pointer that will be passed as the last parameter
+    /// to the conversation callback.
+    pub appdata: *mut AppData,
+    pub life: PhantomData<&'a mut ()>,
+    pub _marker: Immovable,
+}
+
+type pam_handle = LibPamHandle;
+type pam_conv = LibPamConversation<'static>;
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
--- a/src/libpam/question.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/libpam/question.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -1,16 +1,13 @@
 //! Data and types dealing with PAM messages.
 
-use crate::constants::InvalidEnum;
 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA};
 use crate::libpam::conversation::OwnedMessage;
 use crate::libpam::memory;
 use crate::libpam::memory::{CBinaryData, Immovable};
+pub use crate::libpam::pam_ffi::{Question, Style};
 use crate::ErrorCode;
 use crate::Result;
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-use std::ffi::{c_int, c_void, CStr};
-use std::result::Result as StdResult;
+use std::ffi::{c_void, CStr};
 use std::{iter, ptr, slice};
 
 /// Abstraction of a collection of questions to be sent in a PAM conversation.
@@ -57,21 +54,19 @@
 ///                                                  ╟──────────────╢
 ///                                                  ║ ...          ║
 /// ```
-///
-/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
-pub struct Questions {
+pub struct GenericQuestions<I: IndirectTrait> {
     /// An indirection to the questions themselves, stored on the C heap.
-    indirect: *mut Indirect,
+    indirect: *mut I,
     /// The number of questions.
     count: usize,
 }
 
-impl Questions {
+impl<I: IndirectTrait> GenericQuestions<I> {
     /// Stores the provided questions on the C heap.
     pub fn new(messages: &[Message]) -> Result<Self> {
         let count = messages.len();
         let mut ret = Self {
-            indirect: Indirect::alloc(count),
+            indirect: I::alloc(count),
             count,
         };
         // Even if we fail partway through this, all our memory will be freed.
@@ -82,8 +77,8 @@
     }
 
     /// The pointer to the thing with the actual list.
-    pub fn indirect(&self) -> *const Indirect {
-        self.indirect
+    pub fn indirect(&self) -> *const *const Question {
+        self.indirect.cast()
     }
 
     pub fn iter(&self) -> impl Iterator<Item = &Question> {
@@ -96,13 +91,13 @@
     }
 }
 
-impl Drop for Questions {
+impl<I: IndirectTrait> Drop for GenericQuestions<I> {
     fn drop(&mut self) {
         // SAFETY: We are valid and have a valid pointer.
         // Once we're done, everything will be safe.
         unsafe {
             if let Some(indirect) = self.indirect.as_mut() {
-                indirect.free(self.count)
+                indirect.free_contents(self.count)
             }
             memory::free(self.indirect);
             self.indirect = ptr::null_mut();
@@ -110,56 +105,131 @@
     }
 }
 
+/// The trait that each of the `Indirect` implementations implement.
+///
+/// Basically a slice but with more meat.
+pub trait IndirectTrait {
+    /// Converts a pointer into a borrowed `Self`.
+    ///
+    /// # Safety
+    ///
+    /// You have to provide a valid pointer.
+    unsafe fn borrow_ptr<'a>(ptr: *const *const Question) -> Option<&'a Self>
+    where
+        Self: Sized,
+    {
+        ptr.cast::<Self>().as_ref()
+    }
+
+    /// Allocates memory for this indirector and all its members.
+    fn alloc(count: usize) -> *mut Self;
+
+    /// Returns an iterator yielding the given number of messages.
+    ///
+    /// # Safety
+    ///
+    /// You have to provide the right count.
+    unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question>;
+
+    /// Returns a mutable iterator yielding the given number of messages.
+    ///
+    /// # Safety
+    ///
+    /// You have to provide the right count.
+    unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question>;
+
+    /// Frees everything this points to.
+    ///
+    /// # Safety
+    ///
+    /// You have to pass the right size.
+    unsafe fn free_contents(&mut self, count: usize);
+}
+
+/// An indirect reference to messages.
+///
+/// This is kept separate to provide a place where we can separate
+/// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
+#[cfg(pam_impl = "linux-pam")]
+pub type Indirect = LinuxPamIndirect;
+
 /// An indirect reference to messages.
 ///
 /// This is kept separate to provide a place where we can separate
 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
+#[cfg(not(pam_impl = "linux-pam"))]
+pub type Indirect = XssoIndirect;
+
+pub type Questions = GenericQuestions<Indirect>;
+
+/// The XSSO standard version of the indirection layer between Question and Questions.
 #[repr(transparent)]
-pub struct Indirect {
+pub struct StandardIndirect {
+    base: *mut Question,
+    _marker: Immovable,
+}
+
+impl IndirectTrait for StandardIndirect {
+    fn alloc(count: usize) -> *mut Self {
+        let questions = memory::calloc(count);
+        let me_ptr: *mut Self = memory::calloc(1);
+        // SAFETY: We just allocated this, and we're putting a valid pointer in.
+        unsafe {
+            let me = &mut *me_ptr;
+            me.base = questions;
+        }
+        me_ptr
+    }
+
+    unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> {
+        (0..count).map(|idx| &*self.base.add(idx))
+    }
+
+    unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> {
+        (0..count).map(|idx| &mut *self.base.add(idx))
+    }
+
+    unsafe fn free_contents(&mut self, count: usize) {
+        let msgs = slice::from_raw_parts_mut(self.base, count);
+        for msg in msgs {
+            msg.clear()
+        }
+        memory::free(self.base);
+        self.base = ptr::null_mut()
+    }
+}
+
+/// The Linux version of the indirection layer between Question and Questions.
+#[repr(transparent)]
+pub struct LinuxPamIndirect {
     base: [*mut Question; 0],
     _marker: Immovable,
 }
 
-impl Indirect {
-    /// Allocates memory for this indirector and all its members.
+impl IndirectTrait for LinuxPamIndirect {
     fn alloc(count: usize) -> *mut Self {
         // SAFETY: We're only allocating, and when we're done,
         // everything will be in a known-good state.
-        let me_ptr: *mut Indirect = memory::calloc::<Question>(count).cast();
+        let me_ptr: *mut Self = memory::calloc::<*mut Question>(count).cast();
         unsafe {
             let me = &mut *me_ptr;
             let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count);
             for entry in ptr_list {
                 *entry = memory::calloc(1);
             }
-            me
         }
+        me_ptr
     }
 
-    /// Returns an iterator yielding the given number of messages.
-    ///
-    /// # Safety
-    ///
-    /// You have to provide the right count.
-    pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> {
+    unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> {
         (0..count).map(|idx| &**self.base.as_ptr().add(idx))
     }
 
-    /// Returns a mutable iterator yielding the given number of messages.
-    ///
-    /// # Safety
-    ///
-    /// You have to provide the right count.
-    pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> {
+    unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> {
         (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx))
     }
 
-    /// Frees everything this points to.
-    ///
-    /// # Safety
-    ///
-    /// You have to pass the right size.
-    unsafe fn free(&mut self, count: usize) {
+    unsafe fn free_contents(&mut self, count: usize) {
         let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count);
         for msg in msgs {
             if let Some(msg) = msg.as_mut() {
@@ -171,66 +241,23 @@
     }
 }
 
-/// The C enum values for messages shown to the user.
-#[derive(Debug, PartialEq, FromPrimitive)]
-pub enum Style {
-    /// Requests information from the user; will be masked when typing.
-    PromptEchoOff = 1,
-    /// Requests information from the user; will not be masked.
-    PromptEchoOn = 2,
-    /// An error message.
-    ErrorMsg = 3,
-    /// An informational message.
-    TextInfo = 4,
-    /// Yes/No/Maybe conditionals. A Linux-PAM extension.
-    RadioType = 5,
-    /// For server–client non-human interaction.
-    ///
-    /// NOT part of the X/Open PAM specification.
-    /// A Linux-PAM extension.
-    BinaryPrompt = 7,
-}
-
-impl TryFrom<c_int> for Style {
-    type Error = InvalidEnum<Self>;
-    fn try_from(value: c_int) -> StdResult<Self, Self::Error> {
-        Self::from_i32(value).ok_or(value.into())
+impl Default for Question {
+    fn default() -> Self {
+        Self {
+            style: Default::default(),
+            data: ptr::null_mut(),
+            _marker: Default::default(),
+        }
     }
 }
 
-impl From<Style> for c_int {
-    fn from(val: Style) -> Self {
-        val as Self
-    }
-}
-
-/// A question sent by PAM or a module to an application.
-///
-/// PAM refers to this as a "message", but we call it a question
-/// to avoid confusion with [`Message`].
-///
-/// This question, and its internal data, is owned by its creator
-/// (either the module or PAM itself).
-#[repr(C)]
-pub struct Question {
-    /// The style of message to request.
-    style: c_int,
-    /// A description of the data requested.
-    ///
-    /// For most requests, this will be an owned [`CStr`], but for requests
-    /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`]
-    /// (a Linux-PAM extension).
-    data: *mut c_void,
-    _marker: Immovable,
-}
-
 impl Question {
     /// Replaces the contents of this question with the question
     /// from the message.
     pub fn fill(&mut self, msg: &Message) -> Result<()> {
         let (style, data) = copy_to_heap(msg)?;
         self.clear();
-        self.style = style as c_int;
+        self.style = style.into();
         self.data = data;
         Ok(())
     }
@@ -327,45 +354,52 @@
 #[cfg(test)]
 mod tests {
 
-    use super::{MaskedQAndA, Questions, Result};
-    use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, QAndA, RadioQAndA};
-    use crate::libpam::conversation::OwnedMessage;
+    use super::{
+        BinaryQAndA, ErrorMsg, GenericQuestions, IndirectTrait, InfoMsg, LinuxPamIndirect,
+        MaskedQAndA, OwnedMessage, QAndA, RadioQAndA, Result, StandardIndirect,
+    };
 
-    #[test]
-    fn test_round_trip() {
-        let interrogation = Questions::new(&[
-            MaskedQAndA::new("hocus pocus").message(),
-            BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
-            QAndA::new("what").message(),
-            QAndA::new("who").message(),
-            InfoMsg::new("hey").message(),
-            ErrorMsg::new("gasp").message(),
-            RadioQAndA::new("you must choose").message(),
-        ])
-        .unwrap();
-        let indirect = interrogation.indirect();
+    macro_rules! assert_matches {
+        ($id:ident => $variant:path, $q:expr) => {
+            if let $variant($id) = $id {
+                assert_eq!($q, $id.question());
+            } else {
+                panic!("mismatched enum variant {x:?}", x = $id);
+            }
+        };
+    }
 
-        let remade = unsafe { indirect.as_ref() }.unwrap();
-        let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) }
-            .map(TryInto::try_into)
-            .collect::<Result<_>>()
+    macro_rules! tests { ($fn_name:ident<$typ:ident>) => {
+        #[test]
+        fn $fn_name() {
+            let interrogation = GenericQuestions::<$typ>::new(&[
+                MaskedQAndA::new("hocus pocus").message(),
+                BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
+                QAndA::new("what").message(),
+                QAndA::new("who").message(),
+                InfoMsg::new("hey").message(),
+                ErrorMsg::new("gasp").message(),
+                RadioQAndA::new("you must choose").message(),
+            ])
             .unwrap();
-        let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap();
-        macro_rules! assert_matches {
-            ($id:ident => $variant:path, $q:expr) => {
-                if let $variant($id) = $id {
-                    assert_eq!($q, $id.question());
-                } else {
-                    panic!("mismatched enum variant {x:?}", x = $id);
-                }
-            };
+            let indirect = interrogation.indirect();
+
+            let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap();
+            let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) }
+                .map(TryInto::try_into)
+                .collect::<Result<_>>()
+                .unwrap();
+            let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap();
+            assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus");
+            assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66));
+            assert_matches!(what => OwnedMessage::Prompt, "what");
+            assert_matches!(who => OwnedMessage::Prompt, "who");
+            assert_matches!(hey => OwnedMessage::Info, "hey");
+            assert_matches!(gasp => OwnedMessage::Error, "gasp");
+            assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose");
         }
-        assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus");
-        assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66));
-        assert_matches!(what => OwnedMessage::Prompt, "what");
-        assert_matches!(who => OwnedMessage::Prompt, "who");
-        assert_matches!(hey => OwnedMessage::Info, "hey");
-        assert_matches!(gasp => OwnedMessage::Error, "gasp");
-        assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose");
-    }
+    }}
+
+    tests!(test_xsso<StandardIndirect>);
+    tests!(test_linux<LinuxPamIndirect>);
 }
--- a/src/module.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/module.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -29,7 +29,7 @@
     /// This is probably the first thing you want to implement.
     /// In most cases, you will want to get the user and password,
     /// using [`PamShared::get_user`](crate::PamShared::get_user)
-    /// and [`PamModuleOnly::get_authtok`](crate::handle::PamModuleOnly::get_authtok),
+    /// and [`PamHandleModule::get_authtok`],
     /// and verify them against something.
     ///
     /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg]