nx_jrnl_archive_gate_t138.nx source
↩ module page · 319 lines · 17299 B
1// nx_jrnl_archive_gate.nx -- GATE for nx_jrnl_archive (lib composed in-process, nothing forked).
2// The verb's whole contract is PRESERVE-BEFORE-REMOVE, so the load-bearing teeth are the ones where
3// the archive step FAILS and the live journal must come out byte-identical (T6), and the one where
4// the verify step can actually see a corrupted copy (T10 -- a hash compare that cannot fail is not a
5// verify). Fixtures live under /tmp/nx_jrnl_archive_gate_<pid>/ (data only; nothing is executed from
6// /tmp, which the NAS mounts noexec). license_tier: ORIGINAL No hw writes (Rule 26).
7import "nx_syscalls.nx"
8import "nx_gate_verdict.nx"
9import "nx_jrnl_archive_lib_t138.nx"
10
11const JAG_FIX_LINES: i64 = 40 // enough rows that a short write is detectable, small enough to read by eye
12const JAG_EPOCH_A: i64 = 1700000001 // fixed epochs: archive names must be reproducible across runs
13const JAG_EPOCH_B: i64 = 1700000002
14const JAG_EPOCH_C: i64 = 1700000003
15
16func jg_pid() -> i64 { return __syscall(172, 0, 0, 0, 0, 0, 0) }
17func jg_path(base: *u8, leaf: *u8) -> *u8 {
18 let p: *u8 = sys_mmap(JA_PATH_MAX)
19 var o: i64 = ja_cat(p, 0, base)
20 o = ja_cat(p, o, "/" as *u8)
21 o = ja_cat(p, o, leaf)
22 return p
23}
24func jg_size(path: *u8) -> i64 {
25 let ln: *i64 = sys_mmap(16) as *i64
26 let b: *u8 = sys_read_file(path, ln)
27 if (b as i64) == 0 { return 0 - 1 }
28 let n: i64 = ln[0]
29 sys_free_file(b, n)
30 return n
31}
32func jg_digest(path: *u8, out: *u8) -> i64 {
33 let ln: *i64 = sys_mmap(16) as *i64
34 let b: *u8 = sys_read_file(path, ln)
35 if (b as i64) == 0 { return 0 - 1 }
36 let n: i64 = ln[0]
37 sha256_digest(b, n, out)
38 sys_free_file(b, n)
39 return n
40}
41func jg_same(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while i < JA_DIGEST_BYTES { if a[i] != b[i] { return 0 } i = i + 1 } return 1 }
42// a journal fixture: JAG_FIX_LINES rows "epoch<TAB>target<TAB>bytes" -- the comparepub shape
43func jg_make_journal(path: *u8) -> i64 {
44 let buf: *u8 = sys_mmap(JAG_FIX_LINES * 64)
45 var o: i64 = 0
46 var i: i64 = 0
47 while i < JAG_FIX_LINES {
48 o = ja_catn(buf, o, 1787500000 + i)
49 buf[o] = 9 as u8; o = o + 1
50 o = ja_cat(buf, o, "../sites/nishifamily/compare/fixture/api.json" as *u8)
51 buf[o] = 9 as u8; o = o + 1
52 o = ja_catn(buf, o, 1000 + i * 7)
53 buf[o] = 10 as u8; o = o + 1
54 i = i + 1
55 }
56 return ja_write_all(path, buf, o, JA_ARCH_MODE)
57}
58
59// One child writer, explicit pipe acknowledgements. Timeout bounds failures, never schedules success.
60const JAG_SYNC_TIMEOUT_MS: i64 = 10000
61func jg_receive(fd: i64, expected: i64) -> i64 {
62 let pollfd: *i64 = sys_mmap(16) as *i64
63 pollfd[0] = (fd & 0xFFFFFFFF) | (1 << 32)
64 if sys_poll(pollfd as *u8, 1, JAG_SYNC_TIMEOUT_MS) <= 0 { return 0 }
65 let byte: *u8 = sys_mmap(1)
66 if sys_read(fd, byte, 1) != 1 { return 0 }
67 return (byte[0] as i64) == expected
68}
69func jg_child_writer(journal: *u8, commands: i64, events: i64) -> i64 {
70 let lock: i64 = ja_lock(journal)
71 if lock < 0 { return 1 }
72 if sys_write(events, "A" as *u8, 1) != 1 { ja_unlock(lock); return 2 }
73 if jg_receive(commands, 82) != 1 { ja_unlock(lock); return 3 }
74 if ja_unlock(lock) != 0 { return 4 }
75 if sys_write(events, "B" as *u8, 1) != 1 { return 5 }
76 if jg_receive(commands, 84) != 1 { return 6 }
77 let busy: i64 = ja_lock(journal)
78 if busy >= 0 { ja_unlock(busy); return 7 }
79 if sys_write(events, "C" as *u8, 1) != 1 { return 8 }
80 if jg_receive(commands, 87) != 1 { return 9 }
81 let writer: i64 = ja_lock(journal)
82 if writer < 0 { return 10 }
83 let fd: i64 = sys_openat_append(journal, JA_ARCH_MODE)
84 if fd < 0 { ja_unlock(writer); return 11 }
85 let wrote: i64 = sys_write(fd, "CHILD-AFTER-ARCHIVE\n" as *u8, 20)
86 let sync: i64 = sys_fsync(fd)
87 let closed: i64 = sys_close(fd)
88 let unlocked: i64 = ja_unlock(writer)
89 if wrote != 20 || sync != 0 || closed != 0 || unlocked != 0 { return 12 }
90 if sys_write(events, "D" as *u8, 1) != 1 { return 13 }
91 return 0
92}
93func jg_process_gate(base: *u8, ctr: *i64) -> i64 {
94 let journal: *u8 = jg_path(base, "process.jrnl" as *u8)
95 jg_make_journal(journal)
96 let initial: i64 = jg_size(journal)
97 let commands: *i64 = sys_mmap(16) as *i64
98 let events: *i64 = sys_mmap(16) as *i64
99 if sys_pipe2(commands, 0) != 0 { return 0 }
100 let cr: i64 = commands[0] & 0xFFFFFFFF
101 let cw: i64 = (commands[0] >> 32) & 0xFFFFFFFF
102 if sys_pipe2(events, 0) != 0 { sys_close(cr); sys_close(cw); return 0 }
103 let er: i64 = events[0] & 0xFFFFFFFF
104 let ew: i64 = (events[0] >> 32) & 0xFFFFFFFF
105 let pid: i64 = sys_fork()
106 if pid < 0 { sys_close(cr); sys_close(cw); sys_close(er); sys_close(ew); return 0 }
107 if pid == 0 {
108 sys_close(cw); sys_close(er)
109 let result: i64 = jg_child_writer(journal, cr, ew)
110 sys_close(cr); sys_close(ew)
111 sys_exit(result); return result
112 }
113 sys_close(cr); sys_close(ew)
114 let rep: *i64 = sys_mmap(32) as *i64
115 var sequence: i64 = jg_receive(er, 65)
116 var blocked_archive: i64 = 0
117 var blocked_writer: i64 = 0
118 var archived: i64 = 0
119 var appended: i64 = 0
120 if sequence == 1 {
121 let busy: i64 = ja_archive_cooperative(journal, 0 as *u8, JAG_EPOCH_A, rep)
122 blocked_archive = (busy == 7) & (jg_size(journal) == initial)
123 if sys_write(cw, "R" as *u8, 1) != 1 { sequence = 0 }
124 if sequence == 1 { sequence = jg_receive(er, 66) }
125 }
126 var held: i64 = 0 - 1
127 if sequence == 1 {
128 held = ja_lock(journal)
129 if held < 0 { sequence = 0 } else {
130 if sys_write(cw, "T" as *u8, 1) != 1 { sequence = 0 }
131 if sequence == 1 { sequence = jg_receive(er, 67); blocked_writer = sequence }
132 }
133 }
134 if sequence == 1 {
135 let done: i64 = ja_archive_locked(journal, 0 as *u8, JAG_EPOCH_A, rep)
136 archived = (done == 0) & (rep[0] == initial) & (jg_size(journal) == 0)
137 }
138 if held >= 0 { if ja_unlock(held) != 0 { sequence = 0 } }
139 if sequence == 1 {
140 if sys_write(cw, "W" as *u8, 1) != 1 { sequence = 0 }
141 if sequence == 1 { sequence = jg_receive(er, 68) }
142 if sequence == 1 {
143 let actual: *u8 = sys_mmap(JA_DIGEST_BYTES)
144 let wanted: *u8 = sys_mmap(JA_DIGEST_BYTES)
145 let n: *i64 = sys_mmap(16) as *i64
146 sha256_digest("CHILD-AFTER-ARCHIVE\n" as *u8, 20, wanted)
147 if ja_transfer(journal, 0 - 1, actual, n) == 0 { appended = (n[0] == 20) & ja_digest_same(actual, wanted) }
148 }
149 }
150 sys_close(cw); sys_close(er)
151 if sequence == 0 { nx_kill(pid, 9) }
152 let status: *i64 = sys_mmap(16) as *i64
153 let reaped: i64 = sys_wait4(pid, status, 0)
154 gv_check("T25-independent-writer-held-lock-refuses-archive-preserves-live" as *u8, blocked_archive, ctr)
155 gv_check("T26-independent-writer-refused-while-parent-archive-holds-lock" as *u8, blocked_writer & archived, ctr)
156 gv_check("T27-independent-writer-after-unlock-durable-row-retained-exactly" as *u8, appended & (reaped == pid) & (wait_status_rc(status[0]) == 0), ctr)
157 return sequence
158}
159
160func main(argc: i64, argv: *i64) -> i64 {
161 sys_ignore_sigpipe()
162 gv_head("=== nx_jrnl_archive_gate -- the archive verb is cooperative-lock durable exclusive archive ===" as *u8)
163 let ctr: *i64 = gv_ctr()
164 let base: *u8 = sys_mmap(JA_PATH_MAX)
165 var bo: i64 = ja_cat(base, 0, "/tmp/nx_jrnl_archive_gate_" as *u8)
166 bo = ja_catn(base, bo, jg_pid())
167 sys_mkdir(base, JA_DIR_MODE)
168 let jr: *u8 = jg_path(base, "fixture.jrnl" as *u8)
169 let mk: i64 = jg_make_journal(jr)
170 let n0: i64 = jg_size(jr)
171 ja_puts(" fixture_bytes=" as *u8); ja_pn(n0); ja_puts("\n" as *u8)
172 var reached: i64 = 0
173 if mk == 0 { if n0 > 0 { reached = 1 } }
174 gv_check("T1 fixture-reached-the-condition (journal written, non-empty)" as *u8, reached, ctr)
175 let d0: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
176 jg_digest(jr, d0)
177 let rep: *i64 = sys_mmap(32) as *i64
178
179 // ---- the happy path: archive, verify, reset ----
180 let rcA: i64 = ja_archive_cooperative(jr, 0 as *u8, JAG_EPOCH_A, rep)
181 gv_check("T2 archive-returns-OK" as *u8, rcA == 0, ctr)
182 let archA: *u8 = jg_path(base, "archive/fixture.jrnl.1700000001" as *u8)
183 let dA: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
184 let nA: i64 = jg_digest(archA, dA)
185 ja_puts(" archived_bytes=" as *u8); ja_pn(nA); ja_puts(" original_bytes=" as *u8); ja_pn(n0); ja_puts("\n" as *u8)
186 var sameA: i64 = 0
187 if nA == n0 { if jg_same(d0, dA) == 1 { sameA = 1 } }
188 gv_check("T3 archived-copy-byte-identical-to-original (re-read from disk, sha256 equal)" as *u8, sameA, ctr)
189 let nLive: i64 = jg_size(jr)
190 ja_puts(" live_bytes_after=" as *u8); ja_pn(nLive); ja_puts("\n" as *u8)
191 gv_check("T4 live-journal-reset-to-zero-bytes" as *u8, nLive == 0, ctr)
192 var repok: i64 = 0
193 if rep[0] == n0 { if rep[1] == 1 { if rep[2] == 1 { repok = 1 } } }
194 gv_check("T5 announce-carries-bytes-verified-reset (report slots match the measurement)" as *u8, repok, ctr)
195
196 // ---- neg-control: an unwritable archive dir must leave the live journal byte-identical ----
197 let jr2: *u8 = jg_path(base, "fixture2.jrnl" as *u8)
198 jg_make_journal(jr2)
199 let d2: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
200 let n2: i64 = jg_digest(jr2, d2)
201 // a directory path UNDER A FILE cannot be created or written into (ENOTDIR)
202 let baddir: *u8 = jg_path(base, "fixture2.jrnl/notadir" as *u8)
203 let rcB: i64 = ja_archive_cooperative(jr2, baddir, JAG_EPOCH_B, rep)
204 let d2b: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
205 let n2b: i64 = jg_digest(jr2, d2b)
206 var untouched: i64 = 0
207 if n2b == n2 { if jg_same(d2, d2b) == 1 { untouched = 1 } }
208 ja_puts(" unwritable_dir_rc=" as *u8); ja_pn(rcB); ja_puts(" live_bytes_before=" as *u8); ja_pn(n2); ja_puts(" after=" as *u8); ja_pn(n2b); ja_puts("\n" as *u8)
209 gv_check("T6 neg-control-archive-write-failure-leaves-live-journal-byte-identical (rc 4, preserve-before-remove)" as *u8, untouched, ctr)
210 gv_check("T6b neg-control-archive-write-failure-returns-4" as *u8, rcB == 4, ctr)
211
212 // ---- neg-control: a missing journal is rc 3 and creates nothing ----
213 let jr3: *u8 = jg_path(base, "does_not_exist.jrnl" as *u8)
214 let rcC: i64 = ja_archive_cooperative(jr3, 0 as *u8, JAG_EPOCH_C, rep)
215 let arch3: *u8 = jg_path(base, "archive/does_not_exist.jrnl.1700000003" as *u8)
216 gv_check("T7 neg-control-missing-journal-returns-3" as *u8, rcC == 3, ctr)
217 gv_check("T7b neg-control-missing-journal-creates-no-archive" as *u8, jg_size(arch3) < 0, ctr)
218
219 // ---- an empty journal: nothing to do, rc 1, no file made, live still empty ----
220 let rcD: i64 = ja_archive_cooperative(jr, 0 as *u8, JAG_EPOCH_B, rep)
221 let archD: *u8 = jg_path(base, "archive/fixture.jrnl.1700000002" as *u8)
222 gv_check("T8 empty-journal-returns-1-nothing-to-archive" as *u8, rcD == 1, ctr)
223 gv_check("T8b empty-journal-creates-no-archive-file" as *u8, jg_size(archD) < 0, ctr)
224 gv_check("T8c empty-journal-stays-empty" as *u8, jg_size(jr) == 0, ctr)
225
226 // ---- the verify tooth can FAIL: corrupt one byte of the archived copy and re-hash ----
227 let ln: *i64 = sys_mmap(16) as *i64
228 let ab: *u8 = sys_read_file(archA, ln)
229 var corrupt_seen: i64 = 0
230 if (ab as i64) != 0 { if ln[0] > 0 {
231 if ab[0] == (48 as u8) { ab[0] = 49 as u8 } else { ab[0] = 48 as u8 }
232 ja_write_all(archA, ab, ln[0], JA_ARCH_MODE)
233 let dC: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
234 jg_digest(archA, dC)
235 if jg_same(d0, dC) == 0 { corrupt_seen = 1 }
236 } }
237 gv_check("T9 neg-control-one-byte-corruption-of-the-archive-changes-the-digest (the verify compare is not vacuous)" as *u8, corrupt_seen, ctr)
238
239 // ---- second fixture, named dir: the archive_dir argument is honoured ----
240 let jr4: *u8 = jg_path(base, "fixture4.jrnl" as *u8)
241 jg_make_journal(jr4)
242 let dir4: *u8 = jg_path(base, "elsewhere" as *u8)
243 let rcE: i64 = ja_archive_cooperative(jr4, dir4, JAG_EPOCH_A, rep)
244 let arch4: *u8 = jg_path(base, "elsewhere/fixture4.jrnl.1700000001" as *u8)
245 var dir_ok: i64 = 0
246 if rcE == 0 { if jg_size(arch4) > 0 { dir_ok = 1 } }
247 gv_check("T10 named-archive-dir-is-honoured (file lands there, rc 0)" as *u8, dir_ok, ctr)
248
249 // Stable sidecar contention must reject archive without changing the journal.
250 let jr5: *u8 = jg_path(base, "locked.jrnl" as *u8)
251 jg_make_journal(jr5)
252 let before5: i64 = jg_size(jr5)
253 let held: i64 = ja_lock(jr5)
254 gv_check("T11 lock-acquired-on-real-filesystem" as *u8, held >= 0, ctr)
255 let busy: i64 = ja_archive_cooperative(jr5, 0 as *u8, JAG_EPOCH_A, rep)
256 gv_check("T12 held-writer-lock-refuses-archive-with-live-intact" as *u8, (busy == 7) & (jg_size(jr5) == before5), ctr)
257 ja_unlock(held)
258 let released: i64 = ja_archive_cooperative(jr5, 0 as *u8, JAG_EPOCH_A, rep)
259 gv_check("T13 released-lock-allows-durable-archive" as *u8, released == 0, ctr)
260 let again: i64 = ja_archive_cooperative(jr5, 0 as *u8, JAG_EPOCH_A, rep)
261 gv_check("T14-repeat-empty-is-idempotent" as *u8, again == 1, ctr)
262 // Same-second reuse must not truncate the old archive even when new journal bytes differ.
263 let arch5: *u8 = jg_path(base, "archive/locked.jrnl.1700000001" as *u8)
264 let saved5: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
265 jg_digest(arch5, saved5)
266 ja_write_all(jr5, "NEW-UNARCHIVED-HISTORY\n" as *u8, 23, JA_ARCH_MODE)
267 let collision: i64 = ja_archive_cooperative(jr5, 0 as *u8, JAG_EPOCH_A, rep)
268 let after5: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
269 jg_digest(arch5, after5)
270 gv_check("T15-epoch-collision-preserves-prior-archive-and-new-live-data" as *u8, (collision == 4) & (jg_size(jr5) == 23) & jg_same(saved5, after5), ctr)
271 let strictlen: *i64 = sys_mmap(16) as *i64
272 let strict_digest: *u8 = sys_mmap(JA_DIGEST_BYTES)
273 let strict: i64 = ja_transfer(base, 0 - 1, strict_digest, strictlen)
274 gv_check("T16-directory-is-not-successful-empty-journal-read" as *u8, strict != 0, ctr)
275 gv_check("T17-directory-sync-errors-are-observable" as *u8, ja_sync_dir(jr5) < 0, ctr)
276 let refused: i64 = ja_archive(jr5, 0 as *u8, JAG_EPOCH_B, rep)
277 gv_check("T18-public-entry-refuses-without-writer-migration-proof" as *u8, (refused == 7) & (jg_size(jr5) == 23), ctr)
278 let writer: i64 = ja_lock(jr5)
279 let append: i64 = sys_openat_append(jr5, JA_ARCH_MODE)
280 let appended: i64 = sys_write(append, "AFTER\n" as *u8, 6)
281 let durable: i64 = sys_fsync(append)
282 sys_close(append)
283 ja_unlock(writer)
284 gv_check("T19-cooperating-writer-after-archive-retains-new-row" as *u8, (writer >= 0) & (appended == 6) & (durable == 0) & (jg_size(jr5) == 29), ctr)
285 let big: *u8 = jg_path(base, "stream.jrnl" as *u8)
286 let fsobs: *i64 = sys_mmap(144) as *i64
287 sys_statfs(base, fsobs)
288 let stream_bytes: i64 = 1048576 + fsobs[1] + 63
289 let payload: *u8 = sys_mmap(stream_bytes)
290 var ix: i64 = 0
291 while ix < stream_bytes { payload[ix] = (ix % 251) as u8; ix = ix + 1 }
292 ja_write_all(big, payload, stream_bytes, JA_ARCH_MODE)
293 let oracle: *u8 = sys_mmap(JA_DIGEST_BYTES)
294 sha256_digest(payload, stream_bytes, oracle)
295 let streaming: *u8 = sys_mmap(JA_DIGEST_BYTES)
296 let total: *i64 = sys_mmap(16) as *i64
297 let streamrc: i64 = ja_transfer(big, 0 - 1, streaming, total)
298 ja_puts(" streaming_fixture_bytes=" as *u8); ja_pn(stream_bytes)
299 ja_puts(" transfer_buffer_bytes=" as *u8); ja_pn(fsobs[1])
300 ja_puts(" sha_workspace_bytes=" as *u8); ja_pn(sha256_workspace_bytes()); ja_puts("\n" as *u8)
301 gv_check("T20-streaming-over-old-1MiB-budget-matches-one-shot-sha-across-boundaries" as *u8, (streamrc == 0) & (total[0] == stream_bytes) & ja_digest_same(oracle, streaming), ctr)
302 let bigrc: i64 = ja_archive_cooperative(big, 0 as *u8, JAG_EPOCH_A, rep)
303 gv_check("T21-streaming-archive-larger-than-old-budget-verifies-and-resets" as *u8, (bigrc == 0) & (rep[0] == stream_bytes) & (jg_size(big) == 0), ctr)
304 let shortfd: i64 = sys_openat_rd(jr5)
305 let short_read: i64 = ja_transfer_fd(shortfd, 0 - 1, 30, fsobs[1], streaming)
306 sys_close(shortfd)
307 gv_check("T22-early-EOF-is-refused-not-a-successful-prefix" as *u8, short_read != 0, ctr)
308 let read_error: i64 = ja_transfer_fd(0 - 1, 0 - 1, 1, fsobs[1], streaming)
309 gv_check("T23-real-EBADF-read-error-is-refused" as *u8, read_error != 0, ctr)
310 let input: i64 = sys_openat_rd(jr5)
311 let full: i64 = sys_openat_wr("/dev/full" as *u8, JA_ARCH_MODE)
312 let write_error: i64 = ja_transfer_fd(input, full, 29, fsobs[1], streaming)
313 sys_close(input); sys_close(full)
314 gv_check("T24-real-ENOSPC-destination-error-is-refused-with-source-intact" as *u8, (full >= 0) & (write_error != 0) & (jg_size(jr5) == 29), ctr)
315 gv_check("T28-independent-process-handshake-completed-without-time-based-ordering" as *u8, jg_process_gate(base, ctr), ctr)
316 let nomem: i64 = ja_transfer_fd(0 - 1, 0 - 1, 1, SHA256_SIGNED_MAX - sha256_workspace_bytes(), streaming)
317 gv_check("T29-real-kernel-mapping-refusal-returns-structured-error" as *u8, nomem == (0 - 2), ctr)
318 return gv_verdict("JRNL-ARCHIVE" as *u8, ctr, "cooperative writers serialized; collisions and durability failure preserve history" as *u8)
319}