Mercurial > crates > nonstick
changeset 70:9f8381a1c09c
Implement low-level conversation primitives.
This change does two primary things:
1. Introduces new Conversation traits, to be implemented both
by the library and by PAM client applications.
2. Builds the memory-management infrastructure for passing messages
through the conversation.
...and it adds tests for both of the above, including ASAN tests.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 03 Jun 2025 01:21:59 -0400 |
parents | 8f3ae0c7ab92 |
children | 58f9d2a4df38 |
files | Cargo.toml src/constants.rs src/conv.rs src/handle.rs src/lib.rs src/module.rs src/pam_ffi.rs |
diffstat | 7 files changed, 680 insertions(+), 111 deletions(-) [+] |
line wrap: on
line diff
--- a/Cargo.toml Sun Jun 01 01:15:04 2025 -0400 +++ b/Cargo.toml Tue Jun 03 01:21:59 2025 -0400 @@ -9,11 +9,9 @@ license = "MIT" edition = "2021" -[features] -experimental = [] - [dependencies] bitflags = "2.9.0" +derive_more = { version = "2.0.1", features = ["from"] } libc = "0.2.97" num-derive = "0.4.2" num-traits = "0.2.19"
--- a/src/constants.rs Sun Jun 01 01:15:04 2025 -0400 +++ b/src/constants.rs Tue Jun 03 01:21:59 2025 -0400 @@ -190,6 +190,21 @@ } } +/// Returned when text that should not have any `\0` bytes in it does. +/// Analogous to [`std::ffi::NulError`], but the data it was created from +/// is borrowed. +#[derive(Debug, thiserror::Error)] +#[error("null byte within input at byte {0}")] +pub struct NulError(pub usize); + +/// Returned when trying to fit too much data into a binary message. +#[derive(Debug, thiserror::Error)] +#[error("cannot create a message of {actual} bytes; maximum is {max}")] +pub struct TooBigError { + pub actual: usize, + pub max: usize, +} + #[cfg(test)] mod tests { use super::*;
--- a/src/conv.rs Sun Jun 01 01:15:04 2025 -0400 +++ b/src/conv.rs Tue Jun 03 01:21:59 2025 -0400 @@ -1,9 +1,156 @@ //! The PAM conversation and associated Stuff. -use crate::pam_ffi::{BinaryResponseInner, NulError, TextResponseInner}; -use std::num::TryFromIntError; -use std::ops::Deref; +// Temporarily allowed until we get the actual conversation functions hooked up. +#![allow(dead_code)] + +use crate::constants::{NulError, Result, TooBigError}; +use crate::pam_ffi::{BinaryResponseInner, GenericResponse, TextResponseInner}; +use secure_string::SecureString; +use std::mem; use std::result::Result as StdResult; +use std::str::Utf8Error; + +// 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 +// trait definitions/implementations into a HUGE MESS. +// +// Ideally, we would be using some kind of `TD: TextData` and `BD: BinaryData` +// 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(Debug)] +pub enum Message<'a> { + /// Requests information from the user; will be masked when typing. + /// + /// Response: [`Response::MaskedText`] + MaskedPrompt(&'a str), + /// Requests information from the user; will not be masked. + /// + /// Response: [`Response::Text`] + Prompt(&'a str), + /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). + /// + /// Response: [`Response::Text`] + /// (Linux-PAM documentation doesn't define its contents.) + RadioPrompt(&'a str), + /// Raises an error message to the user. + /// + /// Response: [`Response::NoResponse`] + Error(&'a str), + /// Sends an informational message to the user. + /// + /// Response: [`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: [`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 { + /// Used to fill in list entries where there is no response expected. + /// + /// Used in response to: + /// + /// - [`Error`](Message::Error) + /// - [`Info`](Message::Info) + NoResponse, + /// A response with text data from the user. + /// + /// Used in response to: + /// + /// - [`Prompt`](Message::Prompt) + /// - [`RadioPrompt`](Message::RadioPrompt) (a Linux-PAM extension) + Text(String), + /// A response to a masked request with text data from the user. + /// + /// Used in response to: + /// + /// - [`MaskedPrompt`](Message::MaskedPrompt) + MaskedText(SecureString), + /// A response to a binary request (a Linux-PAM extension). + /// + /// Used in response to: + /// + /// - [`BinaryPrompt`](Message::BinaryPrompt) + Binary(BinaryData), +} + +/// A channel for PAM modules to request information from the user. +/// +/// This trait is used by both applications and PAM modules: +/// +/// - Applications implement Conversation and provide a user interface +/// to allow the user to respond to PAM questions. +/// - Modules call a Conversation implementation to request information +/// or send information to the user. +pub trait Conversation { + /// Sends messages to the user. + /// + /// The returned Vec of messages always contains exactly as many entries + /// as there were messages in the request; one corresponding to each. + /// + /// Messages with no response (e.g. [info](Message::Info) and + /// [error](Message::Error)) will have a `None` entry instead of a `Response`. + fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>>; +} + +/// Trait that an application can implement if they want to handle messages +/// one at a time. +pub trait DemuxedConversation { + /// Prompts the user for some text. + fn prompt(&mut self, request: &str) -> Result<String>; + /// Prompts the user for some text, but hides their typing. + fn masked_prompt(&mut self, request: &str) -> Result<SecureString>; + /// Prompts the user for a radio option (a Linux-PAM extension). + /// + /// The Linux-PAM documentation doesn't give 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<D: DemuxedConversation> Conversation for D { + fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> { + messages + .iter() + .map(|msg| match *msg { + Message::Prompt(prompt) => self.prompt(prompt).map(Response::from), + Message::MaskedPrompt(prompt) => self.masked_prompt(prompt).map(Response::from), + Message::RadioPrompt(prompt) => self.radio_prompt(prompt).map(Response::from), + Message::Info(message) => { + self.info(message); + Ok(Response::NoResponse) + } + Message::Error(message) => { + self.error(message); + Ok(Response::NoResponse) + } + Message::BinaryPrompt { data_type, data } => { + self.binary_prompt(data, data_type).map(Response::from) + } + }) + .collect() + } +} /// An owned text response to a PAM conversation. /// @@ -12,17 +159,24 @@ struct TextResponse(*mut TextResponseInner); impl TextResponse { - /// Creates a text response. + /// Allocates a new response with the given text. + /// + /// A copy of the provided text will be allocated on the C heap. pub fn new(text: impl AsRef<str>) -> StdResult<Self, NulError> { TextResponseInner::alloc(text).map(Self) } -} -impl Deref for TextResponse { - type Target = TextResponseInner; - fn deref(&self) -> &Self::Target { - // SAFETY: We allocated this ourselves, or it was provided by PAM. - unsafe { &*self.0 } + /// Converts this into a GenericResponse. + fn generic(self) -> *mut GenericResponse { + let ret = self.0 as *mut GenericResponse; + mem::forget(self); + ret + } + + /// Gets the string data, if possible. + pub fn as_str(&self) -> StdResult<&str, Utf8Error> { + // SAFETY: We allocated this ourselves or got it back from PAM. + unsafe { &*self.0 }.contents().to_str() } } @@ -38,20 +192,33 @@ /// /// It points to a value on the C heap. #[repr(C)] -struct BinaryResponse(*mut BinaryResponseInner); +pub struct BinaryResponse(pub(super) *mut BinaryResponseInner); impl BinaryResponse { /// Creates a binary response with the given data. - pub fn new(data: impl AsRef<[u8]>, data_type: u8) -> StdResult<Self, TryFromIntError> { + /// + /// A copy of the data will be made and allocated on the C heap. + pub fn new(data: &[u8], data_type: u8) -> StdResult<Self, TooBigError> { BinaryResponseInner::alloc(data, data_type).map(Self) } -} + + /// Converts this into a GenericResponse. + fn generic(self) -> *mut GenericResponse { + let ret = self.0 as *mut GenericResponse; + mem::forget(self); + ret + } -impl Deref for BinaryResponse { - type Target = BinaryResponseInner; - fn deref(&self) -> &Self::Target { - // SAFETY: We allocated this ourselves, or it was provided by PAM. - unsafe { &*self.0 } + /// The data type we point to. + pub fn data_type(&self) -> u8 { + // SAFETY: We allocated this ourselves or got it back from PAM. + unsafe { &*self.0 }.data_type() + } + + /// The data we point to. + pub fn data(&self) -> &[u8] { + // SAFETY: We allocated this ourselves or got it back from PAM. + unsafe { &*self.0 }.contents() } } @@ -63,19 +230,165 @@ } } +/// Owned binary data. +#[derive(Debug, PartialEq)] +pub struct BinaryData { + data: Vec<u8>, + data_type: u8, +} + +impl BinaryData { + pub fn new(data: Vec<u8>, data_type: u8) -> Self { + Self { data, data_type } + } + pub fn data(&self) -> &[u8] { + &self.data + } + pub fn data_type(&self) -> u8 { + self.data_type + } +} + +impl From<BinaryResponse> for BinaryData { + /// Copies the data onto the Rust heap. + fn from(value: BinaryResponse) -> Self { + Self { + data: value.data().to_vec(), + data_type: value.data_type(), + } + } +} + +impl From<BinaryData> for Vec<u8> { + /// Extracts the inner vector from the BinaryData. + fn from(value: BinaryData) -> Self { + value.data + } +} + #[cfg(test)] mod test { - use super::{BinaryResponse, TextResponse}; + use super::{ + BinaryResponse, Conversation, DemuxedConversation, Message, Response, SecureString, + TextResponse, + }; + use crate::constants::ErrorCode; + use crate::pam_ffi::GenericResponse; + + #[test] + fn test_demux() { + #[derive(Default)] + struct DemuxTester { + error_ran: bool, + info_ran: bool, + } + + impl DemuxedConversation for DemuxTester { + fn prompt(&mut self, request: &str) -> crate::Result<String> { + match request { + "what" => Ok("whatwhat".to_owned()), + "give_err" => Err(ErrorCode::PermissionDenied), + _ => panic!("unexpected prompt!"), + } + } + fn masked_prompt(&mut self, request: &str) -> crate::Result<SecureString> { + assert_eq!("reveal", request); + Ok(SecureString::from("my secrets")) + } + fn radio_prompt(&mut self, request: &str) -> crate::Result<String> { + assert_eq!("channel?", request); + Ok("zero".to_owned()) + } + fn error(&mut self, message: &str) { + self.error_ran = true; + assert_eq!("whoopsie", message); + } + fn info(&mut self, message: &str) { + self.info_ran = true; + assert_eq!("did you know", message); + } + fn binary_prompt( + &mut self, + data: &[u8], + data_type: u8, + ) -> crate::Result<super::BinaryData> { + assert_eq!(&[10, 9, 8], data); + assert_eq!(66, data_type); + Ok(super::BinaryData::new(vec![5, 5, 5], 5)) + } + } + + let mut tester = DemuxTester::default(); + + assert_eq!( + vec![ + Response::Text("whatwhat".to_owned()), + Response::MaskedText("my secrets".into()), + Response::NoResponse, + Response::NoResponse, + ], + tester + .send(&[ + Message::Prompt("what"), + Message::MaskedPrompt("reveal"), + Message::Error("whoopsie"), + Message::Info("did you know"), + ]) + .unwrap() + ); + assert!(tester.error_ran); + assert!(tester.info_ran); + + assert_eq!( + ErrorCode::PermissionDenied, + tester.send(&[Message::Prompt("give_err")]).unwrap_err(), + ); + + // Test the Linux-PAM extensions separately. + + assert_eq!( + vec![ + Response::Text("zero".to_owned()), + Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)), + ], + tester + .send(&[ + Message::RadioPrompt("channel?"), + Message::BinaryPrompt { + data: &[10, 9, 8], + data_type: 66 + }, + ]) + .unwrap() + ); + } + + // The below tests are used in conjunction with ASAN to verify + // that we correctly clean up all our memory. #[test] fn test_text_response() { let resp = TextResponse::new("it's a-me!").unwrap(); - assert_eq!("it's a-me!", resp.contents().to_str().unwrap()); + assert_eq!("it's a-me!", resp.as_str().unwrap()); } + #[test] fn test_binary_response() { let data = [123, 210, 55]; let resp = BinaryResponse::new(&data, 99).unwrap(); - assert_eq!(&data, resp.contents()); + assert_eq!(data, resp.data()); + assert_eq!(99, resp.data_type()); + } + + #[test] + fn test_to_generic() { + let text = TextResponse::new("oh no").unwrap(); + let text = text.generic(); + let binary = BinaryResponse::new(&[], 33).unwrap(); + let binary = binary.generic(); + unsafe { + GenericResponse::free(text); + GenericResponse::free(binary); + } } }
--- a/src/handle.rs Sun Jun 01 01:15:04 2025 -0400 +++ b/src/handle.rs Tue Jun 03 01:21:59 2025 -0400 @@ -22,7 +22,7 @@ /// /// ```no_run /// # use nonstick::PamHandle; - /// # fn _doc(handle: &impl PamHandle) -> Result<(), Box<dyn std::error::Error>> { + /// # fn _doc(handle: &mut impl PamHandle) -> Result<(), Box<dyn std::error::Error>> { /// // Get the username using the default prompt. /// let user = handle.get_user(None)?; /// // Get the username using a custom prompt. @@ -33,7 +33,7 @@ /// /// [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(&self, prompt: Option<&str>) -> Result<String>; + fn get_user(&mut self, prompt: Option<&str>) -> Result<String>; /// Retrieves the authentication token from the user. /// @@ -46,7 +46,7 @@ /// /// ```no_run /// # use nonstick::PamHandle; - /// # fn _doc(handle: &impl PamHandle) -> Result<(), Box<dyn std::error::Error>> { + /// # fn _doc(handle: &mut impl PamHandle) -> Result<(), Box<dyn std::error::Error>> { /// // Get the user's password using the default prompt. /// let pass = handle.get_authtok(None)?; /// // Get the user's password using a custom prompt. @@ -57,7 +57,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(&self, prompt: Option<&str>) -> Result<SecureString>; + fn get_authtok(&mut self, prompt: Option<&str>) -> Result<SecureString>; /// Retrieves an [Item] that has been set, possibly by the PAM client. /// @@ -74,7 +74,7 @@ /// # use nonstick::PamHandle; /// use nonstick::items::Service; /// - /// # fn _doc(pam_handle: &impl PamHandle) -> Result<(), Box<dyn std::error::Error>> { + /// # fn _doc(pam_handle: &mut impl PamHandle) -> Result<(), Box<dyn std::error::Error>> { /// let svc: Option<Service> = pam_handle.get_item()?; /// match svc { /// Some(name) => eprintln!("The calling service name is {:?}", name.to_string_lossy()), @@ -86,7 +86,7 @@ /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item - fn get_item<T: Item>(&self) -> Result<Option<T>>; + fn get_item<T: Item>(&mut self) -> Result<Option<T>>; /// Sets an item in the PAM context. It can be retrieved using [`get_item`](Self::get_item). /// @@ -154,7 +154,7 @@ /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_data.3.html /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_data - unsafe fn get_data<T>(&self, key: &str) -> Result<Option<&T>>; + unsafe fn get_data<T>(&mut self, key: &str) -> Result<Option<&T>>; /// Stores a pointer that can be retrieved later with [`get_data`](Self::get_data). /// @@ -201,7 +201,7 @@ } impl PamHandle for LibPamHandle { - fn get_user(&self, prompt: Option<&str>) -> crate::Result<String> { + fn get_user(&mut self, prompt: Option<&str>) -> crate::Result<String> { let prompt = memory::option_cstr(prompt)?; let mut output: *const c_char = std::ptr::null_mut(); let ret = unsafe { @@ -211,7 +211,7 @@ memory::copy_pam_string(output) } - fn get_authtok(&self, prompt: Option<&str>) -> crate::Result<SecureString> { + fn get_authtok(&mut self, prompt: Option<&str>) -> crate::Result<SecureString> { let prompt = memory::option_cstr(prompt)?; let mut output: *const c_char = std::ptr::null_mut(); let res = unsafe { @@ -226,7 +226,7 @@ memory::copy_pam_string(output).map(SecureString::from) } - fn get_item<T: Item>(&self) -> crate::Result<Option<T>> { + fn get_item<T: Item>(&mut self) -> crate::Result<Option<T>> { let mut ptr: *const libc::c_void = std::ptr::null(); let out = unsafe { let ret = pam_ffi::pam_get_item(&self.0, T::type_id().into(), &mut ptr); @@ -259,7 +259,7 @@ } impl PamModuleHandle for LibPamHandle { - unsafe fn get_data<T>(&self, key: &str) -> crate::Result<Option<&T>> { + unsafe fn get_data<T>(&mut self, key: &str) -> crate::Result<Option<&T>> { let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?; let mut ptr: *const libc::c_void = std::ptr::null(); ErrorCode::result_from(pam_ffi::pam_get_data(&self.0, c_key.as_ptr(), &mut ptr))?;
--- a/src/lib.rs Sun Jun 01 01:15:04 2025 -0400 +++ b/src/lib.rs Tue Jun 03 01:21:59 2025 -0400 @@ -23,10 +23,9 @@ //! [module-guide]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_MWG.html pub mod constants; -#[cfg(feature = "experimental")] pub mod conv; pub mod items; -mod module; +pub mod module; mod handle; mod memory;
--- a/src/module.rs Sun Jun 01 01:15:04 2025 -0400 +++ b/src/module.rs Tue Jun 03 01:21:59 2025 -0400 @@ -1,7 +1,13 @@ //! Functions and types useful for implementing a PAM module. +// Temporarily allowed until we get the actual conversation functions hooked up. +#![allow(dead_code)] + use crate::constants::{ErrorCode, Flags, Result}; +use crate::conv::BinaryData; +use crate::conv::{Conversation, Message, Response}; use crate::handle::PamModuleHandle; +use secure_string::SecureString; use std::ffi::CStr; /// A trait for a PAM module to implement. @@ -233,6 +239,81 @@ } } +/// 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<C: Conversation>(pub 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 @@ -379,11 +460,91 @@ } #[cfg(test)] -pub mod test { - use crate::module::{PamModule, PamModuleHandle}; +mod test { + 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; - struct Foo; - impl<T: PamModuleHandle> 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()) + } + } + } - pam_hooks!(Foo); + let mut mux = ConversationMux(MuxTester); + 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() + ) + } }
--- a/src/pam_ffi.rs Sun Jun 01 01:15:04 2025 -0400 +++ b/src/pam_ffi.rs Tue Jun 03 01:21:59 2025 -0400 @@ -9,14 +9,12 @@ // Temporarily allow dead code. #![allow(dead_code)] -use crate::constants::InvalidEnum; +use crate::constants::{InvalidEnum, NulError, TooBigError}; use num_derive::FromPrimitive; use num_traits::FromPrimitive; use std::ffi::{c_char, c_int, c_void, CStr}; use std::marker::{PhantomData, PhantomPinned}; -use std::num::TryFromIntError; use std::slice; -use thiserror::Error; /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. type Immovable = PhantomData<(*mut u8, PhantomPinned)>; @@ -40,10 +38,12 @@ ErrorMsg = 3, /// An informational message. TextInfo = 4, - /// Yes/No/Maybe conditionals. Linux-PAM specific. + /// Yes/No/Maybe conditionals. A Linux-PAM extension. RadioType = 5, /// For server–client non-human interaction. + /// /// NOT part of the X/Open PAM specification. + /// A Linux-PAM extension. BinaryPrompt = 7, } @@ -68,20 +68,18 @@ /// The style of message to request. style: c_int, /// A description of the data requested. + /// /// For most requests, this will be an owned [`CStr`], but for requests - /// with [`MessageStyle::BinaryPrompt`], this will be [`BinaryData`]. + /// with [`MessageStyle::BinaryPrompt`], this will be [`BinaryData`] + /// (a Linux-PAM extension). data: *const c_void, } -/// Returned when text that should not have any `\0` bytes in it does. -/// Analogous to [`std::ffi::NulError`], but the data it was created from -/// is borrowed. -#[derive(Debug, Error)] -#[error("null byte within input at byte {0}")] -pub struct NulError(usize); - -#[repr(transparent)] -pub struct TextResponseInner(ResponseInner); +#[repr(C)] +pub struct TextResponseInner { + data: *mut c_char, + _unused: c_int, +} impl TextResponseInner { /// Allocates a new text response on the C heap. @@ -91,7 +89,7 @@ /// on the pointer you get back when you're done with it. pub fn alloc(text: impl AsRef<str>) -> Result<*mut Self, NulError> { let str_data = Self::malloc_str(text)?; - let inner = ResponseInner::alloc(str_data); + let inner = GenericResponse::alloc(str_data); Ok(inner as *mut Self) } @@ -100,7 +98,7 @@ // 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) } + unsafe { CStr::from_ptr(self.data) } } /// Releases memory owned by this response. @@ -109,7 +107,14 @@ /// /// You are responsible for no longer using this after calling free. pub unsafe fn free(me: *mut Self) { - ResponseInner::free(me as *mut ResponseInner) + if !me.is_null() { + let data = (*me).data; + if !data.is_null() { + libc::memset(data as *mut c_void, 0, libc::strlen(data)); + } + libc::free(data as *mut c_void); + } + libc::free(me as *mut c_void); } /// Allocates a string with the given contents on the C heap. @@ -118,7 +123,7 @@ /// /// - it allocates data on the C heap with [`libc::malloc`]. /// - it doesn't take ownership of the data passed in. - fn malloc_str(text: impl AsRef<str>) -> Result<*const c_void, NulError> { + fn malloc_str(text: impl AsRef<str>) -> Result<*mut c_void, NulError> { let data = text.as_ref().as_bytes(); if let Some(nul) = data.iter().position(|x| *x == 0) { return Err(NulError(nul)); @@ -126,14 +131,17 @@ unsafe { let data_alloc = libc::calloc(data.len() + 1, 1); libc::memcpy(data_alloc, data.as_ptr() as *const c_void, data.len()); - Ok(data_alloc as *const c_void) + Ok(data_alloc) } } } -/// A [`ResponseInner`] with [`BinaryData`] in it. -#[repr(transparent)] -pub struct BinaryResponseInner(ResponseInner); +/// A [`GenericResponse`] with [`BinaryData`] in it. +#[repr(C)] +pub struct BinaryResponseInner { + data: *mut BinaryData, + _unused: c_int, +} impl BinaryResponseInner { /// Allocates a new binary response on the C heap. @@ -144,17 +152,15 @@ /// The referenced data is copied to the C heap. We do not take ownership. /// You are responsible for calling [`free`](Self::free) /// on the pointer you get back when you're done with it. - pub fn alloc(data: impl AsRef<[u8]>, data_type: u8) -> Result<*mut Self, TryFromIntError> { + pub fn alloc(data: &[u8], data_type: u8) -> Result<*mut Self, TooBigError> { let bin_data = BinaryData::alloc(data, data_type)?; - let inner = ResponseInner::alloc(bin_data as *const c_void); + let inner = GenericResponse::alloc(bin_data as *mut c_void); Ok(inner as *mut Self) } /// Gets the binary data in this response. pub fn contents(&self) -> &[u8] { - let data = self.data(); - let length = (u32::from_be_bytes(data.total_length) - 5) as usize; - unsafe { slice::from_raw_parts(data.data.as_ptr(), length) } + self.data().contents() } /// Gets the `data_type` tag that was embedded with the message. @@ -167,7 +173,7 @@ // 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 BinaryData) } + unsafe { &*(self.data) } } /// Releases memory owned by this response. @@ -176,38 +182,9 @@ /// /// You are responsible for not using this after calling free. pub unsafe fn free(me: *mut Self) { - ResponseInner::free(me as *mut ResponseInner) - } -} - -#[repr(C)] -pub struct ResponseInner { - /// Pointer to the data returned in a response. - /// For most responses, this will be a [`CStr`], but for responses to - /// [`MessageStyle::BinaryPrompt`]s, this will be [`BinaryData`] - data: *const c_void, - /// Unused. - return_code: c_int, -} - -impl ResponseInner { - /// Allocates a response on the C heap pointing to the given data. - fn alloc(data: *const c_void) -> *mut Self { - unsafe { - let alloc = libc::calloc(1, size_of::<ResponseInner>()) as *mut ResponseInner; - (*alloc).data = data; - alloc + if !me.is_null() { + BinaryData::free((*me).data); } - } - - /// Frees one of these that was created with [`Self::alloc`] - /// (or provided by PAM). - /// - /// # Safety - /// - /// It's up to you to stop using `me` after calling this. - unsafe fn free(me: *mut Self) { - libc::free((*me).data as *mut c_void); libc::free(me as *mut c_void) } } @@ -216,6 +193,8 @@ /// /// This is an unsized data type whose memory goes beyond its data. /// This must be allocated on the C heap. +/// +/// A Linux-PAM extension. #[repr(C)] struct BinaryData { /// The total length of the structure; a u32 in network byte order (BE). @@ -228,12 +207,12 @@ } impl BinaryData { - fn alloc( - source: impl AsRef<[u8]>, - data_type: u8, - ) -> Result<*const BinaryData, TryFromIntError> { - let source = source.as_ref(); - let buffer_size = u32::try_from(source.len() + 5)?; + /// Copies the given data to a new BinaryData on the heap. + fn alloc(source: &[u8], data_type: u8) -> Result<*mut BinaryData, TooBigError> { + let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError { + max: (u32::MAX - 5) as usize, + actual: source.len(), + })?; let data = unsafe { let dest_buffer = libc::malloc(buffer_size as usize) as *mut BinaryData; let data = &mut *dest_buffer; @@ -249,6 +228,69 @@ }; Ok(data) } + + fn length(&self) -> usize { + u32::from_be_bytes(self.total_length).saturating_sub(5) as usize + } + + fn contents(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) } + } + + /// Clears this data and frees it. + fn free(me: *mut Self) { + if me.is_null() { + return; + } + unsafe { + let me_too = &mut *me; + let contents = slice::from_raw_parts_mut(me_too.data.as_mut_ptr(), me_too.length()); + for v in contents { + *v = 0 + } + me_too.data_type = 0; + me_too.total_length = [0; 4]; + libc::free(me as *mut c_void); + } + } +} + +/// Generic version of response data. +/// +/// This has the same structure as [`BinaryResponseInner`] +/// and [`TextResponseInner`]. +#[repr(C)] +pub struct GenericResponse { + /// Pointer to the data returned in a response. + /// For most responses, this will be a [`CStr`], but for responses to + /// [`MessageStyle::BinaryPrompt`]s, this will be [`BinaryData`] + /// (a Linux-PAM extension). + data: *mut c_void, + /// Unused. + return_code: c_int, +} + +impl GenericResponse { + /// Allocates a response on the C heap pointing to the given data. + fn alloc(data: *mut c_void) -> *mut Self { + unsafe { + let alloc = libc::calloc(1, size_of::<Self>()) as *mut Self; + (*alloc).data = data; + alloc + } + } + + /// Frees a response on the C heap. + /// + /// # Safety + /// + /// It's on you to stop using this GenericResponse after freeing it. + pub unsafe fn free(me: *mut GenericResponse) { + if !me.is_null() { + libc::free((*me).data); + } + libc::free(me as *mut c_void); + } } /// An opaque pointer we provide to PAM for callbacks. @@ -261,14 +303,47 @@ /// 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 an array of pointers to [`Message`]s. -/// - `responses` is a pointer to an array of [`ResponseInner`]s, +/// - `messages` is a pointer to some [`Message`]s (see note). +/// - `responses` is a pointer to an array of [`GenericResponse`]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 [`Conversation`] we were passed. +/// +/// NOTE: On Linux-PAM and other compatible implementations, `messages` +/// is treated as a pointer-to-pointers, like `int argc, char **argv`. +/// +/// ```text +/// ┌──────────┐ points to ┌─────────────┐ ╔═ Message ═╗ +/// │ messages │ ┄┄┄┄┄┄┄┄┄┄> │ messages[0] │ ┄┄┄┄> ║ style ║ +/// └──────────┘ │ messages[1] │ ┄┄╮ ║ data ║ +/// │ ... │ ┆ ╚═══════════╝ +/// ┆ +/// ┆ ╔═ Message ═╗ +/// ╰┄┄> ║ style ║ +/// ║ data ║ +/// ╚═══════════╝ +/// ``` +/// +/// On OpenPAM and other compatible implementations (like Solaris), +/// `messages` is a pointer-to-pointer-to-array. +/// +/// ```text +/// ┌──────────┐ points to ┌───────────┐ ╔═ Message[] ═╗ +/// │ messages │ ┄┄┄┄┄┄┄┄┄┄> │ *messages │ ┄┄┄┄> ║ style ║ +/// └──────────┘ └───────────┘ ║ data ║ +/// ╟─────────────╢ +/// ║ style ║ +/// ║ data ║ +/// ╟─────────────╢ +/// ║ ... ║ +/// ``` +/// +/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** pub type ConversationCallback = extern "C" fn( num_msg: c_int, messages: *const *const Message, - responses: &mut *const ResponseInner, + responses: &mut *const GenericResponse, appdata: *const AppData, ) -> c_int; @@ -316,7 +391,7 @@ #[cfg(test)] mod test { - use super::{BinaryResponseInner, TextResponseInner}; + use super::{BinaryResponseInner, GenericResponse, TextResponseInner}; #[test] fn test_text_response() { @@ -342,6 +417,14 @@ } #[test] + fn test_free_safety() { + unsafe { + TextResponseInner::free(std::ptr::null_mut()); + BinaryResponseInner::free(std::ptr::null_mut()); + } + } + + #[test] #[ignore] fn test_binary_response_too_big() { let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000];