vetto 0.2.17

Daemon-less sandbox + security layer for AI coding agents (Landlock/Seatbelt, TUI statusline, post-session audit reports)
Documentation
//! Namespace helpers: unshare wrappers, unprivileged-userns probing and the
//! uid_map/gid_map writing dance performed by the parent.

use std::fs;
use std::io::Write;
use std::path::Path;

use crate::error::{VettoError, VettoResult};

pub const CLONE_NEWNS: libc::c_int = 0x0002_0000;
pub const CLONE_NEWUSER: libc::c_int = 0x1000_0000;
pub const CLONE_NEWPID: libc::c_int = 0x2000_0000;
pub const CLONE_NEWNET: libc::c_int = 0x4000_0000;
pub const CLONE_NEWIPC: libc::c_int = 0x0800_0000;

/// unshare(2) with a mapped error.
pub fn unshare(flags: libc::c_int) -> VettoResult<()> {
    // SAFETY: direct syscall with no pointers involved.
    let r = unsafe { libc::unshare(flags) };
    if r != 0 {
        return Err(VettoError::Namespace(format!(
            "unshare({flags:#x}) failed: {}",
            std::io::Error::last_os_error()
        )));
    }
    Ok(())
}

fn proc_sys_u32(path: &str) -> Option<u64> {
    fs::read_to_string(path).ok()?.trim().parse().ok()
}

/// Cheap pre-check for unprivileged userns support knobs.
/// The authoritative test remains `probe_unprivileged_userns()` (fork-based).
pub fn userns_knobs_look_enabled() -> bool {
    let clone_ok = match proc_sys_u32("/proc/sys/kernel/unprivileged_userns_clone") {
        Some(v) => v != 0,
        None => true, // knob absent => not gated
    };
    let max_ok = proc_sys_u32("/proc/sys/user/max_user_namespaces").unwrap_or(u64::MAX) > 0;
    clone_ok && max_ok
}

/// Authoritative probe: fork a child that unshares a user namespace while
/// the parent performs the real map writes over a two-pipe handshake; success
/// proves the user-namespace mapping prerequisite (unshare + uid_map +
/// gid_map). `probe_full_tier` separately exercises the remaining FULL stack.
///
/// Two SEPARATE pipes: a single bidirectional pipe would let the child race
/// the parent for its own ready byte.
///
/// SAFETY: fork in a single-threaded context (called before any tokio
/// runtime exists); the child only performs syscalls and _exit().
pub fn probe_unprivileged_userns() -> bool {
    probe_userns(false)
}

/// Authoritative FULL-tier probe. Besides user-id mappings this exercises the
/// namespace and mount operations used by the real child, including a private
/// procfs mount from inside the new PID namespace. This prevents selecting
/// FULL on container hosts that permit CLONE_NEWUSER but reject a later mount.
pub fn probe_full_tier() -> bool {
    probe_userns(true)
}

fn probe_userns(require_full_stack: bool) -> bool {
    if !userns_knobs_look_enabled() {
        return false;
    }
    let mut ready_fds = [0 as libc::c_int; 2]; // child -> parent
    let mut ack_fds = [0 as libc::c_int; 2]; // parent -> child
                                             // SAFETY: valid out-arrays; scalar flags.
    if unsafe { libc::pipe2(ready_fds.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
        return false;
    }
    // SAFETY: valid out-arrays; scalar flags.
    if unsafe { libc::pipe2(ack_fds.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
        return false;
    }
    // SAFETY: see fn docs.
    match unsafe { libc::fork() } {
        -1 => false,
        0 => {
            // The child never touches the ready pipe's read end, so it can
            // never race the parent for its own status byte.
            // SAFETY: closing the ends we do not use.
            unsafe {
                libc::close(ready_fds[0]);
                libc::close(ack_fds[1]);
            }
            let entered = unshare(CLONE_NEWUSER).is_ok();
            // SAFETY: raw write of one status byte (1 = unshared).
            let byte: &[u8] = if entered { &[1] } else { &[0] };
            let _ = unsafe { libc::write(ready_fds[1], byte.as_ptr().cast(), 1) };
            if !entered {
                unsafe { libc::_exit(1) };
            }
            let mut ack = [0u8; 1];
            // SAFETY: raw blocking read of the parent's verdict.
            let n = unsafe { libc::read(ack_fds[0], ack.as_mut_ptr().cast(), 1) };
            // SAFETY: plain closes on spent probe fds.
            unsafe {
                libc::close(ready_fds[1]);
                libc::close(ack_fds[0]);
            }
            let mapped = n == 1 && ack[0] == 0;
            let ready = mapped && (!require_full_stack || probe_full_stack_in_child());
            unsafe { libc::_exit(if ready { 0 } else { 1 }) };
        }
        pid => {
            let mut status = 0i32;
            parent_probe_side(pid, ready_fds, ack_fds, &mut status)
        }
    }
}

fn probe_full_stack_in_child() -> bool {
    if unshare(CLONE_NEWNS).is_err() || super::mounts::make_root_private().is_err() {
        return false;
    }
    if super::mounts::isolate_dev_shm().is_err()
        || unshare(CLONE_NEWIPC).is_err()
        || unshare(CLONE_NEWNET).is_err()
        || unshare(CLONE_NEWPID).is_err()
    {
        return false;
    }

    // CLONE_NEWPID affects the next child. Mount procfs there so the probe
    // covers the same capability boundary as the real PID-1 supervisor.
    let pid = unsafe { libc::fork() };
    if pid < 0 {
        return false;
    }
    if pid == 0 {
        let ok = super::mounts::mount_restricted_proc().is_ok();
        unsafe { libc::_exit(if ok { 0 } else { 1 }) };
    }
    let mut status = 0;
    loop {
        let result = unsafe { libc::waitpid(pid, &mut status, 0) };
        if result == pid {
            break;
        }
        if result < 0 && std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
            return false;
        }
    }
    libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0
}

fn parent_probe_side(
    pid: libc::pid_t,
    ready_fds: [libc::c_int; 2],
    ack_fds: [libc::c_int; 2],
    status: &mut i32,
) -> bool {
    // SAFETY: closing the ends we do not use.
    unsafe {
        libc::close(ready_fds[1]);
        libc::close(ack_fds[0]);
    }
    let mut ready = [0u8; 1];
    // SAFETY: raw read of the child's status byte.
    let n = unsafe { libc::read(ready_fds[0], ready.as_mut_ptr().cast(), 1) };
    let entered = n == 1 && ready[0] == 1;
    let mut maps_ok = false;
    if entered {
        maps_ok = write_id_maps(pid).is_ok();
        // SAFETY: raw write of one verdict byte (0 = maps ok).
        let byte: &[u8] = if maps_ok { &[0] } else { &[1] };
        let _ = unsafe { libc::write(ack_fds[1], byte.as_ptr().cast(), 1) };
    }
    // SAFETY: closing the spent probe fds; EOF unblocks a child that is
    // still waiting when maps were never attempted.
    unsafe {
        libc::close(ready_fds[0]);
        libc::close(ack_fds[1]);
    }
    loop {
        // SAFETY: plain waitpid.
        let r = unsafe { libc::waitpid(pid, status, 0) };
        if r == pid
            || (r < 0 && std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR))
        {
            break;
        }
    }
    // SAFETY: scalar WIF/WEXIT macros.
    let exited_zero = libc::WIFEXITED(*status) && libc::WEXITSTATUS(*status) == 0;
    entered && maps_ok && exited_zero
}

/// Write setgroups-deny + uid_map + gid_map for `pid` mapping the caller's
/// own ids 1:1. Must be called by the REAL parent right after the child
/// enters its new user namespace.
pub fn write_id_maps(pid: libc::pid_t) -> VettoResult<()> {
    let base = Path::new("/proc").join(pid.to_string());
    let uid = unsafe { libc::getuid() }; // SAFETY: no pointers
    let gid = unsafe { libc::getgid() };

    let _ = fs::write(base.join("setgroups"), "deny");

    let w = |name: &str, content: String| -> VettoResult<()> {
        let path = base.join(name);
        let mut f = fs::OpenOptions::new()
            .write(true)
            .open(&path)
            .map_err(|e| VettoError::Namespace(format!("open {}: {e}", path.display())))?;
        f.write_all(content.as_bytes())
            .map_err(|e| VettoError::Namespace(format!("write {}: {e}", path.display())))?;
        Ok(())
    };

    w("uid_map", format!("0 {uid} 1\n"))?;
    w("gid_map", format!("0 {gid} 1\n"))?;
    Ok(())
}