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/spec.rs

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

//! The OCI runtime config (`config.json`) of one protected job: heimdall-plain's
//! ordinary container config, tightened.
//!
//! Heimdall is a defensive sandbox. On top of heimdall-plain's config
//! (namespaces incl. an empty netns, read-only root, tmpfs `/tmp`, `/in`
//! read-only, `/out`, every capability dropped, no-new-privs, cgroup v2
//! memory/pids/cpu): the stricter [`crate::profile`], rlimits nofile, nproc
//! (= `pids` + 1, a backstop above the cgroup), fsize (= `max_output_file_bytes`) and core = 0, no
//! `/dev/mqueue`. `io.weight` is NOT here: libcgroups 0.7.0 overflows u16
//! converting an OCI blockIO.weight, so the shim writes `io.weight` itself;
//! io.max throttles are added by [`crate::iolimit::apply`].
//! Landlock is not in the config: the shim's executor applies it.

use heimdall_plain::spec::SpecInput;
use serde_json::{json, Value};

use crate::{profile, UnlistedMount};

/// `RLIMIT_NOFILE` of every job, unless the runner
/// ([`crate::HeimdallRunner::with_nofile`]) or the job
/// ([`heimdall_core::Limits::nofile`]) says otherwise.
pub const NOFILE: u64 = heimdall_core::NOFILE_DEFAULT;

/// The smallest `RLIMIT_NOFILE` a runner or a job may set (stdio plus a few files).
pub const NOFILE_MIN: u64 = heimdall_core::NOFILE_MIN;

/// The largest `RLIMIT_NOFILE` a runner or a job may set (also capped by the
/// runner's own hard limit, which a job can never exceed).
pub const NOFILE_MAX: u64 = heimdall_core::NOFILE_MAX;

/// Set the job's `RLIMIT_NOFILE` (soft = hard = `n`) in a built config.
pub fn set_nofile(c: &mut Value, n: u64) {
    if let Some(rl) = c["process"]["rlimits"].as_array_mut() {
        for r in rl.iter_mut().filter(|r| r["type"] == "RLIMIT_NOFILE") {
            r["soft"] = json!(n);
            r["hard"] = json!(n);
        }
    }
}

/// The one `execveat` the fixed profile allows when a generated profile is
/// applied on top (crate::genprof): on heimdall-seccomp's `EXEC_FD` with
/// `AT_EMPTY_PATH` only, the executor's start of the job's program. Without
/// a generated profile the fixed profile refuses every `execveat`.
pub fn exec_fd_rule() -> Value {
    json!({
        "names": ["execveat"],
        "action": "SCMP_ACT_ALLOW",
        "args": [
            {"index": 0, "value": heimdall_seccomp::launch::EXEC_FD, "op": "SCMP_CMP_EQ"},
            {"index": 4, "value": libc::AT_EMPTY_PATH, "op": "SCMP_CMP_EQ"}
        ],
    })
}

/// The full `config.json` for one job. `generated`: a generated profile is
/// applied on top of the fixed one (adds [`exec_fd_rule`]).
pub fn build(s: &SpecInput<'_>, unlisted: &[UnlistedMount], generated: bool) -> Value {
    let mut c = heimdall_plain::spec::build(s);
    let l = s.limits;
    c["process"]["rlimits"] = json!([
        {"type": "RLIMIT_NOFILE", "hard": NOFILE, "soft": NOFILE},
        // One above the cgroup's pids.max: the cgroup refuses a fork first and
        // counts it in pids.events (heimdall-limit-pids); the rlimit is the backstop.
        {"type": "RLIMIT_NPROC", "hard": u64::from(l.pids) + 1, "soft": u64::from(l.pids) + 1},
        {"type": "RLIMIT_FSIZE", "hard": l.max_output_file_bytes, "soft": l.max_output_file_bytes},
        {"type": "RLIMIT_CORE", "hard": 0u64, "soft": 0u64}
    ]);
    c["linux"]["seccomp"] = profile::seccomp_json();
    if generated {
        if let Some(r) = c["linux"]["seccomp"]["syscalls"].as_array_mut() {
            r.push(exec_fd_rule());
        }
    }
    if let Some(m) = c["mounts"].as_array_mut() {
        m.retain(|m| m["destination"] != "/dev/mqueue");
        for u in unlisted {
            m.push(json!({
                "destination": u.destination,
                "type": "bind",
                "source": u.source,
                "options": ["rbind", if u.writable { "rw" } else { "ro" }, "nosuid", "nodev"],
            }));
        }
    }
    c
}