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 }