sudo-rs 0.2.13

A memory safe implementation of sudo and su.
Documentation
use std::{ffi::c_int, io, process::Command};

use super::{
    ExitReason, HandleSigchld,
    event::PollEvent,
    event::{EventRegistry, Process, StopReason},
    io_util::was_interrupted,
    terminate_process,
};
use crate::{
    common::bin_serde::BinPipe,
    system::signal::{
        SignalHandler, SignalHandlerBehavior, SignalNumber, SignalSet, SignalStream, SignalsState,
        consts::*, register_handlers,
    },
};
use crate::{
    exec::{SpawnNoexecHandler, exec_command, handle_sigchld, signal_fmt},
    log::{dev_error, dev_info, dev_warn},
    system::{
        ForkResult, fork, getpgid, getpgrp,
        interface::ProcessId,
        kill, killpg,
        term::{Terminal, UserTerm},
        wait::WaitOptions,
    },
};

pub(super) fn exec_no_pty(
    sudo_pid: ProcessId,
    spawn_noexec_handler: Option<SpawnNoexecHandler>,
    command: Command,
) -> io::Result<ExitReason> {
    // FIXME (ogsudo): Initialize the policy plugin's session here.

    let original_signals = SignalsState::save()?;

    // Block all the signals until we are done setting up the signal handlers so we don't miss
    // SIGCHLD.
    let original_set = match SignalSet::full().and_then(|set| set.block()) {
        Ok(original_set) => Some(original_set),
        Err(err) => {
            dev_warn!("cannot block signals: {err}");
            None
        }
    };

    // FIXME (ogsudo): Some extra config happens here if selinux is available.

    // Use a pipe to get the IO error if `exec` fails.
    let (errpipe_tx, errpipe_rx) = BinPipe::pair()?;

    // SAFETY: There should be no other threads at this point.
    let ForkResult::Parent(command_pid) = unsafe { fork() }.map_err(|err| {
        dev_warn!("unable to fork command process: {err}");
        err
    })?
    else {
        exec_command(command, original_set, original_signals, errpipe_tx);
    };

    if let Some(spawner) = spawn_noexec_handler {
        spawner.spawn();
    }

    dev_info!("executed command with pid {command_pid}");

    let mut registry = EventRegistry::new();

    let mut closure = ExecClosure::new(
        command_pid,
        sudo_pid,
        errpipe_rx,
        &mut registry,
        original_signals,
    )?;

    // Restore the signal mask now that the handlers have been setup.
    if let Some(set) = original_set {
        if let Err(err) = set.set_mask() {
            dev_warn!("cannot restore signal mask: {err}");
        }
    }

    let command_exit_reason = match registry.event_loop(&mut closure) {
        StopReason::Break(err) => return Err(err),
        StopReason::Exit(reason) => reason,
    };

    // Restore signal handlers
    drop(closure.signal_handlers);

    Ok(command_exit_reason)
}

struct ExecClosure {
    command_pid: Option<ProcessId>,
    sudo_pid: ProcessId,
    parent_pgrp: ProcessId,
    errpipe_rx: BinPipe<i32>,
    original_signals: SignalsState,
    signal_stream: &'static SignalStream,
    signal_handlers: [SignalHandler; ExecClosure::SIGNALS.len()],
}

impl ExecClosure {
    const SIGNALS: [SignalNumber; 12] = [
        SIGINT, SIGQUIT, SIGTSTP, SIGTERM, SIGHUP, SIGALRM, SIGPIPE, SIGUSR1, SIGUSR2, SIGCHLD,
        SIGCONT, SIGWINCH,
    ];

    fn new(
        command_pid: ProcessId,
        sudo_pid: ProcessId,
        errpipe_rx: BinPipe<i32>,
        registry: &mut EventRegistry<Self>,
        mut original_signals: SignalsState,
    ) -> io::Result<Self> {
        registry.register_event(&errpipe_rx, PollEvent::Readable, |_| ExecEvent::ErrPipe);

        let signal_stream = SignalStream::init()?;

        registry.register_event(signal_stream, PollEvent::Readable, |_| ExecEvent::Signal);

        let signal_handlers = register_handlers(Self::SIGNALS, &mut original_signals)?;

        Ok(Self {
            command_pid: Some(command_pid),
            errpipe_rx,
            sudo_pid,
            parent_pgrp: getpgrp(),
            original_signals,
            signal_stream,
            signal_handlers,
        })
    }

    /// Decides if the signal sent by the process with `signaler_pid` PID is self-terminating.
    ///
    /// A signal is self-terminating if `signaler_pid`:
    /// - is the same PID of the command, or
    /// - is in the process group of the command and either sudo or the command is the leader.
    fn is_self_terminating(&self, signaler_pid: ProcessId) -> bool {
        if signaler_pid.is_valid() {
            if Some(signaler_pid) == self.command_pid {
                return true;
            }

            if let Ok(signaler_pgrp) = getpgid(signaler_pid) {
                if Some(signaler_pgrp) == self.command_pid || signaler_pgrp == self.sudo_pid {
                    return true;
                }
            }
        }

        false
    }

    /// Suspend the main process.
    fn suspend_parent(&mut self, signal: SignalNumber) {
        let mut opt_tty = UserTerm::open().ok();
        let mut opt_pgrp = None;

        if let Some(tty) = opt_tty.as_ref() {
            if let Ok(saved_pgrp) = tty.tcgetpgrp() {
                // Save the terminal's foreground process group so we can restore it after resuming
                // if needed.
                opt_pgrp = Some(saved_pgrp);
            } else {
                opt_tty.take();
            }
        }

        if let Some(saved_pgrp) = opt_pgrp {
            // This means that the command was stopped trying to access the terminal. If the
            // terminal has a different foreground process group and we own the terminal, we give
            // it to the command and let it continue.
            if let SIGTTOU | SIGTTIN = signal {
                if saved_pgrp == self.parent_pgrp {
                    if let Some(command_pgrp) = self.command_pid.and_then(|pid| getpgid(pid).ok()) {
                        if command_pgrp != self.parent_pgrp
                            && opt_tty
                                .as_ref()
                                .is_some_and(|tty| tty.tcsetpgrp_nobg(command_pgrp).is_ok())
                        {
                            if let Err(err) = killpg(command_pgrp, SIGCONT) {
                                dev_warn!("cannot send SIGCONT to command ({command_pgrp}): {err}");
                            }

                            return;
                        }
                    }
                }
            }
        }

        let sigtstp_handler = if signal == SIGTSTP {
            SignalHandler::register(
                signal,
                SignalHandlerBehavior::Default,
                &mut self.original_signals,
            )
            .map_err(|err| dev_warn!("cannot set handler for {}: {err}", signal_fmt(signal)))
            .ok()
        } else {
            None
        };

        if let Err(err) = kill(self.sudo_pid, signal) {
            dev_warn!(
                "cannot send {} to {} (sudo): {err}",
                signal_fmt(signal),
                self.sudo_pid
            );
        }

        drop(sigtstp_handler);

        if let Some(saved_pgrp) = opt_pgrp {
            // Restore the foreground process group after resuming.
            if saved_pgrp != self.parent_pgrp {
                if let Some(tty) = opt_tty {
                    tty.tcsetpgrp_nobg(saved_pgrp).ok();
                }
            }
        }
    }

    fn on_signal(&mut self, registry: &mut EventRegistry<Self>) {
        let info = match self.signal_stream.recv() {
            Ok(info) => info,
            Err(err) => {
                dev_error!("sudo could not receive signal: {err}");
                return;
            }
        };

        dev_info!("received{}", info);

        let Some(command_pid) = self.command_pid else {
            dev_info!("command was terminated, ignoring signal");
            return;
        };

        match info.signal() {
            SIGCHLD => handle_sigchld(self, registry, "command", command_pid),
            signal => {
                // FIXME: we should handle SIGWINCH here if we want to support I/O plugins that
                // react on window change events.

                if let Some(pid) = info.signaler_pid() {
                    if self.is_self_terminating(pid) {
                        // Skip the signal if it was sent by the user and it is self-terminating.
                        return;
                    }
                }

                if signal == SIGALRM {
                    terminate_process(command_pid, false);
                } else {
                    kill(command_pid, signal).ok();
                }
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExecEvent {
    Signal,
    ErrPipe,
}

impl Process for ExecClosure {
    type Event = ExecEvent;
    type Break = io::Error;
    type Exit = ExitReason;

    fn on_event(&mut self, event: Self::Event, registry: &mut EventRegistry<Self>) {
        match event {
            ExecEvent::Signal => self.on_signal(registry),
            ExecEvent::ErrPipe => {
                match self.errpipe_rx.read() {
                    Err(err) if was_interrupted(&err) => { /* Retry later */ }
                    Err(err) => registry.set_break(err),
                    Ok(error_code) => {
                        // Received error code from the command, forward it to the parent.
                        registry.set_break(io::Error::from_raw_os_error(error_code));
                    }
                }
            }
        }
    }
}

impl HandleSigchld for ExecClosure {
    const OPTIONS: WaitOptions = WaitOptions::new().all().untraced().no_hang();

    fn on_exit(&mut self, exit_code: c_int, registry: &mut EventRegistry<Self>) {
        registry.set_exit(ExitReason::Code(exit_code));
        self.command_pid = None;
    }

    fn on_term(&mut self, signal: SignalNumber, registry: &mut EventRegistry<Self>) {
        registry.set_exit(ExitReason::Signal(signal));
        self.command_pid = None;
    }

    fn on_stop(&mut self, signal: SignalNumber, _registry: &mut EventRegistry<Self>) {
        self.suspend_parent(signal);
    }
}