Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/heimdall

crates/heimdall-broker/src/spawn.rs

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Ignalina ApS and Vetra Automation AB

//! Launching a worker under the broker: fork, install the broker filter in
//! the child (before exec), hand its listener to the broker thread, exec.
//!
//! Heimdall is a defensive sandbox. This is the minimal launcher (the kernel
//! tests use it, and so can a runner without a container); heimdall-container
//! installs the same filter inside its container and serves it with the same
//! [`Broker`].

use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
use std::os::unix::process::CommandExt;
use std::process::{Child, Command, ExitStatus};
use std::sync::Arc;
use std::thread::JoinHandle;

use heimdall_seccomp::launch::{install, FILTER_FLAG_NEW_LISTENER};
use heimdall_seccomp::notif::Listener;

use crate::broker::{Broker, BrokerReport, DebugKnobs};
use crate::filter::{self, FilterMode};
use crate::policy::BrokerPolicy;
use crate::support::{Mode, Support};
use crate::sys;
use crate::BrokerError;

/// Options for [`spawn_brokered`].
#[derive(Clone, Debug, Default)]
pub struct SpawnOptions {
    /// Run in this mode instead of the best this kernel allows (the
    /// fallback tests force [`Mode::CheckOnly`]). Refused when the kernel
    /// cannot do it.
    pub force_mode: Option<Mode>,
    pub knobs: DebugKnobs,
}

/// A worker running under the broker.
pub struct BrokeredChild {
    pub child: Child,
    pub mode: Mode,
    stop: Arc<OwnedFd>,
    broker: Option<JoinHandle<Result<BrokerReport, BrokerError>>>,
}

impl BrokeredChild {
    pub fn pid(&self) -> u32 {
        self.child.id()
    }

    /// Waits for the worker, stops the broker and returns its report.
    pub fn wait(mut self) -> Result<(ExitStatus, BrokerReport), BrokerError> {
        let status = self.child.wait().map_err(|e| BrokerError::Io {
            what: "wait for the worker".into(),
            source: e,
        })?;
        let report = self.stop_broker()?;
        Ok((status, report))
    }

    /// Like [`wait`](Self::wait) but also returns the worker's stdout
    /// (the command must have `Stdio::piped()` stdout).
    pub fn wait_with_output(mut self) -> Result<(ExitStatus, Vec<u8>, BrokerReport), BrokerError> {
        use std::io::Read;
        let mut out = Vec::new();
        if let Some(mut s) = self.child.stdout.take() {
            let _ = s.read_to_end(&mut out);
        }
        let (st, rep) = self.wait()?;
        Ok((st, out, rep))
    }

    fn stop_broker(&mut self) -> Result<BrokerReport, BrokerError> {
        sys::eventfd_signal(self.stop.as_raw_fd());
        match self.broker.take() {
            Some(h) => h.join().map_err(|_| BrokerError::Panicked)?,
            None => Err(BrokerError::Panicked),
        }
    }
}

impl Drop for BrokeredChild {
    fn drop(&mut self) {
        if self.broker.is_some() {
            let _ = self.child.kill();
            let _ = self.child.wait();
            let _ = self.stop_broker();
        }
    }
}

/// Spawns `cmd` with the broker filter installed in the child before its
/// exec, served by a broker thread in this process. The program path should
/// be absolute (one exec, no `PATH` search). Spec §10: without USER_NOTIF
/// this refuses ([`BrokerError::Unsupported`]); a runner installs
/// [`filter::program`] with [`FilterMode::Deny`] inside the worker instead.
pub fn spawn_brokered(
    cmd: &mut Command,
    policy: BrokerPolicy,
    opts: SpawnOptions,
) -> Result<BrokeredChild, BrokerError> {
    policy.validate().map_err(BrokerError::Policy)?;
    let support = Support::probe();
    let mode = opts.force_mode.unwrap_or_else(|| support.mode());
    if mode == Mode::DenyOnly {
        return Err(BrokerError::Unsupported(
            "no USER_NOTIF: brokered calls are plain denies; the launcher cannot exec a worker under it".into(),
        ));
    }
    if !support.allows(mode) {
        return Err(BrokerError::Unsupported(format!(
            "mode {mode:?} on this kernel ({support:?})"
        )));
    }
    let prog = Arc::new(
        filter::program(FilterMode::Notify, policy.exec)
            .map_err(|e| BrokerError::Policy(format!("the broker filter: {e}")))?,
    );
    let (parent_end, child_end) = sys::seqpacket_pair().map_err(|e| BrokerError::Io {
        what: "socketpair".into(),
        source: e,
    })?;
    let stop = Arc::new(sys::eventfd().map_err(|e| BrokerError::Io {
        what: "eventfd".into(),
        source: e,
    })?);

    let bstop = Arc::clone(&stop);
    let knobs = opts.knobs.clone();
    let broker = std::thread::Builder::new()
        .name("heimdall-broker".into())
        .spawn(move || -> Result<BrokerReport, BrokerError> {
            serve_handoff(
                parent_end.as_fd(),
                bstop.as_raw_fd(),
                None,
                policy,
                mode,
                knobs,
            )?
            .ok_or_else(|| BrokerError::Unsupported("the worker never started".into()))
        })
        .map_err(|e| BrokerError::Io {
            what: "spawn the broker thread".into(),
            source: e,
        })?;

    let child_raw = child_end.as_raw_fd();
    let hook_prog = Arc::clone(&prog);
    // SAFETY: the hook makes system calls only (no allocation, no locks).
    unsafe {
        cmd.pre_exec(move || {
            let fd = install(&hook_prog, FILTER_FLAG_NEW_LISTENER)?
                .ok_or_else(|| std::io::Error::from_raw_os_error(libc::EINVAL))?;
            sys::send_fd_and_pid(child_raw, fd.as_raw_fd(), libc::getpid())
        });
    }
    let spawned = cmd.spawn();
    drop(child_end);
    match spawned {
        Ok(child) => Ok(BrokeredChild {
            child,
            mode,
            stop,
            broker: Some(broker),
        }),
        Err(e) => {
            sys::eventfd_signal(stop.as_raw_fd());
            let _ = broker.join();
            Err(BrokerError::Io {
                what: "spawn the worker".into(),
                source: e,
            })
        }
    }
}

/// A unix socket pair for handing a worker's listener to its broker
/// (`SOCK_SEQPACKET`, close-on-exec): `(broker end, worker end)`.
pub fn handoff_pair() -> std::io::Result<(OwnedFd, OwnedFd)> {
    sys::seqpacket_pair()
}

/// A stop signal for [`serve_handoff`] / [`Broker::serve`]: readable once
/// [`StopSignal::signal`] was called.
#[derive(Debug)]
pub struct StopSignal(OwnedFd);

impl StopSignal {
    pub fn new() -> std::io::Result<StopSignal> {
        sys::eventfd().map(StopSignal)
    }
    pub fn signal(&self) {
        sys::eventfd_signal(self.0.as_raw_fd());
    }
    pub fn raw(&self) -> RawFd {
        self.0.as_raw_fd()
    }
}

/// Inside the worker, right before its exec: install the broker filter (no
/// new privileges) and, in [`FilterMode::Notify`], send its listener over
/// `handoff` (the worker end of [`handoff_pair`]). The listener is
/// close-on-exec and closed here, so the worker's program never holds it.
pub fn install_worker_filter(
    mode: FilterMode,
    exec: crate::ExecPolicy,
    handoff: Option<BorrowedFd<'_>>,
) -> std::io::Result<()> {
    let prog = filter::program(mode, exec).map_err(std::io::Error::other)?;
    match mode {
        FilterMode::Deny => install(&prog, 0).map(|_| ()),
        FilterMode::Notify => {
            let sock = handoff.ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidInput, "no hand-off socket")
            })?;
            let l = install(&prog, FILTER_FLAG_NEW_LISTENER)?
                .ok_or_else(|| std::io::Error::from_raw_os_error(libc::EINVAL))?;
            // SAFETY: both fds are open for the call.
            unsafe { sys::send_fd_and_pid(sock.as_raw_fd(), l.as_raw_fd(), libc::getpid()) }
        }
    }
}

/// The broker side: wait on `sock` (the broker end of [`handoff_pair`]) for
/// the worker's listener, then serve it until every task using the filter is
/// gone or `stop` is readable. `worker_pid`: the worker's pid as THIS process
/// sees it (a container's init); `None`: the pid the worker sent (same pid
/// namespace). `Ok(None)`: the worker never handed a listener over (it
/// failed before its exec, or `stop` came first).
pub fn serve_handoff(
    sock: BorrowedFd<'_>,
    stop: RawFd,
    worker_pid: Option<u32>,
    policy: BrokerPolicy,
    mode: Mode,
    knobs: DebugKnobs,
) -> Result<Option<BrokerReport>, BrokerError> {
    let io = |what: &str| {
        let what = what.to_string();
        move |source| BrokerError::Io { what, source }
    };
    loop {
        let rev = sys::poll_in(&[sock.as_raw_fd(), stop], -1).map_err(io("poll"))?;
        if rev[0] & libc::POLLIN != 0 {
            break;
        }
        if rev[1] != 0 || rev[0] != 0 {
            return Ok(None);
        }
    }
    let Some((listener, pid)) =
        sys::recv_fd_and_pid(sock.as_raw_fd()).map_err(io("receive the listener"))?
    else {
        return Ok(None);
    };
    let pid = worker_pid.unwrap_or(pid as u32);
    let b = Broker::new(Listener::from_fd(listener), pid, policy, mode, knobs)?;
    Ok(Some(b.serve(stop)))
}