code wiki / _hdl_build / nx_pkg.nx

nx_pkg.nx source

↩ module page · 740 lines · 38041 B

1// nx_pkg.nx -- THE SOVEREIGN CONTENT-ADDRESSED PACKAGE LANE (LR3 of /compare/lang), carrying the original 2// dependency-resolution self-test it grew from (verb `selftest`, still the NO-ARG DEFAULT so no existing 3// caller loses behaviour). 4// 5// WHY THIS EXISTS. The 2025 registry worms self-replicated across 500+ npm packages by exploiting exactly 6// two assumptions: install-time code execution, and blind trust that a NAME resolves to whatever `latest` 7// happens to be. Go answered with a module proxy, a signed transparency log (sumdb), and no install 8// scripts. This lane does not GUARD that attack surface, it REMOVES it: THE IDENTITY OF A MODULE IS THE 9// HASH OF ITS BYTES. There is no version negotiation, so there is no `latest` to poison. The resolve 10// primitive takes a digest, not a name, so there is nothing to squat. And resolution never interprets, 11// forks or executes one byte of module content -- it reads, verifies, and writes. 12// 13// THE ONE LAW: THE STORE PATH IS A HINT, THE HASH IS THE IDENTITY. 14// A blob at <store>/<digest> is RE-HASHED on every resolve and REFUSED if it does not hash to the digest 15// that was asked for. An attacker who can write the store cannot make it serve different bytes; the most 16// they achieve is a named, loud refusal. 17// 18// EXTENDS THE ESTATE'S EXISTING CONTENT-ADDRESSING, DOES NOT ADD A SECOND ONE: 19// sha256_digest (nx_sha256.nx) -- the same digest nx_filehash / nx_closurehash / nx_content_put use 20// sys_read_file (nx_syscalls.nx) -- sizes its buffer from the file, so a module CANNOT short-read 21// lcf_* (nx_lineconf_lib.nx) -- line-anchored conf, LCF_MISS -> REFUSE, never a silent default 22// There is no new hash, no new store format, no second ruler. 23// 24// VERBS (every path and policy value comes from a conf file; an absent key REFUSES, it never defaults): 25// nx_pkg cas_put <file> [conf] -> address a file by content; prints its digest 26// nx_pkg cas_resolve <digest64> <dest> [conf] -> THE CONTRACT PRIMITIVE (pkg_cas_resolve) 27// nx_pkg pin <name> <digest64> [conf] -> bind a logical name to a digest (THE TRUST BOUNDARY) 28// nx_pkg resolve <name> <dest> [conf] -> exact-match pinned name -> digest -> cas_resolve 29// nx_pkg logverify [conf] -> walk the hash-chained transparency journal 30// nx_pkg selftest -> the original dep-resolution teeth (T1..T4) 31// An UNRECOGNISED verb prints usage and exits 3. It never falls through to a permissive default -- the 32// route-mode defect (an unknown token silently selecting the permissive branch) is live in this estate 33// today and this is the one place it must not be repeated. 34// 35// WHAT THE TRANSPARENCY JOURNAL PROVES, STATED HONESTLY. Every put/resolve appends 36// <seq>|<op>|<digest>|<bytes>|<prev_chain>|<chain> with chain = sha256(prev|digest|bytes) 37// so a retroactive edit to any past row breaks every chain link after it, and an append REFUSES on a 38// broken chain rather than extending it. That is TAMPER-EVIDENCE against an editor who does not rewrite 39// the whole file. It is NOT Go's sumdb: there is one writer, no signature, no gossiped tree head and no 40// inclusion proofs, so an attacker holding write access to the entire journal can recompute it from 41// genesis and it will verify. Only a copy of an earlier chain head, held elsewhere, detects that. 42// 43// RESIDUAL -- WHAT THIS DOES NOT PROTECT AGAINST (read this before trusting the lane): 44// R1 PIN TIME. `pin` is the trust boundary. CAS proves you got the bytes you asked for; it can never 45// prove you asked for the right ones. A poisoned digest, pinned, resolves perfectly forever. 46// R2 WHOLE-JOURNAL REWRITE (above). Single writer, unwitnessed, unsigned. 47// R3 HONEST DELIVERY OF MALICIOUS CODE. Content addressing is integrity, never trustworthiness. 48// Zero-install-execution shrinks the blast radius to `it runs when you run it`; it does not make 49// the module safe. 50// R4 NO SIGNATURE LAYER IS WIRED HERE. nx_install_attest_signed.nx (Ed25519) exists in this estate and 51// is deliberately NOT composed yet -- naming the next rung beats claiming it. 52// R5 THIS IS THE PACKAGE LANE, NOT THE COMPILER FRONT END. nx_import.nx still resolves 53// `import \x22x.nx\x22` by filesystem name. Requiring a pinned digest at import time is a further rung. 54// R6 sha256 preimage/collision resistance is ASSUMED, not proven here. 55// 56// exit: 0 ok | 3 usage | 4 conf | 5 malformed-digest | 6 digest-absent | 7 digest-mismatch | 57// 8 dest-refused | 9 name-not-pinned | 10 pin-conflict | 11 chain-break | 12 io | 13 pin-ambiguous 58// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 59import "nx_syscalls.nx" 60import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 61import "nx_g_puts_lib.nx" 62import "nx_sha256.nx" 63import "nx_lineconf_lib.nx" 64 65// ---- named constants; no bare literal carries meaning ------------------------------------------- 66const PK_DIGEST_BYTES: i64 = 32 // SHA-256 output width, fixed by the algorithm 67const PK_SHAHEX: i64 = 64 // = PK_DIGEST_BYTES * 2, asserted at runtime below 68const PK_I64_DIGITS: i64 = 20 // widest decimal i64 incl sign, fixed by the type 69const PK_BAR: i64 = 124 // '|' field separator 70const PK_NL: i64 = 10 71const PK_SLASH: i64 = 47 72const PK_DOT: i64 = 46 73const PK_ASCII_0: i64 = 48 74const PK_ASCII_9: i64 = 57 75const PK_ASCII_a: i64 = 97 76const PK_ASCII_f: i64 = 102 77const PK_FD_OUT: i64 = 1 78const PK_FD_ERR: i64 = 2 79const PK_NFIELDS: i64 = 6 80const PK_F_SEQ: i64 = 0 81const PK_F_OP: i64 = 1 82const PK_F_DIG: i64 = 2 83const PK_F_BYTES: i64 = 3 84const PK_F_PREV: i64 = 4 85const PK_F_CHAIN: i64 = 5 86const PK_CONF_DEFAULT: *u8 = "knowledge/pkg_cas.conf" as *u8 87 88// refusal codes -- ONE owner for the rule NAME and ONE owner for the exit code, so a message and a 89// status can never drift apart. 90const PK_OK: i64 = 0 91const PK_R_MALFORMED: i64 = 1 92const PK_R_ABSENT: i64 = 2 93const PK_R_MISMATCH: i64 = 3 94const PK_R_DEST: i64 = 4 95const PK_R_IO: i64 = 5 96const PK_R_CHAIN: i64 = 6 97const PK_R_NOTPINNED: i64 = 7 98const PK_R_PINCONFLICT: i64 = 8 99const PK_R_PINAMBIG: i64 = 9 100const PK_R_CONF: i64 = 10 101 102const PK_EXIT_USAGE: i64 = 3 103const PK_EXIT_CONF: i64 = 4 104const PK_EXIT_MALFORMED: i64 = 5 105const PK_EXIT_ABSENT: i64 = 6 106const PK_EXIT_MISMATCH: i64 = 7 107const PK_EXIT_DEST: i64 = 8 108const PK_EXIT_NOTPINNED: i64 = 9 109const PK_EXIT_PINCONFLICT: i64 = 10 110const PK_EXIT_CHAIN: i64 = 11 111const PK_EXIT_IO: i64 = 12 112const PK_EXIT_PINAMBIG: i64 = 13 113 114func pk_rule(code: i64) -> *u8 { 115 if code == PK_R_MALFORMED { return "malformed-digest" as *u8 } 116 if code == PK_R_ABSENT { return "digest-absent" as *u8 } 117 if code == PK_R_MISMATCH { return "digest-mismatch" as *u8 } 118 if code == PK_R_DEST { return "dest-refused" as *u8 } 119 if code == PK_R_IO { return "io-error" as *u8 } 120 if code == PK_R_CHAIN { return "chain-break" as *u8 } 121 if code == PK_R_NOTPINNED { return "name-not-pinned" as *u8 } 122 if code == PK_R_PINCONFLICT { return "pin-conflict" as *u8 } 123 if code == PK_R_PINAMBIG { return "pin-ambiguous" as *u8 } 124 if code == PK_R_CONF { return "conf-missing-key" as *u8 } 125 return "unclassified-refusal" as *u8 126} 127func pk_exit_for(code: i64) -> i64 { 128 if code == PK_OK { return 0 } 129 if code == PK_R_MALFORMED { return PK_EXIT_MALFORMED } 130 if code == PK_R_ABSENT { return PK_EXIT_ABSENT } 131 if code == PK_R_MISMATCH { return PK_EXIT_MISMATCH } 132 if code == PK_R_DEST { return PK_EXIT_DEST } 133 if code == PK_R_IO { return PK_EXIT_IO } 134 if code == PK_R_CHAIN { return PK_EXIT_CHAIN } 135 if code == PK_R_NOTPINNED { return PK_EXIT_NOTPINNED } 136 if code == PK_R_PINCONFLICT { return PK_EXIT_PINCONFLICT } 137 if code == PK_R_PINAMBIG { return PK_EXIT_PINAMBIG } 138 if code == PK_R_CONF { return PK_EXIT_CONF } 139 return PK_EXIT_IO 140} 141 142// ---- small helpers (pk_ prefixed: a NishiLang import OVERRIDES a same-name local def) -------------- 143func pk_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 144func pk_eputs(s: *u8) -> i64 { sys_write(PK_FD_ERR, s, pk_slen(s)); return 0 } 145func pk_num(v: i64) -> i64 { nxi_out(v); return 0 } 146func pk_cat(d: *u8, o: i64, s: *u8) -> i64 { var p: i64 = o; var i: i64 = 0; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } d[p] = 0 as u8; return p } 147func pk_streq(a: *u8, b: *u8) -> i64 { 148 var i: i64 = 0 149 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 150 if b[i] != (0 as u8) { return 0 } 151 return 1 152} 153func pk_starts(p: *u8, pre: *u8) -> i64 { var i: i64 = 0; while pre[i] != (0 as u8) { if p[i] != pre[i] { return 0 } i = i + 1 } return 1 } 154func pk_dotdot(p: *u8) -> i64 { var i: i64 = 0; while p[i] != (0 as u8) { if (p[i] as i64) == PK_DOT { if (p[i+1] as i64) == PK_DOT { return 1 } } i = i + 1 } return 0 } 155func pk_atoi(s: *u8) -> i64 { 156 var v: i64 = 0 157 var i: i64 = 0 158 var seen: i64 = 0 159 while s[i] != (0 as u8) { 160 let c: i64 = s[i] as i64 161 if c < PK_ASCII_0 { return 0 - 1 } 162 if c > PK_ASCII_9 { return 0 - 1 } 163 v = v * 10 + (c - PK_ASCII_0) 164 seen = 1 165 i = i + 1 166 } 167 if seen == 0 { return 0 - 1 } 168 return v 169} 170func pk_exists(path: *u8) -> i64 { let fd: i64 = sys_openat_rd(path); if fd < 0 { return 0 } sys_close(fd); return 1 } 171// RUNTIME-COMPUTED syscall number: a literal passes through the rv64->x86 constant-translate path, so 172// storing it first is the documented escape hatch (same body as nx_mkdirp.mp_mkdir_one). 173func pk_mkdir(path: *u8) -> i64 { 174 let nbox: *i64 = sys_mmap(16) as *i64 175 nbox[0] = 258 176 let rc: i64 = __syscall(nbox[0], 0 - 100, path as i64, 0x1ed, 0, 0, 0) 177 sys_munmap(nbox as *u8, 16) 178 return rc 179} 180func pk_hex_into(dig: *u8, out: *u8) -> i64 { 181 let h: *u8 = "0123456789abcdef" as *u8 182 var i: i64 = 0 183 while i < PK_DIGEST_BYTES { 184 let v: i64 = dig[i] as i64 185 out[i*2] = h[(v >> 4) & 15] 186 out[i*2+1] = h[v & 15] 187 i = i + 1 188 } 189 out[PK_SHAHEX] = 0 as u8 190 return PK_SHAHEX 191} 192func pk_sha_hex(buf: *u8, n: i64, outhex: *u8) -> i64 { 193 let d: *u8 = sys_mmap(PK_DIGEST_BYTES + 8) 194 sha256_digest(buf, n, d) 195 pk_hex_into(d, outhex) 196 sys_munmap(d, PK_DIGEST_BYTES + 8) 197 return 0 198} 199// STRICT digest form: exactly PK_SHAHEX bytes, lowercase hex, NUL-terminated. Anything else is 200// MALFORMED and refuses -- an unparseable input must fail CLOSED, never select a permissive branch. 201func pk_is_hex64(s: *u8) -> i64 { 202 var i: i64 = 0 203 while i < PK_SHAHEX { 204 let c: i64 = s[i] as i64 205 var ok: i64 = 0 206 if c >= PK_ASCII_0 { if c <= PK_ASCII_9 { ok = 1 } } 207 if c >= PK_ASCII_a { if c <= PK_ASCII_f { ok = 1 } } 208 if ok == 0 { return 0 } 209 i = i + 1 210 } 211 if s[PK_SHAHEX] != (0 as u8) { return 0 } 212 return 1 213} 214// destination allowlist -- the same safe-root set nx_mkdirp and nx_content_put enforce. 215func pk_dest_ok(p: *u8) -> i64 { 216 if pk_dotdot(p) == 1 { return 0 } 217 if pk_starts(p, "knowledge/" as *u8) == 1 { return 1 } 218 if pk_starts(p, "sites/" as *u8) == 1 { return 1 } 219 if pk_starts(p, "world/" as *u8) == 1 { return 1 } 220 if pk_starts(p, "/tmp/" as *u8) == 1 { return 1 } 221 return 0 222} 223// <store>/<digest> ; buffer DERIVED from the inputs, never a guessed cap 224func pk_blob_path(store: *u8, dighex: *u8) -> *u8 { 225 let need: i64 = pk_slen(store) + 1 + PK_SHAHEX + 8 226 let b: *u8 = sys_mmap(need) 227 var o: i64 = pk_cat(b, 0, store) 228 b[o] = PK_SLASH as u8; o = o + 1 229 o = pk_cat(b, o, dighex) 230 return b 231} 232func pk_write_atomic(finalpath: *u8, buf: *u8, n: i64) -> i64 { 233 let need: i64 = pk_slen(finalpath) + 16 234 let tmp: *u8 = sys_mmap(need) 235 var o: i64 = pk_cat(tmp, 0, finalpath) 236 o = pk_cat(tmp, o, ".pkgtmp" as *u8) 237 let fd: i64 = sys_openat_wr(tmp, MODE_0644) 238 if fd < 0 { sys_munmap(tmp, need); return 0 - 1 } 239 var w: i64 = 0 240 while w < n { 241 let r: i64 = sys_write(fd, ((buf as i64) + w) as *u8, n - w) 242 if r <= 0 { sys_close(fd); sys_unlinkat(tmp); sys_munmap(tmp, need); return 0 - 1 } 243 w = w + r 244 } 245 sys_fsync(fd) 246 sys_close(fd) 247 if sys_renameat(tmp, finalpath) < 0 { sys_unlinkat(tmp); sys_munmap(tmp, need); return 0 - 1 } 248 sys_munmap(tmp, need) 249 return n 250} 251 252// ---- the hash-chained transparency journal -------------------------------------------------------- 253func pk_genesis(out: *u8) -> i64 { var i: i64 = 0; while i < PK_SHAHEX { out[i] = PK_ASCII_0 as u8; i = i + 1 } out[PK_SHAHEX] = 0 as u8; return PK_SHAHEX } 254// chain_i = sha256( prev | digest | bytes ). Input buffer size DERIVED from the field widths. 255func pk_chain_step(prevhex: *u8, dighex: *u8, nbytes: i64, outhex: *u8) -> i64 { 256 let need: i64 = PK_SHAHEX + 1 + PK_SHAHEX + 1 + PK_I64_DIGITS + 8 257 let b: *u8 = sys_mmap(need) 258 var o: i64 = pk_cat(b, 0, prevhex) 259 b[o] = PK_BAR as u8; o = o + 1 260 o = pk_cat(b, o, dighex) 261 b[o] = PK_BAR as u8; o = o + 1 262 o = ccz_cat_num(b, o, nbytes) 263 pk_sha_hex(b, o, outhex) 264 sys_munmap(b, need) 265 return 0 266} 267// field `idx` of the line starting at `st` (len `ln`) into out (cap bytes); returns length or -1 268func pk_field(buf: *u8, st: i64, ln: i64, idx: i64, out: *u8, cap: i64) -> i64 { 269 var f: i64 = 0 270 var i: i64 = 0 271 var w: i64 = 0 272 var started: i64 = 0 273 while i < ln { 274 let c: i64 = buf[st + i] as i64 275 if c == PK_BAR { f = f + 1; if f > idx { out[w] = 0 as u8; return w } } 276 else { if f == idx { if w < cap - 1 { out[w] = buf[st + i]; w = w + 1; started = 1 } } } 277 i = i + 1 278 } 279 out[w] = 0 as u8 280 if f < idx { return 0 - 1 } 281 if started == 0 { if w == 0 { return 0 - 1 } } 282 return w 283} 284// Walk the whole journal from genesis. Returns -1 when every link verifies, else the 0-based index of 285// the FIRST row that does not. entries[0] receives the row count, head[] the final chain value. 286// A row is bad when: it has fewer than PK_NFIELDS fields, its seq is out of order, its prev is not the 287// running chain, or its chain is not sha256(prev|digest|bytes). 288func pk_log_verify(logpath: *u8, entries: *i64, head: *u8) -> i64 { 289 pk_genesis(head) 290 entries[0] = 0 291 let lp: *i64 = sys_mmap(16) as *i64 292 let buf: *u8 = sys_read_file(logpath, lp) 293 if (buf as i64) == 0 { return 0 - 1 } // absent journal == empty chain, not a break 294 let n: i64 = lp[0] 295 let fcap: i64 = n + 2 // a field can never exceed the whole file: DERIVED 296 let fdig: *u8 = sys_mmap(fcap) 297 let fprev: *u8 = sys_mmap(fcap) 298 let fchain: *u8 = sys_mmap(fcap) 299 let fbytes: *u8 = sys_mmap(fcap) 300 let fseq: *u8 = sys_mmap(fcap) 301 let calc: *u8 = sys_mmap(PK_SHAHEX + 8) 302 var st: i64 = 0 303 var idx: i64 = 0 304 var bad: i64 = 0 - 1 305 while st < n { 306 var e: i64 = st 307 while e < n { if (buf[e] as i64) == PK_NL { break } e = e + 1 } 308 let ln: i64 = e - st 309 if ln > 0 { 310 if bad < 0 { 311 var rowok: i64 = 1 312 if pk_field(buf, st, ln, PK_F_SEQ, fseq, fcap) < 0 { rowok = 0 } 313 if pk_field(buf, st, ln, PK_F_DIG, fdig, fcap) < 0 { rowok = 0 } 314 if pk_field(buf, st, ln, PK_F_BYTES, fbytes, fcap) < 0 { rowok = 0 } 315 if pk_field(buf, st, ln, PK_F_PREV, fprev, fcap) < 0 { rowok = 0 } 316 if pk_field(buf, st, ln, PK_F_CHAIN, fchain, fcap) < 0 { rowok = 0 } 317 if rowok == 1 { 318 if pk_atoi(fseq) != idx { rowok = 0 } 319 if pk_streq(fprev, head) == 0 { rowok = 0 } 320 let nb: i64 = pk_atoi(fbytes) 321 if nb < 0 { rowok = 0 } 322 if rowok == 1 { 323 pk_chain_step(fprev, fdig, nb, calc) 324 if pk_streq(calc, fchain) == 0 { rowok = 0 } 325 } 326 } 327 if rowok == 0 { bad = idx } else { pk_cat(head, 0, fchain) } 328 } 329 idx = idx + 1 330 } 331 st = e + 1 332 } 333 entries[0] = idx 334 sys_free_file(buf, n) 335 return bad 336} 337// Append one row. REFUSES on a broken chain rather than extending it: you cannot append to a tampered 338// journal. Returns PK_OK or PK_R_CHAIN / PK_R_IO. 339func pk_log_append(logpath: *u8, op: *u8, dighex: *u8, nbytes: i64) -> i64 { 340 let entries: *i64 = sys_mmap(16) as *i64 341 let head: *u8 = sys_mmap(PK_SHAHEX + 8) 342 let bad: i64 = pk_log_verify(logpath, entries, head) 343 if bad >= 0 { return PK_R_CHAIN } 344 let chain: *u8 = sys_mmap(PK_SHAHEX + 8) 345 pk_chain_step(head, dighex, nbytes, chain) 346 let need: i64 = PK_I64_DIGITS + 1 + pk_slen(op) + 1 + PK_SHAHEX + 1 + PK_I64_DIGITS + 1 + PK_SHAHEX + 1 + PK_SHAHEX + 8 347 let ln: *u8 = sys_mmap(need) 348 var o: i64 = ccz_cat_num(ln, 0, entries[0]) 349 ln[o] = PK_BAR as u8; o = o + 1 350 o = pk_cat(ln, o, op) 351 ln[o] = PK_BAR as u8; o = o + 1 352 o = pk_cat(ln, o, dighex) 353 ln[o] = PK_BAR as u8; o = o + 1 354 o = ccz_cat_num(ln, o, nbytes) 355 ln[o] = PK_BAR as u8; o = o + 1 356 o = pk_cat(ln, o, head) 357 ln[o] = PK_BAR as u8; o = o + 1 358 o = pk_cat(ln, o, chain) 359 ln[o] = PK_NL as u8; o = o + 1 360 let fd: i64 = sys_openat_append(logpath, MODE_0644) 361 if fd < 0 { sys_munmap(ln, need); return PK_R_IO } 362 sys_write(fd, ln, o) 363 sys_close(fd) 364 sys_munmap(ln, need) 365 return PK_OK 366} 367 368// ================= THE CONTRACT PRIMITIVE ========================================================= 369// pkg_cas_resolve -- resolve a module BY CONTENT HASH. 370// 371// It takes a DIGEST, never a name: there is no version negotiation and nothing to typosquat at this 372// layer. The blob's location is only a hint; its bytes are re-hashed and compared before anything is 373// written, so a poisoned store yields a named refusal, never different code. 374// 375// IT PERFORMS NO EXECUTION AND NO INTERPRETATION. The module's bytes are read, hashed and written. 376// They are never parsed, never scanned for a manifest, never handed to fork/execve. There is no 377// install hook to honour because there is no code path that could honour one. 378// 379// outbytes[0] receives the byte count on success. Returns PK_OK or a PK_R_* code whose rule NAME comes 380// from pk_rule() and whose exit code comes from pk_exit_for() -- one owner each, so they cannot drift. 381func pkg_cas_resolve(store: *u8, dighex: *u8, dest: *u8, outbytes: *i64) -> i64 { 382 outbytes[0] = 0 383 if pk_is_hex64(dighex) == 0 { return PK_R_MALFORMED } 384 if pk_dest_ok(dest) == 0 { return PK_R_DEST } 385 let bp: *u8 = pk_blob_path(store, dighex) 386 let lp: *i64 = sys_mmap(16) as *i64 387 let buf: *u8 = sys_read_file(bp, lp) 388 if (buf as i64) == 0 { return PK_R_ABSENT } 389 let n: i64 = lp[0] 390 let got: *u8 = sys_mmap(PK_SHAHEX + 8) 391 pk_sha_hex(buf, n, got) 392 // THE ONE LAW: the store path is a hint, the hash is the identity. 393 if pk_streq(got, dighex) == 0 { sys_free_file(buf, n); return PK_R_MISMATCH } 394 if pk_write_atomic(dest, buf, n) < 0 { sys_free_file(buf, n); return PK_R_IO } 395 sys_free_file(buf, n) 396 outbytes[0] = n 397 return PK_OK 398} 399 400// ---- the pin lockfile: <name>|<digest>\n ---------------------------------------------------------- 401// EXACT name match at a line start, terminated by the separator. A lookalike name shares no row with 402// the real one, and a prefix cannot match. Returns 1 found / 0 not found / 2 AMBIGUOUS (the same name 403// declared twice) -- a declaration file that permits two answers for one key is a coin flip wearing a 404// schema, so a duplicate REFUSES instead of picking first-match or last-match. 405func pk_lock_lookup(buf: *u8, n: i64, name: *u8, outdig: *u8) -> i64 { 406 let nl: i64 = pk_slen(name) 407 var st: i64 = 0 408 var hits: i64 = 0 409 while st < n { 410 var e: i64 = st 411 while e < n { if (buf[e] as i64) == PK_NL { break } e = e + 1 } 412 let ln: i64 = e - st 413 if ln > nl { 414 var same: i64 = 1 415 var i: i64 = 0 416 while i < nl { if buf[st + i] != name[i] { same = 0; i = nl } else { i = i + 1 } } 417 if same == 1 { 418 if (buf[st + nl] as i64) == PK_BAR { 419 var w: i64 = 0 420 var j: i64 = st + nl + 1 421 while j < e { if w < PK_SHAHEX { outdig[w] = buf[j]; w = w + 1 } j = j + 1 } 422 outdig[w] = 0 as u8 423 hits = hits + 1 424 } 425 } 426 } 427 st = e + 1 428 } 429 if hits == 0 { return 0 } 430 if hits > 1 { return 2 } 431 return 1 432} 433 434// ---- conf: every path is data, an absent key REFUSES -------------------------------------------- 435struct PkConf { store: *u8, lock: *u8, log: *u8, ok: i64 } 436func pk_conf_load(confpath: *u8) -> *PkConf { 437 let c: *PkConf = sys_mmap(64) as *PkConf 438 c.ok = 0 439 let lp: *i64 = sys_mmap(16) as *i64 440 let buf: *u8 = sys_read_file(confpath, lp) 441 if (buf as i64) == 0 { return c } 442 let n: i64 = lp[0] 443 let cap: i64 = n + 2 // a value can never exceed the file: DERIVED, not capped 444 c.store = sys_mmap(cap) 445 c.lock = sys_mmap(cap) 446 c.log = sys_mmap(cap) 447 var good: i64 = 1 448 if lcf_str(buf, n, "store_root" as *u8, c.store, cap) < 0 { good = 0 } 449 if lcf_str(buf, n, "lock_path" as *u8, c.lock, cap) < 0 { good = 0 } 450 if lcf_str(buf, n, "log_path" as *u8, c.log, cap) < 0 { good = 0 } 451 c.ok = good 452 return c 453} 454 455// ---- ORIGINAL DEPENDENCY-RESOLUTION SELF-TEST (unchanged behaviour, now the `selftest` verb) ------- 456func g_pn(v: i64) -> i64 { nxi_out(v); return 0 } 457func ck(name: *u8, c: i64) -> i64 { if c==1 { g_puts(" PASS " as *u8) } else { g_puts(" FAIL " as *u8) } g_puts(name); g_puts("\n" as *u8); return c } 458func slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 459func psum(s: *u8) -> i64 { var acc: i64=0; var i: i64=0; while s[i]!=(0 as u8){ acc=acc+(s[i] as i64); i=i+1 } return acc } 460 461const MAXD: i64 = 4 462 463func pkg_needed(target: i64, ndep: *i64, dep: *i64, N: i64, needed: *i64) -> i64 { 464 var i: i64=0; while i<N { needed[i]=0; i=i+1 } 465 needed[target]=1; var changed: i64=1 466 while changed==1 { changed=0; var u: i64=0; while u<N { if needed[u]==1 { var k: i64=0; while k<ndep[u] { let dpk: i64=dep[u*MAXD+k]; if needed[dpk]==0 { needed[dpk]=1; changed=1 } k=k+1 } } u=u+1 } } 467 return 0 468} 469func pkg_topo(ndep: *i64, dep: *i64, N: i64, order: *i64) -> i64 { 470 let indeg: *i64 = sys_mmap(N*8) as *i64; let done: *i64 = sys_mmap(N*8) as *i64 471 var i: i64=0; while i<N { indeg[i]=ndep[i]; done[i]=0; i=i+1 } 472 var oc: i64=0; var changed: i64=1 473 while changed==1 { changed=0; var u: i64=0 474 while u<N { if done[u]==0 { if indeg[u]==0 { 475 order[oc]=u; oc=oc+1; done[u]=1; changed=1 476 var v: i64=0; while v<N { if done[v]==0 { var k: i64=0; while k<ndep[v] { if dep[v*MAXD+k]==u { indeg[v]=indeg[v]-1 } k=k+1 } } v=v+1 } } } 477 u=u+1 } } 478 return oc 479} 480func pkg_verify(content: *u8, expected: i64) -> i64 { if psum(content)==expected { return 1 } return 0 } 481 482func pk_selftest() -> i64 { 483 g_puts("nx_pkg selftest (dependency resolution: transitive closure + topo order + integrity)\n" as *u8) 484 var pass: i64=0; var total: i64=0 485 let N: i64=6 486 let ndep: *i64 = sys_mmap(N*8) as *i64; let dep: *i64 = sys_mmap(N*MAXD*8) as *i64 487 let nm: *i64 = sys_mmap(N*8) as *i64; let ct: *i64 = sys_mmap(N*8) as *i64; let sha: *i64 = sys_mmap(N*8) as *i64 488 nm[0]=("syscalls" as *u8) as i64; nm[1]=("alloc" as *u8) as i64; nm[2]=("net" as *u8) as i64; nm[3]=("blockfs" as *u8) as i64; nm[4]=("tls" as *u8) as i64; nm[5]=("shell" as *u8) as i64 489 ct[0]=("pkg:syscalls:v1:payload" as *u8) as i64; ct[1]=("pkg:alloc:v1:payload" as *u8) as i64; ct[2]=("pkg:net:v1:payload" as *u8) as i64; ct[3]=("pkg:blockfs:v1:payload" as *u8) as i64; ct[4]=("pkg:tls:v1:payload" as *u8) as i64; ct[5]=("pkg:shell:v1:payload" as *u8) as i64 490 ndep[0]=0 491 ndep[1]=1; dep[1*MAXD+0]=0 492 ndep[2]=1; dep[2*MAXD+0]=0 493 ndep[3]=2; dep[3*MAXD+0]=1; dep[3*MAXD+1]=0 494 ndep[4]=1; dep[4*MAXD+0]=2 495 ndep[5]=2; dep[5*MAXD+0]=3; dep[5*MAXD+1]=1 496 var i: i64=0; while i<N { sha[i]=psum(ct[i] as *u8); i=i+1 } 497 498 let needed: *i64 = sys_mmap(N*8) as *i64; pkg_needed(5, ndep, dep, N, needed) 499 let order: *i64 = sys_mmap(N*8) as *i64; let oc: i64 = pkg_topo(ndep, dep, N, order) 500 g_puts(" install shell -> order: "); var nneed: i64=0; var ii: i64=0 501 while ii<oc { if needed[order[ii]]==1 { g_puts(nm[order[ii]] as *u8); g_puts(" " as *u8); nneed=nneed+1 } ii=ii+1 } g_puts("\n" as *u8) 502 let posneeded: *i64 = sys_mmap(N*8) as *i64; var pp: i64=0; var seq: i64=0 503 while pp<oc { if needed[order[pp]]==1 { posneeded[order[pp]]=seq; seq=seq+1 } pp=pp+1 } 504 var depok: i64=1; var u: i64=0 505 while u<N { if needed[u]==1 { var k: i64=0; while k<ndep[u] { if posneeded[dep[u*MAXD+k]] >= posneeded[u] { depok=0 } k=k+1 } } u=u+1 } 506 var t1: i64=0; if nneed==4 { if needed[2]==0 { if needed[4]==0 { if depok==1 { t1=1 } } } } 507 pass=pass+ck("T1: install 'shell' -> only transitive deps needed (4: syscalls/alloc/blockfs/shell), deps-ordered" as *u8, t1); total=total+1 508 509 var allok: i64=1; i=0; while i<N { if pkg_verify(ct[i] as *u8, sha[i])==0 { allok=0 } i=i+1 } 510 var t2: i64=0; if allok==1 { t2=1 } 511 pass=pass+ck("T2: every package verifies (content sum == manifest sum)" as *u8, t2); total=total+1 512 513 var t3: i64=0; if pkg_verify(ct[3] as *u8, sha[3] ^ 0x55)==0 { if pkg_verify(ct[3] as *u8, sha[3])==1 { t3=1 } } 514 pass=pass+ck("T3 (teeth): a checksum MISMATCH is rejected while the true one passes" as *u8, t3); total=total+1 515 516 let cn: i64=2; let cnd: *i64 = sys_mmap(cn*8) as *i64; let cdp: *i64 = sys_mmap(cn*MAXD*8) as *i64; let cord: *i64 = sys_mmap(cn*8) as *i64 517 cnd[0]=1; cdp[0*MAXD+0]=1; cnd[1]=1; cdp[1*MAXD+0]=0 518 let coc: i64 = pkg_topo(cnd, cdp, cn, cord) 519 var t4: i64=0; if coc<cn { t4=1 } 520 pass=pass+ck("T4 (teeth): a dependency cycle is detected (cannot resolve install order)" as *u8, t4); total=total+1 521 522 // T5: the SHA-256 lane the CAS resolver actually uses -- the byte-sum above is a toy and is kept 523 // only so the original teeth stay runnable. This asserts the real width the whole lane depends on. 524 let probe: *u8 = sys_mmap(PK_SHAHEX + 8) 525 pk_sha_hex("abc" as *u8, 3, probe) 526 var t5: i64=0 527 if pk_is_hex64(probe) == 1 { if PK_SHAHEX == PK_DIGEST_BYTES * 2 { t5 = 1 } } 528 pass=pass+ck("T5: the CAS digest lane emits a well-formed sha256 hex of the declared width" as *u8, t5); total=total+1 529 530 var okall: i64=0; if pass==total { okall=1 } 531 g_puts("---- nx_pkg selftest: passed "); g_pn(pass); g_puts(" / "); g_pn(total); g_puts(" ----\n" as *u8) 532 if okall==1 { g_puts("verdict=GREEN\n" as *u8); return 0 } 533 g_puts("verdict=RED\n" as *u8); return 1 534} 535 536func pk_usage() -> i64 { 537 pk_eputs("usage: nx_pkg <verb> ...\n" as *u8) 538 pk_eputs(" cas_put <file> [conf]\n" as *u8) 539 pk_eputs(" cas_resolve <digest64> <dest> [conf]\n" as *u8) 540 pk_eputs(" pin <name> <digest64> [conf]\n" as *u8) 541 pk_eputs(" resolve <name> <dest> [conf]\n" as *u8) 542 pk_eputs(" logverify [conf]\n" as *u8) 543 pk_eputs(" selftest\n" as *u8) 544 pk_eputs("an unrecognised verb REFUSES here; it never falls through to a permissive default.\n" as *u8) 545 // Every refusal names a rule AND carries a distinct status, so a caller can branch on the reason 546 // rather than on the fact of refusal. MEASURED 2026-08-25 by a mutation of the pin lookup: with the 547 // typosquat guard disabled the lookalike name was STILL REFUSED -- as malformed-digest, exit 5 -- 548 // and a test that only asked `was it refused?` would have scored that broken build a PASS. 549 pk_eputs("exit: 0 ok | 3 usage/unknown-verb | 4 conf-missing-key | 5 malformed-digest | 6 digest-absent\n" as *u8) 550 pk_eputs(" 7 digest-mismatch | 8 dest-refused | 9 name-not-pinned | 10 pin-conflict | 11 chain-break | 12 io | 13 pin-ambiguous\n" as *u8) 551 return PK_EXIT_USAGE 552} 553func pk_refuse(prefix: *u8, code: i64) -> i64 { 554 g_puts(prefix); g_puts(" rule=" as *u8); g_puts(pk_rule(code)); g_puts("\n" as *u8) 555 return pk_exit_for(code) 556} 557 558func main(argc: i64, argv: *i64) -> i64 { 559 // NO ARGS = the original self-test, byte-for-byte the behaviour this organ shipped with. 560 if argc < 2 { let rc: i64 = pk_selftest(); sys_exit(rc); return rc } 561 let verb: *u8 = argv[1] as *u8 562 if pk_streq(verb, "selftest" as *u8) == 1 { let rc: i64 = pk_selftest(); sys_exit(rc); return rc } 563 564 // THE VERB IS VALIDATED BEFORE ANYTHING ELSE IS TOUCHED. Measured 2026-08-25 by this organ's own 565 // gate: an unrecognised verb used to fall through to the conf loader and refuse with exit 4 566 // (conf-missing-key), so a TYPO reported as a CONFIGURATION FAULT and sent the reader at the wrong 567 // subject entirely. A refusal must name the conjunct that actually fired. 568 var known: i64 = 0 569 if pk_streq(verb, "cas_put" as *u8) == 1 { known = 1 } 570 if pk_streq(verb, "cas_resolve" as *u8) == 1 { known = 1 } 571 if pk_streq(verb, "pin" as *u8) == 1 { known = 1 } 572 if pk_streq(verb, "resolve" as *u8) == 1 { known = 1 } 573 if pk_streq(verb, "logverify" as *u8) == 1 { known = 1 } 574 if known == 0 { 575 pk_eputs("PKG-REFUSED rule=unknown-verb verb=" as *u8); pk_eputs(verb); pk_eputs("\n" as *u8) 576 let u0: i64 = pk_usage(); sys_exit(u0); return u0 577 } 578 579 // which conf this run reads is positional per verb; a non-default run therefore touches ONLY the 580 // store and journal its own arguments name -- it can never write the production journal. 581 var confpath: *u8 = PK_CONF_DEFAULT 582 if pk_streq(verb, "cas_put" as *u8) == 1 { if argc >= 4 { confpath = argv[3] as *u8 } } 583 if pk_streq(verb, "cas_resolve" as *u8) == 1 { if argc >= 5 { confpath = argv[4] as *u8 } } 584 if pk_streq(verb, "pin" as *u8) == 1 { if argc >= 5 { confpath = argv[4] as *u8 } } 585 if pk_streq(verb, "resolve" as *u8) == 1 { if argc >= 5 { confpath = argv[4] as *u8 } } 586 if pk_streq(verb, "logverify" as *u8) == 1 { if argc >= 3 { confpath = argv[2] as *u8 } } 587 588 let cf: *PkConf = pk_conf_load(confpath) 589 if cf.ok == 0 { 590 g_puts("PKG-REFUSED rule=" as *u8); g_puts(pk_rule(PK_R_CONF)) 591 g_puts(" conf=" as *u8); g_puts(confpath) 592 g_puts(" (needs store_root, lock_path, log_path -- an absent key REFUSES, it never defaults)\n" as *u8) 593 sys_exit(PK_EXIT_CONF); return PK_EXIT_CONF 594 } 595 596 // ---- cas_put --------------------------------------------------------------------------------- 597 if pk_streq(verb, "cas_put" as *u8) == 1 { 598 if argc < 3 { let u: i64 = pk_usage(); sys_exit(u); return u } 599 let src: *u8 = argv[2] as *u8 600 let lp: *i64 = sys_mmap(16) as *i64 601 let buf: *u8 = sys_read_file(src, lp) 602 if (buf as i64) == 0 { let r: i64 = pk_refuse("PKG-CAS-PUT-REFUSED" as *u8, PK_R_IO); sys_exit(r); return r } 603 let n: i64 = lp[0] 604 let dig: *u8 = sys_mmap(PK_SHAHEX + 8) 605 pk_sha_hex(buf, n, dig) 606 pk_mkdir(cf.store) 607 let bp: *u8 = pk_blob_path(cf.store, dig) 608 var isnew: i64 = 1 609 if pk_exists(bp) == 1 { isnew = 0 } 610 if pk_write_atomic(bp, buf, n) < 0 { sys_free_file(buf, n); let r: i64 = pk_refuse("PKG-CAS-PUT-REFUSED" as *u8, PK_R_IO); sys_exit(r); return r } 611 sys_free_file(buf, n) 612 let lg: i64 = pk_log_append(cf.log, "put" as *u8, dig, n) 613 if lg != PK_OK { let r: i64 = pk_refuse("PKG-CAS-PUT-REFUSED" as *u8, lg); sys_exit(r); return r } 614 g_puts("PKG-CAS-PUT ok digest=" as *u8); g_puts(dig) 615 g_puts(" bytes=" as *u8); pk_num(n) 616 g_puts(" new=" as *u8); pk_num(isnew) 617 g_puts(" store=" as *u8); g_puts(cf.store); g_puts("\n" as *u8) 618 sys_exit(0); return 0 619 } 620 621 // ---- cas_resolve : THE CONTRACT PRIMITIVE ---------------------------------------------------- 622 if pk_streq(verb, "cas_resolve" as *u8) == 1 { 623 if argc < 4 { let u: i64 = pk_usage(); sys_exit(u); return u } 624 let dig: *u8 = argv[2] as *u8 625 let dest: *u8 = argv[3] as *u8 626 let nb: *i64 = sys_mmap(16) as *i64 627 let code: i64 = pkg_cas_resolve(cf.store, dig, dest, nb) 628 if code != PK_OK { 629 g_puts("PKG-CAS-REFUSED rule=" as *u8); g_puts(pk_rule(code)) 630 g_puts(" digest=" as *u8); g_puts(dig) 631 g_puts(" dest=" as *u8); g_puts(dest); g_puts("\n" as *u8) 632 let r: i64 = pk_exit_for(code); sys_exit(r); return r 633 } 634 let lg: i64 = pk_log_append(cf.log, "resolve" as *u8, dig, nb[0]) 635 if lg != PK_OK { let r: i64 = pk_refuse("PKG-CAS-REFUSED" as *u8, lg); sys_exit(r); return r } 636 g_puts("PKG-CAS-RESOLVE ok digest=" as *u8); g_puts(dig) 637 g_puts(" bytes=" as *u8); pk_num(nb[0]) 638 g_puts(" dest=" as *u8); g_puts(dest); g_puts("\n" as *u8) 639 sys_exit(0); return 0 640 } 641 642 // ---- pin : THE TRUST BOUNDARY ---------------------------------------------------------------- 643 if pk_streq(verb, "pin" as *u8) == 1 { 644 if argc < 4 { let u: i64 = pk_usage(); sys_exit(u); return u } 645 let name: *u8 = argv[2] as *u8 646 let dig: *u8 = argv[3] as *u8 647 if pk_is_hex64(dig) == 0 { let r: i64 = pk_refuse("PKG-PIN-REFUSED" as *u8, PK_R_MALFORMED); sys_exit(r); return r } 648 let lp: *i64 = sys_mmap(16) as *i64 649 let lb: *u8 = sys_read_file(cf.lock, lp) 650 var have: i64 = 0 651 let cur: *u8 = sys_mmap(PK_SHAHEX + 8) 652 cur[0] = 0 as u8 653 if (lb as i64) != 0 { have = pk_lock_lookup(lb, lp[0], name, cur) } 654 if have == 2 { let r: i64 = pk_refuse("PKG-PIN-REFUSED" as *u8, PK_R_PINAMBIG); sys_exit(r); return r } 655 if have == 1 { 656 if pk_streq(cur, dig) == 1 { 657 g_puts("PKG-PIN ok name=" as *u8); g_puts(name); g_puts(" digest=" as *u8); g_puts(dig); g_puts(" new=0\n" as *u8) 658 sys_exit(0); return 0 659 } 660 g_puts("PKG-PIN-REFUSED rule=" as *u8); g_puts(pk_rule(PK_R_PINCONFLICT)) 661 g_puts(" name=" as *u8); g_puts(name) 662 g_puts(" pinned=" as *u8); g_puts(cur) 663 g_puts(" requested=" as *u8); g_puts(dig); g_puts("\n" as *u8) 664 sys_exit(PK_EXIT_PINCONFLICT); return PK_EXIT_PINCONFLICT 665 } 666 let need: i64 = pk_slen(name) + PK_SHAHEX + 8 667 let row: *u8 = sys_mmap(need) 668 var o: i64 = pk_cat(row, 0, name) 669 row[o] = PK_BAR as u8; o = o + 1 670 o = pk_cat(row, o, dig) 671 row[o] = PK_NL as u8; o = o + 1 672 let fd: i64 = sys_openat_append(cf.lock, MODE_0644) 673 if fd < 0 { let r: i64 = pk_refuse("PKG-PIN-REFUSED" as *u8, PK_R_IO); sys_exit(r); return r } 674 sys_write(fd, row, o) 675 sys_close(fd) 676 g_puts("PKG-PIN ok name=" as *u8); g_puts(name); g_puts(" digest=" as *u8); g_puts(dig); g_puts(" new=1\n" as *u8) 677 sys_exit(0); return 0 678 } 679 680 // ---- resolve <name> : exact-match pin, then the digest lane ---------------------------------- 681 if pk_streq(verb, "resolve" as *u8) == 1 { 682 if argc < 4 { let u: i64 = pk_usage(); sys_exit(u); return u } 683 let name: *u8 = argv[2] as *u8 684 let dest: *u8 = argv[3] as *u8 685 let lp: *i64 = sys_mmap(16) as *i64 686 let lb: *u8 = sys_read_file(cf.lock, lp) 687 let dig: *u8 = sys_mmap(PK_SHAHEX + 8) 688 dig[0] = 0 as u8 689 var have: i64 = 0 690 if (lb as i64) != 0 { have = pk_lock_lookup(lb, lp[0], name, dig) } 691 if have == 2 { 692 g_puts("PKG-RESOLVE-REFUSED rule=" as *u8); g_puts(pk_rule(PK_R_PINAMBIG)) 693 g_puts(" name=" as *u8); g_puts(name); g_puts("\n" as *u8) 694 sys_exit(PK_EXIT_PINAMBIG); return PK_EXIT_PINAMBIG 695 } 696 if have == 0 { 697 // THE TYPOSQUAT KILL: an unpinned name fails CLOSED. There is no store scan, no nearest 698 // match, no `latest` -- a lookalike name simply has no row, so it resolves to nothing. 699 g_puts("PKG-RESOLVE-REFUSED rule=" as *u8); g_puts(pk_rule(PK_R_NOTPINNED)) 700 g_puts(" name=" as *u8); g_puts(name) 701 g_puts(" (no exact pin; resolution never guesses a near name)\n" as *u8) 702 sys_exit(PK_EXIT_NOTPINNED); return PK_EXIT_NOTPINNED 703 } 704 let nb: *i64 = sys_mmap(16) as *i64 705 let code: i64 = pkg_cas_resolve(cf.store, dig, dest, nb) 706 if code != PK_OK { 707 g_puts("PKG-RESOLVE-REFUSED rule=" as *u8); g_puts(pk_rule(code)) 708 g_puts(" name=" as *u8); g_puts(name) 709 g_puts(" digest=" as *u8); g_puts(dig); g_puts("\n" as *u8) 710 let r: i64 = pk_exit_for(code); sys_exit(r); return r 711 } 712 let lg: i64 = pk_log_append(cf.log, "resolve" as *u8, dig, nb[0]) 713 if lg != PK_OK { let r: i64 = pk_refuse("PKG-RESOLVE-REFUSED" as *u8, lg); sys_exit(r); return r } 714 g_puts("PKG-RESOLVE ok name=" as *u8); g_puts(name) 715 g_puts(" digest=" as *u8); g_puts(dig) 716 g_puts(" bytes=" as *u8); pk_num(nb[0]) 717 g_puts(" dest=" as *u8); g_puts(dest); g_puts("\n" as *u8) 718 sys_exit(0); return 0 719 } 720 721 // ---- logverify ------------------------------------------------------------------------------- 722 if pk_streq(verb, "logverify" as *u8) == 1 { 723 let entries: *i64 = sys_mmap(16) as *i64 724 let head: *u8 = sys_mmap(PK_SHAHEX + 8) 725 let bad: i64 = pk_log_verify(cf.log, entries, head) 726 g_puts("PKG-LOG entries=" as *u8); pk_num(entries[0]) 727 if bad >= 0 { 728 g_puts(" chain=BREAK at=" as *u8); pk_num(bad) 729 g_puts(" rule=" as *u8); g_puts(pk_rule(PK_R_CHAIN)); g_puts("\n" as *u8) 730 sys_exit(PK_EXIT_CHAIN); return PK_EXIT_CHAIN 731 } 732 g_puts(" chain=OK head=" as *u8); g_puts(head) 733 g_puts(" (tamper-evident against a partial edit; NOT sumdb -- one writer, unsigned, unwitnessed)\n" as *u8) 734 sys_exit(0); return 0 735 } 736 737 let u: i64 = pk_usage() 738 sys_exit(u) 739 return u 740}