code wiki / _hdl_build / nx_gpt_relocate.nx

nx_gpt_relocate.nx source

↩ module page · 318 lines · 15747 B

1// nx_gpt_relocate.nx -- FIX THE WRITE, NOT THE IMAGE (debt 1785972535). 2// 3// An image is a FIXED-SIZE disk. A USB stick is usually BIGGER. So a raw byte-for-byte write leaves 4// the BACKUP GPT sitting in the middle of the device instead of at its final LBA, and the primary 5// header still describes the image's geometry. Every gate we had passed because every gate judged 6// the IMAGE -- but the artifact that ships is the DEVICE. Measured on a real stick: 7,987,200 device 7// sectors vs 135,269 image sectors; the disk came up GPT with the ESP visible and the data partition 8// missing entirely. 9// 10// This organ makes a written device's GPT describe THAT DEVICE: 11// * primary header : AlternateLBA = last LBA, LastUsableLBA = last - 33, header CRC32 recomputed 12// * backup entry array: copied to (last - 32) 13// * backup header : written at the last LBA, MyLBA/AlternateLBA/PartitionEntryLBA corrected 14// * the STALE backup : zeroed where it used to sit -- a leftover secondary GPT mid-device is a 15// landmine, because a future repair tool can "restore" the old geometry from 16// it and silently shrink the disk back. 17// It REFUSES rather than corrupting: no "EFI PART" signature, a device smaller than the image, or a 18// partition that would extend past the new last-usable LBA all stop before a single byte is written. 19// 20// HOST-INDEPENDENT BY CONSTRUCTION: it seeks and writes a block device or a file through plain 21// syscalls, so the same organ serves Linux, Nishi OS itself, and any host that can run our ELF; hosts 22// that cannot (Windows today) apply the identical byte ranges, and `plan` prints exactly those ranges 23// so the two can be compared instead of trusted. One brain, many hands. 24// 25// usage: nx_gpt_relocate selftest 26// nx_gpt_relocate plan <path> <device_sectors> (prints the byte ranges, writes nothing) 27// nx_gpt_relocate apply <path> <device_sectors> (writes, then RE-READS and verifies) 28// license_tier: ORIGINAL expect_exit: 0 29import "nx_syscalls.nx" 30 31const SEC: i64 = 512 32const GPT_ENTRIES: i64 = 128 33const GPT_ENTSZ: i64 = 128 34const GPT_ARR_SEC: i64 = 32 35const GPT_HDR_SZ: i64 = 92 36const HDR_ALT: i64 = 32 37const HDR_LASTUSE: i64 = 48 38const HDR_ARRLBA: i64 = 72 39const HDR_CRC: i64 = 16 40const HDR_MY: i64 = 24 41 42func p(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 43func pn(v: i64) -> i64 { 44 var m: i64 = v; if m < 0 { p("-" as *u8); m = 0 - m } 45 let t: *u8 = sys_mmap(24); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } 46 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 47 let o: *u8 = sys_mmap(24); var i: i64 = 0 48 while i < k { o[i] = t[k-1-i]; i = i + 1 } 49 sys_write(1, o, k); return 0 50} 51func r32(b: *u8, o: i64) -> i64 { 52 return (b[o] as i64) | ((b[o+1] as i64)<<8) | ((b[o+2] as i64)<<16) | ((b[o+3] as i64)<<24) 53} 54func r64(b: *u8, o: i64) -> i64 { 55 var v: i64 = 0; var i: i64 = 0 56 while i < 8 { v = v | ((b[o+i] as i64) << (8*i)); i = i + 1 } 57 return v 58} 59func w32(b: *u8, o: i64, v: i64) -> i64 { 60 b[o]=(v&0xff) as u8; b[o+1]=((v>>8)&0xff) as u8; b[o+2]=((v>>16)&0xff) as u8; b[o+3]=((v>>24)&0xff) as u8 61 return 0 62} 63func w64(b: *u8, o: i64, v: i64) -> i64 { w32(b,o,v & 0xFFFFFFFF); w32(b,o+4,(v>>32) & 0xFFFFFFFF); return 0 } 64 65// CRC32 (IEEE, reflected) -- restated here rather than imported so a change in another lane can 66// never silently alter a partition table. 67func crc32(b: *u8, off: i64, n: i64) -> i64 { 68 var crc: i64 = 0xFFFFFFFF 69 var i: i64 = 0 70 while i < n { 71 crc = crc ^ ((b[off+i] as i64) & 0xff) 72 var bit: i64 = 0 73 while bit < 8 { 74 let lsb: i64 = crc & 1 75 let mask: i64 = 0 - lsb 76 crc = ((crc >> 1) & 0x7FFFFFFFFFFFFFFF) ^ (mask & 0xEDB88320) 77 crc = crc & 0xFFFFFFFF 78 bit = bit + 1 79 } 80 i = i + 1 81 } 82 return (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF 83} 84func is_efipart(b: *u8, o: i64) -> i64 { 85 let s: *u8 = "EFI PART" as *u8 86 var i: i64 = 0 87 while i < 8 { if (b[o+i] as i64) != (s[i] as i64) { return 0 } i = i + 1 } 88 return 1 89} 90// read n bytes at byte offset off; returns 1 ok 91func pread(fd: i64, off: i64, buf: *u8, n: i64) -> i64 { 92 if sys_lseek(fd, off, 0) < 0 { return 0 } 93 var got: i64 = 0 94 while got < n { 95 let r: i64 = sys_read(fd, ((buf as i64) + got) as *u8, n - got) 96 if r <= 0 { return 0 } 97 got = got + r 98 } 99 return 1 100} 101func pwrite(fd: i64, off: i64, buf: *u8, n: i64) -> i64 { 102 if sys_lseek(fd, off, 0) < 0 { return 0 } 103 var put: i64 = 0 104 while put < n { 105 let r: i64 = sys_write(fd, ((buf as i64) + put) as *u8, n - put) 106 if r <= 0 { return 0 } 107 put = put + r 108 } 109 return 1 110} 111 112// Rewrite one header in place: set the three geometry fields, zero the CRC field, recompute, store. 113func fix_hdr(h: *u8, my_lba: i64, alt_lba: i64, arr_lba: i64, last_use: i64) -> i64 { 114 w64(h, HDR_MY, my_lba) 115 w64(h, HDR_ALT, alt_lba) 116 w64(h, HDR_ARRLBA, arr_lba) 117 w64(h, HDR_LASTUSE, last_use) 118 w32(h, HDR_CRC, 0) 119 let c: i64 = crc32(h, 0, GPT_HDR_SZ) 120 w32(h, HDR_CRC, c) 121 return c 122} 123 124// The whole job. mode 0 = plan (writes nothing), 1 = apply. Returns 0 ok, negative = refusal. 125func relocate(path: *u8, dev_sectors: i64, mode: i64) -> i64 { 126 let fd: i64 = sys_openat_rdwr(path, 420) 127 if fd < 0 { p("GPTRELOC REFUSED: cannot open " as *u8); p(path); p("\n" as *u8); return 0 - 1 } 128 let hdr: *u8 = sys_mmap(SEC) 129 let arr: *u8 = sys_mmap(GPT_ARR_SEC * SEC) 130 let zero: *u8 = sys_mmap(GPT_ARR_SEC * SEC) 131 if pread(fd, SEC, hdr, SEC) == 0 { p("GPTRELOC REFUSED: cannot read the primary header\n" as *u8); sys_close(fd); return 0 - 2 } 132 if is_efipart(hdr, 0) == 0 { p("GPTRELOC REFUSED: no EFI PART signature -- this is not a GPT disk\n" as *u8); sys_close(fd); return 0 - 3 } 133 let old_alt: i64 = r64(hdr, HDR_ALT) 134 let arr_lba: i64 = r64(hdr, HDR_ARRLBA) 135 let nent: i64 = r32(hdr, 80) 136 let entsz: i64 = r32(hdr, 84) 137 if nent != GPT_ENTRIES { p("GPTRELOC REFUSED: unexpected entry count\n" as *u8); sys_close(fd); return 0 - 4 } 138 if entsz != GPT_ENTSZ { p("GPTRELOC REFUSED: unexpected entry size\n" as *u8); sys_close(fd); return 0 - 4 } 139 if pread(fd, arr_lba * SEC, arr, GPT_ARR_SEC * SEC) == 0 { p("GPTRELOC REFUSED: cannot read the entry array\n" as *u8); sys_close(fd); return 0 - 5 } 140 if r32(hdr, 88) != crc32(arr, 0, GPT_ENTRIES * GPT_ENTSZ) { 141 p("GPTRELOC REFUSED: entry-array CRC mismatch -- refusing to relocate a table we cannot trust\n" as *u8) 142 sys_close(fd); return 0 - 6 143 } 144 let new_last: i64 = dev_sectors - 1 145 let new_bak_arr: i64 = new_last - GPT_ARR_SEC 146 let new_last_use: i64 = new_last - GPT_ARR_SEC - 1 147 if dev_sectors < (old_alt + 1) { 148 p("GPTRELOC REFUSED: device is SMALLER than the image it holds (" as *u8); pn(dev_sectors) 149 p(" < " as *u8); pn(old_alt + 1); p(" sectors)\n" as *u8) 150 sys_close(fd); return 0 - 7 151 } 152 // No partition may extend past the new last-usable LBA. Checked BEFORE any write: growing a disk 153 // must never be able to truncate a partition. 154 var i: i64 = 0 155 var maxend: i64 = 0 156 var used: i64 = 0 157 while i < GPT_ENTRIES { 158 let e: i64 = i * GPT_ENTSZ 159 var nz: i64 = 0 160 var j: i64 = 0 161 while j < 16 { if (arr[e+j] as i64) != 0 { nz = 1 } j = j + 1 } 162 if nz == 1 { 163 used = used + 1 164 let end: i64 = r64(arr, e + 40) 165 if end > maxend { maxend = end } 166 } 167 i = i + 1 168 } 169 if maxend > new_last_use { 170 p("GPTRELOC REFUSED: a partition ends at LBA " as *u8); pn(maxend) 171 p(" which is past the new last usable " as *u8); pn(new_last_use); p("\n" as *u8) 172 sys_close(fd); return 0 - 8 173 } 174 p("GPTRELOC path=" as *u8); p(path); p(" partitions=" as *u8); pn(used) 175 p(" device_sectors=" as *u8); pn(dev_sectors); p(" old_backup_lba=" as *u8); pn(old_alt) 176 p(" new_backup_lba=" as *u8); pn(new_last); p("\n" as *u8) 177 if old_alt == new_last { 178 p("GPTRELOC NOTHING-TO-DO: the backup GPT is already at the device's last LBA\n" as *u8) 179 sys_close(fd); return 0 180 } 181 // Build the corrected structures in memory first, so `plan` and `apply` describe the SAME bytes. 182 let bak: *u8 = sys_mmap(SEC) 183 var bi: i64 = 0 184 while bi < SEC { bak[bi] = hdr[bi]; bi = bi + 1 } 185 fix_hdr(hdr, 1, new_last, arr_lba, new_last_use) 186 fix_hdr(bak, new_last, 1, new_bak_arr, new_last_use) 187 p(" range1 off=" as *u8); pn(SEC); p(" len=" as *u8); pn(SEC); p(" (primary header)\n" as *u8) 188 p(" range2 off=" as *u8); pn(new_bak_arr * SEC); p(" len=" as *u8); pn(GPT_ARR_SEC * SEC); p(" (backup entry array)\n" as *u8) 189 p(" range3 off=" as *u8); pn(new_last * SEC); p(" len=" as *u8); pn(SEC); p(" (backup header)\n" as *u8) 190 p(" range4 off=" as *u8); pn((old_alt - GPT_ARR_SEC) * SEC); p(" len=" as *u8); pn((GPT_ARR_SEC + 1) * SEC) 191 p(" (ZERO the stale backup -- a leftover secondary GPT is a landmine a repair tool will restore from)\n" as *u8) 192 if mode == 0 { p("GPTRELOC PLAN-ONLY: nothing written\n" as *u8); sys_close(fd); return 0 } 193 // Order matters: write the NEW backup first, then the primary. If power is lost midway, a disk 194 // whose primary still points at the old backup is recoverable; one whose primary points at a 195 // backup that was never written is not. 196 if pwrite(fd, new_bak_arr * SEC, arr, GPT_ARR_SEC * SEC) == 0 { p("GPTRELOC RED: backup array write failed\n" as *u8); sys_close(fd); return 0 - 9 } 197 if pwrite(fd, new_last * SEC, bak, SEC) == 0 { p("GPTRELOC RED: backup header write failed\n" as *u8); sys_close(fd); return 0 - 9 } 198 if pwrite(fd, SEC, hdr, SEC) == 0 { p("GPTRELOC RED: primary header write failed\n" as *u8); sys_close(fd); return 0 - 9 } 199 if pwrite(fd, (old_alt - GPT_ARR_SEC) * SEC, zero, (GPT_ARR_SEC + 1) * SEC) == 0 { p("GPTRELOC RED: stale-backup zero failed\n" as *u8); sys_close(fd); return 0 - 9 } 200 sys_fsync(fd) 201 // RE-READ and verify: the only evidence that counts is what the device says afterwards. 202 let vh: *u8 = sys_mmap(SEC) 203 let vb: *u8 = sys_mmap(SEC) 204 let va: *u8 = sys_mmap(GPT_ARR_SEC * SEC) 205 var ok: i64 = 1 206 if pread(fd, SEC, vh, SEC) == 0 { ok = 0 } 207 if pread(fd, new_last * SEC, vb, SEC) == 0 { ok = 0 } 208 if pread(fd, new_bak_arr * SEC, va, GPT_ARR_SEC * SEC) == 0 { ok = 0 } 209 if is_efipart(vh, 0) == 0 { ok = 0 } 210 if is_efipart(vb, 0) == 0 { ok = 0 } 211 if r64(vh, HDR_ALT) != new_last { ok = 0 } 212 if r64(vh, HDR_LASTUSE) != new_last_use { ok = 0 } 213 if r64(vb, HDR_MY) != new_last { ok = 0 } 214 if r64(vb, HDR_ARRLBA) != new_bak_arr { ok = 0 } 215 let acrc: i64 = crc32(va, 0, GPT_ENTRIES * GPT_ENTSZ) 216 if r32(vh, 88) != acrc { ok = 0 } 217 if r32(vb, 88) != acrc { ok = 0 } 218 // both header CRCs must verify the way firmware checks them: zero the field, recompute 219 let sv1: i64 = r32(vh, HDR_CRC); w32(vh, HDR_CRC, 0) 220 if crc32(vh, 0, GPT_HDR_SZ) != sv1 { ok = 0 } 221 let sv2: i64 = r32(vb, HDR_CRC); w32(vb, HDR_CRC, 0) 222 if crc32(vb, 0, GPT_HDR_SZ) != sv2 { ok = 0 } 223 sys_close(fd) 224 if ok == 0 { p("GPTRELOC RED: read-back verification FAILED\n" as *u8); return 0 - 10 } 225 p("GPTRELOC GREEN: backup GPT now at the device's last LBA, both header CRCs verify on re-read\n" as *u8) 226 return 0 227} 228 229func selftest() -> i64 { 230 p("=== NX-GPT-RELOCATE SELFTEST ===\n" as *u8) 231 var pass: i64 = 0 232 var total: i64 = 0 233 let src: *u8 = "_offc/nishi-os.img" as *u8 234 let tmp: *u8 = "_build/gptreloc_test.img" as *u8 235 let lp: *i64 = sys_mmap(16) as *i64 236 let blob: *u8 = sys_read_file(src, lp) 237 let blen: i64 = lp[0] 238 if blen <= 0 { p("SELFTEST REFUSED: _offc/nishi-os.img absent -- build it first\n" as *u8); sys_exit(2); return 2 } 239 let img_sectors: i64 = blen / SEC 240 // A device 10x the image, which is the real-world case that produced the defect. 241 let dev_sectors: i64 = img_sectors * 10 242 let fd: i64 = sys_openat_wr(tmp, 420) 243 if fd < 0 { p("SELFTEST REFUSED: cannot create the scratch image\n" as *u8); sys_exit(2); return 2 } 244 sys_write(fd, blob, blen) 245 // grow the file to the device size, exactly as a raw write to a bigger stick leaves it 246 let pad: *u8 = sys_mmap(SEC) 247 sys_lseek(fd, dev_sectors * SEC - SEC, 0) 248 sys_write(fd, pad, SEC) 249 sys_close(fd) 250 251 total = total + 1 252 if relocate(tmp, dev_sectors, 1) == 0 { pass = pass + 1; p(" T1 relocate+verify GREEN\n" as *u8) } else { p(" T1 RED\n" as *u8) } 253 254 // T2 idempotent: running it again must report nothing to do, not shuffle the table around. 255 total = total + 1 256 if relocate(tmp, dev_sectors, 1) == 0 { pass = pass + 1; p(" T2 idempotent GREEN\n" as *u8) } else { p(" T2 RED\n" as *u8) } 257 258 // T3 the stale backup really is gone -- otherwise a repair tool restores the old geometry. 259 total = total + 1 260 let f2: i64 = sys_openat_rd(tmp) 261 let chk: *u8 = sys_mmap(SEC) 262 var t3: i64 = 0 263 if pread(f2, (img_sectors - 1) * SEC, chk, SEC) == 1 { if is_efipart(chk, 0) == 0 { t3 = 1 } } 264 sys_close(f2) 265 if t3 == 1 { pass = pass + 1; p(" T3 stale backup ZEROED GREEN\n" as *u8) } else { p(" T3 RED (a second EFI PART is still sitting mid-device)\n" as *u8) } 266 267 // T4 NEGATIVE: a device SMALLER than the image must be refused, never truncated into. 268 total = total + 1 269 if relocate(tmp, img_sectors / 2, 1) < 0 { pass = pass + 1; p(" T4 smaller-device REFUSED GREEN\n" as *u8) } else { p(" T4 RED (it accepted a device too small)\n" as *u8) } 270 271 // T5 NEGATIVE: a non-GPT target must be refused on the signature, before any write. 272 total = total + 1 273 let f3: i64 = sys_openat_wr("_build/gptreloc_notgpt.img" as *u8, 420) 274 let junk: *u8 = sys_mmap(SEC * 4) 275 sys_write(f3, junk, SEC * 4) 276 sys_close(f3) 277 if relocate("_build/gptreloc_notgpt.img" as *u8, 8, 1) < 0 { pass = pass + 1; p(" T5 non-GPT REFUSED GREEN\n" as *u8) } else { p(" T5 RED\n" as *u8) } 278 279 p("NX-GPT-RELOCATE selftest " as *u8); pn(pass); p("/" as *u8); pn(total); p("\n" as *u8) 280 if pass == total { p("NX-GPT-RELOCATE GREEN\n" as *u8); sys_exit(0); return 0 } 281 p("NX-GPT-RELOCATE RED\n" as *u8); sys_exit(1); return 1 282} 283 284func s_eq(a: *u8, b: *u8) -> i64 { 285 var i: i64 = 0 286 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 287 if b[i] != (0 as u8) { return 0 } 288 return 1 289} 290func atoi(s: *u8) -> i64 { 291 var v: i64 = 0; var i: i64 = 0 292 while s[i] != (0 as u8) { 293 let c: i64 = s[i] as i64 294 if c < 48 { return 0 - 1 } 295 if c > 57 { return 0 - 1 } 296 v = v * 10 + (c - 48); i = i + 1 297 } 298 return v 299} 300 301func main(argc: i64, argv: *i64) -> i64 { 302 if argc < 2 { 303 p("usage: nx_gpt_relocate selftest | plan <path> <device_sectors> | apply <path> <device_sectors>\n" as *u8) 304 sys_exit(2); return 2 305 } 306 let verb: *u8 = argv[1] as *u8 307 if s_eq(verb, "selftest" as *u8) == 1 { return selftest() } 308 if argc < 4 { p("GPTRELOC REFUSED: plan/apply need <path> <device_sectors>\n" as *u8); sys_exit(2); return 2 } 309 let path: *u8 = argv[2] as *u8 310 let ds: i64 = atoi(argv[3] as *u8) 311 if ds <= 0 { p("GPTRELOC REFUSED: device_sectors must be a positive integer\n" as *u8); sys_exit(2); return 2 } 312 var mode: i64 = 0 313 if s_eq(verb, "apply" as *u8) == 1 { mode = 1 } 314 else { if s_eq(verb, "plan" as *u8) == 0 { p("GPTRELOC REFUSED: unknown verb\n" as *u8); sys_exit(2); return 2 } } 315 let r: i64 = relocate(path, ds, mode) 316 if r < 0 { sys_exit(1); return 1 } 317 sys_exit(0); return 0 318}