From: lzwind Date: Thu, 23 Jul 2026 06:55:54 +0000 (+0800) Subject: fdisk: derive first usable LBA from the real primary entries location X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b59501852674e65b719f393f1d7152023567758c;p=thirdparty%2Futil-linux.git fdisk: derive first usable LBA from the real primary entries location fdisk_gpt_set_npartitions() (the expert "change table length" command) computed the first usable LBA with gpt_calculate_first_lba(), which assumes the primary partition entries array is at the default LBA 2. The primary entries array is not relocated when the table length changes, so on a GPT whose primary entries are not at the default location the usable area ended up overlapping the entries array and a partition could be created on top of it. Derive the first usable LBA from the actual on-disk location of the primary entries array (pheader->partition_entry_lba) instead. The backup entries array is recalculated to the default end-of-device location by gpt_mknew_header_common(), so gpt_calculate_last_lba() is still based on the default layout and is left unchanged. Add a regression test that relocates the primary entries array to a non-default LBA and checks the first usable LBA after a "change table length" operation. Fixes: https://github.com/util-linux/util-linux/issues/3923 Signed-off-by: lzwind --- diff --git a/libfdisk/src/gpt.c b/libfdisk/src/gpt.c index 9f0139cf2..cf4f77808 100644 --- a/libfdisk/src/gpt.c +++ b/libfdisk/src/gpt.c @@ -2881,7 +2881,7 @@ int fdisk_gpt_set_npartitions(struct fdisk_context *cxt, uint32_t nents) struct fdisk_gpt_label *gpt; size_t new_size = 0; uint32_t old_nents; - uint64_t first_usable = 0ULL, last_usable = 0ULL; + uint64_t first_usable = 0ULL, last_usable = 0ULL, esects = 0ULL; int rc; assert(cxt); @@ -2909,11 +2909,18 @@ int fdisk_gpt_set_npartitions(struct fdisk_context *cxt, uint32_t nents) return rc; } - rc = gpt_calculate_first_lba(gpt->pheader, nents, &first_usable, cxt); + /* The primary entries array is not relocated when the table length + * changes, so derive the first usable LBA from its real on-disk + * location rather than the default LBA 2. */ + rc = gpt_calculate_sectorsof_entries(gpt->pheader, nents, &esects, cxt); + if (rc == 0) + first_usable = le64_to_cpu(gpt->pheader->partition_entry_lba) + esects; if (rc == 0) rc = gpt_calculate_last_lba(gpt->pheader, nents, &last_usable, cxt); if (rc) return rc; + if (first_usable > last_usable) + return -ENOSPC; /* if expanding the table, first check that everything fits, * then allocate more memory and zero. */ diff --git a/tests/expected/fdisk/gpt-table-length-first-usable-nondefault-entries b/tests/expected/fdisk/gpt-table-length-first-usable-nondefault-entries new file mode 100644 index 000000000..1b6bc0efc --- /dev/null +++ b/tests/expected/fdisk/gpt-table-length-first-usable-nondefault-entries @@ -0,0 +1 @@ +firstlba=32 diff --git a/tests/ts/fdisk/gpt-table-length b/tests/ts/fdisk/gpt-table-length new file mode 100755 index 000000000..a4d13e23f --- /dev/null +++ b/tests/ts/fdisk/gpt-table-length @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# This file is part of util-linux. +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# + +# +# Regression test for the "expert" command "change table length" (l) on a GPT +# whose primary partition entries array is not at the default LBA 2. +# +# fdisk_gpt_set_npartitions() must derive the first usable LBA from the actual +# on-disk location of the primary entries array rather than from the default +# layout, otherwise the usable area could overlap the entries array. +# +# See https://github.com/util-linux/util-linux/issues/3923 +# + +TS_TOPDIR="${0%/*}/../.." +TS_DESC="gpt-table-length" + +. "$TS_TOPDIR"/functions.sh +ts_init "$*" + +ts_check_test_command "$TS_CMD_FDISK" +ts_check_test_command "$TS_CMD_SFDISK" +ts_check_prog "python3" + +ts_init_subtest "first-usable-nondefault-entries" + +TEST_IMAGE_NAME=$(ts_image_init 10) + +# A standard GPT: primary partition entries at the default LBA 2. +echo -ne "g\nw\nq\n" | $TS_CMD_FDISK --noauto-pt ${TEST_IMAGE_NAME} &> /dev/null + +# Relocate the primary partition entries array to a non-default LBA (16). Neither +# fdisk nor sfdisk can move the entries array, so do it directly on the image and +# recompute the GPT header CRCs. The backup entries stay at the default +# end-of-device location. +python3 - "$TEST_IMAGE_NAME" <<'PYEOF' >&2 +import struct, zlib, os, sys +SEC = 512 +def crc32(b): return zlib.crc32(b) & 0xffffffff +path = sys.argv[1] +total = os.path.getsize(path) // SEC +with open(path, 'r+b') as f: + f.seek(SEC) + ph = bytearray(f.read(SEC)) + n = struct.unpack_from(' /dev/null + +# sfdisk -J reports the GPT first_usable_lba as "firstlba". With the primary +# entries at LBA 16 and 64 entries (16 sectors), it must be 16 + 16 = 32; the +# buggy implementation reported 2 + 16 = 18. +$TS_CMD_SFDISK -J ${TEST_IMAGE_NAME} 2> /dev/null \ + | sed -n 's/.*"firstlba": \([0-9]*\),.*/firstlba=\1/p' >> "$TS_OUTPUT" + +ts_finalize_subtest +ts_finalize