Mercurial > crates > nonstick
diff src/pam_ffi.rs @ 69:8f3ae0c7ab92
Rework conversation data types and make safe wrappers.
This removes the old `Conversation` type and reworks the FFI types
used for PAM conversations.
This creates safe `TestResponse` and `BinaryResponse` structures in `conv`,
providing a safe way to pass response messages to PAM Conversations.
The internals of these types are allocated on the C heap, as required by PAM.
We also remove the Conversation struct, which was specific to the real PAM
implementation so that we can introduce a better abstraction.
Also splits a new `PamApplicationHandle` trait from `PamHandle`,
for the parts of a PAM handle that are specific to the application side
of a PAM transaction.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Sun, 01 Jun 2025 01:15:04 -0400 |
parents | a674799a5cd3 |
children | 9f8381a1c09c |
line wrap: on
line diff
--- a/src/pam_ffi.rs Tue May 27 16:40:49 2025 -0400 +++ b/src/pam_ffi.rs Sun Jun 01 01:15:04 2025 -0400 @@ -1,14 +1,282 @@ -//! FFI to the PAM library. +//! 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 libc::c_char; -use std::ffi::c_int; +use crate::constants::InvalidEnum; +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::num::TryFromIntError; +use std::slice; +use thiserror::Error; + +/// 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: PhantomData<(*mut u8, PhantomPinned)>, + _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. Linux-PAM specific. + RadioType = 5, + /// For server–client non-human interaction. + /// NOT part of the X/Open PAM specification. + 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`]. + data: *const c_void, +} + +/// 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, Error)] +#[error("null byte within input at byte {0}")] +pub struct NulError(usize); + +#[repr(transparent)] +pub struct TextResponseInner(ResponseInner); + +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 = ResponseInner::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.0.data as *const c_char) } + } + + /// 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) { + ResponseInner::free(me as *mut ResponseInner) + } + + /// 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<*const 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 as *const c_void) + } + } +} + +/// A [`ResponseInner`] with [`BinaryData`] in it. +#[repr(transparent)] +pub struct BinaryResponseInner(ResponseInner); + +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: impl AsRef<[u8]>, data_type: u8) -> Result<*mut Self, TryFromIntError> { + let bin_data = BinaryData::alloc(data, data_type)?; + let inner = ResponseInner::alloc(bin_data as *const c_void); + Ok(inner as *mut Self) + } + + /// Gets the binary data in this response. + pub fn contents(&self) -> &[u8] { + let data = self.data(); + let length = (u32::from_be_bytes(data.total_length) - 5) as usize; + unsafe { slice::from_raw_parts(data.data.as_ptr(), length) } + } + + /// 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.0.data as *const BinaryData) } + } + + /// Releases memory owned by this response. + /// + /// # Safety + /// + /// You are responsible for not using this after calling free. + pub unsafe fn free(me: *mut Self) { + ResponseInner::free(me as *mut ResponseInner) + } +} + +#[repr(C)] +pub struct ResponseInner { + /// 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`] + data: *const c_void, + /// Unused. + return_code: c_int, +} + +impl ResponseInner { + /// Allocates a response on the C heap pointing to the given data. + fn alloc(data: *const c_void) -> *mut Self { + unsafe { + let alloc = libc::calloc(1, size_of::<ResponseInner>()) as *mut ResponseInner; + (*alloc).data = data; + alloc + } + } + + /// Frees one of these that was created with [`Self::alloc`] + /// (or provided by PAM). + /// + /// # Safety + /// + /// It's up to you to stop using `me` after calling this. + unsafe fn free(me: *mut Self) { + libc::free((*me).data as *mut c_void); + 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. +#[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 { + fn alloc( + source: impl AsRef<[u8]>, + data_type: u8, + ) -> Result<*const BinaryData, TryFromIntError> { + let source = source.as_ref(); + let buffer_size = u32::try_from(source.len() + 5)?; + 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) + } +} + +/// 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 an array of pointers to [`Message`]s. +/// - `responses` is a pointer to an array of [`ResponseInner`]s, +/// which PAM sets in response to a module's request. +/// - `appdata` is the `appdata` field of the [`Conversation`] we were passed. +pub type ConversationCallback = extern "C" fn( + num_msg: c_int, + messages: *const *const Message, + responses: &mut *const ResponseInner, + 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")] @@ -16,27 +284,19 @@ pub fn pam_get_data( pamh: *const Handle, module_data_name: *const c_char, - data: &mut *const libc::c_void, + data: &mut *const c_void, ) -> c_int; pub fn pam_set_data( pamh: *mut Handle, module_data_name: *const c_char, - data: *const libc::c_void, - cleanup: extern "C" fn( - pamh: *const libc::c_void, - data: *mut libc::c_void, - error_status: c_int, - ), + 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 libc::c_void, - ) -> 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 libc::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, @@ -53,3 +313,38 @@ pub fn pam_end(pamh: *mut Handle, status: c_int) -> c_int; } + +#[cfg(test)] +mod test { + use super::{BinaryResponseInner, 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] + #[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!"); + } +}