Measure the volume key and unlock keyslot through io.systemd.PCRExtend
instead of driving the TPM directly via tpm2-util, matching how the
verity and imds measurements already work.
Bank selection and the TPM context now live entirely in
systemd-pcrextend. As a result the tpm2-measure-bank= crypttab option
can no longer be honored per volume and is now a deprecated no-op.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
pcrextend_verity_now() and pcrextend_imds_userdata_now() carried
near-identical copies of the connect + io.systemd.PCRExtend.Extend call.
Factor that into pcrextend_pcr_now() and pcrextend_nvpcr_now() and
reimplement both on top of them. No functional change.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Add an optional 'secret' input to io.systemd.PCRExtend.Extend. When set,
the HMAC of the measured data keyed by the secret is extended instead of
a plain hash, matching the existing tpm2_{pcr,nvpcr}_extend_bytes()
secret parameter. This lets callers measure a secret (e.g. a volume key)
without leaking a hash of it.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
extend_pcr_now(), extend_nvpcr_now() and escape_and_truncate_data() took
a (void *data, size_t) pair, even though both the dispatch layer and
tpm2_{pcr,nvpcr}_extend_bytes() already speak struct iovec. Drop the
pointless deconstruct/reassemble and pass the iovec through directly.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
The userspace measurement log carries a sticky-bit marker while a writer
is between updating a measurement register and appending the matching
record, so that a writer dying in between leaves the log detectably
incomplete.
However, the next successful writer used to clear the marker again after
appending its own record, erasing the evidence that an earlier writer
had died and the log is still missing a record. Keep the marker set in
that case instead; the new record is appended and synced regardless. The
marker likewise stays set if the measurement itself fails, so that any
non-clean completion remains flagged: a spurious flag on a failed but
harmless measurement is preferable to erasing evidence of a real gap,
and PCR replay stays authoritative either way.
Finally, warn when systemd-pcrlock loads a marked log, so the resulting
PCR validation failures come with a hint at their cause.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Both callers of tpm2_nvpcr_extend_bytes() duplicate the same dance: on
-ENETDOWN, i.e. when the NvPCR isn't anchored yet because
systemd-tpm2-setup hasn't run, they acquire the anchor secret,
initialize the NvPCR, and extend again. Move this into
tpm2_nvpcr_extend_bytes() itself. The only caller-specific bit, whether
the anchor secret shall be synced to /var, is passed in as a parameter.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
CoCo firmware is stateless, so Secure Boot keys cannot be enrolled at
runtime: it only enforces Secure Boot with keys baked in at build time,
refusing unsigned images from the first boot. The default exclusion of
the enrolled-keys firmware feature hence keeps unsigned images bootable,
but it also makes firmware discovery fail on distros that only ship
SNP/TDX firmware with pre-enrolled keys (e.g. Fedora's TDVF). Drop the
rejection of --secure-boot=yes with --coco= and instead treat it as an
opt-in to such firmware, by lifting the enrolled-keys exclusion.
While at it, reject --efi-nvram-template= and an explicit
--efi-nvram-state= path with --coco=, which were silently ignored, as
stateless firmware has no NVRAM to instantiate or persist.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Carry the firmware descriptor's feature list in OvmfConfig, and verify
that firmware selected via an explicit --firmware= descriptor declares
the amd-sev-snp/intel-tdx feature, is stateless, and is in raw format,
the same constraints automatic discovery already imposes. This replaces
the cached supports_sb boolean with a generic feature lookup.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Instead of requiring --firmware= to point at a raw .fd image for
--coco=sev-snp/tdx, pick a suitable firmware automatically from the
QEMU firmware descriptors, requiring the amd-sev-snp/intel-tdx feature.
CoCo firmware is stateless (it carries no NVRAM template), so teach
find_ovmf_config() to select stateless raw-format firmware via new
flags. --firmware= now uniformly takes a firmware descriptor path.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
The JSON output keyed every section by name, so a UKI with repeated
sections only showed the last one: in a multi-profile UKI all but one
.profile/.cmdline/.pcrsig were dropped, as were extra .dtbauto/.efifw.
Report the shared base sections by name at the top level (so .cmdline
etc. stay where they were), each profile as its own by-name object under
a new "_profiles" array, and the alternative-set sections (.dtbauto,
.efifw) as arrays.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
In commit 29c6d1c125 ("boot: measure select SMBIOS objects
explicitly") we added an explicit SMBIOS measurement in systemd-boot
and systemd-stub, covering cases where firmware doesn't measure SMBIOS
itself. measure_smbios() bailed unless a TPM was present, so it skipped
the measurement on confidential guests that only expose a CC
measurement protocol (e.g. Intel TDX RTMRs).
We can have much higher expectations of the virtual firmware used for
confidential computing, and the firmware is attested, so we can expect
it to always measure SMBIOS in this case. We still do our own
measurement to get a measurement structure similar to that of
non-confidential guests, and as another line of defense.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
SMBIOS OEM strings are host-controlled and normally distrusted by PID1
in confidential VMs. Under TDX, however, the SMBIOS table (including
Type 11) is measured into RTMR0 by the firmware (TDVF), so a remote
verifier can detect host tampering with credentials delivered this
way. Accept them there, while keeping fw_cfg distrusted as those
items are not measured even on TDX.
This lets systemd-vmspawn deliver credentials to TDX guests via the
normal SMBIOS path, unlike SNP which requires the initrd cpio channel.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Previously we would return a valid signature containing the empty
outblob, which is undesirable. In other cases where we cannot query a
report because the guest doesn't support it we currently return an
empty response so the signature aggregator in systemd-report silently
skips it. In this case, we have everything we need to actually get a
report on the guest side, but the host isn't providing us with the
quote, so we fail.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
To query a TD quote, the TDX guest must connect to the Quote Generation
Service (QGS), a SGX enclave running on the host. We check if the
service is exposed via a well-known unix socket, then pass that socket
or a fallback well-known vsock address to QEMU.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Wire up --coco=tdx alongside the existing SEV-SNP path. TDX requires KVM
on x86_64, a raw TDVF firmware loaded via -bios (no pflash/NVRAM split),
kernel-irqchip=split, and the "host" CPU model since QEMU rejects named
models. Sets up the tdx-guest object and confidential-guest-support=tdx0.
TDX measurement is different from QEMU's kernel-hashes injection: TDX
provides runtime measurements via RTMRs, so the initial measurement only
covers the firmware, which then measures the rest of the boot chain into
those RTMRs (done by OVMF today). Therefore a restriction to direct
kernel boot isn't required either.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
setup_swtpm() decided whether a software TPM had already been
manufactured by checking whether the state directory was empty. But
manufacture_swtpm() writes swtpm's config files before forking
swtpm_setup, so an interrupted manufacture leaves the directory
non-empty yet without a usable TPM. The next boot then mistook it for a
complete TPM and started swtpm against a broken state directory.
Keying off a swtpm state file like tpm2-00.permall is no better, as
swtpm_setup gives no guarantee any single one is written atomically or
last. Instead, have manufacture_swtpm() write a marker (.manufactured)
as its very last step, once swtpm_setup has exited successfully, and
gate on it: re-manufacture when it is missing in the initrd, and refuse
rather than start a broken TPM outside it.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Open the swtpm state directory once and write the three config files
relative to that fd with WRITE_STRING_FILE_ATOMIC, rather than by path
with a plain truncating write. Writing atomically ensures a crash or a
concurrent reader never observes a half-written config file, and
operating through a single directory fd lets later steps reuse it.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Apply sandboxing. The plain backend's needs writable StateDirectory and
/dev/urandom for key generation. The service must stay root (the
private key is root-only), but everything else is locked down.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Lock down the software TPM service: restrict the runtime directory (which
holds the AES key sealing swtpm's state) to 0700, and apply the usual
sandboxing (NoNewPrivileges, MemoryDenyWriteExecute, ProtectSystem-adjacent
Protect*/Restrict* knobs, PrivateNetwork, PrivateTmp, a @system-service
syscall filter, etc.).
A few common knobs can't be used here: the service must keep CAP_SYS_ADMIN
(needed for the ioctl that creates the vtpm proxy device on /dev/vtpmx),
and it needs runtime access to the ESP and its backing block device at a
path only known at runtime, which rules out PrivateDevices=, DevicePolicy=,
ProtectSystem= and User=/DynamicUser=.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
swtpm keeps its state on the ESP (--tpmstate=dir=) and thus holds it
busy for as long as it runs, but nothing ensured it was stopped before
the ESP was unmounted on shutdown, leaving boot.mount failing to
unmount.
Two things were missing:
- systemd-tpm2-swtpm.service has DefaultDependencies=no, which strips
the implicit shutdown.target membership, so it was torn down late
rather than stopped in an ordered manner. Add
Conflicts=/Before=shutdown.target, as the sibling
systemd-tpm2-setup{,-early}.service units already do.
- The generator only ordered the service
After=boot.automount/efi.automount. Ordering after the .automount
units is enough for start-up, but only an ordering against the actual
.mount units makes the service stop (releasing the ESP) before the
file system is unmounted. Add boot.mount/efi.mount to the After= line;
this is a no-op at start-up, as the mount has no job of its own there
(it is triggered on access via the automount).
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Boot a VM in EFI mode without a hardware/firmware TPM and with
systemd.tpm2_software_fallback=yes, so systemd-tpm2-generator manufactures a
software TPM on the ESP in the initrd and chainloads swtpm. Assert the service
starts, the vtpm-proxy device shows up, and a systemd-creds TPM2 seal/unseal
round-trip works. Then reboot and confirm the sealed secret still unseals,
i.e. the TPM state persisted on the ESP across the reboot.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Under --coco=sev-snp, credentials no longer flow through SMBIOS/fw_cfg
(which the guest PID1 discards as unmeasured in confidential VMs) but
through a cpio archive appended to the initrd, landing in the @system
bucket via the new /.extra/system_credentials/ initrd path. Update
systemd-vmspawn(1) to describe this and the guest systemd version
requirement.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Previously, --load-credential / --set-credential were rejected outright
under --coco=sev-snp because the SMBIOS type-11 transport isn't covered
by the launch measurement. PID1 wouldn't have accepted those credentials
anyway (import_credentials_smbios() refuses any SMBIOS-sourced credentials
under a confidential VM).
Instead, when SNP is in use and credentials are present, synthesize a
newc cpio archive containing each credential at
.extra/system_credentials/<id>.cred and append it to the initrd list.
The existing merge_initrds() path then concatenates it into the single
initrd file QEMU loads, which kernel-hashes=on covers in the SEV-SNP
launch digest. PID1's import_credentials_boot() picks them up from the
trusted /.extra/system_credentials/ path and routes them to the @system
bucket, so units can consume them via LoadCredential= unchanged.
Direct kernel boot (--linux=) is already required under SNP, so the
initrd is always under our control here. The cpio synthesis happens
after all internal machine_credential_add()/machine_credential_load()
call sites so the archive captures the complete credential set (journal
forwarding, vmm.notify_socket, ssh ephemeral keys, etc.).
The cpio path is intentionally scoped to SNP: it requires a guest PID1
that knows about /.extra/system_credentials/, and we don't want to
regress credential delivery for non-CoCo guests running older systemd
versions in the guest. Consider switching when the new path is widely
available.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Add a small newc-format cpio encoder that builds an archive with each
credential as a file under .extra/system_credentials/<id>.cred and
writes it to a temp file. This mirrors what systemd-stub produces from
ESP credentials, so PID1's import_credentials_boot() picks them up
unchanged via the new /.extra/system_credentials/ initrd path.
Motivated by vmspawn under SEV-SNP, where SMBIOS credentials aren't
covered by the launch measurement and are discarded by PID1 in
confidential guests, so they must be delivered via the measured initrd
instead. The writer lives in src/shared/ so other host-side tooling
can reuse it.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
PID1's import_credentials_boot() so far always treated initrd-delivered
credentials as untrusted: anything found under /.extra/credentials/
was routed into ENCRYPTED_CREDENTIALS_DIRECTORY, forcing consumers to
use LoadCredentialEncrypted= and provide credentials in systemd-creds
encrypted form. That matches the trust model for stub which sources
credentials from the EFI System Partition (mountable and editable
offline).
Host-side producers that take responsibility for the trust of the cpio
they hand to the kernel have a different model: e.g. systemd-vmspawn
builds an initrd-credentials cpio whose bytes are covered by the SEV-SNP
launch measurement via QEMU's kernel-hashes=on (or, in non-confidential
setups, by the host itself being the trust root). Forcing those through
the @encrypted bucket would require null-key wrapping on the host and
LoadCredentialEncrypted= on the consumer side, a needless API split for
unit files that should otherwise be portable between confidential and
non-confidential boots.
Extend import_credentials_boot() to import credentials from
/.extra/system_credentials/, routed into SYSTEM_CREDENTIALS_DIRECTORY.
Consumers access these via LoadCredential= directly, with no
encrypted-credential ceremony.
The per-directory walk is hoisted into a small static helper so the new
target bucket can share the existing copy/validation logic; behavior
for the existing untrusted paths is unchanged.
Also set the new RECURSE_DIR_MUST_BE_REGULAR flag as a drive-by.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
vmm.c carries the confidential-VM detection used by sd-boot/sd-stub.
Its detect_tdx() had the same dead guard as the userspace copy: it
gated the 0x21 read on CPUID_GET_HIGHEST_FUNCTION (0x80000000, the
extended max function), which is always >= 0x80000000, so the guard
never held.
Mirror the userspace fix: read leaf 0x21 directly and rely on the
IntelTDX signature, matching the kernel. An out-of-range CPUID leaf
returns the highest basic leaf's data (no fault), and 0x21 is a
synthetic TDX leaf whose presence need not be reflected in the max
basic function, so it must not be gated on it.
Ref: Linux 59bd54a84d15 ("x86/tdx: Detect running as a TDX guest in
early boot"), arch/x86/coco/tdx/tdx.c:1119 (tdx_early_init()).
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
detect_tdx() guarded the read of the TDX enumeration leaf (0x21, a
standard leaf) with CPUID_GET_HIGHEST_FUNCTION (0x80000000), which
returns the highest *extended* function. eax is therefore always
>= 0x80000000, so the "eax < 0x21" guard never held and the leaf was
read unconditionally anyway.
Drop the guard rather than re-gate it on the basic max function
(leaf 0), and read 0x21 directly, relying on the IntelTDX signature
compare. This matches the kernel, which reads the leaf unconditionally
on purpose: an out-of-range CPUID leaf returns the highest basic leaf's
data (no fault, per the Intel SDM), and 0x21 is a synthetic TDX leaf
whose presence need not be reflected in the reported max basic function,
so gating the read on it risks missing a genuine TDX guest. With no
guard the Hyper-V isolation fallback (Azure TDX guests have 0x21
blocked) also stays reachable.
Ref: Linux 59bd54a84d15 ("x86/tdx: Detect running as a TDX guest in
early boot"), arch/x86/coco/tdx/tdx.c:1119 (tdx_early_init()).
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
msr() returned 0 on failure, indistinguishable from a real MSR value of
0. With /dev/cpu/0/msr unavailable (e.g. the msr module not loaded in the
initrd), detect_sev() read 0 and reported a genuine SEV/-ES/-SNP guest as
CONFIDENTIAL_VIRTUALIZATION_NONE.
That inverts the firmware-credential trust gate: import_credentials_*()
skip fw_cfg/SMBIOS credentials only when detect_confidential_virtualization()
is > 0 ("don't trust firmware in confidential VMs"). A false NONE makes a
confidential guest trust and import credentials injected by the untrusted
hypervisor.
msr() now returns a negative errno, and detect_sev() assumes plain SEV when
the MSR is unreadable but CPUID already advertised SEV under a hypervisor,
so the gate still trips.
The conservative branch only fires when CPUID already advertised SEV, i.e.
for a guest the hypervisor marked SEV-capable. QEMU gates that CPUID leaf on
the SEV launch object and does not expose it to ordinary guests even under
-cpu host, so it does not misfire for non-confidential guests. Were a
hypervisor to expose the bit anyway the outcome is fail-safe (we only
decline to trust firmware-supplied data); nothing in-tree branches on the
specific SEV tier.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
vl_method_extend() accepted an empty text/data value and measured an
empty word, bypassing the empty-word refusal the CLI path already
enforces. Measured words are joined with ":" in the record, so an empty
word is ambiguous. Reject it.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
tpm2_index_to_handle() returns 0 with a NULL handle when the NV index is
not present on the TPM. tpm2_nvpcr_extend_bytes() only checked for r < 0,
so a tombstoned NvPCR (anchor file present, NV slot cleared out from under
us) passed the NULL handle to tpm2_extend_nvpcr_nv_index() and aborted the
process via its assert(). Handle r == 0 explicitly, as the other
tpm2_index_to_handle() callers already do.
The newly introduced -ENODEV is mapped together with -ENOENT to the
io.systemd.PCRExtend.NoSuchNvPCR varlink error.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
A report signing backend that returns a confidential-computing
attestation report obtained via configfs-tsm. Implements
io.systemd.Report.Signer.Sign(): embeds the digest as the report's
inblob and returns the outblob (plus provider and any aux/manifest
blobs). Wired up as the "tsm" mechanism with a socket-activated service.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Add tsm_report_acquire(), a thin wrapper around the kernel's
/sys/kernel/config/tsm/report/ configfs interface for fetching a
confidential-computing attestation report (SEV-SNP, TDX, ...), including
a caller supplied input.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
verb_attach() falls back to unsigned activation (crypt_activate_by_volume_key)
when signed activation fails, but still passed the signature to
pcrextend_verity_now(). The signer is parsed out of the (unverified)
signature and folded into the dm_verity NvPCR measurement, making an
unsigned fallback indistinguishable from a genuinely signed activation to
an attester. Only measure the signature when signed activation succeeded.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
When start_tpm() failed under TPM autodetect (arg_tpm <= 0), the swtpm
block logged "Failed to start tpm, ignoring" but then fell through to
event_add_child_pidref() with the still-unset child PidRef. That returns
-ESRCH (PIDREF_NULL fails its pidref_is_set() guard), and the VM launch
fails. Register the child only on start_tpm() success.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
architecture_to_qemu_table mapped four 64-bit/LE architectures to wrong
qemu-system-<target> names: MIPS64_LE and MIPS_LE both to "mips" (the BE
MIPS32 target), and PPC64/PPC64_LE to "ppc" (32-bit PowerPC). The string
feeds both find_qemu_binary() ("qemu-system-" + name) and the
firmware.json architecture match, so on those hosts vmspawn looked up a
wrong or nonexistent binary and never matched a firmware descriptor. Map
them to the real targets: mips64el, mipsel, and ppc64 (qemu-system-ppc64
runs LE pSeries guests; there is no qemu-system-ppc64le).
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
The fstab.extra merge prepends the existing credential via
asprintf("%.*s", (int) existing->size, …). MachineCredential.size is
size_t, so for a credential >INT_MAX the (int) cast yields a negative
precision, which C treats as omitted — turning %.*s into an unbounded
read past the allocation. Reject such a credential up front with EFBIG;
for all realistic sizes the merge is unchanged.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
errno_or_else() already returns a negative errno, so "return
-errno_or_else(EIO)" returned a positive value on fprintf()/ferror()
failure. Callers checking "r < 0" therefore missed the write error and
treated it as success. Drop the spurious negation; these were the only
two "-errno_or_else" uses in the tree.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
vmspawn_varlink_context_free() discarded the sd_varlink_server_unref()
and vmspawn_qmp_bridge_free() return values, leaving ctx->varlink_server
and ctx->bridge dangling. No current handler reads those fields, but use
the assign-back idiom so the fields are NULL during any synchronous
callback regardless of future changes.
Also drain subscribers before freeing the bridge, so subscriber teardown
can't run against a half-freed bridge.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
determine_kernel() ran boot-entry discovery under a !arg_linux guard
only. With --initrd= but no --linux=, discovery overwrote arg_initrds
with the discovered entry's own initrds (leaking the user's strv) and
set arg_linux, so verify_arguments()'s "--initrd= needs --linux=" check,
which runs afterwards, never fired: the user's --initrd= was
silently discarded. Reject the combination before discovery. This also
keeps the discovery store leak-free.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
The ephemeral-snapshot branch did a bare arg_directory = strdup(...),
orphaning the -D/--directory allocation from parse_argv.
Use free_and_strdup(), matching the idiom already used elsewhere in
the file.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
qemu_config_section()/qemu_config_key() write section types and keys
unquoted ([type], key = …), but the type check only rejected '\n' while
the structurally identical id/key checks rejected more, even though the
unquoted type slot is the most sensitive. Replace the three ad-hoc
denylists with one allowlist matching QEMU's identifier grammar
([A-Za-z0-9._-], non-empty). This is merely hardening, all callers pass
constant input today.
For the value check use string_is_safe() for now, which is a bit
stricter than QEMUs own config validation.
Add test-vmspawn-qemu-config covering both.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
do_mcopy() invoked 'mcopy -s' with the top-level sources sorted by name,
but then relied on mtools' built-in recursion to walk into subdirectories.
mtools uses platform readdir(), so the order in which it inserts entries
into the freshly-formatted FAT image inherits whatever order the source
filesystem returns. With an ext4 staging directory the directory hash
changes the readdir order between hosts (and between runs after edits),
and because FAT stores directory entries in insertion order the resulting
ESP differs byte-for-byte across otherwise identical builds.
Stop using 'mcopy -s'. Walk the source ourselves with
readdir_all_at(RECURSE_DIR_SORT) at every level and emit per-item mtools
commands interleaved per parent: each subdirectory is created with 'mmd'
before the recursion descends into it, and consecutive sibling files are
batched into a single 'mcopy' invocation so the fork cost stays
proportional to the number of directory transitions rather than to the
total number of files.
Combined with the existing 'mkfs.fat --invariant' and SOURCE_DATE_EPOCH
plumbing, this makes vfat populated via repart byte-stable regardless of
the staging filesystem's readdir() behaviour.
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Only .dtbauto and .efifw may appear more than once, .hwids is a
singleton per the UKI specification and the stub reads a single .hwids
section (per profile), matching hardware IDs against entries within it.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
strv_split_full() returns the number of parsed entries, which was
assigned to r, the variable that also holds the run's exit status.
With TESTFUNCS set, a passing run of only void tests then exited with
the entry count (e.g. 1) instead of EXIT_SUCCESS. Use a separate
variable for the split result.
Before:
$ env TESTFUNCS=test_copy_tree meson test -C build -v test-copy
1/1 test - systemd:test-copy FAIL 0.01s exit status 1
$ env TESTFUNCS=test_copy_tree:test_copy_file_fd meson test -C build -v test-copy
1/1 test - systemd:test-copy FAIL 0.01s exit status 2
Now:
$ env TESTFUNCS=test_copy_tree:test_copy_file_fd meson test -C build -v test-copy
1/1 test - systemd:test-copy OK 0.01s
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Exercise the early "goto finish" path in fd_copy_directory() by putting
a directory's inode in the denylist as DENY_CONTENTS, and assert the
directory is created in the destination but its contents are not copied.
This path previously had no coverage.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
fd_copy_directory() iterates the source directory with FOREACH_DIRENT_ALL,
i.e. plain readdir(). For ext4 sources (and similar) that order depends on
the directory hash and varies between hosts, which leaks into destinations
that record entries in insertion order. systemd-repart's
partition_populate_directory() stages CopyFiles= into a temp directory and
then later builds a filesystem from it; with the unsorted walk the staging
order ended up baked into the output (most visibly when the destination is
vfat, where dir entries are stored in insertion order).
Replace the raw readdir loop with readdir_all(RECURSE_DIR_SORT), which
collects and qsort()s the entries by name. Dot/dot-dot are filtered by
readdir_all() so the explicit dot_or_dot_dot() guard is dropped.
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
SNP requires a named, stable CPU model so the launch measurement is
reproducible across hosts. EPYC-v4 is the baseline that covers all
SNP-capable processors (Milan and later).
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Add --confidential-computing=sev-snp to run the guest as an AMD SEV-SNP
confidential VM. Loads a raw OVMF firmware blob via -bios (SNP doesn't
support the pflash + NVRAM split), attaches a sev-snp-guest object,
and hashes the kernel, initrd and cmdline into the launch measurement
when direct kernel boot is used. Incompatible features (Secure Boot,
CXL, virtio-balloon, SMBIOS credentials) are rejected or disabled; an
attached vTPM must be treated as untrusted by the guest.
The feature is marked experimental in the man page.
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
get_firmware_search_dirs() previously hardcoded /usr/share/qemu/firmware
as the only system-wide search path. That assumption breaks on
distributions that deliberately do not populate /usr/share, making
vmspawn fail: "Failed to find OVMF config: No such file or directory".
NixOS exposes those firmware locations through XDG_DATA_DIRS.
Extend the search list with XDG_DATA_HOME/XDG_DATA_DIRS. This is the
standard XDG mechanism and is already what QEMU itself uses for the same
descriptors, so behavior matches user expectations across tooling.
To avoid regressing setups where user has set XDG_DATA_DIRS to a custom
value that omits /usr/share, keep /usr/share/qemu/firmware as an
unconditional fallback.
Precedence is unchanged: XDG_CONFIG_HOME/qemu/firmware still wins
over /etc/qemu/firmware, which still wins over any shared-data dir.
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>