Mercurial > crates > nonstick
changeset 80:5aa1a010f1e8
Start using PAM headers; improve owned/borrowed distinction.
- Uses bindgen to generate bindings (only if needed).
- Gets the story together on owned vs. borrowed handles.
- Reduces number of mutable borrows in handle operation
(since `PamHandle` is neither `Send` nor `Sync`,
we never have to worry about thread safety.
- Improves a bunch of macros so we don't have our own
special syntax for docs.
- Implement question indirection for standard XSSO PAM implementations.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 10 Jun 2025 01:09:30 -0400 |
parents | 2128123b9406 |
children | a8f4718fed5d |
files | Cargo.toml build.rs src/constants.rs src/handle.rs src/libpam/answer.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/memory.rs src/libpam/mod.rs src/libpam/pam_ffi.rs src/libpam/question.rs src/module.rs |
diffstat | 12 files changed, 721 insertions(+), 568 deletions(-) [+] |
line wrap: on
line diff
--- a/Cargo.toml Sun Jun 08 04:21:58 2025 -0400 +++ b/Cargo.toml Tue Jun 10 01:09:30 2025 -0400 @@ -13,11 +13,17 @@ default = ["link"] # Enable this to actually link against your system's PAM library. link = [] +# Enable this to get access to Linux-PAM extensions. +linux-pam-extensions = [] +# Enable this to get access to OpenPAM features not available in Linux-PAM. +openpam = [] [dependencies] bitflags = "2.9.0" libc = "0.2.97" -num-derive = "0.4.2" -num-traits = "0.2.19" +num_enum = "0.7.3" secure-string = "0.3.0" thiserror = "2.0.12" + +[build-dependencies] +bindgen = "0.72.0"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/build.rs Tue Jun 10 01:09:30 2025 -0400 @@ -0,0 +1,58 @@ +use bindgen::MacroTypeVariation; +use std::env; +use std::path::PathBuf; + +fn main() { + if cfg!(feature = "link") { + println!("cargo::rustc-check-cfg=cfg(pam_impl, values(\"linux-pam\",\"openpam\"))"); + let common_builder = bindgen::Builder::default() + .merge_extern_blocks(true) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .blocklist_type("pam_handle") + .blocklist_type("pam_conv") + .allowlist_var(".*") + .allowlist_function("pam_start") + .allowlist_function("pam_[gs]et_item") + .allowlist_function("pam_get_user") + .allowlist_function("pam_get_authtok") + .allowlist_function("pam_end") + .dynamic_link_require_all(true) + .default_macro_constant_type(MacroTypeVariation::Signed); + + let linux_builder = common_builder.clone().header_contents( + "linux-pam.h", + r#" + #include <security/_pam_types.h> + #include <security/pam_appl.h> + #include <security/pam_ext.h> + #include <security/pam_modules.h> + "#, + ); + let openpam_builder = common_builder.clone().header_contents( + "openpam.h", + r#" + #include <security/openpam.h> + #include <security/pam_appl.h> + #include <security/pam_constants.h> + #include <security/pam_types.h> + "#, + ); + + let (pam_impl, bindings) = { + let bb = linux_builder.generate(); + bb.as_ref().unwrap(); + if let Ok(bindings) = bb { + ("linux-pam", bindings) + } else if let Ok(bindings) = openpam_builder.generate() { + ("openpam", bindings) + } else { + panic!("unrecognized PAM implementation") + } + }; + println!("cargo::rustc-cfg=pam_impl={pam_impl:?}"); + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .unwrap(); + } +}
--- a/src/constants.rs Sun Jun 08 04:21:58 2025 -0400 +++ b/src/constants.rs Tue Jun 10 01:09:30 2025 -0400 @@ -1,13 +1,79 @@ //! Constants and enum values from the PAM library. +#[cfg(feature = "link")] +use crate::libpam::pam_ffi; use bitflags::bitflags; -use libc::{c_int, c_uint}; -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; -use std::any; -use std::marker::PhantomData; +use libc::c_int; +use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::result::Result as StdResult; +/// Arbitrary values for PAM constants when not linking against system PAM. +/// +/// **The values of these constants are deliberately selected _not_ to match +/// any PAM implementations. Applications should always use the symbolic value +/// and not a magic number.** +#[cfg(not(feature = "link"))] +mod ffi { + macro_rules! define { + ($(#[$attr:meta])* $($name:ident = $value:expr),+) => { + define!( + @meta { $(#[$attr])* } + $(pub const $name: i32 = $value;)+ + ); + }; + (@meta $m:tt $($i:item)+) => { define!(@expand $($m $i)+); }; + (@expand $({ $(#[$m:meta])* } $i:item)+) => {$($(#[$m])* $i)+}; + } + const fn bit(n: i8) -> i32 { + 1 << n + } + define!( + PAM_SILENT = bit(13), + PAM_DISALLOW_NULL_AUTHTOK = bit(14), + PAM_ESTABLISH_CRED = bit(15), + PAM_DELETE_CRED = bit(16), + PAM_REINITIALIZE_CRED = bit(17), + PAM_REFRESH_CRED = bit(18), + PAM_CHANGE_EXPIRED_AUTHTOK = bit(19), + PAM_PRELIM_CHECK = bit(20), + PAM_UPDATE_AUTHTOK = bit(21) + ); + + define!( + PAM_ABORT = 513, + PAM_ACCT_EXPIRED = 514, + PAM_AUTHINFO_UNAVAIL = 515, + PAM_AUTHTOK_DISABLE_AGING = 516, + PAM_AUTHTOK_ERR = 517, + PAM_AUTHTOK_EXPIRED = 518, + PAM_AUTHTOK_LOCK_BUSY = 519, + PAM_AUTHTOK_RECOVERY_ERR = 520, + PAM_AUTH_ERR = 521, + PAM_BAD_ITEM = 522, + PAM_BUF_ERR = 533, + PAM_CONV_AGAIN = 534, + PAM_CONV_ERR = 535, + PAM_CRED_ERR = 536, + PAM_CRED_EXPIRED = 537, + PAM_CRED_INSUFFICIENT = 538, + PAM_CRED_UNAVAIL = 539, + PAM_IGNORE = 540, + PAM_INCOMPLETE = 541, + PAM_MAXTRIES = 542, + PAM_MODULE_UNKNOWN = 543, + PAM_NEW_AUTHTOK_REQD = 544, + PAM_NO_MODULE_DATA = 545, + PAM_OPEN_ERR = 546, + PAM_PERM_DENIED = 547, + PAM_SERVICE_ERR = 548, + PAM_SESSION_ERR = 549, + PAM_SYMBOL_ERR = 550, + PAM_SYSTEM_ERR = 551, + PAM_TRY_AGAIN = 552, + PAM_USER_UNKNOWN = 553 + ); +} + bitflags! { /// The available PAM flags. /// @@ -15,26 +81,26 @@ /// See `/usr/include/security/pam_modules.h` for more details. #[derive(Debug, PartialEq)] #[repr(transparent)] - pub struct Flags: c_uint { + pub struct Flags: c_int { /// The module should not generate any messages. - const SILENT = 0x8000; + const SILENT = pam_ffi::PAM_SILENT; /// The module should return [ErrorCode::AuthError] /// if the user has an empty authentication token /// rather than immediately accepting them. - const DISALLOW_NULL_AUTHTOK = 0x0001; + const DISALLOW_NULL_AUTHTOK = pam_ffi::PAM_DISALLOW_NULL_AUTHTOK; // Flag used for `set_credentials`. /// Set user credentials for an authentication service. - const ESTABLISH_CREDENTIALS = 0x0002; + const ESTABLISH_CREDENTIALS = pam_ffi::PAM_ESTABLISH_CRED; /// Delete user credentials associated with /// an authentication service. - const DELETE_CREDENTIALS = 0x0004; + const DELETE_CREDENTIALS = pam_ffi::PAM_DELETE_CRED; /// Reinitialize user credentials. - const REINITIALIZE_CREDENTIALS = 0x0008; + const REINITIALIZE_CREDENTIALS = pam_ffi::PAM_REINITIALIZE_CRED; /// Extend the lifetime of user credentials. - const REFRESH_CREDENTIALS = 0x0010; + const REFRESH_CREDENTIALS = pam_ffi::PAM_REFRESH_CRED; // Flags used for password changing. @@ -43,7 +109,7 @@ /// the password service should update all passwords. /// /// This flag is only used by `change_authtok`. - const CHANGE_EXPIRED_AUTHTOK = 0x0020; + const CHANGE_EXPIRED_AUTHTOK = pam_ffi::PAM_CHANGE_EXPIRED_AUTHTOK; /// This is a preliminary check for password changing. /// The password should not be changed. @@ -52,7 +118,7 @@ /// Applications may not use this flag. /// /// This flag is only used by `change_authtok`. - const PRELIMINARY_CHECK = 0x4000; + const PRELIMINARY_CHECK = pam_ffi::PAM_PRELIM_CHECK; /// The password should actuallyPR be updated. /// This and [Self::PRELIMINARY_CHECK] are mutually exclusive. /// @@ -60,7 +126,7 @@ /// Applications may not use this flag. /// /// This flag is only used by `change_authtok`. - const UPDATE_AUTHTOK = 0x2000; + const UPDATE_AUTHTOK = pam_ffi::PAM_UPDATE_AUTHTOK; } } @@ -70,71 +136,72 @@ /// 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, FromPrimitive)] +#[derive(Copy, Clone, Debug, PartialEq, thiserror::Error, TryFromPrimitive, IntoPrimitive)] #[non_exhaustive] // C might give us anything! +#[repr(i32)] pub enum ErrorCode { #[error("dlopen() failure when dynamically loading a service module")] - OpenError = 1, + OpenError = pam_ffi::PAM_OPEN_ERR, #[error("symbol not found")] - SymbolError = 2, + SymbolError = pam_ffi::PAM_SYMBOL_ERR, #[error("error in service module")] - ServiceError = 3, + ServiceError = pam_ffi::PAM_SERVICE_ERR, #[error("system error")] - SystemError = 4, + SystemError = pam_ffi::PAM_SYSTEM_ERR, #[error("memory buffer error")] - BufferError = 5, + BufferError = pam_ffi::PAM_BUF_ERR, #[error("permission denied")] - PermissionDenied = 6, + PermissionDenied = pam_ffi::PAM_PERM_DENIED, #[error("authentication failure")] - AuthenticationError = 7, + AuthenticationError = pam_ffi::PAM_AUTH_ERR, #[error("cannot access authentication data due to insufficient credentials")] - CredentialsInsufficient = 8, + CredentialsInsufficient = pam_ffi::PAM_CRED_INSUFFICIENT, #[error("underlying authentication service cannot retrieve authentication information")] - AuthInfoUnavailable = 9, + AuthInfoUnavailable = pam_ffi::PAM_AUTHINFO_UNAVAIL, #[error("user not known to the underlying authentication module")] - UserUnknown = 10, + UserUnknown = pam_ffi::PAM_USER_UNKNOWN, #[error("retry limit reached; do not attempt further")] - MaxTries = 11, + MaxTries = pam_ffi::PAM_MAXTRIES, #[error("new authentication token required")] - NewAuthTokRequired = 12, + NewAuthTokRequired = pam_ffi::PAM_NEW_AUTHTOK_REQD, #[error("user account has expired")] - AccountExpired = 13, + AccountExpired = pam_ffi::PAM_ACCT_EXPIRED, #[error("cannot make/remove an entry for the specified session")] - SessionError = 14, + SessionError = pam_ffi::PAM_SESSION_ERR, #[error("underlying authentication service cannot retrieve user credentials")] - CredentialsUnavailable = 15, + CredentialsUnavailable = pam_ffi::PAM_CRED_UNAVAIL, #[error("user credentials expired")] - CredentialsExpired = 16, + CredentialsExpired = pam_ffi::PAM_CRED_EXPIRED, #[error("failure setting user credentials")] - CredentialsError = 17, + CredentialsError = pam_ffi::PAM_CRED_ERR, #[error("no module-specific data is present")] - NoModuleData = 18, + NoModuleData = pam_ffi::PAM_NO_MODULE_DATA, #[error("conversation error")] - ConversationError = 19, + ConversationError = pam_ffi::PAM_CONV_ERR, #[error("authentication token manipulation error")] - AuthTokError = 20, + AuthTokError = pam_ffi::PAM_AUTHTOK_ERR, #[error("authentication information cannot be recovered")] - AuthTokRecoveryError = 21, + AuthTokRecoveryError = pam_ffi::PAM_AUTHTOK_RECOVERY_ERR, #[error("authentication token lock busy")] - AuthTokLockBusy = 22, + AuthTokLockBusy = pam_ffi::PAM_AUTHTOK_LOCK_BUSY, #[error("authentication token aging disabled")] - AuthTokDisableAging = 23, + AuthTokDisableAging = pam_ffi::PAM_AUTHTOK_DISABLE_AGING, #[error("preliminary password check failed")] - TryAgain = 24, + TryAgain = pam_ffi::PAM_TRY_AGAIN, #[error("ignore underlying account module, regardless of control flag")] - Ignore = 25, + Ignore = pam_ffi::PAM_IGNORE, #[error("critical error; this module should fail now")] - Abort = 26, + Abort = pam_ffi::PAM_ABORT, #[error("authentication token has expired")] - AuthTokExpired = 27, + AuthTokExpired = pam_ffi::PAM_AUTHTOK_EXPIRED, #[error("module is not known")] - ModuleUnknown = 28, + ModuleUnknown = pam_ffi::PAM_MODULE_UNKNOWN, #[error("bad item passed to pam_[whatever]_item")] - BadItem = 29, + BadItem = pam_ffi::PAM_BAD_ITEM, #[error("conversation function is event-driven and data is not available yet")] - ConversationAgain = 30, + ConversationAgain = pam_ffi::PAM_CONV_AGAIN, #[error("call this function again to complete authentication stack")] - Incomplete = 31, + Incomplete = pam_ffi::PAM_INCOMPLETE, } /// A PAM-specific Result type with an [ErrorCode] error. @@ -159,37 +226,6 @@ } } -impl TryFrom<c_int> for ErrorCode { - type Error = InvalidEnum<Self>; - - fn try_from(value: c_int) -> StdResult<Self, Self::Error> { - Self::from_i32(value).ok_or(value.into()) - } -} - -impl From<ErrorCode> for c_int { - fn from(val: ErrorCode) -> Self { - val as Self - } -} - -/// Error returned when attempting to coerce an invalid C integer into an enum. -#[derive(Debug, PartialEq, thiserror::Error)] -#[error("{0} is not a valid {type}", type = any::type_name::<T>())] -pub struct InvalidEnum<T>(c_int, PhantomData<T>); - -impl<T> From<InvalidEnum<T>> for c_int { - fn from(value: InvalidEnum<T>) -> Self { - value.0 - } -} - -impl<T> From<c_int> for InvalidEnum<T> { - fn from(value: c_int) -> Self { - Self(value, PhantomData) - } -} - /// 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. @@ -199,13 +235,15 @@ #[test] fn test_enums() { - assert_eq!(Ok(ErrorCode::ServiceError), 3.try_into()); - assert_eq!(Err(InvalidEnum::from(999)), ErrorCode::try_from(999)); assert_eq!(Ok(()), ErrorCode::result_from(0)); - assert_eq!(Err(ErrorCode::Abort), ErrorCode::result_from(26)); + assert_eq!( + pam_ffi::PAM_BAD_ITEM, + ErrorCode::result_to_c::<()>(Err(ErrorCode::BadItem)) + ); + assert_eq!( + Err(ErrorCode::Abort), + ErrorCode::result_from(pam_ffi::PAM_ABORT) + ); assert_eq!(Err(ErrorCode::SystemError), ErrorCode::result_from(423)); - assert!(InvalidEnum::<ErrorCode>(33, PhantomData) - .to_string() - .starts_with("33 is not a valid ")); } }
--- a/src/handle.rs Sun Jun 08 04:21:58 2025 -0400 +++ b/src/handle.rs Tue Jun 10 01:09:30 2025 -0400 @@ -1,64 +1,52 @@ //! The wrapper types and traits for handles into the PAM library. + use crate::constants::Result; use crate::conv::Conversation; macro_rules! trait_item { - (get = $getter:ident, item = $item:literal $(, see = $see:path)? $(, $($doc:literal)*)?) => { - $( - $(#[doc = $doc])* - #[doc = ""] - )? + ($(#[$md:meta])* get = $getter:ident, item = $item:literal $(, see = $see:path)?) => { + $(#[$md])* + #[doc = ""] #[doc = concat!("Gets the `", $item, "` of the PAM handle.")] $( #[doc = concat!("See [`", stringify!($see), "`].")] )? - #[doc = ""] - #[doc = "Returns a reference to the item's value, owned by PAM."] - #[doc = "The item is assumed to be valid UTF-8 text."] - #[doc = "If it is not, `ConversationError` is returned."] - #[doc = ""] - #[doc = "See the [`pam_get_item`][man] manual page,"] - #[doc = "[`pam_get_item` in the Module Writers' Guide][mwg], or"] - #[doc = "[`pam_get_item` in the Application Developers' Guide][adg]."] - #[doc = ""] - #[doc = "[man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html"] - #[doc = "[adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_get_item"] - #[doc = "[mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item"] - fn $getter(&mut self) -> Result<Option<&str>>; + /// + /// Returns a reference to the item's value, owned by PAM. + /// The item is assumed to be valid UTF-8 text. + /// If it is not, `ConversationError` is returned. + /// + /// See the [`pam_get_item`][man] manual page, + /// [`pam_get_item` in the Module Writers' Guide][mwg], or + /// [`pam_get_item` in the Application Developers' Guide][adg]. + /// + /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html + /// [adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_get_item + /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item + fn $getter(&self) -> Result<Option<&str>>; }; - (set = $setter:ident, item = $item:literal $(, see = $see:path)? $(, $($doc:literal)*)?) => { - $( - $(#[doc = $doc])* - #[doc = ""] - )? + ($(#[$md:meta])* set = $setter:ident, item = $item:literal $(, see = $see:path)?) => { + $(#[$md])* #[doc = concat!("Sets the `", $item, "` from the PAM handle.")] $( #[doc = concat!("See [`", stringify!($see), "`].")] )? - #[doc = ""] - #[doc = "Sets the item's value. PAM copies the string's contents."] - #[doc = "If the string contains a null byte, this will return "] - #[doc = "a `ConversationError`."] - #[doc = ""] - #[doc = "See the [`pam_set_item`][man] manual page,"] - #[doc = "[`pam_set_item` in the Module Writers' Guide][mwg], or"] - #[doc = "[`pam_set_item` in the Application Developers' Guide][adg]."] - #[doc = ""] - #[doc = "[man]: https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html"] - #[doc = "[adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_set_item"] - #[doc = "[mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_item"] + /// + /// Sets the item's value. PAM copies the string's contents. + /// If the string contains a null byte, this will return + /// a `ConversationError`. + /// + /// See the [`pam_set_item`][man] manual page, + /// [`pam_set_item` in the Module Writers' Guide][mwg], or + /// [`pam_set_item` in the Application Developers' Guide][adg]. + /// + /// [man]: https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html + /// [adg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/adg-interface-by-app-expected.html#adg-pam_set_item + /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_set_item fn $setter(&mut self, value: Option<&str>) -> Result<()>; }; } -/// All-in-one trait for what you should expect from PAM as an application. -pub trait PamHandleApplication: PamApplicationOnly + PamShared {} -impl<T> PamHandleApplication for T where T: PamApplicationOnly + PamShared {} - -/// All-in-one trait for what you should expect from PAM as a module. -pub trait PamHandleModule: PamModuleOnly + PamShared {} -impl<T> PamHandleModule for T where T: PamModuleOnly + PamShared {} - /// Functionality for both PAM applications and PAM modules. /// /// This base trait includes features of a PAM handle that are available @@ -100,130 +88,130 @@ fn get_user(&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 + /// the current state of the user item, and not request the username. + /// While PAM usually sets this automatically in the `get_user` 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, - "The identity of the user for whom service is being requested." - "" - "Unlike [`get_user`](Self::get_user), 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, " - "it may be changed by a module during the PAM transaction. " - "Applications should check it after each step of the PAM process." + see = Self::get_user ); 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 + /// or change it during the PAM process. set = set_user_item, item = "PAM_USER", - see = Self::user_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 " - "or change it during the PAM process." + see = Self::user_item ); trait_item!( + /// The service name, which identifies the PAM stack which is used + /// to perform authentication. get = service, - item = "PAM_SERVICE", - "The service name, which identifies the PAM stack which is used " - "to perform authentication." + item = "PAM_SERVICE" ); trait_item!( + /// The service name, which identifies the PAM stack which is used + /// to perform authentication. It's probably a bad idea to change this. set = set_service, item = "PAM_SERVICE", - see = Self::service, - "The service name, which identifies the PAM stack which is used " - "to perform authentication. It's probably a bad idea to change this." + see = Self::service ); trait_item!( + /// The string used to prompt for a user's name. + /// By default, this is a localized version of `login: `. get = user_prompt, - item = "PAM_USER_PROMPT", - "The string used to prompt for a user's name." - "By default, this is a localized version of `login: `." + item = "PAM_USER_PROMPT" ); trait_item!( + /// Sets the string used to prompt for a user's name. set = set_user_prompt, item = "PAM_USER_PROMPT", - see = Self::user_prompt, - "Sets the string used to prompt for a user's name." + see = Self::user_prompt ); trait_item!( + /// "The terminal name prefixed by /dev/ for device files." + /// + /// This is the terminal the user is logging in on. + /// Very old applications may use this instead of `PAM_XDISPLAY`. get = tty_name, - item = "PAM_TTY", - "\"The terminal name prefixed by /dev/ for device files.\"" - "" - "This is the terminal the user is logging in on." - "Very old applications may use this instead of `PAM_XDISPLAY`." + item = "PAM_TTY" ); trait_item!( + /// Sets the terminal name. + /// + /// (TODO: See if libpam sets this itself or if the application does.) set = set_tty_name, item = "PAM_TTY", - see = Self::tty_name, - "Sets the terminal name." - "" - "(TODO: See if libpam sets this itself or if the application does.)" + see = Self::tty_name ); trait_item!( + /// If set, the identity of the remote user logging in. + /// + /// This is only as trustworthy as the application calling PAM. + /// Also see [`remote_host`](Self::remote_host). get = remote_user, - item = "PAM_RUSER", - "If set, the identity of the remote user logging in." - "" - "This is only as trustworthy as the application calling PAM." - "Also see [`remote_host`](Self::remote_host)." + item = "PAM_RUSER" ); trait_item!( + /// Sets the identity of the remote user logging in. + /// + /// This is usually set by the application before making calls + /// into a PAM session. (TODO: check this!) set = set_remote_user, - item = "PAM_RUSER", - "Sets the identity of the remote user logging in." - "" - "This is usually set by the application before making calls " - "into a PAM session. (TODO: check this!)" + item = "PAM_RUSER" ); trait_item!( + /// If set, the remote location where the user is coming from. + /// + /// This is only as trustworthy as the application calling PAM. + /// This can be combined with [`Self::remote_user`] to identify + /// the account the user is attempting to log in from, + /// with `remote_user@remote_host`. + /// + /// If unset, "it is unclear where the authentication request + /// is originating from." get = remote_host, - item = "PAM_RHOST", - "If set, the remote location where the user is coming from." - "" - "This is only as trustworthy as the application calling PAM. " - "This can be combined with [`Self::remote_user`] to identify " - "the account the user is attempting to log in from, " - "with `remote_user@remote_host`." - "" - "If unset, \"it is unclear where the authentication request " - "is originating from.\"" + item = "PAM_RHOST" ); trait_item!( + /// Sets the location where the user is coming from. + /// + /// This is usually set by the application before making calls + /// into a PAM session. (TODO: check this!) set = set_remote_host, item = "PAM_RHOST", - see = Self::remote_host, - "Sets the location where the user is coming from." - "" - "This is usually set by the application before making calls " - "into a PAM session. (TODO: check this!)" + see = Self::remote_host ); trait_item!( + /// Gets the user's authentication token (e.g., password). + /// + /// This is usually set automatically when + /// [`get_authtok`](PamHandleModule::get_authtok) is called, + /// but can be manually set. set = set_authtok_item, item = "PAM_AUTHTOK", - see = PamModuleOnly::authtok_item, - "Gets the user's authentication token (e.g., password)." - "" - "This is usually set automatically when " - "[`get_authtok`](PamModuleOnly::get_authtok) is called, " - "but can be manually set." + see = PamHandleModule::authtok_item ); trait_item!( + /// Sets the user's "old authentication token" when changing passwords. + // + /// This is usually set automatically by PAM. set = set_old_authtok_item, item = "PAM_OLDAUTHTOK", - see = PamModuleOnly::old_authtok_item, - "Sets the user's \"old authentication token\" when changing passwords." - "" - "This is usually set automatically by PAM." + see = PamHandleModule::old_authtok_item ); } @@ -234,29 +222,8 @@ /// /// Like [`PamShared`], this is intended to allow creating mock implementations /// of PAM for testing PAM applications. -pub trait PamApplicationOnly { - /// 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::handle::PamApplicationOnly; - /// # use std::error::Error; - /// # fn _doc(handle: impl PamApplicationOnly, 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<()>; +pub trait PamHandleApplication: PamShared { + // reserved! } /// Functionality of a PAM handle that can be expected by a PAM module. @@ -266,7 +233,7 @@ /// /// Like [`PamShared`], this is intended to allow creating mock implementations /// of PAM for testing PAM modules. -pub trait PamModuleOnly: Conversation { +pub trait PamHandleModule: Conversation + PamShared { /// Retrieves the authentication token from the user. /// /// This should only be used by *authentication* and *password-change* @@ -278,8 +245,8 @@ /// # Example /// /// ```no_run - /// # use nonstick::handle::PamModuleOnly; - /// # fn _doc(handle: &mut impl PamModuleOnly) -> Result<(), Box<dyn std::error::Error>> { + /// # 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)?; /// // Get the user's password using a custom prompt. @@ -293,25 +260,25 @@ fn get_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. + /// + /// Like `get_authtok`, this should only ever be called + /// by *authentication* and *password-change* PAM modules. get = authtok_item, item = "PAM_AUTHTOK", - see = Self::get_authtok, - "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." - "" - "Like `get_authtok`, this should only ever be called " - "by *authentication* and *password-change* PAM modules." + see = Self::get_authtok ); trait_item!( + /// Gets the user's old authentication token when changing passwords. + /// + /// This should only ever be called by *password-change* PAM modules. get = old_authtok_item, item = "PAM_OLDAUTHTOK", - see = PamShared::set_old_authtok_item, - "Gets the user's old authentication token when changing passwords." - "" - "This should only ever be called by *password-change* PAM modules." + see = PamShared::set_old_authtok_item ); /*
--- a/src/libpam/answer.rs Sun Jun 08 04:21:58 2025 -0400 +++ b/src/libpam/answer.rs Tue Jun 10 01:09:30 2025 -0400 @@ -2,9 +2,10 @@ use crate::libpam::conversation::OwnedMessage; use crate::libpam::memory; -use crate::libpam::memory::{CBinaryData, Immovable}; +use crate::libpam::memory::CBinaryData; +pub use crate::libpam::pam_ffi::Answer; use crate::{ErrorCode, Result}; -use std::ffi::{c_int, c_void, CStr}; +use std::ffi::CStr; use std::ops::{Deref, DerefMut}; use std::{iter, mem, ptr, slice}; @@ -195,23 +196,6 @@ } } -/// Generic version of answer data. -/// -/// This has the same structure as [`BinaryAnswer`] -/// and [`TextAnswer`]. -#[repr(C)] -#[derive(Debug)] -pub struct Answer { - /// Pointer to the data returned in an answer. - /// For most answers, this will be a [`CStr`], but for answers to - /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`] - /// (a Linux-PAM extension). - data: *mut c_void, - /// Unused. - return_code: c_int, - _marker: Immovable, -} - impl Answer { /// Frees the contents of this answer. ///
--- a/src/libpam/conversation.rs Sun Jun 08 04:21:58 2025 -0400 +++ b/src/libpam/conversation.rs Tue Jun 10 01:09:30 2025 -0400 @@ -3,50 +3,15 @@ }; use crate::libpam::answer::{Answer, Answers, BinaryAnswer, TextAnswer}; use crate::libpam::memory::Immovable; -use crate::libpam::question::{Indirect, Questions}; +use crate::libpam::pam_ffi::AppData; +pub use crate::libpam::pam_ffi::LibPamConversation; +use crate::libpam::question::{Indirect, IndirectTrait, Question, Questions}; use crate::ErrorCode; use crate::Result; use std::ffi::c_int; use std::iter; use std::marker::PhantomData; -/// 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 [`Answer`]s, -/// which PAM sets in response to a module's request. -/// This is an array of structs, not an array of pointers to a struct. -/// There should always be exactly as many `responses` as `num_msg`. -/// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed. -pub type ConversationCallback = unsafe extern "C" fn( - num_msg: c_int, - messages: *const Indirect, - responses: *mut *mut Answer, - appdata: *mut AppData, -) -> c_int; - -/// The type used by PAM to call back into a conversation. -#[repr(C)] -pub struct LibPamConversation<'a> { - /// The function that is called to get information from the user. - callback: ConversationCallback, - /// The pointer that will be passed as the last parameter - /// to the conversation callback. - appdata: *mut AppData, - life: PhantomData<&'a mut ()>, - _marker: Immovable, -} - impl LibPamConversation<'_> { fn wrap<C: Conversation>(conv: &mut C) -> Self { Self { @@ -59,7 +24,7 @@ unsafe extern "C" fn wrapper_callback<C: Conversation>( count: c_int, - questions: *const Indirect, + questions: *const *const Question, answers: *mut *mut Answer, me: *mut AppData, ) -> c_int { @@ -69,7 +34,7 @@ .cast::<C>() .as_mut() .ok_or(ErrorCode::ConversationError)?; - let indirect = questions.as_ref().ok_or(ErrorCode::ConversationError)?; + let indirect = Indirect::borrow_ptr(questions).ok_or(ErrorCode::ConversationError)?; let answers_ptr = answers.as_mut().ok_or(ErrorCode::ConversationError)?; // Build our owned list of Q&As from the questions we've been asked
--- a/src/libpam/handle.rs Sun Jun 08 04:21:58 2025 -0400 +++ b/src/libpam/handle.rs Tue Jun 10 01:09:30 2025 -0400 @@ -1,110 +1,66 @@ use super::conversation::LibPamConversation; -use crate::constants::{ErrorCode, InvalidEnum, Result}; +use crate::constants::{ErrorCode, Result}; use crate::conv::Message; -use crate::handle::{PamApplicationOnly, PamModuleOnly, PamShared}; -use crate::libpam::memory; -use crate::libpam::memory::Immovable; -use crate::Conversation; -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; +use crate::handle::PamShared; +pub use crate::libpam::pam_ffi::LibPamHandle; +use crate::libpam::{memory, pam_ffi}; +use crate::{Conversation, PamHandleModule}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use std::cell::Cell; use std::ffi::{c_char, c_int}; use std::ops::{Deref, DerefMut}; -use std::result::Result as StdResult; -use std::{mem, ptr}; - -/// An owned PAM handle. -#[repr(transparent)] -pub struct OwnedLibPamHandle(*mut LibPamHandle); - -/// An opaque structure that a PAM handle points to. -#[repr(C)] -pub struct LibPamHandle { - _data: (), - _marker: Immovable, -} - -impl LibPamHandle { - /// Gets a C string item. - /// - /// # Safety - /// - /// You better be requesting an item which is a C string. - unsafe fn get_cstr_item(&mut self, item_type: ItemType) -> Result<Option<&str>> { - let mut output = ptr::null(); - let ret = unsafe { super::pam_get_item(self, item_type as c_int, &mut output) }; - ErrorCode::result_from(ret)?; - memory::wrap_string(output.cast()) - } +use std::ptr; - /// Sets a C string item. - /// - /// # Safety - /// - /// You better be setting an item which is a C string. - unsafe fn set_cstr_item(&mut self, item_type: ItemType, data: Option<&str>) -> Result<()> { - let data_str = memory::option_cstr(data)?; - let ret = unsafe { - super::pam_set_item( - self, - item_type as c_int, - memory::prompt_ptr(data_str.as_ref()).cast(), - ) - }; - ErrorCode::result_from(ret) - } +struct HandleWrap(*mut LibPamHandle); - /// Gets the `PAM_CONV` item from the handle. - fn conversation_item(&mut self) -> Result<&mut LibPamConversation<'_>> { - let output: *mut LibPamConversation = ptr::null_mut(); - let result = unsafe { - super::pam_get_item( - self, - ItemType::Conversation.into(), - &mut output.cast_const().cast(), - ) - }; - ErrorCode::result_from(result)?; - // SAFETY: We got this result from PAM, and we're checking if it's null. - unsafe { output.as_mut() }.ok_or(ErrorCode::ConversationError) - } -} - -impl PamApplicationOnly for OwnedLibPamHandle { - fn close(self, status: Result<()>) -> Result<()> { - let ret = unsafe { super::pam_end(self.0, ErrorCode::result_to_c(status)) }; - // Forget rather than dropping, since dropping also calls pam_end. - mem::forget(self); - ErrorCode::result_from(ret) - } -} - -impl Deref for OwnedLibPamHandle { +impl Deref for HandleWrap { type Target = LibPamHandle; fn deref(&self) -> &Self::Target { unsafe { &*self.0 } } } -impl DerefMut for OwnedLibPamHandle { +impl DerefMut for HandleWrap { fn deref_mut(&mut self) -> &mut Self::Target { unsafe { &mut *self.0 } } } +/// An owned PAM handle. +pub struct OwnedLibPamHandle { + handle: HandleWrap, + last_return: Cell<Result<()>>, +} + +// TODO: pam_authenticate - app +// pam_setcred - app +// pam_acct_mgmt - app +// pam_chauthtok - app +// pam_open_session - app +// pam_close_session - app +// pam_putenv - shared +// pam_getenv - shared +// pam_getenvlist - shared + impl Drop for OwnedLibPamHandle { - /// Ends the PAM session with a zero error code. - /// You probably want to call [`close`](Self::close) instead of - /// letting this drop by itself. + /// Closes the PAM session on an owned PAM handle. + /// + /// See the [`pam_end` manual page][man] for more information. + /// + /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html fn drop(&mut self) { unsafe { - super::pam_end(self.0, 0); + pam_ffi::pam_end( + self.handle.0, + ErrorCode::result_to_c(self.last_return.get()), + ); } } } macro_rules! cstr_item { (get = $getter:ident, item = $item_type:path) => { - fn $getter(&mut self) -> Result<Option<&str>> { + fn $getter(&self) -> Result<Option<&str>> { unsafe { self.get_cstr_item($item_type) } } }; @@ -119,8 +75,9 @@ fn get_user(&mut self, prompt: Option<&str>) -> Result<&str> { let prompt = memory::option_cstr(prompt)?; let mut output: *const c_char = ptr::null(); - let ret = - unsafe { super::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref())) }; + let ret = unsafe { + pam_ffi::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref())) + }; ErrorCode::result_from(ret)?; unsafe { memory::wrap_string(output) } .transpose() @@ -156,13 +113,13 @@ } } -impl PamModuleOnly for LibPamHandle { +impl PamHandleModule for LibPamHandle { fn get_authtok(&mut self, prompt: Option<&str>) -> Result<&str> { let prompt = memory::option_cstr(prompt)?; let mut output: *const c_char = ptr::null_mut(); // SAFETY: We're calling this with known-good values. let res = unsafe { - super::pam_get_authtok( + pam_ffi::pam_get_authtok( self, ItemType::AuthTok.into(), &mut output, @@ -190,9 +147,95 @@ } } +impl LibPamHandle { + /// Gets a C string item. + /// + /// # Safety + /// + /// You better be requesting an item which is a C string. + unsafe fn get_cstr_item(&self, item_type: ItemType) -> Result<Option<&str>> { + let mut output = ptr::null(); + let ret = unsafe { pam_ffi::pam_get_item(self, item_type as c_int, &mut output) }; + ErrorCode::result_from(ret)?; + memory::wrap_string(output.cast()) + } + + /// Sets a C string item. + /// + /// # Safety + /// + /// You better be setting an item which is a C string. + unsafe fn set_cstr_item(&mut self, item_type: ItemType, data: Option<&str>) -> Result<()> { + let data_str = memory::option_cstr(data)?; + let ret = unsafe { + pam_ffi::pam_set_item( + self, + item_type as c_int, + memory::prompt_ptr(data_str.as_ref()).cast(), + ) + }; + ErrorCode::result_from(ret) + } + + /// Gets the `PAM_CONV` item from the handle. + fn conversation_item(&mut self) -> Result<&mut LibPamConversation<'_>> { + let output: *mut LibPamConversation = ptr::null_mut(); + let result = unsafe { + pam_ffi::pam_get_item( + self, + ItemType::Conversation.into(), + &mut output.cast_const().cast(), + ) + }; + ErrorCode::result_from(result)?; + // SAFETY: We got this result from PAM, and we're checking if it's null. + unsafe { output.as_mut() }.ok_or(ErrorCode::ConversationError) + } +} + +macro_rules! delegate { + (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => { + fn $meth(&self $(, $param: $typ)*) -> Result<$ret> { + let result = self.handle.$meth($($param),*); + self.last_return.set(split(&result)); + result + } + }; + (fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => { + fn $meth(&mut self $(, $param: $typ)*) -> Result<$ret> { + let result = self.handle.$meth($($param),*); + self.last_return.set(split(&result)); + result + } + }; + (get = $get:ident$(, set = $set:ident)?) => { + delegate!(fn $get(&self) -> Result<Option<&str>>); + $(delegate!(set = $set);)? + }; + (set = $set:ident) => { + delegate!(fn $set(&mut self, value: Option<&str>) -> Result<()>); + }; +} + +fn split<T>(result: &Result<T>) -> Result<()> { + result.as_ref().map(drop).map_err(|&e| e) +} + +impl PamShared for OwnedLibPamHandle { + delegate!(fn get_user(&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); + delegate!(get = tty_name, set = set_tty_name); + delegate!(get = remote_user, set = set_remote_user); + delegate!(get = remote_host, set = set_remote_host); + delegate!(set = set_authtok_item); + delegate!(set = set_old_authtok_item); +} + /// Identifies what is being gotten or set with `pam_get_item` /// or `pam_set_item`. -#[derive(FromPrimitive)] +#[derive(TryFromPrimitive, IntoPrimitive)] #[repr(i32)] #[non_exhaustive] // because C could give us anything! pub enum ItemType { @@ -223,16 +266,3 @@ /// The type of `pam_get_authtok`. AuthTokType = 13, } - -impl TryFrom<c_int> for ItemType { - type Error = InvalidEnum<Self>; - fn try_from(value: c_int) -> StdResult<Self, Self::Error> { - Self::from_i32(value).ok_or(value.into()) - } -} - -impl From<ItemType> for c_int { - fn from(val: ItemType) -> Self { - val as Self - } -}
--- a/src/libpam/memory.rs Sun Jun 08 04:21:58 2025 -0400 +++ b/src/libpam/memory.rs Tue Jun 10 01:09:30 2025 -0400 @@ -25,7 +25,7 @@ /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. #[repr(C)] -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Immovable(pub PhantomData<(*mut u8, PhantomPinned)>); /// Safely converts a `&str` option to a `CString` option.
--- a/src/libpam/mod.rs Sun Jun 08 04:21:58 2025 -0400 +++ b/src/libpam/mod.rs Tue Jun 10 01:09:30 2025 -0400 @@ -13,52 +13,8 @@ mod handle; mod memory; mod module; +pub(crate) mod pam_ffi; mod question; +#[doc(inline)] pub use handle::{LibPamHandle, OwnedLibPamHandle}; -use std::ffi::{c_char, c_int, c_void}; - -#[link(name = "pam")] -extern "C" { - fn pam_get_data( - pamh: *mut LibPamHandle, - module_data_name: *const c_char, - data: &mut *const c_void, - ) -> c_int; - - fn pam_set_data( - pamh: *mut LibPamHandle, - module_data_name: *const c_char, - data: *const c_void, - cleanup: extern "C" fn(pamh: *const c_void, data: *mut c_void, error_status: c_int), - ) -> c_int; - - fn pam_get_item(pamh: *mut LibPamHandle, item_type: c_int, item: &mut *const c_void) -> c_int; - - fn pam_set_item(pamh: *mut LibPamHandle, item_type: c_int, item: *const c_void) -> c_int; - - fn pam_get_user( - pamh: *mut LibPamHandle, - user: &mut *const c_char, - prompt: *const c_char, - ) -> c_int; - - fn pam_get_authtok( - pamh: *mut LibPamHandle, - item_type: c_int, - data: &mut *const c_char, - prompt: *const c_char, - ) -> c_int; - - fn pam_end(pamh: *mut LibPamHandle, status: c_int) -> c_int; - - // TODO: pam_authenticate - app - // pam_setcred - app - // pam_acct_mgmt - app - // pam_chauthtok - app - // pam_open_session - app - // pam_close_session - app - // pam_putenv - shared - // pam_getenv - shared - // pam_getenvlist - shared -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/pam_ffi.rs Tue Jun 10 01:09:30 2025 -0400 @@ -0,0 +1,115 @@ +//! The types that are directly represented in PAM function signatures. + +#![allow(non_camel_case_types)] + +use crate::libpam::memory::Immovable; +use std::ffi::{c_int, c_void}; +use std::marker::PhantomData; +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +/// An opaque structure that a PAM handle points to. +#[repr(C)] +pub struct LibPamHandle { + _data: (), + _marker: Immovable, +} + +/// An opaque structure that is passed through PAM in a conversation. +#[repr(C)] +pub struct AppData { + _data: (), + _marker: Immovable, +} + +/// Generic version of answer data. +/// +/// This has the same structure as [`BinaryAnswer`](crate::libpam::answer::BinaryAnswer) +/// and [`TextAnswer`](crate::libpam::answer::TextAnswer). +#[repr(C)] +#[derive(Debug)] +pub struct Answer { + /// Pointer to the data returned in an answer. + /// For most answers, this will be a [`CStr`](std::ffi::CStr), + /// but for [`BinaryQAndA`](crate::conv::BinaryQAndA)s (a Linux-PAM extension), + /// this will be [`CBinaryData`](crate::libpam::memory::CBinaryData) + pub data: *mut c_void, + /// Unused. + return_code: c_int, + _marker: Immovable, +} + +/// The C enum values for messages shown to the user. +#[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] +#[repr(i32)] +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, +} + +/// A question sent by PAM or a module to an application. +/// +/// PAM refers to this as a "message", but we call it a question +/// to avoid confusion with [`Message`](crate::Message). +/// +/// This question, and its internal data, is owned by its creator +/// (either the module or PAM itself). +#[repr(C)] +pub struct Question { + /// The style of message to request. + pub style: c_int, + /// A description of the data requested. + /// + /// For most requests, this will be an owned [`CStr`](std::ffi::CStr), but for requests + /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] + /// (a Linux-PAM extension). + pub data: *mut c_void, + pub _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. +/// - `questions` is a pointer to the [`Question`]s being sent to the user. +/// For information about its structure, +/// see [`GenericQuestions`](super::question::GenericQuestions). +/// - `answers` is a pointer to an array of [`Answer`]s, +/// which PAM sets in response to a module's request. +/// This is an array of structs, not an array of pointers to a struct. +/// There should always be exactly as many `answers` as `num_msg`. +/// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed. +pub type ConversationCallback = unsafe extern "C" fn( + num_msg: c_int, + questions: *const *const Question, + answers: *mut *mut Answer, + appdata: *mut AppData, +) -> c_int; + +/// The type used by PAM to call back into a conversation. +#[repr(C)] +pub struct LibPamConversation<'a> { + /// The function that is called to get information from the user. + pub callback: ConversationCallback, + /// The pointer that will be passed as the last parameter + /// to the conversation callback. + pub appdata: *mut AppData, + pub life: PhantomData<&'a mut ()>, + pub _marker: Immovable, +} + +type pam_handle = LibPamHandle; +type pam_conv = LibPamConversation<'static>; + +include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
--- a/src/libpam/question.rs Sun Jun 08 04:21:58 2025 -0400 +++ b/src/libpam/question.rs Tue Jun 10 01:09:30 2025 -0400 @@ -1,16 +1,13 @@ //! Data and types dealing with PAM messages. -use crate::constants::InvalidEnum; use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA}; use crate::libpam::conversation::OwnedMessage; use crate::libpam::memory; use crate::libpam::memory::{CBinaryData, Immovable}; +pub use crate::libpam::pam_ffi::{Question, Style}; use crate::ErrorCode; use crate::Result; -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; -use std::ffi::{c_int, c_void, CStr}; -use std::result::Result as StdResult; +use std::ffi::{c_void, CStr}; use std::{iter, ptr, slice}; /// Abstraction of a collection of questions to be sent in a PAM conversation. @@ -57,21 +54,19 @@ /// ╟──────────────╢ /// ║ ... ║ /// ``` -/// -/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** -pub struct Questions { +pub struct GenericQuestions<I: IndirectTrait> { /// An indirection to the questions themselves, stored on the C heap. - indirect: *mut Indirect, + indirect: *mut I, /// The number of questions. count: usize, } -impl Questions { +impl<I: IndirectTrait> GenericQuestions<I> { /// Stores the provided questions on the C heap. pub fn new(messages: &[Message]) -> Result<Self> { let count = messages.len(); let mut ret = Self { - indirect: Indirect::alloc(count), + indirect: I::alloc(count), count, }; // Even if we fail partway through this, all our memory will be freed. @@ -82,8 +77,8 @@ } /// The pointer to the thing with the actual list. - pub fn indirect(&self) -> *const Indirect { - self.indirect + pub fn indirect(&self) -> *const *const Question { + self.indirect.cast() } pub fn iter(&self) -> impl Iterator<Item = &Question> { @@ -96,13 +91,13 @@ } } -impl Drop for Questions { +impl<I: IndirectTrait> Drop for GenericQuestions<I> { fn drop(&mut self) { // SAFETY: We are valid and have a valid pointer. // Once we're done, everything will be safe. unsafe { if let Some(indirect) = self.indirect.as_mut() { - indirect.free(self.count) + indirect.free_contents(self.count) } memory::free(self.indirect); self.indirect = ptr::null_mut(); @@ -110,56 +105,131 @@ } } +/// The trait that each of the `Indirect` implementations implement. +/// +/// Basically a slice but with more meat. +pub trait IndirectTrait { + /// Converts a pointer into a borrowed `Self`. + /// + /// # Safety + /// + /// You have to provide a valid pointer. + unsafe fn borrow_ptr<'a>(ptr: *const *const Question) -> Option<&'a Self> + where + Self: Sized, + { + ptr.cast::<Self>().as_ref() + } + + /// Allocates memory for this indirector and all its members. + fn alloc(count: usize) -> *mut Self; + + /// Returns an iterator yielding the given number of messages. + /// + /// # Safety + /// + /// You have to provide the right count. + unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question>; + + /// Returns a mutable iterator yielding the given number of messages. + /// + /// # Safety + /// + /// You have to provide the right count. + unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question>; + + /// Frees everything this points to. + /// + /// # Safety + /// + /// You have to pass the right size. + unsafe fn free_contents(&mut self, count: usize); +} + +/// An indirect reference to messages. +/// +/// This is kept separate to provide a place where we can separate +/// the pointer-to-pointer-to-list from pointer-to-list-of-pointers. +#[cfg(pam_impl = "linux-pam")] +pub type Indirect = LinuxPamIndirect; + /// An indirect reference to messages. /// /// This is kept separate to provide a place where we can separate /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers. +#[cfg(not(pam_impl = "linux-pam"))] +pub type Indirect = XssoIndirect; + +pub type Questions = GenericQuestions<Indirect>; + +/// The XSSO standard version of the indirection layer between Question and Questions. #[repr(transparent)] -pub struct Indirect { +pub struct StandardIndirect { + base: *mut Question, + _marker: Immovable, +} + +impl IndirectTrait for StandardIndirect { + fn alloc(count: usize) -> *mut Self { + let questions = memory::calloc(count); + let me_ptr: *mut Self = memory::calloc(1); + // SAFETY: We just allocated this, and we're putting a valid pointer in. + unsafe { + let me = &mut *me_ptr; + me.base = questions; + } + me_ptr + } + + unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { + (0..count).map(|idx| &*self.base.add(idx)) + } + + unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { + (0..count).map(|idx| &mut *self.base.add(idx)) + } + + unsafe fn free_contents(&mut self, count: usize) { + let msgs = slice::from_raw_parts_mut(self.base, count); + for msg in msgs { + msg.clear() + } + memory::free(self.base); + self.base = ptr::null_mut() + } +} + +/// The Linux version of the indirection layer between Question and Questions. +#[repr(transparent)] +pub struct LinuxPamIndirect { base: [*mut Question; 0], _marker: Immovable, } -impl Indirect { - /// Allocates memory for this indirector and all its members. +impl IndirectTrait for LinuxPamIndirect { fn alloc(count: usize) -> *mut Self { // SAFETY: We're only allocating, and when we're done, // everything will be in a known-good state. - let me_ptr: *mut Indirect = memory::calloc::<Question>(count).cast(); + let me_ptr: *mut Self = memory::calloc::<*mut Question>(count).cast(); unsafe { let me = &mut *me_ptr; let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count); for entry in ptr_list { *entry = memory::calloc(1); } - me } + me_ptr } - /// Returns an iterator yielding the given number of messages. - /// - /// # Safety - /// - /// You have to provide the right count. - pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { + unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { (0..count).map(|idx| &**self.base.as_ptr().add(idx)) } - /// Returns a mutable iterator yielding the given number of messages. - /// - /// # Safety - /// - /// You have to provide the right count. - pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { + unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx)) } - /// Frees everything this points to. - /// - /// # Safety - /// - /// You have to pass the right size. - unsafe fn free(&mut self, count: usize) { + unsafe fn free_contents(&mut self, count: usize) { let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count); for msg in msgs { if let Some(msg) = msg.as_mut() { @@ -171,66 +241,23 @@ } } -/// 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 Default for Question { + fn default() -> Self { + Self { + style: Default::default(), + data: ptr::null_mut(), + _marker: Default::default(), + } } } -impl From<Style> for c_int { - fn from(val: Style) -> Self { - val as Self - } -} - -/// A question sent by PAM or a module to an application. -/// -/// PAM refers to this as a "message", but we call it a question -/// to avoid confusion with [`Message`]. -/// -/// This question, and its internal data, is owned by its creator -/// (either the module or PAM itself). -#[repr(C)] -pub struct Question { - /// The style of message to request. - style: c_int, - /// A description of the data requested. - /// - /// For most requests, this will be an owned [`CStr`], but for requests - /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] - /// (a Linux-PAM extension). - data: *mut c_void, - _marker: Immovable, -} - impl Question { /// Replaces the contents of this question with the question /// from the message. pub fn fill(&mut self, msg: &Message) -> Result<()> { let (style, data) = copy_to_heap(msg)?; self.clear(); - self.style = style as c_int; + self.style = style.into(); self.data = data; Ok(()) } @@ -327,45 +354,52 @@ #[cfg(test)] mod tests { - use super::{MaskedQAndA, Questions, Result}; - use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, QAndA, RadioQAndA}; - use crate::libpam::conversation::OwnedMessage; + use super::{ + BinaryQAndA, ErrorMsg, GenericQuestions, IndirectTrait, InfoMsg, LinuxPamIndirect, + MaskedQAndA, OwnedMessage, QAndA, RadioQAndA, Result, StandardIndirect, + }; - #[test] - fn test_round_trip() { - let interrogation = Questions::new(&[ - MaskedQAndA::new("hocus pocus").message(), - BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), - QAndA::new("what").message(), - QAndA::new("who").message(), - InfoMsg::new("hey").message(), - ErrorMsg::new("gasp").message(), - RadioQAndA::new("you must choose").message(), - ]) - .unwrap(); - let indirect = interrogation.indirect(); + macro_rules! assert_matches { + ($id:ident => $variant:path, $q:expr) => { + if let $variant($id) = $id { + assert_eq!($q, $id.question()); + } else { + panic!("mismatched enum variant {x:?}", x = $id); + } + }; + } - let remade = unsafe { indirect.as_ref() }.unwrap(); - let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } - .map(TryInto::try_into) - .collect::<Result<_>>() + macro_rules! tests { ($fn_name:ident<$typ:ident>) => { + #[test] + fn $fn_name() { + let interrogation = GenericQuestions::<$typ>::new(&[ + MaskedQAndA::new("hocus pocus").message(), + BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), + QAndA::new("what").message(), + QAndA::new("who").message(), + InfoMsg::new("hey").message(), + ErrorMsg::new("gasp").message(), + RadioQAndA::new("you must choose").message(), + ]) .unwrap(); - let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap(); - macro_rules! assert_matches { - ($id:ident => $variant:path, $q:expr) => { - if let $variant($id) = $id { - assert_eq!($q, $id.question()); - } else { - panic!("mismatched enum variant {x:?}", x = $id); - } - }; + let indirect = interrogation.indirect(); + + let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap(); + let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } + .map(TryInto::try_into) + .collect::<Result<_>>() + .unwrap(); + let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap(); + assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); + assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); + assert_matches!(what => OwnedMessage::Prompt, "what"); + assert_matches!(who => OwnedMessage::Prompt, "who"); + assert_matches!(hey => OwnedMessage::Info, "hey"); + assert_matches!(gasp => OwnedMessage::Error, "gasp"); + assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); } - assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); - assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); - assert_matches!(what => OwnedMessage::Prompt, "what"); - assert_matches!(who => OwnedMessage::Prompt, "who"); - assert_matches!(hey => OwnedMessage::Info, "hey"); - assert_matches!(gasp => OwnedMessage::Error, "gasp"); - assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); - } + }} + + tests!(test_xsso<StandardIndirect>); + tests!(test_linux<LinuxPamIndirect>); }
--- a/src/module.rs Sun Jun 08 04:21:58 2025 -0400 +++ b/src/module.rs Tue Jun 10 01:09:30 2025 -0400 @@ -29,7 +29,7 @@ /// 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 [`PamModuleOnly::get_authtok`](crate::handle::PamModuleOnly::get_authtok), + /// and [`PamHandleModule::get_authtok`], /// and verify them against something. /// /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg]