diff --git a/man/sysupdate.d.xml b/man/sysupdate.d.xml
index 3572322682..af415ed7fa 100644
--- a/man/sysupdate.d.xml
+++ b/man/sysupdate.d.xml
@@ -795,6 +795,28 @@
+
+ PartitionZeroFill=
+
+ Takes a boolean. When the target Type= is picked as
+ partition, controls whether the remainder of the partition is overwritten with
+ zeroes after the payload has been written into it. If disabled, only as much of the partition is
+ written as the payload is large, and whatever data was previously stored in the rest of the partition
+ remains in place. If enabled, everything from the end of the payload up to the end of the partition
+ is zeroed out, so that no remnants of an earlier, larger payload survive. On block devices this is
+ offloaded to the kernel and hence the storage device where possible.
+
+ If not specified, defaults to on for Verity signature partitions (i.e. if
+ MatchPartitionType= selects one of the *-verity-sig types) and
+ off for all other partition types. Verity signature partitions carry a JSON object that must be
+ followed by zero bytes only, up to the end of the partition, while the artifacts installed into them
+ (for example as generated via the mode of
+ systemd-repart8)
+ generally do not carry this padding.
+
+
+
+
ReadOnly=
diff --git a/src/sysupdate/sysupdate-transfer.c b/src/sysupdate/sysupdate-transfer.c
index 226dbd93ae..930e453737 100644
--- a/src/sysupdate/sysupdate-transfer.c
+++ b/src/sysupdate/sysupdate-transfer.c
@@ -104,6 +104,9 @@ Transfer* transfer_new(Context *ctx) {
.read_only = -1,
.growfs = -1,
+ /* whether to fill up the rest of the partition with zeroes, as configured by the user */
+ .zero_fill = -1,
+
/* the read only flag, as ultimately determined */
.install_read_only = -1,
@@ -564,6 +567,7 @@ int transfer_read_definition(Transfer *t, const char *path, const char **dirs, H
{ "Target", "PartitionFlags", config_parse_partition_flags, 0, t },
{ "Target", "PartitionNoAuto", config_parse_tristate, 0, &t->no_auto },
{ "Target", "PartitionGrowFileSystem", config_parse_tristate, 0, &t->growfs },
+ { "Target", "PartitionZeroFill", config_parse_tristate, 0, &t->zero_fill },
{ "Target", "ReadOnly", config_parse_tristate, 0, &t->read_only },
{ "Target", "Mode", config_parse_mode, 0, &t->mode },
{ "Target", "TriesLeft", config_parse_uint64, 0, &t->tries_left },
@@ -1326,6 +1330,25 @@ int transfer_compute_temporary_paths(Transfer *t, Instance *i, InstanceMetadata
return 0;
}
+static bool transfer_zero_fill(const Transfer *t) {
+ assert(t);
+ assert(t->target.type == RESOURCE_PARTITION);
+ assert(t->target.partition_type_set);
+
+ /* Returns whether to fill up the rest of the partition with zeroes after writing the payload. If
+ * configured explicitly, use that. Otherwise, do so for verity signature partitions only: they carry
+ * a JSON object that must be followed by NUL bytes only, up to the end of the partition. The
+ * artifacts we install there generally come without that padding though (repart's --split= mode
+ * trims it, so that the file is a plain JSON text file), hence without zero filling the tail of a
+ * previous, longer signature would survive right after the new one. These partitions are tiny, hence
+ * this is cheap. */
+
+ if (t->zero_fill >= 0)
+ return t->zero_fill;
+
+ return partition_designator_is_verity_sig(t->target.partition_type.designator);
+}
+
int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, TransferProgress cb, void *userdata) {
_cleanup_free_ char *digest = NULL;
char offset[DECIMAL_STR_MAX(uint64_t)+1], max_size[DECIMAL_STR_MAX(uint64_t)+1];
@@ -1449,6 +1472,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, Tra
"--direct", /* just copy/unpack the specified file, don't do anything else */
"--offset", offset,
"--size-max", max_size,
+ transfer_zero_fill(t) ? "--zero-fill=yes" : "--zero-fill=no",
t->context->sync ? "--sync=yes" : "--sync=no",
i->path,
t->target.path),
@@ -1528,6 +1552,7 @@ int transfer_acquire_instance(Transfer *t, Instance *i, InstanceMetadata *f, Tra
"--verify", digest, /* validate by explicit SHA256 sum */
"--offset", offset,
"--size-max", max_size,
+ transfer_zero_fill(t) ? "--zero-fill=yes" : "--zero-fill=no",
t->context->sync ? "--sync=yes" : "--sync=no",
i->path,
t->target.path),
diff --git a/src/sysupdate/sysupdate-transfer.h b/src/sysupdate/sysupdate-transfer.h
index 64af8c2fc3..958b02215f 100644
--- a/src/sysupdate/sysupdate-transfer.h
+++ b/src/sysupdate/sysupdate-transfer.h
@@ -37,6 +37,7 @@ typedef struct Transfer {
int no_auto;
int read_only;
int growfs;
+ int zero_fill;
/* If we create a new file/dir/subvol in the fs, the temporary and final path we create it under, as well as the read-only flag for it */
char *temporary_partial_path;
diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh
index fc98dd8963..63c4136022 100755
--- a/test/units/TEST-72-SYSUPDATE.sh
+++ b/test/units/TEST-72-SYSUPDATE.sh
@@ -58,6 +58,7 @@ systemctl daemon-reload
SIGTEST_GPGHOME=
SIGTEST_OTHERHOME=
+ZEROFILL_LOOP=
at_exit() {
set +e
@@ -76,6 +77,9 @@ at_exit() {
if [ "$SIGTEST_OTHERHOME" != "" ]; then
gpgconf --homedir "$SIGTEST_OTHERHOME" --kill all 2>/dev/null
fi
+ if [ "$ZEROFILL_LOOP" != "" ]; then
+ losetup --detach "$ZEROFILL_LOOP"
+ fi
rm -rf "$WORKDIR"
}
@@ -1255,6 +1259,161 @@ rm -rf "$CONFIGDIR" "$WORKDIR/blobs"
rm -f "$WORKDIR/source/notifytest-v1.bin" "$WORKDIR/source/SHA256SUMS" \
"$WORKDIR/notify-recorder.py" "$NOTIFY_LOG"
+verify_zero_fill_partition() {
+ local blockdev="${1:?}"
+ local sigfile="${2:?}"
+ local version="${3:?}"
+ local expect_zeroed="${4:?}"
+ local start size dump tail
+
+ start="$(sfdisk --json "$blockdev" | jq -r ".partitiontable.partitions[] | select(.name == \"sig-$version\") | .start")"
+ test -n "$start"
+ size="$(stat -c %s "$sigfile")"
+ dump="$WORKDIR/zerofill-dump"
+
+ dd if="$blockdev" of="$dump" bs=512 skip="$start" count=64 status=none
+
+ # The partition must start with the payload file in any case
+ cmp -n "$size" "$dump" "$sigfile"
+
+ # Everything after it must be NUL bytes if zero filling is on, and must not be if it is off (the tail of
+ # the longer payload of the version previously stored in the partition then survives)
+ tail="$(tail -c +$((size + 1)) "$dump" | tr -d '\0' | wc -c)"
+ if [[ "$expect_zeroed" == "1" ]]; then
+ test "$tail" -eq 0
+ else
+ test "$tail" -gt 0
+ fi
+}
+
+make_zero_fill_payload() {
+ local size="${1:?}"
+ local file="${2:?}"
+
+ # Generates a JSON object resembling a verity signature of precisely the requested size: the fixed part of
+ # the object is 94 bytes, the signature string makes up the rest
+ test "$size" -gt 94
+ printf '{"rootHash":"%s","signature":"%s"}' \
+ "$(head -c 32 /dev/zero | od -An -v -tx1 | tr -d ' \n')" \
+ "$(head -c $((size - 94)) /dev/zero | tr '\0' A)" >"$file"
+ test "$(stat -c %s "$file")" -eq "$size"
+}
+
+test_zero_fill_partition() {
+ local source_type="${1:?}" # Source Type= to use (regular-file or url-file)
+ local ptype_uuid="${2:?}" # partition type UUID to create the partitions with
+ local ptype_name="${3:?}" # matching partition type identifier for MatchPartitionType=
+ local setting="${4}" # explicit PartitionZeroFill= line, or empty to test the default
+ local expect_zeroed="${5:?}" # whether the tail of the partition is expected to be zeroed out
+ local sigdir="$WORKDIR/zerofill-source"
+ local defdir="$WORKDIR/zerofill-defs"
+ local backing="$WORKDIR/zerofill-disk.raw"
+ local blockdev source_path
+
+ rm -rf "$sigdir" "$defdir" "$backing"
+ mkdir -p "$sigdir" "$defdir"
+ truncate -s 4M "$backing"
+
+ if [[ -e /dev/loop-control ]]; then
+ blockdev="$(losetup --find --show "$backing")"
+ ZEROFILL_LOOP="$blockdev"
+ else
+ blockdev="$backing"
+ fi
+
+ sfdisk "$blockdev" <"$defdir/01-zerofill.transfer" <SHA256SUMS)
+
+ "$SYSUPDATE" --verify=no --definitions="$defdir" update
+ verify_zero_fill_partition "$blockdev" "$sigdir/sig-$v.raw" "$v" "${zeroed[$v]}"
+
+ # The version written in the previous step is in the other slot, and must be untouched, i.e. the zero
+ # filling must not have overshot the partition
+ if [[ -n "$prev" ]]; then
+ verify_zero_fill_partition "$blockdev" "$sigdir/sig-$prev.raw" "$prev" "${zeroed[$prev]}"
+ fi
+ prev="$v"
+ done
+
+ # v3 and v4 must have replaced v1 and v2
+ (! sfdisk --json "$blockdev" | jq -e '.partitiontable.partitions[] | select(.name == "sig-v1" or .name == "sig-v2")' >/dev/null)
+
+ # And the partition table itself (including the backup GPT at the end of the device, right after the last
+ # slot) must be intact
+ sfdisk --verify "$blockdev"
+
+ if [[ "$ZEROFILL_LOOP" != "" ]]; then
+ losetup --detach "$ZEROFILL_LOOP"
+ ZEROFILL_LOOP=
+ fi
+
+ rm -rf "$sigdir" "$defdir" "$backing"
+}
+
+test_zero_fill() {
+ # A verity signature partition carries a JSON object that must be followed by NUL bytes only. The split-out
+ # artifact is not padded (so that it's a valid JSON text file), hence sysupdate must make sure that no tail
+ # of a previously installed, longer signature survives when it installs a shorter one into a reused
+ # partition, see https://github.com/systemd/systemd/issues/43567. This is controlled by the
+ # PartitionZeroFill= setting, which defaults to on for verity signature partitions, and off otherwise.
+ local vsig_uuid=41092b05-9fc8-4523-994f-2def0408b176 # root-x86-64-verity-sig
+ local generic_uuid=0fc63daf-8483-4772-8e79-3d69d8477de4 # linux-generic
+
+ # Default for verity signature partitions is on, both for local and URL sources
+ test_zero_fill_partition regular-file "$vsig_uuid" root-x86-64-verity-sig "" 1
+ test_zero_fill_partition url-file "$vsig_uuid" root-x86-64-verity-sig "" 1
+
+ # ... and can be turned off explicitly
+ test_zero_fill_partition regular-file "$vsig_uuid" root-x86-64-verity-sig "PartitionZeroFill=no" 0
+
+ # Default for other partitions is off, but can be turned on explicitly
+ test_zero_fill_partition regular-file "$generic_uuid" linux-generic "" 0
+ test_zero_fill_partition regular-file "$generic_uuid" linux-generic "PartitionZeroFill=yes" 1
+ test_zero_fill_partition url-file "$generic_uuid" linux-generic "PartitionZeroFill=yes" 1
+}
+
test_signature_verification() {
if ! command -v gpg >/dev/null; then
echo "gpg not available, skipping signature verification test"
@@ -1500,6 +1659,7 @@ EOF
)
test_signature_verification
+test_zero_fill
test_signature_keyring_overlay
# Test '**/' as prefix in MatchPattern= for subpaths in SHA256SUMS