Mercurial > crates > nonstick
view src/pam_ffi/memory.rs @ 74:c7c596e6388f
Make conversations type-safe (last big reorg) (REAL) (NOT CLICKBAIT)
In previous versions of Conversation, you could send messages and then
return messages of the wrong type or in the wrong order or whatever.
The receiver would then have to make sure that there were the right
number of messages and that each message was the right type.
That's annoying.
This change makes the `Message` enum a two-way channel, where the asker
puts their question into it, and then the answerer (the conversation)
puts the answer in and returns control to the asker. The asker then
only has to pull the Answer of the type they wanted out of the message.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Fri, 06 Jun 2025 22:21:17 -0400 |
parents | ac6881304c78 |
children |
line wrap: on
line source
//! Things for dealing with memory. use crate::ErrorCode; use crate::Result; use std::ffi::{c_char, c_void, CStr, CString}; use std::marker::{PhantomData, PhantomPinned}; use std::result::Result as StdResult; use std::{ptr, slice}; /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. #[repr(C)] #[derive(Debug)] pub struct Immovable(pub PhantomData<(*mut u8, PhantomPinned)>); /// Safely converts a `&str` option to a `CString` option. pub fn option_cstr(prompt: Option<&str>) -> Result<Option<CString>> { prompt .map(CString::new) .transpose() .map_err(|_| ErrorCode::ConversationError) } /// Gets the pointer to the given CString, or a null pointer if absent. pub fn prompt_ptr(prompt: Option<&CString>) -> *const c_char { match prompt { Some(c_str) => c_str.as_ptr(), None => ptr::null(), } } /// Creates an owned copy of a string that is returned from a /// <code>pam_get_<var>whatever</var></code> function. /// /// # Safety /// /// It's on you to provide a valid string. pub unsafe fn copy_pam_string(result_ptr: *const libc::c_char) -> Result<String> { // We really shouldn't get a null pointer back here, but if we do, return nothing. if result_ptr.is_null() { return Ok(String::new()); } let bytes = unsafe { CStr::from_ptr(result_ptr) }; bytes .to_str() .map(String::from) .map_err(|_| ErrorCode::ConversationError) } /// Wraps a string returned from PAM as an `Option<&str>`. pub unsafe fn wrap_string<'a>(data: *const libc::c_char) -> Result<Option<&'a str>> { let ret = if data.is_null() { None } else { Some( CStr::from_ptr(data) .to_str() .map_err(|_| ErrorCode::ConversationError)?, ) }; Ok(ret) } /// Allocates a string with the given contents on the C heap. /// /// This is like [`CString::new`](std::ffi::CString::new), but: /// /// - it allocates data on the C heap with [`libc::malloc`]. /// - it doesn't take ownership of the data passed in. pub fn malloc_str(text: impl AsRef<str>) -> StdResult<*mut c_char, NulError> { let data = text.as_ref().as_bytes(); if let Some(nul) = data.iter().position(|x| *x == 0) { return Err(NulError(nul)); } unsafe { let data_alloc = libc::calloc(data.len() + 1, 1); libc::memcpy(data_alloc, data.as_ptr().cast(), data.len()); Ok(data_alloc.cast()) } } /// Writes zeroes over the contents of a C string. /// /// This won't overwrite a null pointer. /// /// # Safety /// /// It's up to you to provide a valid C string. pub unsafe fn zero_c_string(cstr: *mut c_void) { if !cstr.is_null() { libc::memset(cstr, 0, libc::strlen(cstr.cast())); } } /// Binary data used in requests and responses. /// /// This is an unsized data type whose memory goes beyond its data. /// This must be allocated on the C heap. /// /// A Linux-PAM extension. #[repr(C)] pub struct CBinaryData { /// The total length of the structure; a u32 in network byte order (BE). total_length: [u8; 4], /// A tag of undefined meaning. data_type: u8, /// Pointer to an array of length [`length`](Self::length) − 5 data: [u8; 0], _marker: Immovable, } impl CBinaryData { /// Copies the given data to a new BinaryData on the heap. pub fn alloc(source: &[u8], data_type: u8) -> StdResult<*mut CBinaryData, TooBigError> { let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError { max: (u32::MAX - 5) as usize, actual: source.len(), })?; // SAFETY: We're only allocating here. let data = unsafe { let dest_buffer: *mut CBinaryData = libc::malloc(buffer_size as usize).cast(); let data = &mut *dest_buffer; data.total_length = buffer_size.to_be_bytes(); data.data_type = data_type; let dest = data.data.as_mut_ptr(); libc::memcpy(dest.cast(), source.as_ptr().cast(), source.len()); dest_buffer }; Ok(data) } fn length(&self) -> usize { u32::from_be_bytes(self.total_length).saturating_sub(5) as usize } pub fn contents(&self) -> &[u8] { unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) } } pub fn data_type(&self) -> u8 { self.data_type } /// Clears this data and frees it. pub unsafe fn zero_contents(&mut self) { let contents = slice::from_raw_parts_mut(self.data.as_mut_ptr(), self.length()); for v in contents { *v = 0 } self.data_type = 0; self.total_length = [0; 4]; } } #[derive(Debug, thiserror::Error)] #[error("null byte within input at byte {0}")] pub struct NulError(pub usize); /// Returned when trying to fit too much data into a binary message. #[derive(Debug, thiserror::Error)] #[error("cannot create a message of {actual} bytes; maximum is {max}")] pub struct TooBigError { pub actual: usize, pub max: usize, } #[cfg(test)] mod tests { use super::{copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string}; use crate::ErrorCode; use std::ffi::CString; #[test] fn test_strings() { let str = malloc_str("hello there").unwrap(); malloc_str("hell\0 there").unwrap_err(); unsafe { let copied = copy_pam_string(str.cast()).unwrap(); assert_eq!("hello there", copied); zero_c_string(str.cast()); let idx_three = str.add(3).as_mut().unwrap(); *idx_three = 0x80u8 as i8; let zeroed = copy_pam_string(str.cast()).unwrap(); assert!(zeroed.is_empty()); libc::free(str.cast()); } } #[test] fn test_option_str() { let good = option_cstr(Some("whatever")).unwrap(); assert_eq!("whatever", good.unwrap().to_str().unwrap()); let no_str = option_cstr(None).unwrap(); assert!(no_str.is_none()); let bad_str = option_cstr(Some("what\0ever")).unwrap_err(); assert_eq!(ErrorCode::ConversationError, bad_str); } #[test] fn test_prompt() { let prompt_cstr = CString::new("good").ok(); let prompt = prompt_ptr(prompt_cstr.as_ref()); assert!(!prompt.is_null()); let no_prompt = prompt_ptr(None); assert!(no_prompt.is_null()); } }