view src/libpam/handle.rs @ 156:66e662cde087 default tip

fix return type of get_authtok for weird PAMs
author Paul Fisher <paul@pfish.zone>
date Tue, 08 Jul 2025 01:04:30 -0400
parents ab8020566cd9
children
line wrap: on
line source

use super::conversation::{OwnedConversation, PamConv};
use crate::_doc::{guide, linklist, man7, stdlinks};
use crate::constants::{ErrorCode, Result};
use crate::conv::Exchange;
use crate::environ::EnvironMapMut;
use crate::handle::PamShared;
use crate::items::{Items, ItemsMut};
use crate::libpam::environ::{LibPamEnviron, LibPamEnvironMut};
use crate::libpam::items::{LibPamItems, LibPamItemsMut};
use crate::libpam::{items, memory};
use crate::logging::{Level, Location};
use crate::{Conversation, EnvironMap, Flags, ModuleClient, Transaction};
use libpam_sys_consts::constants;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::any::TypeId;
use std::cell::Cell;
use std::ffi::{c_char, c_int, c_void, CString, OsStr, OsString};
use std::mem::ManuallyDrop;
use std::os::unix::ffi::OsStrExt;
use std::ptr;
use std::ptr::NonNull;

/// An owned PAM handle.
pub struct LibPamTransaction<C: Conversation> {
    /// The handle itself.
    handle: ManuallyDrop<LibPamHandle>,
    /// The last return value from the handle.
    last_return: Cell<Result<()>>,
    /// If set, the Conversation that this PAM handle owns.
    ///
    /// We have to hold on to this because the PAM specification doesn't
    /// actually say what the PAM library should do with a passed-in
    /// conversation. Linux-PAM copies the contents of the `pam_conv` struct
    /// that you pass in to `pam_start`, but OpenPAM uses the pointer itself,
    /// so you have to keep it in one place.
    conversation: Box<OwnedConversation<C>>,
}

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

impl TransactionBuilder {
    /// Updates the service name.
    pub fn service_name(mut self, service_name: OsString) -> Self {
        self.service_name = service_name;
        self
    }
    /// Sets the username. Setting this will avoid the need for an extra
    /// round trip through the conversation and may otherwise improve
    /// the login experience.
    pub fn username(mut self, username: OsString) -> Self {
        self.username = Some(username);
        self
    }
    /// Builds a PAM handle and starts the transaction.
    pub fn build(self, conv: impl Conversation) -> Result<LibPamTransaction<impl Conversation>> {
        LibPamTransaction::start(self.service_name, self.username, conv)
    }
}

impl<C: Conversation> LibPamTransaction<C> {
    /// Creates a builder to start a PAM transaction for the given service.
    ///
    /// The service name is what controls the steps and checks PAM goes through
    /// when authenticating a user. This corresponds to the configuration file
    /// named <code>/etc/pam.d/<var>service_name</var></code>.
    ///
    /// # References
    #[doc = linklist!(pam_start: adg, _std)]
    ///
    #[doc = stdlinks!(3 pam_start)]
    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_start")]
    pub fn build_with_service(service_name: OsString) -> TransactionBuilder {
        TransactionBuilder {
            service_name,
            username: None,
        }
    }

    fn start(service_name: OsString, username: Option<OsString>, conversation: C) -> Result<Self> {
        let conv = Box::new(OwnedConversation::new(conversation));
        let service_cstr = CString::new(service_name.as_bytes()).expect("null is forbidden");
        let username_cstr = memory::option_cstr_os(username.as_deref());
        let username_cstr = memory::prompt_ptr(username_cstr.as_deref());

        let mut handle: *mut libpam_sys::pam_handle = 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 {
            libpam_sys::pam_start(
                service_cstr.as_ptr(),
                username_cstr,
                (conv.as_ref() as *const OwnedConversation<C>)
                    .cast_mut()
                    .cast(),
                &mut handle,
            )
        };
        ErrorCode::result_from(result)?;
        let handle = NonNull::new(handle).ok_or(ErrorCode::BufferError)?;
        Ok(Self {
            handle: ManuallyDrop::new(LibPamHandle(handle)),
            last_return: Cell::new(Ok(())),
            conversation: conv,
        })
    }

    #[cfg_attr(
        pam_impl = "LinuxPam",
        doc = "Ends the PAM transaction \"quietly\" (on Linux-PAM only)."
    )]
    #[cfg_attr(
        not(pam_impl = "LinuxPam"),
        doc = "Exactly equivalent to `drop(self)` (except on Linux-PAM)."
    )]
    ///
    /// On Linux-PAM, this is equivalent to passing the `PAM_DATA_SILENT` flag
    /// to [`pam_end` on Linux-PAM][man7], which signals that data cleanup
    /// should "not treat the call too seriously" \[sic].
    ///
    /// On other platforms, this is no different than letting the transaction
    /// end on its own.
    ///
    #[doc = man7!(3 pam_end)]
    pub fn end_silent(self) {
        #[cfg(pam_impl = "LinuxPam")]
        {
            let mut me = ManuallyDrop::new(self);
            me.end_internal(libpam_sys::PAM_DATA_SILENT);
        }
        // If it's not LinuxPam, we just drop normally.
    }

    /// Internal "end" function, which binary-ORs the status with `or_with`.
    fn end_internal(&mut self, or_with: i32) {
        let result = ErrorCode::result_to_c(self.last_return.get()) | or_with;
        unsafe { libpam_sys::pam_end(self.handle.raw_mut(), result) };
    }
}

macro_rules! wrap {
    (fn $name:ident { $pam_func:ident }) => {
        fn $name(&mut self, flags: Flags) -> Result<()> {
            ErrorCode::result_from(unsafe { libpam_sys::$pam_func(self.0.as_mut(), flags.bits()) })
        }
    };
}

impl Transaction for LibPamHandle {
    wrap!(fn authenticate { pam_authenticate });
    wrap!(fn account_management { pam_acct_mgmt });
    wrap!(fn change_authtok { pam_chauthtok });
}

// TODO: pam_setcred - app
//       pam_open_session - app
//       pam_close_session - app
//       pam_set/get_data - module

impl<C: Conversation> Drop for LibPamTransaction<C> {
    /// Closes the PAM session on an owned PAM handle.
    ///
    /// This internally calls `pam_end` with the appropriate error code.
    ///
    /// # References
    #[doc = linklist!(pam_end: adg, _std)]
    ///
    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
    #[doc = stdlinks!(3 pam_end)]
    fn drop(&mut self) {
        self.end_internal(0)
    }
}

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<OsString>>);
        $(delegate!(set = $set);)?
    };
    (set = $set:ident) => {
        delegate!(fn $set(&mut self, value: Option<&OsStr>) -> Result<()>);
    };
}

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

impl<C: Conversation> Transaction for LibPamTransaction<C> {
    delegate!(fn authenticate(&mut self, flags: Flags) -> Result<()>);
    delegate!(fn account_management(&mut self, flags: Flags) -> Result<()>);
    delegate!(fn change_authtok(&mut self, flags: Flags) -> Result<()>);
}

impl<C: Conversation> PamShared for LibPamTransaction<C> {
    delegate!(fn log(&self, level: Level, location: Location<'_>, entry: &str) -> ());
    delegate!(fn environ(&self) -> impl EnvironMap);
    delegate!(fn environ_mut(&mut self) -> impl EnvironMapMut);
    delegate!(fn username(&mut self, prompt: Option<&OsStr>) -> Result<OsString>);
    delegate!(fn items(&self) -> impl Items);
    delegate!(fn items_mut(&mut self) -> impl ItemsMut);
}

/// An owned variation of a basic PAM handle.
///
/// This is the most basic version of a wrapped PAM handle. It's mostly used
/// as the inside of the [`LibPamTransaction`], but can also be used to "adopt"
/// a PAM handle created by another library.
///
/// If [`Self::end`] is not called, this will always call `pam_end` reporting
/// successful completion.
#[repr(transparent)]
pub struct LibPamHandle(NonNull<libpam_sys::pam_handle>);

impl LibPamHandle {
    /// Takes ownership of the pointer to the given PAM handle.
    ///
    /// **Do not use this just to get a reference to a PAM handle.**
    ///
    /// # Safety
    ///
    /// - The pointer must point to a valid PAM handle.
    /// - The conversation associated with the handle must remain valid
    ///   for as long as the handle is open.
    pub unsafe fn from_ptr(handle: NonNull<libpam_sys::pam_handle>) -> Self {
        Self(handle)
    }

    /// Ends the transaction, reporting `error_code` to cleanup callbacks.
    ///
    /// # References
    #[doc = linklist!(pam_end: adg, _std)]
    ///
    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
    #[doc = stdlinks!(3 pam_end)]
    pub fn end(self, result: Result<()>) {
        let mut me = ManuallyDrop::new(self);
        unsafe { libpam_sys::pam_end(me.raw_mut(), ErrorCode::result_to_c(result)) };
    }

    #[cfg_attr(
        not(pam_impl = "LinuxPam"),
        doc = "Exactly equivalent to [`Self::end`], except on Linux-PAM."
    )]
    #[cfg_attr(
        pam_impl = "LinuxPam",
        doc = "Ends the transaction \"quietly\", reporting `error_code` to cleanup callbacks."
    )]
    ///
    /// On Linux-PAM only, this sets the
    /// [`PAM_DATA_SILENT`](libpam_sys::PAM_DATA_SILENT) flag on the flags
    /// passed to the cleanup callbacks. This conventionally means that this
    /// `pam_end` call is occurring on a forked process, and that a session
    /// may still be open on the parent process, and modules "should not treat
    /// the call too seriously".
    ///
    /// # References
    #[doc = linklist!(pam_end: adg, _std)]
    ///
    #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
    #[doc = stdlinks!(3 pam_end)]
    pub fn end_silent(self, result: Result<()>) {
        let mut me = ManuallyDrop::new(self);
        let result = ErrorCode::result_to_c(result);
        #[cfg(pam_impl = "LinuxPam")]
        let result = result | libpam_sys::PAM_DATA_SILENT;
        unsafe {
            libpam_sys::pam_end(me.raw_mut(), result);
        }
    }

    /// Consumes this and gives you back the raw PAM handle.
    pub fn into_inner(self) -> NonNull<libpam_sys::pam_handle> {
        let me = ManuallyDrop::new(self);
        me.0
    }

    /// Gets a reference to the inner PAM handle.
    pub fn raw_ref(&self) -> &libpam_sys::pam_handle {
        unsafe { self.0.as_ref() }
    }
    /// Gets a mutable reference to the inner PAM handle.
    pub fn raw_mut(&mut self) -> &mut libpam_sys::pam_handle {
        unsafe { self.0.as_mut() }
    }
}

impl Drop for LibPamHandle {
    fn drop(&mut self) {
        unsafe { libpam_sys::pam_end(self.0.as_mut(), 0) };
    }
}

impl PamShared for LibPamHandle {
    fn log(&self, level: Level, loc: Location<'_>, entry: &str) {
        let entry = match CString::new(entry).or_else(|_| CString::new(dbg!(entry))) {
            Ok(cstr) => cstr,
            _ => return,
        };
        #[cfg(pam_impl = "LinuxPam")]
        {
            let level = match level {
                Level::Error => libc::LOG_ERR,
                Level::Warning => libc::LOG_WARNING,
                Level::Info => libc::LOG_INFO,
                Level::Debug => libc::LOG_DEBUG,
            };
            _ = loc;
            // SAFETY: We're calling this function with a known value.
            unsafe {
                libpam_sys::pam_syslog(self.raw_ref(), level, "%s\0".as_ptr().cast(), entry.as_ptr())
            }
        }
        #[cfg(pam_impl = "OpenPam")]
        {
            let func = CString::new(loc.function).unwrap_or(CString::default());
            let level = match level {
                Level::Error => libpam_sys::PAM_LOG_ERROR,
                Level::Warning => libpam_sys::PAM_LOG_NOTICE,
                Level::Info => libpam_sys::PAM_LOG_VERBOSE,
                Level::Debug => libpam_sys::PAM_LOG_DEBUG,
            };
            // SAFETY: We're calling this function with a known value.
            unsafe {
                libpam_sys::_openpam_log(
                    level as c_int,
                    func.as_ptr(),
                    "%s\0".as_ptr().cast(),
                    entry.as_ptr(),
                )
            }
        }
    }

    fn username(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
        let prompt = memory::option_cstr_os(prompt);
        let mut output: *const c_char = ptr::null();
        let ret = unsafe {
            libpam_sys::pam_get_user(
                self.raw_mut(),
                &mut output,
                memory::prompt_ptr(prompt.as_deref()),
            )
        };
        ErrorCode::result_from(ret)?;
        Ok(unsafe { memory::copy_pam_string(output).ok_or(ErrorCode::ConversationError)? })
    }

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

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

    fn items(&self) -> impl Items {
        LibPamItems(self)
    }

    fn items_mut(&mut self) -> impl ItemsMut {
        LibPamItemsMut(self)
    }
}

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

impl ModuleClient for LibPamHandle {
    fn authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
        self.get_authtok(prompt, ItemType::AuthTok)
    }

    fn old_authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
        self.get_authtok(prompt, ItemType::OldAuthTok)
    }

    fn get_module_data<T: 'static>(&self, key: &str) -> Option<&T> {
        // It's technically unsafe to do this, but we assume that other modules
        // aren't going to go out of their way to find the key we've used
        // and corrupt its value's data.
        let full_key = module_data_key::<T>(key);
        let mut ptr: *const c_void = ptr::null();
        unsafe {
            ErrorCode::result_from(libpam_sys::pam_get_data(
                self.raw_ref(),
                full_key.as_ptr(),
                &mut ptr,
            ))
            .ok()?;

            (ptr as *const T).as_ref()
        }
    }

    fn set_module_data<T: 'static>(&mut self, key: &str, data: T) -> Result<()> {
        let full_key = module_data_key::<T>(key);
        let data = Box::new(data);
        ErrorCode::result_from(unsafe {
            libpam_sys::pam_set_data(
                self.raw_mut(),
                full_key.as_ptr(),
                Box::into_raw(data).cast(),
                drop_module_data::<T>,
            )
        })
    }

    fn authtok_item(&self) -> Result<Option<OsString>> {
        unsafe { items::get_cstr_item(self, ItemType::AuthTok) }
    }
    fn old_authtok_item(&self) -> Result<Option<OsString>> {
        unsafe { items::get_cstr_item(self, ItemType::OldAuthTok) }
    }
}

/// Constructs a type-specific, module-specific key for this data.
fn module_data_key<T: 'static>(key: &str) -> CString {
    // The type ID is unique per-type.
    let tid = TypeId::of::<T>();
    // The `set_data_cleanup` function lives statically inside each PAM module,
    // so its address will be different between `pam_a.so` and `pam_b.so`,
    // even if both modules .so files are byte-for-byte identical.
    let cleanup_addr = drop_module_data::<T> as usize;
    // Then, by adding the key,
    let key = format!("{key:?}::{tid:?}::{cleanup_addr:016x}");
    CString::new(key).expect("null bytes somehow got into a debug string?")
}

/// 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 drop_module_data<T>(_: *mut libpam_sys::pam_handle, c_data: *mut c_void, _: c_int) {
    unsafe {
        // Adopt the pointer into a Box and immediately drop it.
        let _: Box<T> = Box::from_raw(c_data.cast());
    }
}

// Implementations of internal functions.
impl LibPamHandle {
    #[cfg(any(pam_impl = "LinuxPam", pam_impl = "OpenPam"))]
    fn get_authtok(&mut self, prompt: Option<&OsStr>, item_type: ItemType) -> Result<OsString> {
        let prompt = memory::option_cstr_os(prompt);
        let mut output: *const c_char = ptr::null_mut();
        // SAFETY: We're calling this with known-good values.
        let res = unsafe {
            libpam_sys::pam_get_authtok(
                self.raw_mut(),
                item_type.into(),
                &mut output,
                memory::prompt_ptr(prompt.as_deref()),
            )
        };
        ErrorCode::result_from(res)?;
        // SAFETY: We got this string from PAM.
        unsafe { memory::copy_pam_string(output) }.ok_or(ErrorCode::ConversationError)
    }

    #[cfg(not(any(pam_impl = "LinuxPam", pam_impl = "OpenPam")))]
    fn get_authtok(&mut self, prompt: Option<&OsStr>, item_type: ItemType) -> Result<OsString> {
        Err(ErrorCode::ConversationError)
    }

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

/// 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 = constants::PAM_SERVICE,
    /// The user's login name.
    User = constants::PAM_USER,
    /// The TTY name.
    Tty = constants::PAM_TTY,
    /// The remote host (if applicable).
    RemoteHost = constants::PAM_RHOST,
    /// The conversation struct (not a CStr-based item).
    Conversation = constants::PAM_CONV,
    /// The authentication token (password).
    AuthTok = constants::PAM_AUTHTOK,
    /// The old authentication token (when changing passwords).
    OldAuthTok = constants::PAM_OLDAUTHTOK,
    /// The remote user's name.
    RemoteUser = constants::PAM_RUSER,
    /// The prompt shown when requesting a username.
    UserPrompt = constants::PAM_USER_PROMPT,
    #[cfg(feature = "linux-pam-ext")]
    /// App-supplied function to override failure delays.
    FailDelay = constants::PAM_FAIL_DELAY,
    #[cfg(feature = "linux-pam-ext")]
    /// X display name.
    XDisplay = constants::PAM_XDISPLAY,
    #[cfg(feature = "linux-pam-ext")]
    /// X server authentication data.
    XAuthData = constants::PAM_XAUTHDATA,
    #[cfg(feature = "linux-pam-ext")]
    /// The type of `pam_get_authtok`.
    AuthTokType = constants::PAM_AUTHTOK_TYPE,
}