Mercurial > crates > nonstick
changeset 77:351bdc13005e
Update the libpam module to work with the new structure.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Sun, 08 Jun 2025 01:03:46 -0400 |
parents | e58d24849e82 |
children | 002adfb98c5c |
files | Cargo.toml src/constants.rs src/conv.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/memory.rs src/libpam/message.rs src/libpam/module.rs src/libpam/response.rs |
diffstat | 9 files changed, 527 insertions(+), 479 deletions(-) [+] |
line wrap: on
line diff
--- a/Cargo.toml Sat Jun 07 18:55:27 2025 -0400 +++ b/Cargo.toml Sun Jun 08 01:03:46 2025 -0400 @@ -10,6 +10,7 @@ edition = "2021" [features] +default = ["link"] # Enable this to actually link against your system's PAM library. link = []
--- a/src/constants.rs Sat Jun 07 18:55:27 2025 -0400 +++ b/src/constants.rs Sun Jun 08 01:03:46 2025 -0400 @@ -174,9 +174,8 @@ } /// Error returned when attempting to coerce an invalid C integer into an enum. -#[derive(thiserror::Error)] +#[derive(Debug, PartialEq, thiserror::Error)] #[error("{0} is not a valid {type}", type = any::type_name::<T>())] -#[derive(Debug, PartialEq)] pub struct InvalidEnum<T>(c_int, PhantomData<T>); impl<T> From<InvalidEnum<T>> for c_int {
--- a/src/conv.rs Sat Jun 07 18:55:27 2025 -0400 +++ b/src/conv.rs Sun Jun 08 01:03:46 2025 -0400 @@ -14,12 +14,12 @@ /// that will be presented to the user. #[non_exhaustive] pub enum Message<'a> { - MaskedPrompt(&'a MaskedPrompt<'a>), - Prompt(&'a Prompt<'a>), - RadioPrompt(&'a RadioPrompt<'a>), - BinaryPrompt(&'a BinaryPrompt<'a>), - InfoMsg(&'a InfoMsg<'a>), - ErrorMsg(&'a ErrorMsg<'a>), + Prompt(&'a QAndA<'a>), + MaskedPrompt(&'a MaskedQAndA<'a>), + RadioPrompt(&'a RadioQAndA<'a>), + BinaryPrompt(&'a BinaryQAndA<'a>), + Error(&'a ErrorMsg<'a>), + Info(&'a InfoMsg<'a>), } impl Message<'_> { @@ -33,11 +33,11 @@ /// /// fn cant_respond(message: Message) { /// match message { - /// Message::InfoMsg(i) => { + /// Message::Info(i) => { /// eprintln!("fyi, {}", i.question()); /// i.set_answer(Ok(())) /// } - /// Message::ErrorMsg(e) => { + /// Message::Error(e) => { /// eprintln!("ERROR: {}", e.question()); /// e.set_answer(Ok(())) /// } @@ -46,55 +46,19 @@ /// } /// } pub fn set_error(&self, err: ErrorCode) { - match self { + match *self { + Message::Prompt(m) => m.set_answer(Err(err)), Message::MaskedPrompt(m) => m.set_answer(Err(err)), - Message::Prompt(m) => m.set_answer(Err(err)), Message::RadioPrompt(m) => m.set_answer(Err(err)), Message::BinaryPrompt(m) => m.set_answer(Err(err)), - Message::InfoMsg(m) => m.set_answer(Err(err)), - Message::ErrorMsg(m) => m.set_answer(Err(err)), + Message::Error(m) => m.set_answer(Err(err)), + Message::Info(m) => m.set_answer(Err(err)), } } } -/// A question-and-answer pair that can be communicated in a [`Conversation`]. -/// -/// The asking side creates a `QAndA`, then converts it to a [`Message`] -/// and sends it via a [`Conversation`]. The Conversation then retrieves -/// the answer to the question (if needed) and sets the response. -/// Once control returns to the asker, the asker gets the answer from this -/// `QAndA` and uses it however it wants. -/// -/// For a more detailed explanation of how this works, -/// see [`Conversation::communicate`]. -pub trait QAndA<'a> { - /// The type of the content of the question. - type Question: Copy; - /// The type of the answer to the question. - type Answer; - - /// Converts this Q-and-A pair into a [`Message`] for the [`Conversation`]. - fn message(&self) -> Message; - - /// The contents of the question being asked. - /// - /// For instance, this might say `"Username:"` to prompt the user - /// for their name. - fn question(&self) -> Self::Question; - - /// Sets the answer to the question. - /// - /// The [`Conversation`] implementation calls this to set the answer. - /// The conversation should *always call this function*, even for messages - /// that don't have "an answer" (like error or info messages). - fn set_answer(&self, answer: Result<Self::Answer>); - - /// Gets the answer to the question. - fn answer(self) -> Result<Self::Answer>; -} - macro_rules! q_and_a { - ($name:ident<'a, Q=$qt:ty, A=$at:ty>, $($doc:literal)*) => { + ($name:ident<'a, Q=$qt:ty, A=$at:ty>, $val:path, $($doc:literal)*) => { $( #[doc = $doc] )* @@ -103,25 +67,34 @@ a: Cell<Result<$at>>, } - impl<'a> QAndA<'a> for $name<'a> { - type Question = $qt; - type Answer = $at; + impl<'a> $name<'a> { + /// Converts this Q&A into a [`Message`] for the [`Conversation`]. + pub fn message(&self) -> Message { + $val(self) + } - fn question(&self) -> Self::Question { + /// The contents of the question being asked. + /// + /// For instance, this might say `"Username:"` to prompt the user + /// for their name, or the text of an error message. + pub fn question(&self) -> $qt { self.q } - fn set_answer(&self, answer: Result<Self::Answer>) { + /// Sets the answer to the question. + /// + /// The [`Conversation`] implementation calls this to set the answer. + /// The conversation should *always call this function*, + /// even for Q&A messages that don't have "an answer" + /// (like error or info messages). + pub fn set_answer(&self, answer: Result<$at>) { self.a.set(answer) } - fn answer(self) -> Result<Self::Answer> { + /// Gets the answer to the question. + pub fn answer(self) -> Result<$at> { self.a.into_inner() } - - fn message(&self) -> Message { - Message::$name(self) - } } }; } @@ -130,7 +103,7 @@ ($t:ident) => { impl<'a> $t<'a> { #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")] - fn ask(question: &'a str) -> Self { + pub fn new(question: &'a str) -> Self { Self { q: question, a: Cell::new(Err(ErrorCode::ConversationError)), @@ -141,35 +114,39 @@ } q_and_a!( - MaskedPrompt<'a, Q=&'a str, A=SecureString>, - "Asks the user for data and does not echo it back while being entered." + MaskedQAndA<'a, Q=&'a str, A=SecureString>, + Message::MaskedPrompt, + "A Q&A that asks the user for text and does not show it while typing." "" "In other words, a password entry prompt." ); -ask!(MaskedPrompt); +ask!(MaskedQAndA); q_and_a!( - Prompt<'a, Q=&'a str, A=String>, - "Asks the user for data." + QAndA<'a, Q=&'a str, A=String>, + Message::Prompt, + "A standard Q&A prompt that asks the user for text." "" "This is the normal \"ask a person a question\" prompt." "When the user types, their input will be shown to them." "It can be used for things like usernames." ); -ask!(Prompt); +ask!(QAndA); q_and_a!( - RadioPrompt<'a, Q=&'a str, A=String>, - "Asks the user for \"radio button\"–style data. (Linux-PAM extension)" + RadioQAndA<'a, Q=&'a str, A=String>, + Message::RadioPrompt, + "A Q&A for \"radio button\"–style data. (Linux-PAM extension)" "" "This message type is theoretically useful for \"yes/no/maybe\"" "questions, but nowhere in the documentation is it specified" "what the format of the answer will be, or how this should be shown." ); -ask!(RadioPrompt); +ask!(RadioQAndA); q_and_a!( - BinaryPrompt<'a, Q=BinaryQuestion<'a>, A=BinaryData>, + BinaryQAndA<'a, Q=BorrowedBinaryData<'a>, A=BinaryData>, + Message::BinaryPrompt, "Asks for binary data. (Linux-PAM extension)" "" "This sends a binary message to the client application." @@ -179,29 +156,44 @@ "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> BinaryPrompt<'a> { +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. - fn ask(data: &'a [u8], data_type: u8) -> Self { + 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: BinaryQuestion { data, data_type }, + q, a: Cell::new(Err(ErrorCode::ConversationError)), } } } -/// The contents of a question requesting binary data. -/// -/// A borrowed version of [`BinaryData`]. -#[derive(Copy, Clone, Debug)] -pub struct BinaryQuestion<'a> { +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 BinaryQuestion<'_> { +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 @@ -213,11 +205,17 @@ } } +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 [`BinaryQuestion`]. +/// For borrowed data, see [`BorrowedBinaryData`]. /// You can take ownership of the stored data with `.into::<Vec<u8>>()`. -#[derive(Debug, PartialEq)] +#[derive(Debug, Default, PartialEq)] pub struct BinaryData { data: Vec<u8>, data_type: u8, @@ -238,6 +236,15 @@ } } +impl<'a> From<BorrowedBinaryData<'a>> for BinaryData { + fn from(value: BorrowedBinaryData) -> Self { + Self { + data: value.data.to_vec(), + data_type: value.data_type, + } + } +} + impl From<BinaryData> for Vec<u8> { /// Takes ownership of the data stored herein. fn from(value: BinaryData) -> Self { @@ -247,6 +254,7 @@ q_and_a!( InfoMsg<'a, Q = &'a str, A = ()>, + Message::Info, "A message containing information to be passed to the user." "" "While this does not have an answer, [`Conversation`] implementations" @@ -255,7 +263,7 @@ ); impl<'a> InfoMsg<'a> { /// Creates an informational message to send to the user. - fn new(message: &'a str) -> Self { + pub fn new(message: &'a str) -> Self { Self { q: message, a: Cell::new(Err(ErrorCode::ConversationError)), @@ -265,6 +273,7 @@ q_and_a!( ErrorMsg<'a, Q = &'a str, A = ()>, + Message::Error, "An error message to be passed to the user." "" "While this does not have an answer, [`Conversation`] implementations" @@ -274,7 +283,7 @@ ); impl<'a> ErrorMsg<'a> { /// Creates an error message to send to the user. - fn new(message: &'a str) -> Self { + pub fn new(message: &'a str) -> Self { Self { q: message, a: Cell::new(Err(ErrorCode::ConversationError)), @@ -427,27 +436,27 @@ } macro_rules! conv_fn { - ($fn_name:ident($($param:ident: $pt:ty),+) -> $resp_type:ty { $ask:path }) => { + ($fn_name:ident($($param:ident: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> { - let prompt = $ask($($param),*); + let prompt = <$msg>::new($($param),*); self.communicate(&[prompt.message()]); prompt.answer() } }; - ($fn_name:ident($($param:ident: $pt:ty),+) { $ask:path }) => { + ($fn_name:ident($($param:ident: $pt:ty),+) { $msg:ty }) => { fn $fn_name(&mut self, $($param: $pt),*) { - self.communicate(&[$ask($($param),*).message()]); + self.communicate(&[<$msg>::new($($param),*).message()]); } }; } impl<C: Conversation> SimpleConversation for C { - conv_fn!(prompt(message: &str) -> String { Prompt::ask }); - conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedPrompt::ask }); - conv_fn!(radio_prompt(message: &str) -> String { RadioPrompt::ask }); - conv_fn!(error_msg(message: &str) { ErrorMsg::new }); - conv_fn!(info_msg(message: &str) { InfoMsg::new }); - conv_fn!(binary_prompt(data: &[u8], data_type: u8) -> BinaryData { BinaryPrompt::ask }); + conv_fn!(prompt(message: &str) -> String { QAndA }); + conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedQAndA } ); + 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 }); } /// A [`Conversation`] which asks the questions one at a time. @@ -466,11 +475,11 @@ Message::RadioPrompt(prompt) => { prompt.set_answer(self.0.radio_prompt(prompt.question())) } - Message::InfoMsg(prompt) => { + Message::Info(prompt) => { self.0.info_msg(prompt.question()); prompt.set_answer(Ok(())) } - Message::ErrorMsg(prompt) => { + Message::Error(prompt) => { self.0.error_msg(prompt.question()); prompt.set_answer(Ok(())) } @@ -486,8 +495,8 @@ #[cfg(test)] mod tests { use super::{ - BinaryPrompt, Conversation, ErrorMsg, InfoMsg, MaskedPrompt, Message, Prompt, QAndA, - RadioPrompt, Result, SecureString, SimpleConversation, + BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, + RadioQAndA, Result, SecureString, SimpleConversation, }; use crate::constants::ErrorCode; use crate::BinaryData; @@ -533,11 +542,11 @@ let mut tester = DemuxTester::default(); - let what = Prompt::ask("what"); - let pass = MaskedPrompt::ask("reveal"); + let what = QAndA::new("what"); + let pass = MaskedQAndA::new("reveal"); let err = ErrorMsg::new("whoopsie"); let info = InfoMsg::new("did you know"); - let has_err = Prompt::ask("give_err"); + let has_err = QAndA::new("give_err"); let mut conv = tester.as_conversation(); @@ -563,8 +572,8 @@ let mut conv = tester.as_conversation(); - let radio = RadioPrompt::ask("channel?"); - let bin = BinaryPrompt::ask(&[10, 9, 8], 66); + let radio = RadioQAndA::new("channel?"); + let bin = BinaryQAndA::new(&[10, 9, 8], 66); conv.communicate(&[radio.message(), bin.message()]); assert_eq!("zero", radio.answer().unwrap()); @@ -578,11 +587,11 @@ fn communicate(&mut self, messages: &[Message]) { if let [msg] = messages { match *msg { - Message::InfoMsg(info) => { + Message::Info(info) => { assert_eq!("let me tell you", info.question()); info.set_answer(Ok(())) } - Message::ErrorMsg(error) => { + Message::Error(error) => { assert_eq!("oh no", error.question()); error.set_answer(Ok(())) }
--- a/src/libpam/conversation.rs Sat Jun 07 18:55:27 2025 -0400 +++ b/src/libpam/conversation.rs Sun Jun 08 01:03:46 2025 -0400 @@ -1,14 +1,15 @@ -use crate::constants::Result; -use crate::conv::{Conversation, Message, Response}; +use crate::conv::{ + BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, + RadioQAndA, +}; use crate::libpam::memory::Immovable; -use crate::libpam::message::{MessageIndirector, OwnedMessages}; -use crate::libpam::response::{OwnedResponses, RawBinaryResponse, RawResponse, RawTextResponse}; +use crate::libpam::message::{Indirect, Questions}; +use crate::libpam::response::{Answer, Answers, BinaryAnswer, TextAnswer}; use crate::ErrorCode; -use crate::ErrorCode::ConversationError; +use crate::Result; use std::ffi::c_int; use std::iter; use std::marker::PhantomData; -use std::result::Result as StdResult; /// An opaque structure that is passed through PAM in a conversation. #[repr(C)] @@ -23,15 +24,15 @@ /// - `messages` is a pointer to the messages being sent to the user. /// For details about its structure, see the documentation of /// [`OwnedMessages`](super::OwnedMessages). -/// - `responses` is a pointer to an array of [`RawResponse`]s, +/// - `responses` 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 `responses` as `num_msg`. /// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed. pub type ConversationCallback = unsafe extern "C" fn( num_msg: c_int, - messages: *const MessageIndirector, - responses: *mut *mut RawResponse, + messages: *const Indirect, + responses: *mut *mut Answer, appdata: *mut AppData, ) -> c_int; @@ -59,104 +60,116 @@ unsafe extern "C" fn wrapper_callback<C: Conversation>( count: c_int, - messages: *const MessageIndirector, - responses: *mut *mut RawResponse, + questions: *const Indirect, + answers: *mut *mut Answer, me: *mut AppData, ) -> c_int { - let call = || { + let internal = || { + // Collect all our pointers let conv = me .cast::<C>() .as_mut() .ok_or(ErrorCode::ConversationError)?; - let indir = messages.as_ref().ok_or(ErrorCode::ConversationError)?; - let response_ptr = responses.as_mut().ok_or(ErrorCode::ConversationError)?; - let messages: Vec<Message> = indir + let indirect = questions.as_ref().ok_or(ErrorCode::ConversationError)?; + 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(Message::try_from) - .collect::<StdResult<_, _>>() + .map(OwnedMessage::try_from) + .collect::<Result<_>>() .map_err(|_| ErrorCode::ConversationError)?; - let responses = conv.communicate(&messages)?; - let owned = - OwnedResponses::build(&responses).map_err(|_| ErrorCode::ConversationError)?; - *response_ptr = owned.into_ptr(); + // Borrow all those Q&As and ask them + let borrowed: Vec<Message> = messages.iter().map(Into::into).collect(); + conv.communicate(&borrowed); + + // Send our answers back + let owned = Answers::build(messages).map_err(|_| ErrorCode::ConversationError)?; + *answers_ptr = owned.into_ptr(); Ok(()) }; - ErrorCode::result_to_c(call()) + ErrorCode::result_to_c(internal()) } } impl Conversation for LibPamConversation<'_> { - fn communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> { - let mut msgs_to_send = OwnedMessages::alloc(messages.len()); - for (dst, src) in iter::zip(msgs_to_send.iter_mut(), messages.iter()) { - dst.set(*src).map_err(|_| ErrorCode::ConversationError)? + 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 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(), + &mut response_pointer, + self.appdata, + ) + }; + ErrorCode::result_from(result)?; + // SAFETY: This is a pointer we just got back from PAM. + // We have to trust that the responses from PAM match up + // with the questions we sent. + unsafe { + let mut owned_responses = Answers::from_c_heap(response_pointer, messages.len()); + for (msg, response) in iter::zip(messages, owned_responses.iter_mut()) { + convert(msg, response); + } + }; + Ok(()) + }; + if let Err(e) = internal() { + messages.iter().for_each(|m| m.set_error(e)) } - 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.indirector(), - &mut response_pointer, - self.appdata, - ) - }; - ErrorCode::result_from(result)?; - // SAFETY: This is a pointer we just got back from PAM. - let owned_responses = - unsafe { OwnedResponses::from_c_heap(response_pointer, messages.len()) }; - convert_responses(messages, owned_responses) } } -fn convert_responses( - messages: &[Message], - mut raw_responses: OwnedResponses, -) -> Result<Vec<Response>> { - let pairs = iter::zip(messages.iter(), raw_responses.iter_mut()); - // We first collect into a Vec of Results so that we always process - // every single entry, which may involve freeing it. - let responses: Vec<_> = pairs.map(convert).collect(); - // Only then do we return the first error, if present. - responses.into_iter().collect() +/// Like [`Message`], but this time we own the contents. +pub enum OwnedMessage<'a> { + MaskedPrompt(MaskedQAndA<'a>), + Prompt(QAndA<'a>), + RadioPrompt(RadioQAndA<'a>), + BinaryPrompt(BinaryQAndA<'a>), + Info(InfoMsg<'a>), + Error(ErrorMsg<'a>), +} + +impl<'a> From<&'a OwnedMessage<'a>> for Message<'a> { + fn from(src: &'a OwnedMessage) -> Self { + match src { + OwnedMessage::MaskedPrompt(m) => Message::MaskedPrompt(m), + OwnedMessage::Prompt(m) => Message::Prompt(m), + OwnedMessage::RadioPrompt(m) => Message::RadioPrompt(m), + OwnedMessage::BinaryPrompt(m) => Message::BinaryPrompt(m), + OwnedMessage::Info(m) => Message::Info(m), + OwnedMessage::Error(m) => Message::Error(m), + } + } } -/// Converts one message-to-raw pair to a Response. -fn convert((sent, received): (&Message, &mut RawResponse)) -> Result<Response> { - Ok(match sent { - Message::MaskedPrompt(_) => { - // SAFETY: Since this is a response to a text message, - // we know it is text. - let text_resp = unsafe { RawTextResponse::upcast(received) }; - let ret = Response::MaskedText( - text_resp - .contents() - .map_err(|_| ErrorCode::ConversationError)? - .into(), - ); - // SAFETY: We're the only ones using this, - // and we haven't freed it. - text_resp.free_contents(); - ret +/// Fills in the answer of the Message with the given response. +/// +/// # Safety +/// +/// 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));}} +} + match *msg { + Message::MaskedPrompt(qa) => fill_text!(qa, resp), + Message::Prompt(qa) => fill_text!(qa, resp), + Message::RadioPrompt(qa) => fill_text!(qa, resp), + Message::Error(m) => m.set_answer(Ok(())), + Message::Info(m) => m.set_answer(Ok(())), + Message::BinaryPrompt(qa) => { + let bin_resp = unsafe { BinaryAnswer::upcast(resp) }; + qa.set_answer(Ok(bin_resp.data().into())); + bin_resp.zero_contents() } - Message::Prompt(_) | Message::RadioPrompt(_) => { - // SAFETY: Since this is a response to a text message, - // we know it is text. - let text_resp = unsafe { RawTextResponse::upcast(received) }; - let ret = Response::Text(text_resp.contents().map_err(|_| ConversationError)?.into()); - // SAFETY: We're the only ones using this, - // and we haven't freed it. - text_resp.free_contents(); - ret - } - Message::ErrorMsg(_) | Message::InfoMsg(_) => Response::NoResponse, - Message::BinaryPrompt { .. } => { - let bin_resp = unsafe { RawBinaryResponse::upcast(received) }; - let ret = Response::Binary(bin_resp.to_owned()); - // SAFETY: We're the only ones using this, - // and we haven't freed it. - bin_resp.free_contents(); - ret - } - }) + } }
--- a/src/libpam/handle.rs Sat Jun 07 18:55:27 2025 -0400 +++ b/src/libpam/handle.rs Sun Jun 08 01:03:46 2025 -0400 @@ -4,7 +4,7 @@ use crate::handle::{PamApplicationOnly, PamModuleOnly, PamShared}; use crate::libpam::memory; use crate::libpam::memory::Immovable; -use crate::{Conversation, Response}; +use crate::Conversation; use num_derive::FromPrimitive; use num_traits::FromPrimitive; use std::ffi::{c_char, c_int}; @@ -144,8 +144,15 @@ } impl Conversation for LibPamHandle { - fn communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> { - self.conversation_item()?.communicate(messages) + fn communicate(&mut self, messages: &[Message]) { + match self.conversation_item() { + Ok(conv) => conv.communicate(messages), + Err(e) => { + for msg in messages { + msg.set_error(e) + } + } + } } }
--- a/src/libpam/memory.rs Sat Jun 07 18:55:27 2025 -0400 +++ b/src/libpam/memory.rs Sun Jun 08 01:03:46 2025 -0400 @@ -1,10 +1,10 @@ //! Things for dealing with memory. -use crate::ErrorCode; +use crate::conv::BorrowedBinaryData; use crate::Result; +use crate::{BinaryData, ErrorCode}; use std::ffi::{c_char, c_void, CStr, CString}; use std::marker::{PhantomData, PhantomPinned}; -use std::result::Result as StdResult; use std::{ptr, slice}; /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. @@ -66,10 +66,10 @@ /// /// - it allocates data on the C heap with [`libc::malloc`]. /// - it doesn't take ownership of the data passed in. -pub fn malloc_str(text: impl AsRef<str>) -> StdResult<*mut c_char, NulError> { - let data = text.as_ref().as_bytes(); - if let Some(nul) = data.iter().position(|x| *x == 0) { - return Err(NulError(nul)); +pub fn malloc_str(text: &str) -> Result<*mut c_char> { + let data = text.as_bytes(); + if data.contains(&0) { + return Err(ErrorCode::ConversationError); } unsafe { let data_alloc = libc::calloc(data.len() + 1, 1); @@ -110,11 +110,9 @@ impl CBinaryData { /// Copies the given data to a new BinaryData on the heap. - pub fn alloc(source: &[u8], data_type: u8) -> StdResult<*mut CBinaryData, TooBigError> { - let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError { - max: (u32::MAX - 5) as usize, - actual: source.len(), - })?; + pub fn alloc(source: &[u8], data_type: u8) -> Result<*mut CBinaryData> { + let buffer_size = + u32::try_from(source.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(); @@ -132,13 +130,6 @@ u32::from_be_bytes(self.total_length).saturating_sub(5) as usize } - pub fn contents(&self) -> &[u8] { - unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) } - } - pub fn data_type(&self) -> u8 { - self.data_type - } - /// Clears this data and frees it. pub unsafe fn zero_contents(&mut self) { let contents = slice::from_raw_parts_mut(self.data.as_mut_ptr(), self.length()); @@ -150,16 +141,19 @@ } } -#[derive(Debug, thiserror::Error)] -#[error("null byte within input at byte {0}")] -pub struct NulError(pub usize); +impl<'a> From<&'a CBinaryData> for BorrowedBinaryData<'a> { + fn from(value: &'a CBinaryData) -> Self { + BorrowedBinaryData::new( + unsafe { slice::from_raw_parts(value.data.as_ptr(), value.length()) }, + value.data_type, + ) + } +} -/// Returned when trying to fit too much data into a binary message. -#[derive(Debug, thiserror::Error)] -#[error("cannot create a message of {actual} bytes; maximum is {max}")] -pub struct TooBigError { - pub actual: usize, - pub max: usize, +impl From<Option<&'_ CBinaryData>> for BinaryData { + fn from(value: Option<&CBinaryData>) -> Self { + value.map(BorrowedBinaryData::from).map(Into::into).unwrap_or_default() + } } #[cfg(test)]
--- a/src/libpam/message.rs Sat Jun 07 18:55:27 2025 -0400 +++ b/src/libpam/message.rs Sun Jun 08 01:03:46 2025 -0400 @@ -1,24 +1,17 @@ //! Data and types dealing with PAM messages. use crate::constants::InvalidEnum; -use crate::conv::Message; +use crate::libpam::conversation::OwnedMessage; use crate::libpam::memory; -use crate::libpam::memory::{CBinaryData, Immovable, NulError, TooBigError}; +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::str::Utf8Error; use std::{ptr, slice}; - -#[derive(Debug, thiserror::Error)] -#[error("error creating PAM message: {0}")] -pub enum ConversionError { - InvalidEnum(#[from] InvalidEnum<Style>), - Utf8Error(#[from] Utf8Error), - NulError(#[from] NulError), - TooBigError(#[from] TooBigError), -} +use crate::conv::{BorrowedBinaryData, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA}; /// The C enum values for messages shown to the user. #[derive(Debug, PartialEq, FromPrimitive)] @@ -53,11 +46,15 @@ } } -/// A message sent by PAM or a module to an application. -/// This message, and its internal data, is owned by the creator +/// 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 RawMessage { +pub struct Question { /// The style of message to request. style: c_int, /// A description of the data requested. @@ -69,8 +66,8 @@ _marker: Immovable, } -impl RawMessage { - pub fn set(&mut self, msg: Message) -> StdResult<(), ConversionError> { +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. @@ -86,14 +83,25 @@ /// # Safety /// /// It's up to you to pass this only on types with a string value. - unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> { + unsafe fn string_data(&self) -> Result<&str> { if self.data.is_null() { Ok("") } else { - CStr::from_ptr(self.data.cast()).to_str() + 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. @@ -119,23 +127,61 @@ } } -/// Copies the contents of this message to the C heap. -fn copy_to_heap(msg: Message) -> StdResult<(Style, *mut c_void), ConversionError> { - let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); - match msg { - Message::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text), - Message::Prompt(text) => alloc(Style::PromptEchoOn, text), - Message::RadioPrompt(text) => alloc(Style::RadioType, text), - Message::ErrorMsg(text) => alloc(Style::ErrorMsg, text), - Message::InfoMsg(text) => alloc(Style::TextInfo, text), - Message::BinaryPrompt { data, data_type } => Ok(( - Style::BinaryPrompt, - (CBinaryData::alloc(data, data_type)?).cast(), - )), +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) } } -/// Abstraction of a list-of-messages to be sent in a PAM conversation. +/// 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`. @@ -170,39 +216,39 @@ /// ``` /// /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** -pub struct OwnedMessages { - /// An indirection to the messages themselves, stored on the C heap. - indirect: *mut MessageIndirector, - /// The number of messages in the list. +pub struct Questions { + /// An indirection to the questions themselves, stored on the C heap. + indirect: *mut Indirect, + /// The number of questions. count: usize, } -impl OwnedMessages { - /// Allocates data to store messages on the C heap. +impl Questions { + /// Allocates data to store questions on the C heap. pub fn alloc(count: usize) -> Self { Self { - indirect: MessageIndirector::alloc(count), + indirect: Indirect::alloc(count), count, } } /// The pointer to the thing with the actual list. - pub fn indirector(&self) -> *const MessageIndirector { + pub fn indirect(&self) -> *const Indirect { self.indirect } - pub fn iter(&self) -> impl Iterator<Item = &RawMessage> { + 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 RawMessage> { + 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 OwnedMessages { +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. @@ -221,23 +267,22 @@ /// 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 MessageIndirector { - base: [*mut RawMessage; 0], +pub struct Indirect { + base: [*mut Question; 0], _marker: Immovable, } -impl MessageIndirector { +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 MessageIndirector = - libc::calloc(count, size_of::<*mut RawMessage>()).cast(); + 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::<RawMessage>()).cast(); + *entry = libc::calloc(1, size_of::<Question>()).cast(); } me } @@ -248,7 +293,7 @@ /// # Safety /// /// You have to provide the right count. - pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &RawMessage> { + pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { (0..count).map(|idx| &**self.base.as_ptr().add(idx)) } @@ -257,7 +302,7 @@ /// # Safety /// /// You have to provide the right count. - pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut RawMessage> { + 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)) } @@ -278,57 +323,26 @@ } } -impl<'a> TryFrom<&'a RawMessage> for Message<'a> { - type Error = ConversionError; - - /// Retrieves the data stored in this message. - fn try_from(input: &RawMessage) -> StdResult<Message, ConversionError> { - let style: Style = input.style.try_into()?; - // SAFETY: We either allocated this message ourselves or were provided it by PAM. - let result = unsafe { - match style { - Style::PromptEchoOff => Message::MaskedPrompt(input.string_data()?), - Style::PromptEchoOn => Message::Prompt(input.string_data()?), - Style::TextInfo => Message::InfoMsg(input.string_data()?), - Style::ErrorMsg => Message::ErrorMsg(input.string_data()?), - Style::RadioType => Message::ErrorMsg(input.string_data()?), - Style::BinaryPrompt => input.data.cast::<CBinaryData>().as_ref().map_or_else( - || Message::BinaryPrompt { - data_type: 0, - data: &[], - }, - |data| Message::BinaryPrompt { - data_type: data.data_type(), - data: data.contents(), - }, - ), - } - }; - Ok(result) - } -} - #[cfg(test)] mod tests { - use crate::conv::Message; - use crate::libpam::message::OwnedMessages; + use super::{MaskedQAndA, Questions}; + use crate::conv::{BinaryQAndA, QAndA}; #[test] fn test_owned_messages() { - let mut tons_of_messages = OwnedMessages::alloc(10); + 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.set(Message::MaskedPrompt("hocus pocus")).unwrap(); + last_msg + .fill(&MaskedQAndA::new("hocus pocus").message()) + .unwrap(); let another_msg = &mut msgs[0]; another_msg - .set(Message::BinaryPrompt { - data: &[5, 4, 3, 2, 1], - data_type: 99, - }) + .fill(&BinaryQAndA::new(&[5, 4, 3, 2, 1], 66).message()) .unwrap(); let overwrite = &mut msgs[3]; - overwrite.set(Message::Prompt("what")).unwrap(); - overwrite.set(Message::Prompt("who")).unwrap(); + overwrite.fill(&QAndA::new("what").message()).unwrap(); + overwrite.fill(&QAndA::new("who").message()).unwrap(); } }
--- a/src/libpam/module.rs Sat Jun 07 18:55:27 2025 -0400 +++ b/src/libpam/module.rs Sun Jun 08 01:03:46 2025 -0400 @@ -1,5 +1,3 @@ -use std::ffi::CStr; - /// Generates the dynamic library entry points for a [PamModule] implementation. /// /// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will @@ -10,8 +8,8 @@ /// /// Here is full example of a PAM module that would authenticate and authorize everybody: /// -/// ``` -/// use nonstick::{Flags, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks}; +/// ```no_run +/// use nonstick::{Flags, SimpleConversation, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks}; /// use std::ffi::CStr; /// # fn main() {} /// @@ -21,12 +19,15 @@ /// 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?"))?; -/// handle.info_msg(fmt!("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(()) /// } /// /// fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> { /// let username = handle.get_user(None)?; -/// handle.info_msg(fmt!("Hello {username}! I trust you unconditionally.")) +/// let response = format!("Hello {username}! I trust you unconditionally."); +/// handle.info_msg(&response); /// Ok(()) /// } /// }
--- a/src/libpam/response.rs Sat Jun 07 18:55:27 2025 -0400 +++ b/src/libpam/response.rs Sun Jun 08 01:03:46 2025 -0400 @@ -1,55 +1,59 @@ -//! Types used when dealing with PAM conversations. +//! Types used to communicate data from the application to the module. -use crate::conv::BinaryData; +use crate::conv::BorrowedBinaryData; +use crate::libpam::conversation::OwnedMessage; use crate::libpam::memory; -use crate::libpam::memory::{CBinaryData, Immovable, NulError, TooBigError}; -use crate::Response; +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::result::Result as StdResult; -use std::str::Utf8Error; use std::{iter, mem, ptr, slice}; #[repr(transparent)] #[derive(Debug)] -pub struct RawTextResponse(RawResponse); +pub struct TextAnswer(Answer); -impl RawTextResponse { - /// Interprets the provided `RawResponse` as a text response. +impl TextAnswer { + /// Interprets the provided `Answer` as a text answer. /// /// # Safety /// - /// It's up to you to provide a response that is a `RawTextResponse`. - pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self { + /// 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 RawResponse).cast::<Self>() + &mut *(from as *mut Answer).cast::<Self>() } - /// Fills in the provided `RawResponse` with the given text. - /// - /// You are responsible for calling [`free`](Self::free_contents) - /// on the pointer you get back when you're done with it. - pub fn fill(dest: &mut RawResponse, text: impl AsRef<str>) -> StdResult<&mut Self, NulError> { - dest.data = memory::malloc_str(text)?.cast(); - // SAFETY: We just filled this in so we know it's a text response. - Ok(unsafe { Self::upcast(dest) }) + /// 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 response. - pub fn contents(&self) -> StdResult<&str, Utf8Error> { + /// 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 TextResponseInner::alloc. + // 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() + unsafe { CStr::from_ptr(self.0.data.cast()) } + .to_str() + .map_err(|_| ErrorCode::ConversationError) } } - /// Releases memory owned by this response. + /// 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 know we own this data. + // 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); @@ -59,65 +63,51 @@ } } -/// A [`RawResponse`] with [`CBinaryData`] in it. +/// A [`Answer`] with [`CBinaryData`] in it. #[repr(transparent)] #[derive(Debug)] -pub struct RawBinaryResponse(RawResponse); +pub struct BinaryAnswer(Answer); -impl RawBinaryResponse { - /// Interprets the provided `RawResponse` as a binary response. +impl BinaryAnswer { + /// Interprets the provided [`Answer`] as a binary answer. /// /// # Safety /// - /// It's up to you to provide a response that is a `RawBinaryResponse`. - pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self { + /// 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 RawResponse).cast::<Self>() + &mut *(from as *mut Answer).cast::<Self>() } - /// Fills in a `RawResponse` with the provided binary data. + /// 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. - /// You are responsible for calling [`free`](Self::free_contents) - /// on the pointer you get back when you're done with it. - pub fn fill<'a>( - dest: &'a mut RawResponse, - data: &[u8], - data_type: u8, - ) -> StdResult<&'a mut Self, TooBigError> { - dest.data = CBinaryData::alloc(data, data_type)?.cast(); - // SAFETY: We just filled this in, so we know it's binary. - Ok(unsafe { Self::upcast(dest) }) + /// 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 response. - pub fn data(&self) -> &[u8] { - self.contents().map(CBinaryData::contents).unwrap_or(&[]) - } - - /// Gets the `data_type` tag that was embedded with the message. - pub fn data_type(&self) -> u8 { - self.contents().map(CBinaryData::data_type).unwrap_or(0) - } - - fn contents(&self) -> Option<&CBinaryData> { - // SAFETY: This was either something we got from PAM (in which case - // we trust it), or something that was created with - // BinaryResponseInner::alloc. In both cases, it points to valid data. + /// 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() } } - pub fn to_owned(&self) -> BinaryData { - BinaryData::new(self.data().into(), self.data_type()) - } - - /// Releases memory owned by this response. - pub fn free_contents(&mut self) { + /// 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 response is safe. + // 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 { @@ -129,15 +119,15 @@ } } -/// Generic version of response data. +/// Generic version of answer data. /// -/// This has the same structure as [`RawBinaryResponse`] -/// and [`RawTextResponse`]. +/// This has the same structure as [`BinaryAnswer`] +/// and [`TextAnswer`]. #[repr(C)] #[derive(Debug)] -pub struct RawResponse { - /// Pointer to the data returned in a response. - /// For most responses, this will be a [`CStr`], but for responses to +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, @@ -146,94 +136,95 @@ _marker: Immovable, } -/// A contiguous block of responses. +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 OwnedResponses { - base: *mut RawResponse, +pub struct Answers { + base: *mut Answer, count: usize, } -impl OwnedResponses { - /// Allocates an owned list of responses on the C heap. +impl Answers { + /// Allocates an owned list of answers on the C heap. fn alloc(count: usize) -> Self { - OwnedResponses { + Answers { // SAFETY: We are doing allocation here. - base: unsafe { libc::calloc(count, size_of::<RawResponse>()) }.cast(), + base: unsafe { libc::calloc(count, size_of::<Answer>()) }.cast(), count, } } - pub fn build(value: &[Response]) -> StdResult<Self, FillError> { - let mut outputs = OwnedResponses::alloc(value.len()); - // If we fail in here after allocating OwnedResponses, - // we still free all memory, even though we don't zero it first. - // This is an acceptable level of risk. - for (input, output) in iter::zip(value.iter(), outputs.iter_mut()) { + 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 { - Response::NoResponse => { - RawTextResponse::fill(output, "")?; - } - Response::Text(data) => { - RawTextResponse::fill(output, data)?; - } - Response::MaskedText(data) => { - RawTextResponse::fill(output, data.unsecure())?; - } - Response::Binary(data) => { - RawBinaryResponse::fill(output, data.data(), data.data_type())?; - } + 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 `*RawResponse` for passing to PAM. + /// 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 RawResponse { + pub fn into_ptr(self) -> *mut Answer { let ret = self.base; mem::forget(self); ret } - /// Takes ownership of a list of responses allocated on the C heap. + /// 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 RawResponse, count: usize) -> Self { - OwnedResponses { base, count } + pub unsafe fn from_c_heap(base: *mut Answer, count: usize) -> Self { + Answers { base, count } } } -#[derive(Debug, thiserror::Error)] -#[error("error converting responses: {0}")] -pub enum FillError { - NulError(#[from] NulError), - TooBigError(#[from] TooBigError), -} - -impl Deref for OwnedResponses { - type Target = [RawResponse]; +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 OwnedResponses { +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 OwnedResponses { +impl Drop for Answers { fn drop(&mut self) { // SAFETY: We allocated this ourselves, or it was provided to us by PAM. unsafe { - for resp in self.iter_mut() { - libc::free(resp.data) + for answer in self.iter_mut() { + answer.free_contents() } libc::free(self.base.cast()) } @@ -242,76 +233,95 @@ #[cfg(test)] mod tests { - use super::{BinaryData, OwnedResponses, RawBinaryResponse, RawTextResponse, Response}; + 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 responses = [ - Response::Binary(BinaryData::new(vec![1, 2, 3], 99)), - Response::Text("whats going on".to_owned()), - Response::MaskedText("well then".into()), - Response::NoResponse, - Response::Text("bogus".to_owned()), + 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 sent = OwnedResponses::build(&responses).unwrap(); - let heap_resps = sent.into_ptr(); - let mut received = unsafe { OwnedResponses::from_c_heap(heap_resps, 5) }; + 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 { RawTextResponse::upcast(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 { RawBinaryResponse::upcast(raw) }; - assert_eq!(want_data, up.data()); - assert_eq!(want_type, up.data_type()); - up.free_contents(); - let empty: [u8; 0] = []; - assert_eq!(&empty, up.data()); - assert_eq!(0, up.data_type()); + 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] = &mut received[..] { + 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("bogus", four); + assert_text("", four); + assert_text("beep boop", five); } else { - panic!("wrong size!") + panic!("received wrong size {len}!", len = received.len()) } } #[test] - fn test_text_response() { - let mut responses = OwnedResponses::alloc(2); - let text = RawTextResponse::fill(&mut responses[0], "hello").unwrap(); - let data = text.contents().expect("valid"); + 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); - text.free_contents(); - text.free_contents(); - RawTextResponse::fill(&mut responses[1], "hell\0").expect_err("should error; contains nul"); + 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_response() { - let mut responses = OwnedResponses::alloc(1); - let real_data = [1, 2, 3, 4, 5, 6, 7, 8]; - let resp = RawBinaryResponse::fill(&mut responses[0], &real_data, 7) - .expect("alloc should succeed"); - let data = resp.data(); - assert_eq!(&real_data, data); - assert_eq!(7, resp.data_type()); - resp.free_contents(); - resp.free_contents(); + 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_response_too_big() { + fn test_binary_answer_too_big() { let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000]; - let mut responses = OwnedResponses::alloc(1); - RawBinaryResponse::fill(&mut responses[0], &big_data, 0).expect_err("this is too big!"); + let mut answers = Answers::alloc(1); + BinaryAnswer::fill(&mut answers[0], BorrowedBinaryData::new(&big_data, 100)) + .expect_err("this is too big!"); } }