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);
}
}