]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
repart: add --grain-size= option for partition alignment
authorNandakumar Raghavan <naraghavan@microsoft.com>
Wed, 25 Feb 2026 06:38:31 +0000 (06:38 +0000)
committerLuca Boccassi <luca.boccassi@gmail.com>
Wed, 18 Mar 2026 10:39:39 +0000 (10:39 +0000)
Add a --grain-size= CLI option to override the default 4 KiB partition
alignment grain. Setting --grain-size=1M matches the alignment used by
fdisk/parted and fixes misaligned partitions after small fixed-size
partitions like the 16 KiB verity-sig partition.

Also fix context_place_partitions() to re-align the start offset after
each partition, not just once per free area. Without this, a small
partition would cause all subsequent partitions in the same free area
to start at an unaligned offset.

man/systemd-repart.xml
src/repart/repart.c

index e13dac3cd53c445cfd5d0c955e8a49f5c9faf4a6..dac4759538446d31f0669783cb5a5a8c9a7b4de6 100644 (file)
         <xi:include href="version-info.xml" xpointer="v253"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><option>--grain-size=<replaceable>BYTES</replaceable></option></term>
+
+        <listitem><para>This option controls the partition alignment granularity used when placing
+        partitions. It takes a power-of-2 value that is at least the sector size. All partition start
+        offsets will be rounded up to a multiple of this value. Defaults to
+        <constant>MAX(4096, sector_size)</constant>, matching the conventional 4 KiB alignment. Setting
+        this to <literal>1M</literal> ensures proper alignment for modern storage devices even after
+        small fixed-size partitions such as a verity signature partition.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><option>--architecture=<replaceable>ARCH</replaceable></option></term>
 
index f6ca9aca0565a64481ea35f89f499b8220314eb9..eb334a7c4013d7d03fda6eaa4fc635a7859d3bc0 100644 (file)
@@ -200,6 +200,7 @@ static size_t arg_n_defer_partitions = 0;
 static bool arg_defer_partitions_empty = false;
 static bool arg_defer_partitions_factory_reset = false;
 static uint64_t arg_sector_size = 0;
+static uint64_t arg_grain_size = 0;
 static ImagePolicy *arg_image_policy = NULL;
 static Architecture arg_architecture = _ARCHITECTURE_INVALID;
 static int arg_offline = -1;
@@ -617,6 +618,10 @@ static const char *progress_phase_table[_PROGRESS_PHASE_MAX] = {
         [PROGRESS_REREADING_TABLE]            = "rereading-table",
 };
 
+static uint64_t determine_grain_size(uint64_t sector_size) {
+        return MAX(arg_grain_size > 0 ? arg_grain_size : 4096U, sector_size);
+}
+
 DEFINE_PRIVATE_STRING_TABLE_LOOKUP(empty_mode, EmptyMode);
 DEFINE_PRIVATE_STRING_TABLE_LOOKUP(append_mode, AppendMode);
 DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(encrypt_mode, EncryptMode, ENCRYPT_KEY_FILE);
@@ -1701,7 +1706,7 @@ static void context_place_partitions(Context *context) {
 
         for (size_t i = 0; i < context->n_free_areas; i++) {
                 FreeArea *a = context->free_areas[i];
-                _unused_ uint64_t left;
+                uint64_t left;
                 uint64_t start;
 
                 if (a->after) {
@@ -1717,6 +1722,8 @@ static void context_place_partitions(Context *context) {
                 left = a->size;
 
                 LIST_FOREACH(partitions, p, context->partitions) {
+                        uint64_t gap;
+
                         if (p->allocated_to_area != a)
                                 continue;
 
@@ -1730,6 +1737,21 @@ static void context_place_partitions(Context *context) {
                         assert(left >= p->new_padding);
                         start += p->new_padding;
                         left -= p->new_padding;
+
+                        /* Re-align start to the grain after each partition, so that the next
+                         * partition placed into this free area also starts on a grain boundary.
+                         * This matters when the grain is larger than the default (e.g. 1 MiB via
+                         * --grain-size=) and a small partition like verity-sig (16 KiB) precedes
+                         * a larger one: without this, the successor would start at an unaligned
+                         * offset. */
+                        gap = round_up_size(start, context->grain_size) - start;
+                        if (gap > left) {
+                                log_warning("Not enough space left in free area to re-align partition start to grain size, "
+                                            "next partition may start at an unaligned offset.");
+                                gap = 0;
+                        }
+                        start += gap;
+                        left -= gap;
                 }
         }
 }
@@ -3579,7 +3601,7 @@ static int context_load_fallback_metrics(Context *context) {
         assert(context);
 
         context->sector_size = arg_sector_size > 0 ? arg_sector_size : 512;
-        context->grain_size = MAX(context->sector_size, 4096U);
+        context->grain_size = determine_grain_size(context->sector_size);
         context->default_fs_sector_size = arg_sector_size > 0 ? arg_sector_size : DEFAULT_FILESYSTEM_SECTOR_SIZE;
         return 1; /* Starting from scratch */
 }
@@ -3672,7 +3694,7 @@ static int context_load_partition_table(Context *context) {
                         /* Use the fallback values if we have no better idea */
                         context->sector_size = fdisk_get_sector_size(c);
                         context->default_fs_sector_size = fs_secsz;
-                        context->grain_size = MAX(context->sector_size, 4096U);
+                        context->grain_size = determine_grain_size(context->sector_size);
                         return /* from_scratch= */ true;
                 }
 
@@ -3701,9 +3723,9 @@ static int context_load_partition_table(Context *context) {
         if (secsz < 512 || !ISPOWEROF2(secsz))
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Sector size %lu is not a power of two larger than 512? Refusing.", secsz);
 
-        /* Use at least 4K, and ensure it's a multiple of the sector size, regardless if that is smaller or
-         * larger */
-        grainsz = MAX(secsz, 4096U);
+        /* Determine the grain size: by default at least 4K and a multiple of the sector size, but may be
+         * overridden via --grain-size=. */
+        grainsz = determine_grain_size(secsz);
 
         log_debug("Sector size of device is %lu bytes. Using default filesystem sector size of %" PRIu64 " and grain size of %" PRIu64 ".", secsz, fs_secsz, grainsz);
 
@@ -9069,6 +9091,7 @@ static int help(void) {
                "     --offline=BOOL       Whether to build the image offline\n"
                "     --discard=BOOL       Whether to discard backing blocks for new partitions\n"
                "     --sector-size=SIZE   Set the logical sector size for the image\n"
+               "     --grain-size=BYTES   Set the grain size for partition alignment\n"
                "     --architecture=ARCH  Set the generic architecture for the image\n"
                "     --size=BYTES         Grow loopback file to specified size\n"
                "     --seed=UUID          128-bit seed UUID to derive all UUIDs from\n"
@@ -9199,6 +9222,7 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_DEFER_PARTITIONS_EMPTY,
                 ARG_DEFER_PARTITIONS_FACTORY_RESET,
                 ARG_SECTOR_SIZE,
+                ARG_GRAIN_SIZE,
                 ARG_SKIP_PARTITIONS,
                 ARG_ARCHITECTURE,
                 ARG_OFFLINE,
@@ -9248,6 +9272,7 @@ static int parse_argv(int argc, char *argv[]) {
                 { "defer-partitions-empty",         required_argument, NULL, ARG_DEFER_PARTITIONS_EMPTY         },
                 { "defer-partitions-factory-reset", required_argument, NULL, ARG_DEFER_PARTITIONS_FACTORY_RESET },
                 { "sector-size",                    required_argument, NULL, ARG_SECTOR_SIZE                    },
+                { "grain-size",                     required_argument, NULL, ARG_GRAIN_SIZE                     },
                 { "architecture",                   required_argument, NULL, ARG_ARCHITECTURE                   },
                 { "offline",                        required_argument, NULL, ARG_OFFLINE                        },
                 { "copy-from",                      required_argument, NULL, ARG_COPY_FROM                      },
@@ -9570,6 +9595,15 @@ static int parse_argv(int argc, char *argv[]) {
 
                         break;
 
+                case ARG_GRAIN_SIZE:
+                        r = parse_size(optarg, 1024, &arg_grain_size);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse --grain-size= parameter: %s", optarg);
+                        if (arg_grain_size < 512 || !ISPOWEROF2(arg_grain_size))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Grain size must be a power of 2 >= 512.");
+
+                        break;
+
                 case ARG_ARCHITECTURE:
                         r = architecture_from_string(optarg);
                         if (r < 0)