From 2b1be5d909358fe883d38dcd091bc2d1ec9be8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Dre=C3=9Fler?= Date: Tue, 7 Jul 2026 14:41:16 +0200 Subject: [PATCH 1/3] repart: Don't get old grain size from fdisk for --copy-from= This is a little bit confusing, but grain size is not actually stored in the gpt metadata. Rather, fdisk's `get_grain_size()` returns an autodiscovered "optimal io size" value as grain size. This might not actually be the grain size that the disk we're copying is using. Since we're setting the padding of the copied partitions using that value from fdisk, we're rounding the new paddings by fdisk's optimal grain size, which is usually 1MiB (a lot more then the default 4KiB that we're using otherwise). Set the grain size here to 1 byte instead, ensuring that the min/max padding set is exactly the padding that was present before. Also add a test to confirm the behavior is fixed: The test calls --copy-from= on an existing disk with 4MiB grain size, and because we pass --grain-size=512, now no rounding should happen and the paddings should be transferred to exactly the same size. --- src/repart/repart.c | 7 +++--- test/units/TEST-58-REPART.sh | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/repart/repart.c b/src/repart/repart.c index 3eada3bc42..838786e153 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -3355,7 +3355,7 @@ static int context_copy_from_one(Context *context, const char *src) { _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL; Partition *last = NULL; - unsigned long secsz, grainsz; + unsigned long secsz; size_t n_partitions; int r; @@ -3374,7 +3374,6 @@ static int context_copy_from_one(Context *context, const char *src) { return log_error_errno(r, "Failed to create fdisk context: %m"); secsz = sym_fdisk_get_sector_size(c); - grainsz = sym_fdisk_get_grain_size(c); /* Insist on a power of two, and that it's a multiple of 512, i.e. the traditional sector size. */ if (secsz < 512 || !ISPOWEROF2(secsz)) @@ -3455,7 +3454,9 @@ static int context_copy_from_one(Context *context, const char *src) { if (!np->split_name_format) return log_oom(); - r = determine_current_padding(c, t, p, secsz, grainsz, &padding); + /* Pass grain size of 1 to disable rounding by grain as we don't know the grain size + * of the old image. We'll round paddings to the grain size of the new image later. */ + r = determine_current_padding(c, t, p, secsz, /* grainsz= */ 1, &padding); if (r < 0) return r; diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh index 88d486ebc0..f5dfd88c4a 100755 --- a/test/units/TEST-58-REPART.sh +++ b/test/units/TEST-58-REPART.sh @@ -451,6 +451,48 @@ $imgs/zzz8 : start= 6422488, size= 131072, type=4D21B016-B534-45C2-A9FB losetup -d "$loop" } +testcase_copy_from_grain_padding() { + local defs imgs output + + defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")" + imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$imgs'" RETURN + chmod 0755 "$defs" + + truncate -s 80MiB "$imgs/copy_from" + sfdisk "$imgs/copy_from" < Date: Tue, 7 Jul 2026 13:15:36 +0200 Subject: [PATCH 2/3] repart: Clarify and test that --copy-from= argument respects grain size The --copy-from= argument currently is documented as "copied partitions will have the same size". This doesn't hold true in the case where a different grain-size is passed to repart. Because `partition_min/max_size()` currently do rounding, the size is implicitly rounded to grain size, and therefore partitions are enlarged to align to grain size whenever possible. Clarify this behavior and change the manpage, and also add a test for it. --- man/systemd-repart.xml | 11 ++++++----- test/units/TEST-58-REPART.sh | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/man/systemd-repart.xml b/man/systemd-repart.xml index dbf9edad21..27e35d19da 100644 --- a/man/systemd-repart.xml +++ b/man/systemd-repart.xml @@ -609,11 +609,12 @@ Instructs systemd-repart to synthesize partition definitions from the partition table in the given image or device. This option can be specified multiple times to synthesize definitions from each of the given images or devices. The generated definitions will copy - the partitions into the destination partition table. The copied partitions will have the same size, - metadata and contents but might have a different partition number and might be located at a different - offset in the destination partition table. These definitions can be combined with partition - definitions read from regular partition definition files. The synthesized definitions take precedence - over the definitions read from partition definition files. + the partitions into the destination partition table. The copied partitions will have the same or a + slightly larger size (partitions may be enlarged to align to the new grain or sector size), metadata + and contents but might have a different partition number and might be located at a different offset + in the destination partition table. These definitions can be combined with partition definitions read + from regular partition definition files. The synthesized definitions take precedence over the + definitions read from partition definition files. diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh index f5dfd88c4a..7bd18312df 100755 --- a/test/units/TEST-58-REPART.sh +++ b/test/units/TEST-58-REPART.sh @@ -493,6 +493,40 @@ EOF assert_in "$imgs/copy_to2 : start= 51200, size= 10240, type=${xbootldr_guid}," "$output" } +testcase_copy_from_respects_new_grain() { + local defs imgs output + + defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")" + imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$imgs'" RETURN + chmod 0755 "$defs" + + truncate -s 80MiB "$imgs/copy_from" + sfdisk "$imgs/copy_from" < Date: Thu, 9 Jul 2026 15:22:13 +0200 Subject: [PATCH 3/3] repart: Don't copy trailing padding when using --copy-from= Currently, --copy-from= copies the paddings in between the source partitions, as well as the trailing padding that is at the end of the source partition table. It doesn't copy the leading padding at the start of the source partition table though. This seems inconsistent, and likely it was an oversight that the trailing padding is copied. Fix that, and add a test to ensure we don't regress. --- src/repart/repart.c | 33 ++++++++++++++++++++++++----- test/units/TEST-58-REPART.sh | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/repart/repart.c b/src/repart/repart.c index 838786e153..271056a06a 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -3287,10 +3287,12 @@ static int determine_current_padding( struct fdisk_partition *p, uint64_t secsz, uint64_t grainsz, - uint64_t *ret) { + uint64_t *ret, + bool *ret_is_last_partition) { size_t n_partitions; uint64_t offset, next = UINT64_MAX; + bool is_last_partition = false; assert(c); assert(t); @@ -3330,6 +3332,8 @@ static int determine_current_padding( } if (next == UINT64_MAX) { + is_last_partition = true; + /* No later partition? In that case check the end of the usable area */ next = sym_fdisk_get_last_lba(c); assert(next < UINT64_MAX); @@ -3346,6 +3350,9 @@ static int determine_current_padding( offset = round_up_size(offset, grainsz); next = round_down_size(next, grainsz); + if (ret_is_last_partition) + *ret_is_last_partition = is_last_partition; + *ret = LESS_BY(next, offset); /* Saturated subtraction, rounding might have fucked things up */ return 0; } @@ -3395,6 +3402,7 @@ static int context_copy_from_one(Context *context, const char *src) { uint64_t sz, start, padding; sd_id128_t ptid, id; GptPartitionType type; + bool is_last_partition; p = sym_fdisk_table_get_partition(t, i); if (!p) @@ -3456,11 +3464,12 @@ static int context_copy_from_one(Context *context, const char *src) { /* Pass grain size of 1 to disable rounding by grain as we don't know the grain size * of the old image. We'll round paddings to the grain size of the new image later. */ - r = determine_current_padding(c, t, p, secsz, /* grainsz= */ 1, &padding); + r = determine_current_padding(c, t, p, secsz, /* grainsz= */ 1, &padding, &is_last_partition); if (r < 0) return r; - np->padding_min = np->padding_max = padding; + if (!is_last_partition) + np->padding_min = np->padding_max = padding; np->copy_blocks_path = strdup(src); if (!np->copy_blocks_path) @@ -4056,7 +4065,14 @@ static int context_load_partition_table(Context *context) { pp->current_partition = p; sym_fdisk_ref_partition(p); - r = determine_current_padding(c, t, p, secsz, grainsz, &pp->current_padding); + r = determine_current_padding( + c, + t, + p, + secsz, + grainsz, + &pp->current_padding, + /* ret_is_last_partition= */ NULL); if (r < 0) return r; @@ -4089,7 +4105,14 @@ static int context_load_partition_table(Context *context) { np->current_partition = p; sym_fdisk_ref_partition(p); - r = determine_current_padding(c, t, p, secsz, grainsz, &np->current_padding); + r = determine_current_padding( + c, + t, + p, + secsz, + grainsz, + &np->current_padding, + /* ret_is_last_partition= */ NULL); if (r < 0) return r; diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh index 7bd18312df..863023454e 100755 --- a/test/units/TEST-58-REPART.sh +++ b/test/units/TEST-58-REPART.sh @@ -527,6 +527,47 @@ EOF assert_in "$imgs/copy_to2 : start= 32768, size= 16384, type=${xbootldr_guid}," "$output" } +testcase_copy_from_no_padding_at_beginning_and_end() { + local defs imgs output + + defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")" + imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$imgs'" RETURN + chmod 0755 "$defs" + + tee "$defs/03-esp.conf" <