fix: gpt_read checks every entry, and says when there are more than max (#180)

gpt_read() stopped at max used entries (16 for the identity code, 4 in the
mint): a disk whose StarshipOS partitions came after the first max read as
having none, and the entries after max were never range-checked.

Now every entry is checked and every used one counted; a good table with more
used entries than max is GPT_MANY (the first max in parts), and the identity
code says "GPT: a USB drive has more than 16 partitions -- not used". Other
callers treat anything but GPT_OK as before.

Tested first, seen to fail: qemu_session.py gpt_many -- a data stick with its
two partitions as entries 17 and 18 after 16 others, and the same with an entry
19 past the usable range: both "not this system's stick" before (silently cut,
entry 19 never checked; logs/20261112-162236). After: "more than 16
partitions" and "a damaged partition table" (logs/20261112-162921).
Regressions: gpt_bad_entries, gpt_foreign PASS. gpt_zuse FAILS, on a stale
expectation unrelated to this change -- an identity-only GPT drive with no
system stick has been held silently as a possible real Zuse since #92 Task 4;
its log is kept here. The gpt_many images are made before the boot: a first
version made them after, failed in sgdisk and left its QEMU running, stopped
by hand (logs/20261112-162003).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RzZgrf7V7Zk2iczBXDRbm
This commit is contained in:
Claude
2026-11-12 16:41:00 +00:00
parent ab63789e5b
commit 9700a7b404
16 changed files with 83 additions and 7 deletions
+6 -2
View File
@@ -26,10 +26,14 @@ typedef struct {
#define GPT_BAD 2 /* a protective MBR, but its header or entry array does not check out: not used */ #define GPT_BAD 2 /* a protective MBR, but its header or entry array does not check out: not used */
#define GPT_EIO 3 /* its first block could not be read: not used; gpt_write_one: nothing was written */ #define GPT_EIO 3 /* its first block could not be read: not used; gpt_write_one: nothing was written */
#define GPT_PART 4 /* gpt_write_one only: a write failed after others were made -- the disk is changed (#168) */ #define GPT_PART 4 /* gpt_write_one only: a write failed after others were made -- the disk is changed (#168) */
#define GPT_MANY 5 /* gpt_read: a valid GPT with more used entries than max -- every entry checked, none cut (#180) */
/* Read `disk`'s protective MBR, primary GPT header and partition-entry array, /* Read `disk`'s protective MBR, primary GPT header and partition-entry array,
* checking both CRC-32s, the revision, the sizes and the geometry. On GPT_OK, * checking both CRC-32s, the revision, the sizes and the geometry, and every
* `*count` partitions with a non-zero type (at most `max`) are in `parts`. * entry's range. On GPT_OK, `*count` partitions with a non-zero type (at most
* `max`) are in `parts`. GPT_MANY when the table is good but has more used
* entries than `max`: the first `max` are in `parts`, `*count` is `max`, and the
* caller says so rather than taking the disk as having only those (#180).
* Reads only. */ * Reads only. */
int gpt_read(blkio_dev_t *disk, gpt_part_t *parts, unsigned max, unsigned *count); int gpt_read(blkio_dev_t *disk, gpt_part_t *parts, unsigned max, unsigned *count);
+5 -1
View File
@@ -124,17 +124,21 @@ int gpt_read(blkio_dev_t *disk, gpt_part_t *parts, unsigned max, unsigned *count
} }
if (gpt_crc32(g_gpt_entries, ebytes) != ecrc) return GPT_BAD; if (gpt_crc32(g_gpt_entries, ebytes) != ecrc) return GPT_BAD;
for (i = 0; i < nent && n < max; i++) { /* every entry is checked, and every used one counted -- not only the first `max` (#180) */
for (i = 0; i < nent; i++) {
const uint8_t *e = g_gpt_entries + (size_t)i * esize; const uint8_t *e = g_gpt_entries + (size_t)i * esize;
static const uint8_t zero[16]; static const uint8_t zero[16];
uint64_t first = le64(e + 32), last = le64(e + 40); uint64_t first = le64(e + 32), last = le64(e + 40);
if (memcmp(e, zero, 16) == 0) continue; /* an unused entry */ if (memcmp(e, zero, 16) == 0) continue; /* an unused entry */
if (first < first_usable || last > last_usable || first > last) return GPT_BAD; if (first < first_usable || last > last_usable || first > last) return GPT_BAD;
if (n < max) {
memcpy(parts[n].type, e, 16); memcpy(parts[n].type, e, 16);
parts[n].first_lba = first; parts[n].first_lba = first;
parts[n].last_lba = last; parts[n].last_lba = last;
}
n++; n++;
} }
if (n > max) { *count = max; return GPT_MANY; } /* more than the caller can hold: said, not cut */
*count = n; *count = n;
memcpy(g_gpt_disk_guid, h + 56, 16); memcpy(g_gpt_disk_guid, h + 56, 16);
g_gpt_alternate_lba = le64(h + 32); g_gpt_alternate_lba = le64(h + 32);
+4
View File
@@ -335,6 +335,10 @@ static int sk_v4_drive_arrived(sk_v4_drive_t *d)
g_offered = d; g_offered = d;
return 1; return 1;
} }
if (gpt_rc == GPT_MANY) { /* SK_V4_GPT_PARTS: 16 (#180) */
console_println("GPT: a USB drive has more than 16 partitions -- not used");
return 1;
}
if (gpt_rc == GPT_BAD) { if (gpt_rc == GPT_BAD) {
console_println("GPT: a USB drive has a damaged partition table -- not used"); console_println("GPT: a USB drive has a damaged partition table -- not used");
return 1; return 1;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+47 -1
View File
@@ -87,6 +87,10 @@ the stick's, plan 2026-10-26-stick-1-screen-and-keyboard.md):
no ok> for 30 s; the new one added: the prompt no ok> for 30 s; the new one added: the prompt
mint_fail the fixed screen; R, a stick whose writes all fail (blkdebug): "failed -- nothing mint_fail the fixed screen; R, a stick whose writes all fail (blkdebug): "failed -- nothing
changed", the screen again; the old stick added: the prompt (Review Focus 4) changed", the screen again; the old stick added: the prompt (Review Focus 4)
gpt_many no drive at boot; a data stick whose two StarshipOS partitions are entries 17 and 18,
after 16 others: "GPT: a USB drive has more than 16 partitions -- not used", not cut
to 16 (#180); pulled; the same with an entry 19 outside the usable range:
"a damaged partition table" -- every entry checked (#180)
ident_attach_fail no drive at boot; a fresh data stick hot-plugged whose reads at its identity ident_attach_fail no drive at boot; a fresh data stick hot-plugged whose reads at its identity
partition's first sector (LBA 2048) fail, so that partition cannot be attached after partition's first sector (LBA 2048) fail, so that partition cannot be attached after
Artemis is: a message naming the identity partition, Artemis let go -- both or Artemis is: a message naming the identity partition, Artemis let go -- both or
@@ -137,7 +141,7 @@ ap.add_argument("--target", default="qemu") # qemu-stick: boot the PC stick
ap.add_argument("--stick-gib", type=int, default=None) # qemu-stick: the copy extended to N GiB ap.add_argument("--stick-gib", type=int, default=None) # qemu-stick: the copy extended to N GiB
a = ap.parse_args() a = ap.parse_args()
NO_ZUSE_MODES = {"ident_attach_fail", "gpt_zuse", "gpt_hotplug", "gpt_foreign", "gpt_bad_crc", "gpt_bad_entries", "gpt_no_header", NO_ZUSE_MODES = {"gpt_many", "ident_attach_fail", "gpt_zuse", "gpt_hotplug", "gpt_foreign", "gpt_bad_crc", "gpt_bad_entries", "gpt_no_header",
"gpt_read_error", "headless", "hotplug", "midpull", "waitpull", "typeahead", "emergency", "gpt_read_error", "headless", "hotplug", "midpull", "waitpull", "typeahead", "emergency",
"alttab_after_logout", "alttab_before_pull", "emergency_pull", "alttab_after_logout", "alttab_before_pull", "emergency_pull",
"screen_dark_until_login", "screen_console_after_login", "screen_dark_after_logout", "screen_dark_until_login", "screen_console_after_login", "screen_dark_after_logout",
@@ -201,6 +205,32 @@ if a.mode in ("zuse_stick_first", "fallback", "replace", "mint_fail", "mint_part
subprocess.run(["make", "-f", "kernel/Makefile", f"ARCH={a.arch}", "datastick", f"DATASTICK_IMG={dstick}"], subprocess.run(["make", "-f", "kernel/Makefile", f"ARCH={a.arch}", "datastick", f"DATASTICK_IMG={dstick}"],
cwd=ROOT, check=True, stdout=subprocess.DEVNULL) cwd=ROOT, check=True, stdout=subprocess.DEVNULL)
make_args += ["DATASTICK=1", f"DATASTICK_IMG={dstick}", "DATASTICK_DEP="] make_args += ["DATASTICK=1", f"DATASTICK_IMG={dstick}", "DATASTICK_DEP="]
if a.mode == "gpt_many":
# made before the boot, so a failure here leaves no QEMU running
import struct, zlib
many = os.path.join(work, "many.img")
subprocess.run(["make", "-f", "kernel/Makefile", f"ARCH={a.arch}", "datastick", f"DATASTICK_IMG={many}"],
cwd=ROOT, check=True, stdout=subprocess.DEVNULL)
sg = ["sgdisk", "-a", "1", "--transpose=1:17", "--transpose=2:18"]
for k in range(1, 17): # 16 fillers in the free sectors after Artemis (the
lo = 3143680 + (k - 1) * 64 # first usable is 2048: none before the identity)
sg += ["-n", f"{k}:{lo}:{lo + 63}", "-t", f"{k}:8300"]
subprocess.run(sg + [many], check=True, stdout=subprocess.DEVNULL)
bad = os.path.join(work, "many-bad.img")
subprocess.run(["cp", "--sparse=always", many, bad], check=True)
with open(bad, "r+b") as fh: # entry 19 past the last usable LBA, CRCs made good
fh.seek(512); hdr = bytearray(fh.read(92))
ent_lba, nent, esz = struct.unpack_from("<QII", hdr, 72)
last_usable = struct.unpack_from("<Q", hdr, 48)[0]
fh.seek(ent_lba * 512); ents = bytearray(fh.read(nent * esz))
o = 18 * esz
ents[o:o + 16] = bytes.fromhex("af3dc60f838472478e793d69d8477de4")
ents[o + 16:o + 32] = bytes(range(1, 17))
struct.pack_into("<QQ", ents, o + 32, last_usable - 8, last_usable + 64)
struct.pack_into("<I", hdr, 88, zlib.crc32(ents) & 0xFFFFFFFF)
struct.pack_into("<I", hdr, 16, 0); struct.pack_into("<I", hdr, 16, zlib.crc32(hdr) & 0xFFFFFFFF)
fh.seek(ent_lba * 512); fh.write(ents)
fh.seek(512); fh.write(hdr)
if a.mode == "genesis": if a.mode == "genesis":
# a fresh data stick, made once into this session's directory and booted twice: the second # a fresh data stick, made once into this session's directory and booted twice: the second
# boot is the same copy, so it finds what the first wrote (plan 2026-10-29-stick-5, Task 1) # boot is the same copy, so it finds what the first wrote (plan 2026-10-29-stick-5, Task 1)
@@ -937,6 +967,22 @@ elif m == "entropy":
check("[zuse@Hera] ok>", wait_for(PROMPT, 120)) check("[zuse@Hera] ok>", wait_for(PROMPT, 120))
check("no fault", count(r"EXCEPTION|PANIC|panic") == 0) check("no fault", count(r"EXCEPTION|PANIC|panic") == 0)
elif m == "gpt_many":
# #180: gpt_read() stopped reading entries at its max (16 here) and checked none after them
# the two sticks were made before the boot (see "gpt_many" above)
check("no ok> for 20 s with no drive", not wait_for(ANYPROMPT, 20))
add_stick(1, many, 2)
check("GPT: a USB drive has more than 16 partitions -- not used",
wait_for(r"GPT: a USB drive has more than 16 partitions -- not used", 120), near=r"GPT|USB|Artemis|Zuse")
qcmd("device_del", id="stickdev1")
time.sleep(3)
qcmd("blockdev-del", **{"node-name": "stick1"})
d = count(r"GPT: a USB drive has a damaged partition table -- not used")
add_stick(2, bad, 2)
check("an entry past the usable range, after the 16th: a damaged partition table",
wait_for(r"GPT: a USB drive has a damaged partition table -- not used", 120, d), near=r"GPT|USB|Artemis|Zuse")
check("no ok> throughout", count(ANYPROMPT) == 0)
elif m == "ident_attach_fail": elif m == "ident_attach_fail":
# #179: the identity partition (LBA 2048 on a data stick) cannot be read, so it cannot be # #179: the identity partition (LBA 2048 on a data stick) cannot be read, so it cannot be
# attached -- after Artemis has been. Both partitions or neither. # attached -- after Artemis has been. Both partitions or neither.