changeset 130:80c07e5ab22f

Transfer over (almost) completely to using libpam-sys. This reimplements everything in nonstick on top of the new -sys crate. We don't yet use libpam-sys's helpers for binary message payloads. Soon.
author Paul Fisher <paul@pfish.zone>
date Tue, 01 Jul 2025 06:11:43 -0400
parents 5b2de52dd8b2
children a632a8874131
files Cargo.toml build.rs libpam-sys/build.rs libpam-sys/libpam-sys-test/build.rs libpam-sys/src/ffi.rs src/constants.rs src/conv.rs src/libpam/answer.rs src/libpam/conversation.rs src/libpam/environ.rs src/libpam/handle.rs src/libpam/mod.rs src/libpam/pam_ffi.rs src/libpam/question.rs src/logging.rs testharness/Cargo.toml
diffstat 16 files changed, 374 insertions(+), 688 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.toml	Mon Jun 30 23:49:54 2025 -0400
+++ b/Cargo.toml	Tue Jul 01 06:11:43 2025 -0400
@@ -21,7 +21,7 @@
 rust-version.workspace = true
 
 [features]
-default = ["link"]
+default = ["link", "basic-ext"]
 
 # Enable this to actually link against your system's PAM library.
 #
@@ -30,9 +30,9 @@
 link = []
 
 basic-ext = []
-illumos-ext = []
 linux-pam-ext = []
 openpam-ext = []
+sun-ext = []
 
 # This feature exists only for testing.
 test-install = []
--- a/build.rs	Mon Jun 30 23:49:54 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-use bindgen::MacroTypeVariation;
-use std::env;
-use std::path::PathBuf;
-
-fn main() {
-    if cfg!(feature = "link") {
-        println!("cargo:rustc-link-lib=pam");
-        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")
-            .blocklist_var(".*")
-            .allowlist_function("pam_start")
-            .allowlist_function("pam_[gs]et_item")
-            .allowlist_function("pam_get_(user|authtok)")
-            .allowlist_function("pam_end")
-            .allowlist_function("pam_strerror")
-            .allowlist_function("pam_authenticate")
-            .allowlist_function("pam_chauthtok")
-            .allowlist_function("pam_acct_mgmt")
-            .allowlist_function("pam_(ge|pu)tenv(list)?")
-            .default_macro_constant_type(MacroTypeVariation::Unsigned);
-
-        let linux_builder = common_builder
-            .clone()
-            // This function is not available in OpenPAM.
-            // That means if somebody tries to run a binary compiled for
-            // Linux-PAM against a different impl, it will fail.
-            .allowlist_function("pam_syslog")
-            .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()
-            // This function is not available in Linux-PAM.
-            // That means if somebody tries to run a binary compiled for
-            // OpenPAM against a different impl, it will fail.
-            .allowlist_function("_openpam_log")
-            .header_contents(
-                "openpam.h",
-                r#"
-                #include <security/pam_types.h>
-                #include <security/openpam.h>
-                #include <security/pam_appl.h>
-                #include <security/pam_constants.h>
-                "#,
-            );
-
-        let (pam_impl, bindings) = {
-            if let Ok(bindings) = linux_builder.generate() {
-                ("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/libpam-sys/build.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/libpam-sys/build.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -9,5 +9,8 @@
     let pam_impls = pam_impl_strs.join(",");
     // We use this for ctest. Don't do what we've done; just use cfg_pam_impl.
     println!("cargo:rustc-check-cfg=cfg(_private_pam_impl_hack, values({pam_impls}))");
-    println!("cargo:rustc-cfg=_private_pam_impl_hack=\"{:?}\"", PamImpl::CURRENT);
+    println!(
+        "cargo:rustc-cfg=_private_pam_impl_hack=\"{:?}\"",
+        PamImpl::CURRENT
+    );
 }
--- a/libpam-sys/libpam-sys-test/build.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/libpam-sys/libpam-sys-test/build.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -1,6 +1,6 @@
 use bindgen::MacroTypeVariation;
 use libpam_sys_impls::__pam_impl_enum__;
-use proc_macro2::{Group, TokenStream, TokenTree};
+use proc_macro2::{Group, Ident, TokenStream, TokenTree};
 use quote::{format_ident, ToTokens};
 use std::path::Path;
 use std::process::Command;
@@ -150,11 +150,7 @@
 fn remove_consts(file_contents: &str, out_file: impl AsRef<Path>) {
     let deconstified = deconstify(
         TokenStream::from_str(file_contents).unwrap(),
-        &TokenStream::from_str("mut")
-            .unwrap()
-            .into_iter()
-            .next()
-            .unwrap(),
+        &format_ident!("mut"),
     )
     .to_string();
     let out_file = out_file.as_ref();
@@ -170,7 +166,7 @@
     assert!(status.success(), "rustfmt exited with code {status}");
 }
 
-fn deconstify(stream: TokenStream, mut_token: &TokenTree) -> TokenStream {
+fn deconstify(stream: TokenStream, mut_token: &Ident) -> TokenStream {
     TokenStream::from_iter(stream.into_iter().map(|token| {
         match token {
             TokenTree::Group(g) => {
--- a/libpam-sys/src/ffi.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/libpam-sys/src/ffi.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -78,11 +78,8 @@
 ///     pam_set_data(handle, name.as_ptr().cast_mut(), data_ptr.cast(), cleanup());
 /// }
 /// ```
-pub type CleanupCallback = unsafe extern "C" fn(
-    pamh: *mut pam_handle,
-    data: *mut c_void,
-    pam_end_status: c_int,
-);
+pub type CleanupCallback =
+    unsafe extern "C" fn(pamh: *mut pam_handle, data: *mut c_void, pam_end_status: c_int);
 
 /// Used by PAM to communicate between the module and the application.
 #[repr(C)]
@@ -111,7 +108,6 @@
     pub resp_retcode: c_int,
 }
 
-
 // These are the functions specified in X/SSO. Everybody exports them.
 extern "C" {
     /// Account validation.
@@ -139,16 +135,16 @@
     ) -> c_int;
 
     /// Gets an environment variable.  You own the return value.
-    pub fn pam_getenv(pamh: *mut pam_handle, name: *const c_char) -> *mut c_char;
+    pub fn pam_getenv(pamh: *const pam_handle, name: *const c_char) -> *mut c_char;
 
     /// Gets all the environment variables.  You own everything it points to.
-    pub fn pam_getenvlist(pamh: *mut pam_handle) -> *mut *mut c_char;
+    pub fn pam_getenvlist(pamh: *const pam_handle) -> *mut *mut c_char;
 
     /// Get information about the transaction.
     ///
     /// The item is owned by PAM.
     pub fn pam_get_item(
-        pamh: *mut pam_handle,
+        pamh: *const pam_handle,
         item_type: c_int,
         item: *mut *const c_void,
     ) -> c_int;
@@ -214,7 +210,12 @@
 // when it comes across the `cfg_pam_impl` macro.
 // This is a custom cfg variable set in our build.rs. Don't do this; just use
 // cfg_pam_impl.
-#[cfg(_private_pam_impl_hack = "LinuxPam")]
+#[cfg(any(_private_pam_impl_hack = "LinuxPam", _private_pam_impl_hack = "OpenPam"))]
 extern "C" {
-    pub fn pam_get_authtok(pamh: *mut pam_handle, x: c_int, token: *mut *const c_char, prompt: *const c_char) -> c_int;
+    pub fn pam_get_authtok(
+        pamh: *mut pam_handle,
+        x: c_int,
+        token: *mut *const c_char,
+        prompt: *const c_char,
+    ) -> c_int;
 }
--- a/src/constants.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/constants.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -4,29 +4,25 @@
 // between Linux-PAM and OpenPAM header files.
 #![allow(clippy::unnecessary_cast)]
 
-#[cfg(feature = "link")]
-use crate::libpam::pam_ffi;
 use crate::{linklist, man7, manbsd, xsso};
 use bitflags::bitflags;
-use libc::c_int;
+use std::ffi::c_int;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::error::Error;
-use std::ffi::c_uint;
-use std::fmt;
 use std::fmt::{Display, Formatter};
 use std::result::Result as StdResult;
+use std::fmt;
 
-/// Arbitrary values for PAM constants when not linking against system PAM.
+/// Values for constants not provided by certain PAM implementations.
 ///
 /// **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 pam_ffi {
-    use std::ffi::c_uint;
+    pub use libpam_sys::*;
 
     macro_rules! define {
-        ($(#[$attr:meta])* $($name:ident = $value:expr),+) => {
+        ($(#[$attr:meta])* $($name:ident = $value:expr;)+) => {
             define!(
                 @meta { $(#[$attr])* }
                 $(pub const $name: i32 = $value;)+
@@ -35,60 +31,32 @@
         (@meta $m:tt $($i:item)+) => { define!(@expand $($m $i)+); };
         (@expand $({ $(#[$m:meta])* } $i:item)+) => {$($(#[$m])* $i)+};
     }
-    const fn bit(n: u8) -> 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)
+        /// A fictitious constant for testing purposes.
+        #[cfg(not(feature = "link"))]
+        #[cfg_pam_impl(not("OpenPam"))]
+        PAM_BAD_CONSTANT = 513;
+        PAM_BAD_FEATURE = 514;
     );
 
     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
+        /// A fictitious constant for testing purposes.
+        #[cfg(not(feature = "link"))]
+        #[cfg_pam_impl(not(any("LinuxPam", "OpenPam")))]
+        PAM_BAD_ITEM = 515;
+        PAM_MODULE_UNKNOWN = 516;
     );
 
-    /// Dummy implementation of strerror so that it always returns None.
-    pub fn strerror(val: c_uint) -> Option<&'static str> {
-        _ = val;
-        None
-    }
+    define!(
+        /// A fictitious constant for testing purposes.
+        #[cfg(not(feature = "link"))]
+        #[cfg_pam_impl(not("LinuxPam"))]
+        PAM_CONV_AGAIN = 517;
+        PAM_INCOMPLETE = 518;
+    );
+
 }
 
 bitflags! {
@@ -100,24 +68,24 @@
     #[repr(transparent)]
     pub struct Flags: c_int {
         /// The module should not generate any messages.
-        const SILENT = pam_ffi::PAM_SILENT;
+        const SILENT = libpam_sys::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 = pam_ffi::PAM_DISALLOW_NULL_AUTHTOK;
+        const DISALLOW_NULL_AUTHTOK = libpam_sys::PAM_DISALLOW_NULL_AUTHTOK;
 
         // Flag used for `set_credentials`.
 
         /// Set user credentials for an authentication service.
-        const ESTABLISH_CREDENTIALS = pam_ffi::PAM_ESTABLISH_CRED;
+        const ESTABLISH_CREDENTIALS = libpam_sys::PAM_ESTABLISH_CRED;
         /// Delete user credentials associated with
         /// an authentication service.
-        const DELETE_CREDENTIALS = pam_ffi::PAM_DELETE_CRED;
+        const DELETE_CREDENTIALS = libpam_sys::PAM_DELETE_CRED;
         /// Reinitialize user credentials.
-        const REINITIALIZE_CREDENTIALS = pam_ffi::PAM_REINITIALIZE_CRED;
+        const REINITIALIZE_CREDENTIALS = libpam_sys::PAM_REINITIALIZE_CRED;
         /// Extend the lifetime of user credentials.
-        const REFRESH_CREDENTIALS = pam_ffi::PAM_REFRESH_CRED;
+        const REFRESH_CREDENTIALS = libpam_sys::PAM_REFRESH_CRED;
 
         // Flags used for password changing.
 
@@ -126,7 +94,7 @@
         /// the password service should update all passwords.
         ///
         /// This flag is only used by `change_authtok`.
-        const CHANGE_EXPIRED_AUTHTOK = pam_ffi::PAM_CHANGE_EXPIRED_AUTHTOK;
+        const CHANGE_EXPIRED_AUTHTOK = libpam_sys::PAM_CHANGE_EXPIRED_AUTHTOK;
         /// This is a preliminary check for password changing.
         /// The password should not be changed.
         ///
@@ -134,7 +102,7 @@
         /// Applications may not use this flag.
         ///
         /// This flag is only used by `change_authtok`.
-        const PRELIMINARY_CHECK = pam_ffi::PAM_PRELIM_CHECK;
+        const PRELIMINARY_CHECK = libpam_sys::PAM_PRELIM_CHECK;
         /// The password should actuallyPR be updated.
         /// This and [Self::PRELIMINARY_CHECK] are mutually exclusive.
         ///
@@ -142,7 +110,7 @@
         /// Applications may not use this flag.
         ///
         /// This flag is only used by `change_authtok`.
-        const UPDATE_AUTHTOK = pam_ffi::PAM_UPDATE_AUTHTOK;
+        const UPDATE_AUTHTOK = libpam_sys::PAM_UPDATE_AUTHTOK;
     }
 }
 
@@ -207,7 +175,7 @@
 
 impl Display for ErrorCode {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        match pam_ffi::strerror((*self).into()) {
+        match strerror((*self).into()) {
             Some(err) => f.write_str(err),
             None => self.fmt_internal(f),
         }
@@ -236,10 +204,31 @@
 
     /// A basic Display implementation for if we don't link against PAM.
     fn fmt_internal(self, f: &mut Formatter<'_>) -> fmt::Result {
-        write!(f, "PAM error: {self:?} ({n})", n = self as c_uint)
+        let n : c_int = self.into();
+        write!(f, "PAM error: {self:?} ({n})")
     }
 }
 
+/// Gets a string version of an error message.
+#[cfg(feature = "link")]
+pub fn strerror(code: c_int) -> Option<&'static str> {
+    use std::ptr;
+    use std::ffi::CStr;
+    // SAFETY: PAM impls don't care about the PAM handle and always return
+    // static strings.
+    let strerror = unsafe { libpam_sys::pam_strerror(ptr::null(), code as c_int) };
+    // SAFETY: We just got this back from PAM and we checked if it's null.
+    (!strerror.is_null())
+        .then(|| unsafe { CStr::from_ptr(strerror) }.to_str().ok())
+        .flatten()
+}
+
+/// Dummy implementation of strerror so that it always returns None.
+#[cfg(not(feature = "link"))]
+pub fn strerror(_: c_int) -> Option<&'static str> {
+    None
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
--- a/src/conv.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/conv.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -9,12 +9,10 @@
 use std::fmt::Debug;
 use std::result::Result as StdResult;
 
-/// The types of message and request that can be sent to a user.
-///
-/// The data within each enum value is the prompt (or other information)
-/// that will be presented to the user.
+/// An individual pair of request/response to be sent to the user.
+#[derive(Debug)]
 #[non_exhaustive]
-pub enum Message<'a> {
+pub enum Exchange<'a> {
     Prompt(&'a QAndA<'a>),
     MaskedPrompt(&'a MaskedQAndA<'a>),
     Error(&'a ErrorMsg<'a>),
@@ -23,22 +21,22 @@
     BinaryPrompt(&'a BinaryQAndA<'a>),
 }
 
-impl Message<'_> {
+impl Exchange<'_> {
     /// Sets an error answer on this question, without having to inspect it.
     ///
     /// Use this as a default match case:
     ///
     /// ```
-    /// use nonstick::conv::{Message, QAndA};
+    /// use nonstick::conv::{Exchange, QAndA};
     /// use nonstick::ErrorCode;
     ///
-    /// fn cant_respond(message: Message) {
+    /// fn cant_respond(message: Exchange) {
     ///     match message {
-    ///         Message::Info(i) => {
+    ///         Exchange::Info(i) => {
     ///             eprintln!("fyi, {}", i.question());
     ///             i.set_answer(Ok(()))
     ///         }
-    ///         Message::Error(e) => {
+    ///         Exchange::Error(e) => {
     ///             eprintln!("ERROR: {}", e.question());
     ///             e.set_answer(Ok(()))
     ///         }
@@ -48,12 +46,12 @@
     /// }
     pub fn set_error(&self, err: ErrorCode) {
         match *self {
-            Message::Prompt(m) => m.set_answer(Err(err)),
-            Message::MaskedPrompt(m) => m.set_answer(Err(err)),
-            Message::Error(m) => m.set_answer(Err(err)),
-            Message::Info(m) => m.set_answer(Err(err)),
-            Message::RadioPrompt(m) => m.set_answer(Err(err)),
-            Message::BinaryPrompt(m) => m.set_answer(Err(err)),
+            Exchange::Prompt(m) => m.set_answer(Err(err)),
+            Exchange::MaskedPrompt(m) => m.set_answer(Err(err)),
+            Exchange::Error(m) => m.set_answer(Err(err)),
+            Exchange::Info(m) => m.set_answer(Err(err)),
+            Exchange::RadioPrompt(m) => m.set_answer(Err(err)),
+            Exchange::BinaryPrompt(m) => m.set_answer(Err(err)),
         }
     }
 }
@@ -76,8 +74,8 @@
                 }
             }
 
-            /// Converts this Q&A into a [`Message`] for the [`Conversation`].
-            pub fn message(&self) -> Message<'_> {
+            /// Converts this Q&A into a [`Exchange`] for the [`Conversation`].
+            pub fn exchange(&self) -> Exchange<'_> {
                 $val(self)
             }
 
@@ -110,9 +108,7 @@
         $(#[$m])*
         impl fmt::Debug for $name<'_> {
             fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> {
-                #[derive(Debug)]
-                struct $name<'a> { q: $qt }
-                fmt::Debug::fmt(&$name { q: self.q }, f)
+                f.debug_struct(stringify!($name)).field("q", &self.q).finish_non_exhaustive()
             }
         }
     };
@@ -123,7 +119,7 @@
     ///
     /// In other words, a password entry prompt.
     MaskedQAndA<'a, Q=&'a str, A=String>,
-    Message::MaskedPrompt
+    Exchange::MaskedPrompt
 );
 
 q_and_a!(
@@ -133,7 +129,7 @@
     /// When the user types, their input will be shown to them.
     /// It can be used for things like usernames.
     QAndA<'a, Q=&'a str, A=String>,
-    Message::Prompt
+    Exchange::Prompt
 );
 
 q_and_a!(
@@ -143,7 +139,7 @@
     /// questions, but nowhere in the documentation is it specified
     /// what the format of the answer will be, or how this should be shown.
     RadioQAndA<'a, Q=&'a str, A=String>,
-    Message::RadioPrompt
+    Exchange::RadioPrompt
 );
 
 q_and_a!(
@@ -156,7 +152,7 @@
     /// The `data_type` tag is a value that is simply passed through
     /// to the application. PAM does not define any meaning for it.
     BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>,
-    Message::BinaryPrompt
+    Exchange::BinaryPrompt
 );
 
 /// Owned binary data.
@@ -208,7 +204,7 @@
     /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
     /// the message has been displayed (or actively discarded).
     InfoMsg<'a, Q = &'a str, A = ()>,
-    Message::Info
+    Exchange::Info
 );
 
 q_and_a!(
@@ -218,7 +214,7 @@
     /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
     /// the message has been displayed (or actively discarded).
     ErrorMsg<'a, Q = &'a str, A = ()>,
-    Message::Error
+    Exchange::Error
 );
 
 /// A channel for PAM modules to request information from the user.
@@ -236,7 +232,7 @@
     /// as there were messages in the request; one corresponding to each.
     ///
     /// TODO: write detailed documentation about how to use this.
-    fn communicate(&self, messages: &[Message]);
+    fn communicate(&self, messages: &[Exchange]);
 }
 
 /// Turns a simple function into a [`Conversation`].
@@ -245,7 +241,7 @@
 /// Conversation:
 ///
 /// ```
-/// use nonstick::conv::{conversation_func, Conversation, Message};
+/// use nonstick::conv::{conversation_func, Conversation, Exchange};
 /// mod some_library {
 /// #    use nonstick::Conversation;
 ///     pub fn get_auth_data(conv: &mut impl Conversation) {
@@ -253,7 +249,7 @@
 ///     }
 /// }
 ///
-/// fn my_terminal_prompt(messages: &[Message]) {
+/// fn my_terminal_prompt(messages: &[Exchange]) {
 ///     // ...
 /// #    unimplemented!()
 /// }
@@ -262,14 +258,14 @@
 ///     some_library::get_auth_data(&mut conversation_func(my_terminal_prompt));
 /// }
 /// ```
-pub fn conversation_func(func: impl Fn(&[Message])) -> impl Conversation {
+pub fn conversation_func(func: impl Fn(&[Exchange])) -> impl Conversation {
     FunctionConvo(func)
 }
 
-struct FunctionConvo<C: Fn(&[Message])>(C);
+struct FunctionConvo<C: Fn(&[Exchange])>(C);
 
-impl<C: Fn(&[Message])> Conversation for FunctionConvo<C> {
-    fn communicate(&self, messages: &[Message]) {
+impl<C: Fn(&[Exchange])> Conversation for FunctionConvo<C> {
+    fn communicate(&self, messages: &[Exchange]) {
         self.0(messages)
     }
 }
@@ -399,14 +395,14 @@
         $(#[$m])*
         fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> {
             let prompt = <$msg>::new($($param),*);
-            self.communicate(&[prompt.message()]);
+            self.communicate(&[prompt.exchange()]);
             prompt.answer()
         }
     };
     ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => {
         $(#[$m])*
         fn $fn_name(&self, $($param: $pt),*) {
-            self.communicate(&[<$msg>::new($($param),*).message()]);
+            self.communicate(&[<$msg>::new($($param),*).exchange()]);
         }
     };
 }
@@ -433,25 +429,25 @@
 }
 
 impl<CA: ConversationAdapter> Conversation for Demux<CA> {
-    fn communicate(&self, messages: &[Message]) {
+    fn communicate(&self, messages: &[Exchange]) {
         for msg in messages {
             match msg {
-                Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())),
-                Message::MaskedPrompt(prompt) => {
+                Exchange::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())),
+                Exchange::MaskedPrompt(prompt) => {
                     prompt.set_answer(self.0.masked_prompt(prompt.question()))
                 }
-                Message::RadioPrompt(prompt) => {
+                Exchange::RadioPrompt(prompt) => {
                     prompt.set_answer(self.0.radio_prompt(prompt.question()))
                 }
-                Message::Info(prompt) => {
+                Exchange::Info(prompt) => {
                     self.0.info_msg(prompt.question());
                     prompt.set_answer(Ok(()))
                 }
-                Message::Error(prompt) => {
+                Exchange::Error(prompt) => {
                     self.0.error_msg(prompt.question());
                     prompt.set_answer(Ok(()))
                 }
-                Message::BinaryPrompt(prompt) => {
+                Exchange::BinaryPrompt(prompt) => {
                     let q = prompt.question();
                     prompt.set_answer(self.0.binary_prompt(q))
                 }
@@ -516,11 +512,11 @@
         // Basic tests.
 
         conv.communicate(&[
-            what.message(),
-            pass.message(),
-            err.message(),
-            info.message(),
-            has_err.message(),
+            what.exchange(),
+            pass.exchange(),
+            err.exchange(),
+            info.exchange(),
+            has_err.exchange(),
         ]);
 
         assert_eq!("whatwhat", what.answer().unwrap());
@@ -538,7 +534,7 @@
 
             let radio = RadioQAndA::new("channel?");
             let bin = BinaryQAndA::new((&[10, 9, 8], 66));
-            conv.communicate(&[radio.message(), bin.message()]);
+            conv.communicate(&[radio.exchange(), bin.exchange()]);
 
             assert_eq!("zero", radio.answer().unwrap());
             assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap());
@@ -549,31 +545,31 @@
         struct MuxTester;
 
         impl Conversation for MuxTester {
-            fn communicate(&self, messages: &[Message]) {
+            fn communicate(&self, messages: &[Exchange]) {
                 if let [msg] = messages {
                     match *msg {
-                        Message::Info(info) => {
+                        Exchange::Info(info) => {
                             assert_eq!("let me tell you", info.question());
                             info.set_answer(Ok(()))
                         }
-                        Message::Error(error) => {
+                        Exchange::Error(error) => {
                             assert_eq!("oh no", error.question());
                             error.set_answer(Ok(()))
                         }
-                        Message::Prompt(prompt) => prompt.set_answer(match prompt.question() {
+                        Exchange::Prompt(prompt) => prompt.set_answer(match prompt.question() {
                             "should_err" => Err(ErrorCode::PermissionDenied),
                             "question" => Ok("answer".to_owned()),
                             other => panic!("unexpected question {other:?}"),
                         }),
-                        Message::MaskedPrompt(ask) => {
+                        Exchange::MaskedPrompt(ask) => {
                             assert_eq!("password!", ask.question());
                             ask.set_answer(Ok("open sesame".into()))
                         }
-                        Message::BinaryPrompt(prompt) => {
+                        Exchange::BinaryPrompt(prompt) => {
                             assert_eq!((&[1, 2, 3][..], 69), prompt.question());
                             prompt.set_answer(Ok(BinaryData::from((&[3, 2, 1], 42))))
                         }
-                        Message::RadioPrompt(ask) => {
+                        Exchange::RadioPrompt(ask) => {
                             assert_eq!("radio?", ask.question());
                             ask.set_answer(Ok("yes".to_owned()))
                         }
--- a/src/libpam/answer.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/libpam/answer.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -1,11 +1,10 @@
 //! Types used to communicate data from the application to the module.
 
-use crate::libpam::conversation::OwnedMessage;
+use crate::libpam::conversation::OwnedExchange;
 use crate::libpam::memory;
-use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString};
-pub use crate::libpam::pam_ffi::Answer;
+use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString, Immovable};
 use crate::{ErrorCode, Result};
-use std::ffi::CStr;
+use std::ffi::{c_int, c_void, CStr};
 use std::mem::ManuallyDrop;
 use std::ops::{Deref, DerefMut};
 use std::ptr::NonNull;
@@ -22,7 +21,7 @@
 
 impl Answers {
     /// Builds an Answers out of the given answered Message Q&As.
-    pub fn build(value: Vec<OwnedMessage>) -> Result<Self> {
+    pub fn build(value: Vec<OwnedExchange>) -> Result<Self> {
         let mut outputs = Self {
             base: memory::calloc(value.len())?,
             count: value.len(),
@@ -31,14 +30,16 @@
         // all allocated answer memory.
         for (input, output) in iter::zip(value, outputs.iter_mut()) {
             match input {
-                OwnedMessage::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.as_ref())?,
-                OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
-                OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
-                OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
+                OwnedExchange::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.as_ref())?,
+                OwnedExchange::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
+                OwnedExchange::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
+                OwnedExchange::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
                 // If we're here, that means that we *got* a Linux-PAM
                 // question from PAM, so we're OK to answer it.
-                OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
-                OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?,
+                OwnedExchange::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
+                OwnedExchange::BinaryPrompt(p) => {
+                    BinaryAnswer::fill(output, (&p.answer()?).into())?
+                }
             }
         }
         Ok(outputs)
@@ -48,8 +49,8 @@
     ///
     /// This object is consumed and the `Answer` pointer now owns its data.
     /// It can be recreated with [`Self::from_c_heap`].
-    pub fn into_ptr(self) -> NonNull<Answer> {
-        ManuallyDrop::new(self).base
+    pub fn into_ptr(self) -> *mut libpam_sys::pam_response {
+        ManuallyDrop::new(self).base.as_ptr().cast()
     }
 
     /// Takes ownership of a list of answers allocated on the C heap.
@@ -58,8 +59,11 @@
     ///
     /// It's up to you to make sure you pass a valid pointer,
     /// like one that you got from PAM, or maybe [`Self::into_ptr`].
-    pub unsafe fn from_c_heap(base: NonNull<Answer>, count: usize) -> Self {
-        Answers { base, count }
+    pub unsafe fn from_c_heap(base: NonNull<libpam_sys::pam_response>, count: usize) -> Self {
+        Answers {
+            base: NonNull::new_unchecked(base.as_ptr().cast()),
+            count,
+        }
     }
 }
 
@@ -91,6 +95,25 @@
     }
 }
 
+/// 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, Default)]
+pub struct Answer {
+    /// Owned pointer to the data returned in an answer.
+    /// For most answers, this will be a
+    /// [`CHeapString`](crate::libpam::memory::CHeapString),
+    /// but for [`BinaryQAndA`](crate::conv::BinaryQAndA)s
+    /// (a Linux-PAM extension), this will be a [`CHeapBox`] of
+    /// [`CBinaryData`](crate::libpam::memory::CBinaryData).
+    pub data: Option<CHeapBox<c_void>>,
+    /// Unused. Just here for the padding.
+    return_code: c_int,
+    _marker: Immovable,
+}
+
 #[repr(transparent)]
 #[derive(Debug)]
 pub struct TextAnswer(Answer);
@@ -223,19 +246,19 @@
         assert_eq!("", up.contents().unwrap());
     }
 
-    fn round_trip(msgs: Vec<OwnedMessage>) -> Answers {
+    fn round_trip(msgs: Vec<OwnedExchange>) -> Answers {
         let n = msgs.len();
         let sent = Answers::build(msgs).unwrap();
-        unsafe { Answers::from_c_heap(sent.into_ptr(), n) }
+        unsafe { Answers::from_c_heap(NonNull::new_unchecked(sent.into_ptr()), n) }
     }
 
     #[test]
     fn test_round_trip() {
         let mut answers = round_trip(vec![
-            answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()),
-            answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()),
-            answered!(ErrorMsg, OwnedMessage::Error, ()),
-            answered!(InfoMsg, OwnedMessage::Info, ()),
+            answered!(QAndA, OwnedExchange::Prompt, "whats going on".to_owned()),
+            answered!(MaskedQAndA, OwnedExchange::MaskedPrompt, "well then".into()),
+            answered!(ErrorMsg, OwnedExchange::Error, ()),
+            answered!(InfoMsg, OwnedExchange::Info, ()),
         ]);
 
         if let [going, well, err, info] = &mut answers[..] {
@@ -254,13 +277,13 @@
         let binary_msg = {
             let qa = BinaryQAndA::new((&[][..], 0));
             qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99)));
-            OwnedMessage::BinaryPrompt(qa)
+            OwnedExchange::BinaryPrompt(qa)
         };
         let mut answers = round_trip(vec![
             binary_msg,
             answered!(
                 RadioQAndA,
-                OwnedMessage::RadioPrompt,
+                OwnedExchange::RadioPrompt,
                 "beep boop".to_owned()
             ),
         ]);
--- a/src/libpam/conversation.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/libpam/conversation.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -1,25 +1,35 @@
 use crate::conv::{BinaryQAndA, RadioQAndA};
-use crate::conv::{Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA};
+use crate::conv::{Conversation, ErrorMsg, Exchange, InfoMsg, MaskedQAndA, QAndA};
 use crate::libpam::answer::BinaryAnswer;
 use crate::libpam::answer::{Answer, Answers, TextAnswer};
 use crate::libpam::memory::CBinaryData;
-use crate::libpam::pam_ffi::AppData;
-pub use crate::libpam::pam_ffi::LibPamConversation;
-use crate::libpam::question::QuestionsTrait;
-use crate::libpam::question::{Question, Questions};
+use crate::libpam::question::Question;
 use crate::ErrorCode;
 use crate::Result;
+use libpam_sys::helpers::PtrPtrVec;
+use libpam_sys::AppData;
 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.
+#[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 ()>,
+}
+
 impl LibPamConversation<'_> {
     pub fn wrap<C: Conversation>(conv: &C) -> Self {
         Self {
-            callback: Self::wrapper_callback::<C>,
-            appdata: (conv as *const C).cast(),
+            pam_conv: libpam_sys::pam_conv {
+                conv: Self::wrapper_callback::<C>,
+                appdata_ptr: (conv as *const C).cast_mut().cast(),
+            },
             life: PhantomData,
         }
     }
@@ -29,9 +39,9 @@
     /// PAM calls this, we compute answers, then send them back.
     unsafe extern "C" fn wrapper_callback<C: Conversation>(
         count: c_int,
-        questions: *const *const Question,
-        answers: *mut *mut Answer,
-        me: *const AppData,
+        questions: *const *const libpam_sys::pam_message,
+        answers: *mut *mut libpam_sys::pam_response,
+        me: *mut AppData,
     ) -> c_int {
         let internal = || {
             // Collect all our pointers
@@ -39,23 +49,23 @@
                 .cast::<C>()
                 .as_ref()
                 .ok_or(ErrorCode::ConversationError)?;
-            let indirect = Questions::borrow_ptr(questions, count as usize);
+            let q_iter = PtrPtrVec::<Question>::iter_over(questions, count as usize);
             let answers_ptr = answers.as_mut().ok_or(ErrorCode::ConversationError)?;
 
             // Build our owned list of Q&As from the questions we've been asked
-            let messages: Vec<OwnedMessage> = indirect
+            let messages: Vec<OwnedExchange> = q_iter
                 .map(TryInto::try_into)
                 .collect::<Result<_>>()
                 .map_err(|_| ErrorCode::ConversationError)?;
             // Borrow all those Q&As and ask them.
             // If we got an invalid message type, bail before sending.
-            let borrowed: Result<Vec<_>> = messages.iter().map(Message::try_from).collect();
+            let borrowed: Result<Vec<_>> = messages.iter().map(Exchange::try_from).collect();
             // TODO: Do we want to log something here?
             conv.communicate(&borrowed?);
 
             // Send our answers back.
             let owned = Answers::build(messages)?;
-            *answers_ptr = owned.into_ptr().as_ptr();
+            *answers_ptr = owned.into_ptr();
             Ok(())
         };
         ErrorCode::result_to_c(internal())
@@ -63,17 +73,18 @@
 }
 
 impl Conversation for LibPamConversation<'_> {
-    fn communicate(&self, messages: &[Message]) {
+    fn communicate(&self, messages: &[Exchange]) {
         let internal = || {
-            let questions = Box::pin(Questions::new(messages)?);
+            let questions: Result<_> = messages.iter().map(Question::try_from).collect();
+            let questions = PtrPtrVec::new(questions?);
             let mut response_pointer = std::ptr::null_mut();
             // SAFETY: We're calling into PAM with valid everything.
             let result = unsafe {
-                (self.callback)(
+                (self.pam_conv.conv)(
                     messages.len() as c_int,
-                    questions.as_ref().ptr(),
+                    questions.as_ptr(),
                     &mut response_pointer,
-                    self.appdata,
+                    self.pam_conv.appdata_ptr,
                 )
             };
             ErrorCode::result_from(result)?;
@@ -96,9 +107,9 @@
     }
 }
 
-/// Like [`Message`], but this time we own the contents.
+/// Like [`Exchange`], but this time we own the contents.
 #[derive(Debug)]
-pub enum OwnedMessage<'a> {
+pub enum OwnedExchange<'a> {
     MaskedPrompt(MaskedQAndA<'a>),
     Prompt(QAndA<'a>),
     Info(InfoMsg<'a>),
@@ -107,16 +118,16 @@
     BinaryPrompt(BinaryQAndA<'a>),
 }
 
-impl<'a> TryFrom<&'a OwnedMessage<'a>> for Message<'a> {
+impl<'a> TryFrom<&'a OwnedExchange<'a>> for Exchange<'a> {
     type Error = ErrorCode;
-    fn try_from(src: &'a OwnedMessage) -> StdResult<Self, ErrorCode> {
+    fn try_from(src: &'a OwnedExchange) -> StdResult<Self, ErrorCode> {
         match src {
-            OwnedMessage::MaskedPrompt(m) => Ok(Message::MaskedPrompt(m)),
-            OwnedMessage::Prompt(m) => Ok(Message::Prompt(m)),
-            OwnedMessage::Info(m) => Ok(Message::Info(m)),
-            OwnedMessage::Error(m) => Ok(Message::Error(m)),
-            OwnedMessage::RadioPrompt(m) => Ok(Message::RadioPrompt(m)),
-            OwnedMessage::BinaryPrompt(m) => Ok(Message::BinaryPrompt(m)),
+            OwnedExchange::MaskedPrompt(m) => Ok(Exchange::MaskedPrompt(m)),
+            OwnedExchange::Prompt(m) => Ok(Exchange::Prompt(m)),
+            OwnedExchange::Info(m) => Ok(Exchange::Info(m)),
+            OwnedExchange::Error(m) => Ok(Exchange::Error(m)),
+            OwnedExchange::RadioPrompt(m) => Ok(Exchange::RadioPrompt(m)),
+            OwnedExchange::BinaryPrompt(m) => Ok(Exchange::BinaryPrompt(m)),
         }
     }
 }
@@ -126,7 +137,7 @@
 /// # Safety
 ///
 /// You are responsible for ensuring that the src-dst pair matches.
-unsafe fn convert(msg: &Message, resp: &mut Answer) {
+unsafe fn convert(msg: &Exchange, resp: &mut Answer) {
     macro_rules! fill_text {
         ($dst:ident, $src:ident) => {{
             let text_resp = unsafe { TextAnswer::upcast($src) };
@@ -134,12 +145,12 @@
         }};
     }
     match *msg {
-        Message::MaskedPrompt(qa) => fill_text!(qa, resp),
-        Message::Prompt(qa) => fill_text!(qa, resp),
-        Message::Error(m) => m.set_answer(Ok(())),
-        Message::Info(m) => m.set_answer(Ok(())),
-        Message::RadioPrompt(qa) => fill_text!(qa, resp),
-        Message::BinaryPrompt(qa) => {
+        Exchange::MaskedPrompt(qa) => fill_text!(qa, resp),
+        Exchange::Prompt(qa) => fill_text!(qa, resp),
+        Exchange::Error(m) => m.set_answer(Ok(())),
+        Exchange::Info(m) => m.set_answer(Ok(())),
+        Exchange::RadioPrompt(qa) => fill_text!(qa, resp),
+        Exchange::BinaryPrompt(qa) => {
             let bin_resp = unsafe { BinaryAnswer::upcast(resp) };
             qa.set_answer(Ok(bin_resp
                 .data()
--- a/src/libpam/environ.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/libpam/environ.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -1,7 +1,7 @@
 use crate::constants::{ErrorCode, Result};
 use crate::environ::{EnvironMap, EnvironMapMut};
 use crate::libpam::memory::CHeapString;
-use crate::libpam::{memory, pam_ffi, LibPamHandle};
+use crate::libpam::{memory, LibPamHandle};
 use std::ffi::{c_char, CStr, CString, OsStr, OsString};
 use std::marker::PhantomData;
 use std::os::unix::ffi::{OsStrExt, OsStringExt};
@@ -20,12 +20,7 @@
     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(pam_ffi::pam_getenv(
-                (self as *const LibPamHandle).cast_mut(),
-                key.as_ptr(),
-            ))
-        }
+        unsafe { copy_env(libpam_sys::pam_getenv(self.0.as_ref(), key.as_ptr())) }
     }
 
     fn environ_set(&mut self, key: &OsStr, value: Option<&OsStr>) -> Result<Option<OsString>> {
@@ -44,18 +39,16 @@
         }
         let put = CString::new(result).map_err(|_| ErrorCode::ConversationError)?;
         // SAFETY: This is a valid handle and a valid environment string.
-        ErrorCode::result_from(unsafe { pam_ffi::pam_putenv(self, put.as_ptr()) })?;
+        ErrorCode::result_from(unsafe { libpam_sys::pam_putenv(self.0.as_mut(), put.as_ptr()) })?;
         Ok(old)
     }
 
     fn environ_iter(&self) -> Result<impl Iterator<Item = (OsString, OsString)>> {
         // SAFETY: This is a valid PAM handle. It will return valid data.
         unsafe {
-            NonNull::new(pam_ffi::pam_getenvlist(
-                (self as *const LibPamHandle).cast_mut(),
-            ))
-            .map(|ptr| EnvList::from_ptr(ptr.cast()))
-            .ok_or(ErrorCode::BufferError)
+            NonNull::new(libpam_sys::pam_getenvlist(self.0.as_ref()))
+                .map(|ptr| EnvList::from_ptr(ptr.cast()))
+                .ok_or(ErrorCode::BufferError)
         }
     }
 }
--- a/src/libpam/handle.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/libpam/handle.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -1,11 +1,10 @@
 use super::conversation::LibPamConversation;
 use crate::constants::{ErrorCode, Result};
-use crate::conv::Message;
+use crate::conv::Exchange;
 use crate::environ::EnvironMapMut;
 use crate::handle::PamShared;
 use crate::libpam::environ::{LibPamEnviron, LibPamEnvironMut};
-pub use crate::libpam::pam_ffi::LibPamHandle;
-use crate::libpam::{memory, pam_ffi};
+use crate::libpam::memory;
 use crate::logging::{Level, Location};
 use crate::{
     guide, linklist, stdlinks, Conversation, EnvironMap, Flags, PamHandleApplication,
@@ -14,29 +13,30 @@
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::cell::Cell;
 use std::ffi::{c_char, c_int, CString};
-use std::ops::{Deref, DerefMut};
+
 use std::ptr;
+use std::ptr::NonNull;
+use libpam_sys::cfg_pam_impl;
 
 /// Owner for a PAM handle.
-struct HandleWrap(*mut LibPamHandle);
+pub struct LibPamHandle(pub NonNull<libpam_sys::pam_handle>);
 
-impl Deref for HandleWrap {
-    type Target = LibPamHandle;
-    fn deref(&self) -> &Self::Target {
-        unsafe { &*self.0 }
+impl AsRef<libpam_sys::pam_handle> for LibPamHandle {
+    fn as_ref(&self) -> &libpam_sys::pam_handle {
+        unsafe { self.0.as_ref() }
     }
 }
 
-impl DerefMut for HandleWrap {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        unsafe { &mut *self.0 }
+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> {
     /// The handle itself.
-    handle: HandleWrap,
+    handle: LibPamHandle,
     /// The last return value from the handle.
     last_return: Cell<Result<()>>,
     /// If set, the Conversation that this PAM handle owns.
@@ -102,20 +102,23 @@
         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());
 
-        let mut handle: *mut LibPamHandle = ptr::null_mut();
+        let mut handle: *mut libpam_sys::pam_handle = ptr::null_mut();
         // SAFETY: We've set everything up properly to call `pam_start`.
         // The returned value will be a valid pointer provided the result is OK.
         let result = unsafe {
-            pam_ffi::pam_start(
+            libpam_sys::pam_start(
                 service_cstr.as_ptr(),
                 username_cstr,
-                conv.as_ref(),
+                (conv.as_ref() as *const LibPamConversation)
+                    .cast_mut()
+                    .cast(),
                 &mut handle,
             )
         };
         ErrorCode::result_from(result)?;
+        let handle = NonNull::new(handle).ok_or(ErrorCode::BufferError)?;
         Ok(Self {
-            handle: HandleWrap(handle),
+            handle: LibPamHandle(handle),
             last_return: Cell::new(Ok(())),
             conversation: Some(conv),
         })
@@ -124,21 +127,22 @@
 
 impl PamHandleApplication for OwnedLibPamHandle<'_> {
     fn authenticate(&mut self, flags: Flags) -> Result<()> {
-        let ret = unsafe { pam_ffi::pam_authenticate(self.handle.0, flags.bits() as c_int) };
+        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
     }
 
     fn account_management(&mut self, flags: Flags) -> Result<()> {
-        let ret = unsafe { pam_ffi::pam_acct_mgmt(self.handle.0, flags.bits() as c_int) };
+        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 { pam_ffi::pam_chauthtok(self.handle.0, flags.bits() as c_int) };
+        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
@@ -167,8 +171,8 @@
     #[doc = stdlinks!(3 pam_end)]
     fn drop(&mut self) {
         unsafe {
-            pam_ffi::pam_end(
-                self.handle.0,
+            libpam_sys::pam_end(
+                self.handle.as_mut(),
                 ErrorCode::result_to_c(self.last_return.get()),
             );
         }
@@ -190,6 +194,7 @@
 }
 
 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,
@@ -200,7 +205,7 @@
             _ = loc;
             // SAFETY: We're calling this function with a known value.
             unsafe {
-                pam_ffi::pam_syslog(self, level as c_int, "%s\0".as_ptr().cast(), entry.as_ptr())
+                libpam_sys::pam_syslog(self, level as c_int, "%s\0".as_ptr().cast(), entry.as_ptr())
             }
         }
         #[cfg(pam_impl = "openpam")]
@@ -208,7 +213,7 @@
             let func = CString::new(loc.function).unwrap_or(CString::default());
             // SAFETY: We're calling this function with a known value.
             unsafe {
-                pam_ffi::_openpam_log(
+                libpam_sys::_openpam_log(
                     level as c_int,
                     func.as_ptr(),
                     "%s\0".as_ptr().cast(),
@@ -218,11 +223,17 @@
         }
     }
 
+    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 {
-            pam_ffi::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref()))
+            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) }
@@ -255,7 +266,7 @@
 }
 
 impl Conversation for LibPamHandle {
-    fn communicate(&self, messages: &[Message]) {
+    fn communicate(&self, messages: &[Exchange]) {
         match self.conversation_item() {
             Ok(conv) => conv.communicate(messages),
             Err(e) => {
@@ -268,13 +279,14 @@
 }
 
 impl PamHandleModule for LibPamHandle {
+    #[cfg_pam_impl(any("LinuxPam", "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 {
-            pam_ffi::pam_get_authtok(
-                self,
+            libpam_sys::pam_get_authtok(
+                self.as_mut(),
                 ItemType::AuthTok.into(),
                 &mut output,
                 memory::prompt_ptr(prompt.as_ref()),
@@ -287,6 +299,11 @@
             .unwrap_or(Err(ErrorCode::ConversationError))
     }
 
+    #[cfg_pam_impl(not(any("LinuxPam", "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);
 }
@@ -309,7 +326,8 @@
     /// 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 { pam_ffi::pam_get_item(self, item_type as c_int, &mut output) };
+        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())
     }
@@ -322,8 +340,8 @@
     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,
+            libpam_sys::pam_set_item(
+                self.as_mut(),
                 item_type as c_int,
                 memory::prompt_ptr(data_str.as_ref()).cast(),
             )
@@ -335,8 +353,8 @@
     fn conversation_item(&self) -> Result<&mut LibPamConversation<'_>> {
         let output: *mut LibPamConversation = ptr::null_mut();
         let result = unsafe {
-            pam_ffi::pam_get_item(
-                self,
+            libpam_sys::pam_get_item(
+                self.as_ref(),
                 ItemType::Conversation.into(),
                 &mut output.cast_const().cast(),
             )
--- a/src/libpam/mod.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/libpam/mod.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -12,7 +12,6 @@
 mod handle;
 mod memory;
 mod module;
-pub(crate) mod pam_ffi;
 mod question;
 
 #[doc(inline)]
--- a/src/libpam/pam_ffi.rs	Mon Jun 30 23:49:54 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-//! The types that are directly represented in PAM function signatures.
-
-#![allow(non_camel_case_types, non_upper_case_globals)]
-
-use crate::libpam::memory::{CHeapBox, Immovable};
-use std::ffi::{c_int, c_void, CStr};
-use std::marker::PhantomData;
-use std::ptr;
-
-/// 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, Default)]
-pub struct Answer {
-    /// Owned pointer to the data returned in an answer.
-    /// For most answers, this will be a
-    /// [`CHeapString`](crate::libpam::memory::CHeapString),
-    /// but for [`BinaryQAndA`](crate::conv::BinaryQAndA)s
-    /// (a Linux-PAM extension), this will be a [`CHeapBox`] of
-    /// [`CBinaryData`](crate::libpam::memory::CBinaryData).
-    pub data: Option<CHeapBox<c_void>>,
-    /// Unused. Just here for the padding.
-    return_code: c_int,
-    _marker: Immovable,
-}
-
-/// 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::conv::Message).
-///
-/// This question, and its internal data, is owned by its creator
-/// (either the module or PAM itself).
-#[repr(C)]
-#[derive(Debug)]
-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`],
-    /// but for requests with style `PAM_BINARY_PROMPT`,
-    /// this will be `CBinaryData` (a Linux-PAM extension).
-    pub data: Option<CHeapBox<c_void>>,
-}
-
-/// The callback that PAM uses to get information in a conversation.
-///
-/// - `num_msg` is the number of messages in the `questions` array.
-/// - `questions` is a pointer to the [`Question`]s being sent to the user.
-///   For information about its structure,
-///   see [`QuestionsTrait`](super::question::QuestionsTrait).
-/// - `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 must always be exactly as many `answers` as `num_msg`.
-/// - `appdata` is the `appdata` field of the [`LibPamConversation`].
-pub type ConversationCallback = unsafe extern "C" fn(
-    num_msg: c_int,
-    questions: *const *const Question,
-    answers: *mut *mut Answer,
-    appdata: *const 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: *const AppData,
-    /// Marker to associate the lifetime of this with the conversation
-    /// that was passed in.
-    pub life: PhantomData<&'a mut ()>,
-}
-
-/// Gets a string version of an error message.
-pub fn strerror(code: c_int) -> Option<&'static str> {
-    // SAFETY: Every single PAM implementation I can find (Linux-PAM, OpenPAM,
-    // Sun, etc.) returns a static string and ignores the handle value.
-    let strerror = unsafe { pam_strerror(ptr::null_mut(), code as c_int) };
-    if strerror.is_null() {
-        None
-    } else {
-        unsafe { CStr::from_ptr(strerror) }.to_str().ok()
-    }
-}
-
-pub use libpam_sys::*;
-
-type pam_handle = LibPamHandle;
-type pam_conv = LibPamConversation<'static>;
-
-include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
--- a/src/libpam/question.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/libpam/question.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -2,177 +2,22 @@
 
 #[cfg(feature = "linux-pam-ext")]
 use crate::conv::{BinaryQAndA, RadioQAndA};
-use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA};
-use crate::libpam::conversation::OwnedMessage;
-use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString, Immovable};
-use crate::libpam::pam_ffi;
-pub use crate::libpam::pam_ffi::Question;
+use crate::conv::{ErrorMsg, Exchange, InfoMsg, MaskedQAndA, QAndA};
+use crate::libpam::conversation::OwnedExchange;
+use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString};
 use crate::ErrorCode;
 use crate::Result;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
-use std::cell::Cell;
-use std::ffi::{c_void, CStr};
-use std::pin::Pin;
-use std::{ptr, slice};
-
-/// Abstraction of a collection of questions to be sent in a PAM conversation.
-///
-/// The PAM C API conversation function looks like this:
-///
-/// ```c
-/// int pam_conv(
-///     int count,
-///     const struct pam_message **questions,
-///     struct pam_response **answers,
-///     void *appdata_ptr,
-/// )
-/// ```
-///
-/// On Linux-PAM and other compatible implementations, `questions`
-/// is treated as a pointer-to-pointers, like `int argc, char **argv`.
-/// (In this situation, the value of `Questions.indirect` is
-/// the pointer passed to `pam_conv`.)
-///
-/// ```text
-///            points to  ┌───────────────┐      ╔═ Question ═╗
-/// questions ┄┄┄┄┄┄┄┄┄┄> │ questions[0] ┄┼┄┄┄┄> ║ style      ║
-///                       │ questions[1] ┄┼┄┄┄╮  ║ data ┄┄┄┄┄┄╫┄┄> ...
-///                       │ ...           │   ┆  ╚════════════╝
-///                                           ┆
-///                                           ┆    ╔═ Question ═╗
-///                                           ╰┄┄> ║ style      ║
-///                                                ║ data ┄┄┄┄┄┄╫┄┄> ...
-///                                                ╚════════════╝
-/// ```
-///
-/// On OpenPAM and other compatible implementations (like Solaris),
-/// `messages` is a pointer-to-pointer-to-array.  This appears to be
-/// the correct implementation as required by the XSSO specification.
-///
-/// ```text
-///            points to  ┌─────────────┐       ╔═ Question[] ═╗
-/// questions ┄┄┄┄┄┄┄┄┄┄> │ *questions ┄┼┄┄┄┄┄> ║ style        ║
-///                       └─────────────┘       ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
-///                                             ╟──────────────╢
-///                                             ║ style        ║
-///                                             ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
-///                                             ╟──────────────╢
-///                                             ║ ...          ║
-/// ```
-pub trait QuestionsTrait {
-    /// Allocates memory for this indirector and all its members.
-    fn new(messages: &[Message]) -> Result<Self>
-    where
-        Self: Sized;
-
-    /// Gets the pointer that is passed .
-    fn ptr(self: Pin<&Self>) -> *const *const Question;
-
-    /// Converts a pointer into a borrowed list of Questions.
-    ///
-    /// # Safety
-    ///
-    /// You have to provide a valid pointer.
-    unsafe fn borrow_ptr<'a>(
-        ptr: *const *const Question,
-        count: usize,
-    ) -> impl Iterator<Item = &'a Question>;
-}
-
-#[cfg(pam_impl = "linux-pam")]
-pub type Questions = LinuxPamQuestions;
-
-#[cfg(not(pam_impl = "linux-pam"))]
-pub type Questions = XSsoQuestions;
+use std::ffi::{c_int, c_void, CStr};
 
-/// The XSSO standard version of the pointer train to questions.
-#[derive(Debug)]
-#[repr(C)]
-pub struct XSsoQuestions {
-    /// Points to the memory address where the meat of `questions` is.
-    /// **The memory layout of Vec is not specified**, and we need to return
-    /// a pointer to the pointer, hence we have to store it here.
-    pointer: Cell<*const Question>,
-    questions: Vec<Question>,
-    _marker: Immovable,
-}
-
-impl XSsoQuestions {
-    fn len(&self) -> usize {
-        self.questions.len()
-    }
-    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
-        self.questions.iter_mut()
-    }
-}
-
-impl QuestionsTrait for XSsoQuestions {
-    fn new(messages: &[Message]) -> Result<Self> {
-        let questions: Result<Vec<_>> = messages.iter().map(Question::try_from).collect();
-        let questions = questions?;
-        Ok(Self {
-            pointer: Cell::new(ptr::null()),
-            questions,
-            _marker: Default::default(),
-        })
-    }
-
-    fn ptr(self: Pin<&Self>) -> *const *const Question {
-        let me = self.get_ref();
-        me.pointer.set(self.questions.as_ptr());
-        me.pointer.as_ptr()
-    }
-
-    unsafe fn borrow_ptr<'a>(
-        ptr: *const *const Question,
-        count: usize,
-    ) -> impl Iterator<Item = &'a Question> {
-        slice::from_raw_parts(*ptr, count).iter()
-    }
-}
-
-/// The Linux version of the pointer train to questions.
-#[derive(Debug)]
-#[repr(C)]
-pub struct LinuxPamQuestions {
-    #[allow(clippy::vec_box)] // we need to box vec items.
-    /// The place where the questions are.
-    questions: Vec<Box<Question>>,
-}
-
-impl LinuxPamQuestions {
-    fn len(&self) -> usize {
-        self.questions.len()
-    }
-
-    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
-        self.questions.iter_mut().map(AsMut::as_mut)
-    }
-}
-
-impl QuestionsTrait for LinuxPamQuestions {
-    fn new(messages: &[Message]) -> Result<Self> {
-        let questions: Result<_> = messages
-            .iter()
-            .map(|msg| Question::try_from(msg).map(Box::new))
-            .collect();
-        Ok(Self {
-            questions: questions?,
-        })
-    }
-
-    fn ptr(self: Pin<&Self>) -> *const *const Question {
-        self.questions.as_ptr().cast()
-    }
-
-    unsafe fn borrow_ptr<'a>(
-        ptr: *const *const Question,
-        count: usize,
-    ) -> impl Iterator<Item = &'a Question> {
-        slice::from_raw_parts(ptr.cast::<&Question>(), count)
-            .iter()
-            .copied()
-    }
+mod style_const {
+    pub use libpam_sys::*;
+    #[cfg(not(feature = "link"))]
+    #[cfg_pam_impl(not("LinuxPam"))]
+    pub const PAM_RADIO_TYPE: i32 = 897;
+    #[cfg(not(feature = "link"))]
+    #[cfg_pam_impl(not("LinuxPam"))]
+    pub const PAM_BINARY_PROMPT: i32 = 10010101;
 }
 
 /// The C enum values for messages shown to the user.
@@ -180,22 +25,42 @@
 #[repr(i32)]
 enum Style {
     /// Requests information from the user; will be masked when typing.
-    PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF,
+    PromptEchoOff = style_const::PAM_PROMPT_ECHO_OFF,
     /// Requests information from the user; will not be masked.
-    PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON,
+    PromptEchoOn = style_const::PAM_PROMPT_ECHO_ON,
     /// An error message.
-    ErrorMsg = pam_ffi::PAM_ERROR_MSG,
+    ErrorMsg = style_const::PAM_ERROR_MSG,
     /// An informational message.
-    TextInfo = pam_ffi::PAM_TEXT_INFO,
+    TextInfo = style_const::PAM_TEXT_INFO,
     /// Yes/No/Maybe conditionals. A Linux-PAM extension.
     #[cfg(feature = "linux-pam-ext")]
-    RadioType = pam_ffi::PAM_RADIO_TYPE,
+    RadioType = style_const::PAM_RADIO_TYPE,
     /// For server–client non-human interaction.
     ///
     /// NOT part of the X/Open PAM specification.
     /// A Linux-PAM extension.
     #[cfg(feature = "linux-pam-ext")]
-    BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT,
+    BinaryPrompt = style_const::PAM_BINARY_PROMPT,
+}
+
+/// 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::conv::Exchange).
+///
+/// This question, and its internal data, is owned by its creator
+/// (either the module or PAM itself).
+#[repr(C)]
+#[derive(Debug)]
+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`],
+    /// but for requests with style `PAM_BINARY_PROMPT`,
+    /// this will be `CBinaryData` (a Linux-PAM extension).
+    pub data: Option<CHeapBox<c_void>>,
 }
 
 impl Question {
@@ -222,9 +87,9 @@
     }
 }
 
-impl TryFrom<&Message<'_>> for Question {
+impl TryFrom<&Exchange<'_>> for Question {
     type Error = ErrorCode;
-    fn try_from(msg: &Message) -> Result<Self> {
+    fn try_from(msg: &Exchange) -> Result<Self> {
         let alloc = |style, text| -> Result<_> {
             Ok((style, unsafe {
                 CHeapBox::cast(CHeapString::new(text)?.into_box())
@@ -232,18 +97,20 @@
         };
         // We will only allocate heap data if we have a valid input.
         let (style, data): (_, CHeapBox<c_void>) = match *msg {
-            Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
-            Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
-            Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
-            Message::Info(p) => alloc(Style::TextInfo, p.question()),
+            Exchange::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
+            Exchange::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
+            Exchange::Error(p) => alloc(Style::ErrorMsg, p.question()),
+            Exchange::Info(p) => alloc(Style::TextInfo, p.question()),
             #[cfg(feature = "linux-pam-ext")]
-            Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
+            Exchange::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
             #[cfg(feature = "linux-pam-ext")]
-            Message::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe {
+            Exchange::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe {
                 CHeapBox::cast(CBinaryData::alloc(p.question())?)
             })),
             #[cfg(not(feature = "linux-pam-ext"))]
-            Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError),
+            Exchange::RadioPrompt(_) | Exchange::BinaryPrompt(_) => {
+                Err(ErrorCode::ConversationError)
+            }
         }?;
         Ok(Self {
             style: style.into(),
@@ -284,7 +151,7 @@
     }
 }
 
-impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> {
+impl<'a> TryFrom<&'a Question> for OwnedExchange<'a> {
     type Error = ErrorCode;
     fn try_from(question: &'a Question) -> Result<Self> {
         let style: Style = question
@@ -313,76 +180,47 @@
 
 #[cfg(test)]
 mod tests {
+    use super::*;
 
     macro_rules! assert_matches {
-        ($id:ident => $variant:path, $q:expr) => {
-            if let $variant($id) = $id {
-                assert_eq!($q, $id.question());
+        (($variant:path, $q:expr), $input:expr) => {
+            let input = $input;
+            let exc = input.exchange();
+            if let $variant(msg) = exc {
+                assert_eq!($q, msg.question());
             } else {
-                panic!("mismatched enum variant {x:?}", x = $id);
+                panic!(
+                    "want enum variant {v}, got {exc:?}",
+                    v = stringify!($variant)
+                );
             }
         };
     }
 
-    macro_rules! tests { ($fn_name:ident<$typ:ident>) => {
-        mod $fn_name {
-            use super::super::*;
-            #[test]
-            fn standard() {
-                let interrogation = Box::pin(<$typ>::new(&[
-                    MaskedQAndA::new("hocus pocus").message(),
-                    QAndA::new("what").message(),
-                    QAndA::new("who").message(),
-                    InfoMsg::new("hey").message(),
-                    ErrorMsg::new("gasp").message(),
-                ])
-                .unwrap());
-                let indirect = interrogation.as_ref().ptr();
+    // TODO: Test TryFrom<Exchange> for Question/OwnedQuestion.
 
-                let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
-                let messages: Vec<OwnedMessage> = remade
-                    .map(TryInto::try_into)
-                    .collect::<Result<_>>()
-                    .unwrap();
-                let [masked, what, who, hey, gasp] = messages.try_into().unwrap();
-                assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus");
-                assert_matches!(what => OwnedMessage::Prompt, "what");
-                assert_matches!(who => OwnedMessage::Prompt, "who");
-                assert_matches!(hey => OwnedMessage::Info, "hey");
-                assert_matches!(gasp => OwnedMessage::Error, "gasp");
-            }
+    #[test]
+    fn standard() {
+        assert_matches!(
+            (Exchange::MaskedPrompt, "hocus pocus"),
+            MaskedQAndA::new("hocus pocus")
+        );
+        assert_matches!((Exchange::Prompt, "what"), QAndA::new("what"));
+        assert_matches!((Exchange::Prompt, "who"), QAndA::new("who"));
+        assert_matches!((Exchange::Info, "hey"), InfoMsg::new("hey"));
+        assert_matches!((Exchange::Error, "gasp"), ErrorMsg::new("gasp"));
+    }
 
-            #[test]
-            #[cfg(not(feature = "linux-pam-ext"))]
-            fn no_linux_extensions() {
-                use crate::conv::{BinaryQAndA, RadioQAndA};
-                <$typ>::new(&[
-                    BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
-                    RadioQAndA::new("you must choose").message(),
-                ]).unwrap_err();
-            }
-
-            #[test]
-            #[cfg(feature = "linux-pam-ext")]
-            fn linux_extensions() {
-                let interrogation = Box::pin(<$typ>::new(&[
-                    BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
-                    RadioQAndA::new("you must choose").message(),
-                ]).unwrap());
-                let indirect = interrogation.as_ref().ptr();
-
-                let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
-                let messages: Vec<OwnedMessage> = remade
-                    .map(TryInto::try_into)
-                    .collect::<Result<_>>()
-                    .unwrap();
-                let [bin, choose] = messages.try_into().unwrap();
-                assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66));
-                assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose");
-            }
-        }
-    }}
-
-    tests!(test_xsso<XSsoQuestions>);
-    tests!(test_linux<LinuxPamQuestions>);
+    #[test]
+    #[cfg(feature = "linux-pam-ext")]
+    fn linux_extensions() {
+        assert_matches!(
+            (Exchange::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)),
+            BinaryQAndA::new((&[5, 4, 3, 2, 1], 66))
+        );
+        assert_matches!(
+            (Exchange::RadioPrompt, "you must choose"),
+            RadioQAndA::new("you must choose")
+        );
+    }
 }
--- a/src/logging.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/logging.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -15,15 +15,16 @@
 //! and may even itself implement `log::Log`, but that interface is not exposed
 //! to the generic PAM user.
 
-#[cfg(all(feature = "link", pam_impl = "openpam"))]
+use libpam_sys::cfg_pam_impl;
+
+#[cfg_pam_impl("OpenPam")]
 mod levels {
-    use crate::libpam::pam_ffi;
-    pub const ERROR: i32 = pam_ffi::PAM_LOG_ERROR;
-    pub const WARN: i32 = pam_ffi::PAM_LOG_NOTICE;
-    pub const INFO: i32 = pam_ffi::PAM_LOG_VERBOSE;
-    pub const DEBUG: i32 = pam_ffi::PAM_LOG_DEBUG;
+    pub const ERROR: i32 = libpam_sys::PAM_LOG_ERROR;
+    pub const WARN: i32 = libpam_sys::PAM_LOG_NOTICE;
+    pub const INFO: i32 = libpam_sys::PAM_LOG_VERBOSE;
+    pub const DEBUG: i32 = libpam_sys::PAM_LOG_DEBUG;
 }
-#[cfg(not(all(feature = "link", pam_impl = "openpam")))]
+#[cfg_pam_impl(not("OpenPam"))]
 mod levels {
     pub const ERROR: i32 = libc::LOG_ERR;
     pub const WARN: i32 = libc::LOG_WARNING;
--- a/testharness/Cargo.toml	Mon Jun 30 23:49:54 2025 -0400
+++ b/testharness/Cargo.toml	Tue Jul 01 06:11:43 2025 -0400
@@ -11,9 +11,10 @@
 crate-type = ["cdylib"]
 
 [features]
-illumos-ext = ["nonstick/illumos-ext"]
+basic-ext = ["nonstick/basic-ext"]
 linux-pam-ext = ["nonstick/linux-pam-ext"]
 openpam-ext = ["nonstick/openpam-ext"]
+sun-ext = ["nonstick/sun-ext"]
 test-install = []
 
 [dependencies]