nx_artifact_id.nx
buildroot/runtime/nx_artifact_id.nx
about
nx_artifact_id.nx -- name a built artifact by its CONTENT so build and deploy can agree on WHICH BYTES.
THE RACE THIS CLOSES (measured 2026-07-30, and it shipped foreign bytes to production):
nx_sov_build_run writes _build/<t>.sov.elf. A separate step later copies that file to a staged name and
promotes it. ANY sibling session that rebuilds the same target in between OVERWRITES _build/<t>.sov.elf.
I built nx_mgmt_api at 578865, verified it, and by deploy time _build held a sibling's 579263 -- WHICH IS
WHAT WENT LIVE. I verified one artifact and shipped another, and nothing anywhere detected it.
It is a TOCTOU on the build output. The `build-<target>` lease does not cover it: that lease is released
when the COMPILE ends, so the whole build->deploy handoff is unprotected by design.
THE FIX IS THE CONTAINER-ECOSYSTEM ONE: DEPLOY A DIGEST, NOT A TAG. A path is a mutable tag; a
(size, content-hash) pair is an immutable name. Bind them and the race becomes DETECTABLE instead of
silent -- `verify` REFUSES rather than shipping whatever happens to be sitting at the path.
COMPOSES THE CANONICAL HASH. nx_fnv.nx's own header carries the cardinal law: "All other primitives
needing FNV-1a MUST import nx_fnv.nx ... never re-implement a hash inline." A tree-wide grep today found
~25 hand-rolled FNV variants ignoring exactly that, so this organ composes rather than adding a 26th.
FNV-1a is a FINGERPRINT, not a cryptographic seal -- it detects an ACCIDENTAL swap by a sibling build,
which is the measured failure. It is NOT a defence against a deliberately crafted collision; that rung is
sha256 via the signing path, and this file must not be described as more than it is.
VERBS
nx_artifact_id id <path> -> `ARTIFACT size=<n> fnv=<16hex> path=<p>`, exit 0
nx_artifact_id verify <path> <size> <fnvhex> -> exit 0 MATCH / exit 3 MISMATCH (prints BOTH) / exit 4 unreadable
license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
dependencies 2 imports · 0 importers
imports: nx_syscalls.nxnx_fnv.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 30 | const AI_CHUNK: i64 = 262144 |
| 31 | const AI_EXIT_MISMATCH: i64 = 3 |
| 32 | const AI_EXIT_UNREADABLE: i64 = 4 |
| 33 | const AI_EXIT_USAGE: i64 = 2 |
functions
| 35 | func ai_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } |
| 36 | func ai_w(s: *u8) -> i64 { sys_write(1, s, ai_len(s)); return 0 } |
| 37 | func ai_wn(v: i64) -> i64 |
| 51 | func ai_wx(v: i64) -> i64 |
| 64 | func ai_hexval(c: i64) -> i64 called by 1: ai_parse_hex |
| 70 | func ai_parse_hex(s: *u8) -> i64 |
| 83 | func ai_parse_dec(s: *u8) -> i64 |
| 101 | func ai_digest(path: *u8, out_size: *i64, out_hash: *i64) -> i64 |
| 122 | func main(argc: i64, argv: *i64) -> i64 |