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}