comparison src/libpam/question.rs @ 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 src/libpam/message.rs@351bdc13005e
children 2128123b9406
comparison
equal deleted inserted replaced
77:351bdc13005e 78:002adfb98c5c
1 //! Data and types dealing with PAM messages.
2
3 use crate::constants::InvalidEnum;
4 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA};
5 use crate::libpam::conversation::OwnedMessage;
6 use crate::libpam::memory;
7 use crate::libpam::memory::{CBinaryData, Immovable};
8 use crate::ErrorCode;
9 use crate::Result;
10 use num_derive::FromPrimitive;
11 use num_traits::FromPrimitive;
12 use std::ffi::{c_int, c_void, CStr};
13 use std::result::Result as StdResult;
14 use std::{iter, ptr, slice};
15
16 /// Abstraction of a collection of questions to be sent in a PAM conversation.
17 ///
18 /// The PAM C API conversation function looks like this:
19 ///
20 /// ```c
21 /// int pam_conv(
22 /// int count,
23 /// const struct pam_message **questions,
24 /// struct pam_response **answers,
25 /// void *appdata_ptr,
26 /// )
27 /// ```
28 ///
29 /// On Linux-PAM and other compatible implementations, `questions`
30 /// is treated as a pointer-to-pointers, like `int argc, char **argv`.
31 /// (In this situation, the value of `Questions.indirect` is
32 /// the pointer passed to `pam_conv`.)
33 ///
34 /// ```text
35 /// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question ═╗
36 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║
37 /// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ...
38 /// ╚═════════════╝ │ ... │ ┆ ╚════════════╝
39 /// ┆
40 /// ┆ ╔═ Question ═╗
41 /// ╰┄┄> ║ style ║
42 /// ║ data ┄┄┄┄┄┄╫┄┄> ...
43 /// ╚════════════╝
44 /// ```
45 ///
46 /// On OpenPAM and other compatible implementations (like Solaris),
47 /// `messages` is a pointer-to-pointer-to-array. This appears to be
48 /// the correct implementation as required by the XSSO specification.
49 ///
50 /// ```text
51 /// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question[] ═╗
52 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║
53 /// ║ count ║ └────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
54 /// ╚═════════════╝ ╟──────────────╢
55 /// ║ style ║
56 /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
57 /// ╟──────────────╢
58 /// ║ ... ║
59 /// ```
60 ///
61 /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
62 pub struct Questions {
63 /// An indirection to the questions themselves, stored on the C heap.
64 indirect: *mut Indirect,
65 /// The number of questions.
66 count: usize,
67 }
68
69 impl Questions {
70 /// Stores the provided questions on the C heap.
71 pub fn new(messages: &[Message]) -> Result<Self> {
72 let count = messages.len();
73 let mut ret = Self {
74 indirect: Indirect::alloc(count),
75 count,
76 };
77 // Even if we fail partway through this, all our memory will be freed.
78 for (question, message) in iter::zip(ret.iter_mut(), messages) {
79 question.fill(message)?
80 }
81 Ok(ret)
82 }
83
84 /// The pointer to the thing with the actual list.
85 pub fn indirect(&self) -> *const Indirect {
86 self.indirect
87 }
88
89 pub fn iter(&self) -> impl Iterator<Item = &Question> {
90 // SAFETY: we're iterating over an amount we know.
91 unsafe { (*self.indirect).iter(self.count) }
92 }
93 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
94 // SAFETY: we're iterating over an amount we know.
95 unsafe { (*self.indirect).iter_mut(self.count) }
96 }
97 }
98
99 impl Drop for Questions {
100 fn drop(&mut self) {
101 // SAFETY: We are valid and have a valid pointer.
102 // Once we're done, everything will be safe.
103 unsafe {
104 if let Some(indirect) = self.indirect.as_mut() {
105 indirect.free(self.count)
106 }
107 memory::free(self.indirect);
108 self.indirect = ptr::null_mut();
109 }
110 }
111 }
112
113 /// An indirect reference to messages.
114 ///
115 /// This is kept separate to provide a place where we can separate
116 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
117 #[repr(transparent)]
118 pub struct Indirect {
119 base: [*mut Question; 0],
120 _marker: Immovable,
121 }
122
123 impl Indirect {
124 /// Allocates memory for this indirector and all its members.
125 fn alloc(count: usize) -> *mut Self {
126 // SAFETY: We're only allocating, and when we're done,
127 // everything will be in a known-good state.
128 let me_ptr: *mut Indirect = memory::calloc::<Question>(count).cast();
129 unsafe {
130 let me = &mut *me_ptr;
131 let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count);
132 for entry in ptr_list {
133 *entry = memory::calloc(1);
134 }
135 me
136 }
137 }
138
139 /// Returns an iterator yielding the given number of messages.
140 ///
141 /// # Safety
142 ///
143 /// You have to provide the right count.
144 pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> {
145 (0..count).map(|idx| &**self.base.as_ptr().add(idx))
146 }
147
148 /// Returns a mutable iterator yielding the given number of messages.
149 ///
150 /// # Safety
151 ///
152 /// You have to provide the right count.
153 pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> {
154 (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx))
155 }
156
157 /// Frees everything this points to.
158 ///
159 /// # Safety
160 ///
161 /// You have to pass the right size.
162 unsafe fn free(&mut self, count: usize) {
163 let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count);
164 for msg in msgs {
165 if let Some(msg) = msg.as_mut() {
166 msg.clear();
167 }
168 memory::free(*msg);
169 *msg = ptr::null_mut();
170 }
171 }
172 }
173
174 /// The C enum values for messages shown to the user.
175 #[derive(Debug, PartialEq, FromPrimitive)]
176 pub enum Style {
177 /// Requests information from the user; will be masked when typing.
178 PromptEchoOff = 1,
179 /// Requests information from the user; will not be masked.
180 PromptEchoOn = 2,
181 /// An error message.
182 ErrorMsg = 3,
183 /// An informational message.
184 TextInfo = 4,
185 /// Yes/No/Maybe conditionals. A Linux-PAM extension.
186 RadioType = 5,
187 /// For server–client non-human interaction.
188 ///
189 /// NOT part of the X/Open PAM specification.
190 /// A Linux-PAM extension.
191 BinaryPrompt = 7,
192 }
193
194 impl TryFrom<c_int> for Style {
195 type Error = InvalidEnum<Self>;
196 fn try_from(value: c_int) -> StdResult<Self, Self::Error> {
197 Self::from_i32(value).ok_or(value.into())
198 }
199 }
200
201 impl From<Style> for c_int {
202 fn from(val: Style) -> Self {
203 val as Self
204 }
205 }
206
207 /// A question sent by PAM or a module to an application.
208 ///
209 /// PAM refers to this as a "message", but we call it a question
210 /// to avoid confusion with [`Message`].
211 ///
212 /// This question, and its internal data, is owned by its creator
213 /// (either the module or PAM itself).
214 #[repr(C)]
215 pub struct Question {
216 /// The style of message to request.
217 style: c_int,
218 /// A description of the data requested.
219 ///
220 /// For most requests, this will be an owned [`CStr`], but for requests
221 /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`]
222 /// (a Linux-PAM extension).
223 data: *mut c_void,
224 _marker: Immovable,
225 }
226
227 impl Question {
228 /// Replaces the contents of this question with the question
229 /// from the message.
230 pub fn fill(&mut self, msg: &Message) -> Result<()> {
231 let (style, data) = copy_to_heap(msg)?;
232 self.clear();
233 self.style = style as c_int;
234 self.data = data;
235 Ok(())
236 }
237
238 /// Gets this message's data pointer as a string.
239 ///
240 /// # Safety
241 ///
242 /// It's up to you to pass this only on types with a string value.
243 unsafe fn string_data(&self) -> Result<&str> {
244 if self.data.is_null() {
245 Ok("")
246 } else {
247 CStr::from_ptr(self.data.cast())
248 .to_str()
249 .map_err(|_| ErrorCode::ConversationError)
250 }
251 }
252
253 /// Gets this message's data pointer as borrowed binary data.
254 unsafe fn binary_data(&self) -> (&[u8], u8) {
255 self.data
256 .cast::<CBinaryData>()
257 .as_ref()
258 .map(Into::into)
259 .unwrap_or_default()
260 }
261
262 /// Zeroes out the data stored here.
263 fn clear(&mut self) {
264 // SAFETY: We either created this data or we got it from PAM.
265 // After this function is done, it will be zeroed out.
266 unsafe {
267 if let Ok(style) = Style::try_from(self.style) {
268 match style {
269 Style::BinaryPrompt => {
270 if let Some(d) = self.data.cast::<CBinaryData>().as_mut() {
271 d.zero_contents()
272 }
273 }
274 Style::TextInfo
275 | Style::RadioType
276 | Style::ErrorMsg
277 | Style::PromptEchoOff
278 | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()),
279 }
280 };
281 memory::free(self.data);
282 self.data = ptr::null_mut();
283 }
284 }
285 }
286
287 impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> {
288 type Error = ErrorCode;
289 fn try_from(question: &'a Question) -> Result<Self> {
290 let style: Style = question
291 .style
292 .try_into()
293 .map_err(|_| ErrorCode::ConversationError)?;
294 // SAFETY: In all cases below, we're matching the
295 let prompt = unsafe {
296 match style {
297 Style::PromptEchoOff => {
298 Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?))
299 }
300 Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)),
301 Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)),
302 Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)),
303 Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)),
304 Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())),
305 }
306 };
307 Ok(prompt)
308 }
309 }
310
311 /// Copies the contents of this message to the C heap.
312 fn copy_to_heap(msg: &Message) -> Result<(Style, *mut c_void)> {
313 let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
314 match *msg {
315 Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
316 Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
317 Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
318 Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
319 Message::Info(p) => alloc(Style::TextInfo, p.question()),
320 Message::BinaryPrompt(p) => {
321 let q = p.question();
322 Ok((
323 Style::BinaryPrompt,
324 CBinaryData::alloc(q)?.cast(),
325 ))
326 }
327 }
328 }
329
330 #[cfg(test)]
331 mod tests {
332
333 use super::{MaskedQAndA, Questions, Result};
334 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, QAndA, RadioQAndA};
335 use crate::libpam::conversation::OwnedMessage;
336
337 #[test]
338 fn test_round_trip() {
339 let interrogation = Questions::new(&[
340 MaskedQAndA::new("hocus pocus").message(),
341 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
342 QAndA::new("what").message(),
343 QAndA::new("who").message(),
344 InfoMsg::new("hey").message(),
345 ErrorMsg::new("gasp").message(),
346 RadioQAndA::new("you must choose").message(),
347 ])
348 .unwrap();
349 let indirect = interrogation.indirect();
350
351 let remade = unsafe { indirect.as_ref() }.unwrap();
352 let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) }
353 .map(TryInto::try_into)
354 .collect::<Result<_>>()
355 .unwrap();
356 let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap();
357 macro_rules! assert_matches {
358 ($id:ident => $variant:path, $q:expr) => {
359 if let $variant($id) = $id {
360 assert_eq!($q, $id.question());
361 } else {
362 panic!("mismatched enum variant {x:?}", x = $id);
363 }
364 };
365 }
366 assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus");
367 assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66));
368 assert_matches!(what => OwnedMessage::Prompt, "what");
369 assert_matches!(who => OwnedMessage::Prompt, "who");
370 assert_matches!(hey => OwnedMessage::Info, "hey");
371 assert_matches!(gasp => OwnedMessage::Error, "gasp");
372 assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose");
373 }
374 }