Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ jobs:
TEST_NAME: ${{ inputs.test-name }}
CONTRAST_GHCR_READ: ${{ secrets.CONTRAST_GHCR_READ }}
DEBUG_SHELL: ${{ inputs.debug-shell }}
SET: base
SET: >-
${{
(inputs.test-name == 'badaml-sandbox' && 'badaml-sandbox') ||
(inputs.test-name == 'badaml-vuln' && 'badaml-vuln') ||
'base'
}}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/e2e_badaml.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: e2e test badaml

on:
pull_request:
paths:
- sets/badaml-vuln
- sets/badaml-sandbox
- packages/kata/kernel-uvm/**
- packages/qemu-badaml/**
- packages/badaml-payload/**
- e2e/badaml-vuln/**
- e2e/badaml-sandbox/**

jobs:
tests:
strategy:
matrix:
platform:
- name: Metal-QEMU-SNP
runner: SNP
self-hosted: true
- name: Metal-QEMU-TDX
runner: TDX
self-hosted: true
- name: Metal-QEMU-SNP-GPU
runner: SNP-GPU
self-hosted: true
- name: Metal-QEMU-TDX-GPU
runner: TDX-GPU
self-hosted: true
test-name:
- badaml-vuln
- badaml-sandbox
fail-fast: false
name: "${{ matrix.platform.name }}"
uses: ./.github/workflows/e2e.yml
with:
skip-undeploy: false
test-name: ${{ matrix.test-name }}
platform: ${{ matrix.platform.name }}
runner: ${{ matrix.platform.runner }}
self-hosted: ${{ matrix.platform.self-hosted }}
debug-shell: true
secrets:
GITHUB_TOKEN_IN: ${{ secrets.GITHUB_TOKEN }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
NUNKI_CI_COMMIT_PUSH_PR: ${{ secrets.NUNKI_CI_COMMIT_PUSH_PR }}
TEAMS_CI_WEBHOOK: ${{ secrets.TEAMS_CI_WEBHOOK }}
CONTRAST_GHCR_READ: ${{ secrets.CONTRAST_GHCR_READ }}
permissions:
contents: read
packages: write
2 changes: 2 additions & 0 deletions .github/workflows/e2e_manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ on:
# keep-sorted start
- atls
- attestation
- badaml-sandbox
- badaml-vuln
- containerd-11644-reproducer
- genpolicy-unsupported
- gpu
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/e2e_nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
# keep-sorted start
- atls
- attestation
- badaml-sandbox
- badaml-vuln
- containerd-11644-reproducer
- genpolicy-unsupported
- gpu
Expand All @@ -72,14 +74,14 @@ jobs:
exclude:
- platform:
name: Metal-QEMU-SNP
runner: SNP
self-hosted: true
test-name: gpu
- platform:
name: Metal-QEMU-TDX
runner: TDX
self-hosted: true
test-name: gpu
- debug-shell: false
test-name: badaml-vuln
- debug-shell: false
test-name: badaml-sandbox
fail-fast: false
name: "${{ matrix.platform.name }}"
uses: ./.github/workflows/e2e.yml
Expand Down
25 changes: 25 additions & 0 deletions e2e/badaml-sandbox/badaml_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2026 Edgeless Systems GmbH
// SPDX-License-Identifier: BUSL-1.1

//go:build e2e

package badamlsandbox

import (
"flag"
"os"
"testing"

badamlvuln "github.com/edgelesssys/contrast/e2e/badaml-vuln"
"github.com/edgelesssys/contrast/e2e/internal/contrasttest"
)

func TestBadAMLVulnerability(t *testing.T) {
badamlvuln.BadAMLTest(t, false)
}

func TestMain(m *testing.M) {
contrasttest.RegisterFlags()
flag.Parse()
os.Exit(m.Run())
}
82 changes: 82 additions & 0 deletions e2e/badaml-vuln/badaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2026 Edgeless Systems GmbH
// SPDX-License-Identifier: BUSL-1.1

//go:build e2e

package badamlvuln

import (
"context"
"testing"
"time"

"github.com/edgelesssys/contrast/e2e/internal/contrasttest"
"github.com/edgelesssys/contrast/internal/kuberesource"
"github.com/edgelesssys/contrast/internal/manifest"
"github.com/edgelesssys/contrast/internal/platforms"
"github.com/stretchr/testify/require"
)

// BadAMLTest is the shared logic between the badaml-vuln and badaml-sandbox tests.
func BadAMLTest(t *testing.T, expectSuccessfulAttack bool) {
platform, err := platforms.FromString(contrasttest.Flags.PlatformStr)
require.NoError(t, err)

ct := contrasttest.New(t)

require.True(t, contrasttest.Flags.InsecureEnableDebugShell, "the --insecure-enable-debug-shell-access flag must be set to true to extract the initrd start address")

runtimeHandler, err := manifest.RuntimeHandler(platform)
require.NoError(t, err)
resources := kuberesource.CoordinatorBundle()
resources = kuberesource.PatchRuntimeHandlers(resources, runtimeHandler)
resources = kuberesource.AddPortForwarders(resources)
ct.Init(t, resources)

require.True(t, t.Run("generate", ct.Generate), "contrast generate needs to succeed for subsequent tests")
require.True(t, t.Run("apply", ct.Apply), "Kubernetes resources need to be applied for subsequent tests")
if platform == platforms.MetalQEMUTDX {
// TODO(katexochen): Set on TDX currently fails, as we are still measuring the ACPI tables, so
// the injected BadAML table is detected during remote attestation. For TDX-GPU, we don't measure
// the table so the attack works.
require.True(t, t.Run("wait for debugshell", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), ct.FactorPlatformTimeout(2*time.Minute))
defer cancel()
require.NoError(t, ct.Kubeclient.WaitForContainer(ctx, ct.Namespace, "coordinator-0", "contrast-debug-shell"))
}), "debugshell start must succeed for subsequent tests")
} else {
require.True(t, t.Run("set", ct.Set), "contrast set needs to succeed for subsequent tests")
}

var content string
require.True(t, t.Run("get content of /run/deadbeef.bin", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), ct.FactorPlatformTimeout(1*time.Minute))
defer cancel()
cmd := []string{"debugshell", `hexdump -e '1/1 "%02x"' -n4 /run/deadbeef.bin`}
stdout, stderr, err := ct.Kubeclient.ExecContainer(ctx, ct.Namespace, "coordinator-0", "contrast-debug-shell", cmd)
require.NoError(t, err, "running %q:\nstdout:\n%s\nstderr:\n%s", cmd, stdout, stderr)
content = stdout
}), "getting content of /run/deadbeef.bin needs to succeed for subsequent tests")

require.True(t, t.Run("get dmesg logs", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), ct.FactorPlatformTimeout(1*time.Minute))
defer cancel()
cmd := []string{"debugshell", "dmesg | grep -i acpi"}
stdout, stderr, err := ct.Kubeclient.ExecContainer(ctx, ct.Namespace, "coordinator-0", "contrast-debug-shell", cmd)
require.NoError(t, err, "running %q:\nstdout:\n%s\nstderr:\n%s", cmd, stdout, stderr)
t.Log(stdout)
}))

name := "check attack is"
if !expectSuccessfulAttack {
name += " not"
}
name += " successful"
require.True(t, t.Run(name, func(t *testing.T) {
if expectSuccessfulAttack {
require.Equal(t, "cafebabe", content, "the content of /run/deadbeef.bin should be 'cafebabe' if the attack was successful")
} else {
require.Equal(t, "deadbeef", content, "the content of /run/deadbeef.bin should be 'deadbeef' if the attack was not successful")
}
}))
}
24 changes: 24 additions & 0 deletions e2e/badaml-vuln/badaml_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2026 Edgeless Systems GmbH
// SPDX-License-Identifier: BUSL-1.1

//go:build e2e

package badamlvuln

import (
"flag"
"os"
"testing"

"github.com/edgelesssys/contrast/e2e/internal/contrasttest"
)

func TestBadAMLVulnerability(t *testing.T) {
BadAMLTest(t, true)
}

func TestMain(m *testing.M) {
contrasttest.RegisterFlags()
flag.Parse()
os.Exit(m.Run())
}
43 changes: 43 additions & 0 deletions e2e/internal/kubeclient/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ func (c *Kubeclient) WaitForPod(ctx context.Context, namespace, name string) err
return c.WaitForPodCondition(ctx, namespace, &singlePodReady{name: name})
}

// WaitForContainer waits until a specific container in the named pod is ready.
func (c *Kubeclient) WaitForContainer(ctx context.Context, namespace, podName, containerName string) error {
return c.WaitForPodCondition(ctx, namespace, &containerReady{podName: podName, containerName: containerName})
}

// WaitForJob waits until the Job succeeded.
//
// We consider the Job succeeded if the Pod belonging to the Job succeeded.
Expand Down Expand Up @@ -284,6 +289,44 @@ func (f *singlePodReady) String() string {
return fmt.Sprintf("PodCondition(pod %s is ready)", f.name)
}

// containerReady checks that a named container in a named pod is ready.
type containerReady struct {
podName string
containerName string
}

func (cr *containerReady) Check(lister listerscorev1.PodLister) (bool, error) {
pods, err := lister.List(labels.Everything())
if err != nil {
return false, err
}
for _, pod := range pods {
if pod.Name != cr.podName {
continue
}
if pod.DeletionTimestamp != nil {
return false, nil
}
for _, statuses := range [][]corev1.ContainerStatus{
pod.Status.InitContainerStatuses,
pod.Status.ContainerStatuses,
pod.Status.EphemeralContainerStatuses,
} {
for _, cs := range statuses {
if cs.Name == cr.containerName {
return cs.Ready, nil
}
}
}
return false, nil
}
return false, nil
}

func (cr *containerReady) String() string {
return fmt.Sprintf("PodCondition(container %s in pod %s is ready)", cr.containerName, cr.podName)
}

type oneRunning struct {
ls labels.Selector
}
Expand Down
66 changes: 66 additions & 0 deletions overlays/sets/badaml-sandbox.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright 2026 Edgeless Systems GmbH
# SPDX-License-Identifier: BUSL-1.1

# TODO(katexochen): This is currently a near-copy of badaml-vuln.nix, without the disabling of the AML sandbox in the image.
# Refactor it to reuse the code between both sets.

_final: prev: {
contrastPkgs = prev.contrastPkgs.overrideScope (
contrastPkgsFinal: contrastPkgsPrev: {
kata = contrastPkgsPrev.kata.overrideScope (
_kataFinal: kataPrev: {
kernel-uvm = kataPrev.kernel-uvm.override {
# Enable ACPI debug logging to make it easier to verify that the attack is working,
# or debug it if it isn't.
withACPIDebug = true;
};
image = kataPrev.image.override {
withBadAMLTarget = true;
};
}
);
contrast = contrastPkgsPrev.contrast.overrideScope (
_contrastFinal: contrastPrev: {
node-installer-image = contrastPrev.node-installer-image.override {
withExtraLayers = [
(contrastPkgsFinal.ociLayerTar {
files = [
{
# The wrapper script that replaces the original qemu binary.
source = "${contrastPkgsFinal.contrastPkgsStatic.qemu-badaml}/bin/qemu-system-x86_64";
destination = "/opt/edgeless/bin/qemu-system-x86_64";
}
{
# The actual qemu binary that is wrapped by qemu-badaml.
source = "${contrastPkgsFinal.contrastPkgsStatic.qemu-badaml}/bin/qemu-system-x86_64-wrapped";
destination = "/opt/edgeless/bin/qemu-system-x86_64-wrapped";
}
{
# The AML payload injected by the wrapper script.
source = "${contrastPkgsFinal.badaml-payload}/deadbeef-file.aml"; # Modify payload here.
destination = "/opt/edgeless/bin/payload.aml";
}
];
})
];
withExtraInstallFilesConfig = [
{
# qemu-badaml is a wrapper script that invokes the real qemu-cc binary.
# The original install entry for qemu-cc will only install the wrapper scripts,
# so we need to add an additional step to install the actual, wrapped qemu binary.
url = "file:///opt/edgeless/bin/qemu-system-x86_64-wrapped";
path = "/opt/edgeless/@@runtimeName@@/bin/qemu-system-x86_64-wrapped";
executable = true;
}
{
# The AML payload injected by the wrapper script.
url = "file:///opt/edgeless/bin/payload.aml";
path = "/opt/edgeless/@@runtimeName@@/bin/payload.aml";
}
];
};
}
);
}
);
}
Loading
Loading