Mercurial > crates > nonstick
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/message.rs Fri Jun 06 22:35:08 2025 -0400 @@ -0,0 +1,334 @@ +//! 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(); + } +}