code wiki / (root) / nx_jrnl_archive_lib_t138.nx

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}