view src/libpam/message.rs @ 75:c30811b4afae

rename pam_ffi submodule to libpam.
author Paul Fisher <paul@pfish.zone>
date Fri, 06 Jun 2025 22:35:08 -0400
parents src/pam_ffi/message.rs@c7c596e6388f
children 351bdc13005e
line wrap: on
line source

//! Data and types dealing with PAM messages.

use crate::constants::InvalidEnum;
use crate::conv::Message;
use crate::libpam::memory;
use crate::libpam::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::ErrorMsg(text) => alloc(Style::ErrorMsg, text),
        Message::InfoMsg(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::InfoMsg(input.string_data()?),
                Style::ErrorMsg => Message::ErrorMsg(input.string_data()?),
                Style::RadioType => Message::ErrorMsg(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::libpam::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();
    }
}