view src/libpam/answer.rs @ 100:3f11b8d30f63

Implement environment variable management. This actually wires up the environment variable handling to libpam, so that applications and modules can manage the environment through the authentication process.
author Paul Fisher <paul@pfish.zone>
date Tue, 24 Jun 2025 17:08:01 -0400
parents b87100c5eed4
children 94b51fa4f797
line wrap: on
line source

//! Types used to communicate data from the application to the module.

use crate::libpam::conversation::OwnedMessage;
use crate::libpam::memory;
use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString};
pub use crate::libpam::pam_ffi::Answer;
use crate::{ErrorCode, Result};
use std::ffi::CStr;
use std::ops::{Deref, DerefMut};
use std::ptr::NonNull;
use std::{iter, mem, ptr, slice};
use std::mem::ManuallyDrop;

/// The corridor via which the answer to Messages navigate through PAM.
#[derive(Debug)]
pub struct Answers {
    /// The actual list of answers. This can't be a [`CHeapBox`] because
    /// this is the pointer to the start of an array, not a single Answer.
    base: NonNull<Answer>,
    count: usize,
}

impl Answers {
    /// Builds an Answers out of the given answered Message Q&As.
    pub fn build(value: Vec<OwnedMessage>) -> Result<Self> {
        let mut outputs = Self {
            base: memory::calloc(value.len())?,
            count: value.len(),
        };
        // Even if we fail during this process, we still end up freeing
        // all allocated answer memory.
        for (input, output) in iter::zip(value, outputs.iter_mut()) {
            match input {
                OwnedMessage::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.as_ref())?,
                OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
                OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
                OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
                // If we're here, that means that we *got* a Linux-PAM
                // question from PAM, so we're OK to answer it.
                OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
                OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?,
            }
        }
        Ok(outputs)
    }

    /// Converts this into a `*Answer` for passing to PAM.
    ///
    /// This object is consumed and the `Answer` pointer now owns its data.
    /// It can be recreated with [`Self::from_c_heap`].
    pub fn into_ptr(self) -> NonNull<Answer> {
        ManuallyDrop::new(self).base
    }

    /// Takes ownership of a list of answers allocated on the C heap.
    ///
    /// # Safety
    ///
    /// It's up to you to make sure you pass a valid pointer,
    /// like one that you got from PAM, or maybe [`Self::into_ptr`].
    pub unsafe fn from_c_heap(base: NonNull<Answer>, count: usize) -> Self {
        Answers { base, count }
    }
}

impl Deref for Answers {
    type Target = [Answer];
    fn deref(&self) -> &Self::Target {
        // SAFETY: This is the memory we manage ourselves.
        unsafe { slice::from_raw_parts(self.base.as_ptr(), self.count) }
    }
}

impl DerefMut for Answers {
    fn deref_mut(&mut self) -> &mut Self::Target {
        // SAFETY: This is the memory we manage ourselves.
        unsafe { slice::from_raw_parts_mut(self.base.as_ptr(), self.count) }
    }
}

impl Drop for Answers {
    fn drop(&mut self) {
        // SAFETY: We allocated this ourselves, or it was provided to us by PAM.
        // We own these pointers, and they will never be used after this.
        unsafe {
            for answer in self.iter_mut() {
                ptr::drop_in_place(answer)
            }
            memory::free(self.base.as_ptr())
        }
    }
}

#[repr(transparent)]
#[derive(Debug)]
pub struct TextAnswer(Answer);

impl TextAnswer {
    /// Interprets the provided `Answer` as a text answer.
    ///
    /// # Safety
    ///
    /// It's up to you to provide an answer that is a `TextAnswer`.
    pub unsafe fn upcast(from: &mut Answer) -> &mut Self {
        // SAFETY: We're provided a valid reference.
        &mut *(from as *mut Answer).cast::<Self>()
    }

    /// Converts the `Answer` to a `TextAnswer` with the given text.
    fn fill(dest: &mut Answer, text: &str) -> Result<()> {
        let allocated = CHeapString::new(text)?;
        let _ = dest
            .data
            .replace(unsafe { CHeapBox::cast(allocated.into_box()) });
        Ok(())
    }

    /// Gets the string stored in this answer.
    pub fn contents(&self) -> Result<&str> {
        match self.0.data.as_ref() {
            None => Ok(""),
            Some(data) => {
                // SAFETY: This data is either passed from PAM (so we are forced
                // to trust it) or was created by us in TextAnswerInner::alloc.
                // In either case, it's going to be a valid null-terminated string.
                unsafe { CStr::from_ptr(CHeapBox::as_ptr(data).as_ptr().cast()) }
                    .to_str()
                    .map_err(|_| ErrorCode::ConversationError)
            }
        }
    }

    /// Zeroes out the answer data, frees it, and points our data to `null`.
    ///
    /// When this `TextAnswer` is part of an [`Answers`],
    /// this is optional (since that will perform the `free`),
    /// but it will clear potentially sensitive data.
    pub fn zero_contents(&mut self) {
        // SAFETY: We own this data and know it's valid.
        // If it's null, this is a no-op.
        // After we're done, it will be null.
        unsafe {
            if let Some(ptr) = self.0.data.as_ref() {
                CHeapString::zero(CHeapBox::as_ptr(ptr).cast());
            }
        }
    }
}

/// A [`Answer`] with [`CBinaryData`] in it.
#[repr(transparent)]
#[derive(Debug)]
pub struct BinaryAnswer(Answer);

impl BinaryAnswer {
    /// Interprets the provided [`Answer`] as a binary answer.
    ///
    /// # Safety
    ///
    /// It's up to you to provide an answer that is a `BinaryAnswer`.
    pub unsafe fn upcast(from: &mut Answer) -> &mut Self {
        // SAFETY: We're provided a valid reference.
        &mut *(from as *mut Answer).cast::<Self>()
    }

    /// Fills in a [`Answer`] with the provided binary data.
    ///
    /// The `data_type` is a tag you can use for whatever.
    /// It is passed through PAM unchanged.
    ///
    /// The referenced data is copied to the C heap.
    /// We do not take ownership of the original data.
    pub fn fill(dest: &mut Answer, data_and_type: (&[u8], u8)) -> Result<()> {
        let allocated = CBinaryData::alloc(data_and_type)?;
        let _ = dest.data.replace(unsafe { CHeapBox::cast(allocated) });
        Ok(())
    }

    /// Gets the binary data in this answer.
    pub fn data(&self) -> Option<NonNull<CBinaryData>> {
        // SAFETY: We either got this data from PAM or allocated it ourselves.
        // Either way, we trust that it is either valid data or null.
        self.0
            .data
            .as_ref()
            .map(CHeapBox::as_ptr)
            .map(NonNull::cast)
    }

    /// Zeroes out the answer data, frees it, and points our data to `null`.
    ///
    /// When this `BinaryAnswer` is part of an [`Answers`],
    /// this is optional (since that will perform the `free`),
    /// but it will clear potentially sensitive data.
    pub fn zero_contents(&mut self) {
        // SAFETY: We know that our data pointer is either valid or null.
        // Once we're done, it's null and the answer is safe.
        unsafe {
            if let Some(ptr) = self.0.data.as_ref() {
                CBinaryData::zero_contents(CHeapBox::as_ptr(ptr).cast())
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, QAndA};

    macro_rules! answered {
        ($typ:ty, $msg:path, $data:expr) => {{
            let qa = <$typ>::new("");
            qa.set_answer(Ok($data));
            $msg(qa)
        }};
    }

    fn assert_text_answer(want: &str, answer: &mut Answer) {
        let up = unsafe { TextAnswer::upcast(answer) };
        assert_eq!(want, up.contents().unwrap());
        up.zero_contents();
        assert_eq!("", up.contents().unwrap());
    }

    fn round_trip(msgs: Vec<OwnedMessage>) -> Answers {
        let n = msgs.len();
        let sent = Answers::build(msgs).unwrap();
        unsafe { Answers::from_c_heap(sent.into_ptr(), n) }
    }

    #[test]
    fn test_round_trip() {
        let mut answers = round_trip(vec![
            answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()),
            answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()),
            answered!(ErrorMsg, OwnedMessage::Error, ()),
            answered!(InfoMsg, OwnedMessage::Info, ()),
        ]);

        if let [going, well, err, info] = &mut answers[..] {
            assert_text_answer("whats going on", going);
            assert_text_answer("well then", well);
            assert_text_answer("", err);
            assert_text_answer("", info);
        } else {
            panic!("received wrong size {len}!", len = answers.len())
        }
    }

    #[cfg(feature = "linux-pam-extensions")]
    fn test_round_trip_linux() {
        use crate::conv::{BinaryData, BinaryQAndA, RadioQAndA};
        let binary_msg = {
            let qa = BinaryQAndA::new((&[][..], 0));
            qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99)));
            OwnedMessage::BinaryPrompt(qa)
        };
        let mut answers = round_trip(vec![
            binary_msg,
            answered!(
                RadioQAndA,
                OwnedMessage::RadioPrompt,
                "beep boop".to_owned()
            ),
        ]);

        if let [bin, radio] = &mut answers[..] {
            let up = unsafe { BinaryAnswer::upcast(bin) };
            assert_eq!(BinaryData::from((&[1, 2, 3][..], 99)), unsafe {
                CBinaryData::as_binary_data(up.data().unwrap())
            });
            up.zero_contents();
            assert_eq!(BinaryData::default(), unsafe {
                CBinaryData::as_binary_data(up.data().unwrap())
            });

            assert_text_answer("beep boop", radio);
        } else {
            panic!("received wrong size {len}!", len = answers.len())
        }
    }

    #[test]
    fn test_text_answer() {
        let mut answer: CHeapBox<Answer> = CHeapBox::default();
        TextAnswer::fill(&mut answer, "hello").unwrap();
        let zeroth_text = unsafe { TextAnswer::upcast(&mut answer) };
        let data = zeroth_text.contents().expect("valid");
        assert_eq!("hello", data);
        zeroth_text.zero_contents();
        zeroth_text.zero_contents();
        TextAnswer::fill(&mut answer, "hell\0").expect_err("should error; contains nul");
    }

    #[test]
    fn test_binary_answer() {
        use crate::conv::BinaryData;
        let mut answer: CHeapBox<Answer> = CHeapBox::default();
        let real_data = BinaryData::new([1, 2, 3, 4, 5, 6, 7, 8], 9);
        BinaryAnswer::fill(&mut answer, (&real_data).into()).expect("alloc should succeed");
        let bin_answer = unsafe { BinaryAnswer::upcast(&mut answer) };
        assert_eq!(real_data, unsafe {
            CBinaryData::as_binary_data(bin_answer.data().unwrap())
        });
    }

    #[test]
    #[ignore]
    fn test_binary_answer_too_big() {
        let big_data: Vec<u8> = vec![0xFFu8; 0x1_0000_0001];
        let mut answer: CHeapBox<Answer> = CHeapBox::default();
        BinaryAnswer::fill(&mut answer, (&big_data, 100)).expect_err("this is too big!");
    }
}