Mercurial > crates > nonstick
changeset 75:c30811b4afae
rename pam_ffi submodule to libpam.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Fri, 06 Jun 2025 22:35:08 -0400 |
parents | c7c596e6388f |
children | e58d24849e82 |
files | src/handle.rs src/lib.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/memory.rs src/libpam/message.rs src/libpam/mod.rs src/libpam/module.rs src/libpam/response.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/module.rs src/pam_ffi/response.rs |
diffstat | 16 files changed, 1469 insertions(+), 1469 deletions(-) [+] |
line wrap: on
line diff
--- a/src/handle.rs Fri Jun 06 22:21:17 2025 -0400 +++ b/src/handle.rs Fri Jun 06 22:35:08 2025 -0400 @@ -64,7 +64,7 @@ /// 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). +/// You probably want [`LibPamHandle`](crate::libpam::OwnedLibPamHandle). /// This trait is intended to allow creating mock PAM handle types /// to test PAM modules and applications. pub trait PamShared {
--- a/src/lib.rs Fri Jun 06 22:21:17 2025 -0400 +++ b/src/lib.rs Fri Jun 06 22:35:08 2025 -0400 @@ -32,10 +32,10 @@ pub mod handle; #[cfg(feature = "link")] -mod pam_ffi; +mod libpam; #[cfg(feature = "link")] -pub use crate::pam_ffi::{LibPamHandle, OwnedLibPamHandle}; +pub use crate::libpam::{LibPamHandle, OwnedLibPamHandle}; #[doc(inline)] pub use crate::{ constants::{ErrorCode, Flags, Result},
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/conversation.rs Fri Jun 06 22:35:08 2025 -0400 @@ -0,0 +1,162 @@ +use crate::constants::Result; +use crate::conv::{Conversation, Message, Response}; +use crate::libpam::memory::Immovable; +use crate::libpam::message::{MessageIndirector, OwnedMessages}; +use crate::libpam::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.communicate(&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 communicate(&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::ErrorMsg(_) | Message::InfoMsg(_) => 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/libpam/handle.rs Fri Jun 06 22:35:08 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::libpam::memory; +use crate::libpam::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 communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> { + self.conversation_item()?.communicate(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 + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/memory.rs Fri Jun 06 22:35:08 2025 -0400 @@ -0,0 +1,204 @@ +//! Things for dealing with memory. + +use crate::ErrorCode; +use crate::Result; +use std::ffi::{c_char, c_void, CStr, CString}; +use std::marker::{PhantomData, PhantomPinned}; +use std::result::Result as StdResult; +use std::{ptr, slice}; + +/// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. +#[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>> { + prompt + .map(CString::new) + .transpose() + .map_err(|_| ErrorCode::ConversationError) +} + +/// Gets the pointer to the given CString, or a null pointer if absent. +pub fn prompt_ptr(prompt: Option<&CString>) -> *const c_char { + match prompt { + Some(c_str) => c_str.as_ptr(), + None => ptr::null(), + } +} + +/// Creates an owned copy of a string that is returned from a +/// <code>pam_get_<var>whatever</var></code> function. +/// +/// # Safety +/// +/// It's on you to provide a valid string. +pub unsafe fn copy_pam_string(result_ptr: *const libc::c_char) -> Result<String> { + // We really shouldn't get a null pointer back here, but if we do, return nothing. + if result_ptr.is_null() { + return Ok(String::new()); + } + let bytes = unsafe { CStr::from_ptr(result_ptr) }; + bytes + .to_str() + .map(String::from) + .map_err(|_| ErrorCode::ConversationError) +} + +/// Wraps a string returned from PAM as an `Option<&str>`. +pub unsafe fn wrap_string<'a>(data: *const libc::c_char) -> Result<Option<&'a str>> { + let ret = if data.is_null() { + None + } else { + Some( + CStr::from_ptr(data) + .to_str() + .map_err(|_| ErrorCode::ConversationError)?, + ) + }; + Ok(ret) +} + +/// Allocates a string with the given contents on the C heap. +/// +/// This is like [`CString::new`](std::ffi::CString::new), but: +/// +/// - it allocates data on the C heap with [`libc::malloc`]. +/// - it doesn't take ownership of the data passed in. +pub fn malloc_str(text: impl AsRef<str>) -> StdResult<*mut c_char, NulError> { + let data = text.as_ref().as_bytes(); + if let Some(nul) = data.iter().position(|x| *x == 0) { + return Err(NulError(nul)); + } + unsafe { + let data_alloc = libc::calloc(data.len() + 1, 1); + libc::memcpy(data_alloc, data.as_ptr().cast(), data.len()); + Ok(data_alloc.cast()) + } +} + +/// Writes zeroes over the contents of a C string. +/// +/// This won't overwrite a null pointer. +/// +/// # Safety +/// +/// 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.cast())); + } +} + +/// Binary data used in requests and responses. +/// +/// 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)] +pub struct CBinaryData { + /// The total length of the structure; a u32 in network byte order (BE). + total_length: [u8; 4], + /// A tag of undefined meaning. + data_type: u8, + /// Pointer to an array of length [`length`](Self::length) − 5 + data: [u8; 0], + _marker: Immovable, +} + +impl CBinaryData { + /// Copies the given data to a new BinaryData on the heap. + pub fn alloc(source: &[u8], data_type: u8) -> StdResult<*mut CBinaryData, TooBigError> { + let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError { + max: (u32::MAX - 5) as usize, + actual: source.len(), + })?; + // SAFETY: We're only allocating here. + let data = unsafe { + 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.cast(), source.as_ptr().cast(), source.len()); + dest_buffer + }; + Ok(data) + } + + fn length(&self) -> usize { + u32::from_be_bytes(self.total_length).saturating_sub(5) as usize + } + + pub fn contents(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) } + } + pub fn data_type(&self) -> u8 { + self.data_type + } + + /// Clears this data and frees it. + pub unsafe fn zero_contents(&mut self) { + let contents = slice::from_raw_parts_mut(self.data.as_mut_ptr(), self.length()); + for v in contents { + *v = 0 + } + self.data_type = 0; + self.total_length = [0; 4]; + } +} + +#[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::{copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string}; + use crate::ErrorCode; + use std::ffi::CString; + #[test] + fn test_strings() { + let str = malloc_str("hello there").unwrap(); + malloc_str("hell\0 there").unwrap_err(); + unsafe { + let copied = copy_pam_string(str.cast()).unwrap(); + assert_eq!("hello there", copied); + zero_c_string(str.cast()); + let idx_three = str.add(3).as_mut().unwrap(); + *idx_three = 0x80u8 as i8; + let zeroed = copy_pam_string(str.cast()).unwrap(); + assert!(zeroed.is_empty()); + libc::free(str.cast()); + } + } + + #[test] + fn test_option_str() { + let good = option_cstr(Some("whatever")).unwrap(); + assert_eq!("whatever", good.unwrap().to_str().unwrap()); + let no_str = option_cstr(None).unwrap(); + assert!(no_str.is_none()); + let bad_str = option_cstr(Some("what\0ever")).unwrap_err(); + assert_eq!(ErrorCode::ConversationError, bad_str); + } + + #[test] + fn test_prompt() { + let prompt_cstr = CString::new("good").ok(); + let prompt = prompt_ptr(prompt_cstr.as_ref()); + assert!(!prompt.is_null()); + let no_prompt = prompt_ptr(None); + assert!(no_prompt.is_null()); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/message.rs Fri Jun 06 22:35:08 2025 -0400 @@ -0,0 +1,334 @@ +//! Data and types dealing with PAM messages. + +use crate::constants::InvalidEnum; +use crate::conv::Message; +use crate::libpam::memory; +use crate::libpam::memory::{CBinaryData, Immovable, NulError, TooBigError}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use std::ffi::{c_int, c_void, CStr}; +use std::result::Result as StdResult; +use std::str::Utf8Error; +use std::{ptr, slice}; + +#[derive(Debug, thiserror::Error)] +#[error("error creating PAM message: {0}")] +pub enum ConversionError { + InvalidEnum(#[from] InvalidEnum<Style>), + Utf8Error(#[from] Utf8Error), + NulError(#[from] NulError), + TooBigError(#[from] TooBigError), +} + +/// The C enum values for messages shown to the user. +#[derive(Debug, PartialEq, FromPrimitive)] +pub enum Style { + /// Requests information from the user; will be masked when typing. + PromptEchoOff = 1, + /// Requests information from the user; will not be masked. + PromptEchoOn = 2, + /// An error message. + ErrorMsg = 3, + /// An informational message. + TextInfo = 4, + /// Yes/No/Maybe conditionals. A Linux-PAM extension. + RadioType = 5, + /// For server–client non-human interaction. + /// + /// NOT part of the X/Open PAM specification. + /// A Linux-PAM extension. + BinaryPrompt = 7, +} + +impl TryFrom<c_int> for Style { + type Error = InvalidEnum<Self>; + fn try_from(value: c_int) -> StdResult<Self, Self::Error> { + Self::from_i32(value).ok_or(value.into()) + } +} + +impl From<Style> for c_int { + fn from(val: Style) -> Self { + val as Self + } +} + +/// A message sent by PAM or a module to an application. +/// This message, and its internal data, is owned by the creator +/// (either the module or PAM itself). +#[repr(C)] +pub struct RawMessage { + /// The style of message to request. + style: c_int, + /// A description of the data requested. + /// + /// For most requests, this will be an owned [`CStr`], but for requests + /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] + /// (a Linux-PAM extension). + data: *mut c_void, + _marker: Immovable, +} + +impl RawMessage { + 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. + unsafe { libc::free(self.data) }; + self.style = style as c_int; + self.data = data; + Ok(()) + } + + /// Gets this message's data pointer as a string. + /// + /// # Safety + /// + /// It's up to you to pass this only on types with a string value. + unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> { + if self.data.is_null() { + Ok("") + } else { + CStr::from_ptr(self.data.cast()).to_str() + } + } + + /// Zeroes out the data stored here. + fn clear(&mut self) { + // SAFETY: We either created this data or we got it from PAM. + // After this function is done, it will be zeroed out. + unsafe { + if let Ok(style) = Style::try_from(self.style) { + match style { + Style::BinaryPrompt => { + if let Some(d) = self.data.cast::<CBinaryData>().as_mut() { + d.zero_contents() + } + } + Style::TextInfo + | Style::RadioType + | Style::ErrorMsg + | Style::PromptEchoOff + | 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::ErrorMsg(text) => alloc(Style::ErrorMsg, text), + Message::InfoMsg(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` +/// is treated as a pointer-to-pointers, like `int argc, char **argv`. +/// (In this situation, the value of `OwnedMessages.indirect` is +/// the pointer passed to `pam_conv`.) +/// +/// ```text +/// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message ═╗ +/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ +/// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ║ +/// ╚═════════════╝ │ ... │ ┆ ╚═══════════╝ +/// ┆ +/// ┆ ╔═ Message ═╗ +/// ╰┄┄> ║ style ║ +/// ║ data ║ +/// ╚═══════════╝ +/// ``` +/// +/// On OpenPAM and other compatible implementations (like Solaris), +/// `messages` is a pointer-to-pointer-to-array. This appears to be +/// the correct implementation as required by the XSSO specification. +/// +/// ```text +/// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message[] ═╗ +/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ +/// ║ count ║ └────────────┘ ║ data ║ +/// ╚═════════════╝ ╟─────────────╢ +/// ║ style ║ +/// ║ data ║ +/// ╟─────────────╢ +/// ║ ... ║ +/// ``` +/// +/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** +pub struct OwnedMessages { + /// An indirection to the messages themselves, stored on the C heap. + indirect: *mut MessageIndirector, + /// The number of messages in the list. + count: usize, +} + +impl OwnedMessages { + /// Allocates data to store messages on the C heap. + pub fn alloc(count: usize) -> Self { + 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 { + if let Some(indirect) = self.indirect.as_mut() { + indirect.free(self.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 + } + } + + /// 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)) + } + + /// 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(); + } + } +} + +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::InfoMsg(input.string_data()?), + Style::ErrorMsg => Message::ErrorMsg(input.string_data()?), + Style::RadioType => Message::ErrorMsg(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) + } +} + +#[cfg(test)] +mod tests { + use crate::conv::Message; + use crate::libpam::message::OwnedMessages; + + #[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(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/mod.rs Fri Jun 06 22:35:08 2025 -0400 @@ -0,0 +1,65 @@ +//! The PAM library FFI and helpers for managing it. +//! +//! This includes the functions provided by PAM and the data structures +//! used by PAM, as well as a few low-level abstractions for dealing with +//! those data structures. +//! +//! Everything in here is hazmat. +//! + +#![allow(dead_code)] + +mod conversation; +mod handle; +mod memory; +mod message; +mod module; +mod response; + +pub use handle::{LibPamHandle, OwnedLibPamHandle}; +use std::ffi::{c_char, c_int, c_void}; + +#[link(name = "pam")] +extern "C" { + fn pam_get_data( + pamh: *mut LibPamHandle, + module_data_name: *const c_char, + data: &mut *const c_void, + ) -> c_int; + + fn pam_set_data( + pamh: *mut LibPamHandle, + module_data_name: *const c_char, + data: *const c_void, + cleanup: extern "C" fn(pamh: *const c_void, data: *mut c_void, error_status: c_int), + ) -> c_int; + + fn pam_get_item(pamh: *mut LibPamHandle, item_type: c_int, item: &mut *const c_void) -> c_int; + + fn pam_set_item(pamh: *mut LibPamHandle, item_type: c_int, item: *const c_void) -> c_int; + + fn pam_get_user( + pamh: *mut LibPamHandle, + user: &mut *const c_char, + prompt: *const c_char, + ) -> c_int; + + fn pam_get_authtok( + pamh: *mut LibPamHandle, + item_type: c_int, + data: &mut *const c_char, + prompt: *const c_char, + ) -> c_int; + + fn pam_end(pamh: *mut LibPamHandle, status: c_int) -> c_int; + + // TODO: pam_authenticate - app + // pam_setcred - app + // pam_acct_mgmt - app + // pam_chauthtok - app + // pam_open_session - app + // pam_close_session - app + // pam_putenv - shared + // pam_getenv - shared + // pam_getenvlist - shared +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/module.rs Fri Jun 06 22:35:08 2025 -0400 @@ -0,0 +1,153 @@ +use std::ffi::CStr; + +/// Generates the dynamic library entry points for a [PamModule] implementation. +/// +/// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will +/// generate the exported `extern "C"` functions that PAM uses to call into +/// your module. +/// +/// ## Examples: +/// +/// Here is full example of a PAM module that would authenticate and authorize everybody: +/// +/// ``` +/// use nonstick::{Flags, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks}; +/// use std::ffi::CStr; +/// # fn main() {} +/// +/// struct MyPamModule; +/// pam_hooks!(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?"))?; +/// handle.info_msg(fmt!("If you say your password is {password:?}, who am I to disagree?")); +/// } +/// +/// fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> { +/// let username = handle.get_user(None)?; +/// handle.info_msg(fmt!("Hello {username}! I trust you unconditionally.")) +/// Ok(()) +/// } +/// } +/// ``` +#[macro_export] +macro_rules! pam_hooks { + ($ident:ident) => { + mod _pam_hooks_scope { + use std::ffi::{c_char, c_int, CStr}; + use $crate::{ErrorCode, Flags, LibPamHandle, PamModule}; + + #[no_mangle] + extern "C" fn pam_sm_acct_mgmt( + pamh: *mut libc::c_void, + flags: Flags, + argc: c_int, + argv: *const *const c_char, + ) -> c_int { + if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { + let args = extract_argv(argc, argv); + ErrorCode::result_to_c(super::$ident::account_management(handle, args, flags)) + } else { + ErrorCode::Ignore as c_int + } + } + + #[no_mangle] + extern "C" fn pam_sm_authenticate( + pamh: *mut libc::c_void, + flags: Flags, + argc: c_int, + argv: *const *const c_char, + ) -> c_int { + if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { + let args = extract_argv(argc, argv); + ErrorCode::result_to_c(super::$ident::authenticate(handle, args, flags)) + } else { + ErrorCode::Ignore as c_int + } + } + + #[no_mangle] + extern "C" fn pam_sm_chauthtok( + pamh: *mut libc::c_void, + flags: Flags, + argc: c_int, + argv: *const *const c_char, + ) -> c_int { + if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { + let args = extract_argv(argc, argv); + ErrorCode::result_to_c(super::$ident::change_authtok(handle, args, flags)) + } else { + ErrorCode::Ignore as c_int + } + } + + #[no_mangle] + extern "C" fn pam_sm_close_session( + pamh: *mut libc::c_void, + flags: Flags, + argc: c_int, + argv: *const *const c_char, + ) -> c_int { + if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { + let args = extract_argv(argc, argv); + ErrorCode::result_to_c(super::$ident::close_session(handle, args, flags)) + } else { + ErrorCode::Ignore as c_int + } + } + + #[no_mangle] + extern "C" fn pam_sm_open_session( + pamh: *mut libc::c_void, + flags: Flags, + argc: c_int, + argv: *const *const c_char, + ) -> c_int { + let args = extract_argv(argc, argv); + if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { + ErrorCode::result_to_c(super::$ident::open_session(handle, args, flags)) + } else { + ErrorCode::Ignore as c_int + } + } + + #[no_mangle] + extern "C" fn pam_sm_setcred( + pamh: *mut libc::c_void, + flags: Flags, + argc: c_int, + argv: *const *const c_char, + ) -> c_int { + let args = extract_argv(argc, argv); + if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { + ErrorCode::result_to_c(super::$ident::set_credentials(handle, args, flags)) + } else { + ErrorCode::Ignore as c_int + } + } + + /// Turns `argc`/`argv` into a [Vec] of [CStr]s. + /// + /// # Safety + /// + /// We use this only with arguments we get from `libpam`, which we kind of have to trust. + fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> { + (0..argc) + .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) }) + .collect() + } + } + }; +} + +#[cfg(test)] +mod tests { + // Compile-time test that the `pam_hooks` macro compiles. + use crate::{PamHandleModule, PamModule}; + struct Foo; + impl<T: PamHandleModule> PamModule<T> for Foo {} + + pam_hooks!(Foo); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/response.rs Fri Jun 06 22:35:08 2025 -0400 @@ -0,0 +1,317 @@ +//! Types used when dealing with PAM conversations. + +use crate::conv::BinaryData; +use crate::libpam::memory; +use crate::libpam::memory::{CBinaryData, Immovable, NulError, TooBigError}; +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::{iter, mem, ptr, slice}; + +#[repr(transparent)] +#[derive(Debug)] +pub struct RawTextResponse(RawResponse); + +impl RawTextResponse { + /// Interprets the provided `RawResponse` as a text response. + /// + /// # Safety + /// + /// 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(); + // 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> { + 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. + 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() + } + } +} + +/// A [`RawResponse`] with [`CBinaryData`] in it. +#[repr(transparent)] +#[derive(Debug)] +pub struct RawBinaryResponse(RawResponse); + +impl RawBinaryResponse { + /// 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. + /// + /// The referenced data is copied to the C heap. We do not take ownership. + /// You are responsible for calling [`free`](Self::free_contents) + /// on the pointer you get back when you're done with it. + pub fn fill<'a>( + dest: &'a mut RawResponse, + data: &[u8], + data_type: u8, + ) -> StdResult<&'a mut Self, TooBigError> { + 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 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.contents().map(CBinaryData::data_type).unwrap_or(0) + } + + 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.cast::<CBinaryData>().as_ref() } + } + + pub fn to_owned(&self) -> BinaryData { + BinaryData::new(self.data().into(), self.data_type()) + } + + /// Releases memory owned by this response. + 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() + } + } +} + +/// Generic version of response data. +/// +/// This has the same structure as [`RawBinaryResponse`] +/// and [`RawTextResponse`]. +#[repr(C)] +#[derive(Debug)] +pub struct RawResponse { + /// 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 [`CBinaryData`] + /// (a Linux-PAM extension). + data: *mut c_void, + /// Unused. + return_code: c_int, + _marker: Immovable, +} + +/// A contiguous block of responses. +#[derive(Debug)] +pub struct OwnedResponses { + base: *mut RawResponse, + count: usize, +} + +impl OwnedResponses { + /// Allocates an owned list of responses on the C heap. + fn alloc(count: usize) -> Self { + OwnedResponses { + // SAFETY: We are doing allocation here. + 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. + pub unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self { + OwnedResponses { base, count } + } +} + +#[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: 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: This is the memory we manage ourselves. + unsafe { slice::from_raw_parts_mut(self.base, self.count) } + } +} + +impl Drop for OwnedResponses { + fn drop(&mut self) { + // SAFETY: We allocated this ourselves, or it was provided to us by PAM. + unsafe { + for resp in self.iter_mut() { + libc::free(resp.data) + } + libc::free(self.base.cast()) + } + } +} + +#[cfg(test)] +mod tests { + use super::{BinaryData, OwnedResponses, RawBinaryResponse, RawTextResponse, Response}; + + #[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() { + let mut responses = OwnedResponses::alloc(2); + let text = RawTextResponse::fill(&mut responses[0], "hello").unwrap(); + let data = text.contents().expect("valid"); + assert_eq!("hello", data); + text.free_contents(); + text.free_contents(); + RawTextResponse::fill(&mut responses[1], "hell\0").expect_err("should error; contains nul"); + } + + #[test] + fn test_binary_response() { + let mut responses = OwnedResponses::alloc(1); + 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.data(); + assert_eq!(&real_data, data); + assert_eq!(7, resp.data_type()); + resp.free_contents(); + resp.free_contents(); + } + + #[test] + #[ignore] + fn test_binary_response_too_big() { + let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000]; + let mut responses = OwnedResponses::alloc(1); + RawBinaryResponse::fill(&mut responses[0], &big_data, 0).expect_err("this is too big!"); + } +}
--- a/src/pam_ffi/conversation.rs Fri Jun 06 22:21:17 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,162 +0,0 @@ -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.communicate(&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 communicate(&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::ErrorMsg(_) | Message::InfoMsg(_) => 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 - } - }) -}
--- a/src/pam_ffi/handle.rs Fri Jun 06 22:21:17 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,231 +0,0 @@ -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 communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> { - self.conversation_item()?.communicate(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 Fri Jun 06 22:21:17 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,204 +0,0 @@ -//! Things for dealing with memory. - -use crate::ErrorCode; -use crate::Result; -use std::ffi::{c_char, c_void, CStr, CString}; -use std::marker::{PhantomData, PhantomPinned}; -use std::result::Result as StdResult; -use std::{ptr, slice}; - -/// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. -#[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>> { - prompt - .map(CString::new) - .transpose() - .map_err(|_| ErrorCode::ConversationError) -} - -/// Gets the pointer to the given CString, or a null pointer if absent. -pub fn prompt_ptr(prompt: Option<&CString>) -> *const c_char { - match prompt { - Some(c_str) => c_str.as_ptr(), - None => ptr::null(), - } -} - -/// Creates an owned copy of a string that is returned from a -/// <code>pam_get_<var>whatever</var></code> function. -/// -/// # Safety -/// -/// It's on you to provide a valid string. -pub unsafe fn copy_pam_string(result_ptr: *const libc::c_char) -> Result<String> { - // We really shouldn't get a null pointer back here, but if we do, return nothing. - if result_ptr.is_null() { - return Ok(String::new()); - } - let bytes = unsafe { CStr::from_ptr(result_ptr) }; - bytes - .to_str() - .map(String::from) - .map_err(|_| ErrorCode::ConversationError) -} - -/// Wraps a string returned from PAM as an `Option<&str>`. -pub unsafe fn wrap_string<'a>(data: *const libc::c_char) -> Result<Option<&'a str>> { - let ret = if data.is_null() { - None - } else { - Some( - CStr::from_ptr(data) - .to_str() - .map_err(|_| ErrorCode::ConversationError)?, - ) - }; - Ok(ret) -} - -/// Allocates a string with the given contents on the C heap. -/// -/// This is like [`CString::new`](std::ffi::CString::new), but: -/// -/// - it allocates data on the C heap with [`libc::malloc`]. -/// - it doesn't take ownership of the data passed in. -pub fn malloc_str(text: impl AsRef<str>) -> StdResult<*mut c_char, NulError> { - let data = text.as_ref().as_bytes(); - if let Some(nul) = data.iter().position(|x| *x == 0) { - return Err(NulError(nul)); - } - unsafe { - let data_alloc = libc::calloc(data.len() + 1, 1); - libc::memcpy(data_alloc, data.as_ptr().cast(), data.len()); - Ok(data_alloc.cast()) - } -} - -/// Writes zeroes over the contents of a C string. -/// -/// This won't overwrite a null pointer. -/// -/// # Safety -/// -/// 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.cast())); - } -} - -/// Binary data used in requests and responses. -/// -/// 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)] -pub struct CBinaryData { - /// The total length of the structure; a u32 in network byte order (BE). - total_length: [u8; 4], - /// A tag of undefined meaning. - data_type: u8, - /// Pointer to an array of length [`length`](Self::length) − 5 - data: [u8; 0], - _marker: Immovable, -} - -impl CBinaryData { - /// Copies the given data to a new BinaryData on the heap. - pub fn alloc(source: &[u8], data_type: u8) -> StdResult<*mut CBinaryData, TooBigError> { - let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError { - max: (u32::MAX - 5) as usize, - actual: source.len(), - })?; - // SAFETY: We're only allocating here. - let data = unsafe { - 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.cast(), source.as_ptr().cast(), source.len()); - dest_buffer - }; - Ok(data) - } - - fn length(&self) -> usize { - u32::from_be_bytes(self.total_length).saturating_sub(5) as usize - } - - pub fn contents(&self) -> &[u8] { - unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) } - } - pub fn data_type(&self) -> u8 { - self.data_type - } - - /// Clears this data and frees it. - pub unsafe fn zero_contents(&mut self) { - let contents = slice::from_raw_parts_mut(self.data.as_mut_ptr(), self.length()); - for v in contents { - *v = 0 - } - self.data_type = 0; - self.total_length = [0; 4]; - } -} - -#[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::{copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string}; - use crate::ErrorCode; - use std::ffi::CString; - #[test] - fn test_strings() { - let str = malloc_str("hello there").unwrap(); - malloc_str("hell\0 there").unwrap_err(); - unsafe { - let copied = copy_pam_string(str.cast()).unwrap(); - assert_eq!("hello there", copied); - zero_c_string(str.cast()); - let idx_three = str.add(3).as_mut().unwrap(); - *idx_three = 0x80u8 as i8; - let zeroed = copy_pam_string(str.cast()).unwrap(); - assert!(zeroed.is_empty()); - libc::free(str.cast()); - } - } - - #[test] - fn test_option_str() { - let good = option_cstr(Some("whatever")).unwrap(); - assert_eq!("whatever", good.unwrap().to_str().unwrap()); - let no_str = option_cstr(None).unwrap(); - assert!(no_str.is_none()); - let bad_str = option_cstr(Some("what\0ever")).unwrap_err(); - assert_eq!(ErrorCode::ConversationError, bad_str); - } - - #[test] - fn test_prompt() { - let prompt_cstr = CString::new("good").ok(); - let prompt = prompt_ptr(prompt_cstr.as_ref()); - assert!(!prompt.is_null()); - let no_prompt = prompt_ptr(None); - assert!(no_prompt.is_null()); - } -}
--- a/src/pam_ffi/message.rs Fri Jun 06 22:21:17 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,334 +0,0 @@ -//! 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, Immovable, NulError, TooBigError}; -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; -use std::ffi::{c_int, c_void, CStr}; -use std::result::Result as StdResult; -use std::str::Utf8Error; -use std::{ptr, slice}; - -#[derive(Debug, thiserror::Error)] -#[error("error creating PAM message: {0}")] -pub enum ConversionError { - InvalidEnum(#[from] InvalidEnum<Style>), - Utf8Error(#[from] Utf8Error), - NulError(#[from] NulError), - TooBigError(#[from] TooBigError), -} - -/// The C enum values for messages shown to the user. -#[derive(Debug, PartialEq, FromPrimitive)] -pub enum Style { - /// Requests information from the user; will be masked when typing. - PromptEchoOff = 1, - /// Requests information from the user; will not be masked. - PromptEchoOn = 2, - /// An error message. - ErrorMsg = 3, - /// An informational message. - TextInfo = 4, - /// Yes/No/Maybe conditionals. A Linux-PAM extension. - RadioType = 5, - /// For server–client non-human interaction. - /// - /// NOT part of the X/Open PAM specification. - /// A Linux-PAM extension. - BinaryPrompt = 7, -} - -impl TryFrom<c_int> for Style { - type Error = InvalidEnum<Self>; - fn try_from(value: c_int) -> StdResult<Self, Self::Error> { - Self::from_i32(value).ok_or(value.into()) - } -} - -impl From<Style> for c_int { - fn from(val: Style) -> Self { - val as Self - } -} - -/// A message sent by PAM or a module to an application. -/// This message, and its internal data, is owned by the creator -/// (either the module or PAM itself). -#[repr(C)] -pub struct RawMessage { - /// The style of message to request. - style: c_int, - /// A description of the data requested. - /// - /// For most requests, this will be an owned [`CStr`], but for requests - /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] - /// (a Linux-PAM extension). - data: *mut c_void, - _marker: Immovable, -} - -impl RawMessage { - 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. - unsafe { libc::free(self.data) }; - self.style = style as c_int; - self.data = data; - Ok(()) - } - - /// Gets this message's data pointer as a string. - /// - /// # Safety - /// - /// It's up to you to pass this only on types with a string value. - unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> { - if self.data.is_null() { - Ok("") - } else { - CStr::from_ptr(self.data.cast()).to_str() - } - } - - /// Zeroes out the data stored here. - fn clear(&mut self) { - // SAFETY: We either created this data or we got it from PAM. - // After this function is done, it will be zeroed out. - unsafe { - if let Ok(style) = Style::try_from(self.style) { - match style { - Style::BinaryPrompt => { - if let Some(d) = self.data.cast::<CBinaryData>().as_mut() { - d.zero_contents() - } - } - Style::TextInfo - | Style::RadioType - | Style::ErrorMsg - | Style::PromptEchoOff - | 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::ErrorMsg(text) => alloc(Style::ErrorMsg, text), - Message::InfoMsg(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` -/// is treated as a pointer-to-pointers, like `int argc, char **argv`. -/// (In this situation, the value of `OwnedMessages.indirect` is -/// the pointer passed to `pam_conv`.) -/// -/// ```text -/// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message ═╗ -/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ -/// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ║ -/// ╚═════════════╝ │ ... │ ┆ ╚═══════════╝ -/// ┆ -/// ┆ ╔═ Message ═╗ -/// ╰┄┄> ║ style ║ -/// ║ data ║ -/// ╚═══════════╝ -/// ``` -/// -/// On OpenPAM and other compatible implementations (like Solaris), -/// `messages` is a pointer-to-pointer-to-array. This appears to be -/// the correct implementation as required by the XSSO specification. -/// -/// ```text -/// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message[] ═╗ -/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ -/// ║ count ║ └────────────┘ ║ data ║ -/// ╚═════════════╝ ╟─────────────╢ -/// ║ style ║ -/// ║ data ║ -/// ╟─────────────╢ -/// ║ ... ║ -/// ``` -/// -/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** -pub struct OwnedMessages { - /// An indirection to the messages themselves, stored on the C heap. - indirect: *mut MessageIndirector, - /// The number of messages in the list. - count: usize, -} - -impl OwnedMessages { - /// Allocates data to store messages on the C heap. - pub fn alloc(count: usize) -> Self { - 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 { - if let Some(indirect) = self.indirect.as_mut() { - indirect.free(self.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 - } - } - - /// 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)) - } - - /// 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(); - } - } -} - -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::InfoMsg(input.string_data()?), - Style::ErrorMsg => Message::ErrorMsg(input.string_data()?), - Style::RadioType => Message::ErrorMsg(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) - } -} - -#[cfg(test)] -mod tests { - use crate::conv::Message; - use crate::pam_ffi::message::OwnedMessages; - - #[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 Fri Jun 06 22:21:17 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +0,0 @@ -//! The PAM library FFI and helpers for managing it. -//! -//! This includes the functions provided by PAM and the data structures -//! used by PAM, as well as a few low-level abstractions for dealing with -//! those data structures. -//! -//! Everything in here is hazmat. -//! - -#![allow(dead_code)] - -mod conversation; -mod handle; -mod memory; -mod message; -mod module; -mod response; - -pub use handle::{LibPamHandle, OwnedLibPamHandle}; -use std::ffi::{c_char, c_int, c_void}; - -#[link(name = "pam")] -extern "C" { - fn pam_get_data( - pamh: *mut LibPamHandle, - module_data_name: *const c_char, - data: &mut *const c_void, - ) -> c_int; - - fn pam_set_data( - pamh: *mut LibPamHandle, - module_data_name: *const c_char, - data: *const c_void, - cleanup: extern "C" fn(pamh: *const c_void, data: *mut c_void, error_status: c_int), - ) -> c_int; - - fn pam_get_item(pamh: *mut LibPamHandle, item_type: c_int, item: &mut *const c_void) -> c_int; - - fn pam_set_item(pamh: *mut LibPamHandle, item_type: c_int, item: *const c_void) -> c_int; - - fn pam_get_user( - pamh: *mut LibPamHandle, - user: &mut *const c_char, - prompt: *const c_char, - ) -> c_int; - - fn pam_get_authtok( - pamh: *mut LibPamHandle, - item_type: c_int, - data: &mut *const c_char, - prompt: *const c_char, - ) -> c_int; - - fn pam_end(pamh: *mut LibPamHandle, status: c_int) -> c_int; - - // TODO: pam_authenticate - app - // pam_setcred - app - // pam_acct_mgmt - app - // pam_chauthtok - app - // pam_open_session - app - // pam_close_session - app - // pam_putenv - shared - // pam_getenv - shared - // pam_getenvlist - shared -}
--- a/src/pam_ffi/module.rs Fri Jun 06 22:21:17 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,153 +0,0 @@ -use std::ffi::CStr; - -/// Generates the dynamic library entry points for a [PamModule] implementation. -/// -/// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will -/// generate the exported `extern "C"` functions that PAM uses to call into -/// your module. -/// -/// ## Examples: -/// -/// Here is full example of a PAM module that would authenticate and authorize everybody: -/// -/// ``` -/// use nonstick::{Flags, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks}; -/// use std::ffi::CStr; -/// # fn main() {} -/// -/// struct MyPamModule; -/// pam_hooks!(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?"))?; -/// handle.info_msg(fmt!("If you say your password is {password:?}, who am I to disagree?")); -/// } -/// -/// fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> { -/// let username = handle.get_user(None)?; -/// handle.info_msg(fmt!("Hello {username}! I trust you unconditionally.")) -/// Ok(()) -/// } -/// } -/// ``` -#[macro_export] -macro_rules! pam_hooks { - ($ident:ident) => { - mod _pam_hooks_scope { - use std::ffi::{c_char, c_int, CStr}; - use $crate::{ErrorCode, Flags, LibPamHandle, PamModule}; - - #[no_mangle] - extern "C" fn pam_sm_acct_mgmt( - pamh: *mut libc::c_void, - flags: Flags, - argc: c_int, - argv: *const *const c_char, - ) -> c_int { - if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { - let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::account_management(handle, args, flags)) - } else { - ErrorCode::Ignore as c_int - } - } - - #[no_mangle] - extern "C" fn pam_sm_authenticate( - pamh: *mut libc::c_void, - flags: Flags, - argc: c_int, - argv: *const *const c_char, - ) -> c_int { - if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { - let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::authenticate(handle, args, flags)) - } else { - ErrorCode::Ignore as c_int - } - } - - #[no_mangle] - extern "C" fn pam_sm_chauthtok( - pamh: *mut libc::c_void, - flags: Flags, - argc: c_int, - argv: *const *const c_char, - ) -> c_int { - if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { - let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::change_authtok(handle, args, flags)) - } else { - ErrorCode::Ignore as c_int - } - } - - #[no_mangle] - extern "C" fn pam_sm_close_session( - pamh: *mut libc::c_void, - flags: Flags, - argc: c_int, - argv: *const *const c_char, - ) -> c_int { - if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { - let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::close_session(handle, args, flags)) - } else { - ErrorCode::Ignore as c_int - } - } - - #[no_mangle] - extern "C" fn pam_sm_open_session( - pamh: *mut libc::c_void, - flags: Flags, - argc: c_int, - argv: *const *const c_char, - ) -> c_int { - let args = extract_argv(argc, argv); - if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { - ErrorCode::result_to_c(super::$ident::open_session(handle, args, flags)) - } else { - ErrorCode::Ignore as c_int - } - } - - #[no_mangle] - extern "C" fn pam_sm_setcred( - pamh: *mut libc::c_void, - flags: Flags, - argc: c_int, - argv: *const *const c_char, - ) -> c_int { - let args = extract_argv(argc, argv); - if let Some(handle) = unsafe { pamh.cast::<LibPamHandle>().as_mut() } { - ErrorCode::result_to_c(super::$ident::set_credentials(handle, args, flags)) - } else { - ErrorCode::Ignore as c_int - } - } - - /// Turns `argc`/`argv` into a [Vec] of [CStr]s. - /// - /// # Safety - /// - /// We use this only with arguments we get from `libpam`, which we kind of have to trust. - fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> { - (0..argc) - .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) }) - .collect() - } - } - }; -} - -#[cfg(test)] -mod tests { - // Compile-time test that the `pam_hooks` macro compiles. - use crate::{PamHandleModule, PamModule}; - struct Foo; - impl<T: PamHandleModule> PamModule<T> for Foo {} - - pam_hooks!(Foo); -}
--- a/src/pam_ffi/response.rs Fri Jun 06 22:21:17 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,317 +0,0 @@ -//! 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 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::{iter, mem, ptr, slice}; - -#[repr(transparent)] -#[derive(Debug)] -pub struct RawTextResponse(RawResponse); - -impl RawTextResponse { - /// Interprets the provided `RawResponse` as a text response. - /// - /// # Safety - /// - /// 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(); - // 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> { - 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. - 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() - } - } -} - -/// A [`RawResponse`] with [`CBinaryData`] in it. -#[repr(transparent)] -#[derive(Debug)] -pub struct RawBinaryResponse(RawResponse); - -impl RawBinaryResponse { - /// 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. - /// - /// The referenced data is copied to the C heap. We do not take ownership. - /// You are responsible for calling [`free`](Self::free_contents) - /// on the pointer you get back when you're done with it. - pub fn fill<'a>( - dest: &'a mut RawResponse, - data: &[u8], - data_type: u8, - ) -> StdResult<&'a mut Self, TooBigError> { - 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 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.contents().map(CBinaryData::data_type).unwrap_or(0) - } - - 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.cast::<CBinaryData>().as_ref() } - } - - pub fn to_owned(&self) -> BinaryData { - BinaryData::new(self.data().into(), self.data_type()) - } - - /// Releases memory owned by this response. - 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() - } - } -} - -/// Generic version of response data. -/// -/// This has the same structure as [`RawBinaryResponse`] -/// and [`RawTextResponse`]. -#[repr(C)] -#[derive(Debug)] -pub struct RawResponse { - /// 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 [`CBinaryData`] - /// (a Linux-PAM extension). - data: *mut c_void, - /// Unused. - return_code: c_int, - _marker: Immovable, -} - -/// A contiguous block of responses. -#[derive(Debug)] -pub struct OwnedResponses { - base: *mut RawResponse, - count: usize, -} - -impl OwnedResponses { - /// Allocates an owned list of responses on the C heap. - fn alloc(count: usize) -> Self { - OwnedResponses { - // SAFETY: We are doing allocation here. - 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. - pub unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self { - OwnedResponses { base, count } - } -} - -#[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: 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: This is the memory we manage ourselves. - unsafe { slice::from_raw_parts_mut(self.base, self.count) } - } -} - -impl Drop for OwnedResponses { - fn drop(&mut self) { - // SAFETY: We allocated this ourselves, or it was provided to us by PAM. - unsafe { - for resp in self.iter_mut() { - libc::free(resp.data) - } - libc::free(self.base.cast()) - } - } -} - -#[cfg(test)] -mod tests { - use super::{BinaryData, OwnedResponses, RawBinaryResponse, RawTextResponse, Response}; - - #[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() { - let mut responses = OwnedResponses::alloc(2); - let text = RawTextResponse::fill(&mut responses[0], "hello").unwrap(); - let data = text.contents().expect("valid"); - assert_eq!("hello", data); - text.free_contents(); - text.free_contents(); - RawTextResponse::fill(&mut responses[1], "hell\0").expect_err("should error; contains nul"); - } - - #[test] - fn test_binary_response() { - let mut responses = OwnedResponses::alloc(1); - 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.data(); - assert_eq!(&real_data, data); - assert_eq!(7, resp.data_type()); - resp.free_contents(); - resp.free_contents(); - } - - #[test] - #[ignore] - fn test_binary_response_too_big() { - let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000]; - let mut responses = OwnedResponses::alloc(1); - RawBinaryResponse::fill(&mut responses[0], &big_data, 0).expect_err("this is too big!"); - } -}