nx_atomic_rewrite.nx source
↩ module page · 141 lines · 6031 B
1// nx_atomic_rewrite.nx -- WMS-R3: a GENERIC, callable atomic whole-file rewrite
2// primitive. The reusable twin of the seg-store commit idiom (ss_writefile +
3// sys_renameat + ss_syncdir), lifted out of the segment/manifest model into a
4// plain (path, bytes, n) API so ANY whole-file rewriter (e.g. nx_reconcile's
5// assignment_queue.tsv rewrite, capability ladders, status snapshots) can stop
6// truncating-in-place.
7//
8// ROOT CAUSE this closes: an in-place rewrite opens O_WRONLY|O_CREAT|O_TRUNC
9// (0x241), which TRUNCATES the live file to 0 BEFORE the new bytes land, then
10// streams a write-loop. A crash anywhere in that loop leaves the live file
11// truncated/torn. O_APPEND atomicity does NOT help -- it only makes ONE
12// write() atomic, never a truncate-then-rewrite sequence.
13//
14// THE FIX (atomic_rewrite): stage the FULL new contents into a SIBLING temp
15// file (same directory => same filesystem => renameat(2) is atomic), fsync the
16// temp so its bytes are durable, then renameat(tmp -> path) -- the single
17// atomic COMMIT POINT (a concurrent reader sees the whole old file or the whole
18// new file, never a torn read), then fsync the parent directory so the rename
19// itself survives power loss. A crash BEFORE the rename leaves the live file
20// completely untouched (only the temp is partial, and the temp is never read).
21//
22// REUSE: idiom lifted verbatim from nx_seg_store.nx ss_writefile (write-loop +
23// sys_fsync), ss_syncdir (dir fsync), and the temp->rename commit discipline.
24// SOVEREIGN: nx_syscalls only; no gcc / 3rd-party. license_tier: ORIGINAL
25import "nx_syscalls.nx"
26const K_MAGIC_1024: i64 = 1024
27
28// strlen for null-terminated paths (callers pass C-strings).
29func ar_len(s: *u8) -> i64 {
30 var n: i64 = 0
31 while s[n] != (0 as u8) { n = n + 1 }
32 return n
33}
34
35// Build "<path>.nxtmp" into out (null-terminated); returns its length. Fixed
36// ".nxtmp" suffix keeps the temp a SIBLING of the target (same dir => same FS
37// => renameat is atomic; cross-FS rename is NOT atomic and would copy+unlink).
38// Contract: one writer per path at a time -- the rename is the serialization
39// point, so a fixed suffix is safe for the single-writer whole-file-rewrite
40// pattern (the exact shape of every victim).
41func ar_tmpname(path: *u8, out: *u8) -> i64 {
42 var i: i64 = 0
43 while path[i] != (0 as u8) { out[i] = path[i]; i = i + 1 }
44 let suf: *u8 = ".nxtmp" as *u8
45 var j: i64 = 0
46 while suf[j] != (0 as u8) { out[i] = suf[j]; i = i + 1; j = j + 1 }
47 out[i] = 0 as u8
48 return i
49}
50
51// fsync the directory holding `path` (lifted from ss_syncdir): scan to the last
52// '/', open that dir O_RDONLY, fsync it, close. No '/' => current directory.
53// Power-loss closure of the rename itself. Best-effort (returns rc).
54func ar_syncdir(path: *u8) -> i64 {
55 let d: *u8 = sys_mmap(512)
56 var last: i64 = 0 - 1
57 var i: i64 = 0
58 while path[i] != (0 as u8) {
59 if path[i] == (47 as u8) { last = i }
60 i = i + 1
61 }
62 if last < 0 {
63 d[0] = 46 as u8
64 d[1] = 0 as u8
65 }
66 if last >= 0 {
67 var t: i64 = 0
68 while t <= last { d[t] = path[t]; t = t + 1 }
69 d[t] = 0 as u8
70 }
71 let fd: i64 = sys_openat_rd(d)
72 if fd < 0 { return 0 - 1 }
73 let rc: i64 = sys_fsync(fd)
74 sys_close(fd)
75 return rc
76}
77
78// THE PRIMITIVE. Atomically replace the whole contents of `path` with the `n`
79// bytes at `bytes`. Returns 0 on success; negative on the failing step:
80// -1 open temp failed -2 short/failed write -3 rename (commit) failed.
81// Dir-sync is best-effort (post-commit power-loss hardening), never fails the
82// call. Steps: stage temp -> fsync temp -> RENAME (commit) -> fsync dir.
83func atomic_rewrite(path: *u8, bytes: *u8, n: i64) -> i64 {
84 let tmp: *u8 = sys_mmap(K_MAGIC_1024)
85 ar_tmpname(path, tmp)
86 let fd: i64 = sys_openat_wr(tmp, 0x1a4)
87 if fd < 0 { return 0 - 1 }
88 var off: i64 = 0
89 while off < n {
90 let wr: i64 = sys_write(fd, (bytes as i64 + off) as *u8, n - off)
91 if wr <= 0 { sys_close(fd); return 0 - 2 }
92 off = off + wr
93 }
94 sys_fsync(fd)
95 sys_close(fd)
96 // THE ATOMIC COMMIT POINT: a reader of `path` sees the whole old file
97 // until this returns, then the whole new file -- never a torn read.
98 if sys_renameat(tmp, path) != 0 { return 0 - 3 }
99 ar_syncdir(path)
100 return 0
101}
102
103// CRASH-INJECTION hook for the gate ONLY (twin of ss_crashwrite): stage the
104// temp + fsync but RETURN BEFORE the rename -- simulates death after staging,
105// before commit. A reader of `path` must STILL see the intact OLD file
106// (the temp is a sibling that no reader of `path` ever opens).
107func ar_crashwrite(path: *u8, bytes: *u8, n: i64) -> i64 {
108 let tmp: *u8 = sys_mmap(K_MAGIC_1024)
109 ar_tmpname(path, tmp)
110 let fd: i64 = sys_openat_wr(tmp, 0x1a4)
111 if fd < 0 { return 0 - 1 }
112 var off: i64 = 0
113 while off < n {
114 let wr: i64 = sys_write(fd, (bytes as i64 + off) as *u8, n - off)
115 if wr <= 0 { sys_close(fd); return 0 - 2 }
116 off = off + wr
117 }
118 sys_fsync(fd)
119 sys_close(fd)
120 // DELIBERATELY NO rename -- the staged temp never commits.
121 return 0
122}
123
124// NEGATIVE-CONTROL primitive: the OLD unguarded in-place rewrite. Opens the
125// LIVE path with O_TRUNC (truncates to 0 immediately), then writes ONLY the
126// first `n` bytes. The gate forks a child that calls this with a HALF count
127// and exits -- modelling a crash mid-write -- leaving the live file torn.
128// This is the wrong-variant that MUST tear so the gate can prove atomicity
129// actually matters (a neg-control that does not bite proves nothing).
130func ar_unsafe_inplace(path: *u8, bytes: *u8, n: i64) -> i64 {
131 let fd: i64 = sys_openat_wr(path, 0x1a4) // TRUNCATES the live file NOW
132 if fd < 0 { return 0 - 1 }
133 var off: i64 = 0
134 while off < n {
135 let wr: i64 = sys_write(fd, (bytes as i64 + off) as *u8, n - off)
136 if wr <= 0 { sys_close(fd); return 0 - 2 }
137 off = off + wr
138 }
139 sys_close(fd)
140 return 0
141}