Mercurial > crates > nonstick
comparison src/pam_ffi.rs @ 70:9f8381a1c09c
Implement low-level conversation primitives.
This change does two primary things:
1. Introduces new Conversation traits, to be implemented both
by the library and by PAM client applications.
2. Builds the memory-management infrastructure for passing messages
through the conversation.
...and it adds tests for both of the above, including ASAN tests.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 03 Jun 2025 01:21:59 -0400 |
parents | 8f3ae0c7ab92 |
children |
comparison
equal
deleted
inserted
replaced
69:8f3ae0c7ab92 | 70:9f8381a1c09c |
---|---|
7 //! Everything in here is hazmat. | 7 //! Everything in here is hazmat. |
8 | 8 |
9 // Temporarily allow dead code. | 9 // Temporarily allow dead code. |
10 #![allow(dead_code)] | 10 #![allow(dead_code)] |
11 | 11 |
12 use crate::constants::InvalidEnum; | 12 use crate::constants::{InvalidEnum, NulError, TooBigError}; |
13 use num_derive::FromPrimitive; | 13 use num_derive::FromPrimitive; |
14 use num_traits::FromPrimitive; | 14 use num_traits::FromPrimitive; |
15 use std::ffi::{c_char, c_int, c_void, CStr}; | 15 use std::ffi::{c_char, c_int, c_void, CStr}; |
16 use std::marker::{PhantomData, PhantomPinned}; | 16 use std::marker::{PhantomData, PhantomPinned}; |
17 use std::num::TryFromIntError; | |
18 use std::slice; | 17 use std::slice; |
19 use thiserror::Error; | |
20 | 18 |
21 /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. | 19 /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. |
22 type Immovable = PhantomData<(*mut u8, PhantomPinned)>; | 20 type Immovable = PhantomData<(*mut u8, PhantomPinned)>; |
23 | 21 |
24 /// An opaque pointer given to us by PAM. | 22 /// An opaque pointer given to us by PAM. |
38 PromptEchoOn = 2, | 36 PromptEchoOn = 2, |
39 /// An error message. | 37 /// An error message. |
40 ErrorMsg = 3, | 38 ErrorMsg = 3, |
41 /// An informational message. | 39 /// An informational message. |
42 TextInfo = 4, | 40 TextInfo = 4, |
43 /// Yes/No/Maybe conditionals. Linux-PAM specific. | 41 /// Yes/No/Maybe conditionals. A Linux-PAM extension. |
44 RadioType = 5, | 42 RadioType = 5, |
45 /// For server–client non-human interaction. | 43 /// For server–client non-human interaction. |
44 /// | |
46 /// NOT part of the X/Open PAM specification. | 45 /// NOT part of the X/Open PAM specification. |
46 /// A Linux-PAM extension. | |
47 BinaryPrompt = 7, | 47 BinaryPrompt = 7, |
48 } | 48 } |
49 | 49 |
50 impl TryFrom<c_int> for MessageStyle { | 50 impl TryFrom<c_int> for MessageStyle { |
51 type Error = InvalidEnum<Self>; | 51 type Error = InvalidEnum<Self>; |
66 #[repr(C)] | 66 #[repr(C)] |
67 pub struct Message { | 67 pub struct Message { |
68 /// The style of message to request. | 68 /// The style of message to request. |
69 style: c_int, | 69 style: c_int, |
70 /// A description of the data requested. | 70 /// A description of the data requested. |
71 /// | |
71 /// For most requests, this will be an owned [`CStr`], but for requests | 72 /// For most requests, this will be an owned [`CStr`], but for requests |
72 /// with [`MessageStyle::BinaryPrompt`], this will be [`BinaryData`]. | 73 /// with [`MessageStyle::BinaryPrompt`], this will be [`BinaryData`] |
74 /// (a Linux-PAM extension). | |
73 data: *const c_void, | 75 data: *const c_void, |
74 } | 76 } |
75 | 77 |
76 /// Returned when text that should not have any `\0` bytes in it does. | 78 #[repr(C)] |
77 /// Analogous to [`std::ffi::NulError`], but the data it was created from | 79 pub struct TextResponseInner { |
78 /// is borrowed. | 80 data: *mut c_char, |
79 #[derive(Debug, Error)] | 81 _unused: c_int, |
80 #[error("null byte within input at byte {0}")] | 82 } |
81 pub struct NulError(usize); | |
82 | |
83 #[repr(transparent)] | |
84 pub struct TextResponseInner(ResponseInner); | |
85 | 83 |
86 impl TextResponseInner { | 84 impl TextResponseInner { |
87 /// Allocates a new text response on the C heap. | 85 /// Allocates a new text response on the C heap. |
88 /// | 86 /// |
89 /// Both `self` and its internal pointer are located on the C heap. | 87 /// Both `self` and its internal pointer are located on the C heap. |
90 /// You are responsible for calling [`free`](Self::free) | 88 /// You are responsible for calling [`free`](Self::free) |
91 /// on the pointer you get back when you're done with it. | 89 /// on the pointer you get back when you're done with it. |
92 pub fn alloc(text: impl AsRef<str>) -> Result<*mut Self, NulError> { | 90 pub fn alloc(text: impl AsRef<str>) -> Result<*mut Self, NulError> { |
93 let str_data = Self::malloc_str(text)?; | 91 let str_data = Self::malloc_str(text)?; |
94 let inner = ResponseInner::alloc(str_data); | 92 let inner = GenericResponse::alloc(str_data); |
95 Ok(inner as *mut Self) | 93 Ok(inner as *mut Self) |
96 } | 94 } |
97 | 95 |
98 /// Gets the string stored in this response. | 96 /// Gets the string stored in this response. |
99 pub fn contents(&self) -> &CStr { | 97 pub fn contents(&self) -> &CStr { |
100 // SAFETY: This data is either passed from PAM (so we are forced to | 98 // SAFETY: This data is either passed from PAM (so we are forced to |
101 // trust it) or was created by us in TextResponseInner::alloc. | 99 // trust it) or was created by us in TextResponseInner::alloc. |
102 // In either case, it's going to be a valid null-terminated string. | 100 // In either case, it's going to be a valid null-terminated string. |
103 unsafe { CStr::from_ptr(self.0.data as *const c_char) } | 101 unsafe { CStr::from_ptr(self.data) } |
104 } | 102 } |
105 | 103 |
106 /// Releases memory owned by this response. | 104 /// Releases memory owned by this response. |
107 /// | 105 /// |
108 /// # Safety | 106 /// # Safety |
109 /// | 107 /// |
110 /// You are responsible for no longer using this after calling free. | 108 /// You are responsible for no longer using this after calling free. |
111 pub unsafe fn free(me: *mut Self) { | 109 pub unsafe fn free(me: *mut Self) { |
112 ResponseInner::free(me as *mut ResponseInner) | 110 if !me.is_null() { |
111 let data = (*me).data; | |
112 if !data.is_null() { | |
113 libc::memset(data as *mut c_void, 0, libc::strlen(data)); | |
114 } | |
115 libc::free(data as *mut c_void); | |
116 } | |
117 libc::free(me as *mut c_void); | |
113 } | 118 } |
114 | 119 |
115 /// Allocates a string with the given contents on the C heap. | 120 /// Allocates a string with the given contents on the C heap. |
116 /// | 121 /// |
117 /// This is like [`CString::new`](std::ffi::CString::new), but: | 122 /// This is like [`CString::new`](std::ffi::CString::new), but: |
118 /// | 123 /// |
119 /// - it allocates data on the C heap with [`libc::malloc`]. | 124 /// - it allocates data on the C heap with [`libc::malloc`]. |
120 /// - it doesn't take ownership of the data passed in. | 125 /// - it doesn't take ownership of the data passed in. |
121 fn malloc_str(text: impl AsRef<str>) -> Result<*const c_void, NulError> { | 126 fn malloc_str(text: impl AsRef<str>) -> Result<*mut c_void, NulError> { |
122 let data = text.as_ref().as_bytes(); | 127 let data = text.as_ref().as_bytes(); |
123 if let Some(nul) = data.iter().position(|x| *x == 0) { | 128 if let Some(nul) = data.iter().position(|x| *x == 0) { |
124 return Err(NulError(nul)); | 129 return Err(NulError(nul)); |
125 } | 130 } |
126 unsafe { | 131 unsafe { |
127 let data_alloc = libc::calloc(data.len() + 1, 1); | 132 let data_alloc = libc::calloc(data.len() + 1, 1); |
128 libc::memcpy(data_alloc, data.as_ptr() as *const c_void, data.len()); | 133 libc::memcpy(data_alloc, data.as_ptr() as *const c_void, data.len()); |
129 Ok(data_alloc as *const c_void) | 134 Ok(data_alloc) |
130 } | 135 } |
131 } | 136 } |
132 } | 137 } |
133 | 138 |
134 /// A [`ResponseInner`] with [`BinaryData`] in it. | 139 /// A [`GenericResponse`] with [`BinaryData`] in it. |
135 #[repr(transparent)] | 140 #[repr(C)] |
136 pub struct BinaryResponseInner(ResponseInner); | 141 pub struct BinaryResponseInner { |
142 data: *mut BinaryData, | |
143 _unused: c_int, | |
144 } | |
137 | 145 |
138 impl BinaryResponseInner { | 146 impl BinaryResponseInner { |
139 /// Allocates a new binary response on the C heap. | 147 /// Allocates a new binary response on the C heap. |
140 /// | 148 /// |
141 /// The `data_type` is a tag you can use for whatever. | 149 /// The `data_type` is a tag you can use for whatever. |
142 /// It is passed through PAM unchanged. | 150 /// It is passed through PAM unchanged. |
143 /// | 151 /// |
144 /// The referenced data is copied to the C heap. We do not take ownership. | 152 /// The referenced data is copied to the C heap. We do not take ownership. |
145 /// You are responsible for calling [`free`](Self::free) | 153 /// You are responsible for calling [`free`](Self::free) |
146 /// on the pointer you get back when you're done with it. | 154 /// on the pointer you get back when you're done with it. |
147 pub fn alloc(data: impl AsRef<[u8]>, data_type: u8) -> Result<*mut Self, TryFromIntError> { | 155 pub fn alloc(data: &[u8], data_type: u8) -> Result<*mut Self, TooBigError> { |
148 let bin_data = BinaryData::alloc(data, data_type)?; | 156 let bin_data = BinaryData::alloc(data, data_type)?; |
149 let inner = ResponseInner::alloc(bin_data as *const c_void); | 157 let inner = GenericResponse::alloc(bin_data as *mut c_void); |
150 Ok(inner as *mut Self) | 158 Ok(inner as *mut Self) |
151 } | 159 } |
152 | 160 |
153 /// Gets the binary data in this response. | 161 /// Gets the binary data in this response. |
154 pub fn contents(&self) -> &[u8] { | 162 pub fn contents(&self) -> &[u8] { |
155 let data = self.data(); | 163 self.data().contents() |
156 let length = (u32::from_be_bytes(data.total_length) - 5) as usize; | |
157 unsafe { slice::from_raw_parts(data.data.as_ptr(), length) } | |
158 } | 164 } |
159 | 165 |
160 /// Gets the `data_type` tag that was embedded with the message. | 166 /// Gets the `data_type` tag that was embedded with the message. |
161 pub fn data_type(&self) -> u8 { | 167 pub fn data_type(&self) -> u8 { |
162 self.data().data_type | 168 self.data().data_type |
165 #[inline] | 171 #[inline] |
166 fn data(&self) -> &BinaryData { | 172 fn data(&self) -> &BinaryData { |
167 // SAFETY: This was either something we got from PAM (in which case | 173 // SAFETY: This was either something we got from PAM (in which case |
168 // we trust it), or something that was created with | 174 // we trust it), or something that was created with |
169 // BinaryResponseInner::alloc. In both cases, it points to valid data. | 175 // BinaryResponseInner::alloc. In both cases, it points to valid data. |
170 unsafe { &*(self.0.data as *const BinaryData) } | 176 unsafe { &*(self.data) } |
171 } | 177 } |
172 | 178 |
173 /// Releases memory owned by this response. | 179 /// Releases memory owned by this response. |
174 /// | 180 /// |
175 /// # Safety | 181 /// # Safety |
176 /// | 182 /// |
177 /// You are responsible for not using this after calling free. | 183 /// You are responsible for not using this after calling free. |
178 pub unsafe fn free(me: *mut Self) { | 184 pub unsafe fn free(me: *mut Self) { |
179 ResponseInner::free(me as *mut ResponseInner) | 185 if !me.is_null() { |
180 } | 186 BinaryData::free((*me).data); |
181 } | 187 } |
182 | |
183 #[repr(C)] | |
184 pub struct ResponseInner { | |
185 /// Pointer to the data returned in a response. | |
186 /// For most responses, this will be a [`CStr`], but for responses to | |
187 /// [`MessageStyle::BinaryPrompt`]s, this will be [`BinaryData`] | |
188 data: *const c_void, | |
189 /// Unused. | |
190 return_code: c_int, | |
191 } | |
192 | |
193 impl ResponseInner { | |
194 /// Allocates a response on the C heap pointing to the given data. | |
195 fn alloc(data: *const c_void) -> *mut Self { | |
196 unsafe { | |
197 let alloc = libc::calloc(1, size_of::<ResponseInner>()) as *mut ResponseInner; | |
198 (*alloc).data = data; | |
199 alloc | |
200 } | |
201 } | |
202 | |
203 /// Frees one of these that was created with [`Self::alloc`] | |
204 /// (or provided by PAM). | |
205 /// | |
206 /// # Safety | |
207 /// | |
208 /// It's up to you to stop using `me` after calling this. | |
209 unsafe fn free(me: *mut Self) { | |
210 libc::free((*me).data as *mut c_void); | |
211 libc::free(me as *mut c_void) | 188 libc::free(me as *mut c_void) |
212 } | 189 } |
213 } | 190 } |
214 | 191 |
215 /// Binary data used in requests and responses. | 192 /// Binary data used in requests and responses. |
216 /// | 193 /// |
217 /// This is an unsized data type whose memory goes beyond its data. | 194 /// This is an unsized data type whose memory goes beyond its data. |
218 /// This must be allocated on the C heap. | 195 /// This must be allocated on the C heap. |
196 /// | |
197 /// A Linux-PAM extension. | |
219 #[repr(C)] | 198 #[repr(C)] |
220 struct BinaryData { | 199 struct BinaryData { |
221 /// The total length of the structure; a u32 in network byte order (BE). | 200 /// The total length of the structure; a u32 in network byte order (BE). |
222 total_length: [u8; 4], | 201 total_length: [u8; 4], |
223 /// A tag of undefined meaning. | 202 /// A tag of undefined meaning. |
226 data: [u8; 0], | 205 data: [u8; 0], |
227 _marker: Immovable, | 206 _marker: Immovable, |
228 } | 207 } |
229 | 208 |
230 impl BinaryData { | 209 impl BinaryData { |
231 fn alloc( | 210 /// Copies the given data to a new BinaryData on the heap. |
232 source: impl AsRef<[u8]>, | 211 fn alloc(source: &[u8], data_type: u8) -> Result<*mut BinaryData, TooBigError> { |
233 data_type: u8, | 212 let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError { |
234 ) -> Result<*const BinaryData, TryFromIntError> { | 213 max: (u32::MAX - 5) as usize, |
235 let source = source.as_ref(); | 214 actual: source.len(), |
236 let buffer_size = u32::try_from(source.len() + 5)?; | 215 })?; |
237 let data = unsafe { | 216 let data = unsafe { |
238 let dest_buffer = libc::malloc(buffer_size as usize) as *mut BinaryData; | 217 let dest_buffer = libc::malloc(buffer_size as usize) as *mut BinaryData; |
239 let data = &mut *dest_buffer; | 218 let data = &mut *dest_buffer; |
240 data.total_length = buffer_size.to_be_bytes(); | 219 data.total_length = buffer_size.to_be_bytes(); |
241 data.data_type = data_type; | 220 data.data_type = data_type; |
247 ); | 226 ); |
248 dest_buffer | 227 dest_buffer |
249 }; | 228 }; |
250 Ok(data) | 229 Ok(data) |
251 } | 230 } |
231 | |
232 fn length(&self) -> usize { | |
233 u32::from_be_bytes(self.total_length).saturating_sub(5) as usize | |
234 } | |
235 | |
236 fn contents(&self) -> &[u8] { | |
237 unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) } | |
238 } | |
239 | |
240 /// Clears this data and frees it. | |
241 fn free(me: *mut Self) { | |
242 if me.is_null() { | |
243 return; | |
244 } | |
245 unsafe { | |
246 let me_too = &mut *me; | |
247 let contents = slice::from_raw_parts_mut(me_too.data.as_mut_ptr(), me_too.length()); | |
248 for v in contents { | |
249 *v = 0 | |
250 } | |
251 me_too.data_type = 0; | |
252 me_too.total_length = [0; 4]; | |
253 libc::free(me as *mut c_void); | |
254 } | |
255 } | |
256 } | |
257 | |
258 /// Generic version of response data. | |
259 /// | |
260 /// This has the same structure as [`BinaryResponseInner`] | |
261 /// and [`TextResponseInner`]. | |
262 #[repr(C)] | |
263 pub struct GenericResponse { | |
264 /// Pointer to the data returned in a response. | |
265 /// For most responses, this will be a [`CStr`], but for responses to | |
266 /// [`MessageStyle::BinaryPrompt`]s, this will be [`BinaryData`] | |
267 /// (a Linux-PAM extension). | |
268 data: *mut c_void, | |
269 /// Unused. | |
270 return_code: c_int, | |
271 } | |
272 | |
273 impl GenericResponse { | |
274 /// Allocates a response on the C heap pointing to the given data. | |
275 fn alloc(data: *mut c_void) -> *mut Self { | |
276 unsafe { | |
277 let alloc = libc::calloc(1, size_of::<Self>()) as *mut Self; | |
278 (*alloc).data = data; | |
279 alloc | |
280 } | |
281 } | |
282 | |
283 /// Frees a response on the C heap. | |
284 /// | |
285 /// # Safety | |
286 /// | |
287 /// It's on you to stop using this GenericResponse after freeing it. | |
288 pub unsafe fn free(me: *mut GenericResponse) { | |
289 if !me.is_null() { | |
290 libc::free((*me).data); | |
291 } | |
292 libc::free(me as *mut c_void); | |
293 } | |
252 } | 294 } |
253 | 295 |
254 /// An opaque pointer we provide to PAM for callbacks. | 296 /// An opaque pointer we provide to PAM for callbacks. |
255 #[repr(C)] | 297 #[repr(C)] |
256 pub struct AppData { | 298 pub struct AppData { |
259 } | 301 } |
260 | 302 |
261 /// The callback that PAM uses to get information in a conversation. | 303 /// The callback that PAM uses to get information in a conversation. |
262 /// | 304 /// |
263 /// - `num_msg` is the number of messages in the `pam_message` array. | 305 /// - `num_msg` is the number of messages in the `pam_message` array. |
264 /// - `messages` is a pointer to an array of pointers to [`Message`]s. | 306 /// - `messages` is a pointer to some [`Message`]s (see note). |
265 /// - `responses` is a pointer to an array of [`ResponseInner`]s, | 307 /// - `responses` is a pointer to an array of [`GenericResponse`]s, |
266 /// which PAM sets in response to a module's request. | 308 /// which PAM sets in response to a module's request. |
309 /// This is an array of structs, not an array of pointers to a struct. | |
310 /// There should always be exactly as many `responses` as `num_msg`. | |
267 /// - `appdata` is the `appdata` field of the [`Conversation`] we were passed. | 311 /// - `appdata` is the `appdata` field of the [`Conversation`] we were passed. |
312 /// | |
313 /// NOTE: On Linux-PAM and other compatible implementations, `messages` | |
314 /// is treated as a pointer-to-pointers, like `int argc, char **argv`. | |
315 /// | |
316 /// ```text | |
317 /// ┌──────────┐ points to ┌─────────────┐ ╔═ Message ═╗ | |
318 /// │ messages │ ┄┄┄┄┄┄┄┄┄┄> │ messages[0] │ ┄┄┄┄> ║ style ║ | |
319 /// └──────────┘ │ messages[1] │ ┄┄╮ ║ data ║ | |
320 /// │ ... │ ┆ ╚═══════════╝ | |
321 /// ┆ | |
322 /// ┆ ╔═ Message ═╗ | |
323 /// ╰┄┄> ║ style ║ | |
324 /// ║ data ║ | |
325 /// ╚═══════════╝ | |
326 /// ``` | |
327 /// | |
328 /// On OpenPAM and other compatible implementations (like Solaris), | |
329 /// `messages` is a pointer-to-pointer-to-array. | |
330 /// | |
331 /// ```text | |
332 /// ┌──────────┐ points to ┌───────────┐ ╔═ Message[] ═╗ | |
333 /// │ messages │ ┄┄┄┄┄┄┄┄┄┄> │ *messages │ ┄┄┄┄> ║ style ║ | |
334 /// └──────────┘ └───────────┘ ║ data ║ | |
335 /// ╟─────────────╢ | |
336 /// ║ style ║ | |
337 /// ║ data ║ | |
338 /// ╟─────────────╢ | |
339 /// ║ ... ║ | |
340 /// ``` | |
341 /// | |
342 /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** | |
268 pub type ConversationCallback = extern "C" fn( | 343 pub type ConversationCallback = extern "C" fn( |
269 num_msg: c_int, | 344 num_msg: c_int, |
270 messages: *const *const Message, | 345 messages: *const *const Message, |
271 responses: &mut *const ResponseInner, | 346 responses: &mut *const GenericResponse, |
272 appdata: *const AppData, | 347 appdata: *const AppData, |
273 ) -> c_int; | 348 ) -> c_int; |
274 | 349 |
275 /// A callback and the associated [`AppData`] pointer that needs to be passed back to it. | 350 /// A callback and the associated [`AppData`] pointer that needs to be passed back to it. |
276 #[repr(C)] | 351 #[repr(C)] |
314 pub fn pam_end(pamh: *mut Handle, status: c_int) -> c_int; | 389 pub fn pam_end(pamh: *mut Handle, status: c_int) -> c_int; |
315 } | 390 } |
316 | 391 |
317 #[cfg(test)] | 392 #[cfg(test)] |
318 mod test { | 393 mod test { |
319 use super::{BinaryResponseInner, TextResponseInner}; | 394 use super::{BinaryResponseInner, GenericResponse, TextResponseInner}; |
320 | 395 |
321 #[test] | 396 #[test] |
322 fn test_text_response() { | 397 fn test_text_response() { |
323 let resp = TextResponseInner::alloc("hello").expect("alloc should succeed"); | 398 let resp = TextResponseInner::alloc("hello").expect("alloc should succeed"); |
324 let borrow_resp = unsafe { &*resp }; | 399 let borrow_resp = unsafe { &*resp }; |
340 assert_eq!(7, borrow_resp.data_type()); | 415 assert_eq!(7, borrow_resp.data_type()); |
341 unsafe { BinaryResponseInner::free(resp) }; | 416 unsafe { BinaryResponseInner::free(resp) }; |
342 } | 417 } |
343 | 418 |
344 #[test] | 419 #[test] |
420 fn test_free_safety() { | |
421 unsafe { | |
422 TextResponseInner::free(std::ptr::null_mut()); | |
423 BinaryResponseInner::free(std::ptr::null_mut()); | |
424 } | |
425 } | |
426 | |
427 #[test] | |
345 #[ignore] | 428 #[ignore] |
346 fn test_binary_response_too_big() { | 429 fn test_binary_response_too_big() { |
347 let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000]; | 430 let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000]; |
348 BinaryResponseInner::alloc(&big_data, 0).expect_err("this is too big!"); | 431 BinaryResponseInner::alloc(&big_data, 0).expect_err("this is too big!"); |
349 } | 432 } |