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!");
+    }
+}