From 956eaec5049c1a606dda658c979f7dd8117270ac Mon Sep 17 00:00:00 2001 From: Dylan Steele Date: Tue, 4 Aug 2026 22:34:43 -0400 Subject: [PATCH 1/4] virtio/console: reject unknown control port IDs Guest control messages currently index ports and derived queues without validating the supplied port ID. Reject unknown PORT_READY and PORT_OPEN IDs before any port or queue access so malformed guest input cannot panic the VMM or change console state. Complete driver-to-device control chains with a used length of zero because the device does not write into them. Assisted-by: Codex:gpt-5.6-sol --- src/devices/src/virtio/console/device.rs | 124 +++++++++++++++++------ 1 file changed, 92 insertions(+), 32 deletions(-) diff --git a/src/devices/src/virtio/console/device.rs b/src/devices/src/virtio/console/device.rs index 7c9325ac19..f20b42d842 100644 --- a/src/devices/src/virtio/console/device.rs +++ b/src/devices/src/virtio/console/device.rs @@ -1,7 +1,7 @@ use std::cmp; use std::io::Write; use std::iter::zip; -use std::mem::{size_of, size_of_val}; +use std::mem::size_of; use std::os::unix::io::{AsRawFd, RawFd}; use std::sync::Arc; @@ -29,12 +29,6 @@ fn control_descriptor_shape_valid(len: u32, write_only: bool, chained: bool) -> !write_only && !chained && len as usize == size_of::() } -fn checked_port_index(port_count: usize, port_id: u32) -> Option { - usize::try_from(port_id) - .ok() - .filter(|port_id| *port_id < port_count) -} - fn schedule_port_start(pending: &mut Vec, port_id: usize, active: bool) { if !active && !pending.contains(&port_id) { pending.push(port_id); @@ -219,10 +213,7 @@ impl Console { continue; } }; - if let Err(e) = control_tx - .queue - .add_used(mem, head.index, size_of_val(&cmd) as u32) - { + if let Err(e) = control_tx.queue.add_used(mem, head.index, 0) { error!("failed to add used elements to the queue: {e:?}"); } @@ -238,17 +229,17 @@ impl Console { } } control_event::VIRTIO_CONSOLE_PORT_READY => { + let Some(port) = self.ports.get(cmd.id as usize) else { + log::warn!("Guest reported unknown console port {} ready", cmd.id); + continue; + }; + if cmd.value != 1 { log::error!("Port initialization failed: {cmd:?}"); continue; } - let Some(port_id) = checked_port_index(self.ports.len(), cmd.id) else { - log::warn!("Ignoring ready event for unknown console port {}", cmd.id); - continue; - }; - - if let Some(term) = self.ports[port_id].terminal() { + if let Some(term) = port.terminal() { self.control.mark_console_port(mem, cmd.id); self.control.port_open(cmd.id, true); let (cols, rows) = term.get_win_size(); @@ -260,15 +251,17 @@ impl Console { self.control.port_open(cmd.id, true) } - let name = self.ports[port_id].name(); + let name = port.name(); log::trace!("Port ready {id}: {name}", id = cmd.id); if !name.is_empty() { self.control.port_name(cmd.id, name) } } control_event::VIRTIO_CONSOLE_PORT_OPEN => { - let Some(port_id) = checked_port_index(self.ports.len(), cmd.id) else { - log::warn!("Ignoring open event for unknown console port {}", cmd.id); + let Some(port_id) = + ((cmd.id as usize) < self.ports.len()).then_some(cmd.id as usize) + else { + log::warn!("Guest reported unknown console port {} open", cmd.id); continue; }; @@ -422,11 +415,18 @@ impl VmmExitObserver for Console { #[cfg(test)] mod tests { - use super::{ - checked_port_index, control_descriptor_shape_valid, schedule_port_start, - VirtioConsoleControl, - }; use std::mem::size_of; + use std::sync::Arc; + + use utils::eventfd::EventFd; + use vm_memory::{Bytes, GuestAddress, GuestMemoryMmap}; + + use super::*; + use crate::legacy::DummyIrqChip; + use crate::virtio::queue::tests::VirtQueue; + + const QUEUE_SIZE: u16 = 8; + const CONTROL_ADDR: u64 = 0x4000; #[test] fn control_descriptor_requires_one_exact_readable_object() { @@ -438,14 +438,6 @@ mod tests { assert!(!control_descriptor_shape_valid(exact, false, true)); } - #[test] - fn port_index_rejects_unknown_identifiers() { - assert_eq!(checked_port_index(2, 0), Some(0)); - assert_eq!(checked_port_index(2, 1), Some(1)); - assert_eq!(checked_port_index(2, 2), None); - assert_eq!(checked_port_index(2, u32::MAX), None); - } - #[test] fn repeated_or_active_port_start_is_not_scheduled_twice() { let mut pending = Vec::new(); @@ -454,4 +446,72 @@ mod tests { schedule_port_start(&mut pending, 2, true); assert_eq!(pending, vec![1]); } + + fn process_control_command(command: VirtioConsoleControl) -> (Console, u16, u32, u32) { + let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(); + let queues = [0, 0x400, 0x800, 0xc00] + .map(|start| VirtQueue::new(GuestAddress(start), &mem, QUEUE_SIZE)); + + mem.write_obj(command, GuestAddress(CONTROL_ADDR)).unwrap(); + queues[CONTROL_TXQ_INDEX].dtable[0].set( + CONTROL_ADDR, + size_of::() as u32, + 0, + 0, + ); + queues[CONTROL_TXQ_INDEX].avail.ring[0].set(0); + queues[CONTROL_TXQ_INDEX].avail.idx.set(1); + + let device_queues = queues + .iter() + .map(|queue| { + DeviceQueue::new( + queue.create_queue(), + Arc::new(EventFd::new(0).expect("create queue event")), + ) + }) + .collect(); + let interrupt = InterruptTransport::new(DummyIrqChip::new().into(), "test".into()) + .expect("create interrupt transport"); + let mut console = Console::new(vec![PortDescription { + name: "test".into(), + input: None, + output: None, + terminal: None, + }]) + .unwrap(); + console + .activate(mem.clone(), interrupt, device_queues) + .unwrap(); + + assert!(console.process_control_tx()); + assert!(!console.process_control_tx()); + + let used_index = queues[CONTROL_TXQ_INDEX].used.idx.get(); + let used = queues[CONTROL_TXQ_INDEX].used.ring[0].get(); + (console, used_index, used.id, used.len) + } + + #[test] + fn unknown_port_ids_are_completed_without_side_effects() { + for event in [ + control_event::VIRTIO_CONSOLE_PORT_READY, + control_event::VIRTIO_CONSOLE_PORT_OPEN, + ] { + for id in [1, u32::MAX] { + let command = VirtioConsoleControl { + id, + event, + value: 1, + }; + let (console, used_index, used_id, used_len) = process_control_command(command); + + assert_eq!(used_index, 1); + assert_eq!(used_id, 0); + assert_eq!(used_len, 0); + assert!(console.queues.iter().all(Option::is_some)); + assert!(console.control.queue_pop().is_none()); + } + } + } } From 754c4b1ddd45d39659d180a2288d7731ced30fde Mon Sep 17 00:00:00 2001 From: Dylan Steele Date: Tue, 4 Aug 2026 22:39:22 -0400 Subject: [PATCH 2/4] virtio/console: validate control transmit chains The control transmit path currently reads an object from the head address without validating descriptor direction, declared length, chained layout, or guest memory ranges. That can read outside the driver-declared buffer and leave unreadable elements stranded. Validate the complete readable chain, accept scatter/gather layouts totaling exactly one control message, reject writable or malformed chains, and parse through the established descriptor Reader. Return every popped chain once with no device-written bytes. Assisted-by: Codex:gpt-5.6-sol --- src/devices/src/virtio/console/device.rs | 281 ++++++++++++++++++----- 1 file changed, 221 insertions(+), 60 deletions(-) diff --git a/src/devices/src/virtio/console/device.rs b/src/devices/src/virtio/console/device.rs index f20b42d842..a7b99c5add 100644 --- a/src/devices/src/virtio/console/device.rs +++ b/src/devices/src/virtio/console/device.rs @@ -20,13 +20,76 @@ use crate::virtio::console::port::Port; use crate::virtio::console::port_queue_mapping::{ num_queues, port_id_to_queue_idx, QueueDirection, }; -use crate::virtio::{InterruptTransport, PortDescription, VmmExitObserver}; +use crate::virtio::descriptor_utils::Reader; +use crate::virtio::queue::{VIRTQ_DESC_F_NEXT, VIRTQ_DESC_F_WRITE}; +use crate::virtio::{DescriptorChain, InterruptTransport, PortDescription, VmmExitObserver}; pub(crate) const CONTROL_RXQ_INDEX: usize = 2; pub(crate) const CONTROL_TXQ_INDEX: usize = 3; -fn control_descriptor_shape_valid(len: u32, write_only: bool, chained: bool) -> bool { - !write_only && !chained && len as usize == size_of::() +fn read_control_command( + mem: &GuestMemoryMmap, + head: DescriptorChain<'_>, +) -> Option { + let mut descriptor = Some(head.clone()); + let mut descriptor_indices = Vec::new(); + let mut readable_len = 0usize; + + while let Some(current) = descriptor { + if descriptor_indices.contains(¤t.index) { + log::error!("Console control descriptor chain contains a loop"); + return None; + } + descriptor_indices.push(current.index); + + if current.flags & !(VIRTQ_DESC_F_NEXT | VIRTQ_DESC_F_WRITE) != 0 { + log::error!("Console control transmit chain uses unsupported descriptor flags"); + return None; + } + + if current.is_write_only() { + log::error!("Console control transmit chain contains a writable descriptor"); + return None; + } + + readable_len = match readable_len.checked_add(current.len as usize) { + Some(len) => len, + None => { + log::error!("Console control descriptor length overflow"); + return None; + } + }; + + let has_next = current.flags & VIRTQ_DESC_F_NEXT != 0; + descriptor = current.next_descriptor(); + if has_next && descriptor.is_none() { + log::error!("Malformed console control descriptor chain"); + return None; + } + } + + if readable_len != size_of::() { + log::error!( + "Invalid console control payload length: expected {}, got {readable_len}", + size_of::() + ); + return None; + } + + let mut reader = match Reader::new(mem, head) { + Ok(reader) => reader, + Err(e) => { + log::error!("Invalid console control descriptor memory: {e}"); + return None; + } + }; + match reader.read_obj() { + Ok(command) => Some(command), + Err(e) => { + log::error!("Failed to read console control payload: {e}"); + None + } + } } fn schedule_port_start(pending: &mut Vec, port_id: usize, active: bool) { @@ -184,38 +247,14 @@ impl Console { while let Some(head) = control_tx.queue.pop(mem) { raise_irq = true; - if !control_descriptor_shape_valid(head.len, head.is_write_only(), head.has_next()) { - log::warn!( - "Ignoring malformed console control descriptor: len={}, write_only={}, chained={}", - head.len, - head.is_write_only(), - head.has_next(), - ); - if let Err(e) = control_tx.queue.add_used(mem, head.index, 0) { - error!("failed to add rejected control element to the queue: {e:?}"); - } - continue; - } - - let cmd: VirtioConsoleControl = match mem.read_obj(head.addr) { - Ok(cmd) => cmd, - Err(e) => { - log::error!( - "Failed to read VirtioConsoleControl struct: {e:?}, struct len = {len}, head.len = {head_len}", - len = size_of::(), - head_len = head.len, - ); - if let Err(add_error) = control_tx.queue.add_used(mem, head.index, 0) { - error!( - "failed to add unreadable control element to the queue: {add_error:?}" - ); - } - continue; - } - }; - if let Err(e) = control_tx.queue.add_used(mem, head.index, 0) { + let head_index = head.index; + let cmd = read_control_command(mem, head); + if let Err(e) = control_tx.queue.add_used(mem, head_index, 0) { error!("failed to add used elements to the queue: {e:?}"); } + let Some(cmd) = cmd else { + continue; + }; log::trace!("VirtioConsoleControl cmd: {cmd:?}"); match cmd.event { @@ -423,21 +462,13 @@ mod tests { use super::*; use crate::legacy::DummyIrqChip; + use crate::virtio::console::console_control::Payload; use crate::virtio::queue::tests::VirtQueue; + use crate::virtio::queue::{VIRTQ_DESC_F_NEXT, VIRTQ_DESC_F_WRITE}; const QUEUE_SIZE: u16 = 8; const CONTROL_ADDR: u64 = 0x4000; - #[test] - fn control_descriptor_requires_one_exact_readable_object() { - let exact = size_of::() as u32; - assert!(control_descriptor_shape_valid(exact, false, false)); - assert!(!control_descriptor_shape_valid(exact - 1, false, false)); - assert!(!control_descriptor_shape_valid(exact + 1, false, false)); - assert!(!control_descriptor_shape_valid(exact, true, false)); - assert!(!control_descriptor_shape_valid(exact, false, true)); - } - #[test] fn repeated_or_active_port_start_is_not_scheduled_twice() { let mut pending = Vec::new(); @@ -447,18 +478,39 @@ mod tests { assert_eq!(pending, vec![1]); } - fn process_control_command(command: VirtioConsoleControl) -> (Console, u16, u32, u32) { + struct ProcessResult { + console: Console, + raised_irq: bool, + used_index: u16, + used_id: u32, + used_len: u32, + } + + fn process_control_chain( + command: VirtioConsoleControl, + descriptors: &[(u64, u32, u16, u16)], + ) -> ProcessResult { let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(); let queues = [0, 0x400, 0x800, 0xc00] .map(|start| VirtQueue::new(GuestAddress(start), &mem, QUEUE_SIZE)); - mem.write_obj(command, GuestAddress(CONTROL_ADDR)).unwrap(); - queues[CONTROL_TXQ_INDEX].dtable[0].set( - CONTROL_ADDR, - size_of::() as u32, - 0, - 0, - ); + let command_bytes = command.as_slice(); + let mut command_offset = 0; + for (index, &(addr, len, flags, next)) in descriptors.iter().enumerate() { + queues[CONTROL_TXQ_INDEX].dtable[index].set(addr, len, flags, next); + if flags & VIRTQ_DESC_F_WRITE == 0 && command_offset < command_bytes.len() { + let write_len = usize::min(len as usize, command_bytes.len() - command_offset); + if mem + .write( + &command_bytes[command_offset..command_offset + write_len], + GuestAddress(addr), + ) + .is_ok() + { + command_offset += write_len; + } + } + } queues[CONTROL_TXQ_INDEX].avail.ring[0].set(0); queues[CONTROL_TXQ_INDEX].avail.idx.set(1); @@ -484,12 +536,19 @@ mod tests { .activate(mem.clone(), interrupt, device_queues) .unwrap(); - assert!(console.process_control_tx()); + let raised_irq = console.process_control_tx(); + let used_index = queues[CONTROL_TXQ_INDEX].used.idx.get(); assert!(!console.process_control_tx()); + assert_eq!(queues[CONTROL_TXQ_INDEX].used.idx.get(), used_index); - let used_index = queues[CONTROL_TXQ_INDEX].used.idx.get(); let used = queues[CONTROL_TXQ_INDEX].used.ring[0].get(); - (console, used_index, used.id, used.len) + ProcessResult { + console, + raised_irq, + used_index, + used_id: used.id, + used_len: used.len, + } } #[test] @@ -504,14 +563,116 @@ mod tests { event, value: 1, }; - let (console, used_index, used_id, used_len) = process_control_command(command); + let result = process_control_chain( + command, + &[(CONTROL_ADDR, size_of::() as u32, 0, 0)], + ); - assert_eq!(used_index, 1); - assert_eq!(used_id, 0); - assert_eq!(used_len, 0); - assert!(console.queues.iter().all(Option::is_some)); - assert!(console.control.queue_pop().is_none()); + assert!(result.raised_irq); + assert_eq!(result.used_index, 1); + assert_eq!(result.used_id, 0); + assert_eq!(result.used_len, 0); + assert!(result.console.queues.iter().all(Option::is_some)); + assert!(result.console.control.queue_pop().is_none()); } } } + + #[test] + fn valid_control_chains_are_processed() { + let command = VirtioConsoleControl { + id: 0, + event: control_event::VIRTIO_CONSOLE_DEVICE_READY, + value: 1, + }; + let chains = [ + ("contiguous", vec![(CONTROL_ADDR, 8, 0, 0)]), + ( + "split", + vec![ + (CONTROL_ADDR, 3, VIRTQ_DESC_F_NEXT, 1), + (CONTROL_ADDR + 0x100, 5, 0, 0), + ], + ), + ]; + + for (name, chain) in chains { + let result = process_control_chain(command, &chain); + + assert!(result.raised_irq, "{name}"); + assert_eq!(result.used_index, 1, "{name}"); + assert_eq!(result.used_id, 0, "{name}"); + assert_eq!(result.used_len, 0, "{name}"); + let Some(Payload::ConsoleControl(response)) = result.console.control.queue_pop() else { + panic!("expected port-add response"); + }; + let response_id = response.id; + let response_event = response.event; + assert_eq!(response_id, 0); + assert_eq!(response_event, control_event::VIRTIO_CONSOLE_PORT_ADD); + assert!(result.console.control.queue_pop().is_none()); + } + } + + #[test] + fn invalid_control_chains_are_completed_without_side_effects() { + let command = VirtioConsoleControl { + id: 0, + event: control_event::VIRTIO_CONSOLE_PORT_READY, + value: 1, + }; + let chains = [ + ("short", vec![(CONTROL_ADDR, 7, 0, 0)]), + ("trailing payload", vec![(CONTROL_ADDR, 9, 0, 0)]), + ("write only", vec![(CONTROL_ADDR, 8, VIRTQ_DESC_F_WRITE, 0)]), + ( + "mixed direction", + vec![ + (CONTROL_ADDR, 4, VIRTQ_DESC_F_NEXT, 1), + (CONTROL_ADDR + 0x100, 4, VIRTQ_DESC_F_WRITE, 0), + ], + ), + ("invalid first range", vec![(0x10000, 8, 0, 0)]), + ( + "invalid second range", + vec![(CONTROL_ADDR, 4, VIRTQ_DESC_F_NEXT, 1), (0x10000, 4, 0, 0)], + ), + ( + "loop", + vec![ + (CONTROL_ADDR, 4, VIRTQ_DESC_F_NEXT, 1), + (CONTROL_ADDR + 0x100, 4, VIRTQ_DESC_F_NEXT, 0), + ], + ), + ("oversized length", vec![(CONTROL_ADDR, u32::MAX, 0, 0)]), + ("unsupported flags", vec![(CONTROL_ADDR, 8, 4, 0)]), + ]; + + for (name, chain) in chains { + let result = process_control_chain(command, &chain); + + assert!(result.raised_irq, "{name}"); + assert_eq!(result.used_index, 1, "{name}"); + assert_eq!(result.used_id, 0, "{name}"); + assert_eq!(result.used_len, 0, "{name}"); + assert!(result.console.queues.iter().all(Option::is_some), "{name}"); + assert!(result.console.control.queue_pop().is_none(), "{name}"); + } + } + + #[test] + fn malformed_control_head_has_no_side_effects() { + let command = VirtioConsoleControl { + id: 0, + event: control_event::VIRTIO_CONSOLE_PORT_READY, + value: 1, + }; + let result = + process_control_chain(command, &[(CONTROL_ADDR, 8, VIRTQ_DESC_F_NEXT, QUEUE_SIZE)]); + + assert!(!result.raised_irq); + assert_eq!(result.used_index, 0); + assert!(result.console.queues.iter().all(Option::is_some)); + assert!(result.console.control.queue_pop().is_none()); + } } From a10a155810fb57b1c3fd8954b350da59b498a448 Mon Sep 17 00:00:00 2001 From: Dylan Steele Date: Wed, 5 Aug 2026 09:42:36 -0400 Subject: [PATCH 3/4] governance: version governed fork baselines Preserve the original v1.19.4 patch-queue merge under an immutable baseline ref and route later governed updates through fresh versioned targets. Treat fork main as upstream-oriented integration state and require separate backports for Capsule adoption. Update workflow routing, corpus counts, and restoration mutations for the console control validation backport. Assisted-by: Codex:gpt-5.6-sol --- .github/workflows/capsule-governed.yml | 3 ++- .github/workflows/code-quality.yml | 12 ++++++------ .github/workflows/formatting.yml | 6 +++--- .github/workflows/integration_tests.yml | 4 ++-- governance/capsule-v1.19.4/PATCH_QUEUE.json | 11 ++++++----- governance/capsule-v1.19.4/README.md | 12 +++++++----- .../restore-malformed-control-acceptance.patch | 8 +++----- .../mutations/restore-unchecked-port-id.patch | 14 ++++++-------- .../capsule-v1.19.4/scripts/verify-library.sh | 8 ++++---- .../capsule-v1.19.4/scripts/verify-mutations.sh | 4 ++-- .../capsule-v1.19.4/scripts/verify-patch-queue.sh | 4 ++-- 11 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.github/workflows/capsule-governed.yml b/.github/workflows/capsule-governed.yml index c9971a8403..e796d12a3b 100644 --- a/.github/workflows/capsule-governed.yml +++ b/.github/workflows/capsule-governed.yml @@ -3,7 +3,7 @@ name: Capsule governed libkrun v1.19.4 on: pull_request: branches: - - capsule/upstream-v1.19.4 + - "capsule/upstream-v1.19.4*" paths: - .github/CODEOWNERS - .github/workflows/capsule-governed.yml @@ -20,6 +20,7 @@ on: branches: - codex/governed-capsule-v1.19.4 - codex/governed-console-fd-coverage-v1.19.4 + - codex/governed-console-control-validation-v1.19.4 paths: - .github/CODEOWNERS - .github/workflows/capsule-governed.yml diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 5be2405f94..1a90c46241 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -6,7 +6,7 @@ jobs: name: libkrun (Linux x86_64) runs-on: ubuntu-26.04 env: - CAPSULE_DEPRECATED_LEVEL: ${{ github.base_ref == 'capsule/upstream-v1.19.4' && 'A' || 'D' }} + CAPSULE_DEPRECATED_LEVEL: ${{ startsWith(github.base_ref, 'capsule/upstream-v1.19.4') && 'A' || 'D' }} steps: - uses: actions/checkout@v4 @@ -14,7 +14,7 @@ jobs: uses: ./.github/actions/setup-build-env - name: Pin governed Clippy toolchain - if: github.base_ref == 'capsule/upstream-v1.19.4' + if: ${{ startsWith(github.base_ref, 'capsule/upstream-v1.19.4') }} run: | rustup toolchain install 1.93.1 --profile minimal --component clippy rustup default 1.93.1 @@ -38,7 +38,7 @@ jobs: name: libkrun (Linux aarch64) runs-on: ubuntu-26.04-arm env: - CAPSULE_DEPRECATED_LEVEL: ${{ github.base_ref == 'capsule/upstream-v1.19.4' && 'A' || 'D' }} + CAPSULE_DEPRECATED_LEVEL: ${{ startsWith(github.base_ref, 'capsule/upstream-v1.19.4') && 'A' || 'D' }} steps: - uses: actions/checkout@v4 @@ -46,7 +46,7 @@ jobs: uses: ./.github/actions/setup-build-env - name: Pin governed Clippy toolchain - if: github.base_ref == 'capsule/upstream-v1.19.4' + if: ${{ startsWith(github.base_ref, 'capsule/upstream-v1.19.4') }} run: | rustup toolchain install 1.93.1 --profile minimal --component clippy rustup default 1.93.1 @@ -64,7 +64,7 @@ jobs: name: libkrun (macOS aarch64) runs-on: macos-latest env: - CAPSULE_DEPRECATED_LEVEL: ${{ github.base_ref == 'capsule/upstream-v1.19.4' && 'A' || 'D' }} + CAPSULE_DEPRECATED_LEVEL: ${{ startsWith(github.base_ref, 'capsule/upstream-v1.19.4') && 'A' || 'D' }} steps: - uses: actions/checkout@v4 @@ -72,7 +72,7 @@ jobs: uses: ./.github/actions/setup-build-env - name: Pin governed Clippy toolchain - if: github.base_ref == 'capsule/upstream-v1.19.4' + if: ${{ startsWith(github.base_ref, 'capsule/upstream-v1.19.4') }} run: | rustup toolchain install 1.93.1 --profile minimal --component clippy rustup default 1.93.1 diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 650fa1a64d..880a502b7b 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -15,15 +15,15 @@ jobs: run: find init -iname '*.h' -o -iname '*.c' | xargs clang-format -n -Werror - name: Install governed formatting toolchain - if: github.base_ref == 'capsule/upstream-v1.19.4' + if: ${{ startsWith(github.base_ref, 'capsule/upstream-v1.19.4') }} run: rustup toolchain install 1.97.1 --profile minimal --component rustfmt - name: Rust code formatting (governed profile) - if: github.base_ref == 'capsule/upstream-v1.19.4' + if: ${{ startsWith(github.base_ref, 'capsule/upstream-v1.19.4') }} run: governance/capsule-v1.19.4/scripts/verify-cargo-fmt.sh - name: Rust code formatting - if: github.base_ref != 'capsule/upstream-v1.19.4' + if: ${{ !startsWith(github.base_ref, 'capsule/upstream-v1.19.4') }} run: cargo fmt -- --check - name: Rust code formatting (examples) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 20a4fe2f88..37312d2568 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -4,7 +4,7 @@ on: [pull_request] jobs: integration-tests-x86_64: name: Integration Tests (Linux x86_64) - if: github.base_ref != 'capsule/upstream-v1.19.4' + if: ${{ !startsWith(github.base_ref, 'capsule/upstream-v1.19.4') }} runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 @@ -74,7 +74,7 @@ jobs: integration-tests-aarch64: name: Integration Tests (Linux aarch64) - if: github.base_ref != 'capsule/upstream-v1.19.4' + if: ${{ !startsWith(github.base_ref, 'capsule/upstream-v1.19.4') }} runs-on: self-hosted steps: - uses: actions/checkout@v4 diff --git a/governance/capsule-v1.19.4/PATCH_QUEUE.json b/governance/capsule-v1.19.4/PATCH_QUEUE.json index 67f6397152..4e1d526d13 100644 --- a/governance/capsule-v1.19.4/PATCH_QUEUE.json +++ b/governance/capsule-v1.19.4/PATCH_QUEUE.json @@ -8,10 +8,11 @@ "commit": "728df8125077d0db44265f6e997c72b81b65c015" }, "fork": { - "repository": "https://github.com/dills122/libkrun", - "baselineBranch": "capsule/upstream-v1.19.4", + "repository": "https://github.com/Shrimpworks/libkrun", + "baselineBranch": "capsule/baseline-v1.19.4-r1", "baselineCommit": "4ea8d1de861ed1c0636fc800b6da8fb71a086aa5", - "workBranch": "codex/governed-console-fd-coverage-v1.19.4" + "historicalAcceptedHead": "cf0333cdba478cc34a8570a65b38412da7fd3ecc", + "candidateBranch": "capsule/upstream-v1.19.4-r3" }, "capsuleEvidence": { "repository": "https://github.com/dills122/capsule-corp", @@ -57,9 +58,9 @@ ], "restorationMutations": [ { "file": "mutations/restore-duplicate-start.patch", "sha256": "07dfafaf9008d8a0ec588fae398fa6363a3a2575ceb1f6efe36c3bb8344f412d" }, - { "file": "mutations/restore-malformed-control-acceptance.patch", "sha256": "197ec5b3d5e0b81728a841bfbdd2b38b0f895a98899f6c535694dfcde8434d8d" }, + { "file": "mutations/restore-malformed-control-acceptance.patch", "sha256": "b9f488b862f695d04406e7259e86cd461ae6be60f3b513cc52856621c86a6545" }, { "file": "mutations/restore-stop-blind-output-wait.patch", "sha256": "59d63e70f74586c9c418718cb6b6ec16c1a63b8f23d82501edb38b46e33f44e6" }, - { "file": "mutations/restore-unchecked-port-id.patch", "sha256": "869e3b4c1959b2c8f7fa8eac919d82ed10ac932288125a15474d111cd7e44221" } + { "file": "mutations/restore-unchecked-port-id.patch", "sha256": "d390f65363b965b95036f18908fb7359edf73e6b1cdee66718717a5083cccf2c" } ], "governedSourcePaths": [ "include/libkrun.h", diff --git a/governance/capsule-v1.19.4/README.md b/governance/capsule-v1.19.4/README.md index 04e7fb921f..8f55389275 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 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 directory governs one narrowly scoped downstream patch queue over the immutable upstream libkrun v1.19.4 commit `728df8125077d0db44265f6e997c72b81b65c015`. The queue was merged as `4ea8d1de861ed1c0636fc800b6da8fb71a086aa5` and is preserved by the locked `capsule/baseline-v1.19.4-r1` ref. The historical `capsule/upstream-v1.19.4` line ended at coverage follow-up merge `cf0333cdba478cc34a8570a65b38412da7fd3ecc` and is also locked. Later governed updates use a fresh versioned target branch based on the preceding accepted head. 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,11 +18,13 @@ The first two patches are prerequisites. They remain independently hashed and ar ## Review and branch policy -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. +The upstream anchor, retained patch-queue merge, and every accepted governed head are immutable. `capsule/baseline-v1.19.4-r1` must remain at the exact original governed merge and must never be rebased, force-pushed, or advanced. Each update starts a fresh versioned target branch from the preceding accepted head; after acceptance that target is locked and may become the fork default. Patch reconstruction always starts from the upstream anchor and compares the retained queue to the original governed merge, so reviewed follow-up changes cannot rewrite its provenance. + +The fork's `main` branch is upstream-oriented integration state, not Capsule product state. A change merged only into `main` is unadopted by Capsule. Applicable fixes must be backported as logical commits through a separate governed pull request; never merge `main` wholesale into a governed line. Every pull request must name and read back its base and head explicitly. Changes to this line require: -- a draft pull request targeting the exact versioned baseline; +- a draft pull request targeting a fresh versioned branch created from the preceding accepted head; - CODEOWNER review by `@dills122` and an independent human review before merge; - DCO sign-off and the repository's required assistance trailer on every commit; - exact patch reconstruction plus all governed checks in `scripts/verify-governed.sh`; @@ -33,11 +35,11 @@ A green workflow is necessary but not sufficient for merge. The PR stays draft w ## CI routing -`.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. +`.github/workflows/capsule-governed.yml` runs only for versioned governed work branches, pull requests targeting a `capsule/upstream-v1.19.4*` branch, 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 upstream integration workflow is precisely routed away from pull requests whose base starts with `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 55-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. diff --git a/governance/capsule-v1.19.4/mutations/restore-malformed-control-acceptance.patch b/governance/capsule-v1.19.4/mutations/restore-malformed-control-acceptance.patch index 03e8a9b7e0..f55a8f7299 100644 --- a/governance/capsule-v1.19.4/mutations/restore-malformed-control-acceptance.patch +++ b/governance/capsule-v1.19.4/mutations/restore-malformed-control-acceptance.patch @@ -1,8 +1,6 @@ diff --git a/src/devices/src/virtio/console/device.rs b/src/devices/src/virtio/console/device.rs --- a/src/devices/src/virtio/console/device.rs +++ b/src/devices/src/virtio/console/device.rs -@@ -28,3 +28,3 @@ fn control_descriptor_shape_valid(len: u32, write_only: bool, chained: bool) -> bool { - fn control_descriptor_shape_valid(len: u32, write_only: bool, chained: bool) -> bool { -- !write_only && !chained && len as usize == size_of::() -+ let _ = (len, write_only, chained); true - } +@@ -71 +71 @@ fn read_control_command( +- if readable_len != size_of::() { ++ if false && readable_len != size_of::() { diff --git a/governance/capsule-v1.19.4/mutations/restore-unchecked-port-id.patch b/governance/capsule-v1.19.4/mutations/restore-unchecked-port-id.patch index ef2101d64c..f5b94c1685 100644 --- a/governance/capsule-v1.19.4/mutations/restore-unchecked-port-id.patch +++ b/governance/capsule-v1.19.4/mutations/restore-unchecked-port-id.patch @@ -1,11 +1,9 @@ diff --git a/src/devices/src/virtio/console/device.rs b/src/devices/src/virtio/console/device.rs --- a/src/devices/src/virtio/console/device.rs +++ b/src/devices/src/virtio/console/device.rs -@@ -32,5 +32,4 @@ fn checked_port_index(port_count: usize, port_id: u32) -> Option { - fn checked_port_index(port_count: usize, port_id: u32) -> Option { -- usize::try_from(port_id) -- .ok() -- .filter(|port_id| *port_id < port_count) -+ let _ = port_count; -+ usize::try_from(port_id).ok() - } +@@ -271,4 +271 @@ impl Console { +- let Some(port) = self.ports.get(cmd.id as usize) else { +- log::warn!("Guest reported unknown console port {} ready", cmd.id); +- continue; +- }; ++ let port = &self.ports[cmd.id as usize]; diff --git a/governance/capsule-v1.19.4/scripts/verify-library.sh b/governance/capsule-v1.19.4/scripts/verify-library.sh index c521689823..b75fcd7665 100755 --- a/governance/capsule-v1.19.4/scripts/verify-library.sh +++ b/governance/capsule-v1.19.4/scripts/verify-library.sh @@ -37,7 +37,7 @@ default_log="$task_tmp/default-tests.log" CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$target_dir/tests" \ cargo test --locked --offline -p krun-devices --lib ) | tee "$default_log" -grep -Eq 'test result: ok\. 51 passed; 0 failed' "$default_log" +grep -Eq 'test result: ok\. 53 passed; 0 failed' "$default_log" bounded_console_log="$task_tmp/bounded-console-tests.log" ( @@ -55,7 +55,7 @@ blk_log="$task_tmp/blk-tests.log" cargo test --locked --offline -p krun-devices --lib --features blk -- \ --test-threads=1 ) | tee "$blk_log" -grep -Eq 'test result: ok\. 53 passed; 0 failed' "$blk_log" +grep -Eq 'test result: ok\. 55 passed; 0 failed' "$blk_log" raw_fd_log="$task_tmp/raw-fd-tests.log" ( @@ -145,9 +145,9 @@ fi printf 'governedConsoleRustfmt=PASS\n' printf 'cargoFmt=PASS_EXACT_RETAINED_DRIFT_ONLY\n' printf 'cargoCheck=PASS\n' -printf 'consoleCorpusTests=51\n' +printf 'consoleCorpusTests=53\n' printf 'boundedConsoleCoverageTests=4\n' -printf 'blockFeatureTests=53\n' +printf 'blockFeatureTests=55\n' printf 'rawFdContractTests=2\n' printf 'rawFdLibraryBoundaryTests=2\n' printf 'clippyWarningsDenied=PASS\n' diff --git a/governance/capsule-v1.19.4/scripts/verify-mutations.sh b/governance/capsule-v1.19.4/scripts/verify-mutations.sh index 917df48984..3acdac7927 100755 --- a/governance/capsule-v1.19.4/scripts/verify-mutations.sh +++ b/governance/capsule-v1.19.4/scripts/verify-mutations.sh @@ -82,9 +82,9 @@ run_console_mutation() { } run_console_mutation restore-malformed-control-acceptance \ - virtio::console::device::tests::control_descriptor_requires_one_exact_readable_object + virtio::console::device::tests::invalid_control_chains_are_completed_without_side_effects run_console_mutation restore-unchecked-port-id \ - virtio::console::device::tests::port_index_rejects_unknown_identifiers + virtio::console::device::tests::unknown_port_ids_are_completed_without_side_effects run_console_mutation restore-duplicate-start \ virtio::console::device::tests::repeated_or_active_port_start_is_not_scheduled_twice run_console_mutation restore-stop-blind-output-wait \ 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 ccf4f2ecb6..c897c9a7f5 100755 --- a/governance/capsule-v1.19.4/scripts/verify-patch-queue.sh +++ b/governance/capsule-v1.19.4/scripts/verify-patch-queue.sh @@ -37,9 +37,9 @@ expected_hash_for() { 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=$(git -C "$repo_dir" rev-parse --verify refs/heads/capsule/baseline-v1.19.4-r1 2>/dev/null || git -C "$repo_dir" rev-parse --verify refs/remotes/origin/capsule/baseline-v1.19.4-r1) [ "$actual_base" = "$governed_base_commit" ] || { - printf 'governed baseline branch moved: got %s, want %s\n' "$actual_base" "$governed_base_commit" >&2 + printf 'governed immutable baseline moved: got %s, want %s\n' "$actual_base" "$governed_base_commit" >&2 exit 1 } From 445df8823a9aa46f7121db8a24a4deac530989aa Mon Sep 17 00:00:00 2001 From: Dylan Steele Date: Wed, 5 Aug 2026 11:44:23 -0400 Subject: [PATCH 4/4] governance: align sole-maintainer review policy Assisted-by: Codex:gpt-5.6-sol --- .github/CODEOWNERS | 3 ++- governance/capsule-v1.19.4/README.md | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4cdbcd99e6..633fb58a34 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,5 @@ -# The governed Capsule patch line requires review by the fork owner. +# This records governed ownership; sole-maintainer branch protection does not +# require CODEOWNER approval. * @dills122 /governance/capsule-v1.19.4/ @dills122 diff --git a/governance/capsule-v1.19.4/README.md b/governance/capsule-v1.19.4/README.md index 8f55389275..aca98406ca 100644 --- a/governance/capsule-v1.19.4/README.md +++ b/governance/capsule-v1.19.4/README.md @@ -25,8 +25,9 @@ The fork's `main` branch is upstream-oriented integration state, not Capsule pro Changes to this line require: - a draft pull request targeting a fresh versioned branch created from the preceding accepted head; -- CODEOWNER review by `@dills122` and an independent human review before merge; -- DCO sign-off and the repository's required assistance trailer on every commit; +- maintainer self-review with green required checks, resolved conversations, and exact evidence and settings readback; +- zero GitHub-required approving reviews, no most-recent-push approval, and no required CODEOWNER approval while `@dills122` is the only qualified maintainer; external approval enforcement may be enabled when a second qualified maintainer is available; +- separate human acceptance of DCO responsibility and the repository's required assistance trailer on every commit; automation must not add a human `Signed-off-by` trailer; - exact patch reconstruction plus all governed checks in `scripts/verify-governed.sh`; - explicit resolution of the blockers below; and - no force-push after review begins unless reviewers are told exactly what changed. @@ -60,6 +61,6 @@ The compile-only C header contract treats the pre-existing `/dev/input/*` text i - 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. -- Independent human/CODEOWNER review remains required. +- Zero GitHub approval enforcement does not satisfy or waive later independent product-admission review, DCO acceptance, or final upstream-submission authorization. Security reports for upstream behavior should follow the private contact documented by upstream. Capsule-specific review must not disclose credentials, proprietary user data, or third-party targets.