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
30 changes: 30 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ members = [
"lib/untrusted",
"lib/update",
"lib/userbound",
"lib/vdagent",
"lib/virtio",
"lib/wallclock",
"lib/xhci",
Expand Down Expand Up @@ -126,6 +127,8 @@ members = [
"system/update",
"system/update/copies",
"system/usbd",
"system/vdagent",
"system/vdagent/bridge",
"tests/guest/kernelprobe",
"tests/guest/metalprobe",
"tests/guest/test-runner",
Expand Down Expand Up @@ -216,6 +219,9 @@ toyos-osrelease = { path = "sdk/osrelease" }
# The swap port's name and the one program the image may give it to, so the
# gate on every manifest reads what the supervisor reads.
toyos-swap = { path = "lib/swap" }
# QEMU's clipboard channel's port name, id and port count, so the launch reads
# the declaration the guest's agent drives.
toyos-vdagent = { path = "lib/vdagent" }
# The watchdog's parameter name, so the gate over `kernel/src/params.rs` reads
# the same constant the kernel and the bootloader do.
toyos-tco = { path = "lib/tco" }
Expand Down
11 changes: 9 additions & 2 deletions build/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,12 +596,14 @@ const SUPERVISOR_SERVED: &[&str] = &[toyos_swap::PORT, "power", toyos_manifest::

/// The ports that one row alone may receive, each with that row: the swap
/// port, whose holder replaces any service's binary, the grants port, whose
/// holder writes which folder of the home a package sees, and the prompt
/// port, whose holder draws above every window and has every key while it does.
/// holder writes which folder of the home a package sees, the prompt port,
/// whose holder draws above every window and has every key while it does, and
/// the clipboard port, whose holder is sent every copy made in the session.
const HELD_ALONE: &[(&str, &str)] = &[
(toyos_swap::PORT, toyos_swap::HOLDER),
(toyos_manifest::grants::PORT, toyos_manifest::grants::HOLDER),
(toyos_manifest::consent::PROMPT, toyos_manifest::consent::SERVER),
(toyos_manifest::clipboard::PORT, toyos_manifest::clipboard::HOLDER),
];

/// Who may hold the authorities that change what the machine runs or what a
Expand Down Expand Up @@ -3012,6 +3014,7 @@ mod tests {
"images/diag.toml",
"images/console.toml",
"tests/images/acpicase.toml",
"tests/images/clipboardcase.toml",
"tests/images/hostedclangcase.toml",
"tests/images/jobcase.toml",
"tests/images/latencycase.toml",
Expand Down Expand Up @@ -3098,6 +3101,10 @@ mod tests {
assert!(held_by_their_holders_alone(&shell).is_err());
let apps: SystemConfig = toml::from_str("[apps]\nreceives = [\"grants\"]\n").unwrap();
assert!(held_by_their_holders_alone(&apps).is_err());
let vdagent: SystemConfig = toml::from_str("[programs.vdagent]\nreceives = [\"clipboard\"]\n").unwrap();
assert!(held_by_their_holders_alone(&vdagent).is_ok());
let editor: SystemConfig = toml::from_str("[programs.editor]\nreceives = [\"clipboard\"]\n").unwrap();
assert!(held_by_their_holders_alone(&editor).is_err());
}

/// The prompt port reaches its server alone, and the consent port no row,
Expand Down
1 change: 1 addition & 0 deletions build/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub mod sysroot;
pub mod tether;
pub mod toolchain;
pub mod userlandhost;
pub mod vdagent;
pub mod wallpaper;

use std::path::{Path, PathBuf};
Expand Down
29 changes: 28 additions & 1 deletion build/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ pub enum Profile {
struct Shape {
/// The display device. `Virtio` is the only profile with no firmware GOP.
virtio_gpu: bool,
/// virtio-net, virtio-sound, and the console on virtio-serial.
/// virtio-net, virtio-sound, the console on virtio-serial, and the
/// clipboard's channel on a virtio-serial function of its own.
virtio: bool,
/// A USB HID on the xHCI. The T14's keyboard is PS/2 and its touchpad
/// I2C-HID; it has no USB HID at all.
Expand Down Expand Up @@ -127,6 +128,17 @@ pub struct Options {
pub fn launch(opts: &Options) {
let shape = opts.profile.shape();
let arch = opts.arch;
// Before anything launches: every virtio machine carries the clipboard's
// channel, and every host's display is the one with its clipboard.
let display = display();
let wanted = [
shape.virtio.then(|| toyos_build::vdagent::available(arch)),
(display != "cocoa").then(|| toyos_build::vdagent::gtk(arch)),
];
if let Some(refused) = wanted.into_iter().flatten().find_map(Result::err) {
eprintln!("Error: {refused}");
std::process::exit(1);
}
let mut qemu = Command::new(arch.qemu());

// Without this QEMU runs its default-device pass whenever no network
Expand Down Expand Up @@ -155,6 +167,7 @@ pub fn launch(opts: &Options) {
if let Some(boot) = arch.boot() {
qemu.arg("-boot").arg(boot);
}
qemu.arg("-display").arg(display);
qemu.arg("-machine")
.arg(machine(arch, shape.iommu))
.arg("-smp")
Expand Down Expand Up @@ -243,6 +256,10 @@ pub fn launch(opts: &Options) {
qemu.arg("-device")
.arg(format!("virtio-sound-pci,audiodev=audio0,streams=1{platform}"));

// The clipboard's channel, just before the console's function, so it
// enumerates first and the kernel's console rule runs.
qemu.args(toyos_build::vdagent::args(platform));

// Console wiring: virtio-console on stdio is the primary I/O channel
// (the kernel switches to it once virtio-console init completes —
// see drivers/virtio_console.rs). UART stays on a file so early-boot
Expand Down Expand Up @@ -340,6 +357,16 @@ fn iommu_device(arch: Arch) -> Option<&'static str> {
}
}

/// The display, with the host's clipboard: QEMU's own on macOS, and GTK's,
/// whose clipboard QEMU leaves off unless asked, everywhere else.
fn display() -> &'static str {
if cfg!(target_os = "macos") {
"cocoa"
} else {
"gtk,clipboard=on"
}
}

fn audio_backend() -> &'static str {
if cfg!(target_os = "macos") {
"coreaudio"
Expand Down
83 changes: 83 additions & 0 deletions build/vdagent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! QEMU's clipboard channel at the launch: its `qemu-vdagent` chardev, the
//! virtio-serial function of its own it is on, and the agent's port, from the
//! one declaration the guest's agent reads (`toyos-vdagent`).
//!
//! **A function of its own**, created just before the console's: the console's
//! function is the kernel's log channel from early boot, and the agent's
//! enumerates first, so every virtio boot runs the kernel's choice of console
//! (`toyos_pci::console`).
//!
//! **The chardev exists only in a QEMU built with spice-protocol** (QEMU's
//! `qapi/char.json`, `'if': 'CONFIG_SPICE_PROTOCOL'`), so a launch asks QEMU
//! for it first and refuses by name; nothing falls back.

use std::process::Command;
use std::sync::OnceLock;

use toyos_vdagent::{MAX_PORTS, PORT_ID, PORT_NAME};

use crate::arch::Arch;

/// The arguments that give a guest the channel. `platform` is the virtio
/// function's `iommu_platform` property, as every other virtio function on the
/// machine is given it.
pub fn args(platform: &str) -> [String; 6] {
[
"-chardev".into(),
"qemu-vdagent,id=vdagent0,clipboard=on,mouse=off".into(),
"-device".into(),
format!("virtio-serial-pci-non-transitional,id=vdagent-serial,max_ports={MAX_PORTS}{platform}"),
"-device".into(),
format!("virtserialport,bus=vdagent-serial.0,nr={PORT_ID},chardev=vdagent0,name={PORT_NAME}"),
]
}

/// Whether `arch`'s QEMU has the chardev, asked once per process.
pub fn available(arch: Arch) -> Result<(), String> {
static ASKED: [OnceLock<Result<(), String>>; 2] = [OnceLock::new(), OnceLock::new()];
let slot = &ASKED[Arch::ALL.iter().position(|a| *a == arch).expect("every Arch is in ALL")];
slot.get_or_init(|| {
let listed = help(arch, "-chardev")?;
named(&listed, "qemu-vdagent").then_some(()).ok_or_else(|| {
format!(
"{} has no `qemu-vdagent` chardev: it was built without spice-protocol, and every \
virtio machine carries the shared clipboard's channel",
arch.qemu()
)
})
})
.clone()
}

/// Whether `arch`'s QEMU has the GTK display, which `cargo run` takes on a
/// host other than macOS for its clipboard.
pub fn gtk(arch: Arch) -> Result<(), String> {
let listed = help(arch, "-display")?;
named(&listed, "gtk").then_some(()).ok_or_else(|| {
format!("{} has no GTK display; Debian ships it as `qemu-system-gui`", arch.qemu())
})
}

/// What `<qemu> -machine none <option> help` lists. The machine is named
/// because `qemu-system-aarch64` has no default one and lists nothing without
/// it; `none` is the empty machine on both.
fn help(arch: Arch, option: &str) -> Result<String, String> {
let out = Command::new(arch.qemu())
.args(["-machine", "none", option, "help"])
.output()
.map_err(|e| format!("{} -machine none {option} help: {e}", arch.qemu()))?;
if !out.status.success() {
return Err(format!(
"{} -machine none {option} help: {}: {}",
arch.qemu(),
out.status,
String::from_utf8_lossy(&out.stderr)
));
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}

/// Whether a help listing names `backend` on a line of its own.
fn named(listed: &str, backend: &str) -> bool {
listed.lines().any(|line| line.trim() == backend)
}
16 changes: 13 additions & 3 deletions images/system.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ assets = ["assets"]
# while the compositor had no way to ask for a program's *declared* authority —
# the launcher gives it one, and starting a service from `[boot] start` is not
# the thing that needed fixing.
start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "netstack", "filepicker", "acpiserver"]
start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "netstack", "filepicker", "acpiserver", "vdagent"]

# What every program launched out of `/apps` holds, whatever its own directory
# says. A package is unpacked by `/system/bin/pkg` into a writable directory, so
Expand Down Expand Up @@ -69,10 +69,11 @@ receives = ["power"]
# makes the local desktop a login session, so `swap` and `update` run from its
# shell. `prompt` is the layer above every window, which `filepicker` alone
# reaches: serving it makes the sessions its launches open the screen's, whose
# packages may ask the person at it for a folder.
# packages may ask the person at it for a folder. `clipboard` is sent every copy
# made in the session, and `vdagent` alone receives it.
[programs.compositor]
service = true
serves = ["compositor", "prompt"]
serves = ["compositor", "prompt", "clipboard"]
receives = ["soundserver", "filepicker"]
devices = ["framebuffer", "keyboard", "mouse"]
starts = ["terminal", "files", "/apps"]
Expand All @@ -93,6 +94,15 @@ service = true
serves = ["netstack"]
devices = ["pci:1af4:1041"]

# The shared clipboard with QEMU's host: the virtio-serial function its
# vdagent chardev is on, claimed by vendor and device among the functions the
# kernel does not drive (the kernel's console is `1af4:1043` too), and the
# compositor's clipboard port. A machine with no such function, the T14 among
# them, leaves it to say so and exit.
[programs.vdagent]
devices = ["pci:1af4:1043"]
receives = ["clipboard"]

# Every launch it makes is in a login session, the one `update` and `swap`
# below start in: one it opens when it runs in the machine's session, and the
# one it was launched in otherwise, as when a desktop shell starts it.
Expand Down
26 changes: 26 additions & 0 deletions issues/a-cargo-run-boot-claims-every-pcidev-slot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
status: assigned
kind: defect
opened: 2026-10-13
---

# A `cargo run` boot claims every one of pcidev's slots

Held by the orchestrator.

`images/system.toml`'s rows claim four PCI functions on `cargo run`'s q35:
virtio-sound (`soundserver`), virtio-net (`netstack`), the NVMe controller
(`diskserver`) and the clipboard's virtio-serial function (`vdagent`). That is
every one of `pcidev::MAX_FUNCTIONS` (`kernel/src/pcidev/mod.rs`), so a fifth
claim on that machine is refused `Exhausted`.

None of them goes back: a virtio function on q35's root bus publishes no power
management capability and no PCI Express capability, so it has no
function-level reset, and an ended holder leaves its slot as that function's
residue (`kernel/pci/src/slot.rs`).

More slots reach both architectures' interrupt tables, which declare one
interrupt per slot (`kernel/src/arch/x86_64/idt/user_dev.rs`,
`kernel/src/arch/aarch64/irqchip/its.rs`).

**Exit condition.** A `cargo run` boot leaves a claim slot free.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
status: assigned
kind: defect
opened: 2026-10-13
---

# A declined console function keeps a kernel mapping of its firmware BAR

Held by the orchestrator.

The kernel's console reads every virtio console function's device features
before it picks one (`kernel/src/drivers/virtio_console.rs`,
`VirtioDevice::offer` in `kernel/src/drivers/virtio.rs`). That read maps the
function's capability windows through `map_mmio`, at the BAR addresses
firmware assigned. A function the console declines is reset and left to a
claim, and its windows stay mapped in the kernel's direct map and listed in
x86_64's driven windows (`kernel/src/arch/x86_64/paging.rs`), which nothing
removes. `pcidev` then moves that BAR when a process claims the function, so
the kernel keeps a mapping of an address range the function no longer
decodes. Nothing reads through it today.

Virtio 1.2 §4.1.4.9's PCI configuration access capability reaches a BAR's
registers through configuration space with nothing mapped.

**Exit condition.** A function the console declines leaves no kernel mapping
and no driven window behind.
Loading