diff src/libpam/answer.rs @ 98:b87100c5eed4

Start on environment variables, and make pointers nicer. This starts work on the PAM environment handling, and in so doing, introduces the CHeapBox and CHeapString structs. These are analogous to Box and CString, but they're located on the C heap rather than being Rust-managed memory. This is because environment variables deal with even more pointers and it turns out we can lose a lot of manual freeing using homemade smart pointers.
author Paul Fisher <paul@pfish.zone>
date Tue, 24 Jun 2025 04:25:25 -0400
parents efc2b56c8928
children 3f11b8d30f63
line wrap: on
line diff
--- a/src/libpam/answer.rs	Mon Jun 23 19:10:34 2025 -0400
+++ b/src/libpam/answer.rs	Tue Jun 24 04:25:25 2025 -0400
@@ -2,7 +2,7 @@
 
 use crate::libpam::conversation::OwnedMessage;
 use crate::libpam::memory;
-use crate::libpam::memory::CBinaryData;
+use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString};
 pub use crate::libpam::pam_ffi::Answer;
 use crate::{ErrorCode, Result};
 use std::ffi::CStr;
@@ -13,7 +13,9 @@
 /// The corridor via which the answer to Messages navigate through PAM.
 #[derive(Debug)]
 pub struct Answers {
-    base: *mut Answer,
+    /// 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,
 }
 
@@ -21,7 +23,7 @@
     /// 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()),
+            base: memory::calloc(value.len())?,
             count: value.len(),
         };
         // Even if we fail during this process, we still end up freeing
@@ -45,7 +47,7 @@
     ///
     /// 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 {
+    pub fn into_ptr(self) -> NonNull<Answer> {
         let ret = self.base;
         mem::forget(self);
         ret
@@ -57,7 +59,7 @@
     ///
     /// 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 {
+    pub unsafe fn from_c_heap(base: NonNull<Answer>, count: usize) -> Self {
         Answers { base, count }
     }
 }
@@ -66,25 +68,26 @@
     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) }
+        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, self.count) }
+        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() {
-                answer.free_contents()
+                ptr::drop_in_place(answer)
             }
-            memory::free(self.base)
+            memory::free(self.base.as_ptr())
         }
     }
 }
@@ -106,23 +109,25 @@
 
     /// 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.as_ptr().cast();
+        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> {
-        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)
+        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)
+            }
         }
     }
 
@@ -131,14 +136,14 @@
     /// 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) {
+    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 {
-            memory::zero_c_string(self.0.data.cast());
-            memory::free(self.0.data);
-            self.0.data = ptr::null_mut()
+            if let Some(ptr) = self.0.data.as_ref() {
+                CHeapString::zero(CHeapBox::as_ptr(ptr).cast());
+            }
         }
     }
 }
@@ -168,8 +173,7 @@
     /// 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.as_ptr().cast();
+        let _ = dest.data.replace(unsafe { CHeapBox::cast(allocated) });
         Ok(())
     }
 
@@ -177,7 +181,11 @@
     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.
-        NonNull::new(self.0.data.cast::<CBinaryData>())
+        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`.
@@ -189,26 +197,9 @@
         // 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) = NonNull::new(self.0.data) {
-                CBinaryData::zero_contents(ptr.cast())
+            if let Some(ptr) = self.0.data.as_ref() {
+                CBinaryData::zero_contents(CHeapBox::as_ptr(ptr).cast())
             }
-            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();
         }
     }
 }
@@ -229,7 +220,7 @@
     fn assert_text_answer(want: &str, answer: &mut Answer) {
         let up = unsafe { TextAnswer::upcast(answer) };
         assert_eq!(want, up.contents().unwrap());
-        up.free_contents();
+        up.zero_contents();
         assert_eq!("", up.contents().unwrap());
     }
 
@@ -293,42 +284,33 @@
 
     #[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 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.free_contents();
-        zeroth_text.free_contents();
-        TextAnswer::fill(answer, "hell\0").expect_err("should error; contains nul");
-        unsafe { memory::free(answer_ptr) }
+        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 answer_ptr: *mut Answer = memory::calloc(1);
-        let answer = unsafe { &mut *answer_ptr };
+        let mut answer: CHeapBox<Answer> = CHeapBox::default();
         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) };
+        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())
         });
-        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) }
+        let mut answer: CHeapBox<Answer> = CHeapBox::default();
+        BinaryAnswer::fill(&mut answer, (&big_data, 100)).expect_err("this is too big!");
     }
 }