sysupdate: add PartitionZeroFill= and enable it by default for verity signature partitions
A verity signature partition carries a JSON object that must be followed by
NUL bytes only, up to the end of the partition (dissect refuses anything
else). The split-out artifact repart generates for it however is trimmed of
that padding since b54ef83414, so that it is a plain JSON text file (which
jq nowadays insists on).
sysupdate installs such a file into a partition by simply writing it at the
partition's offset, leaving whatever follows in place. Hence, once a shorter
signature (e.g. after rotating to a different key type) is installed into a
partition that previously held a longer one, the tail of the old signature
survives right after the new JSON object, and the partition becomes
unparsable.
Add a new PartitionZeroFill= boolean setting to the [Target] section, which
makes the importer continue writing zeroes up to the end of the partition
once the payload has been written, by passing --zero-fill= to systemd-import
and systemd-pull. It defaults to off, except for verity signature partitions
(which are tiny, hence this is cheap) where it defaults to on.
Fixes: #43567
(cherry picked from commit d3b6d765a5b320d6d864591c6a7b87e1047d0c78)
This commit is contained in:
committed by
Luca Boccassi
parent
f9cfc019f9
commit
2c4e3b9a6b
@@ -795,6 +795,28 @@
|
||||
<xi:include href="version-info.xml" xpointer="v251"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>PartitionZeroFill=</varname></term>
|
||||
|
||||
<listitem><para>Takes a boolean. When the target <varname>Type=</varname> is picked as
|
||||
<constant>partition</constant>, 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.</para>
|
||||
|
||||
<para>If not specified, defaults to on for Verity signature partitions (i.e. if
|
||||
<varname>MatchPartitionType=</varname> selects one of the <literal>*-verity-sig</literal> 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 <option>--split=</option> mode of
|
||||
<citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry>)
|
||||
generally do not carry this padding.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v263"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>ReadOnly=</varname></term>
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" <<EOF
|
||||
label: gpt
|
||||
unit: sectors
|
||||
|
||||
size=64, type=$ptype_uuid, name=_empty
|
||||
size=64, type=$ptype_uuid, name=_empty
|
||||
EOF
|
||||
|
||||
if [[ "$source_type" == "url-file" ]]; then
|
||||
source_path="file://$sigdir"
|
||||
else
|
||||
source_path="$sigdir"
|
||||
fi
|
||||
|
||||
cat >"$defdir/01-zerofill.transfer" <<EOF
|
||||
[Source]
|
||||
Type=$source_type
|
||||
Path=$source_path
|
||||
MatchPattern=sig-@v.raw
|
||||
|
||||
[Target]
|
||||
Type=partition
|
||||
Path=$blockdev
|
||||
MatchPattern=sig-@v
|
||||
MatchPartitionType=$ptype_name
|
||||
$setting
|
||||
EOF
|
||||
|
||||
# The partition slots are 64 sectors (32768 bytes) each. Payload sizes and the tail state to expect in the
|
||||
# partition after each version, given that with two slots each version replaces the one two steps before:
|
||||
#
|
||||
# v1: 3000 bytes into an empty slot, hence nothing can survive after it
|
||||
# v2: exactly fills the slot, nothing is left to zero out at all
|
||||
# v3: 130 bytes over the 3000 of v1, the tail of v1 survives unless zero filling is on
|
||||
# v4: leaves less than a sector free after it over the full slot of v2, i.e. the remainder is too small
|
||||
# for BLKZEROOUT, and the tail of v2 survives unless zero filling is on
|
||||
local -a versions=(v1 v2 v3 v4)
|
||||
local -A payload_size=([v1]=3000 [v2]=32768 [v3]=130 [v4]=32668)
|
||||
local -A zeroed=([v1]=1 [v2]=1 [v3]="$expect_zeroed" [v4]="$expect_zeroed")
|
||||
local v prev=
|
||||
|
||||
for v in "${versions[@]}"; do
|
||||
make_zero_fill_payload "${payload_size[$v]}" "$sigdir/sig-$v.raw"
|
||||
(cd "$sigdir" && sha256sum sig-*.raw >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
|
||||
|
||||
Reference in New Issue
Block a user