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