changeset 66:a674799a5cd3

Make `PamHandle` and `PamModuleHandle` traits. This creates traits for PAM functionality and pulls the definitions of that functionality out of the original `PamHandle` (renamed to `LibPamHandle`) and into those traits. This supports testing PAM module implementations using mock PAM library implementations. Also uses a better representation of opaque pointers.
author Paul Fisher <paul@pfish.zone>
date Tue, 27 May 2025 14:37:28 -0400
parents 8e507c7af9cf
children 71e432a213ee
files Cargo.toml src/handle.rs src/items.rs src/lib.rs src/module.rs src/pam_ffi.rs
diffstat 6 files changed, 219 insertions(+), 121 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.toml	Thu May 22 02:08:10 2025 -0400
+++ b/Cargo.toml	Tue May 27 14:37:28 2025 -0400
@@ -1,7 +1,7 @@
 [package]
 name = "nonstick"
 description = "PAM bindings for Rust"
-version = "0.0.5"
+version = "0.0.6-dev0"
 authors = ["Paul Fisher <paul@pfish.zone>", "Anthony Nowell <anowell@gmail.com>" ]
 repository = "https://hg.pfish.zone/crates/nonstick/"
 readme = "README.md"
--- a/src/handle.rs	Thu May 22 02:08:10 2025 -0400
+++ b/src/handle.rs	Tue May 27 14:37:28 2025 -0400
@@ -1,18 +1,17 @@
-//! Where [PamHandle] lives.
+//! 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, ErrorCode};
+use crate::{memory, pam_ffi};
 use libc::c_char;
 use secure_string::SecureString;
 use std::ffi::{c_int, CString};
+use std::mem;
 
-/// Your interface to a PAM handle.
+/// Features of a PAM handle that are available to applications and modules.
 ///
-/// This structure wraps an opaque PAM-provided pointer and gives you
-/// a safe and familiar struct-based API to interact with PAM.
-#[repr(transparent)]
-pub struct PamHandle(*mut libc::c_void);
-
-impl PamHandle {
+/// You probably want [`LibPamHandle`]. This trait is intended to allow creating
+/// mock PAM handle types used for testing PAM modules and applications.
+pub trait PamHandle {
     /// Retrieves the name of the user who is authenticating or logging in.
     ///
     /// This is effectively like `handle.get_item::<Item::User>()`.
@@ -23,7 +22,7 @@
     ///
     /// ```no_run
     /// # use nonstick::PamHandle;
-    /// # fn _doc(handle: &PamHandle) -> Result<(), Box<dyn std::error::Error>> {
+    /// # fn _doc(handle: &impl PamHandle) -> Result<(), Box<dyn std::error::Error>> {
     /// // Get the username using the default prompt.
     /// let user = handle.get_user(None)?;
     /// // Get the username using a custom prompt.
@@ -31,18 +30,10 @@
     /// # Ok(())
     /// # }
     /// ```
-    /// 
+    ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_user.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_user
-    pub fn get_user(&self, prompt: Option<&str>) -> crate::Result<String> {
-        let prompt = memory::option_cstr(prompt)?;
-        let mut output: *const c_char = std::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)
-    }
+    fn get_user(&self, prompt: Option<&str>) -> Result<String>;
 
     /// Retrieves the authentication token from the user.
     ///
@@ -55,7 +46,7 @@
     ///
     /// ```no_run
     /// # use nonstick::PamHandle;
-    /// # fn _doc(handle: &PamHandle) -> Result<(), Box<dyn std::error::Error>> {
+    /// # fn _doc(handle: &impl PamHandle) -> Result<(), Box<dyn std::error::Error>> {
     /// // Get the user's password using the default prompt.
     /// let pass = handle.get_authtok(None)?;
     /// // Get the user's password using a custom prompt.
@@ -63,23 +54,10 @@
     /// Ok(())
     /// # }
     /// ```
-    /// 
+    ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_authtok.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item
-    pub fn get_authtok(&self, prompt: Option<&str>) -> crate::Result<SecureString> {
-        let prompt = memory::option_cstr(prompt)?;
-        let mut output: *const c_char = std::ptr::null_mut();
-        let res = unsafe {
-            pam_ffi::pam_get_authtok(
-                self.0,
-                ItemType::AuthTok.into(),
-                &mut output,
-                memory::prompt_ptr(prompt.as_ref()),
-            )
-        };
-        ErrorCode::result_from(res)?;
-        memory::copy_pam_string(output).map(SecureString::from)
-    }
+    fn get_authtok(&self, prompt: Option<&str>) -> Result<SecureString>;
 
     /// Retrieves an [Item] that has been set, possibly by the PAM client.
     ///
@@ -96,7 +74,7 @@
     /// # use nonstick::PamHandle;
     /// use nonstick::items::Service;
     ///
-    /// # fn _doc(pam_handle: &PamHandle) -> Result<(), Box<dyn std::error::Error>> {
+    /// # fn _doc(pam_handle: &impl PamHandle) -> Result<(), Box<dyn std::error::Error>> {
     /// let svc: Option<Service> = pam_handle.get_item()?;
     /// match svc {
     ///     Some(name) => eprintln!("The calling service name is {:?}", name.to_string_lossy()),
@@ -108,19 +86,7 @@
     ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item
-    pub fn get_item<T: Item>(&self) -> crate::Result<Option<T>> {
-        let mut ptr: *const libc::c_void = std::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)),
-            }
-        };
-        Ok(out)
-    }
+    fn get_item<T: Item>(&self) -> Result<Option<T>>;
 
     /// Sets an item in the PAM context. It can be retrieved using [`get_item`](Self::get_item).
     ///
@@ -129,12 +95,40 @@
     ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_item
-    pub fn set_item<T: Item>(&mut self, item: T) -> crate::Result<()> {
-        let ret =
-            unsafe { pam_ffi::pam_set_item(self.0, T::type_id().into(), item.into_raw().cast()) };
-        ErrorCode::result_from(ret)
-    }
+    fn set_item<T: Item>(&mut self, item: T) -> Result<()>;
 
+    /// Closes the PAM session on an owned PAM handle.
+    ///
+    /// This should be called with the result of the application's last call
+    /// into PAM services. Since this is only applicable to *owned* PAM handles,
+    /// a PAM module should never call this (and it will never be handed
+    /// an owned `PamHandle` that it can `close`).
+    ///
+    /// See the [`pam_end` manual page][man] for more information.
+    ///
+    /// ```no_run
+    /// # use nonstick::PamHandle;
+    /// # use std::error::Error;
+    /// # fn _doc(handle: impl PamHandle, auth_result: nonstick::Result<()>) -> Result<(), Box<dyn Error>> {
+    /// // Earlier: authentication was performed and the result was stored
+    /// // into auth_result.
+    /// handle.close(auth_result)?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html
+    fn close(self, status: Result<()>) -> Result<()>;
+}
+
+/// Functionality of a PAM handle that can be expected by a PAM module.
+///
+/// If you are not writing a PAM module (e.g., you are writing an application),
+/// you should not use any of the functionality exposed by this trait.
+///
+/// Like [`PamHandle`], this is intended to allow creating mock implementations
+/// of PAM for testing PAM modules.
+pub trait PamModuleHandle: PamHandle {
     /// Gets some pointer, identified by `key`, that has been set previously
     /// using [`set_data`](Self::set_data).
     ///
@@ -151,18 +145,7 @@
     ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_data.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_data
-    pub unsafe fn get_data<T>(&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();
-        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))
-            }
-        }
-    }
+    unsafe fn get_data<T>(&self, key: &str) -> Result<Option<&T>>;
 
     /// Stores a pointer that can be retrieved later with [`get_data`](Self::get_data).
     ///
@@ -176,33 +159,126 @@
     ///
     /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_data.3.html
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_data
-    pub fn set_data<T>(&mut self, key: &str, data: Box<T>) -> crate::Result<()> {
-        let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?;
+    fn set_data<T>(&mut self, key: &str, data: Box<T>) -> Result<()>;
+}
+
+/// A [`PamHandle`] backed by `libpam`, i.e., a real PAM handle.
+///
+/// This structure wraps an opaque PAM handle and gives you a nice Rusty
+/// interface to use PAM.
+#[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.
+    fn drop(&mut self) {
+        unsafe {
+            pam_ffi::pam_end(&mut self.0, 0);
+        }
+    }
+}
+
+impl PamHandle for LibPamHandle {
+    fn get_user(&self, prompt: Option<&str>) -> crate::Result<String> {
+        let prompt = memory::option_cstr(prompt)?;
+        let mut output: *const c_char = std::ptr::null_mut();
         let ret = unsafe {
-            pam_ffi::pam_set_data(
-                self.0,
-                c_key.as_ptr(),
-                Box::into_raw(data).cast(),
-                Self::set_data_cleanup::<T>,
+            pam_ffi::pam_get_user(&self.0, &mut output, memory::prompt_ptr(prompt.as_ref()))
+        };
+        ErrorCode::result_from(ret)?;
+        memory::copy_pam_string(output)
+    }
+
+    fn get_authtok(&self, prompt: Option<&str>) -> crate::Result<SecureString> {
+        let prompt = memory::option_cstr(prompt)?;
+        let mut output: *const c_char = std::ptr::null_mut();
+        let res = unsafe {
+            pam_ffi::pam_get_authtok(
+                &self.0,
+                ItemType::AuthTok.into(),
+                &mut output,
+                memory::prompt_ptr(prompt.as_ref()),
             )
         };
+        ErrorCode::result_from(res)?;
+        memory::copy_pam_string(output).map(SecureString::from)
+    }
+
+    fn get_item<T: Item>(&self) -> crate::Result<Option<T>> {
+        let mut ptr: *const libc::c_void = std::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)),
+            }
+        };
+        Ok(out)
+    }
+
+    fn set_item<T: Item>(&mut self, item: T) -> crate::Result<()> {
+        let ret = unsafe {
+            pam_ffi::pam_set_item(&mut self.0, T::type_id().into(), item.into_raw().cast())
+        };
         ErrorCode::result_from(ret)
     }
 
-    /// 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());
-        }
+    fn close(mut self, status: Result<()>) -> Result<()> {
+        let result = unsafe { pam_ffi::pam_end(&mut self.0, ErrorCode::result_to_c(status)) };
+        // Since we've already `pam_end`ed this session, we don't want it to be
+        // double-freed on drop.
+        mem::forget(self);
+        ErrorCode::result_from(result)
     }
 }
 
-impl From<*mut libc::c_void> for PamHandle {
-    /// Wraps an internal Handle pointer.
-    fn from(value: *mut libc::c_void) -> Self {
-        Self(value)
+impl PamModuleHandle for LibPamHandle {
+    unsafe fn get_data<T>(&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();
+        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))
+            }
+        }
+    }
+
+    fn set_data<T>(&mut self, key: &str, data: Box<T>) -> crate::Result<()> {
+        let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?;
+        let ret = unsafe {
+            pam_ffi::pam_set_data(
+                &mut self.0,
+                c_key.as_ptr(),
+                Box::into_raw(data).cast(),
+                set_data_cleanup::<T>,
+            )
+        };
+        ErrorCode::result_from(ret)
     }
 }
+
+/// 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());
+    }
+}
--- a/src/items.rs	Thu May 22 02:08:10 2025 -0400
+++ b/src/items.rs	Tue May 27 14:37:28 2025 -0400
@@ -54,7 +54,7 @@
     }
 }
 
-/// A type that can be requested by [`PamHandle::get_item`](crate::PamHandle::get_item).
+/// A type that can be requested by [`PamHandle::get_item`](crate::LibPamHandle::get_item).
 pub trait Item {
     /// The `repr(C)` type that is returned (by pointer) by the underlying `pam_get_item` function.
     /// This memory is owned by the PAM session.
--- a/src/lib.rs	Thu May 22 02:08:10 2025 -0400
+++ b/src/lib.rs	Tue May 27 14:37:28 2025 -0400
@@ -35,6 +35,6 @@
 #[doc(inline)]
 pub use crate::{
     constants::{ErrorCode, Flags, Result},
-    handle::PamHandle,
+    handle::{LibPamHandle, PamHandle, PamModuleHandle},
     module::PamModule,
 };
--- a/src/module.rs	Thu May 22 02:08:10 2025 -0400
+++ b/src/module.rs	Tue May 27 14:37:28 2025 -0400
@@ -1,7 +1,7 @@
 //! Functions and types useful for implementing a PAM module.
 
 use crate::constants::{ErrorCode, Flags, Result};
-use crate::handle::PamHandle;
+use crate::handle::PamModuleHandle;
 use std::ffi::CStr;
 
 /// A trait for a PAM module to implement.
@@ -18,14 +18,14 @@
 /// [manpage]: https://www.man7.org/linux/man-pages/man3/pam.3.html
 /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_MWG.html
 #[allow(unused_variables)]
-pub trait PamModule {
+pub trait PamModule<T: PamModuleHandle> {
     // Functions for auth modules.
 
     /// Authenticate the user.
     ///
     /// This is probably the first thing you want to implement.
     /// In most cases, you will want to get the user and password,
-    /// using [`PamHandle::get_user`] and [`PamHandle::get_authtok`],
+    /// using [`LibPamHandle::get_user`] and [`LibPamHandle::get_authtok`],
     /// and verify them against something.
     ///
     /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg]
@@ -55,7 +55,7 @@
     ///   They should not try again.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-auth.html#mwg-pam_sm_authenticate
-    fn authenticate(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -98,7 +98,7 @@
     /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-acct.html#mwg-pam_sm_acct_mgmt
-    fn account_management(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -134,7 +134,7 @@
     /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-auth.html#mwg-pam_sm_setcred
-    fn set_credentials(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn set_credentials(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -182,7 +182,7 @@
     /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-chauthtok.html#mwg-pam_sm_chauthtok
-    fn change_authtok(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn change_authtok(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -206,7 +206,7 @@
     /// - [`ErrorCode::SessionError`]: Cannot make an entry for this session.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-session.html#mwg-pam_sm_open_session
-    fn open_session(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn open_session(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -228,7 +228,7 @@
     /// - [`ErrorCode::SessionError`]: Cannot remove an entry for this session.
     ///
     /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-session.html#mwg-pam_sm_close_session
-    fn close_session(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
+    fn close_session(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 }
@@ -244,21 +244,21 @@
 /// Here is full example of a PAM module that would authenticate and authorize everybody:
 ///
 /// ```no_run
-/// use nonstick::{Flags, PamHandle, PamModule, Result as PamResult, pam_hooks};
+/// use nonstick::{Flags, LibPamHandle, PamModule, PamModuleHandle, Result as PamResult, pam_hooks};
 /// use std::ffi::CStr;
 /// # fn main() {}
 ///
 /// struct MyPamModule;
 /// pam_hooks!(MyPamModule);
 ///
-/// impl PamModule for MyPamModule {
-///     fn authenticate(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+/// impl<T: PamModuleHandle> 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?"))?;
 ///         eprintln!("If you say your password is {:?}, who am I to disagree!", password.unsecure());
 ///         Ok(())
 ///     }
 ///
-///     fn account_management(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+///     fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
 ///         let username = handle.get_user(None)?;
 ///         // You should use a Conversation to communicate with the user
 ///         // instead of writing to the console, but this is just an example.
@@ -272,7 +272,7 @@
     ($ident:ident) => {
         mod _pam_hooks_scope {
             use std::ffi::{c_char, c_int, CStr};
-            use $crate::{ErrorCode, Flags, PamModule};
+            use $crate::{ErrorCode, Flags, LibPamHandle, PamModule};
 
             #[no_mangle]
             extern "C" fn pam_sm_acct_mgmt(
@@ -283,7 +283,7 @@
             ) -> c_int {
                 let args = extract_argv(argc, argv);
                 ErrorCode::result_to_c(super::$ident::account_management(
-                    &mut pamh.into(),
+                    unsafe { LibPamHandle::from_ptr(pamh) },
                     args,
                     flags,
                 ))
@@ -297,7 +297,11 @@
                 argv: *const *const c_char,
             ) -> c_int {
                 let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::authenticate(&mut pamh.into(), args, flags))
+                ErrorCode::result_to_c(super::$ident::authenticate(
+                    unsafe { LibPamHandle::from_ptr(pamh) },
+                    args,
+                    flags,
+                ))
             }
 
             #[no_mangle]
@@ -308,7 +312,11 @@
                 argv: *const *const c_char,
             ) -> c_int {
                 let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::change_authtok(&mut pamh.into(), args, flags))
+                ErrorCode::result_to_c(super::$ident::change_authtok(
+                    unsafe { LibPamHandle::from_ptr(pamh) },
+                    args,
+                    flags,
+                ))
             }
 
             #[no_mangle]
@@ -319,7 +327,11 @@
                 argv: *const *const c_char,
             ) -> c_int {
                 let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::close_session(&mut pamh.into(), args, flags))
+                ErrorCode::result_to_c(super::$ident::close_session(
+                    unsafe { LibPamHandle::from_ptr(pamh) },
+                    args,
+                    flags,
+                ))
             }
 
             #[no_mangle]
@@ -330,7 +342,11 @@
                 argv: *const *const c_char,
             ) -> c_int {
                 let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::open_session(&mut pamh.into(), args, flags))
+                ErrorCode::result_to_c(super::$ident::open_session(
+                    unsafe { LibPamHandle::from_ptr(pamh) },
+                    args,
+                    flags,
+                ))
             }
 
             #[no_mangle]
@@ -342,7 +358,7 @@
             ) -> c_int {
                 let args = extract_argv(argc, argv);
                 ErrorCode::result_to_c(super::$ident::set_credentials(
-                    &mut pamh.into(),
+                    unsafe { LibPamHandle::from_ptr(pamh) },
                     args,
                     flags,
                 ))
@@ -364,10 +380,10 @@
 
 #[cfg(test)]
 pub mod test {
-    use crate::module::PamModule;
+    use crate::module::{PamModule, PamModuleHandle};
 
     struct Foo;
-    impl PamModule for Foo {}
+    impl<T: PamModuleHandle> PamModule<T> for Foo {}
 
     pam_hooks!(Foo);
 }
--- a/src/pam_ffi.rs	Thu May 22 02:08:10 2025 -0400
+++ b/src/pam_ffi.rs	Tue May 27 14:37:28 2025 -0400
@@ -2,17 +2,25 @@
 
 use libc::c_char;
 use std::ffi::c_int;
+use std::marker::{PhantomData, PhantomPinned};
+
+/// An opaque pointer given to us by PAM.
+#[repr(C)]
+pub struct Handle {
+    _data: (),
+    _marker: PhantomData<(*mut u8, PhantomPinned)>,
+}
 
 #[link(name = "pam")]
 extern "C" {
     pub fn pam_get_data(
-        pamh: *const libc::c_void,
+        pamh: *const Handle,
         module_data_name: *const c_char,
         data: &mut *const libc::c_void,
     ) -> c_int;
 
     pub fn pam_set_data(
-        pamh: *mut libc::c_void,
+        pamh: *mut Handle,
         module_data_name: *const c_char,
         data: *const libc::c_void,
         cleanup: extern "C" fn(
@@ -23,27 +31,25 @@
     ) -> c_int;
 
     pub fn pam_get_item(
-        pamh: *const libc::c_void,
+        pamh: *const Handle,
         item_type: c_int,
         item: &mut *const libc::c_void,
     ) -> c_int;
 
-    pub fn pam_set_item(
-        pamh: *mut libc::c_void,
-        item_type: c_int,
-        item: *const libc::c_void,
-    ) -> c_int;
+    pub fn pam_set_item(pamh: *mut Handle, item_type: c_int, item: *const libc::c_void) -> c_int;
 
     pub fn pam_get_user(
-        pamh: *const libc::c_void,
+        pamh: *const Handle,
         user: &mut *const c_char,
         prompt: *const c_char,
     ) -> c_int;
 
     pub fn pam_get_authtok(
-        pamh: *const libc::c_void,
+        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;
 }