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
4 changes: 4 additions & 0 deletions build/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ struct ProgramConfig {
/// PCI matchers: one process of this row per function one names
/// (`toyos_manifest::Program::drives`).
drives: Vec<String>,
/// Firmware windows each function's claim is granted
/// (`toyos_manifest::Program::firmware`).
firmware: Vec<String>,
/// Rights on the `SysCap` duplicate the supervisor endows this program, by the names
/// `toyos_manifest::syscap_rights` takes. A handful of rows in the whole
/// tree declare one.
Expand Down Expand Up @@ -883,6 +886,7 @@ fn render_manifest(config: &SystemConfig) -> Vec<u8> {
receives: cfg.receives.clone(),
devices: cfg.devices.clone(),
drives: cfg.drives.clone(),
firmware: cfg.firmware.clone(),
syscap: cfg.syscap.clone(),
slots: cfg.slots,
authorize: cfg.authorize.clone(),
Expand Down
4 changes: 4 additions & 0 deletions images/system.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,13 @@ syscap = ["rt"]
# the compositor's presents on `display-output`. Not started again: the
# claim's release resets the function, which ends the firmware's scan-out
# (`issues/each-gpu-drives-its-displays-from-a-userland-driver.md`).
# `firmware` grants its claim the IGD OpRegion, the firmware memory its VBT
# and backlight mailbox live in: a privilege beyond the claim, which only this
# signed image's rows can name.
[programs.tgl-display]
drives = ["pci:03:00:00/8086:9a49"]
receives = ["display-output"]
firmware = ["igd-opregion"]

# The network stack, over every link a NIC driver brings on `link`; it claims
# no device. It serves from the start, links or none.
Expand Down
52 changes: 29 additions & 23 deletions issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,39 @@ opened: 2026-10-08

# A BAR's mapping outlives the claim it was asked on

`SYS_DEVICE_BAR_MAP` answers a `SharedMem` handle over a claimed function's
register window, carrying `MAP`, `DUP` and `TRANSFER` like any other
(`ops::initial_rights`). The object's life is its handles' and nothing ties it
to the claim: `pcidev::tear_down` (`kernel/src/pcidev/mod.rs`) stops the
function, resets it and takes its grants back, and touches no mapping of its
BARs. A window belongs to its BAR for the boot (`Machine::windows`), so the
next claim on the function is given the same address. The claim holds no
reference to an object `pcidev::bar_object` made for it, so the exit below
needs the claim to learn what it handed out.
Since #1137, a claim records every aperture it hands out: each BAR window
`pcidev::bar_object` makes, and each firmware window. Each is taken back from
every space that maps it, through the revoke an aperture's last handle
already does, so the old process's next access faults:
- at the claim's last handle (its release, its holder's death), through
`pcidev::revoke_handed`;
- when the claim moves, by IPC or a spawn's endowment. The move takes what
the claim handed out under the lock that moves it, so the new holder's
fresh mappings are not taken, and revokes it once that lock is let go.

So a process that asked for a BAR, kept that handle — or sent it on — and let
the claim go still reads and writes the registers of a function its next
holder drives: it can aim that function's queues at any address the new
holder's grants map, which is the new holder's memory read out and written
through a device neither the unit nor the domain refuses.
An aperture's handle is installed only while the caller's table still holds
the claim, under that same lock, so one a sibling thread asks for across a
release or a move is refused `Gone`.

**Read from the code, not run.** A guest arm is the one `bar_map_again`
(`tests/toyos.rs`) boots: claim the virtio NIC, map a BAR, close the claim,
claim again from a second process, and read the first mapping.
**What is left:**
- **A function's departure.** #934 adds a function leaving (Gone). It calls
`pcidev::revoke_handed(number)` from its refresh loop, never from the
hot-plug interrupt and never with `MACHINE` held. Until then, a mapping of
a function that departs while its claim is held outlives the departure.
The next device behind that port may then be given the same BAR addresses.
- **What no guest test reads.** `bar_map_again` (`tests/toyos.rs`) reads a
BAR handle kept past an IPC move and past the claim's end mapping `Gone`,
and the new holder's fresh mapping reading the device. Still unread: a
live mapping faulting on its next access after a release or a move, a
spawn's endowment, and the races the install check closes.

The tracker has ruled on taking a mapping back from a running process
The tracker's ruling on taking a mapping back from a running process
(`issues/a-moved-handle-is-always-re-movable.md`, "Revocation is the other
answer"): forced reclaim is killing the holder. What that ruling means for a
window whose authority was a claim that has ended is not decided there.
answer") is that forced reclaim is killing the holder. A revoked mapping is
that: the old holder's next access to it faults.

**Exit condition**: after a claim ends, no mapping made through it reaches
the function's registers once another claim holds the function, and a guest
test reads that.
**Exit condition**: after a claim ends, moves, or its function departs, no
mapping made through it reaches the function's registers, and a guest test
reads that.

**Owner**: whoever holds `issues/every-driver-is-still-in-the-kernel.md`.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The kernel keeps only generic mechanisms:

- A claim keeps its function's firmware-reserved ranges mapped in its domain from attach on: VT-d RMRRs, and AMD-Vi's unity ranges.
- The panic surface: a linear buffer the driver lent, and the bounded register writes in its own BAR that make it visible.
- A claim's firmware windows, which only the signed image's `drives` row can name (`firmware = [...]`). Today that is the IGD OpRegion: the kernel finds it at ASLS, holds it to firmware's own memory and maps it for the driver. Its VBT is handed out raw. Intel has published no VBT layout, so parsing it waits for a clean-room fact brief.

## AMD's adapter boundary

Expand Down Expand Up @@ -94,7 +95,7 @@ The kernel keeps only generic mechanisms:
4. **PSR on the eDP panel**, measured together with stage 2. PSR's first-frame exit latency goes to the owner in absolute numbers before PSR lands.
- **The T14's panel has no PSR** (2026-11-01). Its DPCD reads 00070h (PSR_SUPPORT) `00` and 00700h `01`, eDP 1.2; PSR1 is an eDP 1.3 feature. Two boots read it, `823066157` and `f714a5161`, both with the lid closed. The panel still answered AUX with a coherent capability set (eDP 1.2, 2 lanes at 2.7 Gbps), and PSR_SUPPORT is a capability field, so the reading is taken as the panel's and not as a lid artefact. That is an inference: a read with the lid open would confirm it. SRD_STATUS stayed IDLE with the link full on (evidence: https://github.com/ToyOSOrg/evidence/blob/5830b5b51493cce5d952acd8f2c2c98a0b16fe7e/r4/README.md). No other bench has an Intel panel. So #1136 (PSR1) is closed unlanded, since no bench can verify it and nothing calls it.
- **This caps the T14's display-side idle saving.** With the pipe lit, DC5 and DC6 are reached only while PSR is active (Vol 12, "Display C5 and C6"). On this panel they are unreachable, so stage 2's DC6 allow saves nothing while the panel is lit, and stage 1's watermarks are the display's idle saving there.
- **The restart point**, if a PSR-capable panel appears: branch `display-stage4` (`f714a5161`), and the clean-room PSR brief the display task was given, which the orchestrator holds outside this repository.
- **The restart point**, if a PSR-capable panel appears: branch `display-stage4` (`f714a5161`), and the clean-room PSR brief the display task was given, `facts.md` at https://github.com/ToyOSOrg/evidence/tree/1ebd11e0490130a244e0fa362b1a06a190167f8c. An implementer reads `facts.md` alone; `provenance.md` beside it is evidence and stays unread.
5. **The rest of `display-output`.** virtio-gpu and the framebuffer become drivers. The panic surface lands here.
6. **Intel modesetting**: combo-PHY DDIs, DPLLs and CDCLK, DP link training with DSC, HDMI 2.0 with SCDC, EDID over AUX and GMBUS, several outputs.
7. **Intel Type-C DP alt mode**: FIA/IOM state, TCCOLD, the Dekel PHY, hotplug.
Expand Down
1 change: 1 addition & 0 deletions kernel/src/arch/aarch64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub mod fpu;
pub mod hw;
pub mod idle;
pub mod irqchip;
pub mod opregion;
pub mod paging;
pub mod percpu;
pub mod pio;
Expand Down
10 changes: 10 additions & 0 deletions kernel/src/arch/aarch64/opregion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! This architecture's direct map does not reach firmware's reserved memory,
//! so an IGD OpRegion, which an Intel display card in a slot may have, is
//! refused here.

use toyos_abi::pci::WindowRefused;
use toyos_userbound::window::Window;

pub fn window(_asls: u32) -> Result<Window, WindowRefused> {
Err(WindowRefused::NotFirmware)
}
1 change: 1 addition & 0 deletions kernel/src/arch/x86_64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub mod iommu_unit;
pub mod ioapic;
pub mod microcode;
pub mod mtrr;
pub mod opregion;
pub mod paging;
pub mod pat;
pub mod percpu;
Expand Down
40 changes: 40 additions & 0 deletions kernel/src/arch/x86_64/opregion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! The IGD OpRegion's window on this architecture, whose direct map reaches
//! firmware's reserved memory: decided by `toyos_userbound::window`, its
//! header read through the direct map, and every 2 MiB leaf of it held to
//! the direct map's own cacheable type, so the aperture aliases no other.

use toyos_abi::pci::WindowRefused;
use toyos_userbound::window::{self, Window, OPREGION_HEADER};

use crate::mm::policy::CachePolicy;
use crate::mm::PAGE_2M;

/// The OpRegion at `asls`, or why it was refused.
pub fn window(asls: u32) -> Result<Window, WindowRefused> {
let map = crate::mm::firmware_map();
let mapped_end = crate::mm::direct_map_end().get();
let decided = crate::pcidev::with_bar_memory(|bars| {
super::paging::with_driven_windows(|driven| {
window::opregion(map, mapped_end, driven.iter().copied().chain(bars), asls)
})
})?;
// `map_mmio` retypes whole 2 MiB leaves, and `DRIVEN` holds exact
// extents: a leaf a driven window shares would be another type here.
let mut leaf = decided.start & !(PAGE_2M - 1);
while leaf < decided.start + decided.bytes {
if super::paging::direct_map_policy(leaf) != Some(CachePolicy::Normal) {
return Err(WindowRefused::Device);
}
leaf += PAGE_2M;
}
let mut header = [0u8; OPREGION_HEADER];
let at = crate::mm::DirectMap::from_phys(decided.start + decided.offset);
for (n, byte) in header.iter_mut().enumerate() {
// SAFETY: the policy passed every page of the window as firmware's
// own, inside the direct map and in no memory this kernel hands out:
// mapped for the machine's life, and no Rust object.
*byte = unsafe { at.as_ptr::<u8>().add(n).read_volatile() };
}
window::opregion_header(&header)?;
Ok(decided)
}
10 changes: 7 additions & 3 deletions kernel/src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ impl From<crate::mm::charge::Exhausted> for ClaimError {
/// failure after `acquire` cannot leave a device held by nobody.
///
/// `selector` says *which* device where the class alone does not — a PCI
/// function's arrival in its first word, an ISA set's ports and lines — and the
/// syscall has refused a word the class does not read.
/// function's arrival in its first word and its firmware windows in its
/// second, an ISA set's ports and lines — and the syscall has refused a word
/// the class does not read and a window there is none of.
pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result<Arc<DeviceClaim>, ClaimError> {
// Availability is checked before acquiring, so an absent device reports `Absent`, not `Owned`.
match class {
Expand All @@ -167,7 +168,10 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result<Arc<DeviceClai
// The function's own guard, taken inside: a PCI claim's exclusivity is
// per function rather than per class, so there is no flag here to
// acquire first.
let (info, claim) = crate::pcidev::claim(toyos_abi::inventory::Arrival(arrival))?;
// A window is the mint's, and nothing after it can add one.
let windows = toyos_abi::pci::FirmwareWindows::from_mask(selector[1])
.expect("the syscall refused a window bit there is none of");
let (info, claim) = crate::pcidev::claim(toyos_abi::inventory::Arrival(arrival), windows)?;
Ok(DeviceClaim::new(class, DeviceInfo::PciFunction(info), claim)?)
}
DeviceType::Isa => {
Expand Down
10 changes: 10 additions & 0 deletions kernel/src/loader/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ impl Commit for PendingHandles {
let mut entries = Vec::with_capacity(moving.len());
// Moved-out console handles, dropped once the parent's lock is let go.
let mut displaced = Vec::new();
// What each device claim moving into the child handed out, taken off
// it under the caller's lock and revoked once that lock is let go.
let mut handed = Vec::new();
for (mut entry, parent_handle) in moving {
let moved = data
.handles
Expand All @@ -249,6 +252,10 @@ impl Commit for PendingHandles {
displaced.push(moved);
HandleEntry::new(object, rights)
}
KObjectRef::Device(claim) => {
handed.extend(claim.take_mappings());
moved
}
_ => moved,
};
entry.handle = table
Expand All @@ -261,6 +268,9 @@ impl Commit for PendingHandles {
.expect("a caller's table with verified room refused its child");
drop(data);
drop(displaced);
for handed in handed {
handed.revoke();
}
own.endow(&mut table, &mut entries, &mut labels);
Ok((table, Endowments::new(entries, labels), held))
}
Expand Down
15 changes: 15 additions & 0 deletions kernel/src/object/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,24 @@ impl DeviceClaim {
}
}

impl DeviceClaim {
/// What this claim handed out so far, taken off it for a move: taken
/// under the lock that moves it, and revoked once that lock is let go,
/// so its last holder keeps no mapping and its next one loses none.
pub fn take_mappings(&self) -> Option<crate::pcidev::Handed> {
self.pci(crate::pcidev::take_handed)
}

/// `handed` given back, for a move refused.
pub fn restore_mappings(&self, handed: crate::pcidev::Handed) {
self.pci(|binding| crate::pcidev::restore_handed(binding, handed));
}
}

// Released on last handle, not last Arc: a parked daemon can strand an Arc without releasing.
impl ZeroHandles for DeviceClaim {
fn on_zero_handles(&self) {
self.pci(|binding| crate::pcidev::revoke_handed(binding.number()));
self.reference.release();
}
}
Expand Down
9 changes: 9 additions & 0 deletions kernel/src/object/shm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,15 @@ impl Drop for SharedMemObject {
/// An aperture's last handle takes it out of every space that still maps it.
impl ZeroHandles for SharedMemObject {
fn on_zero_handles(&self) {
self.revoke();
}
}

impl SharedMemObject {
/// An aperture taken out of every space that maps it, and refused every
/// later mapping: what its last handle does, and what the end of the
/// authority it was asked on does. Nothing for any other object.
pub fn revoke(&self) {
let Backing::Aperture { mapped, .. } = &self.backing else { return };
let Some((mapped, _charge)) = mapped.take() else { return };
for (space, at, len) in mapped {
Expand Down
Loading