view src/libpam/handle.rs @ 98:b87100c5eed4

Start on environment variables, and make pointers nicer. This starts work on the PAM environment handling, and in so doing, introduces the CHeapBox and CHeapString structs. These are analogous to Box and CString, but they're located on the C heap rather than being Rust-managed memory. This is because environment variables deal with even more pointers and it turns out we can lose a lot of manual freeing using homemade smart pointers.
author Paul Fisher <paul@pfish.zone>
date Tue, 24 Jun 2025 04:25:25 -0400
parents efe2f5f8b5b2
children 94b51fa4f797
line wrap: on
line source

use super::conversation::LibPamConversation;
use crate::constants::{ErrorCode, Result};
use crate::conv::Message;
use crate::environ::EnvironMapMut;
use crate::handle::PamShared;
use crate::libpam::environ::{LibPamEnviron, LibPamEnvironMut};
pub use crate::libpam::pam_ffi::LibPamHandle;
use crate::libpam::{memory, pam_ffi};
use crate::logging::Level;
use crate::{Conversation, EnvironMap, Flags, PamHandleApplication, PamHandleModule};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::cell::Cell;
use std::ffi::{c_char, c_int, CString};
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::ptr;

/// Owner for a PAM handle.
struct HandleWrap(*mut LibPamHandle);

impl Deref for HandleWrap {
    type Target = LibPamHandle;
    fn deref(&self) -> &Self::Target {
        unsafe { &*self.0 }
    }
}

impl DerefMut for HandleWrap {
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe { &mut *self.0 }
    }
}

/// An owned PAM handle.
pub struct OwnedLibPamHandle<'a> {
    handle: HandleWrap,
    last_return: Cell<Result<()>>,
    _conversation_lifetime: PhantomData<&'a mut ()>,
}

#[derive(Debug, PartialEq)]
pub struct HandleBuilder {
    service_name: String,
    username: Option<String>,
}

impl HandleBuilder {
    /// Creates a new HandleBuilder for the given service.
    fn new(service_name: String) -> Self {
        Self {
            service_name,
            username: Default::default(),
        }
    }
    /// Updates the service name.
    pub fn service_name(mut self, service_name: String) -> Self {
        self.service_name = service_name;
        self
    }
    /// Updates the username.
    pub fn username(mut self, username: String) -> Self {
        self.username = Some(username);
        self
    }

    pub fn build(self, conv: &impl Conversation) -> Result<OwnedLibPamHandle> {
        OwnedLibPamHandle::start(self.service_name, self.username, conv)
    }
}

impl OwnedLibPamHandle<'_> {
    pub fn build_with_service(service_name: String) -> HandleBuilder {
        HandleBuilder::new(service_name)
    }
    fn start(
        service_name: String,
        username: Option<String>,
        conversation: &impl Conversation,
    ) -> Result<Self> {
        let conv = LibPamConversation::wrap(conversation);
        let service_cstr = CString::new(service_name).map_err(|_| ErrorCode::ConversationError)?;
        let username_cstr = memory::prompt_ptr(memory::option_cstr(username.as_deref())?.as_ref());

        let mut handle: *mut LibPamHandle = ptr::null_mut();
        // SAFETY: We've set everything up properly to call `pam_start`.
        // The returned value will be a valid pointer provided the result is OK.
        let result =
            unsafe { pam_ffi::pam_start(service_cstr.as_ptr(), username_cstr, &conv, &mut handle) };
        ErrorCode::result_from(result)?;
        Ok(Self {
            handle: HandleWrap(handle),
            last_return: Cell::new(Ok(())),
            _conversation_lifetime: Default::default(),
        })
    }
}

impl PamHandleApplication for OwnedLibPamHandle<'_> {
    fn authenticate(&mut self, flags: Flags) -> Result<()> {
        let ret = unsafe { pam_ffi::pam_authenticate(self.handle.0, flags.bits() as c_int) };
        let result = ErrorCode::result_from(ret);
        self.last_return.set(result);
        result
    }

    fn account_management(&mut self, flags: Flags) -> Result<()> {
        let ret = unsafe { pam_ffi::pam_acct_mgmt(self.handle.0, flags.bits() as c_int) };
        let result = ErrorCode::result_from(ret);
        self.last_return.set(result);
        result
    }

    fn change_authtok(&mut self, flags: Flags) -> Result<()> {
        let ret = unsafe { pam_ffi::pam_chauthtok(self.handle.0, flags.bits() as c_int) };
        let result = ErrorCode::result_from(ret);
        self.last_return.set(result);
        result
    }
}

// TODO: pam_authenticate - app
//       pam_setcred - app
//       pam_acct_mgmt - app
//       pam_chauthtok - app
//       pam_open_session - app
//       pam_close_session - app
//       pam_putenv - shared
//       pam_getenv - shared
//       pam_getenvlist - shared

impl Drop for OwnedLibPamHandle<'_> {
    /// Closes the PAM session on an owned PAM handle.
    ///
    /// See the [`pam_end` manual page][man] for more information.
    ///
    /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html
    fn drop(&mut self) {
        unsafe {
            pam_ffi::pam_end(
                self.handle.0,
                ErrorCode::result_to_c(self.last_return.get()),
            );
        }
    }
}

/// Macro to implement getting/setting a CStr-based item.
macro_rules! cstr_item {
    (get = $getter:ident, item = $item_type:path) => {
        fn $getter(&self) -> Result<Option<String>> {
            unsafe { self.get_cstr_item($item_type) }
        }
    };
    (set = $setter:ident, item = $item_type:path) => {
        fn $setter(&mut self, value: Option<&str>) -> Result<()> {
            unsafe { self.set_cstr_item($item_type, value) }
        }
    };
}

impl PamShared for LibPamHandle {
    fn log(&self, level: Level, entry: &str) {
        let entry = match CString::new(entry).or_else(|_| CString::new(dbg!(entry))) {
            Ok(cstr) => cstr,
            _ => return,
        };
        #[cfg(pam_impl = "linux-pam")]
        {
            // SAFETY: We're calling this function with a known value.
            unsafe {
                pam_ffi::pam_syslog(self, level as c_int, c"%s".as_ptr().cast(), entry.as_ptr())
            }
        }
        #[cfg(pam_impl = "openpam")]
        {
            // SAFETY: We're calling this function with a known value.
            unsafe {
                pam_ffi::openpam_log(self, level as c_int, c"%s".as_ptr().cast(), entry.as_ptr())
            }
        }
    }

    fn username(&mut self, prompt: Option<&str>) -> Result<String> {
        let prompt = memory::option_cstr(prompt)?;
        let mut output: *const c_char = ptr::null();
        let ret = unsafe {
            pam_ffi::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref()))
        };
        ErrorCode::result_from(ret)?;
        unsafe { memory::copy_pam_string(output) }
            .transpose()
            .unwrap_or(Err(ErrorCode::ConversationError))
    }

    fn environ(&self) -> impl EnvironMap {
        LibPamEnviron::new(self)
    }

    fn environ_mut(&mut self) -> impl EnvironMapMut {
        LibPamEnvironMut::new(self)
    }

    cstr_item!(get = user_item, item = ItemType::User);
    cstr_item!(set = set_user_item, item = ItemType::User);
    cstr_item!(get = service, item = ItemType::Service);
    cstr_item!(set = set_service, item = ItemType::Service);
    cstr_item!(get = user_prompt, item = ItemType::UserPrompt);
    cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt);
    cstr_item!(get = tty_name, item = ItemType::Tty);
    cstr_item!(set = set_tty_name, item = ItemType::Tty);
    cstr_item!(get = remote_user, item = ItemType::RemoteUser);
    cstr_item!(set = set_remote_user, item = ItemType::RemoteUser);
    cstr_item!(get = remote_host, item = ItemType::RemoteHost);
    cstr_item!(set = set_remote_host, item = ItemType::RemoteHost);
    cstr_item!(set = set_authtok_item, item = ItemType::AuthTok);
    cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok);
}

impl Conversation for LibPamHandle {
    fn communicate(&self, messages: &[Message]) {
        match self.conversation_item() {
            Ok(conv) => conv.communicate(messages),
            Err(e) => {
                for msg in messages {
                    msg.set_error(e)
                }
            }
        }
    }
}

impl PamHandleModule for LibPamHandle {
    fn authtok(&mut self, prompt: Option<&str>) -> Result<String> {
        let prompt = memory::option_cstr(prompt)?;
        let mut output: *const c_char = ptr::null_mut();
        // SAFETY: We're calling this with known-good values.
        let res = unsafe {
            pam_ffi::pam_get_authtok(
                self,
                ItemType::AuthTok.into(),
                &mut output,
                memory::prompt_ptr(prompt.as_ref()),
            )
        };
        ErrorCode::result_from(res)?;
        // SAFETY: We got this string from PAM.
        unsafe { memory::copy_pam_string(output) }
            .transpose()
            .unwrap_or(Err(ErrorCode::ConversationError))
    }

    cstr_item!(get = authtok_item, item = ItemType::AuthTok);
    cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok);
}

/// Function called at the end of a PAM session that is called to clean up
/// a value previously provided to PAM in a `pam_set_data` call.
///
/// You should never call this yourself.
extern "C" fn set_data_cleanup<T>(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) {
    unsafe {
        let _data: Box<T> = Box::from_raw(c_data.cast());
    }
}

impl LibPamHandle {
    /// Gets a C string item.
    ///
    /// # Safety
    ///
    /// You better be requesting an item which is a C string.
    unsafe fn get_cstr_item(&self, item_type: ItemType) -> Result<Option<String>> {
        let mut output = ptr::null();
        let ret = unsafe { pam_ffi::pam_get_item(self, item_type as c_int, &mut output) };
        ErrorCode::result_from(ret)?;
        memory::copy_pam_string(output.cast())
    }

    /// Sets a C string item.
    ///
    /// # Safety
    ///
    /// You better be setting an item which is a C string.
    unsafe fn set_cstr_item(&mut self, item_type: ItemType, data: Option<&str>) -> Result<()> {
        let data_str = memory::option_cstr(data)?;
        let ret = unsafe {
            pam_ffi::pam_set_item(
                self,
                item_type as c_int,
                memory::prompt_ptr(data_str.as_ref()).cast(),
            )
        };
        ErrorCode::result_from(ret)
    }

    /// Gets the `PAM_CONV` item from the handle.
    fn conversation_item(&self) -> Result<&mut LibPamConversation<'_>> {
        let output: *mut LibPamConversation = ptr::null_mut();
        let result = unsafe {
            pam_ffi::pam_get_item(
                self,
                ItemType::Conversation.into(),
                &mut output.cast_const().cast(),
            )
        };
        ErrorCode::result_from(result)?;
        // SAFETY: We got this result from PAM, and we're checking if it's null.
        unsafe { output.as_mut() }.ok_or(ErrorCode::ConversationError)
    }
}

macro_rules! delegate {
    // First have the kind that save the result after delegation.
    (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => {
        fn $meth(&self $(, $param: $typ)*) -> Result<$ret> {
            let result = self.handle.$meth($($param),*);
            self.last_return.set(split(&result));
            result
        }
    };
    (fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => {
        fn $meth(&mut self $(, $param: $typ)*) -> Result<$ret> {
            let result = self.handle.$meth($($param),*);
            self.last_return.set(split(&result));
            result
        }
    };
    // Then have the kind that are just raw delegates
    (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> $ret:ty) => {
        fn $meth(&self $(, $param: $typ)*) -> $ret {
            self.handle.$meth($($param),*)
        }
    };
    (fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> $ret:ty) => {
        fn $meth(&mut self $(, $param: $typ)*) -> $ret {
            self.handle.$meth($($param),*)
        }
    };
    // Then have item getters / setters
    (get = $get:ident$(, set = $set:ident)?) => {
        delegate!(fn $get(&self) -> Result<Option<String>>);
        $(delegate!(set = $set);)?
    };
    (set = $set:ident) => {
        delegate!(fn $set(&mut self, value: Option<&str>) -> Result<()>);
    };
}

fn split<T>(result: &Result<T>) -> Result<()> {
    result.as_ref().map(drop).map_err(|&e| e)
}

impl PamShared for OwnedLibPamHandle<'_> {
    delegate!(fn log(&self, level: Level, entry: &str) -> ());
    delegate!(fn environ(&self) -> impl EnvironMap);
    delegate!(fn environ_mut(&mut self) -> impl EnvironMapMut);
    delegate!(fn username(&mut self, prompt: Option<&str>) -> Result<String>);
    delegate!(get = user_item, set = set_user_item);
    delegate!(get = service, set = set_service);
    delegate!(get = user_prompt, set = set_user_prompt);
    delegate!(get = tty_name, set = set_tty_name);
    delegate!(get = remote_user, set = set_remote_user);
    delegate!(get = remote_host, set = set_remote_host);
    delegate!(set = set_authtok_item);
    delegate!(set = set_old_authtok_item);
}

/// Identifies what is being gotten or set with `pam_get_item`
/// or `pam_set_item`.
#[derive(TryFromPrimitive, IntoPrimitive)]
#[repr(i32)]
#[non_exhaustive] // because C could give us anything!
pub enum ItemType {
    /// The PAM service name.
    Service = 1,
    /// The user's login name.
    User = 2,
    /// The TTY name.
    Tty = 3,
    /// The remote host (if applicable).
    RemoteHost = 4,
    /// The conversation struct (not a CStr-based item).
    Conversation = 5,
    /// The authentication token (password).
    AuthTok = 6,
    /// The old authentication token (when changing passwords).
    OldAuthTok = 7,
    /// The remote user's name.
    RemoteUser = 8,
    /// The prompt shown when requesting a username.
    UserPrompt = 9,
    /// App-supplied function to override failure delays.
    FailDelay = 10,
    /// X display name.
    XDisplay = 11,
    /// X server authentication data.
    XAuthData = 12,
    /// The type of `pam_get_authtok`.
    AuthTokType = 13,
}