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

nordisk/heimdall

crates/heimdall-container/src/sys.rs

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

//! The few raw system calls heimdall-container needs (the only `unsafe`).
//! Heimdall is a defensive sandbox; each call here only reads the calling
//! process's own state, makes a pipe, moves an fd over a socket it holds, or
//! signals its own shim or job (learning runs, crate::learn).
#![allow(unsafe_code)]

/// The kernel's Landlock ABI (`landlock_create_ruleset(NULL, 0, VERSION)`),
/// `None` when Landlock is not built in or not enabled.
pub fn landlock_abi() -> Option<u32> {
    const LANDLOCK_CREATE_RULESET_VERSION: libc::c_ulong = 1;
    // SAFETY: with a NULL attr and size 0 the kernel only returns the ABI
    // version; no memory is read or written.
    let v = unsafe {
        libc::syscall(
            libc::SYS_landlock_create_ruleset,
            std::ptr::null::<libc::c_void>(),
            0usize,
            LANDLOCK_CREATE_RULESET_VERSION,
        )
    };
    (v > 0).then_some(v as u32)
}

/// `prctl(PR_GET_NO_NEW_PRIVS)` of the calling thread.
#[cfg_attr(not(feature = "youki"), allow(dead_code))]
pub fn no_new_privs() -> bool {
    // SAFETY: PR_GET_NO_NEW_PRIVS takes no pointers.
    unsafe { libc::prctl(libc::PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) == 1 }
}

/// `prctl(PR_GET_SECCOMP)` of the calling thread: 0 off, 2 filter.
#[cfg_attr(not(feature = "youki"), allow(dead_code))]
pub fn seccomp_mode() -> i32 {
    // SAFETY: PR_GET_SECCOMP takes no pointers.
    unsafe { libc::prctl(libc::PR_GET_SECCOMP, 0, 0, 0, 0) }
}

/// A close-on-exec pipe: (read end, write end).
#[cfg(feature = "youki")]
pub fn pipe_cloexec() -> std::io::Result<(std::os::fd::OwnedFd, std::os::fd::OwnedFd)> {
    use std::os::fd::FromRawFd;
    let mut fds = [0 as libc::c_int; 2];
    // SAFETY: `fds` is a valid array of two ints for the kernel to fill.
    if unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
        return Err(std::io::Error::last_os_error());
    }
    // SAFETY: both fds were just created and are owned by nobody else.
    Ok(unsafe {
        (
            std::os::fd::OwnedFd::from_raw_fd(fds[0]),
            std::os::fd::OwnedFd::from_raw_fd(fds[1]),
        )
    })
}

/// Write all of `buf` to a raw fd this process inherited (the executor's
/// report pipe; the fd is not owned here and stays open).
#[cfg(feature = "youki")]
pub fn write_fd(fd: i32, mut buf: &[u8]) -> std::io::Result<()> {
    while !buf.is_empty() {
        // SAFETY: `buf` is a valid readable slice of `buf.len()` bytes.
        let n = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
        if n < 0 {
            let e = std::io::Error::last_os_error();
            if e.kind() == std::io::ErrorKind::Interrupted {
                continue;
            }
            return Err(e);
        }
        buf = &buf[n as usize..];
    }
    Ok(())
}

/// Set `O_NONBLOCK` on a raw fd this process owns.
#[cfg(feature = "youki")]
pub fn set_nonblocking(fd: i32) -> std::io::Result<()> {
    // SAFETY: F_GETFL / F_SETFL take no pointers.
    unsafe {
        let fl = libc::fcntl(fd, libc::F_GETFL);
        if fl < 0 || libc::fcntl(fd, libc::F_SETFL, fl | libc::O_NONBLOCK) < 0 {
            return Err(std::io::Error::last_os_error());
        }
    }
    Ok(())
}

/// `dup3(fd, target, O_CLOEXEC)`: put an inherited fd at a fixed number
/// (the executor's report fd, crate::genprof::REPORT_FD).
#[cfg(feature = "youki")]
pub fn dup_cloexec_to(fd: i32, target: i32) -> std::io::Result<()> {
    // SAFETY: dup3 takes no pointers; `target` is replaced if open.
    if unsafe { libc::dup3(fd, target, libc::O_CLOEXEC) } < 0 {
        return Err(std::io::Error::last_os_error());
    }
    Ok(())
}

/// `_exit(code)`: leave without running anything else (after the generated
/// filter is installed, only `exit_group` is sure to be allowed).
#[cfg(feature = "youki")]
pub fn exit_now(code: i32) -> ! {
    // SAFETY: _exit never returns and touches no memory of ours.
    unsafe { libc::_exit(code) }
}

/// `kill(pid, sig)`: the runner forwards a learning run's signals to its shim.
pub fn kill(pid: i32, sig: i32) -> std::io::Result<()> {
    // SAFETY: kill takes no pointers.
    if unsafe { libc::kill(pid, sig) } != 0 {
        return Err(std::io::Error::last_os_error());
    }
    Ok(())
}

/// The pid of a connected unix socket's peer (`SO_PEERCRED`).
pub fn peer_pid(fd: std::os::fd::BorrowedFd<'_>) -> std::io::Result<i32> {
    use std::os::fd::AsRawFd;
    let mut cred = libc::ucred {
        pid: 0,
        uid: 0,
        gid: 0,
    };
    let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
    // SAFETY: getsockopt writes at most `len` bytes into `cred`.
    let r = unsafe {
        libc::getsockopt(
            fd.as_raw_fd(),
            libc::SOL_SOCKET,
            libc::SO_PEERCRED,
            (&mut cred as *mut libc::ucred).cast(),
            &mut len,
        )
    };
    if r != 0 {
        return Err(std::io::Error::last_os_error());
    }
    Ok(cred.pid)
}

/// Signals a learning shim caught (bit `1 << signo`), not yet forwarded.
#[cfg(feature = "youki")]
static CAUGHT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);

#[cfg(feature = "youki")]
extern "C" fn caught(sig: libc::c_int) {
    if (0..64).contains(&sig) {
        CAUGHT.fetch_or(1u64 << sig, std::sync::atomic::Ordering::SeqCst);
    }
}

/// Catch `sigs` into a set the shim drains with [`take_signals`] (a learning
/// run forwards them to its program instead of dying from them).
#[cfg(feature = "youki")]
pub fn catch_signals(sigs: &[i32]) {
    for &s in sigs {
        // SAFETY: the handler only does an atomic fetch_or (async-signal-safe).
        unsafe { libc::signal(s, caught as *const () as libc::sighandler_t) };
    }
}

/// The signals caught since the last call.
#[cfg(feature = "youki")]
pub fn take_signals() -> u64 {
    CAUGHT.swap(0, std::sync::atomic::Ordering::SeqCst)
}

/// Send `fd` over the unix socket at the raw fd `sock` (the executor's
/// hand-off fd, heimdall-seccomp's `HANDOFF_FD`, which this process holds).
#[cfg(feature = "youki")]
pub fn send_fd_at(sock: i32, fd: std::os::fd::BorrowedFd<'_>) -> std::io::Result<()> {
    // SAFETY: `sock` was dup3'd to this number by the caller and stays open
    // for the duration of the call.
    let s = unsafe { std::os::fd::BorrowedFd::borrow_raw(sock) };
    heimdall_seccomp::notif::send_fd(s, fd)
}

/// Move the calling (single-threaded) process into a new, NESTED user
/// namespace where its uid and gid (root of the job's namespace, the
/// runner's uid on the host) are `id`: a learning run's program that must not
/// run as root (crate::learn::LearnPlan::uid). The maps are written by a
/// short-lived child that stays in the job's namespace (the owner of the new
/// one, so it may map it; the caller itself may be denied every capability
/// in its new namespace, e.g. by Ubuntu's
/// `apparmor_restrict_unprivileged_userns`). Nothing gains a capability: the
/// only mapping is the caller's own id, and `setgroups` is denied.
#[cfg(feature = "youki")]
pub fn become_uid(id: u32) -> std::io::Result<()> {
    use std::io::{Error, Read, Write};
    let (go_r, go_w) = pipe_cloexec()?;
    // SAFETY: getpid takes no pointers.
    let me = unsafe { libc::getpid() };
    // SAFETY: the job's init is single-threaded here (the executor, before
    // execve); the child only writes three /proc files and _exits.
    let child = unsafe { libc::fork() };
    if child < 0 {
        return Err(Error::last_os_error());
    }
    if child == 0 {
        drop(go_w);
        let mut b = [0u8; 1];
        let go = std::fs::File::from(go_r).read(&mut b).is_ok_and(|n| n == 1) && b[0] == 1;
        let mut code = 0;
        if go {
            // The exit code names the step (1..=3, x 64) and the errno (< 64).
            let steps = [
                ("setgroups", "deny".to_string()),
                ("uid_map", format!("{id} 0 1")),
                ("gid_map", format!("{id} 0 1")),
            ];
            for (i, (f, v)) in steps.iter().enumerate() {
                if let Err(e) = std::fs::write(format!("/proc/{me}/{f}"), v) {
                    code = (i as i32 + 1) * 64 + e.raw_os_error().unwrap_or(63).clamp(1, 63);
                    break;
                }
            }
        }
        exit_now(code)
    }
    drop(go_r);
    // SAFETY: unshare takes no pointers.
    let unshared = unsafe { libc::unshare(libc::CLONE_NEWUSER) };
    let unshare_err = Error::last_os_error();
    let _ = std::fs::File::from(go_w).write_all(&[u8::from(unshared == 0)]);
    let mut st: libc::c_int = 0;
    // SAFETY: waitpid on our own child, `st` is a valid int.
    while unsafe { libc::waitpid(child, &mut st, 0) } < 0 {
        let e = Error::last_os_error();
        if e.kind() != std::io::ErrorKind::Interrupted {
            return Err(e);
        }
    }
    if unshared != 0 {
        return Err(Error::other(format!(
            "unshare(CLONE_NEWUSER): {unshare_err}"
        )));
    }
    if !libc::WIFEXITED(st) || libc::WEXITSTATUS(st) != 0 {
        let code = if libc::WIFEXITED(st) {
            libc::WEXITSTATUS(st)
        } else {
            0
        };
        let step = ["?", "setgroups", "uid_map", "gid_map"]
            [usize::try_from(code / 64).unwrap_or(0).min(3)];
        return Err(Error::other(format!(
            "writing {step}: {}",
            Error::from_raw_os_error(code % 64)
        )));
    }
    drop_all_caps()?;
    // SAFETY: getuid / getgid take no pointers.
    let (u, g) = unsafe { (libc::getuid(), libc::getgid()) };
    if (u, g) != (id, id) {
        return Err(Error::other(format!(
            "uid {u} gid {g} after the maps, not {id}"
        )));
    }
    Ok(())
}

/// Drop every capability of the calling thread (bounding, effective,
/// permitted, inheritable; ambient cleared).
#[cfg(feature = "youki")]
pub fn drop_all_caps() -> std::io::Result<()> {
    #[repr(C)]
    struct Header {
        version: u32,
        pid: i32,
    }
    #[repr(C)]
    #[derive(Clone, Copy)]
    struct Data {
        effective: u32,
        permitted: u32,
        inheritable: u32,
    }
    const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
    let mut h = Header {
        version: LINUX_CAPABILITY_VERSION_3,
        pid: 0,
    };
    let d = [Data {
        effective: 0,
        permitted: 0,
        inheritable: 0,
    }; 2];
    // SAFETY: prctl takes no pointers; capset reads a valid header and two
    // data structs (version 3).
    unsafe {
        // A new user namespace starts with a full bounding set: empty it
        // (needs CAP_SETPCAP there, which the namespace's creator has).
        for cap in 0..64 {
            if libc::prctl(libc::PR_CAPBSET_DROP, cap, 0, 0, 0) != 0 {
                let e = std::io::Error::last_os_error();
                if e.raw_os_error() == Some(libc::EINVAL) {
                    break; // past the kernel's last capability
                }
                return Err(std::io::Error::other(format!("bounding set: {e}")));
            }
        }
        libc::prctl(
            libc::PR_CAP_AMBIENT,
            libc::PR_CAP_AMBIENT_CLEAR_ALL,
            0,
            0,
            0,
        );
        if libc::syscall(libc::SYS_capset, &mut h as *mut Header, d.as_ptr()) != 0 {
            return Err(std::io::Error::last_os_error());
        }
    }
    Ok(())
}