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

nordisk/dwarves

dwarves-gui/tests/robot_exec_sandbox.rs

//! **The `exe` pane's run is containerised — guarded, and watched failing.**
//!
//! Decided 2026-08-06: ▶ KÖR used to spawn the built artifact as a bare child process
//! with the operator's environment, cwd and write access. "What does this build do?" was
//! answered by letting it do it, to this machine. Now it runs inside a container with the
//! artifact bind-mounted read-only and no network.
//!
//! The property worth guarding needs no engine to check: **the command the container runs
//! is the CONTAINER path, and the mount is read-only.** A spec carrying the host path
//! would be a bare-process run wearing a container's clothes, and that is the exact
//! regression these tests are pointed at.
//!
//! Nothing here starts a container. The one-per-pane rule is guarded structurally
//! ([`every_start_tears_the_previous_one_down_first`]) rather than by racing two.

use dwarves_gui::boot_run::{ArtifactExecLauncher, ContainerLauncher, ExecSandbox};

/// A host path that is obviously not a container path, so the two cannot be confused.
const HOST: &str = "/home/someone/checkout/target/release/orten-server";

/// **The container runs the container path. Never the host path.**
///
/// Watched red by asserting the two strings differ first: if the sandbox handed the host
/// path through, `cmd()[0]` would equal `HOST` and the assertion below would be
/// unfalsifiable rather than merely wrong.
#[test]
fn the_command_is_the_container_path_and_never_the_host_path() {
    let s = ExecSandbox::new(HOST, vec![]);

    // RED — the discrimination itself. These must not be the same string, or nothing
    // below distinguishes a sandboxed run from a bare one.
    assert_ne!(
        s.in_container(),
        HOST,
        "the in-container path IS the host path, so the guard cannot tell them apart"
    );

    let cmd = s.cmd();
    assert_eq!(
        cmd.first().map(String::as_str),
        Some("/artefakt/orten-server")
    );
    assert!(
        !cmd.iter().any(|a| a.contains("/home/")),
        "a host path reached the container's argv: {cmd:?}"
    );
    // The mount point is the one the design names, not an incidental /tmp.
    assert!(s.in_container().starts_with(ExecSandbox::MOUNT_DIR));
}

/// The artifact is mounted READ-ONLY. "It cannot write over anything outside" is the
/// whole reason for the change, and a read-write bind gives it back the artifact itself.
#[test]
fn the_artifact_is_mounted_read_only() {
    let bind = ExecSandbox::new(HOST, vec![]).bind();
    assert_eq!(bind, format!("{HOST}:/artefakt/orten-server:ro"));
    // RED — the same assertion stated as the failure it prevents.
    assert!(
        bind.ends_with(":ro"),
        "the artifact is mounted read-write: {bind}"
    );
    assert!(!bind.ends_with(":rw"), "{bind}");
}

/// The operator's argv rides AFTER the artifact, in the order typed — the pane is still
/// "an argv field and stdout", only the process is somewhere safer.
#[test]
fn the_typed_argv_rides_after_the_artifact_in_order() {
    let s = ExecSandbox::new(HOST, vec!["--help".into(), "-v".into()]);
    assert_eq!(s.cmd(), vec!["/artefakt/orten-server", "--help", "-v"]);

    // RED — an empty argv is just the artifact, so the two cases are not the same shape.
    assert_eq!(ExecSandbox::new(HOST, vec![]).cmd().len(), 1);
}

/// The pane SHOWS the sandbox rather than implying it: an operator reading stdout has to
/// be able to see it came out of a container, which one, and that it had no network.
#[test]
fn the_banner_names_the_image_the_mount_and_the_missing_network() {
    let s = ExecSandbox::new(HOST, vec![]);
    let b = s.banner();
    assert!(
        b.contains(&s.image),
        "the banner does not name the image: {b}"
    );
    assert!(
        b.contains(":ro"),
        "the banner does not say the mount is read-only: {b}"
    );
    assert!(
        b.contains("utan nät"),
        "the banner does not say there is no network: {b}"
    );
    assert!(!s.image.is_empty(), "the sandbox has no image at all");
}

/// **Without an OCI engine the run REFUSES.** It does not quietly become the bare process
/// it replaced — that fallback is the one outcome this whole change exists to remove.
///
/// Only meaningful in a build without the engine, which is the default build.
#[cfg(not(feature = "container-run"))]
#[test]
fn without_an_oci_engine_the_run_refuses_instead_of_running_against_the_host() {
    let launcher = ArtifactExecLauncher {
        sandbox: ExecSandbox::new(HOST, vec!["--help".into()]),
    };
    let Err(err) = launcher.launch("orten-server", std::path::Path::new("/nonexistent")) else {
        panic!("a build with no OCI engine cannot have run anything");
    };
    assert!(
        err.contains("dwarves-launch-3"),
        "the refusal carries no stable code: {err}"
    );
    assert!(
        err.contains("container-run"),
        "the refusal does not say what to rebuild with: {err}"
    );
    // RED — the refusal must not read as "ran it anyway, somewhere else".
    assert!(
        !err.to_ascii_lowercase().contains("host process"),
        "the refusal offers a host fallback: {err}"
    );
}

/// **One running container per pane.**
///
/// The rule is structural, so the guard is: every assignment to the forge's `live` slot
/// is immediately preceded by the teardown call. Racing two real containers to prove it
/// would be the opposite of keeping concurrent containers down.
///
/// Watched red on a synthetic source that assigns without tearing down first, and on one
/// that has no assignments at all (which would otherwise pass vacuously).
#[test]
fn every_start_tears_the_previous_one_down_first() {
    /// Every `self.live = Some(` in `src` that is NOT immediately preceded by
    /// `self.take_live();`, by line number.
    fn unguarded(src: &str) -> Vec<usize> {
        let lines: Vec<&str> = src.lines().collect();
        lines
            .iter()
            .enumerate()
            .filter(|(_, l)| l.trim_start().starts_with("self.live = Some("))
            .filter(|(i, _)| {
                *i == 0
                    || !lines[..*i]
                        .iter()
                        .rev()
                        .find(|p| !p.trim().is_empty() && !p.trim_start().starts_with("//"))
                        .is_some_and(|p| p.trim() == "self.take_live();")
            })
            .map(|(i, _)| i + 1)
            .collect()
    }

    // RED 1 — an assignment with no teardown before it is found.
    let bad = "fn a() {\n    self.live = Some(x);\n}\n";
    assert_eq!(
        unguarded(bad),
        vec![2],
        "the scanner cannot see an unguarded start"
    );

    // RED 2 — a teardown that is not the immediately preceding statement does not count.
    let sneaky = "fn a() {\n    self.take_live();\n    let y = 1;\n    self.live = Some(x);\n}\n";
    assert_eq!(
        unguarded(sneaky),
        vec![4],
        "an intervening statement was accepted"
    );

    // The guarded shape passes, comments and blank lines in between allowed.
    let good = "fn a() {\n    self.take_live();\n    // a comment\n\n    self.live = Some(x);\n}\n";
    assert!(unguarded(good).is_empty(), "the guarded shape was rejected");

    // RED 3 — the real file must actually CONTAIN starts, or the green below is vacuous.
    const FORGE: &str = include_str!("../src/build_forge.rs");
    let starts = FORGE
        .lines()
        .filter(|l| l.trim_start().starts_with("self.live = Some("))
        .count();
    assert!(
        starts >= 2,
        "the forge starts nothing, so the rule guards nothing"
    );

    // GREEN, and it means something because of RED 3.
    let found = unguarded(FORGE);
    assert!(
        found.is_empty(),
        "build_forge.rs starts a container without tearing the previous one down, at \
         line(s) {found:?} — that is two containers alive at once"
    );
}

/// **The real thing: an artifact run through podman, proven by what it CANNOT see.**
///
/// Every check above is about the plan. This one starts a container (`--features
/// container-run`, the OCI engine linked) and asserts the two properties that separate a
/// sandboxed run from the bare process it replaced:
///
/// * a file sitting NEXT TO the artifact on the host, at an absolute path the artifact
///   knows, is **not visible** — a bare child process would have read it;
/// * a write into the mount **fails** — it is `:ro`.
///
/// The artifact under test is a `/bin/sh` script, so nothing here depends on the host's
/// libc matching the image's. It is watched red by the same script: the script itself
/// reports `HOST-FILE-VISIBLE` when it can see the sibling, so a run that escaped the
/// container fails this test with its own words rather than silently passing.
///
/// ONE container, torn down by the engine on exit (`run_to_completion`). No second test
/// in this file starts one.
#[cfg(feature = "container-run")]
#[test]
fn a_real_container_run_cannot_see_the_host_file_beside_the_artifact() {
    #[allow(unused_imports)]
    use dwarves_gui::boot_run::SerialConsole;
    use std::io::Write;
    use std::os::unix::fs::PermissionsExt;

    let dir = tempfile::tempdir().expect("tempdir");
    // The sibling the artifact must NOT be able to read. A bare child process would.
    let secret = dir.path().join("granne.txt");
    std::fs::write(&secret, "this is host-only\n").expect("write sibling");

    let artifact = dir.path().join("orten-probe");
    let mut f = std::fs::File::create(&artifact).expect("create artifact");
    write!(
        f,
        "#!/bin/sh\n\
         echo \"ARGV:$*\"\n\
         if [ -e '{}' ]; then echo HOST-FILE-VISIBLE; else echo HOST-FILE-HIDDEN; fi\n\
         if echo x > '{}' 2>/dev/null; then echo MOUNT-WRITABLE; else echo MOUNT-READONLY; fi\n",
        secret.display(),
        ExecSandbox::MOUNT_DIR.to_string() + "/orten-probe",
    )
    .expect("write script");
    drop(f);
    std::fs::set_permissions(&artifact, std::fs::Permissions::from_mode(0o755)).expect("chmod +x");

    let sandbox = ExecSandbox::new(&artifact, vec!["--hej".into()]);
    let launcher = ArtifactExecLauncher { sandbox };
    let mut console = match launcher.launch("orten-probe", dir.path()) {
        Ok(c) => c,
        Err(e) => {
            // An unreachable engine is NOT a pass. Say which, and fail.
            panic!("the OCI engine did not start the run: {e}");
        }
    };

    // Drain until terminal or the budget runs out — a container run is not instant.
    for _ in 0..600 {
        let _ = console.log();
        if !console.phase().is_running() {
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
    // KEEP DRAINING PAST THE EXIT. This script finishes in milliseconds, and the exit is
    // observable before the engine's follow-task has put a single line in the buffer — so
    // a loop that stops at the first terminal phase reads an EMPTY log and every
    // assertion below "passes" by finding no forbidden string in nothing. That is the
    // shape this file exists to catch, and it was caught here first: the run came back
    // green-looking with no output at all. A pane drains across frames for the same
    // reason; this is that, bounded.
    let mut log = String::new();
    for _ in 0..50 {
        log = console.log();
        if !log.is_empty() {
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
    console.kill();
    log = console.log();
    assert!(
        !log.is_empty(),
        "the container produced no output at all, so nothing below is being tested"
    );

    assert!(
        log.contains("ARGV:--hej"),
        "the argv the operator typed never reached the process: {log}"
    );
    assert!(
        log.contains("HOST-FILE-HIDDEN"),
        "the run could see a host file beside the artifact — it is not sandboxed: {log}"
    );
    assert!(
        !log.contains("HOST-FILE-VISIBLE"),
        "the artifact read a host-only file: {log}"
    );
    assert!(
        log.contains("MOUNT-READONLY"),
        "the artifact could write into its own mount: {log}"
    );
}