Mercurial > crates > nonstick
changeset 71:58f9d2a4df38
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.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 03 Jun 2025 21:54:58 -0400 |
parents | 9f8381a1c09c |
children | 47eb242a4f88 |
files | src/constants.rs src/conv.rs src/handle.rs src/lib.rs src/memory.rs src/module.rs src/pam_ffi.rs src/pam_ffi/memory.rs src/pam_ffi/message.rs src/pam_ffi/mod.rs src/pam_ffi/response.rs |
diffstat | 11 files changed, 862 insertions(+), 727 deletions(-) [+] |
line wrap: on
line diff
--- 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<T> = std::result::Result<T, ErrorCode>; +pub type Result<T> = StdResult<T, ErrorCode>; impl ErrorCode { /// Converts this [Result] into a C-compatible result code. @@ -161,7 +162,7 @@ impl TryFrom<c_int> for ErrorCode { type Error = InvalidEnum<Self>; - fn try_from(value: c_int) -> std::result::Result<Self, Self::Error> { + fn try_from(value: c_int) -> StdResult<Self, Self::Error> { 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::*;
--- 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<Vec<Response>>; } @@ -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<str>) -> StdResult<Self, NulError> { - 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<Self, TooBigError> { - BinaryResponseInner::alloc(data, data_type).map(Self) - } - - /// Converts this into a GenericResponse. - fn generic(self) -> *mut GenericResponse { - let ret = self.0 as *mut GenericResponse; - mem::forget(self); - ret - } - - /// 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<BinaryResponse> for BinaryData { - /// Copies the data onto the Rust heap. - fn from(value: BinaryResponse) -> Self { - Self { - data: value.data().to_vec(), - data_type: value.data_type(), - } - } -} - impl From<BinaryData> for Vec<u8> { /// Extracts the inner vector from the BinaryData. fn from(value: BinaryData) -> Self { @@ -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); - } - } }
--- 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<String> { 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<SecureString> { 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<T: Item>(&mut self) -> crate::Result<Option<T>> { - let mut ptr: *const libc::c_void = std::ptr::null(); + fn get_item<T: Item>(&mut self) -> Result<Option<T>> { + 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<T: Item>(&mut self, item: T) -> crate::Result<()> { + fn set_item<T: 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<T>(&mut self, key: &str) -> crate::Result<Option<&T>> { let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?; - let mut ptr: *const libc::c_void = std::ptr::null(); + 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<T>(&mut self, key: &str, data: Box<T>) -> crate::Result<()> {
--- 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)]
--- 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<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 => std::ptr::null(), - } -} - -/// Creates an owned copy of a string that is returned from a -/// <code>pam_get_<var>whatever</var></code> function. -pub fn copy_pam_string(result_ptr: *const 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) -}
--- 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::<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] @@ -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::<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] @@ -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::<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] @@ -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::<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] @@ -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::<LibPamHandle>().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::<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. @@ -460,7 +461,7 @@ } #[cfg(test)] -mod test { +mod tests { use super::{ Conversation, ConversationMux, ErrorCode, Message, Response, Result, SecureString, };
--- 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<c_int> for MessageStyle { - type Error = InvalidEnum<Self>; - fn try_from(value: c_int) -> std::result::Result<Self, Self::Error> { - Self::from_i32(value).ok_or(value.into()) - } -} - -impl From<MessageStyle> 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<str>) -> 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<str>) -> Result<*mut c_void, NulError> { - let data = text.as_ref().as_bytes(); - if let Some(nul) = data.iter().position(|x| *x == 0) { - return Err(NulError(nul)); - } - 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::<Self>()) as *mut Self; - (*alloc).data = data; - alloc - } - } - - /// Frees a response on the C heap. - /// - /// # Safety - /// - /// It's on you to stop using this GenericResponse after freeing it. - pub unsafe fn free(me: *mut GenericResponse) { - if !me.is_null() { - libc::free((*me).data); - } - libc::free(me as *mut c_void); - } -} - -/// An opaque pointer we provide to PAM for callbacks. -#[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<u8> = vec![0xFFu8; 10_000_000_000]; - BinaryResponseInner::alloc(&big_data, 0).expect_err("this is too big!"); - } -}
--- /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<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. +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) +} + +/// 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() 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()); + } +}
--- /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<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 [`BinaryData`] + /// (a Linux-PAM extension). + data: *mut c_void, +} + +impl RawMessage { + fn set(&mut self, msg: &Message) -> StdResult<(), ConversionError> { + let (style, data) = msg.copy_to_heap()?; + 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(()) + } + + /// Retrieves the data stored in this message. + fn data(&self) -> StdResult<Message, ConversionError> { + let style: Style = self.style.try_into()?; + // SAFETY: We either allocated this message ourselves or were provided it by PAM. + let result = unsafe { + match style { + Style::PromptEchoOff => Message::MaskedPrompt(self.string_data()?), + Style::PromptEchoOn => Message::Prompt(self.string_data()?), + Style::TextInfo => Message::Info(self.string_data()?), + Style::ErrorMsg => Message::Error(self.string_data()?), + Style::RadioType => Message::Error(self.string_data()?), + Style::BinaryPrompt => (self.data as *const CBinaryData).as_ref().map_or_else( + || Message::BinaryPrompt { + data_type: 0, + data: &[], + }, + |data| Message::BinaryPrompt { + data_type: data.data_type(), + data: data.contents(), + }, + ), + } + }; + Ok(result) + } + + /// Gets this message's data pointer as a string. + /// + /// # Safety + /// + /// 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 as *const c_char).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 as *mut CBinaryData).as_mut() { + d.zero_contents() + } + } + Style::TextInfo + | Style::RadioType + | Style::ErrorMsg + | Style::PromptEchoOff + | Style::PromptEchoOn => memory::zero_c_string(self.data), + } + }; + } + } +} + +/// 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.*** +#[repr(C)] +pub struct OwnedMessages { + /// An indirection to the messages themselves, stored on the C heap. + indirect: *mut Indirect<RawMessage>, + /// 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 { + // SAFETY: We're allocating. That's safe. + unsafe { + // Since this is Linux-PAM, the indirect is a list of pointers. + let indirect = + libc::calloc(count, size_of::<Indirect<RawMessage>>()) as *mut Indirect<RawMessage>; + let indir_ptrs = slice::from_raw_parts_mut(indirect, count); + for ptr in indir_ptrs { + ptr.base = libc::calloc(1, size_of::<RawMessage>()) as *mut RawMessage; + } + Self { indirect, count } + } + } + + /// Gets a reference to the message at the given index. + pub fn get(&self, index: usize) -> Option<&RawMessage> { + (index < self.count).then(|| unsafe { (*self.indirect).at(index) }) + } + + /// Gets a mutable reference to the message at the given index. + pub fn get_mut(&mut self, index: usize) -> Option<&mut RawMessage> { + (index < self.count).then(|| unsafe { (*self.indirect).at_mut(index) }) + } +} + +#[repr(transparent)] +struct Indirect<T> { + /// The starting address for the T. + base: *mut T, +} + +impl<T> Indirect<T> { + /// Gets a mutable reference to the element at the given index. + /// + /// # Safety + /// + /// We don't check `index`. + unsafe fn at_mut(&mut self, index: usize) -> &mut T { + &mut *self.base.add(index) + } + + unsafe fn at(&self, index: usize) -> &T { + &*self.base.add(index) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pam_ffi/mod.rs Tue Jun 03 21:54:58 2025 -0400 @@ -0,0 +1,94 @@ +//! 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)] + +pub mod memory; +mod message; +mod response; + +use crate::pam_ffi::memory::Immovable; +use crate::pam_ffi::message::OwnedMessages; +pub use message::Message; +pub use response::RawResponse; +use std::ffi::{c_char, c_int, c_void}; + +/// An opaque structure that a PAM handle points to. +#[repr(C)] +pub struct Handle { + _data: (), + _marker: Immovable, +} + +/// An opaque structure that is passed through PAM in a conversation. +#[repr(C)] +pub struct AppData { + _data: (), + _marker: Immovable, +} + +/// The callback that PAM uses to get information in a conversation. +/// +/// - `num_msg` is the number of messages in the `pam_message` array. +/// - `messages` is a pointer to the messages being sent to the user. +/// For details about its structure, see the documentation of +/// [`OwnedMessages`](super::OwnedMessages). +/// - `responses` is a pointer to an array of [`RawResponse`]s, +/// which PAM sets in response to a module's request. +/// This is an array of structs, not an array of pointers to a struct. +/// There should always be exactly as many `responses` as `num_msg`. +/// - `appdata` is the `appdata` field of the [`Conversation`] we were passed. +pub type ConversationCallback = extern "C" fn( + num_msg: c_int, + messages: &OwnedMessages, + responses: &mut *mut RawResponse, + 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; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pam_ffi/response.rs Tue Jun 03 21:54:58 2025 -0400 @@ -0,0 +1,231 @@ +//! Types used when dealing with PAM conversations. + +use crate::pam_ffi::memory; +use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError}; +use std::ffi::{c_char, c_int, c_void, CStr}; +use std::ops::{Deref, DerefMut}; +use std::result::Result as StdResult; +use std::str::Utf8Error; +use std::{mem, ptr, slice}; + +#[repr(transparent)] +#[derive(Debug)] +pub struct RawTextResponse(RawResponse); + +impl RawTextResponse { + /// 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_contents) + /// on the pointer you get back when you're done with it. + pub fn fill(dest: &mut RawResponse, text: impl AsRef<str>) -> StdResult<&mut Self, NulError> { + dest.data = memory::malloc_str(text)?.cast(); + Ok(unsafe { &mut *(dest as *mut RawResponse as *mut Self) }) + } + + /// Gets the string stored in this response. + pub fn contents(&self) -> StdResult<&str, Utf8Error> { + // SAFETY: This data is either passed from PAM (so we are forced to + // trust it) or was created by us in TextResponseInner::alloc. + // In either case, it's going to be a valid null-terminated string. + unsafe { CStr::from_ptr(self.0.data as *const c_char) }.to_str() + } + + /// Releases memory owned by this response. + /// + /// # Safety + /// + /// You are responsible for no longer using this after calling free. + pub unsafe fn free_contents(&mut self) { + let data = self.0.data; + memory::zero_c_string(data); + libc::free(data); + self.0.data = ptr::null_mut() + } +} + +/// A [`RawResponse`] with [`CBinaryData`] in it. +#[repr(transparent)] +#[derive(Debug)] +pub struct RawBinaryResponse(RawResponse); + +impl RawBinaryResponse { + /// 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_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)? as *mut c_void; + Ok(unsafe { + (dest as *mut RawResponse) + .cast::<RawBinaryResponse>() + .as_mut() + .unwrap() + }) + } + + /// 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) -> &CBinaryData { + // SAFETY: This was either something we got from PAM (in which case + // we trust it), or something that was created with + // BinaryResponseInner::alloc. In both cases, it points to valid data. + unsafe { &*(self.0.data as *const CBinaryData) } + } + + /// Releases memory owned by this response. + /// + /// # Safety + /// + /// You are responsible for not using this after calling free. + pub unsafe fn free_contents(&mut self) { + let data_ref = (self.0.data as *mut CBinaryData).as_mut(); + if let Some(d) = data_ref { + d.zero_contents() + } + 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)] +#[repr(C)] +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>()) } as *mut RawResponse, + count: count, + } + } + + /// Takes ownership of a list of responses allocated on the C heap. + /// + /// # Safety + /// + /// It's up to you to make sure you pass a valid pointer. + unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self { + OwnedResponses { base, count } + } +} + +impl From<OwnedResponses> for *mut RawResponse { + /// Converts this into a pointer to `RawResponse`. + /// + /// The backing data is no longer freed. + fn from(value: OwnedResponses) -> Self { + let ret = value.base; + mem::forget(value); + ret + } +} + +impl Deref for OwnedResponses { + type Target = [RawResponse]; + fn deref(&self) -> &Self::Target { + // SAFETY: We allocated this ourselves, or it was provided to us by PAM. + unsafe { slice::from_raw_parts(self.base, self.count) } + } +} + +impl DerefMut for OwnedResponses { + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: We allocated this ourselves, or it was provided to us by PAM. + 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 as *mut c_void) + } + } +} + +#[cfg(test)] +mod tests { + + use super::{OwnedResponses, RawBinaryResponse, RawTextResponse}; + + #[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); + unsafe { + 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.contents(); + assert_eq!(&real_data, data); + assert_eq!(7, resp.data_type()); + unsafe { + 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!"); + } +}