Mercurial > crates > nonstick
diff 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 diff
--- a/src/libpam/question.rs Tue Jun 10 02:43:31 2025 -0400 +++ b/src/libpam/question.rs Tue Jun 10 04:40:01 2025 -0400 @@ -1,12 +1,15 @@ //! Data and types dealing with PAM messages. -use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA}; +#[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; use crate::libpam::memory::{CBinaryData, Immovable}; -pub use crate::libpam::pam_ffi::{Question, Style}; +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}; @@ -54,6 +57,7 @@ /// ╟──────────────╢ /// ║ ... ║ /// ``` +#[derive(Debug)] pub struct GenericQuestions<I: IndirectTrait> { /// An indirection to the questions themselves, stored on the C heap. indirect: *mut I, @@ -71,7 +75,7 @@ }; // Even if we fail partway through this, all our memory will be freed. for (question, message) in iter::zip(ret.iter_mut(), messages) { - question.fill(message)? + question.try_fill(message)? } Ok(ret) } @@ -158,12 +162,13 @@ /// 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 = XSsoIndirect; +pub type Indirect = StandardIndirect; pub type Questions = GenericQuestions<Indirect>; /// The XSSO standard version of the indirection layer between Question and Questions. -#[repr(transparent)] +#[derive(Debug)] +#[repr(C)] pub struct StandardIndirect { base: *mut Question, _marker: Immovable, @@ -200,7 +205,8 @@ } /// The Linux version of the indirection layer between Question and Questions. -#[repr(transparent)] +#[derive(Debug)] +#[repr(C)] pub struct LinuxPamIndirect { base: [*mut Question; 0], _marker: Immovable, @@ -241,6 +247,29 @@ } } +/// 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 { @@ -254,8 +283,28 @@ impl Question { /// Replaces the contents of this question with the question /// from the message. - pub fn fill(&mut self, msg: &Message) -> Result<()> { - let (style, data) = copy_to_heap(msg)?; + /// + /// 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; @@ -291,15 +340,19 @@ // 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::RadioType | Style::ErrorMsg | Style::PromptEchoOff | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()), @@ -318,7 +371,8 @@ .style .try_into() .map_err(|_| ErrorCode::ConversationError)?; - // SAFETY: In all cases below, we're matching the + // 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 => { @@ -327,7 +381,9 @@ 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())), } }; @@ -335,30 +391,9 @@ } } -/// Copies the contents of this message to the C heap. -fn copy_to_heap(msg: &Message) -> Result<(Style, *mut c_void)> { - let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); - match *msg { - Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), - Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), - Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), - Message::Error(p) => alloc(Style::ErrorMsg, p.question()), - Message::Info(p) => alloc(Style::TextInfo, p.question()), - Message::BinaryPrompt(p) => Ok(( - Style::BinaryPrompt, - CBinaryData::alloc(p.question())?.cast(), - )), - } -} - #[cfg(test)] mod tests { - use super::{ - BinaryQAndA, ErrorMsg, GenericQuestions, IndirectTrait, InfoMsg, LinuxPamIndirect, - MaskedQAndA, OwnedMessage, QAndA, RadioQAndA, Result, StandardIndirect, - }; - macro_rules! assert_matches { ($id:ident => $variant:path, $q:expr) => { if let $variant($id) = $id { @@ -370,33 +405,61 @@ } macro_rules! tests { ($fn_name:ident<$typ:ident>) => { - #[test] - fn $fn_name() { - let interrogation = GenericQuestions::<$typ>::new(&[ - MaskedQAndA::new("hocus pocus").message(), - BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), - QAndA::new("what").message(), - QAndA::new("who").message(), - InfoMsg::new("hey").message(), - ErrorMsg::new("gasp").message(), - RadioQAndA::new("you must choose").message(), - ]) - .unwrap(); - let indirect = interrogation.indirect(); + 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"); + } - 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, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap(); - assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); - assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); - assert_matches!(what => OwnedMessage::Prompt, "what"); - assert_matches!(who => OwnedMessage::Prompt, "who"); - assert_matches!(hey => OwnedMessage::Info, "hey"); - assert_matches!(gasp => OwnedMessage::Error, "gasp"); - assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); + #[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"); + } } }}