Mercurial > crates > nonstick
view 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 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; 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: 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")] 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, 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!"); } }