Mercurial > crates > nonstick
comparison src/pam_ffi.rs @ 69:8f3ae0c7ab92
Rework conversation data types and make safe wrappers.
This removes the old `Conversation` type and reworks the FFI types
used for PAM conversations.
This creates safe `TestResponse` and `BinaryResponse` structures in `conv`,
providing a safe way to pass response messages to PAM Conversations.
The internals of these types are allocated on the C heap, as required by PAM.
We also remove the Conversation struct, which was specific to the real PAM
implementation so that we can introduce a better abstraction.
Also splits a new `PamApplicationHandle` trait from `PamHandle`,
for the parts of a PAM handle that are specific to the application side
of a PAM transaction.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Sun, 01 Jun 2025 01:15:04 -0400 |
parents | a674799a5cd3 |
children | 9f8381a1c09c |
comparison
equal
deleted
inserted
replaced
68:e4e7d68234d0 | 69:8f3ae0c7ab92 |
---|---|
1 //! FFI to the PAM library. | 1 //! The PAM library FFI and helpers for managing it. |
2 | 2 //! |
3 use libc::c_char; | 3 //! This includes the functions provided by PAM and the data structures |
4 use std::ffi::c_int; | 4 //! used by PAM, as well as a few low-level abstractions for dealing with |
5 //! those data structures. | |
6 //! | |
7 //! Everything in here is hazmat. | |
8 | |
9 // Temporarily allow dead code. | |
10 #![allow(dead_code)] | |
11 | |
12 use crate::constants::InvalidEnum; | |
13 use num_derive::FromPrimitive; | |
14 use num_traits::FromPrimitive; | |
15 use std::ffi::{c_char, c_int, c_void, CStr}; | |
5 use std::marker::{PhantomData, PhantomPinned}; | 16 use std::marker::{PhantomData, PhantomPinned}; |
17 use std::num::TryFromIntError; | |
18 use std::slice; | |
19 use thiserror::Error; | |
20 | |
21 /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. | |
22 type Immovable = PhantomData<(*mut u8, PhantomPinned)>; | |
6 | 23 |
7 /// An opaque pointer given to us by PAM. | 24 /// An opaque pointer given to us by PAM. |
8 #[repr(C)] | 25 #[repr(C)] |
9 pub struct Handle { | 26 pub struct Handle { |
10 _data: (), | 27 _data: (), |
11 _marker: PhantomData<(*mut u8, PhantomPinned)>, | 28 _marker: Immovable, |
29 } | |
30 | |
31 /// Styles of message that are shown to the user. | |
32 #[derive(Debug, PartialEq, FromPrimitive)] | |
33 #[non_exhaustive] // non-exhaustive because C might give us back anything! | |
34 pub enum MessageStyle { | |
35 /// Requests information from the user; will be masked when typing. | |
36 PromptEchoOff = 1, | |
37 /// Requests information from the user; will not be masked. | |
38 PromptEchoOn = 2, | |
39 /// An error message. | |
40 ErrorMsg = 3, | |
41 /// An informational message. | |
42 TextInfo = 4, | |
43 /// Yes/No/Maybe conditionals. Linux-PAM specific. | |
44 RadioType = 5, | |
45 /// For server–client non-human interaction. | |
46 /// NOT part of the X/Open PAM specification. | |
47 BinaryPrompt = 7, | |
48 } | |
49 | |
50 impl TryFrom<c_int> for MessageStyle { | |
51 type Error = InvalidEnum<Self>; | |
52 fn try_from(value: c_int) -> std::result::Result<Self, Self::Error> { | |
53 Self::from_i32(value).ok_or(value.into()) | |
54 } | |
55 } | |
56 | |
57 impl From<MessageStyle> for c_int { | |
58 fn from(val: MessageStyle) -> Self { | |
59 val as Self | |
60 } | |
61 } | |
62 | |
63 /// A message sent by PAM or a module to an application. | |
64 /// This message, and its internal data, is owned by the creator | |
65 /// (either the module or PAM itself). | |
66 #[repr(C)] | |
67 pub struct Message { | |
68 /// The style of message to request. | |
69 style: c_int, | |
70 /// A description of the data requested. | |
71 /// For most requests, this will be an owned [`CStr`], but for requests | |
72 /// with [`MessageStyle::BinaryPrompt`], this will be [`BinaryData`]. | |
73 data: *const c_void, | |
74 } | |
75 | |
76 /// Returned when text that should not have any `\0` bytes in it does. | |
77 /// Analogous to [`std::ffi::NulError`], but the data it was created from | |
78 /// is borrowed. | |
79 #[derive(Debug, Error)] | |
80 #[error("null byte within input at byte {0}")] | |
81 pub struct NulError(usize); | |
82 | |
83 #[repr(transparent)] | |
84 pub struct TextResponseInner(ResponseInner); | |
85 | |
86 impl TextResponseInner { | |
87 /// Allocates a new text response on the C heap. | |
88 /// | |
89 /// Both `self` and its internal pointer are located on the C heap. | |
90 /// You are responsible for calling [`free`](Self::free) | |
91 /// on the pointer you get back when you're done with it. | |
92 pub fn alloc(text: impl AsRef<str>) -> Result<*mut Self, NulError> { | |
93 let str_data = Self::malloc_str(text)?; | |
94 let inner = ResponseInner::alloc(str_data); | |
95 Ok(inner as *mut Self) | |
96 } | |
97 | |
98 /// Gets the string stored in this response. | |
99 pub fn contents(&self) -> &CStr { | |
100 // SAFETY: This data is either passed from PAM (so we are forced to | |
101 // trust it) or was created by us in TextResponseInner::alloc. | |
102 // 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) } | |
104 } | |
105 | |
106 /// Releases memory owned by this response. | |
107 /// | |
108 /// # Safety | |
109 /// | |
110 /// You are responsible for no longer using this after calling free. | |
111 pub unsafe fn free(me: *mut Self) { | |
112 ResponseInner::free(me as *mut ResponseInner) | |
113 } | |
114 | |
115 /// Allocates a string with the given contents on the C heap. | |
116 /// | |
117 /// This is like [`CString::new`](std::ffi::CString::new), but: | |
118 /// | |
119 /// - it allocates data on the C heap with [`libc::malloc`]. | |
120 /// - it doesn't take ownership of the data passed in. | |
121 fn malloc_str(text: impl AsRef<str>) -> Result<*const c_void, NulError> { | |
122 let data = text.as_ref().as_bytes(); | |
123 if let Some(nul) = data.iter().position(|x| *x == 0) { | |
124 return Err(NulError(nul)); | |
125 } | |
126 unsafe { | |
127 let data_alloc = libc::calloc(data.len() + 1, 1); | |
128 libc::memcpy(data_alloc, data.as_ptr() as *const c_void, data.len()); | |
129 Ok(data_alloc as *const c_void) | |
130 } | |
131 } | |
132 } | |
133 | |
134 /// A [`ResponseInner`] with [`BinaryData`] in it. | |
135 #[repr(transparent)] | |
136 pub struct BinaryResponseInner(ResponseInner); | |
137 | |
138 impl BinaryResponseInner { | |
139 /// Allocates a new binary response on the C heap. | |
140 /// | |
141 /// The `data_type` is a tag you can use for whatever. | |
142 /// It is passed through PAM unchanged. | |
143 /// | |
144 /// The referenced data is copied to the C heap. We do not take ownership. | |
145 /// You are responsible for calling [`free`](Self::free) | |
146 /// 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> { | |
148 let bin_data = BinaryData::alloc(data, data_type)?; | |
149 let inner = ResponseInner::alloc(bin_data as *const c_void); | |
150 Ok(inner as *mut Self) | |
151 } | |
152 | |
153 /// Gets the binary data in this response. | |
154 pub fn contents(&self) -> &[u8] { | |
155 let data = self.data(); | |
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 } | |
159 | |
160 /// Gets the `data_type` tag that was embedded with the message. | |
161 pub fn data_type(&self) -> u8 { | |
162 self.data().data_type | |
163 } | |
164 | |
165 #[inline] | |
166 fn data(&self) -> &BinaryData { | |
167 // SAFETY: This was either something we got from PAM (in which case | |
168 // we trust it), or something that was created with | |
169 // BinaryResponseInner::alloc. In both cases, it points to valid data. | |
170 unsafe { &*(self.0.data as *const BinaryData) } | |
171 } | |
172 | |
173 /// Releases memory owned by this response. | |
174 /// | |
175 /// # Safety | |
176 /// | |
177 /// You are responsible for not using this after calling free. | |
178 pub unsafe fn free(me: *mut Self) { | |
179 ResponseInner::free(me as *mut ResponseInner) | |
180 } | |
181 } | |
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) | |
212 } | |
213 } | |
214 | |
215 /// Binary data used in requests and responses. | |
216 /// | |
217 /// This is an unsized data type whose memory goes beyond its data. | |
218 /// This must be allocated on the C heap. | |
219 #[repr(C)] | |
220 struct BinaryData { | |
221 /// The total length of the structure; a u32 in network byte order (BE). | |
222 total_length: [u8; 4], | |
223 /// A tag of undefined meaning. | |
224 data_type: u8, | |
225 /// Pointer to an array of length [`length`](Self::length) − 5 | |
226 data: [u8; 0], | |
227 _marker: Immovable, | |
228 } | |
229 | |
230 impl BinaryData { | |
231 fn alloc( | |
232 source: impl AsRef<[u8]>, | |
233 data_type: u8, | |
234 ) -> Result<*const BinaryData, TryFromIntError> { | |
235 let source = source.as_ref(); | |
236 let buffer_size = u32::try_from(source.len() + 5)?; | |
237 let data = unsafe { | |
238 let dest_buffer = libc::malloc(buffer_size as usize) as *mut BinaryData; | |
239 let data = &mut *dest_buffer; | |
240 data.total_length = buffer_size.to_be_bytes(); | |
241 data.data_type = data_type; | |
242 let dest = data.data.as_mut_ptr(); | |
243 libc::memcpy( | |
244 dest as *mut c_void, | |
245 source.as_ptr() as *const c_void, | |
246 source.len(), | |
247 ); | |
248 dest_buffer | |
249 }; | |
250 Ok(data) | |
251 } | |
252 } | |
253 | |
254 /// An opaque pointer we provide to PAM for callbacks. | |
255 #[repr(C)] | |
256 pub struct AppData { | |
257 _data: (), | |
258 _marker: Immovable, | |
259 } | |
260 | |
261 /// The callback that PAM uses to get information in a conversation. | |
262 /// | |
263 /// - `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. | |
265 /// - `responses` is a pointer to an array of [`ResponseInner`]s, | |
266 /// which PAM sets in response to a module's request. | |
267 /// - `appdata` is the `appdata` field of the [`Conversation`] we were passed. | |
268 pub type ConversationCallback = extern "C" fn( | |
269 num_msg: c_int, | |
270 messages: *const *const Message, | |
271 responses: &mut *const ResponseInner, | |
272 appdata: *const AppData, | |
273 ) -> c_int; | |
274 | |
275 /// A callback and the associated [`AppData`] pointer that needs to be passed back to it. | |
276 #[repr(C)] | |
277 pub struct Conversation { | |
278 callback: ConversationCallback, | |
279 appdata: *const AppData, | |
12 } | 280 } |
13 | 281 |
14 #[link(name = "pam")] | 282 #[link(name = "pam")] |
15 extern "C" { | 283 extern "C" { |
16 pub fn pam_get_data( | 284 pub fn pam_get_data( |
17 pamh: *const Handle, | 285 pamh: *const Handle, |
18 module_data_name: *const c_char, | 286 module_data_name: *const c_char, |
19 data: &mut *const libc::c_void, | 287 data: &mut *const c_void, |
20 ) -> c_int; | 288 ) -> c_int; |
21 | 289 |
22 pub fn pam_set_data( | 290 pub fn pam_set_data( |
23 pamh: *mut Handle, | 291 pamh: *mut Handle, |
24 module_data_name: *const c_char, | 292 module_data_name: *const c_char, |
25 data: *const libc::c_void, | 293 data: *const c_void, |
26 cleanup: extern "C" fn( | 294 cleanup: extern "C" fn(pamh: *const c_void, data: *mut c_void, error_status: c_int), |
27 pamh: *const libc::c_void, | |
28 data: *mut libc::c_void, | |
29 error_status: c_int, | |
30 ), | |
31 ) -> c_int; | 295 ) -> c_int; |
32 | 296 |
33 pub fn pam_get_item( | 297 pub fn pam_get_item(pamh: *const Handle, item_type: c_int, item: &mut *const c_void) -> c_int; |
34 pamh: *const Handle, | 298 |
35 item_type: c_int, | 299 pub fn pam_set_item(pamh: *mut Handle, item_type: c_int, item: *const c_void) -> c_int; |
36 item: &mut *const libc::c_void, | |
37 ) -> c_int; | |
38 | |
39 pub fn pam_set_item(pamh: *mut Handle, item_type: c_int, item: *const libc::c_void) -> c_int; | |
40 | 300 |
41 pub fn pam_get_user( | 301 pub fn pam_get_user( |
42 pamh: *const Handle, | 302 pamh: *const Handle, |
43 user: &mut *const c_char, | 303 user: &mut *const c_char, |
44 prompt: *const c_char, | 304 prompt: *const c_char, |
51 prompt: *const c_char, | 311 prompt: *const c_char, |
52 ) -> c_int; | 312 ) -> c_int; |
53 | 313 |
54 pub fn pam_end(pamh: *mut Handle, status: c_int) -> c_int; | 314 pub fn pam_end(pamh: *mut Handle, status: c_int) -> c_int; |
55 } | 315 } |
316 | |
317 #[cfg(test)] | |
318 mod test { | |
319 use super::{BinaryResponseInner, TextResponseInner}; | |
320 | |
321 #[test] | |
322 fn test_text_response() { | |
323 let resp = TextResponseInner::alloc("hello").expect("alloc should succeed"); | |
324 let borrow_resp = unsafe { &*resp }; | |
325 let data = borrow_resp.contents().to_str().expect("valid"); | |
326 assert_eq!("hello", data); | |
327 unsafe { | |
328 TextResponseInner::free(resp); | |
329 } | |
330 TextResponseInner::alloc("hell\0o").expect_err("should error; contains nul"); | |
331 } | |
332 | |
333 #[test] | |
334 fn test_binary_response() { | |
335 let real_data = [1, 2, 3, 4, 5, 6, 7, 8]; | |
336 let resp = BinaryResponseInner::alloc(&real_data, 7).expect("alloc should succeed"); | |
337 let borrow_resp = unsafe { &*resp }; | |
338 let data = borrow_resp.contents(); | |
339 assert_eq!(&real_data, data); | |
340 assert_eq!(7, borrow_resp.data_type()); | |
341 unsafe { BinaryResponseInner::free(resp) }; | |
342 } | |
343 | |
344 #[test] | |
345 #[ignore] | |
346 fn test_binary_response_too_big() { | |
347 let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000]; | |
348 BinaryResponseInner::alloc(&big_data, 0).expect_err("this is too big!"); | |
349 } | |
350 } |