Gunnar

nordisk/heimdall

crates/heimdall-run/tests/policy_file.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! The policy file only narrows a job: these tests prove each refusal.
//! Heimdall is a defensive sandbox; nothing here touches anything outside a
//! temporary directory.

use heimdall_run::policy_file::{LIMIT_KEYS, MAX_POLICY_FILE_BYTES, POLICY_FORMAT_VERSION};
use heimdall_run::{PolicyError, PolicyFile, RunLimits};
use proptest::prelude::*;

fn parse(s: &str) -> Result<PolicyFile, PolicyError> {
    PolicyFile::parse(s)
}

#[test]
fn documented_example_policy_parses() {
    let text = include_str!("../../../docs/examples/policy.toml");
    let p = parse(text).unwrap();
    assert_eq!(p.job.as_deref(), Some("wasm"));
    assert_eq!(p.limits.wall_time_ms, 10_000);
}

#[test]
fn empty_file_is_refused_for_missing_version() {
    assert_eq!(parse(""), Err(PolicyError::MissingVersion));
}

#[test]
fn version_zero_is_refused() {
    assert_eq!(
        parse("version = 0"),
        Err(PolicyError::UnsupportedVersion(0))
    );
}

#[test]
fn future_version_is_refused() {
    assert_eq!(
        parse("version = 2"),
        Err(PolicyError::UnsupportedVersion(2))
    );
}

#[test]
fn version_as_string_is_a_syntax_error() {
    assert!(matches!(
        parse("version = \"1\""),
        Err(PolicyError::Syntax(_))
    ));
}

#[test]
fn current_version_constant_is_one() {
    assert_eq!(POLICY_FORMAT_VERSION, 1);
}

#[test]
fn unknown_top_level_key_is_refused() {
    assert!(matches!(
        parse("version = 1\nfoo = 3"),
        Err(PolicyError::Syntax(_))
    ));
}

#[test]
fn misspelled_limit_key_is_refused_not_defaulted() {
    let r = parse("version = 1\n[limits]\nfeul = 5");
    assert!(matches!(r, Err(PolicyError::Syntax(m)) if m.contains("feul")));
}

#[test]
fn read_paths_are_refused_in_milestone_1() {
    assert_eq!(
        parse("version = 1\nread = [\"/etc\"]"),
        Err(PolicyError::WideningKey("read"))
    );
}

#[test]
fn write_paths_are_refused_in_milestone_1() {
    assert_eq!(
        parse("version = 1\nwrite = [\"/tmp\"]"),
        Err(PolicyError::WideningKey("write"))
    );
}

#[test]
fn net_table_is_refused_for_wasm_jobs() {
    assert_eq!(
        parse("version = 1\n[net]\nhost = \"example.org\"\nport = 443"),
        Err(PolicyError::WideningKey("net"))
    );
}

#[test]
fn empty_read_list_is_still_refused_because_the_key_itself_widens() {
    assert_eq!(
        parse("version = 1\nread = []"),
        Err(PolicyError::WideningKey("read"))
    );
}

#[test]
fn job_kind_wasm_is_accepted() {
    assert_eq!(
        parse("version = 1\n[job]\nkind = \"wasm\"")
            .unwrap()
            .job
            .as_deref(),
        Some("wasm")
    );
}

#[test]
fn job_kind_native_is_refused_in_milestone_1() {
    assert_eq!(
        parse("version = 1\n[job]\nkind = \"native\""),
        Err(PolicyError::UnsupportedJob("native".into()))
    );
}

#[test]
fn unknown_key_in_job_table_is_refused() {
    assert!(matches!(
        parse("version = 1\n[job]\nkind = \"wasm\"\nprivileged = true"),
        Err(PolicyError::Syntax(_))
    ));
}

#[test]
fn zero_fuel_is_refused() {
    assert_eq!(
        parse("version = 1\n[limits]\nfuel = 0"),
        Err(PolicyError::LimitZero { key: "fuel" })
    );
}

#[test]
fn zero_timeout_is_refused() {
    assert_eq!(
        parse("version = 1\n[limits]\nwall_time_ms = 0"),
        Err(PolicyError::LimitZero {
            key: "wall_time_ms"
        })
    );
}

#[test]
fn negative_limit_is_refused() {
    assert_eq!(
        parse("version = 1\n[limits]\nmemory_bytes = -1"),
        Err(PolicyError::LimitNotInteger {
            key: "memory_bytes"
        })
    );
}

#[test]
fn float_limit_is_refused() {
    assert_eq!(
        parse("version = 1\n[limits]\nfuel = 1.5"),
        Err(PolicyError::LimitNotInteger { key: "fuel" })
    );
}

#[test]
fn string_limit_is_refused() {
    assert_eq!(
        parse("version = 1\n[limits]\nwall_time_ms = \"10s\""),
        Err(PolicyError::LimitNotInteger {
            key: "wall_time_ms"
        })
    );
}

#[test]
fn memory_above_ceiling_is_refused() {
    let c = RunLimits::ceilings().memory_bytes;
    let r = parse(&format!("version = 1\n[limits]\nmemory_bytes = {}", c + 1));
    assert_eq!(
        r,
        Err(PolicyError::LimitAboveCeiling {
            key: "memory_bytes",
            value: c + 1,
            ceiling: c
        })
    );
}

#[test]
fn every_limit_exactly_at_its_ceiling_is_accepted() {
    let mut text = String::from("version = 1\n[limits]\n");
    for (key, ceiling) in LIMIT_KEYS {
        text.push_str(&format!("{key} = {ceiling}\n"));
    }
    assert_eq!(parse(&text).unwrap().limits, RunLimits::ceilings());
}

#[test]
fn every_limit_one_above_its_ceiling_is_refused_by_name() {
    for (key, ceiling) in LIMIT_KEYS {
        let r = parse(&format!("version = 1\n[limits]\n{key} = {}\n", ceiling + 1));
        assert_eq!(
            r,
            Err(PolicyError::LimitAboveCeiling {
                key,
                value: ceiling + 1,
                ceiling
            }),
            "{key}"
        );
    }
}

#[test]
fn every_limit_at_zero_is_refused_by_name() {
    for (key, _) in LIMIT_KEYS {
        let r = parse(&format!("version = 1\n[limits]\n{key} = 0\n"));
        assert_eq!(r, Err(PolicyError::LimitZero { key }), "{key}");
    }
}

#[test]
fn policy_maps_onto_core_limits_with_fuel_metered() {
    let p = parse("version = 1\n[limits]\nfuel = 5\nmemories = 2\n").unwrap();
    let core = p.limits.to_core();
    assert_eq!(core.fuel, Some(5));
    assert_eq!(core.memories, 2);
}

#[test]
fn table_elements_above_u32_range_is_refused_by_ceiling() {
    let r = parse("version = 1\n[limits]\ntable_elements = 4294967296");
    assert!(matches!(
        r,
        Err(PolicyError::LimitAboveCeiling {
            key: "table_elements",
            ..
        })
    ));
}

#[test]
fn file_cap_above_total_cap_is_refused() {
    let r =
        parse("version = 1\n[limits]\nmax_output_file_bytes = 100\nmax_output_total_bytes = 99");
    assert_eq!(
        r,
        Err(PolicyError::FileAboveTotal {
            file: 100,
            total: 99
        })
    );
}

#[test]
fn limits_not_named_keep_their_defaults() {
    let p = parse("version = 1\n[limits]\nfuel = 7").unwrap();
    assert_eq!(p.limits.fuel, 7);
    assert_eq!(p.limits.wall_time_ms, RunLimits::default().wall_time_ms);
}

#[test]
fn limits_as_a_non_table_is_a_syntax_error() {
    assert!(matches!(
        parse("version = 1\nlimits = 5"),
        Err(PolicyError::Syntax(_))
    ));
}

#[test]
fn duplicate_key_is_a_syntax_error() {
    assert!(matches!(
        parse("version = 1\nversion = 1"),
        Err(PolicyError::Syntax(_))
    ));
}

#[test]
fn oversized_file_is_refused_before_parsing() {
    let mut text = String::from("version = 1\n");
    text.push_str(&"# padding\n".repeat(MAX_POLICY_FILE_BYTES / 10 + 1));
    assert_eq!(parse(&text), Err(PolicyError::TooLarge));
    assert_eq!(
        PolicyFile::from_bytes(text.as_bytes()),
        Err(PolicyError::TooLarge)
    );
}

#[test]
fn invalid_utf8_bytes_are_refused() {
    assert_eq!(
        PolicyFile::from_bytes(b"version = 1\n# \xff\xfe"),
        Err(PolicyError::NotUtf8)
    );
}

#[test]
fn comments_and_underscored_numbers_are_accepted() {
    let p = parse("# a job\nversion = 1 # v\n[limits]\nfuel = 1_000_000\n").unwrap();
    assert_eq!(p.limits.fuel, 1_000_000);
}

#[test]
fn load_reads_a_regular_file() {
    let d = tempfile::tempdir().unwrap();
    let p = d.path().join("p.toml");
    std::fs::write(&p, "version = 1\n[limits]\ninstances = 3\n").unwrap();
    assert_eq!(PolicyFile::load(&p).unwrap().limits.instances, 3);
}

#[test]
fn load_refuses_a_symlinked_policy_file() {
    let d = tempfile::tempdir().unwrap();
    let real = d.path().join("real.toml");
    std::fs::write(&real, "version = 1\n").unwrap();
    let link = d.path().join("link.toml");
    std::os::unix::fs::symlink(&real, &link).unwrap();
    assert_eq!(PolicyFile::load(&link), Err(PolicyError::NotRegularFile));
}

#[test]
fn load_refuses_a_directory() {
    let d = tempfile::tempdir().unwrap();
    assert_eq!(PolicyFile::load(d.path()), Err(PolicyError::NotRegularFile));
}

#[test]
fn load_of_a_missing_file_is_an_io_error() {
    let d = tempfile::tempdir().unwrap();
    assert!(matches!(
        PolicyFile::load(&d.path().join("nope")),
        Err(PolicyError::Io(_))
    ));
}

#[test]
fn load_refuses_an_oversized_file() {
    let d = tempfile::tempdir().unwrap();
    let p = d.path().join("big.toml");
    std::fs::write(&p, vec![b'#'; MAX_POLICY_FILE_BYTES + 1]).unwrap();
    assert_eq!(PolicyFile::load(&p), Err(PolicyError::TooLarge));
}

proptest! {
    #[test]
    fn prop_arbitrary_bytes_never_panic_the_parser(bytes in proptest::collection::vec(any::<u8>(), 0..2048)) {
        let _ = PolicyFile::from_bytes(&bytes);
    }

    #[test]
    fn prop_any_accepted_policy_stays_within_ceilings(
        fuel in 0u64..=2_000_000_000_000,
        mem in 0u64..=(8u64 << 30),
        files in 0u64..=20_000,
    ) {
        let text = format!("version = 1\n[limits]\nfuel = {fuel}\nmemory_bytes = {mem}\nmax_output_files = {files}\n");
        if let Ok(p) = PolicyFile::parse(&text) {
            let c = RunLimits::ceilings();
            prop_assert!(p.limits.fuel >= 1 && p.limits.fuel <= c.fuel);
            prop_assert!(p.limits.memory_bytes >= 1 && p.limits.memory_bytes <= c.memory_bytes);
            prop_assert!(p.limits.max_output_files >= 1 && p.limits.max_output_files <= c.max_output_files);
        }
    }

    #[test]
    fn prop_in_range_limits_round_trip_exactly(fuel in 1u64..=1_000_000_000_000, t in 1u64..=3_600_000) {
        let p = PolicyFile::parse(&format!("version = 1\n[limits]\nfuel = {fuel}\nwall_time_ms = {t}\n")).unwrap();
        prop_assert_eq!(p.limits.fuel, fuel);
        prop_assert_eq!(p.limits.wall_time_ms, t);
    }
}