From d441f1d081623ac7dbe3ece306f34a2a694c131b Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 1 Nov 2026 21:29:08 +0100 Subject: [PATCH 1/4] kernel, supervisor: a claim's firmware window, granted at its mint by the signed image's row, and every PCI claim's mappings ending with it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The display driver needs the IGD OpRegion live. Its VBT holds the panel's PSR and backlight settings, and its mailboxes are firmware's channel to the driver. It is firmware memory outside the claim's BARs, which root CLAUDE.md allows only as a privilege the signed image's system.toml grants to a program it ships. - **Manifest.** `firmware = ["igd-opregion"]` on a `drives` row of the image's manifest. The render refuses an unknown window and one on a row with no `drives`. Packages have no spelling for it. - **Mint.** The supervisor mints the function's claim with the windows its row names, carried in the mint's second selector word (`DeviceRequest::Function(arrival, FirmwareWindows)`). A window exists only from the mint, by construction: there is no call that adds one later. A window the function has none of refuses the mint, so a row naming one fails its start by name. The IGD OpRegion goes only to an Intel function of class 03h, a bug fence on the row. - **Window.** SYS_DEVICE_FIRMWARE_WINDOW (164) answers a granted claim's window, by IGD OpRegion specification rev 0.5: - It reads ASLS (config FCh, §5.1.2). - `arch::opregion::window` decides the 8 KB at it in one pass with `toyos_userbound::window` (§3.1.2's size; §2.3's firmware-reserved NVS). Every page must be reserved or ACPI NVS in firmware's map, inside the direct map, none memory the kernel hands out, and none a BAR or driven window. Every 2 MiB leaf must be cacheable in the direct map. - It reads the header there and holds it to "IntelGraphicsMem" and 8 KB (§3.1.1-3.1.2). - A refusal comes back named (`WindowRefused`). The aperture is cacheable, and its handle carries MAP and WRITE alone. - AArch64 has no such machine, and its direct map no such reach. - **Every mapping made through a claim ends with it.** The claim records each aperture it hands out, BARs and firmware windows alike. `pcidev::revoke_handed`, one function, takes each back from every space that maps it, through the revoke an aperture's last handle already does. It runs at the claim's last handle (its release, its holder's death) and when the claim moves, by IPC or a spawn's endowment. So no process keeps a mapping of a function its claim no longer gives it. `issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md` keeps its remaining case, a function's departure, open. - **tgl-display** maps the window, validates a copy with `opregion::OpRegion::parse`, says OVER and MBOX raw, and lets the mapping go. - MBOX needs only bit 3, the VBT's. The bits the 2016 revision calls reserved may name a later revision's mailboxes. - The VBT is handed out raw: Intel has published no layout for it. - `bar_map_again` keeps a BAR's handle past its claim's end and requires its map to be refused, `Gone`. - metalcase runs tgl-display with the grant, and the T14 row `igd_opregion_window` requires the window read and valid. Co-Authored-By: Claude Opus 5.5 --- build/build.rs | 4 + images/system.toml | 4 + ...ping-outlives-the-claim-it-was-asked-on.md | 47 +++--- ...ves-its-displays-from-a-userland-driver.md | 1 + kernel/src/arch/aarch64/mod.rs | 1 + kernel/src/arch/aarch64/opregion.rs | 11 ++ kernel/src/arch/x86_64/mod.rs | 1 + kernel/src/arch/x86_64/opregion.rs | 40 +++++ kernel/src/device.rs | 7 + kernel/src/loader/start.rs | 10 ++ kernel/src/object/device.rs | 10 ++ kernel/src/object/shm.rs | 9 ++ kernel/src/pcidev/mod.rs | 96 +++++++++++- kernel/src/syscall/device.rs | 40 ++++- kernel/src/syscall/dispatch.rs | 6 +- kernel/src/syscall/ipc.rs | 21 ++- lib/manifest/src/lib.rs | 42 +++++ lib/userbound/src/firmware.rs | 4 +- lib/userbound/src/lib.rs | 1 + lib/userbound/src/window.rs | 146 ++++++++++++++++++ sdk/abi/src/pci.rs | 95 ++++++++++++ sdk/abi/src/syscall.rs | 40 ++++- sdk/toyos/src/device.rs | 16 ++ sdk/toyos/src/syscap.rs | 13 +- system/supervisor/src/main.rs | 17 +- system/tgl-display/driver/src/lib.rs | 1 + system/tgl-display/driver/src/opregion.rs | 107 +++++++++++++ system/tgl-display/src/main.rs | 23 +++ .../guest/rust-tests/src/bin/bar_map_again.rs | 11 +- tests/images/metalcase.toml | 7 +- tests/toyos.rs | 18 +++ 31 files changed, 796 insertions(+), 53 deletions(-) create mode 100644 kernel/src/arch/aarch64/opregion.rs create mode 100644 kernel/src/arch/x86_64/opregion.rs create mode 100644 lib/userbound/src/window.rs create mode 100644 system/tgl-display/driver/src/opregion.rs diff --git a/build/build.rs b/build/build.rs index d8ba705aaff..9333e1cc691 100644 --- a/build/build.rs +++ b/build/build.rs @@ -187,6 +187,9 @@ struct ProgramConfig { /// PCI matchers: one process of this row per function one names /// (`toyos_manifest::Program::drives`). drives: Vec, + /// Firmware windows each function's claim is granted + /// (`toyos_manifest::Program::firmware`). + firmware: Vec, /// 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. @@ -883,6 +886,7 @@ fn render_manifest(config: &SystemConfig) -> Vec { 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(), diff --git a/images/system.toml b/images/system.toml index 6c7cddbdb22..08e6eb7d06b 100644 --- a/images/system.toml +++ b/images/system.toml @@ -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. diff --git a/issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md b/issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md index a3c7ce317b5..75bb4fbbdf5 100644 --- a/issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md +++ b/issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md @@ -6,33 +6,32 @@ 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. `pcidev::revoke_handed`, +the one function every end of a claim calls, takes each back from every +space that maps it. It does so through the revoke an aperture's last handle +already does, so the old process's next access faults. It runs at the +claim's last handle (its release, its holder's death) and when the claim +moves, by IPC or a spawn's endowment. -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. +**What is left:** +- **A function's departure.** #934 adds a function leaving (Gone). Whichever + of #934 and #1137 lands second calls `revoke_handed` from it. 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. +- **No guest test reads it yet.** The 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 have the first process's + read fault. -**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. - -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`. diff --git a/issues/each-gpu-drives-its-displays-from-a-userland-driver.md b/issues/each-gpu-drives-its-displays-from-a-userland-driver.md index a87995294c8..318a3f405f6 100644 --- a/issues/each-gpu-drives-its-displays-from-a-userland-driver.md +++ b/issues/each-gpu-drives-its-displays-from-a-userland-driver.md @@ -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 diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index cf8fed2ad31..9e0934ea37d 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -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; diff --git a/kernel/src/arch/aarch64/opregion.rs b/kernel/src/arch/aarch64/opregion.rs new file mode 100644 index 00000000000..da53f0b707c --- /dev/null +++ b/kernel/src/arch/aarch64/opregion.rs @@ -0,0 +1,11 @@ +//! No machine of this architecture has an IGD OpRegion, and its direct map +//! does not reach firmware's reserved memory. + +use toyos_abi::pci::WindowRefused; +use toyos_userbound::window::Window; + +/// Never reached: the mint grants the window only to an Intel display +/// function, which no machine of this architecture has. +pub fn window(_asls: u32) -> Result { + Err(WindowRefused::NotFirmware) +} diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index 4fccc30d21d..366e5e0c139 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -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; diff --git a/kernel/src/arch/x86_64/opregion.rs b/kernel/src/arch/x86_64/opregion.rs new file mode 100644 index 00000000000..32ab27a3582 --- /dev/null +++ b/kernel/src/arch/x86_64/opregion.rs @@ -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 { + 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::().add(n).read_volatile() }; + } + window::opregion_header(&header)?; + Ok(decided) +} diff --git a/kernel/src/device.rs b/kernel/src/device.rs index 94be7f910d1..4337fda0ef6 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -167,7 +167,14 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result { diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs index 9c32f66f383..f49add1f9ee 100644 --- a/kernel/src/loader/start.rs +++ b/kernel/src/loader/start.rs @@ -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(); + // Device claims moving into the child, whose mappings in the caller + // are taken back once the caller's lock is let go. + let mut claims = Vec::new(); for (mut entry, parent_handle) in moving { let moved = data .handles @@ -249,6 +252,10 @@ impl Commit for PendingHandles { displaced.push(moved); HandleEntry::new(object, rights) } + KObjectRef::Device(claim) => { + claims.push(claim.clone()); + moved + } _ => moved, }; entry.handle = table @@ -261,6 +268,9 @@ impl Commit for PendingHandles { .expect("a caller's table with verified room refused its child"); drop(data); drop(displaced); + for claim in claims { + claim.revoke_mappings(); + } own.endow(&mut table, &mut entries, &mut labels); Ok((table, Endowments::new(entries, labels), held)) } diff --git a/kernel/src/object/device.rs b/kernel/src/object/device.rs index 26369ce9c39..a0ffa5ce759 100644 --- a/kernel/src/object/device.rs +++ b/kernel/src/object/device.rs @@ -145,9 +145,19 @@ impl DeviceClaim { } } +impl DeviceClaim { + /// Every mapping made through this claim taken back + /// (`pcidev::revoke_handed`): at its end, and when it moves to another + /// holder, so its last holder keeps none. + pub fn revoke_mappings(&self) { + self.pci(crate::pcidev::revoke_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.revoke_mappings(); self.reference.release(); } } diff --git a/kernel/src/object/shm.rs b/kernel/src/object/shm.rs index 0ee14a5f364..c501dbcd187 100644 --- a/kernel/src/object/shm.rs +++ b/kernel/src/object/shm.rs @@ -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 { diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index e3b67dee58f..ff1f736b27c 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -122,7 +122,7 @@ use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use record::Interrupt; use toyos_abi::boot::RootBridgeWindow; use toyos_abi::inventory::Arrival; -use toyos_abi::pci::{DeviceIrqRecord, PciFunctionInfo, BARS, LEND_WINDOW}; +use toyos_abi::pci::{DeviceIrqRecord, FirmwareWindow, PciFunctionInfo, WindowRefused, BARS, LEND_WINDOW}; use toyos_abi::syscall::{PciId, RegWidth, SyscallError}; use toyos_register::Register; use toyos_pci::{af, aperture, bar, express, pages, pm}; @@ -312,6 +312,12 @@ struct Bound { /// holder's or firmware's queue addresses acts on none of them before its /// holder has stopped it. mastering: bool, + /// The firmware windows the supervisor granted this claim, one bit per + /// [`FirmwareWindow`]. + firmware: u8, + /// Every aperture this claim handed out, BARs and firmware windows: each + /// is revoked when the claim ends or moves ([`revoke_handed`]). + handed: Vec>, } impl Bound { @@ -886,6 +892,8 @@ fn bring_up(pci: PciDevice, id: PciId, number: usize, windows: &[RootBridgeWindo residue: Vec::new(), limit: None, mastering: false, + firmware: 0, + handed: Vec::new(), }) } @@ -1360,14 +1368,96 @@ pub fn bar_object(binding: &Binding, index: u64) -> Result, return Err(SyscallError::InvalidArgument); } // Registers, and the memory type is not firmware's to decide. - Ok(SharedMemObject::aperture( + let object = SharedMemObject::aperture( bound.bar_at[index], bound.bar_bytes[index].next_multiple_of(pages::PAGE), CachePolicy::Uncacheable, - )?) + )?; + bound.hand(&object); + Ok(object) }) } +impl Bound { + /// Record an aperture handed out on this claim; the ones gone already are + /// let go, so the list holds only what may still be mapped. + fn hand(&mut self, object: &Arc) { + self.handed.retain(|weak| weak.strong_count() > 0); + self.handed.push(Arc::downgrade(object)); + } +} + +/// Every aperture the claim handed out revoked: taken out of every space +/// that maps it, so no mapping made through the claim outlives it. The one +/// function every end of a claim calls: its release, its holder's death, its +/// move to another holder, and its function's departure. +pub fn revoke_handed(binding: &Binding) { + // Under the lock alone and not `with_bound`, which refuses a faulted + // function: that one's mappings go too. Revoked after it is let go. + let handed = binding.function().bound.lock().as_mut().map(|bound| core::mem::take(&mut bound.handed)).unwrap_or_default(); + for object in handed.iter().filter_map(alloc::sync::Weak::upgrade) { + object.revoke(); + } +} + +/// The ASL Storage register (IGD OpRegion specification §5.1.2): "The +/// system BIOS places the memory offset of the OpRegion in this register". +const ASLS: u64 = 0xFC; + +/// Grant the claim `window` at its mint, which the signed image's row names +/// (`toyos_abi::pci::FirmwareWindows`). A window only one kind of function +/// has is `NotSupported` on any other: a bug fence on the row. +pub fn grant_firmware(binding: &Binding, window: FirmwareWindow) -> Result<(), SyscallError> { + with_bound(binding, |bound| { + let has = match window { + // An Intel function of class 03h, display controller. + FirmwareWindow::IgdOpRegion => bound.id.vendor == 0x8086 && bound.pci.read_config_u8(0x0B) == 0x03, + }; + if !has { + return Err(SyscallError::NotSupported); + } + bound.firmware |= 1 << window as u64; + Ok(()) + }) +} + +/// A firmware window, mapped: an object over its whole pages, and where in +/// them the window lies. +pub struct Opened { + pub object: Arc, + pub offset: u64, + pub len: u64, +} + +/// The firmware window `window` this claim was granted, as an object over +/// its whole pages, its offset into them and its length; or why it was +/// refused (`toyos_userbound::window`). A fresh object for every ask. +pub fn firmware_window( + binding: &Binding, + window: FirmwareWindow, +) -> Result, SyscallError> { + let asls = with_bound(binding, |bound| { + if bound.firmware & 1 << window as u64 == 0 { + return Err(SyscallError::PermissionDenied); + } + Ok(bound.pci.read_config_u32(ASLS)) + })?; + if asls == 0 { + return Err(SyscallError::NotFound); + } + let window = match crate::arch::opregion::window(asls) { + Ok(window) => window, + Err(refused) => return Ok(Err(refused)), + }; + // Cacheable, as the direct map has it and §2.3 types it. + let object = SharedMemObject::aperture(window.start, window.bytes, CachePolicy::Normal)?; + with_bound(binding, |bound| { + bound.hand(&object); + Ok(()) + })?; + Ok(Ok(Opened { object, offset: window.offset, len: window.len })) +} + /// Memory this function may reach, and nothing else may. pub fn dma_alloc( binding: &Binding, diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index 3bc5a56ec74..d1cae454680 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -122,9 +122,11 @@ pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: [u64; 2] let Some(class) = device::DeviceType::from_raw(class) else { return SyscallError::InvalidArgument.to_u64(); }; + // A PCI function's second word is the firmware windows its claim is + // granted at the mint (`toyos_abi::pci::FirmwareWindows`). let read = match class { device::DeviceType::Framebuffer | device::DeviceType::Acpi => 0, - device::DeviceType::PciFunction => 1, + device::DeviceType::PciFunction => 2, device::DeviceType::Isa => 2, }; if selector[read..].iter().any(|&word| word != 0) { @@ -200,6 +202,42 @@ pub(super) fn sys_device_bar_map(handle: RawHandle, index: u64) -> u64 { }) } +/// A firmware window the claim was granted, as an object its holder can map +/// and neither duplicate nor move: [`Rights::MAP`] and [`Rights::WRITE`] +/// alone. A refusal of what firmware set up goes back in the grant's word. +pub(super) fn sys_device_firmware_window(ctx: &SyscallContext, handle: RawHandle, window: u64, out: UserAddr) -> u64 { + let claim = match pci_claim(handle) { + Ok(claim) => claim, + Err(e) => return e.refuse(), + }; + let Some(window) = toyos_abi::pci::FirmwareWindow::from_raw(window) else { + return SyscallError::InvalidArgument.to_u64(); + }; + let len = core::mem::size_of::() as u64; + let Some(mut out) = ctx.user_bytes_mut(out, len) else { + return SyscallError::BadAddress.to_u64(); + }; + let mut grant = toyos_abi::pci::WindowGrant { shm: toyos_abi::HANDLE_INVALID, refused: 0, offset: 0, len: 0 }; + match on_function(&claim, |function| crate::pcidev::firmware_window(function, window)) { + Err(e) => return e.to_u64(), + Ok(Err(refused)) => grant.refused = refused as u32, + Ok(Ok(crate::pcidev::Opened { object, offset, len })) => { + let entry = crate::object::HandleEntry::new(KObjectRef::SharedMem(object), Rights::MAP.union(Rights::WRITE)); + let installed = process::with_process_data(|data| data.handles.install(entry)); + match installed { + Ok(shm) => grant = toyos_abi::pci::WindowGrant { shm, refused: 0, offset, len }, + Err(_) => return SyscallError::ResourceExhausted.to_u64(), + } + } + } + // SAFETY: `grant` is a live reference readable for its own size, and the + // layout assertion beside its declaration proves every byte of it is an + // initialised field. + let bytes = unsafe { core::slice::from_raw_parts(&grant as *const _ as *const u8, len as usize) }; + out.write_at(0, bytes); + 0 +} + /// Memory the claimed function may reach and nothing else may. pub(super) fn sys_device_dma_alloc( ctx: &SyscallContext, diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 2d5767926ff..575f26ee549 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -22,7 +22,7 @@ use super::HANDLE_LEN; #[cfg(feature = "test-actuators")] use super::debug::{canary, debug_heap_alloc, ring0_timer_in_syscall, FATAL_HALT_NONCE, LOCK_ACROSS_SWITCH}; use super::device::{ - sys_acpi, sys_device_bar_map, sys_device_claim, sys_device_dma_alloc, sys_device_dma_map, + sys_acpi, sys_device_bar_map, sys_device_claim, sys_device_dma_alloc, sys_device_dma_map, sys_device_firmware_window, sys_device_dma_limit, sys_device_dma_start, sys_device_dma_unmap, sys_device_reg_read, }; @@ -621,6 +621,10 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> SYS_EVENT_TAKE => sys_event_take(RawHandle(a1 as u32)), SYS_HANDLE_WAIT => sys_handle_wait(RawHandle(a1 as u32), a2, a3), SYS_CLOCK_NOW => crate::arch::clocksource::stamp_now(), + SYS_DEVICE_FIRMWARE_WINDOW => { + let Some(out) = UserAddr::checked(a3) else { return bad_addr }; + sys_device_firmware_window(&ctx, RawHandle(a1 as u32), a2, out) + } SYS_ACCOUNT_INFO => { if a3 < core::mem::size_of::() as u64 { return SyscallError::InvalidArgument.to_u64(); } let Some(addr) = UserAddr::checked(a2) else { return bad_addr }; diff --git a/kernel/src/syscall/ipc.rs b/kernel/src/syscall/ipc.rs index 0d54af17cc7..b493bbd2b62 100644 --- a/kernel/src/syscall/ipc.rs +++ b/kernel/src/syscall/ipc.rs @@ -456,20 +456,29 @@ fn send(conn_h: RawHandle, out: &Outgoing<'_>, nonblock: bool) -> Result<(), u64 return Err(SyscallError::PermissionDenied.into()); } } + // A device claim that moves takes back every mapping made + // through it: its last holder keeps none. + let claims: Vec<_> = wanted + .iter() + .filter_map(|h| data.handles.get::(*h, Rights::TRANSFER).ok()) + .collect(); // A refusal restores every handle at its own number, so it is invisible. let tx = conn.tx().clone(); let moved = data.handles.transfer(wanted, |batch| tx.send(&out.bytes, batch)); - Ok::<_, crate::object::Refusal>((tx, moved)) + Ok::<_, crate::object::Refusal>((tx, moved, claims)) }); let tx = match sent { - Ok((tx, Ok(()))) => { + Ok((tx, Ok(()), claims)) => { + for claim in claims { + claim.revoke_mappings(); + } crate::scheduler::post_lending(tx.read_watch(), || tx.set_rt_boost_pending()); return Ok(()); } - Ok((tx, Err(SendRefused::NoRoom))) if !nonblock => tx, - Ok((_, Err(SendRefused::NoRoom))) => return Err(SyscallError::WouldBlock.to_u64()), - Ok((_, Err(SendRefused::Gone))) => return Err(SyscallError::Gone.to_u64()), - Ok((_, Err(SendRefused::NoMemory))) => return Err(SyscallError::ResourceExhausted.to_u64()), + Ok((tx, Err(SendRefused::NoRoom), _)) if !nonblock => tx, + Ok((_, Err(SendRefused::NoRoom), _)) => return Err(SyscallError::WouldBlock.to_u64()), + Ok((_, Err(SendRefused::Gone), _)) => return Err(SyscallError::Gone.to_u64()), + Ok((_, Err(SendRefused::NoMemory), _)) => return Err(SyscallError::ResourceExhausted.to_u64()), Err(e) => return Err(e.refuse()), }; // Outside every lock: room appears as the receiver takes messages, and each receive posts this watch. diff --git a/lib/manifest/src/lib.rs b/lib/manifest/src/lib.rs index 7ad8c8a9c0c..1922794704e 100644 --- a/lib/manifest/src/lib.rs +++ b/lib/manifest/src/lib.rs @@ -308,6 +308,10 @@ pub struct Program { /// holding that function's claim. Such a row serves nothing and is never /// in [`Manifest::start`]; it reaches its consumer through `receives`. pub drives: Vec, + /// Firmware windows ([`firmware_window`]) the supervisor grants each + /// function's claim of a `drives` row, before it endows it: a privilege + /// beyond the claim, which only the signed image's rows can name. + pub firmware: Vec, /// Rights on the `SysCap` duplicate the supervisor endows this program, by the names /// [`syscap_rights`] takes. Empty for all but a handful: nothing else in /// the system may enter the RT band, mint a device claim, read the machine @@ -510,6 +514,18 @@ pub enum RenderError { /// another row's `starts`, either of which would start it holding no /// function. Drives { program: String, also: &'static str }, + /// A `firmware` entry naming no window ([`firmware_window`]). + NoSuchFirmware { program: String, window: String }, + /// A `firmware` entry on a row with no `drives`, whose claim it would be. + FirmwareWithoutDrives(String), +} + +/// The firmware window a row's `firmware` entry names. +pub fn firmware_window(word: &str) -> Option { + match word { + "igd-opregion" => Some(toyos_abi::pci::FirmwareWindow::IgdOpRegion), + _ => None, + } } pub fn render(manifest: &Manifest) -> Result, RenderError> { @@ -555,6 +571,15 @@ pub fn render(manifest: &Manifest) -> Result, RenderError> { out.push_str(&format!("{word} {value}\n")); } } + for window in &program.firmware { + if firmware_window(window).is_none() { + return Err(RenderError::NoSuchFirmware { program: program.name.clone(), window: window.clone() }); + } + if program.drives.is_empty() { + return Err(RenderError::FirmwareWithoutDrives(program.name.clone())); + } + out.push_str(&format!("firmware {window}\n")); + } if program.slots { out.push_str("slots\n"); } @@ -746,6 +771,7 @@ pub fn parse(text: &str) -> Manifest { "receive" => program.receives.push(rest.to_string()), "device" => program.devices.push(rest.to_string()), "drive" => program.drives.push(rest.to_string()), + "firmware" if firmware_window(rest).is_some() => program.firmware.push(rest.to_string()), "syscap" => program.syscap.push(rest.to_string()), "slots" if rest.is_empty() => program.slots = true, "authorize" if authorize::Authorization::parse(rest).is_some() => program.authorize.push(rest.to_string()), @@ -1203,6 +1229,22 @@ mod tests { assert_eq!(drives(|m| m.programs[0].starts = vec!["a".into()]), refused("starts")); } + /// A firmware window is a privilege beyond the claim of a `drives` row: + /// a named one, on such a row, round-trips; anything else is refused. + #[test] + fn a_firmware_window_is_named_and_only_on_a_drives_row() { + let mut m = with_rows(&[("a", &[], &["pci:03:00:00/8086:9a49"])]); + m.programs[0].firmware = vec!["igd-opregion".into()]; + let rendered = render(&m).expect("renders"); + assert_eq!(parse(core::str::from_utf8(&rendered).expect("text")).programs[0].firmware, ["igd-opregion"]); + m.programs[0].firmware = vec!["opregion".into()]; + assert_eq!(render(&m), Err(RenderError::NoSuchFirmware { program: "a".into(), window: "opregion".into() })); + let mut m = with_rows(&[("a", &["acpi"], &[])]); + m.programs[0].firmware = vec!["igd-opregion".into()]; + assert_eq!(render(&m), Err(RenderError::FirmwareWithoutDrives("a".into()))); + assert_eq!(firmware_window("igd-opregion"), Some(toyos_abi::pci::FirmwareWindow::IgdOpRegion)); + } + #[test] fn a_device_class_name_is_the_abi_s() { assert_eq!(DeviceType::from_class_name("framebuffer"), Some(DeviceType::Framebuffer)); diff --git a/lib/userbound/src/firmware.rs b/lib/userbound/src/firmware.rs index 6469da5d075..3dec576517b 100644 --- a/lib/userbound/src/firmware.rs +++ b/lib/userbound/src/firmware.rs @@ -78,10 +78,10 @@ use crate::span::PAGE_4K; /// `EfiReservedMemoryType`, `EfiRuntimeServicesData`, `EfiACPIReclaimMemory` /// and `EfiACPIMemoryNVS`. -const EFI_RESERVED: u32 = 0; +pub(crate) const EFI_RESERVED: u32 = 0; const EFI_RUNTIME_DATA: u32 = 6; const EFI_ACPI_RECLAIM: u32 = 9; -const EFI_ACPI_NVS: u32 = 10; +pub(crate) const EFI_ACPI_NVS: u32 = 10; /// `EfiMemoryMappedIO`. const EFI_MMIO: u32 = 11; diff --git a/lib/userbound/src/lib.rs b/lib/userbound/src/lib.rs index d7a97f0be45..bfe24cce909 100644 --- a/lib/userbound/src/lib.rs +++ b/lib/userbound/src/lib.rs @@ -29,6 +29,7 @@ pub mod idle; pub mod firmware; pub mod port; pub mod span; +pub mod window; pub use fault::Ring; pub use port::{port_access, IoBitmap, KeptCommands, Mediated, PortAccess, Ports, Reserved, Undeclared, IO_PORTS}; diff --git a/lib/userbound/src/window.rs b/lib/userbound/src/window.rs new file mode 100644 index 00000000000..a88f9c03db7 --- /dev/null +++ b/lib/userbound/src/window.rs @@ -0,0 +1,146 @@ +//! Where a firmware window a signed image's row grants a claim lies, decided +//! from firmware's own words and checked against what the kernel owns. +//! +//! **Firmware is trusted, and every check here is a bug fence**: a window is +//! refused where a page of it is memory the kernel hands out, a page a device +//! decodes, or memory firmware's map does not keep for firmware. +//! +//! The IGD OpRegion's facts are Intel's "IGD OpRegion/Software SCI/_DSM for +//! Skylake Processors BIOS Specification", revision 0.5 (2016): the ASLS +//! register holds its physical address, its header starts with the signature +//! "IntelGraphicsMem" (§3.1.1) and gives the whole region's size in KB at 10h +//! (§3.1.2), and it is "Firmware Reserved", "Non-Volatile Storage (NVS)" +//! memory under 4 GB (§2.3). + +use toyos_abi::boot::MemoryMapEntry; +use toyos_abi::pci::WindowRefused; + +use crate::firmware::{EFI_ACPI_NVS, EFI_RESERVED}; +use crate::span::PAGE_4K; + +/// §3.1.1. +pub const OPREGION_SIGNATURE: &[u8; 16] = b"IntelGraphicsMem"; +/// The header bytes held to §3.1.1-§3.1.2: the signature, then the size in +/// KB. +pub const OPREGION_HEADER: usize = 20; +/// The size §3.1.2 gives: "OpRegion Size = (incl. Header) in KB = 8 KB". +pub const OPREGION_KB: u32 = 8; +const OPREGION_BYTES: u64 = OPREGION_KB as u64 * 1024; + +/// A window decided: `bytes` of whole pages from `start`, the window +/// `offset` bytes into them and `len` long. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Window { + pub start: u64, + pub bytes: u64, + pub offset: u64, + pub len: u64, +} + +/// The OpRegion's 8 KB at `asls`, decided before a byte of it is read: every +/// page inside what the kernel's direct map reaches (`mapped_end`), reserved +/// or ACPI NVS in firmware's map, none memory the kernel hands out and none +/// a device decodes. +pub fn opregion( + map: &[MemoryMapEntry], + mapped_end: u64, + devices: impl IntoIterator, + asls: u32, +) -> Result { + let at = u64::from(asls); + let start = at & !(PAGE_4K - 1); + let bytes = (at + OPREGION_BYTES).next_multiple_of(PAGE_4K) - start; + let end = start + bytes; + if end > mapped_end { + return Err(WindowRefused::NotFirmware); + } + let mut page = start; + while page < end { + let mut holding = map.iter().filter(|entry| (entry.start..entry.end).contains(&page)); + if holding.clone().any(|entry| toyos_bootmap::is_usable_type(entry.uefi_type)) { + return Err(WindowRefused::Usable); + } + if !holding.any(|entry| matches!(entry.uefi_type, EFI_RESERVED | EFI_ACPI_NVS)) { + return Err(WindowRefused::NotFirmware); + } + page += PAGE_4K; + } + if map.iter().any(|entry| toyos_bootmap::is_usable_type(entry.uefi_type) && entry.start < end && start < entry.end) { + return Err(WindowRefused::Usable); + } + if devices.into_iter().any(|(first, past)| first < end && start < past) { + return Err(WindowRefused::Device); + } + Ok(Window { start, bytes, offset: at - start, len: OPREGION_BYTES }) +} + +/// The header read from inside [`opregion`]'s window: its signature, and +/// the size §3.1.2 gives. +pub fn opregion_header(header: &[u8; OPREGION_HEADER]) -> Result<(), WindowRefused> { + if &header[..16] != OPREGION_SIGNATURE { + return Err(WindowRefused::Signature); + } + match u32::from_le_bytes(header[16..20].try_into().expect("four bytes")) { + OPREGION_KB => Ok(()), + _ => Err(WindowRefused::Size), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const EFI_CONVENTIONAL: u32 = 7; + const EFI_RUNTIME_DATA: u32 = 6; + /// The direct map's end. + const END: u64 = 0x1_0000_0000; + + fn entry(start: u64, end: u64, uefi_type: u32) -> MemoryMapEntry { + MemoryMapEntry { start, end, uefi_type } + } + + /// A T14-like map: RAM below, the OpRegion's NVS range, RAM above. + fn map() -> [MemoryMapEntry; 3] { + [ + entry(0x1000_0000, 0x4000_0000, EFI_CONVENTIONAL), + entry(0x4000_0000, 0x4010_0000, EFI_ACPI_NVS), + entry(0x4010_0000, 0x8000_0000, EFI_CONVENTIONAL), + ] + } + + fn header(signature: &[u8; 16], kb: u32) -> [u8; OPREGION_HEADER] { + let mut out = [0u8; OPREGION_HEADER]; + out[..16].copy_from_slice(signature); + out[16..].copy_from_slice(&kb.to_le_bytes()); + out + } + + #[test] + fn an_opregion_in_nvs_is_its_eight_kb_in_whole_pages() { + assert_eq!(opregion(&map(), END, [], 0x4002_0018), Ok(Window { start: 0x4002_0000, bytes: 0x3000, offset: 0x18, len: 0x2000 })); + assert_eq!(opregion_header(&header(OPREGION_SIGNATURE, 8)), Ok(())); + } + + #[test] + fn a_header_firmware_did_not_write_is_refused() { + assert_eq!(opregion_header(&header(b"IntelGraphicsMeM", 8)), Err(WindowRefused::Signature)); + for kb in [0, 7, 9, 64, u32::MAX] { + assert_eq!(opregion_header(&header(OPREGION_SIGNATURE, kb)), Err(WindowRefused::Size), "{kb} KB"); + } + } + + /// A window reaching RAM the kernel hands out, memory firmware keeps for + /// something else, an unlisted address, a device's window or past the + /// direct map is refused. + #[test] + fn a_window_past_firmwares_own_memory_is_refused() { + assert_eq!(opregion(&map(), END, [], 0x400f_f000), Err(WindowRefused::Usable), "its last pages are RAM"); + assert_eq!(opregion(&map(), END, [], 0x3fff_f000), Err(WindowRefused::Usable)); + assert_eq!(opregion(&[entry(0x4000_0000, 0x4010_0000, EFI_RUNTIME_DATA)], END, [], 0x4000_0000), Err(WindowRefused::NotFirmware)); + assert_eq!(opregion(&map(), END, [], 0x9000_0000), Err(WindowRefused::NotFirmware), "unlisted"); + assert_eq!(opregion(&map(), 0x4000_1000, [], 0x4000_0000), Err(WindowRefused::NotFirmware), "past the direct map"); + assert_eq!(opregion(&map(), END, [(0x4002_1000, 0x4002_2000)], 0x4002_0000), Err(WindowRefused::Device)); + let overlapping = [entry(0x4000_0000, 0x4010_0000, EFI_ACPI_NVS), entry(0x4002_1000, 0x4002_2000, EFI_CONVENTIONAL)]; + assert_eq!(opregion(&overlapping, END, [], 0x4002_0000), Err(WindowRefused::Usable), "RAM wherever the map lists it"); + } +} diff --git a/sdk/abi/src/pci.rs b/sdk/abi/src/pci.rs index c8eb921c79b..78879294c1b 100644 --- a/sdk/abi/src/pci.rs +++ b/sdk/abi/src/pci.rs @@ -65,6 +65,101 @@ pub struct DmaGrant { const _: () = assert!(core::mem::size_of::() == 4 + 4 + 8 + 8); +/// A window into memory firmware set up for a function, granted only by the +/// signed image's manifest (`firmware = [...]`). +#[repr(u64)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum FirmwareWindow { + /// The IGD OpRegion of an Intel graphics function, at its ASLS register. + IgdOpRegion = 1, +} + +impl FirmwareWindow { + pub const fn from_raw(raw: u64) -> Option { + match raw { + 1 => Some(Self::IgdOpRegion), + _ => None, + } + } +} + +/// The firmware windows a PCI function claim is granted at its mint, one bit +/// per [`FirmwareWindow`], carried in the mint's second selector word. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub struct FirmwareWindows(u64); + +impl FirmwareWindows { + pub const NONE: Self = Self(0); + const KNOWN: u64 = 1 << FirmwareWindow::IgdOpRegion as u64; + + pub fn of(windows: &[FirmwareWindow]) -> Self { + Self(windows.iter().fold(0, |mask, &window| mask | 1 << window as u64)) + } + + /// The mask off the wire, or `None` naming a window there is none of. + pub const fn from_mask(mask: u64) -> Option { + if mask & !Self::KNOWN != 0 { None } else { Some(Self(mask)) } + } + + pub const fn mask(self) -> u64 { + self.0 + } + + pub fn contains(self, window: FirmwareWindow) -> bool { + self.0 & 1 << window as u64 != 0 + } + + pub fn iter(self) -> impl Iterator { + [FirmwareWindow::IgdOpRegion].into_iter().filter(move |&window| self.contains(window)) + } +} + +/// A firmware window as [`crate::syscall::device_firmware_window`] answers +/// it: whole pages in `shm`, and the window `offset` bytes into them. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct WindowGrant { + /// The pages, as an object to map; the caller owns this handle. + pub shm: crate::RawHandle, + /// Where the kernel refused the window, a [`WindowRefused`]; 0 + /// otherwise. + pub refused: u32, + pub offset: u64, + /// The window's length; its pages run to `offset + len`, rounded up. + pub len: u64, +} + +const _: () = assert!(core::mem::size_of::() == 4 + 4 + 8 + 8); + +/// Why the kernel refused a firmware window firmware set up. +#[repr(u32)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum WindowRefused { + /// The header does not carry the window's signature. + Signature = 1, + /// The size its header gives is none the window can be. + Size = 2, + /// A page of it is not memory firmware keeps for itself. + NotFirmware = 3, + /// A page of it is memory the kernel hands out. + Usable = 4, + /// A page of it is one a device decodes. + Device = 5, +} + +impl WindowRefused { + pub const fn from_raw(raw: u32) -> Option { + match raw { + 1 => Some(Self::Signature), + 2 => Some(Self::Size), + 3 => Some(Self::NotFirmware), + 4 => Some(Self::Usable), + 5 => Some(Self::Device), + _ => None, + } + } +} + /// Where a function reaches a region its holder mapped into its address space /// ([`crate::syscall::device_dma_map`]). /// diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index d4c2f98e573..b27b844ed2d 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -370,6 +370,9 @@ pub const SYS_THREAD_SET_AFFINITY: u64 = 161; pub const SYS_THREAD_GET_AFFINITY: u64 = 162; /// The CPU the caller is running on. See [`current_cpu`]. pub const SYS_CURRENT_CPU: u64 = 163; +/// Map a firmware window a claim was granted at its mint. See +/// [`device_firmware_window`]. +pub const SYS_DEVICE_FIRMWARE_WINDOW: u64 = 164; /// Bins in the per-process syscall profile — one for every number this ABI /// issues, and one at the end for every number it does not. @@ -377,13 +380,13 @@ pub const SYS_CURRENT_CPU: u64 = 163; /// **The profile's parts sum to its total, and that is the whole requirement.** /// A bin array narrower than the ABI reaches drops calls out of the line while /// the total goes on counting them. -pub const SYSCALL_PROFILE_BINS: usize = 165; +pub const SYSCALL_PROFILE_BINS: usize = 166; /// Where a number this ABI does not issue is counted. Merging is a degradation /// a reader can see in the line; dropping is one nobody can. pub const SYSCALL_PROFILE_OTHER: usize = SYSCALL_PROFILE_BINS - 1; -const _: () = assert!(SYS_CURRENT_CPU < SYSCALL_PROFILE_OTHER as u64); +const _: () = assert!(SYS_DEVICE_FIRMWARE_WINDOW < SYSCALL_PROFILE_OTHER as u64); pub const WNOHANG: u64 = 1; /// Every bit `SYS_PROCESS_WAIT`'s flag word defines; a wait carrying another @@ -1747,7 +1750,8 @@ fn decimal_line(text: &str) -> Option { } /// What a claim asks for: a class, and for [`DeviceType::PciFunction`] which -/// function, and for an ISA function which ports and lines. +/// function and the firmware windows its claim is granted at the mint, and +/// for an ISA function which ports and lines. /// /// Every request but a PCI function's is also a `devices` entry, spelled once /// here ([`Self::parse`], [`Self::write_name`]): the manifest that names it and @@ -1755,7 +1759,7 @@ fn decimal_line(text: &str) -> Option { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum DeviceRequest { Class(DeviceType), - Function(crate::inventory::Arrival), + Function(crate::inventory::Arrival, crate::pci::FirmwareWindows), Isa(IsaId), } @@ -1785,7 +1789,7 @@ impl DeviceRequest { pub const fn class(self) -> DeviceType { match self { Self::Class(class) => class, - Self::Function(_) => DeviceType::PciFunction, + Self::Function(..) => DeviceType::PciFunction, Self::Isa(_) => DeviceType::Isa, } } @@ -1795,7 +1799,7 @@ impl DeviceRequest { pub fn selector(self) -> [u64; 2] { match self { Self::Class(_) => [0, 0], - Self::Function(arrival) => [arrival.0 as u64, 0], + Self::Function(arrival, windows) => [arrival.0 as u64, windows.mask()], Self::Isa(set) => set.wire(), } } @@ -1806,7 +1810,7 @@ impl DeviceRequest { /// under, and it is composed here so the two cannot drift. pub fn write_name(self, buf: &mut [u8; Self::MAX_NAME]) -> Option<&str> { Some(match self { - Self::Function(_) => return None, + Self::Function(..) => return None, Self::Class(class) => { let name = class.class_name(); buf[..name.len()].copy_from_slice(name.as_bytes()); @@ -2785,6 +2789,24 @@ pub fn device_bar_map(claim: RawHandle, bar: u32) -> Result Result, SyscallError> { + let mut grant = crate::pci::WindowGrant { shm: crate::HANDLE_INVALID, refused: 0, offset: 0, len: 0 }; + check(syscall(SYS_DEVICE_FIRMWARE_WINDOW, claim.0 as u64, window as u64, &mut grant as *mut _ as u64, 0))?; + Ok(match crate::pci::WindowRefused::from_raw(grant.refused) { + Some(refused) => Err(refused), + None => Ok(grant), + }) +} + /// A DMA buffer for a claimed PCI function: memory in this process's address /// space and in that function's own at the unit, and in no other's. /// @@ -3146,9 +3168,11 @@ mod tests { /// arrival, which no entry spells. #[test] fn a_devices_entry_is_a_class_or_one_named_device() { - let function = DeviceRequest::Function(crate::inventory::Arrival(7)); + let function = DeviceRequest::Function(crate::inventory::Arrival(7), crate::pci::FirmwareWindows::NONE); assert_eq!(function.class(), DeviceType::PciFunction); assert_eq!(function.selector(), [7, 0]); + let granted = crate::pci::FirmwareWindows::of(&[crate::pci::FirmwareWindow::IgdOpRegion]); + assert_eq!(DeviceRequest::Function(crate::inventory::Arrival(7), granted).selector(), [7, 1 << 1]); assert_eq!(function.write_name(&mut [0; DeviceRequest::MAX_NAME]), None); assert_eq!( DeviceRequest::parse("framebuffer"), diff --git a/sdk/toyos/src/device.rs b/sdk/toyos/src/device.rs index 2912a78e293..e4b5bdbdfea 100644 --- a/sdk/toyos/src/device.rs +++ b/sdk/toyos/src/device.rs @@ -138,6 +138,22 @@ impl PciDev { crate::shm::SharedMemory::adopt(crate::OwnedHandle(handle), bytes as usize) } + /// The firmware window `window` the signed image's row granted this claim + /// at its mint: + /// whole pages mapped cacheable, the window's offset into them and its + /// length. Or why the kernel refused what firmware set up. + pub fn firmware_window( + &self, + window: toyos_abi::pci::FirmwareWindow, + ) -> Result, SyscallError> { + let grant = match syscall::device_firmware_window(self.0.as_handle().raw(), window)? { + Ok(grant) => grant, + Err(refused) => return Ok(Err(refused)), + }; + let memory = crate::shm::SharedMemory::adopt(crate::OwnedHandle(grant.shm), (grant.offset + grant.len) as usize)?; + Ok(Ok((memory, grant.offset as usize, grant.len as usize))) + } + /// Give up ownership, for a claim about to be endowed. pub fn into_raw(self) -> RawHandle { self.0.into_raw() diff --git a/sdk/toyos/src/syscap.rs b/sdk/toyos/src/syscap.rs index f6d8c6171f1..0e811186a2f 100644 --- a/sdk/toyos/src/syscap.rs +++ b/sdk/toyos/src/syscap.rs @@ -43,7 +43,18 @@ impl SysCap { &self, arrival: toyos_abi::inventory::Arrival, ) -> Result { - self.mint(DeviceRequest::Function(arrival)) + self.mint(DeviceRequest::Function(arrival, toyos_abi::pci::FirmwareWindows::NONE)) + } + + /// [`Self::claim_function`], its claim granted `windows` at the mint: the + /// firmware windows the signed image's `drives` row names. `NotSupported` + /// where the function has none of one. + pub fn claim_function_granted( + &self, + arrival: toyos_abi::inventory::Arrival, + windows: toyos_abi::pci::FirmwareWindows, + ) -> Result { + self.mint(DeviceRequest::Function(arrival, windows)) } /// Mint the claim for one legacy ISA function, by exactly its ports and diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index 3fc6a17f8aa..911400e832f 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -1207,7 +1207,18 @@ impl<'a> Supervisor<'a> { return Err(format!("the supervisor watches {MAX_CHILDREN} processes already")); } let name = entry.to_string(); - let claim = self.syscap.claim_function::(function.arrival).map_err(|e| refused(&name, e))?; + // The privileges beyond the claim this signed image's row names, + // granted at the mint. + let windows: Vec<_> = row + .firmware + .iter() + .map(|word| { + toyos_manifest::firmware_window(word) + .unwrap_or_else(|| panic!("supervisor: {} names firmware window `{word}`, which the build refuses", row.name)) + }) + .collect(); + let windows = toyos_abi::pci::FirmwareWindows::of(&windows); + let claim = self.syscap.claim_function_granted::(function.arrival, windows).map_err(|e| refused(&name, e))?; let claim = translated(claim, &function).map_err(|why| missing(&name, &why))?; self.make_state_dir(row); let mut extra = Extra { claims: vec![(format!("{DEV_PREFIX}{name}"), claim)], ..Extra::default() }; @@ -3069,7 +3080,7 @@ fn start<'a>( }; let found = claims::function(&matcher, records, boot_controller()); let function = found.as_ref().ok().and_then(|at| records.iter().find(|pci| pci.arrival == *at)).copied(); - (found.map(DeviceRequest::Function).map_err(Missing::Unresolved), function) + (found.map(|at| DeviceRequest::Function(at, toyos_abi::pci::FirmwareWindows::NONE)).map_err(Missing::Unresolved), function) } }; let minted = request.and_then(|request| claim(syscap, request).map_err(Missing::Refused)).and_then(|minted| { @@ -3551,7 +3562,7 @@ fn pci_records(syscap: &SysCap) -> Result, String> { fn claim(syscap: &SysCap, request: DeviceRequest) -> Result { match request { DeviceRequest::Class(class) => syscap.claim(class), - DeviceRequest::Function(arrival) => syscap.claim_function(arrival), + DeviceRequest::Function(arrival, windows) => syscap.claim_function_granted(arrival, windows), DeviceRequest::Isa(set) => syscap.claim_isa(set), } } diff --git a/system/tgl-display/driver/src/lib.rs b/system/tgl-display/driver/src/lib.rs index 8abbdb5fb77..6ebe79de697 100644 --- a/system/tgl-display/driver/src/lib.rs +++ b/system/tgl-display/driver/src/lib.rs @@ -23,6 +23,7 @@ use toyos_device_memory::Registers; pub mod dmc; pub mod interrupt; +pub mod opregion; pub mod regs; pub mod watermark; diff --git a/system/tgl-display/driver/src/opregion.rs b/system/tgl-display/driver/src/opregion.rs new file mode 100644 index 00000000000..3f8dff53a67 --- /dev/null +++ b/system/tgl-display/driver/src/opregion.rs @@ -0,0 +1,107 @@ +//! The IGD OpRegion, read as input from outside, by Intel's "IGD +//! OpRegion/Software SCI/_DSM for Skylake Processors BIOS Specification", +//! revision 0.5: its header (§3.1) and the mailboxes it says it carries. +//! +//! The kernel already held the region to its signature and 8 KB; this is +//! read from a copy, so every field is validated here again, and the raw VBT +//! is handed out unparsed. + +/// §3.1.1. +const SIGNATURE: &[u8; 16] = b"IntelGraphicsMem"; +/// §3.1.2: "OpRegion Size = (incl. Header) in KB = 8 KB". +pub const BYTES: usize = 8 * 1024; +const SIZE_AT: usize = 0x10; +const OVER_AT: usize = 0x14; +const MBOX_AT: usize = 0x58; +/// Table 3-1: mailbox 4, the VBT, is 6 KB at 400h. +const VBT_AT: usize = 0x400; +const VBT_BYTES: usize = 6 * 1024; +/// §3.1.7 MBOX: bit 3 mailbox 4, the VBT, which "Must be supported". The +/// bits the 2016 revision calls reserved may name mailboxes a later one +/// defines, so they are not refused. +const MBOX_VBT: u32 = 1 << 3; + +/// Why a copy of the OpRegion was refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Malformed { + /// Not the 8 KB §3.1.2 gives. + Length(usize), + Signature, + Size(u32), + /// MBOX does not name the VBT's mailbox. + Mailboxes(u32), +} + +/// A validated copy of the OpRegion. +#[derive(Debug)] +pub struct OpRegion<'a> { + bytes: &'a [u8], + /// OVER: the OpRegion/SWSCI interface version firmware implements. + pub version: u32, + /// MBOX: one bit per mailbox it carries. + pub mailboxes: u32, +} + +impl<'a> OpRegion<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + if bytes.len() != BYTES { + return Err(Malformed::Length(bytes.len())); + } + let word = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().expect("four bytes")); + if &bytes[..16] != SIGNATURE { + return Err(Malformed::Signature); + } + if word(SIZE_AT) != (BYTES / 1024) as u32 { + return Err(Malformed::Size(word(SIZE_AT))); + } + let mailboxes = word(MBOX_AT); + if mailboxes & MBOX_VBT == 0 { + return Err(Malformed::Mailboxes(mailboxes)); + } + Ok(Self { bytes, version: word(OVER_AT), mailboxes }) + } + + /// Mailbox 4's raw VBT, 6 KB of it, unparsed. + pub fn vbt(&self) -> &'a [u8] { + &self.bytes[VBT_AT..VBT_AT + VBT_BYTES] + } +} + +#[cfg(test)] +mod tests { + extern crate std; + + use super::*; + use std::vec::Vec; + + fn region(mailboxes: u32) -> Vec { + let mut bytes = std::vec![0u8; BYTES]; + bytes[..16].copy_from_slice(SIGNATURE); + bytes[SIZE_AT..SIZE_AT + 4].copy_from_slice(&8u32.to_le_bytes()); + bytes[OVER_AT..OVER_AT + 4].copy_from_slice(&0x0200_0000u32.to_le_bytes()); + bytes[MBOX_AT..MBOX_AT + 4].copy_from_slice(&mailboxes.to_le_bytes()); + bytes[VBT_AT] = 0x24; + bytes + } + + #[test] + fn a_region_firmware_set_up_gives_its_version_mailboxes_and_vbt() { + let bytes = region(0b1_1111); + let opregion = OpRegion::parse(&bytes).expect("parses"); + assert_eq!((opregion.version, opregion.mailboxes), (0x0200_0000, 0b1_1111)); + assert_eq!((opregion.vbt().len(), opregion.vbt()[0]), (6 * 1024, 0x24)); + } + + #[test] + fn a_copy_that_does_not_hold_together_is_refused() { + assert_eq!(OpRegion::parse(®ion(0b1111)[..BYTES - 1]).err(), Some(Malformed::Length(BYTES - 1))); + let mut bad = region(0b1111); + bad[15] = b'M'; + assert_eq!(OpRegion::parse(&bad).err(), Some(Malformed::Signature)); + let mut bad = region(0b1111); + bad[SIZE_AT] = 16; + assert_eq!(OpRegion::parse(&bad).err(), Some(Malformed::Size(16))); + assert_eq!(OpRegion::parse(®ion(0b0111)).err(), Some(Malformed::Mailboxes(0b0111)), "no VBT"); + assert!(OpRegion::parse(®ion(1 << 5 | 0b1000)).is_ok(), "a mailbox a later revision defines"); + } +} diff --git a/system/tgl-display/src/main.rs b/system/tgl-display/src/main.rs index ebef820a03e..bbe9d1e0cb9 100644 --- a/system/tgl-display/src/main.rs +++ b/system/tgl-display/src/main.rs @@ -15,12 +15,14 @@ use toyos::ipc::{FrameRx, RxStep}; use toyos::poller::{Poller, READABLE}; use toyos::{endow, say, Connection, PciDev}; +use toyos_abi::pci::FirmwareWindow; use toyos_abi::syscall::{PciId, RegWidth, SyscallError}; use toyos_display_output::{Hello, Present, Presented, Size, MAX_FRAME, MSG_HELLO, MSG_PRESENTED, PORT}; use toyos_device_memory::Registers; use toyos_manifest::device::PciMatch; use toyos_pci_claim::Bar; use toyos_tgl_display::dmc::{Package, Stepping, TGL_PACKAGE}; +use toyos_tgl_display::opregion::OpRegion; use toyos_tgl_display::{interrupt, Programmed, Refusal, FRAME_CEILING_NANOS}; /// The T14's iGPU, Tiger Lake GT2 (`8086:9a49`), a VGA-compatible display @@ -56,6 +58,7 @@ fn main() { // once, since the claim's domain already maps what it reads. toyos_pci_claim::start(&dev).unwrap_or_else(|why| panic!("tgl-display: {why}")); say!("tgl-display: PCI {:02x}:{:02x}.{} registers in BAR {BAR}, {span:#x} of its {bytes:#x} bytes", info.bus, info.dev, info.func); + opregion(&dev); let package = Package::parse(TGL_PACKAGE).unwrap_or_else(|why| panic!("tgl-display: the DMC package is malformed: {why:?}")); let revision = dev.config_read(0x08, RegWidth::U32).unwrap_or_else(|why| panic!("tgl-display: config space: {why:?}")) as u8; @@ -232,3 +235,23 @@ fn answer(dev: &PciDev, regs: &Bar, conn: &Connection, size: Size, refresh_inter } } } + +/// What a copy of the IGD OpRegion the signed image's row grants this claim +/// says, or why there is none to read. +fn opregion(dev: &PciDev) { + let (memory, offset, len) = match dev.firmware_window(FirmwareWindow::IgdOpRegion) { + Ok(Ok(window)) => window, + Ok(Err(refused)) => return say!("tgl-display: the kernel refused the IGD OpRegion firmware set up: {refused:?}"), + Err(why) => return say!("tgl-display: no IGD OpRegion ({why:?})"), + }; + let copy = memory.bytes().sub(offset, len).to_vec(); + match OpRegion::parse(©) { + Ok(region) => say!( + "tgl-display: IGD OpRegion, {len} bytes at offset {offset:#x} of its pages; OVER {:08x}, MBOX {:08x}, VBT mailbox {} bytes", + region.version, + region.mailboxes, + region.vbt().len(), + ), + Err(why) => say!("tgl-display: the IGD OpRegion is malformed: {why:?}"), + } +} diff --git a/tests/guest/rust-tests/src/bin/bar_map_again.rs b/tests/guest/rust-tests/src/bin/bar_map_again.rs index 0dd5ceb0c91..7dbe648a457 100644 --- a/tests/guest/rust-tests/src/bin/bar_map_again.rs +++ b/tests/guest/rust-tests/src/bin/bar_map_again.rs @@ -4,7 +4,7 @@ //! answer is still held, where the two are two mappings and the later one //! outlives the earlier; after a close; and after an ask a full handle table //! refused, which is an object whose only handle went before its holder ever -//! had it. +//! had it. And a handle kept past the claim's end maps nothing. //! //! The function is QEMU's virtio NIC, which no program of `tests/images/testcases.toml` //! holds. @@ -107,6 +107,15 @@ fn main() { let after = ask(&nic, bar, bytes); assert_eq!(after, first, "bar_map_again: the ask after the refusal maps another window"); println!("bar_map_again: answered after the refusal with a handle that maps"); + + // A BAR's handle kept past its claim's end maps nothing: the claim's end + // revoked every aperture it handed out. + let kept = syscall::device_bar_map(nic.as_handle().raw(), bar).expect("bar_map_again: the BAR, asked for"); + drop(nic); + // SAFETY: a refused map makes no mapping; one made is never written. + let mapped = unsafe { syscall::shm_map(kept, toyos_abi::syscall::VmProt::ReadWrite) }; + assert_eq!(mapped.err(), Some(SyscallError::Gone), "bar_map_again: a BAR's handle mapped after its claim ended"); + println!("bar_map_again: a BAR's handle kept past its claim's end maps nothing"); } /// The kernel handle this process's standard output is, descriptor 1's. diff --git a/tests/images/metalcase.toml b/tests/images/metalcase.toml index d03c1ba21f1..2fff1232785 100644 --- a/tests/images/metalcase.toml +++ b/tests/images/metalcase.toml @@ -45,12 +45,13 @@ drives = ["pci:04:03:*"] receives = ["sound-output"] syscap = ["rt"] -# The T14's display driver: the firmware's output on `display-output`, and -# PG3, which holds `hda`'s display codec, powered down beside `hda`. No other -# bench has its iGPU, so none starts it. +# The T14's display driver: the firmware's output on `display-output`, PG3, +# which holds `hda`'s display codec, powered down beside `hda`, and the IGD +# OpRegion its row grants. No other bench has its iGPU, so none starts it. [programs.tgl-display] drives = ["pci:03:00:00/8086:9a49"] receives = ["display-output"] +firmware = ["igd-opregion"] # netstack over every link a NIC driver brings; on a machine with no virtio NIC # the supervisor starts no driver, and netstack serves with no address. diff --git a/tests/toyos.rs b/tests/toyos.rs index 5ab2ecebfe8..7929bae138b 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1435,6 +1435,15 @@ const METAL: &[(&str, metal::Metal)] = &[ "display_output_presents", metal::Metal { arms: METALCASE_TONE, judge: |b| display_output_presents(b[0]) }, ), + ( + // The IGD OpRegion the signed image grants tgl-display's claim: the + // kernel finds it at the iGPU's ASLS register, holds it to its + // signature, its 8 KB and firmware's own memory, and maps it; the + // driver's copy validates as the specification lays it out. Only the + // T14's firmware sets one up, and QEMU's has no IGD. + "igd_opregion_window", + metal::Metal { arms: METALCASE, judge: |b| igd_opregion_window(&b[0].log()) }, + ), // ---- the `SYS_DEBUG` members' boot, armed ---- // The cheapest cluster there is: every one of these arms a check that runs // at init, logs its verdict and does nothing else, so they cost no flash @@ -2074,6 +2083,15 @@ fn display_output_presents(boot: &metal::Readback) -> Result<(), String> { Ok(()) } +/// What [`igd_opregion_window`] reads: the window granted at the mint, +/// mapped, and the driver's copy valid. Any refusal on the way leaves this +/// line unsaid, and `must_say` prints the log. +fn igd_opregion_window(log: &serial::Serial) -> Result<(), String> { + let read = log.must_say("tgl-display: IGD OpRegion, 8192 bytes")?; + println!("igd_opregion_window: {read}"); + Ok(()) +} + /// `tests/images/metalcase.toml`, ended once ps2server's probe has: a boot /// that ended sooner would cut off what `ps2server_arms` reads. const METALCASE: &[metal::Arm] = &[metal::Arm { From 57a9a1fc93f72855f04e36adc6773c8220670b5d Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 1 Nov 2026 22:19:15 +0100 Subject: [PATCH 2/4] review r2 of #1137: a move revokes only what its sender was handed, and no aperture is handed past the revoke it should meet - **A move takes what the claim handed out under the lock that moves it.** IPC's send and a spawn's commit take the claim's `handed` list under the sender's process lock, before the claim is visible to its receiver, and revoke it once that lock is let go. A send that is refused gives it back. So a receiver that takes the claim and maps a BAR before the sender's revoke runs keeps its mapping (r2 required 2). - **An aperture is installed only while the caller still holds the claim.** `bar_map` and `firmware_window` install the new handle under the caller's process lock, and only if its table holds this claim at `handle`. A release or a move takes the claim out under that lock and revokes after it, so every aperture either met that revoke or is refused `Gone` (required 3). - `pcidev::revoke_handed(number)` is the by-number form #934's departure calls from its refresh loop. `Handed::revoke` documents its terms: thread context only, never the hot-plug interrupt, never with `MACHINE` held. The false clause about a departure is gone (required 4). - The BAR issue states what `bar_map_again` now reads and what it still does not (required 5). - `bar_map_again` sends its claim to itself over a connection. A BAR handle kept past the move maps `Gone`, and the new holder's fresh BAR maps and reads the device. The judge requires both lines (optional 5). - The window fence is decided from `Machine::identities` in `pcidev::claim`, before the function is touched, so a refused window no longer resets it (optional 1). An unknown window bit is `InvalidArgument` in `sys_device_claim` (optional 2). - `Bound::firmware` is `FirmwareWindows`, and `FirmwareWindow::ALL` is the one list of windows (optional 8). The ABI doc says what the kernel checks (optional 4), and the AArch64 stub keeps only the direct map's reason (optional 3). Co-Authored-By: Claude Opus 5.5 --- ...ping-outlives-the-claim-it-was-asked-on.md | 37 +++--- kernel/src/arch/aarch64/opregion.rs | 7 +- kernel/src/device.rs | 17 ++- kernel/src/loader/start.rs | 12 +- kernel/src/object/device.rs | 17 ++- kernel/src/pcidev/mod.rs | 114 ++++++++++++------ kernel/src/syscall/device.rs | 30 ++++- kernel/src/syscall/ipc.rs | 24 +++- sdk/abi/src/pci.rs | 38 ++++-- .../guest/rust-tests/src/bin/bar_map_again.rs | 48 ++++++-- tests/toyos.rs | 2 + 11 files changed, 241 insertions(+), 105 deletions(-) diff --git a/issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md b/issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md index 75bb4fbbdf5..3eb928eea9f 100644 --- a/issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md +++ b/issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md @@ -7,23 +7,30 @@ opened: 2026-10-08 # A BAR's mapping outlives the claim it was asked on Since #1137, a claim records every aperture it hands out: each BAR window -`pcidev::bar_object` makes, and each firmware window. `pcidev::revoke_handed`, -the one function every end of a claim calls, takes each back from every -space that maps it. It does so through the revoke an aperture's last handle -already does, so the old process's next access faults. It runs at the -claim's last handle (its release, its holder's death) and when the claim -moves, by IPC or a spawn's endowment. +`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. + +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`. **What is left:** -- **A function's departure.** #934 adds a function leaving (Gone). Whichever - of #934 and #1137 lands second calls `revoke_handed` from it. 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. -- **No guest test reads it yet.** The 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 have the first process's - read fault. +- **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's ruling on taking a mapping back from a running process (`issues/a-moved-handle-is-always-re-movable.md`, "Revocation is the other diff --git a/kernel/src/arch/aarch64/opregion.rs b/kernel/src/arch/aarch64/opregion.rs index da53f0b707c..5a6f06da24e 100644 --- a/kernel/src/arch/aarch64/opregion.rs +++ b/kernel/src/arch/aarch64/opregion.rs @@ -1,11 +1,10 @@ -//! No machine of this architecture has an IGD OpRegion, and its direct map -//! does not reach firmware's reserved memory. +//! 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; -/// Never reached: the mint grants the window only to an Intel display -/// function, which no machine of this architecture has. pub fn window(_asls: u32) -> Result { Err(WindowRefused::NotFirmware) } diff --git a/kernel/src/device.rs b/kernel/src/device.rs index 4337fda0ef6..0ad3242f855 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -150,8 +150,9 @@ impl From 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, ClaimError> { // Availability is checked before acquiring, so an absent device reports `Absent`, not `Owned`. match class { @@ -167,14 +168,10 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result { diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs index f49add1f9ee..8847cb1e5a7 100644 --- a/kernel/src/loader/start.rs +++ b/kernel/src/loader/start.rs @@ -235,9 +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(); - // Device claims moving into the child, whose mappings in the caller - // are taken back once the caller's lock is let go. - let mut claims = 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 @@ -253,7 +253,7 @@ impl Commit for PendingHandles { HandleEntry::new(object, rights) } KObjectRef::Device(claim) => { - claims.push(claim.clone()); + handed.extend(claim.take_mappings()); moved } _ => moved, @@ -268,8 +268,8 @@ impl Commit for PendingHandles { .expect("a caller's table with verified room refused its child"); drop(data); drop(displaced); - for claim in claims { - claim.revoke_mappings(); + for handed in handed { + handed.revoke(); } own.endow(&mut table, &mut entries, &mut labels); Ok((table, Endowments::new(entries, labels), held)) diff --git a/kernel/src/object/device.rs b/kernel/src/object/device.rs index a0ffa5ce759..755376fcc8a 100644 --- a/kernel/src/object/device.rs +++ b/kernel/src/object/device.rs @@ -146,18 +146,23 @@ impl DeviceClaim { } impl DeviceClaim { - /// Every mapping made through this claim taken back - /// (`pcidev::revoke_handed`): at its end, and when it moves to another - /// holder, so its last holder keeps none. - pub fn revoke_mappings(&self) { - self.pci(crate::pcidev::revoke_handed); + /// 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 { + 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.revoke_mappings(); + self.pci(|binding| crate::pcidev::revoke_handed(binding.number())); self.reference.release(); } } diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index ff1f736b27c..c91e8524834 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -122,7 +122,7 @@ use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use record::Interrupt; use toyos_abi::boot::RootBridgeWindow; use toyos_abi::inventory::Arrival; -use toyos_abi::pci::{DeviceIrqRecord, FirmwareWindow, PciFunctionInfo, WindowRefused, BARS, LEND_WINDOW}; +use toyos_abi::pci::{DeviceIrqRecord, FirmwareWindow, FirmwareWindows, PciFunctionInfo, WindowRefused, BARS, LEND_WINDOW}; use toyos_abi::syscall::{PciId, RegWidth, SyscallError}; use toyos_register::Register; use toyos_pci::{af, aperture, bar, express, pages, pm}; @@ -312,11 +312,10 @@ struct Bound { /// holder's or firmware's queue addresses acts on none of them before its /// holder has stopped it. mastering: bool, - /// The firmware windows the supervisor granted this claim, one bit per - /// [`FirmwareWindow`]. - firmware: u8, + /// The firmware windows this claim was granted at its mint. + firmware: FirmwareWindows, /// Every aperture this claim handed out, BARs and firmware windows: each - /// is revoked when the claim ends or moves ([`revoke_handed`]). + /// is revoked when the claim ends or moves ([`Handed`]). handed: Vec>, } @@ -755,14 +754,20 @@ impl core::fmt::Display for Refusal { /// Claim the function that arrived as `arrival`, bring it up to the point a /// driver takes over, and answer what that driver needs to know. -pub fn claim(arrival: Arrival) -> Result<(PciFunctionInfo, Claim), ClaimError> { - let (number, pci, id, driven) = { +pub fn claim(arrival: Arrival, firmware: FirmwareWindows) -> Result<(PciFunctionInfo, Claim), ClaimError> { + let (number, pci, id, driven, fenced) = { let machine = MACHINE.lock(); let number = machine.identities.iter().position(|pci| pci.arrival == arrival).ok_or(ClaimError::Absent)?; let (pci, identity) = (machine.functions[number], &machine.identities[number]); let driven = machine.kernel_driven.contains(&requester(&pci)); - (number, pci, PciId { vendor: identity.vendor, device: identity.device }, driven) + let fenced = firmware.iter().find(|&window| !has_window(identity, window)); + (number, pci, PciId { vendor: identity.vendor, device: identity.device }, driven, fenced) }; + // Before the function is touched, as every refusal here is. + if let Some(window) = fenced { + log!("pcidev: PCI {:02x}:{:02x}.{} has no {window:?}, which its claim was to be granted", pci.bus, pci.dev, pci.func); + return Err(ClaimError::Unusable); + } if driven { log!("pcidev: PCI {:02x}:{:02x}.{} is driven by this kernel and cannot be claimed", pci.bus, pci.dev, pci.func); return Err(ClaimError::KernelDriven); @@ -795,6 +800,7 @@ pub fn claim(arrival: Arrival) -> Result<(PciFunctionInfo, Claim), ClaimError> { match bring_up(pci, id, number, declaration.windows()) { Ok(mut bound) => { bound.residue = take_residue(number); + bound.firmware = firmware; let info = PciFunctionInfo { bar_bytes: bound.bar_bytes, bus: pci.bus, @@ -892,7 +898,7 @@ fn bring_up(pci: PciDevice, id: PciId, number: usize, windows: &[RootBridgeWindo residue: Vec::new(), limit: None, mastering: false, - firmware: 0, + firmware: FirmwareWindows::NONE, handed: Vec::new(), }) } @@ -1387,16 +1393,67 @@ impl Bound { } } -/// Every aperture the claim handed out revoked: taken out of every space -/// that maps it, so no mapping made through the claim outlives it. The one -/// function every end of a claim calls: its release, its holder's death, its -/// move to another holder, and its function's departure. -pub fn revoke_handed(binding: &Binding) { - // Under the lock alone and not `with_bound`, which refuses a faulted - // function: that one's mappings go too. Revoked after it is let go. - let handed = binding.function().bound.lock().as_mut().map(|bound| core::mem::take(&mut bound.handed)).unwrap_or_default(); - for object in handed.iter().filter_map(alloc::sync::Weak::upgrade) { - object.revoke(); +/// The apertures a claim handed out, taken off it ([`take_handed`]): each +/// revoked by [`Self::revoke`], or given back by [`restore_handed`] where the +/// move they were taken for is refused. +#[must_use] +pub struct Handed(Vec>); + +impl Handed { + /// Each aperture taken out of every space that maps it, so no mapping + /// made through the claim outlives what it was taken for. + /// + /// Thread context only, with no `MACHINE` held: the shootdown waits on + /// other CPUs, which an interrupt handler cannot. The order is the + /// aperture's `Held`, then its spaces. + pub fn revoke(self) { + for object in self.0.iter().filter_map(alloc::sync::Weak::upgrade) { + object.revoke(); + } + } +} + +/// What `function`'s claim handed out, taken off it. Under the lock +/// alone and not `with_bound`, which refuses a faulted function: that one's +/// mappings go too. +fn take(function: &Function) -> Handed { + Handed(function.bound.lock().as_mut().map(|bound| core::mem::take(&mut bound.handed)).unwrap_or_default()) +} + +/// What the claim handed out so far, taken off it. A move takes it under the +/// lock that moves the claim, before the new holder can hand out its own. +pub fn take_handed(binding: &Binding) -> Handed { + take(binding.function()) +} + +/// `handed` given back to the claim it was taken off, for a move refused. +pub fn restore_handed(binding: &Binding, handed: Handed) { + if let Some(bound) = binding.function().bound.lock().as_mut() { + bound.handed.extend(handed.0); + } +} + +/// Every aperture function `number`'s claim handed out, revoked: at the +/// claim's last handle, and at its function's departure, which has no +/// binding. Under [`Handed::revoke`]'s terms: never from the hot-plug +/// interrupt, and never with `MACHINE` held. +pub fn revoke_handed(number: usize) { + take(function(number)).revoke(); +} + +impl Binding { + /// Its index in the functions, for [`revoke_handed`]. + pub fn number(&self) -> usize { + self.number + } +} + +/// Whether the function `identity` names has `window`: a bug fence on the +/// row that grants it, decided before the claim touches the function. +fn has_window(identity: &toyos_abi::inventory::Pci, window: FirmwareWindow) -> bool { + match window { + // An Intel function of class 03h, display controller. + FirmwareWindow::IgdOpRegion => identity.vendor == 0x8086 && identity.class == 0x03, } } @@ -1404,23 +1461,6 @@ pub fn revoke_handed(binding: &Binding) { /// system BIOS places the memory offset of the OpRegion in this register". const ASLS: u64 = 0xFC; -/// Grant the claim `window` at its mint, which the signed image's row names -/// (`toyos_abi::pci::FirmwareWindows`). A window only one kind of function -/// has is `NotSupported` on any other: a bug fence on the row. -pub fn grant_firmware(binding: &Binding, window: FirmwareWindow) -> Result<(), SyscallError> { - with_bound(binding, |bound| { - let has = match window { - // An Intel function of class 03h, display controller. - FirmwareWindow::IgdOpRegion => bound.id.vendor == 0x8086 && bound.pci.read_config_u8(0x0B) == 0x03, - }; - if !has { - return Err(SyscallError::NotSupported); - } - bound.firmware |= 1 << window as u64; - Ok(()) - }) -} - /// A firmware window, mapped: an object over its whole pages, and where in /// them the window lies. pub struct Opened { @@ -1437,7 +1477,7 @@ pub fn firmware_window( window: FirmwareWindow, ) -> Result, SyscallError> { let asls = with_bound(binding, |bound| { - if bound.firmware & 1 << window as u64 == 0 { + if !bound.firmware.contains(window) { return Err(SyscallError::PermissionDenied); } Ok(bound.pci.read_config_u32(ASLS)) diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index d1cae454680..15644475a7f 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -129,7 +129,9 @@ pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: [u64; 2] device::DeviceType::PciFunction => 2, device::DeviceType::Isa => 2, }; - if selector[read..].iter().any(|&word| word != 0) { + let windows_known = class != device::DeviceType::PciFunction + || toyos_abi::pci::FirmwareWindows::from_mask(selector[1]).is_some(); + if !windows_known || selector[read..].iter().any(|&word| word != 0) { return SyscallError::InvalidArgument.to_u64(); } if let Err(e) = demand_syscap(syscap, Rights::DEVICE) { @@ -197,8 +199,27 @@ pub(super) fn sys_device_bar_map(handle: RawHandle, index: u64) -> u64 { Ok(object) => object, Err(e) => return e.to_u64(), }; + let object = KObjectRef::SharedMem(object); + let rights = ops::initial_rights(&object); + handle_result(install_handed(handle, &claim, crate::object::HandleEntry::new(object, rights))) +} + +/// `entry`, an aperture `claim` handed out, installed only while the +/// caller's table still holds `claim` at `handle`, under the one lock its +/// release or move takes it out under. That move or release revokes what the +/// claim handed out once the lock is let go, so every aperture either met +/// that revoke or is refused here, `Gone`, and its object ends unmapped. +fn install_handed( + handle: RawHandle, + claim: &Arc, + entry: crate::object::HandleEntry, +) -> Result { process::with_process_data(|data| { - handle_result(ops::install(&mut data.handles, KObjectRef::SharedMem(object))) + let held = data.handles.get::(handle, Rights::NONE).is_ok_and(|held| Arc::ptr_eq(&held, claim)); + if !held { + return Err(SyscallError::Gone); + } + data.handles.install(entry).map_err(|_| SyscallError::ResourceExhausted) }) } @@ -223,10 +244,9 @@ pub(super) fn sys_device_firmware_window(ctx: &SyscallContext, handle: RawHandle Ok(Err(refused)) => grant.refused = refused as u32, Ok(Ok(crate::pcidev::Opened { object, offset, len })) => { let entry = crate::object::HandleEntry::new(KObjectRef::SharedMem(object), Rights::MAP.union(Rights::WRITE)); - let installed = process::with_process_data(|data| data.handles.install(entry)); - match installed { + match install_handed(handle, &claim, entry) { Ok(shm) => grant = toyos_abi::pci::WindowGrant { shm, refused: 0, offset, len }, - Err(_) => return SyscallError::ResourceExhausted.to_u64(), + Err(e) => return e.to_u64(), } } } diff --git a/kernel/src/syscall/ipc.rs b/kernel/src/syscall/ipc.rs index b493bbd2b62..90dfe083089 100644 --- a/kernel/src/syscall/ipc.rs +++ b/kernel/src/syscall/ipc.rs @@ -456,21 +456,33 @@ fn send(conn_h: RawHandle, out: &Outgoing<'_>, nonblock: bool) -> Result<(), u64 return Err(SyscallError::PermissionDenied.into()); } } - // A device claim that moves takes back every mapping made - // through it: its last holder keeps none. + // A device claim that moves takes back every mapping made through + // it: what it handed out is taken off it here, before the send + // shows it to a receiver that may hand out its own, and revoked + // once this lock is let go. let claims: Vec<_> = wanted .iter() .filter_map(|h| data.handles.get::(*h, Rights::TRANSFER).ok()) + .filter_map(|claim| claim.take_mappings().map(|handed| (claim, handed))) .collect(); // A refusal restores every handle at its own number, so it is invisible. let tx = conn.tx().clone(); let moved = data.handles.transfer(wanted, |batch| tx.send(&out.bytes, batch)); - Ok::<_, crate::object::Refusal>((tx, moved, claims)) + let handed: Vec<_> = match moved { + Ok(()) => claims.into_iter().map(|(_, handed)| handed).collect(), + Err(_) => { + for (claim, handed) in claims { + claim.restore_mappings(handed); + } + Vec::new() + } + }; + Ok::<_, crate::object::Refusal>((tx, moved, handed)) }); let tx = match sent { - Ok((tx, Ok(()), claims)) => { - for claim in claims { - claim.revoke_mappings(); + Ok((tx, Ok(()), handed)) => { + for handed in handed { + handed.revoke(); } crate::scheduler::post_lending(tx.read_watch(), || tx.set_rt_boost_pending()); return Ok(()); diff --git a/sdk/abi/src/pci.rs b/sdk/abi/src/pci.rs index 78879294c1b..12e6ab17adc 100644 --- a/sdk/abi/src/pci.rs +++ b/sdk/abi/src/pci.rs @@ -65,8 +65,10 @@ pub struct DmaGrant { const _: () = assert!(core::mem::size_of::() == 4 + 4 + 8 + 8); -/// A window into memory firmware set up for a function, granted only by the -/// signed image's manifest (`firmware = [...]`). +/// A window into memory firmware set up for a function, granted at its +/// claim's mint ([`FirmwareWindows`]). The kernel checks only the minter's +/// `DEVICE` right; the supervisor sets a window only from the signed image's +/// own row (`firmware = [...]`). #[repr(u64)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum FirmwareWindow { @@ -75,11 +77,22 @@ pub enum FirmwareWindow { } impl FirmwareWindow { + /// Every window there is. + pub const ALL: [Self; 1] = [Self::IgdOpRegion]; + pub const fn from_raw(raw: u64) -> Option { - match raw { - 1 => Some(Self::IgdOpRegion), - _ => None, + let mut i = 0; + while i < Self::ALL.len() { + if Self::ALL[i] as u64 == raw { + return Some(Self::ALL[i]); + } + i += 1; } + None + } + + const fn bit(self) -> u64 { + 1 << self as u64 } } @@ -90,10 +103,17 @@ pub struct FirmwareWindows(u64); impl FirmwareWindows { pub const NONE: Self = Self(0); - const KNOWN: u64 = 1 << FirmwareWindow::IgdOpRegion as u64; + const KNOWN: u64 = { + let (mut mask, mut i) = (0, 0); + while i < FirmwareWindow::ALL.len() { + mask |= FirmwareWindow::ALL[i].bit(); + i += 1; + } + mask + }; pub fn of(windows: &[FirmwareWindow]) -> Self { - Self(windows.iter().fold(0, |mask, &window| mask | 1 << window as u64)) + Self(windows.iter().fold(0, |mask, &window| mask | window.bit())) } /// The mask off the wire, or `None` naming a window there is none of. @@ -106,11 +126,11 @@ impl FirmwareWindows { } pub fn contains(self, window: FirmwareWindow) -> bool { - self.0 & 1 << window as u64 != 0 + self.0 & window.bit() != 0 } pub fn iter(self) -> impl Iterator { - [FirmwareWindow::IgdOpRegion].into_iter().filter(move |&window| self.contains(window)) + FirmwareWindow::ALL.into_iter().filter(move |&window| self.contains(window)) } } diff --git a/tests/guest/rust-tests/src/bin/bar_map_again.rs b/tests/guest/rust-tests/src/bin/bar_map_again.rs index 7dbe648a457..cdba60721ff 100644 --- a/tests/guest/rust-tests/src/bin/bar_map_again.rs +++ b/tests/guest/rust-tests/src/bin/bar_map_again.rs @@ -4,7 +4,9 @@ //! answer is still held, where the two are two mappings and the later one //! outlives the earlier; after a close; and after an ask a full handle table //! refused, which is an object whose only handle went before its holder ever -//! had it. And a handle kept past the claim's end maps nothing. +//! had it. And a BAR handle kept past its claim's move, here over a +//! connection to this process itself, maps nothing, while the claim's new +//! holder maps the BAR; and one kept past the claim's end maps nothing. //! //! The function is QEMU's virtio NIC, which no program of `tests/images/testcases.toml` //! holds. @@ -13,9 +15,10 @@ mod function; use toyos::endow::Endowments; +use toyos::ipc::FrameRx; use toyos::shm::SharedMemory; use toyos::syscap::SysCap; -use toyos::{AsHandle, PciDev}; +use toyos::{namespace, port, AsHandle, OwnedHandle, PciDev}; use toyos_abi::handle::RawHandle; use toyos_abi::syscall::{self, PciId, SyscallError, SYSCAP_LABEL}; @@ -108,16 +111,47 @@ fn main() { assert_eq!(after, first, "bar_map_again: the ask after the refusal maps another window"); println!("bar_map_again: answered after the refusal with a handle that maps"); + // A BAR's handle kept past its claim's move maps nothing: the move revoked + // every aperture the claim had handed out. The claim's new holder, which + // is this process again, maps the BAR afresh. + let kept = syscall::device_bar_map(nic.as_handle().raw(), bar).expect("bar_map_again: the BAR, asked for"); + let (acceptor, connector) = port::create().expect("bar_map_again: a port"); + let names = namespace::build().add("conn", &connector).finish().expect("bar_map_again: a namespace"); + let client = names.open("conn").expect("bar_map_again: a connection"); + let server = acceptor.accept().expect("bar_map_again: the connection accepted"); + let claim = nic.as_handle().raw(); + // The claim's handle goes with the frame: nothing may close it here. + core::mem::forget(nic); + // SAFETY: the claim's live handle, which the forgotten `PciDev` no longer + // answers for. + client.signal_with(1, [unsafe { OwnedHandle::from_raw(claim) }]).expect("bar_map_again: the claim sent"); + assert_eq!(map(kept).err(), Some(SyscallError::Gone), "bar_map_again: a BAR's handle mapped after its claim moved"); + let mut rx = FrameRx::<0>::new(); + let frame = rx.recv(&server).expect("bar_map_again: the claim received"); + assert_eq!(frame.handles, 1, "bar_map_again: the frame carries no claim"); + let [claim] = rx.take_handles::<1>().expect("bar_map_again: the claim's handle"); + let fresh = syscall::device_bar_map(claim.as_handle().raw(), bar).expect("bar_map_again: the BAR, asked for on the moved claim"); + let window = map(fresh).expect("bar_map_again: the moved claim's BAR mapped"); + // SAFETY: as in `read`, through the mapping just made. + let answered = unsafe { window.add(DEVICE_FEATURE).cast::().read_volatile() }; + assert_eq!(answered, first, "bar_map_again: the moved claim's mapping is another window"); + println!("bar_map_again: a BAR's handle kept past its claim's move maps nothing, and the new holder's maps"); + // A BAR's handle kept past its claim's end maps nothing: the claim's end // revoked every aperture it handed out. - let kept = syscall::device_bar_map(nic.as_handle().raw(), bar).expect("bar_map_again: the BAR, asked for"); - drop(nic); - // SAFETY: a refused map makes no mapping; one made is never written. - let mapped = unsafe { syscall::shm_map(kept, toyos_abi::syscall::VmProt::ReadWrite) }; - assert_eq!(mapped.err(), Some(SyscallError::Gone), "bar_map_again: a BAR's handle mapped after its claim ended"); + let kept = syscall::device_bar_map(claim.as_handle().raw(), bar).expect("bar_map_again: the BAR, asked for"); + drop(claim); + assert_eq!(map(kept).err(), Some(SyscallError::Gone), "bar_map_again: a BAR's handle mapped after its claim ended"); println!("bar_map_again: a BAR's handle kept past its claim's end maps nothing"); } +/// `shm` mapped, or why not. +fn map(shm: RawHandle) -> Result<*mut u8, SyscallError> { + // SAFETY: a refused map makes no mapping; one made is only read, as a + // device register. + unsafe { syscall::shm_map(shm, toyos_abi::syscall::VmProt::ReadWrite) } +} + /// The kernel handle this process's standard output is, descriptor 1's. fn stdout() -> RawHandle { toyos::fd::with(1, |o| o.handle().map(toyos::BorrowedHandle::raw)).ok().flatten().expect("the test is given a standard output") diff --git a/tests/toyos.rs b/tests/toyos.rs index 7929bae138b..8c68ce8db5e 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -6144,6 +6144,8 @@ fn bar_map_again(test_config: &Path) -> Result<(), String> { "bar_map_again: two answers held at once map apart, and the later outlives the earlier", "bar_map_again: answered with a handle that maps", "bar_map_again: answered after the refusal with a handle that maps", + "bar_map_again: a BAR's handle kept past its claim's move maps nothing, and the new holder's maps", + "bar_map_again: a BAR's handle kept past its claim's end maps nothing", ] { let Some(line) = result.stdout.lines().find(|l| l.contains(answered)) else { return Err(format!("the job never said `{answered}`:\n{}", result.stdout)); From 1357d65b13a64adb2cd0a05cb7fafe43c2a6ed2b Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 1 Nov 2026 23:48:03 +0100 Subject: [PATCH 3/4] #1137 r3: the window syscall is 164 past #1062's and #1116's, benchcase's display row is granted its window, and the PSR restart point is the durable brief Rebased onto main c81fc83eb. #1062 took syscall 160 and #1116 took 161-163, so SYS_DEVICE_FIRMWARE_WINDOW is 164. The profile has 166 bins, and the guard names 164. #1149 runs tgl-display in benchcase "as images/system.toml runs it", so its row carries the same `firmware = ["igd-opregion"]` grant. The display track's PSR restart line now links the clean-room brief at its durable evidence commit. Only facts.md there is an implementer's input. Co-Authored-By: Claude Opus 5.5 --- .../each-gpu-drives-its-displays-from-a-userland-driver.md | 2 +- tests/images/benchcase.toml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/issues/each-gpu-drives-its-displays-from-a-userland-driver.md b/issues/each-gpu-drives-its-displays-from-a-userland-driver.md index 318a3f405f6..0ff31671268 100644 --- a/issues/each-gpu-drives-its-displays-from-a-userland-driver.md +++ b/issues/each-gpu-drives-its-displays-from-a-userland-driver.md @@ -95,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. diff --git a/tests/images/benchcase.toml b/tests/images/benchcase.toml index 36d3702182f..b3714fc441e 100644 --- a/tests/images/benchcase.toml +++ b/tests/images/benchcase.toml @@ -49,10 +49,12 @@ receives = ["link"] # The T14's display driver, as `images/system.toml` runs it, so the power a # row reads is the display engine the shipped system leaves. With no # compositor, its presents on `display-output` have no consumer, so the row -# receives no port; the driver says so and keeps the scan-out. No other bench -# has the T14's iGPU, so none starts it. +# receives no port; the driver says so and keeps the scan-out. It is granted +# the IGD OpRegion as the shipped row is. No other bench has the T14's iGPU, +# so none starts it. [programs.tgl-display] drives = ["pci:03:00:00/8086:9a49"] +firmware = ["igd-opregion"] # The order `osbench all` runs the groups in. The bound is the metal row's, its # 465 s on the 60 s every list has (`tests/toyos.rs`), restated for a boot of From 577298fadbc2bf540e556a3339a9d41e005dd0f1 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 2 Nov 2026 06:39:03 +0100 Subject: [PATCH 4/4] review r3 of #1137: revoke_handed states its by-number contract alone, and a BAR handle's life is in the ABI - `pcidev::revoke_handed` says only what it is: by number, for a caller with no binding, in thread context and never under `MACHINE`. - `device_bar_map` says a BAR's object lives no longer than the claim's hold: an end or move takes its mappings back, and a kept handle maps `Gone`. - `sys_device_claim` names the window bits its selector carries and refuses. Co-Authored-By: Claude Opus 5.5 --- kernel/src/pcidev/mod.rs | 8 ++++---- kernel/src/syscall/device.rs | 7 ++++--- sdk/abi/src/syscall.rs | 5 +++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index c91e8524834..857c712ccde 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -1433,10 +1433,10 @@ pub fn restore_handed(binding: &Binding, handed: Handed) { } } -/// Every aperture function `number`'s claim handed out, revoked: at the -/// claim's last handle, and at its function's departure, which has no -/// binding. Under [`Handed::revoke`]'s terms: never from the hot-plug -/// interrupt, and never with `MACHINE` held. +/// Every aperture function `number`'s claim handed out, revoked, for a +/// caller that holds the function's number and no binding. Under +/// [`Handed::revoke`]'s terms: thread context only, and never with `MACHINE` +/// held. pub fn revoke_handed(number: usize) { take(function(number)).revoke(); } diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index 15644475a7f..19dcc51c95c 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -115,9 +115,10 @@ pub(super) fn sys_acpi(ctx: &SyscallContext, handle: RawHandle, op: u64, at: u64 /// Mints a device claim, gated on a `SysCap` carrying [`Rights::DEVICE`]. /// /// `selector` says which device where the class alone does not — a PCI -/// function's arrival, an ISA set — and a word the class -/// does not read is refused, never dropped: whoever set it meant a device this -/// claim would not name. +/// function's arrival, with the firmware windows its claim is granted in the +/// second word, or an ISA set. A word the class does not read, or a window +/// bit there is none of, is refused, never dropped: whoever set it meant a +/// device this claim would not name. pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: [u64; 2]) -> u64 { let Some(class) = device::DeviceType::from_raw(class) else { return SyscallError::InvalidArgument.to_u64(); diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index b27b844ed2d..390a8bca26b 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -2782,6 +2782,11 @@ pub fn write_nonblock(handle: RawHandle, buf: &[u8]) -> Result Result {