changeset 119:476a22db8639

Add PtrPtrVec to make it easy to pass pointer-to-pointers to PAM.
author Paul Fisher <paul@pfish.zone>
date Mon, 30 Jun 2025 01:40:28 -0400
parents 39760dfc9b3b
children 0f913ec120ac
files libpam-sys/src/helpers.rs libpam-sys/src/structs.rs
diffstat 2 files changed, 241 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/libpam-sys/src/helpers.rs	Sun Jun 29 20:13:03 2025 -0400
+++ b/libpam-sys/src/helpers.rs	Mon Jun 30 01:40:28 2025 -0400
@@ -5,7 +5,178 @@
 use std::marker::{PhantomData, PhantomPinned};
 use std::mem::ManuallyDrop;
 use std::ptr::NonNull;
-use std::{fmt, slice};
+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.
 ///
@@ -241,6 +412,7 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::ptr;
 
     type VecPayload = OwnedBinaryPayload<Vec<u8>>;
 
@@ -282,4 +454,67 @@
             VecPayload::new(5, &data).unwrap_err()
         )
     }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn test_new_wrong_size() {
+        let bad_vec = vec![0; 19];
+        let msg = PtrPtrVec::new(bad_vec);
+        let _ = msg.as_ptr::<u64>();
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn test_iter_xsso_wrong_size() {
+        unsafe {
+            _ = PtrPtrVec::<u8>::iter_over_xsso::<f64>(ptr::null(), 1);
+        }
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    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 = vec![-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);
+    }
 }
--- a/libpam-sys/src/structs.rs	Sun Jun 29 20:13:03 2025 -0400
+++ b/libpam-sys/src/structs.rs	Mon Jun 30 01:40:28 2025 -0400
@@ -44,7 +44,12 @@
 /// see the [`helpers`](crate::helpers) module.
 pub type ConversationCallback = unsafe extern "C" fn(
     num_msg: c_int,
+    // This is a *const *const because accessing memory from a reference
+    // outside its bounds is undefined behavior, and *messages is an array
+    // in X/SSO PAM impls.
     messages: *const *const Message,
+    // This is a &mut *mut because the caller sets the pointer in `responses`
+    // but does not mess around outside its memory space.
     responses: &mut *mut Response,
     appdata: *const AppData,
 ) -> c_int;