changeset 75:c30811b4afae

rename pam_ffi submodule to libpam.
author Paul Fisher <paul@pfish.zone>
date Fri, 06 Jun 2025 22:35:08 -0400
parents c7c596e6388f
children e58d24849e82
files src/handle.rs src/lib.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/memory.rs src/libpam/message.rs src/libpam/mod.rs src/libpam/module.rs src/libpam/response.rs src/pam_ffi/conversation.rs src/pam_ffi/handle.rs src/pam_ffi/memory.rs src/pam_ffi/message.rs src/pam_ffi/mod.rs src/pam_ffi/module.rs src/pam_ffi/response.rs
diffstat 16 files changed, 1469 insertions(+), 1469 deletions(-) [+]
line wrap: on
line diff
--- a/src/handle.rs	Fri Jun 06 22:21:17 2025 -0400
+++ b/src/handle.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -64,7 +64,7 @@
 /// This base trait includes features of a PAM handle that are available
 /// to both applications and modules.
 ///
-/// You probably want [`LibPamHandle`](crate::pam_ffi::OwnedLibPamHandle).
+/// You probably want [`LibPamHandle`](crate::libpam::OwnedLibPamHandle).
 /// This trait is intended to allow creating mock PAM handle types
 /// to test PAM modules and applications.
 pub trait PamShared {
--- a/src/lib.rs	Fri Jun 06 22:21:17 2025 -0400
+++ b/src/lib.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -32,10 +32,10 @@
 pub mod handle;
 
 #[cfg(feature = "link")]
-mod pam_ffi;
+mod libpam;
 
 #[cfg(feature = "link")]
-pub use crate::pam_ffi::{LibPamHandle, OwnedLibPamHandle};
+pub use crate::libpam::{LibPamHandle, OwnedLibPamHandle};
 #[doc(inline)]
 pub use crate::{
     constants::{ErrorCode, Flags, Result},
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/conversation.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -0,0 +1,162 @@
+use crate::constants::Result;
+use crate::conv::{Conversation, Message, Response};
+use crate::libpam::memory::Immovable;
+use crate::libpam::message::{MessageIndirector, OwnedMessages};
+use crate::libpam::response::{OwnedResponses, RawBinaryResponse, RawResponse, RawTextResponse};
+use crate::ErrorCode;
+use crate::ErrorCode::ConversationError;
+use std::ffi::c_int;
+use std::iter;
+use std::marker::PhantomData;
+use std::result::Result as StdResult;
+
+/// 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 [`RawResponse`]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 MessageIndirector,
+    responses: *mut *mut RawResponse,
+    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 {
+            callback: Self::wrapper_callback::<C>,
+            appdata: (conv as *mut C).cast(),
+            life: PhantomData,
+            _marker: Immovable(PhantomData),
+        }
+    }
+
+    unsafe extern "C" fn wrapper_callback<C: Conversation>(
+        count: c_int,
+        messages: *const MessageIndirector,
+        responses: *mut *mut RawResponse,
+        me: *mut AppData,
+    ) -> c_int {
+        let call = || {
+            let conv = me
+                .cast::<C>()
+                .as_mut()
+                .ok_or(ErrorCode::ConversationError)?;
+            let indir = messages.as_ref().ok_or(ErrorCode::ConversationError)?;
+            let response_ptr = responses.as_mut().ok_or(ErrorCode::ConversationError)?;
+            let messages: Vec<Message> = indir
+                .iter(count as usize)
+                .map(Message::try_from)
+                .collect::<StdResult<_, _>>()
+                .map_err(|_| ErrorCode::ConversationError)?;
+            let responses = conv.communicate(&messages)?;
+            let owned =
+                OwnedResponses::build(&responses).map_err(|_| ErrorCode::ConversationError)?;
+            *response_ptr = owned.into_ptr();
+            Ok(())
+        };
+        ErrorCode::result_to_c(call())
+    }
+}
+
+impl Conversation for LibPamConversation<'_> {
+    fn communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
+        let mut msgs_to_send = OwnedMessages::alloc(messages.len());
+        for (dst, src) in iter::zip(msgs_to_send.iter_mut(), messages.iter()) {
+            dst.set(*src).map_err(|_| ErrorCode::ConversationError)?
+        }
+        let mut response_pointer = std::ptr::null_mut();
+        // SAFETY: We're calling into PAM with valid everything.
+        let result = unsafe {
+            (self.callback)(
+                messages.len() as c_int,
+                msgs_to_send.indirector(),
+                &mut response_pointer,
+                self.appdata,
+            )
+        };
+        ErrorCode::result_from(result)?;
+        // SAFETY: This is a pointer we just got back from PAM.
+        let owned_responses =
+            unsafe { OwnedResponses::from_c_heap(response_pointer, messages.len()) };
+        convert_responses(messages, owned_responses)
+    }
+}
+
+fn convert_responses(
+    messages: &[Message],
+    mut raw_responses: OwnedResponses,
+) -> Result<Vec<Response>> {
+    let pairs = iter::zip(messages.iter(), raw_responses.iter_mut());
+    // We first collect into a Vec of Results so that we always process
+    // every single entry, which may involve freeing it.
+    let responses: Vec<_> = pairs.map(convert).collect();
+    // Only then do we return the first error, if present.
+    responses.into_iter().collect()
+}
+
+/// Converts one message-to-raw pair to a Response.
+fn convert((sent, received): (&Message, &mut RawResponse)) -> Result<Response> {
+    Ok(match sent {
+        Message::MaskedPrompt(_) => {
+            // SAFETY: Since this is a response to a text message,
+            // we know it is text.
+            let text_resp = unsafe { RawTextResponse::upcast(received) };
+            let ret = Response::MaskedText(
+                text_resp
+                    .contents()
+                    .map_err(|_| ErrorCode::ConversationError)?
+                    .into(),
+            );
+            // SAFETY: We're the only ones using this,
+            // and we haven't freed it.
+            text_resp.free_contents();
+            ret
+        }
+        Message::Prompt(_) | Message::RadioPrompt(_) => {
+            // SAFETY: Since this is a response to a text message,
+            // we know it is text.
+            let text_resp = unsafe { RawTextResponse::upcast(received) };
+            let ret = Response::Text(text_resp.contents().map_err(|_| ConversationError)?.into());
+            // SAFETY: We're the only ones using this,
+            // and we haven't freed it.
+            text_resp.free_contents();
+            ret
+        }
+        Message::ErrorMsg(_) | Message::InfoMsg(_) => Response::NoResponse,
+        Message::BinaryPrompt { .. } => {
+            let bin_resp = unsafe { RawBinaryResponse::upcast(received) };
+            let ret = Response::Binary(bin_resp.to_owned());
+            // SAFETY: We're the only ones using this,
+            // and we haven't freed it.
+            bin_resp.free_contents();
+            ret
+        }
+    })
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/handle.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -0,0 +1,231 @@
+use super::conversation::LibPamConversation;
+use crate::constants::{ErrorCode, InvalidEnum, Result};
+use crate::conv::Message;
+use crate::handle::{PamApplicationOnly, PamModuleOnly, PamShared};
+use crate::libpam::memory;
+use crate::libpam::memory::Immovable;
+use crate::{Conversation, Response};
+use num_derive::FromPrimitive;
+use num_traits::FromPrimitive;
+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())
+    }
+
+    /// 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)
+    }
+
+    /// 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 {
+    type Target = LibPamHandle;
+    fn deref(&self) -> &Self::Target {
+        unsafe { &*self.0 }
+    }
+}
+
+impl DerefMut for OwnedLibPamHandle {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        unsafe { &mut *self.0 }
+    }
+}
+
+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.
+    fn drop(&mut self) {
+        unsafe {
+            super::pam_end(self.0, 0);
+        }
+    }
+}
+
+macro_rules! cstr_item {
+    (get = $getter:ident, item = $item_type:path) => {
+        fn $getter(&mut self) -> Result<Option<&str>> {
+            unsafe { self.get_cstr_item($item_type) }
+        }
+    };
+    (set = $setter:ident, item = $item_type:path) => {
+        fn $setter(&mut self, value: Option<&str>) -> Result<()> {
+            unsafe { self.set_cstr_item($item_type, value) }
+        }
+    };
+}
+
+impl PamShared for LibPamHandle {
+    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())) };
+        ErrorCode::result_from(ret)?;
+        unsafe { memory::wrap_string(output) }
+            .transpose()
+            .unwrap_or(Err(ErrorCode::ConversationError))
+    }
+
+    cstr_item!(get = user_item, item = ItemType::User);
+    cstr_item!(set = set_user_item, item = ItemType::User);
+    cstr_item!(get = service, item = ItemType::Service);
+    cstr_item!(set = set_service, item = ItemType::Service);
+    cstr_item!(get = user_prompt, item = ItemType::UserPrompt);
+    cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt);
+    cstr_item!(get = tty_name, item = ItemType::Tty);
+    cstr_item!(set = set_tty_name, item = ItemType::Tty);
+    cstr_item!(get = remote_user, item = ItemType::RemoteUser);
+    cstr_item!(set = set_remote_user, item = ItemType::RemoteUser);
+    cstr_item!(get = remote_host, item = ItemType::RemoteHost);
+    cstr_item!(set = set_remote_host, item = ItemType::RemoteHost);
+    cstr_item!(set = set_authtok_item, item = ItemType::AuthTok);
+    cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok);
+}
+
+impl Conversation for LibPamHandle {
+    fn communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
+        self.conversation_item()?.communicate(messages)
+    }
+}
+
+impl PamModuleOnly 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(
+                self,
+                ItemType::AuthTok.into(),
+                &mut output,
+                memory::prompt_ptr(prompt.as_ref()),
+            )
+        };
+        ErrorCode::result_from(res)?;
+        // SAFETY: We got this string from PAM.
+        unsafe { memory::wrap_string(output) }
+            .transpose()
+            .unwrap_or(Err(ErrorCode::ConversationError))
+    }
+
+    cstr_item!(get = authtok_item, item = ItemType::AuthTok);
+    cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok);
+}
+
+/// Function called at the end of a PAM session that is called to clean up
+/// a value previously provided to PAM in a `pam_set_data` call.
+///
+/// You should never call this yourself.
+extern "C" fn set_data_cleanup<T>(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) {
+    unsafe {
+        let _data: Box<T> = Box::from_raw(c_data.cast());
+    }
+}
+
+/// Identifies what is being gotten or set with `pam_get_item`
+/// or `pam_set_item`.
+#[derive(FromPrimitive)]
+#[repr(i32)]
+#[non_exhaustive] // because C could give us anything!
+pub enum ItemType {
+    /// The PAM service name.
+    Service = 1,
+    /// The user's login name.
+    User = 2,
+    /// The TTY name.
+    Tty = 3,
+    /// The remote host (if applicable).
+    RemoteHost = 4,
+    /// The conversation struct (not a CStr-based item).
+    Conversation = 5,
+    /// The authentication token (password).
+    AuthTok = 6,
+    /// The old authentication token (when changing passwords).
+    OldAuthTok = 7,
+    /// The remote user's name.
+    RemoteUser = 8,
+    /// The prompt shown when requesting a username.
+    UserPrompt = 9,
+    /// App-supplied function to override failure delays.
+    FailDelay = 10,
+    /// X display name.
+    XDisplay = 11,
+    /// X server authentication data.
+    XAuthData = 12,
+    /// 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
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/memory.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -0,0 +1,204 @@
+//! Things for dealing with memory.
+
+use crate::ErrorCode;
+use crate::Result;
+use std::ffi::{c_char, c_void, CStr, CString};
+use std::marker::{PhantomData, PhantomPinned};
+use std::result::Result as StdResult;
+use std::{ptr, slice};
+
+/// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`].
+#[repr(C)]
+#[derive(Debug)]
+pub struct Immovable(pub PhantomData<(*mut u8, PhantomPinned)>);
+
+/// Safely converts a `&str` option to a `CString` option.
+pub fn option_cstr(prompt: Option<&str>) -> Result<Option<CString>> {
+    prompt
+        .map(CString::new)
+        .transpose()
+        .map_err(|_| ErrorCode::ConversationError)
+}
+
+/// Gets the pointer to the given CString, or a null pointer if absent.
+pub fn prompt_ptr(prompt: Option<&CString>) -> *const c_char {
+    match prompt {
+        Some(c_str) => c_str.as_ptr(),
+        None => ptr::null(),
+    }
+}
+
+/// Creates an owned copy of a string that is returned from a
+/// <code>pam_get_<var>whatever</var></code> function.
+///
+/// # Safety
+///
+/// It's on you to provide a valid string.
+pub unsafe fn copy_pam_string(result_ptr: *const libc::c_char) -> Result<String> {
+    // We really shouldn't get a null pointer back here, but if we do, return nothing.
+    if result_ptr.is_null() {
+        return Ok(String::new());
+    }
+    let bytes = unsafe { CStr::from_ptr(result_ptr) };
+    bytes
+        .to_str()
+        .map(String::from)
+        .map_err(|_| ErrorCode::ConversationError)
+}
+
+/// Wraps a string returned from PAM as an `Option<&str>`.
+pub unsafe fn wrap_string<'a>(data: *const libc::c_char) -> Result<Option<&'a str>> {
+    let ret = if data.is_null() {
+        None
+    } else {
+        Some(
+            CStr::from_ptr(data)
+                .to_str()
+                .map_err(|_| ErrorCode::ConversationError)?,
+        )
+    };
+    Ok(ret)
+}
+
+/// Allocates a string with the given contents on the C heap.
+///
+/// This is like [`CString::new`](std::ffi::CString::new), but:
+///
+/// - it allocates data on the C heap with [`libc::malloc`].
+/// - it doesn't take ownership of the data passed in.
+pub fn malloc_str(text: impl AsRef<str>) -> StdResult<*mut c_char, NulError> {
+    let data = text.as_ref().as_bytes();
+    if let Some(nul) = data.iter().position(|x| *x == 0) {
+        return Err(NulError(nul));
+    }
+    unsafe {
+        let data_alloc = libc::calloc(data.len() + 1, 1);
+        libc::memcpy(data_alloc, data.as_ptr().cast(), data.len());
+        Ok(data_alloc.cast())
+    }
+}
+
+/// Writes zeroes over the contents of a C string.
+///
+/// This won't overwrite a null pointer.
+///
+/// # Safety
+///
+/// It's up to you to provide a valid C string.
+pub unsafe fn zero_c_string(cstr: *mut c_void) {
+    if !cstr.is_null() {
+        libc::memset(cstr, 0, libc::strlen(cstr.cast()));
+    }
+}
+
+/// Binary data used in requests and responses.
+///
+/// This is an unsized data type whose memory goes beyond its data.
+/// This must be allocated on the C heap.
+///
+/// A Linux-PAM extension.
+#[repr(C)]
+pub struct CBinaryData {
+    /// The total length of the structure; a u32 in network byte order (BE).
+    total_length: [u8; 4],
+    /// A tag of undefined meaning.
+    data_type: u8,
+    /// Pointer to an array of length [`length`](Self::length) − 5
+    data: [u8; 0],
+    _marker: Immovable,
+}
+
+impl CBinaryData {
+    /// Copies the given data to a new BinaryData on the heap.
+    pub fn alloc(source: &[u8], data_type: u8) -> StdResult<*mut CBinaryData, TooBigError> {
+        let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError {
+            max: (u32::MAX - 5) as usize,
+            actual: source.len(),
+        })?;
+        // SAFETY: We're only allocating here.
+        let data = unsafe {
+            let dest_buffer: *mut CBinaryData = libc::malloc(buffer_size as usize).cast();
+            let data = &mut *dest_buffer;
+            data.total_length = buffer_size.to_be_bytes();
+            data.data_type = data_type;
+            let dest = data.data.as_mut_ptr();
+            libc::memcpy(dest.cast(), source.as_ptr().cast(), source.len());
+            dest_buffer
+        };
+        Ok(data)
+    }
+
+    fn length(&self) -> usize {
+        u32::from_be_bytes(self.total_length).saturating_sub(5) as usize
+    }
+
+    pub fn contents(&self) -> &[u8] {
+        unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) }
+    }
+    pub fn data_type(&self) -> u8 {
+        self.data_type
+    }
+
+    /// Clears this data and frees it.
+    pub unsafe fn zero_contents(&mut self) {
+        let contents = slice::from_raw_parts_mut(self.data.as_mut_ptr(), self.length());
+        for v in contents {
+            *v = 0
+        }
+        self.data_type = 0;
+        self.total_length = [0; 4];
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("null byte within input at byte {0}")]
+pub struct NulError(pub usize);
+
+/// Returned when trying to fit too much data into a binary message.
+#[derive(Debug, thiserror::Error)]
+#[error("cannot create a message of {actual} bytes; maximum is {max}")]
+pub struct TooBigError {
+    pub actual: usize,
+    pub max: usize,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string};
+    use crate::ErrorCode;
+    use std::ffi::CString;
+    #[test]
+    fn test_strings() {
+        let str = malloc_str("hello there").unwrap();
+        malloc_str("hell\0 there").unwrap_err();
+        unsafe {
+            let copied = copy_pam_string(str.cast()).unwrap();
+            assert_eq!("hello there", copied);
+            zero_c_string(str.cast());
+            let idx_three = str.add(3).as_mut().unwrap();
+            *idx_three = 0x80u8 as i8;
+            let zeroed = copy_pam_string(str.cast()).unwrap();
+            assert!(zeroed.is_empty());
+            libc::free(str.cast());
+        }
+    }
+
+    #[test]
+    fn test_option_str() {
+        let good = option_cstr(Some("whatever")).unwrap();
+        assert_eq!("whatever", good.unwrap().to_str().unwrap());
+        let no_str = option_cstr(None).unwrap();
+        assert!(no_str.is_none());
+        let bad_str = option_cstr(Some("what\0ever")).unwrap_err();
+        assert_eq!(ErrorCode::ConversationError, bad_str);
+    }
+
+    #[test]
+    fn test_prompt() {
+        let prompt_cstr = CString::new("good").ok();
+        let prompt = prompt_ptr(prompt_cstr.as_ref());
+        assert!(!prompt.is_null());
+        let no_prompt = prompt_ptr(None);
+        assert!(no_prompt.is_null());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/message.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -0,0 +1,334 @@
+//! Data and types dealing with PAM messages.
+
+use crate::constants::InvalidEnum;
+use crate::conv::Message;
+use crate::libpam::memory;
+use crate::libpam::memory::{CBinaryData, Immovable, NulError, TooBigError};
+use num_derive::FromPrimitive;
+use num_traits::FromPrimitive;
+use std::ffi::{c_int, c_void, CStr};
+use std::result::Result as StdResult;
+use std::str::Utf8Error;
+use std::{ptr, slice};
+
+#[derive(Debug, thiserror::Error)]
+#[error("error creating PAM message: {0}")]
+pub enum ConversionError {
+    InvalidEnum(#[from] InvalidEnum<Style>),
+    Utf8Error(#[from] Utf8Error),
+    NulError(#[from] NulError),
+    TooBigError(#[from] TooBigError),
+}
+
+/// 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 From<Style> for c_int {
+    fn from(val: Style) -> Self {
+        val as Self
+    }
+}
+
+/// A message sent by PAM or a module to an application.
+/// This message, and its internal data, is owned by the creator
+/// (either the module or PAM itself).
+#[repr(C)]
+pub struct RawMessage {
+    /// 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 RawMessage {
+    pub fn set(&mut self, msg: Message) -> StdResult<(), ConversionError> {
+        let (style, data) = copy_to_heap(msg)?;
+        self.clear();
+        // SAFETY: We allocated this ourselves or were given it by PAM.
+        // Otherwise, it's null, but free(null) is fine.
+        unsafe { libc::free(self.data) };
+        self.style = style as c_int;
+        self.data = data;
+        Ok(())
+    }
+
+    /// Gets this message's data pointer as a string.
+    ///
+    /// # Safety
+    ///
+    /// It's up to you to pass this only on types with a string value.
+    unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> {
+        if self.data.is_null() {
+            Ok("")
+        } else {
+            CStr::from_ptr(self.data.cast()).to_str()
+        }
+    }
+
+    /// Zeroes out the data stored here.
+    fn clear(&mut self) {
+        // SAFETY: We either created this data or we got it from PAM.
+        // After this function is done, it will be zeroed out.
+        unsafe {
+            if let Ok(style) = Style::try_from(self.style) {
+                match style {
+                    Style::BinaryPrompt => {
+                        if let Some(d) = self.data.cast::<CBinaryData>().as_mut() {
+                            d.zero_contents()
+                        }
+                    }
+                    Style::TextInfo
+                    | Style::RadioType
+                    | Style::ErrorMsg
+                    | Style::PromptEchoOff
+                    | Style::PromptEchoOn => memory::zero_c_string(self.data),
+                }
+            };
+            libc::free(self.data);
+            self.data = ptr::null_mut();
+        }
+    }
+}
+
+/// Copies the contents of this message to the C heap.
+fn copy_to_heap(msg: Message) -> StdResult<(Style, *mut c_void), ConversionError> {
+    let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
+    match msg {
+        Message::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text),
+        Message::Prompt(text) => alloc(Style::PromptEchoOn, text),
+        Message::RadioPrompt(text) => alloc(Style::RadioType, text),
+        Message::ErrorMsg(text) => alloc(Style::ErrorMsg, text),
+        Message::InfoMsg(text) => alloc(Style::TextInfo, text),
+        Message::BinaryPrompt { data, data_type } => Ok((
+            Style::BinaryPrompt,
+            (CBinaryData::alloc(data, data_type)?).cast(),
+        )),
+    }
+}
+
+/// Abstraction of a list-of-messages to be sent in a PAM conversation.
+///
+/// On Linux-PAM and other compatible implementations, `messages`
+/// is treated as a pointer-to-pointers, like `int argc, char **argv`.
+/// (In this situation, the value of `OwnedMessages.indirect` is
+/// the pointer passed to `pam_conv`.)
+///
+/// ```text
+/// ╔═ OwnedMsgs ═╗  points to  ┌─ Indirect ─┐       ╔═ Message ═╗
+/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style     ║
+/// ║ count       ║             │ base[1] ┄┄┄┼┄┄┄╮   ║ data      ║
+/// ╚═════════════╝             │ ...        │   ┆   ╚═══════════╝
+///                                              ┆
+///                                              ┆    ╔═ Message ═╗
+///                                              ╰┄┄> ║ 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
+/// ╔═ OwnedMsgs ═╗  points to  ┌─ Indirect ─┐       ╔═ Message[] ═╗
+/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style       ║
+/// ║ count       ║             └────────────┘       ║ data        ║
+/// ╚═════════════╝                                  ╟─────────────╢
+///                                                  ║ style       ║
+///                                                  ║ data        ║
+///                                                  ╟─────────────╢
+///                                                  ║ ...         ║
+/// ```
+///
+/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
+pub struct OwnedMessages {
+    /// An indirection to the messages themselves, stored on the C heap.
+    indirect: *mut MessageIndirector,
+    /// The number of messages in the list.
+    count: usize,
+}
+
+impl OwnedMessages {
+    /// Allocates data to store messages on the C heap.
+    pub fn alloc(count: usize) -> Self {
+        Self {
+            indirect: MessageIndirector::alloc(count),
+            count,
+        }
+    }
+
+    /// The pointer to the thing with the actual list.
+    pub fn indirector(&self) -> *const MessageIndirector {
+        self.indirect
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = &RawMessage> {
+        // SAFETY: we're iterating over an amount we know.
+        unsafe { (*self.indirect).iter(self.count) }
+    }
+
+    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut RawMessage> {
+        // SAFETY: we're iterating over an amount we know.
+        unsafe { (*self.indirect).iter_mut(self.count) }
+    }
+}
+
+impl Drop for OwnedMessages {
+    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)
+            }
+            libc::free(self.indirect.cast());
+            self.indirect = ptr::null_mut();
+        }
+    }
+}
+
+/// 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.
+#[repr(transparent)]
+pub struct MessageIndirector {
+    base: [*mut RawMessage; 0],
+    _marker: Immovable,
+}
+
+impl MessageIndirector {
+    /// Allocates memory for this indirector and all its members.
+    fn alloc(count: usize) -> *mut Self {
+        // SAFETY: We're only allocating, and when we're done,
+        // everything will be in a known-good state.
+        unsafe {
+            let me_ptr: *mut MessageIndirector =
+                libc::calloc(count, size_of::<*mut RawMessage>()).cast();
+            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 = libc::calloc(1, size_of::<RawMessage>()).cast();
+            }
+            me
+        }
+    }
+
+    /// 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 = &RawMessage> {
+        (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 RawMessage> {
+        (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx))
+    }
+
+    /// Frees this and everything it points to.
+    ///
+    /// # Safety
+    ///
+    /// You have to pass the right size.
+    unsafe fn free(&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() {
+                msg.clear();
+            }
+            libc::free(msg.cast());
+            *msg = ptr::null_mut();
+        }
+    }
+}
+
+impl<'a> TryFrom<&'a RawMessage> for Message<'a> {
+    type Error = ConversionError;
+
+    /// Retrieves the data stored in this message.
+    fn try_from(input: &RawMessage) -> StdResult<Message, ConversionError> {
+        let style: Style = input.style.try_into()?;
+        // SAFETY: We either allocated this message ourselves or were provided it by PAM.
+        let result = unsafe {
+            match style {
+                Style::PromptEchoOff => Message::MaskedPrompt(input.string_data()?),
+                Style::PromptEchoOn => Message::Prompt(input.string_data()?),
+                Style::TextInfo => Message::InfoMsg(input.string_data()?),
+                Style::ErrorMsg => Message::ErrorMsg(input.string_data()?),
+                Style::RadioType => Message::ErrorMsg(input.string_data()?),
+                Style::BinaryPrompt => input.data.cast::<CBinaryData>().as_ref().map_or_else(
+                    || Message::BinaryPrompt {
+                        data_type: 0,
+                        data: &[],
+                    },
+                    |data| Message::BinaryPrompt {
+                        data_type: data.data_type(),
+                        data: data.contents(),
+                    },
+                ),
+            }
+        };
+        Ok(result)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::conv::Message;
+    use crate::libpam::message::OwnedMessages;
+
+    #[test]
+    fn test_owned_messages() {
+        let mut tons_of_messages = OwnedMessages::alloc(10);
+        let mut msgs: Vec<_> = tons_of_messages.iter_mut().collect();
+        assert!(msgs.get(10).is_none());
+        let last_msg = &mut msgs[9];
+        last_msg.set(Message::MaskedPrompt("hocus pocus")).unwrap();
+        let another_msg = &mut msgs[0];
+        another_msg
+            .set(Message::BinaryPrompt {
+                data: &[5, 4, 3, 2, 1],
+                data_type: 99,
+            })
+            .unwrap();
+        let overwrite = &mut msgs[3];
+        overwrite.set(Message::Prompt("what")).unwrap();
+        overwrite.set(Message::Prompt("who")).unwrap();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/mod.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -0,0 +1,65 @@
+//! The PAM library FFI and helpers for managing it.
+//!
+//! This includes the functions provided by PAM and the data structures
+//! used by PAM, as well as a few low-level abstractions for dealing with
+//! those data structures.
+//!
+//! Everything in here is hazmat.
+//!
+
+#![allow(dead_code)]
+
+mod conversation;
+mod handle;
+mod memory;
+mod message;
+mod module;
+mod response;
+
+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/module.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -0,0 +1,153 @@
+use std::ffi::CStr;
+
+/// Generates the dynamic library entry points for a [PamModule] implementation.
+///
+/// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will
+/// generate the exported `extern "C"` functions that PAM uses to call into
+/// your module.
+///
+/// ## Examples:
+///
+/// Here is full example of a PAM module that would authenticate and authorize everybody:
+///
+/// ```
+/// use nonstick::{Flags, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks};
+/// use std::ffi::CStr;
+/// # fn main() {}
+///
+/// struct MyPamModule;
+/// pam_hooks!(MyPamModule);
+///
+/// impl<T: PamHandleModule> PamModule<T> for MyPamModule {
+///     fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+///         let password = handle.get_authtok(Some("what's your password?"))?;
+///         handle.info_msg(fmt!("If you say your password is {password:?}, who am I to disagree?"));
+///     }
+///
+///     fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+///         let username = handle.get_user(None)?;
+///         handle.info_msg(fmt!("Hello {username}! I trust you unconditionally."))
+///         Ok(())
+///     }
+/// }
+/// ```
+#[macro_export]
+macro_rules! pam_hooks {
+    ($ident:ident) => {
+        mod _pam_hooks_scope {
+            use std::ffi::{c_char, c_int, CStr};
+            use $crate::{ErrorCode, Flags, LibPamHandle, PamModule};
+
+            #[no_mangle]
+            extern "C" fn pam_sm_acct_mgmt(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                    let args = extract_argv(argc, argv);
+                    ErrorCode::result_to_c(super::$ident::account_management(handle, args, flags))
+                } else {
+                    ErrorCode::Ignore as c_int
+                }
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_authenticate(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                    let args = extract_argv(argc, argv);
+                    ErrorCode::result_to_c(super::$ident::authenticate(handle, args, flags))
+                } else {
+                    ErrorCode::Ignore as c_int
+                }
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_chauthtok(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                    let args = extract_argv(argc, argv);
+                    ErrorCode::result_to_c(super::$ident::change_authtok(handle, args, flags))
+                } else {
+                    ErrorCode::Ignore as c_int
+                }
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_close_session(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                    let args = extract_argv(argc, argv);
+                    ErrorCode::result_to_c(super::$ident::close_session(handle, args, flags))
+                } else {
+                    ErrorCode::Ignore as c_int
+                }
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_open_session(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                let args = extract_argv(argc, argv);
+                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                    ErrorCode::result_to_c(super::$ident::open_session(handle, args, flags))
+                } else {
+                    ErrorCode::Ignore as c_int
+                }
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_setcred(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                let args = extract_argv(argc, argv);
+                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
+                    ErrorCode::result_to_c(super::$ident::set_credentials(handle, args, flags))
+                } else {
+                    ErrorCode::Ignore as c_int
+                }
+            }
+
+            /// Turns `argc`/`argv` into a [Vec] of [CStr]s.
+            ///
+            /// # Safety
+            ///
+            /// We use this only with arguments we get from `libpam`, which we kind of have to trust.
+            fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
+                (0..argc)
+                    .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) })
+                    .collect()
+            }
+        }
+    };
+}
+
+#[cfg(test)]
+mod tests {
+    // Compile-time test that the `pam_hooks` macro compiles.
+    use crate::{PamHandleModule, PamModule};
+    struct Foo;
+    impl<T: PamHandleModule> PamModule<T> for Foo {}
+
+    pam_hooks!(Foo);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/response.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -0,0 +1,317 @@
+//! Types used when dealing with PAM conversations.
+
+use crate::conv::BinaryData;
+use crate::libpam::memory;
+use crate::libpam::memory::{CBinaryData, Immovable, NulError, TooBigError};
+use crate::Response;
+use std::ffi::{c_int, c_void, CStr};
+use std::ops::{Deref, DerefMut};
+use std::result::Result as StdResult;
+use std::str::Utf8Error;
+use std::{iter, mem, ptr, slice};
+
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct RawTextResponse(RawResponse);
+
+impl RawTextResponse {
+    /// Interprets the provided `RawResponse` as a text response.
+    ///
+    /// # Safety
+    ///
+    /// It's up to you to provide a response that is a `RawTextResponse`.
+    pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self {
+        // SAFETY: We're provided a valid reference.
+        &mut *(from as *mut RawResponse).cast::<Self>()
+    }
+
+    /// Fills in the provided `RawResponse` with the given text.
+    ///
+    /// You are responsible for calling [`free`](Self::free_contents)
+    /// on the pointer you get back when you're done with it.
+    pub fn fill(dest: &mut RawResponse, text: impl AsRef<str>) -> StdResult<&mut Self, NulError> {
+        dest.data = memory::malloc_str(text)?.cast();
+        // SAFETY: We just filled this in so we know it's a text response.
+        Ok(unsafe { Self::upcast(dest) })
+    }
+
+    /// Gets the string stored in this response.
+    pub fn contents(&self) -> StdResult<&str, Utf8Error> {
+        if self.0.data.is_null() {
+            Ok("")
+        } else {
+            // SAFETY: This data is either passed from PAM (so we are forced to
+            // trust it) or was created by us in TextResponseInner::alloc.
+            // In either case, it's going to be a valid null-terminated string.
+            unsafe { CStr::from_ptr(self.0.data.cast()) }.to_str()
+        }
+    }
+
+    /// Releases memory owned by this response.
+    pub fn free_contents(&mut self) {
+        // SAFETY: We know we own this data.
+        // After we're done, it will be null.
+        unsafe {
+            memory::zero_c_string(self.0.data);
+            libc::free(self.0.data);
+            self.0.data = ptr::null_mut()
+        }
+    }
+}
+
+/// A [`RawResponse`] with [`CBinaryData`] in it.
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct RawBinaryResponse(RawResponse);
+
+impl RawBinaryResponse {
+    /// Interprets the provided `RawResponse` as a binary response.
+    ///
+    /// # Safety
+    ///
+    /// It's up to you to provide a response that is a `RawBinaryResponse`.
+    pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self {
+        // SAFETY: We're provided a valid reference.
+        &mut *(from as *mut RawResponse).cast::<Self>()
+    }
+
+    /// Fills in a `RawResponse` with the provided binary data.
+    ///
+    /// The `data_type` is a tag you can use for whatever.
+    /// It is passed through PAM unchanged.
+    ///
+    /// The referenced data is copied to the C heap. We do not take ownership.
+    /// You are responsible for calling [`free`](Self::free_contents)
+    /// on the pointer you get back when you're done with it.
+    pub fn fill<'a>(
+        dest: &'a mut RawResponse,
+        data: &[u8],
+        data_type: u8,
+    ) -> StdResult<&'a mut Self, TooBigError> {
+        dest.data = CBinaryData::alloc(data, data_type)?.cast();
+        // SAFETY: We just filled this in, so we know it's binary.
+        Ok(unsafe { Self::upcast(dest) })
+    }
+
+    /// Gets the binary data in this response.
+    pub fn data(&self) -> &[u8] {
+        self.contents().map(CBinaryData::contents).unwrap_or(&[])
+    }
+
+    /// Gets the `data_type` tag that was embedded with the message.
+    pub fn data_type(&self) -> u8 {
+        self.contents().map(CBinaryData::data_type).unwrap_or(0)
+    }
+
+    fn contents(&self) -> Option<&CBinaryData> {
+        // SAFETY: This was either something we got from PAM (in which case
+        // we trust it), or something that was created with
+        // BinaryResponseInner::alloc. In both cases, it points to valid data.
+        unsafe { self.0.data.cast::<CBinaryData>().as_ref() }
+    }
+
+    pub fn to_owned(&self) -> BinaryData {
+        BinaryData::new(self.data().into(), self.data_type())
+    }
+
+    /// Releases memory owned by this response.
+    pub fn free_contents(&mut self) {
+        // SAFETY: We know that our data pointer is either valid or null.
+        // Once we're done, it's null and the response is safe.
+        unsafe {
+            let data_ref = self.0.data.cast::<CBinaryData>().as_mut();
+            if let Some(d) = data_ref {
+                d.zero_contents()
+            }
+            libc::free(self.0.data);
+            self.0.data = ptr::null_mut()
+        }
+    }
+}
+
+/// Generic version of response data.
+///
+/// This has the same structure as [`RawBinaryResponse`]
+/// and [`RawTextResponse`].
+#[repr(C)]
+#[derive(Debug)]
+pub struct RawResponse {
+    /// Pointer to the data returned in a response.
+    /// For most responses, this will be a [`CStr`], but for responses to
+    /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`]
+    /// (a Linux-PAM extension).
+    data: *mut c_void,
+    /// Unused.
+    return_code: c_int,
+    _marker: Immovable,
+}
+
+/// A contiguous block of responses.
+#[derive(Debug)]
+pub struct OwnedResponses {
+    base: *mut RawResponse,
+    count: usize,
+}
+
+impl OwnedResponses {
+    /// Allocates an owned list of responses on the C heap.
+    fn alloc(count: usize) -> Self {
+        OwnedResponses {
+            // SAFETY: We are doing allocation here.
+            base: unsafe { libc::calloc(count, size_of::<RawResponse>()) }.cast(),
+            count,
+        }
+    }
+
+    pub fn build(value: &[Response]) -> StdResult<Self, FillError> {
+        let mut outputs = OwnedResponses::alloc(value.len());
+        // If we fail in here after allocating OwnedResponses,
+        // we still free all memory, even though we don't zero it first.
+        // This is an acceptable level of risk.
+        for (input, output) in iter::zip(value.iter(), outputs.iter_mut()) {
+            match input {
+                Response::NoResponse => {
+                    RawTextResponse::fill(output, "")?;
+                }
+                Response::Text(data) => {
+                    RawTextResponse::fill(output, data)?;
+                }
+                Response::MaskedText(data) => {
+                    RawTextResponse::fill(output, data.unsecure())?;
+                }
+                Response::Binary(data) => {
+                    RawBinaryResponse::fill(output, data.data(), data.data_type())?;
+                }
+            }
+        }
+        Ok(outputs)
+    }
+
+    /// Converts this into a `*RawResponse` for passing to PAM.
+    ///
+    /// The pointer "owns" its own data (i.e., this will not be dropped).
+    pub fn into_ptr(self) -> *mut RawResponse {
+        let ret = self.base;
+        mem::forget(self);
+        ret
+    }
+
+    /// Takes ownership of a list of responses allocated on the C heap.
+    ///
+    /// # Safety
+    ///
+    /// It's up to you to make sure you pass a valid pointer.
+    pub unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self {
+        OwnedResponses { base, count }
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("error converting responses: {0}")]
+pub enum FillError {
+    NulError(#[from] NulError),
+    TooBigError(#[from] TooBigError),
+}
+
+impl Deref for OwnedResponses {
+    type Target = [RawResponse];
+    fn deref(&self) -> &Self::Target {
+        // SAFETY: This is the memory we manage ourselves.
+        unsafe { slice::from_raw_parts(self.base, self.count) }
+    }
+}
+
+impl DerefMut for OwnedResponses {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        // SAFETY: This is the memory we manage ourselves.
+        unsafe { slice::from_raw_parts_mut(self.base, self.count) }
+    }
+}
+
+impl Drop for OwnedResponses {
+    fn drop(&mut self) {
+        // SAFETY: We allocated this ourselves, or it was provided to us by PAM.
+        unsafe {
+            for resp in self.iter_mut() {
+                libc::free(resp.data)
+            }
+            libc::free(self.base.cast())
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{BinaryData, OwnedResponses, RawBinaryResponse, RawTextResponse, Response};
+
+    #[test]
+    fn test_round_trip() {
+        let responses = [
+            Response::Binary(BinaryData::new(vec![1, 2, 3], 99)),
+            Response::Text("whats going on".to_owned()),
+            Response::MaskedText("well then".into()),
+            Response::NoResponse,
+            Response::Text("bogus".to_owned()),
+        ];
+        let sent = OwnedResponses::build(&responses).unwrap();
+        let heap_resps = sent.into_ptr();
+        let mut received = unsafe { OwnedResponses::from_c_heap(heap_resps, 5) };
+
+        let assert_text = |want, raw| {
+            let up = unsafe { RawTextResponse::upcast(raw) };
+            assert_eq!(want, up.contents().unwrap());
+            up.free_contents();
+            assert_eq!("", up.contents().unwrap());
+        };
+        let assert_bin = |want_data: &[u8], want_type, raw| {
+            let up = unsafe { RawBinaryResponse::upcast(raw) };
+            assert_eq!(want_data, up.data());
+            assert_eq!(want_type, up.data_type());
+            up.free_contents();
+            let empty: [u8; 0] = [];
+            assert_eq!(&empty, up.data());
+            assert_eq!(0, up.data_type());
+        };
+        if let [zero, one, two, three, four] = &mut received[..] {
+            assert_bin(&[1, 2, 3], 99, zero);
+            assert_text("whats going on", one);
+            assert_text("well then", two);
+            assert_text("", three);
+            assert_text("bogus", four);
+        } else {
+            panic!("wrong size!")
+        }
+    }
+
+    #[test]
+    fn test_text_response() {
+        let mut responses = OwnedResponses::alloc(2);
+        let text = RawTextResponse::fill(&mut responses[0], "hello").unwrap();
+        let data = text.contents().expect("valid");
+        assert_eq!("hello", data);
+        text.free_contents();
+        text.free_contents();
+        RawTextResponse::fill(&mut responses[1], "hell\0").expect_err("should error; contains nul");
+    }
+
+    #[test]
+    fn test_binary_response() {
+        let mut responses = OwnedResponses::alloc(1);
+        let real_data = [1, 2, 3, 4, 5, 6, 7, 8];
+        let resp = RawBinaryResponse::fill(&mut responses[0], &real_data, 7)
+            .expect("alloc should succeed");
+        let data = resp.data();
+        assert_eq!(&real_data, data);
+        assert_eq!(7, resp.data_type());
+        resp.free_contents();
+        resp.free_contents();
+    }
+
+    #[test]
+    #[ignore]
+    fn test_binary_response_too_big() {
+        let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000];
+        let mut responses = OwnedResponses::alloc(1);
+        RawBinaryResponse::fill(&mut responses[0], &big_data, 0).expect_err("this is too big!");
+    }
+}
--- a/src/pam_ffi/conversation.rs	Fri Jun 06 22:21:17 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,162 +0,0 @@
-use crate::constants::Result;
-use crate::conv::{Conversation, Message, Response};
-use crate::pam_ffi::memory::Immovable;
-use crate::pam_ffi::message::{MessageIndirector, OwnedMessages};
-use crate::pam_ffi::response::{OwnedResponses, RawBinaryResponse, RawResponse, RawTextResponse};
-use crate::ErrorCode;
-use crate::ErrorCode::ConversationError;
-use std::ffi::c_int;
-use std::iter;
-use std::marker::PhantomData;
-use std::result::Result as StdResult;
-
-/// 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 [`RawResponse`]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 MessageIndirector,
-    responses: *mut *mut RawResponse,
-    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 {
-            callback: Self::wrapper_callback::<C>,
-            appdata: (conv as *mut C).cast(),
-            life: PhantomData,
-            _marker: Immovable(PhantomData),
-        }
-    }
-
-    unsafe extern "C" fn wrapper_callback<C: Conversation>(
-        count: c_int,
-        messages: *const MessageIndirector,
-        responses: *mut *mut RawResponse,
-        me: *mut AppData,
-    ) -> c_int {
-        let call = || {
-            let conv = me
-                .cast::<C>()
-                .as_mut()
-                .ok_or(ErrorCode::ConversationError)?;
-            let indir = messages.as_ref().ok_or(ErrorCode::ConversationError)?;
-            let response_ptr = responses.as_mut().ok_or(ErrorCode::ConversationError)?;
-            let messages: Vec<Message> = indir
-                .iter(count as usize)
-                .map(Message::try_from)
-                .collect::<StdResult<_, _>>()
-                .map_err(|_| ErrorCode::ConversationError)?;
-            let responses = conv.communicate(&messages)?;
-            let owned =
-                OwnedResponses::build(&responses).map_err(|_| ErrorCode::ConversationError)?;
-            *response_ptr = owned.into_ptr();
-            Ok(())
-        };
-        ErrorCode::result_to_c(call())
-    }
-}
-
-impl Conversation for LibPamConversation<'_> {
-    fn communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
-        let mut msgs_to_send = OwnedMessages::alloc(messages.len());
-        for (dst, src) in iter::zip(msgs_to_send.iter_mut(), messages.iter()) {
-            dst.set(*src).map_err(|_| ErrorCode::ConversationError)?
-        }
-        let mut response_pointer = std::ptr::null_mut();
-        // SAFETY: We're calling into PAM with valid everything.
-        let result = unsafe {
-            (self.callback)(
-                messages.len() as c_int,
-                msgs_to_send.indirector(),
-                &mut response_pointer,
-                self.appdata,
-            )
-        };
-        ErrorCode::result_from(result)?;
-        // SAFETY: This is a pointer we just got back from PAM.
-        let owned_responses =
-            unsafe { OwnedResponses::from_c_heap(response_pointer, messages.len()) };
-        convert_responses(messages, owned_responses)
-    }
-}
-
-fn convert_responses(
-    messages: &[Message],
-    mut raw_responses: OwnedResponses,
-) -> Result<Vec<Response>> {
-    let pairs = iter::zip(messages.iter(), raw_responses.iter_mut());
-    // We first collect into a Vec of Results so that we always process
-    // every single entry, which may involve freeing it.
-    let responses: Vec<_> = pairs.map(convert).collect();
-    // Only then do we return the first error, if present.
-    responses.into_iter().collect()
-}
-
-/// Converts one message-to-raw pair to a Response.
-fn convert((sent, received): (&Message, &mut RawResponse)) -> Result<Response> {
-    Ok(match sent {
-        Message::MaskedPrompt(_) => {
-            // SAFETY: Since this is a response to a text message,
-            // we know it is text.
-            let text_resp = unsafe { RawTextResponse::upcast(received) };
-            let ret = Response::MaskedText(
-                text_resp
-                    .contents()
-                    .map_err(|_| ErrorCode::ConversationError)?
-                    .into(),
-            );
-            // SAFETY: We're the only ones using this,
-            // and we haven't freed it.
-            text_resp.free_contents();
-            ret
-        }
-        Message::Prompt(_) | Message::RadioPrompt(_) => {
-            // SAFETY: Since this is a response to a text message,
-            // we know it is text.
-            let text_resp = unsafe { RawTextResponse::upcast(received) };
-            let ret = Response::Text(text_resp.contents().map_err(|_| ConversationError)?.into());
-            // SAFETY: We're the only ones using this,
-            // and we haven't freed it.
-            text_resp.free_contents();
-            ret
-        }
-        Message::ErrorMsg(_) | Message::InfoMsg(_) => Response::NoResponse,
-        Message::BinaryPrompt { .. } => {
-            let bin_resp = unsafe { RawBinaryResponse::upcast(received) };
-            let ret = Response::Binary(bin_resp.to_owned());
-            // SAFETY: We're the only ones using this,
-            // and we haven't freed it.
-            bin_resp.free_contents();
-            ret
-        }
-    })
-}
--- a/src/pam_ffi/handle.rs	Fri Jun 06 22:21:17 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,231 +0,0 @@
-use super::conversation::LibPamConversation;
-use crate::constants::{ErrorCode, InvalidEnum, Result};
-use crate::conv::Message;
-use crate::handle::{PamApplicationOnly, PamModuleOnly, PamShared};
-use crate::pam_ffi::memory;
-use crate::pam_ffi::memory::Immovable;
-use crate::{Conversation, Response};
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-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())
-    }
-
-    /// 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)
-    }
-
-    /// 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 {
-    type Target = LibPamHandle;
-    fn deref(&self) -> &Self::Target {
-        unsafe { &*self.0 }
-    }
-}
-
-impl DerefMut for OwnedLibPamHandle {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        unsafe { &mut *self.0 }
-    }
-}
-
-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.
-    fn drop(&mut self) {
-        unsafe {
-            super::pam_end(self.0, 0);
-        }
-    }
-}
-
-macro_rules! cstr_item {
-    (get = $getter:ident, item = $item_type:path) => {
-        fn $getter(&mut self) -> Result<Option<&str>> {
-            unsafe { self.get_cstr_item($item_type) }
-        }
-    };
-    (set = $setter:ident, item = $item_type:path) => {
-        fn $setter(&mut self, value: Option<&str>) -> Result<()> {
-            unsafe { self.set_cstr_item($item_type, value) }
-        }
-    };
-}
-
-impl PamShared for LibPamHandle {
-    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())) };
-        ErrorCode::result_from(ret)?;
-        unsafe { memory::wrap_string(output) }
-            .transpose()
-            .unwrap_or(Err(ErrorCode::ConversationError))
-    }
-
-    cstr_item!(get = user_item, item = ItemType::User);
-    cstr_item!(set = set_user_item, item = ItemType::User);
-    cstr_item!(get = service, item = ItemType::Service);
-    cstr_item!(set = set_service, item = ItemType::Service);
-    cstr_item!(get = user_prompt, item = ItemType::UserPrompt);
-    cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt);
-    cstr_item!(get = tty_name, item = ItemType::Tty);
-    cstr_item!(set = set_tty_name, item = ItemType::Tty);
-    cstr_item!(get = remote_user, item = ItemType::RemoteUser);
-    cstr_item!(set = set_remote_user, item = ItemType::RemoteUser);
-    cstr_item!(get = remote_host, item = ItemType::RemoteHost);
-    cstr_item!(set = set_remote_host, item = ItemType::RemoteHost);
-    cstr_item!(set = set_authtok_item, item = ItemType::AuthTok);
-    cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok);
-}
-
-impl Conversation for LibPamHandle {
-    fn communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
-        self.conversation_item()?.communicate(messages)
-    }
-}
-
-impl PamModuleOnly 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(
-                self,
-                ItemType::AuthTok.into(),
-                &mut output,
-                memory::prompt_ptr(prompt.as_ref()),
-            )
-        };
-        ErrorCode::result_from(res)?;
-        // SAFETY: We got this string from PAM.
-        unsafe { memory::wrap_string(output) }
-            .transpose()
-            .unwrap_or(Err(ErrorCode::ConversationError))
-    }
-
-    cstr_item!(get = authtok_item, item = ItemType::AuthTok);
-    cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok);
-}
-
-/// Function called at the end of a PAM session that is called to clean up
-/// a value previously provided to PAM in a `pam_set_data` call.
-///
-/// You should never call this yourself.
-extern "C" fn set_data_cleanup<T>(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) {
-    unsafe {
-        let _data: Box<T> = Box::from_raw(c_data.cast());
-    }
-}
-
-/// Identifies what is being gotten or set with `pam_get_item`
-/// or `pam_set_item`.
-#[derive(FromPrimitive)]
-#[repr(i32)]
-#[non_exhaustive] // because C could give us anything!
-pub enum ItemType {
-    /// The PAM service name.
-    Service = 1,
-    /// The user's login name.
-    User = 2,
-    /// The TTY name.
-    Tty = 3,
-    /// The remote host (if applicable).
-    RemoteHost = 4,
-    /// The conversation struct (not a CStr-based item).
-    Conversation = 5,
-    /// The authentication token (password).
-    AuthTok = 6,
-    /// The old authentication token (when changing passwords).
-    OldAuthTok = 7,
-    /// The remote user's name.
-    RemoteUser = 8,
-    /// The prompt shown when requesting a username.
-    UserPrompt = 9,
-    /// App-supplied function to override failure delays.
-    FailDelay = 10,
-    /// X display name.
-    XDisplay = 11,
-    /// X server authentication data.
-    XAuthData = 12,
-    /// 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/pam_ffi/memory.rs	Fri Jun 06 22:21:17 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,204 +0,0 @@
-//! Things for dealing with memory.
-
-use crate::ErrorCode;
-use crate::Result;
-use std::ffi::{c_char, c_void, CStr, CString};
-use std::marker::{PhantomData, PhantomPinned};
-use std::result::Result as StdResult;
-use std::{ptr, slice};
-
-/// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`].
-#[repr(C)]
-#[derive(Debug)]
-pub struct Immovable(pub PhantomData<(*mut u8, PhantomPinned)>);
-
-/// Safely converts a `&str` option to a `CString` option.
-pub fn option_cstr(prompt: Option<&str>) -> Result<Option<CString>> {
-    prompt
-        .map(CString::new)
-        .transpose()
-        .map_err(|_| ErrorCode::ConversationError)
-}
-
-/// Gets the pointer to the given CString, or a null pointer if absent.
-pub fn prompt_ptr(prompt: Option<&CString>) -> *const c_char {
-    match prompt {
-        Some(c_str) => c_str.as_ptr(),
-        None => ptr::null(),
-    }
-}
-
-/// Creates an owned copy of a string that is returned from a
-/// <code>pam_get_<var>whatever</var></code> function.
-///
-/// # Safety
-///
-/// It's on you to provide a valid string.
-pub unsafe fn copy_pam_string(result_ptr: *const libc::c_char) -> Result<String> {
-    // We really shouldn't get a null pointer back here, but if we do, return nothing.
-    if result_ptr.is_null() {
-        return Ok(String::new());
-    }
-    let bytes = unsafe { CStr::from_ptr(result_ptr) };
-    bytes
-        .to_str()
-        .map(String::from)
-        .map_err(|_| ErrorCode::ConversationError)
-}
-
-/// Wraps a string returned from PAM as an `Option<&str>`.
-pub unsafe fn wrap_string<'a>(data: *const libc::c_char) -> Result<Option<&'a str>> {
-    let ret = if data.is_null() {
-        None
-    } else {
-        Some(
-            CStr::from_ptr(data)
-                .to_str()
-                .map_err(|_| ErrorCode::ConversationError)?,
-        )
-    };
-    Ok(ret)
-}
-
-/// Allocates a string with the given contents on the C heap.
-///
-/// This is like [`CString::new`](std::ffi::CString::new), but:
-///
-/// - it allocates data on the C heap with [`libc::malloc`].
-/// - it doesn't take ownership of the data passed in.
-pub fn malloc_str(text: impl AsRef<str>) -> StdResult<*mut c_char, NulError> {
-    let data = text.as_ref().as_bytes();
-    if let Some(nul) = data.iter().position(|x| *x == 0) {
-        return Err(NulError(nul));
-    }
-    unsafe {
-        let data_alloc = libc::calloc(data.len() + 1, 1);
-        libc::memcpy(data_alloc, data.as_ptr().cast(), data.len());
-        Ok(data_alloc.cast())
-    }
-}
-
-/// Writes zeroes over the contents of a C string.
-///
-/// This won't overwrite a null pointer.
-///
-/// # Safety
-///
-/// It's up to you to provide a valid C string.
-pub unsafe fn zero_c_string(cstr: *mut c_void) {
-    if !cstr.is_null() {
-        libc::memset(cstr, 0, libc::strlen(cstr.cast()));
-    }
-}
-
-/// Binary data used in requests and responses.
-///
-/// This is an unsized data type whose memory goes beyond its data.
-/// This must be allocated on the C heap.
-///
-/// A Linux-PAM extension.
-#[repr(C)]
-pub struct CBinaryData {
-    /// The total length of the structure; a u32 in network byte order (BE).
-    total_length: [u8; 4],
-    /// A tag of undefined meaning.
-    data_type: u8,
-    /// Pointer to an array of length [`length`](Self::length) − 5
-    data: [u8; 0],
-    _marker: Immovable,
-}
-
-impl CBinaryData {
-    /// Copies the given data to a new BinaryData on the heap.
-    pub fn alloc(source: &[u8], data_type: u8) -> StdResult<*mut CBinaryData, TooBigError> {
-        let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError {
-            max: (u32::MAX - 5) as usize,
-            actual: source.len(),
-        })?;
-        // SAFETY: We're only allocating here.
-        let data = unsafe {
-            let dest_buffer: *mut CBinaryData = libc::malloc(buffer_size as usize).cast();
-            let data = &mut *dest_buffer;
-            data.total_length = buffer_size.to_be_bytes();
-            data.data_type = data_type;
-            let dest = data.data.as_mut_ptr();
-            libc::memcpy(dest.cast(), source.as_ptr().cast(), source.len());
-            dest_buffer
-        };
-        Ok(data)
-    }
-
-    fn length(&self) -> usize {
-        u32::from_be_bytes(self.total_length).saturating_sub(5) as usize
-    }
-
-    pub fn contents(&self) -> &[u8] {
-        unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) }
-    }
-    pub fn data_type(&self) -> u8 {
-        self.data_type
-    }
-
-    /// Clears this data and frees it.
-    pub unsafe fn zero_contents(&mut self) {
-        let contents = slice::from_raw_parts_mut(self.data.as_mut_ptr(), self.length());
-        for v in contents {
-            *v = 0
-        }
-        self.data_type = 0;
-        self.total_length = [0; 4];
-    }
-}
-
-#[derive(Debug, thiserror::Error)]
-#[error("null byte within input at byte {0}")]
-pub struct NulError(pub usize);
-
-/// Returned when trying to fit too much data into a binary message.
-#[derive(Debug, thiserror::Error)]
-#[error("cannot create a message of {actual} bytes; maximum is {max}")]
-pub struct TooBigError {
-    pub actual: usize,
-    pub max: usize,
-}
-
-#[cfg(test)]
-mod tests {
-    use super::{copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string};
-    use crate::ErrorCode;
-    use std::ffi::CString;
-    #[test]
-    fn test_strings() {
-        let str = malloc_str("hello there").unwrap();
-        malloc_str("hell\0 there").unwrap_err();
-        unsafe {
-            let copied = copy_pam_string(str.cast()).unwrap();
-            assert_eq!("hello there", copied);
-            zero_c_string(str.cast());
-            let idx_three = str.add(3).as_mut().unwrap();
-            *idx_three = 0x80u8 as i8;
-            let zeroed = copy_pam_string(str.cast()).unwrap();
-            assert!(zeroed.is_empty());
-            libc::free(str.cast());
-        }
-    }
-
-    #[test]
-    fn test_option_str() {
-        let good = option_cstr(Some("whatever")).unwrap();
-        assert_eq!("whatever", good.unwrap().to_str().unwrap());
-        let no_str = option_cstr(None).unwrap();
-        assert!(no_str.is_none());
-        let bad_str = option_cstr(Some("what\0ever")).unwrap_err();
-        assert_eq!(ErrorCode::ConversationError, bad_str);
-    }
-
-    #[test]
-    fn test_prompt() {
-        let prompt_cstr = CString::new("good").ok();
-        let prompt = prompt_ptr(prompt_cstr.as_ref());
-        assert!(!prompt.is_null());
-        let no_prompt = prompt_ptr(None);
-        assert!(no_prompt.is_null());
-    }
-}
--- a/src/pam_ffi/message.rs	Fri Jun 06 22:21:17 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,334 +0,0 @@
-//! Data and types dealing with PAM messages.
-
-use crate::constants::InvalidEnum;
-use crate::conv::Message;
-use crate::pam_ffi::memory;
-use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError};
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-use std::ffi::{c_int, c_void, CStr};
-use std::result::Result as StdResult;
-use std::str::Utf8Error;
-use std::{ptr, slice};
-
-#[derive(Debug, thiserror::Error)]
-#[error("error creating PAM message: {0}")]
-pub enum ConversionError {
-    InvalidEnum(#[from] InvalidEnum<Style>),
-    Utf8Error(#[from] Utf8Error),
-    NulError(#[from] NulError),
-    TooBigError(#[from] TooBigError),
-}
-
-/// 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 From<Style> for c_int {
-    fn from(val: Style) -> Self {
-        val as Self
-    }
-}
-
-/// A message sent by PAM or a module to an application.
-/// This message, and its internal data, is owned by the creator
-/// (either the module or PAM itself).
-#[repr(C)]
-pub struct RawMessage {
-    /// 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 RawMessage {
-    pub fn set(&mut self, msg: Message) -> StdResult<(), ConversionError> {
-        let (style, data) = copy_to_heap(msg)?;
-        self.clear();
-        // SAFETY: We allocated this ourselves or were given it by PAM.
-        // Otherwise, it's null, but free(null) is fine.
-        unsafe { libc::free(self.data) };
-        self.style = style as c_int;
-        self.data = data;
-        Ok(())
-    }
-
-    /// Gets this message's data pointer as a string.
-    ///
-    /// # Safety
-    ///
-    /// It's up to you to pass this only on types with a string value.
-    unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> {
-        if self.data.is_null() {
-            Ok("")
-        } else {
-            CStr::from_ptr(self.data.cast()).to_str()
-        }
-    }
-
-    /// Zeroes out the data stored here.
-    fn clear(&mut self) {
-        // SAFETY: We either created this data or we got it from PAM.
-        // After this function is done, it will be zeroed out.
-        unsafe {
-            if let Ok(style) = Style::try_from(self.style) {
-                match style {
-                    Style::BinaryPrompt => {
-                        if let Some(d) = self.data.cast::<CBinaryData>().as_mut() {
-                            d.zero_contents()
-                        }
-                    }
-                    Style::TextInfo
-                    | Style::RadioType
-                    | Style::ErrorMsg
-                    | Style::PromptEchoOff
-                    | Style::PromptEchoOn => memory::zero_c_string(self.data),
-                }
-            };
-            libc::free(self.data);
-            self.data = ptr::null_mut();
-        }
-    }
-}
-
-/// Copies the contents of this message to the C heap.
-fn copy_to_heap(msg: Message) -> StdResult<(Style, *mut c_void), ConversionError> {
-    let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
-    match msg {
-        Message::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text),
-        Message::Prompt(text) => alloc(Style::PromptEchoOn, text),
-        Message::RadioPrompt(text) => alloc(Style::RadioType, text),
-        Message::ErrorMsg(text) => alloc(Style::ErrorMsg, text),
-        Message::InfoMsg(text) => alloc(Style::TextInfo, text),
-        Message::BinaryPrompt { data, data_type } => Ok((
-            Style::BinaryPrompt,
-            (CBinaryData::alloc(data, data_type)?).cast(),
-        )),
-    }
-}
-
-/// Abstraction of a list-of-messages to be sent in a PAM conversation.
-///
-/// On Linux-PAM and other compatible implementations, `messages`
-/// is treated as a pointer-to-pointers, like `int argc, char **argv`.
-/// (In this situation, the value of `OwnedMessages.indirect` is
-/// the pointer passed to `pam_conv`.)
-///
-/// ```text
-/// ╔═ OwnedMsgs ═╗  points to  ┌─ Indirect ─┐       ╔═ Message ═╗
-/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style     ║
-/// ║ count       ║             │ base[1] ┄┄┄┼┄┄┄╮   ║ data      ║
-/// ╚═════════════╝             │ ...        │   ┆   ╚═══════════╝
-///                                              ┆
-///                                              ┆    ╔═ Message ═╗
-///                                              ╰┄┄> ║ 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
-/// ╔═ OwnedMsgs ═╗  points to  ┌─ Indirect ─┐       ╔═ Message[] ═╗
-/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style       ║
-/// ║ count       ║             └────────────┘       ║ data        ║
-/// ╚═════════════╝                                  ╟─────────────╢
-///                                                  ║ style       ║
-///                                                  ║ data        ║
-///                                                  ╟─────────────╢
-///                                                  ║ ...         ║
-/// ```
-///
-/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
-pub struct OwnedMessages {
-    /// An indirection to the messages themselves, stored on the C heap.
-    indirect: *mut MessageIndirector,
-    /// The number of messages in the list.
-    count: usize,
-}
-
-impl OwnedMessages {
-    /// Allocates data to store messages on the C heap.
-    pub fn alloc(count: usize) -> Self {
-        Self {
-            indirect: MessageIndirector::alloc(count),
-            count,
-        }
-    }
-
-    /// The pointer to the thing with the actual list.
-    pub fn indirector(&self) -> *const MessageIndirector {
-        self.indirect
-    }
-
-    pub fn iter(&self) -> impl Iterator<Item = &RawMessage> {
-        // SAFETY: we're iterating over an amount we know.
-        unsafe { (*self.indirect).iter(self.count) }
-    }
-
-    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut RawMessage> {
-        // SAFETY: we're iterating over an amount we know.
-        unsafe { (*self.indirect).iter_mut(self.count) }
-    }
-}
-
-impl Drop for OwnedMessages {
-    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)
-            }
-            libc::free(self.indirect.cast());
-            self.indirect = ptr::null_mut();
-        }
-    }
-}
-
-/// 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.
-#[repr(transparent)]
-pub struct MessageIndirector {
-    base: [*mut RawMessage; 0],
-    _marker: Immovable,
-}
-
-impl MessageIndirector {
-    /// Allocates memory for this indirector and all its members.
-    fn alloc(count: usize) -> *mut Self {
-        // SAFETY: We're only allocating, and when we're done,
-        // everything will be in a known-good state.
-        unsafe {
-            let me_ptr: *mut MessageIndirector =
-                libc::calloc(count, size_of::<*mut RawMessage>()).cast();
-            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 = libc::calloc(1, size_of::<RawMessage>()).cast();
-            }
-            me
-        }
-    }
-
-    /// 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 = &RawMessage> {
-        (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 RawMessage> {
-        (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx))
-    }
-
-    /// Frees this and everything it points to.
-    ///
-    /// # Safety
-    ///
-    /// You have to pass the right size.
-    unsafe fn free(&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() {
-                msg.clear();
-            }
-            libc::free(msg.cast());
-            *msg = ptr::null_mut();
-        }
-    }
-}
-
-impl<'a> TryFrom<&'a RawMessage> for Message<'a> {
-    type Error = ConversionError;
-
-    /// Retrieves the data stored in this message.
-    fn try_from(input: &RawMessage) -> StdResult<Message, ConversionError> {
-        let style: Style = input.style.try_into()?;
-        // SAFETY: We either allocated this message ourselves or were provided it by PAM.
-        let result = unsafe {
-            match style {
-                Style::PromptEchoOff => Message::MaskedPrompt(input.string_data()?),
-                Style::PromptEchoOn => Message::Prompt(input.string_data()?),
-                Style::TextInfo => Message::InfoMsg(input.string_data()?),
-                Style::ErrorMsg => Message::ErrorMsg(input.string_data()?),
-                Style::RadioType => Message::ErrorMsg(input.string_data()?),
-                Style::BinaryPrompt => input.data.cast::<CBinaryData>().as_ref().map_or_else(
-                    || Message::BinaryPrompt {
-                        data_type: 0,
-                        data: &[],
-                    },
-                    |data| Message::BinaryPrompt {
-                        data_type: data.data_type(),
-                        data: data.contents(),
-                    },
-                ),
-            }
-        };
-        Ok(result)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::conv::Message;
-    use crate::pam_ffi::message::OwnedMessages;
-
-    #[test]
-    fn test_owned_messages() {
-        let mut tons_of_messages = OwnedMessages::alloc(10);
-        let mut msgs: Vec<_> = tons_of_messages.iter_mut().collect();
-        assert!(msgs.get(10).is_none());
-        let last_msg = &mut msgs[9];
-        last_msg.set(Message::MaskedPrompt("hocus pocus")).unwrap();
-        let another_msg = &mut msgs[0];
-        another_msg
-            .set(Message::BinaryPrompt {
-                data: &[5, 4, 3, 2, 1],
-                data_type: 99,
-            })
-            .unwrap();
-        let overwrite = &mut msgs[3];
-        overwrite.set(Message::Prompt("what")).unwrap();
-        overwrite.set(Message::Prompt("who")).unwrap();
-    }
-}
--- a/src/pam_ffi/mod.rs	Fri Jun 06 22:21:17 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-//! The PAM library FFI and helpers for managing it.
-//!
-//! This includes the functions provided by PAM and the data structures
-//! used by PAM, as well as a few low-level abstractions for dealing with
-//! those data structures.
-//!
-//! Everything in here is hazmat.
-//!
-
-#![allow(dead_code)]
-
-mod conversation;
-mod handle;
-mod memory;
-mod message;
-mod module;
-mod response;
-
-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
-}
--- a/src/pam_ffi/module.rs	Fri Jun 06 22:21:17 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-use std::ffi::CStr;
-
-/// Generates the dynamic library entry points for a [PamModule] implementation.
-///
-/// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will
-/// generate the exported `extern "C"` functions that PAM uses to call into
-/// your module.
-///
-/// ## Examples:
-///
-/// Here is full example of a PAM module that would authenticate and authorize everybody:
-///
-/// ```
-/// use nonstick::{Flags, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks};
-/// use std::ffi::CStr;
-/// # fn main() {}
-///
-/// struct MyPamModule;
-/// pam_hooks!(MyPamModule);
-///
-/// impl<T: PamHandleModule> PamModule<T> for MyPamModule {
-///     fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
-///         let password = handle.get_authtok(Some("what's your password?"))?;
-///         handle.info_msg(fmt!("If you say your password is {password:?}, who am I to disagree?"));
-///     }
-///
-///     fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
-///         let username = handle.get_user(None)?;
-///         handle.info_msg(fmt!("Hello {username}! I trust you unconditionally."))
-///         Ok(())
-///     }
-/// }
-/// ```
-#[macro_export]
-macro_rules! pam_hooks {
-    ($ident:ident) => {
-        mod _pam_hooks_scope {
-            use std::ffi::{c_char, c_int, CStr};
-            use $crate::{ErrorCode, Flags, LibPamHandle, PamModule};
-
-            #[no_mangle]
-            extern "C" fn pam_sm_acct_mgmt(
-                pamh: *mut libc::c_void,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    let args = extract_argv(argc, argv);
-                    ErrorCode::result_to_c(super::$ident::account_management(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_authenticate(
-                pamh: *mut libc::c_void,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    let args = extract_argv(argc, argv);
-                    ErrorCode::result_to_c(super::$ident::authenticate(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_chauthtok(
-                pamh: *mut libc::c_void,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    let args = extract_argv(argc, argv);
-                    ErrorCode::result_to_c(super::$ident::change_authtok(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_close_session(
-                pamh: *mut libc::c_void,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    let args = extract_argv(argc, argv);
-                    ErrorCode::result_to_c(super::$ident::close_session(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_open_session(
-                pamh: *mut libc::c_void,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let args = extract_argv(argc, argv);
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    ErrorCode::result_to_c(super::$ident::open_session(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_setcred(
-                pamh: *mut libc::c_void,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let args = extract_argv(argc, argv);
-                if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } {
-                    ErrorCode::result_to_c(super::$ident::set_credentials(handle, args, flags))
-                } else {
-                    ErrorCode::Ignore as c_int
-                }
-            }
-
-            /// Turns `argc`/`argv` into a [Vec] of [CStr]s.
-            ///
-            /// # Safety
-            ///
-            /// We use this only with arguments we get from `libpam`, which we kind of have to trust.
-            fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
-                (0..argc)
-                    .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) })
-                    .collect()
-            }
-        }
-    };
-}
-
-#[cfg(test)]
-mod tests {
-    // Compile-time test that the `pam_hooks` macro compiles.
-    use crate::{PamHandleModule, PamModule};
-    struct Foo;
-    impl<T: PamHandleModule> PamModule<T> for Foo {}
-
-    pam_hooks!(Foo);
-}
--- a/src/pam_ffi/response.rs	Fri Jun 06 22:21:17 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,317 +0,0 @@
-//! Types used when dealing with PAM conversations.
-
-use crate::conv::BinaryData;
-use crate::pam_ffi::memory;
-use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError};
-use crate::Response;
-use std::ffi::{c_int, c_void, CStr};
-use std::ops::{Deref, DerefMut};
-use std::result::Result as StdResult;
-use std::str::Utf8Error;
-use std::{iter, mem, ptr, slice};
-
-#[repr(transparent)]
-#[derive(Debug)]
-pub struct RawTextResponse(RawResponse);
-
-impl RawTextResponse {
-    /// Interprets the provided `RawResponse` as a text response.
-    ///
-    /// # Safety
-    ///
-    /// It's up to you to provide a response that is a `RawTextResponse`.
-    pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self {
-        // SAFETY: We're provided a valid reference.
-        &mut *(from as *mut RawResponse).cast::<Self>()
-    }
-
-    /// Fills in the provided `RawResponse` with the given text.
-    ///
-    /// You are responsible for calling [`free`](Self::free_contents)
-    /// on the pointer you get back when you're done with it.
-    pub fn fill(dest: &mut RawResponse, text: impl AsRef<str>) -> StdResult<&mut Self, NulError> {
-        dest.data = memory::malloc_str(text)?.cast();
-        // SAFETY: We just filled this in so we know it's a text response.
-        Ok(unsafe { Self::upcast(dest) })
-    }
-
-    /// Gets the string stored in this response.
-    pub fn contents(&self) -> StdResult<&str, Utf8Error> {
-        if self.0.data.is_null() {
-            Ok("")
-        } else {
-            // SAFETY: This data is either passed from PAM (so we are forced to
-            // trust it) or was created by us in TextResponseInner::alloc.
-            // In either case, it's going to be a valid null-terminated string.
-            unsafe { CStr::from_ptr(self.0.data.cast()) }.to_str()
-        }
-    }
-
-    /// Releases memory owned by this response.
-    pub fn free_contents(&mut self) {
-        // SAFETY: We know we own this data.
-        // After we're done, it will be null.
-        unsafe {
-            memory::zero_c_string(self.0.data);
-            libc::free(self.0.data);
-            self.0.data = ptr::null_mut()
-        }
-    }
-}
-
-/// A [`RawResponse`] with [`CBinaryData`] in it.
-#[repr(transparent)]
-#[derive(Debug)]
-pub struct RawBinaryResponse(RawResponse);
-
-impl RawBinaryResponse {
-    /// Interprets the provided `RawResponse` as a binary response.
-    ///
-    /// # Safety
-    ///
-    /// It's up to you to provide a response that is a `RawBinaryResponse`.
-    pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self {
-        // SAFETY: We're provided a valid reference.
-        &mut *(from as *mut RawResponse).cast::<Self>()
-    }
-
-    /// Fills in a `RawResponse` with the provided binary data.
-    ///
-    /// The `data_type` is a tag you can use for whatever.
-    /// It is passed through PAM unchanged.
-    ///
-    /// The referenced data is copied to the C heap. We do not take ownership.
-    /// You are responsible for calling [`free`](Self::free_contents)
-    /// on the pointer you get back when you're done with it.
-    pub fn fill<'a>(
-        dest: &'a mut RawResponse,
-        data: &[u8],
-        data_type: u8,
-    ) -> StdResult<&'a mut Self, TooBigError> {
-        dest.data = CBinaryData::alloc(data, data_type)?.cast();
-        // SAFETY: We just filled this in, so we know it's binary.
-        Ok(unsafe { Self::upcast(dest) })
-    }
-
-    /// Gets the binary data in this response.
-    pub fn data(&self) -> &[u8] {
-        self.contents().map(CBinaryData::contents).unwrap_or(&[])
-    }
-
-    /// Gets the `data_type` tag that was embedded with the message.
-    pub fn data_type(&self) -> u8 {
-        self.contents().map(CBinaryData::data_type).unwrap_or(0)
-    }
-
-    fn contents(&self) -> Option<&CBinaryData> {
-        // SAFETY: This was either something we got from PAM (in which case
-        // we trust it), or something that was created with
-        // BinaryResponseInner::alloc. In both cases, it points to valid data.
-        unsafe { self.0.data.cast::<CBinaryData>().as_ref() }
-    }
-
-    pub fn to_owned(&self) -> BinaryData {
-        BinaryData::new(self.data().into(), self.data_type())
-    }
-
-    /// Releases memory owned by this response.
-    pub fn free_contents(&mut self) {
-        // SAFETY: We know that our data pointer is either valid or null.
-        // Once we're done, it's null and the response is safe.
-        unsafe {
-            let data_ref = self.0.data.cast::<CBinaryData>().as_mut();
-            if let Some(d) = data_ref {
-                d.zero_contents()
-            }
-            libc::free(self.0.data);
-            self.0.data = ptr::null_mut()
-        }
-    }
-}
-
-/// Generic version of response data.
-///
-/// This has the same structure as [`RawBinaryResponse`]
-/// and [`RawTextResponse`].
-#[repr(C)]
-#[derive(Debug)]
-pub struct RawResponse {
-    /// Pointer to the data returned in a response.
-    /// For most responses, this will be a [`CStr`], but for responses to
-    /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`]
-    /// (a Linux-PAM extension).
-    data: *mut c_void,
-    /// Unused.
-    return_code: c_int,
-    _marker: Immovable,
-}
-
-/// A contiguous block of responses.
-#[derive(Debug)]
-pub struct OwnedResponses {
-    base: *mut RawResponse,
-    count: usize,
-}
-
-impl OwnedResponses {
-    /// Allocates an owned list of responses on the C heap.
-    fn alloc(count: usize) -> Self {
-        OwnedResponses {
-            // SAFETY: We are doing allocation here.
-            base: unsafe { libc::calloc(count, size_of::<RawResponse>()) }.cast(),
-            count,
-        }
-    }
-
-    pub fn build(value: &[Response]) -> StdResult<Self, FillError> {
-        let mut outputs = OwnedResponses::alloc(value.len());
-        // If we fail in here after allocating OwnedResponses,
-        // we still free all memory, even though we don't zero it first.
-        // This is an acceptable level of risk.
-        for (input, output) in iter::zip(value.iter(), outputs.iter_mut()) {
-            match input {
-                Response::NoResponse => {
-                    RawTextResponse::fill(output, "")?;
-                }
-                Response::Text(data) => {
-                    RawTextResponse::fill(output, data)?;
-                }
-                Response::MaskedText(data) => {
-                    RawTextResponse::fill(output, data.unsecure())?;
-                }
-                Response::Binary(data) => {
-                    RawBinaryResponse::fill(output, data.data(), data.data_type())?;
-                }
-            }
-        }
-        Ok(outputs)
-    }
-
-    /// Converts this into a `*RawResponse` for passing to PAM.
-    ///
-    /// The pointer "owns" its own data (i.e., this will not be dropped).
-    pub fn into_ptr(self) -> *mut RawResponse {
-        let ret = self.base;
-        mem::forget(self);
-        ret
-    }
-
-    /// Takes ownership of a list of responses allocated on the C heap.
-    ///
-    /// # Safety
-    ///
-    /// It's up to you to make sure you pass a valid pointer.
-    pub unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self {
-        OwnedResponses { base, count }
-    }
-}
-
-#[derive(Debug, thiserror::Error)]
-#[error("error converting responses: {0}")]
-pub enum FillError {
-    NulError(#[from] NulError),
-    TooBigError(#[from] TooBigError),
-}
-
-impl Deref for OwnedResponses {
-    type Target = [RawResponse];
-    fn deref(&self) -> &Self::Target {
-        // SAFETY: This is the memory we manage ourselves.
-        unsafe { slice::from_raw_parts(self.base, self.count) }
-    }
-}
-
-impl DerefMut for OwnedResponses {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        // SAFETY: This is the memory we manage ourselves.
-        unsafe { slice::from_raw_parts_mut(self.base, self.count) }
-    }
-}
-
-impl Drop for OwnedResponses {
-    fn drop(&mut self) {
-        // SAFETY: We allocated this ourselves, or it was provided to us by PAM.
-        unsafe {
-            for resp in self.iter_mut() {
-                libc::free(resp.data)
-            }
-            libc::free(self.base.cast())
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::{BinaryData, OwnedResponses, RawBinaryResponse, RawTextResponse, Response};
-
-    #[test]
-    fn test_round_trip() {
-        let responses = [
-            Response::Binary(BinaryData::new(vec![1, 2, 3], 99)),
-            Response::Text("whats going on".to_owned()),
-            Response::MaskedText("well then".into()),
-            Response::NoResponse,
-            Response::Text("bogus".to_owned()),
-        ];
-        let sent = OwnedResponses::build(&responses).unwrap();
-        let heap_resps = sent.into_ptr();
-        let mut received = unsafe { OwnedResponses::from_c_heap(heap_resps, 5) };
-
-        let assert_text = |want, raw| {
-            let up = unsafe { RawTextResponse::upcast(raw) };
-            assert_eq!(want, up.contents().unwrap());
-            up.free_contents();
-            assert_eq!("", up.contents().unwrap());
-        };
-        let assert_bin = |want_data: &[u8], want_type, raw| {
-            let up = unsafe { RawBinaryResponse::upcast(raw) };
-            assert_eq!(want_data, up.data());
-            assert_eq!(want_type, up.data_type());
-            up.free_contents();
-            let empty: [u8; 0] = [];
-            assert_eq!(&empty, up.data());
-            assert_eq!(0, up.data_type());
-        };
-        if let [zero, one, two, three, four] = &mut received[..] {
-            assert_bin(&[1, 2, 3], 99, zero);
-            assert_text("whats going on", one);
-            assert_text("well then", two);
-            assert_text("", three);
-            assert_text("bogus", four);
-        } else {
-            panic!("wrong size!")
-        }
-    }
-
-    #[test]
-    fn test_text_response() {
-        let mut responses = OwnedResponses::alloc(2);
-        let text = RawTextResponse::fill(&mut responses[0], "hello").unwrap();
-        let data = text.contents().expect("valid");
-        assert_eq!("hello", data);
-        text.free_contents();
-        text.free_contents();
-        RawTextResponse::fill(&mut responses[1], "hell\0").expect_err("should error; contains nul");
-    }
-
-    #[test]
-    fn test_binary_response() {
-        let mut responses = OwnedResponses::alloc(1);
-        let real_data = [1, 2, 3, 4, 5, 6, 7, 8];
-        let resp = RawBinaryResponse::fill(&mut responses[0], &real_data, 7)
-            .expect("alloc should succeed");
-        let data = resp.data();
-        assert_eq!(&real_data, data);
-        assert_eq!(7, resp.data_type());
-        resp.free_contents();
-        resp.free_contents();
-    }
-
-    #[test]
-    #[ignore]
-    fn test_binary_response_too_big() {
-        let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000];
-        let mut responses = OwnedResponses::alloc(1);
-        RawBinaryResponse::fill(&mut responses[0], &big_data, 0).expect_err("this is too big!");
-    }
-}