view src/libpam/response.rs @ 77:351bdc13005e

Update the libpam module to work with the new structure.
author Paul Fisher <paul@pfish.zone>
date Sun, 08 Jun 2025 01:03:46 -0400
parents c30811b4afae
children
line wrap: on
line source

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

use crate::conv::BorrowedBinaryData;
use crate::libpam::conversation::OwnedMessage;
use crate::libpam::memory;
use crate::libpam::memory::{CBinaryData, Immovable};
use crate::{ErrorCode, Result};
use std::ffi::{c_int, c_void, CStr};
use std::ops::{Deref, DerefMut};
use std::{iter, mem, ptr, slice};

#[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 = memory::malloc_str(text)?;
        dest.free_contents();
        dest.data = allocated.cast();
        Ok(())
    }

    /// Gets the string stored in this answer.
    pub fn contents(&self) -> Result<&str> {
        if self.0.data.is_null() {
            Ok("")
        } else {
            // 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(self.0.data.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 free_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 {
            memory::zero_c_string(self.0.data);
            libc::free(self.0.data);
            self.0.data = ptr::null_mut()
        }
    }
}

/// 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: BorrowedBinaryData) -> Result<()> {
        let allocated = CBinaryData::alloc(data.data(), data.data_type())?;
        dest.free_contents();
        dest.data = allocated.cast();
        Ok(())
    }

    /// Gets the binary data in this answer.
    pub fn data(&self) -> Option<&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.
        unsafe { self.0.data.cast::<CBinaryData>().as_ref() }
    }

    /// 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 know that our data pointer is either valid or null.
        // Once we're done, it's null and the answer is safe.
        unsafe {
            let data_ref = self.0.data.cast::<CBinaryData>().as_mut();
            if let Some(d) = data_ref {
                d.zero_contents()
            }
            libc::free(self.0.data);
            self.0.data = ptr::null_mut()
        }
    }
}

/// Generic version of answer data.
///
/// This has the same structure as [`BinaryAnswer`]
/// and [`TextAnswer`].
#[repr(C)]
#[derive(Debug)]
pub struct Answer {
    /// Pointer to the data returned in an answer.
    /// For most answers, this will be a [`CStr`], but for answers to
    /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`]
    /// (a Linux-PAM extension).
    data: *mut c_void,
    /// Unused.
    return_code: c_int,
    _marker: Immovable,
}

impl Answer {
    /// Frees the contents of this answer.
    ///
    /// After this is done, this answer's `data` will be `null`,
    /// which is a valid (empty) state.
    fn free_contents(&mut self) {
        // SAFETY: We have either an owned valid pointer, or null.
        // We can free our owned pointer, and `free(null)` is a no-op.
        unsafe {
            libc::free(self.data);
            self.data = ptr::null_mut();
        }
    }
}

/// An owned, contiguous block of [`Answer`]s.
#[derive(Debug)]
pub struct Answers {
    base: *mut Answer,
    count: usize,
}

impl Answers {
    /// Allocates an owned list of answers on the C heap.
    fn alloc(count: usize) -> Self {
        Answers {
            // SAFETY: We are doing allocation here.
            base: unsafe { libc::calloc(count, size_of::<Answer>()) }.cast(),
            count,
        }
    }

    pub fn build(value: Vec<OwnedMessage>) -> Result<Self> {
        let mut outputs = Answers::alloc(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()?.unsecure())?,
                OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
                OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?,
                OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
                OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
                OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
            }
        }
        Ok(outputs)
    }

    /// Converts this into a `*Answer` for passing to PAM.
    ///
    /// The pointer "owns" its own data (i.e., this will not be dropped).
    pub fn into_ptr(self) -> *mut Answer {
        let ret = self.base;
        mem::forget(self);
        ret
    }

    /// 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.
    pub unsafe fn from_c_heap(base: *mut 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, 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, self.count) }
    }
}

impl Drop for Answers {
    fn drop(&mut self) {
        // SAFETY: We allocated this ourselves, or it was provided to us by PAM.
        unsafe {
            for answer in self.iter_mut() {
                answer.free_contents()
            }
            libc::free(self.base.cast())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{Answers, BinaryAnswer, TextAnswer, BorrowedBinaryData};
    use crate::BinaryData;
    use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, QAndA, RadioQAndA};
    use crate::libpam::conversation::OwnedMessage;

    #[test]
    fn test_round_trip() {
        let binary_msg = {
            let qa = BinaryQAndA::new(&[], 0);
            qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99)));
            OwnedMessage::BinaryPrompt(qa)
        };

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


        let answers = vec![
            binary_msg,
            answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()),
            answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()),
            answered!(ErrorMsg, OwnedMessage::Error, ()),
            answered!(InfoMsg, OwnedMessage::Info, ()),
            answered!(RadioQAndA, OwnedMessage::RadioPrompt, "beep boop".to_owned()),
        ];
        let n = answers.len();
        let sent = Answers::build(answers).unwrap();
        let heap_answers = sent.into_ptr();
        let mut received = unsafe { Answers::from_c_heap(heap_answers, n) };

        let assert_text = |want, raw| {
            let up = unsafe { TextAnswer::upcast(raw) };
            assert_eq!(want, up.contents().unwrap());
            up.free_contents();
            assert_eq!("", up.contents().unwrap());
        };
        let assert_bin = |want_data: &[u8], want_type, raw| {
            let up = unsafe { BinaryAnswer::upcast(raw) };
            assert_eq!(BinaryData::new(want_data.into(), want_type), up.data().into());
            up.zero_contents();
            assert_eq!(BinaryData::default(), up.data().into());
        };
        if let [zero, one, two, three, four, five] = &mut received[..] {
            assert_bin(&[1, 2, 3], 99, zero);
            assert_text("whats going on", one);
            assert_text("well then", two);
            assert_text("", three);
            assert_text("", four);
            assert_text("beep boop", five);
        } else {
            panic!("received wrong size {len}!", len = received.len())
        }
    }

    #[test]
    fn test_text_answer() {
        let mut answers = Answers::alloc(2);
        let zeroth = &mut answers[0];
        TextAnswer::fill(zeroth, "hello").unwrap();
        let zeroth_text = unsafe { TextAnswer::upcast(zeroth) };
        let data = zeroth_text.contents().expect("valid");
        assert_eq!("hello", data);
        zeroth_text.free_contents();
        zeroth_text.free_contents();
        TextAnswer::fill(&mut answers[1], "hell\0").expect_err("should error; contains nul");
    }

    #[test]
    fn test_binary_answer() {
        let mut answers = Answers::alloc(1);
        let real_data = BinaryData::new(vec![1, 2, 3, 4, 5, 6, 7, 8], 9);
        let answer = &mut answers[0];
        BinaryAnswer::fill(answer, (&real_data).into()).expect("alloc should succeed");
        let bin_answer = unsafe { BinaryAnswer::upcast(answer) };
        assert_eq!(real_data, bin_answer.data().into());
        answer.free_contents();
        answer.free_contents();
    }

    #[test]
    #[ignore]
    fn test_binary_answer_too_big() {
        let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000];
        let mut answers = Answers::alloc(1);
        BinaryAnswer::fill(&mut answers[0], BorrowedBinaryData::new(&big_data, 100))
            .expect_err("this is too big!");
    }
}