code wiki / (root) / nx_self_build.nx

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}