Mercurial > crates > nonstick
changeset 130:80c07e5ab22f
Transfer over (almost) completely to using libpam-sys.
This reimplements everything in nonstick on top of the new -sys crate.
We don't yet use libpam-sys's helpers for binary message payloads. Soon.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 01 Jul 2025 06:11:43 -0400 |
parents | 5b2de52dd8b2 |
children | a632a8874131 |
files | Cargo.toml build.rs libpam-sys/build.rs libpam-sys/libpam-sys-test/build.rs libpam-sys/src/ffi.rs src/constants.rs src/conv.rs src/libpam/answer.rs src/libpam/conversation.rs src/libpam/environ.rs src/libpam/handle.rs src/libpam/mod.rs src/libpam/pam_ffi.rs src/libpam/question.rs src/logging.rs testharness/Cargo.toml |
diffstat | 16 files changed, 374 insertions(+), 688 deletions(-) [+] |
line wrap: on
line diff
--- a/Cargo.toml Mon Jun 30 23:49:54 2025 -0400 +++ b/Cargo.toml Tue Jul 01 06:11:43 2025 -0400 @@ -21,7 +21,7 @@ rust-version.workspace = true [features] -default = ["link"] +default = ["link", "basic-ext"] # Enable this to actually link against your system's PAM library. # @@ -30,9 +30,9 @@ link = [] basic-ext = [] -illumos-ext = [] linux-pam-ext = [] openpam-ext = [] +sun-ext = [] # This feature exists only for testing. test-install = []
--- a/build.rs Mon Jun 30 23:49:54 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,72 +0,0 @@ -use bindgen::MacroTypeVariation; -use std::env; -use std::path::PathBuf; - -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) - .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) - .blocklist_type("pam_handle") - .blocklist_type("pam_conv") - .blocklist_var(".*") - .allowlist_function("pam_start") - .allowlist_function("pam_[gs]et_item") - .allowlist_function("pam_get_(user|authtok)") - .allowlist_function("pam_end") - .allowlist_function("pam_strerror") - .allowlist_function("pam_authenticate") - .allowlist_function("pam_chauthtok") - .allowlist_function("pam_acct_mgmt") - .allowlist_function("pam_(ge|pu)tenv(list)?") - .default_macro_constant_type(MacroTypeVariation::Unsigned); - - let linux_builder = common_builder - .clone() - // This function is not available in OpenPAM. - // That means if somebody tries to run a binary compiled for - // Linux-PAM against a different impl, it will fail. - .allowlist_function("pam_syslog") - .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() - // This function is not available in Linux-PAM. - // That means if somebody tries to run a binary compiled for - // OpenPAM against a different impl, it will fail. - .allowlist_function("_openpam_log") - .header_contents( - "openpam.h", - r#" - #include <security/pam_types.h> - #include <security/openpam.h> - #include <security/pam_appl.h> - #include <security/pam_constants.h> - "#, - ); - - let (pam_impl, bindings) = { - if let Ok(bindings) = linux_builder.generate() { - ("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/libpam-sys/build.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/libpam-sys/build.rs Tue Jul 01 06:11:43 2025 -0400 @@ -9,5 +9,8 @@ let pam_impls = pam_impl_strs.join(","); // We use this for ctest. Don't do what we've done; just use cfg_pam_impl. println!("cargo:rustc-check-cfg=cfg(_private_pam_impl_hack, values({pam_impls}))"); - println!("cargo:rustc-cfg=_private_pam_impl_hack=\"{:?}\"", PamImpl::CURRENT); + println!( + "cargo:rustc-cfg=_private_pam_impl_hack=\"{:?}\"", + PamImpl::CURRENT + ); }
--- a/libpam-sys/libpam-sys-test/build.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/libpam-sys/libpam-sys-test/build.rs Tue Jul 01 06:11:43 2025 -0400 @@ -1,6 +1,6 @@ use bindgen::MacroTypeVariation; use libpam_sys_impls::__pam_impl_enum__; -use proc_macro2::{Group, TokenStream, TokenTree}; +use proc_macro2::{Group, Ident, TokenStream, TokenTree}; use quote::{format_ident, ToTokens}; use std::path::Path; use std::process::Command; @@ -150,11 +150,7 @@ fn remove_consts(file_contents: &str, out_file: impl AsRef<Path>) { let deconstified = deconstify( TokenStream::from_str(file_contents).unwrap(), - &TokenStream::from_str("mut") - .unwrap() - .into_iter() - .next() - .unwrap(), + &format_ident!("mut"), ) .to_string(); let out_file = out_file.as_ref(); @@ -170,7 +166,7 @@ assert!(status.success(), "rustfmt exited with code {status}"); } -fn deconstify(stream: TokenStream, mut_token: &TokenTree) -> TokenStream { +fn deconstify(stream: TokenStream, mut_token: &Ident) -> TokenStream { TokenStream::from_iter(stream.into_iter().map(|token| { match token { TokenTree::Group(g) => {
--- a/libpam-sys/src/ffi.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/libpam-sys/src/ffi.rs Tue Jul 01 06:11:43 2025 -0400 @@ -78,11 +78,8 @@ /// pam_set_data(handle, name.as_ptr().cast_mut(), data_ptr.cast(), cleanup()); /// } /// ``` -pub type CleanupCallback = unsafe extern "C" fn( - pamh: *mut pam_handle, - data: *mut c_void, - pam_end_status: c_int, -); +pub type CleanupCallback = + unsafe extern "C" fn(pamh: *mut pam_handle, data: *mut c_void, pam_end_status: c_int); /// Used by PAM to communicate between the module and the application. #[repr(C)] @@ -111,7 +108,6 @@ pub resp_retcode: c_int, } - // These are the functions specified in X/SSO. Everybody exports them. extern "C" { /// Account validation. @@ -139,16 +135,16 @@ ) -> c_int; /// Gets an environment variable. You own the return value. - pub fn pam_getenv(pamh: *mut pam_handle, name: *const c_char) -> *mut c_char; + pub fn pam_getenv(pamh: *const pam_handle, name: *const c_char) -> *mut c_char; /// Gets all the environment variables. You own everything it points to. - pub fn pam_getenvlist(pamh: *mut pam_handle) -> *mut *mut c_char; + pub fn pam_getenvlist(pamh: *const pam_handle) -> *mut *mut c_char; /// Get information about the transaction. /// /// The item is owned by PAM. pub fn pam_get_item( - pamh: *mut pam_handle, + pamh: *const pam_handle, item_type: c_int, item: *mut *const c_void, ) -> c_int; @@ -214,7 +210,12 @@ // when it comes across the `cfg_pam_impl` macro. // This is a custom cfg variable set in our build.rs. Don't do this; just use // cfg_pam_impl. -#[cfg(_private_pam_impl_hack = "LinuxPam")] +#[cfg(any(_private_pam_impl_hack = "LinuxPam", _private_pam_impl_hack = "OpenPam"))] extern "C" { - pub fn pam_get_authtok(pamh: *mut pam_handle, x: c_int, token: *mut *const c_char, prompt: *const c_char) -> c_int; + pub fn pam_get_authtok( + pamh: *mut pam_handle, + x: c_int, + token: *mut *const c_char, + prompt: *const c_char, + ) -> c_int; }
--- a/src/constants.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/src/constants.rs Tue Jul 01 06:11:43 2025 -0400 @@ -4,29 +4,25 @@ // between Linux-PAM and OpenPAM header files. #![allow(clippy::unnecessary_cast)] -#[cfg(feature = "link")] -use crate::libpam::pam_ffi; use crate::{linklist, man7, manbsd, xsso}; use bitflags::bitflags; -use libc::c_int; +use std::ffi::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; +use std::fmt; -/// Arbitrary values for PAM constants when not linking against system PAM. +/// Values for constants not provided by certain PAM implementations. /// /// **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 pam_ffi { - use std::ffi::c_uint; + pub use libpam_sys::*; macro_rules! define { - ($(#[$attr:meta])* $($name:ident = $value:expr),+) => { + ($(#[$attr:meta])* $($name:ident = $value:expr;)+) => { define!( @meta { $(#[$attr])* } $(pub const $name: i32 = $value;)+ @@ -35,60 +31,32 @@ (@meta $m:tt $($i:item)+) => { define!(@expand $($m $i)+); }; (@expand $({ $(#[$m:meta])* } $i:item)+) => {$($(#[$m])* $i)+}; } - const fn bit(n: u8) -> 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) + /// A fictitious constant for testing purposes. + #[cfg(not(feature = "link"))] + #[cfg_pam_impl(not("OpenPam"))] + PAM_BAD_CONSTANT = 513; + PAM_BAD_FEATURE = 514; ); 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 + /// A fictitious constant for testing purposes. + #[cfg(not(feature = "link"))] + #[cfg_pam_impl(not(any("LinuxPam", "OpenPam")))] + PAM_BAD_ITEM = 515; + PAM_MODULE_UNKNOWN = 516; ); - /// Dummy implementation of strerror so that it always returns None. - pub fn strerror(val: c_uint) -> Option<&'static str> { - _ = val; - None - } + define!( + /// A fictitious constant for testing purposes. + #[cfg(not(feature = "link"))] + #[cfg_pam_impl(not("LinuxPam"))] + PAM_CONV_AGAIN = 517; + PAM_INCOMPLETE = 518; + ); + } bitflags! { @@ -100,24 +68,24 @@ #[repr(transparent)] pub struct Flags: c_int { /// The module should not generate any messages. - const SILENT = pam_ffi::PAM_SILENT; + const SILENT = libpam_sys::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 = pam_ffi::PAM_DISALLOW_NULL_AUTHTOK; + const DISALLOW_NULL_AUTHTOK = libpam_sys::PAM_DISALLOW_NULL_AUTHTOK; // Flag used for `set_credentials`. /// Set user credentials for an authentication service. - const ESTABLISH_CREDENTIALS = pam_ffi::PAM_ESTABLISH_CRED; + const ESTABLISH_CREDENTIALS = libpam_sys::PAM_ESTABLISH_CRED; /// Delete user credentials associated with /// an authentication service. - const DELETE_CREDENTIALS = pam_ffi::PAM_DELETE_CRED; + const DELETE_CREDENTIALS = libpam_sys::PAM_DELETE_CRED; /// Reinitialize user credentials. - const REINITIALIZE_CREDENTIALS = pam_ffi::PAM_REINITIALIZE_CRED; + const REINITIALIZE_CREDENTIALS = libpam_sys::PAM_REINITIALIZE_CRED; /// Extend the lifetime of user credentials. - const REFRESH_CREDENTIALS = pam_ffi::PAM_REFRESH_CRED; + const REFRESH_CREDENTIALS = libpam_sys::PAM_REFRESH_CRED; // Flags used for password changing. @@ -126,7 +94,7 @@ /// the password service should update all passwords. /// /// This flag is only used by `change_authtok`. - const CHANGE_EXPIRED_AUTHTOK = pam_ffi::PAM_CHANGE_EXPIRED_AUTHTOK; + const CHANGE_EXPIRED_AUTHTOK = libpam_sys::PAM_CHANGE_EXPIRED_AUTHTOK; /// This is a preliminary check for password changing. /// The password should not be changed. /// @@ -134,7 +102,7 @@ /// Applications may not use this flag. /// /// This flag is only used by `change_authtok`. - const PRELIMINARY_CHECK = pam_ffi::PAM_PRELIM_CHECK; + const PRELIMINARY_CHECK = libpam_sys::PAM_PRELIM_CHECK; /// The password should actuallyPR be updated. /// This and [Self::PRELIMINARY_CHECK] are mutually exclusive. /// @@ -142,7 +110,7 @@ /// Applications may not use this flag. /// /// This flag is only used by `change_authtok`. - const UPDATE_AUTHTOK = pam_ffi::PAM_UPDATE_AUTHTOK; + const UPDATE_AUTHTOK = libpam_sys::PAM_UPDATE_AUTHTOK; } } @@ -207,7 +175,7 @@ impl Display for ErrorCode { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match pam_ffi::strerror((*self).into()) { + match strerror((*self).into()) { Some(err) => f.write_str(err), None => self.fmt_internal(f), } @@ -236,10 +204,31 @@ /// 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) + let n : c_int = self.into(); + write!(f, "PAM error: {self:?} ({n})") } } +/// Gets a string version of an error message. +#[cfg(feature = "link")] +pub fn strerror(code: c_int) -> Option<&'static str> { + use std::ptr; + use std::ffi::CStr; + // SAFETY: PAM impls don't care about the PAM handle and always return + // static strings. + let strerror = unsafe { libpam_sys::pam_strerror(ptr::null(), code as c_int) }; + // SAFETY: We just got this back from PAM and we checked if it's null. + (!strerror.is_null()) + .then(|| unsafe { CStr::from_ptr(strerror) }.to_str().ok()) + .flatten() +} + +/// Dummy implementation of strerror so that it always returns None. +#[cfg(not(feature = "link"))] +pub fn strerror(_: c_int) -> Option<&'static str> { + None +} + #[cfg(test)] mod tests { use super::*;
--- a/src/conv.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/src/conv.rs Tue Jul 01 06:11:43 2025 -0400 @@ -9,12 +9,10 @@ use std::fmt::Debug; use std::result::Result as StdResult; -/// The types of message and request that can be sent to a user. -/// -/// The data within each enum value is the prompt (or other information) -/// that will be presented to the user. +/// An individual pair of request/response to be sent to the user. +#[derive(Debug)] #[non_exhaustive] -pub enum Message<'a> { +pub enum Exchange<'a> { Prompt(&'a QAndA<'a>), MaskedPrompt(&'a MaskedQAndA<'a>), Error(&'a ErrorMsg<'a>), @@ -23,22 +21,22 @@ BinaryPrompt(&'a BinaryQAndA<'a>), } -impl Message<'_> { +impl Exchange<'_> { /// Sets an error answer on this question, without having to inspect it. /// /// Use this as a default match case: /// /// ``` - /// use nonstick::conv::{Message, QAndA}; + /// use nonstick::conv::{Exchange, QAndA}; /// use nonstick::ErrorCode; /// - /// fn cant_respond(message: Message) { + /// fn cant_respond(message: Exchange) { /// match message { - /// Message::Info(i) => { + /// Exchange::Info(i) => { /// eprintln!("fyi, {}", i.question()); /// i.set_answer(Ok(())) /// } - /// Message::Error(e) => { + /// Exchange::Error(e) => { /// eprintln!("ERROR: {}", e.question()); /// e.set_answer(Ok(())) /// } @@ -48,12 +46,12 @@ /// } pub fn set_error(&self, err: ErrorCode) { match *self { - Message::Prompt(m) => m.set_answer(Err(err)), - Message::MaskedPrompt(m) => m.set_answer(Err(err)), - Message::Error(m) => m.set_answer(Err(err)), - Message::Info(m) => m.set_answer(Err(err)), - Message::RadioPrompt(m) => m.set_answer(Err(err)), - Message::BinaryPrompt(m) => m.set_answer(Err(err)), + Exchange::Prompt(m) => m.set_answer(Err(err)), + Exchange::MaskedPrompt(m) => m.set_answer(Err(err)), + Exchange::Error(m) => m.set_answer(Err(err)), + Exchange::Info(m) => m.set_answer(Err(err)), + Exchange::RadioPrompt(m) => m.set_answer(Err(err)), + Exchange::BinaryPrompt(m) => m.set_answer(Err(err)), } } } @@ -76,8 +74,8 @@ } } - /// Converts this Q&A into a [`Message`] for the [`Conversation`]. - pub fn message(&self) -> Message<'_> { + /// Converts this Q&A into a [`Exchange`] for the [`Conversation`]. + pub fn exchange(&self) -> Exchange<'_> { $val(self) } @@ -110,9 +108,7 @@ $(#[$m])* impl fmt::Debug for $name<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { - #[derive(Debug)] - struct $name<'a> { q: $qt } - fmt::Debug::fmt(&$name { q: self.q }, f) + f.debug_struct(stringify!($name)).field("q", &self.q).finish_non_exhaustive() } } }; @@ -123,7 +119,7 @@ /// /// In other words, a password entry prompt. MaskedQAndA<'a, Q=&'a str, A=String>, - Message::MaskedPrompt + Exchange::MaskedPrompt ); q_and_a!( @@ -133,7 +129,7 @@ /// When the user types, their input will be shown to them. /// It can be used for things like usernames. QAndA<'a, Q=&'a str, A=String>, - Message::Prompt + Exchange::Prompt ); q_and_a!( @@ -143,7 +139,7 @@ /// questions, but nowhere in the documentation is it specified /// what the format of the answer will be, or how this should be shown. RadioQAndA<'a, Q=&'a str, A=String>, - Message::RadioPrompt + Exchange::RadioPrompt ); q_and_a!( @@ -156,7 +152,7 @@ /// The `data_type` tag is a value that is simply passed through /// to the application. PAM does not define any meaning for it. BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>, - Message::BinaryPrompt + Exchange::BinaryPrompt ); /// Owned binary data. @@ -208,7 +204,7 @@ /// should still call [`set_answer`][`QAndA::set_answer`] to verify that /// the message has been displayed (or actively discarded). InfoMsg<'a, Q = &'a str, A = ()>, - Message::Info + Exchange::Info ); q_and_a!( @@ -218,7 +214,7 @@ /// should still call [`set_answer`][`QAndA::set_answer`] to verify that /// the message has been displayed (or actively discarded). ErrorMsg<'a, Q = &'a str, A = ()>, - Message::Error + Exchange::Error ); /// A channel for PAM modules to request information from the user. @@ -236,7 +232,7 @@ /// as there were messages in the request; one corresponding to each. /// /// TODO: write detailed documentation about how to use this. - fn communicate(&self, messages: &[Message]); + fn communicate(&self, messages: &[Exchange]); } /// Turns a simple function into a [`Conversation`]. @@ -245,7 +241,7 @@ /// Conversation: /// /// ``` -/// use nonstick::conv::{conversation_func, Conversation, Message}; +/// use nonstick::conv::{conversation_func, Conversation, Exchange}; /// mod some_library { /// # use nonstick::Conversation; /// pub fn get_auth_data(conv: &mut impl Conversation) { @@ -253,7 +249,7 @@ /// } /// } /// -/// fn my_terminal_prompt(messages: &[Message]) { +/// fn my_terminal_prompt(messages: &[Exchange]) { /// // ... /// # unimplemented!() /// } @@ -262,14 +258,14 @@ /// some_library::get_auth_data(&mut conversation_func(my_terminal_prompt)); /// } /// ``` -pub fn conversation_func(func: impl Fn(&[Message])) -> impl Conversation { +pub fn conversation_func(func: impl Fn(&[Exchange])) -> impl Conversation { FunctionConvo(func) } -struct FunctionConvo<C: Fn(&[Message])>(C); +struct FunctionConvo<C: Fn(&[Exchange])>(C); -impl<C: Fn(&[Message])> Conversation for FunctionConvo<C> { - fn communicate(&self, messages: &[Message]) { +impl<C: Fn(&[Exchange])> Conversation for FunctionConvo<C> { + fn communicate(&self, messages: &[Exchange]) { self.0(messages) } } @@ -399,14 +395,14 @@ $(#[$m])* fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> { let prompt = <$msg>::new($($param),*); - self.communicate(&[prompt.message()]); + self.communicate(&[prompt.exchange()]); prompt.answer() } }; ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { $(#[$m])* fn $fn_name(&self, $($param: $pt),*) { - self.communicate(&[<$msg>::new($($param),*).message()]); + self.communicate(&[<$msg>::new($($param),*).exchange()]); } }; } @@ -433,25 +429,25 @@ } impl<CA: ConversationAdapter> Conversation for Demux<CA> { - fn communicate(&self, messages: &[Message]) { + fn communicate(&self, messages: &[Exchange]) { for msg in messages { match msg { - Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())), - Message::MaskedPrompt(prompt) => { + Exchange::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())), + Exchange::MaskedPrompt(prompt) => { prompt.set_answer(self.0.masked_prompt(prompt.question())) } - Message::RadioPrompt(prompt) => { + Exchange::RadioPrompt(prompt) => { prompt.set_answer(self.0.radio_prompt(prompt.question())) } - Message::Info(prompt) => { + Exchange::Info(prompt) => { self.0.info_msg(prompt.question()); prompt.set_answer(Ok(())) } - Message::Error(prompt) => { + Exchange::Error(prompt) => { self.0.error_msg(prompt.question()); prompt.set_answer(Ok(())) } - Message::BinaryPrompt(prompt) => { + Exchange::BinaryPrompt(prompt) => { let q = prompt.question(); prompt.set_answer(self.0.binary_prompt(q)) } @@ -516,11 +512,11 @@ // Basic tests. conv.communicate(&[ - what.message(), - pass.message(), - err.message(), - info.message(), - has_err.message(), + what.exchange(), + pass.exchange(), + err.exchange(), + info.exchange(), + has_err.exchange(), ]); assert_eq!("whatwhat", what.answer().unwrap()); @@ -538,7 +534,7 @@ let radio = RadioQAndA::new("channel?"); let bin = BinaryQAndA::new((&[10, 9, 8], 66)); - conv.communicate(&[radio.message(), bin.message()]); + conv.communicate(&[radio.exchange(), bin.exchange()]); assert_eq!("zero", radio.answer().unwrap()); assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap()); @@ -549,31 +545,31 @@ struct MuxTester; impl Conversation for MuxTester { - fn communicate(&self, messages: &[Message]) { + fn communicate(&self, messages: &[Exchange]) { if let [msg] = messages { match *msg { - Message::Info(info) => { + Exchange::Info(info) => { assert_eq!("let me tell you", info.question()); info.set_answer(Ok(())) } - Message::Error(error) => { + Exchange::Error(error) => { assert_eq!("oh no", error.question()); error.set_answer(Ok(())) } - Message::Prompt(prompt) => prompt.set_answer(match prompt.question() { + Exchange::Prompt(prompt) => prompt.set_answer(match prompt.question() { "should_err" => Err(ErrorCode::PermissionDenied), "question" => Ok("answer".to_owned()), other => panic!("unexpected question {other:?}"), }), - Message::MaskedPrompt(ask) => { + Exchange::MaskedPrompt(ask) => { assert_eq!("password!", ask.question()); ask.set_answer(Ok("open sesame".into())) } - Message::BinaryPrompt(prompt) => { + Exchange::BinaryPrompt(prompt) => { assert_eq!((&[1, 2, 3][..], 69), prompt.question()); prompt.set_answer(Ok(BinaryData::from((&[3, 2, 1], 42)))) } - Message::RadioPrompt(ask) => { + Exchange::RadioPrompt(ask) => { assert_eq!("radio?", ask.question()); ask.set_answer(Ok("yes".to_owned())) }
--- a/src/libpam/answer.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/src/libpam/answer.rs Tue Jul 01 06:11:43 2025 -0400 @@ -1,11 +1,10 @@ //! Types used to communicate data from the application to the module. -use crate::libpam::conversation::OwnedMessage; +use crate::libpam::conversation::OwnedExchange; use crate::libpam::memory; -use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString}; -pub use crate::libpam::pam_ffi::Answer; +use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString, Immovable}; use crate::{ErrorCode, Result}; -use std::ffi::CStr; +use std::ffi::{c_int, c_void, CStr}; use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; @@ -22,7 +21,7 @@ impl Answers { /// Builds an Answers out of the given answered Message Q&As. - pub fn build(value: Vec<OwnedMessage>) -> Result<Self> { + pub fn build(value: Vec<OwnedExchange>) -> Result<Self> { let mut outputs = Self { base: memory::calloc(value.len())?, count: value.len(), @@ -31,14 +30,16 @@ // all allocated answer memory. for (input, output) in iter::zip(value, outputs.iter_mut()) { match input { - OwnedMessage::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.as_ref())?, - OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?, - OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, - OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, + OwnedExchange::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.as_ref())?, + OwnedExchange::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?, + OwnedExchange::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, + OwnedExchange::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, // If we're here, that means that we *got* a Linux-PAM // question from PAM, so we're OK to answer it. - OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?, - OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?, + OwnedExchange::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?, + OwnedExchange::BinaryPrompt(p) => { + BinaryAnswer::fill(output, (&p.answer()?).into())? + } } } Ok(outputs) @@ -48,8 +49,8 @@ /// /// This object is consumed and the `Answer` pointer now owns its data. /// It can be recreated with [`Self::from_c_heap`]. - pub fn into_ptr(self) -> NonNull<Answer> { - ManuallyDrop::new(self).base + pub fn into_ptr(self) -> *mut libpam_sys::pam_response { + ManuallyDrop::new(self).base.as_ptr().cast() } /// Takes ownership of a list of answers allocated on the C heap. @@ -58,8 +59,11 @@ /// /// It's up to you to make sure you pass a valid pointer, /// like one that you got from PAM, or maybe [`Self::into_ptr`]. - pub unsafe fn from_c_heap(base: NonNull<Answer>, count: usize) -> Self { - Answers { base, count } + pub unsafe fn from_c_heap(base: NonNull<libpam_sys::pam_response>, count: usize) -> Self { + Answers { + base: NonNull::new_unchecked(base.as_ptr().cast()), + count, + } } } @@ -91,6 +95,25 @@ } } +/// 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, Default)] +pub struct Answer { + /// Owned pointer to the data returned in an answer. + /// For most answers, this will be a + /// [`CHeapString`](crate::libpam::memory::CHeapString), + /// but for [`BinaryQAndA`](crate::conv::BinaryQAndA)s + /// (a Linux-PAM extension), this will be a [`CHeapBox`] of + /// [`CBinaryData`](crate::libpam::memory::CBinaryData). + pub data: Option<CHeapBox<c_void>>, + /// Unused. Just here for the padding. + return_code: c_int, + _marker: Immovable, +} + #[repr(transparent)] #[derive(Debug)] pub struct TextAnswer(Answer); @@ -223,19 +246,19 @@ assert_eq!("", up.contents().unwrap()); } - fn round_trip(msgs: Vec<OwnedMessage>) -> Answers { + fn round_trip(msgs: Vec<OwnedExchange>) -> Answers { let n = msgs.len(); let sent = Answers::build(msgs).unwrap(); - unsafe { Answers::from_c_heap(sent.into_ptr(), n) } + unsafe { Answers::from_c_heap(NonNull::new_unchecked(sent.into_ptr()), n) } } #[test] fn test_round_trip() { let mut answers = round_trip(vec![ - answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()), - answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()), - answered!(ErrorMsg, OwnedMessage::Error, ()), - answered!(InfoMsg, OwnedMessage::Info, ()), + answered!(QAndA, OwnedExchange::Prompt, "whats going on".to_owned()), + answered!(MaskedQAndA, OwnedExchange::MaskedPrompt, "well then".into()), + answered!(ErrorMsg, OwnedExchange::Error, ()), + answered!(InfoMsg, OwnedExchange::Info, ()), ]); if let [going, well, err, info] = &mut answers[..] { @@ -254,13 +277,13 @@ let binary_msg = { let qa = BinaryQAndA::new((&[][..], 0)); qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99))); - OwnedMessage::BinaryPrompt(qa) + OwnedExchange::BinaryPrompt(qa) }; let mut answers = round_trip(vec![ binary_msg, answered!( RadioQAndA, - OwnedMessage::RadioPrompt, + OwnedExchange::RadioPrompt, "beep boop".to_owned() ), ]);
--- a/src/libpam/conversation.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/src/libpam/conversation.rs Tue Jul 01 06:11:43 2025 -0400 @@ -1,25 +1,35 @@ use crate::conv::{BinaryQAndA, RadioQAndA}; -use crate::conv::{Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA}; +use crate::conv::{Conversation, ErrorMsg, Exchange, InfoMsg, MaskedQAndA, QAndA}; use crate::libpam::answer::BinaryAnswer; use crate::libpam::answer::{Answer, Answers, TextAnswer}; use crate::libpam::memory::CBinaryData; -use crate::libpam::pam_ffi::AppData; -pub use crate::libpam::pam_ffi::LibPamConversation; -use crate::libpam::question::QuestionsTrait; -use crate::libpam::question::{Question, Questions}; +use crate::libpam::question::Question; use crate::ErrorCode; use crate::Result; +use libpam_sys::helpers::PtrPtrVec; +use libpam_sys::AppData; use std::ffi::c_int; use std::iter; use std::marker::PhantomData; use std::ptr::NonNull; use std::result::Result as StdResult; +/// The type used by PAM to call back into a conversation. +#[repr(C)] +pub struct LibPamConversation<'a> { + pam_conv: libpam_sys::pam_conv, + /// Marker to associate the lifetime of this with the conversation + /// that was passed in. + pub life: PhantomData<&'a mut ()>, +} + impl LibPamConversation<'_> { pub fn wrap<C: Conversation>(conv: &C) -> Self { Self { - callback: Self::wrapper_callback::<C>, - appdata: (conv as *const C).cast(), + pam_conv: libpam_sys::pam_conv { + conv: Self::wrapper_callback::<C>, + appdata_ptr: (conv as *const C).cast_mut().cast(), + }, life: PhantomData, } } @@ -29,9 +39,9 @@ /// PAM calls this, we compute answers, then send them back. unsafe extern "C" fn wrapper_callback<C: Conversation>( count: c_int, - questions: *const *const Question, - answers: *mut *mut Answer, - me: *const AppData, + questions: *const *const libpam_sys::pam_message, + answers: *mut *mut libpam_sys::pam_response, + me: *mut AppData, ) -> c_int { let internal = || { // Collect all our pointers @@ -39,23 +49,23 @@ .cast::<C>() .as_ref() .ok_or(ErrorCode::ConversationError)?; - let indirect = Questions::borrow_ptr(questions, count as usize); + let q_iter = PtrPtrVec::<Question>::iter_over(questions, count as usize); let answers_ptr = answers.as_mut().ok_or(ErrorCode::ConversationError)?; // Build our owned list of Q&As from the questions we've been asked - let messages: Vec<OwnedMessage> = indirect + let messages: Vec<OwnedExchange> = q_iter .map(TryInto::try_into) .collect::<Result<_>>() .map_err(|_| ErrorCode::ConversationError)?; // Borrow all those Q&As and ask them. // If we got an invalid message type, bail before sending. - let borrowed: Result<Vec<_>> = messages.iter().map(Message::try_from).collect(); + let borrowed: Result<Vec<_>> = messages.iter().map(Exchange::try_from).collect(); // TODO: Do we want to log something here? conv.communicate(&borrowed?); // Send our answers back. let owned = Answers::build(messages)?; - *answers_ptr = owned.into_ptr().as_ptr(); + *answers_ptr = owned.into_ptr(); Ok(()) }; ErrorCode::result_to_c(internal()) @@ -63,17 +73,18 @@ } impl Conversation for LibPamConversation<'_> { - fn communicate(&self, messages: &[Message]) { + fn communicate(&self, messages: &[Exchange]) { let internal = || { - let questions = Box::pin(Questions::new(messages)?); + let questions: Result<_> = messages.iter().map(Question::try_from).collect(); + let questions = PtrPtrVec::new(questions?); let mut response_pointer = std::ptr::null_mut(); // SAFETY: We're calling into PAM with valid everything. let result = unsafe { - (self.callback)( + (self.pam_conv.conv)( messages.len() as c_int, - questions.as_ref().ptr(), + questions.as_ptr(), &mut response_pointer, - self.appdata, + self.pam_conv.appdata_ptr, ) }; ErrorCode::result_from(result)?; @@ -96,9 +107,9 @@ } } -/// Like [`Message`], but this time we own the contents. +/// Like [`Exchange`], but this time we own the contents. #[derive(Debug)] -pub enum OwnedMessage<'a> { +pub enum OwnedExchange<'a> { MaskedPrompt(MaskedQAndA<'a>), Prompt(QAndA<'a>), Info(InfoMsg<'a>), @@ -107,16 +118,16 @@ BinaryPrompt(BinaryQAndA<'a>), } -impl<'a> TryFrom<&'a OwnedMessage<'a>> for Message<'a> { +impl<'a> TryFrom<&'a OwnedExchange<'a>> for Exchange<'a> { type Error = ErrorCode; - fn try_from(src: &'a OwnedMessage) -> StdResult<Self, ErrorCode> { + fn try_from(src: &'a OwnedExchange) -> StdResult<Self, ErrorCode> { match src { - OwnedMessage::MaskedPrompt(m) => Ok(Message::MaskedPrompt(m)), - OwnedMessage::Prompt(m) => Ok(Message::Prompt(m)), - OwnedMessage::Info(m) => Ok(Message::Info(m)), - OwnedMessage::Error(m) => Ok(Message::Error(m)), - OwnedMessage::RadioPrompt(m) => Ok(Message::RadioPrompt(m)), - OwnedMessage::BinaryPrompt(m) => Ok(Message::BinaryPrompt(m)), + OwnedExchange::MaskedPrompt(m) => Ok(Exchange::MaskedPrompt(m)), + OwnedExchange::Prompt(m) => Ok(Exchange::Prompt(m)), + OwnedExchange::Info(m) => Ok(Exchange::Info(m)), + OwnedExchange::Error(m) => Ok(Exchange::Error(m)), + OwnedExchange::RadioPrompt(m) => Ok(Exchange::RadioPrompt(m)), + OwnedExchange::BinaryPrompt(m) => Ok(Exchange::BinaryPrompt(m)), } } } @@ -126,7 +137,7 @@ /// # Safety /// /// You are responsible for ensuring that the src-dst pair matches. -unsafe fn convert(msg: &Message, resp: &mut Answer) { +unsafe fn convert(msg: &Exchange, resp: &mut Answer) { macro_rules! fill_text { ($dst:ident, $src:ident) => {{ let text_resp = unsafe { TextAnswer::upcast($src) }; @@ -134,12 +145,12 @@ }}; } match *msg { - Message::MaskedPrompt(qa) => fill_text!(qa, resp), - Message::Prompt(qa) => fill_text!(qa, resp), - Message::Error(m) => m.set_answer(Ok(())), - Message::Info(m) => m.set_answer(Ok(())), - Message::RadioPrompt(qa) => fill_text!(qa, resp), - Message::BinaryPrompt(qa) => { + Exchange::MaskedPrompt(qa) => fill_text!(qa, resp), + Exchange::Prompt(qa) => fill_text!(qa, resp), + Exchange::Error(m) => m.set_answer(Ok(())), + Exchange::Info(m) => m.set_answer(Ok(())), + Exchange::RadioPrompt(qa) => fill_text!(qa, resp), + Exchange::BinaryPrompt(qa) => { let bin_resp = unsafe { BinaryAnswer::upcast(resp) }; qa.set_answer(Ok(bin_resp .data()
--- a/src/libpam/environ.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/src/libpam/environ.rs Tue Jul 01 06:11:43 2025 -0400 @@ -1,7 +1,7 @@ use crate::constants::{ErrorCode, Result}; use crate::environ::{EnvironMap, EnvironMapMut}; use crate::libpam::memory::CHeapString; -use crate::libpam::{memory, pam_ffi, LibPamHandle}; +use crate::libpam::{memory, LibPamHandle}; use std::ffi::{c_char, CStr, CString, OsStr, OsString}; use std::marker::PhantomData; use std::os::unix::ffi::{OsStrExt, OsStringExt}; @@ -20,12 +20,7 @@ fn environ_get(&self, key: &OsStr) -> Option<OsString> { let key = CString::new(key.as_bytes()).ok()?; // SAFETY: We are a valid handle and are calling with a good key. - unsafe { - copy_env(pam_ffi::pam_getenv( - (self as *const LibPamHandle).cast_mut(), - key.as_ptr(), - )) - } + unsafe { copy_env(libpam_sys::pam_getenv(self.0.as_ref(), key.as_ptr())) } } fn environ_set(&mut self, key: &OsStr, value: Option<&OsStr>) -> Result<Option<OsString>> { @@ -44,18 +39,16 @@ } let put = CString::new(result).map_err(|_| ErrorCode::ConversationError)?; // SAFETY: This is a valid handle and a valid environment string. - ErrorCode::result_from(unsafe { pam_ffi::pam_putenv(self, put.as_ptr()) })?; + ErrorCode::result_from(unsafe { libpam_sys::pam_putenv(self.0.as_mut(), put.as_ptr()) })?; Ok(old) } fn environ_iter(&self) -> Result<impl Iterator<Item = (OsString, OsString)>> { // SAFETY: This is a valid PAM handle. It will return valid data. unsafe { - NonNull::new(pam_ffi::pam_getenvlist( - (self as *const LibPamHandle).cast_mut(), - )) - .map(|ptr| EnvList::from_ptr(ptr.cast())) - .ok_or(ErrorCode::BufferError) + NonNull::new(libpam_sys::pam_getenvlist(self.0.as_ref())) + .map(|ptr| EnvList::from_ptr(ptr.cast())) + .ok_or(ErrorCode::BufferError) } } }
--- a/src/libpam/handle.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/src/libpam/handle.rs Tue Jul 01 06:11:43 2025 -0400 @@ -1,11 +1,10 @@ use super::conversation::LibPamConversation; use crate::constants::{ErrorCode, Result}; -use crate::conv::Message; +use crate::conv::Exchange; use crate::environ::EnvironMapMut; use crate::handle::PamShared; use crate::libpam::environ::{LibPamEnviron, LibPamEnvironMut}; -pub use crate::libpam::pam_ffi::LibPamHandle; -use crate::libpam::{memory, pam_ffi}; +use crate::libpam::memory; use crate::logging::{Level, Location}; use crate::{ guide, linklist, stdlinks, Conversation, EnvironMap, Flags, PamHandleApplication, @@ -14,29 +13,30 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::cell::Cell; use std::ffi::{c_char, c_int, CString}; -use std::ops::{Deref, DerefMut}; + use std::ptr; +use std::ptr::NonNull; +use libpam_sys::cfg_pam_impl; /// Owner for a PAM handle. -struct HandleWrap(*mut LibPamHandle); +pub struct LibPamHandle(pub NonNull<libpam_sys::pam_handle>); -impl Deref for HandleWrap { - type Target = LibPamHandle; - fn deref(&self) -> &Self::Target { - unsafe { &*self.0 } +impl AsRef<libpam_sys::pam_handle> for LibPamHandle { + fn as_ref(&self) -> &libpam_sys::pam_handle { + unsafe { self.0.as_ref() } } } -impl DerefMut for HandleWrap { - fn deref_mut(&mut self) -> &mut Self::Target { - unsafe { &mut *self.0 } +impl AsMut<libpam_sys::pam_handle> for LibPamHandle { + fn as_mut(&mut self) -> &mut libpam_sys::pam_handle { + unsafe { self.0.as_mut() } } } /// An owned PAM handle. pub struct OwnedLibPamHandle<'a> { /// The handle itself. - handle: HandleWrap, + handle: LibPamHandle, /// The last return value from the handle. last_return: Cell<Result<()>>, /// If set, the Conversation that this PAM handle owns. @@ -102,20 +102,23 @@ let service_cstr = CString::new(service_name).map_err(|_| ErrorCode::ConversationError)?; let username_cstr = memory::prompt_ptr(memory::option_cstr(username.as_deref())?.as_ref()); - let mut handle: *mut LibPamHandle = ptr::null_mut(); + let mut handle: *mut libpam_sys::pam_handle = ptr::null_mut(); // SAFETY: We've set everything up properly to call `pam_start`. // The returned value will be a valid pointer provided the result is OK. let result = unsafe { - pam_ffi::pam_start( + libpam_sys::pam_start( service_cstr.as_ptr(), username_cstr, - conv.as_ref(), + (conv.as_ref() as *const LibPamConversation) + .cast_mut() + .cast(), &mut handle, ) }; ErrorCode::result_from(result)?; + let handle = NonNull::new(handle).ok_or(ErrorCode::BufferError)?; Ok(Self { - handle: HandleWrap(handle), + handle: LibPamHandle(handle), last_return: Cell::new(Ok(())), conversation: Some(conv), }) @@ -124,21 +127,22 @@ impl PamHandleApplication for OwnedLibPamHandle<'_> { fn authenticate(&mut self, flags: Flags) -> Result<()> { - let ret = unsafe { pam_ffi::pam_authenticate(self.handle.0, flags.bits() as c_int) }; + let ret = + unsafe { libpam_sys::pam_authenticate(self.handle.as_mut(), flags.bits() as c_int) }; let result = ErrorCode::result_from(ret); self.last_return.set(result); result } fn account_management(&mut self, flags: Flags) -> Result<()> { - let ret = unsafe { pam_ffi::pam_acct_mgmt(self.handle.0, flags.bits() as c_int) }; + let ret = unsafe { libpam_sys::pam_acct_mgmt(self.handle.as_mut(), flags.bits() as c_int) }; let result = ErrorCode::result_from(ret); self.last_return.set(result); result } fn change_authtok(&mut self, flags: Flags) -> Result<()> { - let ret = unsafe { pam_ffi::pam_chauthtok(self.handle.0, flags.bits() as c_int) }; + let ret = unsafe { libpam_sys::pam_chauthtok(self.handle.as_mut(), flags.bits() as c_int) }; let result = ErrorCode::result_from(ret); self.last_return.set(result); result @@ -167,8 +171,8 @@ #[doc = stdlinks!(3 pam_end)] fn drop(&mut self) { unsafe { - pam_ffi::pam_end( - self.handle.0, + libpam_sys::pam_end( + self.handle.as_mut(), ErrorCode::result_to_c(self.last_return.get()), ); } @@ -190,6 +194,7 @@ } impl PamShared for LibPamHandle { + #[cfg(any())] fn log(&self, level: Level, loc: Location<'_>, entry: &str) { let entry = match CString::new(entry).or_else(|_| CString::new(dbg!(entry))) { Ok(cstr) => cstr, @@ -200,7 +205,7 @@ _ = loc; // SAFETY: We're calling this function with a known value. unsafe { - pam_ffi::pam_syslog(self, level as c_int, "%s\0".as_ptr().cast(), entry.as_ptr()) + libpam_sys::pam_syslog(self, level as c_int, "%s\0".as_ptr().cast(), entry.as_ptr()) } } #[cfg(pam_impl = "openpam")] @@ -208,7 +213,7 @@ let func = CString::new(loc.function).unwrap_or(CString::default()); // SAFETY: We're calling this function with a known value. unsafe { - pam_ffi::_openpam_log( + libpam_sys::_openpam_log( level as c_int, func.as_ptr(), "%s\0".as_ptr().cast(), @@ -218,11 +223,17 @@ } } + fn log(&self, _level: Level, _loc: Location<'_>, _entry: &str) {} + fn username(&mut self, prompt: Option<&str>) -> Result<String> { let prompt = memory::option_cstr(prompt)?; let mut output: *const c_char = ptr::null(); let ret = unsafe { - pam_ffi::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref())) + libpam_sys::pam_get_user( + self.as_mut(), + &mut output, + memory::prompt_ptr(prompt.as_ref()), + ) }; ErrorCode::result_from(ret)?; unsafe { memory::copy_pam_string(output) } @@ -255,7 +266,7 @@ } impl Conversation for LibPamHandle { - fn communicate(&self, messages: &[Message]) { + fn communicate(&self, messages: &[Exchange]) { match self.conversation_item() { Ok(conv) => conv.communicate(messages), Err(e) => { @@ -268,13 +279,14 @@ } impl PamHandleModule for LibPamHandle { + #[cfg_pam_impl(any("LinuxPam", "OpenPam"))] fn authtok(&mut self, prompt: Option<&str>) -> Result<String> { 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 { - pam_ffi::pam_get_authtok( - self, + libpam_sys::pam_get_authtok( + self.as_mut(), ItemType::AuthTok.into(), &mut output, memory::prompt_ptr(prompt.as_ref()), @@ -287,6 +299,11 @@ .unwrap_or(Err(ErrorCode::ConversationError)) } + #[cfg_pam_impl(not(any("LinuxPam", "OpenPam")))] + fn authtok(&mut self, prompt: Option<&str>) -> Result<String> { + Err(ErrorCode::ConversationError) + } + cstr_item!(get = authtok_item, item = ItemType::AuthTok); cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok); } @@ -309,7 +326,8 @@ /// You better be requesting an item which is a C string. unsafe fn get_cstr_item(&self, item_type: ItemType) -> Result<Option<String>> { let mut output = ptr::null(); - let ret = unsafe { pam_ffi::pam_get_item(self, item_type as c_int, &mut output) }; + let ret = + unsafe { libpam_sys::pam_get_item(self.as_ref(), item_type as c_int, &mut output) }; ErrorCode::result_from(ret)?; memory::copy_pam_string(output.cast()) } @@ -322,8 +340,8 @@ 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, + libpam_sys::pam_set_item( + self.as_mut(), item_type as c_int, memory::prompt_ptr(data_str.as_ref()).cast(), ) @@ -335,8 +353,8 @@ fn conversation_item(&self) -> Result<&mut LibPamConversation<'_>> { let output: *mut LibPamConversation = ptr::null_mut(); let result = unsafe { - pam_ffi::pam_get_item( - self, + libpam_sys::pam_get_item( + self.as_ref(), ItemType::Conversation.into(), &mut output.cast_const().cast(), )
--- a/src/libpam/mod.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/src/libpam/mod.rs Tue Jul 01 06:11:43 2025 -0400 @@ -12,7 +12,6 @@ mod handle; mod memory; mod module; -pub(crate) mod pam_ffi; mod question; #[doc(inline)]
--- a/src/libpam/pam_ffi.rs Mon Jun 30 23:49:54 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -//! The types that are directly represented in PAM function signatures. - -#![allow(non_camel_case_types, non_upper_case_globals)] - -use crate::libpam::memory::{CHeapBox, Immovable}; -use std::ffi::{c_int, c_void, CStr}; -use std::marker::PhantomData; -use std::ptr; - -/// 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, Default)] -pub struct Answer { - /// Owned pointer to the data returned in an answer. - /// For most answers, this will be a - /// [`CHeapString`](crate::libpam::memory::CHeapString), - /// but for [`BinaryQAndA`](crate::conv::BinaryQAndA)s - /// (a Linux-PAM extension), this will be a [`CHeapBox`] of - /// [`CBinaryData`](crate::libpam::memory::CBinaryData). - pub data: Option<CHeapBox<c_void>>, - /// Unused. Just here for the padding. - return_code: c_int, - _marker: Immovable, -} - -/// 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::conv::Message). -/// -/// This question, and its internal data, is owned by its creator -/// (either the module or PAM itself). -#[repr(C)] -#[derive(Debug)] -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`], - /// but for requests with style `PAM_BINARY_PROMPT`, - /// this will be `CBinaryData` (a Linux-PAM extension). - pub data: Option<CHeapBox<c_void>>, -} - -/// The callback that PAM uses to get information in a conversation. -/// -/// - `num_msg` is the number of messages in the `questions` array. -/// - `questions` is a pointer to the [`Question`]s being sent to the user. -/// For information about its structure, -/// see [`QuestionsTrait`](super::question::QuestionsTrait). -/// - `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 must always be exactly as many `answers` as `num_msg`. -/// - `appdata` is the `appdata` field of the [`LibPamConversation`]. -pub type ConversationCallback = unsafe extern "C" fn( - num_msg: c_int, - questions: *const *const Question, - answers: *mut *mut Answer, - appdata: *const 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: *const AppData, - /// Marker to associate the lifetime of this with the conversation - /// that was passed in. - pub life: PhantomData<&'a mut ()>, -} - -/// Gets a string version of an error message. -pub fn strerror(code: c_int) -> Option<&'static str> { - // SAFETY: Every single PAM implementation I can find (Linux-PAM, OpenPAM, - // Sun, 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() - } -} - -pub use libpam_sys::*; - -type pam_handle = LibPamHandle; -type pam_conv = LibPamConversation<'static>; - -include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
--- a/src/libpam/question.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/src/libpam/question.rs Tue Jul 01 06:11:43 2025 -0400 @@ -2,177 +2,22 @@ #[cfg(feature = "linux-pam-ext")] use crate::conv::{BinaryQAndA, RadioQAndA}; -use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA}; -use crate::libpam::conversation::OwnedMessage; -use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString, Immovable}; -use crate::libpam::pam_ffi; -pub use crate::libpam::pam_ffi::Question; +use crate::conv::{ErrorMsg, Exchange, InfoMsg, MaskedQAndA, QAndA}; +use crate::libpam::conversation::OwnedExchange; +use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString}; use crate::ErrorCode; use crate::Result; use num_enum::{IntoPrimitive, TryFromPrimitive}; -use std::cell::Cell; -use std::ffi::{c_void, CStr}; -use std::pin::Pin; -use std::{ptr, slice}; - -/// Abstraction of a collection of questions to be sent in a PAM conversation. -/// -/// The PAM C API conversation function looks like this: -/// -/// ```c -/// int pam_conv( -/// int count, -/// const struct pam_message **questions, -/// struct pam_response **answers, -/// void *appdata_ptr, -/// ) -/// ``` -/// -/// On Linux-PAM and other compatible implementations, `questions` -/// is treated as a pointer-to-pointers, like `int argc, char **argv`. -/// (In this situation, the value of `Questions.indirect` is -/// the pointer passed to `pam_conv`.) -/// -/// ```text -/// points to ┌───────────────┐ ╔═ Question ═╗ -/// questions ┄┄┄┄┄┄┄┄┄┄> │ questions[0] ┄┼┄┄┄┄> ║ style ║ -/// │ questions[1] ┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ... -/// │ ... │ ┆ ╚════════════╝ -/// ┆ -/// ┆ ╔═ Question ═╗ -/// ╰┄┄> ║ style ║ -/// ║ data ┄┄┄┄┄┄╫┄┄> ... -/// ╚════════════╝ -/// ``` -/// -/// On OpenPAM and other compatible implementations (like Solaris), -/// `messages` is a pointer-to-pointer-to-array. This appears to be -/// the correct implementation as required by the XSSO specification. -/// -/// ```text -/// points to ┌─────────────┐ ╔═ Question[] ═╗ -/// questions ┄┄┄┄┄┄┄┄┄┄> │ *questions ┄┼┄┄┄┄┄> ║ style ║ -/// └─────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... -/// ╟──────────────╢ -/// ║ style ║ -/// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... -/// ╟──────────────╢ -/// ║ ... ║ -/// ``` -pub trait QuestionsTrait { - /// Allocates memory for this indirector and all its members. - fn new(messages: &[Message]) -> Result<Self> - where - Self: Sized; - - /// Gets the pointer that is passed . - fn ptr(self: Pin<&Self>) -> *const *const Question; - - /// Converts a pointer into a borrowed list of Questions. - /// - /// # Safety - /// - /// You have to provide a valid pointer. - unsafe fn borrow_ptr<'a>( - ptr: *const *const Question, - count: usize, - ) -> impl Iterator<Item = &'a Question>; -} - -#[cfg(pam_impl = "linux-pam")] -pub type Questions = LinuxPamQuestions; - -#[cfg(not(pam_impl = "linux-pam"))] -pub type Questions = XSsoQuestions; +use std::ffi::{c_int, c_void, CStr}; -/// The XSSO standard version of the pointer train to questions. -#[derive(Debug)] -#[repr(C)] -pub struct XSsoQuestions { - /// Points to the memory address where the meat of `questions` is. - /// **The memory layout of Vec is not specified**, and we need to return - /// a pointer to the pointer, hence we have to store it here. - pointer: Cell<*const Question>, - questions: Vec<Question>, - _marker: Immovable, -} - -impl XSsoQuestions { - fn len(&self) -> usize { - self.questions.len() - } - fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { - self.questions.iter_mut() - } -} - -impl QuestionsTrait for XSsoQuestions { - fn new(messages: &[Message]) -> Result<Self> { - let questions: Result<Vec<_>> = messages.iter().map(Question::try_from).collect(); - let questions = questions?; - Ok(Self { - pointer: Cell::new(ptr::null()), - questions, - _marker: Default::default(), - }) - } - - fn ptr(self: Pin<&Self>) -> *const *const Question { - let me = self.get_ref(); - me.pointer.set(self.questions.as_ptr()); - me.pointer.as_ptr() - } - - unsafe fn borrow_ptr<'a>( - ptr: *const *const Question, - count: usize, - ) -> impl Iterator<Item = &'a Question> { - slice::from_raw_parts(*ptr, count).iter() - } -} - -/// The Linux version of the pointer train to questions. -#[derive(Debug)] -#[repr(C)] -pub struct LinuxPamQuestions { - #[allow(clippy::vec_box)] // we need to box vec items. - /// The place where the questions are. - questions: Vec<Box<Question>>, -} - -impl LinuxPamQuestions { - fn len(&self) -> usize { - self.questions.len() - } - - fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { - self.questions.iter_mut().map(AsMut::as_mut) - } -} - -impl QuestionsTrait for LinuxPamQuestions { - fn new(messages: &[Message]) -> Result<Self> { - let questions: Result<_> = messages - .iter() - .map(|msg| Question::try_from(msg).map(Box::new)) - .collect(); - Ok(Self { - questions: questions?, - }) - } - - fn ptr(self: Pin<&Self>) -> *const *const Question { - self.questions.as_ptr().cast() - } - - unsafe fn borrow_ptr<'a>( - ptr: *const *const Question, - count: usize, - ) -> impl Iterator<Item = &'a Question> { - slice::from_raw_parts(ptr.cast::<&Question>(), count) - .iter() - .copied() - } +mod style_const { + pub use libpam_sys::*; + #[cfg(not(feature = "link"))] + #[cfg_pam_impl(not("LinuxPam"))] + pub const PAM_RADIO_TYPE: i32 = 897; + #[cfg(not(feature = "link"))] + #[cfg_pam_impl(not("LinuxPam"))] + pub const PAM_BINARY_PROMPT: i32 = 10010101; } /// The C enum values for messages shown to the user. @@ -180,22 +25,42 @@ #[repr(i32)] enum Style { /// Requests information from the user; will be masked when typing. - PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF, + PromptEchoOff = style_const::PAM_PROMPT_ECHO_OFF, /// Requests information from the user; will not be masked. - PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON, + PromptEchoOn = style_const::PAM_PROMPT_ECHO_ON, /// An error message. - ErrorMsg = pam_ffi::PAM_ERROR_MSG, + ErrorMsg = style_const::PAM_ERROR_MSG, /// An informational message. - TextInfo = pam_ffi::PAM_TEXT_INFO, + TextInfo = style_const::PAM_TEXT_INFO, /// Yes/No/Maybe conditionals. A Linux-PAM extension. #[cfg(feature = "linux-pam-ext")] - RadioType = pam_ffi::PAM_RADIO_TYPE, + RadioType = style_const::PAM_RADIO_TYPE, /// For server–client non-human interaction. /// /// NOT part of the X/Open PAM specification. /// A Linux-PAM extension. #[cfg(feature = "linux-pam-ext")] - BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT, + BinaryPrompt = style_const::PAM_BINARY_PROMPT, +} + +/// 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::conv::Exchange). +/// +/// This question, and its internal data, is owned by its creator +/// (either the module or PAM itself). +#[repr(C)] +#[derive(Debug)] +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`], + /// but for requests with style `PAM_BINARY_PROMPT`, + /// this will be `CBinaryData` (a Linux-PAM extension). + pub data: Option<CHeapBox<c_void>>, } impl Question { @@ -222,9 +87,9 @@ } } -impl TryFrom<&Message<'_>> for Question { +impl TryFrom<&Exchange<'_>> for Question { type Error = ErrorCode; - fn try_from(msg: &Message) -> Result<Self> { + fn try_from(msg: &Exchange) -> Result<Self> { let alloc = |style, text| -> Result<_> { Ok((style, unsafe { CHeapBox::cast(CHeapString::new(text)?.into_box()) @@ -232,18 +97,20 @@ }; // We will only allocate heap data if we have a valid input. let (style, data): (_, CHeapBox<c_void>) = match *msg { - Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), - Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), - Message::Error(p) => alloc(Style::ErrorMsg, p.question()), - Message::Info(p) => alloc(Style::TextInfo, p.question()), + Exchange::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), + Exchange::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), + Exchange::Error(p) => alloc(Style::ErrorMsg, p.question()), + Exchange::Info(p) => alloc(Style::TextInfo, p.question()), #[cfg(feature = "linux-pam-ext")] - Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), + Exchange::RadioPrompt(p) => alloc(Style::RadioType, p.question()), #[cfg(feature = "linux-pam-ext")] - Message::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe { + Exchange::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe { CHeapBox::cast(CBinaryData::alloc(p.question())?) })), #[cfg(not(feature = "linux-pam-ext"))] - Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError), + Exchange::RadioPrompt(_) | Exchange::BinaryPrompt(_) => { + Err(ErrorCode::ConversationError) + } }?; Ok(Self { style: style.into(), @@ -284,7 +151,7 @@ } } -impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> { +impl<'a> TryFrom<&'a Question> for OwnedExchange<'a> { type Error = ErrorCode; fn try_from(question: &'a Question) -> Result<Self> { let style: Style = question @@ -313,76 +180,47 @@ #[cfg(test)] mod tests { + use super::*; macro_rules! assert_matches { - ($id:ident => $variant:path, $q:expr) => { - if let $variant($id) = $id { - assert_eq!($q, $id.question()); + (($variant:path, $q:expr), $input:expr) => { + let input = $input; + let exc = input.exchange(); + if let $variant(msg) = exc { + assert_eq!($q, msg.question()); } else { - panic!("mismatched enum variant {x:?}", x = $id); + panic!( + "want enum variant {v}, got {exc:?}", + v = stringify!($variant) + ); } }; } - macro_rules! tests { ($fn_name:ident<$typ:ident>) => { - mod $fn_name { - use super::super::*; - #[test] - fn standard() { - let interrogation = Box::pin(<$typ>::new(&[ - MaskedQAndA::new("hocus pocus").message(), - QAndA::new("what").message(), - QAndA::new("who").message(), - InfoMsg::new("hey").message(), - ErrorMsg::new("gasp").message(), - ]) - .unwrap()); - let indirect = interrogation.as_ref().ptr(); + // TODO: Test TryFrom<Exchange> for Question/OwnedQuestion. - let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) }; - let messages: Vec<OwnedMessage> = remade - .map(TryInto::try_into) - .collect::<Result<_>>() - .unwrap(); - let [masked, what, who, hey, gasp] = messages.try_into().unwrap(); - assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); - assert_matches!(what => OwnedMessage::Prompt, "what"); - assert_matches!(who => OwnedMessage::Prompt, "who"); - assert_matches!(hey => OwnedMessage::Info, "hey"); - assert_matches!(gasp => OwnedMessage::Error, "gasp"); - } + #[test] + fn standard() { + assert_matches!( + (Exchange::MaskedPrompt, "hocus pocus"), + MaskedQAndA::new("hocus pocus") + ); + assert_matches!((Exchange::Prompt, "what"), QAndA::new("what")); + assert_matches!((Exchange::Prompt, "who"), QAndA::new("who")); + assert_matches!((Exchange::Info, "hey"), InfoMsg::new("hey")); + assert_matches!((Exchange::Error, "gasp"), ErrorMsg::new("gasp")); + } - #[test] - #[cfg(not(feature = "linux-pam-ext"))] - fn no_linux_extensions() { - use crate::conv::{BinaryQAndA, RadioQAndA}; - <$typ>::new(&[ - BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), - RadioQAndA::new("you must choose").message(), - ]).unwrap_err(); - } - - #[test] - #[cfg(feature = "linux-pam-ext")] - fn linux_extensions() { - let interrogation = Box::pin(<$typ>::new(&[ - BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), - RadioQAndA::new("you must choose").message(), - ]).unwrap()); - let indirect = interrogation.as_ref().ptr(); - - let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) }; - let messages: Vec<OwnedMessage> = remade - .map(TryInto::try_into) - .collect::<Result<_>>() - .unwrap(); - let [bin, choose] = messages.try_into().unwrap(); - assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); - assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); - } - } - }} - - tests!(test_xsso<XSsoQuestions>); - tests!(test_linux<LinuxPamQuestions>); + #[test] + #[cfg(feature = "linux-pam-ext")] + fn linux_extensions() { + assert_matches!( + (Exchange::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)), + BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)) + ); + assert_matches!( + (Exchange::RadioPrompt, "you must choose"), + RadioQAndA::new("you must choose") + ); + } }
--- a/src/logging.rs Mon Jun 30 23:49:54 2025 -0400 +++ b/src/logging.rs Tue Jul 01 06:11:43 2025 -0400 @@ -15,15 +15,16 @@ //! and may even itself implement `log::Log`, but that interface is not exposed //! to the generic PAM user. -#[cfg(all(feature = "link", pam_impl = "openpam"))] +use libpam_sys::cfg_pam_impl; + +#[cfg_pam_impl("OpenPam")] mod levels { - use crate::libpam::pam_ffi; - pub const ERROR: i32 = pam_ffi::PAM_LOG_ERROR; - pub const WARN: i32 = pam_ffi::PAM_LOG_NOTICE; - pub const INFO: i32 = pam_ffi::PAM_LOG_VERBOSE; - pub const DEBUG: i32 = pam_ffi::PAM_LOG_DEBUG; + pub const ERROR: i32 = libpam_sys::PAM_LOG_ERROR; + pub const WARN: i32 = libpam_sys::PAM_LOG_NOTICE; + pub const INFO: i32 = libpam_sys::PAM_LOG_VERBOSE; + pub const DEBUG: i32 = libpam_sys::PAM_LOG_DEBUG; } -#[cfg(not(all(feature = "link", pam_impl = "openpam")))] +#[cfg_pam_impl(not("OpenPam"))] mod levels { pub const ERROR: i32 = libc::LOG_ERR; pub const WARN: i32 = libc::LOG_WARNING;
--- a/testharness/Cargo.toml Mon Jun 30 23:49:54 2025 -0400 +++ b/testharness/Cargo.toml Tue Jul 01 06:11:43 2025 -0400 @@ -11,9 +11,10 @@ crate-type = ["cdylib"] [features] -illumos-ext = ["nonstick/illumos-ext"] +basic-ext = ["nonstick/basic-ext"] linux-pam-ext = ["nonstick/linux-pam-ext"] openpam-ext = ["nonstick/openpam-ext"] +sun-ext = ["nonstick/sun-ext"] test-install = [] [dependencies]