From 671206e266bb84e881ccc7eff486a49e325ae152 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Wed, 19 Aug 2026 07:59:18 +0200 Subject: [PATCH 1/4] test: split coco test per-platform Grow TEST-94-COCO from a single inline check into the reusable structure that can be used by multiple future subtests: - lib.sh: code shared between subtests on the host side. - guest-test.sh: the testcase_coco_* checks. - guest-test-runner.sh: runs each testcase_coco_* check, ships guest side testcase results over vsock. Helpers live under test/units/TEST-94-COCO/. The vsock records are best-effort detail, the aggregate exit status stays the authoritative pass/fail. Split the test into subtests per platform/boot flow. Signed-off-by: Paul Meyer --- test/integration-tests/README.md | 9 +- test/units/TEST-94-COCO.sh | 103 ++++------------- test/units/TEST-94-COCO.snp-direct.sh | 32 ++++++ test/units/TEST-94-COCO.tdx-direct.sh | 32 ++++++ test/units/TEST-94-COCO/guest-test-runner.sh | 73 ++++++++++++ test/units/TEST-94-COCO/guest-test.sh | 17 +++ test/units/TEST-94-COCO/lib.sh | 110 +++++++++++++++++++ 7 files changed, 293 insertions(+), 83 deletions(-) create mode 100755 test/units/TEST-94-COCO.snp-direct.sh create mode 100755 test/units/TEST-94-COCO.tdx-direct.sh create mode 100755 test/units/TEST-94-COCO/guest-test-runner.sh create mode 100644 test/units/TEST-94-COCO/guest-test.sh create mode 100644 test/units/TEST-94-COCO/lib.sh diff --git a/test/integration-tests/README.md b/test/integration-tests/README.md index e5f7c76713..c02020ac8e 100644 --- a/test/integration-tests/README.md +++ b/test/integration-tests/README.md @@ -160,12 +160,19 @@ as root. The host must have coco enabled in firmware and in the kernel (for SEV- 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. +Inside each guest, `guest-test-runner.sh` runs the guest-side checks and reports +their results back to the host over a vsock socket, while the guest's overall +pass/fail is relayed separately through its exit status. The host side collects +both to produce the subtest result. + ``` meson test └─ integration-test-wrapper.py └─ mkosi boot ──► systemd-nspawn container └─ TEST-94-COCO.sh - └─ systemd-vmspawn --coco= ──► confidential VM + └─ TEST-94-COCO.*.sh + └─ systemd-vmspawn --coco= ──► confidential VM + └─ guest-test-runner.sh ``` ## Running the integration tests without building systemd from source diff --git a/test/units/TEST-94-COCO.sh b/test/units/TEST-94-COCO.sh index fc9750b635..cc8ab572f1 100755 --- a/test/units/TEST-94-COCO.sh +++ b/test/units/TEST-94-COCO.sh @@ -1,20 +1,23 @@ #!/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. +# Confidential Computing test. # -# 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. +# These only run on coco hardware and if the test environment isn't already virtualized, see +# the harness --coco= flag and the documentation in test/integration-tests/README.md. +# +# Subtests are split per platform and launch path to reduce the number of required boots. +# Test code runs on both sides: +# - host: test vmspawn by running confidential guests. TEST-94-COCO/lib.sh provides helpers. +# - guest: test guest side component coco behavior. Assertions via TEST-94-COCO/guest-test{,-runner}.sh. + set -eux set -o pipefail # shellcheck source=test/units/util.sh . "$(dirname "$0")"/util.sh +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh if [[ -v ASAN_OPTIONS ]]; then echo "vmspawn launches QEMU which doesn't work under ASan, skipping" | tee --append /skipped @@ -31,83 +34,19 @@ if ! find_qemu_binary; then 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. +# The harness binds the guest image artifacts read-only at /work/vm-images. +export IMAGE_DIR=/work/vm-images +if [[ ! -f "$IMAGE_DIR/image.raw" || ! -f "$IMAGE_DIR/image.vmlinuz" || ! -f "$IMAGE_DIR/image.initrd" ]]; then + echo "image artifacts not found in $IMAGE_DIR, skipping" | tee --append /skipped + exit 77 +fi + +# The integration-test-wrapper resolves the host's coco technology (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 +run_subtests_and_exit diff --git a/test/units/TEST-94-COCO.snp-direct.sh b/test/units/TEST-94-COCO.snp-direct.sh new file mode 100755 index 0000000000..32dda28cea --- /dev/null +++ b/test/units/TEST-94-COCO.snp-direct.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# SEV-SNP, direct kernel boot: launch a confidential guest with --linux=/--initrd=. + +set -eux +set -o pipefail + +# shellcheck source=test/units/TEST-94-COCO/lib.sh +. "$(dirname "$0")"/TEST-94-COCO/lib.sh + +if [[ "${COCO_TYPE:?}" != "sev-snp" ]]; then + echo "host coco type is '$COCO_TYPE', not sev-snp, skipping" + exit 77 +fi + +MACHINE="coco-snp-direct-$$" +WORKDIR="$(mktemp -d)" +at_exit() { + set +e + jobs -p | xargs -r kill 2>/dev/null + machinectl terminate "$MACHINE" 2>/dev/null || : + rm -rf "$WORKDIR" +} +trap at_exit EXIT + +vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" "" \ + --image="$IMAGE_DIR/image.raw" \ + --linux="$IMAGE_DIR/image.vmlinuz" \ + --initrd="$IMAGE_DIR/image.initrd" \ + selinux=0 systemd.firstboot=no rw +echo "SEV-SNP direct-boot guest correctly reported confidential virtualization" diff --git a/test/units/TEST-94-COCO.tdx-direct.sh b/test/units/TEST-94-COCO.tdx-direct.sh new file mode 100755 index 0000000000..e378bb3ca1 --- /dev/null +++ b/test/units/TEST-94-COCO.tdx-direct.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# TDX, direct kernel boot: launch a confidential guest with --linux=/--initrd=. + +set -eux +set -o pipefail + +# shellcheck source=test/units/TEST-94-COCO/lib.sh +. "$(dirname "$0")"/TEST-94-COCO/lib.sh + +if [[ "${COCO_TYPE:?}" != "tdx" ]]; then + echo "host coco type is '$COCO_TYPE', not tdx, skipping" + exit 77 +fi + +MACHINE="coco-tdx-direct-$$" +WORKDIR="$(mktemp -d)" +at_exit() { + set +e + jobs -p | xargs -r kill 2>/dev/null + machinectl terminate "$MACHINE" 2>/dev/null || : + rm -rf "$WORKDIR" +} +trap at_exit EXIT + +vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" "" \ + --image="$IMAGE_DIR/image.raw" \ + --linux="$IMAGE_DIR/image.vmlinuz" \ + --initrd="$IMAGE_DIR/image.initrd" \ + selinux=0 systemd.firstboot=no rw +echo "TDX direct-boot guest correctly reported confidential virtualization" diff --git a/test/units/TEST-94-COCO/guest-test-runner.sh b/test/units/TEST-94-COCO/guest-test-runner.sh new file mode 100755 index 0000000000..c0532963b4 --- /dev/null +++ b/test/units/TEST-94-COCO/guest-test-runner.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Runs inside the confidential guest launched by TEST-94-COCO.sh. Runs the requested guest-side checks +# (testcase_coco_* from guest-test.sh), ships a per-check result record to the host over the vsock +# result socket, and lets the injected unit's SuccessAction/FailureAction relay the aggregate verdict +# to the host via vmspawn's exit status. +set -eux +set -o pipefail + +_COCO_DIR="$(dirname "$(readlink -f "$0")")" +# util.sh (assert_*) lives one level up in the shared units dir. +# shellcheck source=test/units/util.sh +. "$(dirname "$_COCO_DIR")/util.sh" +# shellcheck source=test/units/TEST-94-COCO/guest-test.sh +. "$_COCO_DIR/guest-test.sh" + +# Ship the accumulated result records to the host's vsock listener (CID 2 = host). Best-effort: the +# aggregate verdict travels independently via the unit's exit status, so a vsock hiccup never masks a +# pass/fail — it only drops the per-check breakdown. +_coco_ship_results() { + local records="${1:?}" + [[ -n "${COCO_RESULT_PORT:-}" ]] || return 0 + socat -u "OPEN:$records,rdonly" "VSOCK-CONNECT:2:$COCO_RESULT_PORT" || + echo >&2 "WARNING: failed to ship coco results over vsock port $COCO_RESULT_PORT" +} + +# Run the guest-side checks named by $COCO_TESTCASES (testcase_coco_* names without the prefix), +# record pass/fail per check, and ship the records. Each named check is something its boot scenario +# exists to prove, so an unknown name or an empty request fails the aggregate rather than letting the +# boot pass without having verified anything. +_coco_run_tests() { + local testcases testcase rc result failed=0 records + records="$(mktemp)" + + read -ra testcases <<<"${COCO_TESTCASES:-}" + if [[ "${#testcases[@]}" -eq 0 ]]; then + echo >&2 "No coco testcases requested via \$COCO_TESTCASES" + return 1 + fi + testcases=("${testcases[@]/#/testcase_coco_}") + + for testcase in "${testcases[@]}"; do + if ! declare -F "$testcase" >/dev/null; then + echo >&2 "Requested coco testcase '$testcase' is not defined" + printf 'ID=%s RESULT=missing\n' "$testcase" >>"$records" + failed=1 + continue + fi + # Subshell so a testcase's RETURN trap can't fire twice (see run_testcases in test-control.sh). + # Bash disables 'set -e' throughout a function called in a context that tests its exit status, + # so '("$testcase") || rc=$?' would let a testcase run past its first failed assertion and + # report only its last one. Reading $? from a standalone call avoids that context; the inner + # 'set -e' re-arms what the surrounding 'set +e' turned off. + set +e + (set -e; "$testcase") + rc=$? + set -e + if [[ "$rc" -eq 0 ]]; then + result=pass + else + result=fail + failed=1 + fi + echo "coco check $testcase: $result (rc=$rc)" + printf 'ID=%s RESULT=%s\n' "$testcase" "$result" >>"$records" + done + + _coco_ship_results "$records" + return "$failed" +} + +_coco_run_tests diff --git a/test/units/TEST-94-COCO/guest-test.sh b/test/units/TEST-94-COCO/guest-test.sh new file mode 100644 index 0000000000..3c8826422d --- /dev/null +++ b/test/units/TEST-94-COCO/guest-test.sh @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck shell=bash +# +# Guest-side checks for the confidential computing integration test (TEST-94-COCO). Sourced by +# guest-test-runner.sh inside the confidential guest; each testcase_coco_* function is one +# sub-subtest. The upcoming credential-trust checks land here as further functions. + +if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then + echo >&2 "This file should not be executed directly" + exit 1 +fi + +# Assert the guest observes the confidential-virtualization technology the host launched it with. +# COCO_TYPE is provided by the injected guest unit (Environment=, see lib.sh). +testcase_coco_detect_virt() { + assert_eq "$(systemd-detect-virt --cvm)" "${COCO_TYPE:?}" +} diff --git a/test/units/TEST-94-COCO/lib.sh b/test/units/TEST-94-COCO/lib.sh new file mode 100644 index 0000000000..e4e8166228 --- /dev/null +++ b/test/units/TEST-94-COCO/lib.sh @@ -0,0 +1,110 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck shell=bash +# +# Host-side helpers for the confidential computing integration test (TEST-94-COCO). +# Sourced by the TEST-94-COCO.sh driver. + +if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then + echo >&2 "This file should not be executed directly" + exit 1 +fi + +# Exit status the guest test runner reports on success (its unit's SuccessActionExitStatus=). vmspawn +# forwards the guest PID1's EXIT_STATUS over the vsock notify socket and exits with it. +_COCO_GUEST_PASS=123 +# vsock port the host listens on for the guest's per-check result records, distinct from vmspawn's own +# (kernel-assigned) notify port. The host binds CID_ANY:PORT; the guest connects to CID 2 (host):PORT. +_COCO_RESULT_PORT=4321 + +# _coco_guest_unit COCO_TYPE UNITS_DIR TESTCASES +# Emit the one-shot unit that runs the guest test runner: passes the coco type, result port and +# the requested checks via the environment, and relays the aggregate verdict via +# SuccessAction/EXIT_STATUS. +_coco_guest_unit() { + local coco_type="${1:?}" units_dir="${2:?}" testcases="${3:?}" + cat <"$results" + + guest_unit="$(_coco_guest_unit "$coco_type" "$units_dir" "$testcases")" + guest_dropin="$(_coco_guest_dropin)" + + # Collect the per-check records the guest ships over vsock (single connection, guest closes when + # done). Best-effort: the aggregate exit status below is the authoritative pass/fail, so a vsock + # hiccup only costs the per-check breakdown, never the verdict. + socat -u "VSOCK-LISTEN:$_COCO_RESULT_PORT" "OPEN:$results,creat" & + listener_pid=$! + + timeout -k 30 300 systemd-vmspawn \ + --machine="$machine" \ + --coco="$coco_type" \ + --ram=1G \ + --ephemeral \ + --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" \ + "$@" \ + 2>&1 | tee "$console" || rc="${PIPESTATUS[0]}" + + kill "$listener_pid" 2>/dev/null || : + wait "$listener_pid" 2>/dev/null || : + + if [[ -s "$results" ]]; then + echo "coco guest per-check results:" + cat "$results" + fi + + if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then + echo "systemd-vmspawn was killed by timeout (exit $rc)" >&2 + cat "$console" >&2 + return 1 + fi + if [[ "$rc" -ne "$_COCO_GUEST_PASS" ]]; then + echo "coco guest test runner failed: vmspawn exit $rc (expected $_COCO_GUEST_PASS)" >&2 + cat "$console" >&2 + return 1 + fi + return 0 +} From 28a1975396076dda45e614d14da1170b23f497c1 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Fri, 11 Sep 2026 11:48:34 +0200 Subject: [PATCH 2/4] test: coco credential transport on snp Check that credentials passed via systemd-vmspawn --set-credential and on the command line via systemd.set_credential= are successfully delivered on a SNP coco guest. Signed-off-by: Paul Meyer --- test/units/TEST-94-COCO.snp-direct.sh | 13 ++++++++++--- test/units/TEST-94-COCO.tdx-direct.sh | 2 +- test/units/TEST-94-COCO/fixtures.sh | 22 ++++++++++++++++++++++ test/units/TEST-94-COCO/guest-test.sh | 21 ++++++++++++++++++++- test/units/TEST-94-COCO/lib.sh | 9 ++++++--- 5 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 test/units/TEST-94-COCO/fixtures.sh diff --git a/test/units/TEST-94-COCO.snp-direct.sh b/test/units/TEST-94-COCO.snp-direct.sh index 32dda28cea..3bb9bda902 100755 --- a/test/units/TEST-94-COCO.snp-direct.sh +++ b/test/units/TEST-94-COCO.snp-direct.sh @@ -24,9 +24,16 @@ at_exit() { } trap at_exit EXIT -vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" "" \ +# General test case for SNP direct boot, covers: +# +# - detect_virt: systemd-detect-virt --cvm detects SNP as the CVM type +# - creds_vmspawn: credential transport by vmspawn is working (via initrd for SNP) +# - creds_cmdline: credential transport via cmdline is working (cmdline is measured on SNP) +vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" 'detect_virt creds_vmspawn creds_cmdline' \ --image="$IMAGE_DIR/image.raw" \ --linux="$IMAGE_DIR/image.vmlinuz" \ --initrd="$IMAGE_DIR/image.initrd" \ - selinux=0 systemd.firstboot=no rw -echo "SEV-SNP direct-boot guest correctly reported confidential virtualization" + --set-credential="$COCO_CRED_TRUSTED_ID:$COCO_CRED_TRUSTED_VALUE" \ + selinux=0 systemd.firstboot=no rw \ + "systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE" +echo "SEV-SNP direct-boot guest correctly reported confidential virtualization and trusted credentials" diff --git a/test/units/TEST-94-COCO.tdx-direct.sh b/test/units/TEST-94-COCO.tdx-direct.sh index e378bb3ca1..3999ea96d0 100755 --- a/test/units/TEST-94-COCO.tdx-direct.sh +++ b/test/units/TEST-94-COCO.tdx-direct.sh @@ -24,7 +24,7 @@ at_exit() { } trap at_exit EXIT -vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" "" \ +vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" 'detect_virt' \ --image="$IMAGE_DIR/image.raw" \ --linux="$IMAGE_DIR/image.vmlinuz" \ --initrd="$IMAGE_DIR/image.initrd" \ diff --git a/test/units/TEST-94-COCO/fixtures.sh b/test/units/TEST-94-COCO/fixtures.sh new file mode 100644 index 0000000000..c690537b18 --- /dev/null +++ b/test/units/TEST-94-COCO/fixtures.sh @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck shell=bash +# Every constant here is consumed by whichever side sources this file, never by this file itself. +# shellcheck disable=SC2034 +# +# Fixtures for the confidential computing integration test (TEST-94-COCO). Shared by the host-side +# boot scenarios, which deliver these credentials over trusted and untrusted channels alike, and by +# the guest-side checks, which assert where each one ended up, so the two sides cannot drift apart. + +if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then + echo >&2 "This file should not be executed directly" + exit 1 +fi + +# Name of the guest self-check unit lib.sh injects. +COCO_GUEST_UNIT="coco-guest.service" + +# Credentials by channel +COCO_CRED_TRUSTED_ID="coco.trusted" +COCO_CRED_TRUSTED_VALUE="trusted-via-vmspawn" +COCO_CRED_CMDLINE_ID="coco.cmdline" +COCO_CRED_CMDLINE_VALUE="trusted-via-cmdline" diff --git a/test/units/TEST-94-COCO/guest-test.sh b/test/units/TEST-94-COCO/guest-test.sh index 3c8826422d..b7778c07db 100644 --- a/test/units/TEST-94-COCO/guest-test.sh +++ b/test/units/TEST-94-COCO/guest-test.sh @@ -3,15 +3,34 @@ # # Guest-side checks for the confidential computing integration test (TEST-94-COCO). Sourced by # guest-test-runner.sh inside the confidential guest; each testcase_coco_* function is one -# sub-subtest. The upcoming credential-trust checks land here as further functions. +# sub-subtest, and a boot scenario names the checks it requires via $COCO_TESTCASES. if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then echo >&2 "This file should not be executed directly" exit 1 fi +# shellcheck source=test/units/TEST-94-COCO/fixtures.sh +. "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/fixtures.sh" + # Assert the guest observes the confidential-virtualization technology the host launched it with. # COCO_TYPE is provided by the injected guest unit (Environment=, see lib.sh). testcase_coco_detect_virt() { assert_eq "$(systemd-detect-virt --cvm)" "${COCO_TYPE:?}" } + +# Assert a credential handed to systemd-vmspawn --set-credential lands in the trusted bucket. +# +# SNP: creds should be delivered via the initrd cpio channel. +testcase_coco_creds_vmspawn() { + assert_eq "$(systemd-creds --system cat "$COCO_CRED_TRUSTED_ID")" "$COCO_CRED_TRUSTED_VALUE" + assert_not_in "$COCO_CRED_TRUSTED_ID" "$(&1 | tee "$console" || rc="${PIPESTATUS[0]}" From d6f96ac3112ea9809108d8599abe3fd7b0eb53c6 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Fri, 11 Sep 2026 15:02:35 +0200 Subject: [PATCH 3/4] test: coco credential rejection on snp Not all channels used for credential transport in systemd are trusted on SNP: while cmdline and initrd are measured and should be used, smbios and fwcfg are untrusted and the guest should ignore creds from those delivery paths. Implement an offensive credential test that tries to inject credentials via those untrusted channels and check the guest won't consume those credentials. Signed-off-by: Paul Meyer --- test/units/TEST-94-COCO.snp-direct.sh | 27 ++++++++++-- test/units/TEST-94-COCO/fixtures.sh | 4 ++ test/units/TEST-94-COCO/guest-test.sh | 60 +++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/test/units/TEST-94-COCO.snp-direct.sh b/test/units/TEST-94-COCO.snp-direct.sh index 3bb9bda902..7933a8775f 100755 --- a/test/units/TEST-94-COCO.snp-direct.sh +++ b/test/units/TEST-94-COCO.snp-direct.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # SPDX-License-Identifier: LGPL-2.1-or-later # -# SEV-SNP, direct kernel boot: launch a confidential guest with --linux=/--initrd=. +# SEV-SNP, direct kernel boot: launch confidential guests with --linux=/--initrd=. set -eux set -o pipefail @@ -16,10 +16,12 @@ fi MACHINE="coco-snp-direct-$$" WORKDIR="$(mktemp -d)" +mkdir -p "$WORKDIR/honest" "$WORKDIR/hostile" at_exit() { set +e jobs -p | xargs -r kill 2>/dev/null - machinectl terminate "$MACHINE" 2>/dev/null || : + machinectl terminate "$MACHINE-honest" 2>/dev/null || : + machinectl terminate "$MACHINE-hostile" 2>/dev/null || : rm -rf "$WORKDIR" } trap at_exit EXIT @@ -29,7 +31,7 @@ trap at_exit EXIT # - detect_virt: systemd-detect-virt --cvm detects SNP as the CVM type # - creds_vmspawn: credential transport by vmspawn is working (via initrd for SNP) # - creds_cmdline: credential transport via cmdline is working (cmdline is measured on SNP) -vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" 'detect_virt creds_vmspawn creds_cmdline' \ +vmspawn_boot_coco "$MACHINE-honest" "$COCO_TYPE" "$WORKDIR/honest" 'detect_virt creds_vmspawn creds_cmdline' \ --image="$IMAGE_DIR/image.raw" \ --linux="$IMAGE_DIR/image.vmlinuz" \ --initrd="$IMAGE_DIR/image.initrd" \ @@ -37,3 +39,22 @@ vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" 'detect_virt creds_vmspawn selinux=0 systemd.firstboot=no rw \ "systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE" echo "SEV-SNP direct-boot guest correctly reported confidential virtualization and trusted credentials" + +# Offensive test: +# +# - creds_hostile_*: ensure the guest isn't tricked into consuming untrusted/unmeasured credentials. +QEMU_EXTRA=( + -smbios "type=11,value=io.systemd.credential:$COCO_CRED_SMBIOS_ID=$COCO_CRED_SMBIOS_VALUE" + -smbios "type=11,value=io.systemd.credential:$COCO_CRED_TRUSTED_ID=$COCO_CRED_SMBIOS_VALUE" + -fw_cfg "name=opt/io.systemd.credentials/$COCO_CRED_FWCFG_ID,string=$COCO_CRED_FWCFG_VALUE" +) + +SYSTEMD_VMSPAWN_QEMU_EXTRA="${QEMU_EXTRA[*]}" \ +vmspawn_boot_coco "$MACHINE-hostile" "$COCO_TYPE" "$WORKDIR/hostile" \ + 'detect_virt creds_vmspawn creds_hostile_fwcfg creds_hostile_snp_smbios creds_hostile_shadow' \ + --image="$IMAGE_DIR/image.raw" \ + --linux="$IMAGE_DIR/image.vmlinuz" \ + --initrd="$IMAGE_DIR/image.initrd" \ + --set-credential="$COCO_CRED_TRUSTED_ID:$COCO_CRED_TRUSTED_VALUE" \ + selinux=0 systemd.firstboot=no rw +echo "SEV-SNP direct-boot guest correctly ignored the host-injected credentials" diff --git a/test/units/TEST-94-COCO/fixtures.sh b/test/units/TEST-94-COCO/fixtures.sh index c690537b18..5676aabd14 100644 --- a/test/units/TEST-94-COCO/fixtures.sh +++ b/test/units/TEST-94-COCO/fixtures.sh @@ -20,3 +20,7 @@ COCO_CRED_TRUSTED_ID="coco.trusted" COCO_CRED_TRUSTED_VALUE="trusted-via-vmspawn" COCO_CRED_CMDLINE_ID="coco.cmdline" COCO_CRED_CMDLINE_VALUE="trusted-via-cmdline" +COCO_CRED_SMBIOS_ID="coco.hostile.smbios" +COCO_CRED_SMBIOS_VALUE="injected-via-smbios" +COCO_CRED_FWCFG_ID="coco.hostile.fwcfg" +COCO_CRED_FWCFG_VALUE="injected-via-fwcfg" diff --git a/test/units/TEST-94-COCO/guest-test.sh b/test/units/TEST-94-COCO/guest-test.sh index b7778c07db..c2f89258d8 100644 --- a/test/units/TEST-94-COCO/guest-test.sh +++ b/test/units/TEST-94-COCO/guest-test.sh @@ -13,6 +13,40 @@ fi # shellcheck source=test/units/TEST-94-COCO/fixtures.sh . "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/fixtures.sh" +# _coco_assert_cred_absent ID +# Assert no credential named ID reached the guest at all, neither trusted nor encrypted. +_coco_assert_cred_absent() { + local id="${1:?}" + local system_creds_dir=/run/credentials/@system + local encrypted_creds_dir=/run/credentials/@encrypted + + assert_fail test -e "$system_creds_dir/$id" + assert_fail test -e "$encrypted_creds_dir/$id" +} + +# _coco_assert_smbios_injected ID +# Assert the guest received an SMBIOS type #11 OEM string carrying credential ID. The scenario +# injected it, so a miss means the injection was lost on the way in and concluding anything from the +# credential's absence afterwards would be meaningless. +_coco_assert_smbios_injected() { + local id="${1:?}" raw + + [[ -d /sys/firmware/dmi/entries ]] || modprobe dmi-sysfs || : + + for raw in /sys/firmware/dmi/entries/11-*/raw; do + [[ -e "$raw" ]] || continue + # The entry is a blob of NUL-separated strings: --text to search it anyway, --fixed-strings + # because credential names contain dots. Redirect rather than -q, which would exit early and + # risk SIGPIPE on the reader. + if grep --text --fixed-strings "io.systemd.credential:$id=" "$raw" >/dev/null; then + return 0 + fi + done + + echo "FAIL: no SMBIOS OEM string for credential '$id' in the guest's DMI type #11 tables" >&2 + return 1 +} + # Assert the guest observes the confidential-virtualization technology the host launched it with. # COCO_TYPE is provided by the injected guest unit (Environment=, see lib.sh). testcase_coco_detect_virt() { @@ -34,3 +68,29 @@ testcase_coco_creds_cmdline() { assert_in "systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE" "$( Date: Tue, 22 Sep 2026 10:29:11 +0200 Subject: [PATCH 4/4] test: strip directory from subtest name in summary The integration tests are started with the absolute path of the test unit as $0, so run_subtests() derives absolute paths for the subtests and the summary prints them in full. Show just the script name, the directory is the same for every entry and only adds noise. Signed-off-by: Paul Meyer --- test/units/test-control.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/units/test-control.sh b/test/units/test-control.sh index 1e352c44b3..a1f9de0f87 100644 --- a/test/units/test-control.sh +++ b/test/units/test-control.sh @@ -84,6 +84,8 @@ _show_summary() {( _record_subtest_rc() { local subtest="${1:?}" rc="${2:?}" + subtest="${subtest##*/}" + if [[ $rc -eq $_SUBTEST_SKIP_RC ]]; then echo "Subtest $subtest skipped" _SKIPPED_TESTS+=("$subtest")