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 } |
