Mercurial > crates > nonstick
view src/libpam/answer.rs @ 101:94b51fa4f797
Fix memory soundness issues:
- Ensure Questions are pinned in memory when sending them through PAM.
- Hold on to the PAM conversation struct after we build it.
(Linux-PAM is leninent about this and copies the pam_conv structure.)
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 24 Jun 2025 17:54:33 -0400 |
parents | 3f11b8d30f63 |
children |
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::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; use std::{iter, ptr, slice}; /// 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!"); } }