Mercurial > crates > nonstick
changeset 89:dd3e9c4bcde3
Simplify memory management in Questions.
When we're sending Questions to the client, we don't need them to be
C-managed, we just need the pointers going to the right place.
This replaces a bunch of Question management cruft with Vecs and Boxes.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Fri, 13 Jun 2025 05:22:48 -0400 |
parents | c9fc7e6257d3 |
children | f6186e41399b |
files | src/libpam/conversation.rs src/libpam/pam_ffi.rs src/libpam/question.rs |
diffstat | 3 files changed, 156 insertions(+), 238 deletions(-) [+] |
line wrap: on
line diff
--- a/src/libpam/conversation.rs Tue Jun 10 05:36:58 2025 -0400 +++ b/src/libpam/conversation.rs Fri Jun 13 05:22:48 2025 -0400 @@ -5,7 +5,8 @@ use crate::libpam::memory::Immovable; use crate::libpam::pam_ffi::AppData; pub use crate::libpam::pam_ffi::LibPamConversation; -use crate::libpam::question::{Indirect, IndirectTrait, Question, Questions}; +use crate::libpam::question::QuestionsTrait; +use crate::libpam::question::{Question, Questions}; use crate::ErrorCode; use crate::Result; use std::ffi::c_int; @@ -38,12 +39,11 @@ .cast::<C>() .as_mut() .ok_or(ErrorCode::ConversationError)?; - let indirect = Indirect::borrow_ptr(questions).ok_or(ErrorCode::ConversationError)?; + let indirect = Questions::borrow_ptr(questions, count as usize); let answers_ptr = answers.as_mut().ok_or(ErrorCode::ConversationError)?; // Build our owned list of Q&As from the questions we've been asked let messages: Vec<OwnedMessage> = indirect - .iter(count as usize) .map(TryInto::try_into) .collect::<Result<_>>() .map_err(|_| ErrorCode::ConversationError)?; @@ -71,7 +71,7 @@ let result = unsafe { (self.callback)( messages.len() as c_int, - questions.indirect(), + questions.ptr(), &mut response_pointer, self.appdata, )
--- a/src/libpam/pam_ffi.rs Tue Jun 10 05:36:58 2025 -0400 +++ b/src/libpam/pam_ffi.rs Fri Jun 13 05:22:48 2025 -0400 @@ -30,9 +30,11 @@ /// Pointer to the data returned in an answer. /// For most answers, this will be a [`CStr`](std::ffi::CStr), /// but for [`BinaryQAndA`](crate::conv::BinaryQAndA)s (a Linux-PAM extension), - /// this will be [`CBinaryData`](crate::libpam::memory::CBinaryData) + /// this will be [`CBinaryData`](crate::libpam::memory::CBinaryData). + /// + /// No matter what, this can be freed with a simple [`libc::free`]. pub data: *mut c_void, - /// Unused. + /// Unused. Just here for the padding. return_code: c_int, _marker: Immovable, } @@ -40,34 +42,35 @@ /// A question sent by PAM or a module to an application. /// /// PAM refers to this as a "message", but we call it a question -/// to avoid confusion with [`Message`](crate::Message). +/// to avoid confusion with [`Message`](crate::conv::Message). /// /// This question, and its internal data, is owned by its creator /// (either the module or PAM itself). #[repr(C)] +#[derive(Debug)] pub struct Question { /// The style of message to request. pub style: c_uint, /// A description of the data requested. /// - /// For most requests, this will be an owned [`CStr`](std::ffi::CStr), but for requests - /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] - /// (a Linux-PAM extension). + /// For most requests, this will be an owned [`CStr`](std::ffi::CStr), + /// but for requests with style `PAM_BINARY_PROMPT`, + /// this will be `CBinaryData` (a Linux-PAM extension). pub data: *mut c_void, pub _marker: Immovable, } /// The callback that PAM uses to get information in a conversation. /// -/// - `num_msg` is the number of messages in the `pam_message` array. +/// - `num_msg` is the number of messages in the `questions` array. /// - `questions` is a pointer to the [`Question`]s being sent to the user. /// For information about its structure, -/// see [`GenericQuestions`](super::question::GenericQuestions). +/// see [`QuestionsTrait`](super::question::QuestionsTrait). /// - `answers` is a pointer to an array of [`Answer`]s, /// which PAM sets in response to a module's request. /// This is an array of structs, not an array of pointers to a struct. -/// There should always be exactly as many `answers` as `num_msg`. -/// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed. +/// There must always be exactly as many `answers` as `num_msg`. +/// - `appdata` is the `appdata` field of the [`LibPamConversation`]. pub type ConversationCallback = unsafe extern "C" fn( num_msg: c_int, questions: *const *const Question,
--- a/src/libpam/question.rs Tue Jun 10 05:36:58 2025 -0400 +++ b/src/libpam/question.rs Fri Jun 13 05:22:48 2025 -0400 @@ -11,7 +11,7 @@ use crate::Result; use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::ffi::{c_void, CStr}; -use std::{iter, ptr, slice}; +use std::{ptr, slice}; /// Abstraction of a collection of questions to be sent in a PAM conversation. /// @@ -32,15 +32,15 @@ /// the pointer passed to `pam_conv`.) /// /// ```text -/// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question ═╗ -/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ -/// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ... -/// ╚═════════════╝ │ ... │ ┆ ╚════════════╝ -/// ┆ -/// ┆ ╔═ Question ═╗ -/// ╰┄┄> ║ style ║ -/// ║ data ┄┄┄┄┄┄╫┄┄> ... -/// ╚════════════╝ +/// points to ┌───────────────┐ ╔═ Question ═╗ +/// questions ┄┄┄┄┄┄┄┄┄┄> │ questions[0] ┄┼┄┄┄┄> ║ style ║ +/// │ questions[1] ┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ... +/// │ ... │ ┆ ╚════════════╝ +/// ┆ +/// ┆ ╔═ Question ═╗ +/// ╰┄┄> ║ style ║ +/// ║ data ┄┄┄┄┄┄╫┄┄> ... +/// ╚════════════╝ /// ``` /// /// On OpenPAM and other compatible implementations (like Solaris), @@ -48,209 +48,135 @@ /// the correct implementation as required by the XSSO specification. /// /// ```text -/// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question[] ═╗ -/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ -/// ║ count ║ └────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... -/// ╚═════════════╝ ╟──────────────╢ -/// ║ style ║ -/// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... -/// ╟──────────────╢ -/// ║ ... ║ +/// points to ┌─────────────┐ ╔═ Question[] ═╗ +/// questions ┄┄┄┄┄┄┄┄┄┄> │ *questions ┄┼┄┄┄┄┄> ║ style ║ +/// └─────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... +/// ╟──────────────╢ +/// ║ style ║ +/// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... +/// ╟──────────────╢ +/// ║ ... ║ /// ``` -#[derive(Debug)] -pub struct GenericQuestions<I: IndirectTrait> { - /// An indirection to the questions themselves, stored on the C heap. - indirect: *mut I, - /// The number of questions. - count: usize, -} - -impl<I: IndirectTrait> GenericQuestions<I> { - /// Stores the provided questions on the C heap. - pub fn new(messages: &[Message]) -> Result<Self> { - let count = messages.len(); - let mut ret = Self { - indirect: I::alloc(count), - count, - }; - // Even if we fail partway through this, all our memory will be freed. - for (question, message) in iter::zip(ret.iter_mut(), messages) { - question.try_fill(message)? - } - Ok(ret) - } +pub trait QuestionsTrait { + /// Allocates memory for this indirector and all its members. + fn new(messages: &[Message]) -> Result<Self> + where + Self: Sized; - /// The pointer to the thing with the actual list. - pub fn indirect(&self) -> *const *const Question { - self.indirect.cast() - } - - pub fn iter(&self) -> impl Iterator<Item = &Question> { - // SAFETY: we're iterating over an amount we know. - unsafe { (*self.indirect).iter(self.count) } - } - pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { - // SAFETY: we're iterating over an amount we know. - unsafe { (*self.indirect).iter_mut(self.count) } - } -} + /// Gets the pointer that is passed . + fn ptr(&self) -> *const *const Question; -impl<I: IndirectTrait> Drop for GenericQuestions<I> { - fn drop(&mut self) { - // SAFETY: We are valid and have a valid pointer. - // Once we're done, everything will be safe. - unsafe { - if let Some(indirect) = self.indirect.as_mut() { - indirect.free_contents(self.count) - } - memory::free(self.indirect); - self.indirect = ptr::null_mut(); - } - } -} - -/// The trait that each of the `Indirect` implementations implement. -/// -/// Basically a slice but with more meat. -pub trait IndirectTrait { - /// Converts a pointer into a borrowed `Self`. + /// Converts a pointer into a borrowed list of Questions. /// /// # Safety /// /// You have to provide a valid pointer. - unsafe fn borrow_ptr<'a>(ptr: *const *const Question) -> Option<&'a Self> - where - Self: Sized, - { - ptr.cast::<Self>().as_ref() - } - - /// Allocates memory for this indirector and all its members. - fn alloc(count: usize) -> *mut Self; - - /// Returns an iterator yielding the given number of messages. - /// - /// # Safety - /// - /// You have to provide the right count. - unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question>; - - /// Returns a mutable iterator yielding the given number of messages. - /// - /// # Safety - /// - /// You have to provide the right count. - unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question>; - - /// Frees everything this points to. - /// - /// # Safety - /// - /// You have to pass the right size. - unsafe fn free_contents(&mut self, count: usize); + unsafe fn borrow_ptr<'a>( + ptr: *const *const Question, + count: usize, + ) -> impl Iterator<Item = &'a Question>; } -/// An indirect reference to messages. -/// -/// This is kept separate to provide a place where we can separate -/// the pointer-to-pointer-to-list from pointer-to-list-of-pointers. #[cfg(pam_impl = "linux-pam")] -pub type Indirect = LinuxPamIndirect; +pub type Questions = LinuxPamQuestions; -/// An indirect reference to messages. -/// -/// This is kept separate to provide a place where we can separate -/// the pointer-to-pointer-to-list from pointer-to-list-of-pointers. #[cfg(not(pam_impl = "linux-pam"))] -pub type Indirect = StandardIndirect; +pub type Questions = XSsoQuestions; -pub type Questions = GenericQuestions<Indirect>; - -/// The XSSO standard version of the indirection layer between Question and Questions. +/// The XSSO standard version of the pointer train to questions. #[derive(Debug)] #[repr(C)] -pub struct StandardIndirect { - base: *mut Question, +pub struct XSsoQuestions { + /// Points to the memory address where the meat of `questions` is. + /// **The memory layout of Vec is not specified**, and we need to return + /// a pointer to the pointer, hence we have to store it here. + pointer: *const Question, + questions: Vec<Question>, _marker: Immovable, } -impl IndirectTrait for StandardIndirect { - fn alloc(count: usize) -> *mut Self { - let questions = memory::calloc(count); - let me_ptr: *mut Self = memory::calloc(1); - // SAFETY: We just allocated this, and we're putting a valid pointer in. - unsafe { - let me = &mut *me_ptr; - me.base = questions; - } - me_ptr +impl XSsoQuestions { + fn len(&self) -> usize { + self.questions.len() + } + fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { + self.questions.iter_mut() + } +} + +impl QuestionsTrait for XSsoQuestions { + fn new(messages: &[Message]) -> Result<Self> { + let questions: Result<Vec<_>> = messages.iter().map(Question::try_from).collect(); + let questions = questions?; + Ok(Self { + pointer: questions.as_ptr(), + questions, + _marker: Default::default(), + }) } - unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { - (0..count).map(|idx| &*self.base.add(idx)) - } - - unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { - (0..count).map(|idx| &mut *self.base.add(idx)) + fn ptr(&self) -> *const *const Question { + &self.pointer as *const *const Question } - unsafe fn free_contents(&mut self, count: usize) { - let msgs = slice::from_raw_parts_mut(self.base, count); - for msg in msgs { - msg.clear() - } - memory::free(self.base); - self.base = ptr::null_mut() + unsafe fn borrow_ptr<'a>( + ptr: *const *const Question, + count: usize, + ) -> impl Iterator<Item = &'a Question> { + slice::from_raw_parts(*ptr, count).iter() } } -/// The Linux version of the indirection layer between Question and Questions. +/// The Linux version of the pointer train to questions. #[derive(Debug)] #[repr(C)] -pub struct LinuxPamIndirect { - base: [*mut Question; 0], +pub struct LinuxPamQuestions { + #[allow(clippy::vec_box)] // we need to do this. + /// The place where the questions are. + questions: Vec<Box<Question>>, _marker: Immovable, } -impl IndirectTrait for LinuxPamIndirect { - fn alloc(count: usize) -> *mut Self { - // SAFETY: We're only allocating, and when we're done, - // everything will be in a known-good state. - let me_ptr: *mut Self = memory::calloc::<*mut Question>(count).cast(); - unsafe { - let me = &mut *me_ptr; - let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count); - for entry in ptr_list { - *entry = memory::calloc(1); - } - } - me_ptr +impl LinuxPamQuestions { + fn len(&self) -> usize { + self.questions.len() } - unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { - (0..count).map(|idx| &**self.base.as_ptr().add(idx)) + fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { + self.questions.iter_mut().map(AsMut::as_mut) } +} - unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { - (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx)) +impl QuestionsTrait for LinuxPamQuestions { + fn new(messages: &[Message]) -> Result<Self> { + let questions: Result<_> = messages + .iter() + .map(|msg| Question::try_from(msg).map(Box::new)) + .collect(); + Ok(Self { + questions: questions?, + _marker: Default::default(), + }) } - unsafe fn free_contents(&mut self, count: usize) { - let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count); - for msg in msgs { - if let Some(msg) = msg.as_mut() { - msg.clear(); - } - memory::free(*msg); - *msg = ptr::null_mut(); - } + fn ptr(&self) -> *const *const Question { + self.questions.as_ptr().cast() + } + + unsafe fn borrow_ptr<'a>( + ptr: *const *const Question, + count: usize, + ) -> impl Iterator<Item = &'a Question> { + slice::from_raw_parts(ptr.cast::<&Question>(), count) + .iter() + .copied() } } /// The C enum values for messages shown to the user. #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] #[repr(u32)] -pub enum Style { +enum Style { /// Requests information from the user; will be masked when typing. PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF, /// Requests information from the user; will not be masked. @@ -270,47 +196,7 @@ BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT, } -impl Default for Question { - fn default() -> Self { - Self { - style: Default::default(), - data: ptr::null_mut(), - _marker: Default::default(), - } - } -} - impl Question { - /// Replaces the contents of this question with the question - /// from the message. - /// - /// If the message is not valid (invalid message type, bad contents, etc.), - /// this will fail. - pub fn try_fill(&mut self, msg: &Message) -> Result<()> { - let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); - // We will only allocate heap data if we have a valid input. - let (style, data): (_, *mut c_void) = match *msg { - Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), - Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), - Message::Error(p) => alloc(Style::ErrorMsg, p.question()), - Message::Info(p) => alloc(Style::TextInfo, p.question()), - #[cfg(feature = "linux-pam-extensions")] - Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), - #[cfg(feature = "linux-pam-extensions")] - Message::BinaryPrompt(p) => Ok(( - Style::BinaryPrompt, - CBinaryData::alloc(p.question())?.cast(), - )), - #[cfg(not(feature = "linux-pam-extensions"))] - Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError), - }?; - // Now that we know everything is valid, fill ourselves in. - self.clear(); - self.style = style.into(); - self.data = data; - Ok(()) - } - /// Gets this message's data pointer as a string. /// /// # Safety @@ -334,9 +220,38 @@ .map(Into::into) .unwrap_or_default() } +} - /// Zeroes out the data stored here. - fn clear(&mut self) { +impl TryFrom<&Message<'_>> for Question { + type Error = ErrorCode; + fn try_from(msg: &Message) -> Result<Self> { + let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); + // We will only allocate heap data if we have a valid input. + let (style, data): (_, *mut c_void) = match *msg { + Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), + Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), + Message::Error(p) => alloc(Style::ErrorMsg, p.question()), + Message::Info(p) => alloc(Style::TextInfo, p.question()), + #[cfg(feature = "linux-pam-extensions")] + Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), + #[cfg(feature = "linux-pam-extensions")] + Message::BinaryPrompt(p) => Ok(( + Style::BinaryPrompt, + CBinaryData::alloc(p.question())?.cast(), + )), + #[cfg(not(feature = "linux-pam-extensions"))] + Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError), + }?; + Ok(Self { + style: style.into(), + data, + _marker: Default::default(), + }) + } +} + +impl Drop for Question { + fn drop(&mut self) { // SAFETY: We either created this data or we got it from PAM. // After this function is done, it will be zeroed out. unsafe { @@ -409,7 +324,7 @@ use super::super::*; #[test] fn standard() { - let interrogation = GenericQuestions::<$typ>::new(&[ + let interrogation = <$typ>::new(&[ MaskedQAndA::new("hocus pocus").message(), QAndA::new("what").message(), QAndA::new("who").message(), @@ -417,10 +332,10 @@ ErrorMsg::new("gasp").message(), ]) .unwrap(); - let indirect = interrogation.indirect(); + let indirect = interrogation.ptr(); - let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap(); - let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } + let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) }; + let messages: Vec<OwnedMessage> = remade .map(TryInto::try_into) .collect::<Result<_>>() .unwrap(); @@ -436,7 +351,7 @@ #[cfg(not(feature = "linux-pam-extensions"))] fn no_linux_extensions() { use crate::conv::{BinaryQAndA, RadioQAndA}; - GenericQuestions::<$typ>::new(&[ + <$typ>::new(&[ BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), RadioQAndA::new("you must choose").message(), ]).unwrap_err(); @@ -445,14 +360,14 @@ #[test] #[cfg(feature = "linux-pam-extensions")] fn linux_extensions() { - let interrogation = GenericQuestions::<$typ>::new(&[ + let interrogation = <$typ>::new(&[ BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), RadioQAndA::new("you must choose").message(), ]).unwrap(); - let indirect = interrogation.indirect(); + let indirect = interrogation.ptr(); - let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap(); - let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } + let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) }; + let messages: Vec<OwnedMessage> = unsafe { remade } .map(TryInto::try_into) .collect::<Result<_>>() .unwrap(); @@ -463,6 +378,6 @@ } }} - tests!(test_xsso<StandardIndirect>); - tests!(test_linux<LinuxPamIndirect>); + tests!(test_xsso<XSsoQuestions>); + tests!(test_linux<LinuxPamQuestions>); }