Mercurial > crates > nonstick
diff src/libpam/memory.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 | 51c9d7e8261a |
children | 3f11b8d30f63 |
line wrap: on
line diff
--- a/src/libpam/memory.rs Mon Jun 23 19:10:34 2025 -0400 +++ b/src/libpam/memory.rs Tue Jun 24 04:25:25 2025 -0400 @@ -2,17 +2,39 @@ use crate::Result; use crate::{BinaryData, ErrorCode}; +use std::error::Error; use std::ffi::{c_char, CStr, CString}; +use std::fmt::{Display, Formatter, Result as FmtResult}; use std::marker::{PhantomData, PhantomPinned}; use std::mem::offset_of; +use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; +use std::result::Result as StdResult; use std::{mem, ptr, slice}; +/// Raised from `calloc` when you have no memory! +#[derive(Debug)] +pub struct NoMem; + +impl Display for NoMem { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "out of memory!") + } +} + +impl Error for NoMem {} + +impl From<NoMem> for ErrorCode { + fn from(_: NoMem) -> Self { + ErrorCode::BufferError + } +} + /// Allocates `count` elements to hold `T`. #[inline] -pub fn calloc<T>(count: usize) -> *mut T { +pub fn calloc<T>(count: usize) -> StdResult<NonNull<T>, NoMem> { // SAFETY: it's always safe to allocate! Leaking memory is fun! - unsafe { libc::calloc(count, size_of::<T>()) }.cast() + NonNull::new(unsafe { libc::calloc(count, size_of::<T>()) }.cast()).ok_or(NoMem) } /// Wrapper for [`libc::free`] to make debugging calls/frees easier. @@ -46,6 +68,170 @@ } } +/// It's like a [`Box`], but C heap managed. +#[derive(Debug)] +#[repr(transparent)] +pub struct CHeapBox<T>(NonNull<T>); + +// Lots of "as" and "into" associated functions. +#[allow(clippy::wrong_self_convention)] +impl<T> CHeapBox<T> { + /// Creates a new CHeapBox holding the given data. + pub fn new(value: T) -> Result<Self> { + let memory = calloc(1)?; + unsafe { ptr::write(memory.as_ptr(), value) } + // SAFETY: We literally just allocated this. + Ok(Self(memory)) + } + + /// Takes ownership of the given pointer. + /// + /// # Safety + /// + /// You have to provide a valid pointer to the start of an allocation + /// that was made with `malloc`. + pub unsafe fn from_ptr(ptr: NonNull<T>) -> Self { + Self(ptr) + } + + /// Converts this CBox into a raw pointer. + pub fn into_ptr(this: Self) -> NonNull<T> { + let ret = this.0; + mem::forget(this); + ret + } + + /// Gets a pointer from this but doesn't convert this into a raw pointer. + /// + /// You are responsible for ensuring the CHeapBox lives long enough. + pub fn as_ptr(this: &Self) -> NonNull<T> { + this.0 + } + + /// Converts this into a Box of a different type. + /// + /// # Safety + /// + /// The different type has to be compatible in size/alignment and drop behavior. + pub unsafe fn cast<R>(this: Self) -> CHeapBox<R> { + mem::transmute(this) + } +} + +impl<T: Default> Default for CHeapBox<T> { + fn default() -> Self { + Self::new(Default::default()).expect("allocation should not fail") + } +} + +impl<T> Deref for CHeapBox<T> { + type Target = T; + fn deref(&self) -> &Self::Target { + // SAFETY: We own this pointer and it is guaranteed valid. + unsafe { Self::as_ptr(self).as_ref() } + } +} + +impl<T> DerefMut for CHeapBox<T> { + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: We own this pointer and it is guaranteed valid. + unsafe { Self::as_ptr(self).as_mut() } + } +} + +impl<T> Drop for CHeapBox<T> { + fn drop(&mut self) { + // SAFETY: We own a valid pointer, and will never use it after this. + unsafe { + let ptr = self.0.as_ptr(); + ptr::drop_in_place(ptr); + free(ptr) + } + } +} + +/// A null-terminated string allocated on the C heap. +/// +/// Basically [`CString`], but managed by malloc. +#[derive(Debug)] +#[repr(transparent)] +pub struct CHeapString(CHeapBox<c_char>); + +impl CHeapString { + /// Creates a new C heap string with the given contents. + pub fn new(text: &str) -> Result<Self> { + let data = text.as_bytes(); + if data.contains(&0) { + return Err(ErrorCode::ConversationError); + } + // +1 for the null terminator + let data_alloc: NonNull<c_char> = calloc(data.len() + 1)?; + // SAFETY: we just allocated this and we have enough room. + unsafe { + libc::memcpy(data_alloc.as_ptr().cast(), data.as_ptr().cast(), data.len()); + Ok(Self(CHeapBox::from_ptr(data_alloc))) + } + } + + /// Converts this C heap string into a raw pointer. + /// + /// You are responsible for freeing it later. + pub fn into_ptr(self) -> NonNull<c_char> { + let ptr = CHeapBox::as_ptr(&self.0); + mem::forget(self); + ptr + } + + /// Converts this into a dumb box. It will no longer be zeroed upon drop. + pub fn into_box(self) -> CHeapBox<c_char> { + unsafe { mem::transmute(self) } + } + + /// Takes ownership of a C heap string. + /// + /// # Safety + /// + /// You have to provide a pointer to the start of an allocation that is + /// a valid 0-terminated C string. + unsafe fn from_ptr(ptr: *mut c_char) -> Option<Self> { + NonNull::new(ptr).map(|p| unsafe { Self(CHeapBox::from_ptr(p)) }) + } + + unsafe fn from_box<T>(bx: CHeapBox<T>) -> Self { + Self(CHeapBox::cast(bx)) + } + + /// Zeroes the contents of a C string. + /// + /// # Safety + /// + /// You have to provide a valid pointer to a null-terminated C string. + pub unsafe fn zero(ptr: NonNull<c_char>) { + let cstr = ptr.as_ptr(); + let len = libc::strlen(cstr.cast()); + for x in 0..len { + ptr::write_volatile(cstr.byte_offset(x as isize), mem::zeroed()) + } + } +} + +impl Drop for CHeapString { + fn drop(&mut self) { + // SAFETY: We own a valid C String + unsafe { Self::zero(CHeapBox::as_ptr(&self.0)) } + } +} + +impl Deref for CHeapString { + type Target = CStr; + + fn deref(&self) -> &Self::Target { + // SAFETY: We know we own a valid C string pointer. + let ptr = CHeapBox::as_ptr(&self.0).as_ptr(); + unsafe { CStr::from_ptr(ptr) } + } +} + /// Creates an owned copy of a string that is returned from a /// <code>pam_get_<var>whatever</var></code> function. /// @@ -64,42 +250,6 @@ Ok(borrowed.map(String::from)) } -/// Allocates a string with the given contents on the C heap. -/// -/// This is like [`CString::new`], but: -/// -/// - it allocates data on the C heap with [`libc::malloc`]. -/// - it doesn't take ownership of the data passed in. -pub fn malloc_str(text: &str) -> Result<NonNull<c_char>> { - let data = text.as_bytes(); - if data.contains(&0) { - return Err(ErrorCode::ConversationError); - } - // +1 for the null terminator - let data_alloc: *mut c_char = calloc(data.len() + 1); - // SAFETY: we just allocated this and we have enough room. - unsafe { - libc::memcpy(data_alloc.cast(), data.as_ptr().cast(), data.len()); - Ok(NonNull::new_unchecked(data_alloc)) - } -} - -/// Writes zeroes over the contents of a C string. -/// -/// This won't overwrite a null pointer. -/// -/// # Safety -/// -/// It's up to you to provide a valid C string. -pub unsafe fn zero_c_string(cstr: *mut c_char) { - if !cstr.is_null() { - let len = libc::strlen(cstr.cast()); - for x in 0..len { - ptr::write_volatile(cstr.byte_offset(x as isize), mem::zeroed()) - } - } -} - /// Binary data used in requests and responses. /// /// This is an unsized data type whose memory goes beyond its data. @@ -119,13 +269,12 @@ impl CBinaryData { /// Copies the given data to a new BinaryData on the heap. - pub fn alloc((data, data_type): (&[u8], u8)) -> Result<NonNull<CBinaryData>> { + pub fn alloc((data, data_type): (&[u8], u8)) -> Result<CHeapBox<CBinaryData>> { let buffer_size = u32::try_from(data.len() + 5).map_err(|_| ErrorCode::ConversationError)?; // SAFETY: We're only allocating here. - let dest = unsafe { - let mut dest_buffer: NonNull<Self> = - NonNull::new_unchecked(calloc::<u8>(buffer_size as usize).cast()); + unsafe { + let mut dest_buffer: NonNull<Self> = calloc::<u8>(buffer_size as usize)?.cast(); let dest = dest_buffer.as_mut(); dest.total_length = buffer_size.to_be_bytes(); dest.data_type = data_type; @@ -134,9 +283,8 @@ data.as_ptr().cast(), data.len(), ); - dest_buffer - }; - Ok(dest) + Ok(CHeapBox::from_ptr(dest_buffer)) + } } fn length(&self) -> usize { @@ -175,24 +323,43 @@ #[cfg(test)] mod tests { - use super::{ - copy_pam_string, free, malloc_str, option_cstr, prompt_ptr, zero_c_string, CString, - ErrorCode, - }; + use super::*; + use std::hint; + #[test] + fn test_box() { + #[allow(non_upper_case_globals)] + static mut drop_count: u32 = 0; + + struct Dropper(i32); + + impl Drop for Dropper { + fn drop(&mut self) { + unsafe { drop_count += 1 } + } + } + + let mut dropbox = CHeapBox::new(Dropper(9)).unwrap(); + hint::black_box(dropbox.0); + dropbox = CHeapBox::new(Dropper(10)).unwrap(); + assert_eq!(1, unsafe { drop_count }); + hint::black_box(dropbox.0); + drop(dropbox); + assert_eq!(2, unsafe { drop_count }); + } #[test] fn test_strings() { - let str = malloc_str("hello there").unwrap(); - let str = str.as_ptr(); - malloc_str("hell\0 there").unwrap_err(); + let str = CHeapString::new("hello there").unwrap(); + let str_ptr = str.into_ptr().as_ptr(); + CHeapString::new("hell\0 there").unwrap_err(); unsafe { - let copied = copy_pam_string(str).unwrap(); + let copied = copy_pam_string(str_ptr).unwrap(); assert_eq!("hello there", copied.unwrap()); - zero_c_string(str); - let idx_three = str.add(3).as_mut().unwrap(); + CHeapString::zero(NonNull::new(str_ptr).unwrap()); + let idx_three = str_ptr.add(3).as_mut().unwrap(); *idx_three = 0x80u8 as i8; - let zeroed = copy_pam_string(str).unwrap().unwrap(); + let zeroed = copy_pam_string(str_ptr).unwrap().unwrap(); assert!(zeroed.is_empty()); - free(str); + let _ = CHeapString::from_ptr(str_ptr); } }