changeset 71:58f9d2a4df38

Reorganize everything again??? - Splits ffi/memory stuff into a bunch of stuff in the pam_ffi module. - Builds infrastructure for passing Messages and Responses. - Adds tests for some things at least.
author Paul Fisher <paul@pfish.zone>
date Tue, 03 Jun 2025 21:54:58 -0400
parents 9f8381a1c09c
children 47eb242a4f88
files src/constants.rs src/conv.rs src/handle.rs src/lib.rs src/memory.rs src/module.rs src/pam_ffi.rs src/pam_ffi/memory.rs src/pam_ffi/message.rs src/pam_ffi/mod.rs src/pam_ffi/response.rs
diffstat 11 files changed, 862 insertions(+), 727 deletions(-) [+]
line wrap: on
line diff
--- a/src/constants.rs	Tue Jun 03 01:21:59 2025 -0400
+++ b/src/constants.rs	Tue Jun 03 21:54:58 2025 -0400
@@ -6,6 +6,7 @@
 use num_traits::FromPrimitive;
 use std::any;
 use std::marker::PhantomData;
+use std::result::Result as StdResult;
 
 bitflags! {
     /// The available PAM flags.
@@ -137,7 +138,7 @@
 }
 
 /// A PAM-specific Result type with an [ErrorCode] error.
-pub type Result<T> = std::result::Result<T, ErrorCode>;
+pub type Result<T> = StdResult<T, ErrorCode>;
 
 impl ErrorCode {
     /// Converts this [Result] into a C-compatible result code.
@@ -161,7 +162,7 @@
 impl TryFrom<c_int> for ErrorCode {
     type Error = InvalidEnum<Self>;
 
-    fn try_from(value: c_int) -> std::result::Result<Self, Self::Error> {
+    fn try_from(value: c_int) -> StdResult<Self, Self::Error> {
         Self::from_i32(value).ok_or(value.into())
     }
 }
@@ -193,18 +194,6 @@
 /// Returned when text that should not have any `\0` bytes in it does.
 /// Analogous to [`std::ffi::NulError`], but the data it was created from
 /// is borrowed.
-#[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::*;
--- a/src/conv.rs	Tue Jun 03 01:21:59 2025 -0400
+++ b/src/conv.rs	Tue Jun 03 21:54:58 2025 -0400
@@ -3,13 +3,9 @@
 // Temporarily allowed until we get the actual conversation functions hooked up.
 #![allow(dead_code)]
 
-use crate::constants::{NulError, Result, TooBigError};
-use crate::pam_ffi::{BinaryResponseInner, GenericResponse, TextResponseInner};
+use crate::constants::Result;
+use crate::pam_ffi::Message;
 use secure_string::SecureString;
-use std::mem;
-use std::result::Result as StdResult;
-use std::str::Utf8Error;
-
 // TODO: In most cases, we should be passing around references to strings
 // or binary data. Right now we don't because that turns type inference and
 // trait definitions/implementations into a HUGE MESS.
@@ -18,47 +14,6 @@
 // associated types in the various Conversation traits to avoid copying
 // when unnecessary.
 
-/// The types of message and request that can be sent to a user.
-///
-/// The data within each enum value is the prompt (or other information)
-/// that will be presented to the user.
-#[derive(Debug)]
-pub enum Message<'a> {
-    /// Requests information from the user; will be masked when typing.
-    ///
-    /// Response: [`Response::MaskedText`]
-    MaskedPrompt(&'a str),
-    /// Requests information from the user; will not be masked.
-    ///
-    /// Response: [`Response::Text`]
-    Prompt(&'a str),
-    /// "Yes/No/Maybe conditionals" (a Linux-PAM extension).
-    ///
-    /// Response: [`Response::Text`]
-    /// (Linux-PAM documentation doesn't define its contents.)
-    RadioPrompt(&'a str),
-    /// Raises an error message to the user.
-    ///
-    /// Response: [`Response::NoResponse`]
-    Error(&'a str),
-    /// Sends an informational message to the user.
-    ///
-    /// Response: [`Response::NoResponse`]
-    Info(&'a str),
-    /// Requests binary data from the client (a Linux-PAM extension).
-    ///
-    /// This is used for non-human or non-keyboard prompts (security key?).
-    /// NOT part of the X/Open PAM specification.
-    ///
-    /// Response: [`Response::Binary`]
-    BinaryPrompt {
-        /// Some binary data.
-        data: &'a [u8],
-        /// A "type" that you can use for signalling. Has no strict definition in PAM.
-        data_type: u8,
-    },
-}
-
 /// The responses that PAM will return from a request.
 #[derive(Debug, PartialEq, derive_more::From)]
 pub enum Response {
@@ -103,9 +58,6 @@
     ///
     /// The returned Vec of messages always contains exactly as many entries
     /// as there were messages in the request; one corresponding to each.
-    ///
-    /// Messages with no response (e.g. [info](Message::Info) and
-    /// [error](Message::Error)) will have a `None` entry instead of a `Response`.
     fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>>;
 }
 
@@ -152,84 +104,6 @@
     }
 }
 
-/// An owned text response to a PAM conversation.
-///
-/// It points to a value on the C heap.
-#[repr(C)]
-struct TextResponse(*mut TextResponseInner);
-
-impl TextResponse {
-    /// Allocates a new response with the given text.
-    ///
-    /// A copy of the provided text will be allocated on the C heap.
-    pub fn new(text: impl AsRef<str>) -> StdResult<Self, NulError> {
-        TextResponseInner::alloc(text).map(Self)
-    }
-
-    /// Converts this into a GenericResponse.
-    fn generic(self) -> *mut GenericResponse {
-        let ret = self.0 as *mut GenericResponse;
-        mem::forget(self);
-        ret
-    }
-
-    /// Gets the string data, if possible.
-    pub fn as_str(&self) -> StdResult<&str, Utf8Error> {
-        // SAFETY: We allocated this ourselves or got it back from PAM.
-        unsafe { &*self.0 }.contents().to_str()
-    }
-}
-
-impl Drop for TextResponse {
-    /// Frees an owned response.
-    fn drop(&mut self) {
-        // SAFETY: We allocated this ourselves, or it was provided by PAM.
-        unsafe { TextResponseInner::free(self.0) }
-    }
-}
-
-/// An owned binary response to a PAM conversation.
-///
-/// It points to a value on the C heap.
-#[repr(C)]
-pub struct BinaryResponse(pub(super) *mut BinaryResponseInner);
-
-impl BinaryResponse {
-    /// Creates a binary response with the given data.
-    ///
-    /// A copy of the data will be made and allocated on the C heap.
-    pub fn new(data: &[u8], data_type: u8) -> StdResult<Self, TooBigError> {
-        BinaryResponseInner::alloc(data, data_type).map(Self)
-    }
-
-    /// Converts this into a GenericResponse.
-    fn generic(self) -> *mut GenericResponse {
-        let ret = self.0 as *mut GenericResponse;
-        mem::forget(self);
-        ret
-    }
-
-    /// The data type we point to.
-    pub fn data_type(&self) -> u8 {
-        // SAFETY: We allocated this ourselves or got it back from PAM.
-        unsafe { &*self.0 }.data_type()
-    }
-
-    /// The data we point to.
-    pub fn data(&self) -> &[u8] {
-        // SAFETY: We allocated this ourselves or got it back from PAM.
-        unsafe { &*self.0 }.contents()
-    }
-}
-
-impl Drop for BinaryResponse {
-    /// Frees an owned response.
-    fn drop(&mut self) {
-        // SAFETY: We allocated this ourselves, or it was provided by PAM.
-        unsafe { BinaryResponseInner::free(self.0) }
-    }
-}
-
 /// Owned binary data.
 #[derive(Debug, PartialEq)]
 pub struct BinaryData {
@@ -249,16 +123,6 @@
     }
 }
 
-impl From<BinaryResponse> for BinaryData {
-    /// Copies the data onto the Rust heap.
-    fn from(value: BinaryResponse) -> Self {
-        Self {
-            data: value.data().to_vec(),
-            data_type: value.data_type(),
-        }
-    }
-}
-
 impl From<BinaryData> for Vec<u8> {
     /// Extracts the inner vector from the BinaryData.
     fn from(value: BinaryData) -> Self {
@@ -267,13 +131,9 @@
 }
 
 #[cfg(test)]
-mod test {
-    use super::{
-        BinaryResponse, Conversation, DemuxedConversation, Message, Response, SecureString,
-        TextResponse,
-    };
+mod tests {
+    use super::{Conversation, DemuxedConversation, Message, Response, SecureString};
     use crate::constants::ErrorCode;
-    use crate::pam_ffi::GenericResponse;
 
     #[test]
     fn test_demux() {
@@ -362,33 +222,4 @@
                 .unwrap()
         );
     }
-
-    // The below tests are used in conjunction with ASAN to verify
-    // that we correctly clean up all our memory.
-
-    #[test]
-    fn test_text_response() {
-        let resp = TextResponse::new("it's a-me!").unwrap();
-        assert_eq!("it's a-me!", resp.as_str().unwrap());
-    }
-
-    #[test]
-    fn test_binary_response() {
-        let data = [123, 210, 55];
-        let resp = BinaryResponse::new(&data, 99).unwrap();
-        assert_eq!(data, resp.data());
-        assert_eq!(99, resp.data_type());
-    }
-
-    #[test]
-    fn test_to_generic() {
-        let text = TextResponse::new("oh no").unwrap();
-        let text = text.generic();
-        let binary = BinaryResponse::new(&[], 33).unwrap();
-        let binary = binary.generic();
-        unsafe {
-            GenericResponse::free(text);
-            GenericResponse::free(binary);
-        }
-    }
 }
--- a/src/handle.rs	Tue Jun 03 01:21:59 2025 -0400
+++ b/src/handle.rs	Tue Jun 03 21:54:58 2025 -0400
@@ -1,11 +1,11 @@
 //! The wrapper types and traits for handles into the PAM library.
 use crate::constants::{ErrorCode, Result};
 use crate::items::{Item, ItemType};
-use crate::{memory, pam_ffi};
-use libc::c_char;
+use crate::pam_ffi;
+use crate::pam_ffi::memory;
 use secure_string::SecureString;
-use std::ffi::{c_int, CString};
-use std::mem;
+use std::ffi::{c_char, c_int, c_void, CString};
+use std::{mem, ptr};
 
 /// Features of a PAM handle that are available to applications and modules.
 ///
@@ -178,17 +178,6 @@
 #[repr(C)]
 pub struct LibPamHandle(pam_ffi::Handle);
 
-impl LibPamHandle {
-    /// Converts a pointer passed from PAM into a borrowed handle.
-    ///
-    /// # Safety
-    ///
-    /// It is your responsibility to provide a valid pointer.
-    pub unsafe fn from_ptr<'a>(ptr: *mut libc::c_void) -> &'a mut LibPamHandle {
-        &mut *(ptr as *mut LibPamHandle)
-    }
-}
-
 impl Drop for LibPamHandle {
     /// Ends the PAM session with a zero error code.
     /// You probably want to call [`close`](Self::close) instead of
@@ -203,17 +192,17 @@
 impl PamHandle for LibPamHandle {
     fn get_user(&mut self, prompt: Option<&str>) -> crate::Result<String> {
         let prompt = memory::option_cstr(prompt)?;
-        let mut output: *const c_char = std::ptr::null_mut();
+        let mut output: *const c_char = ptr::null_mut();
         let ret = unsafe {
             pam_ffi::pam_get_user(&self.0, &mut output, memory::prompt_ptr(prompt.as_ref()))
         };
         ErrorCode::result_from(ret)?;
-        memory::copy_pam_string(output)
+        unsafe {memory::copy_pam_string(output)}
     }
 
     fn get_authtok(&mut self, prompt: Option<&str>) -> crate::Result<SecureString> {
         let prompt = memory::option_cstr(prompt)?;
-        let mut output: *const c_char = std::ptr::null_mut();
+        let mut output: *const c_char = ptr::null_mut();
         let res = unsafe {
             pam_ffi::pam_get_authtok(
                 &self.0,
@@ -223,24 +212,20 @@
             )
         };
         ErrorCode::result_from(res)?;
-        memory::copy_pam_string(output).map(SecureString::from)
+        unsafe {memory::copy_pam_string(output)}.map(SecureString::from)
     }
 
-    fn get_item<T: Item>(&mut self) -> crate::Result<Option<T>> {
-        let mut ptr: *const libc::c_void = std::ptr::null();
+    fn get_item<T: Item>(&mut self) -> Result<Option<T>> {
+        let mut ptr: *const c_void = ptr::null();
         let out = unsafe {
             let ret = pam_ffi::pam_get_item(&self.0, T::type_id().into(), &mut ptr);
             ErrorCode::result_from(ret)?;
-            let typed_ptr: *const T::Raw = ptr.cast();
-            match typed_ptr.is_null() {
-                true => None,
-                false => Some(T::from_raw(typed_ptr)),
-            }
+            (ptr as *const T::Raw).as_ref().map(|p| T::from_raw(p))
         };
         Ok(out)
     }
 
-    fn set_item<T: Item>(&mut self, item: T) -> crate::Result<()> {
+    fn set_item<T: Item>(&mut self, item: T) -> Result<()> {
         let ret = unsafe {
             pam_ffi::pam_set_item(&mut self.0, T::type_id().into(), item.into_raw().cast())
         };
@@ -261,15 +246,9 @@
 impl PamModuleHandle for LibPamHandle {
     unsafe fn get_data<T>(&mut self, key: &str) -> crate::Result<Option<&T>> {
         let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?;
-        let mut ptr: *const libc::c_void = std::ptr::null();
+        let mut ptr: *const c_void = ptr::null();
         ErrorCode::result_from(pam_ffi::pam_get_data(&self.0, c_key.as_ptr(), &mut ptr))?;
-        match ptr.is_null() {
-            true => Ok(None),
-            false => {
-                let typed_ptr = ptr.cast();
-                Ok(Some(&*typed_ptr))
-            }
-        }
+        Ok((ptr as *const T).as_ref())
     }
 
     fn set_data<T>(&mut self, key: &str, data: Box<T>) -> crate::Result<()> {
--- a/src/lib.rs	Tue Jun 03 01:21:59 2025 -0400
+++ b/src/lib.rs	Tue Jun 03 21:54:58 2025 -0400
@@ -28,7 +28,6 @@
 pub mod module;
 
 mod handle;
-mod memory;
 mod pam_ffi;
 
 #[doc(inline)]
--- a/src/memory.rs	Tue Jun 03 01:21:59 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-//! Utility functions for dealing with memory copying and stuff.
-
-use crate::constants::{ErrorCode, Result};
-use libc::c_char;
-use std::ffi::{CStr, CString};
-
-/// 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 => std::ptr::null(),
-    }
-}
-
-/// Creates an owned copy of a string that is returned from a
-/// <code>pam_get_<var>whatever</var></code> function.
-pub fn copy_pam_string(result_ptr: *const 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)
-}
--- a/src/module.rs	Tue Jun 03 01:21:59 2025 -0400
+++ b/src/module.rs	Tue Jun 03 21:54:58 2025 -0400
@@ -5,8 +5,9 @@
 
 use crate::constants::{ErrorCode, Flags, Result};
 use crate::conv::BinaryData;
-use crate::conv::{Conversation, Message, Response};
+use crate::conv::{Conversation, Response};
 use crate::handle::PamModuleHandle;
+use crate::pam_ffi::Message;
 use secure_string::SecureString;
 use std::ffi::CStr;
 
@@ -362,12 +363,12 @@
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::account_management(
-                    unsafe { LibPamHandle::from_ptr(pamh) },
-                    args,
-                    flags,
-                ))
+                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]
@@ -377,12 +378,12 @@
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::authenticate(
-                    unsafe { LibPamHandle::from_ptr(pamh) },
-                    args,
-                    flags,
-                ))
+                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]
@@ -392,12 +393,12 @@
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::change_authtok(
-                    unsafe { LibPamHandle::from_ptr(pamh) },
-                    args,
-                    flags,
-                ))
+                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]
@@ -407,12 +408,12 @@
                 argc: c_int,
                 argv: *const *const c_char,
             ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::close_session(
-                    unsafe { LibPamHandle::from_ptr(pamh) },
-                    args,
-                    flags,
-                ))
+                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]
@@ -423,11 +424,11 @@
                 argv: *const *const c_char,
             ) -> c_int {
                 let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::open_session(
-                    unsafe { LibPamHandle::from_ptr(pamh) },
-                    args,
-                    flags,
-                ))
+                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]
@@ -438,11 +439,11 @@
                 argv: *const *const c_char,
             ) -> c_int {
                 let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::set_credentials(
-                    unsafe { LibPamHandle::from_ptr(pamh) },
-                    args,
-                    flags,
-                ))
+                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.
@@ -460,7 +461,7 @@
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use super::{
         Conversation, ConversationMux, ErrorCode, Message, Response, Result, SecureString,
     };
--- a/src/pam_ffi.rs	Tue Jun 03 01:21:59 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,433 +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.
-
-// Temporarily allow dead code.
-#![allow(dead_code)]
-
-use crate::constants::{InvalidEnum, NulError, TooBigError};
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-use std::ffi::{c_char, c_int, c_void, CStr};
-use std::marker::{PhantomData, PhantomPinned};
-use std::slice;
-
-/// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`].
-type Immovable = PhantomData<(*mut u8, PhantomPinned)>;
-
-/// An opaque pointer given to us by PAM.
-#[repr(C)]
-pub struct Handle {
-    _data: (),
-    _marker: Immovable,
-}
-
-/// Styles of message that are shown to the user.
-#[derive(Debug, PartialEq, FromPrimitive)]
-#[non_exhaustive] // non-exhaustive because C might give us back anything!
-pub enum MessageStyle {
-    /// 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 MessageStyle {
-    type Error = InvalidEnum<Self>;
-    fn try_from(value: c_int) -> std::result::Result<Self, Self::Error> {
-        Self::from_i32(value).ok_or(value.into())
-    }
-}
-
-impl From<MessageStyle> for c_int {
-    fn from(val: MessageStyle) -> 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 Message {
-    /// 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 [`MessageStyle::BinaryPrompt`], this will be [`BinaryData`]
-    /// (a Linux-PAM extension).
-    data: *const c_void,
-}
-
-#[repr(C)]
-pub struct TextResponseInner {
-    data: *mut c_char,
-    _unused: c_int,
-}
-
-impl TextResponseInner {
-    /// Allocates a new text response on the C heap.
-    ///
-    /// Both `self` and its internal pointer are located on the C heap.
-    /// You are responsible for calling [`free`](Self::free)
-    /// on the pointer you get back when you're done with it.
-    pub fn alloc(text: impl AsRef<str>) -> Result<*mut Self, NulError> {
-        let str_data = Self::malloc_str(text)?;
-        let inner = GenericResponse::alloc(str_data);
-        Ok(inner as *mut Self)
-    }
-
-    /// Gets the string stored in this response.
-    pub fn contents(&self) -> &CStr {
-        // 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.data) }
-    }
-
-    /// Releases memory owned by this response.
-    ///
-    /// # Safety
-    ///
-    /// You are responsible for no longer using this after calling free.
-    pub unsafe fn free(me: *mut Self) {
-        if !me.is_null() {
-            let data = (*me).data;
-            if !data.is_null() {
-                libc::memset(data as *mut c_void, 0, libc::strlen(data));
-            }
-            libc::free(data as *mut c_void);
-        }
-        libc::free(me as *mut c_void);
-    }
-
-    /// 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.
-    fn malloc_str(text: impl AsRef<str>) -> Result<*mut c_void, 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() as *const c_void, data.len());
-            Ok(data_alloc)
-        }
-    }
-}
-
-/// A [`GenericResponse`] with [`BinaryData`] in it.
-#[repr(C)]
-pub struct BinaryResponseInner {
-    data: *mut BinaryData,
-    _unused: c_int,
-}
-
-impl BinaryResponseInner {
-    /// Allocates a new binary response on the C heap.
-    ///
-    /// 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)
-    /// on the pointer you get back when you're done with it.
-    pub fn alloc(data: &[u8], data_type: u8) -> Result<*mut Self, TooBigError> {
-        let bin_data = BinaryData::alloc(data, data_type)?;
-        let inner = GenericResponse::alloc(bin_data as *mut c_void);
-        Ok(inner as *mut Self)
-    }
-
-    /// Gets the binary data in this response.
-    pub fn contents(&self) -> &[u8] {
-        self.data().contents()
-    }
-
-    /// Gets the `data_type` tag that was embedded with the message.
-    pub fn data_type(&self) -> u8 {
-        self.data().data_type
-    }
-
-    #[inline]
-    fn data(&self) -> &BinaryData {
-        // 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.data) }
-    }
-
-    /// Releases memory owned by this response.
-    ///
-    /// # Safety
-    ///
-    /// You are responsible for not using this after calling free.
-    pub unsafe fn free(me: *mut Self) {
-        if !me.is_null() {
-            BinaryData::free((*me).data);
-        }
-        libc::free(me as *mut c_void)
-    }
-}
-
-/// 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)]
-struct BinaryData {
-    /// 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 BinaryData {
-    /// Copies the given data to a new BinaryData on the heap.
-    fn alloc(source: &[u8], data_type: u8) -> Result<*mut BinaryData, TooBigError> {
-        let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError {
-            max: (u32::MAX - 5) as usize,
-            actual: source.len(),
-        })?;
-        let data = unsafe {
-            let dest_buffer = libc::malloc(buffer_size as usize) as *mut BinaryData;
-            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 as *mut c_void,
-                source.as_ptr() as *const c_void,
-                source.len(),
-            );
-            dest_buffer
-        };
-        Ok(data)
-    }
-
-    fn length(&self) -> usize {
-        u32::from_be_bytes(self.total_length).saturating_sub(5) as usize
-    }
-
-    fn contents(&self) -> &[u8] {
-        unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) }
-    }
-
-    /// Clears this data and frees it.
-    fn free(me: *mut Self) {
-        if me.is_null() {
-            return;
-        }
-        unsafe {
-            let me_too = &mut *me;
-            let contents = slice::from_raw_parts_mut(me_too.data.as_mut_ptr(), me_too.length());
-            for v in contents {
-                *v = 0
-            }
-            me_too.data_type = 0;
-            me_too.total_length = [0; 4];
-            libc::free(me as *mut c_void);
-        }
-    }
-}
-
-/// Generic version of response data.
-///
-/// This has the same structure as [`BinaryResponseInner`]
-/// and [`TextResponseInner`].
-#[repr(C)]
-pub struct GenericResponse {
-    /// 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 [`BinaryData`]
-    /// (a Linux-PAM extension).
-    data: *mut c_void,
-    /// Unused.
-    return_code: c_int,
-}
-
-impl GenericResponse {
-    /// Allocates a response on the C heap pointing to the given data.
-    fn alloc(data: *mut c_void) -> *mut Self {
-        unsafe {
-            let alloc = libc::calloc(1, size_of::<Self>()) as *mut Self;
-            (*alloc).data = data;
-            alloc
-        }
-    }
-
-    /// Frees a response on the C heap.
-    ///
-    /// # Safety
-    ///
-    /// It's on you to stop using this GenericResponse after freeing it.
-    pub unsafe fn free(me: *mut GenericResponse) {
-        if !me.is_null() {
-            libc::free((*me).data);
-        }
-        libc::free(me as *mut c_void);
-    }
-}
-
-/// An opaque pointer we provide to PAM for callbacks.
-#[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 some [`Message`]s (see note).
-/// - `responses` is a pointer to an array of [`GenericResponse`]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 [`Conversation`] we were passed.
-///
-/// NOTE: On Linux-PAM and other compatible implementations, `messages`
-/// is treated as a pointer-to-pointers, like `int argc, char **argv`.
-///
-/// ```text
-/// ┌──────────┐  points to  ┌─────────────┐       ╔═ Message ═╗
-/// │ messages │ ┄┄┄┄┄┄┄┄┄┄> │ messages[0] │ ┄┄┄┄> ║ style     ║
-/// └──────────┘             │ messages[1] │ ┄┄╮   ║ data      ║
-///                          │ ...         │   ┆   ╚═══════════╝
-///                                            ┆
-///                                            ┆    ╔═ Message ═╗
-///                                            ╰┄┄> ║ style     ║
-///                                                 ║ data      ║
-///                                                 ╚═══════════╝
-/// ```
-///
-/// On OpenPAM and other compatible implementations (like Solaris),
-/// `messages` is a pointer-to-pointer-to-array.
-///
-/// ```text
-/// ┌──────────┐  points to  ┌───────────┐       ╔═ Message[] ═╗
-/// │ messages │ ┄┄┄┄┄┄┄┄┄┄> │ *messages │ ┄┄┄┄> ║ style       ║
-/// └──────────┘             └───────────┘       ║ data        ║
-///                                              ╟─────────────╢
-///                                              ║ style       ║
-///                                              ║ data        ║
-///                                              ╟─────────────╢
-///                                              ║ ...         ║
-/// ```
-///
-/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
-pub type ConversationCallback = extern "C" fn(
-    num_msg: c_int,
-    messages: *const *const Message,
-    responses: &mut *const GenericResponse,
-    appdata: *const AppData,
-) -> c_int;
-
-/// A callback and the associated [`AppData`] pointer that needs to be passed back to it.
-#[repr(C)]
-pub struct Conversation {
-    callback: ConversationCallback,
-    appdata: *const AppData,
-}
-
-#[link(name = "pam")]
-extern "C" {
-    pub fn pam_get_data(
-        pamh: *const Handle,
-        module_data_name: *const c_char,
-        data: &mut *const c_void,
-    ) -> c_int;
-
-    pub fn pam_set_data(
-        pamh: *mut Handle,
-        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;
-
-    pub fn pam_get_item(pamh: *const Handle, item_type: c_int, item: &mut *const c_void) -> c_int;
-
-    pub fn pam_set_item(pamh: *mut Handle, item_type: c_int, item: *const c_void) -> c_int;
-
-    pub fn pam_get_user(
-        pamh: *const Handle,
-        user: &mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-    pub fn pam_get_authtok(
-        pamh: *const Handle,
-        item_type: c_int,
-        data: &mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-    pub fn pam_end(pamh: *mut Handle, status: c_int) -> c_int;
-}
-
-#[cfg(test)]
-mod test {
-    use super::{BinaryResponseInner, GenericResponse, TextResponseInner};
-
-    #[test]
-    fn test_text_response() {
-        let resp = TextResponseInner::alloc("hello").expect("alloc should succeed");
-        let borrow_resp = unsafe { &*resp };
-        let data = borrow_resp.contents().to_str().expect("valid");
-        assert_eq!("hello", data);
-        unsafe {
-            TextResponseInner::free(resp);
-        }
-        TextResponseInner::alloc("hell\0o").expect_err("should error; contains nul");
-    }
-
-    #[test]
-    fn test_binary_response() {
-        let real_data = [1, 2, 3, 4, 5, 6, 7, 8];
-        let resp = BinaryResponseInner::alloc(&real_data, 7).expect("alloc should succeed");
-        let borrow_resp = unsafe { &*resp };
-        let data = borrow_resp.contents();
-        assert_eq!(&real_data, data);
-        assert_eq!(7, borrow_resp.data_type());
-        unsafe { BinaryResponseInner::free(resp) };
-    }
-
-    #[test]
-    fn test_free_safety() {
-        unsafe {
-            TextResponseInner::free(std::ptr::null_mut());
-            BinaryResponseInner::free(std::ptr::null_mut());
-        }
-    }
-
-    #[test]
-    #[ignore]
-    fn test_binary_response_too_big() {
-        let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000];
-        BinaryResponseInner::alloc(&big_data, 0).expect_err("this is too big!");
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pam_ffi/memory.rs	Tue Jun 03 21:54:58 2025 -0400
@@ -0,0 +1,187 @@
+//! 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`].
+pub type Immovable = 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.
+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)
+}
+
+/// 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() as *const c_void, 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 as *const c_char));
+    }
+}
+
+/// 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(),
+        })?;
+        let data = unsafe {
+            let dest_buffer = libc::malloc(buffer_size as usize) as *mut CBinaryData;
+            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 as *mut c_void,
+                source.as_ptr() as *const c_void,
+                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 std::ffi::CString;
+    use crate::ErrorCode;
+    use super::{copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string};
+    #[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/pam_ffi/message.rs	Tue Jun 03 21:54:58 2025 -0400
@@ -0,0 +1,292 @@
+//! Data and types dealing with PAM messages.
+
+use crate::constants::InvalidEnum;
+use crate::pam_ffi::memory;
+use crate::pam_ffi::memory::{CBinaryData, NulError, TooBigError};
+use num_derive::FromPrimitive;
+use num_traits::FromPrimitive;
+use std::ffi::{c_char, c_int, c_void, CStr};
+use std::result::Result as StdResult;
+use std::slice;
+use std::str::Utf8Error;
+
+/// The types of message and request that can be sent to a user.
+///
+/// The data within each enum value is the prompt (or other information)
+/// that will be presented to the user.
+#[derive(Debug)]
+pub enum Message<'a> {
+    /// Requests information from the user; will be masked when typing.
+    ///
+    /// Response: [`Response::MaskedText`]
+    MaskedPrompt(&'a str),
+    /// Requests information from the user; will not be masked.
+    ///
+    /// Response: [`Response::Text`]
+    Prompt(&'a str),
+    /// "Yes/No/Maybe conditionals" (a Linux-PAM extension).
+    ///
+    /// Response: [`Response::Text`]
+    /// (Linux-PAM documentation doesn't define its contents.)
+    RadioPrompt(&'a str),
+    /// Raises an error message to the user.
+    ///
+    /// Response: [`Response::NoResponse`]
+    Error(&'a str),
+    /// Sends an informational message to the user.
+    ///
+    /// Response: [`Response::NoResponse`]
+    Info(&'a str),
+    /// Requests binary data from the client (a Linux-PAM extension).
+    ///
+    /// This is used for non-human or non-keyboard prompts (security key?).
+    /// NOT part of the X/Open PAM specification.
+    ///
+    /// Response: [`Response::Binary`]
+    BinaryPrompt {
+        /// Some binary data.
+        data: &'a [u8],
+        /// A "type" that you can use for signalling. Has no strict definition in PAM.
+        data_type: u8,
+    },
+}
+
+impl Message<'_> {
+    /// Copies the contents of this message to the C heap.
+    fn copy_to_heap(&self) -> StdResult<(Style, *mut c_void), ConversionError> {
+        let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
+        match *self {
+            Self::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text),
+            Self::Prompt(text) => alloc(Style::PromptEchoOn, text),
+            Self::RadioPrompt(text) => alloc(Style::RadioType, text),
+            Self::Error(text) => alloc(Style::ErrorMsg, text),
+            Self::Info(text) => alloc(Style::TextInfo, text),
+            Self::BinaryPrompt { data, data_type } => Ok((
+                Style::BinaryPrompt,
+                (CBinaryData::alloc(data, data_type)?).cast(),
+            )),
+        }
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("error creating PAM message: {0}")]
+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 [`BinaryData`]
+    /// (a Linux-PAM extension).
+    data: *mut c_void,
+}
+
+impl RawMessage {
+    fn set(&mut self, msg: &Message) -> StdResult<(), ConversionError> {
+        let (style, data) = msg.copy_to_heap()?;
+        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(())
+    }
+
+    /// Retrieves the data stored in this message.
+    fn data(&self) -> StdResult<Message, ConversionError> {
+        let style: Style = self.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(self.string_data()?),
+                Style::PromptEchoOn => Message::Prompt(self.string_data()?),
+                Style::TextInfo => Message::Info(self.string_data()?),
+                Style::ErrorMsg => Message::Error(self.string_data()?),
+                Style::RadioType => Message::Error(self.string_data()?),
+                Style::BinaryPrompt => (self.data as *const 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)
+    }
+
+    /// 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 as *const c_char).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 as *mut CBinaryData).as_mut() {
+                            d.zero_contents()
+                        }
+                    }
+                    Style::TextInfo
+                    | Style::RadioType
+                    | Style::ErrorMsg
+                    | Style::PromptEchoOff
+                    | Style::PromptEchoOn => memory::zero_c_string(self.data),
+                }
+            };
+        }
+    }
+}
+
+/// 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.***
+#[repr(C)]
+pub struct OwnedMessages {
+    /// An indirection to the messages themselves, stored on the C heap.
+    indirect: *mut Indirect<RawMessage>,
+    /// 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 {
+        // SAFETY: We're allocating. That's safe.
+        unsafe {
+            // Since this is Linux-PAM, the indirect is a list of pointers.
+            let indirect =
+                libc::calloc(count, size_of::<Indirect<RawMessage>>()) as *mut Indirect<RawMessage>;
+            let indir_ptrs = slice::from_raw_parts_mut(indirect, count);
+            for ptr in indir_ptrs {
+                ptr.base = libc::calloc(1, size_of::<RawMessage>()) as *mut RawMessage;
+            }
+            Self { indirect, count }
+        }
+    }
+
+    /// Gets a reference to the message at the given index.
+    pub fn get(&self, index: usize) -> Option<&RawMessage> {
+        (index < self.count).then(|| unsafe { (*self.indirect).at(index) })
+    }
+
+    /// Gets a mutable reference to the message at the given index.
+    pub fn get_mut(&mut self, index: usize) -> Option<&mut RawMessage> {
+        (index < self.count).then(|| unsafe { (*self.indirect).at_mut(index) })
+    }
+}
+
+#[repr(transparent)]
+struct Indirect<T> {
+    /// The starting address for the T.
+    base: *mut T,
+}
+
+impl<T> Indirect<T> {
+    /// Gets a mutable reference to the element at the given index.
+    ///
+    /// # Safety
+    ///
+    /// We don't check `index`.
+    unsafe fn at_mut(&mut self, index: usize) -> &mut T {
+        &mut *self.base.add(index)
+    }
+
+    unsafe fn at(&self, index: usize) -> &T {
+        &*self.base.add(index)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pam_ffi/mod.rs	Tue Jun 03 21:54:58 2025 -0400
@@ -0,0 +1,94 @@
+//! 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)]
+
+pub mod memory;
+mod message;
+mod response;
+
+use crate::pam_ffi::memory::Immovable;
+use crate::pam_ffi::message::OwnedMessages;
+pub use message::Message;
+pub use response::RawResponse;
+use std::ffi::{c_char, c_int, c_void};
+
+/// An opaque structure that a PAM handle points to.
+#[repr(C)]
+pub struct Handle {
+    _data: (),
+    _marker: Immovable,
+}
+
+/// 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 [`Conversation`] we were passed.
+pub type ConversationCallback = extern "C" fn(
+    num_msg: c_int,
+    messages: &OwnedMessages,
+    responses: &mut *mut RawResponse,
+    appdata: *const AppData,
+) -> c_int;
+
+/// A callback and the associated [`AppData`] pointer that needs to be passed back to it.
+#[repr(C)]
+pub struct Conversation {
+    callback: ConversationCallback,
+    appdata: *const AppData,
+}
+
+#[link(name = "pam")]
+extern "C" {
+    pub fn pam_get_data(
+        pamh: *const Handle,
+        module_data_name: *const c_char,
+        data: &mut *const c_void,
+    ) -> c_int;
+
+    pub fn pam_set_data(
+        pamh: *mut Handle,
+        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;
+
+    pub fn pam_get_item(pamh: *const Handle, item_type: c_int, item: &mut *const c_void) -> c_int;
+
+    pub fn pam_set_item(pamh: *mut Handle, item_type: c_int, item: *const c_void) -> c_int;
+
+    pub fn pam_get_user(
+        pamh: *const Handle,
+        user: &mut *const c_char,
+        prompt: *const c_char,
+    ) -> c_int;
+
+    pub fn pam_get_authtok(
+        pamh: *const Handle,
+        item_type: c_int,
+        data: &mut *const c_char,
+        prompt: *const c_char,
+    ) -> c_int;
+
+    pub fn pam_end(pamh: *mut Handle, status: c_int) -> c_int;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pam_ffi/response.rs	Tue Jun 03 21:54:58 2025 -0400
@@ -0,0 +1,231 @@
+//! Types used when dealing with PAM conversations.
+
+use crate::pam_ffi::memory;
+use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError};
+use std::ffi::{c_char, c_int, c_void, CStr};
+use std::ops::{Deref, DerefMut};
+use std::result::Result as StdResult;
+use std::str::Utf8Error;
+use std::{mem, ptr, slice};
+
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct RawTextResponse(RawResponse);
+
+impl RawTextResponse {
+    /// Allocates a new text response on the C heap.
+    ///
+    /// Both `self` and its internal pointer are located on the C heap.
+    /// 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();
+        Ok(unsafe { &mut *(dest as *mut RawResponse as *mut Self) })
+    }
+
+    /// Gets the string stored in this response.
+    pub fn contents(&self) -> StdResult<&str, Utf8Error> {
+        // 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 as *const c_char) }.to_str()
+    }
+
+    /// Releases memory owned by this response.
+    ///
+    /// # Safety
+    ///
+    /// You are responsible for no longer using this after calling free.
+    pub unsafe fn free_contents(&mut self) {
+        let data = self.0.data;
+        memory::zero_c_string(data);
+        libc::free(data);
+        self.0.data = ptr::null_mut()
+    }
+}
+
+/// A [`RawResponse`] with [`CBinaryData`] in it.
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct RawBinaryResponse(RawResponse);
+
+impl RawBinaryResponse {
+    /// Allocates a new binary response on the C heap.
+    ///
+    /// 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)? as *mut c_void;
+        Ok(unsafe {
+            (dest as *mut RawResponse)
+                .cast::<RawBinaryResponse>()
+                .as_mut()
+                .unwrap()
+        })
+    }
+
+    /// Gets the binary data in this response.
+    pub fn contents(&self) -> &[u8] {
+        self.data().contents()
+    }
+
+    /// Gets the `data_type` tag that was embedded with the message.
+    pub fn data_type(&self) -> u8 {
+        self.data().data_type()
+    }
+
+    #[inline]
+    fn data(&self) -> &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 as *const CBinaryData) }
+    }
+
+    /// Releases memory owned by this response.
+    ///
+    /// # Safety
+    ///
+    /// You are responsible for not using this after calling free.
+    pub unsafe fn free_contents(&mut self) {
+        let data_ref = (self.0.data as *mut 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)]
+#[repr(C)]
+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>()) } as *mut RawResponse,
+            count: count,
+        }
+    }
+
+    /// 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.
+    unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self {
+        OwnedResponses { base, count }
+    }
+}
+
+impl From<OwnedResponses> for *mut RawResponse {
+    /// Converts this into a pointer to `RawResponse`.
+    ///
+    /// The backing data is no longer freed.
+    fn from(value: OwnedResponses) -> Self {
+        let ret = value.base;
+        mem::forget(value);
+        ret
+    }
+}
+
+impl Deref for OwnedResponses {
+    type Target = [RawResponse];
+    fn deref(&self) -> &Self::Target {
+        // SAFETY: We allocated this ourselves, or it was provided to us by PAM.
+        unsafe { slice::from_raw_parts(self.base, self.count) }
+    }
+}
+
+impl DerefMut for OwnedResponses {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        // SAFETY: We allocated this ourselves, or it was provided to us by PAM.
+        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 as *mut c_void)
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::{OwnedResponses, RawBinaryResponse, RawTextResponse};
+
+    #[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);
+        unsafe {
+            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.contents();
+        assert_eq!(&real_data, data);
+        assert_eq!(7, resp.data_type());
+        unsafe {
+            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!");
+    }
+}