From 0e187fae072662c1e8a2cea1185daf36380bd725 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Mon, 30 Mar 2026 21:50:27 +0200 Subject: [PATCH] shared/gpt: add gpt_probe() for GPT header and partition entry reading Add gpt_probe() which probes for a GPT partition table at various sector sizes (512-4096) and optionally returns the header and partition entries. Returns the detected sector size on success, 0 if no GPT was found, or negative errno on error. Refactor probe_sector_size() in dissect-image.c to be a thin wrapper around gpt_probe(). Co-developed-by: Claude Opus 4.6 --- src/shared/dissect-image.c | 56 +++---------- src/shared/gpt.c | 85 ++++++++++++++++++++ src/shared/gpt.h | 7 ++ src/test/test-gpt.c | 159 +++++++++++++++++++++++++++++++++++-- 4 files changed, 257 insertions(+), 50 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 94d6f0545d..2bef82dd34 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -139,54 +139,24 @@ static const char *getenv_fstype(PartitionDesignator d) { int probe_sector_size(int fd, uint32_t *ret) { - /* Disk images might be for 512B or for 4096 sector sizes, let's try to auto-detect that by searching - * for the GPT headers at the relevant byte offsets */ - - assert_cc(sizeof(GptHeader) == 92); - - /* We expect a sector size in the range 512…4096. The GPT header is located in the second - * sector. Hence it could be at byte 512 at the earliest, and at byte 4096 at the latest. And we must - * read with granularity of the largest sector size we care about. Which means 8K. */ - uint8_t sectors[2 * 4096]; - uint32_t found = 0; - ssize_t n; - assert(fd >= 0); assert(ret); - n = pread(fd, sectors, sizeof(sectors), 0); - if (n < 0) - return -errno; - if (n != sizeof(sectors)) /* too short? */ - goto not_found; - - /* Let's see if we find the GPT partition header with various expected sector sizes */ - for (uint32_t sz = 512; sz <= 4096; sz <<= 1) { - const GptHeader *p; - - assert(sizeof(sectors) >= sz * 2); - p = (const GptHeader*) (sectors + sz); - - if (!gpt_header_has_signature(p)) - continue; - - if (found != 0) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), - "Detected valid partition table at offsets matching multiple sector sizes, refusing."); - - found = sz; + ssize_t ssz = gpt_probe(fd, /* ret_header= */ NULL, /* ret_entries= */ NULL, /* ret_n_entries= */ NULL, /* ret_entry_size= */ NULL); + if (ssz == -ENOTUNIQ) + return log_debug_errno(ssz, + "Detected valid partition table at offsets matching multiple sector sizes, refusing."); + if (ssz < 0) + return ssz; + if (ssz == 0) { + log_debug("Couldn't find any partition table to derive sector size of."); + *ret = 512; /* pick the traditional default */ + return 0; /* indicate we didn't find it */ } - if (found != 0) { - log_debug("Determined sector size %" PRIu32 " based on discovered partition table.", found); - *ret = found; - return 1; /* indicate we *did* find it */ - } - -not_found: - log_debug("Couldn't find any partition table to derive sector size of."); - *ret = 512; /* pick the traditional default */ - return 0; /* indicate we didn't find it */ + log_debug("Determined sector size %" PRIu32 " based on discovered partition table.", (uint32_t) ssz); + *ret = ssz; + return 1; /* indicate we *did* find it */ } int probe_sector_size_prefer_ioctl(int fd, uint32_t *ret) { diff --git a/src/shared/gpt.c b/src/shared/gpt.c index 9308159ebe..d6f264fbe9 100644 --- a/src/shared/gpt.c +++ b/src/shared/gpt.c @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + #include "alloc-util.h" #include "gpt.h" #include "string-table.h" @@ -391,3 +393,86 @@ bool gpt_header_has_signature(const GptHeader *p) { return true; } + +ssize_t gpt_probe( + int fd, + GptHeader *ret_header, + void **ret_entries, + uint32_t *ret_n_entries, + uint32_t *ret_entry_size) { + + assert(fd >= 0); + + /* Disk images might be for 512B or for 4096 sector sizes, let's try to auto-detect that by searching + * for the GPT headers at the relevant byte offsets. */ + + assert_cc(sizeof(GptHeader) == 92); + + /* We expect a sector size in the range 512…4096. The GPT header is located in the second + * sector. Hence it could be at byte 512 at the earliest, and at byte 4096 at the latest. And we must + * read with granularity of the largest sector size we care about. Which means 8K. */ + uint8_t sectors[2 * 4096]; + + ssize_t n = pread(fd, sectors, sizeof(sectors), 0); + if (n < 0) + return -errno; + if ((size_t) n < sizeof(sectors)) + return 0; /* too short */ + + /* Let's see if we find the GPT partition header with various expected sector sizes */ + uint32_t found = 0; + for (uint32_t sz = 512; sz <= 4096; sz <<= 1) { + const GptHeader *p = (const GptHeader *) (sectors + sz); + + if (!gpt_header_has_signature(p)) + continue; + + if (found != 0) + return -ENOTUNIQ; + + found = sz; + } + + if (found == 0) + return 0; + + const GptHeader *h = (const GptHeader *) (sectors + found); + + uint32_t entry_sz = le32toh(h->size_of_partition_entry); + uint32_t entry_count = le32toh(h->number_of_partition_entries); + + if (ret_entries) { + uint64_t entry_lba = le64toh(h->partition_entry_lba); + if (entry_lba > (uint64_t) INT64_MAX / found) + return -EBADMSG; + + uint64_t entry_offset = entry_lba * found; + + if (entry_sz < sizeof(GptPartitionEntry) || entry_count == 0 || entry_count > 1024) + return -EBADMSG; + if (entry_sz > SIZE_MAX / entry_count) + return -EBADMSG; + + size_t entries_size = (size_t) entry_sz * entry_count; + _cleanup_free_ void *entries = malloc(entries_size); + if (!entries) + return -ENOMEM; + + n = pread(fd, entries, entries_size, entry_offset); + if (n < 0) + return -errno; + if ((size_t) n < entries_size) + return -EBADMSG; + + *ret_entries = TAKE_PTR(entries); + } + + if (ret_header) + *ret_header = *h; + if (ret_n_entries) + *ret_n_entries = entry_count; + if (ret_entry_size) + *ret_entry_size = entry_sz; + + return found; /* sector size */ +} diff --git a/src/shared/gpt.h b/src/shared/gpt.h index f59f2da29f..18d665441c 100644 --- a/src/shared/gpt.h +++ b/src/shared/gpt.h @@ -113,3 +113,10 @@ typedef struct { } _packed_ GptHeader; bool gpt_header_has_signature(const GptHeader *p) _pure_; + +ssize_t gpt_probe( + int fd, + GptHeader *ret_header, + void **ret_entries, + uint32_t *ret_n_entries, + uint32_t *ret_entry_size); diff --git a/src/test/test-gpt.c b/src/test/test-gpt.c index 6772d46ef6..430f8e07fd 100644 --- a/src/test/test-gpt.c +++ b/src/test/test-gpt.c @@ -1,8 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + #include "architecture.h" +#include "fd-util.h" #include "gpt.h" #include "log.h" +#include "memfd-util.h" +#include "memory-util.h" #include "pretty-print.h" #include "strv.h" #include "tests.h" @@ -49,19 +54,19 @@ TEST(verity_mappings) { PartitionDesignator q; q = partition_verity_hash_of(p); - assert_se(q < 0 || partition_verity_hash_to_data(q) == p); + ASSERT_TRUE(q < 0 || partition_verity_hash_to_data(q) == p); q = partition_verity_sig_of(p); - assert_se(q < 0 || partition_verity_sig_to_data(q) == p); + ASSERT_TRUE(q < 0 || partition_verity_sig_to_data(q) == p); q = partition_verity_hash_to_data(p); - assert_se(q < 0 || partition_verity_hash_of(q) == p); + ASSERT_TRUE(q < 0 || partition_verity_hash_of(q) == p); q = partition_verity_sig_to_data(p); - assert_se(q < 0 || partition_verity_sig_of(q) == p); + ASSERT_TRUE(q < 0 || partition_verity_sig_of(q) == p); q = partition_verity_to_data(p); - assert_se(q < 0 || partition_verity_hash_of(q) == p || partition_verity_sig_of(q) == p); + ASSERT_TRUE(q < 0 || partition_verity_hash_of(q) == p || partition_verity_sig_of(q) == p); } } @@ -94,7 +99,7 @@ TEST(override_architecture) { x = gpt_partition_type_override_architecture(x, ARCHITECTURE_ARM64); ASSERT_EQ(x.arch, y.arch); ASSERT_EQ(x.designator, y.designator); - assert_se(sd_id128_equal(x.uuid, y.uuid)); + ASSERT_EQ_ID128(x.uuid, y.uuid); ASSERT_STREQ(x.name, y.name); /* If the partition type does not have an architecture, nothing should change. */ @@ -105,8 +110,148 @@ TEST(override_architecture) { x = gpt_partition_type_override_architecture(x, ARCHITECTURE_ARM64); ASSERT_EQ(x.arch, y.arch); ASSERT_EQ(x.designator, y.designator); - assert_se(sd_id128_equal(x.uuid, y.uuid)); + ASSERT_EQ_ID128(x.uuid, y.uuid); ASSERT_STREQ(x.name, y.name); } +static void make_gpt(int fd, uint32_t sector_size, const GptPartitionEntry *part_entries, size_t n_entries) { + /* Zero-fill enough for header probing (gpt_probe reads 2*4096 = 8KB) */ + static const uint8_t zeros[2 * 4096] = {}; + ASSERT_OK_EQ_ERRNO(pwrite(fd, zeros, sizeof(zeros), 0), (ssize_t) sizeof(zeros)); + + GptHeader h = { + .signature = { 'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T' }, + .revision = htole32(UINT32_C(0x00010000)), + .header_size = htole32(sizeof(GptHeader)), + .my_lba = htole64(1), + .partition_entry_lba = htole64(2), + .number_of_partition_entries = htole32(n_entries), + .size_of_partition_entry = htole32(sizeof(GptPartitionEntry)), + }; + ASSERT_OK_EQ_ERRNO(pwrite(fd, &h, sizeof(h), sector_size), (ssize_t) sizeof(h)); + + if (n_entries > 0) { + size_t entries_size = n_entries * sizeof(GptPartitionEntry); + ASSERT_OK_EQ_ERRNO(pwrite(fd, part_entries, entries_size, 2 * sector_size), (ssize_t) entries_size); + } +} + +TEST(gpt_probe_empty) { + _cleanup_close_ int fd = -EBADF; + + fd = ASSERT_OK(memfd_new("test-gpt-probe")); + ASSERT_OK_EQ(gpt_probe(fd, /* ret_header= */ NULL, /* ret_entries= */ NULL, /* ret_n_entries= */ NULL, /* ret_entry_size= */ NULL), (ssize_t) 0); +} + +TEST(gpt_probe_too_short) { + _cleanup_close_ int fd = -EBADF; + static const uint8_t buf[4096] = {}; + + fd = ASSERT_OK(memfd_new("test-gpt-probe")); + ASSERT_OK_EQ_ERRNO(pwrite(fd, buf, sizeof(buf), 0), (ssize_t) sizeof(buf)); + ASSERT_OK_EQ(gpt_probe(fd, /* ret_header= */ NULL, /* ret_entries= */ NULL, /* ret_n_entries= */ NULL, /* ret_entry_size= */ NULL), (ssize_t) 0); +} + +TEST(gpt_probe_no_signature) { + _cleanup_close_ int fd = -EBADF; + static const uint8_t buf[2 * 4096] = {}; + + fd = ASSERT_OK(memfd_new("test-gpt-probe")); + ASSERT_OK_EQ_ERRNO(pwrite(fd, buf, sizeof(buf), 0), (ssize_t) sizeof(buf)); + ASSERT_OK_EQ(gpt_probe(fd, /* ret_header= */ NULL, /* ret_entries= */ NULL, /* ret_n_entries= */ NULL, /* ret_entry_size= */ NULL), (ssize_t) 0); +} + +TEST(gpt_probe_sector_512) { + _cleanup_close_ int fd = -EBADF; + + const GptPartitionEntry entries[2] = { + { + .unique_partition_guid = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 }, + .starting_lba = htole64(100), + .ending_lba = htole64(200), + }, + { + .unique_partition_guid = { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20 }, + .starting_lba = htole64(300), + .ending_lba = htole64(400), + }, + }; + + fd = ASSERT_OK(memfd_new("test-gpt-probe")); + make_gpt(fd, 512, entries, 2); + + /* Sector size detection only */ + ASSERT_OK_EQ(gpt_probe(fd, /* ret_header= */ NULL, /* ret_entries= */ NULL, /* ret_n_entries= */ NULL, /* ret_entry_size= */ NULL), (ssize_t) 512); + + /* Header return */ + GptHeader h; + ASSERT_OK_EQ(gpt_probe(fd, &h, /* ret_entries= */ NULL, /* ret_n_entries= */ NULL, /* ret_entry_size= */ NULL), (ssize_t) 512); + ASSERT_EQ(le32toh(h.number_of_partition_entries), 2u); + ASSERT_EQ(le32toh(h.size_of_partition_entry), (uint32_t) sizeof(GptPartitionEntry)); + + /* Full probe with entries */ + _cleanup_free_ void *ret_entries = NULL; + uint32_t n, sz; + ASSERT_OK_EQ(gpt_probe(fd, /* ret_header= */ NULL, &ret_entries, &n, &sz), (ssize_t) 512); + ASSERT_EQ(n, 2u); + ASSERT_EQ(sz, (uint32_t) sizeof(GptPartitionEntry)); + ASSERT_NOT_NULL(ret_entries); + + GptPartitionEntry *e = ret_entries; + ASSERT_EQ(memcmp_nn(e[0].unique_partition_guid, sizeof(e[0].unique_partition_guid), entries[0].unique_partition_guid, sizeof(entries[0].unique_partition_guid)), 0); + ASSERT_EQ(memcmp_nn(e[1].unique_partition_guid, sizeof(e[1].unique_partition_guid), entries[1].unique_partition_guid, sizeof(entries[1].unique_partition_guid)), 0); + ASSERT_EQ(le64toh(e[0].starting_lba), UINT64_C(100)); + ASSERT_EQ(le64toh(e[1].starting_lba), UINT64_C(300)); +} + +TEST(gpt_probe_sector_4096) { + _cleanup_close_ int fd = -EBADF; + + const GptPartitionEntry entry = { + .unique_partition_guid = { 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, + 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99 }, + .starting_lba = htole64(50), + .ending_lba = htole64(100), + }; + + fd = ASSERT_OK(memfd_new("test-gpt-probe")); + make_gpt(fd, 4096, &entry, 1); + + ASSERT_OK_EQ(gpt_probe(fd, /* ret_header= */ NULL, /* ret_entries= */ NULL, /* ret_n_entries= */ NULL, /* ret_entry_size= */ NULL), (ssize_t) 4096); + + _cleanup_free_ void *ret_entries = NULL; + uint32_t n, sz; + ASSERT_OK_EQ(gpt_probe(fd, /* ret_header= */ NULL, &ret_entries, &n, &sz), (ssize_t) 4096); + ASSERT_EQ(n, 1u); + + GptPartitionEntry *e = ret_entries; + ASSERT_EQ(memcmp_nn(e[0].unique_partition_guid, sizeof(e[0].unique_partition_guid), entry.unique_partition_guid, sizeof(entry.unique_partition_guid)), 0); + ASSERT_EQ(le64toh(e[0].starting_lba), UINT64_C(50)); +} + +TEST(gpt_probe_ambiguous) { + _cleanup_close_ int fd = -EBADF; + + const GptPartitionEntry entry = {}; + + fd = ASSERT_OK(memfd_new("test-gpt-probe")); + make_gpt(fd, 512, &entry, 1); + + /* Place a second valid header at offset 4096 */ + GptHeader h2 = { + .signature = { 'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T' }, + .revision = htole32(UINT32_C(0x00010000)), + .header_size = htole32(sizeof(GptHeader)), + .my_lba = htole64(1), + .partition_entry_lba = htole64(2), + .number_of_partition_entries = htole32(1), + .size_of_partition_entry = htole32(sizeof(GptPartitionEntry)), + }; + ASSERT_OK_EQ_ERRNO(pwrite(fd, &h2, sizeof(h2), 4096), (ssize_t) sizeof(h2)); + + ASSERT_ERROR(gpt_probe(fd, /* ret_header= */ NULL, /* ret_entries= */ NULL, /* ret_n_entries= */ NULL, /* ret_entry_size= */ NULL), ENOTUNIQ); +} + DEFINE_TEST_MAIN(LOG_INFO);