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 }