Mercurial > crates > nonstick
view libpam-sys/src/helpers.rs @ 124:f469b8d9ad78 default tip
Add tests for the original X/SSO constants list.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Mon, 30 Jun 2025 04:54:38 -0400 |
parents | 98a624cacd82 |
children |
line wrap: on
line source
//! This module contains a few non-required helpers to deal with some of the //! more annoying memory management in the PAM API. use std::error::Error; use std::marker::{PhantomData, PhantomPinned}; use std::mem::ManuallyDrop; use std::ptr::NonNull; use std::{any, fmt, mem, slice}; /// A pointer-to-pointer-to-message container for the [conversation callback]. /// /// The PAM conversation callback requires a pointer to a pointer of [message]s. /// Linux-PAM handles this differently than all other PAM implementations /// (including the X/SSO PAM standard). /// /// X/SSO appears to specify a pointer-to-pointer-to-array: /// /// ```text /// points to ┌────────────┐ ╔═ Message[] ═╗ /// messages ┄┄┄┄┄┄┄┄┄┄> │ *messages ┄┼┄┄┄┄┄> ║ style ║ /// └────────────┘ ║ data ┄┄┄┄┄┄┄╫┄┄> ... /// ╟─────────────╢ /// ║ style ║ /// ║ data ┄┄┄┄┄┄┄╫┄┄> ... /// ╟─────────────╢ /// ║ ... ║ /// ``` /// /// whereas Linux-PAM uses an `**argv`-style pointer-to-array-of-pointers: /// /// ```text /// points to ┌──────────────┐ ╔═ Message ═╗ /// messages ┄┄┄┄┄┄┄┄┄┄> │ messages[0] ┄┼┄┄┄┄> ║ style ║ /// │ messages[1] ┄┼┄┄┄╮ ║ data ┄┄┄┄┄╫┄┄> ... /// │ ... │ ┆ ╚═══════════╝ /// ┆ /// ┆ ╔═ Message ═╗ /// ╰┄┄> ║ style ║ /// ║ data ┄┄┄┄┄╫┄┄> ... /// ╚═══════════╝ /// ``` /// /// Because the `messages` remain owned by the application which calls into PAM, /// we can solve this with One Simple Trick: make the intermediate list point /// into the same array: /// /// ```text /// points to ┌──────────────┐ ╔═ Message[] ═╗ /// messages ┄┄┄┄┄┄┄┄┄┄> │ messages[0] ┄┼┄┄┄┄> ║ style ║ /// │ messages[1] ┄┼┄┄╮ ║ data ┄┄┄┄┄┄┄╫┄┄> ... /// │ ... │ ┆ ╟─────────────╢ /// ╰┄> ║ style ║ /// ║ data ┄┄┄┄┄┄┄╫┄┄> ... /// ╟─────────────╢ /// ║ ... ║ /// /// ``` /// /// [conversation callback]: crate::ConversationCallback /// [message]: crate::Message #[derive(Debug)] pub struct PtrPtrVec<T> { data: Vec<T>, pointers: Vec<*const T>, } impl<T> PtrPtrVec<T> { /// Takes ownership of the given Vec and creates a vec of pointers to it. pub fn new(data: Vec<T>) -> Self { let pointers: Vec<_> = data.iter().map(|r| r as *const T).collect(); Self { data, pointers } } /// Gives you back your Vec. pub fn into_inner(self) -> Vec<T> { self.data } /// Gets a pointer-to-pointer suitable for passing into the Conversation. pub fn as_ptr<Dest>(&self) -> *const *const Dest { Self::assert_size::<Dest>(); self.pointers.as_ptr().cast::<*const Dest>() } /// Iterates over a Linux-PAM–style pointer-to-array-of-pointers. /// /// # Safety /// /// `ptr_ptr` must be a valid pointer to an array of pointers, /// there must be at least `count` valid pointers in the array, /// and each pointer in that array must point to a valid `T`. #[deprecated = "use [`Self::iter_over`] instead, unless you really need this specific version"] #[allow(dead_code)] pub unsafe fn iter_over_linux<'a, Src>( ptr_ptr: *const *const Src, count: usize, ) -> impl Iterator<Item = &'a T> where T: 'a, { Self::assert_size::<Src>(); slice::from_raw_parts(ptr_ptr.cast::<&T>(), count) .iter() .copied() } /// Iterates over an X/SSO–style pointer-to-pointer-to-array. /// /// # Safety /// /// You must pass a valid pointer to a valid pointer to an array, /// there must be at least `count` elements in the array, /// and each value in that array must be a valid `T`. #[deprecated = "use [`Self::iter_over`] instead, unless you really need this specific version"] #[allow(dead_code)] pub unsafe fn iter_over_xsso<'a, Src>( ptr_ptr: *const *const Src, count: usize, ) -> impl Iterator<Item = &'a T> where T: 'a, { Self::assert_size::<Src>(); slice::from_raw_parts(*ptr_ptr.cast(), count).iter() } #[crate::cfg_pam_impl("LinuxPam")] unsafe fn _iter_over<'a, Src>( ptr_ptr: *const *const Src, count: usize, ) -> impl Iterator<Item = &'a T> where T: 'a, { #[allow(deprecated)] Self::iter_over_linux(ptr_ptr, count) } #[crate::cfg_pam_impl(not("LinuxPam"))] unsafe fn _iter_over<'a, Src>( ptr_ptr: *const *const Src, count: usize, ) -> impl Iterator<Item = &'a T> where T: 'a, { #[allow(deprecated)] Self::iter_over_xsso(ptr_ptr, count) } /// Iterates over a PAM message list appropriate to your system's impl. /// /// This selects the correct pointer/array structure to use for a message /// that was given to you by your system. /// /// # Safety /// /// `ptr_ptr` must point to a valid message list, there must be at least /// `count` messages in the list, and all messages must be a valid `Src`. pub unsafe fn iter_over<'a, Src>( ptr_ptr: *const *const Src, count: usize, ) -> impl Iterator<Item = &'a T> where T: 'a, { Self::_iter_over(ptr_ptr, count) } fn assert_size<That>() { debug_assert_eq!( mem::size_of::<T>(), mem::size_of::<That>(), "type {t} is not the size of {that}", t = any::type_name::<T>(), that = any::type_name::<That>(), ); } } /// Error returned when attempting to allocate a buffer that is too big. /// /// This is specifically used in [`OwnedBinaryPayload`] when you try to allocate /// a message larger than 2<sup>32</sup> bytes. #[derive(Debug, PartialEq)] pub struct TooBigError { pub size: usize, pub max: usize, } impl Error for TooBigError {} impl fmt::Display for TooBigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "can't allocate a message of {size} bytes (max {max})", size = self.size, max = self.max ) } } /// A trait wrapping memory management. /// /// This is intended to allow you to bring your own allocator for /// [`OwnedBinaryPayload`]s. /// /// For an implementation example, see the implementation of this trait /// for [`Vec`]. pub trait Buffer<T: Default> { /// Allocates a buffer of `len` elements, filled with the default. fn allocate(len: usize) -> Self; fn as_ptr(&self) -> *const T; /// Returns a slice view of `size` elements of the given memory. /// /// # Safety /// /// The caller must not request more elements than are allocated. unsafe fn as_mut_slice(&mut self, len: usize) -> &mut [T]; /// Consumes this ownership and returns a pointer to the start of the arena. fn into_ptr(self) -> NonNull<T>; /// "Adopts" the memory at the given pointer, taking it under management. /// /// Running the operation: /// /// ``` /// # use libpam_sys::helpers::Buffer; /// # fn test<T: Default, OwnerType: Buffer<T>>(bytes: usize) { /// let owner = OwnerType::allocate(bytes); /// let ptr = owner.into_ptr(); /// let owner = unsafe { OwnerType::from_ptr(ptr, bytes) }; /// # } /// ``` /// /// must be a no-op. /// /// # Safety /// /// The pointer must be valid, and the caller must provide the exact size /// of the given arena. unsafe fn from_ptr(ptr: NonNull<T>, bytes: usize) -> Self; } impl<T: Default> Buffer<T> for Vec<T> { fn allocate(bytes: usize) -> Self { (0..bytes).map(|_| Default::default()).collect() } fn as_ptr(&self) -> *const T { Vec::as_ptr(self) } unsafe fn as_mut_slice(&mut self, bytes: usize) -> &mut [T] { debug_assert!(bytes <= self.len()); Vec::as_mut(self) } fn into_ptr(self) -> NonNull<T> { let mut me = ManuallyDrop::new(self); // SAFETY: a Vec is guaranteed to have a nonzero pointer. unsafe { NonNull::new_unchecked(me.as_mut_ptr()) } } unsafe fn from_ptr(ptr: NonNull<T>, bytes: usize) -> Self { Vec::from_raw_parts(ptr.as_ptr(), bytes, bytes) } } /// The structure of the "binary message" payload for the `PAM_BINARY_PROMPT` /// extension from Linux-PAM. pub struct BinaryPayload { /// The total byte size of the message, including this header, /// as a u32 in network byte order (big endian). pub total_bytes_u32be: [u8; 4], /// A tag used to provide some kind of hint as to what the data is. /// Its meaning is undefined. pub data_type: u8, /// Where the data itself would start, used as a marker to make this /// not [`Unpin`] (since it is effectively an intrusive data structure /// pointing to immediately after itself). pub _marker: PhantomData<PhantomPinned>, } impl BinaryPayload { /// The most data it's possible to put into a [`BinaryPayload`]. pub const MAX_SIZE: usize = (u32::MAX - 5) as usize; /// Fills in the provided buffer with the given data. /// /// This uses [`copy_from_slice`](slice::copy_from_slice) internally, /// so `buf` must be exactly 5 bytes longer than `data`, or this function /// will panic. pub fn fill(buf: &mut [u8], data_type: u8, data: &[u8]) { let ptr: *mut Self = buf.as_mut_ptr().cast(); // SAFETY: We're given a slice, which always has a nonzero pointer. let me = unsafe { ptr.as_mut().unwrap_unchecked() }; me.total_bytes_u32be = u32::to_be_bytes(buf.len() as u32); me.data_type = data_type; buf[5..].copy_from_slice(data) } /// The total storage needed for the message, including header. pub fn total_bytes(&self) -> usize { u32::from_be_bytes(self.total_bytes_u32be) as usize } /// Gets the total byte buffer of the BinaryMessage stored at the pointer. /// /// The returned data slice is borrowed from where the pointer points to. /// /// # Safety /// /// - The pointer must point to a valid `BinaryPayload`. /// - The borrowed data must not outlive the pointer's validity. pub unsafe fn buffer_of<'a>(ptr: *const Self) -> &'a [u8] { let header: &Self = ptr.as_ref().unwrap_unchecked(); slice::from_raw_parts(ptr.cast(), header.total_bytes().max(5)) } /// Gets the contents of the BinaryMessage stored at the given pointer. /// /// The returned data slice is borrowed from where the pointer points to. /// This is a cheap operation and doesn't do *any* copying. /// /// We don't take a `&self` reference here because accessing beyond /// the range of the `Self` data (i.e., beyond the 5 bytes of `self`) /// is undefined behavior. Instead, you have to pass a raw pointer /// directly to the data. /// /// # Safety /// /// - The pointer must point to a valid `BinaryPayload`. /// - The borrowed data must not outlive the pointer's validity. pub unsafe fn contents<'a>(ptr: *const Self) -> (u8, &'a [u8]) { let header: &Self = ptr.as_ref().unwrap_unchecked(); (header.data_type, &Self::buffer_of(ptr)[5..]) } } /// A binary message owned by some storage. /// /// This is an owned, memory-managed version of [`BinaryPayload`]. /// The `O` type manages the memory where the payload lives. /// [`Vec<u8>`] is one such manager and can be used when ownership /// of the data does not need to transit through PAM. #[derive(Debug)] pub struct OwnedBinaryPayload<Owner: Buffer<u8>>(Owner); impl<O: Buffer<u8>> OwnedBinaryPayload<O> { /// Allocates a new OwnedBinaryPayload. /// /// This will return a [`TooBigError`] if you try to allocate too much /// (more than [`BinaryPayload::MAX_SIZE`]). pub fn new(data_type: u8, data: &[u8]) -> Result<Self, TooBigError> { let total_len: u32 = (data.len() + 5).try_into().map_err(|_| TooBigError { size: data.len(), max: BinaryPayload::MAX_SIZE, })?; let total_len = total_len as usize; let mut buf = O::allocate(total_len); // SAFETY: We just allocated this exact size. BinaryPayload::fill(unsafe { buf.as_mut_slice(total_len) }, data_type, data); Ok(Self(buf)) } /// The contents of the buffer. pub fn contents(&self) -> (u8, &[u8]) { unsafe { BinaryPayload::contents(self.as_ptr()) } } /// The total bytes needed to store this, including the header. pub fn total_bytes(&self) -> usize { unsafe { BinaryPayload::buffer_of(self.0.as_ptr().cast()).len() } } /// Unwraps this into the raw storage backing it. pub fn into_inner(self) -> O { self.0 } /// Gets a const pointer to the start of the message's buffer. pub fn as_ptr(&self) -> *const BinaryPayload { self.0.as_ptr().cast() } /// Consumes ownership of this message and converts it to a raw pointer /// to the start of the message. /// /// To clean this up, you should eventually pass it into [`Self::from_ptr`] /// with the same `O` ownership type. pub fn into_ptr(self) -> NonNull<BinaryPayload> { self.0.into_ptr().cast() } /// Takes ownership of the given pointer. /// /// # Safety /// /// You must provide a valid pointer, allocated by (or equivalent to one /// allocated by) [`Self::new`]. For instance, passing a pointer allocated /// by `malloc` to `OwnedBinaryPayload::<Vec<u8>>::from_ptr` is not allowed. pub unsafe fn from_ptr(ptr: NonNull<BinaryPayload>) -> Self { Self(O::from_ptr(ptr.cast(), ptr.as_ref().total_bytes())) } } #[cfg(test)] mod tests { use super::*; use std::ptr; type VecPayload = OwnedBinaryPayload<Vec<u8>>; #[test] fn test_binary_payload() { let simple_message = &[0u8, 0, 0, 16, 0xff, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let empty = &[0u8; 5]; assert_eq!((0xff, &[0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10][..]), unsafe { BinaryPayload::contents(simple_message.as_ptr().cast()) }); assert_eq!((0x00, &[][..]), unsafe { BinaryPayload::contents(empty.as_ptr().cast()) }); } #[test] fn test_owned_binary_payload() { let (typ, data) = ( 112, &[0, 1, 1, 8, 9, 9, 9, 8, 8, 1, 9, 9, 9, 1, 1, 9, 7, 2, 5, 3][..], ); let payload = VecPayload::new(typ, data).unwrap(); assert_eq!((typ, data), payload.contents()); let ptr = payload.into_ptr(); let payload = unsafe { VecPayload::from_ptr(ptr) }; assert_eq!((typ, data), payload.contents()); } #[test] #[ignore] fn test_owned_too_big() { let data = vec![0xFFu8; 0x1_0000_0001]; assert_eq!( TooBigError { max: 0xffff_fffa, size: 0x1_0000_0001 }, VecPayload::new(5, &data).unwrap_err() ) } #[cfg(debug_assertions)] #[test] #[should_panic] fn test_new_wrong_size() { let bad_vec = vec![0; 19]; let msg = PtrPtrVec::new(bad_vec); let _ = msg.as_ptr::<u64>(); } #[allow(deprecated)] #[cfg(debug_assertions)] #[test] #[should_panic] fn test_iter_xsso_wrong_size() { unsafe { _ = PtrPtrVec::<u8>::iter_over_xsso::<f64>(ptr::null(), 1); } } #[allow(deprecated)] #[cfg(debug_assertions)] #[test] #[should_panic] fn test_iter_linux_wrong_size() { unsafe { _ = PtrPtrVec::<u128>::iter_over_linux::<()>(ptr::null(), 1); } } #[allow(deprecated)] #[test] fn test_right_size() { let good_vec = vec![(1u64, 2u64), (3, 4), (5, 6)]; let ptr = good_vec.as_ptr(); let msg = PtrPtrVec::new(good_vec); let msg_ref: *const *const (i64, i64) = msg.as_ptr(); assert_eq!(unsafe { *msg_ref }, ptr.cast()); let linux_result: Vec<(i64, i64)> = unsafe { PtrPtrVec::iter_over_linux(msg_ref, 3) } .cloned() .collect(); let xsso_result: Vec<(i64, i64)> = unsafe { PtrPtrVec::iter_over_xsso(msg_ref, 3) } .cloned() .collect(); assert_eq!(vec![(1, 2), (3, 4), (5, 6)], linux_result); assert_eq!(vec![(1, 2), (3, 4), (5, 6)], xsso_result); drop(msg) } #[allow(deprecated)] #[test] fn test_iter_ptr_ptr() { let strs = vec![Box::new("a"), Box::new("b"), Box::new("c"), Box::new("D")]; let ptr: *const *const &str = strs.as_ptr().cast(); let got: Vec<&str> = unsafe { PtrPtrVec::iter_over_linux(ptr, 4) }.cloned().collect(); assert_eq!(vec!["a", "b", "c", "D"], got); let nums = [-1i8, 2, 3]; let ptr = nums.as_ptr(); let got: Vec<u8> = unsafe { PtrPtrVec::iter_over_xsso(&ptr, 3)}.cloned().collect(); assert_eq!(vec![255, 2, 3], got); } }