Mercurial > crates > nonstick
view src/pam_ffi/response.rs @ 73:ac6881304c78
Do conversations, along with way too much stuff.
This implements conversations, along with all the memory management
brouhaha that goes along with it. The conversation now lives directly
on the handle rather than being a thing you have to get from it
and then call manually. It Turns Out this makes things a lot easier!
I guess we reorganized things again. For the last time. For real.
I promise.
This all passes ASAN, so it seems Pretty Good!
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Thu, 05 Jun 2025 03:41:38 -0400 |
parents | 47eb242a4f88 |
children |
line wrap: on
line source
//! Types used when dealing with PAM conversations. use crate::conv::BinaryData; use crate::pam_ffi::memory; use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError}; use crate::Response; use std::ffi::{c_int, c_void, CStr}; use std::ops::{Deref, DerefMut}; use std::result::Result as StdResult; use std::str::Utf8Error; use std::{iter, mem, ptr, slice}; #[repr(transparent)] #[derive(Debug)] pub struct RawTextResponse(RawResponse); impl RawTextResponse { /// Interprets the provided `RawResponse` as a text response. /// /// # Safety /// /// It's up to you to provide a response that is a `RawTextResponse`. pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self { // SAFETY: We're provided a valid reference. &mut *(from as *mut RawResponse).cast::<Self>() } /// Fills in the provided `RawResponse` with the given text. /// /// You are responsible for calling [`free`](Self::free_contents) /// on the pointer you get back when you're done with it. pub fn fill(dest: &mut RawResponse, text: impl AsRef<str>) -> StdResult<&mut Self, NulError> { dest.data = memory::malloc_str(text)?.cast(); // SAFETY: We just filled this in so we know it's a text response. Ok(unsafe { Self::upcast(dest) }) } /// Gets the string stored in this response. pub fn contents(&self) -> StdResult<&str, Utf8Error> { if self.0.data.is_null() { Ok("") } else { // SAFETY: This data is either passed from PAM (so we are forced to // trust it) or was created by us in TextResponseInner::alloc. // In either case, it's going to be a valid null-terminated string. unsafe { CStr::from_ptr(self.0.data.cast()) }.to_str() } } /// Releases memory owned by this response. pub fn free_contents(&mut self) { // SAFETY: We know we own this data. // After we're done, it will be null. unsafe { memory::zero_c_string(self.0.data); libc::free(self.0.data); self.0.data = ptr::null_mut() } } } /// A [`RawResponse`] with [`CBinaryData`] in it. #[repr(transparent)] #[derive(Debug)] pub struct RawBinaryResponse(RawResponse); impl RawBinaryResponse { /// Interprets the provided `RawResponse` as a binary response. /// /// # Safety /// /// It's up to you to provide a response that is a `RawBinaryResponse`. pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self { // SAFETY: We're provided a valid reference. &mut *(from as *mut RawResponse).cast::<Self>() } /// Fills in a `RawResponse` with the provided binary data. /// /// The `data_type` is a tag you can use for whatever. /// It is passed through PAM unchanged. /// /// The referenced data is copied to the C heap. We do not take ownership. /// You are responsible for calling [`free`](Self::free_contents) /// on the pointer you get back when you're done with it. pub fn fill<'a>( dest: &'a mut RawResponse, data: &[u8], data_type: u8, ) -> StdResult<&'a mut Self, TooBigError> { dest.data = CBinaryData::alloc(data, data_type)?.cast(); // SAFETY: We just filled this in, so we know it's binary. Ok(unsafe { Self::upcast(dest) }) } /// Gets the binary data in this response. pub fn data(&self) -> &[u8] { self.contents().map(CBinaryData::contents).unwrap_or(&[]) } /// Gets the `data_type` tag that was embedded with the message. pub fn data_type(&self) -> u8 { self.contents().map(CBinaryData::data_type).unwrap_or(0) } fn contents(&self) -> Option<&CBinaryData> { // SAFETY: This was either something we got from PAM (in which case // we trust it), or something that was created with // BinaryResponseInner::alloc. In both cases, it points to valid data. unsafe { self.0.data.cast::<CBinaryData>().as_ref() } } pub fn to_owned(&self) -> BinaryData { BinaryData::new(self.data().into(), self.data_type()) } /// Releases memory owned by this response. pub fn free_contents(&mut self) { // SAFETY: We know that our data pointer is either valid or null. // Once we're done, it's null and the response is safe. unsafe { let data_ref = self.0.data.cast::<CBinaryData>().as_mut(); if let Some(d) = data_ref { d.zero_contents() } libc::free(self.0.data); self.0.data = ptr::null_mut() } } } /// Generic version of response data. /// /// This has the same structure as [`RawBinaryResponse`] /// and [`RawTextResponse`]. #[repr(C)] #[derive(Debug)] pub struct RawResponse { /// Pointer to the data returned in a response. /// For most responses, this will be a [`CStr`], but for responses to /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`] /// (a Linux-PAM extension). data: *mut c_void, /// Unused. return_code: c_int, _marker: Immovable, } /// A contiguous block of responses. #[derive(Debug)] pub struct OwnedResponses { base: *mut RawResponse, count: usize, } impl OwnedResponses { /// Allocates an owned list of responses on the C heap. fn alloc(count: usize) -> Self { OwnedResponses { // SAFETY: We are doing allocation here. base: unsafe { libc::calloc(count, size_of::<RawResponse>()) }.cast(), count, } } pub fn build(value: &[Response]) -> StdResult<Self, FillError> { let mut outputs = OwnedResponses::alloc(value.len()); // If we fail in here after allocating OwnedResponses, // we still free all memory, even though we don't zero it first. // This is an acceptable level of risk. for (input, output) in iter::zip(value.iter(), outputs.iter_mut()) { match input { Response::NoResponse => { RawTextResponse::fill(output, "")?; } Response::Text(data) => { RawTextResponse::fill(output, data)?; } Response::MaskedText(data) => { RawTextResponse::fill(output, data.unsecure())?; } Response::Binary(data) => { RawBinaryResponse::fill(output, data.data(), data.data_type())?; } } } Ok(outputs) } /// Converts this into a `*RawResponse` for passing to PAM. /// /// The pointer "owns" its own data (i.e., this will not be dropped). pub fn into_ptr(self) -> *mut RawResponse { let ret = self.base; mem::forget(self); ret } /// Takes ownership of a list of responses allocated on the C heap. /// /// # Safety /// /// It's up to you to make sure you pass a valid pointer. pub unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self { OwnedResponses { base, count } } } #[derive(Debug, thiserror::Error)] #[error("error converting responses: {0}")] pub enum FillError { NulError(#[from] NulError), TooBigError(#[from] TooBigError), } impl Deref for OwnedResponses { type Target = [RawResponse]; fn deref(&self) -> &Self::Target { // SAFETY: This is the memory we manage ourselves. unsafe { slice::from_raw_parts(self.base, self.count) } } } impl DerefMut for OwnedResponses { fn deref_mut(&mut self) -> &mut Self::Target { // SAFETY: This is the memory we manage ourselves. unsafe { slice::from_raw_parts_mut(self.base, self.count) } } } impl Drop for OwnedResponses { fn drop(&mut self) { // SAFETY: We allocated this ourselves, or it was provided to us by PAM. unsafe { for resp in self.iter_mut() { libc::free(resp.data) } libc::free(self.base.cast()) } } } #[cfg(test)] mod tests { use super::{BinaryData, OwnedResponses, RawBinaryResponse, RawTextResponse, Response}; #[test] fn test_round_trip() { let responses = [ Response::Binary(BinaryData::new(vec![1, 2, 3], 99)), Response::Text("whats going on".to_owned()), Response::MaskedText("well then".into()), Response::NoResponse, Response::Text("bogus".to_owned()), ]; let sent = OwnedResponses::build(&responses).unwrap(); let heap_resps = sent.into_ptr(); let mut received = unsafe { OwnedResponses::from_c_heap(heap_resps, 5) }; let assert_text = |want, raw| { let up = unsafe { RawTextResponse::upcast(raw) }; assert_eq!(want, up.contents().unwrap()); up.free_contents(); assert_eq!("", up.contents().unwrap()); }; let assert_bin = |want_data: &[u8], want_type, raw| { let up = unsafe { RawBinaryResponse::upcast(raw) }; assert_eq!(want_data, up.data()); assert_eq!(want_type, up.data_type()); up.free_contents(); let empty: [u8; 0] = []; assert_eq!(&empty, up.data()); assert_eq!(0, up.data_type()); }; if let [zero, one, two, three, four] = &mut received[..] { assert_bin(&[1, 2, 3], 99, zero); assert_text("whats going on", one); assert_text("well then", two); assert_text("", three); assert_text("bogus", four); } else { panic!("wrong size!") } } #[test] fn test_text_response() { let mut responses = OwnedResponses::alloc(2); let text = RawTextResponse::fill(&mut responses[0], "hello").unwrap(); let data = text.contents().expect("valid"); assert_eq!("hello", data); text.free_contents(); text.free_contents(); RawTextResponse::fill(&mut responses[1], "hell\0").expect_err("should error; contains nul"); } #[test] fn test_binary_response() { let mut responses = OwnedResponses::alloc(1); let real_data = [1, 2, 3, 4, 5, 6, 7, 8]; let resp = RawBinaryResponse::fill(&mut responses[0], &real_data, 7) .expect("alloc should succeed"); let data = resp.data(); assert_eq!(&real_data, data); assert_eq!(7, resp.data_type()); resp.free_contents(); resp.free_contents(); } #[test] #[ignore] fn test_binary_response_too_big() { let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000]; let mut responses = OwnedResponses::alloc(1); RawBinaryResponse::fill(&mut responses[0], &big_data, 0).expect_err("this is too big!"); } }