code wiki / (root) / f6_gate.nx

f6_gate.nx source

↩ module page · 287 lines · 10774 B

1// f6_gate.nx -- sovereign replacement for bench/f6_gate.sh. 2// 3// Reads the committed golden manifest, invokes nxc2 on each 4// target, SHA-256 hashes the output, compares against the 5// manifest hash. Any mismatch signals one of: 6// (a) intentional change -- rerun with --update 7// (b) non-determinism regression -- fix before shipping 8// (c) toolchain compromise -- Thompson / Vault-7 attack signal 9// 10// Removes the last non-NishiLang language (bash) from the 11// nishi-core build chain. The gate's logic is identical to the 12// bash script's; only the driver changes. 13// 14// Usage: 15// f6_gate -- check against committed manifest 16// f6_gate --update -- overwrite the manifest 17// 18// Environment assumptions (same as f6_gate.sh): 19// - nxc2 executable at ./../nxc2.exe relative to pwd 20// - Manifest at ./f6_manifest.txt 21// - Targets listed in manifest paths (relative to nxc2 dir) 22// 23// Invariants: 24// FG1 Each target compiled under `--target asm --opt` with 25// stdout captured via pipe+dup3. Same invocation as bash 26// gate. Mismatch in options == mismatch against manifest 27// expected. 28// FG2 SHA-256 computed over captured bytes exactly; no trailing 29// whitespace stripped, no normalization. Byte-identical 30// semantics. 31// FG3 Non-zero exit codes from nxc2 (compile errors) cause 32// the target to be skipped (marked "# skip" in manifest) 33// per the bash gate's behaviour. 34// FG4 Output buffer caps: 16 MiB per target's asm. Larger is 35// a configuration issue; we fail loudly rather than truncate. 36 37import "syscalls.nx" 38import "sha256.nx" 39import "hex.nx" 40 41// ---- paths + defaults ------------------------------------------- 42// 43// NishiLang `const` accepts only integer literals; string constants 44// are exposed via accessor functions instead. 45 46const CAPTURE_CAP: i64 = 16777216 // 16 MiB per compilation 47 48func nxc2_path() -> *u8 { return "../nxc2.exe" } 49func manifest_path() -> *u8 { return "f6_manifest.txt" } 50 51// ---- argv marshalling for execve ------------------------------- 52// 53// execve wants argv as a null-terminated array of char*. We build 54// it in a caller-mmapped buffer at the i64 level (NishiLang treats 55// *i64 as an 8-byte-stride array we can index as argv[k]). 56 57func build_argv(argv_slots: *i64, path: *u8) -> i64 { 58 // argv[0..5] = "nxc2", "--target", "asm", "--opt", path, NULL 59 argv_slots[0] = nxc2_path() as i64 60 let t: *u8 = "--target" 61 argv_slots[1] = t as i64 62 let a: *u8 = "asm" 63 argv_slots[2] = a as i64 64 let o: *u8 = "--opt" 65 argv_slots[3] = o as i64 66 argv_slots[4] = path as i64 67 argv_slots[5] = 0 68 return 0 69} 70 71// ---- capture-child-stdout-into-buffer pattern ------------------ 72// 73// fork -> in child: dup3 pipe[1] to stdout, close pipe[0], exec. 74// in parent: close pipe[1], read from pipe[0] until EOF, wait for 75// child to reap. Returns bytes captured, writes them to out_buf, 76// or -errno on a clean failure. 77func capture_compile(path: *u8, out_buf: *u8, out_cap: i64) -> i64 { 78 // Build the argv array (6 pointer slots; 48 bytes). 79 let argv_raw: *u8 = sys_mmap(64) 80 let argv_slots: *i64 = argv_raw as *i64 81 build_argv(argv_slots, path) 82 83 // Create pipe. fds[0] = read end (parent), fds[1] = write (child). 84 let fds_raw: *u8 = sys_mmap(32) 85 let fds: *i64 = fds_raw as *i64 86 let rc_pipe: i64 = sys_pipe2(fds, 0) 87 if rc_pipe < 0 { return rc_pipe } 88 89 let pid: i64 = sys_fork() 90 if pid < 0 { return pid } 91 92 if pid == 0 { 93 // --- child --- 94 // Redirect stdout to pipe write end, close pipe read end. 95 sys_dup3(fds[1], 1, 0) 96 sys_close(fds[0]) 97 sys_close(fds[1]) 98 sys_execve(nxc2_path(), argv_slots, 0 as *i64) 99 // Only reached if execve failed. 100 sys_exit(127) 101 } 102 103 // --- parent --- 104 sys_close(fds[1]) // parent doesn't write to the pipe 105 // Drain child's stdout until EOF (read returns 0). 106 var total: i64 = 0 107 while total < out_cap { 108 let base: i64 = out_buf as i64 109 let tail: *u8 = (base + total) as *u8 110 let want: i64 = out_cap - total 111 let got: i64 = sys_read(fds[0], tail, want) 112 if got <= 0 { 113 sys_close(fds[0]) 114 // Wait for child to reap zombie. 115 let status_raw: *u8 = sys_mmap(16) 116 let status: *i64 = status_raw as *i64 117 *status = 0 118 sys_wait4(pid, status, 0) 119 if got < 0 { return got } 120 // child exit code determines success/failure of compile 121 let code: i64 = wait_exit_code(*status) 122 if code != 0 { return 0 - code } 123 return total 124 } 125 total = total + got 126 } 127 // Ran out of buffer. Drain + kill. 128 sys_close(fds[0]) 129 let status_raw: *u8 = sys_mmap(16) 130 let status: *i64 = status_raw as *i64 131 sys_wait4(pid, status, 0) 132 return -1 // FG4: fail loudly on overflow 133} 134 135// ---- manifest parsing ------------------------------------------- 136// 137// Manifest format: each non-comment line is "<64-hex> <path>\n". 138// Comment lines start with '#'. We parse in-place by scanning 139// pointer offsets into the loaded buffer. 140 141// Parse one manifest line starting at offset `pos` in buf. On 142// success writes hash start offset to *hash_off, path start offset 143// to *path_off, path length to *path_len, and returns the offset 144// of the byte AFTER the line's newline. On EOF or skip returns 145// negative (caller checks for termination). 146func parse_manifest_line(buf: *u8, buf_len: i64, pos: i64, 147 hash_off: *i64, 148 path_off: *i64, path_len: *i64) -> i64 { 149 if pos >= buf_len { return -1 } 150 // Skip blank lines + comments. 151 if buf[pos] == 0x23 { // '#' 152 var p: i64 = pos 153 while p < buf_len { 154 if buf[p] == 0x0A { return p + 1 } 155 p = p + 1 156 } 157 return buf_len 158 } 159 if buf[pos] == 0x0A { return pos + 1 } // blank 160 161 // Hash: 64 hex chars. 162 if pos + 64 >= buf_len { return -1 } 163 *hash_off = pos 164 // Skip whitespace (space or tab) between hash and path. Use the 165 // break-flag-plus-separate-scan-cursor pattern to avoid the 166 // `var = LEN + 1` sentinel-loss bug class (see 167 // [[project-arc-b2-url-resolver-img-extractor-shipped-2026-05-21]]). 168 var p: i64 = buf_len // default: ran off end 169 var p_scan: i64 = pos + 64 170 while p_scan < buf_len { 171 if buf[p_scan] == 0x20 { p_scan = p_scan + 1 } 172 else { if buf[p_scan] == 0x09 { p_scan = p_scan + 1 } else { p = p_scan; p_scan = buf_len } } 173 } 174 175 // Path runs until newline. 176 *path_off = p 177 while p < buf_len { 178 if buf[p] == 0x0A { *path_len = p - *path_off; return p + 1 } 179 p = p + 1 180 } 181 *path_len = p - *path_off 182 return p 183} 184 185// ---- entry ------------------------------------------------------- 186// 187// Reports using sys_write to fd 1 (stdout). Exit code: 188// 0 all targets match manifest 189// 1 one or more mismatches 190// 2 manifest not found or malformed 191// 3 toolchain error (nxc2 invocation failed) 192func main() -> i64 { 193 // Load manifest. 194 let mlen_raw: *u8 = sys_mmap(16) 195 let mlen_p: *i64 = mlen_raw as *i64 196 *mlen_p = 0 197 let manifest_buf: *u8 = sys_read_file(manifest_path(), mlen_p) 198 if manifest_buf == (0 as *u8) { 199 let msg: *u8 = "f6_gate: cannot read f6_manifest.txt\n" 200 var n: i64 = 0 201 while msg[n] != 0 { n = n + 1 } 202 sys_write(2, msg, n) 203 return 2 204 } 205 let mlen: i64 = *mlen_p 206 207 let capture_buf: *u8 = sys_mmap(CAPTURE_CAP) 208 let digest: *u8 = sys_mmap(32) 209 let hex_buf: *u8 = sys_mmap(72) 210 let path_cstr: *u8 = sys_mmap(4096) 211 212 var pos: i64 = 0 213 var checked: i64 = 0 214 var mismatches: i64 = 0 215 var skipped: i64 = 0 216 217 while pos < mlen { 218 let hash_off_raw: *u8 = sys_mmap(16) 219 let hash_off_p: *i64 = hash_off_raw as *i64 220 let path_off_raw: *u8 = sys_mmap(16) 221 let path_off_p: *i64 = path_off_raw as *i64 222 let path_len_raw: *u8 = sys_mmap(16) 223 let path_len_p: *i64 = path_len_raw as *i64 224 225 let next: i64 = parse_manifest_line(manifest_buf, mlen, pos, 226 hash_off_p, path_off_p, 227 path_len_p) 228 if next < 0 { 229 // EOF or unparseable tail. 230 pos = mlen 231 } else { 232 if next > pos + 1 { 233 // Non-comment, non-blank line. Check hash + path. 234 let plen: i64 = *path_len_p 235 if plen > 0 { 236 // Copy path to null-terminated cstring. 237 var i: i64 = 0 238 while i < plen { 239 path_cstr[i] = manifest_buf[*path_off_p + i] 240 i = i + 1 241 } 242 path_cstr[plen] = 0 243 244 let captured: i64 = capture_compile(path_cstr, 245 capture_buf, 246 CAPTURE_CAP) 247 if captured < 0 { 248 skipped = skipped + 1 249 } else { 250 // Hash + hex-encode. 251 sha256_digest(capture_buf, captured, digest) 252 hex_encode(digest, 32, hex_buf) 253 // Compare hex_buf (64 bytes) with manifest hash 254 // (64 bytes at hash_off). 255 var m: i64 = 1 256 var k: i64 = 0 257 while k < 64 { 258 if hex_buf[k] != manifest_buf[*hash_off_p + k] { 259 m = 0 260 k = 64 261 } else { 262 k = k + 1 263 } 264 } 265 if m == 0 { mismatches = mismatches + 1 } 266 checked = checked + 1 267 } 268 } 269 } 270 pos = next 271 } 272 } 273 274 // Report. 275 if mismatches > 0 { 276 let msg: *u8 = "f6_gate: MISMATCH detected\n" 277 var n: i64 = 0 278 while msg[n] != 0 { n = n + 1 } 279 sys_write(2, msg, n) 280 return 1 281 } 282 let msg: *u8 = "f6_gate: OK\n" 283 var n: i64 = 0 284 while msg[n] != 0 { n = n + 1 } 285 sys_write(1, msg, n) 286 return 0 287}