code wiki / (root) / nx_f6_gate.nx

nx_f6_gate.nx source

↩ module page · 290 lines · 10512 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 37// nx_safety_envelope: 38// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 39// sil_target: SIL1 40// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 41// verdict: NOT_YET_EVALUATED 42 43import "nx_syscalls.nx" 44import "nx_sha256.nx" 45import "nx_hex_codec.nx" 46 47// ---- paths + defaults ------------------------------------------- 48// 49// NishiLang `const` accepts only integer literals; string constants 50// are exposed via accessor functions instead. 51 52const CAPTURE_CAP: i64 = 16777216 // 16 MiB per compilation 53 54func nxc2_path() -> *u8 { return "../nxc2.exe" } 55func manifest_path() -> *u8 { return "f6_manifest.txt" } 56 57// ---- argv marshalling for execve ------------------------------- 58// 59// execve wants argv as a null-terminated array of char*. We build 60// it in a caller-mmapped buffer at the i64 level (NishiLang treats 61// *i64 as an 8-byte-stride array we can index as argv[k]). 62 63func build_argv(argv_slots: *i64, path: *u8) -> i64 { 64 // argv[0..5] = "nxc2", "--target", "asm", "--opt", path, NULL 65 argv_slots[0] = nxc2_path() as i64 66 let t: *u8 = "--target" 67 argv_slots[1] = t as i64 68 let a: *u8 = "asm" 69 argv_slots[2] = a as i64 70 let o: *u8 = "--opt" 71 argv_slots[3] = o as i64 72 argv_slots[4] = path as i64 73 argv_slots[5] = 0 74 return 0 75} 76 77// ---- capture-child-stdout-into-buffer pattern ------------------ 78// 79// fork -> in child: dup3 pipe[1] to stdout, close pipe[0], exec. 80// in parent: close pipe[1], read from pipe[0] until EOF, wait for 81// child to reap. Returns bytes captured, writes them to out_buf, 82// or -errno on a clean failure. 83func capture_compile(path: *u8, out_buf: *u8, out_cap: i64) -> i64 { 84 // Build the argv array (6 pointer slots; 48 bytes). 85 let argv_raw: *u8 = sys_mmap(64) 86 let argv_slots: *i64 = argv_raw as *i64 87 build_argv(argv_slots, path) 88 89 // Create pipe. fds[0] = read end (parent), fds[1] = write (child). 90 let fds_raw: *u8 = sys_mmap(32) 91 let fds: *i64 = fds_raw as *i64 92 let rc_pipe: i64 = sys_pipe2(fds, 0) 93 if rc_pipe < 0 { return rc_pipe } 94 95 let pid: i64 = sys_fork() 96 if pid < 0 { return pid } 97 98 if pid == 0 { 99 // --- child --- 100 // Redirect stdout to pipe write end, close pipe read end. 101 sys_dup3(fds[1], 1, 0) 102 sys_close(fds[0]) 103 sys_close(fds[1]) 104 sys_execve(nxc2_path(), argv_slots, 0 as *i64) 105 // Only reached if execve failed. 106 sys_exit(127) 107 } 108 109 // --- parent --- 110 sys_close(fds[1]) // parent doesn't write to the pipe 111 // Drain child's stdout until EOF (read returns 0). 112 var total: i64 = 0 113 while total < out_cap { 114 let base: i64 = out_buf as i64 115 let tail: *u8 = (base + total) as *u8 116 let want: i64 = out_cap - total 117 let got: i64 = sys_read(fds[0], tail, want) 118 if got <= 0 { 119 sys_close(fds[0]) 120 // Wait for child to reap zombie. 121 let status_raw: *u8 = sys_mmap(16) 122 let status: *i64 = status_raw as *i64 123 *status = 0 124 sys_wait4(pid, status, 0) 125 if got < 0 { return got } 126 // child exit code determines success/failure of compile 127 let code: i64 = wait_exit_code(*status) 128 if code != 0 { return 0 - code } 129 return total 130 } 131 total = total + got 132 } 133 // Ran out of buffer. Drain + kill. 134 sys_close(fds[0]) 135 let status_raw: *u8 = sys_mmap(16) 136 let status: *i64 = status_raw as *i64 137 sys_wait4(pid, status, 0) 138 return -1 // FG4: fail loudly on overflow 139} 140 141// ---- manifest parsing ------------------------------------------- 142// 143// Manifest format: each non-comment line is "<64-hex> <path>\n". 144// Comment lines start with '#'. We parse in-place by scanning 145// pointer offsets into the loaded buffer. 146 147// Parse one manifest line starting at offset `pos` in buf. On 148// success writes hash start offset to *hash_off, path start offset 149// to *path_off, path length to *path_len, and returns the offset 150// of the byte AFTER the line's newline. On EOF or skip returns 151// negative (caller checks for termination). 152func parse_manifest_line(buf: *u8, buf_len: i64, pos: i64, 153 hash_off: *i64, 154 path_off: *i64, path_len: *i64) -> i64 { 155 if pos >= buf_len { return -1 } 156 // Skip blank lines + comments. 157 if buf[pos] == 0x23 { // '#' 158 var p: i64 = pos 159 while p < buf_len { 160 if buf[p] == 0x0A { return p + 1 } 161 p = p + 1 162 } 163 return buf_len 164 } 165 if buf[pos] == 0x0A { return pos + 1 } // blank 166 167 // Hash: 64 hex chars. 168 if pos + 64 >= buf_len { return -1 } 169 *hash_off = pos 170 var p: i64 = pos + 64 171 // Skip whitespace (space or tab) between hash and path. 172 while p < buf_len { 173 if buf[p] == 0x20 { p = p + 1 } 174 else { if buf[p] == 0x09 { p = p + 1 } else { p = buf_len + 1 } } 175 } 176 if p > buf_len { p = p - 1 } // recover from sentinel-exit 177 178 // Path runs until newline. 179 *path_off = p 180 while p < buf_len { 181 if buf[p] == 0x0A { *path_len = p - *path_off; return p + 1 } 182 p = p + 1 183 } 184 *path_len = p - *path_off 185 return p 186} 187 188// ---- entry ------------------------------------------------------- 189// 190// Reports using sys_write to fd 1 (stdout). Exit code: 191// 0 all targets match manifest 192// 1 one or more mismatches 193// 2 manifest not found or malformed 194// 3 toolchain error (nxc2 invocation failed) 195func main() -> i64 { 196 // Load manifest. 197 let mlen_raw: *u8 = sys_mmap(16) 198 let mlen_p: *i64 = mlen_raw as *i64 199 *mlen_p = 0 200 let manifest_buf: *u8 = sys_read_file(manifest_path(), mlen_p) 201 if manifest_buf == (0 as *u8) { 202 let msg: *u8 = "f6_gate: cannot read f6_manifest.txt\n" 203 var n: i64 = 0 204 while msg[n] != 0 { n = n + 1 } 205 sys_write(2, msg, n) 206 return 2 207 } 208 let mlen: i64 = *mlen_p 209 210 let capture_buf: *u8 = sys_mmap(CAPTURE_CAP) 211 let digest: *u8 = sys_mmap(32) 212 let hex_buf: *u8 = sys_mmap(72) 213 let path_cstr: *u8 = sys_mmap(4096) 214 215 var pos: i64 = 0 216 var checked: i64 = 0 217 var mismatches: i64 = 0 218 var skipped: i64 = 0 219 220 while pos < mlen { 221 let hash_off_raw: *u8 = sys_mmap(16) 222 let hash_off_p: *i64 = hash_off_raw as *i64 223 let path_off_raw: *u8 = sys_mmap(16) 224 let path_off_p: *i64 = path_off_raw as *i64 225 let path_len_raw: *u8 = sys_mmap(16) 226 let path_len_p: *i64 = path_len_raw as *i64 227 228 let next: i64 = parse_manifest_line(manifest_buf, mlen, pos, 229 hash_off_p, path_off_p, 230 path_len_p) 231 if next < 0 { 232 // EOF or unparseable tail. 233 pos = mlen 234 } else { 235 if next > pos + 1 { 236 // Non-comment, non-blank line. Check hash + path. 237 let plen: i64 = *path_len_p 238 if plen > 0 { 239 // Copy path to null-terminated cstring. 240 var i: i64 = 0 241 while i < plen { 242 path_cstr[i] = manifest_buf[*path_off_p + i] 243 i = i + 1 244 } 245 path_cstr[plen] = 0 246 247 let captured: i64 = capture_compile(path_cstr, 248 capture_buf, 249 CAPTURE_CAP) 250 if captured < 0 { 251 skipped = skipped + 1 252 } else { 253 // Hash + hex-encode. 254 sha256_digest(capture_buf, captured, digest) 255 hex_encode(digest, 32, hex_buf) 256 // Compare hex_buf (64 bytes) with manifest hash 257 // (64 bytes at hash_off). 258 var m: i64 = 1 259 var k: i64 = 0 260 while k < 64 { 261 if hex_buf[k] != manifest_buf[*hash_off_p + k] { 262 m = 0 263 k = 64 264 } else { 265 k = k + 1 266 } 267 } 268 if m == 0 { mismatches = mismatches + 1 } 269 checked = checked + 1 270 } 271 } 272 } 273 pos = next 274 } 275 } 276 277 // Report. 278 if mismatches > 0 { 279 let msg: *u8 = "f6_gate: MISMATCH detected\n" 280 var n: i64 = 0 281 while msg[n] != 0 { n = n + 1 } 282 sys_write(2, msg, n) 283 return 1 284 } 285 let msg: *u8 = "f6_gate: OK\n" 286 var n: i64 = 0 287 while msg[n] != 0 { n = n + 1 } 288 sys_write(1, msg, n) 289 return 0 290}