view src/pam_ffi/message.rs @ 73:ac6881304c78

Do conversations, along with way too much stuff. This implements conversations, along with all the memory management brouhaha that goes along with it. The conversation now lives directly on the handle rather than being a thing you have to get from it and then call manually. It Turns Out this makes things a lot easier! I guess we reorganized things again. For the last time. For real. I promise. This all passes ASAN, so it seems Pretty Good!
author Paul Fisher <paul@pfish.zone>
date Thu, 05 Jun 2025 03:41:38 -0400
parents 47eb242a4f88
children c7c596e6388f
line wrap: on
line source

//! Data and types dealing with PAM messages.

use crate::constants::InvalidEnum;
use crate::conv::Message;
use crate::pam_ffi::memory;
use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use std::ffi::{c_int, c_void, CStr};
use std::result::Result as StdResult;
use std::str::Utf8Error;
use std::{ptr, slice};

#[derive(Debug, thiserror::Error)]
#[error("error creating PAM message: {0}")]
pub enum ConversionError {
    InvalidEnum(#[from] InvalidEnum<Style>),
    Utf8Error(#[from] Utf8Error),
    NulError(#[from] NulError),
    TooBigError(#[from] TooBigError),
}

/// The C enum values for messages shown to the user.
#[derive(Debug, PartialEq, FromPrimitive)]
pub enum Style {
    /// 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 Style {
    type Error = InvalidEnum<Self>;
    fn try_from(value: c_int) -> StdResult<Self, Self::Error> {
        Self::from_i32(value).ok_or(value.into())
    }
}

impl From<Style> for c_int {
    fn from(val: Style) -> 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 RawMessage {
    /// 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 [`Style::BinaryPrompt`], this will be [`CBinaryData`]
    /// (a Linux-PAM extension).
    data: *mut c_void,
    _marker: Immovable,
}

impl RawMessage {
    pub fn set(&mut self, msg: Message) -> StdResult<(), ConversionError> {
        let (style, data) = copy_to_heap(msg)?;
        self.clear();
        // SAFETY: We allocated this ourselves or were given it by PAM.
        // Otherwise, it's null, but free(null) is fine.
        unsafe { libc::free(self.data) };
        self.style = style as c_int;
        self.data = data;
        Ok(())
    }

    /// Gets this message's data pointer as a string.
    ///
    /// # Safety
    ///
    /// It's up to you to pass this only on types with a string value.
    unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> {
        if self.data.is_null() {
            Ok("")
        } else {
            CStr::from_ptr(self.data.cast()).to_str()
        }
    }

    /// Zeroes out the data stored here.
    fn clear(&mut self) {
        // SAFETY: We either created this data or we got it from PAM.
        // After this function is done, it will be zeroed out.
        unsafe {
            if let Ok(style) = Style::try_from(self.style) {
                match style {
                    Style::BinaryPrompt => {
                        if let Some(d) = self.data.cast::<CBinaryData>().as_mut() {
                            d.zero_contents()
                        }
                    }
                    Style::TextInfo
                    | Style::RadioType
                    | Style::ErrorMsg
                    | Style::PromptEchoOff
                    | Style::PromptEchoOn => memory::zero_c_string(self.data),
                }
            };
            libc::free(self.data);
            self.data = ptr::null_mut();
        }
    }
}

/// Copies the contents of this message to the C heap.
fn copy_to_heap(msg: Message) -> StdResult<(Style, *mut c_void), ConversionError> {
    let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
    match msg {
        Message::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text),
        Message::Prompt(text) => alloc(Style::PromptEchoOn, text),
        Message::RadioPrompt(text) => alloc(Style::RadioType, text),
        Message::Error(text) => alloc(Style::ErrorMsg, text),
        Message::Info(text) => alloc(Style::TextInfo, text),
        Message::BinaryPrompt { data, data_type } => Ok((
            Style::BinaryPrompt,
            (CBinaryData::alloc(data, data_type)?).cast(),
        )),
    }
}

/// Abstraction of a list-of-messages to be sent in a PAM conversation.
///
/// On Linux-PAM and other compatible implementations, `messages`
/// is treated as a pointer-to-pointers, like `int argc, char **argv`.
/// (In this situation, the value of `OwnedMessages.indirect` is
/// the pointer passed to `pam_conv`.)
///
/// ```text
/// ╔═ OwnedMsgs ═╗  points to  ┌─ Indirect ─┐       ╔═ Message ═╗
/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style     ║
/// ║ count       ║             │ base[1] ┄┄┄┼┄┄┄╮   ║ data      ║
/// ╚═════════════╝             │ ...        │   ┆   ╚═══════════╝
///                                              ┆
///                                              ┆    ╔═ Message ═╗
///                                              ╰┄┄> ║ style     ║
///                                                   ║ data      ║
///                                                   ╚═══════════╝
/// ```
///
/// On OpenPAM and other compatible implementations (like Solaris),
/// `messages` is a pointer-to-pointer-to-array.  This appears to be
/// the correct implementation as required by the XSSO specification.
///
/// ```text
/// ╔═ OwnedMsgs ═╗  points to  ┌─ Indirect ─┐       ╔═ Message[] ═╗
/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style       ║
/// ║ count       ║             └────────────┘       ║ data        ║
/// ╚═════════════╝                                  ╟─────────────╢
///                                                  ║ style       ║
///                                                  ║ data        ║
///                                                  ╟─────────────╢
///                                                  ║ ...         ║
/// ```
///
/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
pub struct OwnedMessages {
    /// An indirection to the messages themselves, stored on the C heap.
    indirect: *mut MessageIndirector,
    /// The number of messages in the list.
    count: usize,
}

impl OwnedMessages {
    /// Allocates data to store messages on the C heap.
    pub fn alloc(count: usize) -> Self {
        Self {
            indirect: MessageIndirector::alloc(count),
            count,
        }
    }

    /// The pointer to the thing with the actual list.
    pub fn indirector(&self) -> *const MessageIndirector {
        self.indirect
    }

    pub fn iter(&self) -> impl Iterator<Item = &RawMessage> {
        // SAFETY: we're iterating over an amount we know.
        unsafe { (*self.indirect).iter(self.count) }
    }

    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut RawMessage> {
        // SAFETY: we're iterating over an amount we know.
        unsafe { (*self.indirect).iter_mut(self.count) }
    }
}

impl Drop for OwnedMessages {
    fn drop(&mut self) {
        // SAFETY: We are valid and have a valid pointer.
        // Once we're done, everything will be safe.
        unsafe {
            if let Some(indirect) = self.indirect.as_mut() {
                indirect.free(self.count)
            }
            libc::free(self.indirect.cast());
            self.indirect = ptr::null_mut();
        }
    }
}

/// An indirect reference to messages.
///
/// This is kept separate to provide a place where we can separate
/// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
#[repr(transparent)]
pub struct MessageIndirector {
    base: [*mut RawMessage; 0],
    _marker: Immovable,
}

impl MessageIndirector {
    /// Allocates memory for this indirector and all its members.
    fn alloc(count: usize) -> *mut Self {
        // SAFETY: We're only allocating, and when we're done,
        // everything will be in a known-good state.
        unsafe {
            let me_ptr: *mut MessageIndirector =
                libc::calloc(count, size_of::<*mut RawMessage>()).cast();
            let me = &mut *me_ptr;
            let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count);
            for entry in ptr_list {
                *entry = libc::calloc(1, size_of::<RawMessage>()).cast();
            }
            me
        }
    }

    /// Returns an iterator yielding the given number of messages.
    ///
    /// # Safety
    ///
    /// You have to provide the right count.
    pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &RawMessage> {
        (0..count).map(|idx| &**self.base.as_ptr().add(idx))
    }

    /// Returns a mutable iterator yielding the given number of messages.
    ///
    /// # Safety
    ///
    /// You have to provide the right count.
    pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut RawMessage> {
        (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx))
    }

    /// Frees this and everything it points to.
    ///
    /// # Safety
    ///
    /// You have to pass the right size.
    unsafe fn free(&mut self, count: usize) {
        let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count);
        for msg in msgs {
            if let Some(msg) = msg.as_mut() {
                msg.clear();
            }
            libc::free(msg.cast());
            *msg = ptr::null_mut();
        }
    }
}

impl<'a> TryFrom<&'a RawMessage> for Message<'a> {
    type Error = ConversionError;

    /// Retrieves the data stored in this message.
    fn try_from(input: &RawMessage) -> StdResult<Message, ConversionError> {
        let style: Style = input.style.try_into()?;
        // SAFETY: We either allocated this message ourselves or were provided it by PAM.
        let result = unsafe {
            match style {
                Style::PromptEchoOff => Message::MaskedPrompt(input.string_data()?),
                Style::PromptEchoOn => Message::Prompt(input.string_data()?),
                Style::TextInfo => Message::Info(input.string_data()?),
                Style::ErrorMsg => Message::Error(input.string_data()?),
                Style::RadioType => Message::Error(input.string_data()?),
                Style::BinaryPrompt => input.data.cast::<CBinaryData>().as_ref().map_or_else(
                    || Message::BinaryPrompt {
                        data_type: 0,
                        data: &[],
                    },
                    |data| Message::BinaryPrompt {
                        data_type: data.data_type(),
                        data: data.contents(),
                    },
                ),
            }
        };
        Ok(result)
    }
}

#[cfg(test)]
mod tests {
    use crate::conv::Message;
    use crate::pam_ffi::message::OwnedMessages;

    #[test]
    fn test_owned_messages() {
        let mut tons_of_messages = OwnedMessages::alloc(10);
        let mut msgs: Vec<_> = tons_of_messages.iter_mut().collect();
        assert!(msgs.get(10).is_none());
        let last_msg = &mut msgs[9];
        last_msg.set(Message::MaskedPrompt("hocus pocus")).unwrap();
        let another_msg = &mut msgs[0];
        another_msg
            .set(Message::BinaryPrompt {
                data: &[5, 4, 3, 2, 1],
                data_type: 99,
            })
            .unwrap();
        let overwrite = &mut msgs[3];
        overwrite.set(Message::Prompt("what")).unwrap();
        overwrite.set(Message::Prompt("who")).unwrap();
    }
}