# HG changeset patch # User Paul Fisher # Date 1749263708 14400 # Node ID c30811b4afae4e468564a563262ade8a3e114b4f # Parent c7c596e6388f40ad8a049bfc4b6cefcdeccf0c4b rename pam_ffi submodule to libpam. diff -r c7c596e6388f -r c30811b4afae src/handle.rs --- 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 { diff -r c7c596e6388f -r c30811b4afae src/lib.rs --- 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}, diff -r c7c596e6388f -r c30811b4afae src/libpam/conversation.rs --- /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(conv: &mut C) -> Self { + Self { + callback: Self::wrapper_callback::, + appdata: (conv as *mut C).cast(), + life: PhantomData, + _marker: Immovable(PhantomData), + } + } + + unsafe extern "C" fn wrapper_callback( + count: c_int, + messages: *const MessageIndirector, + responses: *mut *mut RawResponse, + me: *mut AppData, + ) -> c_int { + let call = || { + let conv = me + .cast::() + .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 = indir + .iter(count as usize) + .map(Message::try_from) + .collect::>() + .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> { + 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> { + 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 { + 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 + } + }) +} diff -r c7c596e6388f -r c30811b4afae src/libpam/handle.rs --- /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> { + 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> { + 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> { + 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(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) { + unsafe { + let _data: Box = 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 for ItemType { + type Error = InvalidEnum; + fn try_from(value: c_int) -> StdResult { + Self::from_i32(value).ok_or(value.into()) + } +} + +impl From for c_int { + fn from(val: ItemType) -> Self { + val as Self + } +} diff -r c7c596e6388f -r c30811b4afae src/libpam/memory.rs --- /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> { + 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 +/// pam_get_whatever function. +/// +/// # Safety +/// +/// It's on you to provide a valid string. +pub unsafe fn copy_pam_string(result_ptr: *const libc::c_char) -> Result { + // 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> { + 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) -> 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()); + } +} diff -r c7c596e6388f -r c30811b4afae src/libpam/message.rs --- /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