nx_jrnl_archive_lib_t138.nx source
↩ module page · 285 lines · 15003 B
1// Private archive qualification: cooperating writers must hold journal+".lock" before opening
2// the journal through durable close. The lock file is stable and must never be removed.
3// Unconverted writers remain a deployment blocker; advisory locking cannot stop them.
4// Collision/write/verification/durability failures preserve live data. rc7 lock; rc8 source changed;
5// rc9 reset occurred but durability failed (archive durable); rc4 includes exclusive-name collision.
6import "nx_syscalls.nx"
7import "nx_sha256.nx"
8
9const JA_DIGEST_BYTES: i64 = 32
10const JA_ARCH_MODE: i64 = 0x1a4 // rw-r--r-- for the archived copy
11const JA_DIR_MODE: i64 = 0x1ed // rwxr-xr-x for the archive dir
12const JA_STAT_BYTES: i64 = 144 // x86-64 statfs ABI
13const JA_PATH_MAX: i64 = 4096 // PATH_MAX: the kernel bound on a path
14const JA_FRAME_BYTES: i64 = 4 * JA_PATH_MAX + 5 * JA_DIGEST_BYTES + 4 * 16 + JA_STAT_BYTES
15
16func ja_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
17func ja_puts(s: *u8) -> i64 { sys_write(1, s, ja_slen(s)); return 0 }
18func ja_pn(v: i64) -> i64 {
19 let t: *u8 = sys_mmap(24)
20 var x: i64 = v
21 var k: i64 = 0
22 if x < 0 { t[0] = 45 as u8; sys_write(1, t, 1); x = 0 - x }
23 if x == 0 { t[0] = 48 as u8; k = 1 }
24 while x > 0 { t[k] = (48 + (x % 10)) as u8; x = x / 10; k = k + 1 }
25 let o: *u8 = sys_mmap(24)
26 var i: i64 = 0
27 while i < k { o[i] = t[k - 1 - i]; i = i + 1 }
28 sys_write(1, o, k)
29 return 0
30}
31func ja_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } d[p] = 0 as u8; return p }
32func ja_catn(d: *u8, o: i64, v: i64) -> i64 {
33 let t: *u8 = sys_mmap(24)
34 var x: i64 = v
35 var k: i64 = 0
36 if x == 0 { t[0] = 48 as u8; k = 1 }
37 while x > 0 { t[k] = (48 + (x % 10)) as u8; x = x / 10; k = k + 1 }
38 var p: i64 = o
39 var i: i64 = 0
40 while i < k { d[p] = t[k - 1 - i]; p = p + 1; i = i + 1 }
41 d[p] = 0 as u8
42 return p
43}
44func ja_hex(d: *u8, out: *u8) -> i64 {
45 var i: i64 = 0
46 while i < JA_DIGEST_BYTES {
47 let v: i64 = d[i] as i64
48 let hi: i64 = (v >> 4) & 15
49 let lo: i64 = v & 15
50 if hi < 10 { out[i*2] = (48 + hi) as u8 } else { out[i*2] = (87 + hi) as u8 }
51 if lo < 10 { out[i*2+1] = (48 + lo) as u8 } else { out[i*2+1] = (87 + lo) as u8 }
52 i = i + 1
53 }
54 out[JA_DIGEST_BYTES*2] = 0 as u8
55 return 0
56}
57// basename of a path (the text after the last '/'), and its directory (everything before it, or ".")
58func ja_basename(p: *u8) -> *u8 {
59 var i: i64 = 0
60 var last: i64 = 0 - 1
61 while p[i] != (0 as u8) { if p[i] == (47 as u8) { last = i } i = i + 1 }
62 return (p as i64 + last + 1) as *u8
63}
64func ja_dirname(p: *u8, out: *u8) -> i64 {
65 var i: i64 = 0
66 var last: i64 = 0 - 1
67 while p[i] != (0 as u8) { if p[i] == (47 as u8) { last = i } i = i + 1 }
68 if last < 0 { out[0] = 46 as u8; out[1] = 0 as u8; return 1 }
69 var k: i64 = 0
70 while k < last { out[k] = p[k]; k = k + 1 }
71 out[k] = 0 as u8
72 return k
73}
74// write ALL of buf to path (create/truncate), fsync, close. 0 ok, -1 open failed, -2 short write.
75func ja_write_all(path: *u8, buf: *u8, n: i64, mode: i64) -> i64 {
76 let fd: i64 = sys_openat_wr(path, mode)
77 if fd < 0 { return 0 - 1 }
78 var off: i64 = 0
79 while off < n {
80 let r: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off)
81 if r <= 0 { sys_close(fd); return 0 - 2 }
82 off = off + r
83 }
84 let sync: i64 = sys_fsync(fd)
85 let closed: i64 = sys_close(fd)
86 if sync != 0 { return 0 - 3 }
87 if closed != 0 { return 0 - 4 }
88 return 0
89}
90// Shared journal transaction lock: descriptor lifetime, not a lease or removable marker.
91func ja_lock(journal: *u8) -> i64 {
92 if ja_slen(journal) + 6 >= JA_PATH_MAX { return 0 - 36 }
93 let path: *u8 = sys_mmap_shared(JA_PATH_MAX)
94 if (path as i64) <= 0 { return path as i64 }
95 let off: i64 = ja_cat(path, 0, journal)
96 ja_cat(path, off, ".lock" as *u8)
97 let fd: i64 = sys_openat_lock(path)
98 let released: i64 = sys_munmap(path, JA_PATH_MAX)
99 if released != 0 { if fd >= 0 { sys_close(fd) }; return released }
100 if fd < 0 { return fd }
101 let rc: i64 = sys_flock(fd, SYS_LOCK_EX | SYS_LOCK_NB)
102 if rc != 0 { sys_close(fd); return rc }
103 return fd
104}
105func ja_unlock(fd: i64) -> i64 { return sys_close(fd) }
106// Transfer memory is one measured filesystem block plus canonical SHA workspace, independent of file size.
107// fd remains caller-owned; output digest is accepted only on rc0. Early EOF and read/write errors refuse.
108func ja_transfer_fd(fd: i64, dest: i64, expected: i64, block: i64, digest: *u8) -> i64 {
109 if expected < 0 || expected > SHA256_SIGNED_MAX/SHA256_BITS_PER_BYTE || block <= 0 { return 0 - 1 }
110 let ws: i64 = sha256_workspace_bytes()
111 if block > SHA256_SIGNED_MAX - ws { return 0 - 1 }
112 let alloc: i64 = ws + block
113 let memory: *u8 = sys_mmap_shared(alloc)
114 if (memory as i64) <= 0 { return 0 - 2 }
115 let ctx: *Sha256 = memory as *Sha256
116 let buf: *u8 = (memory as i64 + ws) as *u8
117 var rc: i64 = sha256_init_workspace(memory, ws)
118 var total: i64 = 0
119 while rc == 0 && total < expected {
120 var want: i64 = expected - total
121 if want > block { want = block }
122 let got: i64 = sys_read(fd, buf, want)
123 if got <= 0 { rc = 0 - 3 } else {
124 sha256_update(ctx, buf, got)
125 if dest >= 0 {
126 var off: i64 = 0
127 while rc == 0 && off < got {
128 let written: i64 = sys_write(dest, (buf as i64 + off) as *u8, got - off)
129 if written <= 0 { rc = 0 - 4 } else { off = off + written }
130 }
131 }
132 total = total + got
133 }
134 }
135 if rc == 0 { if sys_read(fd, buf, 1) != 0 { rc = 0 - 3 } }
136 if rc == 0 { sha256_final(ctx, digest) }
137 let released: i64 = sys_munmap(memory, alloc)
138 if rc == 0 && released != 0 { rc = 0 - 5 }
139 return rc
140}
141func ja_transfer(path: *u8, dest: i64, digest: *u8, count: *i64) -> i64 {
142 count[0] = 0 - 1
143 let fd: i64 = sys_openat_rd(path)
144 if fd < 0 { return fd }
145 let n: i64 = sys_lseek(fd, 0, 2)
146 if n < 0 { sys_close(fd); return 0 - 1 }
147 if sys_lseek(fd, 0, 0) != 0 { sys_close(fd); return 0 - 1 }
148 // Own one releasable mapping; sys_munmap deliberately ignores arena-sized allocations.
149 let fsbytes: i64 = JA_STAT_BYTES + NXA_SMALL_MAX
150 let fs: *i64 = sys_mmap_shared(fsbytes) as *i64
151 if (fs as i64) <= 0 { sys_close(fd); return 0 - 2 }
152 let observed: i64 = sys_statfs(path, fs)
153 var rc: i64 = observed
154 if observed == 0 { rc = ja_transfer_fd(fd, dest, n, fs[1], digest) }
155 let freed: i64 = sys_munmap(fs as *u8, fsbytes)
156 let closed: i64 = sys_close(fd)
157 if rc != 0 { return rc }
158 if freed != 0 { return freed }
159 if closed != 0 { return closed }
160 count[0] = n
161 return 0
162}
163func ja_digest_same(a: *u8, b: *u8) -> i64 {
164 var i: i64 = 0
165 while i < JA_DIGEST_BYTES { if a[i] != b[i] { return 0 } i = i + 1 }
166 return 1
167}
168func ja_sync_dir(path: *u8) -> i64 {
169 let fd: i64 = sys_openat_directory(path)
170 if fd < 0 { return fd }
171 let rc: i64 = sys_fsync(fd)
172 let closed: i64 = sys_close(fd)
173 if rc != 0 { return rc }
174 return closed
175}
176func ja_write_exclusive(path: *u8, buf: *u8, n: i64) -> i64 {
177 let fd: i64 = sys_openat_exclusive(path, JA_ARCH_MODE)
178 if fd < 0 { return fd }
179 var off: i64 = 0
180 while off < n {
181 let got: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off)
182 if got <= 0 { sys_close(fd); return 0 - 2 }
183 off = off + got
184 }
185 let sync: i64 = sys_fsync(fd)
186 let closed: i64 = sys_close(fd)
187 if sync != 0 { return sync }
188 return closed
189}
190// rep (may be 0): rep[0]=bytes rep[1]=verified rep[2]=reset -- the announce line as numbers a gate can read
191func ja_announce(src: *u8, bytes: i64, hex: *u8, arch: *u8, verified: i64, reset: i64, rc: i64, why: *u8, rep: *i64) -> i64 {
192 if (rep as i64) != 0 { rep[0] = bytes; rep[1] = verified; rep[2] = reset }
193 ja_puts("NX-JRNL-ARCHIVE src=" as *u8); ja_puts(src)
194 ja_puts(" bytes=" as *u8); ja_pn(bytes)
195 ja_puts(" sha256=" as *u8); ja_puts(hex)
196 ja_puts(" archived=" as *u8); ja_puts(arch)
197 ja_puts(" verified=" as *u8); ja_pn(verified)
198 ja_puts(" reset=" as *u8); ja_pn(reset)
199 ja_puts(" rc=" as *u8); ja_pn(rc)
200 ja_puts(" " as *u8); ja_puts(why); ja_puts("\n" as *u8)
201 return rc
202}
203// THE VERB. archive_dir may be 0: then <dirname(journal)>/archive. epoch names the archive.
204func ja_archive_in_frame(journal: *u8, archive_dir: *u8, epoch: i64, rep: *i64, frame: *u8) -> i64 {
205 if ja_slen(journal) + 32 >= JA_PATH_MAX { return 3 }
206 if (archive_dir as i64) != 0 { if ja_slen(archive_dir) + ja_slen(ja_basename(journal)) + 32 >= JA_PATH_MAX { return 4 } }
207 let hex: *u8 = frame
208 ja_cat(hex, 0, "-" as *u8)
209 let d0: *u8 = (frame as i64 + JA_PATH_MAX * 4) as *u8
210 let count: *i64 = (frame as i64 + JA_PATH_MAX * 4 + JA_DIGEST_BYTES * 5) as *i64
211 if ja_transfer(journal, 0 - 1, d0, count) != 0 { return ja_announce(journal, 0 - 1, hex, "-" as *u8, 0, 0, 3, "CANNOT-READ-COMPLETE-JOURNAL (live untouched)" as *u8, rep) }
212 let n: i64 = count[0]
213 if n == 0 { return ja_announce(journal, 0, hex, "-" as *u8, 0, 0, 1, "EMPTY (nothing touched)" as *u8, rep) }
214 ja_hex(d0, hex)
215 let dir: *u8 = (frame as i64 + JA_PATH_MAX) as *u8
216 if (archive_dir as i64) != 0 { ja_cat(dir, 0, archive_dir) } else {
217 let dl: i64 = ja_dirname(journal, dir)
218 ja_cat(dir, dl, "/archive" as *u8)
219 }
220 sys_mkdir(dir, JA_DIR_MODE)
221 let arch: *u8 = (frame as i64 + JA_PATH_MAX * 2) as *u8
222 var ao: i64 = ja_cat(arch, 0, dir)
223 ao = ja_cat(arch, ao, "/" as *u8)
224 ao = ja_cat(arch, ao, ja_basename(journal))
225 ao = ja_cat(arch, ao, "." as *u8)
226 ja_catn(arch, ao, epoch)
227 // Current physical availability is an observation, not a reservation; actual I/O failures still refuse.
228 let fs: *i64 = (frame as i64 + JA_PATH_MAX * 4 + JA_DIGEST_BYTES * 5 + 16 * 4) as *i64
229 if sys_statfs(dir, fs) != 0 { return ja_announce(journal, n, hex, arch, 0, 0, 4, "CAPACITY-UNAVAILABLE (live untouched)" as *u8, rep) }
230 if fs[1] <= 0 || fs[4] < 0 { return ja_announce(journal, n, hex, arch, 0, 0, 4, "CAPACITY-INVALID (live untouched)" as *u8, rep) }
231 var needed: i64 = n / fs[1]
232 if n % fs[1] != 0 { needed = needed + 1 }
233 if needed > fs[4] { return ja_announce(journal, n, hex, arch, 0, 0, 4, "CAPACITY-INSUFFICIENT (measured available blocks; live untouched)" as *u8, rep) }
234 let afd: i64 = sys_openat_exclusive(arch, JA_ARCH_MODE)
235 if afd < 0 { return ja_announce(journal, n, hex, arch, 0, 0, 4, "ARCHIVE-EXCLUSIVE-OPEN-FAILED (collision or I/O; existing history untouched)" as *u8, rep) }
236 let copied: *u8 = (frame as i64 + JA_PATH_MAX * 4 + JA_DIGEST_BYTES) as *u8
237 let copied_len: *i64 = (frame as i64 + JA_PATH_MAX * 4 + JA_DIGEST_BYTES * 5 + 16) as *i64
238 let copy: i64 = ja_transfer(journal, afd, copied, copied_len)
239 let synced: i64 = sys_fsync(afd)
240 let closed: i64 = sys_close(afd)
241 if copy != 0 || synced != 0 || closed != 0 { return ja_announce(journal, n, hex, arch, 0, 0, 4, "ARCHIVE-COPY-OR-DURABILITY-FAILED (partial archive retained; live untouched)" as *u8, rep) }
242 if copied_len[0] != n || ja_digest_same(d0, copied) == 0 { return ja_announce(journal, n, hex, arch, 0, 0, 8, "SOURCE-CHANGED-DURING-COPY (live untouched)" as *u8, rep) }
243 let back: *u8 = (frame as i64 + JA_PATH_MAX * 4 + JA_DIGEST_BYTES * 2) as *u8
244 let backlen: *i64 = (frame as i64 + JA_PATH_MAX * 4 + JA_DIGEST_BYTES * 5 + 16 * 2) as *i64
245 if ja_transfer(arch, 0 - 1, back, backlen) != 0 { return ja_announce(journal, n, hex, arch, 0, 0, 5, "VERIFY-READ-FAILED (live untouched)" as *u8, rep) }
246 if backlen[0] != n || ja_digest_same(d0, back) == 0 { return ja_announce(journal, n, hex, arch, 0, 0, 5, "VERIFY-MISMATCH (live untouched)" as *u8, rep) }
247 let parent: *u8 = (frame as i64 + JA_PATH_MAX * 3) as *u8
248 ja_dirname(dir, parent)
249 if ja_sync_dir(dir) != 0 { return ja_announce(journal, n, hex, arch, 1, 0, 4, "ARCHIVE-DIR-SYNC-FAILED (live untouched)" as *u8, rep) }
250 if ja_sync_dir(parent) != 0 { return ja_announce(journal, n, hex, arch, 1, 0, 4, "ARCHIVE-PARENT-SYNC-FAILED (live untouched)" as *u8, rep) }
251 let current: *u8 = (frame as i64 + JA_PATH_MAX * 4 + JA_DIGEST_BYTES * 3) as *u8
252 let current_len: *i64 = (frame as i64 + JA_PATH_MAX * 4 + JA_DIGEST_BYTES * 5 + 16 * 3) as *i64
253 if ja_transfer(journal, 0 - 1, current, current_len) != 0 { return ja_announce(journal, n, hex, arch, 1, 0, 8, "SOURCE-RECHECK-FAILED (live untouched)" as *u8, rep) }
254 if current_len[0] != n || ja_digest_same(d0, current) == 0 { return ja_announce(journal, n, hex, arch, 1, 0, 8, "SOURCE-CHANGED (live untouched)" as *u8, rep) }
255 let rfd: i64 = sys_openat_wr(journal, JA_ARCH_MODE)
256 if rfd < 0 { return ja_announce(journal, n, hex, arch, 1, 0, 6, "RESET-OPEN-FAILED (live unchanged)" as *u8, rep) }
257 let reset_sync: i64 = sys_fsync(rfd)
258 let reset_close: i64 = sys_close(rfd)
259 if reset_sync != 0 || reset_close != 0 { return ja_announce(journal, n, hex, arch, 1, 1, 9, "RESET-DURABILITY-FAILED (archive durable; reset durability unknown)" as *u8, rep) }
260 return ja_announce(journal, n, hex, arch, 1, 1, 0, "OK (streamed exclusive durable archive; cooperative lock held through reset)" as *u8, rep)
261}
262func ja_archive_locked(journal: *u8, archive_dir: *u8, epoch: i64, rep: *i64) -> i64 {
263 let frame: *u8 = sys_mmap_shared(JA_FRAME_BYTES)
264 if (frame as i64) <= 0 { return 4 }
265 let rc: i64 = ja_archive_in_frame(journal, archive_dir, epoch, rep, frame)
266 let freed: i64 = sys_munmap(frame, JA_FRAME_BYTES)
267 if freed != 0 { ja_puts("NX-JRNL-ARCHIVE workspace-release-failed; transaction outcome retained in report\n" as *u8); return 10 }
268 return rc
269}
270func ja_archive_cooperative(journal: *u8, archive_dir: *u8, epoch: i64, rep: *i64) -> i64 {
271 let lock: i64 = ja_lock(journal)
272 if lock < 0 { return ja_announce(journal, 0 - 1, "-" as *u8, "-" as *u8, 0, 0, 7, "LOCK-UNAVAILABLE (nothing touched; retry after writer)" as *u8, rep) }
273 let rc: i64 = ja_archive_locked(journal, archive_dir, epoch, rep)
274 let unlocked: i64 = ja_unlock(lock)
275 if unlocked != 0 {
276 ja_puts("NX-JRNL-ARCHIVE lock-release-failed; prior_transaction_rc=" as *u8); ja_pn(rc)
277 ja_puts(" (do not retry close; transaction outcome retained in report)\n" as *u8)
278 return 10
279 }
280 return rc
281}
282// Existing public contract stays refused until an actual writer-inventory migration gate is adopted.
283func ja_archive(journal: *u8, archive_dir: *u8, epoch: i64, rep: *i64) -> i64 {
284 return ja_announce(journal, 0 - 1, "-" as *u8, "-" as *u8, 0, 0, 7, "WRITER-MIGRATION-UNVERIFIED (public reset disabled; no live bytes touched)" as *u8, rep)
285}