Mercurial > crates > nonstick
comparison src/libpam/answer.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/response.rs@351bdc13005e |
| children | 2128123b9406 |
comparison
equal
deleted
inserted
replaced
| 77:351bdc13005e | 78:002adfb98c5c |
|---|---|
| 1 //! Types used to communicate data from the application to the module. | |
| 2 | |
| 3 use crate::libpam::conversation::OwnedMessage; | |
| 4 use crate::libpam::memory; | |
| 5 use crate::libpam::memory::{CBinaryData, Immovable}; | |
| 6 use crate::{ErrorCode, Result}; | |
| 7 use std::ffi::{c_int, c_void, CStr}; | |
| 8 use std::ops::{Deref, DerefMut}; | |
| 9 use std::{iter, mem, ptr, slice}; | |
| 10 | |
| 11 /// The corridor via which the answer to Messages navigate through PAM. | |
| 12 #[derive(Debug)] | |
| 13 pub struct Answers { | |
| 14 base: *mut Answer, | |
| 15 count: usize, | |
| 16 } | |
| 17 | |
| 18 impl Answers { | |
| 19 /// Builds an Answers out of the given answered Message Q&As. | |
| 20 pub fn build(value: Vec<OwnedMessage>) -> Result<Self> { | |
| 21 let mut outputs = Self { | |
| 22 base: memory::calloc(value.len()), | |
| 23 count: value.len(), | |
| 24 }; | |
| 25 // Even if we fail during this process, we still end up freeing | |
| 26 // all allocated answer memory. | |
| 27 for (input, output) in iter::zip(value, outputs.iter_mut()) { | |
| 28 match input { | |
| 29 OwnedMessage::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.unsecure())?, | |
| 30 OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?, | |
| 31 OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?, | |
| 32 OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, | |
| 33 OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?, | |
| 34 OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?, | |
| 35 } | |
| 36 } | |
| 37 Ok(outputs) | |
| 38 } | |
| 39 | |
| 40 /// Converts this into a `*Answer` for passing to PAM. | |
| 41 /// | |
| 42 /// This object is consumed and the `Answer` pointer now owns its data. | |
| 43 /// It can be recreated with [`Self::from_c_heap`]. | |
| 44 pub fn into_ptr(self) -> *mut Answer { | |
| 45 let ret = self.base; | |
| 46 mem::forget(self); | |
| 47 ret | |
| 48 } | |
| 49 | |
| 50 /// Takes ownership of a list of answers allocated on the C heap. | |
| 51 /// | |
| 52 /// # Safety | |
| 53 /// | |
| 54 /// It's up to you to make sure you pass a valid pointer, | |
| 55 /// like one that you got from PAM, or maybe [`Self::into_ptr`]. | |
| 56 pub unsafe fn from_c_heap(base: *mut Answer, count: usize) -> Self { | |
| 57 Answers { base, count } | |
| 58 } | |
| 59 } | |
| 60 | |
| 61 impl Deref for Answers { | |
| 62 type Target = [Answer]; | |
| 63 fn deref(&self) -> &Self::Target { | |
| 64 // SAFETY: This is the memory we manage ourselves. | |
| 65 unsafe { slice::from_raw_parts(self.base, self.count) } | |
| 66 } | |
| 67 } | |
| 68 | |
| 69 impl DerefMut for Answers { | |
| 70 fn deref_mut(&mut self) -> &mut Self::Target { | |
| 71 // SAFETY: This is the memory we manage ourselves. | |
| 72 unsafe { slice::from_raw_parts_mut(self.base, self.count) } | |
| 73 } | |
| 74 } | |
| 75 | |
| 76 impl Drop for Answers { | |
| 77 fn drop(&mut self) { | |
| 78 // SAFETY: We allocated this ourselves, or it was provided to us by PAM. | |
| 79 unsafe { | |
| 80 for answer in self.iter_mut() { | |
| 81 answer.free_contents() | |
| 82 } | |
| 83 memory::free(self.base) | |
| 84 } | |
| 85 } | |
| 86 } | |
| 87 | |
| 88 #[repr(transparent)] | |
| 89 #[derive(Debug)] | |
| 90 pub struct TextAnswer(Answer); | |
| 91 | |
| 92 impl TextAnswer { | |
| 93 /// Interprets the provided `Answer` as a text answer. | |
| 94 /// | |
| 95 /// # Safety | |
| 96 /// | |
| 97 /// It's up to you to provide an answer that is a `TextAnswer`. | |
| 98 pub unsafe fn upcast(from: &mut Answer) -> &mut Self { | |
| 99 // SAFETY: We're provided a valid reference. | |
| 100 &mut *(from as *mut Answer).cast::<Self>() | |
| 101 } | |
| 102 | |
| 103 /// Converts the `Answer` to a `TextAnswer` with the given text. | |
| 104 fn fill(dest: &mut Answer, text: &str) -> Result<()> { | |
| 105 let allocated = memory::malloc_str(text)?; | |
| 106 dest.free_contents(); | |
| 107 dest.data = allocated.cast(); | |
| 108 Ok(()) | |
| 109 } | |
| 110 | |
| 111 /// Gets the string stored in this answer. | |
| 112 pub fn contents(&self) -> Result<&str> { | |
| 113 if self.0.data.is_null() { | |
| 114 Ok("") | |
| 115 } else { | |
| 116 // SAFETY: This data is either passed from PAM (so we are forced | |
| 117 // to trust it) or was created by us in TextAnswerInner::alloc. | |
| 118 // In either case, it's going to be a valid null-terminated string. | |
| 119 unsafe { CStr::from_ptr(self.0.data.cast()) } | |
| 120 .to_str() | |
| 121 .map_err(|_| ErrorCode::ConversationError) | |
| 122 } | |
| 123 } | |
| 124 | |
| 125 /// Zeroes out the answer data, frees it, and points our data to `null`. | |
| 126 /// | |
| 127 /// When this `TextAnswer` is part of an [`Answers`], | |
| 128 /// this is optional (since that will perform the `free`), | |
| 129 /// but it will clear potentially sensitive data. | |
| 130 pub fn free_contents(&mut self) { | |
| 131 // SAFETY: We own this data and know it's valid. | |
| 132 // If it's null, this is a no-op. | |
| 133 // After we're done, it will be null. | |
| 134 unsafe { | |
| 135 memory::zero_c_string(self.0.data.cast()); | |
| 136 memory::free(self.0.data); | |
| 137 self.0.data = ptr::null_mut() | |
| 138 } | |
| 139 } | |
| 140 } | |
| 141 | |
| 142 /// A [`Answer`] with [`CBinaryData`] in it. | |
| 143 #[repr(transparent)] | |
| 144 #[derive(Debug)] | |
| 145 pub struct BinaryAnswer(Answer); | |
| 146 | |
| 147 impl BinaryAnswer { | |
| 148 /// Interprets the provided [`Answer`] as a binary answer. | |
| 149 /// | |
| 150 /// # Safety | |
| 151 /// | |
| 152 /// It's up to you to provide an answer that is a `BinaryAnswer`. | |
| 153 pub unsafe fn upcast(from: &mut Answer) -> &mut Self { | |
| 154 // SAFETY: We're provided a valid reference. | |
| 155 &mut *(from as *mut Answer).cast::<Self>() | |
| 156 } | |
| 157 | |
| 158 /// Fills in a [`Answer`] with the provided binary data. | |
| 159 /// | |
| 160 /// The `data_type` is a tag you can use for whatever. | |
| 161 /// It is passed through PAM unchanged. | |
| 162 /// | |
| 163 /// The referenced data is copied to the C heap. | |
| 164 /// We do not take ownership of the original data. | |
| 165 pub fn fill(dest: &mut Answer, data_and_type: (&[u8], u8)) -> Result<()> { | |
| 166 let allocated = CBinaryData::alloc(data_and_type)?; | |
| 167 dest.free_contents(); | |
| 168 dest.data = allocated.cast(); | |
| 169 Ok(()) | |
| 170 } | |
| 171 | |
| 172 /// Gets the binary data in this answer. | |
| 173 pub fn data(&self) -> Option<&CBinaryData> { | |
| 174 // SAFETY: We either got this data from PAM or allocated it ourselves. | |
| 175 // Either way, we trust that it is either valid data or null. | |
| 176 unsafe { self.0.data.cast::<CBinaryData>().as_ref() } | |
| 177 } | |
| 178 | |
| 179 /// Zeroes out the answer data, frees it, and points our data to `null`. | |
| 180 /// | |
| 181 /// When this `TextAnswer` is part of an [`Answers`], | |
| 182 /// this is optional (since that will perform the `free`), | |
| 183 /// but it will clear potentially sensitive data. | |
| 184 pub fn zero_contents(&mut self) { | |
| 185 // SAFETY: We know that our data pointer is either valid or null. | |
| 186 // Once we're done, it's null and the answer is safe. | |
| 187 unsafe { | |
| 188 let data_ref = self.0.data.cast::<CBinaryData>().as_mut(); | |
| 189 if let Some(d) = data_ref { | |
| 190 d.zero_contents() | |
| 191 } | |
| 192 memory::free(self.0.data); | |
| 193 self.0.data = ptr::null_mut() | |
| 194 } | |
| 195 } | |
| 196 } | |
| 197 | |
| 198 /// Generic version of answer data. | |
| 199 /// | |
| 200 /// This has the same structure as [`BinaryAnswer`] | |
| 201 /// and [`TextAnswer`]. | |
| 202 #[repr(C)] | |
| 203 #[derive(Debug)] | |
| 204 pub struct Answer { | |
| 205 /// Pointer to the data returned in an answer. | |
| 206 /// For most answers, this will be a [`CStr`], but for answers to | |
| 207 /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`] | |
| 208 /// (a Linux-PAM extension). | |
| 209 data: *mut c_void, | |
| 210 /// Unused. | |
| 211 return_code: c_int, | |
| 212 _marker: Immovable, | |
| 213 } | |
| 214 | |
| 215 impl Answer { | |
| 216 /// Frees the contents of this answer. | |
| 217 /// | |
| 218 /// After this is done, this answer's `data` will be `null`, | |
| 219 /// which is a valid (empty) state. | |
| 220 fn free_contents(&mut self) { | |
| 221 // SAFETY: We have either an owned valid pointer, or null. | |
| 222 // We can free our owned pointer, and `free(null)` is a no-op. | |
| 223 unsafe { | |
| 224 memory::free(self.data); | |
| 225 self.data = ptr::null_mut(); | |
| 226 } | |
| 227 } | |
| 228 } | |
| 229 | |
| 230 #[cfg(test)] | |
| 231 mod tests { | |
| 232 use super::{Answer, Answers, BinaryAnswer, TextAnswer}; | |
| 233 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, QAndA, RadioQAndA}; | |
| 234 use crate::libpam::conversation::OwnedMessage; | |
| 235 use crate::BinaryData; | |
| 236 use crate::libpam::memory; | |
| 237 | |
| 238 #[test] | |
| 239 fn test_round_trip() { | |
| 240 let binary_msg = { | |
| 241 let qa = BinaryQAndA::new((&[][..], 0)); | |
| 242 qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99))); | |
| 243 OwnedMessage::BinaryPrompt(qa) | |
| 244 }; | |
| 245 | |
| 246 macro_rules! answered { | |
| 247 ($typ:ty, $msg:path, $data:expr) => {{ | |
| 248 let qa = <$typ>::new(""); | |
| 249 qa.set_answer(Ok($data)); | |
| 250 $msg(qa) | |
| 251 }}; | |
| 252 } | |
| 253 | |
| 254 let answers = vec![ | |
| 255 binary_msg, | |
| 256 answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()), | |
| 257 answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()), | |
| 258 answered!(ErrorMsg, OwnedMessage::Error, ()), | |
| 259 answered!(InfoMsg, OwnedMessage::Info, ()), | |
| 260 answered!( | |
| 261 RadioQAndA, | |
| 262 OwnedMessage::RadioPrompt, | |
| 263 "beep boop".to_owned() | |
| 264 ), | |
| 265 ]; | |
| 266 let n = answers.len(); | |
| 267 let sent = Answers::build(answers).unwrap(); | |
| 268 let heap_answers = sent.into_ptr(); | |
| 269 let mut received = unsafe { Answers::from_c_heap(heap_answers, n) }; | |
| 270 | |
| 271 let assert_text = |want, raw| { | |
| 272 let up = unsafe { TextAnswer::upcast(raw) }; | |
| 273 assert_eq!(want, up.contents().unwrap()); | |
| 274 up.free_contents(); | |
| 275 assert_eq!("", up.contents().unwrap()); | |
| 276 }; | |
| 277 let assert_bin = |want, raw| { | |
| 278 let up = unsafe { BinaryAnswer::upcast(raw) }; | |
| 279 assert_eq!(BinaryData::from(want), up.data().into()); | |
| 280 up.zero_contents(); | |
| 281 assert_eq!(BinaryData::default(), up.data().into()); | |
| 282 }; | |
| 283 if let [zero, one, two, three, four, five] = &mut received[..] { | |
| 284 assert_bin((&[1, 2, 3][..], 99), zero); | |
| 285 assert_text("whats going on", one); | |
| 286 assert_text("well then", two); | |
| 287 assert_text("", three); | |
| 288 assert_text("", four); | |
| 289 assert_text("beep boop", five); | |
| 290 } else { | |
| 291 panic!("received wrong size {len}!", len = received.len()) | |
| 292 } | |
| 293 } | |
| 294 | |
| 295 #[test] | |
| 296 fn test_text_answer() { | |
| 297 let answer_ptr: *mut Answer = memory::calloc(1); | |
| 298 let answer = unsafe {&mut *answer_ptr}; | |
| 299 TextAnswer::fill(answer, "hello").unwrap(); | |
| 300 let zeroth_text = unsafe { TextAnswer::upcast(answer) }; | |
| 301 let data = zeroth_text.contents().expect("valid"); | |
| 302 assert_eq!("hello", data); | |
| 303 zeroth_text.free_contents(); | |
| 304 zeroth_text.free_contents(); | |
| 305 TextAnswer::fill(answer, "hell\0").expect_err("should error; contains nul"); | |
| 306 unsafe { memory::free(answer_ptr) } | |
| 307 } | |
| 308 | |
| 309 #[test] | |
| 310 fn test_binary_answer() { | |
| 311 let answer_ptr: *mut Answer = memory::calloc(1); | |
| 312 let answer = unsafe { &mut *answer_ptr }; | |
| 313 let real_data = BinaryData::new(vec![1, 2, 3, 4, 5, 6, 7, 8], 9); | |
| 314 BinaryAnswer::fill(answer, (&real_data).into()).expect("alloc should succeed"); | |
| 315 let bin_answer = unsafe { BinaryAnswer::upcast(answer) }; | |
| 316 assert_eq!(real_data, bin_answer.data().into()); | |
| 317 answer.free_contents(); | |
| 318 answer.free_contents(); | |
| 319 unsafe { memory::free(answer_ptr) } | |
| 320 } | |
| 321 | |
| 322 #[test] | |
| 323 #[ignore] | |
| 324 fn test_binary_answer_too_big() { | |
| 325 let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000]; | |
| 326 let answer_ptr: *mut Answer = memory::calloc(1); | |
| 327 let answer = unsafe {&mut*answer_ptr}; | |
| 328 BinaryAnswer::fill(answer, (&big_data, 100)) | |
| 329 .expect_err("this is too big!"); | |
| 330 answer.free_contents(); | |
| 331 unsafe { memory::free(answer) } | |
| 332 } | |
| 333 } |
