nx_self_build.nx source
↩ module page · 365 lines · 16119 B
1// nx_self_build.nx -- the 3-tier SELF-BUILD POLICY + the reversible-modify
2// transaction (snapshot -> write -> re-gate -> AUTO-ROLLBACK).
3//
4// THE NET BEFORE THE TRAPEZE. Before the engine is ever pointed at a real
5// source file, this organ proves -- on a DELIBERATE failed write -- that a
6// reversible modify either lands gated-green OR is rolled back BYTE-IDENTICAL
7// to the pre-write snapshot. A rollback you have not proven on a deliberate
8// failure is not a safety net.
9//
10// ============================================================================
11// THE OPERATOR'S 3-TIER POLICY (the spec):
12// TIER 1 ADDITIVE : new file / append / soft-flag is_current=0
13// -> ALLOW autonomously (cannot break existing code).
14// TIER 2 REVERSIBLE : overwrite an existing file
15// -> ALLOW only via SNAPSHOT -> WRITE -> RE-GATE ->
16// AUTO-ROLLBACK on failure. No breaking without a
17// proven, byte-identical rollback.
18// TIER 3 DESTRUCTIVE: delete / drop / overwrite WITHOUT backup / truncate
19// history -> DENY + require explicit operator confirm
20// (Cardinal #13 additive-only). FAIL-CLOSED on unknown.
21// ============================================================================
22//
23// ADDITIVE-OVER-WARDEN: the classifier never WEAKENS any warden verdict. It
24// can only refuse where warden refuses, or ADD the rollback discipline on top
25// of an overwrite that warden hard-denies. Warden stays the single source of
26// cardinal-truth; this is a pure composition layer (no edit to nx_warden_lib).
27//
28// CRASH-SAFETY (the headline trap defended): every destructive write is done
29// write-temp -> fsync -> renameat2 (atomic on the same fs), NEVER truncate-in-
30// place. The ORIGINAL is never mutated until the new version is published; on
31// a gate-fail we restore from a DURABLE on-disk snapshot (not RAM-only) via the
32// same atomic path, then VERIFY the restored bytes rehash to the snapshot hash.
33//
34// RE-GATE DECOUPLING (avoids the gate-runner's hardcoded /tmp scratch-path
35// collision under concurrent ticks, AND keeps this module free of the heavy
36// fork+exec re-gate dependency). The transaction is TWO-PHASE:
37// nx_self_build_begin(txn) : snapshot (durable) -> journal INTENT ->
38// atomic-write the new bytes. The new file now
39// sits on disk for the caller to GATE.
40// <caller runs its gate on `target`> (prod: nx_gate_runner.gr_gate;
41// test: a deterministic known-answer verdict)
42// nx_self_build_finish(txn, verdict) : verdict==0 -> COMMIT (bytes stand);
43// verdict!=0 -> AUTO-ROLLBACK from the durable
44// snapshot + VERIFY byte-identical.
45// This is the snapshot->write->re-gate->rollback flow with the verdict source
46// injected by composition (no function-pointer type, which this pinned
47// self-host compiler does not parse).
48//
49// REUSES (concept-not-copy, DRY -- no reinvention):
50// nx_blob_store : content-addressed snapshot index + hash + byte-identical
51// compare (nx_blob_hash_eq) for the restore-verify assertion
52// nx_journal_log : hash-chained intent/commit/rollback audit trail
53// nx_warden_lib : warden_authorize + W_* verbs (classifier wraps additively)
54// nx_syscalls : raw I/O (read_file / openat_wr / write / close / mmap)
55//
56// license_tier: ORIGINAL
57
58import "nx_syscalls.nx"
59import "nx_blob_store.nx"
60import "nx_journal_log.nx"
61import "nx_warden_lib.nx"
62
63// ===== tiers =================================================================
64const SB_TIER_ADDITIVE: i64 = 1
65const SB_TIER_REVERSIBLE: i64 = 2
66const SB_TIER_DESTRUCTIVE: i64 = 3
67
68// ===== transaction verdicts =================================================
69const SB_OK: i64 = 0 // write passed gate, new bytes stand
70const SB_ROLLED_BACK: i64 = 1 // write failed gate, restored byte-ident
71const SB_ROLLBACK_FAILED: i64 = 2 // LOUD: restore != snapshot (must NEVER)
72const SB_DENIED: i64 = 3 // tier-3, needs explicit operator confirm
73const SB_BAD_INPUT: i64 = 4 // null ctx / unreadable target / etc.
74
75// ===== journal schema ids (the falsifiable audit trail) =====================
76const SB_SCHEMA_INTENT: i64 = 7001
77const SB_SCHEMA_COMMIT: i64 = 7002
78const SB_SCHEMA_ROLLBACK: i64 = 7003
79const SB_SCHEMA_ROLLBACK_FAILED: i64 = 7004
80
81const SB_MODE: i64 = 420 // 0644
82
83// fsync(2) + renameat2(2): syscall numbers.
84//
85// IMPORTANT (verified empirically on this build lane): the x86-emitting
86// known-good compiler (nx_compile_x86) translates a SHORT-LIST of recognized
87// rv64 syscall NUMBERS (read/write/openat/mmap/...) into their x86_64 numbers
88// at codegen time, but fsync + renameat2 are NOT in that table -- their
89// numbers are emitted as LITERAL x86_64 syscall numbers. So in the active
90// (@ifndef TARGET_X86_64) source path -- the one this x86 backend compiles --
91// the constants must carry the x86_64 numbers (fsync=74, renameat2=316) to
92// reach the right kernel entry. Emitting rv64 82/276 hit x86 syscall 82
93// (=rename, EFAULT) and 276 (EINVAL) instead. Proven: 74 + 316 both return 0
94// on this lane. The @ifdef TARGET_X86_64 branch (selected only when nxc2 is
95// invoked --target x86_64, which DOES define the macro) carries the same
96// x86_64 numbers; a future native-RV64 backend would instead translate or use
97// 82/276 -- but no RV64 backend runs this proof.
98@ifdef TARGET_X86_64
99const SB_SYS_RENAMEAT2: i64 = 316
100const SB_SYS_FSYNC: i64 = 74
101@endif
102@ifndef TARGET_X86_64
103const SB_SYS_RENAMEAT2: i64 = 316
104const SB_SYS_FSYNC: i64 = 74
105@endif
106
107// ===== the self-build context (canary-by-composition) =======================
108// store + log carry their OWN canaries + is_valid gates; this struct just
109// composes them.
110struct NxSelfBuild {
111 store: *NxBlobStore,
112 log: *NxJournalLog,
113}
114
115func nx_self_build_new() -> *NxSelfBuild {
116 let ctx: *NxSelfBuild = (sys_mmap(16)) as *NxSelfBuild
117 ctx.store = nx_blob_store_new()
118 ctx.log = nx_journal_log_new(ctx.store)
119 return ctx
120}
121
122func nx_self_build_is_valid(ctx: *NxSelfBuild) -> i64 {
123 if (ctx as i64) == 0 { return 0 }
124 if nx_blob_store_is_valid(ctx.store) != 1 { return 0 }
125 if nx_journal_log_is_valid(ctx.log) != 1 { return 0 }
126 return 1
127}
128
129// ===== the transaction handle ===============================================
130// Carries everything nx_self_build_finish needs to commit or roll back.
131// `state`: 0 = unstarted, 1 = begun (written, awaiting verdict), 2 = settled.
132struct NxSbTxn {
133 ctx: *NxSelfBuild,
134 target: *u8,
135 snap_path: *u8,
136 snap_w0: i64,
137 snap_w1: i64,
138 snap_w2: i64,
139 snap_w3: i64,
140 snap_len: i64,
141 state: i64,
142}
143
144// ===== small string helpers (no external dep) ===============================
145func sb_slen(s: *u8) -> i64 {
146 var n: i64 = 0
147 while s[n] != 0 as u8 { n = n + 1 }
148 return n
149}
150
151// append `suffix` onto a copy of `base` into a fresh buffer; returns the buffer.
152func sb_concat(base: *u8, suffix: *u8) -> *u8 {
153 let bn: i64 = sb_slen(base)
154 let sn: i64 = sb_slen(suffix)
155 let out: *u8 = sys_mmap(bn + sn + 8)
156 var i: i64 = 0
157 while i < bn { out[i] = base[i]; i = i + 1 }
158 var j: i64 = 0
159 while j < sn { out[bn + j] = suffix[j]; j = j + 1 }
160 out[bn + sn] = 0 as u8
161 return out
162}
163
164// ===== raw syscall wrappers (declared locally; nx_syscalls not edited) ======
165func sb_fsync(fd: i64) -> i64 {
166 return __syscall(SB_SYS_FSYNC, fd, 0, 0, 0, 0, 0)
167}
168
169// renameat2(AT_FDCWD, old, AT_FDCWD, new, flags=0).
170func sb_renameat2(old_path: *u8, new_path: *u8) -> i64 {
171 return __syscall(SB_SYS_RENAMEAT2, -100, old_path, -100, new_path, 0, 0)
172}
173
174// ===== write-all (defends the short-write trap) =============================
175// sys_write may write FEWER bytes than asked. Loop until all bytes flush;
176// FAIL LOUD (return -1) on a negative or zero-progress write. Returns 1 ok.
177func sb_write_all(fd: i64, buf: *u8, len: i64) -> i64 {
178 var total: i64 = 0
179 while total < len {
180 let base: i64 = buf as i64
181 let tail: *u8 = (base + total) as *u8
182 let n: i64 = sys_write(fd, tail, len - total)
183 if n <= 0 { return -1 } // -errno OR zero progress -> LOUD
184 total = total + n
185 }
186 return 1
187}
188
189// ===== crash-atomic file write: temp -> fsync -> rename =====================
190// Writes buf[0..len) to `target` atomically. Never truncates `target` in
191// place: writes a sibling temp, fsyncs it, then renames temp->target (atomic
192// on the same fs). A crash anywhere before the rename leaves `target`
193// untouched. Returns 1 ok / 0 fail.
194func sb_atomic_write(target: *u8, buf: *u8, len: i64) -> i64 {
195 let tmp: *u8 = sb_concat(target, ".sbld.tmp" as *u8)
196 let fd: i64 = sys_openat_wr(tmp, SB_MODE) // O_CREAT|O_WRONLY|O_TRUNC
197 if fd < 0 { return 0 }
198 if sb_write_all(fd, buf, len) != 1 { sys_close(fd); return 0 }
199 sb_fsync(fd) // durability before rename
200 sys_close(fd)
201 if sb_renameat2(tmp, target) != 0 { return 0 } // atomic publish
202 return 1
203}
204
205// ===== COMPONENT A: 3-tier classifier (ADDITIVE over warden) ================
206// Pure function -- maps the operator's policy onto EXISTING warden verbs.
207func nx_sb_classify(kind: i64, target: *u8, has_backup: i64, has_gate: i64) -> i64 {
208 if kind == W_ADDITIVE { return SB_TIER_ADDITIVE } // warden: ALLOW
209 if kind == W_DELETE { return SB_TIER_DESTRUCTIVE } // warden: DENY
210 if kind == W_OVERWRITE_SRC {
211 if has_backup == 1 {
212 if has_gate == 1 { return SB_TIER_REVERSIBLE } // net present
213 }
214 return SB_TIER_DESTRUCTIVE // no net -> deny
215 }
216 return SB_TIER_DESTRUCTIVE // unknown -> deny
217}
218
219// Authorize a self-build action. ADDITIVE wrapper -- NEVER weakens warden.
220// Returns W_ALLOW(1) / W_DENY(0).
221func nx_self_build_authorize(kind: i64, target: *u8,
222 has_backup: i64, has_gate: i64) -> i64 {
223 let tier: i64 = nx_sb_classify(kind, target, has_backup, has_gate)
224 if tier == SB_TIER_ADDITIVE {
225 return warden_authorize(W_ADDITIVE, target, has_backup) // ALLOW
226 }
227 if tier == SB_TIER_REVERSIBLE {
228 return W_ALLOW // safety supplied by the rollback below
229 }
230 // tier-3: audit the DENY through warden, then deny.
231 warden_authorize(W_OVERWRITE_SRC, target, has_backup)
232 return W_DENY
233}
234
235// ===== COMPONENT B: the reversible-modify transaction (two-phase) ===========
236
237// new a transaction handle bound to ctx + target.
238func nx_sb_txn_new(ctx: *NxSelfBuild, target: *u8) -> *NxSbTxn {
239 let t: *NxSbTxn = (sys_mmap(80)) as *NxSbTxn
240 t.ctx = ctx
241 t.target = target
242 t.snap_path = 0 as *u8
243 t.snap_w0 = 0
244 t.snap_w1 = 0
245 t.snap_w2 = 0
246 t.snap_w3 = 0
247 t.snap_len = 0
248 t.state = 0
249 return t
250}
251
252// PHASE 1: snapshot (durable: blob index + on-disk copy) -> journal INTENT ->
253// atomic-write the new bytes. After this returns SB_OK the new file is on
254// disk awaiting the caller's gate. Returns SB_BAD_INPUT (fail-closed) if the
255// snapshot net cannot be established or the write fails (original untouched).
256func nx_self_build_begin(txn: *NxSbTxn, new_bytes: *u8, new_len: i64) -> i64 {
257 if (txn as i64) == 0 { return SB_BAD_INPUT }
258 let ctx: *NxSelfBuild = txn.ctx
259 if nx_self_build_is_valid(ctx) != 1 { return SB_BAD_INPUT }
260 if (txn.target as i64) == 0 { return SB_BAD_INPUT }
261 if new_len < 0 { return SB_BAD_INPUT }
262
263 // 1. SNAPSHOT (durable): read original, store in content-addressed index
264 // (the rollback key), AND write a durable on-disk copy keyed sibling so
265 // the snapshot survives a process crash (RAM-only would evaporate
266 // exactly when rollback is needed).
267 let snap_hash: *NxBlobHash = nx_blob_hash_new()
268 let ln: *i64 = (sys_mmap(8)) as *i64
269 let bytes: *u8 = sys_read_file(txn.target, ln)
270 if (bytes as i64) == 0 { return SB_BAD_INPUT }
271 if nx_blob_store_put(ctx.store, bytes, ln[0], snap_hash) != NX_BLOB_OK {
272 return SB_BAD_INPUT
273 }
274 if nx_blob_store_has(ctx.store, snap_hash) != 1 { return SB_BAD_INPUT }
275 let snap_path: *u8 = sb_concat(txn.target, ".sbld.snap" as *u8)
276 if sb_atomic_write(snap_path, bytes, ln[0]) != 1 { return SB_BAD_INPUT }
277
278 txn.snap_path = snap_path
279 txn.snap_w0 = snap_hash.w0
280 txn.snap_w1 = snap_hash.w1
281 txn.snap_w2 = snap_hash.w2
282 txn.snap_w3 = snap_hash.w3
283 txn.snap_len = ln[0]
284
285 // 2. journal INTENT before the write (the falsifiable trail)
286 nx_journal_log_append(ctx.log, txn.target, sb_slen(txn.target), SB_SCHEMA_INTENT)
287
288 // 3. WRITE new bytes ATOMICALLY (original untouched until rename publishes)
289 if sb_atomic_write(txn.target, new_bytes, new_len) != 1 {
290 return SB_BAD_INPUT // original intact (never truncated)
291 }
292
293 txn.state = 1
294 return SB_OK
295}
296
297// rebuild a NxBlobHash struct from the txn's stored snapshot words.
298func sb_txn_snap_hash(txn: *NxSbTxn) -> *NxBlobHash {
299 let h: *NxBlobHash = nx_blob_hash_new()
300 h.w0 = txn.snap_w0
301 h.w1 = txn.snap_w1
302 h.w2 = txn.snap_w2
303 h.w3 = txn.snap_w3
304 return h
305}
306
307// PHASE 2: settle the transaction against the caller's gate `verdict`
308// (0 = PROVEN/pass, nonzero = FAILED).
309// verdict==0 -> COMMIT: journal COMMIT, new bytes stand -> SB_OK.
310// verdict!=0 -> AUTO-ROLLBACK from the durable snapshot (atomic) -> reread ->
311// rehash -> assert nx_blob_hash_eq(reread, snapshot) -> journal
312// ROLLBACK -> SB_ROLLED_BACK. If the rehash != snapshot ->
313// ROLLBACK_FAILED (LOUD): the only path that could leave a
314// damaged file fires the alarm rather than returning success.
315func nx_self_build_finish(txn: *NxSbTxn, verdict: i64) -> i64 {
316 if (txn as i64) == 0 { return SB_BAD_INPUT }
317 if txn.state != 1 { return SB_BAD_INPUT }
318 let ctx: *NxSelfBuild = txn.ctx
319 if nx_self_build_is_valid(ctx) != 1 { return SB_BAD_INPUT }
320
321 if verdict == 0 {
322 nx_journal_log_append(ctx.log, txn.target, sb_slen(txn.target), SB_SCHEMA_COMMIT)
323 txn.state = 2
324 return SB_OK
325 }
326
327 // AUTO-ROLLBACK from the durable snapshot
328 let snap_hash: *NxBlobHash = sb_txn_snap_hash(txn)
329 let rb_len: *i64 = (sys_mmap(8)) as *i64
330 let snap_bytes: *u8 = sys_read_file(txn.snap_path, rb_len)
331 if (snap_bytes as i64) == 0 {
332 nx_journal_log_append(ctx.log, txn.target, sb_slen(txn.target), SB_SCHEMA_ROLLBACK_FAILED)
333 txn.state = 2
334 return SB_ROLLBACK_FAILED
335 }
336 if sb_atomic_write(txn.target, snap_bytes, rb_len[0]) != 1 {
337 nx_journal_log_append(ctx.log, txn.target, sb_slen(txn.target), SB_SCHEMA_ROLLBACK_FAILED)
338 txn.state = 2
339 return SB_ROLLBACK_FAILED
340 }
341
342 // VERIFY-RESTORE: reread the target, rehash, assert byte-identical.
343 let chk_len: *i64 = (sys_mmap(8)) as *i64
344 let chk_bytes: *u8 = sys_read_file(txn.target, chk_len)
345 if (chk_bytes as i64) == 0 {
346 nx_journal_log_append(ctx.log, txn.target, sb_slen(txn.target), SB_SCHEMA_ROLLBACK_FAILED)
347 txn.state = 2
348 return SB_ROLLBACK_FAILED
349 }
350 let chk_hash: *NxBlobHash = nx_blob_hash_new()
351 if nx_blob_store_put(ctx.store, chk_bytes, chk_len[0], chk_hash) != NX_BLOB_OK {
352 nx_journal_log_append(ctx.log, txn.target, sb_slen(txn.target), SB_SCHEMA_ROLLBACK_FAILED)
353 txn.state = 2
354 return SB_ROLLBACK_FAILED
355 }
356 if nx_blob_hash_eq(chk_hash, snap_hash) != 1 {
357 nx_journal_log_append(ctx.log, txn.target, sb_slen(txn.target), SB_SCHEMA_ROLLBACK_FAILED)
358 txn.state = 2
359 return SB_ROLLBACK_FAILED
360 }
361
362 nx_journal_log_append(ctx.log, txn.target, sb_slen(txn.target), SB_SCHEMA_ROLLBACK)
363 txn.state = 2
364 return SB_ROLLED_BACK
365}