Mercurial > crates > nonstick
changeset 73:ac6881304c78
Do conversations, along with way too much stuff.
This implements conversations, along with all the memory management
brouhaha that goes along with it. The conversation now lives directly
on the handle rather than being a thing you have to get from it
and then call manually. It Turns Out this makes things a lot easier!
I guess we reorganized things again. For the last time. For real.
I promise.
This all passes ASAN, so it seems Pretty Good!
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Thu, 05 Jun 2025 03:41:38 -0400 |
parents | 47eb242a4f88 |
children | c7c596e6388f |
files | src/conv.rs src/handle.rs src/items.rs src/lib.rs src/module.rs src/pam_ffi/conversation.rs src/pam_ffi/handle.rs src/pam_ffi/memory.rs src/pam_ffi/message.rs src/pam_ffi/mod.rs src/pam_ffi/response.rs |
diffstat | 11 files changed, 1027 insertions(+), 679 deletions(-) [+] |
line wrap: on
line diff
--- a/src/conv.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/conv.rs Thu Jun 05 03:41:38 2025 -0400 @@ -4,8 +4,7 @@ #![allow(dead_code)] use crate::constants::Result; -use crate::pam_ffi::LibPamConversation; -use crate::pam_ffi::Message; +use crate::ErrorCode; use secure_string::SecureString; // TODO: In most cases, we should be passing around references to strings // or binary data. Right now we don't because that turns type inference and @@ -15,6 +14,47 @@ // associated types in the various Conversation traits to avoid copying // when unnecessary. +/// The types of message and request that can be sent to a user. +/// +/// The data within each enum value is the prompt (or other information) +/// that will be presented to the user. +#[derive(Clone, Copy, Debug)] +pub enum Message<'a> { + /// Requests information from the user; will be masked when typing. + /// + /// Response: [`MaskedText`](Response::MaskedText) + MaskedPrompt(&'a str), + /// Requests information from the user; will not be masked. + /// + /// Response: [`Text`](Response::Text) + Prompt(&'a str), + /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). + /// + /// Response: [`Text`](Response::Text) + /// (Linux-PAM documentation doesn't define its contents.) + RadioPrompt(&'a str), + /// Raises an error message to the user. + /// + /// Response: [`NoResponse`](Response::NoResponse) + Error(&'a str), + /// Sends an informational message to the user. + /// + /// Response: [`NoResponse`](Response::NoResponse) + Info(&'a str), + /// Requests binary data from the client (a Linux-PAM extension). + /// + /// This is used for non-human or non-keyboard prompts (security key?). + /// NOT part of the X/Open PAM specification. + /// + /// Response: [`Binary`](Response::Binary) + BinaryPrompt { + /// Some binary data. + data: &'a [u8], + /// A "type" that you can use for signalling. Has no strict definition in PAM. + data_type: u8, + }, +} + /// The responses that PAM will return from a request. #[derive(Debug, PartialEq, derive_more::From)] pub enum Response { @@ -46,6 +86,15 @@ Binary(BinaryData), } +/// The function type for a conversation. +/// +/// A macro to save typing `FnMut(&[Message]) -> Result<Vec<Response>>`. +#[macro_export] +macro_rules! conv_type { + () => {FnMut(&[Message]) -> Result<Vec<Response>>}; + (impl) => { impl FnMut(&[Message]) -> Result<Vec<Response>> } +} + /// A channel for PAM modules to request information from the user. /// /// This trait is used by both applications and PAM modules: @@ -59,7 +108,102 @@ /// /// The returned Vec of messages always contains exactly as many entries /// as there were messages in the request; one corresponding to each. - fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>>; + fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>>; +} + +fn conversation_func(func: conv_type!(impl)) -> impl Conversation { + Convo(func) +} + +struct Convo<C: FnMut(&[Message]) -> Result<Vec<Response>>>(C); + +impl<C: FnMut(&[Message]) -> Result<Vec<Response>>> Conversation for Convo<C> { + fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> { + self.0(messages) + } +} + +/// Provides methods to make it easier to send exactly one message. +/// +/// This is primarily used by PAM modules, so that a module that only needs +/// one piece of information at a time doesn't have a ton of boilerplate. +/// You may also find it useful for testing PAM application libraries. +/// +/// ``` +/// # use nonstick::{PamHandleModule, Conversation, Result}; +/// # fn _do_test(mut pam_handle: impl PamHandleModule) -> Result<()> { +/// use nonstick::ConversationMux; +/// +/// let token = pam_handle.masked_prompt("enter your one-time token")?; +/// # Ok(()) +/// # } +pub trait ConversationMux { + /// Prompts the user for something. + fn prompt(&mut self, request: &str) -> Result<String>; + /// Prompts the user for something, but hides what the user types. + fn masked_prompt(&mut self, request: &str) -> Result<SecureString>; + /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). + /// + /// PAM documentation doesn't define the format of the response. + fn radio_prompt(&mut self, request: &str) -> Result<String>; + /// Alerts the user to an error. + fn error(&mut self, message: &str); + /// Sends an informational message to the user. + fn info(&mut self, message: &str); + /// Requests binary data from the user (a Linux-PAM extension). + fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>; +} + +impl<C: Conversation> ConversationMux for C { + /// Prompts the user for something. + fn prompt(&mut self, request: &str) -> Result<String> { + let resp = self.converse(&[Message::Prompt(request)])?.pop(); + match resp { + Some(Response::Text(s)) => Ok(s), + _ => Err(ErrorCode::ConversationError), + } + } + + /// Prompts the user for something, but hides what the user types. + fn masked_prompt(&mut self, request: &str) -> Result<SecureString> { + let resp = self.converse(&[Message::MaskedPrompt(request)])?.pop(); + match resp { + Some(Response::MaskedText(s)) => Ok(s), + _ => Err(ErrorCode::ConversationError), + } + } + + /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). + /// + /// PAM documentation doesn't define the format of the response. + fn radio_prompt(&mut self, request: &str) -> Result<String> { + let resp = self.converse(&[Message::RadioPrompt(request)])?.pop(); + match resp { + Some(Response::Text(s)) => Ok(s), + _ => Err(ErrorCode::ConversationError), + } + } + + /// Alerts the user to an error. + fn error(&mut self, message: &str) { + let _ = self.converse(&[Message::Error(message)]); + } + + /// Sends an informational message to the user. + fn info(&mut self, message: &str) { + let _ = self.converse(&[Message::Info(message)]); + } + + /// Requests binary data from the user (a Linux-PAM extension). + fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> { + let resp = self + .converse(&[Message::BinaryPrompt { data, data_type }])? + .pop(); + match resp { + Some(Response::Binary(d)) => Ok(d), + _ => Err(ErrorCode::ConversationError), + } + } } /// Trait that an application can implement if they want to handle messages @@ -81,14 +225,11 @@ fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>; } -impl Conversation for LibPamConversation { - fn send(&mut self, _: &[Message]) -> Result<Vec<Response>> { - todo!() - } -} - -impl<D: DemuxedConversation> Conversation for D { - fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> { +impl<DM> Conversation for DM +where + DM: DemuxedConversation, +{ + fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> { messages .iter() .map(|msg| match *msg { @@ -195,7 +336,7 @@ Response::NoResponse, ], tester - .send(&[ + .converse(&[ Message::Prompt("what"), Message::MaskedPrompt("reveal"), Message::Error("whoopsie"), @@ -208,7 +349,7 @@ assert_eq!( ErrorCode::PermissionDenied, - tester.send(&[Message::Prompt("give_err")]).unwrap_err(), + tester.converse(&[Message::Prompt("give_err")]).unwrap_err(), ); // Test the Linux-PAM extensions separately. @@ -219,7 +360,7 @@ Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)), ], tester - .send(&[ + .converse(&[ Message::RadioPrompt("channel?"), Message::BinaryPrompt { data: &[10, 9, 8], @@ -229,4 +370,80 @@ .unwrap() ); } + + #[test] + fn test_mux() { + use super::ConversationMux; + struct MuxTester; + + impl Conversation for MuxTester { + fn converse(&mut self, messages: &[Message]) -> crate::Result<Vec<Response>> { + if let [msg] = messages { + match msg { + Message::Info(info) => { + assert_eq!("let me tell you", *info); + Ok(vec![Response::NoResponse]) + } + Message::Error(error) => { + assert_eq!("oh no", *error); + Ok(vec![Response::NoResponse]) + } + Message::Prompt("should_error") => Err(ErrorCode::BufferError), + Message::Prompt(ask) => { + assert_eq!("question", *ask); + Ok(vec![Response::Text("answer".to_owned())]) + } + Message::MaskedPrompt("return_wrong_type") => { + Ok(vec![Response::NoResponse]) + } + Message::MaskedPrompt(ask) => { + assert_eq!("password!", *ask); + Ok(vec![Response::MaskedText(SecureString::from( + "open sesame", + ))]) + } + Message::BinaryPrompt { data, data_type } => { + assert_eq!(&[1, 2, 3], data); + assert_eq!(69, *data_type); + Ok(vec![Response::Binary(super::BinaryData::new( + vec![3, 2, 1], + 42, + ))]) + } + Message::RadioPrompt(ask) => { + assert_eq!("radio?", *ask); + Ok(vec![Response::Text("yes".to_owned())]) + } + } + } else { + panic!("messages is the wrong size ({len})", len = messages.len()) + } + } + } + + let mut tester = MuxTester; + + assert_eq!("answer", tester.prompt("question").unwrap()); + assert_eq!( + SecureString::from("open sesame"), + tester.masked_prompt("password!").unwrap() + ); + tester.error("oh no"); + tester.info("let me tell you"); + { + assert_eq!("yes", tester.radio_prompt("radio?").unwrap()); + assert_eq!( + super::BinaryData::new(vec![3, 2, 1], 42), + tester.binary_prompt(&[1, 2, 3], 69).unwrap(), + ) + } + assert_eq!( + ErrorCode::BufferError, + tester.prompt("should_error").unwrap_err(), + ); + assert_eq!( + ErrorCode::ConversationError, + tester.masked_prompt("return_wrong_type").unwrap_err() + ) + } }
--- a/src/handle.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/handle.rs Thu Jun 05 03:41:38 2025 -0400 @@ -1,12 +1,6 @@ //! The wrapper types and traits for handles into the PAM library. -use crate::constants::{ErrorCode, Result}; +use crate::constants::Result; use crate::conv::Conversation; -use crate::items::ItemType; -use crate::module::ConversationMux; -use crate::pam_ffi; -use crate::pam_ffi::{memory, LibPamConversation, LibPamHandle}; -use std::ffi::{c_char, c_int}; -use std::{mem, ptr}; macro_rules! trait_item { (get = $getter:ident, item = $item:literal $(, see = $see:path)? $(, $($doc:literal)*)?) => { @@ -57,12 +51,23 @@ }; } -/// Features of a PAM handle that are available to applications and modules. +/// 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. /// -/// You probably want [`LibPamHandle`]. This trait is intended to allow creating -/// mock PAM handle types used for testing PAM modules and applications. -pub trait PamHandle { - type Conv: Conversation; +/// This base trait includes features of a PAM handle that are available +/// to both applications and modules. +/// +/// You probably want [`LibPamHandle`](crate::pam_ffi::OwnedLibPamHandle). +/// This trait is intended to allow creating mock PAM handle types +/// to test PAM modules and applications. +pub trait PamShared { /// Retrieves the name of the user who is authenticating or logging in. /// /// If the username has previously been obtained, this uses that username; @@ -70,7 +75,7 @@ /// /// 1. The prompt string passed to this function. /// 2. The string returned by `get_user_prompt_item`. - /// 3. The default prompt, `login: ` + /// 3. The default prompt, `login: `. /// /// See the [`pam_get_user` manual page][man] /// or [`pam_get_user` in the Module Writer's Guide][mwg]. @@ -78,8 +83,8 @@ /// # Example /// /// ```no_run - /// # use nonstick::PamModuleHandle; - /// # fn _doc(handle: &mut impl PamModuleHandle) -> Result<(), Box<dyn std::error::Error>> { + /// # use nonstick::PamShared; + /// # fn _doc(handle: &mut impl PamShared) -> Result<(), Box<dyn std::error::Error>> { /// // Get the username using the default prompt. /// let user = handle.get_user(None)?; /// // Get the username using a custom prompt. @@ -92,16 +97,18 @@ /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_user.3.html /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_user - fn get_user(&mut self, prompt: Option<&str>) -> Result<Option<&str>>; + fn get_user(&mut self, prompt: Option<&str>) -> Result<&str>; trait_item!( get = user_item, item = "PAM_USER", + see = Self::get_user, "The identity of the user for whom service is being requested." "" - "While PAM usually sets this automatically during the course of " - "a [`get_user`](Self::get_user) call, it may be changed by a module " - "over the course of the PAM transaction." + "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." ); trait_item!( @@ -225,9 +232,9 @@ /// If you are not writing a PAM client application (e.g., you are writing /// a module), you should not use the functionality exposed by this trait. /// -/// Like [`PamHandle`], this is intended to allow creating mock implementations +/// Like [`PamShared`], this is intended to allow creating mock implementations /// of PAM for testing PAM applications. -pub trait PamApplicationHandle: PamHandle { +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 @@ -238,9 +245,9 @@ /// See the [`pam_end` manual page][man] for more information. /// /// ```no_run - /// # use nonstick::PamApplicationHandle; + /// # use nonstick::handle::PamApplicationOnly; /// # use std::error::Error; - /// # fn _doc(handle: impl PamApplicationHandle, auth_result: nonstick::Result<()>) -> Result<(), Box<dyn 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)?; @@ -250,9 +257,6 @@ /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html fn close(self, status: Result<()>) -> Result<()>; - - /// Uses a new PAM conversation. - fn set_conversation(&mut self, conversation: Self::Conv) -> Result<()>; } /// Functionality of a PAM handle that can be expected by a PAM module. @@ -260,15 +264,9 @@ /// If you are not writing a PAM module (e.g., you are writing an application), /// you should not use any of the functionality exposed by this trait. /// -/// Like [`PamHandle`], this is intended to allow creating mock implementations +/// Like [`PamShared`], this is intended to allow creating mock implementations /// of PAM for testing PAM modules. -pub trait PamModuleHandle: PamHandle { - /// Gets a channel for communication with the user. - /// - /// The Conversation is the conduit which you use for all communication - /// with the user. - fn conversation(&mut self) -> Result<ConversationMux<'_, Self::Conv>>; - +pub trait PamModuleOnly: Conversation { /// Retrieves the authentication token from the user. /// /// This should only be used by *authentication* and *password-change* @@ -280,8 +278,8 @@ /// # Example /// /// ```no_run - /// # use nonstick::PamModuleHandle; - /// # fn _doc(handle: &mut impl PamModuleHandle) -> Result<(), Box<dyn std::error::Error>> { + /// # use nonstick::handle::PamModuleOnly; + /// # fn _doc(handle: &mut impl PamModuleOnly) -> 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. @@ -292,7 +290,7 @@ /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_authtok.3.html /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item - fn get_authtok(&mut self, prompt: Option<&str>) -> Result<Option<&str>>; + fn get_authtok(&mut self, prompt: Option<&str>) -> Result<&str>; trait_item!( get = authtok_item, @@ -351,135 +349,3 @@ fn set_data<T>(&mut self, key: &str, data: Box<T>) -> Result<()>; */ } - - -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 { 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) - } -} - -impl Drop for LibPamHandle { - /// Ends the PAM session with a zero error code. - /// You probably want to call [`close`](Self::close) instead of - /// letting this drop by itself. - fn drop(&mut self) { - unsafe { - pam_ffi::pam_end(self, 0); - } - } -} - -macro_rules! cstr_item { - (get = $getter:ident, item = $item_type:path) => { - fn $getter(&mut self) -> Result<Option<&str>> { - unsafe { self.get_cstr_item($item_type) } - } - }; - (set = $setter:ident, item = $item_type:path) => { - fn $setter(&mut self, value: Option<&str>) -> Result<()> { - unsafe { self.set_cstr_item($item_type, value) } - } - }; -} - -impl PamHandle for LibPamHandle { - type Conv = LibPamConversation; - fn get_user(&mut self, prompt: Option<&str>) -> Result<Option<&str>> { - 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())) - }; - ErrorCode::result_from(ret)?; - unsafe { memory::wrap_string(output) } - } - - cstr_item!(get = user_item, item = ItemType::User); - cstr_item!(set = set_user_item, item = ItemType::User); - cstr_item!(get = service, item = ItemType::Service); - cstr_item!(set = set_service, item = ItemType::Service); - cstr_item!(get = user_prompt, item = ItemType::UserPrompt); - cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt); - cstr_item!(get = tty_name, item = ItemType::Tty); - cstr_item!(set = set_tty_name, item = ItemType::Tty); - cstr_item!(get = remote_user, item = ItemType::RemoteUser); - cstr_item!(set = set_remote_user, item = ItemType::RemoteUser); - cstr_item!(get = remote_host, item = ItemType::RemoteHost); - cstr_item!(set = set_remote_host, item = ItemType::RemoteHost); - cstr_item!(set = set_authtok_item, item = ItemType::AuthTok); - cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok); -} - -impl PamApplicationHandle for LibPamHandle { - fn close(mut self, status: Result<()>) -> Result<()> { - let result = unsafe { pam_ffi::pam_end(&mut self, ErrorCode::result_to_c(status)) }; - // Since we've already `pam_end`ed this session, we don't want it to be - // double-freed on drop. - mem::forget(self); - ErrorCode::result_from(result) - } - - fn set_conversation(&mut self, conversation: Self::Conv) -> Result<()> { - todo!() - } -} - -impl PamModuleHandle for LibPamHandle { - fn conversation(&mut self) -> Result<ConversationMux<'_, Self::Conv>> { - todo!() - } - - fn get_authtok(&mut self, prompt: Option<&str>) -> Result<Option<&str>> { - let prompt = memory::option_cstr(prompt)?; - let mut output: *const c_char = ptr::null_mut(); - let res = unsafe { - pam_ffi::pam_get_authtok( - self, - ItemType::AuthTok.into(), - &mut output, - memory::prompt_ptr(prompt.as_ref()), - ) - }; - ErrorCode::result_from(res)?; - unsafe { memory::wrap_string(output) } - } - - cstr_item!(get = authtok_item, item = ItemType::AuthTok); - cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok); -} - -/// Function called at the end of a PAM session that is called to clean up -/// a value previously provided to PAM in a `pam_set_data` call. -/// -/// You should never call this yourself. -extern "C" fn set_data_cleanup<T>(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) { - unsafe { - let _data: Box<T> = Box::from_raw(c_data.cast()); - } -}
--- a/src/items.rs Wed Jun 04 03:53:36 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -//! Things that can be gotten with the `pam_get_item` function. - -use crate::constants::InvalidEnum; -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; -use std::ffi::c_int; - -/// Identifies what is being gotten or set with `pam_get_item` -/// or `pam_set_item`. -#[derive(FromPrimitive)] -#[repr(i32)] -#[non_exhaustive] // because C could give us anything! -pub enum ItemType { - /// The PAM service name. - Service = 1, - /// The user's login name. - User = 2, - /// The TTY name. - Tty = 3, - /// The remote host (if applicable). - RemoteHost = 4, - /// The conversation struct (not a CStr-based item). - Conversation = 5, - /// The authentication token (password). - AuthTok = 6, - /// The old authentication token (when changing passwords). - OldAuthTok = 7, - /// The remote user's name. - RemoteUser = 8, - /// The prompt shown when requesting a username. - UserPrompt = 9, - /// App-supplied function to override failure delays. - FailDelay = 10, - /// X display name. - XDisplay = 11, - /// X server authentication data. - XAuthData = 12, - /// The type of `pam_get_authtok`. - AuthTokType = 13, -} - -impl TryFrom<c_int> for ItemType { - type Error = InvalidEnum<Self>; - fn try_from(value: c_int) -> Result<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/lib.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/lib.rs Thu Jun 05 03:41:38 2025 -0400 @@ -27,17 +27,16 @@ pub mod constants; mod conv; -mod items; pub mod module; -mod handle; +pub mod handle; mod pam_ffi; #[doc(inline)] pub use crate::{ constants::{ErrorCode, Flags, Result}, - conv::{Conversation, DemuxedConversation, Response}, - handle::{PamApplicationHandle, PamHandle, PamModuleHandle}, + conv::{Conversation, ConversationMux, DemuxedConversation, Response}, + handle::{PamHandleApplication, PamHandleModule, PamShared}, module::PamModule, - pam_ffi::{LibPamHandle, Message}, + pam_ffi::{LibPamHandle, OwnedLibPamHandle}, };
--- a/src/module.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/module.rs Thu Jun 05 03:41:38 2025 -0400 @@ -4,11 +4,7 @@ #![allow(dead_code)] use crate::constants::{ErrorCode, Flags, Result}; -use crate::conv::BinaryData; -use crate::conv::{Conversation, Response}; -use crate::handle::PamModuleHandle; -use crate::pam_ffi::Message; -use secure_string::SecureString; +use crate::handle::PamHandleModule; use std::ffi::CStr; /// A trait for a PAM module to implement. @@ -25,14 +21,14 @@ /// [manpage]: https://www.man7.org/linux/man-pages/man3/pam.3.html /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_MWG.html #[allow(unused_variables)] -pub trait PamModule<T: PamModuleHandle> { +pub trait PamModule<T: PamHandleModule> { // Functions for auth modules. /// Authenticate the user. /// /// This is probably the first thing you want to implement. /// In most cases, you will want to get the user and password, - /// using [`PamHandle::get_user`] and [`PamModuleHandle::get_authtok`], + /// using [`PamHandle::get_user`] and [`PamModuleOnly::get_authtok`], /// and verify them against something. /// /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg] @@ -82,7 +78,6 @@ /// See [the Module Writer's Guide entry for `pam_sm_acct_mgmt`][mwg] /// for more information. /// - /// /// # Valid flags /// /// This function may be called with the following flags set: @@ -240,81 +235,6 @@ } } -/// Provides methods to make it easier to send exactly one message. -/// -/// This is primarily used by PAM modules, so that a module that only needs -/// one piece of information at a time doesn't have a ton of boilerplate. -/// You may also find it useful for testing PAM application libraries. -/// -/// ``` -/// # use nonstick::Result; -/// # use nonstick::conv::Conversation; -/// # use nonstick::module::ConversationMux; -/// # fn _do_test(conv: impl Conversation) -> Result<()> { -/// let mut mux = ConversationMux(conv); -/// let token = mux.masked_prompt("enter your one-time token")?; -/// # Ok(()) -/// # } -pub struct ConversationMux<'a, C: Conversation>(pub &'a mut C); - -impl<C: Conversation> Conversation for ConversationMux<'_, C> { - fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> { - self.0.send(messages) - } -} - -impl<C: Conversation> ConversationMux<'_, C> { - /// Prompts the user for something. - pub fn prompt(&mut self, request: &str) -> Result<String> { - let resp = self.send(&[Message::Prompt(request)])?.pop(); - match resp { - Some(Response::Text(s)) => Ok(s), - _ => Err(ErrorCode::ConversationError), - } - } - - /// Prompts the user for something, but hides what the user types. - pub fn masked_prompt(&mut self, request: &str) -> Result<SecureString> { - let resp = self.send(&[Message::MaskedPrompt(request)])?.pop(); - match resp { - Some(Response::MaskedText(s)) => Ok(s), - _ => Err(ErrorCode::ConversationError), - } - } - - /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). - /// - /// PAM documentation doesn't define the format of the response. - pub fn radio_prompt(&mut self, request: &str) -> Result<String> { - let resp = self.send(&[Message::RadioPrompt(request)])?.pop(); - match resp { - Some(Response::Text(s)) => Ok(s), - _ => Err(ErrorCode::ConversationError), - } - } - - /// Alerts the user to an error. - pub fn error(&mut self, message: &str) { - let _ = self.send(&[Message::Error(message)]); - } - - /// Sends an informational message to the user. - pub fn info(&mut self, message: &str) { - let _ = self.send(&[Message::Info(message)]); - } - - /// Requests binary data from the user (a Linux-PAM extension). - pub fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> { - let resp = self - .send(&[Message::BinaryPrompt { data, data_type }])? - .pop(); - match resp { - Some(Response::Binary(d)) => Ok(d), - _ => Err(ErrorCode::ConversationError), - } - } -} - /// Generates the dynamic library entry points for a [PamModule] implementation. /// /// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will @@ -326,25 +246,25 @@ /// Here is full example of a PAM module that would authenticate and authorize everybody: /// /// ```no_run -/// use nonstick::{Flags, LibPamHandle, PamModule, PamModuleHandle, Result as PamResult, pam_hooks}; +/// use nonstick::{Flags, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks}; /// use std::ffi::CStr; /// # fn main() {} /// /// struct MyPamModule; /// pam_hooks!(MyPamModule); /// -/// impl<T: PamModuleHandle> PamModule<T> for MyPamModule { +/// impl<T: PamHandleModule> PamModule<T> for MyPamModule { /// fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> { /// let password = handle.get_authtok(Some("what's your password?"))?; -/// eprintln!("If you say your password is {:?}, who am I to disagree!", password.unsecure()); +/// // You should use a Conversation to communicate with the user +/// // instead of writing to the console, but this is just an example. +/// eprintln!("If you say your password is {password:?}, who am I to disagree?"); /// Ok(()) /// } /// /// fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> { /// let username = handle.get_user(None)?; -/// // You should use a Conversation to communicate with the user -/// // instead of writing to the console, but this is just an example. -/// eprintln!("Hello {username}! I trust you unconditionally!"); +/// eprintln!("Hello {username:?}! I trust you unconditionally!"); /// Ok(()) /// } /// } @@ -462,92 +382,10 @@ #[cfg(test)] mod tests { - use super::{ - Conversation, ConversationMux, ErrorCode, Message, Response, Result, SecureString, - }; - - /// Compile-time test that the `pam_hooks` macro compiles. - mod hooks { - use super::super::{PamModule, PamModuleHandle}; - struct Foo; - impl<T: PamModuleHandle> PamModule<T> for Foo {} - - pam_hooks!(Foo); - } - - #[test] - fn test_mux() { - struct MuxTester; + // Compile-time test that the `pam_hooks` macro compiles. + use super::super::{PamHandleModule, PamModule}; + struct Foo; + impl<T: PamHandleModule> PamModule<T> for Foo {} - impl Conversation for MuxTester { - fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> { - if let [msg] = messages { - match msg { - Message::Info(info) => { - assert_eq!("let me tell you", *info); - Ok(vec![Response::NoResponse]) - } - Message::Error(error) => { - assert_eq!("oh no", *error); - Ok(vec![Response::NoResponse]) - } - Message::Prompt("should_error") => Err(ErrorCode::BufferError), - Message::Prompt(ask) => { - assert_eq!("question", *ask); - Ok(vec![Response::Text("answer".to_owned())]) - } - Message::MaskedPrompt("return_wrong_type") => { - Ok(vec![Response::NoResponse]) - } - Message::MaskedPrompt(ask) => { - assert_eq!("password!", *ask); - Ok(vec![Response::MaskedText(SecureString::from( - "open sesame", - ))]) - } - Message::BinaryPrompt { data, data_type } => { - assert_eq!(&[1, 2, 3], data); - assert_eq!(69, *data_type); - Ok(vec![Response::Binary(super::BinaryData::new( - vec![3, 2, 1], - 42, - ))]) - } - Message::RadioPrompt(ask) => { - assert_eq!("radio?", *ask); - Ok(vec![Response::Text("yes".to_owned())]) - } - } - } else { - panic!("messages is the wrong size ({len})", len = messages.len()) - } - } - } - - let mut tester = MuxTester; - - let mut mux = ConversationMux(&mut tester); - assert_eq!("answer", mux.prompt("question").unwrap()); - assert_eq!( - SecureString::from("open sesame"), - mux.masked_prompt("password!").unwrap() - ); - mux.error("oh no"); - mux.info("let me tell you"); - { - assert_eq!("yes", mux.radio_prompt("radio?").unwrap()); - assert_eq!( - super::BinaryData::new(vec![3, 2, 1], 42), - mux.binary_prompt(&[1, 2, 3], 69).unwrap(), - ) - } - assert_eq!( - ErrorCode::BufferError, - mux.prompt("should_error").unwrap_err(), - ); - assert_eq!( - ErrorCode::ConversationError, - mux.masked_prompt("return_wrong_type").unwrap_err() - ) - } + pam_hooks!(Foo); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pam_ffi/conversation.rs Thu Jun 05 03:41:38 2025 -0400 @@ -0,0 +1,162 @@ +use crate::constants::Result; +use crate::conv::{Conversation, Message, Response}; +use crate::pam_ffi::memory::Immovable; +use crate::pam_ffi::message::{MessageIndirector, OwnedMessages}; +use crate::pam_ffi::response::{OwnedResponses, RawBinaryResponse, RawResponse, RawTextResponse}; +use crate::ErrorCode; +use crate::ErrorCode::ConversationError; +use std::ffi::c_int; +use std::iter; +use std::marker::PhantomData; +use std::result::Result as StdResult; + +/// An opaque structure that is passed through PAM in a conversation. +#[repr(C)] +pub struct AppData { + _data: (), + _marker: Immovable, +} + +/// The callback that PAM uses to get information in a conversation. +/// +/// - `num_msg` is the number of messages in the `pam_message` array. +/// - `messages` is a pointer to the messages being sent to the user. +/// For details about its structure, see the documentation of +/// [`OwnedMessages`](super::OwnedMessages). +/// - `responses` is a pointer to an array of [`RawResponse`]s, +/// which PAM sets in response to a module's request. +/// This is an array of structs, not an array of pointers to a struct. +/// There should always be exactly as many `responses` as `num_msg`. +/// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed. +pub type ConversationCallback = unsafe extern "C" fn( + num_msg: c_int, + messages: *const MessageIndirector, + responses: *mut *mut RawResponse, + 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 { + callback: Self::wrapper_callback::<C>, + appdata: (conv as *mut C).cast(), + life: PhantomData, + _marker: Immovable(PhantomData), + } + } + + unsafe extern "C" fn wrapper_callback<C: Conversation>( + count: c_int, + messages: *const MessageIndirector, + responses: *mut *mut RawResponse, + me: *mut AppData, + ) -> c_int { + let call = || { + let conv = me + .cast::<C>() + .as_mut() + .ok_or(ErrorCode::ConversationError)?; + let indir = messages.as_ref().ok_or(ErrorCode::ConversationError)?; + let response_ptr = responses.as_mut().ok_or(ErrorCode::ConversationError)?; + let messages: Vec<Message> = indir + .iter(count as usize) + .map(Message::try_from) + .collect::<StdResult<_, _>>() + .map_err(|_| ErrorCode::ConversationError)?; + let responses = conv.converse(&messages)?; + let owned = + OwnedResponses::build(&responses).map_err(|_| ErrorCode::ConversationError)?; + *response_ptr = owned.into_ptr(); + Ok(()) + }; + ErrorCode::result_to_c(call()) + } +} + +impl Conversation for LibPamConversation<'_> { + fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> { + let mut msgs_to_send = OwnedMessages::alloc(messages.len()); + for (dst, src) in iter::zip(msgs_to_send.iter_mut(), messages.iter()) { + dst.set(*src).map_err(|_| ErrorCode::ConversationError)? + } + let mut response_pointer = std::ptr::null_mut(); + // SAFETY: We're calling into PAM with valid everything. + let result = unsafe { + (self.callback)( + messages.len() as c_int, + msgs_to_send.indirector(), + &mut response_pointer, + self.appdata, + ) + }; + ErrorCode::result_from(result)?; + // SAFETY: This is a pointer we just got back from PAM. + let owned_responses = + unsafe { OwnedResponses::from_c_heap(response_pointer, messages.len()) }; + convert_responses(messages, owned_responses) + } +} + +fn convert_responses( + messages: &[Message], + mut raw_responses: OwnedResponses, +) -> Result<Vec<Response>> { + let pairs = iter::zip(messages.iter(), raw_responses.iter_mut()); + // We first collect into a Vec of Results so that we always process + // every single entry, which may involve freeing it. + let responses: Vec<_> = pairs.map(convert).collect(); + // Only then do we return the first error, if present. + responses.into_iter().collect() +} + +/// Converts one message-to-raw pair to a Response. +fn convert((sent, received): (&Message, &mut RawResponse)) -> Result<Response> { + Ok(match sent { + Message::MaskedPrompt(_) => { + // SAFETY: Since this is a response to a text message, + // we know it is text. + let text_resp = unsafe { RawTextResponse::upcast(received) }; + let ret = Response::MaskedText( + text_resp + .contents() + .map_err(|_| ErrorCode::ConversationError)? + .into(), + ); + // SAFETY: We're the only ones using this, + // and we haven't freed it. + text_resp.free_contents(); + ret + } + Message::Prompt(_) | Message::RadioPrompt(_) => { + // SAFETY: Since this is a response to a text message, + // we know it is text. + let text_resp = unsafe { RawTextResponse::upcast(received) }; + let ret = Response::Text(text_resp.contents().map_err(|_| ConversationError)?.into()); + // SAFETY: We're the only ones using this, + // and we haven't freed it. + text_resp.free_contents(); + ret + } + Message::Error(_) | Message::Info(_) => Response::NoResponse, + Message::BinaryPrompt { .. } => { + let bin_resp = unsafe { RawBinaryResponse::upcast(received) }; + let ret = Response::Binary(bin_resp.to_owned()); + // SAFETY: We're the only ones using this, + // and we haven't freed it. + bin_resp.free_contents(); + ret + } + }) +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pam_ffi/handle.rs Thu Jun 05 03:41:38 2025 -0400 @@ -0,0 +1,231 @@ +use super::conversation::LibPamConversation; +use crate::constants::{ErrorCode, InvalidEnum, Result}; +use crate::conv::Message; +use crate::handle::{PamApplicationOnly, PamModuleOnly, PamShared}; +use crate::pam_ffi::memory; +use crate::pam_ffi::memory::Immovable; +use crate::{Conversation, Response}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +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()) + } + + /// 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) + } + + /// 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 { + type Target = LibPamHandle; + fn deref(&self) -> &Self::Target { + unsafe { &*self.0 } + } +} + +impl DerefMut for OwnedLibPamHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut *self.0 } + } +} + +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. + fn drop(&mut self) { + unsafe { + super::pam_end(self.0, 0); + } + } +} + +macro_rules! cstr_item { + (get = $getter:ident, item = $item_type:path) => { + fn $getter(&mut self) -> Result<Option<&str>> { + unsafe { self.get_cstr_item($item_type) } + } + }; + (set = $setter:ident, item = $item_type:path) => { + fn $setter(&mut self, value: Option<&str>) -> Result<()> { + unsafe { self.set_cstr_item($item_type, value) } + } + }; +} + +impl PamShared for LibPamHandle { + 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())) }; + ErrorCode::result_from(ret)?; + unsafe { memory::wrap_string(output) } + .transpose() + .unwrap_or(Err(ErrorCode::ConversationError)) + } + + cstr_item!(get = user_item, item = ItemType::User); + cstr_item!(set = set_user_item, item = ItemType::User); + cstr_item!(get = service, item = ItemType::Service); + cstr_item!(set = set_service, item = ItemType::Service); + cstr_item!(get = user_prompt, item = ItemType::UserPrompt); + cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt); + cstr_item!(get = tty_name, item = ItemType::Tty); + cstr_item!(set = set_tty_name, item = ItemType::Tty); + cstr_item!(get = remote_user, item = ItemType::RemoteUser); + cstr_item!(set = set_remote_user, item = ItemType::RemoteUser); + cstr_item!(get = remote_host, item = ItemType::RemoteHost); + cstr_item!(set = set_remote_host, item = ItemType::RemoteHost); + cstr_item!(set = set_authtok_item, item = ItemType::AuthTok); + cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok); +} + +impl Conversation for LibPamHandle { + fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> { + self.conversation_item()?.converse(messages) + } +} + +impl PamModuleOnly 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( + self, + ItemType::AuthTok.into(), + &mut output, + memory::prompt_ptr(prompt.as_ref()), + ) + }; + ErrorCode::result_from(res)?; + // SAFETY: We got this string from PAM. + unsafe { memory::wrap_string(output) } + .transpose() + .unwrap_or(Err(ErrorCode::ConversationError)) + } + + cstr_item!(get = authtok_item, item = ItemType::AuthTok); + cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok); +} + +/// Function called at the end of a PAM session that is called to clean up +/// a value previously provided to PAM in a `pam_set_data` call. +/// +/// You should never call this yourself. +extern "C" fn set_data_cleanup<T>(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) { + unsafe { + let _data: Box<T> = Box::from_raw(c_data.cast()); + } +} + +/// Identifies what is being gotten or set with `pam_get_item` +/// or `pam_set_item`. +#[derive(FromPrimitive)] +#[repr(i32)] +#[non_exhaustive] // because C could give us anything! +pub enum ItemType { + /// The PAM service name. + Service = 1, + /// The user's login name. + User = 2, + /// The TTY name. + Tty = 3, + /// The remote host (if applicable). + RemoteHost = 4, + /// The conversation struct (not a CStr-based item). + Conversation = 5, + /// The authentication token (password). + AuthTok = 6, + /// The old authentication token (when changing passwords). + OldAuthTok = 7, + /// The remote user's name. + RemoteUser = 8, + /// The prompt shown when requesting a username. + UserPrompt = 9, + /// App-supplied function to override failure delays. + FailDelay = 10, + /// X display name. + XDisplay = 11, + /// X server authentication data. + XAuthData = 12, + /// 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/pam_ffi/memory.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/pam_ffi/memory.rs Thu Jun 05 03:41:38 2025 -0400 @@ -8,7 +8,9 @@ use std::{ptr, slice}; /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. -pub type Immovable = PhantomData<(*mut u8, PhantomPinned)>; +#[repr(C)] +#[derive(Debug)] +pub struct Immovable(pub PhantomData<(*mut u8, PhantomPinned)>); /// Safely converts a `&str` option to a `CString` option. pub fn option_cstr(prompt: Option<&str>) -> Result<Option<CString>> { @@ -71,7 +73,7 @@ } unsafe { let data_alloc = libc::calloc(data.len() + 1, 1); - libc::memcpy(data_alloc, data.as_ptr() as *const c_void, data.len()); + libc::memcpy(data_alloc, data.as_ptr().cast(), data.len()); Ok(data_alloc.cast()) } } @@ -85,7 +87,7 @@ /// It's up to you to provide a valid C string. pub unsafe fn zero_c_string(cstr: *mut c_void) { if !cstr.is_null() { - libc::memset(cstr, 0, libc::strlen(cstr as *const c_char)); + libc::memset(cstr, 0, libc::strlen(cstr.cast())); } } @@ -113,17 +115,14 @@ max: (u32::MAX - 5) as usize, actual: source.len(), })?; + // SAFETY: We're only allocating here. let data = unsafe { - let dest_buffer = libc::malloc(buffer_size as usize) as *mut CBinaryData; + let dest_buffer: *mut CBinaryData = libc::malloc(buffer_size as usize).cast(); let data = &mut *dest_buffer; data.total_length = buffer_size.to_be_bytes(); data.data_type = data_type; let dest = data.data.as_mut_ptr(); - libc::memcpy( - dest as *mut c_void, - source.as_ptr() as *const c_void, - source.len(), - ); + libc::memcpy(dest.cast(), source.as_ptr().cast(), source.len()); dest_buffer }; Ok(data)
--- a/src/pam_ffi/message.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/pam_ffi/message.rs Thu Jun 05 03:41:38 2025 -0400 @@ -1,77 +1,19 @@ //! Data and types dealing with PAM messages. use crate::constants::InvalidEnum; +use crate::conv::Message; use crate::pam_ffi::memory; -use crate::pam_ffi::memory::{CBinaryData, NulError, TooBigError}; +use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError}; use num_derive::FromPrimitive; use num_traits::FromPrimitive; -use std::ffi::{c_char, c_int, c_void, CStr}; +use std::ffi::{c_int, c_void, CStr}; use std::result::Result as StdResult; -use std::slice; use std::str::Utf8Error; - -/// The types of message and request that can be sent to a user. -/// -/// The data within each enum value is the prompt (or other information) -/// that will be presented to the user. -#[derive(Debug)] -pub enum Message<'a> { - /// Requests information from the user; will be masked when typing. - /// - /// Response: [`MaskedText`](crate::conv::Response::MaskedText) - MaskedPrompt(&'a str), - /// Requests information from the user; will not be masked. - /// - /// Response: [`Text`](crate::conv::Response::Text) - Prompt(&'a str), - /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). - /// - /// Response: [`Text`](crate::conv::Response::Text) - /// (Linux-PAM documentation doesn't define its contents.) - RadioPrompt(&'a str), - /// Raises an error message to the user. - /// - /// Response: [`NoResponse`](crate::conv::Response::NoResponse) - Error(&'a str), - /// Sends an informational message to the user. - /// - /// Response: [`NoResponse`](crate::conv::Response::NoResponse) - Info(&'a str), - /// Requests binary data from the client (a Linux-PAM extension). - /// - /// This is used for non-human or non-keyboard prompts (security key?). - /// NOT part of the X/Open PAM specification. - /// - /// Response: [`Binary`](crate::conv::Response::Binary) - BinaryPrompt { - /// Some binary data. - data: &'a [u8], - /// A "type" that you can use for signalling. Has no strict definition in PAM. - data_type: u8, - }, -} - -impl Message<'_> { - /// Copies the contents of this message to the C heap. - fn copy_to_heap(&self) -> StdResult<(Style, *mut c_void), ConversionError> { - let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); - match *self { - Self::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text), - Self::Prompt(text) => alloc(Style::PromptEchoOn, text), - Self::RadioPrompt(text) => alloc(Style::RadioType, text), - Self::Error(text) => alloc(Style::ErrorMsg, text), - Self::Info(text) => alloc(Style::TextInfo, text), - Self::BinaryPrompt { data, data_type } => Ok(( - Style::BinaryPrompt, - (CBinaryData::alloc(data, data_type)?).cast(), - )), - } - } -} +use std::{ptr, slice}; #[derive(Debug, thiserror::Error)] #[error("error creating PAM message: {0}")] -enum ConversionError { +pub enum ConversionError { InvalidEnum(#[from] InvalidEnum<Style>), Utf8Error(#[from] Utf8Error), NulError(#[from] NulError), @@ -121,14 +63,15 @@ /// A description of the data requested. /// /// For most requests, this will be an owned [`CStr`], but for requests - /// with [`Style::BinaryPrompt`], this will be [`BinaryData`] + /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] /// (a Linux-PAM extension). data: *mut c_void, + _marker: Immovable, } impl RawMessage { - fn set(&mut self, msg: &Message) -> StdResult<(), ConversionError> { - let (style, data) = msg.copy_to_heap()?; + pub fn set(&mut self, msg: Message) -> StdResult<(), ConversionError> { + let (style, data) = copy_to_heap(msg)?; self.clear(); // SAFETY: We allocated this ourselves or were given it by PAM. // Otherwise, it's null, but free(null) is fine. @@ -138,32 +81,6 @@ Ok(()) } - /// Retrieves the data stored in this message. - fn data(&self) -> StdResult<Message, ConversionError> { - let style: Style = self.style.try_into()?; - // SAFETY: We either allocated this message ourselves or were provided it by PAM. - let result = unsafe { - match style { - Style::PromptEchoOff => Message::MaskedPrompt(self.string_data()?), - Style::PromptEchoOn => Message::Prompt(self.string_data()?), - Style::TextInfo => Message::Info(self.string_data()?), - Style::ErrorMsg => Message::Error(self.string_data()?), - Style::RadioType => Message::Error(self.string_data()?), - Style::BinaryPrompt => (self.data as *const CBinaryData).as_ref().map_or_else( - || Message::BinaryPrompt { - data_type: 0, - data: &[], - }, - |data| Message::BinaryPrompt { - data_type: data.data_type(), - data: data.contents(), - }, - ), - } - }; - Ok(result) - } - /// Gets this message's data pointer as a string. /// /// # Safety @@ -173,7 +90,7 @@ if self.data.is_null() { Ok("") } else { - CStr::from_ptr(self.data as *const c_char).to_str() + CStr::from_ptr(self.data.cast()).to_str() } } @@ -185,7 +102,7 @@ if let Ok(style) = Style::try_from(self.style) { match style { Style::BinaryPrompt => { - if let Some(d) = (self.data as *mut CBinaryData).as_mut() { + if let Some(d) = self.data.cast::<CBinaryData>().as_mut() { d.zero_contents() } } @@ -196,10 +113,28 @@ | Style::PromptEchoOn => memory::zero_c_string(self.data), } }; + libc::free(self.data); + self.data = ptr::null_mut(); } } } +/// Copies the contents of this message to the C heap. +fn copy_to_heap(msg: Message) -> StdResult<(Style, *mut c_void), ConversionError> { + let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); + match msg { + Message::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text), + Message::Prompt(text) => alloc(Style::PromptEchoOn, text), + Message::RadioPrompt(text) => alloc(Style::RadioType, text), + Message::Error(text) => alloc(Style::ErrorMsg, text), + Message::Info(text) => alloc(Style::TextInfo, text), + Message::BinaryPrompt { data, data_type } => Ok(( + Style::BinaryPrompt, + (CBinaryData::alloc(data, data_type)?).cast(), + )), + } +} + /// Abstraction of a list-of-messages to be sent in a PAM conversation. /// /// On Linux-PAM and other compatible implementations, `messages` @@ -235,10 +170,9 @@ /// ``` /// /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** -#[repr(C)] pub struct OwnedMessages { /// An indirection to the messages themselves, stored on the C heap. - indirect: *mut Indirect<RawMessage>, + indirect: *mut MessageIndirector, /// The number of messages in the list. count: usize, } @@ -246,47 +180,155 @@ impl OwnedMessages { /// Allocates data to store messages on the C heap. pub fn alloc(count: usize) -> Self { - // SAFETY: We're allocating. That's safe. + Self { + indirect: MessageIndirector::alloc(count), + count, + } + } + + /// The pointer to the thing with the actual list. + pub fn indirector(&self) -> *const MessageIndirector { + self.indirect + } + + pub fn iter(&self) -> impl Iterator<Item = &RawMessage> { + // SAFETY: we're iterating over an amount we know. + unsafe { (*self.indirect).iter(self.count) } + } + + pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut RawMessage> { + // SAFETY: we're iterating over an amount we know. + unsafe { (*self.indirect).iter_mut(self.count) } + } +} + +impl Drop for OwnedMessages { + fn drop(&mut self) { + // SAFETY: We are valid and have a valid pointer. + // Once we're done, everything will be safe. unsafe { - // Since this is Linux-PAM, the indirect is a list of pointers. - let indirect = - libc::calloc(count, size_of::<Indirect<RawMessage>>()) as *mut Indirect<RawMessage>; - let indir_ptrs = slice::from_raw_parts_mut(indirect, count); - for ptr in indir_ptrs { - ptr.base = libc::calloc(1, size_of::<RawMessage>()) as *mut RawMessage; + if let Some(indirect) = self.indirect.as_mut() { + indirect.free(self.count) } - Self { indirect, count } + libc::free(self.indirect.cast()); + self.indirect = ptr::null_mut(); + } + } +} + +/// 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. +#[repr(transparent)] +pub struct MessageIndirector { + base: [*mut RawMessage; 0], + _marker: Immovable, +} + +impl MessageIndirector { + /// Allocates memory for this indirector and all its members. + fn alloc(count: usize) -> *mut Self { + // SAFETY: We're only allocating, and when we're done, + // everything will be in a known-good state. + unsafe { + let me_ptr: *mut MessageIndirector = + libc::calloc(count, size_of::<*mut RawMessage>()).cast(); + 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 = libc::calloc(1, size_of::<RawMessage>()).cast(); + } + me } } - /// Gets a reference to the message at the given index. - pub fn get(&self, index: usize) -> Option<&RawMessage> { - (index < self.count).then(|| unsafe { (*self.indirect).at(index) }) + /// 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 = &RawMessage> { + (0..count).map(|idx| &**self.base.as_ptr().add(idx)) } - /// Gets a mutable reference to the message at the given index. - pub fn get_mut(&mut self, index: usize) -> Option<&mut RawMessage> { - (index < self.count).then(|| unsafe { (*self.indirect).at_mut(index) }) + /// 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 RawMessage> { + (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx)) + } + + /// Frees this and everything it points to. + /// + /// # Safety + /// + /// You have to pass the right size. + unsafe fn free(&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() { + msg.clear(); + } + libc::free(msg.cast()); + *msg = ptr::null_mut(); + } } } -#[repr(transparent)] -struct Indirect<T> { - /// The starting address for the T. - base: *mut T, +impl<'a> TryFrom<&'a RawMessage> for Message<'a> { + type Error = ConversionError; + + /// Retrieves the data stored in this message. + fn try_from(input: &RawMessage) -> StdResult<Message, ConversionError> { + let style: Style = input.style.try_into()?; + // SAFETY: We either allocated this message ourselves or were provided it by PAM. + let result = unsafe { + match style { + Style::PromptEchoOff => Message::MaskedPrompt(input.string_data()?), + Style::PromptEchoOn => Message::Prompt(input.string_data()?), + Style::TextInfo => Message::Info(input.string_data()?), + Style::ErrorMsg => Message::Error(input.string_data()?), + Style::RadioType => Message::Error(input.string_data()?), + Style::BinaryPrompt => input.data.cast::<CBinaryData>().as_ref().map_or_else( + || Message::BinaryPrompt { + data_type: 0, + data: &[], + }, + |data| Message::BinaryPrompt { + data_type: data.data_type(), + data: data.contents(), + }, + ), + } + }; + Ok(result) + } } -impl<T> Indirect<T> { - /// Gets a mutable reference to the element at the given index. - /// - /// # Safety - /// - /// We don't check `index`. - unsafe fn at_mut(&mut self, index: usize) -> &mut T { - &mut *self.base.add(index) - } +#[cfg(test)] +mod tests { + use crate::conv::Message; + use crate::pam_ffi::message::OwnedMessages; - unsafe fn at(&self, index: usize) -> &T { - &*self.base.add(index) + #[test] + fn test_owned_messages() { + let mut tons_of_messages = OwnedMessages::alloc(10); + let mut msgs: Vec<_> = tons_of_messages.iter_mut().collect(); + assert!(msgs.get(10).is_none()); + let last_msg = &mut msgs[9]; + last_msg.set(Message::MaskedPrompt("hocus pocus")).unwrap(); + let another_msg = &mut msgs[0]; + another_msg + .set(Message::BinaryPrompt { + data: &[5, 4, 3, 2, 1], + data_type: 99, + }) + .unwrap(); + let overwrite = &mut msgs[3]; + overwrite.set(Message::Prompt("what")).unwrap(); + overwrite.set(Message::Prompt("who")).unwrap(); } }
--- a/src/pam_ffi/mod.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/pam_ffi/mod.rs Thu Jun 05 03:41:38 2025 -0400 @@ -9,95 +9,56 @@ #![allow(dead_code)] -pub mod memory; +mod conversation; +mod handle; +mod memory; mod message; mod response; -use crate::pam_ffi::memory::Immovable; -use crate::pam_ffi::message::OwnedMessages; -#[doc(inline)] -pub use message::Message; -#[doc(inline)] -pub use response::RawResponse; +pub use handle::{LibPamHandle, OwnedLibPamHandle}; use std::ffi::{c_char, c_int, c_void}; -/// 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, -} - -/// The callback that PAM uses to get information in a conversation. -/// -/// - `num_msg` is the number of messages in the `pam_message` array. -/// - `messages` is a pointer to the messages being sent to the user. -/// For details about its structure, see the documentation of -/// [`OwnedMessages`](super::OwnedMessages). -/// - `responses` is a pointer to an array of [`RawResponse`]s, -/// which PAM sets in response to a module's request. -/// This is an array of structs, not an array of pointers to a struct. -/// There should always be exactly as many `responses` as `num_msg`. -/// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed. -pub type ConversationCallback = extern "C" fn( - num_msg: c_int, - messages: &OwnedMessages, - responses: &mut *mut RawResponse, - appdata: *const AppData, -) -> c_int; - -/// The type used by libpam to call back into a conversation. -#[repr(C)] -pub struct LibPamConversation { - /// 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: *const AppData, -} - #[link(name = "pam")] extern "C" { - pub fn pam_get_data( - pamh: *const LibPamHandle, + fn pam_get_data( + pamh: *mut LibPamHandle, module_data_name: *const c_char, data: &mut *const c_void, ) -> c_int; - pub fn pam_set_data( + 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; - pub fn pam_get_item( - pamh: *mut LibPamHandle, - item_type: c_int, - item: &mut *const c_void, - ) -> c_int; + fn pam_get_item(pamh: *mut LibPamHandle, item_type: c_int, item: &mut *const c_void) -> c_int; - pub fn pam_set_item(pamh: *mut LibPamHandle, item_type: c_int, item: *const c_void) -> c_int; + fn pam_set_item(pamh: *mut LibPamHandle, item_type: c_int, item: *const c_void) -> c_int; - pub fn pam_get_user( + fn pam_get_user( pamh: *mut LibPamHandle, user: &mut *const c_char, prompt: *const c_char, ) -> c_int; - pub fn pam_get_authtok( + fn pam_get_authtok( pamh: *mut LibPamHandle, item_type: c_int, data: &mut *const c_char, prompt: *const c_char, ) -> c_int; - pub fn pam_end(pamh: *mut LibPamHandle, status: c_int) -> 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 }
--- a/src/pam_ffi/response.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/pam_ffi/response.rs Thu Jun 05 03:41:38 2025 -0400 @@ -1,46 +1,61 @@ //! Types used when dealing with PAM conversations. +use crate::conv::BinaryData; use crate::pam_ffi::memory; use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError}; -use std::ffi::{c_char, c_int, c_void, CStr}; +use crate::Response; +use std::ffi::{c_int, c_void, CStr}; use std::ops::{Deref, DerefMut}; use std::result::Result as StdResult; use std::str::Utf8Error; -use std::{mem, ptr, slice}; +use std::{iter, mem, ptr, slice}; #[repr(transparent)] #[derive(Debug)] pub struct RawTextResponse(RawResponse); impl RawTextResponse { - /// Allocates a new text response on the C heap. + /// Interprets the provided `RawResponse` as a text response. + /// + /// # Safety /// - /// Both `self` and its internal pointer are located on the C heap. + /// It's up to you to provide a response that is a `RawTextResponse`. + pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self { + // SAFETY: We're provided a valid reference. + &mut *(from as *mut RawResponse).cast::<Self>() + } + + /// Fills in the provided `RawResponse` with the given text. + /// /// You are responsible for calling [`free`](Self::free_contents) /// on the pointer you get back when you're done with it. pub fn fill(dest: &mut RawResponse, text: impl AsRef<str>) -> StdResult<&mut Self, NulError> { dest.data = memory::malloc_str(text)?.cast(); - Ok(unsafe { &mut *(dest as *mut RawResponse as *mut Self) }) + // SAFETY: We just filled this in so we know it's a text response. + Ok(unsafe { Self::upcast(dest) }) } /// Gets the string stored in this response. pub fn contents(&self) -> StdResult<&str, Utf8Error> { - // SAFETY: This data is either passed from PAM (so we are forced to - // trust it) or was created by us in TextResponseInner::alloc. - // In either case, it's going to be a valid null-terminated string. - unsafe { CStr::from_ptr(self.0.data as *const c_char) }.to_str() + if self.0.data.is_null() { + Ok("") + } else { + // SAFETY: This data is either passed from PAM (so we are forced to + // trust it) or was created by us in TextResponseInner::alloc. + // In either case, it's going to be a valid null-terminated string. + unsafe { CStr::from_ptr(self.0.data.cast()) }.to_str() + } } /// Releases memory owned by this response. - /// - /// # Safety - /// - /// You are responsible for no longer using this after calling free. - pub unsafe fn free_contents(&mut self) { - let data = self.0.data; - memory::zero_c_string(data); - libc::free(data); - self.0.data = ptr::null_mut() + pub fn free_contents(&mut self) { + // SAFETY: We know we own this data. + // After we're done, it will be null. + unsafe { + memory::zero_c_string(self.0.data); + libc::free(self.0.data); + self.0.data = ptr::null_mut() + } } } @@ -50,7 +65,17 @@ pub struct RawBinaryResponse(RawResponse); impl RawBinaryResponse { - /// Allocates a new binary response on the C heap. + /// Interprets the provided `RawResponse` as a binary response. + /// + /// # Safety + /// + /// It's up to you to provide a response that is a `RawBinaryResponse`. + pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self { + // SAFETY: We're provided a valid reference. + &mut *(from as *mut RawResponse).cast::<Self>() + } + + /// Fills in a `RawResponse` with the provided binary data. /// /// The `data_type` is a tag you can use for whatever. /// It is passed through PAM unchanged. @@ -63,45 +88,44 @@ data: &[u8], data_type: u8, ) -> StdResult<&'a mut Self, TooBigError> { - dest.data = CBinaryData::alloc(data, data_type)? as *mut c_void; - Ok(unsafe { - (dest as *mut RawResponse) - .cast::<RawBinaryResponse>() - .as_mut() - .unwrap() - }) + dest.data = CBinaryData::alloc(data, data_type)?.cast(); + // SAFETY: We just filled this in, so we know it's binary. + Ok(unsafe { Self::upcast(dest) }) } /// Gets the binary data in this response. - pub fn contents(&self) -> &[u8] { - self.data().contents() + pub fn data(&self) -> &[u8] { + self.contents().map(CBinaryData::contents).unwrap_or(&[]) } /// Gets the `data_type` tag that was embedded with the message. pub fn data_type(&self) -> u8 { - self.data().data_type() + self.contents().map(CBinaryData::data_type).unwrap_or(0) } - #[inline] - fn data(&self) -> &CBinaryData { + fn contents(&self) -> Option<&CBinaryData> { // SAFETY: This was either something we got from PAM (in which case // we trust it), or something that was created with // BinaryResponseInner::alloc. In both cases, it points to valid data. - unsafe { &*(self.0.data as *const CBinaryData) } + unsafe { self.0.data.cast::<CBinaryData>().as_ref() } + } + + pub fn to_owned(&self) -> BinaryData { + BinaryData::new(self.data().into(), self.data_type()) } /// Releases memory owned by this response. - /// - /// # Safety - /// - /// You are responsible for not using this after calling free. - pub unsafe fn free_contents(&mut self) { - let data_ref = (self.0.data as *mut CBinaryData).as_mut(); - if let Some(d) = data_ref { - d.zero_contents() + pub fn free_contents(&mut self) { + // SAFETY: We know that our data pointer is either valid or null. + // Once we're done, it's null and the response is safe. + unsafe { + let data_ref = self.0.data.cast::<CBinaryData>().as_mut(); + if let Some(d) = data_ref { + d.zero_contents() + } + libc::free(self.0.data); + self.0.data = ptr::null_mut() } - libc::free(self.0.data); - self.0.data = ptr::null_mut() } } @@ -124,7 +148,6 @@ /// A contiguous block of responses. #[derive(Debug)] -#[repr(C)] pub struct OwnedResponses { base: *mut RawResponse, count: usize, @@ -135,43 +158,72 @@ fn alloc(count: usize) -> Self { OwnedResponses { // SAFETY: We are doing allocation here. - base: unsafe { libc::calloc(count, size_of::<RawResponse>()) } as *mut RawResponse, + base: unsafe { libc::calloc(count, size_of::<RawResponse>()) }.cast(), count, } } + pub fn build(value: &[Response]) -> StdResult<Self, FillError> { + let mut outputs = OwnedResponses::alloc(value.len()); + // If we fail in here after allocating OwnedResponses, + // we still free all memory, even though we don't zero it first. + // This is an acceptable level of risk. + for (input, output) in iter::zip(value.iter(), outputs.iter_mut()) { + match input { + Response::NoResponse => { + RawTextResponse::fill(output, "")?; + } + Response::Text(data) => { + RawTextResponse::fill(output, data)?; + } + Response::MaskedText(data) => { + RawTextResponse::fill(output, data.unsecure())?; + } + Response::Binary(data) => { + RawBinaryResponse::fill(output, data.data(), data.data_type())?; + } + } + } + Ok(outputs) + } + + /// Converts this into a `*RawResponse` for passing to PAM. + /// + /// The pointer "owns" its own data (i.e., this will not be dropped). + pub fn into_ptr(self) -> *mut RawResponse { + let ret = self.base; + mem::forget(self); + ret + } + /// Takes ownership of a list of responses allocated on the C heap. /// /// # Safety /// /// It's up to you to make sure you pass a valid pointer. - unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self { + pub unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self { OwnedResponses { base, count } } } -impl From<OwnedResponses> for *mut RawResponse { - /// Converts this into a pointer to `RawResponse`. - /// - /// The backing data is no longer freed. - fn from(value: OwnedResponses) -> Self { - let ret = value.base; - mem::forget(value); - ret - } +#[derive(Debug, thiserror::Error)] +#[error("error converting responses: {0}")] +pub enum FillError { + NulError(#[from] NulError), + TooBigError(#[from] TooBigError), } impl Deref for OwnedResponses { type Target = [RawResponse]; fn deref(&self) -> &Self::Target { - // SAFETY: We allocated this ourselves, or it was provided to us by PAM. + // SAFETY: This is the memory we manage ourselves. unsafe { slice::from_raw_parts(self.base, self.count) } } } impl DerefMut for OwnedResponses { fn deref_mut(&mut self) -> &mut Self::Target { - // SAFETY: We allocated this ourselves, or it was provided to us by PAM. + // SAFETY: This is the memory we manage ourselves. unsafe { slice::from_raw_parts_mut(self.base, self.count) } } } @@ -183,15 +235,53 @@ for resp in self.iter_mut() { libc::free(resp.data) } - libc::free(self.base as *mut c_void) + libc::free(self.base.cast()) } } } #[cfg(test)] mod tests { + use super::{BinaryData, OwnedResponses, RawBinaryResponse, RawTextResponse, Response}; - use super::{OwnedResponses, RawBinaryResponse, RawTextResponse}; + #[test] + fn test_round_trip() { + let responses = [ + Response::Binary(BinaryData::new(vec![1, 2, 3], 99)), + Response::Text("whats going on".to_owned()), + Response::MaskedText("well then".into()), + Response::NoResponse, + Response::Text("bogus".to_owned()), + ]; + let sent = OwnedResponses::build(&responses).unwrap(); + let heap_resps = sent.into_ptr(); + let mut received = unsafe { OwnedResponses::from_c_heap(heap_resps, 5) }; + + let assert_text = |want, raw| { + let up = unsafe { RawTextResponse::upcast(raw) }; + assert_eq!(want, up.contents().unwrap()); + up.free_contents(); + assert_eq!("", up.contents().unwrap()); + }; + let assert_bin = |want_data: &[u8], want_type, raw| { + let up = unsafe { RawBinaryResponse::upcast(raw) }; + assert_eq!(want_data, up.data()); + assert_eq!(want_type, up.data_type()); + up.free_contents(); + let empty: [u8; 0] = []; + assert_eq!(&empty, up.data()); + assert_eq!(0, up.data_type()); + }; + if let [zero, one, two, three, four] = &mut received[..] { + assert_bin(&[1, 2, 3], 99, zero); + assert_text("whats going on", one); + assert_text("well then", two); + assert_text("", three); + assert_text("bogus", four); + } else { + panic!("wrong size!") + } + } #[test] fn test_text_response() { @@ -199,10 +289,8 @@ let text = RawTextResponse::fill(&mut responses[0], "hello").unwrap(); let data = text.contents().expect("valid"); assert_eq!("hello", data); - unsafe { - text.free_contents(); - text.free_contents(); - } + text.free_contents(); + text.free_contents(); RawTextResponse::fill(&mut responses[1], "hell\0").expect_err("should error; contains nul"); } @@ -212,13 +300,11 @@ let real_data = [1, 2, 3, 4, 5, 6, 7, 8]; let resp = RawBinaryResponse::fill(&mut responses[0], &real_data, 7) .expect("alloc should succeed"); - let data = resp.contents(); + let data = resp.data(); assert_eq!(&real_data, data); assert_eq!(7, resp.data_type()); - unsafe { - resp.free_contents(); - resp.free_contents(); - } + resp.free_contents(); + resp.free_contents(); } #[test]