test: add TEST-94-COCO integration test for confidential VMs

Teach the harness a --coco=tdx|sev-snp|any flag: it gates the test on
host coco capability (exit 77 otherwise), binds /dev/kvm and the coco
device(s) into the boot-mode container, and passes the resolved type to
the test via $COCO_TYPE.

TEST-94-COCO boots the mkosi image as a confidential guest with
systemd-vmspawn --coco= (using the /vm-images bind) and checks the guest
reports the expected type via systemd-detect-virt --cvm. Runs only on
coco-capable bare-metal hosts, skips elsewhere.

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
This commit is contained in:
Paul Meyer
2026-08-12 15:57:07 +02:00
parent 5f34079a6a
commit b89729b16a
5 changed files with 212 additions and 1 deletions
+18
View File
@@ -141,6 +141,24 @@ journal file will be saved only when the test is failed. Defaults to `fail`.
progress and only move the journal to its final location in the build directory
(`$BUILD_DIR/test/journal`) when the test is finished.
## Confidential computing (coco) tests
Tests that exercise systemd inside a confidential VM (currently `TEST-94-COCO`)
launch a real Intel TDX or AMD SEV-SNP guest with `systemd-vmspawn --coco=`. They
only run on a coco-capable host and skip everywhere else. A confidential VM cannot
be nested, so these tests run in boot (nspawn) mode on a bare-metal coco host,
as root. The host must have coco enabled in firmware and in the kernel (for SEV-SNP
e.g. `kvm_amd.sev_snp=1`, with `/dev/sev` accessible). On a host without the requested
technology, or when running in `mkosi vm` mode, the tests are skipped.
```
meson test
└─ integration-test-wrapper.py
└─ mkosi boot ──► systemd-nspawn container
└─ TEST-94-COCO.sh
└─ systemd-vmspawn --coco=<type> ──► confidential VM
```
## Running the integration tests without building systemd from source
If you want to run the integration tests against prebuilt systemd packages,
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
integration_tests += [
integration_test_template + {
'name' : fs.name(meson.current_source_dir()),
'coco' : 'any',
# Tests need the image.raw build artifact to boot a full guest.
'vm-images' : true,
},
]
@@ -365,6 +365,35 @@ def process_coverage(args: argparse.Namespace, summary: Summary, name: str, jour
print(f'Wrote coverage report for {name} to {output}', file=sys.stderr)
def coco_host_type() -> Optional[str]:
try:
if Path('/sys/module/kvm_intel/parameters/tdx').read_text().strip() == 'Y':
return 'tdx'
except OSError:
pass
try:
if (
Path('/sys/module/kvm_amd/parameters/sev_snp').read_text().strip() == 'Y'
and Path('/dev/sev').exists()
):
return 'sev-snp'
except OSError:
pass
return None
def coco_nspawn_args(coco_type: str) -> list[str]:
# systemd-nspawn args (appended after '--' in boot mode) that expose the devices vmspawn needs to
# launch a confidential guest from inside the boot-mode container.
binds = ['--bind=/dev/kvm', '--bind=/dev/vhost-vsock']
if coco_type == 'sev-snp':
binds += ['--bind=/dev/sev']
# TDX guests obtain quotes via the host's Quote Generation Service; forward its socket if present.
if coco_type == 'tdx' and Path('/run/tdx-qgs/qgs.socket').exists():
binds += ['--bind=/run/tdx-qgs/qgs.socket']
return binds
def statfs(path: Path) -> str:
return subprocess.run(
['stat', '--file-system', os.fspath(path), '--format=%T'],
@@ -407,6 +436,7 @@ def main() -> None:
parser.add_argument('--rtc', action=argparse.BooleanOptionalAction)
parser.add_argument('--tpm', action=argparse.BooleanOptionalAction)
parser.add_argument('--vm-images', action=argparse.BooleanOptionalAction)
parser.add_argument('--coco', default=None, choices=('tdx', 'sev-snp', 'any'))
parser.add_argument('--skip', action=argparse.BooleanOptionalAction)
parser.add_argument('--suppress-sync', action=argparse.BooleanOptionalAction, default=False)
parser.add_argument('mkosi_args', nargs='*')
@@ -597,6 +627,40 @@ def main() -> None:
file=sys.stderr,
)
# Coco needs to launch a real (L1) confidential guest, which requires the boot-mode nspawn on
# a coco-capable bare-metal host. Nested QEMU VM (vm mode) won't work.
coco_boot_args: list[str] = []
if args.coco:
if vm:
print(
f'{args.name} needs bare-metal boot mode (root, no nesting) for confidential VMs, skipping',
file=sys.stderr,
)
exit(77)
host = coco_host_type()
if host is None or (args.coco != 'any' and host != args.coco):
print(
f'Host does not provide coco={args.coco} (detected: {host}), skipping {args.name}',
file=sys.stderr,
)
exit(77)
if not Path('/dev/vhost-vsock').exists():
print(
f'{args.name} needs /dev/vhost-vsock (load the vhost_vsock module) for the guest '
f'notify channel, skipping',
file=sys.stderr,
)
exit(77)
coco_boot_args = coco_nspawn_args(host)
# /sys/module/*/parameters isn't reliably readable inside the nspawn container, so hand the
# resolved coco type to the test via the environment instead of making it re-detect.
dropin += textwrap.dedent(
f'''
[Service]
Environment=COCO_TYPE={host}
'''
)
cmd = [
args.mkosi,
'--directory', os.fspath(args.mkosi_dir),
@@ -642,7 +706,7 @@ def main() -> None:
*(['--runtime-build-sources=no', '--register=no'] if not sys.stdin.isatty() else []),
'vm' if vm else 'boot',
*(
['--', '--capability=CAP_BPF', f'--suppress-sync={"yes" if args.suppress_sync else "no"}', *vm_images_args]
['--', '--capability=CAP_BPF', f'--suppress-sync={"yes" if args.suppress_sync else "no"}', *vm_images_args, *coco_boot_args]
if not vm
else []
),
+6
View File
@@ -34,6 +34,7 @@ integration_test_template = {
# Bind the mkosi output (built images) read-only into the boot-mode container at /work/vm-images, for
# tests that boot a nested VM from a full image.
'vm-images' : false,
'coco' : '',
'suppress-sync' : true,
}
@@ -109,6 +110,7 @@ foreach dirname : [
'TEST-91-LIVEUPDATE',
'TEST-92-TPM2-SWTPM',
'TEST-93-CLONESETUP',
'TEST-94-COCO',
]
subdir(dirname)
endforeach
@@ -142,6 +144,10 @@ foreach integration_test : integration_tests
integration_test_args += ['--vm-images']
endif
if integration_test['coco'] != ''
integration_test_args += ['--coco', integration_test['coco']]
endif
if not integration_test['enabled']
integration_test_args += ['--skip']
endif
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# Launch a confidential VM with systemd-vmspawn --coco= and verify the guest reports the expected
# confidential-virtualization type. Runs only on coco-capable hardware: the harness --coco=any flag gates
# the run (skipped otherwise), binds /dev/kvm + the coco device(s) into this boot-mode container, and
# passes the resolved technology via $COCO_TYPE.
#
# The confidential guest boots the same mkosi test image. A one-shot unit injected into the guest checks
# systemd-detect-virt --cvm and reports the verdict via systemd's exit-status notification over vsock
# (SuccessAction=exit): the guest PID1 sends EXIT_STATUS to vmspawn's vmm notify socket and vmspawn exits
# with it.
set -eux
set -o pipefail
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
if [[ -v ASAN_OPTIONS ]]; then
echo "vmspawn launches QEMU which doesn't work under ASan, skipping" | tee --append /skipped
exit 77
fi
if ! command -v systemd-vmspawn >/dev/null 2>&1; then
echo "systemd-vmspawn not found, skipping" | tee --append /skipped
exit 77
fi
if ! find_qemu_binary; then
echo "QEMU not found, skipping" | tee --append /skipped
exit 77
fi
# The integration-test-wrapper resolves the host's coco technology on the
# bare-metal host (where /sys/module is readable) and passes it via $COCO_TYPE.
if [[ -z "${COCO_TYPE:-}" ]]; then
echo "COCO_TYPE not set (run via the integration harness with --coco), skipping" | tee --append /skipped
exit 77
fi
echo "Host coco type: $COCO_TYPE"
# The harness binds the guest image read-only at /work/vm-images (the 'vm-images' test option).
IMAGE_DIR=/work/vm-images
if [[ ! -f "$IMAGE_DIR/image.raw" || ! -f "$IMAGE_DIR/image.vmlinuz" || ! -f "$IMAGE_DIR/image.initrd" ]]; then
echo "guest image not found in $IMAGE_DIR, skipping" | tee --append /skipped
exit 77
fi
echo "Guest image directory: $IMAGE_DIR"
MACHINE="coco-guest-$$"
WORKDIR="$(mktemp -d)"
at_exit() {
set +e
machinectl terminate "$MACHINE" 2>/dev/null || :
rm -rf "$WORKDIR"
}
trap at_exit EXIT INT TERM
# One-shot unit for the guest: verify systemd-detect-virt --cvm matches the type vmspawn launched.
# Created via the systemd.extra-unit.* credential and pulled into the boot by a multi-user.target drop-in.
GUEST_PASS=123
GUEST_UNIT="$(cat <<'EOF'
[Unit]
Description=coco guest self-check
After=basic.target
SuccessAction=exit
SuccessActionExitStatus=@GUEST_PASS@
FailureAction=exit
FailureActionExitStatus=1
[Service]
Type=oneshot
ExecStart=bash -c 'test "$(systemd-detect-virt --cvm)" = "@COCO_TYPE@"'
EOF
)"
GUEST_UNIT="${GUEST_UNIT//@COCO_TYPE@/$COCO_TYPE}"
GUEST_UNIT="${GUEST_UNIT//@GUEST_PASS@/$GUEST_PASS}"
GUEST_DROPIN="$(cat <<'EOF'
[Unit]
Wants=coco-guest.service
After=coco-guest.service
EOF
)"
# Launch the confidential guest and capture its console output.
# Direct linux boot should work on all coco platforms (the only boot path supported by SNP).
vmspawn_rc=0
timeout -k 30 300 systemd-vmspawn \
--machine="$MACHINE" \
--coco="$COCO_TYPE" \
--ram=1G \
--ephemeral \
--image="$IMAGE_DIR/image.raw" \
--linux="$IMAGE_DIR/image.vmlinuz" \
--initrd="$IMAGE_DIR/image.initrd" \
--tpm=no \
--console=read-only \
--set-credential="systemd.extra-unit.coco-guest.service:$GUEST_UNIT" \
--set-credential="systemd.unit-dropin.multi-user.target:$GUEST_DROPIN" \
selinux=0 systemd.firstboot=no rw \
2>&1 | tee "$WORKDIR/console.log" || vmspawn_rc="${PIPESTATUS[0]}"
if [[ "$vmspawn_rc" -eq 124 || "$vmspawn_rc" -eq 137 ]]; then
echo "systemd-vmspawn was killed by timeout (exit $vmspawn_rc)" >&2
cat "$WORKDIR/console.log" >&2
exit 1
elif [[ "$vmspawn_rc" -ne "$GUEST_PASS" ]]; then
echo "guest coco self-check did not pass: vmspawn exit $vmspawn_rc (expected $GUEST_PASS)" >&2
cat "$WORKDIR/console.log" >&2
exit 1
fi
echo "Guest correctly reported confidential virtualization: $COCO_TYPE"
touch /testok