comparison src/libpam/question.rs @ 87:05291b601f0a

Well and truly separate the Linux extensions. This separates the Linux extensions on the libpam side, and disables the two enums on the interface side. Users can still call the Linux extensions from non-Linux PAM impls, but they'll get a conversation error back.
author Paul Fisher <paul@pfish.zone>
date Tue, 10 Jun 2025 04:40:01 -0400
parents 5e14bb093851
children
comparison
equal deleted inserted replaced
86:23162cd399aa 87:05291b601f0a
1 //! Data and types dealing with PAM messages. 1 //! Data and types dealing with PAM messages.
2 2
3 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA}; 3 #[cfg(feature = "linux-pam-extensions")]
4 use crate::conv::{BinaryQAndA, RadioQAndA};
5 use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA};
4 use crate::libpam::conversation::OwnedMessage; 6 use crate::libpam::conversation::OwnedMessage;
5 use crate::libpam::memory;
6 use crate::libpam::memory::{CBinaryData, Immovable}; 7 use crate::libpam::memory::{CBinaryData, Immovable};
7 pub use crate::libpam::pam_ffi::{Question, Style}; 8 pub use crate::libpam::pam_ffi::Question;
9 use crate::libpam::{memory, pam_ffi};
8 use crate::ErrorCode; 10 use crate::ErrorCode;
9 use crate::Result; 11 use crate::Result;
12 use num_enum::{IntoPrimitive, TryFromPrimitive};
10 use std::ffi::{c_void, CStr}; 13 use std::ffi::{c_void, CStr};
11 use std::{iter, ptr, slice}; 14 use std::{iter, ptr, slice};
12 15
13 /// Abstraction of a collection of questions to be sent in a PAM conversation. 16 /// Abstraction of a collection of questions to be sent in a PAM conversation.
14 /// 17 ///
52 /// ║ style ║ 55 /// ║ style ║
53 /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... 56 /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
54 /// ╟──────────────╢ 57 /// ╟──────────────╢
55 /// ║ ... ║ 58 /// ║ ... ║
56 /// ``` 59 /// ```
60 #[derive(Debug)]
57 pub struct GenericQuestions<I: IndirectTrait> { 61 pub struct GenericQuestions<I: IndirectTrait> {
58 /// An indirection to the questions themselves, stored on the C heap. 62 /// An indirection to the questions themselves, stored on the C heap.
59 indirect: *mut I, 63 indirect: *mut I,
60 /// The number of questions. 64 /// The number of questions.
61 count: usize, 65 count: usize,
69 indirect: I::alloc(count), 73 indirect: I::alloc(count),
70 count, 74 count,
71 }; 75 };
72 // Even if we fail partway through this, all our memory will be freed. 76 // Even if we fail partway through this, all our memory will be freed.
73 for (question, message) in iter::zip(ret.iter_mut(), messages) { 77 for (question, message) in iter::zip(ret.iter_mut(), messages) {
74 question.fill(message)? 78 question.try_fill(message)?
75 } 79 }
76 Ok(ret) 80 Ok(ret)
77 } 81 }
78 82
79 /// The pointer to the thing with the actual list. 83 /// The pointer to the thing with the actual list.
156 /// An indirect reference to messages. 160 /// An indirect reference to messages.
157 /// 161 ///
158 /// This is kept separate to provide a place where we can separate 162 /// This is kept separate to provide a place where we can separate
159 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers. 163 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
160 #[cfg(not(pam_impl = "linux-pam"))] 164 #[cfg(not(pam_impl = "linux-pam"))]
161 pub type Indirect = XSsoIndirect; 165 pub type Indirect = StandardIndirect;
162 166
163 pub type Questions = GenericQuestions<Indirect>; 167 pub type Questions = GenericQuestions<Indirect>;
164 168
165 /// The XSSO standard version of the indirection layer between Question and Questions. 169 /// The XSSO standard version of the indirection layer between Question and Questions.
166 #[repr(transparent)] 170 #[derive(Debug)]
171 #[repr(C)]
167 pub struct StandardIndirect { 172 pub struct StandardIndirect {
168 base: *mut Question, 173 base: *mut Question,
169 _marker: Immovable, 174 _marker: Immovable,
170 } 175 }
171 176
198 self.base = ptr::null_mut() 203 self.base = ptr::null_mut()
199 } 204 }
200 } 205 }
201 206
202 /// The Linux version of the indirection layer between Question and Questions. 207 /// The Linux version of the indirection layer between Question and Questions.
203 #[repr(transparent)] 208 #[derive(Debug)]
209 #[repr(C)]
204 pub struct LinuxPamIndirect { 210 pub struct LinuxPamIndirect {
205 base: [*mut Question; 0], 211 base: [*mut Question; 0],
206 _marker: Immovable, 212 _marker: Immovable,
207 } 213 }
208 214
239 *msg = ptr::null_mut(); 245 *msg = ptr::null_mut();
240 } 246 }
241 } 247 }
242 } 248 }
243 249
250 /// The C enum values for messages shown to the user.
251 #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
252 #[repr(u32)]
253 pub enum Style {
254 /// Requests information from the user; will be masked when typing.
255 PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF,
256 /// Requests information from the user; will not be masked.
257 PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON,
258 /// An error message.
259 ErrorMsg = pam_ffi::PAM_ERROR_MSG,
260 /// An informational message.
261 TextInfo = pam_ffi::PAM_TEXT_INFO,
262 /// Yes/No/Maybe conditionals. A Linux-PAM extension.
263 #[cfg(feature = "linux-pam-extensions")]
264 RadioType = pam_ffi::PAM_RADIO_TYPE,
265 /// For server–client non-human interaction.
266 ///
267 /// NOT part of the X/Open PAM specification.
268 /// A Linux-PAM extension.
269 #[cfg(feature = "linux-pam-extensions")]
270 BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT,
271 }
272
244 impl Default for Question { 273 impl Default for Question {
245 fn default() -> Self { 274 fn default() -> Self {
246 Self { 275 Self {
247 style: Default::default(), 276 style: Default::default(),
248 data: ptr::null_mut(), 277 data: ptr::null_mut(),
252 } 281 }
253 282
254 impl Question { 283 impl Question {
255 /// Replaces the contents of this question with the question 284 /// Replaces the contents of this question with the question
256 /// from the message. 285 /// from the message.
257 pub fn fill(&mut self, msg: &Message) -> Result<()> { 286 ///
258 let (style, data) = copy_to_heap(msg)?; 287 /// If the message is not valid (invalid message type, bad contents, etc.),
288 /// this will fail.
289 pub fn try_fill(&mut self, msg: &Message) -> Result<()> {
290 let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
291 // We will only allocate heap data if we have a valid input.
292 let (style, data): (_, *mut c_void) = match *msg {
293 Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
294 Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
295 Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
296 Message::Info(p) => alloc(Style::TextInfo, p.question()),
297 #[cfg(feature = "linux-pam-extensions")]
298 Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
299 #[cfg(feature = "linux-pam-extensions")]
300 Message::BinaryPrompt(p) => Ok((
301 Style::BinaryPrompt,
302 CBinaryData::alloc(p.question())?.cast(),
303 )),
304 #[cfg(not(feature = "linux-pam-extensions"))]
305 Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError),
306 }?;
307 // Now that we know everything is valid, fill ourselves in.
259 self.clear(); 308 self.clear();
260 self.style = style.into(); 309 self.style = style.into();
261 self.data = data; 310 self.data = data;
262 Ok(()) 311 Ok(())
263 } 312 }
289 /// Zeroes out the data stored here. 338 /// Zeroes out the data stored here.
290 fn clear(&mut self) { 339 fn clear(&mut self) {
291 // SAFETY: We either created this data or we got it from PAM. 340 // SAFETY: We either created this data or we got it from PAM.
292 // After this function is done, it will be zeroed out. 341 // After this function is done, it will be zeroed out.
293 unsafe { 342 unsafe {
343 // This is nice-to-have. We'll try to zero out the data
344 // in the Question. If it's not a supported format, we skip it.
294 if let Ok(style) = Style::try_from(self.style) { 345 if let Ok(style) = Style::try_from(self.style) {
295 match style { 346 match style {
347 #[cfg(feature = "linux-pam-extensions")]
296 Style::BinaryPrompt => { 348 Style::BinaryPrompt => {
297 if let Some(d) = self.data.cast::<CBinaryData>().as_mut() { 349 if let Some(d) = self.data.cast::<CBinaryData>().as_mut() {
298 d.zero_contents() 350 d.zero_contents()
299 } 351 }
300 } 352 }
353 #[cfg(feature = "linux-pam-extensions")]
354 Style::RadioType => memory::zero_c_string(self.data.cast()),
301 Style::TextInfo 355 Style::TextInfo
302 | Style::RadioType
303 | Style::ErrorMsg 356 | Style::ErrorMsg
304 | Style::PromptEchoOff 357 | Style::PromptEchoOff
305 | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()), 358 | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()),
306 } 359 }
307 }; 360 };
316 fn try_from(question: &'a Question) -> Result<Self> { 369 fn try_from(question: &'a Question) -> Result<Self> {
317 let style: Style = question 370 let style: Style = question
318 .style 371 .style
319 .try_into() 372 .try_into()
320 .map_err(|_| ErrorCode::ConversationError)?; 373 .map_err(|_| ErrorCode::ConversationError)?;
321 // SAFETY: In all cases below, we're matching the 374 // SAFETY: In all cases below, we're creating questions based on
375 // known types that we get from PAM and the inner types it should have.
322 let prompt = unsafe { 376 let prompt = unsafe {
323 match style { 377 match style {
324 Style::PromptEchoOff => { 378 Style::PromptEchoOff => {
325 Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?)) 379 Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?))
326 } 380 }
327 Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)), 381 Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)),
328 Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)), 382 Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)),
329 Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)), 383 Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)),
384 #[cfg(feature = "linux-pam-extensions")]
330 Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)), 385 Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)),
386 #[cfg(feature = "linux-pam-extensions")]
331 Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())), 387 Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())),
332 } 388 }
333 }; 389 };
334 Ok(prompt) 390 Ok(prompt)
335 } 391 }
336 } 392 }
337 393
338 /// Copies the contents of this message to the C heap.
339 fn copy_to_heap(msg: &Message) -> Result<(Style, *mut c_void)> {
340 let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
341 match *msg {
342 Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
343 Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
344 Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
345 Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
346 Message::Info(p) => alloc(Style::TextInfo, p.question()),
347 Message::BinaryPrompt(p) => Ok((
348 Style::BinaryPrompt,
349 CBinaryData::alloc(p.question())?.cast(),
350 )),
351 }
352 }
353
354 #[cfg(test)] 394 #[cfg(test)]
355 mod tests { 395 mod tests {
356
357 use super::{
358 BinaryQAndA, ErrorMsg, GenericQuestions, IndirectTrait, InfoMsg, LinuxPamIndirect,
359 MaskedQAndA, OwnedMessage, QAndA, RadioQAndA, Result, StandardIndirect,
360 };
361 396
362 macro_rules! assert_matches { 397 macro_rules! assert_matches {
363 ($id:ident => $variant:path, $q:expr) => { 398 ($id:ident => $variant:path, $q:expr) => {
364 if let $variant($id) = $id { 399 if let $variant($id) = $id {
365 assert_eq!($q, $id.question()); 400 assert_eq!($q, $id.question());
368 } 403 }
369 }; 404 };
370 } 405 }
371 406
372 macro_rules! tests { ($fn_name:ident<$typ:ident>) => { 407 macro_rules! tests { ($fn_name:ident<$typ:ident>) => {
373 #[test] 408 mod $fn_name {
374 fn $fn_name() { 409 use super::super::*;
375 let interrogation = GenericQuestions::<$typ>::new(&[ 410 #[test]
376 MaskedQAndA::new("hocus pocus").message(), 411 fn standard() {
377 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), 412 let interrogation = GenericQuestions::<$typ>::new(&[
378 QAndA::new("what").message(), 413 MaskedQAndA::new("hocus pocus").message(),
379 QAndA::new("who").message(), 414 QAndA::new("what").message(),
380 InfoMsg::new("hey").message(), 415 QAndA::new("who").message(),
381 ErrorMsg::new("gasp").message(), 416 InfoMsg::new("hey").message(),
382 RadioQAndA::new("you must choose").message(), 417 ErrorMsg::new("gasp").message(),
383 ]) 418 ])
384 .unwrap();
385 let indirect = interrogation.indirect();
386
387 let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap();
388 let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) }
389 .map(TryInto::try_into)
390 .collect::<Result<_>>()
391 .unwrap(); 419 .unwrap();
392 let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap(); 420 let indirect = interrogation.indirect();
393 assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); 421
394 assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); 422 let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap();
395 assert_matches!(what => OwnedMessage::Prompt, "what"); 423 let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) }
396 assert_matches!(who => OwnedMessage::Prompt, "who"); 424 .map(TryInto::try_into)
397 assert_matches!(hey => OwnedMessage::Info, "hey"); 425 .collect::<Result<_>>()
398 assert_matches!(gasp => OwnedMessage::Error, "gasp"); 426 .unwrap();
399 assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); 427 let [masked, what, who, hey, gasp] = messages.try_into().unwrap();
428 assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus");
429 assert_matches!(what => OwnedMessage::Prompt, "what");
430 assert_matches!(who => OwnedMessage::Prompt, "who");
431 assert_matches!(hey => OwnedMessage::Info, "hey");
432 assert_matches!(gasp => OwnedMessage::Error, "gasp");
433 }
434
435 #[test]
436 #[cfg(not(feature = "linux-pam-extensions"))]
437 fn no_linux_extensions() {
438 use crate::conv::{BinaryQAndA, RadioQAndA};
439 GenericQuestions::<$typ>::new(&[
440 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
441 RadioQAndA::new("you must choose").message(),
442 ]).unwrap_err();
443 }
444
445 #[test]
446 #[cfg(feature = "linux-pam-extensions")]
447 fn linux_extensions() {
448 let interrogation = GenericQuestions::<$typ>::new(&[
449 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
450 RadioQAndA::new("you must choose").message(),
451 ]).unwrap();
452 let indirect = interrogation.indirect();
453
454 let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap();
455 let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) }
456 .map(TryInto::try_into)
457 .collect::<Result<_>>()
458 .unwrap();
459 let [bin, choose] = messages.try_into().unwrap();
460 assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66));
461 assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose");
462 }
400 } 463 }
401 }} 464 }}
402 465
403 tests!(test_xsso<StandardIndirect>); 466 tests!(test_xsso<StandardIndirect>);
404 tests!(test_linux<LinuxPamIndirect>); 467 tests!(test_linux<LinuxPamIndirect>);