changeset 90:f6186e41399b

Miscellaneous fixes and cleanup: - Rename `get_user` to `username` and `get_authtok` to `authtok`. - Use pam_strerror for error messages. - Add library linkage to build.rs (it was missing???).
author Paul Fisher <paul@pfish.zone>
date Sat, 14 Jun 2025 09:30:16 -0400
parents dd3e9c4bcde3
children 039aae9a01f7
files build.rs src/constants.rs src/handle.rs src/libpam/handle.rs src/libpam/module.rs src/libpam/pam_ffi.rs src/module.rs
diffstat 7 files changed, 68 insertions(+), 60 deletions(-) [+]
line wrap: on
line diff
--- a/build.rs	Fri Jun 13 05:22:48 2025 -0400
+++ b/build.rs	Sat Jun 14 09:30:16 2025 -0400
@@ -4,6 +4,7 @@
 
 fn main() {
     if cfg!(feature = "link") {
+        println!("cargo::rustc-link-lib=pam");
         println!("cargo::rustc-check-cfg=cfg(pam_impl, values(\"linux-pam\",\"openpam\"))");
         let common_builder = bindgen::Builder::default()
             .merge_extern_blocks(true)
@@ -16,7 +17,7 @@
             .allowlist_function("pam_get_user")
             .allowlist_function("pam_get_authtok")
             .allowlist_function("pam_end")
-            .dynamic_link_require_all(true)
+            .allowlist_function("pam_strerror")
             .default_macro_constant_type(MacroTypeVariation::Unsigned);
 
         let linux_builder = common_builder
--- a/src/constants.rs	Fri Jun 13 05:22:48 2025 -0400
+++ b/src/constants.rs	Sat Jun 14 09:30:16 2025 -0400
@@ -9,7 +9,10 @@
 use bitflags::bitflags;
 use libc::c_int;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
+use std::error::Error;
 use std::ffi::c_uint;
+use std::fmt;
+use std::fmt::{Display, Formatter};
 use std::result::Result as StdResult;
 
 /// Arbitrary values for PAM constants when not linking against system PAM.
@@ -77,6 +80,10 @@
         PAM_TRY_AGAIN = 552,
         PAM_USER_UNKNOWN = 553
     );
+
+    fn strerror(val: c_uint) -> Option<&'static str> {
+        None
+    }
 }
 
 bitflags! {
@@ -140,79 +147,59 @@
 /// For more detailed information, see
 /// `/usr/include/security/_pam_types.h`.
 #[allow(non_camel_case_types, dead_code)]
-#[derive(Copy, Clone, Debug, PartialEq, thiserror::Error, TryFromPrimitive, IntoPrimitive)]
+#[derive(Copy, Clone, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
 #[non_exhaustive] // C might give us anything!
 #[repr(u32)]
 pub enum ErrorCode {
-    #[error("dlopen() failure when dynamically loading a service module")]
     OpenError = pam_ffi::PAM_OPEN_ERR,
-    #[error("symbol not found")]
     SymbolError = pam_ffi::PAM_SYMBOL_ERR,
-    #[error("error in service module")]
     ServiceError = pam_ffi::PAM_SERVICE_ERR,
-    #[error("system error")]
     SystemError = pam_ffi::PAM_SYSTEM_ERR,
-    #[error("memory buffer error")]
     BufferError = pam_ffi::PAM_BUF_ERR,
-    #[error("permission denied")]
     PermissionDenied = pam_ffi::PAM_PERM_DENIED,
-    #[error("authentication failure")]
     AuthenticationError = pam_ffi::PAM_AUTH_ERR,
-    #[error("cannot access authentication data due to insufficient credentials")]
     CredentialsInsufficient = pam_ffi::PAM_CRED_INSUFFICIENT,
-    #[error("underlying authentication service cannot retrieve authentication information")]
     AuthInfoUnavailable = pam_ffi::PAM_AUTHINFO_UNAVAIL,
-    #[error("user not known to the underlying authentication module")]
     UserUnknown = pam_ffi::PAM_USER_UNKNOWN,
-    #[error("retry limit reached; do not attempt further")]
     MaxTries = pam_ffi::PAM_MAXTRIES,
-    #[error("new authentication token required")]
     NewAuthTokRequired = pam_ffi::PAM_NEW_AUTHTOK_REQD,
-    #[error("user account has expired")]
     AccountExpired = pam_ffi::PAM_ACCT_EXPIRED,
-    #[error("cannot make/remove an entry for the specified session")]
     SessionError = pam_ffi::PAM_SESSION_ERR,
-    #[error("underlying authentication service cannot retrieve user credentials")]
     CredentialsUnavailable = pam_ffi::PAM_CRED_UNAVAIL,
-    #[error("user credentials expired")]
     CredentialsExpired = pam_ffi::PAM_CRED_EXPIRED,
-    #[error("failure setting user credentials")]
     CredentialsError = pam_ffi::PAM_CRED_ERR,
-    #[error("no module-specific data is present")]
     NoModuleData = pam_ffi::PAM_NO_MODULE_DATA,
-    #[error("conversation error")]
     ConversationError = pam_ffi::PAM_CONV_ERR,
-    #[error("authentication token manipulation error")]
     AuthTokError = pam_ffi::PAM_AUTHTOK_ERR,
-    #[error("authentication information cannot be recovered")]
     AuthTokRecoveryError = pam_ffi::PAM_AUTHTOK_RECOVERY_ERR,
-    #[error("authentication token lock busy")]
     AuthTokLockBusy = pam_ffi::PAM_AUTHTOK_LOCK_BUSY,
-    #[error("authentication token aging disabled")]
     AuthTokDisableAging = pam_ffi::PAM_AUTHTOK_DISABLE_AGING,
-    #[error("preliminary password check failed")]
     TryAgain = pam_ffi::PAM_TRY_AGAIN,
-    #[error("ignore underlying account module, regardless of control flag")]
     Ignore = pam_ffi::PAM_IGNORE,
-    #[error("critical error; this module should fail now")]
     Abort = pam_ffi::PAM_ABORT,
-    #[error("authentication token has expired")]
     AuthTokExpired = pam_ffi::PAM_AUTHTOK_EXPIRED,
-    #[error("module is not known")]
     ModuleUnknown = pam_ffi::PAM_MODULE_UNKNOWN,
-    #[error("bad item passed to pam_[whatever]_item")]
     BadItem = pam_ffi::PAM_BAD_ITEM,
     #[cfg(feature = "linux-pam-extensions")]
-    #[error("conversation function is event-driven and data is not available yet")]
     ConversationAgain = pam_ffi::PAM_CONV_AGAIN,
     #[cfg(feature = "linux-pam-extensions")]
-    #[error("call this function again to complete authentication stack")]
     Incomplete = pam_ffi::PAM_INCOMPLETE,
 }
 
 /// A PAM-specific Result type with an [ErrorCode] error.
 pub type Result<T> = StdResult<T, ErrorCode>;
 
+impl Display for ErrorCode {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match pam_ffi::strerror((*self).into()) {
+            Some(err) => f.write_str(err),
+            None => self.fmt_internal(f),
+        }
+    }
+}
+
+impl Error for ErrorCode {}
+
 impl ErrorCode {
     /// Converts this [Result] into a C-compatible result code.
     pub fn result_to_c<T>(value: Result<T>) -> c_int {
@@ -230,6 +217,11 @@
             value => Err((value as u32).try_into().unwrap_or(Self::SystemError)),
         }
     }
+
+    /// A basic Display implementation for if we don't link against PAM.
+    fn fmt_internal(self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "PAM error: {self:?} ({n})", n = self as c_uint)
+    }
 }
 
 /// Returned when text that should not have any `\0` bytes in it does.
--- a/src/handle.rs	Fri Jun 13 05:22:48 2025 -0400
+++ b/src/handle.rs	Sat Jun 14 09:30:16 2025 -0400
@@ -74,36 +74,36 @@
     /// # use nonstick::PamShared;
     /// # fn _doc(handle: &mut impl PamShared) -> Result<(), Box<dyn std::error::Error>> {
     /// // Get the username using the default prompt.
-    /// let user = handle.get_user(None)?;
+    /// let user = handle.username(None)?;
     /// // Get the username using a custom prompt.
     /// // If this were actually called right after the above,
     /// // both user and user_2 would have the same value.
-    /// let user_2 = handle.get_user(Some("who ARE you even???"))?;
+    /// let user_2 = handle.username(Some("who ARE you even???"))?;
     /// # 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
-    fn get_user(&mut self, prompt: Option<&str>) -> Result<&str>;
+    fn username(&mut self, prompt: Option<&str>) -> Result<&str>;
 
     trait_item!(
         /// The identity of the user for whom service is being requested.
         ///
-        /// Unlike [`get_user`](Self::get_user), this will simply get
+        /// Unlike [`username`](Self::username), this will simply get
         /// the current state of the user item, and not request the username.
-        /// While PAM usually sets this automatically in the `get_user` call,
+        /// While PAM usually sets this automatically in the `username` call,
         /// it may be changed by a module during the PAM transaction.
         /// Applications should check it after each step of the PAM process.
         get = user_item,
         item = "PAM_USER",
-        see = Self::get_user
+        see = Self::username
     );
     trait_item!(
         /// Sets the identity of the logging-in user.
         ///
         /// Usually this will be set during the course of
-        /// a [`get_user`](Self::get_user) call, but you may set it manually
+        /// a [`username`](Self::username) call, but you may set it manually
         /// or change it during the PAM process.
         set = set_user_item,
         item = "PAM_USER",
@@ -198,7 +198,7 @@
         /// Gets the user's authentication token (e.g., password).
         ///
         /// This is usually set automatically when
-        /// [`get_authtok`](PamHandleModule::get_authtok) is called,
+        /// [`authtok`](PamHandleModule::authtok) is called,
         /// but can be manually set.
         set = set_authtok_item,
         item = "PAM_AUTHTOK",
@@ -248,28 +248,28 @@
     /// # use nonstick::handle::PamHandleModule;
     /// # fn _doc(handle: &mut impl PamHandleModule) -> Result<(), Box<dyn std::error::Error>> {
     /// // Get the user's password using the default prompt.
-    /// let pass = handle.get_authtok(None)?;
+    /// let pass = handle.authtok(None)?;
     /// // Get the user's password using a custom prompt.
-    /// let pass = handle.get_authtok(Some("Reveal your secrets!"))?;
+    /// let pass = handle.authtok(Some("Reveal your secrets!"))?;
     /// 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
-    fn get_authtok(&mut self, prompt: Option<&str>) -> Result<&str>;
+    fn authtok(&mut self, prompt: Option<&str>) -> Result<&str>;
 
     trait_item!(
         /// Gets the user's authentication token (e.g., password).
         ///
         /// This is normally set automatically by PAM when calling
-        /// [`get_authtok`](Self::get_authtok), but can be set explicitly.
+        /// [`authtok`](Self::authtok), but can be set explicitly.
         ///
-        /// Like `get_authtok`, this should only ever be called
+        /// Like `authtok`, this should only ever be called
         /// by *authentication* and *password-change* PAM modules.
         get = authtok_item,
         item = "PAM_AUTHTOK",
-        see = Self::get_authtok
+        see = Self::authtok
     );
 
     trait_item!(
--- a/src/libpam/handle.rs	Fri Jun 13 05:22:48 2025 -0400
+++ b/src/libpam/handle.rs	Sat Jun 14 09:30:16 2025 -0400
@@ -8,6 +8,7 @@
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::cell::Cell;
 use std::ffi::{c_char, c_int};
+use std::marker::PhantomData;
 use std::ops::{Deref, DerefMut};
 use std::ptr;
 
@@ -27,9 +28,10 @@
 }
 
 /// An owned PAM handle.
-pub struct OwnedLibPamHandle {
+pub struct OwnedLibPamHandle<'a> {
     handle: HandleWrap,
     last_return: Cell<Result<()>>,
+    _conversation_lifetime: PhantomData<&'a mut ()>,
 }
 
 // TODO: pam_authenticate - app
@@ -42,7 +44,7 @@
 //       pam_getenv - shared
 //       pam_getenvlist - shared
 
-impl Drop for OwnedLibPamHandle {
+impl Drop for OwnedLibPamHandle<'_> {
     /// Closes the PAM session on an owned PAM handle.
     ///
     /// See the [`pam_end` manual page][man] for more information.
@@ -72,7 +74,7 @@
 }
 
 impl PamShared for LibPamHandle {
-    fn get_user(&mut self, prompt: Option<&str>) -> Result<&str> {
+    fn username(&mut self, prompt: Option<&str>) -> Result<&str> {
         let prompt = memory::option_cstr(prompt)?;
         let mut output: *const c_char = ptr::null();
         let ret = unsafe {
@@ -114,7 +116,7 @@
 }
 
 impl PamHandleModule for LibPamHandle {
-    fn get_authtok(&mut self, prompt: Option<&str>) -> Result<&str> {
+    fn 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.
@@ -221,8 +223,8 @@
     result.as_ref().map(drop).map_err(|&e| e)
 }
 
-impl PamShared for OwnedLibPamHandle {
-    delegate!(fn get_user(&mut self, prompt: Option<&str>) -> Result<&str>);
+impl PamShared for OwnedLibPamHandle<'_> {
+    delegate!(fn username(&mut self, prompt: Option<&str>) -> Result<&str>);
     delegate!(get = user_item, set = set_user_item);
     delegate!(get = service, set = set_service);
     delegate!(get = user_prompt, set = set_user_prompt);
--- a/src/libpam/module.rs	Fri Jun 13 05:22:48 2025 -0400
+++ b/src/libpam/module.rs	Sat Jun 14 09:30:16 2025 -0400
@@ -22,7 +22,7 @@
 ///
 /// 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?"))?;
+///         let password = handle.authtok(Some("what's your password?"))?;
 ///         let response =
 ///             format!("If you say your password is {password:?}, who am I to disagree?");
 ///         handle.info_msg(&response);
@@ -30,7 +30,7 @@
 ///     }
 ///
 ///     fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
-///         let username = handle.get_user(None)?;
+///         let username = handle.username(None)?;
 ///         let response = format!("Hello {username}! I trust you unconditionally.");
 ///         handle.info_msg(&response);
 ///         Ok(())
--- a/src/libpam/pam_ffi.rs	Fri Jun 13 05:22:48 2025 -0400
+++ b/src/libpam/pam_ffi.rs	Sat Jun 14 09:30:16 2025 -0400
@@ -3,8 +3,9 @@
 #![allow(non_camel_case_types)]
 
 use crate::libpam::memory::Immovable;
-use std::ffi::{c_int, c_uint, c_void};
+use std::ffi::{c_int, c_uint, c_void, CStr};
 use std::marker::PhantomData;
+use std::ptr;
 
 /// An opaque structure that a PAM handle points to.
 #[repr(C)]
@@ -28,7 +29,7 @@
 #[derive(Debug)]
 pub struct Answer {
     /// Pointer to the data returned in an answer.
-    /// For most answers, this will be a [`CStr`](std::ffi::CStr),
+    /// For most answers, this will be a [`CStr`],
     /// but for [`BinaryQAndA`](crate::conv::BinaryQAndA)s (a Linux-PAM extension),
     /// this will be [`CBinaryData`](crate::libpam::memory::CBinaryData).
     ///
@@ -53,7 +54,7 @@
     pub style: c_uint,
     /// A description of the data requested.
     ///
-    /// For most requests, this will be an owned [`CStr`](std::ffi::CStr),
+    /// For most requests, this will be an owned [`CStr`],
     /// but for requests with style `PAM_BINARY_PROMPT`,
     /// this will be `CBinaryData` (a Linux-PAM extension).
     pub data: *mut c_void,
@@ -90,6 +91,18 @@
     pub _marker: Immovable,
 }
 
+/// Gets a string version of an error message.
+pub fn strerror(code: c_uint) -> Option<&'static str> {
+    // SAFETY: Every single PAM implementation I can find (Linux-PAM, OpenPAM,
+    // Solaris, etc.) returns a static string and ignores the handle value.
+    let strerror = unsafe { pam_strerror(ptr::null_mut(), code as c_int) };
+    if strerror.is_null() {
+        None
+    } else {
+        unsafe { CStr::from_ptr(strerror) }.to_str().ok()
+    }
+}
+
 type pam_handle = LibPamHandle;
 type pam_conv = LibPamConversation<'static>;
 
--- a/src/module.rs	Fri Jun 13 05:22:48 2025 -0400
+++ b/src/module.rs	Sat Jun 14 09:30:16 2025 -0400
@@ -28,8 +28,8 @@
     ///
     /// This is probably the first thing you want to implement.
     /// In most cases, you will want to get the user and password,
-    /// using [`PamShared::get_user`](crate::PamShared::get_user)
-    /// and [`PamHandleModule::get_authtok`],
+    /// using [`PamShared::username`](crate::PamShared::username)
+    /// and [`PamHandleModule::authtok`],
     /// and verify them against something.
     ///
     /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg]