view src/libpam/question.rs @ 95:51c9d7e8261a

Return owned strings rather than borrowed strings. It's going to be irritating to have to work with strings borrowed from the PAM handle rather than just using your own. They're cheap enough to copy.
author Paul Fisher <paul@pfish.zone>
date Mon, 23 Jun 2025 14:03:44 -0400
parents efc2b56c8928
children b87100c5eed4
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::ptr::NonNull;
use std::{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
///            points to  ┌───────────────┐      ╔═ Question ═╗
/// questions ┄┄┄┄┄┄┄┄┄┄> │ questions[0] ┄┼┄┄┄┄> ║ style      ║
///                       │ questions[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
///            points to  ┌─────────────┐       ╔═ Question[] ═╗
/// questions ┄┄┄┄┄┄┄┄┄┄> │ *questions ┄┼┄┄┄┄┄> ║ style        ║
///                       └─────────────┘       ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
///                                             ╟──────────────╢
///                                             ║ style        ║
///                                             ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
///                                             ╟──────────────╢
///                                             ║ ...          ║
/// ```
pub trait QuestionsTrait {
    /// Allocates memory for this indirector and all its members.
    fn new(messages: &[Message]) -> Result<Self>
    where
        Self: Sized;

    /// Gets the pointer that is passed .
    fn ptr(&self) -> *const *const Question;

    /// Converts a pointer into a borrowed list of Questions.
    ///
    /// # Safety
    ///
    /// You have to provide a valid pointer.
    unsafe fn borrow_ptr<'a>(
        ptr: *const *const Question,
        count: usize,
    ) -> impl Iterator<Item = &'a Question>;
}

#[cfg(pam_impl = "linux-pam")]
pub type Questions = LinuxPamQuestions;

#[cfg(not(pam_impl = "linux-pam"))]
pub type Questions = XSsoQuestions;

/// The XSSO standard version of the pointer train to questions.
#[derive(Debug)]
#[repr(C)]
pub struct XSsoQuestions {
    /// Points to the memory address where the meat of `questions` is.
    /// **The memory layout of Vec is not specified**, and we need to return
    /// a pointer to the pointer, hence we have to store it here.
    pointer: *const Question,
    questions: Vec<Question>,
    _marker: Immovable,
}

impl XSsoQuestions {
    fn len(&self) -> usize {
        self.questions.len()
    }
    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
        self.questions.iter_mut()
    }
}

impl QuestionsTrait for XSsoQuestions {
    fn new(messages: &[Message]) -> Result<Self> {
        let questions: Result<Vec<_>> = messages.iter().map(Question::try_from).collect();
        let questions = questions?;
        Ok(Self {
            pointer: questions.as_ptr(),
            questions,
            _marker: Default::default(),
        })
    }

    fn ptr(&self) -> *const *const Question {
        &self.pointer as *const *const Question
    }

    unsafe fn borrow_ptr<'a>(
        ptr: *const *const Question,
        count: usize,
    ) -> impl Iterator<Item = &'a Question> {
        slice::from_raw_parts(*ptr, count).iter()
    }
}

/// The Linux version of the pointer train to questions.
#[derive(Debug)]
#[repr(C)]
pub struct LinuxPamQuestions {
    #[allow(clippy::vec_box)] // we need to do this.
    /// The place where the questions are.
    questions: Vec<Box<Question>>,
    _marker: Immovable,
}

impl LinuxPamQuestions {
    fn len(&self) -> usize {
        self.questions.len()
    }

    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
        self.questions.iter_mut().map(AsMut::as_mut)
    }
}

impl QuestionsTrait for LinuxPamQuestions {
    fn new(messages: &[Message]) -> Result<Self> {
        let questions: Result<_> = messages
            .iter()
            .map(|msg| Question::try_from(msg).map(Box::new))
            .collect();
        Ok(Self {
            questions: questions?,
            _marker: Default::default(),
        })
    }

    fn ptr(&self) -> *const *const Question {
        self.questions.as_ptr().cast()
    }

    unsafe fn borrow_ptr<'a>(
        ptr: *const *const Question,
        count: usize,
    ) -> impl Iterator<Item = &'a Question> {
        slice::from_raw_parts(ptr.cast::<&Question>(), count)
            .iter()
            .copied()
    }
}

/// The C enum values for messages shown to the user.
#[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
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 Question {
    /// 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) {
        NonNull::new(self.data)
            .map(|nn| nn.cast())
            .map(|ptr| CBinaryData::data(ptr))
            .unwrap_or_default()
    }
}

impl TryFrom<&Message<'_>> for Question {
    type Error = ErrorCode;
    fn try_from(msg: &Message) -> Result<Self> {
        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): (_, NonNull<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),
        }?;
        Ok(Self {
            style: style.into(),
            data: data.as_ptr(),
            _marker: Default::default(),
        })
    }
}

impl Drop for Question {
    fn drop(&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) = NonNull::new(self.data) {
                            CBinaryData::zero_contents(d.cast())
                        }
                    }
                    #[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 = <$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.ptr();

                let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
                let messages: Vec<OwnedMessage> = remade
                    .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};
                <$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 = <$typ>::new(&[
                    BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
                    RadioQAndA::new("you must choose").message(),
                ]).unwrap();
                let indirect = interrogation.ptr();

                let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
                let messages: Vec<OwnedMessage> = remade
                    .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<XSsoQuestions>);
    tests!(test_linux<LinuxPamQuestions>);
}