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:
@@ -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 []
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+113
@@ -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
|
||||
Reference in New Issue
Block a user