Mercurial > crates > nonstick
changeset 78:002adfb98c5c
Rename files, reorder structs, remove annoying BorrowedBinaryData type.
This is basically a cleanup change. Also it adds tests.
- Renames the files with Questions and Answers to question and answer.
- Reorders the structs in those files to put the important ones first.
- Removes the BorrowedBinaryData type. It was a bad idea all along.
Instead, we just use (&[u8], u8).
- Adds some tests because I just can't help myself.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Sun, 08 Jun 2025 03:48:40 -0400 |
parents | 351bdc13005e |
children | 2128123b9406 |
files | src/conv.rs src/handle.rs src/libpam/answer.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/memory.rs src/libpam/message.rs src/libpam/mod.rs src/libpam/module.rs src/libpam/question.rs src/libpam/response.rs src/module.rs |
diffstat | 12 files changed, 858 insertions(+), 863 deletions(-) [+] |
line wrap: on
line diff
--- a/src/conv.rs Sun Jun 08 01:03:46 2025 -0400 +++ b/src/conv.rs Sun Jun 08 03:48:40 2025 -0400 @@ -3,10 +3,11 @@ // Temporarily allowed until we get the actual conversation functions hooked up. #![allow(dead_code)] -use crate::constants::Result; -use crate::ErrorCode; +use crate::constants::{ErrorCode, Result}; use secure_string::SecureString; use std::cell::Cell; +use std::fmt; +use std::result::Result as StdResult; /// The types of message and request that can be sent to a user. /// @@ -68,8 +69,16 @@ } impl<'a> $name<'a> { + #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")] + pub fn new(question: $qt) -> Self { + Self { + q: question, + a: Cell::new(Err(ErrorCode::ConversationError)), + } + } + /// Converts this Q&A into a [`Message`] for the [`Conversation`]. - pub fn message(&self) -> Message { + pub fn message(&self) -> Message<'_> { $val(self) } @@ -96,18 +105,14 @@ self.a.into_inner() } } - }; -} -macro_rules! ask { - ($t:ident) => { - impl<'a> $t<'a> { - #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")] - pub fn new(question: &'a str) -> Self { - Self { - q: question, - a: Cell::new(Err(ErrorCode::ConversationError)), - } + // shout out to stackoverflow user ballpointben for this lazy impl: + // https://stackoverflow.com/a/78871280/39808 + impl fmt::Debug for $name<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { + #[derive(Debug)] + struct $name<'a> { q: $qt } + fmt::Debug::fmt(&$name { q: self.q }, f) } } }; @@ -120,7 +125,6 @@ "" "In other words, a password entry prompt." ); -ask!(MaskedQAndA); q_and_a!( QAndA<'a, Q=&'a str, A=String>, @@ -131,7 +135,6 @@ "When the user types, their input will be shown to them." "It can be used for things like usernames." ); -ask!(QAndA); q_and_a!( RadioQAndA<'a, Q=&'a str, A=String>, @@ -142,10 +145,9 @@ "questions, but nowhere in the documentation is it specified" "what the format of the answer will be, or how this should be shown." ); -ask!(RadioQAndA); q_and_a!( - BinaryQAndA<'a, Q=BorrowedBinaryData<'a>, A=BinaryData>, + BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>, Message::BinaryPrompt, "Asks for binary data. (Linux-PAM extension)" "" @@ -156,65 +158,16 @@ "The `data_type` tag is a value that is simply passed through" "to the application. PAM does not define any meaning for it." ); -impl<'a> BinaryQAndA<'a> { - /// Creates a prompt for the given binary data. - /// - /// The `data_type` is a tag you can use for communication between - /// the module and the application. Its meaning is undefined by PAM. - pub fn new(data: &'a [u8], data_type: u8) -> Self { - BorrowedBinaryData { data, data_type }.into() - } -} - -impl<'a> From<BorrowedBinaryData<'a>> for BinaryQAndA<'a> { - fn from(q: BorrowedBinaryData<'a>) -> Self { - Self { - q, - a: Cell::new(Err(ErrorCode::ConversationError)), - } - } -} - -impl<'a> From<&'a BinaryData> for BinaryQAndA<'a> { - fn from(src: &'a BinaryData) -> Self { - BorrowedBinaryData::from(src).into() - } -} - -/// A version of [`BinaryData`] where the `data` is borrowed. -#[derive(Copy, Clone, Debug, Default)] -pub struct BorrowedBinaryData<'a> { - data: &'a [u8], - data_type: u8, -} - -impl<'a> BorrowedBinaryData<'a> { - /// Creates a new BinaryQuestion as a view over the given data. - pub fn new(data: &'a [u8], data_type: u8) -> Self { - Self { data, data_type } - } - - /// Gets the data of this question. - pub fn data(&self) -> &[u8] { - self.data - } - - /// Gets the "type" of this data. - pub fn data_type(&self) -> u8 { - self.data_type - } -} - -impl<'a> From<&'a BinaryData> for BorrowedBinaryData<'a> { - fn from(value: &'a BinaryData) -> Self { - Self::new(&value.data, value.data_type) - } -} /// Owned binary data. /// -/// For borrowed data, see [`BorrowedBinaryData`]. -/// You can take ownership of the stored data with `.into::<Vec<u8>>()`. +/// You can take ownership of the stored data by destructuring it: +/// +/// ``` +/// # use nonstick::BinaryData; +/// # let binary_data = BinaryData::new(vec![99, 88, 77], 66); +/// let (data, data_type) = binary_data.into(); +/// ``` #[derive(Debug, Default, PartialEq)] pub struct BinaryData { data: Vec<u8>, @@ -223,28 +176,32 @@ impl BinaryData { /// Creates a `BinaryData` with the given contents and type. - pub fn new(data: Vec<u8>, data_type: u8) -> Self { - Self { data, data_type } + pub fn new(data: impl Into<Vec<u8>>, data_type: u8) -> Self { + Self { data: data.into(), data_type } } - /// A borrowed view of the data here. - pub fn data(&self) -> &[u8] { - &self.data - } - /// The type of the data stored in this. - pub fn data_type(&self) -> u8 { - self.data_type +} + +impl From<BinaryData> for (Vec<u8>, u8) { + fn from(value: BinaryData) -> Self { + (value.data, value.data_type) } } -impl<'a> From<BorrowedBinaryData<'a>> for BinaryData { - fn from(value: BorrowedBinaryData) -> Self { +impl From<(&'_[u8], u8)> for BinaryData { + fn from((data, data_type): (&'_[u8], u8)) -> Self { Self { - data: value.data.to_vec(), - data_type: value.data_type, + data: data.to_vec(), + data_type, } } } +impl<'a> From<&'a BinaryData> for (&'a[u8], u8) { + fn from(value: &'a BinaryData) -> Self { + (&value.data, value.data_type) + } +} + impl From<BinaryData> for Vec<u8> { /// Takes ownership of the data stored herein. fn from(value: BinaryData) -> Self { @@ -261,15 +218,6 @@ "should still call [`set_answer`][`QAndA::set_answer`] to verify that" "the message has been displayed (or actively discarded)." ); -impl<'a> InfoMsg<'a> { - /// Creates an informational message to send to the user. - pub fn new(message: &'a str) -> Self { - Self { - q: message, - a: Cell::new(Err(ErrorCode::ConversationError)), - } - } -} q_and_a!( ErrorMsg<'a, Q = &'a str, A = ()>, @@ -279,17 +227,7 @@ "While this does not have an answer, [`Conversation`] implementations" "should still call [`set_answer`][`QAndA::set_answer`] to verify that" "the message has been displayed (or actively discarded)." - ); -impl<'a> ErrorMsg<'a> { - /// Creates an error message to send to the user. - pub fn new(message: &'a str) -> Self { - Self { - q: message, - a: Cell::new(Err(ErrorCode::ConversationError)), - } - } -} /// A channel for PAM modules to request information from the user. /// @@ -315,10 +253,12 @@ /// Conversation: /// /// ``` -/// use nonstick::conv::{Conversation, Message, conversation_func}; +/// use nonstick::conv::{conversation_func, Conversation, Message}; /// mod some_library { /// # use nonstick::Conversation; -/// pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */ } +/// pub fn get_auth_data(conv: &mut impl Conversation) { +/// /* ... */ +/// } /// } /// /// fn my_terminal_prompt(messages: &[Message]) { @@ -364,15 +304,16 @@ /// or to use a `SimpleConversation` as a `Conversation`: /// /// ``` +/// use nonstick::{Conversation, SimpleConversation}; /// use secure_string::SecureString; -/// use nonstick::{Conversation, SimpleConversation}; /// # use nonstick::{BinaryData, Result}; /// mod some_library { /// # use nonstick::Conversation; -/// pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */ } +/// pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */ +/// } /// } /// -/// struct MySimpleConvo { /* ... */ } +/// struct MySimpleConvo {/* ... */} /// # impl MySimpleConvo { fn new() -> Self { Self{} } } /// /// impl SimpleConversation for MySimpleConvo { @@ -397,7 +338,7 @@ /// # todo!() /// # } /// # -/// # fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> { +/// # fn binary_prompt(&mut self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> { /// # todo!() /// # } /// } @@ -413,7 +354,7 @@ /// The wrapper takes each message received in [`Conversation::communicate`] /// and passes them one-by-one to the appropriate method, /// then collects responses to return. - fn as_conversation(&mut self) -> Demux<Self> + fn as_conversation(&mut self) -> Demux<'_, Self> where Self: Sized, { @@ -432,18 +373,18 @@ /// Sends an informational message to the user. fn info_msg(&mut self, message: &str); /// Requests binary data from the user (a Linux-PAM extension). - fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>; + fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData>; } macro_rules! conv_fn { - ($fn_name:ident($($param:ident: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { + ($fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> { let prompt = <$msg>::new($($param),*); self.communicate(&[prompt.message()]); prompt.answer() } }; - ($fn_name:ident($($param:ident: $pt:ty),+) { $msg:ty }) => { + ($fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { fn $fn_name(&mut self, $($param: $pt),*) { self.communicate(&[<$msg>::new($($param),*).message()]); } @@ -456,7 +397,7 @@ conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA }); conv_fn!(error_msg(message: &str) { ErrorMsg }); conv_fn!(info_msg(message: &str) { InfoMsg }); - conv_fn!(binary_prompt(data: &[u8], data_type: u8) -> BinaryData { BinaryQAndA }); + conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA }); } /// A [`Conversation`] which asks the questions one at a time. @@ -485,7 +426,7 @@ } Message::BinaryPrompt(prompt) => { let q = prompt.question(); - prompt.set_answer(self.0.binary_prompt(q.data, q.data_type)) + prompt.set_answer(self.0.binary_prompt(q)) } } } @@ -495,8 +436,8 @@ #[cfg(test)] mod tests { use super::{ - BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, - RadioQAndA, Result, SecureString, SimpleConversation, + BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA, + Result, SecureString, SimpleConversation, }; use crate::constants::ErrorCode; use crate::BinaryData; @@ -533,9 +474,8 @@ self.info_ran = true; assert_eq!("did you know", message); } - fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> { - assert_eq!(&[10, 9, 8], data); - assert_eq!(66, data_type); + fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { + assert_eq!((&[10, 9, 8][..], 66), data_and_type); Ok(BinaryData::new(vec![5, 5, 5], 5)) } } @@ -573,7 +513,7 @@ let mut conv = tester.as_conversation(); let radio = RadioQAndA::new("channel?"); - let bin = BinaryQAndA::new(&[10, 9, 8], 66); + let bin = BinaryQAndA::new((&[10, 9, 8], 66)); conv.communicate(&[radio.message(), bin.message()]); assert_eq!("zero", radio.answer().unwrap()); @@ -605,8 +545,7 @@ ask.set_answer(Ok("open sesame".into())) } Message::BinaryPrompt(prompt) => { - assert_eq!(&[1, 2, 3], prompt.question().data); - assert_eq!(69, prompt.question().data_type); + assert_eq!((&[1, 2, 3][..], 69), prompt.question()); prompt.set_answer(Ok(BinaryData::new(vec![3, 2, 1], 42))) } Message::RadioPrompt(ask) => { @@ -636,7 +575,7 @@ assert_eq!("yes", tester.radio_prompt("radio?").unwrap()); assert_eq!( BinaryData::new(vec![3, 2, 1], 42), - tester.binary_prompt(&[1, 2, 3], 69).unwrap(), + tester.binary_prompt((&[1, 2, 3], 69)).unwrap(), ) } assert_eq!(
--- a/src/handle.rs Sun Jun 08 01:03:46 2025 -0400 +++ b/src/handle.rs Sun Jun 08 03:48:40 2025 -0400 @@ -209,18 +209,18 @@ trait_item!( set = set_authtok_item, item = "PAM_AUTHTOK", - see = PamModuleHandle::authtok_item, - "Sets the user's authentication token (e.g., password)." + see = PamModuleOnly::authtok_item, + "Gets the user's authentication token (e.g., password)." "" "This is usually set automatically when " - "[`get_authtok`](PamModuleHandle::get_authtok) is called, " + "[`get_authtok`](PamModuleOnly::get_authtok) is called, " "but can be manually set." ); trait_item!( set = set_old_authtok_item, item = "PAM_OLDAUTHTOK", - see = PamModuleHandle::old_authtok_item, + see = PamModuleOnly::old_authtok_item, "Sets the user's \"old authentication token\" when changing passwords." "" "This is usually set automatically by PAM." @@ -308,7 +308,7 @@ trait_item!( get = old_authtok_item, item = "PAM_OLDAUTHTOK", - see = PamHandle::set_old_authtok_item, + see = PamShared::set_old_authtok_item, "Gets the user's old authentication token when changing passwords." "" "This should only ever be called by *password-change* PAM modules."
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/answer.rs Sun Jun 08 03:48:40 2025 -0400 @@ -0,0 +1,333 @@ +//! Types used to communicate data from the application to the module. + +use crate::libpam::conversation::OwnedMessage; +use crate::libpam::memory; +use crate::libpam::memory::{CBinaryData, Immovable}; +use crate::{ErrorCode, Result}; +use std::ffi::{c_int, c_void, CStr}; +use std::ops::{Deref, DerefMut}; +use std::{iter, mem, ptr, slice}; + +/// The corridor via which the answer to Messages navigate through PAM. +#[derive(Debug)] +pub struct Answers { + base: *mut Answer, + count: usize, +} + +impl Answers { + /// 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()), + count: value.len(), + }; + // Even if we fail during this process, we still end up freeing + // all allocated answer memory. + for (input, output) in iter::zip(value, outputs.iter_mut()) { + match input { + OwnedMessage::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.unsecure())?, + OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?, + OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?, + OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, + OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, + OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?, + } + } + Ok(outputs) + } + + /// Converts this into a `*Answer` for passing to PAM. + /// + /// 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 { + let ret = self.base; + mem::forget(self); + ret + } + + /// Takes ownership of a list of answers allocated on the C heap. + /// + /// # Safety + /// + /// 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 { + Answers { base, count } + } +} + +impl Deref for Answers { + 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) } + } +} + +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) } + } +} + +impl Drop for Answers { + fn drop(&mut self) { + // SAFETY: We allocated this ourselves, or it was provided to us by PAM. + unsafe { + for answer in self.iter_mut() { + answer.free_contents() + } + memory::free(self.base) + } + } +} + +#[repr(transparent)] +#[derive(Debug)] +pub struct TextAnswer(Answer); + +impl TextAnswer { + /// Interprets the provided `Answer` as a text answer. + /// + /// # Safety + /// + /// It's up to you to provide an answer that is a `TextAnswer`. + pub unsafe fn upcast(from: &mut Answer) -> &mut Self { + // SAFETY: We're provided a valid reference. + &mut *(from as *mut Answer).cast::<Self>() + } + + /// 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.cast(); + 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) + } + } + + /// Zeroes out the answer data, frees it, and points our data to `null`. + /// + /// 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) { + // 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() + } + } +} + +/// A [`Answer`] with [`CBinaryData`] in it. +#[repr(transparent)] +#[derive(Debug)] +pub struct BinaryAnswer(Answer); + +impl BinaryAnswer { + /// Interprets the provided [`Answer`] as a binary answer. + /// + /// # Safety + /// + /// It's up to you to provide an answer that is a `BinaryAnswer`. + pub unsafe fn upcast(from: &mut Answer) -> &mut Self { + // SAFETY: We're provided a valid reference. + &mut *(from as *mut Answer).cast::<Self>() + } + + /// Fills in a [`Answer`] with the provided binary data. + /// + /// The `data_type` is a tag you can use for whatever. + /// It is passed through PAM unchanged. + /// + /// The referenced data is copied to the C heap. + /// 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.cast(); + Ok(()) + } + + /// Gets the binary data in this answer. + pub fn data(&self) -> Option<&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. + unsafe { self.0.data.cast::<CBinaryData>().as_ref() } + } + + /// Zeroes out the answer data, frees it, and points our data to `null`. + /// + /// 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 zero_contents(&mut self) { + // 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 { + let data_ref = self.0.data.cast::<CBinaryData>().as_mut(); + if let Some(d) = data_ref { + d.zero_contents() + } + memory::free(self.0.data); + self.0.data = ptr::null_mut() + } + } +} + +/// Generic version of answer data. +/// +/// This has the same structure as [`BinaryAnswer`] +/// and [`TextAnswer`]. +#[repr(C)] +#[derive(Debug)] +pub struct Answer { + /// Pointer to the data returned in an answer. + /// For most answers, this will be a [`CStr`], but for answers to + /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`] + /// (a Linux-PAM extension). + data: *mut c_void, + /// Unused. + return_code: c_int, + _marker: Immovable, +} + +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(); + } + } +} + +#[cfg(test)] +mod tests { + use super::{Answer, Answers, BinaryAnswer, TextAnswer}; + use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, QAndA, RadioQAndA}; + use crate::libpam::conversation::OwnedMessage; + use crate::BinaryData; + use crate::libpam::memory; + + #[test] + fn test_round_trip() { + let binary_msg = { + let qa = BinaryQAndA::new((&[][..], 0)); + qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99))); + OwnedMessage::BinaryPrompt(qa) + }; + + macro_rules! answered { + ($typ:ty, $msg:path, $data:expr) => {{ + let qa = <$typ>::new(""); + qa.set_answer(Ok($data)); + $msg(qa) + }}; + } + + let answers = vec![ + binary_msg, + answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()), + answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()), + answered!(ErrorMsg, OwnedMessage::Error, ()), + answered!(InfoMsg, OwnedMessage::Info, ()), + answered!( + RadioQAndA, + OwnedMessage::RadioPrompt, + "beep boop".to_owned() + ), + ]; + let n = answers.len(); + let sent = Answers::build(answers).unwrap(); + let heap_answers = sent.into_ptr(); + let mut received = unsafe { Answers::from_c_heap(heap_answers, n) }; + + let assert_text = |want, raw| { + let up = unsafe { TextAnswer::upcast(raw) }; + assert_eq!(want, up.contents().unwrap()); + up.free_contents(); + assert_eq!("", up.contents().unwrap()); + }; + let assert_bin = |want, raw| { + let up = unsafe { BinaryAnswer::upcast(raw) }; + assert_eq!(BinaryData::from(want), up.data().into()); + up.zero_contents(); + assert_eq!(BinaryData::default(), up.data().into()); + }; + if let [zero, one, two, three, four, five] = &mut received[..] { + assert_bin((&[1, 2, 3][..], 99), zero); + assert_text("whats going on", one); + assert_text("well then", two); + assert_text("", three); + assert_text("", four); + assert_text("beep boop", five); + } else { + panic!("received wrong size {len}!", len = received.len()) + } + } + + #[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 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) } + } + + #[test] + fn test_binary_answer() { + let answer_ptr: *mut Answer = memory::calloc(1); + let answer = unsafe { &mut *answer_ptr }; + let real_data = BinaryData::new(vec![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) }; + assert_eq!(real_data, bin_answer.data().into()); + 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; 10_000_000_000]; + 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) } + } +}
--- a/src/libpam/conversation.rs Sun Jun 08 01:03:46 2025 -0400 +++ b/src/libpam/conversation.rs Sun Jun 08 03:48:40 2025 -0400 @@ -1,10 +1,9 @@ use crate::conv::{ - BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, - RadioQAndA, + BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA, }; +use crate::libpam::answer::{Answer, Answers, BinaryAnswer, TextAnswer}; use crate::libpam::memory::Immovable; -use crate::libpam::message::{Indirect, Questions}; -use crate::libpam::response::{Answer, Answers, BinaryAnswer, TextAnswer}; +use crate::libpam::question::{Indirect, Questions}; use crate::ErrorCode; use crate::Result; use std::ffi::c_int; @@ -76,7 +75,7 @@ // Build our owned list of Q&As from the questions we've been asked let messages: Vec<OwnedMessage> = indirect .iter(count as usize) - .map(OwnedMessage::try_from) + .map(TryInto::try_into) .collect::<Result<_>>() .map_err(|_| ErrorCode::ConversationError)?; // Borrow all those Q&As and ask them @@ -95,16 +94,13 @@ impl Conversation for LibPamConversation<'_> { fn communicate(&mut self, messages: &[Message]) { let internal = || { - let mut msgs_to_send = Questions::alloc(messages.len()); - for (dst, src) in iter::zip(msgs_to_send.iter_mut(), messages.iter()) { - dst.fill(src).map_err(|_| ErrorCode::ConversationError)? - } + let questions = Questions::new(messages)?; let mut response_pointer = std::ptr::null_mut(); // SAFETY: We're calling into PAM with valid everything. let result = unsafe { (self.callback)( messages.len() as c_int, - msgs_to_send.indirect(), + questions.indirect(), &mut response_pointer, self.appdata, ) @@ -128,6 +124,7 @@ } /// Like [`Message`], but this time we own the contents. +#[derive(Debug)] pub enum OwnedMessage<'a> { MaskedPrompt(MaskedQAndA<'a>), Prompt(QAndA<'a>), @@ -157,9 +154,11 @@ /// You are responsible for ensuring that the src-dst pair matches. unsafe fn convert(msg: &Message, resp: &mut Answer) { macro_rules! fill_text { - ($dst:ident, $src:ident) => {{let text_resp = unsafe {TextAnswer::upcast($src)}; - $dst.set_answer(text_resp.contents().map(Into::into));}} -} + ($dst:ident, $src:ident) => {{ + let text_resp = unsafe { TextAnswer::upcast($src) }; + $dst.set_answer(text_resp.contents().map(Into::into)); + }}; + } match *msg { Message::MaskedPrompt(qa) => fill_text!(qa, resp), Message::Prompt(qa) => fill_text!(qa, resp),
--- a/src/libpam/handle.rs Sun Jun 08 01:03:46 2025 -0400 +++ b/src/libpam/handle.rs Sun Jun 08 03:48:40 2025 -0400 @@ -54,7 +54,7 @@ } /// Gets the `PAM_CONV` item from the handle. - fn conversation_item(&mut self) -> Result<&mut LibPamConversation> { + fn conversation_item(&mut self) -> Result<&mut LibPamConversation<'_>> { let output: *mut LibPamConversation = ptr::null_mut(); let result = unsafe { super::pam_get_item(
--- a/src/libpam/memory.rs Sun Jun 08 01:03:46 2025 -0400 +++ b/src/libpam/memory.rs Sun Jun 08 03:48:40 2025 -0400 @@ -1,12 +1,28 @@ //! Things for dealing with memory. -use crate::conv::BorrowedBinaryData; use crate::Result; use crate::{BinaryData, ErrorCode}; -use std::ffi::{c_char, c_void, CStr, CString}; +use std::ffi::{c_char, CStr, CString}; use std::marker::{PhantomData, PhantomPinned}; use std::{ptr, slice}; +/// Allocates `count` elements to hold `T`. +#[inline] +pub fn calloc<T>(count: usize) -> *mut T { + // SAFETY: it's always safe to allocate! Leaking memory is fun! + unsafe { libc::calloc(count, size_of::<T>()) }.cast() +} + +/// Wrapper for [`libc::free`] to make debugging calls/frees easier. +/// +/// # Safety +/// +/// If you double-free, it's all your fault. +#[inline] +pub unsafe fn free<T>(p: *mut T) { + libc::free(p.cast()) +} + /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. #[repr(C)] #[derive(Debug)] @@ -62,7 +78,7 @@ /// Allocates a string with the given contents on the C heap. /// -/// This is like [`CString::new`](std::ffi::CString::new), but: +/// 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. @@ -71,11 +87,12 @@ if data.contains(&0) { return Err(ErrorCode::ConversationError); } + let data_alloc: *mut c_char = calloc(data.len() + 1); + // SAFETY: we just allocated this and we have enough room. unsafe { - let data_alloc = libc::calloc(data.len() + 1, 1); - libc::memcpy(data_alloc, data.as_ptr().cast(), data.len()); - Ok(data_alloc.cast()) + libc::memcpy(data_alloc.cast(), data.as_ptr().cast(), data.len()); } + Ok(data_alloc) } /// Writes zeroes over the contents of a C string. @@ -85,9 +102,9 @@ /// # Safety /// /// It's up to you to provide a valid C string. -pub unsafe fn zero_c_string(cstr: *mut c_void) { +pub unsafe fn zero_c_string(cstr: *mut c_char) { if !cstr.is_null() { - libc::memset(cstr, 0, libc::strlen(cstr.cast())); + libc::memset(cstr.cast(), 0, libc::strlen(cstr.cast())); } } @@ -110,20 +127,20 @@ impl CBinaryData { /// Copies the given data to a new BinaryData on the heap. - pub fn alloc(source: &[u8], data_type: u8) -> Result<*mut CBinaryData> { + pub fn alloc((data, data_type): (&[u8], u8)) -> Result<*mut CBinaryData> { let buffer_size = - u32::try_from(source.len() + 5).map_err(|_| ErrorCode::ConversationError)?; + u32::try_from(data.len() + 5).map_err(|_| ErrorCode::ConversationError)?; // SAFETY: We're only allocating here. - let data = unsafe { - let dest_buffer: *mut CBinaryData = libc::malloc(buffer_size as usize).cast(); - let data = &mut *dest_buffer; - data.total_length = buffer_size.to_be_bytes(); - data.data_type = data_type; - let dest = data.data.as_mut_ptr(); - libc::memcpy(dest.cast(), source.as_ptr().cast(), source.len()); + let dest = unsafe { + let dest_buffer: *mut CBinaryData = calloc::<u8>(buffer_size as usize).cast(); + let dest = &mut *dest_buffer; + dest.total_length = buffer_size.to_be_bytes(); + dest.data_type = data_type; + let dest = dest.data.as_mut_ptr(); + libc::memcpy(dest.cast(), data.as_ptr().cast(), data.len()); dest_buffer }; - Ok(data) + Ok(dest) } fn length(&self) -> usize { @@ -141,39 +158,42 @@ } } -impl<'a> From<&'a CBinaryData> for BorrowedBinaryData<'a> { +impl<'a> From<&'a CBinaryData> for (&'a[u8], u8) { fn from(value: &'a CBinaryData) -> Self { - BorrowedBinaryData::new( - unsafe { slice::from_raw_parts(value.data.as_ptr(), value.length()) }, - value.data_type, - ) + (unsafe { slice::from_raw_parts(value.data.as_ptr(), value.length()) }, + value.data_type ) } } impl From<Option<&'_ CBinaryData>> for BinaryData { fn from(value: Option<&CBinaryData>) -> Self { - value.map(BorrowedBinaryData::from).map(Into::into).unwrap_or_default() + // This is a dumb trick but I like it because it is simply the presence + // of `.map(|(x, y)| (x, y))` in the middle of this that gives + // type inference the hint it needs to make this work. + value + .map(Into::into) + .map(|(data, data_type)| (data, data_type)) + .map(Into::into) + .unwrap_or_default() } } #[cfg(test)] mod tests { - use super::{copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string}; - use crate::ErrorCode; - use std::ffi::CString; + use super::{free, ErrorCode, CString, copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string}; #[test] fn test_strings() { let str = malloc_str("hello there").unwrap(); malloc_str("hell\0 there").unwrap_err(); unsafe { - let copied = copy_pam_string(str.cast()).unwrap(); + let copied = copy_pam_string(str).unwrap(); assert_eq!("hello there", copied); - zero_c_string(str.cast()); + zero_c_string(str); let idx_three = str.add(3).as_mut().unwrap(); *idx_three = 0x80u8 as i8; - let zeroed = copy_pam_string(str.cast()).unwrap(); + let zeroed = copy_pam_string(str).unwrap(); assert!(zeroed.is_empty()); - libc::free(str.cast()); + free(str); } }
--- a/src/libpam/message.rs Sun Jun 08 01:03:46 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,348 +0,0 @@ -//! Data and types dealing with PAM messages. - -use crate::constants::InvalidEnum; -use crate::libpam::conversation::OwnedMessage; -use crate::libpam::memory; -use crate::libpam::memory::{CBinaryData, Immovable}; -use crate::ErrorCode; -use crate::Result; -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; -use std::ffi::{c_int, c_void, CStr}; -use std::result::Result as StdResult; -use std::{ptr, slice}; -use crate::conv::{BorrowedBinaryData, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA}; - -/// The C enum values for messages shown to the user. -#[derive(Debug, PartialEq, FromPrimitive)] -pub enum Style { - /// Requests information from the user; will be masked when typing. - PromptEchoOff = 1, - /// Requests information from the user; will not be masked. - PromptEchoOn = 2, - /// An error message. - ErrorMsg = 3, - /// An informational message. - TextInfo = 4, - /// Yes/No/Maybe conditionals. A Linux-PAM extension. - RadioType = 5, - /// For server–client non-human interaction. - /// - /// NOT part of the X/Open PAM specification. - /// A Linux-PAM extension. - BinaryPrompt = 7, -} - -impl TryFrom<c_int> for Style { - type Error = InvalidEnum<Self>; - fn try_from(value: c_int) -> StdResult<Self, Self::Error> { - Self::from_i32(value).ok_or(value.into()) - } -} - -impl From<Style> for c_int { - fn from(val: Style) -> Self { - val as Self - } -} - -/// 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`]. -/// -/// This question, and its internal data, is owned by its creator -/// (either the module or PAM itself). -#[repr(C)] -pub struct Question { - /// The style of message to request. - style: c_int, - /// A description of the data requested. - /// - /// For most requests, this will be an owned [`CStr`], but for requests - /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] - /// (a Linux-PAM extension). - data: *mut c_void, - _marker: Immovable, -} - -impl Question { - pub fn fill(&mut self, msg: &Message) -> Result<()> { - let (style, data) = copy_to_heap(msg)?; - self.clear(); - // SAFETY: We allocated this ourselves or were given it by PAM. - // Otherwise, it's null, but free(null) is fine. - unsafe { libc::free(self.data) }; - self.style = style as c_int; - self.data = data; - Ok(()) - } - - /// Gets this message's data pointer as a string. - /// - /// # Safety - /// - /// It's up to you to pass this only on types with a string value. - unsafe fn string_data(&self) -> Result<&str> { - if self.data.is_null() { - Ok("") - } else { - CStr::from_ptr(self.data.cast()) - .to_str() - .map_err(|_| ErrorCode::ConversationError) - } - } - - /// Gets this message's data pointer as borrowed binary data. - unsafe fn binary_data(&self) -> BorrowedBinaryData { - self.data - .cast::<CBinaryData>() - .as_ref() - .map(Into::into) - .unwrap_or_default() - } - - /// Zeroes out the data stored here. - fn clear(&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 { - if let Ok(style) = Style::try_from(self.style) { - match style { - Style::BinaryPrompt => { - if let Some(d) = self.data.cast::<CBinaryData>().as_mut() { - d.zero_contents() - } - } - Style::TextInfo - | Style::RadioType - | Style::ErrorMsg - | Style::PromptEchoOff - | Style::PromptEchoOn => memory::zero_c_string(self.data), - } - }; - libc::free(self.data); - self.data = ptr::null_mut(); - } - } -} - -impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> { - type Error = ErrorCode; - fn try_from(question: &'a Question) -> Result<Self> { - let style: Style = question - .style - .try_into() - .map_err(|_| ErrorCode::ConversationError)?; - // SAFETY: In all cases below, we're matching the - let prompt = unsafe { - match style { - Style::PromptEchoOff => { - Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?)) - } - Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)), - Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)), - Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)), - Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)), - Style::BinaryPrompt => Self::BinaryPrompt(question.binary_data().into()), - } - }; - Ok(prompt) - } -} - -/// Copies the contents of this message to the C heap. -fn copy_to_heap(msg: &Message) -> Result<(Style, *mut c_void)> { - let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); - match *msg { - Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), - Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), - Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), - Message::Error(p) => alloc(Style::ErrorMsg, p.question()), - Message::Info(p) => alloc(Style::TextInfo, p.question()), - Message::BinaryPrompt(p) => { - let q = p.question(); - Ok(( - Style::BinaryPrompt, - CBinaryData::alloc(q.data(), q.data_type())?.cast(), - )) - } - } -} - -/// Abstraction of a collection of questions to be sent in a PAM conversation. -/// -/// The PAM C API conversation function looks like this: -/// -/// ```c -/// int pam_conv( -/// int count, -/// const struct pam_message **questions, -/// struct pam_response **answers, -/// void *appdata_ptr, -/// ) -/// ``` -/// -/// On Linux-PAM and other compatible implementations, `messages` -/// is treated as a pointer-to-pointers, like `int argc, char **argv`. -/// (In this situation, the value of `OwnedMessages.indirect` is -/// the pointer passed to `pam_conv`.) -/// -/// ```text -/// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message ═╗ -/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ -/// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ║ -/// ╚═════════════╝ │ ... │ ┆ ╚═══════════╝ -/// ┆ -/// ┆ ╔═ Message ═╗ -/// ╰┄┄> ║ style ║ -/// ║ data ║ -/// ╚═══════════╝ -/// ``` -/// -/// On OpenPAM and other compatible implementations (like Solaris), -/// `messages` is a pointer-to-pointer-to-array. This appears to be -/// the correct implementation as required by the XSSO specification. -/// -/// ```text -/// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message[] ═╗ -/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ -/// ║ count ║ └────────────┘ ║ data ║ -/// ╚═════════════╝ ╟─────────────╢ -/// ║ style ║ -/// ║ data ║ -/// ╟─────────────╢ -/// ║ ... ║ -/// ``` -/// -/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** -pub struct Questions { - /// An indirection to the questions themselves, stored on the C heap. - indirect: *mut Indirect, - /// The number of questions. - count: usize, -} - -impl Questions { - /// Allocates data to store questions on the C heap. - pub fn alloc(count: usize) -> Self { - Self { - indirect: Indirect::alloc(count), - count, - } - } - - /// The pointer to the thing with the actual list. - pub fn indirect(&self) -> *const Indirect { - self.indirect - } - - 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) } - } -} - -impl Drop for Questions { - 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(self.count) - } - libc::free(self.indirect.cast()); - self.indirect = ptr::null_mut(); - } - } -} - -/// 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. -#[repr(transparent)] -pub struct Indirect { - base: [*mut Question; 0], - _marker: Immovable, -} - -impl Indirect { - /// Allocates memory for this indirector and all its members. - fn alloc(count: usize) -> *mut Self { - // SAFETY: We're only allocating, and when we're done, - // everything will be in a known-good state. - unsafe { - let me_ptr: *mut Indirect = libc::calloc(count, size_of::<*mut Question>()).cast(); - 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 = libc::calloc(1, size_of::<Question>()).cast(); - } - me - } - } - - /// Returns an iterator yielding the given number of messages. - /// - /// # Safety - /// - /// You have to provide the right count. - pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { - (0..count).map(|idx| &**self.base.as_ptr().add(idx)) - } - - /// Returns a mutable iterator yielding the given number of messages. - /// - /// # Safety - /// - /// You have to provide the right count. - pub 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)) - } - - /// Frees this and everything it points to. - /// - /// # Safety - /// - /// You have to pass the right size. - unsafe fn free(&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(); - } - libc::free(msg.cast()); - *msg = ptr::null_mut(); - } - } -} - -#[cfg(test)] -mod tests { - use super::{MaskedQAndA, Questions}; - use crate::conv::{BinaryQAndA, QAndA}; - - #[test] - fn test_owned_messages() { - let mut tons_of_messages = Questions::alloc(10); - let mut msgs: Vec<_> = tons_of_messages.iter_mut().collect(); - assert!(msgs.get(10).is_none()); - let last_msg = &mut msgs[9]; - last_msg - .fill(&MaskedQAndA::new("hocus pocus").message()) - .unwrap(); - let another_msg = &mut msgs[0]; - another_msg - .fill(&BinaryQAndA::new(&[5, 4, 3, 2, 1], 66).message()) - .unwrap(); - let overwrite = &mut msgs[3]; - overwrite.fill(&QAndA::new("what").message()).unwrap(); - overwrite.fill(&QAndA::new("who").message()).unwrap(); - } -}
--- a/src/libpam/mod.rs Sun Jun 08 01:03:46 2025 -0400 +++ b/src/libpam/mod.rs Sun Jun 08 03:48:40 2025 -0400 @@ -5,16 +5,15 @@ //! those data structures. //! //! Everything in here is hazmat. -//! #![allow(dead_code)] +mod answer; mod conversation; mod handle; mod memory; -mod message; mod module; -mod response; +mod question; pub use handle::{LibPamHandle, OwnedLibPamHandle}; use std::ffi::{c_char, c_int, c_void};
--- a/src/libpam/module.rs Sun Jun 08 01:03:46 2025 -0400 +++ b/src/libpam/module.rs Sun Jun 08 03:48:40 2025 -0400 @@ -1,15 +1,19 @@ -/// Generates the dynamic library entry points for a [PamModule] implementation. +/// Generates the dynamic library entry points for a PAM module /// -/// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will -/// generate the exported `extern "C"` functions that PAM uses to call into -/// your module. +/// Calling `pam_hooks!(SomeType)` on a type that implements +/// [`PamModule`](crate::PamModule) will generate the exported +/// `extern "C"` functions that PAM uses to call into your module. /// /// ## Examples: /// -/// Here is full example of a PAM module that would authenticate and authorize everybody: +/// Here is full example of a PAM module that would authenticate +/// and authorize everybody: /// /// ```no_run -/// use nonstick::{Flags, SimpleConversation, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks}; +/// use nonstick::{ +/// pam_hooks, Flags, OwnedLibPamHandle, PamHandleModule, PamModule, Result as PamResult, +/// SimpleConversation, +/// }; /// use std::ffi::CStr; /// # fn main() {} /// @@ -19,7 +23,8 @@ /// impl<T: PamHandleModule> PamModule<T> for MyPamModule { /// fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> { /// let password = handle.get_authtok(Some("what's your password?"))?; -/// let response = format!("If you say your password is {password:?}, who am I to disagree?"); +/// let response = +/// format!("If you say your password is {password:?}, who am I to disagree?"); /// handle.info_msg(&response); /// Ok(()) /// }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libpam/question.rs Sun Jun 08 03:48:40 2025 -0400 @@ -0,0 +1,374 @@ +//! Data and types dealing with PAM messages. + +use crate::constants::InvalidEnum; +use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA}; +use crate::libpam::conversation::OwnedMessage; +use crate::libpam::memory; +use crate::libpam::memory::{CBinaryData, Immovable}; +use crate::ErrorCode; +use crate::Result; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use std::ffi::{c_int, c_void, CStr}; +use std::result::Result as StdResult; +use std::{iter, ptr, slice}; + +/// Abstraction of a collection of questions to be sent in a PAM conversation. +/// +/// The PAM C API conversation function looks like this: +/// +/// ```c +/// int pam_conv( +/// int count, +/// const struct pam_message **questions, +/// struct pam_response **answers, +/// void *appdata_ptr, +/// ) +/// ``` +/// +/// On Linux-PAM and other compatible implementations, `questions` +/// is treated as a pointer-to-pointers, like `int argc, char **argv`. +/// (In this situation, the value of `Questions.indirect` is +/// the pointer passed to `pam_conv`.) +/// +/// ```text +/// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question ═╗ +/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ +/// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ... +/// ╚═════════════╝ │ ... │ ┆ ╚════════════╝ +/// ┆ +/// ┆ ╔═ Question ═╗ +/// ╰┄┄> ║ style ║ +/// ║ data ┄┄┄┄┄┄╫┄┄> ... +/// ╚════════════╝ +/// ``` +/// +/// On OpenPAM and other compatible implementations (like Solaris), +/// `messages` is a pointer-to-pointer-to-array. This appears to be +/// the correct implementation as required by the XSSO specification. +/// +/// ```text +/// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question[] ═╗ +/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ +/// ║ count ║ └────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... +/// ╚═════════════╝ ╟──────────────╢ +/// ║ style ║ +/// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... +/// ╟──────────────╢ +/// ║ ... ║ +/// ``` +/// +/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** +pub struct Questions { + /// An indirection to the questions themselves, stored on the C heap. + indirect: *mut Indirect, + /// The number of questions. + count: usize, +} + +impl Questions { + /// Stores the provided questions on the C heap. + pub fn new(messages: &[Message]) -> Result<Self> { + let count = messages.len(); + let mut ret = Self { + indirect: Indirect::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.fill(message)? + } + Ok(ret) + } + + /// The pointer to the thing with the actual list. + pub fn indirect(&self) -> *const Indirect { + self.indirect + } + + 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) } + } +} + +impl Drop for Questions { + 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(self.count) + } + memory::free(self.indirect); + self.indirect = ptr::null_mut(); + } + } +} + +/// 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. +#[repr(transparent)] +pub struct Indirect { + base: [*mut Question; 0], + _marker: Immovable, +} + +impl Indirect { + /// Allocates memory for this indirector and all its members. + 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 Indirect = memory::calloc::<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 + } + } + + /// Returns an iterator yielding the given number of messages. + /// + /// # Safety + /// + /// You have to provide the right count. + pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { + (0..count).map(|idx| &**self.base.as_ptr().add(idx)) + } + + /// Returns a mutable iterator yielding the given number of messages. + /// + /// # Safety + /// + /// You have to provide the right count. + pub 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)) + } + + /// Frees everything this points to. + /// + /// # Safety + /// + /// You have to pass the right size. + unsafe fn free(&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(); + } + } +} + +/// The C enum values for messages shown to the user. +#[derive(Debug, PartialEq, FromPrimitive)] +pub enum Style { + /// Requests information from the user; will be masked when typing. + PromptEchoOff = 1, + /// Requests information from the user; will not be masked. + PromptEchoOn = 2, + /// An error message. + ErrorMsg = 3, + /// An informational message. + TextInfo = 4, + /// Yes/No/Maybe conditionals. A Linux-PAM extension. + RadioType = 5, + /// For server–client non-human interaction. + /// + /// NOT part of the X/Open PAM specification. + /// A Linux-PAM extension. + BinaryPrompt = 7, +} + +impl TryFrom<c_int> for Style { + type Error = InvalidEnum<Self>; + fn try_from(value: c_int) -> StdResult<Self, Self::Error> { + Self::from_i32(value).ok_or(value.into()) + } +} + +impl From<Style> for c_int { + fn from(val: Style) -> Self { + val as Self + } +} + +/// 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`]. +/// +/// This question, and its internal data, is owned by its creator +/// (either the module or PAM itself). +#[repr(C)] +pub struct Question { + /// The style of message to request. + style: c_int, + /// A description of the data requested. + /// + /// For most requests, this will be an owned [`CStr`], but for requests + /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] + /// (a Linux-PAM extension). + data: *mut c_void, + _marker: Immovable, +} + +impl Question { + /// Replaces the contents of this question with the question + /// from the message. + pub fn fill(&mut self, msg: &Message) -> Result<()> { + let (style, data) = copy_to_heap(msg)?; + self.clear(); + self.style = style as c_int; + self.data = data; + Ok(()) + } + + /// Gets this message's data pointer as a string. + /// + /// # Safety + /// + /// It's up to you to pass this only on types with a string value. + unsafe fn string_data(&self) -> Result<&str> { + if self.data.is_null() { + Ok("") + } else { + CStr::from_ptr(self.data.cast()) + .to_str() + .map_err(|_| ErrorCode::ConversationError) + } + } + + /// Gets this message's data pointer as borrowed binary data. + unsafe fn binary_data(&self) -> (&[u8], u8) { + self.data + .cast::<CBinaryData>() + .as_ref() + .map(Into::into) + .unwrap_or_default() + } + + /// Zeroes out the data stored here. + fn clear(&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 { + if let Ok(style) = Style::try_from(self.style) { + match style { + Style::BinaryPrompt => { + if let Some(d) = self.data.cast::<CBinaryData>().as_mut() { + d.zero_contents() + } + } + Style::TextInfo + | Style::RadioType + | Style::ErrorMsg + | Style::PromptEchoOff + | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()), + } + }; + memory::free(self.data); + self.data = ptr::null_mut(); + } + } +} + +impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> { + type Error = ErrorCode; + fn try_from(question: &'a Question) -> Result<Self> { + let style: Style = question + .style + .try_into() + .map_err(|_| ErrorCode::ConversationError)?; + // SAFETY: In all cases below, we're matching the + let prompt = unsafe { + match style { + Style::PromptEchoOff => { + Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?)) + } + Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)), + Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)), + Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)), + Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)), + Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())), + } + }; + Ok(prompt) + } +} + +/// Copies the contents of this message to the C heap. +fn copy_to_heap(msg: &Message) -> Result<(Style, *mut c_void)> { + let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); + match *msg { + Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), + Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), + Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), + Message::Error(p) => alloc(Style::ErrorMsg, p.question()), + Message::Info(p) => alloc(Style::TextInfo, p.question()), + Message::BinaryPrompt(p) => { + let q = p.question(); + Ok(( + Style::BinaryPrompt, + CBinaryData::alloc(q)?.cast(), + )) + } + } +} + +#[cfg(test)] +mod tests { + + use super::{MaskedQAndA, Questions, Result}; + use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, QAndA, RadioQAndA}; + use crate::libpam::conversation::OwnedMessage; + + #[test] + fn test_round_trip() { + let interrogation = Questions::new(&[ + MaskedQAndA::new("hocus pocus").message(), + BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), + QAndA::new("what").message(), + QAndA::new("who").message(), + InfoMsg::new("hey").message(), + ErrorMsg::new("gasp").message(), + RadioQAndA::new("you must choose").message(), + ]) + .unwrap(); + let indirect = interrogation.indirect(); + + let remade = unsafe { indirect.as_ref() }.unwrap(); + let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } + .map(TryInto::try_into) + .collect::<Result<_>>() + .unwrap(); + let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap(); + macro_rules! assert_matches { + ($id:ident => $variant:path, $q:expr) => { + if let $variant($id) = $id { + assert_eq!($q, $id.question()); + } else { + panic!("mismatched enum variant {x:?}", x = $id); + } + }; + } + assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); + assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); + assert_matches!(what => OwnedMessage::Prompt, "what"); + assert_matches!(who => OwnedMessage::Prompt, "who"); + assert_matches!(hey => OwnedMessage::Info, "hey"); + assert_matches!(gasp => OwnedMessage::Error, "gasp"); + assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); + } +}
--- a/src/libpam/response.rs Sun Jun 08 01:03:46 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,327 +0,0 @@ -//! Types used to communicate data from the application to the module. - -use crate::conv::BorrowedBinaryData; -use crate::libpam::conversation::OwnedMessage; -use crate::libpam::memory; -use crate::libpam::memory::{CBinaryData, Immovable}; -use crate::{ErrorCode, Result}; -use std::ffi::{c_int, c_void, CStr}; -use std::ops::{Deref, DerefMut}; -use std::{iter, mem, ptr, slice}; - -#[repr(transparent)] -#[derive(Debug)] -pub struct TextAnswer(Answer); - -impl TextAnswer { - /// Interprets the provided `Answer` as a text answer. - /// - /// # Safety - /// - /// It's up to you to provide an answer that is a `TextAnswer`. - pub unsafe fn upcast(from: &mut Answer) -> &mut Self { - // SAFETY: We're provided a valid reference. - &mut *(from as *mut Answer).cast::<Self>() - } - - /// 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.cast(); - 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) - } - } - - /// Zeroes out the answer data, frees it, and points our data to `null`. - /// - /// 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) { - // 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); - libc::free(self.0.data); - self.0.data = ptr::null_mut() - } - } -} - -/// A [`Answer`] with [`CBinaryData`] in it. -#[repr(transparent)] -#[derive(Debug)] -pub struct BinaryAnswer(Answer); - -impl BinaryAnswer { - /// Interprets the provided [`Answer`] as a binary answer. - /// - /// # Safety - /// - /// It's up to you to provide an answer that is a `BinaryAnswer`. - pub unsafe fn upcast(from: &mut Answer) -> &mut Self { - // SAFETY: We're provided a valid reference. - &mut *(from as *mut Answer).cast::<Self>() - } - - /// Fills in a [`Answer`] with the provided binary data. - /// - /// The `data_type` is a tag you can use for whatever. - /// It is passed through PAM unchanged. - /// - /// The referenced data is copied to the C heap. - /// We do not take ownership of the original data. - pub fn fill(dest: &mut Answer, data: BorrowedBinaryData) -> Result<()> { - let allocated = CBinaryData::alloc(data.data(), data.data_type())?; - dest.free_contents(); - dest.data = allocated.cast(); - Ok(()) - } - - /// Gets the binary data in this answer. - pub fn data(&self) -> Option<&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. - unsafe { self.0.data.cast::<CBinaryData>().as_ref() } - } - - /// Zeroes out the answer data, frees it, and points our data to `null`. - /// - /// 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 zero_contents(&mut self) { - // 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 { - let data_ref = self.0.data.cast::<CBinaryData>().as_mut(); - if let Some(d) = data_ref { - d.zero_contents() - } - libc::free(self.0.data); - self.0.data = ptr::null_mut() - } - } -} - -/// Generic version of answer data. -/// -/// This has the same structure as [`BinaryAnswer`] -/// and [`TextAnswer`]. -#[repr(C)] -#[derive(Debug)] -pub struct Answer { - /// Pointer to the data returned in an answer. - /// For most answers, this will be a [`CStr`], but for answers to - /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`] - /// (a Linux-PAM extension). - data: *mut c_void, - /// Unused. - return_code: c_int, - _marker: Immovable, -} - -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 { - libc::free(self.data); - self.data = ptr::null_mut(); - } - } -} - -/// An owned, contiguous block of [`Answer`]s. -#[derive(Debug)] -pub struct Answers { - base: *mut Answer, - count: usize, -} - -impl Answers { - /// Allocates an owned list of answers on the C heap. - fn alloc(count: usize) -> Self { - Answers { - // SAFETY: We are doing allocation here. - base: unsafe { libc::calloc(count, size_of::<Answer>()) }.cast(), - count, - } - } - - pub fn build(value: Vec<OwnedMessage>) -> Result<Self> { - let mut outputs = Answers::alloc(value.len()); - // Even if we fail during this process, we still end up freeing - // all allocated answer memory. - for (input, output) in iter::zip(value, outputs.iter_mut()) { - match input { - OwnedMessage::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.unsecure())?, - OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?, - OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?, - OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, - OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, - OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?, - } - } - Ok(outputs) - } - - /// Converts this into a `*Answer` for passing to PAM. - /// - /// The pointer "owns" its own data (i.e., this will not be dropped). - pub fn into_ptr(self) -> *mut Answer { - let ret = self.base; - mem::forget(self); - ret - } - - /// Takes ownership of a list of answers allocated on the C heap. - /// - /// # Safety - /// - /// It's up to you to make sure you pass a valid pointer. - pub unsafe fn from_c_heap(base: *mut Answer, count: usize) -> Self { - Answers { base, count } - } -} - -impl Deref for Answers { - 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) } - } -} - -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) } - } -} - -impl Drop for Answers { - fn drop(&mut self) { - // SAFETY: We allocated this ourselves, or it was provided to us by PAM. - unsafe { - for answer in self.iter_mut() { - answer.free_contents() - } - libc::free(self.base.cast()) - } - } -} - -#[cfg(test)] -mod tests { - use super::{Answers, BinaryAnswer, TextAnswer, BorrowedBinaryData}; - use crate::BinaryData; - use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, QAndA, RadioQAndA}; - use crate::libpam::conversation::OwnedMessage; - - #[test] - fn test_round_trip() { - let binary_msg = { - let qa = BinaryQAndA::new(&[], 0); - qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99))); - OwnedMessage::BinaryPrompt(qa) - }; - - macro_rules! answered { - ($typ:ty, $msg:path, $data:expr) => { - {let qa = <$typ>::new(""); - qa.set_answer(Ok($data)); $msg(qa)} - } - } - - - let answers = vec![ - binary_msg, - answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()), - answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()), - answered!(ErrorMsg, OwnedMessage::Error, ()), - answered!(InfoMsg, OwnedMessage::Info, ()), - answered!(RadioQAndA, OwnedMessage::RadioPrompt, "beep boop".to_owned()), - ]; - let n = answers.len(); - let sent = Answers::build(answers).unwrap(); - let heap_answers = sent.into_ptr(); - let mut received = unsafe { Answers::from_c_heap(heap_answers, n) }; - - let assert_text = |want, raw| { - let up = unsafe { TextAnswer::upcast(raw) }; - assert_eq!(want, up.contents().unwrap()); - up.free_contents(); - assert_eq!("", up.contents().unwrap()); - }; - let assert_bin = |want_data: &[u8], want_type, raw| { - let up = unsafe { BinaryAnswer::upcast(raw) }; - assert_eq!(BinaryData::new(want_data.into(), want_type), up.data().into()); - up.zero_contents(); - assert_eq!(BinaryData::default(), up.data().into()); - }; - if let [zero, one, two, three, four, five] = &mut received[..] { - assert_bin(&[1, 2, 3], 99, zero); - assert_text("whats going on", one); - assert_text("well then", two); - assert_text("", three); - assert_text("", four); - assert_text("beep boop", five); - } else { - panic!("received wrong size {len}!", len = received.len()) - } - } - - #[test] - fn test_text_answer() { - let mut answers = Answers::alloc(2); - let zeroth = &mut answers[0]; - TextAnswer::fill(zeroth, "hello").unwrap(); - let zeroth_text = unsafe { TextAnswer::upcast(zeroth) }; - let data = zeroth_text.contents().expect("valid"); - assert_eq!("hello", data); - zeroth_text.free_contents(); - zeroth_text.free_contents(); - TextAnswer::fill(&mut answers[1], "hell\0").expect_err("should error; contains nul"); - } - - #[test] - fn test_binary_answer() { - let mut answers = Answers::alloc(1); - let real_data = BinaryData::new(vec![1, 2, 3, 4, 5, 6, 7, 8], 9); - let answer = &mut answers[0]; - BinaryAnswer::fill(answer, (&real_data).into()).expect("alloc should succeed"); - let bin_answer = unsafe { BinaryAnswer::upcast(answer) }; - assert_eq!(real_data, bin_answer.data().into()); - answer.free_contents(); - answer.free_contents(); - } - - #[test] - #[ignore] - fn test_binary_answer_too_big() { - let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000]; - let mut answers = Answers::alloc(1); - BinaryAnswer::fill(&mut answers[0], BorrowedBinaryData::new(&big_data, 100)) - .expect_err("this is too big!"); - } -}
--- a/src/module.rs Sun Jun 08 01:03:46 2025 -0400 +++ b/src/module.rs Sun Jun 08 03:48:40 2025 -0400 @@ -28,7 +28,8 @@ /// /// This is probably the first thing you want to implement. /// In most cases, you will want to get the user and password, - /// using [`PamHandle::get_user`] and [`PamModuleOnly::get_authtok`], + /// using [`PamShared::get_user`](crate::PamShared::get_user) + /// and [`PamModuleOnly::get_authtok`](crate::handle::PamModuleOnly::get_authtok), /// and verify them against something. /// /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg]