/* gpt.c -- the GPT reader, and a block device of one partition. * * GPT for boot media and installed disks (Captain Bob, 2026-10-26; * docs/v4.0.0/STICK.md 3 and 4, overturning FABRIC-2.md F.8 for those two * kinds of media). The kernel reads a disk's GPT -- the protective MBR, the * primary header and its partition-entry array, both CRC-checked -- and makes * a block device of a StarshipOS partition: its block 0 is the partition's * first 1 KiB, and nothing past its end can be reached through it. The * identity and Artemis formats inside a partition are exactly those of a raw * drive today. 512-byte logical sectors only (the stick and its data stick); * docs/v4.0.0/plans/2026-10-26-stick-2-gpt-reader.md. * * The writer (plan 2026-10-29-stick-5, Task 2; STICK.md 5a): the stick's last * partition grown to the end of a disk larger than the image written to it -- * the backup header and entry array moved to the disk's end, both headers * rewritten with their CRCs, the backup before the primary. */ #include "starkernel/gpt.h" #include "console.h" #include #include /* The two StarshipOS partition type GUIDs, as they are on disk: the first * three fields little-endian, the last eight bytes as written. * Identity 23BEBFE1-4817-4CF3-A537-9069BF159788, * Artemis BCD7B586-1C8E-4EEF-86A3-273A8D8EE624. */ const uint8_t gpt_type_starship_identity[16] = { 0xE1, 0xBF, 0xBE, 0x23, 0x17, 0x48, 0xF3, 0x4C, 0xA5, 0x37, 0x90, 0x69, 0xBF, 0x15, 0x97, 0x88 }; const uint8_t gpt_type_starship_artemis[16] = { 0x86, 0xB5, 0xD7, 0xBC, 0x8E, 0x1C, 0xEF, 0x4E, 0x86, 0xA3, 0x27, 0x3A, 0x8D, 0x8E, 0xE6, 0x24 }; uint32_t gpt_crc32(const void *buf, size_t len) { const uint8_t *p = (const uint8_t *)buf; uint32_t crc = 0xFFFFFFFFu; size_t i; int k; for (i = 0; i < len; i++) { crc ^= p[i]; for (k = 0; k < 8; k++) crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u))); } return crc ^ 0xFFFFFFFFu; } static uint32_t le32(const uint8_t *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24; } static uint64_t le64(const uint8_t *p) { return (uint64_t)le32(p) | (uint64_t)le32(p + 4) << 32; } #define GPT_SECTOR 512u #define GPT_ENTRIES_MAX (32u * 1024u) /* the entry array read, at most (128 entries of 128 bytes is 16 KiB) */ static uint8_t g_gpt_hdr_block[BLKIO_FORTH_BLOCK_SIZE]; static uint8_t g_gpt_entries[GPT_ENTRIES_MAX]; static uint8_t g_gpt_disk_guid[16]; /* the DiskGUID of the last gpt_read that gave GPT_OK */ static uint64_t g_gpt_alternate_lba; /* and its backup header's LBA */ int gpt_read(blkio_dev_t *disk, gpt_part_t *parts, unsigned max, unsigned *count) { const uint8_t *mbr = g_gpt_hdr_block, *h = g_gpt_hdr_block + GPT_SECTOR; uint32_t hsize, hcrc, nent, esize, ecrc, i; uint64_t my_lba, first_usable, last_usable, ent_lba, disk_sectors; uint8_t hdr[GPT_SECTOR]; size_t ebytes; unsigned n = 0; *count = 0; if (!disk || disk->forth_block_size != BLKIO_FORTH_BLOCK_SIZE) return GPT_NONE; { static int said; blkio_info_t info; if (disk->vt->info(disk, &info) == BLKIO_OK && info.phys_sector_size != 0u && info.phys_sector_size != GPT_SECTOR) { if (!said) { console_println("GPT: a drive with sectors other than 512 bytes is read without its partition table"); said = 1; } return GPT_NONE; } } /* a disk whose first block cannot be read may be a GPT disk: not used */ if (blkio_read(disk, 0u, g_gpt_hdr_block) != BLKIO_OK) return GPT_EIO; /* the protective MBR: 0x55AA, and a partition of type 0xEE */ if (mbr[510] != 0x55u || mbr[511] != 0xAAu) return GPT_NONE; for (i = 0; i < 4u; i++) if (mbr[446u + 16u * i + 4u] == 0xEEu) break; if (i == 4u) return GPT_NONE; /* from here it is a GPT disk -- the protective MBR says so: anything * wrong, the header's signature first, is GPT_BAD */ if (memcmp(h, "EFI PART", 8) != 0) return GPT_BAD; if (le32(h + 8) != 0x00010000u) return GPT_BAD; hsize = le32(h + 12); if (hsize < 92u || hsize > GPT_SECTOR) return GPT_BAD; memcpy(hdr, h, GPT_SECTOR); hcrc = le32(hdr + 16); memset(hdr + 16, 0, 4); if (gpt_crc32(hdr, hsize) != hcrc) return GPT_BAD; my_lba = le64(h + 24); first_usable = le64(h + 40); last_usable = le64(h + 48); ent_lba = le64(h + 72); nent = le32(h + 80); esize = le32(h + 84); ecrc = le32(h + 88); disk_sectors = (uint64_t)disk->total_blocks * (BLKIO_FORTH_BLOCK_SIZE / GPT_SECTOR); if (my_lba != 1u || esize < 128u || esize % 128u != 0u || nent == 0u) return GPT_BAD; if (first_usable > last_usable || last_usable >= disk_sectors) return GPT_BAD; ebytes = (size_t)nent * esize; if (ebytes > GPT_ENTRIES_MAX || ent_lba < 2u || ent_lba >= disk_sectors || (ent_lba % 2u) != 0u) return GPT_BAD; if (ent_lba + (ebytes + GPT_SECTOR - 1u) / GPT_SECTOR > first_usable) return GPT_BAD; /* the entry array, a 1 KiB block at a time */ for (i = 0; (size_t)i * BLKIO_FORTH_BLOCK_SIZE < ebytes; i++) { if (blkio_read(disk, (uint32_t)(ent_lba / 2u) + i, g_gpt_entries + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) return GPT_BAD; } if (gpt_crc32(g_gpt_entries, ebytes) != ecrc) return GPT_BAD; for (i = 0; i < nent && n < max; i++) { const uint8_t *e = g_gpt_entries + (size_t)i * esize; static const uint8_t zero[16]; uint64_t first = le64(e + 32), last = le64(e + 40); if (memcmp(e, zero, 16) == 0) continue; /* an unused entry */ if (first < first_usable || last > last_usable || first > last) return GPT_BAD; memcpy(parts[n].type, e, 16); parts[n].first_lba = first; parts[n].last_lba = last; n++; } *count = n; memcpy(g_gpt_disk_guid, h + 56, 16); g_gpt_alternate_lba = le64(h + 32); return GPT_OK; } void gpt_disk_guid(uint8_t guid[16]) { memcpy(guid, g_gpt_disk_guid, 16); } int gpt_find(const gpt_part_t *parts, unsigned count, const uint8_t type[16]) { unsigned i; for (i = 0; i < count; i++) if (memcmp(parts[i].type, type, 16) == 0) return (int)i; return -1; } /* ---- the writer: growing the last partition to the disk's end ---------- */ static void put_le32(uint8_t *p, uint32_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24); } static void put_le64(uint8_t *p, uint64_t v) { put_le32(p, (uint32_t)v); put_le32(p + 4, (uint32_t)(v >> 32)); } static uint8_t g_gpt_rmw[BLKIO_FORTH_BLOCK_SIZE]; /* Write `n` 512-byte sectors from `src` at sector `lba`: each through a * read-modify-write of the 1 KiB block that holds it, so a sector at an odd * LBA (the backup header, the backup entry array) is written alone. */ static int put_sectors(blkio_dev_t *disk, uint64_t lba, const uint8_t *src, uint32_t n) { uint32_t i; for (i = 0; i < n; i++) { uint64_t s = lba + i; uint32_t blk = (uint32_t)(s / 2u); if (blkio_read(disk, blk, g_gpt_rmw) != BLKIO_OK) return -1; memcpy(g_gpt_rmw + (s % 2u) * GPT_SECTOR, src + (size_t)i * GPT_SECTOR, GPT_SECTOR); if (blkio_write(disk, blk, g_gpt_rmw) != BLKIO_OK) return -1; } return 0; } uint64_t gpt_alternate_lba(void) { return g_gpt_alternate_lba; } int gpt_grow_last(blkio_dev_t *disk, const uint8_t type[16], uint64_t max_part_sectors, uint64_t *new_last_lba) { static gpt_part_t parts[16]; static uint8_t hdr[GPT_SECTOR], bak[GPT_SECTOR]; const uint8_t *h = g_gpt_hdr_block + GPT_SECTOR; uint8_t *mbr = g_gpt_hdr_block, *e = (uint8_t *)0; uint64_t disk_sectors, alt, ent_lba, new_alt, new_ent, new_last_usable, first, last, want, top = 0; uint32_t nent, esize, ent_sectors, i; unsigned count; size_t ebytes; int rc; rc = gpt_read(disk, parts, 16u, &count); if (rc != GPT_OK) return rc == GPT_NONE ? GPT_BAD : rc; nent = le32(h + 80); esize = le32(h + 84); ebytes = (size_t)nent * esize; ent_sectors = (uint32_t)((ebytes + GPT_SECTOR - 1u) / GPT_SECTOR); ent_lba = le64(h + 72); alt = le64(h + 32); disk_sectors = (uint64_t)disk->total_blocks * (BLKIO_FORTH_BLOCK_SIZE / GPT_SECTOR); /* the partition of `type`, which must be the last on the disk */ for (i = 0; i < nent; i++) { uint8_t *x = g_gpt_entries + (size_t)i * esize; static const uint8_t zero[16]; if (memcmp(x, zero, 16) == 0) continue; if (le64(x + 40) > top) top = le64(x + 40); if (!e && memcmp(x, type, 16) == 0) e = x; } if (!e) return GPT_BAD; first = le64(e + 32); last = le64(e + 40); *new_last_lba = last; if (alt == disk_sectors - 1u) return GPT_OK; /* already ends at the disk's end */ if (alt > disk_sectors - 1u || last != top) return GPT_BAD; /* the backup entry array and header at the disk's end; the partition to * the last usable sector before them, on whole MiB, at most max sectors */ new_alt = disk_sectors - 1u; new_ent = new_alt - ent_sectors; new_last_usable = new_ent - 1u; if (new_last_usable < last) return GPT_BAD; want = new_last_usable - first + 1u; if (want > max_part_sectors) want = max_part_sectors; want -= want % 2048u; if (first + want - 1u > last) { last = first + want - 1u; put_le64(e + 40, last); } /* the primary header, then the backup: each with its own LBAs and CRC */ memcpy(hdr, h, GPT_SECTOR); put_le64(hdr + 32, new_alt); put_le64(hdr + 48, new_last_usable); put_le32(hdr + 88, gpt_crc32(g_gpt_entries, ebytes)); memcpy(bak, hdr, GPT_SECTOR); put_le64(bak + 24, new_alt); put_le64(bak + 32, 1u); put_le64(bak + 72, new_ent); put_le32(hdr + 16, 0u); put_le32(hdr + 16, gpt_crc32(hdr, le32(hdr + 12))); put_le32(bak + 16, 0u); put_le32(bak + 16, gpt_crc32(bak, le32(bak + 12))); /* the backup before the primary: a write torn between them leaves the old * primary, which still checks out, and the next boot grows again */ if (put_sectors(disk, new_ent, g_gpt_entries, ent_sectors) != 0 || put_sectors(disk, new_alt, bak, 1u) != 0 || disk->vt->flush(disk) != BLKIO_OK) return GPT_EIO; if (put_sectors(disk, ent_lba, g_gpt_entries, ent_sectors) != 0 || put_sectors(disk, 1u, hdr, 1u) != 0 || disk->vt->flush(disk) != BLKIO_OK) return GPT_EIO; /* the protective MBR's partition covers the disk (as many sectors as fit) */ for (i = 0; i < 4u; i++) { uint8_t *p = mbr + 446u + 16u * i; if (p[4] == 0xEEu) { put_le32(p + 12, disk_sectors - 1u > 0xFFFFFFFFu ? 0xFFFFFFFFu : (uint32_t)(disk_sectors - 1u)); break; } } if (put_sectors(disk, 0u, mbr, 1u) != 0 || disk->vt->flush(disk) != BLKIO_OK) return GPT_EIO; *new_last_lba = last; return GPT_OK; } /* ---- a partition as a blkio device ------------------------------------- */ static int part_open(blkio_dev_t *dev, const blkio_params_t *p) { gpt_part_dev_t *st = (gpt_part_dev_t *)p->opaque; dev->state = st; dev->forth_block_size = BLKIO_FORTH_BLOCK_SIZE; dev->total_blocks = st->fblocks; return BLKIO_OK; } static int part_close(blkio_dev_t *dev) { (void)dev; return BLKIO_OK; } static int part_read(blkio_dev_t *dev, uint32_t fblock, void *dst) { gpt_part_dev_t *st = (gpt_part_dev_t *)dev->state; if (fblock >= st->fblocks) return BLKIO_EINVAL; return blkio_read(st->disk, st->first_fblock + fblock, dst); } static int part_write(blkio_dev_t *dev, uint32_t fblock, const void *src) { gpt_part_dev_t *st = (gpt_part_dev_t *)dev->state; if (fblock >= st->fblocks) return BLKIO_EINVAL; return blkio_write(st->disk, st->first_fblock + fblock, src); } static int part_flush(blkio_dev_t *dev) { gpt_part_dev_t *st = (gpt_part_dev_t *)dev->state; return st->disk->vt->flush(st->disk); } static int part_info(blkio_dev_t *dev, blkio_info_t *out) { gpt_part_dev_t *st = (gpt_part_dev_t *)dev->state; int rc = st->disk->vt->info(st->disk, out); if (rc != BLKIO_OK) return rc; out->total_blocks = st->fblocks; out->phys_size_bytes = (uint64_t)st->fblocks * BLKIO_FORTH_BLOCK_SIZE; return BLKIO_OK; } static const blkio_vtable_t g_part_vt = { part_open, part_close, part_read, part_write, part_flush, part_info }; int gpt_part_open(blkio_dev_t *dev, gpt_part_dev_t *st, blkio_dev_t *disk, const gpt_part_t *p) { blkio_params_t params; uint64_t sectors; if (!dev || !st || !disk || !p) return BLKIO_EINVAL; sectors = p->last_lba - p->first_lba + 1u; if ((p->first_lba % 2u) != 0u || (sectors % 2u) != 0u) return BLKIO_EINVAL; if (p->first_lba / 2u + sectors / 2u > disk->total_blocks) return BLKIO_EINVAL; st->disk = disk; st->first_fblock = (uint32_t)(p->first_lba / 2u); st->fblocks = (uint32_t)(sectors / 2u); params.forth_block_size = BLKIO_FORTH_BLOCK_SIZE; params.total_blocks = st->fblocks; params.opaque = st; return blkio_open(dev, &g_part_vt, ¶ms); }