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}