diff src/libpam/answer.rs @ 78:002adfb98c5c

Rename files, reorder structs, remove annoying BorrowedBinaryData type. This is basically a cleanup change. Also it adds tests. - Renames the files with Questions and Answers to question and answer. - Reorders the structs in those files to put the important ones first. - Removes the BorrowedBinaryData type. It was a bad idea all along. Instead, we just use (&[u8], u8). - Adds some tests because I just can't help myself.
author Paul Fisher <paul@pfish.zone>
date Sun, 08 Jun 2025 03:48:40 -0400
parents src/libpam/response.rs@351bdc13005e
children 2128123b9406
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/answer.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -0,0 +1,333 @@
+//! 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, Immovable};
+use crate::{ErrorCode, Result};
+use std::ffi::{c_int, c_void, CStr};
+use std::ops::{Deref, DerefMut};
+use std::{iter, mem, ptr, slice};
+
+/// The corridor via which the answer to Messages navigate through PAM.
+#[derive(Debug)]
+pub struct Answers {
+    base: *mut 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()?.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.
+    ///
+    /// 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) -> *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,
+    /// like one that you got from PAM, or maybe [`Self::into_ptr`].
+    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()
+            }
+            memory::free(self.base)
+        }
+    }
+}
+
+#[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.cast());
+            memory::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_and_type: (&[u8], u8)) -> Result<()> {
+        let allocated = CBinaryData::alloc(data_and_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()
+            }
+            memory::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 {
+            memory::free(self.data);
+            self.data = ptr::null_mut();
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{Answer, Answers, BinaryAnswer, TextAnswer};
+    use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, QAndA, RadioQAndA};
+    use crate::libpam::conversation::OwnedMessage;
+    use crate::BinaryData;
+    use crate::libpam::memory;
+
+    #[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, raw| {
+            let up = unsafe { BinaryAnswer::upcast(raw) };
+            assert_eq!(BinaryData::from(want), 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 answer_ptr: *mut Answer = memory::calloc(1);
+        let answer = unsafe {&mut *answer_ptr};
+        TextAnswer::fill(answer, "hello").unwrap();
+        let zeroth_text = unsafe { TextAnswer::upcast(answer) };
+        let data = zeroth_text.contents().expect("valid");
+        assert_eq!("hello", data);
+        zeroth_text.free_contents();
+        zeroth_text.free_contents();
+        TextAnswer::fill(answer, "hell\0").expect_err("should error; contains nul");
+        unsafe { memory::free(answer_ptr) }
+    }
+
+    #[test]
+    fn test_binary_answer() {
+        let answer_ptr: *mut Answer = memory::calloc(1);
+        let answer = unsafe { &mut *answer_ptr };
+        let real_data = BinaryData::new(vec![1, 2, 3, 4, 5, 6, 7, 8], 9);
+        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();
+        unsafe { memory::free(answer_ptr) }
+    }
+
+    #[test]
+    #[ignore]
+    fn test_binary_answer_too_big() {
+        let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000];
+        let answer_ptr: *mut Answer = memory::calloc(1);
+        let answer = unsafe {&mut*answer_ptr};
+        BinaryAnswer::fill(answer, (&big_data, 100))
+            .expect_err("this is too big!");
+        answer.free_contents();
+        unsafe { memory::free(answer) }
+    }
+}