Mercurial > crates > nonstick
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) } + } +}