Changelog
All notable changes to Heimdall are listed here, one section per release tag. Each entry names the commit on main it comes from. The format follows Keep a Changelog, and Heimdall uses Semantic Versioning: within a release line (0.2.x) the job API in docs/API.md changes by additions only; a new 0.x line lists its breaks under "Changed (breaking)".
Heimdall is a defensive sandbox. Each fixed vulnerability is listed under Security with its RUSTSEC, CVE or GHSA id where one exists. To report a vulnerability, see SECURITY.md.
[Unreleased] (next: 0.3.0)
Runtime enforcement of GENERATED seccomp profiles (lane genprof). Breaking for code built against 0.2 (struct literals), so the next line is 0.3.0.
Changed (lane prof: generated per-program profiles)
- heimdall-profiles: the
heimdall-wasm-hostprofile (the worker of everyheimdall run --job wasm, the verify-bundle job among them) is measured again over the same corpus (the real verify-bundle guest, single and split N = 1 / 8, good, tampered and bomb bundles) after heimdall-wasm stopped making memfds:memfd_create(and on glibc onefcntlsealing tuple) left the profile, 41 → 39 rules (glibc) and 36 → 35 (musl); nothing was added. New digests: glibcsha256:bd1a5bcdf7418ac874cc472c90d858dbfe09e8f5b2d2ac21a73be62ab2816272, muslsha256:8369f2ec9bf4dcf8878faea508680a6c0535926de1a426d4da9d4ad285642d23. Measured in privileged rootless podman on oden (kernel 7.0). - Holger's other two job kinds have generated profiles too, checked in at Holger (branch
heimdall-allin/profuntil Heimdall 0.3.0 is on crates.io) and listed in docs/SECCOMP.md: the scan worker (32 rules, enforced throughPolicy.seccomp) and clamd (58 rules, in Holger's crun sandbox), both measured withheimdall profile probe-execover real workloads (real packages, real bundles, the EICAR test string). README: the seccomp row is Implemented for these.
Changed (breaking)
-
heimdall-core:
Policyhas a new fieldworkspace(struct literals addworkspace: None);KillReasonhas a new variantWorkspace. -
heimdall-core:
Policyhas a new fieldseccomp: Option<SeccompProfileRef>(serde defaultNone, not serialized whenNone): a struct literal must addseccomp: None(or use..Default::default()); JSON is unchanged. -
heimdall-seccomp:
ProfileErrorhas a new variantOci. -
heimdall-container:
spec::buildtakes a fourth argument (generated);ShimRequest/InsideReporthave a new fieldgenerated; a job killed by SIGSYS is refused with the new codeheimdall-seccomp-1instead ofheimdall-exit-2; a job that hit the pids limit is refusedheimdall-limit-pids(before: accepted when it exited 0,heimdall-exit-1otherwise); onheimdall-out-1/-2the runner emptiesoutput_dir, so the signed empty output list checks. -
heimdall-cli: a release
heimdall run --job wasmruns the guest in a worker process under the generatedheimdall-wasm-hostprofile by default (--seccomp none/HEIMDALL_SECCOMP=none: the old in-process run); its stdout gains aseccomp:line. -
heimdall-probe: generated
.rsprofiles are MIT-0 (generated output, not under Heimdall's AGPL), notAGPL-3.0-only; the checked-in profiles are regenerated with the new header. -
The crate
heimdall-profilesis renamednordisk-heimdall-profiles(lib nameheimdall_profilesunchanged) and is publishable: heimdall-container and heimdall-cli depend on it. Release 0.3.0 must publish it (before container and cli).
Security
- heimdall-container, heimdall-plain:
with_host_ids(base, count)on both runners maps container root to a host id range (also in 0.2.1, see there; forward-ported from release-0.2.x).
Added
-
heimdall-trace (new crate
nordisk-heimdall-trace, lane dyntrace, H32-DYNAMIC-TRACE): the dynamic-trace job. A package's install or import runs in a HeimdallRunner job with no network under heimdall-probe's learning filter; the in-job tracer (heimdall-trace, static, PID 1) records exec with argv, connect/bind/send-to-an-address (refused unless AF_UNIX), file calls with paths and flags (writes outside/tmpand into/outrefused) and the syscall set per program, and writes one canonical, size-boundedtrace.json(heimdall.trace.v1), signed through the job'sSignedResult.TraceJob,run_trace,verify_trace(fails closed toTraceOutcome::Unknown),diffagainst a baseline,image::with_tracer. No new job kind or policy flag. Kernel testsscripts/trace-tests.sh(pip wheel + import, setup.py connect / writes to /etc and /root/.ssh / exec sh, fork bomb, size bound, forged and bit-flipped traces, /out writes, a trace that could not be taken). -
heimdall-container: the fixed profile refuses
io_uring_setupwith EPERM instead of killing the job (node 22's libuv calls it at start whateverUV_USE_IO_URINGsays);io_uring_enterandio_uring_registerstill kill (42 kill calls). -
heimdall-trace: a trace job and a broker are refused together by name (
BROKER_REFUSAL, one seccomp listener per filter chain); every trace sayslistener: "heimdall-trace". -
heimdall-seccomp:
Launch::exec_by_path(start with a plainexecveunder the filter, noexecveatrule in the prelude) andprelude_for. -
heimdall-cli:
heimdall trace run|verify|diff. -
The job workspace (owner decision 2026-11-01; lane HEIM-GUNNAR): heimdall-core
Policy::workspace: Option<Workspace { bytes, exec }>(serde default, not serialized whenNone),WorkspaceKind,WORKSPACE_PATH(/work),WORKSPACE_TMPFS_MAX(1 GiB),KillReason::Workspace(wire tag 5),codes::LIMIT_WORKSPACE. heimdall-container:workspace(mount, host_dir, usage),confine::plan_with_workspace,LandlockPlan::read_write_exec,rights::read_write_exec,HeimdallConfig::allow_workspace_exec(default false),ShimRequest::workspace,ShimResponse::workspace_killed/workspace_bytes. docs/API.md "The job workspace". -
heimdall-scan (new crate
nordisk-heimdall-scan, lane scanguest): the static-scanner guest contract shared by guest and caller: the scan-input envelope (ScanInput,ScanInputRef,DataKind),Purl, the canonical findings document (Report, oneclean/findings/unknownverdict per check, fail closed),in_toto_statement(in-toto Statement v1,PREDICATE_TYPE), guest refusal codesheimdall-scan-1to-4. Release 0.3.0 must publish it. docs/API.md, "Static-scanner guests". -
guests/static-scan (lane scanguest): the static-scanner wasm guest on modgunn 0.0.8 (unchanged, from crates.io): behavior (install hooks, network at install, entropy, sensitive APIs; npm, PyPI sdists, wheel
.pth), typosquat, dependency confusion, registry (yanked, deprecated, retracted) and the malicious-package feed; a hostile-archive walker (gzip'd tar and its own strict zip reader: traversal names, link escapes, bombs, overlapping zips, CRC and size checks), with the guest also run in the real wasm host. -
heimdall-core:
SeccompProfileRef,ProfileSource(Workload,OciFile),Sha256Digest::to_prefixed,codes::SECCOMP_KILL(heimdall-seccomp-1). -
heimdall-seccomp:
oci::from_oci(an enforcing OCI seccomp JSON back into aProfile, fail closed) andlaunch::ExecSelf(install a filter on the calling process andexecveatthe program, the in-placeLaunch). -
heimdall-container:
genprof(resolve a profile reference; apply it inside the job on top of the fixed profile;Protection::Generated { profile_digest }only when the executor confirmed the install),HeimdallConfig::seccomp. -
heimdall-container: io limits (lane HEIM-GUNNAR):
iolimit(IoMax,IoDevice,IoRates,DevNum,resolve,apply,check),HeimdallConfig::io_max+sysfs_root,HeimdallRunner::io_max;ShimRequest::io_weight,ShimResponse::io_max/io_weight;scripts/io-tests.sh(rootful podman, a loop disk) runstests/io.rs. -
heimdall-run:
execute_job(a job without the key: the unsigned result);WasmJobRunnerrefuses a job naming a seccomp profile. -
heimdall-cli:
--seccomp generated|none,--seccomp-sha256, featuretest-hooks(tests only). -
heimdall-broker (new crate
nordisk-heimdall-broker, milestone 4, spec §4): the syscall broker. A seccomp USER_NOTIF supervisor (filter built with heimdall-seccomp's BPF builder, served with itsnotifAPI) that checks a worker'sopenat/openat2/open/creat,socket,connect,bind/listen/accept,io_uring_setup,MSG_FASTOPENsends andexecve/execveatagainst aBrokerPolicy(path prefixes with anAccessof read, read-write or write-only and aResolvekind; at most one fetch peer; exec deny / first only / allow-logged), opens the file or socket itself (openat2beneath the prefix, no magic links, no mount crossing) and injects the fd withSECCOMP_IOCTL_NOTIF_ADDFD; a fetch job's connect is done by the broker to the address it checked. Every decision is aBrokerEvent. Spec §10 fallbacks:Mode::CheckOnly(no ADDFD, with a warning) andMode::DenyOnly(no USER_NOTIF, fail closed).heimdall-broker-selftestis the kernel tests' worker. -
heimdall-container: the syscall broker in every job (
brokered,HeimdallConfig::broker: BrokerSettingAuto/Off). The executor installs the broker filter inside the container after Landlock and beforeexecve(and before a generated profile) and hands its listener to the shim, which serves it; the job's broker policy is never wider than its Landlock plan (/read but not/itself,/inandpolicy.readread,/outwrite-only,/tmpandpolicy.writeread-write,/procread, the five devices; no sockets; exec allowed and logged).JobResult.eventsholds abrokermode event, every decision, and a dropped count past 4096.Features.brokeris true only in full mode; check-only (no ADDFD, with a warning event) and deny-only (no USER_NOTIF, fail closed) per spec §10. A job whose promised broker was not installed or never served is a setup error, never signed.ShimRequest,InsideReportandShimResponsehave a new fieldbroker; the shim's response may be up to 16 MiB. A file the broker creates for a job (O_CREAT) belongs to the job's fs uid/gid (withwith_host_ids: the range's base), never to the broker; a file that already existed is never chowned. -
heimdall-container: fetch jobs.
policy.net = Some(NetAllow { peers })declares a set of TCP peers, eachNetPeer { host, addr, port }:hostis the name for the job's SNI and certificate check (never resolved by Heimdall),addrthe IP literal the caller resolved it to. At mostMAX_NET_PEERS(16); an empty set, a duplicate, a bad host, a non-IP addr or port 0 isInvalidJobby name (NetAllow::check). The job's network namespace stays empty; the broker creates each socket in the runner's namespace, connects it only to a declared address:port and injects it; every other socket, connect, bind or listen is refused and recorded. The job gets its peers as JSON inHEIMDALL_NET_PEERS. Redirects are the caller's business (a new job). A runner without the broker in full mode refuses fetch jobs.brokered::policytakes the peers' addresses,brokered::peers_envbuilds the variable. -
heimdall-core (breaking):
NetAllowis{ peers: Vec<NetPeer> }(was{ host, port }), withNetPeer,MAX_NET_PEERS,NetAllow::oneandNetAllow::check. heimdall-broker:BrokerPolicy::fetchis aVec<SocketAddr>(with_fetchadds one). -
heimdall-plain / heimdall-container: parallel jobs could fail to start with "failed to create dir <cgroup_root>/heimdall: File exists" (libcgroups 0.7.0 checks, then mkdirs each missing component); the runners create the jobs' parent cgroup themselves before the shim starts.
-
heimdall-plain / heimdall-container: a job's
execvecould failETXTBSY("Text file busy") when jobs ran in parallel: a shim forked by one thread inherited the write fd of a program another thread was still unpacking.heimdall_plain::forklock: unpacks holdwriting()(shared), both runners start their shim underspawning()(exclusive). The shim now reports anexecvefailure with its whole error chain (the errno). -
heimdall-container: the tests that prove nothing written into
/tmpcan be executed called the copy/tmp/bb, which busybox answers with "applet not found" whether or not the exec was allowed; they now call it/tmp/busybox, with a control run without Landlock that does execute it. -
heimdall-wasm: the host needs no
memfd_create(memory_init_cow(false),force_memory_init_memfd(false)): Heimdall's own container profile refuses it and a guest with data segments in a HeimdallRunner job trapped "cannot create a memfd". Also in heimdall-wasm 0.2.1 (release-0.2.x). -
heimdall-container:
io.weightwas handed to libcgroups 0.7.0 as an OCIblockIO.weight, whose u16 conversion overflows for every weight above 16: a debug shim panicked, a release shim wrote a wrongio.weight(7 for the default 50). The shim now writesio.weightitself and the runner checks it (only where the io controller is delegated; oden's rootless podman has none, so it was never exercised).spec::buildlost itsio_weightargument (breaking); anio_weightoutside 1..=10000 refuses to start. -
guests/verify-bundle: the guest modules no longer name where cargo found their dependencies (panic locations carried
CARGO_HOME/registry/src/…or the vendor path):tools/guest_build.rsremaps every package's directory to/stage/<name>-<version>(the release layout), so a plaincargo run --example diston any machine gives the bytes of the guests-0.2.1 release (job sha256 531157fe…, verifier 9d2b0082…, decoder 8fb70289…).
[0.2.1] - unreleased
A patch release of nordisk-heimdall-container and nordisk-heimdall-plain, built from the branch release-0.2.x (from the 0.2.0 commit 3e35142e). Bug fixes and one additive security option; nothing that compiled against 0.2.0 breaks. Every other crate stays 0.2.0; container 0.2.1 needs plain 0.2.1. Main's generated-seccomp work (Policy gains seccomp, heimdall-seccomp-1, the wasm worker, the MIT-0 header, nordisk-heimdall-profiles) is breaking and waits for 0.3.0.
Security
- heimdall-container, heimdall-plain: an opt-in host id range for container root,
HeimdallRunner::with_host_ids(base, count)andPlainYoukiRunner::with_host_ids(base, count)(new moduleheimdall_plain::idmap, typeHostIds, re-exported by heimdall-container). Before, container root always mapped to the runner's own uid, so under a runner that runs as host root (Holger's appliance, PID 1) every job's root was host root (capabilities dropped, the other layers still applied, but not rootless). With a range, container ids0..countmap to hostbase..base + count(one map line, no newuidmap); the unpacked root is shifted into it andinput_dir/output_dirare given tobase. Refused by name (host_ids,RunError::Unsupported) for an invalid range, a runner that is not root, or a range holding the runner's own uid or gid. Unset, nothing changes. A method on the runner, not a new config field, because a new pub field would break 0.2 struct literals.
Fixed
- heimdall-container: a job that hit the pids limit (a fork refused, a
maxin its cgroup'spids.events) is now refusedheimdall-limit-pidswhatever its exit (unless it died from SIGSYS). Before, the code existed in heimdall-core but was never produced: such a job was accepted when it exited 0 (its work may be incomplete) andheimdall-exit-1otherwise. Its own outputs stay listed, like after a non-zero exit (main 6c7dcb2, 5719e38). - heimdall-container: when the output scan refuses
output_dir(heimdall-out-1,heimdall-out-2) the runner now empties it (never following a symlink), so the signed empty output list and the directory agree andcheck_outputspasses; if emptying fails the run isRunError::Ioand nothing is signed (main 6c7dcb2, 5719e38). - heimdall-container:
RLIMIT_NPROCispids+ 1, a backstop one above the cgroup'spids.max, so the cgroup refuses a fork first and counts it inpids.events; withRLIMIT_NPROC=pidsthe rlimit refused first andheimdall-limit-pidsnever fired (main 9784dbc). - docs/API.md: the scan-worker job contract (main 89ce178) with both rules, and the codes table gains
heimdall-limit-pidsandguest-refused.
[0.2.0] - 2026-10-31
Built from main after bbfcc8ab (crates.io 0.1.0). Breaking for code built against 0.1 (see docs/API.md, "Upgrading from 0.1"): every crate is 0.2.0. Released: nordisk-heimdall-core, -plain, -wasm, -run, -cli and the new -container, -seccomp and -probe. Not released: heimdall-profiles (publish = false; the checked-in generated profiles, no published crate depends on it) and heimdall-broker (still being built).
Changed (breaking)
- heimdall-core:
JobKind::Wasmhas a second field,decoder: Option<GuestRef>: a match must writeJobKind::Wasm { module, .. }and a literaldecoder: None(or useJobKind::wasm(module)). Its JSON is unchanged when there is no decoder (serde defaultNone, not serialized whenNone) (RED edc2ed5, GREEN cadf095). - heimdall-core:
Limitshas a new fielddecoders(decoder instances a split wasm guest runs at once; defaultdefault_decoders()= min(cores, 8); serde default) (8f455d8). - heimdall-core:
JobResulthas a new fieldkernel: Option<KernelFeatures>: the kernel features the runner got (spec §10, "each signed result names them");Nonefor the wasm and plain runners. The signed wire is now format version 2 (DOMAIN_V2=heimdall.signed-result.v2\0, the canonical encoding gains a trailingkernelfield); a version 1 result from 0.1 is refused asUnsupportedVersion(1), andDOMAIN_V1is gone (RED 339aecf, GREEN 1ab87e0). - heimdall-run:
RunLimitshas a new fielddecoders,LIMIT_KEYShas 13 keys (the policy file keydecoders, 1..=64);WasmLoadErrorhas a new variantDecoder(edc2ed5, cadf095).
Added
-
The split verify-bundle guest (docs/GATLING-WASM.md): WIT worlds
decoder(decode-chunks, imports nothing) andverifier(jobdecode-chunks+digest-members); heimdall-wasmHost::load_decoder,Host::load_decoder_pinned,Host::load_split,Host::load_split_pinned,Decoder::decode,plan_units,RunLimits::decoders; the host drives fresh decoder instances (own memory, fuel, deadline) with gatling over N workers and checks every answer natively (count, length, BLAKE3); the host hashes the verifier's members natively in parallel (digest-members), so every member is hashed once; codesheimdall-wasm-13(a decoder instance answered falsely) andheimdall-bundle-7(the verifier refused an answer from outside); the fuzz targetdecoder_instance(RED 553de0e, GREEN 8f455d8; RED 85ba9a1, GREEN 17bb18a; 65b3a5b, b437857).
-
A job names its decoder module:
JobKind::Wasm { module, decoder: Some(GuestRef) }(JobKind::wasm_split), the decoder signed and pinned like every guest;heimdall_run::wasm::load_split_guest;WasmJobRunnerandheimdall run --decoder <wasm> [--decoder-pin-blake3 <hex>]run the split guest; a refused decoder saysdecoder refused: ...and nothing runs.guests/verify-bundle/tests/cli_split.rsproves split = single (exit code, signed verdict, outputs, files,verify-result) for good, tampered, OpenZL and garbage bundles through theheimdallbinary andWasmJobRunner(edc2ed5, cadf095). -
heimdall-container (new crate
nordisk-heimdall-container):HeimdallRunner, the protected native runner (spec §5, §10): a youki container per job with Landlock, a hand-reviewed seccomp profile, rlimits, cgroup v2 limits and the image verified at every start, fail-closed checks, the kernel report and strict mode; every result names itsKernelFeatures(RED 1a0d364, GREEN d1189c4, 5fc0e99, 1ab87e0). -
heimdall-seccomp (new crate
nordisk-heimdall-seccomp): seccomp profile types per workload/arch/libc, syscall tables (x86_64 + aarch64), a pure-Rust classic-BPF builder (arch check, x32 kill, 4096-instruction limit) and its interpreter, launch with a listener hand-off, the OCI seccomp JSON; enforce =KILL_PROCESS, audit =RET_LOGonly with a test token; the USER_NOTIF API the broker and the probe build on (RED 9265a59, GREEN fa6c809, 74c1aba). -
heimdall-probe (new crate
nordisk-heimdall-probe): learning mode by seccomp USER_NOTIF log-and-allow (no strace, no ptrace), argument facts, merge + minimise, deterministic codegen ofprofiles/<workload>/<arch>-<libc>.{rs,json,oci.json}, and the drift check naming the new call and the test that made it (RED 7de99b2, GREEN 3991355, 74c1aba). -
heimdall-cli:
heimdall profile probe|gen|check <workload>(16f0397, 9037bf1); the first generated profiles, the wasm host itself (profiles/heimdall-wasm-host, x86_64 glibc and musl), in the unpublished workspace crateheimdall-profiles(ffc5532, dcf0be3). -
heimdall-core:
KernelFeatures,JobKind::wasm,JobKind::wasm_split.
Fuel guidance
Fuel is counted per instance (docs/GATLING-WASM.md): a decoder instance burns about 93 fuel per decoded byte (a full 8 MiB unit about 780 M, within the default 1e9 of Limits), the split verifier about 94 per content byte (the default covers bundles up to about 10 MB of content), the single-instance guest about 211. For bigger bundles set fuel to at least 128 x the bundle's content bytes and keep wall as the real bound; the defaults are unchanged.
Other changes on main
- guests/verify-bundle: znippy's real writer test sized from its slice geometry (a93a507); OpenSSF answers and LICENSE text (c62f654, 7ad5190, a27696d, 27fe9ce, d57a007); the 0.1.0 attestation and README (1fc1247, e876b71).
[0.1.0] on crates.io - 2026-10-31
The five crates nordisk-heimdall-{core,plain,wasm,run,cli} 0.1.0, built from bbfcc8ab by dwarves release-build (SLSA v1 provenance, attestations/heimdall-0.1.0.intoto.jsonl) and published byte-identical. Changes after tag v0.1.0, up to bbfcc8ab:
Added
SECURITY.md,CONTRIBUTING.md, this changelog,deny.toml(cargo-deny: advisories, bans, licences, sources; no advisory fires and none is ignored), the CI imageci/ci.Containerfile(rustfmt, clippy, libseccomp, pinned nightly, cargo-fuzz and cargo-deny) anddocs/OPENSSF-ANSWERS.md(the OpenSSF Best Practices answers).- Two cargo-fuzz targets for heimdall-wasm:
wasm_guest_load(arbitrary bytes through the pin, the signature check and the compiler: refused without a panic, only a pinned and signed component loads) andwasm_emit_file(fuzzer-scriptedemit-filecalls checked against a model), with seeds (55f1e61). - The README: what Heimdall is, its users, the layers, the tuning process and the standards table with an honest status (61d215d, 485c440, 831999e, 5e11f93, 25387df).
Changed
-
scripts/fuzz.shruns in the CI imageheim-ci;ci/fuzz.Containerfileis gone (the CI image pins the same nightly and cargo-fuzz). -
The packages are renamed for crates.io to
nordisk-heimdall-core,nordisk-heimdall-wasm,nordisk-heimdall-plain,nordisk-heimdall-runandnordisk-heimdall-cli(heimdall-corethere is an unrelated project). The library names (heimdall_core, …) and the binary nameheimdallare unchanged; dependents writeheimdall-core = { package = "nordisk-heimdall-core", version = "0.1.0" }. The code of every crate is unchanged since v0.1.0, so each first crates.io release is 0.1.0. -
The workspace's path dependencies between the Heimdall crates also name their version (
0.1.0), so cargo-deny's wildcard ban holds and the crates can be published.
[0.1.0] - 2026-10-30
Tag v0.1.0 at 386a67f. The first release: the stable job API that Holger builds against, and an interim plain container runner. Consumed by git tag (see docs/API.md); not on crates.io yet, and not SLSA-attested yet.
Added
- heimdall-core: the job, policy, limits, result, verdict, broker-event and id types (6bbaaaf); the milestone 0 job API: the
JobRunnertrait,Jobwithoutput_dir,JobKind::Native { image, argv }andJobKind::Wasm,ImageRef,GuestRefwith its BLAKE3 pin,Limits(wall, memory, cpus, pids and the wasm and output limits),Verdict::Refused { code, reason },Output,ExitInfo,KillReason,Features,Protection,RunError,verify,check_outputsand the stable refusal codes (RED 27891c3, GREEN 65c0d0e). - heimdall-core: the canonical binary encoding v1, the per-boot Ed25519 key, the domain-separated
SignedResultwire (HMDL-SR, version 1), theVerifierwith a replay guard and job ticket, thePinnedKeytext record and the output check (RED 2cf6c45, GREEN 46cb44e); fuzz targets for the wire, the encoding and the pinned key text, with seeds replayed in the tests (RED b129417, GREEN 52fe299). - heimdall-wasm: a wasmtime 49 component host with the WIT interface
run(input, params) -> result<output, refusal>plusemit-file, and no WASI. A guest is checked before it is compiled: its.wasm.blake3pin and its.wasm.p7ssignature (CMS over the BLAKE3, chained to pinned roots); there is no unsigned mode. Fuel, an epoch timeout, memory and table totals, instance, table and memory counts and the stack are limited, and each limit is a named refusal with a stable code (heimdall-wasm-<n>) (RED 8b84508, GREEN 3b55be4, 9cc78be). - heimdall-run and heimdall-cli: the policy file (TOML v1, docs/POLICY.md), the input and output directory rules (docs/RUN.md),
heimdall run --job wasmandheimdall verify-resultwith a pinned key, a ticket and the output check, and stable exit codes (docs/CLI.md) (RED 98192d8, GREEN 7c2ebd1; RED 9216e61, GREEN 3410553; 1f5cddc); the wasm bridge to heimdall-wasm with end-to-end tests through theheimdallbinary and real signed guests (RED ff14bb7, GREEN 6b0c1d1);WasmJobRunnerbehind theJobRunnertrait (65c0d0e, f99a118). Fuzz targetspolicy_file,output_nameandverify_result_file. - heimdall-plain:
PlainYoukiRunner, an ordinary youki container (libcontainer =0.7.0) per job throughheimdall-plain-shim. The OCI image is verified and unpacked fresh at every start; the default seccomp profile (x86_64, no capabilities), cgroup v2 limits with no swap, user namespaces and no_new_privs; wall and memory kills; the features it reports are truthful (youki-plain, seccompDefault, no landlock, broker or wasm). Theyoukifeature is off by default;scripts/plain-tests.shruns the container tests (RED d55408e, GREEN 386a67f). - The CI script
scripts/ci.sh(fmt, clippy-D warnings, tests, cargo deny whendeny.tomlexists, fuzz smoke runs) (1f5cddc), the fuzz imageci/fuzz.Containerfileandscripts/fuzz.sh(2aeff70). - The specification docs/SPEC.md, docs/BUILD-THING.md (164d038) and the job API reference docs/API.md (386a67f).
- The licence: AGPL-3.0-only plus a commercial licence, and the CLA (a68bce5); the logo (f15cb7e).
Decided
- The signature covers Heimdall's canonical binary wire v1, not canonical JSON: it is deterministic by construction, domain-separated, length-prefixed and fuzzed. Callers call
verifyandcheck_outputsand never touch the bytes (docs/API.md).
Not in this release
heimdall run --job nativein the CLI, the syscall broker, Landlock, the generated per-program seccomp profiles and theverify-bundleguest (spec §12).