]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
fdisk: derive first usable LBA from the real primary entries location
authorlzwind <liuzheng@uniontech.com>
Thu, 23 Jul 2026 06:55:54 +0000 (14:55 +0800)
committerlzwind <liuzheng@uniontech.com>
Thu, 23 Jul 2026 08:19:50 +0000 (16:19 +0800)
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 <liuzheng@uniontech.com>
libfdisk/src/gpt.c
tests/expected/fdisk/gpt-table-length-first-usable-nondefault-entries [new file with mode: 0644]
tests/ts/fdisk/gpt-table-length [new file with mode: 0755]

index 9f0139cf22d6fd867218095ebcaad1ba15271d94..cf4f778085878d18b7dcd3f465240edff0bea3f3 100644 (file)
@@ -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 (file)
index 0000000..1b6bc0e
--- /dev/null
@@ -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 (executable)
index 0000000..a4d13e2
--- /dev/null
@@ -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('<I', ph, 80)[0]
+    esz = struct.unpack_from('<I', ph, 84)[0]
+    esects = (n * esz + SEC - 1) // SEC
+    old = struct.unpack_from('<Q', ph, 72)[0]
+    f.seek(old * SEC)
+    ents = f.read(esects * SEC)
+    # zero the old location
+    f.seek(old * SEC)
+    f.write(b'\x00' * (esects * SEC))
+    # write the entries at the new (non-default) location
+    NEW = 16
+    f.seek(NEW * SEC)
+    f.write(ents)
+    ecrc = crc32(ents[:n * esz])
+    struct.pack_into('<Q', ph, 72, NEW)            # partition_entry_lba
+    struct.pack_into('<Q', ph, 40, NEW + esects)  # first_usable_lba
+    struct.pack_into('<I', ph, 88, ecrc)          # partition_entry_array_crc32
+    struct.pack_into('<I', ph, 16, 0)             # header_crc32 (zero for CRC)
+    struct.pack_into('<I', ph, 16, crc32(bytes(ph[:92])))
+    f.seek(SEC)
+    f.write(ph)
+    # backup header: keep its entry_lba (default end), sync first_usable + entries crc
+    f.seek((total - 1) * SEC)
+    bh = bytearray(f.read(SEC))
+    struct.pack_into('<Q', bh, 40, NEW + esects)
+    struct.pack_into('<I', bh, 88, ecrc)
+    struct.pack_into('<I', bh, 16, 0)
+    struct.pack_into('<I', bh, 16, crc32(bytes(bh[:92])))
+    f.seek((total - 1) * SEC)
+    f.write(bh)
+PYEOF
+
+# Change the table length. The first usable LBA must follow the actual
+# (non-default) primary entries location, not the default LBA 2.
+echo -ne "x\nl\n64\nr\nw\nq\n" | $TS_CMD_FDISK ${TEST_IMAGE_NAME} &> /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