view src/libpam/handle.rs @ 141:a508a69c068a

Remove a lot of Results from functions. Many functions are documented to only return failing Results when given improper inputs or when there is a memory allocation failure (which can be verified by looking at the source). In cases where we know our input is correct, we don't need to check for memory allocation errors for the same reason that Rust doesn't do so when you, e.g., create a new Vec.
author Paul Fisher <paul@pfish.zone>
date Sat, 05 Jul 2025 17:16:56 -0400
parents 6c1e1bdb4164
children ebb71a412b58
line wrap: on
line source

use super::conversation::{OwnedConversation, PamConv};
use crate::constants::{ErrorCode, Result};
use crate::conv::Exchange;
use crate::environ::EnvironMapMut;
use crate::handle::PamShared;
use crate::libpam::environ::{LibPamEnviron, LibPamEnvironMut};
use crate::libpam::memory;
use crate::logging::{Level, Location};
use crate::{
    guide, linklist, stdlinks, Conversation, EnvironMap, Flags, PamHandleApplication,
    PamHandleModule,
};
use libpam_sys_helpers::constants;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::cell::Cell;
use std::ffi::{c_char, c_int, CString};
use std::mem::ManuallyDrop;
use std::ptr;
use std::ptr::NonNull;

/// An owned PAM handle.
pub struct OwnedLibPamHandle<C: Conversation> {
    /// The handle itself.
    handle: ManuallyDrop<RawPamHandle>,
    /// 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 HandleBuilder {
    service_name: String,
    username: Option<String>,
}

impl HandleBuilder {
    /// Updates the service name.
    pub fn service_name(mut self, service_name: String) -> 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: String) -> Self {
        self.username = Some(username);
        self
    }
    /// Builds a PAM handle and starts the transaction.
    pub fn build(self, conv: impl Conversation) -> Result<OwnedLibPamHandle<impl Conversation>> {
        OwnedLibPamHandle::start(self.service_name, self.username, conv)
    }
}

impl<C: Conversation> OwnedLibPamHandle<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: String) -> HandleBuilder {
        HandleBuilder {
            service_name,
            username: None,
        }
    }

    fn start(service_name: String, username: Option<String>, conversation: C) -> Result<Self> {
        let conv = Box::new(OwnedConversation::new(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 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(RawPamHandle(handle)),
            last_return: Cell::new(Ok(())),
            conversation: conv,
        })
    }

    /// "Quietly" 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 end_quiet(self) {}
}

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 PamHandleApplication for RawPamHandle {
    wrap!(fn authenticate { pam_authenticate });
    wrap!(fn account_management { pam_acct_mgmt });
    wrap!(fn change_authtok { pam_chauthtok });
}

// 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<C: Conversation> Drop for OwnedLibPamHandle<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) {
        unsafe {
            libpam_sys::pam_end(
                self.handle.raw_mut(),
                ErrorCode::result_to_c(self.last_return.get()),
            );
        }
    }
}

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<C: Conversation> PamShared for OwnedLibPamHandle<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<&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);
}

/// 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) }
        }
    };
}

/// 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 [`OwnedLibPamHandle`], 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.
pub struct RawPamHandle(NonNull<libpam_sys::pam_handle>);

impl RawPamHandle {
    /// 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_quiet(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 RawPamHandle {
    fn drop(&mut self) {
        unsafe { libpam_sys::pam_end(self.0.as_mut(), 0) };
    }
}

impl PamShared for RawPamHandle {
    #[cfg(any())]
    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 = "linux-pam")]
        {
            _ = loc;
            // SAFETY: We're calling this function with a known value.
            unsafe {
                libpam_sys::pam_syslog(self, level as c_int, "%s\0".as_ptr().cast(), entry.as_ptr())
            }
        }
        #[cfg(pam_impl = "openpam")]
        {
            let func = CString::new(loc.function).unwrap_or(CString::default());
            // 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 log(&self, _level: Level, _loc: Location<'_>, _entry: &str) {}

    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 {
            libpam_sys::pam_get_user(
                self.raw_mut(),
                &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 RawPamHandle {
    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 PamHandleModule for RawPamHandle {
    fn authtok(&mut self, prompt: Option<&str>) -> Result<String> {
        self.get_authtok(prompt, ItemType::AuthTok)
    }

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

    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());
    }
}

// Implementations of internal functions.
impl RawPamHandle {
    #[cfg(any(pam_impl = "LinuxPam", pam_impl = "OpenPam"))]
    fn get_authtok(&mut self, prompt: Option<&str>, item_type: ItemType) -> 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 {
            libpam_sys::pam_get_authtok(
                self.raw_mut(),
                item_type.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))
    }

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

    /// 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 { libpam_sys::pam_get_item(self.raw_ref(), 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 {
            libpam_sys::pam_set_item(
                self.raw_mut(),
                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<&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,
}