Mercurial > crates > nonstick
view src/libpam/answer.rs @ 87:05291b601f0a
Well and truly separate the Linux extensions.
This separates the Linux extensions on the libpam side,
and disables the two enums on the interface side.
Users can still call the Linux extensions from non-Linux PAM impls,
but they'll get a conversation error back.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 10 Jun 2025 04:40:01 -0400 |
parents | 5aa1a010f1e8 |
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; pub use crate::libpam::pam_ffi::Answer; use crate::{ErrorCode, Result}; use std::ffi::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::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) -> *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() } } } 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::*; 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.free_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)), up.data().into()); up.zero_contents(); assert_eq!(BinaryData::default(), up.data().into()); assert_text_answer("beep boop", radio); } else { panic!("received wrong size {len}!", len = answers.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() { use crate::conv::BinaryData; let answer_ptr: *mut Answer = memory::calloc(1); let answer = unsafe { &mut *answer_ptr }; let real_data = BinaryData::new([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; 0x1_0000_0001]; 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) } } }