Mercurial > crates > nonstick
view src/pam_ffi.rs @ 70:9f8381a1c09c
Implement low-level conversation primitives.
This change does two primary things:
1. Introduces new Conversation traits, to be implemented both
by the library and by PAM client applications.
2. Builds the memory-management infrastructure for passing messages
through the conversation.
...and it adds tests for both of the above, including ASAN tests.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 03 Jun 2025 01:21:59 -0400 |
parents | 8f3ae0c7ab92 |
children |
line wrap: on
line source
//! 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!"); } }