Mercurial > crates > nonstick
view src/libpam/question.rs @ 87:05291b601f0a
Well and truly separate the Linux extensions.
This separates the Linux extensions on the libpam side,
and disables the two enums on the interface side.
Users can still call the Linux extensions from non-Linux PAM impls,
but they'll get a conversation error back.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 10 Jun 2025 04:40:01 -0400 |
parents | 5e14bb093851 |
children |
line wrap: on
line source
//! Data and types dealing with PAM messages. #[cfg(feature = "linux-pam-extensions")] use crate::conv::{BinaryQAndA, RadioQAndA}; use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA}; use crate::libpam::conversation::OwnedMessage; use crate::libpam::memory::{CBinaryData, Immovable}; pub use crate::libpam::pam_ffi::Question; use crate::libpam::{memory, pam_ffi}; use crate::ErrorCode; use crate::Result; use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::ffi::{c_void, CStr}; use std::{iter, ptr, slice}; /// Abstraction of a collection of questions to be sent in a PAM conversation. /// /// The PAM C API conversation function looks like this: /// /// ```c /// int pam_conv( /// int count, /// const struct pam_message **questions, /// struct pam_response **answers, /// void *appdata_ptr, /// ) /// ``` /// /// On Linux-PAM and other compatible implementations, `questions` /// is treated as a pointer-to-pointers, like `int argc, char **argv`. /// (In this situation, the value of `Questions.indirect` is /// the pointer passed to `pam_conv`.) /// /// ```text /// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question ═╗ /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ /// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ... /// ╚═════════════╝ │ ... │ ┆ ╚════════════╝ /// ┆ /// ┆ ╔═ Question ═╗ /// ╰┄┄> ║ 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 /// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question[] ═╗ /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ /// ║ count ║ └────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... /// ╚═════════════╝ ╟──────────────╢ /// ║ style ║ /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... /// ╟──────────────╢ /// ║ ... ║ /// ``` #[derive(Debug)] pub struct GenericQuestions<I: IndirectTrait> { /// An indirection to the questions themselves, stored on the C heap. indirect: *mut I, /// The number of questions. count: usize, } impl<I: IndirectTrait> GenericQuestions<I> { /// Stores the provided questions on the C heap. pub fn new(messages: &[Message]) -> Result<Self> { let count = messages.len(); let mut ret = Self { indirect: I::alloc(count), count, }; // Even if we fail partway through this, all our memory will be freed. for (question, message) in iter::zip(ret.iter_mut(), messages) { question.try_fill(message)? } Ok(ret) } /// The pointer to the thing with the actual list. pub fn indirect(&self) -> *const *const Question { self.indirect.cast() } pub fn iter(&self) -> impl Iterator<Item = &Question> { // 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 Question> { // SAFETY: we're iterating over an amount we know. unsafe { (*self.indirect).iter_mut(self.count) } } } impl<I: IndirectTrait> Drop for GenericQuestions<I> { 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_contents(self.count) } memory::free(self.indirect); self.indirect = ptr::null_mut(); } } } /// The trait that each of the `Indirect` implementations implement. /// /// Basically a slice but with more meat. pub trait IndirectTrait { /// Converts a pointer into a borrowed `Self`. /// /// # Safety /// /// You have to provide a valid pointer. unsafe fn borrow_ptr<'a>(ptr: *const *const Question) -> Option<&'a Self> where Self: Sized, { ptr.cast::<Self>().as_ref() } /// Allocates memory for this indirector and all its members. fn alloc(count: usize) -> *mut Self; /// Returns an iterator yielding the given number of messages. /// /// # Safety /// /// You have to provide the right count. unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question>; /// Returns a mutable iterator yielding the given number of messages. /// /// # Safety /// /// You have to provide the right count. unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question>; /// Frees everything this points to. /// /// # Safety /// /// You have to pass the right size. unsafe fn free_contents(&mut self, count: usize); } /// 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. #[cfg(pam_impl = "linux-pam")] pub type Indirect = LinuxPamIndirect; /// 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. #[cfg(not(pam_impl = "linux-pam"))] pub type Indirect = StandardIndirect; pub type Questions = GenericQuestions<Indirect>; /// The XSSO standard version of the indirection layer between Question and Questions. #[derive(Debug)] #[repr(C)] pub struct StandardIndirect { base: *mut Question, _marker: Immovable, } impl IndirectTrait for StandardIndirect { fn alloc(count: usize) -> *mut Self { let questions = memory::calloc(count); let me_ptr: *mut Self = memory::calloc(1); // SAFETY: We just allocated this, and we're putting a valid pointer in. unsafe { let me = &mut *me_ptr; me.base = questions; } me_ptr } unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { (0..count).map(|idx| &*self.base.add(idx)) } unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { (0..count).map(|idx| &mut *self.base.add(idx)) } unsafe fn free_contents(&mut self, count: usize) { let msgs = slice::from_raw_parts_mut(self.base, count); for msg in msgs { msg.clear() } memory::free(self.base); self.base = ptr::null_mut() } } /// The Linux version of the indirection layer between Question and Questions. #[derive(Debug)] #[repr(C)] pub struct LinuxPamIndirect { base: [*mut Question; 0], _marker: Immovable, } impl IndirectTrait for LinuxPamIndirect { fn alloc(count: usize) -> *mut Self { // SAFETY: We're only allocating, and when we're done, // everything will be in a known-good state. let me_ptr: *mut Self = memory::calloc::<*mut Question>(count).cast(); unsafe { 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 = memory::calloc(1); } } me_ptr } unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { (0..count).map(|idx| &**self.base.as_ptr().add(idx)) } unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx)) } unsafe fn free_contents(&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(); } memory::free(*msg); *msg = ptr::null_mut(); } } } /// The C enum values for messages shown to the user. #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] #[repr(u32)] pub enum Style { /// Requests information from the user; will be masked when typing. PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF, /// Requests information from the user; will not be masked. PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON, /// An error message. ErrorMsg = pam_ffi::PAM_ERROR_MSG, /// An informational message. TextInfo = pam_ffi::PAM_TEXT_INFO, /// Yes/No/Maybe conditionals. A Linux-PAM extension. #[cfg(feature = "linux-pam-extensions")] RadioType = pam_ffi::PAM_RADIO_TYPE, /// For server–client non-human interaction. /// /// NOT part of the X/Open PAM specification. /// A Linux-PAM extension. #[cfg(feature = "linux-pam-extensions")] BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT, } impl Default for Question { fn default() -> Self { Self { style: Default::default(), data: ptr::null_mut(), _marker: Default::default(), } } } impl Question { /// Replaces the contents of this question with the question /// from the message. /// /// If the message is not valid (invalid message type, bad contents, etc.), /// this will fail. pub fn try_fill(&mut self, msg: &Message) -> Result<()> { let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); // We will only allocate heap data if we have a valid input. let (style, data): (_, *mut c_void) = match *msg { Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), Message::Error(p) => alloc(Style::ErrorMsg, p.question()), Message::Info(p) => alloc(Style::TextInfo, p.question()), #[cfg(feature = "linux-pam-extensions")] Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), #[cfg(feature = "linux-pam-extensions")] Message::BinaryPrompt(p) => Ok(( Style::BinaryPrompt, CBinaryData::alloc(p.question())?.cast(), )), #[cfg(not(feature = "linux-pam-extensions"))] Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError), }?; // Now that we know everything is valid, fill ourselves in. self.clear(); self.style = style.into(); 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) -> Result<&str> { if self.data.is_null() { Ok("") } else { CStr::from_ptr(self.data.cast()) .to_str() .map_err(|_| ErrorCode::ConversationError) } } /// Gets this message's data pointer as borrowed binary data. unsafe fn binary_data(&self) -> (&[u8], u8) { self.data .cast::<CBinaryData>() .as_ref() .map(Into::into) .unwrap_or_default() } /// 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 { // This is nice-to-have. We'll try to zero out the data // in the Question. If it's not a supported format, we skip it. if let Ok(style) = Style::try_from(self.style) { match style { #[cfg(feature = "linux-pam-extensions")] Style::BinaryPrompt => { if let Some(d) = self.data.cast::<CBinaryData>().as_mut() { d.zero_contents() } } #[cfg(feature = "linux-pam-extensions")] Style::RadioType => memory::zero_c_string(self.data.cast()), Style::TextInfo | Style::ErrorMsg | Style::PromptEchoOff | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()), } }; memory::free(self.data); self.data = ptr::null_mut(); } } } impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> { type Error = ErrorCode; fn try_from(question: &'a Question) -> Result<Self> { let style: Style = question .style .try_into() .map_err(|_| ErrorCode::ConversationError)?; // SAFETY: In all cases below, we're creating questions based on // known types that we get from PAM and the inner types it should have. let prompt = unsafe { match style { Style::PromptEchoOff => { Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?)) } Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)), Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)), Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)), #[cfg(feature = "linux-pam-extensions")] Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)), #[cfg(feature = "linux-pam-extensions")] Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())), } }; Ok(prompt) } } #[cfg(test)] mod tests { macro_rules! assert_matches { ($id:ident => $variant:path, $q:expr) => { if let $variant($id) = $id { assert_eq!($q, $id.question()); } else { panic!("mismatched enum variant {x:?}", x = $id); } }; } macro_rules! tests { ($fn_name:ident<$typ:ident>) => { mod $fn_name { use super::super::*; #[test] fn standard() { let interrogation = GenericQuestions::<$typ>::new(&[ MaskedQAndA::new("hocus pocus").message(), QAndA::new("what").message(), QAndA::new("who").message(), InfoMsg::new("hey").message(), ErrorMsg::new("gasp").message(), ]) .unwrap(); let indirect = interrogation.indirect(); let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap(); let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } .map(TryInto::try_into) .collect::<Result<_>>() .unwrap(); let [masked, what, who, hey, gasp] = messages.try_into().unwrap(); assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); assert_matches!(what => OwnedMessage::Prompt, "what"); assert_matches!(who => OwnedMessage::Prompt, "who"); assert_matches!(hey => OwnedMessage::Info, "hey"); assert_matches!(gasp => OwnedMessage::Error, "gasp"); } #[test] #[cfg(not(feature = "linux-pam-extensions"))] fn no_linux_extensions() { use crate::conv::{BinaryQAndA, RadioQAndA}; GenericQuestions::<$typ>::new(&[ BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), RadioQAndA::new("you must choose").message(), ]).unwrap_err(); } #[test] #[cfg(feature = "linux-pam-extensions")] fn linux_extensions() { let interrogation = GenericQuestions::<$typ>::new(&[ BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), RadioQAndA::new("you must choose").message(), ]).unwrap(); let indirect = interrogation.indirect(); let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap(); let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } .map(TryInto::try_into) .collect::<Result<_>>() .unwrap(); let [bin, choose] = messages.try_into().unwrap(); assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); } } }} tests!(test_xsso<StandardIndirect>); tests!(test_linux<LinuxPamIndirect>); }