From db0185d0b774d45c46d433a644d195b0df81d7a0 Mon Sep 17 00:00:00 2001 From: Dylan Steele Date: Mon, 3 Aug 2026 17:58:03 -0400 Subject: [PATCH 1/3] virtio/console: harden governed boundary coverage Assisted-by: Codex: gpt-5.6 Signed-off-by: Dylan Steele --- .github/workflows/capsule-governed.yml | 3 + governance/capsule-v1.19.4/PATCH_QUEUE.json | 3 +- governance/capsule-v1.19.4/README.md | 7 +- .../capsule-v1.19.4/coverage-followup.json | 55 ++++ .../scripts/verify-default-init.sh | 56 ++++ .../scripts/verify-governed.sh | 3 + .../capsule-v1.19.4/scripts/verify-library.sh | 50 ++- .../scripts/verify-patch-queue.sh | 19 +- .../src/virtio/console/coverage_tests.rs | 290 ++++++++++++++++++ src/devices/src/virtio/console/mod.rs | 2 + src/devices/src/virtio/console/port.rs | 10 +- src/devices/src/virtio/console/process_tx.rs | 5 + src/libkrun/tests/governed_fd.rs | 278 +++++++++++++++++ 13 files changed, 764 insertions(+), 17 deletions(-) create mode 100644 governance/capsule-v1.19.4/coverage-followup.json create mode 100755 governance/capsule-v1.19.4/scripts/verify-default-init.sh create mode 100644 src/devices/src/virtio/console/coverage_tests.rs create mode 100644 src/libkrun/tests/governed_fd.rs diff --git a/.github/workflows/capsule-governed.yml b/.github/workflows/capsule-governed.yml index 67f052d7c4..c9971a8403 100644 --- a/.github/workflows/capsule-governed.yml +++ b/.github/workflows/capsule-governed.yml @@ -13,11 +13,13 @@ on: - src/devices/src/virtio/console/** - src/init_blob/init/init.c - src/libkrun/src/lib.rs + - src/libkrun/tests/** - src/vmm/src/resources.rs - src/vmm/src/vmm_config/block.rs push: branches: - codex/governed-capsule-v1.19.4 + - codex/governed-console-fd-coverage-v1.19.4 paths: - .github/CODEOWNERS - .github/workflows/capsule-governed.yml @@ -27,6 +29,7 @@ on: - src/devices/src/virtio/console/** - src/init_blob/init/init.c - src/libkrun/src/lib.rs + - src/libkrun/tests/** - src/vmm/src/resources.rs - src/vmm/src/vmm_config/block.rs workflow_dispatch: diff --git a/governance/capsule-v1.19.4/PATCH_QUEUE.json b/governance/capsule-v1.19.4/PATCH_QUEUE.json index 57236f457a..67f6397152 100644 --- a/governance/capsule-v1.19.4/PATCH_QUEUE.json +++ b/governance/capsule-v1.19.4/PATCH_QUEUE.json @@ -10,7 +10,8 @@ "fork": { "repository": "https://github.com/dills122/libkrun", "baselineBranch": "capsule/upstream-v1.19.4", - "workBranch": "codex/governed-capsule-v1.19.4" + "baselineCommit": "4ea8d1de861ed1c0636fc800b6da8fb71a086aa5", + "workBranch": "codex/governed-console-fd-coverage-v1.19.4" }, "capsuleEvidence": { "repository": "https://github.com/dills122/capsule-corp", diff --git a/governance/capsule-v1.19.4/README.md b/governance/capsule-v1.19.4/README.md index 8b9b9bef79..8d7420c77b 100644 --- a/governance/capsule-v1.19.4/README.md +++ b/governance/capsule-v1.19.4/README.md @@ -1,6 +1,6 @@ # Capsule governed libkrun v1.19.4 patch line -This directory governs one narrowly scoped downstream patch queue over the immutable upstream libkrun v1.19.4 commit `728df8125077d0db44265f6e997c72b81b65c015`. The baseline branch is `capsule/upstream-v1.19.4`; the proposed work branch is `codex/governed-capsule-v1.19.4`. +This directory governs one narrowly scoped downstream patch queue over the immutable upstream libkrun v1.19.4 commit `728df8125077d0db44265f6e997c72b81b65c015`. The queue was merged as `4ea8d1de861ed1c0636fc800b6da8fb71a086aa5`, which is the immutable head of `capsule/upstream-v1.19.4`; follow-up coverage work uses `codex/governed-console-fd-coverage-v1.19.4`. This line is local library and source-governance evidence only. It does not admit a Capsule backend or profile, create or execute a guest, wire product code, change libkrunfw or a kernel, exercise a Supervisor, sign a release, or grant path, image, network, mount, write, or deployment authority. @@ -18,7 +18,7 @@ The first two patches are prerequisites. They remain independently hashed and ar ## Review and branch policy -The baseline branch is an immutable pointer to the exact upstream tag commit. It must never be rebased, force-pushed, or advanced. Updates use a new versioned baseline and work branch. +The upstream anchor and the governed merge are immutable. The baseline branch must remain at the exact governed merge commit and must never be rebased, force-pushed, or advanced. Updates use a new versioned baseline and work branch. Patch reconstruction always starts from the upstream anchor and compares the retained queue to the governed merge, so reviewed follow-up changes cannot rewrite its provenance. Changes to this line require: @@ -49,7 +49,8 @@ The compile-only C header contract treats the pre-existing `/dev/input/*` text i ## Known blockers and limitations -- The measured retained console corpus has zero line/function coverage in `port.rs` and `process_tx.rs`; `coverage-baseline.json` preserves this rather than hiding it. Bounded library tests must close or explicitly review this gap before merge. +- `coverage-baseline.json` preserves the original zero line/function coverage in `port.rs` and `process_tx.rs`. The follow-up bounded library corpus must report exact before/after measurements without rewriting that baseline evidence. +- `coverage-followup.json` records the bounded follow-up measurement and the remaining uncovered functions, lines, and regions for those two files. - AddressSanitizer is supported only on the pinned macOS AArch64 nightly/toolchain route and remains a required governed check there. - The macOS library gate checks and lints `libkrun` with `blk` and without its default embedded init-blob feature. Compiling the Linux init blob requires the upstream Linux sysroot/cross-toolchain route; this fork supplements, but does not disable, that upstream build gate and retains an installed-build blocker until it passes. - No installed-product, real-guest, VMM transport, fuzzing, backend-admission, signing, firmware, kernel, or Supervisor evidence is produced here. diff --git a/governance/capsule-v1.19.4/coverage-followup.json b/governance/capsule-v1.19.4/coverage-followup.json new file mode 100644 index 0000000000..13f19926e2 --- /dev/null +++ b/governance/capsule-v1.19.4/coverage-followup.json @@ -0,0 +1,55 @@ +{ + "status": "local library coverage only; not real transport, guest, VMM, or admission evidence", + "sourceEvidenceDate": "2026-08-03", + "governedBaseCommit": "4ea8d1de861ed1c0636fc800b6da8fb71a086aa5", + "corpus": { + "retainedDefaultTests": 51, + "retainedBlockFeatureTests": 53, + "boundedConsolePropertyTests": 4, + "rawFdLibraryBoundaryTests": 2 + }, + "before": { + "aggregateChangedConsoleFiles": { + "functions": { "count": 88, "covered": 13, "percent": 14.772727 }, + "lines": { "count": 728, "covered": 90, "percent": 12.362637 }, + "regions": { "count": 1091, "covered": 156, "percent": 14.298808 } + }, + "files": [ + { + "file": "src/devices/src/virtio/console/port.rs", + "functions": { "count": 17, "covered": 0, "percent": 0.0 }, + "lines": { "count": 137, "covered": 0, "percent": 0.0 }, + "regions": { "count": 172, "covered": 0, "percent": 0.0 } + }, + { + "file": "src/devices/src/virtio/console/process_tx.rs", + "functions": { "count": 4, "covered": 0, "percent": 0.0 }, + "lines": { "count": 91, "covered": 0, "percent": 0.0 }, + "regions": { "count": 120, "covered": 0, "percent": 0.0 } + } + ] + }, + "after": { + "aggregateChangedConsoleFiles": { + "functions": { "count": 88, "covered": 37, "percent": 42.045455 }, + "lines": { "count": 733, "covered": 298, "percent": 40.654843 }, + "regions": { "count": 1099, "covered": 399, "percent": 36.305732 } + }, + "files": [ + { + "file": "src/devices/src/virtio/console/port.rs", + "functions": { "count": 17, "covered": 15, "percent": 88.235294 }, + "lines": { "count": 137, "covered": 111, "percent": 81.021898 }, + "regions": { "count": 173, "covered": 121, "percent": 69.942197 }, + "remaining": { "functions": 2, "lines": 26, "regions": 52 } + }, + { + "file": "src/devices/src/virtio/console/process_tx.rs", + "functions": { "count": 4, "covered": 4, "percent": 100.0 }, + "lines": { "count": 96, "covered": 82, "percent": 85.416667 }, + "regions": { "count": 127, "covered": 102, "percent": 80.314961 }, + "remaining": { "functions": 0, "lines": 14, "regions": 25 } + } + ] + } +} diff --git a/governance/capsule-v1.19.4/scripts/verify-default-init.sh b/governance/capsule-v1.19.4/scripts/verify-default-init.sh new file mode 100755 index 0000000000..992b40b5ed --- /dev/null +++ b/governance/capsule-v1.19.4/scripts/verify-default-init.sh @@ -0,0 +1,56 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +governance_dir=$(CDPATH='' cd -- "$script_dir/.." && pwd) +repo_dir=$(CDPATH='' cd -- "$governance_dir/../.." && pwd) +task_tmp=$(mktemp -d "${TMPDIR:-/tmp}/libkrun-capsule-default-init.XXXXXX") +trap 'rm -rf "$task_tmp"' EXIT HUP INT TERM + +if [ "${CAPSULE_ALLOW_GUEST:-0}" != 0 ]; then + printf 'guest execution is outside this governed verification route\n' >&2 + exit 2 +fi + +case "$(uname -s)" in + Darwin) + sysroot=${SYSROOT_LINUX:-$repo_dir/linux-sysroot} + if [ ! -f "$sysroot/.sysroot_ready" ]; then + printf 'defaultInitBlobBuild=BLOCKED\n' + printf 'defaultInitBlobReason=exact Linux sysroot is unavailable for the no-network macOS cross-build\n' + printf 'defaultInitBlobSysroot=%s\n' "$sysroot" + printf 'guestExecution=NOT_RUN\n' + exit 1 + fi + arch=$(uname -m | sed 's/^arm64$/aarch64/') + gcc_triplet="$arch-linux-gnu" + gcc_version=${GCC_VERSION:-12} + gcc_lib_dir="$sysroot/usr/lib/gcc/$gcc_triplet/$gcc_version" + if [ ! -d "$gcc_lib_dir" ] || ! command -v /usr/bin/clang >/dev/null 2>&1 || ! command -v ld.lld >/dev/null 2>&1; then + printf 'defaultInitBlobBuild=BLOCKED\n' + printf 'defaultInitBlobReason=exact clang-lld Linux cross-toolchain is unavailable\n' + printf 'guestExecution=NOT_RUN\n' + exit 1 + fi + cc_linux="/usr/bin/clang -target $gcc_triplet -fuse-ld=lld -Wl,-strip-debug --sysroot $sysroot -B$gcc_lib_dir -L$gcc_lib_dir -Wno-c23-extensions" + ;; + Linux) + cc_linux=${CC_LINUX:-${CC:-cc}} + ;; + *) + printf 'defaultInitBlobBuild=BLOCKED\n' + printf 'defaultInitBlobReason=unsupported host for the upstream Linux default-init build route\n' + printf 'guestExecution=NOT_RUN\n' + exit 1 + ;; +esac + +( + cd "$repo_dir" + CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$task_tmp/target" CC_LINUX="$cc_linux" \ + cargo build --locked --offline -p libkrun --lib --features blk +) + +printf 'defaultInitBlobBuild=PASS\n' +printf 'defaultInitBlobNetwork=DISABLED\n' +printf 'guestExecution=NOT_RUN\n' diff --git a/governance/capsule-v1.19.4/scripts/verify-governed.sh b/governance/capsule-v1.19.4/scripts/verify-governed.sh index 85b3d20511..1716d9d0be 100755 --- a/governance/capsule-v1.19.4/scripts/verify-governed.sh +++ b/governance/capsule-v1.19.4/scripts/verify-governed.sh @@ -11,6 +11,9 @@ fi if ! "$script_dir/verify-mutations.sh"; then status=1 fi +if ! "$script_dir/verify-default-init.sh"; then + status=1 +fi if [ "$status" -eq 0 ]; then printf 'governedLibraryValidation=PASS\n' else diff --git a/governance/capsule-v1.19.4/scripts/verify-library.sh b/governance/capsule-v1.19.4/scripts/verify-library.sh index 47ddec10f3..c521689823 100755 --- a/governance/capsule-v1.19.4/scripts/verify-library.sh +++ b/governance/capsule-v1.19.4/scripts/verify-library.sh @@ -39,6 +39,15 @@ default_log="$task_tmp/default-tests.log" ) | tee "$default_log" grep -Eq 'test result: ok\. 51 passed; 0 failed' "$default_log" +bounded_console_log="$task_tmp/bounded-console-tests.log" +( + cd "$repo_dir" + CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$target_dir/tests" \ + cargo test --locked --offline -p krun-devices --lib \ + virtio::console::coverage_tests -- --ignored --test-threads=1 +) | tee "$bounded_console_log" +grep -Eq 'test result: ok\. 4 passed; 0 failed' "$bounded_console_log" + blk_log="$task_tmp/blk-tests.log" ( cd "$repo_dir" @@ -48,6 +57,15 @@ blk_log="$task_tmp/blk-tests.log" ) | tee "$blk_log" grep -Eq 'test result: ok\. 53 passed; 0 failed' "$blk_log" +raw_fd_log="$task_tmp/raw-fd-tests.log" +( + cd "$repo_dir" + CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$target_dir/tests-libkrun" \ + cargo test --locked --offline -p libkrun --test governed_fd \ + --no-default-features --features blk -- --test-threads=1 +) | tee "$raw_fd_log" +grep -Eq 'test result: ok\. 2 passed; 0 failed' "$raw_fd_log" + ( cd "$repo_dir" CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$target_dir/clippy" \ @@ -56,6 +74,9 @@ grep -Eq 'test result: ok\. 53 passed; 0 failed' "$blk_log" CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$target_dir/clippy-libkrun" \ cargo clippy --locked --offline -p libkrun --lib --no-default-features --features blk --no-deps -- \ -D warnings + CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$target_dir/clippy-libkrun-tests" \ + cargo clippy --locked --offline -p libkrun --test governed_fd \ + --no-default-features --features blk --no-deps -- -D warnings ) repeat=1 @@ -70,13 +91,25 @@ while [ "$repeat" -le 25 ]; do repeat=$((repeat + 1)) done +repeat=1 +while [ "$repeat" -le 25 ]; do + ( + cd "$repo_dir" + CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$target_dir/tests" \ + cargo test --locked --offline -p krun-devices --lib \ + virtio::console::coverage_tests::shutdown_cancels_a_queued_backpressured_write \ + -- --exact --ignored --test-threads=1 >/dev/null 2>&1 + ) + repeat=$((repeat + 1)) +done + coverage_raw="$task_tmp/coverage.json" coverage_summary="$task_tmp/coverage-summary.json" ( cd "$repo_dir" CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$target_dir/coverage" \ cargo llvm-cov --locked --offline -p krun-devices --lib \ - --json --output-path "$coverage_raw" + --json --output-path "$coverage_raw" -- --include-ignored --test-threads=1 ) python3 "$script_dir/summarize-coverage.py" "$coverage_raw" "$coverage_summary" if [ -n "${CAPSULE_COVERAGE_OUTPUT:-}" ]; then @@ -93,6 +126,18 @@ if [ "$(uname -s)" = Darwin ] && [ "$(uname -m)" = arm64 ]; then CARGO_TARGET_DIR="$target_dir/asan" \ cargo +"$sanitizer_toolchain" test --locked --target aarch64-apple-darwin \ --offline -p krun-devices --lib + CARGO_NET_OFFLINE=true \ + CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS=-Zsanitizer=address \ + CARGO_TARGET_DIR="$target_dir/asan" \ + cargo +"$sanitizer_toolchain" test --locked --target aarch64-apple-darwin \ + --offline -p krun-devices --lib virtio::console::coverage_tests -- \ + --ignored --test-threads=1 + CARGO_NET_OFFLINE=true \ + CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS=-Zsanitizer=address \ + CARGO_TARGET_DIR="$target_dir/asan-libkrun" \ + cargo +"$sanitizer_toolchain" test --locked --target aarch64-apple-darwin \ + --offline -p libkrun --test governed_fd --no-default-features --features blk -- \ + --test-threads=1 ) asan_status=PASS fi @@ -101,11 +146,14 @@ printf 'governedConsoleRustfmt=PASS\n' printf 'cargoFmt=PASS_EXACT_RETAINED_DRIFT_ONLY\n' printf 'cargoCheck=PASS\n' printf 'consoleCorpusTests=51\n' +printf 'boundedConsoleCoverageTests=4\n' printf 'blockFeatureTests=53\n' printf 'rawFdContractTests=2\n' +printf 'rawFdLibraryBoundaryTests=2\n' printf 'clippyWarningsDenied=PASS\n' printf 'clippyAllowance=deprecated-GuestMemory-try_access-only\n' printf 'shutdownRepetitions=25\n' +printf 'queuedBackpressureShutdownRepetitions=25\n' printf 'addressSanitizer=%s\n' "$asan_status" cat "$coverage_summary" printf 'guestExecution=NOT_RUN\n' diff --git a/governance/capsule-v1.19.4/scripts/verify-patch-queue.sh b/governance/capsule-v1.19.4/scripts/verify-patch-queue.sh index bf67bd8c0f..ccf4f2ecb6 100755 --- a/governance/capsule-v1.19.4/scripts/verify-patch-queue.sh +++ b/governance/capsule-v1.19.4/scripts/verify-patch-queue.sh @@ -4,7 +4,8 @@ set -eu script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) governance_dir=$(CDPATH='' cd -- "$script_dir/.." && pwd) repo_dir=$(CDPATH='' cd -- "$governance_dir/../.." && pwd) -base_commit=728df8125077d0db44265f6e997c72b81b65c015 +upstream_commit=728df8125077d0db44265f6e997c72b81b65c015 +governed_base_commit=4ea8d1de861ed1c0636fc800b6da8fb71a086aa5 patch_set_sha256=d19fd0ff159c699acccda2621519de45a09408bf3847b418ac34e02b79e805d5 patches='0001-pin-libkrunfw-rpath.patch @@ -34,10 +35,11 @@ expected_hash_for() { esac } -git -C "$repo_dir" cat-file -e "$base_commit^{commit}" +git -C "$repo_dir" cat-file -e "$upstream_commit^{commit}" +git -C "$repo_dir" cat-file -e "$governed_base_commit^{commit}" actual_base=$(git -C "$repo_dir" rev-parse --verify refs/heads/capsule/upstream-v1.19.4 2>/dev/null || git -C "$repo_dir" rev-parse --verify refs/remotes/origin/capsule/upstream-v1.19.4) -[ "$actual_base" = "$base_commit" ] || { - printf 'baseline branch moved: got %s, want %s\n' "$actual_base" "$base_commit" >&2 +[ "$actual_base" = "$governed_base_commit" ] || { + printf 'governed baseline branch moved: got %s, want %s\n' "$actual_base" "$governed_base_commit" >&2 exit 1 } @@ -45,7 +47,7 @@ task_tmp=$(mktemp -d "${TMPDIR:-/tmp}/libkrun-capsule-patches.XXXXXX") trap 'rm -rf "$task_tmp"' EXIT HUP INT TERM reconstructed="$task_tmp/reconstructed" mkdir -p "$reconstructed" -git -C "$repo_dir" archive "$base_commit" | tar -x -C "$reconstructed" +git -C "$repo_dir" archive "$upstream_commit" | tar -x -C "$reconstructed" identity_file="$task_tmp/identities" : >"$identity_file" @@ -70,7 +72,9 @@ actual_patch_set=$(shasum -a 256 "$identity_file" | awk '{print $1}') } for governed_path in $governed_paths; do - cmp "$reconstructed/$governed_path" "$repo_dir/$governed_path" + governed_base_path="$task_tmp/governed-base" + git -C "$repo_dir" show "$governed_base_commit:$governed_path" >"$governed_base_path" + cmp "$reconstructed/$governed_path" "$governed_base_path" done for patch_name in \ @@ -82,7 +86,8 @@ for patch_name in \ patch -d "$reconstructed" -p1 --batch --reverse --dry-run <"$governance_dir/patches/$patch_name" >/dev/null done -printf 'baseCommit=%s\n' "$base_commit" +printf 'upstreamCommit=%s\n' "$upstream_commit" +printf 'governedBaseCommit=%s\n' "$governed_base_commit" printf 'patchSetSha256=%s\n' "$actual_patch_set" printf 'cleanReconstruction=PASS\n' printf 'reverseDryRun=PASS\n' diff --git a/src/devices/src/virtio/console/coverage_tests.rs b/src/devices/src/virtio/console/coverage_tests.rs new file mode 100644 index 0000000000..cdcb00d99c --- /dev/null +++ b/src/devices/src/virtio/console/coverage_tests.rs @@ -0,0 +1,290 @@ +use std::cmp::min; +use std::collections::VecDeque; +use std::io; +use std::os::fd::AsRawFd; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::{Duration, Instant}; + +use nix::errno::Errno; +use nix::unistd::pipe; +use vm_memory::{Bytes, GuestAddress, GuestMemoryMmap, VolatileSlice}; + +use super::port::{Port, PortDescription}; +use super::port_io::{output_to_raw_fd_dup, PortOutput}; +use super::port_queue_mapping::{ + num_queues, port_id_to_queue_idx, queue_idx_to_port_id, QueueDirection, +}; +use crate::legacy::DummyIrqChip; +use crate::virtio::queue::tests::VirtQueue; +use crate::virtio::InterruptTransport; + +const NEXT: u16 = 1; +const WRITE: u16 = 2; + +struct PartialOutput { + actions: VecDeque>, + bytes: Arc>>, + waits: Arc, + complete: mpsc::Sender<()>, + expected: usize, +} + +struct DiscardOutput; + +impl PortOutput for DiscardOutput { + fn write_volatile(&mut self, buf: &VolatileSlice) -> io::Result { + Ok(buf.len()) + } + + fn wait_until_writable(&self, _stopfd: Option<&utils::eventfd::EventFd>) -> bool { + true + } +} + +struct ZeroProgressOutput { + calls: Arc, + attempted: mpsc::Sender<()>, +} + +impl PortOutput for ZeroProgressOutput { + fn write_volatile(&mut self, _buf: &VolatileSlice) -> io::Result { + self.calls.fetch_add(1, Ordering::Relaxed); + self.attempted.send(()).unwrap(); + Ok(0) + } + + fn wait_until_writable(&self, _stopfd: Option<&utils::eventfd::EventFd>) -> bool { + true + } +} + +impl PortOutput for PartialOutput { + fn write_volatile(&mut self, buf: &VolatileSlice) -> io::Result { + let action = self.actions.pop_front().unwrap_or(Ok(buf.len())); + let requested = action?; + let count = min(requested, buf.len()); + let mut chunk = vec![0; count]; + assert_eq!(buf.copy_to(&mut chunk), count); + let mut bytes = self.bytes.lock().unwrap(); + bytes.extend_from_slice(&chunk); + if bytes.len() == self.expected { + self.complete.send(()).unwrap(); + } + Ok(count) + } + + fn wait_until_writable(&self, _stopfd: Option<&utils::eventfd::EventFd>) -> bool { + self.waits.fetch_add(1, Ordering::Relaxed); + true + } +} + +fn interrupt() -> InterruptTransport { + InterruptTransport::new(DummyIrqChip::new().into(), "console-coverage".to_string()).unwrap() +} + +fn empty_queue<'a>( + mem: &'a GuestMemoryMmap, + address: u64, +) -> (VirtQueue<'a>, crate::virtio::Queue) { + let virtq = VirtQueue::new(GuestAddress(address), mem, 8); + let queue = virtq.create_queue(); + (virtq, queue) +} + +#[test] +#[ignore = "run by the governed bounded coverage corpus"] +fn bounded_directional_ids_and_port_lifecycle_properties() { + for port_id in 0..=1024 { + let rx = port_id_to_queue_idx(QueueDirection::Rx, port_id); + let tx = port_id_to_queue_idx(QueueDirection::Tx, port_id); + assert_eq!(queue_idx_to_port_id(rx), (QueueDirection::Rx, port_id)); + assert_eq!(queue_idx_to_port_id(tx), (QueueDirection::Tx, port_id)); + assert_ne!(rx, 2); + assert_ne!(rx, 3); + assert_ne!(tx, 2); + assert_ne!(tx, 3); + assert!(rx < num_queues(port_id + 1)); + assert!(tx < num_queues(port_id + 1)); + } + for control_queue in [2, 3] { + assert!(std::panic::catch_unwind(|| queue_idx_to_port_id(control_queue)).is_err()); + } + + let terminal = super::port_io::term_fixed_size(120, 40); + let mut console = Port::new(0, PortDescription::console(None, None, terminal)); + assert_eq!(console.name(), ""); + assert_eq!(console.terminal().unwrap().get_win_size(), (120, 40)); + console.notify_rx(); + console.notify_tx(); + + let mut output = Port::new( + 1, + PortDescription::output_pipe("stdout", Box::new(DiscardOutput)), + ); + assert_eq!(output.name(), "stdout"); + assert!(output.terminal().is_none()); + + let input = Port::new( + 2, + PortDescription::input_pipe("stdin", super::port_io::input_empty().unwrap()), + ); + assert_eq!(input.name(), "stdin"); + + let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10_000)]).unwrap(); + let (_rx, rx_queue) = empty_queue(&mem, 0); + let (_tx, tx_queue) = empty_queue(&mem, 0x400); + console.start( + mem.clone(), + rx_queue, + tx_queue, + interrupt(), + super::console_control::ConsoleControl::new(), + ); + assert!(console.is_active()); + console.notify_rx(); + console.notify_tx(); + console.shutdown(); + assert!(!console.is_active()); + console.shutdown(); + + output.shutdown(); +} + +#[test] +#[ignore = "run by the governed bounded coverage corpus"] +fn partial_writes_and_descriptor_direction_are_accounted_once() { + let payload = b"bounded-partial-write"; + let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10_000)]).unwrap(); + let (_rx, rx_queue) = empty_queue(&mem, 0); + let tx_virtq = VirtQueue::new(GuestAddress(0x400), &mem, 8); + mem.write_slice(payload, GuestAddress(0x2000)).unwrap(); + mem.write_slice(b"ignored", GuestAddress(0x3000)).unwrap(); + tx_virtq.dtable[0].set(0x2000, payload.len() as u32, NEXT, 1); + tx_virtq.dtable[1].set(0x3000, 7, WRITE, 0); + tx_virtq.avail.ring[0].set(0); + tx_virtq.avail.idx.set(1); + + let bytes = Arc::new(Mutex::new(Vec::new())); + let waits = Arc::new(AtomicUsize::new(0)); + let (complete_tx, complete_rx) = mpsc::channel(); + let partial = PartialOutput { + actions: VecDeque::from([ + Err(io::Error::from(io::ErrorKind::WouldBlock)), + Ok(1), + Ok(2), + Ok(3), + ]), + bytes: Arc::clone(&bytes), + waits: Arc::clone(&waits), + complete: complete_tx, + expected: payload.len(), + }; + let mut port = Port::new( + 7, + PortDescription::output_pipe("partial", Box::new(partial)), + ); + port.start( + mem.clone(), + rx_queue, + tx_virtq.create_queue(), + interrupt(), + super::console_control::ConsoleControl::new(), + ); + complete_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + port.shutdown(); + + assert_eq!(&*bytes.lock().unwrap(), payload); + assert_eq!(waits.load(Ordering::Relaxed), 1); + assert_eq!(tx_virtq.used.idx.get(), 1); + assert_eq!(tx_virtq.used.ring[0].get().id, 0); + assert_eq!(tx_virtq.used.ring[0].get().len, payload.len() as u32); +} + +#[test] +#[ignore = "run by the governed bounded coverage corpus"] +fn shutdown_cancels_a_queued_backpressured_write() { + let (reader, writer) = pipe().unwrap(); + let output = output_to_raw_fd_dup(writer.as_raw_fd()).unwrap(); + let fill = [0u8; 4096]; + loop { + let written = unsafe { + libc::write( + writer.as_raw_fd(), + fill.as_ptr().cast::(), + fill.len(), + ) + }; + if written < 0 { + assert_eq!(Errno::last(), Errno::EAGAIN); + break; + } + } + + let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10_000)]).unwrap(); + let (_rx, rx_queue) = empty_queue(&mem, 0); + let tx_virtq = VirtQueue::new(GuestAddress(0x400), &mem, 8); + mem.write_slice(b"blocked", GuestAddress(0x2000)).unwrap(); + tx_virtq.dtable[0].set(0x2000, 7, 0, 0); + tx_virtq.avail.ring[0].set(0); + tx_virtq.avail.idx.set(1); + + let interrupt = interrupt(); + let mut port = Port::new(9, PortDescription::output_pipe("blocked", output)); + port.start( + mem.clone(), + rx_queue, + tx_virtq.create_queue(), + interrupt.clone(), + super::console_control::ConsoleControl::new(), + ); + + let deadline = Instant::now() + Duration::from_secs(2); + while interrupt.status().load(Ordering::SeqCst) == 0 && Instant::now() < deadline { + std::thread::yield_now(); + } + assert_ne!(interrupt.status().load(Ordering::SeqCst), 0); + + port.shutdown(); + assert!(!port.is_active()); + assert_eq!(tx_virtq.used.idx.get(), 0); + drop(reader); + drop(writer); +} + +#[test] +#[ignore = "run by the governed bounded coverage corpus"] +fn zero_progress_is_parked_instead_of_busy_looping() { + let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10_000)]).unwrap(); + let (_rx, rx_queue) = empty_queue(&mem, 0); + let tx_virtq = VirtQueue::new(GuestAddress(0x400), &mem, 8); + mem.write_slice(b"no-progress", GuestAddress(0x2000)) + .unwrap(); + tx_virtq.dtable[0].set(0x2000, 11, 0, 0); + tx_virtq.avail.ring[0].set(0); + tx_virtq.avail.idx.set(1); + + let calls = Arc::new(AtomicUsize::new(0)); + let (attempted_tx, attempted_rx) = mpsc::channel(); + let output = ZeroProgressOutput { + calls: Arc::clone(&calls), + attempted: attempted_tx, + }; + let mut port = Port::new(10, PortDescription::output_pipe("zero", Box::new(output))); + port.start( + mem.clone(), + rx_queue, + tx_virtq.create_queue(), + interrupt(), + super::console_control::ConsoleControl::new(), + ); + attempted_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + std::thread::sleep(Duration::from_millis(10)); + assert_eq!(calls.load(Ordering::Relaxed), 1); + + port.shutdown(); + assert!(!port.is_active()); + assert_eq!(calls.load(Ordering::Relaxed), 1); + assert_eq!(tx_virtq.used.idx.get(), 0); +} diff --git a/src/devices/src/virtio/console/mod.rs b/src/devices/src/virtio/console/mod.rs index 33e47994cb..0cc538803f 100644 --- a/src/devices/src/virtio/console/mod.rs +++ b/src/devices/src/virtio/console/mod.rs @@ -1,4 +1,6 @@ mod console_control; +#[cfg(test)] +mod coverage_tests; mod device; mod event_handler; mod port; diff --git a/src/devices/src/virtio/console/port.rs b/src/devices/src/virtio/console/port.rs index 9108a1a291..7fb3ea57a3 100644 --- a/src/devices/src/virtio/console/port.rs +++ b/src/devices/src/virtio/console/port.rs @@ -178,9 +178,9 @@ impl Port { if let PortState::Active { stopfd, stop, - tx_thread, - rx_thread, - } = &mut self.state + mut tx_thread, + mut rx_thread, + } = mem::replace(&mut self.state, PortState::Inactive) { stop.store(true, Ordering::Release); if let Err(e) = stopfd.write(1) { @@ -189,7 +189,7 @@ impl Port { port_id = self.port_id ); } - if let Some(tx_thread) = mem::take(tx_thread) { + if let Some(tx_thread) = tx_thread.take() { tx_thread.thread().unpark(); if let Err(e) = tx_thread.join() { log::error!( @@ -198,7 +198,7 @@ impl Port { ) } } - if let Some(rx_thread) = mem::take(rx_thread) { + if let Some(rx_thread) = rx_thread.take() { rx_thread.thread().unpark(); if let Err(e) = rx_thread.join() { log::error!( diff --git a/src/devices/src/virtio/console/process_tx.rs b/src/devices/src/virtio/console/process_tx.rs index 35e13df801..bd5953da25 100644 --- a/src/devices/src/virtio/console/process_tx.rs +++ b/src/devices/src/virtio/console/process_tx.rs @@ -34,6 +34,9 @@ pub(crate) fn process_tx( &stop, ) { Ok(0) => { + if stop.load(Ordering::Acquire) { + return; + } break; } Ok(n) => { @@ -58,6 +61,8 @@ pub(crate) fn process_tx( if bytes_written == 0 { log::trace!("Tx Add used {bytes_written}"); queue.undo_pop(); + interrupt.signal_used_queue(); + thread::park(); } else { log::trace!("Tx add used {bytes_written}"); if let Err(e) = queue.add_used(&mem, head_index, bytes_written as u32) { diff --git a/src/libkrun/tests/governed_fd.rs b/src/libkrun/tests/governed_fd.rs new file mode 100644 index 0000000000..01cdb2914b --- /dev/null +++ b/src/libkrun/tests/governed_fd.rs @@ -0,0 +1,278 @@ +#![cfg(unix)] + +use std::collections::BTreeSet; +use std::fs::{self, File, OpenOptions}; +use std::io::Write; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; +use std::os::unix::fs::{MetadataExt, PermissionsExt}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use krun::{krun_add_read_only_raw_root_fd, krun_create_ctx, krun_free_ctx}; + +const FIXTURE_LEN: u64 = 4096; +static FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); + +struct Fixture { + file: Option, + path: Option, + device: u64, + inode: u64, +} + +impl Fixture { + fn new(label: &str, writable: bool, linked: bool, mode: u32) -> Self { + let (path, mut writer) = loop { + let id = FIXTURE_ID.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "libkrun-governed-{label}-{}-{id}", + std::process::id() + )); + match OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(&path) + { + Ok(file) => break (path, file), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => panic!("create fixture: {error}"), + } + }; + writer.write_all(&[0x5a; FIXTURE_LEN as usize]).unwrap(); + writer.sync_all().unwrap(); + fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap(); + + let file = if writable { + writer + } else { + let reader = OpenOptions::new().read(true).open(&path).unwrap(); + drop(writer); + reader + }; + if !linked { + fs::remove_file(&path).unwrap(); + } + let metadata = file.metadata().unwrap(); + Self { + file: Some(file), + path: linked.then_some(path), + device: metadata.dev(), + inode: metadata.ino(), + } + } + + fn fd(&self) -> RawFd { + self.file.as_ref().unwrap().as_raw_fd() + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + if let Some(path) = self.path.take() { + let _ = fs::remove_file(path); + } + } +} + +struct Context(u32); + +impl Context { + fn new() -> Self { + let id = krun_create_ctx(); + assert!(id >= 0); + Self(id as u32) + } +} + +impl Drop for Context { + fn drop(&mut self) { + assert_eq!(krun_free_ctx(self.0), 0); + } +} + +fn open_fds() -> BTreeSet { + (0..1024) + .filter(|fd| unsafe { libc::fcntl(*fd, libc::F_GETFD) } >= 0) + .collect() +} + +#[test] +fn bounded_raw_fd_validation_corpus() { + let context = Context::new(); + let valid = Fixture::new("valid", false, false, 0o400); + + for (fd, device, inode, length, expected) in [ + (-1, valid.device, valid.inode, FIXTURE_LEN, -libc::EINVAL), + (valid.fd(), 0, valid.inode, FIXTURE_LEN, -libc::EINVAL), + (valid.fd(), valid.device, 0, FIXTURE_LEN, -libc::EINVAL), + (valid.fd(), valid.device, valid.inode, 0, -libc::EINVAL), + (valid.fd(), valid.device, valid.inode, 513, -libc::EINVAL), + ( + valid.fd(), + valid.device, + valid.inode.wrapping_add(1), + FIXTURE_LEN, + -libc::ESTALE, + ), + ( + valid.fd(), + valid.device.wrapping_add(1), + valid.inode, + FIXTURE_LEN, + -libc::ESTALE, + ), + ( + valid.fd(), + valid.device, + valid.inode, + FIXTURE_LEN + 512, + -libc::EINVAL, + ), + ] { + assert_eq!( + krun_add_read_only_raw_root_fd(context.0, fd, device, inode, length), + expected + ); + } + + let writable = Fixture::new("writable", true, false, 0o400); + assert_eq!( + krun_add_read_only_raw_root_fd( + context.0, + writable.fd(), + writable.device, + writable.inode, + FIXTURE_LEN, + ), + -libc::EACCES + ); + + for fixture in [ + Fixture::new("linked", false, true, 0o400), + Fixture::new("mode", false, false, 0o600), + ] { + assert_eq!( + krun_add_read_only_raw_root_fd( + context.0, + fixture.fd(), + fixture.device, + fixture.inode, + FIXTURE_LEN, + ), + -libc::EINVAL + ); + } + + let mut pipe_fds = [-1; 2]; + assert_eq!(unsafe { libc::pipe(pipe_fds.as_mut_ptr()) }, 0); + let pipe_reader = unsafe { OwnedFd::from_raw_fd(pipe_fds[0]) }; + let _pipe_writer = unsafe { OwnedFd::from_raw_fd(pipe_fds[1]) }; + assert_eq!( + krun_add_read_only_raw_root_fd(context.0, pipe_reader.as_raw_fd(), 1, 1, FIXTURE_LEN,), + -libc::EINVAL + ); + + let closed_fd = unsafe { libc::dup(valid.fd()) }; + assert!(closed_fd >= 0); + assert_eq!(unsafe { libc::close(closed_fd) }, 0); + assert_eq!( + krun_add_read_only_raw_root_fd( + context.0, + closed_fd, + valid.device, + valid.inode, + FIXTURE_LEN, + ), + -libc::EBADF + ); + + assert_eq!( + krun_add_read_only_raw_root_fd( + context.0, + valid.fd(), + valid.device, + valid.inode, + FIXTURE_LEN, + ), + 0 + ); + assert_eq!( + krun_add_read_only_raw_root_fd( + context.0, + valid.fd(), + valid.device, + valid.inode, + FIXTURE_LEN, + ), + -libc::EEXIST + ); +} + +#[test] +fn descriptor_is_owned_cloexec_and_survives_caller_fd_reuse() { + let context = Context::new(); + let mut fixture = Fixture::new("ownership", false, false, 0o400); + let caller_fd = fixture.fd(); + let before = open_fds(); + + assert_eq!( + krun_add_read_only_raw_root_fd( + context.0, + caller_fd, + fixture.device, + fixture.inode, + FIXTURE_LEN, + ), + 0 + ); + + let added: Vec<_> = open_fds().difference(&before).copied().collect(); + assert_eq!(added.len(), 1); + let owned_fd = added[0]; + assert_ne!(owned_fd, caller_fd); + assert_ne!( + unsafe { libc::fcntl(owned_fd, libc::F_GETFD) } & libc::FD_CLOEXEC, + 0 + ); + assert_eq!( + unsafe { libc::fcntl(owned_fd, libc::F_GETFL) } & libc::O_ACCMODE, + libc::O_RDONLY + ); + + drop(fixture.file.take()); + let replacement = OpenOptions::new() + .read(true) + .write(true) + .open("/dev/null") + .unwrap(); + let reused = if replacement.as_raw_fd() == caller_fd { + replacement + } else { + assert_eq!( + unsafe { libc::dup2(replacement.as_raw_fd(), caller_fd) }, + caller_fd + ); + drop(replacement); + unsafe { File::from_raw_fd(caller_fd) } + }; + assert_eq!(reused.as_raw_fd(), caller_fd); + + let inspection_fd = unsafe { libc::dup(owned_fd) }; + assert!(inspection_fd >= 0); + let metadata = unsafe { File::from_raw_fd(inspection_fd) } + .metadata() + .unwrap(); + assert_eq!(metadata.dev(), fixture.device); + assert_eq!(metadata.ino(), fixture.inode); + assert_eq!(metadata.nlink(), 0); + assert_eq!(metadata.permissions().mode() & 0o7777, 0o400); + + drop(context); + assert_eq!(unsafe { libc::fcntl(owned_fd, libc::F_GETFD) }, -1); + assert_eq!( + std::io::Error::last_os_error().raw_os_error(), + Some(libc::EBADF) + ); + drop(reused); +} From 2fd862b1c7e0fa9bf38ff106cdb49d5c2b215bd9 Mon Sep 17 00:00:00 2001 From: Dylan Steele Date: Mon, 3 Aug 2026 18:17:47 -0400 Subject: [PATCH 2/3] governance: separate default-init CI evidence Assisted-by: Codex: gpt-5.6 Signed-off-by: Dylan Steele --- governance/capsule-v1.19.4/README.md | 4 +++- governance/capsule-v1.19.4/scripts/verify-governed.sh | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/governance/capsule-v1.19.4/README.md b/governance/capsule-v1.19.4/README.md index 8d7420c77b..04e7fb921f 100644 --- a/governance/capsule-v1.19.4/README.md +++ b/governance/capsule-v1.19.4/README.md @@ -35,6 +35,8 @@ A green workflow is necessary but not sufficient for merge. The PR stays draft w `.github/workflows/capsule-governed.yml` runs only for the versioned governed branch, pull requests targeting the versioned baseline, manual dispatch, and changes to this exact patch line or its touched source paths. It adds no exception to upstream checks. The governed checks use fixed local fixtures and library/unit processes only; the scripts reject opt-in guest execution. +The governed wrapper is an offline library-only gate and does not bootstrap a Linux sysroot. `scripts/verify-default-init.sh` remains a standalone, fail-closed probe for a pre-provisioned exact sysroot and cross-toolchain. The existing upstream macOS cross-compilation job provisions that environment and runs `make` with the default Linux init blob, without executing a guest; its result is the pull request's build evidence for that route. + The upstream integration workflow is precisely routed away from pull requests whose base is `capsule/upstream-v1.19.4`, because it installs firmware and executes guests. All other pull requests retain upstream integration behavior. The governed replacement performs no guest execution. Governed Clippy uses the retained Rust 1.93.1 toolchain with only the documented deprecated `GuestMemory::try_access` allowance. Rust 1.97.1 formatting must report exactly the one retained P0-2 line-wrap drift recorded in `expected/cargo-fmt-1.97.1.txt`; any additional difference fails CI. The 53-test `blk` corpus runs with one test thread because two exact retained raw-FD tests use a clock-derived temporary name that can collide under parallel execution on macOS. Serial routing preserves every assertion and the exact retained source bytes. This preserves exact retained patch bytes without silently exempting another path. The default upstream test surface is intentionally preserved. Where the governed direct-block-root profile conflicts with unmodified upstream NullFs behavior, the difference is isolated to this queue and its `blk` feature tests instead of disabling or weakening an upstream security check. @@ -52,7 +54,7 @@ The compile-only C header contract treats the pre-existing `/dev/input/*` text i - `coverage-baseline.json` preserves the original zero line/function coverage in `port.rs` and `process_tx.rs`. The follow-up bounded library corpus must report exact before/after measurements without rewriting that baseline evidence. - `coverage-followup.json` records the bounded follow-up measurement and the remaining uncovered functions, lines, and regions for those two files. - AddressSanitizer is supported only on the pinned macOS AArch64 nightly/toolchain route and remains a required governed check there. -- The macOS library gate checks and lints `libkrun` with `blk` and without its default embedded init-blob feature. Compiling the Linux init blob requires the upstream Linux sysroot/cross-toolchain route; this fork supplements, but does not disable, that upstream build gate and retains an installed-build blocker until it passes. +- The macOS library gate checks and lints `libkrun` with `blk` and without its default embedded init-blob feature. The standalone no-network default-init probe remains blocked when its exact pre-provisioned Linux sysroot/cross-toolchain is absent; the pull request's upstream macOS cross-compilation job must independently pass the default-init build route. - No installed-product, real-guest, VMM transport, fuzzing, backend-admission, signing, firmware, kernel, or Supervisor evidence is produced here. - The raw-FD contract is validated with Rust library tests, source-route mutations, and a compile-only C header contract. It is not runtime guest evidence. - libkrunfw and kernel license/source obligations remain outside this patch line and must be resolved by any eventual distributor. diff --git a/governance/capsule-v1.19.4/scripts/verify-governed.sh b/governance/capsule-v1.19.4/scripts/verify-governed.sh index 1716d9d0be..85b3d20511 100755 --- a/governance/capsule-v1.19.4/scripts/verify-governed.sh +++ b/governance/capsule-v1.19.4/scripts/verify-governed.sh @@ -11,9 +11,6 @@ fi if ! "$script_dir/verify-mutations.sh"; then status=1 fi -if ! "$script_dir/verify-default-init.sh"; then - status=1 -fi if [ "$status" -eq 0 ]; then printf 'governedLibraryValidation=PASS\n' else From 8a2c91943793668f31a1cf7af431933be935bb58 Mon Sep 17 00:00:00 2001 From: Dylan Steele Date: Mon, 3 Aug 2026 18:26:17 -0400 Subject: [PATCH 3/3] libkrun: gate raw fd tests with blk Assisted-by: Codex: gpt-5.6 Signed-off-by: Dylan Steele --- src/libkrun/tests/governed_fd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libkrun/tests/governed_fd.rs b/src/libkrun/tests/governed_fd.rs index 01cdb2914b..03fb740dfa 100644 --- a/src/libkrun/tests/governed_fd.rs +++ b/src/libkrun/tests/governed_fd.rs @@ -1,4 +1,4 @@ -#![cfg(unix)] +#![cfg(all(unix, feature = "blk"))] use std::collections::BTreeSet; use std::fs::{self, File, OpenOptions};