Mercurial > crates > nonstick
comparison src/libpam/question.rs @ 80:5aa1a010f1e8
Start using PAM headers; improve owned/borrowed distinction.
- Uses bindgen to generate bindings (only if needed).
- Gets the story together on owned vs. borrowed handles.
- Reduces number of mutable borrows in handle operation
(since `PamHandle` is neither `Send` nor `Sync`,
we never have to worry about thread safety.
- Improves a bunch of macros so we don't have our own
special syntax for docs.
- Implement question indirection for standard XSSO PAM implementations.
| author | Paul Fisher <paul@pfish.zone> |
|---|---|
| date | Tue, 10 Jun 2025 01:09:30 -0400 |
| parents | 2128123b9406 |
| children | 5e14bb093851 |
comparison
equal
deleted
inserted
replaced
| 79:2128123b9406 | 80:5aa1a010f1e8 |
|---|---|
| 1 //! Data and types dealing with PAM messages. | 1 //! Data and types dealing with PAM messages. |
| 2 | 2 |
| 3 use crate::constants::InvalidEnum; | |
| 4 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA}; | 3 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA}; |
| 5 use crate::libpam::conversation::OwnedMessage; | 4 use crate::libpam::conversation::OwnedMessage; |
| 6 use crate::libpam::memory; | 5 use crate::libpam::memory; |
| 7 use crate::libpam::memory::{CBinaryData, Immovable}; | 6 use crate::libpam::memory::{CBinaryData, Immovable}; |
| 7 pub use crate::libpam::pam_ffi::{Question, Style}; | |
| 8 use crate::ErrorCode; | 8 use crate::ErrorCode; |
| 9 use crate::Result; | 9 use crate::Result; |
| 10 use num_derive::FromPrimitive; | 10 use std::ffi::{c_void, CStr}; |
| 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}; | 11 use std::{iter, ptr, slice}; |
| 15 | 12 |
| 16 /// Abstraction of a collection of questions to be sent in a PAM conversation. | 13 /// Abstraction of a collection of questions to be sent in a PAM conversation. |
| 17 /// | 14 /// |
| 18 /// The PAM C API conversation function looks like this: | 15 /// The PAM C API conversation function looks like this: |
| 55 /// ║ style ║ | 52 /// ║ style ║ |
| 56 /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... | 53 /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... |
| 57 /// ╟──────────────╢ | 54 /// ╟──────────────╢ |
| 58 /// ║ ... ║ | 55 /// ║ ... ║ |
| 59 /// ``` | 56 /// ``` |
| 60 /// | 57 pub struct GenericQuestions<I: IndirectTrait> { |
| 61 /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** | |
| 62 pub struct Questions { | |
| 63 /// An indirection to the questions themselves, stored on the C heap. | 58 /// An indirection to the questions themselves, stored on the C heap. |
| 64 indirect: *mut Indirect, | 59 indirect: *mut I, |
| 65 /// The number of questions. | 60 /// The number of questions. |
| 66 count: usize, | 61 count: usize, |
| 67 } | 62 } |
| 68 | 63 |
| 69 impl Questions { | 64 impl<I: IndirectTrait> GenericQuestions<I> { |
| 70 /// Stores the provided questions on the C heap. | 65 /// Stores the provided questions on the C heap. |
| 71 pub fn new(messages: &[Message]) -> Result<Self> { | 66 pub fn new(messages: &[Message]) -> Result<Self> { |
| 72 let count = messages.len(); | 67 let count = messages.len(); |
| 73 let mut ret = Self { | 68 let mut ret = Self { |
| 74 indirect: Indirect::alloc(count), | 69 indirect: I::alloc(count), |
| 75 count, | 70 count, |
| 76 }; | 71 }; |
| 77 // Even if we fail partway through this, all our memory will be freed. | 72 // Even if we fail partway through this, all our memory will be freed. |
| 78 for (question, message) in iter::zip(ret.iter_mut(), messages) { | 73 for (question, message) in iter::zip(ret.iter_mut(), messages) { |
| 79 question.fill(message)? | 74 question.fill(message)? |
| 80 } | 75 } |
| 81 Ok(ret) | 76 Ok(ret) |
| 82 } | 77 } |
| 83 | 78 |
| 84 /// The pointer to the thing with the actual list. | 79 /// The pointer to the thing with the actual list. |
| 85 pub fn indirect(&self) -> *const Indirect { | 80 pub fn indirect(&self) -> *const *const Question { |
| 86 self.indirect | 81 self.indirect.cast() |
| 87 } | 82 } |
| 88 | 83 |
| 89 pub fn iter(&self) -> impl Iterator<Item = &Question> { | 84 pub fn iter(&self) -> impl Iterator<Item = &Question> { |
| 90 // SAFETY: we're iterating over an amount we know. | 85 // SAFETY: we're iterating over an amount we know. |
| 91 unsafe { (*self.indirect).iter(self.count) } | 86 unsafe { (*self.indirect).iter(self.count) } |
| 94 // SAFETY: we're iterating over an amount we know. | 89 // SAFETY: we're iterating over an amount we know. |
| 95 unsafe { (*self.indirect).iter_mut(self.count) } | 90 unsafe { (*self.indirect).iter_mut(self.count) } |
| 96 } | 91 } |
| 97 } | 92 } |
| 98 | 93 |
| 99 impl Drop for Questions { | 94 impl<I: IndirectTrait> Drop for GenericQuestions<I> { |
| 100 fn drop(&mut self) { | 95 fn drop(&mut self) { |
| 101 // SAFETY: We are valid and have a valid pointer. | 96 // SAFETY: We are valid and have a valid pointer. |
| 102 // Once we're done, everything will be safe. | 97 // Once we're done, everything will be safe. |
| 103 unsafe { | 98 unsafe { |
| 104 if let Some(indirect) = self.indirect.as_mut() { | 99 if let Some(indirect) = self.indirect.as_mut() { |
| 105 indirect.free(self.count) | 100 indirect.free_contents(self.count) |
| 106 } | 101 } |
| 107 memory::free(self.indirect); | 102 memory::free(self.indirect); |
| 108 self.indirect = ptr::null_mut(); | 103 self.indirect = ptr::null_mut(); |
| 109 } | 104 } |
| 110 } | 105 } |
| 111 } | 106 } |
| 112 | 107 |
| 108 /// The trait that each of the `Indirect` implementations implement. | |
| 109 /// | |
| 110 /// Basically a slice but with more meat. | |
| 111 pub trait IndirectTrait { | |
| 112 /// Converts a pointer into a borrowed `Self`. | |
| 113 /// | |
| 114 /// # Safety | |
| 115 /// | |
| 116 /// You have to provide a valid pointer. | |
| 117 unsafe fn borrow_ptr<'a>(ptr: *const *const Question) -> Option<&'a Self> | |
| 118 where | |
| 119 Self: Sized, | |
| 120 { | |
| 121 ptr.cast::<Self>().as_ref() | |
| 122 } | |
| 123 | |
| 124 /// Allocates memory for this indirector and all its members. | |
| 125 fn alloc(count: usize) -> *mut Self; | |
| 126 | |
| 127 /// Returns an iterator yielding the given number of messages. | |
| 128 /// | |
| 129 /// # Safety | |
| 130 /// | |
| 131 /// You have to provide the right count. | |
| 132 unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question>; | |
| 133 | |
| 134 /// Returns a mutable iterator yielding the given number of messages. | |
| 135 /// | |
| 136 /// # Safety | |
| 137 /// | |
| 138 /// You have to provide the right count. | |
| 139 unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question>; | |
| 140 | |
| 141 /// Frees everything this points to. | |
| 142 /// | |
| 143 /// # Safety | |
| 144 /// | |
| 145 /// You have to pass the right size. | |
| 146 unsafe fn free_contents(&mut self, count: usize); | |
| 147 } | |
| 148 | |
| 113 /// An indirect reference to messages. | 149 /// An indirect reference to messages. |
| 114 /// | 150 /// |
| 115 /// This is kept separate to provide a place where we can separate | 151 /// 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. | 152 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers. |
| 153 #[cfg(pam_impl = "linux-pam")] | |
| 154 pub type Indirect = LinuxPamIndirect; | |
| 155 | |
| 156 /// An indirect reference to messages. | |
| 157 /// | |
| 158 /// 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. | |
| 160 #[cfg(not(pam_impl = "linux-pam"))] | |
| 161 pub type Indirect = XssoIndirect; | |
| 162 | |
| 163 pub type Questions = GenericQuestions<Indirect>; | |
| 164 | |
| 165 /// The XSSO standard version of the indirection layer between Question and Questions. | |
| 117 #[repr(transparent)] | 166 #[repr(transparent)] |
| 118 pub struct Indirect { | 167 pub struct StandardIndirect { |
| 168 base: *mut Question, | |
| 169 _marker: Immovable, | |
| 170 } | |
| 171 | |
| 172 impl IndirectTrait for StandardIndirect { | |
| 173 fn alloc(count: usize) -> *mut Self { | |
| 174 let questions = memory::calloc(count); | |
| 175 let me_ptr: *mut Self = memory::calloc(1); | |
| 176 // SAFETY: We just allocated this, and we're putting a valid pointer in. | |
| 177 unsafe { | |
| 178 let me = &mut *me_ptr; | |
| 179 me.base = questions; | |
| 180 } | |
| 181 me_ptr | |
| 182 } | |
| 183 | |
| 184 unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { | |
| 185 (0..count).map(|idx| &*self.base.add(idx)) | |
| 186 } | |
| 187 | |
| 188 unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { | |
| 189 (0..count).map(|idx| &mut *self.base.add(idx)) | |
| 190 } | |
| 191 | |
| 192 unsafe fn free_contents(&mut self, count: usize) { | |
| 193 let msgs = slice::from_raw_parts_mut(self.base, count); | |
| 194 for msg in msgs { | |
| 195 msg.clear() | |
| 196 } | |
| 197 memory::free(self.base); | |
| 198 self.base = ptr::null_mut() | |
| 199 } | |
| 200 } | |
| 201 | |
| 202 /// The Linux version of the indirection layer between Question and Questions. | |
| 203 #[repr(transparent)] | |
| 204 pub struct LinuxPamIndirect { | |
| 119 base: [*mut Question; 0], | 205 base: [*mut Question; 0], |
| 120 _marker: Immovable, | 206 _marker: Immovable, |
| 121 } | 207 } |
| 122 | 208 |
| 123 impl Indirect { | 209 impl IndirectTrait for LinuxPamIndirect { |
| 124 /// Allocates memory for this indirector and all its members. | |
| 125 fn alloc(count: usize) -> *mut Self { | 210 fn alloc(count: usize) -> *mut Self { |
| 126 // SAFETY: We're only allocating, and when we're done, | 211 // SAFETY: We're only allocating, and when we're done, |
| 127 // everything will be in a known-good state. | 212 // everything will be in a known-good state. |
| 128 let me_ptr: *mut Indirect = memory::calloc::<Question>(count).cast(); | 213 let me_ptr: *mut Self = memory::calloc::<*mut Question>(count).cast(); |
| 129 unsafe { | 214 unsafe { |
| 130 let me = &mut *me_ptr; | 215 let me = &mut *me_ptr; |
| 131 let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count); | 216 let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count); |
| 132 for entry in ptr_list { | 217 for entry in ptr_list { |
| 133 *entry = memory::calloc(1); | 218 *entry = memory::calloc(1); |
| 134 } | 219 } |
| 135 me | 220 } |
| 136 } | 221 me_ptr |
| 137 } | 222 } |
| 138 | 223 |
| 139 /// Returns an iterator yielding the given number of messages. | 224 unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { |
| 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)) | 225 (0..count).map(|idx| &**self.base.as_ptr().add(idx)) |
| 146 } | 226 } |
| 147 | 227 |
| 148 /// Returns a mutable iterator yielding the given number of messages. | 228 unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { |
| 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)) | 229 (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx)) |
| 155 } | 230 } |
| 156 | 231 |
| 157 /// Frees everything this points to. | 232 unsafe fn free_contents(&mut self, count: usize) { |
| 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); | 233 let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count); |
| 164 for msg in msgs { | 234 for msg in msgs { |
| 165 if let Some(msg) = msg.as_mut() { | 235 if let Some(msg) = msg.as_mut() { |
| 166 msg.clear(); | 236 msg.clear(); |
| 167 } | 237 } |
| 169 *msg = ptr::null_mut(); | 239 *msg = ptr::null_mut(); |
| 170 } | 240 } |
| 171 } | 241 } |
| 172 } | 242 } |
| 173 | 243 |
| 174 /// The C enum values for messages shown to the user. | 244 impl Default for Question { |
| 175 #[derive(Debug, PartialEq, FromPrimitive)] | 245 fn default() -> Self { |
| 176 pub enum Style { | 246 Self { |
| 177 /// Requests information from the user; will be masked when typing. | 247 style: Default::default(), |
| 178 PromptEchoOff = 1, | 248 data: ptr::null_mut(), |
| 179 /// Requests information from the user; will not be masked. | 249 _marker: Default::default(), |
| 180 PromptEchoOn = 2, | 250 } |
| 181 /// An error message. | 251 } |
| 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 } | 252 } |
| 226 | 253 |
| 227 impl Question { | 254 impl Question { |
| 228 /// Replaces the contents of this question with the question | 255 /// Replaces the contents of this question with the question |
| 229 /// from the message. | 256 /// from the message. |
| 230 pub fn fill(&mut self, msg: &Message) -> Result<()> { | 257 pub fn fill(&mut self, msg: &Message) -> Result<()> { |
| 231 let (style, data) = copy_to_heap(msg)?; | 258 let (style, data) = copy_to_heap(msg)?; |
| 232 self.clear(); | 259 self.clear(); |
| 233 self.style = style as c_int; | 260 self.style = style.into(); |
| 234 self.data = data; | 261 self.data = data; |
| 235 Ok(()) | 262 Ok(()) |
| 236 } | 263 } |
| 237 | 264 |
| 238 /// Gets this message's data pointer as a string. | 265 /// Gets this message's data pointer as a string. |
| 325 } | 352 } |
| 326 | 353 |
| 327 #[cfg(test)] | 354 #[cfg(test)] |
| 328 mod tests { | 355 mod tests { |
| 329 | 356 |
| 330 use super::{MaskedQAndA, Questions, Result}; | 357 use super::{ |
| 331 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, QAndA, RadioQAndA}; | 358 BinaryQAndA, ErrorMsg, GenericQuestions, IndirectTrait, InfoMsg, LinuxPamIndirect, |
| 332 use crate::libpam::conversation::OwnedMessage; | 359 MaskedQAndA, OwnedMessage, QAndA, RadioQAndA, Result, StandardIndirect, |
| 333 | 360 }; |
| 334 #[test] | 361 |
| 335 fn test_round_trip() { | 362 macro_rules! assert_matches { |
| 336 let interrogation = Questions::new(&[ | 363 ($id:ident => $variant:path, $q:expr) => { |
| 337 MaskedQAndA::new("hocus pocus").message(), | 364 if let $variant($id) = $id { |
| 338 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), | 365 assert_eq!($q, $id.question()); |
| 339 QAndA::new("what").message(), | 366 } else { |
| 340 QAndA::new("who").message(), | 367 panic!("mismatched enum variant {x:?}", x = $id); |
| 341 InfoMsg::new("hey").message(), | 368 } |
| 342 ErrorMsg::new("gasp").message(), | 369 }; |
| 343 RadioQAndA::new("you must choose").message(), | 370 } |
| 344 ]) | 371 |
| 345 .unwrap(); | 372 macro_rules! tests { ($fn_name:ident<$typ:ident>) => { |
| 346 let indirect = interrogation.indirect(); | 373 #[test] |
| 347 | 374 fn $fn_name() { |
| 348 let remade = unsafe { indirect.as_ref() }.unwrap(); | 375 let interrogation = GenericQuestions::<$typ>::new(&[ |
| 349 let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } | 376 MaskedQAndA::new("hocus pocus").message(), |
| 350 .map(TryInto::try_into) | 377 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), |
| 351 .collect::<Result<_>>() | 378 QAndA::new("what").message(), |
| 379 QAndA::new("who").message(), | |
| 380 InfoMsg::new("hey").message(), | |
| 381 ErrorMsg::new("gasp").message(), | |
| 382 RadioQAndA::new("you must choose").message(), | |
| 383 ]) | |
| 352 .unwrap(); | 384 .unwrap(); |
| 353 let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap(); | 385 let indirect = interrogation.indirect(); |
| 354 macro_rules! assert_matches { | 386 |
| 355 ($id:ident => $variant:path, $q:expr) => { | 387 let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap(); |
| 356 if let $variant($id) = $id { | 388 let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } |
| 357 assert_eq!($q, $id.question()); | 389 .map(TryInto::try_into) |
| 358 } else { | 390 .collect::<Result<_>>() |
| 359 panic!("mismatched enum variant {x:?}", x = $id); | 391 .unwrap(); |
| 360 } | 392 let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap(); |
| 361 }; | 393 assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); |
| 362 } | 394 assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); |
| 363 assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); | 395 assert_matches!(what => OwnedMessage::Prompt, "what"); |
| 364 assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); | 396 assert_matches!(who => OwnedMessage::Prompt, "who"); |
| 365 assert_matches!(what => OwnedMessage::Prompt, "what"); | 397 assert_matches!(hey => OwnedMessage::Info, "hey"); |
| 366 assert_matches!(who => OwnedMessage::Prompt, "who"); | 398 assert_matches!(gasp => OwnedMessage::Error, "gasp"); |
| 367 assert_matches!(hey => OwnedMessage::Info, "hey"); | 399 assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); |
| 368 assert_matches!(gasp => OwnedMessage::Error, "gasp"); | 400 } |
| 369 assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); | 401 }} |
| 370 } | 402 |
| 371 } | 403 tests!(test_xsso<StandardIndirect>); |
| 404 tests!(test_linux<LinuxPamIndirect>); | |
| 405 } |
