# HG changeset patch # User Paul Fisher # Date 1749002098 14400 # Node ID 58f9d2a4df3889a5c34a5d9f83e34ef0926e3e66 # Parent 9f8381a1c09c5e398708c126224d455f571b1948 Reorganize everything again??? - Splits ffi/memory stuff into a bunch of stuff in the pam_ffi module. - Builds infrastructure for passing Messages and Responses. - Adds tests for some things at least. diff -r 9f8381a1c09c -r 58f9d2a4df38 src/constants.rs --- a/src/constants.rs Tue Jun 03 01:21:59 2025 -0400 +++ b/src/constants.rs Tue Jun 03 21:54:58 2025 -0400 @@ -6,6 +6,7 @@ use num_traits::FromPrimitive; use std::any; use std::marker::PhantomData; +use std::result::Result as StdResult; bitflags! { /// The available PAM flags. @@ -137,7 +138,7 @@ } /// A PAM-specific Result type with an [ErrorCode] error. -pub type Result = std::result::Result; +pub type Result = StdResult; impl ErrorCode { /// Converts this [Result] into a C-compatible result code. @@ -161,7 +162,7 @@ impl TryFrom for ErrorCode { type Error = InvalidEnum; - fn try_from(value: c_int) -> std::result::Result { + fn try_from(value: c_int) -> StdResult { Self::from_i32(value).ok_or(value.into()) } } @@ -193,18 +194,6 @@ /// Returned when text that should not have any `\0` bytes in it does. /// Analogous to [`std::ffi::NulError`], but the data it was created from /// is borrowed. -#[derive(Debug, thiserror::Error)] -#[error("null byte within input at byte {0}")] -pub struct NulError(pub usize); - -/// Returned when trying to fit too much data into a binary message. -#[derive(Debug, thiserror::Error)] -#[error("cannot create a message of {actual} bytes; maximum is {max}")] -pub struct TooBigError { - pub actual: usize, - pub max: usize, -} - #[cfg(test)] mod tests { use super::*; diff -r 9f8381a1c09c -r 58f9d2a4df38 src/conv.rs --- a/src/conv.rs Tue Jun 03 01:21:59 2025 -0400 +++ b/src/conv.rs Tue Jun 03 21:54:58 2025 -0400 @@ -3,13 +3,9 @@ // Temporarily allowed until we get the actual conversation functions hooked up. #![allow(dead_code)] -use crate::constants::{NulError, Result, TooBigError}; -use crate::pam_ffi::{BinaryResponseInner, GenericResponse, TextResponseInner}; +use crate::constants::Result; +use crate::pam_ffi::Message; use secure_string::SecureString; -use std::mem; -use std::result::Result as StdResult; -use std::str::Utf8Error; - // TODO: In most cases, we should be passing around references to strings // or binary data. Right now we don't because that turns type inference and // trait definitions/implementations into a HUGE MESS. @@ -18,47 +14,6 @@ // associated types in the various Conversation traits to avoid copying // when unnecessary. -/// The types of message and request that can be sent to a user. -/// -/// The data within each enum value is the prompt (or other information) -/// that will be presented to the user. -#[derive(Debug)] -pub enum Message<'a> { - /// Requests information from the user; will be masked when typing. - /// - /// Response: [`Response::MaskedText`] - MaskedPrompt(&'a str), - /// Requests information from the user; will not be masked. - /// - /// Response: [`Response::Text`] - Prompt(&'a str), - /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). - /// - /// Response: [`Response::Text`] - /// (Linux-PAM documentation doesn't define its contents.) - RadioPrompt(&'a str), - /// Raises an error message to the user. - /// - /// Response: [`Response::NoResponse`] - Error(&'a str), - /// Sends an informational message to the user. - /// - /// Response: [`Response::NoResponse`] - Info(&'a str), - /// Requests binary data from the client (a Linux-PAM extension). - /// - /// This is used for non-human or non-keyboard prompts (security key?). - /// NOT part of the X/Open PAM specification. - /// - /// Response: [`Response::Binary`] - BinaryPrompt { - /// Some binary data. - data: &'a [u8], - /// A "type" that you can use for signalling. Has no strict definition in PAM. - data_type: u8, - }, -} - /// The responses that PAM will return from a request. #[derive(Debug, PartialEq, derive_more::From)] pub enum Response { @@ -103,9 +58,6 @@ /// /// The returned Vec of messages always contains exactly as many entries /// as there were messages in the request; one corresponding to each. - /// - /// Messages with no response (e.g. [info](Message::Info) and - /// [error](Message::Error)) will have a `None` entry instead of a `Response`. fn send(&mut self, messages: &[Message]) -> Result>; } @@ -152,84 +104,6 @@ } } -/// An owned text response to a PAM conversation. -/// -/// It points to a value on the C heap. -#[repr(C)] -struct TextResponse(*mut TextResponseInner); - -impl TextResponse { - /// Allocates a new response with the given text. - /// - /// A copy of the provided text will be allocated on the C heap. - pub fn new(text: impl AsRef) -> StdResult { - TextResponseInner::alloc(text).map(Self) - } - - /// Converts this into a GenericResponse. - fn generic(self) -> *mut GenericResponse { - let ret = self.0 as *mut GenericResponse; - mem::forget(self); - ret - } - - /// Gets the string data, if possible. - pub fn as_str(&self) -> StdResult<&str, Utf8Error> { - // SAFETY: We allocated this ourselves or got it back from PAM. - unsafe { &*self.0 }.contents().to_str() - } -} - -impl Drop for TextResponse { - /// Frees an owned response. - fn drop(&mut self) { - // SAFETY: We allocated this ourselves, or it was provided by PAM. - unsafe { TextResponseInner::free(self.0) } - } -} - -/// An owned binary response to a PAM conversation. -/// -/// It points to a value on the C heap. -#[repr(C)] -pub struct BinaryResponse(pub(super) *mut BinaryResponseInner); - -impl BinaryResponse { - /// Creates a binary response with the given data. - /// - /// A copy of the data will be made and allocated on the C heap. - pub fn new(data: &[u8], data_type: u8) -> StdResult { - BinaryResponseInner::alloc(data, data_type).map(Self) - } - - /// Converts this into a GenericResponse. - fn generic(self) -> *mut GenericResponse { - let ret = self.0 as *mut GenericResponse; - mem::forget(self); - ret - } - - /// The data type we point to. - pub fn data_type(&self) -> u8 { - // SAFETY: We allocated this ourselves or got it back from PAM. - unsafe { &*self.0 }.data_type() - } - - /// The data we point to. - pub fn data(&self) -> &[u8] { - // SAFETY: We allocated this ourselves or got it back from PAM. - unsafe { &*self.0 }.contents() - } -} - -impl Drop for BinaryResponse { - /// Frees an owned response. - fn drop(&mut self) { - // SAFETY: We allocated this ourselves, or it was provided by PAM. - unsafe { BinaryResponseInner::free(self.0) } - } -} - /// Owned binary data. #[derive(Debug, PartialEq)] pub struct BinaryData { @@ -249,16 +123,6 @@ } } -impl From for BinaryData { - /// Copies the data onto the Rust heap. - fn from(value: BinaryResponse) -> Self { - Self { - data: value.data().to_vec(), - data_type: value.data_type(), - } - } -} - impl From for Vec { /// Extracts the inner vector from the BinaryData. fn from(value: BinaryData) -> Self { @@ -267,13 +131,9 @@ } #[cfg(test)] -mod test { - use super::{ - BinaryResponse, Conversation, DemuxedConversation, Message, Response, SecureString, - TextResponse, - }; +mod tests { + use super::{Conversation, DemuxedConversation, Message, Response, SecureString}; use crate::constants::ErrorCode; - use crate::pam_ffi::GenericResponse; #[test] fn test_demux() { @@ -362,33 +222,4 @@ .unwrap() ); } - - // The below tests are used in conjunction with ASAN to verify - // that we correctly clean up all our memory. - - #[test] - fn test_text_response() { - let resp = TextResponse::new("it's a-me!").unwrap(); - assert_eq!("it's a-me!", resp.as_str().unwrap()); - } - - #[test] - fn test_binary_response() { - let data = [123, 210, 55]; - let resp = BinaryResponse::new(&data, 99).unwrap(); - assert_eq!(data, resp.data()); - assert_eq!(99, resp.data_type()); - } - - #[test] - fn test_to_generic() { - let text = TextResponse::new("oh no").unwrap(); - let text = text.generic(); - let binary = BinaryResponse::new(&[], 33).unwrap(); - let binary = binary.generic(); - unsafe { - GenericResponse::free(text); - GenericResponse::free(binary); - } - } } diff -r 9f8381a1c09c -r 58f9d2a4df38 src/handle.rs --- a/src/handle.rs Tue Jun 03 01:21:59 2025 -0400 +++ b/src/handle.rs Tue Jun 03 21:54:58 2025 -0400 @@ -1,11 +1,11 @@ //! The wrapper types and traits for handles into the PAM library. use crate::constants::{ErrorCode, Result}; use crate::items::{Item, ItemType}; -use crate::{memory, pam_ffi}; -use libc::c_char; +use crate::pam_ffi; +use crate::pam_ffi::memory; use secure_string::SecureString; -use std::ffi::{c_int, CString}; -use std::mem; +use std::ffi::{c_char, c_int, c_void, CString}; +use std::{mem, ptr}; /// Features of a PAM handle that are available to applications and modules. /// @@ -178,17 +178,6 @@ #[repr(C)] pub struct LibPamHandle(pam_ffi::Handle); -impl LibPamHandle { - /// Converts a pointer passed from PAM into a borrowed handle. - /// - /// # Safety - /// - /// It is your responsibility to provide a valid pointer. - pub unsafe fn from_ptr<'a>(ptr: *mut libc::c_void) -> &'a mut LibPamHandle { - &mut *(ptr as *mut LibPamHandle) - } -} - impl Drop for LibPamHandle { /// Ends the PAM session with a zero error code. /// You probably want to call [`close`](Self::close) instead of @@ -203,17 +192,17 @@ impl PamHandle for LibPamHandle { fn get_user(&mut self, prompt: Option<&str>) -> crate::Result { let prompt = memory::option_cstr(prompt)?; - let mut output: *const c_char = std::ptr::null_mut(); + let mut output: *const c_char = ptr::null_mut(); let ret = unsafe { pam_ffi::pam_get_user(&self.0, &mut output, memory::prompt_ptr(prompt.as_ref())) }; ErrorCode::result_from(ret)?; - memory::copy_pam_string(output) + unsafe {memory::copy_pam_string(output)} } fn get_authtok(&mut self, prompt: Option<&str>) -> crate::Result { let prompt = memory::option_cstr(prompt)?; - let mut output: *const c_char = std::ptr::null_mut(); + let mut output: *const c_char = ptr::null_mut(); let res = unsafe { pam_ffi::pam_get_authtok( &self.0, @@ -223,24 +212,20 @@ ) }; ErrorCode::result_from(res)?; - memory::copy_pam_string(output).map(SecureString::from) + unsafe {memory::copy_pam_string(output)}.map(SecureString::from) } - fn get_item(&mut self) -> crate::Result> { - let mut ptr: *const libc::c_void = std::ptr::null(); + fn get_item(&mut self) -> Result> { + let mut ptr: *const c_void = ptr::null(); let out = unsafe { let ret = pam_ffi::pam_get_item(&self.0, T::type_id().into(), &mut ptr); ErrorCode::result_from(ret)?; - let typed_ptr: *const T::Raw = ptr.cast(); - match typed_ptr.is_null() { - true => None, - false => Some(T::from_raw(typed_ptr)), - } + (ptr as *const T::Raw).as_ref().map(|p| T::from_raw(p)) }; Ok(out) } - fn set_item(&mut self, item: T) -> crate::Result<()> { + fn set_item(&mut self, item: T) -> Result<()> { let ret = unsafe { pam_ffi::pam_set_item(&mut self.0, T::type_id().into(), item.into_raw().cast()) }; @@ -261,15 +246,9 @@ impl PamModuleHandle for LibPamHandle { unsafe fn get_data(&mut self, key: &str) -> crate::Result> { let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?; - let mut ptr: *const libc::c_void = std::ptr::null(); + let mut ptr: *const c_void = ptr::null(); ErrorCode::result_from(pam_ffi::pam_get_data(&self.0, c_key.as_ptr(), &mut ptr))?; - match ptr.is_null() { - true => Ok(None), - false => { - let typed_ptr = ptr.cast(); - Ok(Some(&*typed_ptr)) - } - } + Ok((ptr as *const T).as_ref()) } fn set_data(&mut self, key: &str, data: Box) -> crate::Result<()> { diff -r 9f8381a1c09c -r 58f9d2a4df38 src/lib.rs --- a/src/lib.rs Tue Jun 03 01:21:59 2025 -0400 +++ b/src/lib.rs Tue Jun 03 21:54:58 2025 -0400 @@ -28,7 +28,6 @@ pub mod module; mod handle; -mod memory; mod pam_ffi; #[doc(inline)] diff -r 9f8381a1c09c -r 58f9d2a4df38 src/memory.rs --- a/src/memory.rs Tue Jun 03 01:21:59 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -//! Utility functions for dealing with memory copying and stuff. - -use crate::constants::{ErrorCode, Result}; -use libc::c_char; -use std::ffi::{CStr, CString}; - -/// 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 => std::ptr::null(), - } -} - -/// Creates an owned copy of a string that is returned from a -/// pam_get_whatever function. -pub fn copy_pam_string(result_ptr: *const 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) -} diff -r 9f8381a1c09c -r 58f9d2a4df38 src/module.rs --- a/src/module.rs Tue Jun 03 01:21:59 2025 -0400 +++ b/src/module.rs Tue Jun 03 21:54:58 2025 -0400 @@ -5,8 +5,9 @@ use crate::constants::{ErrorCode, Flags, Result}; use crate::conv::BinaryData; -use crate::conv::{Conversation, Message, Response}; +use crate::conv::{Conversation, Response}; use crate::handle::PamModuleHandle; +use crate::pam_ffi::Message; use secure_string::SecureString; use std::ffi::CStr; @@ -362,12 +363,12 @@ argc: c_int, argv: *const *const c_char, ) -> c_int { - let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::account_management( - unsafe { LibPamHandle::from_ptr(pamh) }, - args, - flags, - )) + if let Some(handle) = unsafe { pamh.cast::().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] @@ -377,12 +378,12 @@ argc: c_int, argv: *const *const c_char, ) -> c_int { - let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::authenticate( - unsafe { LibPamHandle::from_ptr(pamh) }, - args, - flags, - )) + if let Some(handle) = unsafe { pamh.cast::().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] @@ -392,12 +393,12 @@ argc: c_int, argv: *const *const c_char, ) -> c_int { - let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::change_authtok( - unsafe { LibPamHandle::from_ptr(pamh) }, - args, - flags, - )) + if let Some(handle) = unsafe { pamh.cast::().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] @@ -407,12 +408,12 @@ argc: c_int, argv: *const *const c_char, ) -> c_int { - let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::close_session( - unsafe { LibPamHandle::from_ptr(pamh) }, - args, - flags, - )) + if let Some(handle) = unsafe { pamh.cast::().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] @@ -423,11 +424,11 @@ argv: *const *const c_char, ) -> c_int { let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::open_session( - unsafe { LibPamHandle::from_ptr(pamh) }, - args, - flags, - )) + if let Some(handle) = unsafe { pamh.cast::().as_mut() } { + ErrorCode::result_to_c(super::$ident::open_session(handle, args, flags)) + } else { + ErrorCode::Ignore as c_int + } } #[no_mangle] @@ -438,11 +439,11 @@ argv: *const *const c_char, ) -> c_int { let args = extract_argv(argc, argv); - ErrorCode::result_to_c(super::$ident::set_credentials( - unsafe { LibPamHandle::from_ptr(pamh) }, - args, - flags, - )) + if let Some(handle) = unsafe { pamh.cast::().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. @@ -460,7 +461,7 @@ } #[cfg(test)] -mod test { +mod tests { use super::{ Conversation, ConversationMux, ErrorCode, Message, Response, Result, SecureString, }; diff -r 9f8381a1c09c -r 58f9d2a4df38 src/pam_ffi.rs --- a/src/pam_ffi.rs Tue Jun 03 01:21:59 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,433 +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. - -// Temporarily allow dead code. -#![allow(dead_code)] - -use crate::constants::{InvalidEnum, NulError, TooBigError}; -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; -use std::ffi::{c_char, c_int, c_void, CStr}; -use std::marker::{PhantomData, PhantomPinned}; -use std::slice; - -/// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. -type Immovable = PhantomData<(*mut u8, PhantomPinned)>; - -/// An opaque pointer given to us by PAM. -#[repr(C)] -pub struct Handle { - _data: (), - _marker: Immovable, -} - -/// Styles of message that are shown to the user. -#[derive(Debug, PartialEq, FromPrimitive)] -#[non_exhaustive] // non-exhaustive because C might give us back anything! -pub enum MessageStyle { - /// 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 for MessageStyle { - type Error = InvalidEnum; - fn try_from(value: c_int) -> std::result::Result { - Self::from_i32(value).ok_or(value.into()) - } -} - -impl From for c_int { - fn from(val: MessageStyle) -> 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 Message { - /// The style of message to request. - style: c_int, - /// A description of the data requested. - /// - /// For most requests, this will be an owned [`CStr`], but for requests - /// with [`MessageStyle::BinaryPrompt`], this will be [`BinaryData`] - /// (a Linux-PAM extension). - data: *const c_void, -} - -#[repr(C)] -pub struct TextResponseInner { - data: *mut c_char, - _unused: c_int, -} - -impl TextResponseInner { - /// Allocates a new text response on the C heap. - /// - /// Both `self` and its internal pointer are located on the C heap. - /// You are responsible for calling [`free`](Self::free) - /// on the pointer you get back when you're done with it. - pub fn alloc(text: impl AsRef) -> Result<*mut Self, NulError> { - let str_data = Self::malloc_str(text)?; - let inner = GenericResponse::alloc(str_data); - Ok(inner as *mut Self) - } - - /// Gets the string stored in this response. - pub fn contents(&self) -> &CStr { - // 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.data) } - } - - /// Releases memory owned by this response. - /// - /// # Safety - /// - /// You are responsible for no longer using this after calling free. - pub unsafe fn free(me: *mut Self) { - if !me.is_null() { - let data = (*me).data; - if !data.is_null() { - libc::memset(data as *mut c_void, 0, libc::strlen(data)); - } - libc::free(data as *mut c_void); - } - libc::free(me as *mut c_void); - } - - /// Allocates a string with the given contents on the C heap. - /// - /// 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. - fn malloc_str(text: impl AsRef) -> Result<*mut c_void, NulError> { - let data = text.as_ref().as_bytes(); - if let Some(nul) = data.iter().position(|x| *x == 0) { - return Err(NulError(nul)); - } - unsafe { - let data_alloc = libc::calloc(data.len() + 1, 1); - libc::memcpy(data_alloc, data.as_ptr() as *const c_void, data.len()); - Ok(data_alloc) - } - } -} - -/// A [`GenericResponse`] with [`BinaryData`] in it. -#[repr(C)] -pub struct BinaryResponseInner { - data: *mut BinaryData, - _unused: c_int, -} - -impl BinaryResponseInner { - /// Allocates a new binary response on the C heap. - /// - /// 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) - /// on the pointer you get back when you're done with it. - pub fn alloc(data: &[u8], data_type: u8) -> Result<*mut Self, TooBigError> { - let bin_data = BinaryData::alloc(data, data_type)?; - let inner = GenericResponse::alloc(bin_data as *mut c_void); - Ok(inner as *mut Self) - } - - /// Gets the binary data in this response. - pub fn contents(&self) -> &[u8] { - self.data().contents() - } - - /// Gets the `data_type` tag that was embedded with the message. - pub fn data_type(&self) -> u8 { - self.data().data_type - } - - #[inline] - fn data(&self) -> &BinaryData { - // 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.data) } - } - - /// Releases memory owned by this response. - /// - /// # Safety - /// - /// You are responsible for not using this after calling free. - pub unsafe fn free(me: *mut Self) { - if !me.is_null() { - BinaryData::free((*me).data); - } - libc::free(me as *mut c_void) - } -} - -/// 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)] -struct BinaryData { - /// 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 BinaryData { - /// Copies the given data to a new BinaryData on the heap. - fn alloc(source: &[u8], data_type: u8) -> Result<*mut BinaryData, TooBigError> { - let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError { - max: (u32::MAX - 5) as usize, - actual: source.len(), - })?; - let data = unsafe { - let dest_buffer = libc::malloc(buffer_size as usize) as *mut BinaryData; - let data = &mut *dest_buffer; - data.total_length = buffer_size.to_be_bytes(); - data.data_type = data_type; - let dest = data.data.as_mut_ptr(); - libc::memcpy( - dest as *mut c_void, - source.as_ptr() as *const c_void, - source.len(), - ); - dest_buffer - }; - Ok(data) - } - - fn length(&self) -> usize { - u32::from_be_bytes(self.total_length).saturating_sub(5) as usize - } - - fn contents(&self) -> &[u8] { - unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) } - } - - /// Clears this data and frees it. - fn free(me: *mut Self) { - if me.is_null() { - return; - } - unsafe { - let me_too = &mut *me; - let contents = slice::from_raw_parts_mut(me_too.data.as_mut_ptr(), me_too.length()); - for v in contents { - *v = 0 - } - me_too.data_type = 0; - me_too.total_length = [0; 4]; - libc::free(me as *mut c_void); - } - } -} - -/// Generic version of response data. -/// -/// This has the same structure as [`BinaryResponseInner`] -/// and [`TextResponseInner`]. -#[repr(C)] -pub struct GenericResponse { - /// Pointer to the data returned in a response. - /// For most responses, this will be a [`CStr`], but for responses to - /// [`MessageStyle::BinaryPrompt`]s, this will be [`BinaryData`] - /// (a Linux-PAM extension). - data: *mut c_void, - /// Unused. - return_code: c_int, -} - -impl GenericResponse { - /// Allocates a response on the C heap pointing to the given data. - fn alloc(data: *mut c_void) -> *mut Self { - unsafe { - let alloc = libc::calloc(1, size_of::()) as *mut Self; - (*alloc).data = data; - alloc - } - } - - /// Frees a response on the C heap. - /// - /// # Safety - /// - /// It's on you to stop using this GenericResponse after freeing it. - pub unsafe fn free(me: *mut GenericResponse) { - if !me.is_null() { - libc::free((*me).data); - } - libc::free(me as *mut c_void); - } -} - -/// An opaque pointer we provide to PAM for callbacks. -#[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 some [`Message`]s (see note). -/// - `responses` is a pointer to an array of [`GenericResponse`]s, -/// which PAM sets in response to a module's request. -/// This is an array of structs, not an array of pointers to a struct. -/// There should always be exactly as many `responses` as `num_msg`. -/// - `appdata` is the `appdata` field of the [`Conversation`] we were passed. -/// -/// NOTE: On Linux-PAM and other compatible implementations, `messages` -/// is treated as a pointer-to-pointers, like `int argc, char **argv`. -/// -/// ```text -/// ┌──────────┐ points to ┌─────────────┐ ╔═ Message ═╗ -/// │ messages │ ┄┄┄┄┄┄┄┄┄┄> │ messages[0] │ ┄┄┄┄> ║ style ║ -/// └──────────┘ │ messages[1] │ ┄┄╮ ║ data ║ -/// │ ... │ ┆ ╚═══════════╝ -/// ┆ -/// ┆ ╔═ Message ═╗ -/// ╰┄┄> ║ style ║ -/// ║ data ║ -/// ╚═══════════╝ -/// ``` -/// -/// On OpenPAM and other compatible implementations (like Solaris), -/// `messages` is a pointer-to-pointer-to-array. -/// -/// ```text -/// ┌──────────┐ points to ┌───────────┐ ╔═ Message[] ═╗ -/// │ messages │ ┄┄┄┄┄┄┄┄┄┄> │ *messages │ ┄┄┄┄> ║ style ║ -/// └──────────┘ └───────────┘ ║ data ║ -/// ╟─────────────╢ -/// ║ style ║ -/// ║ data ║ -/// ╟─────────────╢ -/// ║ ... ║ -/// ``` -/// -/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** -pub type ConversationCallback = extern "C" fn( - num_msg: c_int, - messages: *const *const Message, - responses: &mut *const GenericResponse, - appdata: *const AppData, -) -> c_int; - -/// A callback and the associated [`AppData`] pointer that needs to be passed back to it. -#[repr(C)] -pub struct Conversation { - callback: ConversationCallback, - appdata: *const AppData, -} - -#[link(name = "pam")] -extern "C" { - pub fn pam_get_data( - pamh: *const Handle, - module_data_name: *const c_char, - data: &mut *const c_void, - ) -> c_int; - - pub fn pam_set_data( - pamh: *mut Handle, - module_data_name: *const c_char, - data: *const c_void, - cleanup: extern "C" fn(pamh: *const c_void, data: *mut c_void, error_status: c_int), - ) -> c_int; - - pub fn pam_get_item(pamh: *const Handle, item_type: c_int, item: &mut *const c_void) -> c_int; - - pub fn pam_set_item(pamh: *mut Handle, item_type: c_int, item: *const c_void) -> c_int; - - pub fn pam_get_user( - pamh: *const Handle, - user: &mut *const c_char, - prompt: *const c_char, - ) -> c_int; - - pub fn pam_get_authtok( - pamh: *const Handle, - item_type: c_int, - data: &mut *const c_char, - prompt: *const c_char, - ) -> c_int; - - pub fn pam_end(pamh: *mut Handle, status: c_int) -> c_int; -} - -#[cfg(test)] -mod test { - use super::{BinaryResponseInner, GenericResponse, TextResponseInner}; - - #[test] - fn test_text_response() { - let resp = TextResponseInner::alloc("hello").expect("alloc should succeed"); - let borrow_resp = unsafe { &*resp }; - let data = borrow_resp.contents().to_str().expect("valid"); - assert_eq!("hello", data); - unsafe { - TextResponseInner::free(resp); - } - TextResponseInner::alloc("hell\0o").expect_err("should error; contains nul"); - } - - #[test] - fn test_binary_response() { - let real_data = [1, 2, 3, 4, 5, 6, 7, 8]; - let resp = BinaryResponseInner::alloc(&real_data, 7).expect("alloc should succeed"); - let borrow_resp = unsafe { &*resp }; - let data = borrow_resp.contents(); - assert_eq!(&real_data, data); - assert_eq!(7, borrow_resp.data_type()); - unsafe { BinaryResponseInner::free(resp) }; - } - - #[test] - fn test_free_safety() { - unsafe { - TextResponseInner::free(std::ptr::null_mut()); - BinaryResponseInner::free(std::ptr::null_mut()); - } - } - - #[test] - #[ignore] - fn test_binary_response_too_big() { - let big_data: Vec = vec![0xFFu8; 10_000_000_000]; - BinaryResponseInner::alloc(&big_data, 0).expect_err("this is too big!"); - } -} diff -r 9f8381a1c09c -r 58f9d2a4df38 src/pam_ffi/memory.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pam_ffi/memory.rs Tue Jun 03 21:54:58 2025 -0400 @@ -0,0 +1,187 @@ +//! 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`]. +pub type Immovable = 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. +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) +} + +/// 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() as *const c_void, 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 as *const c_char)); + } +} + +/// 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(), + })?; + let data = unsafe { + let dest_buffer = libc::malloc(buffer_size as usize) as *mut CBinaryData; + let data = &mut *dest_buffer; + data.total_length = buffer_size.to_be_bytes(); + data.data_type = data_type; + let dest = data.data.as_mut_ptr(); + libc::memcpy( + dest as *mut c_void, + source.as_ptr() as *const c_void, + source.len(), + ); + 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 std::ffi::CString; + use crate::ErrorCode; + use super::{copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string}; + #[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 9f8381a1c09c -r 58f9d2a4df38 src/pam_ffi/message.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pam_ffi/message.rs Tue Jun 03 21:54:58 2025 -0400 @@ -0,0 +1,292 @@ +//! Data and types dealing with PAM messages. + +use crate::constants::InvalidEnum; +use crate::pam_ffi::memory; +use crate::pam_ffi::memory::{CBinaryData, NulError, TooBigError}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use std::ffi::{c_char, c_int, c_void, CStr}; +use std::result::Result as StdResult; +use std::slice; +use std::str::Utf8Error; + +/// The types of message and request that can be sent to a user. +/// +/// The data within each enum value is the prompt (or other information) +/// that will be presented to the user. +#[derive(Debug)] +pub enum Message<'a> { + /// Requests information from the user; will be masked when typing. + /// + /// Response: [`Response::MaskedText`] + MaskedPrompt(&'a str), + /// Requests information from the user; will not be masked. + /// + /// Response: [`Response::Text`] + Prompt(&'a str), + /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). + /// + /// Response: [`Response::Text`] + /// (Linux-PAM documentation doesn't define its contents.) + RadioPrompt(&'a str), + /// Raises an error message to the user. + /// + /// Response: [`Response::NoResponse`] + Error(&'a str), + /// Sends an informational message to the user. + /// + /// Response: [`Response::NoResponse`] + Info(&'a str), + /// Requests binary data from the client (a Linux-PAM extension). + /// + /// This is used for non-human or non-keyboard prompts (security key?). + /// NOT part of the X/Open PAM specification. + /// + /// Response: [`Response::Binary`] + BinaryPrompt { + /// Some binary data. + data: &'a [u8], + /// A "type" that you can use for signalling. Has no strict definition in PAM. + data_type: u8, + }, +} + +impl Message<'_> { + /// Copies the contents of this message to the C heap. + fn copy_to_heap(&self) -> StdResult<(Style, *mut c_void), ConversionError> { + let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); + match *self { + Self::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text), + Self::Prompt(text) => alloc(Style::PromptEchoOn, text), + Self::RadioPrompt(text) => alloc(Style::RadioType, text), + Self::Error(text) => alloc(Style::ErrorMsg, text), + Self::Info(text) => alloc(Style::TextInfo, text), + Self::BinaryPrompt { data, data_type } => Ok(( + Style::BinaryPrompt, + (CBinaryData::alloc(data, data_type)?).cast(), + )), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("error creating PAM message: {0}")] +enum ConversionError { + InvalidEnum(#[from] InvalidEnum