Skip to content

fix(cloud-hypervisor): limit device ACL lifetime #326

fix(cloud-hypervisor): limit device ACL lifetime

fix(cloud-hypervisor): limit device ACL lifetime #326

name: Cloud Hypervisor Preview Integration
on:
workflow_dispatch:
inputs:
run_live_kvm:
description: Run the live KVM job on GitHub-hosted x64 Ubuntu 24.04
required: true
type: boolean
default: true
pull_request:
types: [opened, synchronize, reopened, labeled]
paths:
- '.github/workflows/test-cloud-hypervisor.yml'
- 'guest/cloud-hypervisor/**'
- 'containers/build-tools/**'
- 'guest/microvm-supervisor/**'
- 'src/cloud-hypervisor/**'
- 'src/cloud-hypervisor-runtime-backend.ts'
- 'src/cloud-hypervisor-runtime-backend.test.ts'
- 'src/microvm/**'
- 'src/filesystem-policy.ts'
- 'src/types/runtime-options.ts'
- 'scripts/ci/cloud-hypervisor-*.sh'
- 'docs/cloud-hypervisor-foundation.md'
permissions:
contents: read
id-token: write
attestations: write
concurrency:
group: cloud-hypervisor-preview-${{ github.ref }}
cancel-in-progress: false
jobs:
build-test-artifacts:
name: Build deterministic test guest
if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request'
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.25.0'
cache-dependency-path: guest/microvm-supervisor/go.mod
- name: Run guest supervisor unit tests
working-directory: guest/microvm-supervisor
# Running unit tests here (not just building it) catches
# defects like an incorrect syscall.Mount() fstype before they
# only surface as a guest kernel panic during the live-KVM job
# below, which is much slower to diagnose.
run: go test ./...
- name: Install deterministic guest build prerequisites
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
bc \
binutils \
bison \
build-essential \
ca-certificates \
cpio \
e2fsprogs \
file \
flex \
libelf-dev \
libssl-dev \
rsync \
virtiofsd \
xz-utils
- name: Build and verify pinned artifacts
run: |
docker build \
--platform linux/amd64 \
--tag awf-cloud-hypervisor-build-tools:test \
containers/build-tools
BUILD_TOOLS_IMAGE=awf-cloud-hypervisor-build-tools:test \
./guest/cloud-hypervisor/build-test-artifacts.sh
./guest/cloud-hypervisor/verify-test-artifacts.sh \
release/cloud-hypervisor-test-x86_64
- name: Attest guest artifact provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: release/cloud-hypervisor-test-x86_64/awf-cloud-hypervisor-test-x86_64.tar.gz
- name: Upload guest artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: cloud-hypervisor-test-x86_64
path: release/cloud-hypervisor-test-x86_64/
if-no-files-found: error
retention-days: 7
live-kvm:
name: Live Cloud Hypervisor KVM smoke/security
needs: build-test-artifacts
if: >-
(github.event_name == 'workflow_dispatch' && inputs.run_live_kvm) ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'cloud-hypervisor-kvm'))
runs-on: ubuntu-24.04
# 16 cases, each with up to a 90s boot budget and (since the guest
# connectivity probe raised its own timeout for the same nested-KVM
# scheduling reasons) up to a further 90s probe budget in the worst
# case; 60 minutes leaves headroom without masking a genuine hang.
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '22'
cache: npm
- name: Download verified guest artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: cloud-hypervisor-test-x86_64
path: ${{ runner.temp }}/cloud-hypervisor-test-x86_64
- name: Restore artifact executable permissions
run: |
# actions/upload-artifact + actions/download-artifact do not
# reliably preserve the executable bit on binary files (a known
# GitHub Actions artifact limitation). Digest verification below
# still proves file integrity; this only restores the mode bits
# needed to exec the pinned, already-verified binaries.
chmod 0755 \
"$RUNNER_TEMP/cloud-hypervisor-test-x86_64/cloud-hypervisor" \
"$RUNNER_TEMP/cloud-hypervisor-test-x86_64/virtiofsd" \
"$RUNNER_TEMP/cloud-hypervisor-test-x86_64/awf-supervisor"
- name: Grant workflow user access to KVM
run: |
if [ -e /dev/kvm ]; then
sudo chmod 666 /dev/kvm
fi
- name: Verify capable host and artifact digests
run: |
./scripts/ci/cloud-hypervisor-host-preflight.sh \
"$RUNNER_TEMP/cloud-hypervisor-test-x86_64"
- name: Install, build, and prepare infrastructure images
run: |
npm ci
npm run build
docker build -t ghcr.io/github/gh-aw-firewall/squid:latest containers/squid
docker build -t ghcr.io/github/gh-aw-firewall/api-proxy:latest containers/api-proxy
- name: Run live fail-closed smoke/security coverage
run: |
./scripts/ci/cloud-hypervisor-live-smoke.sh \
"$RUNNER_TEMP/cloud-hypervisor-test-x86_64"
- name: Print guest boot diagnostics on failure
if: failure()
run: |
set -uo pipefail
# Cloud Hypervisor's own collectDiagnostics() runs before the run
# directory is torn down on a startup failure (see
# src/cloud-hypervisor-runtime-backend.ts), so the guest serial
# console and Cloud Hypervisor log land under each case's
# audit/cloud-hypervisor/ directory. Print them directly in the
# job log for fast triage, in addition to the uploaded artifact.
# Redact the test-only secret sentinel defensively, matching the
# scan the next step performs on the copied artifact.
for f in "$RUNNER_TEMP"/awf-cloud-hypervisor-live/*/audit/cloud-hypervisor/{serial.log,cloud-hypervisor.log,network-diagnostics.txt,vm-info.json,counters.json}; do
[ -f "$f" ] || continue
echo "--- $f ---"
sed 's/awf-cloud-hypervisor-real-secret-do-not-expose/[REDACTED]/g' "$f"
done
- name: Collect redacted diagnostics
if: always()
run: |
set -euo pipefail
source_root="$RUNNER_TEMP/awf-cloud-hypervisor-live"
destination="$RUNNER_TEMP/cloud-hypervisor-diagnostics-safe"
rm -rf "$destination"
mkdir -p "$destination"
if [ -d "$source_root" ]; then
# --keep-containers (the keep-containers/preserve-diagnostics
# live case) intentionally leaves its own work/audit files
# root-owned (they are written by the CLI process, itself run
# via sudo, and deliberately never cleaned up so the case's own
# assertions can inspect them afterward). Every other case's
# files are owned by the runner user and readable without
# sudo, but find/cp must run as root here to reach the
# keep-containers case's own preserved files too -- reading a
# root-owned file as the runner user is a permission error,
# not evidence of a missing file.
while IFS= read -r -d '' file; do
relative=${file#"$source_root/"}
mkdir -p "$destination/$(dirname "$relative")"
sudo cp "$file" "$destination/$relative"
done < <(
sudo find "$source_root" -type f \
\( -path '*/audit/*' \
-o -path '*/proxy-logs/*' \
-o -name 'stdout.log' \
-o -name 'stderr.log' \) \
-print0
)
fi
# Hand ownership of the copied destination tree back to the
# runner user: sudo cp above creates new destination files as
# root, which the secret-sentinel scan below and the later
# upload-artifact step (both running as the runner user, not
# root) need to be able to read.
sudo chown -R "$(id -u):$(id -g)" "$destination"
# awf-resolved-config.json's agentCommand field always contains the
# smoke test's own shell command verbatim -- and this specific
# command (the api-proxy-reflect case) intentionally references
# the sentinel string itself as the pattern it greps for, to
# assert the sentinel is absent from `env`. That is expected,
# self-referential test source text, not a leak of the sentinel
# *value* into somewhere it shouldn't be (guest console output,
# network captures, proxy logs, etc. are all still fully scanned
# below) -- so this one, known, always-matching file is excluded
# from the scan rather than silently disabling it everywhere.
if grep -R --binary-files=without-match \
-F 'awf-cloud-hypervisor-real-secret-do-not-expose' \
--exclude='awf-resolved-config.json' \
"$destination"; then
echo "::error::Secret sentinel found in diagnostic artifacts"
rm -rf "$destination"
exit 1
fi
- name: Upload actionable diagnostics
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: cloud-hypervisor-live-diagnostics
path: ${{ runner.temp }}/cloud-hypervisor-diagnostics-safe/
if-no-files-found: warn
retention-days: 7
- name: Enforce final residue cleanup
if: always()
run: |
set -euo pipefail
if sudo ip netns list | grep -q '^awfvm-'; then
sudo ip netns list >&2
echo "::error::Cloud Hypervisor namespace residue remains after cleanup"
exit 1
fi
if sudo iptables -S DOCKER-USER | grep -q -- '--comment awf:awf_vm_'; then
sudo iptables -S DOCKER-USER >&2
echo "::error::Cloud Hypervisor bridge-rule residue remains after cleanup"
exit 1
fi
# /sys/fs/cgroup/awf-cloud-hypervisor is a parent cgroup that
# persists across the whole job; only per-run sub-cgroups are
# created one level inside it (see cgroupPath in
# src/cloud-hypervisor/manager.ts). Any cgroup v2 directory --
# including this parent itself -- always contains standard
# controller interface files (cpu.max, memory.max,
# cgroup.controllers, ...) simply by virtue of existing;
# matching all entries here (not just directories) made this
# check a guaranteed false positive the moment the live-KVM job
# ever actually completed successfully. See the identical fix
# in scripts/ci/cloud-hypervisor-live-smoke.sh's own
# assert_no_residue for the same root cause.
if [ -d /sys/fs/cgroup/awf-cloud-hypervisor ] && \
[ -n "$(sudo find /sys/fs/cgroup/awf-cloud-hypervisor -mindepth 1 -maxdepth 1 -type d 2>/dev/null)" ]; then
echo "::error::Cloud Hypervisor cgroup residue remains after cleanup"
exit 1
fi
if sudo pgrep -f '/run/awf-cloud-hypervisor/trusted-artifacts/run-[^/]*/[c]loud-hypervisor --api-socket' >/dev/null 2>&1; then
echo "::error::Cloud Hypervisor process residue remains after cleanup"
exit 1
fi