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}