Mercurial > crates > nonstick
comparison src/pam_ffi/message.rs @ 73:ac6881304c78
Do conversations, along with way too much stuff.
This implements conversations, along with all the memory management
brouhaha that goes along with it. The conversation now lives directly
on the handle rather than being a thing you have to get from it
and then call manually. It Turns Out this makes things a lot easier!
I guess we reorganized things again. For the last time. For real.
I promise.
This all passes ASAN, so it seems Pretty Good!
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Thu, 05 Jun 2025 03:41:38 -0400 |
parents | 47eb242a4f88 |
children | c7c596e6388f |
comparison
equal
deleted
inserted
replaced
72:47eb242a4f88 | 73:ac6881304c78 |
---|---|
1 //! Data and types dealing with PAM messages. | 1 //! Data and types dealing with PAM messages. |
2 | 2 |
3 use crate::constants::InvalidEnum; | 3 use crate::constants::InvalidEnum; |
4 use crate::conv::Message; | |
4 use crate::pam_ffi::memory; | 5 use crate::pam_ffi::memory; |
5 use crate::pam_ffi::memory::{CBinaryData, NulError, TooBigError}; | 6 use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError}; |
6 use num_derive::FromPrimitive; | 7 use num_derive::FromPrimitive; |
7 use num_traits::FromPrimitive; | 8 use num_traits::FromPrimitive; |
8 use std::ffi::{c_char, c_int, c_void, CStr}; | 9 use std::ffi::{c_int, c_void, CStr}; |
9 use std::result::Result as StdResult; | 10 use std::result::Result as StdResult; |
10 use std::slice; | |
11 use std::str::Utf8Error; | 11 use std::str::Utf8Error; |
12 | 12 use std::{ptr, slice}; |
13 /// The types of message and request that can be sent to a user. | |
14 /// | |
15 /// The data within each enum value is the prompt (or other information) | |
16 /// that will be presented to the user. | |
17 #[derive(Debug)] | |
18 pub enum Message<'a> { | |
19 /// Requests information from the user; will be masked when typing. | |
20 /// | |
21 /// Response: [`MaskedText`](crate::conv::Response::MaskedText) | |
22 MaskedPrompt(&'a str), | |
23 /// Requests information from the user; will not be masked. | |
24 /// | |
25 /// Response: [`Text`](crate::conv::Response::Text) | |
26 Prompt(&'a str), | |
27 /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). | |
28 /// | |
29 /// Response: [`Text`](crate::conv::Response::Text) | |
30 /// (Linux-PAM documentation doesn't define its contents.) | |
31 RadioPrompt(&'a str), | |
32 /// Raises an error message to the user. | |
33 /// | |
34 /// Response: [`NoResponse`](crate::conv::Response::NoResponse) | |
35 Error(&'a str), | |
36 /// Sends an informational message to the user. | |
37 /// | |
38 /// Response: [`NoResponse`](crate::conv::Response::NoResponse) | |
39 Info(&'a str), | |
40 /// Requests binary data from the client (a Linux-PAM extension). | |
41 /// | |
42 /// This is used for non-human or non-keyboard prompts (security key?). | |
43 /// NOT part of the X/Open PAM specification. | |
44 /// | |
45 /// Response: [`Binary`](crate::conv::Response::Binary) | |
46 BinaryPrompt { | |
47 /// Some binary data. | |
48 data: &'a [u8], | |
49 /// A "type" that you can use for signalling. Has no strict definition in PAM. | |
50 data_type: u8, | |
51 }, | |
52 } | |
53 | |
54 impl Message<'_> { | |
55 /// Copies the contents of this message to the C heap. | |
56 fn copy_to_heap(&self) -> StdResult<(Style, *mut c_void), ConversionError> { | |
57 let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); | |
58 match *self { | |
59 Self::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text), | |
60 Self::Prompt(text) => alloc(Style::PromptEchoOn, text), | |
61 Self::RadioPrompt(text) => alloc(Style::RadioType, text), | |
62 Self::Error(text) => alloc(Style::ErrorMsg, text), | |
63 Self::Info(text) => alloc(Style::TextInfo, text), | |
64 Self::BinaryPrompt { data, data_type } => Ok(( | |
65 Style::BinaryPrompt, | |
66 (CBinaryData::alloc(data, data_type)?).cast(), | |
67 )), | |
68 } | |
69 } | |
70 } | |
71 | 13 |
72 #[derive(Debug, thiserror::Error)] | 14 #[derive(Debug, thiserror::Error)] |
73 #[error("error creating PAM message: {0}")] | 15 #[error("error creating PAM message: {0}")] |
74 enum ConversionError { | 16 pub enum ConversionError { |
75 InvalidEnum(#[from] InvalidEnum<Style>), | 17 InvalidEnum(#[from] InvalidEnum<Style>), |
76 Utf8Error(#[from] Utf8Error), | 18 Utf8Error(#[from] Utf8Error), |
77 NulError(#[from] NulError), | 19 NulError(#[from] NulError), |
78 TooBigError(#[from] TooBigError), | 20 TooBigError(#[from] TooBigError), |
79 } | 21 } |
119 /// The style of message to request. | 61 /// The style of message to request. |
120 style: c_int, | 62 style: c_int, |
121 /// A description of the data requested. | 63 /// A description of the data requested. |
122 /// | 64 /// |
123 /// For most requests, this will be an owned [`CStr`], but for requests | 65 /// For most requests, this will be an owned [`CStr`], but for requests |
124 /// with [`Style::BinaryPrompt`], this will be [`BinaryData`] | 66 /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] |
125 /// (a Linux-PAM extension). | 67 /// (a Linux-PAM extension). |
126 data: *mut c_void, | 68 data: *mut c_void, |
69 _marker: Immovable, | |
127 } | 70 } |
128 | 71 |
129 impl RawMessage { | 72 impl RawMessage { |
130 fn set(&mut self, msg: &Message) -> StdResult<(), ConversionError> { | 73 pub fn set(&mut self, msg: Message) -> StdResult<(), ConversionError> { |
131 let (style, data) = msg.copy_to_heap()?; | 74 let (style, data) = copy_to_heap(msg)?; |
132 self.clear(); | 75 self.clear(); |
133 // SAFETY: We allocated this ourselves or were given it by PAM. | 76 // SAFETY: We allocated this ourselves or were given it by PAM. |
134 // Otherwise, it's null, but free(null) is fine. | 77 // Otherwise, it's null, but free(null) is fine. |
135 unsafe { libc::free(self.data) }; | 78 unsafe { libc::free(self.data) }; |
136 self.style = style as c_int; | 79 self.style = style as c_int; |
137 self.data = data; | 80 self.data = data; |
138 Ok(()) | 81 Ok(()) |
139 } | 82 } |
140 | 83 |
84 /// Gets this message's data pointer as a string. | |
85 /// | |
86 /// # Safety | |
87 /// | |
88 /// It's up to you to pass this only on types with a string value. | |
89 unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> { | |
90 if self.data.is_null() { | |
91 Ok("") | |
92 } else { | |
93 CStr::from_ptr(self.data.cast()).to_str() | |
94 } | |
95 } | |
96 | |
97 /// Zeroes out the data stored here. | |
98 fn clear(&mut self) { | |
99 // SAFETY: We either created this data or we got it from PAM. | |
100 // After this function is done, it will be zeroed out. | |
101 unsafe { | |
102 if let Ok(style) = Style::try_from(self.style) { | |
103 match style { | |
104 Style::BinaryPrompt => { | |
105 if let Some(d) = self.data.cast::<CBinaryData>().as_mut() { | |
106 d.zero_contents() | |
107 } | |
108 } | |
109 Style::TextInfo | |
110 | Style::RadioType | |
111 | Style::ErrorMsg | |
112 | Style::PromptEchoOff | |
113 | Style::PromptEchoOn => memory::zero_c_string(self.data), | |
114 } | |
115 }; | |
116 libc::free(self.data); | |
117 self.data = ptr::null_mut(); | |
118 } | |
119 } | |
120 } | |
121 | |
122 /// Copies the contents of this message to the C heap. | |
123 fn copy_to_heap(msg: Message) -> StdResult<(Style, *mut c_void), ConversionError> { | |
124 let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); | |
125 match msg { | |
126 Message::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text), | |
127 Message::Prompt(text) => alloc(Style::PromptEchoOn, text), | |
128 Message::RadioPrompt(text) => alloc(Style::RadioType, text), | |
129 Message::Error(text) => alloc(Style::ErrorMsg, text), | |
130 Message::Info(text) => alloc(Style::TextInfo, text), | |
131 Message::BinaryPrompt { data, data_type } => Ok(( | |
132 Style::BinaryPrompt, | |
133 (CBinaryData::alloc(data, data_type)?).cast(), | |
134 )), | |
135 } | |
136 } | |
137 | |
138 /// Abstraction of a list-of-messages to be sent in a PAM conversation. | |
139 /// | |
140 /// On Linux-PAM and other compatible implementations, `messages` | |
141 /// is treated as a pointer-to-pointers, like `int argc, char **argv`. | |
142 /// (In this situation, the value of `OwnedMessages.indirect` is | |
143 /// the pointer passed to `pam_conv`.) | |
144 /// | |
145 /// ```text | |
146 /// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message ═╗ | |
147 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ | |
148 /// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ║ | |
149 /// ╚═════════════╝ │ ... │ ┆ ╚═══════════╝ | |
150 /// ┆ | |
151 /// ┆ ╔═ Message ═╗ | |
152 /// ╰┄┄> ║ style ║ | |
153 /// ║ data ║ | |
154 /// ╚═══════════╝ | |
155 /// ``` | |
156 /// | |
157 /// On OpenPAM and other compatible implementations (like Solaris), | |
158 /// `messages` is a pointer-to-pointer-to-array. This appears to be | |
159 /// the correct implementation as required by the XSSO specification. | |
160 /// | |
161 /// ```text | |
162 /// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message[] ═╗ | |
163 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ | |
164 /// ║ count ║ └────────────┘ ║ data ║ | |
165 /// ╚═════════════╝ ╟─────────────╢ | |
166 /// ║ style ║ | |
167 /// ║ data ║ | |
168 /// ╟─────────────╢ | |
169 /// ║ ... ║ | |
170 /// ``` | |
171 /// | |
172 /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** | |
173 pub struct OwnedMessages { | |
174 /// An indirection to the messages themselves, stored on the C heap. | |
175 indirect: *mut MessageIndirector, | |
176 /// The number of messages in the list. | |
177 count: usize, | |
178 } | |
179 | |
180 impl OwnedMessages { | |
181 /// Allocates data to store messages on the C heap. | |
182 pub fn alloc(count: usize) -> Self { | |
183 Self { | |
184 indirect: MessageIndirector::alloc(count), | |
185 count, | |
186 } | |
187 } | |
188 | |
189 /// The pointer to the thing with the actual list. | |
190 pub fn indirector(&self) -> *const MessageIndirector { | |
191 self.indirect | |
192 } | |
193 | |
194 pub fn iter(&self) -> impl Iterator<Item = &RawMessage> { | |
195 // SAFETY: we're iterating over an amount we know. | |
196 unsafe { (*self.indirect).iter(self.count) } | |
197 } | |
198 | |
199 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut RawMessage> { | |
200 // SAFETY: we're iterating over an amount we know. | |
201 unsafe { (*self.indirect).iter_mut(self.count) } | |
202 } | |
203 } | |
204 | |
205 impl Drop for OwnedMessages { | |
206 fn drop(&mut self) { | |
207 // SAFETY: We are valid and have a valid pointer. | |
208 // Once we're done, everything will be safe. | |
209 unsafe { | |
210 if let Some(indirect) = self.indirect.as_mut() { | |
211 indirect.free(self.count) | |
212 } | |
213 libc::free(self.indirect.cast()); | |
214 self.indirect = ptr::null_mut(); | |
215 } | |
216 } | |
217 } | |
218 | |
219 /// An indirect reference to messages. | |
220 /// | |
221 /// This is kept separate to provide a place where we can separate | |
222 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers. | |
223 #[repr(transparent)] | |
224 pub struct MessageIndirector { | |
225 base: [*mut RawMessage; 0], | |
226 _marker: Immovable, | |
227 } | |
228 | |
229 impl MessageIndirector { | |
230 /// Allocates memory for this indirector and all its members. | |
231 fn alloc(count: usize) -> *mut Self { | |
232 // SAFETY: We're only allocating, and when we're done, | |
233 // everything will be in a known-good state. | |
234 unsafe { | |
235 let me_ptr: *mut MessageIndirector = | |
236 libc::calloc(count, size_of::<*mut RawMessage>()).cast(); | |
237 let me = &mut *me_ptr; | |
238 let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count); | |
239 for entry in ptr_list { | |
240 *entry = libc::calloc(1, size_of::<RawMessage>()).cast(); | |
241 } | |
242 me | |
243 } | |
244 } | |
245 | |
246 /// Returns an iterator yielding the given number of messages. | |
247 /// | |
248 /// # Safety | |
249 /// | |
250 /// You have to provide the right count. | |
251 pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &RawMessage> { | |
252 (0..count).map(|idx| &**self.base.as_ptr().add(idx)) | |
253 } | |
254 | |
255 /// Returns a mutable iterator yielding the given number of messages. | |
256 /// | |
257 /// # Safety | |
258 /// | |
259 /// You have to provide the right count. | |
260 pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut RawMessage> { | |
261 (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx)) | |
262 } | |
263 | |
264 /// Frees this and everything it points to. | |
265 /// | |
266 /// # Safety | |
267 /// | |
268 /// You have to pass the right size. | |
269 unsafe fn free(&mut self, count: usize) { | |
270 let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count); | |
271 for msg in msgs { | |
272 if let Some(msg) = msg.as_mut() { | |
273 msg.clear(); | |
274 } | |
275 libc::free(msg.cast()); | |
276 *msg = ptr::null_mut(); | |
277 } | |
278 } | |
279 } | |
280 | |
281 impl<'a> TryFrom<&'a RawMessage> for Message<'a> { | |
282 type Error = ConversionError; | |
283 | |
141 /// Retrieves the data stored in this message. | 284 /// Retrieves the data stored in this message. |
142 fn data(&self) -> StdResult<Message, ConversionError> { | 285 fn try_from(input: &RawMessage) -> StdResult<Message, ConversionError> { |
143 let style: Style = self.style.try_into()?; | 286 let style: Style = input.style.try_into()?; |
144 // SAFETY: We either allocated this message ourselves or were provided it by PAM. | 287 // SAFETY: We either allocated this message ourselves or were provided it by PAM. |
145 let result = unsafe { | 288 let result = unsafe { |
146 match style { | 289 match style { |
147 Style::PromptEchoOff => Message::MaskedPrompt(self.string_data()?), | 290 Style::PromptEchoOff => Message::MaskedPrompt(input.string_data()?), |
148 Style::PromptEchoOn => Message::Prompt(self.string_data()?), | 291 Style::PromptEchoOn => Message::Prompt(input.string_data()?), |
149 Style::TextInfo => Message::Info(self.string_data()?), | 292 Style::TextInfo => Message::Info(input.string_data()?), |
150 Style::ErrorMsg => Message::Error(self.string_data()?), | 293 Style::ErrorMsg => Message::Error(input.string_data()?), |
151 Style::RadioType => Message::Error(self.string_data()?), | 294 Style::RadioType => Message::Error(input.string_data()?), |
152 Style::BinaryPrompt => (self.data as *const CBinaryData).as_ref().map_or_else( | 295 Style::BinaryPrompt => input.data.cast::<CBinaryData>().as_ref().map_or_else( |
153 || Message::BinaryPrompt { | 296 || Message::BinaryPrompt { |
154 data_type: 0, | 297 data_type: 0, |
155 data: &[], | 298 data: &[], |
156 }, | 299 }, |
157 |data| Message::BinaryPrompt { | 300 |data| Message::BinaryPrompt { |
161 ), | 304 ), |
162 } | 305 } |
163 }; | 306 }; |
164 Ok(result) | 307 Ok(result) |
165 } | 308 } |
166 | 309 } |
167 /// Gets this message's data pointer as a string. | 310 |
168 /// | 311 #[cfg(test)] |
169 /// # Safety | 312 mod tests { |
170 /// | 313 use crate::conv::Message; |
171 /// It's up to you to pass this only on types with a string value. | 314 use crate::pam_ffi::message::OwnedMessages; |
172 unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> { | 315 |
173 if self.data.is_null() { | 316 #[test] |
174 Ok("") | 317 fn test_owned_messages() { |
175 } else { | 318 let mut tons_of_messages = OwnedMessages::alloc(10); |
176 CStr::from_ptr(self.data as *const c_char).to_str() | 319 let mut msgs: Vec<_> = tons_of_messages.iter_mut().collect(); |
177 } | 320 assert!(msgs.get(10).is_none()); |
178 } | 321 let last_msg = &mut msgs[9]; |
179 | 322 last_msg.set(Message::MaskedPrompt("hocus pocus")).unwrap(); |
180 /// Zeroes out the data stored here. | 323 let another_msg = &mut msgs[0]; |
181 fn clear(&mut self) { | 324 another_msg |
182 // SAFETY: We either created this data or we got it from PAM. | 325 .set(Message::BinaryPrompt { |
183 // After this function is done, it will be zeroed out. | 326 data: &[5, 4, 3, 2, 1], |
184 unsafe { | 327 data_type: 99, |
185 if let Ok(style) = Style::try_from(self.style) { | 328 }) |
186 match style { | 329 .unwrap(); |
187 Style::BinaryPrompt => { | 330 let overwrite = &mut msgs[3]; |
188 if let Some(d) = (self.data as *mut CBinaryData).as_mut() { | 331 overwrite.set(Message::Prompt("what")).unwrap(); |
189 d.zero_contents() | 332 overwrite.set(Message::Prompt("who")).unwrap(); |
190 } | 333 } |
191 } | 334 } |
192 Style::TextInfo | |
193 | Style::RadioType | |
194 | Style::ErrorMsg | |
195 | Style::PromptEchoOff | |
196 | Style::PromptEchoOn => memory::zero_c_string(self.data), | |
197 } | |
198 }; | |
199 } | |
200 } | |
201 } | |
202 | |
203 /// Abstraction of a list-of-messages to be sent in a PAM conversation. | |
204 /// | |
205 /// On Linux-PAM and other compatible implementations, `messages` | |
206 /// is treated as a pointer-to-pointers, like `int argc, char **argv`. | |
207 /// (In this situation, the value of `OwnedMessages.indirect` is | |
208 /// the pointer passed to `pam_conv`.) | |
209 /// | |
210 /// ```text | |
211 /// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message ═╗ | |
212 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ | |
213 /// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ║ | |
214 /// ╚═════════════╝ │ ... │ ┆ ╚═══════════╝ | |
215 /// ┆ | |
216 /// ┆ ╔═ Message ═╗ | |
217 /// ╰┄┄> ║ style ║ | |
218 /// ║ data ║ | |
219 /// ╚═══════════╝ | |
220 /// ``` | |
221 /// | |
222 /// On OpenPAM and other compatible implementations (like Solaris), | |
223 /// `messages` is a pointer-to-pointer-to-array. This appears to be | |
224 /// the correct implementation as required by the XSSO specification. | |
225 /// | |
226 /// ```text | |
227 /// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message[] ═╗ | |
228 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ | |
229 /// ║ count ║ └────────────┘ ║ data ║ | |
230 /// ╚═════════════╝ ╟─────────────╢ | |
231 /// ║ style ║ | |
232 /// ║ data ║ | |
233 /// ╟─────────────╢ | |
234 /// ║ ... ║ | |
235 /// ``` | |
236 /// | |
237 /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** | |
238 #[repr(C)] | |
239 pub struct OwnedMessages { | |
240 /// An indirection to the messages themselves, stored on the C heap. | |
241 indirect: *mut Indirect<RawMessage>, | |
242 /// The number of messages in the list. | |
243 count: usize, | |
244 } | |
245 | |
246 impl OwnedMessages { | |
247 /// Allocates data to store messages on the C heap. | |
248 pub fn alloc(count: usize) -> Self { | |
249 // SAFETY: We're allocating. That's safe. | |
250 unsafe { | |
251 // Since this is Linux-PAM, the indirect is a list of pointers. | |
252 let indirect = | |
253 libc::calloc(count, size_of::<Indirect<RawMessage>>()) as *mut Indirect<RawMessage>; | |
254 let indir_ptrs = slice::from_raw_parts_mut(indirect, count); | |
255 for ptr in indir_ptrs { | |
256 ptr.base = libc::calloc(1, size_of::<RawMessage>()) as *mut RawMessage; | |
257 } | |
258 Self { indirect, count } | |
259 } | |
260 } | |
261 | |
262 /// Gets a reference to the message at the given index. | |
263 pub fn get(&self, index: usize) -> Option<&RawMessage> { | |
264 (index < self.count).then(|| unsafe { (*self.indirect).at(index) }) | |
265 } | |
266 | |
267 /// Gets a mutable reference to the message at the given index. | |
268 pub fn get_mut(&mut self, index: usize) -> Option<&mut RawMessage> { | |
269 (index < self.count).then(|| unsafe { (*self.indirect).at_mut(index) }) | |
270 } | |
271 } | |
272 | |
273 #[repr(transparent)] | |
274 struct Indirect<T> { | |
275 /// The starting address for the T. | |
276 base: *mut T, | |
277 } | |
278 | |
279 impl<T> Indirect<T> { | |
280 /// Gets a mutable reference to the element at the given index. | |
281 /// | |
282 /// # Safety | |
283 /// | |
284 /// We don't check `index`. | |
285 unsafe fn at_mut(&mut self, index: usize) -> &mut T { | |
286 &mut *self.base.add(index) | |
287 } | |
288 | |
289 unsafe fn at(&self, index: usize) -> &T { | |
290 &*self.base.add(index) | |
291 } | |
292 } |