Skip to content

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 - #1137

Merged
Japabu merged 4 commits into
mainfrom
firmware-window
Nov 2, 2026
Merged

Japabu merged 4 commits into
mainfrom
firmware-window

Conversation

@Japabu

@Japabu Japabu commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Draft. On main at c81fc83eb (#1132, #1134, #1138, #1062, #1116, #1148 and #1149 included), with opregion() wired into tgl-display over stages 2 and 3.

Problem

The display driver needs the IGD OpRegion: its VBT holds the panel's PSR and backlight settings, and its mailboxes are firmware's live channel to the driver. It is firmware memory outside the iGPU's BARs. Root CLAUDE.md allows that only as a privilege the signed image's system.toml grants to a program it ships.

Separately, a BAR's mapping outlived the claim it was asked on (issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md). The window would have had the same defect.

What this PR does

  • Manifest. firmware = ["igd-opregion"] on a drives row. The render refuses an unknown window, and one on a row without drives. Installed packages have no spelling for it.
  • Granted at the mint.
    • The supervisor mints a drives row's claim with the windows the row names, carried in the mint's second selector word (DeviceRequest::Function(arrival, FirmwareWindows)).
    • No call adds a window later.
    • The IGD OpRegion goes only to an Intel function of class 03h, a bug fence on the row. pcidev::claim decides it from the enumerated identity before the function is touched. A window the function lacks refuses the mint, so the row fails its start by name, and the function is not reset. An unknown window bit is InvalidArgument.
    • The kernel checks only the minter's DEVICE right. The supervisor sets a window only from the signed image's own row.
  • The window, SYS_DEVICE_FIRMWARE_WINDOW (164).
    • It reads ASLS (config FCh, IGD OpRegion specification §5.1.2).
    • arch::opregion::window decides the 8 KB at it in one pass with toyos_userbound::window. Every page must be reserved or ACPI NVS in firmware's map (§2.3), inside the direct map, and none memory the kernel hands out, a BAR or a driven window. Every 2 MiB leaf must be cacheable in the direct map.
    • It reads the header there and holds it to the signature "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.
    • On AArch64 it is refused (NotFirmware): the direct map there does not reach firmware's reserved memory. An Intel display card in a slot could ask.
  • Every mapping made through a claim ends with it.
    • The claim records each aperture it hands out, BARs and windows alike.
    • Each is taken back from every space that maps it, through the revoke an aperture's last handle already does. This happens at the claim's last handle (its release, its holder's death) and when the claim moves, by IPC or a spawn's endowment.
    • A move takes the claim's handed list under the sender's process lock, before the receiver can see the claim, and revokes it once that lock is let go. A refused send puts it back. So a receiver that maps a BAR before the sender's revoke runs keeps its mapping.
    • An aperture's handle is installed only while the caller's table still holds this claim at handle, under that same lock. A release or move takes the claim out under it and revokes after. So every aperture either met that revoke or is refused Gone, including one a sibling thread asks for across the release or move.
    • pcidev::revoke_handed(number) revokes by function number, for a caller with no binding, such as PCI root bridges declared by acpiserver, device lifecycle and native hot-plug #934's departure from its refresh loop. It must run in thread context and never with MACHINE held, because the shootdown waits on other CPUs.
    • Locks: the list is taken under bound, which is let go before any revoke. Each revoke then takes the aperture's Held, then the spaces that map it. A move takes bound inside the sender's process lock and the claim's Held, and revokes only after letting all of them go.
  • tgl-display maps the window, validates a copy (§3.1, MBOX bit 3 required), logs OVER and MBOX raw, and lets the mapping go. The VBT is handed out raw.
  • Tests on hardware. metalcase and benchcase run tgl-display with the grant, as images/system.toml does. On the T14, metalcase's row igd_opregion_window requires the window read and valid. bar_map_again (QEMU) keeps a BAR handle past its claim's move, by sending the claim to itself over a connection, and past its end. It requires both maps refused Gone, and requires the new holder's fresh BAR to map and read the device.

Decisions

  • Source. Intel's "IGD OpRegion/Software SCI/_DSM for Skylake Processors BIOS Specification", revision 0.5 (2016). It is the newest Intel publishes, and it is cited at each site.
  • The kernel holds the size to the 8 KB §3.1.2 gives, so a header cannot widen the window.
  • No VBT parse. Intel has published no VBT layout. By the orchestrator's ruling the VBT stays raw until PSR's link state calls for its training times.
  • A revoked mapping faults its old holder's next access, which the tracker's ruling on reclaim allows ("forced reclaim is killing the holder").

An observation: could acpiserver's acpi_access become a window?

Not as one mechanism for both, on present facts. acpiserver's accesses are decided one at a time because the AML chooses each address at evaluation:

  • an address in ECAM becomes a configuration access;
  • a write to a register the kernel keeps is refused;
  • a port is mediated or protected;
  • an SMI command byte is rate-limited;
  • a register's uncacheability is checked against the range registers.

A mapped window can apply none of these after the mapping. A narrower simplification is possible: a window over every reserved and ACPI NVS range, for the AML's ordinary memory accesses, with registers, configuration and ports still mediated. That would remove a syscall per NVS access, but it splits one policy across two mechanisms, and the gain is unmeasured. So I am not proposing it now. It is worth measuring if acpiserver's access rate ever matters.

Checks

At adc35d850 (the head 577298fad differs only in doc comments and commit messages). Evidence: https://github.com/ToyOSOrg/evidence/blob/b9d05ee6d839cb88868ca8e4aed541008371ef96/r6/README.md

  • cargo run -- --ci host: exit 0, 109 steps green. An earlier run at this head died of a full shared disk (ENOSPC); the rerun followed once space was freed.
  • bar_map_again (QEMU, x86_64): PASS, with the move and end lines.
  • T14, at 2831e6e52: one metalcase boot, rc=0, judged by hand from the readback (r4). igd_opregion_window and display_output_presents both PASS.
    • The log line is IGD OpRegion, 8192 bytes at offset 0x18 of its pages; OVER 02010000, MBOX 0000001d, VBT mailbox 6144 bytes.
    • MBOX has bits 0, 2, 3 and 4. Bit 3 is the VBT, which §3.1.7 requires, and bit 4 is one the 2016 revision calls reserved.
    • Stages 2 and 3 are unchanged: DC6 is allowed on DDI A, the first present is answered at a later frame, the tone plays with 0 underruns, and nothing is dropped.
    • The T14 evidence is reused at this head. The rebase changed only the syscall number, the profile size, the restart line and benchcase's row; the window and revocation code are the same.
  • Negative controls:
    • without the IPC move's revoke, bar_map_again fails at the move assertion (r3);
    • r2's controls: without the claim-end revoke, and with RAM or a device allowed in the window policy, the tests fail.
  • Earlier rounds: r1, r2 (r2 was of the mis-parented tree).

Risks

  • metalcase on the T14 runs tgl-display, so its other rows boot with stage 1's watermarks.
  • No guest test reads the concurrent receiver (r2 required 2) or the sibling-thread race (required 3), because a self-send cannot interleave them. Code reasoning closes both, and it is stated above. A spawn's move is also unread.
  • A claim's mappings now end with it. A driver that kept a BAR handle after closing its claim faults on its next access. No in-tree driver does.

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1137 at 4be4b0f

Architecture

The authority path is right. A grant needs a SysCap carrying DEVICE. Only rows of a signed image get that right, and an installed package's row is built by Manifest::app_row with no firmware, devices or syscap. The grant is recorded on the claim's Bound, and bring_up builds a fresh one with firmware: 0, so a later claim on the function never inherits it. ASLS is read once per request while the claim is held. The kernel has no config-write path to it, and acpiserver's write is refused on a held function. The extent is fixed at 8 KB whatever the header says, so rewriting the header cannot widen the window. I found no TOCTOU between the check and the map. The size arithmetic starts from a u32, so it cannot overflow. The window's handle carries MAP|WRITE alone: dup and send need DUP and TRANSFER, and shm_sub refuses an aperture. The kernel ABI types and the driver's own Malformed are kept separate.

Four things need fixing:

  • the lifetime the approved design specified is not implemented;
  • an architecture cfg is in generic kernel code;
  • the docs at the grant syscall state a contract the kernel does not enforce;
  • the T14 evidence is still missing.

Net: +680/−3. About 140 of those lines are tests: userbound 60, opregion 35, manifest 15, judge 27.

Required

  1. kernel/src/syscall/device.rs:242, kernel/src/pcidev/mod.rs:1269 (tear_down): the window outlives its claim.
    • The approved shape is "a cacheable mapping … for the claim's lifetime". The aperture lives as long as its handles. Nothing in Bound refers to it, and tear_down leaves it alone.
    • Failure scenario: tgl-display maps the window, then drops or moves on its claim handle (Device carries TRANSFER). The new holder asks again and gets its own writable window, while the old process still writes the OpRegion's mailboxes. The same happens if the old process closes its claim and a later claim is minted.
    • This is the defect recorded for BARs in issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md. It is not recorded for this window, and the PR presents only the handle's missing DUP/TRANSFER.
    • Either tie the window to the claim, or take the lifetime back to the orchestrator. The issue's own text says the tracker has not decided what ending a mapping means when its claim ends. Then record the window in that issue with its exit.
  2. kernel/src/pcidev/mod.rs:1425, :1454: #[cfg(target_arch = …)] in generic kernel code.
    • The repository rule puts target_arch in architecture modules and selectors only.
    • The x86 body also relies on an x86-only fact: the direct map covers reserved memory up to direct_map_end. On AArch64 it does not (lib/bootmap/tests/aarch64_direct_map.rs).
    • Move opregion into arch/x86_64, with the AArch64 NotSupported behind the arch selector.
  3. sdk/abi/src/syscall.rs:2655, sdk/toyos/src/syscap.rs:76, kernel/src/pcidev/mod.rs:1380: a false authority contract.
    • The docs say the claim must be "minted and not yet endowed" and one "this capability minted". sys_device_firmware_grant accepts any PCI claim the caller holds with WRITE, from any DEVICE holder, at any time.
    • The test images give device to non-supervisor rows (testcases.toml:72, virtclaimcase.toml:20). Such a row can grant a window on a claim it has already endowed or received.
    • Either make the docs state what the kernel checks, or enforce the contract. The simpler enforcement is to carry the window mask in the mint's unused second selector word (sys_device_claim already refuses it non-zero). That deletes SYS_DEVICE_FIRMWARE_GRANT, its wrapper and SysCap::grant_firmware, and the "minted, not yet granted" state goes with them.
  4. sdk/abi/src/syscall.rs:371: the stale guard.
    • assert!(SYS_CLOCK_NOW < SYSCALL_PROFILE_OTHER) still names 152. The highest number is now SYS_DEVICE_FIRMWARE_WINDOW (154).
    • Name the last syscall, or the guard no longer checks the top number it exists for.
  5. Evidence: the T14 readback (igd_opregion_window) has not run.
    • Everything the kernel does with real firmware is unverified until it runs: ASLS, the memory type in the T14's map, the direct-map read, the WB aperture, and the driver's parse of real MBOX and OVER values.
    • Run it at the landing head before landing. Report the logged version and mailboxes. See optional item 4 for why MBOX can fail.

Optional

  1. lib/userbound/src/window.rs:79–114: two passes where one would do. The size is fixed at 8 KB, so the header page needs no separate decision. Decide asls..asls+8K once, then read the 20 header bytes inside it. That deletes opregion_header, the second firmware_kept pass and one Window.
  2. kernel/src/syscall/device.rs:253: a new unsafe block. Declare WindowGrant with toyos_abi::user_safe! and write toyos_abi::usersafe::bytes(&grant), keeping the early user_bytes_mut. The macro proves there is no padding, so the manual const _ assertion goes too.
  3. sdk/abi/src/pci.rs:97 against lib/userbound/src/window.rs:36: one word, two meanings. On one path bytes is the window's length in WindowGrant and the whole-page span in Window, which also has len. Rename WindowGrant::bytes to len.
  4. system/tgl-display/driver/src/opregion.rs:22, :58: reserved bits are refused. Revision 0.5 is a 2016 Skylake document. The T14 is Tiger Lake, and its OVER may name a later interface that defines MBOX bit 5 or higher. Refusing reserved bits then rejects a valid region and reds the judge. Ignore unknown bits, require only bit 3, and log the raw word.
  5. lib/manifest/src/lib.rs:313, system/supervisor/src/main.rs:1221: a check repeated after validation. parse already panics on an unknown firmware word, so the supervisor's unwrap_or_else(panic!) cannot be reached. Store Vec<FirmwareWindow> once it is validated.
  6. lib/userbound/src/window.rs:20–21: a private copy. EFI_RESERVED and EFI_ACPI_NVS copy firmware.rs:81,84 in the same crate. Share them.
  7. sdk/toyos/src/device.rs:152: a page-size literal. Replace next_multiple_of(4096) with PAGE_SIZE, or pass offset + bytes to adopt and drop the rounding.
  8. system/tgl-display/src/main.rs:54–56: the mapping is kept for future work. It is held for a later stage that does not exist yet. Drop it after the copy, along with its comment.
  9. kernel/src/pcidev/mod.rs:1430: the device check uses exact extents. map_mmio retypes whole 2 MiB leaves of the direct map, but DRIVEN records exact extents. A reserved page beside a driven window would be UC in the direct map and WB in the aperture. That cannot happen on the T14's layout. A fence costs one lookup: direct_map_policy(start) == Normal for each leaf the window covers.
  10. tests/toyos.rs:2023: a redundant refusal list. Any refusal also removes the positive line must_say requires. The four copied literals only improve the error message. Drop them, or print the log on failure.
  11. No negative test of the authority check. The only kernel-side test is the T14's positive path. A virtclaimcase guest test would hold the refusals cheaply: PermissionDenied on an ungranted claim, and NotSupported for a grant to QEMU's non-Intel class-03 function. Inspection does establish both today.

OpRegion facts against rev 0.5

These match the specification as cited:

  • the header layout: SIGN 00h, SIZE 10h in KB = 8, OVER 14h, MBOX 58h;
  • MBOX bits 0–4 for mailboxes 1–5, with the VBT in mailbox 4, 6 KB at 400h;
  • ASLS at FCh.

Tests and evidence

  • At 4be4b0f5b: host CI exit 0 (108 steps, with the new tests visible in the log) and build-only exit 0.
  • The three negative controls are real and fail the right tests. They ran on the tree before the clippy rename, which touched neither package.
  • The kernel glue was not exercised anywhere: firmware_window, the syscalls and the aperture.

SEND BACK

@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1, addressed at 990eb5633 (rebased onto main d057f2ada). Evidence: https://github.com/ToyOSOrg/evidence/blob/0461662003b7e8c28ed1cec1a93855443ac74e50/r2/README.md

Required

  1. The window ends with its claim, and so does every BAR mapping, by the orchestrator's ruling.
    • The claim records each aperture it hands out (Bound::handed, weak).
    • pcidev::revoke_handed, the one function every end of a claim calls, takes each back from every space that maps it. It uses the revoke an aperture's last handle already does (SharedMemObject::revoke).
    • It runs at the claim's last handle (release, the holder's death) and when the claim moves, by IPC or a spawn's endowment, after the locks are let go. It takes the binding's lock directly, so a faulted function's mappings go too.
    • A function's departure is PCI root bridges declared by acpiserver, device lifecycle and native hot-plug #934's, which calls the same function; the BAR issue records that.
    • bar_map_again now keeps a BAR's handle past its claim's end and requires the map refused Gone. QEMU PASS. With the revoke removed it fails (mutation-no-revoke).
  2. The arch body moved:
    • arch::opregion::window, x86_64 and AArch64.
    • The AArch64 one is never reached, since the mint grants the window only to an Intel display function.
    • Generic code has no target_arch.
  3. Granted at the mint:
    • The window mask rides the mint's second selector word (DeviceRequest::Function(arrival, FirmwareWindows), SysCap::claim_function_granted).
    • SYS_DEVICE_FIRMWARE_GRANT, its wrapper and SysCap::grant_firmware are deleted, so "minted, not yet granted" no longer exists.
    • A window the function lacks refuses the mint, so a misconfigured row fails its start by name.
  4. The guard names SYS_DEVICE_FIRMWARE_WINDOW. That is 160, above main's SYS_MESSAGE_RECV (159), checked at the rebase.
  5. T14: restaged at this head (fwwin-t14/request.txt).

Optional

  • Taken:
    • 1: one pass. window::opregion decides the 8 KB, and opregion_header checks the bytes read inside it.
    • 3: WindowGrant::len.
    • 4: MBOX needs bit 3 only, and OVER and MBOX are logged raw.
    • 6: the EFI constants are shared.
    • 7: adopt(offset + len).
    • 8: the mapping is let go after the copy.
    • 9: each 2 MiB leaf must be cacheable in the direct map.
    • 10: the judge's refusal list is gone.
    • 11: partly. The revocation has its guest check above. The ungranted PermissionDenied and the non-Intel NotSupported stay established by inspection.
  • Declined:
    • 2: RawHandle is a tuple struct and not UserSafe, so user_safe! can't declare WindowGrant. The documented unsafe copy stays.
    • 5: Program::firmware stays words. Typing it would move the render's refusal into the build.

Host CI: exit 0, 108 steps green, at 82a688be4. Its tree differs from the head only by bar_map_again's check, which the QEMU run covers.

@Japabu Japabu changed the title kernel, supervisor: a claim's firmware window, granted only by the signed image's row; tgl-display reads the IGD OpRegion kernel, supervisor: a claim's firmware window, granted at its mint by the signed image's row, and every claim's mappings ending with it Nov 1, 2026
@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1137 at 990eb56

Architecture

The r1 shape is in place:

  • The window mask travels in the mint's second selector word, and the grant syscall, its wrapper and SysCap::grant_firmware are gone.
  • Every aperture the PCI claim hands out, BARs included, is recorded on Bound::handed and taken back through the existing SharedMemObject::revoke.
  • The x86 body is in arch::opregion, and generic code has no target_arch.
  • The guard names 160.

I found no way for userland to panic the kernel on these paths. An in-flight shm_map loses cleanly to a revoke: mapped_at answers Gone and the map is undone. A fault on the aperture during a revoke is serialized by the space lock. Duplicates of an aperture are the same object, so they are covered. The shootdown is the one release already does, and spinners poll it.

Two things block landing. First, the pushed commit is not the rebase the PR describes. Second, the revocation on move and on release has two races that defeat the property the PR claims.

The diff against d057f2ada is +798/−50. About 140 of those lines are tests.

Required

  1. The head commit's parent is wrong: as pushed, the PR reverts tgl-display: the DMC's program loaded, PG3-PG5 down and DC6 allowed (stage 2) #1132, display-output: the compositor presents to the display driver, and tgl-display answers at vblank (stage 3) #1134 and tcp ramp: the 100 ms miss is below the bench's resolution; osbench reports each round's marks #1138.

    • 990eb5633's parent is 4768c7650 (main at tcp ramp: the 100 ms miss is below the bench's resolution; osbench reports each round's marks #1138), but its tree is d057f2ada plus this change.
    • Against its parent, it deletes the DMC program and dmc.rs, interrupt.rs, dc_tests.rs, lib/display-output, the compositor's output.rs, tone_after_dc6, the Intel firmware licence and the NOTICE rows, and the ramp marks in osbench/src/lan.rs.
    • GitHub shows +846/−2220 over 60 files, CONFLICTING.
    • Rebase onto current main (9b3711cdb) for real. system/tgl-display/src/main.rs and driver/src/lib.rs need opregion() re-applied over stages 2 and 3. Metalcase's tgl-display row keeps receives = ["display-output"] and adds firmware.
    • The evidence (host CI, bar_map_again, the T14 image hash f36b9e24…) is of the old tree. It must be retaken at the rebased head, because tgl-display's main changed under it.
  2. kernel/src/syscall/ipc.rs:473: a move's revoke runs after the receiver can already use the claim, and takes the receiver's mappings back.

    • tx.send makes the message visible under the sender's lock. revoke_mappings runs after that lock is let go.
    • Failure sequence: a receiver already in recv on another CPU, or polling non-blocking, can take the claim and bar_map it before the revoke runs. It does not wait for post_lending. A preempted sender widens the window.
    • revoke_handed takes the whole handed list, so it revokes the new holder's fresh aperture. The new holder's next access faults and the holder dies.
    • Spawn is safe, because the child runs only after commit.
    • Fix: decide what a move revokes under the lock that does the move. For example, take handed inside the with_process_data closure before transfer, revoke it after, and put it back on a refused send. Alternatively, stamp each entry with a move generation.
  3. kernel/src/syscall/device.rs:191, :208 with object/device.rs:159: an aperture can be handed after the revoke it should have met.

    • on_function's own doc names the case: a sibling thread resolved the claim before its handle went.
    • On release, on_zero_handles runs revoke_mappings and lets the Held lock go, then reference.release() takes it again. Between the two, a sibling's bar_object or firmware_window can hand a fresh aperture. tear_down then drops handed unrevoked, and the old process maps the function's registers after its claim ended.
    • The same holds after an IPC or spawn move: a thread that resolved the claim before the move hands an aperture after the revoke, into the old holder's table.
    • Fix (one way): install the new aperture's handle only if the caller's table still holds this claim at handle (Arc::ptr_eq), under the same process lock. Removal or transfer happens under that lock, and the revoke follows it, so every aperture either met the revoke or is refused.
    • Revoking bound.handed inside release would close the release half on its own. It is also where PCI root bridges declared by acpiserver, device lifecycle and native hot-plug #934's tear_down_departed needs it.
  4. kernel/src/pcidev/mod.rs:1393: false contract.

    • "The one function every end of a claim calls: … and its function's departure." Nothing calls it on a departure at this head.
    • Delete the clause. The BAR issue already records the gap.
  5. issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md:23: stale record.

    • "No guest test reads it yet" is no longer true. bar_map_again now reads one case: a handle kept past the claim's end maps Gone.
    • State what is still unread: a live mapping faulting after release, and either kind of move.
  6. T14 evidence (r1 required 5) has still not run. It is staged, exit 2. Run it at the rebased head before landing, and report the logged OVER and MBOX.

Optional

  1. kernel/src/device.rs:171: a refused window resets the function. grant_firmware runs after pcidev::claim has brought the function up, armed it and attached its domain. The refusal then runs the full tear_down, including the reset. Vendor and class are in Machine::identities, so the bug fence can be decided before claim() touches the function. That is the rule bring_up states.
  2. kernel/src/device.rs:170: an unknown window bit answers NotSupported, after demand_syscap. It is a malformed selector word. Refuse it as InvalidArgument in sys_device_claim with the other words.
  3. kernel/src/arch/aarch64/opregion.rs:7: "Never reached … which no machine of this architecture has." An Intel discrete display card (8086, class 03) can sit in an AArch64 machine's slot. The module header's reason, that the direct map does not reach reserved memory, is the true one. Keep that and drop the "Never reached" doc. The PR body repeats the claim.
  4. sdk/abi/src/pci.rs:68: "granted only by the signed image's manifest (firmware = [...])". The kernel checks a DEVICE SysCap at the mint. Any such holder, such as the test rows with device, can set the bit without a firmware row. Say what the kernel checks.
  5. The move paths have no guest check. A few lines in bar_map_again would hold the move code the fix to item 2 rewrites: keep a BAR handle, send the claim to self over a connection (or endow a child), then require shm_map to answer Gone. Adding the new line to the judge's answered list would also make the PASS show it ran. Today only the exit code and the mutation do.
  6. Commit title: "every claim's mappings ending with it". The framebuffer claim's scanout aperture is not covered. Scope the title to PCI claims.
  7. PCI root bridges declared by acpiserver, device lifecycle and native hot-plug #934's departure hook.
    • revoke_handed takes &Binding, which the departure path does not have. It needs a &Function or number form.
    • It must run from refresh (thread context, under REFRESHING), not from the hot-plug ISR's depart. That ISR takes no sync::Lock, and a shootdown cannot run there.
    • From refresh the order is REFRESHING → bound → the aperture's Held → space. Nothing in either branch takes those in reverse, and revoke_handed takes no MACHINE. It is safe as long as it is not called with MACHINE held.
  8. Bound::firmware: u8 duplicates FirmwareWindows. FirmwareWindows::iter is a third list of windows beside from_raw and KNOWN.

r1 items

  • 1 (lifetime): partly. Recording, the one function and the lock taken directly are done. Required 2 and 3 remain.
  • 2 (arch): resolved.
  • 3 (mint): resolved. A lacking function refuses the mint, with optional 1 noted.
  • 4 (guard): resolved.
  • 5 (T14): open.

Tests and evidence

  • The bar_map_again claim is valid for the release case of an unmapped handle. With revoke_mappings removed from on_zero_handles, the new assert fails, exit 101 (mutation-no-revoke).
  • It does not read a live mapping being taken away, an IPC move or a spawn move.
  • All evidence is of the mis-parented tree. See required 1.

SEND BACK

Japabu added a commit that referenced this pull request Nov 1, 2026
…nd 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 <noreply@anthropic.com>
@Japabu Japabu changed the title kernel, supervisor: a claim's firmware window, granted at its mint by the signed image's row, and every claim's mappings ending with it 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 Nov 1, 2026
@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2, addressed at 2831e6e52. Evidence: https://github.com/ToyOSOrg/evidence/blob/38d423204f8e1e3e574bfb159ab9e12fa1999e85/r3/README.md

Required

  1. Rebased for real. 31cfd2754 is r2's change (d057f2ada..990eb5633) applied onto 9b3711cdb.
    • opregion() runs over stages 2 and 3.
    • Metalcase's row keeps receives and adds firmware.
    • git diff origin/main...HEAD --name-only lists exactly r2's 31 files. GitHub shows MERGEABLE.
  2. The move takes the claim's handed list under the sender's lock, before transfer.
    • A refused send restores it, and a successful one revokes it after the lock.
    • The receiver can see the claim only after tx.send. Its apertures go on the emptied list, so they are never revoked by the sender.
    • Spawn takes the list the same way, under the parent's lock.
  3. install_handed. A BAR's or window's handle is installed only if the caller's table holds this claim at handle (Arc::ptr_eq), under the same process lock.
    • A claim carries no DUP, so it has one handle in one table.
    • A release or move removes it under that lock and revokes after, so every aperture met the revoke or is refused Gone. A refused object never gets a handle and ends unmapped.
  4. The departure clause is deleted.
  5. The issue now states what bar_map_again reads and what it does not.
  6. The T14 boot is restaged at the head. display_output_presents rides the same boot, since tgl-display's main changed.

Optional, taken

  • 1: the fence is decided from Machine::identities in pcidev::claim, before anything is touched.
  • 2: an unknown bit is InvalidArgument in sys_device_claim.
  • 3: the AArch64 stub keeps only the direct-map reason.
  • 4: the ABI doc says what the kernel checks.
  • 5: bar_map_again now sends its claim to itself. A kept BAR handle maps Gone, and the new holder's fresh BAR reads the device. Both lines are in the judge's list. Dropping the move's revoke fails it (exit 101).
  • 6: the title is scoped to PCI claims.
  • 7: pcidev::revoke_handed(number) is the form PCI root bridges declared by acpiserver, device lifecycle and native hot-plug #934 calls from refresh. Handed::revoke documents thread context only, never the ISR and never with MACHINE held.
  • 8: Bound::firmware is FirmwareWindows, and FirmwareWindow::ALL is the one list.

🤖 Generated with Claude Code

Japabu added a commit that referenced this pull request Nov 2, 2026
…nd 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 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 2, 2026
…se's display row is granted its window, and the PSR restart point is the durable brief

Rebased onto main c81fc83. #1062 took syscall 160 and #1116 took 161-163,
so SYS_DEVICE_FIRMWARE_WINDOW is 164, the first commit's message
notwithstanding. 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 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3, at adc35d850, rebased onto main c81fc83eb.

Ready for the focused review of r2's fixes and this round.

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review r3 of #1137 at adc35d8

Architecture

The shape r2 accepted is unchanged. The r2 fixes put the move's revoke decision under the lock that does the move, and they gate every aperture install on the claim still being held. That is the smallest design that closes both races. It adds no new state: one Vec<Weak> on Bound, which was already there, and a Handed token. Net change against c81fc83eb is +945/−64 over 32 files. About 70 lines are tests: bar_map_again +45, the manifest test, and the opregion and window unit tests.

r2 required items

  1. Rebase: resolved.
  2. Move: resolved.
    • In ipc.rs send, all of these happen inside one with_process_data closure: take_mappings runs before transfer, a refused transfer restores, and a successful one hands Handed out. revoke() runs only after the lock is let go.
    • A receiver can reach the claim only after tx.send, so every aperture it is handed lands in the emptied list, and the sender's revoke cannot reach it.
    • A sibling's bar_object that runs between take and restore is kept, because restore uses extend.
    • Spawn's commit takes the list in the same loop that removes the handle under the parent's data, and revokes after drop(data). No error return follows the take, and the child runs only after commit.
    • Lock order: the move path is process → claim Held → bound. None of the on_function/pci closures takes the process lock, so I found no cycle.
  3. Install gate: resolved.
    • install_handed checks Arc::ptr_eq at handle under the process lock.
    • Release is close or close_all, which remove under that lock, and the zero-handle hook is deferred after it. A move removes under it too, as above. Program order gives hand → install → removal → take, so any aperture installed was in the list the revoke took. Anything handed later fails the gate with Gone.
    • A refused entry is dropped inside the closure. Its own zero-handle revoke is deferred, so no hook runs under the lock, and the object was never mapped.
    • If the slot is reused by the same claim coming back, the caller legitimately holds it again.
    • Teardown runs only after every sibling has left.
    • The race is closed exactly.
  4. pcidev contract: resolved where r2 pointed, and reintroduced at revoke_handed. See required 1 below.
  5. BAR issue: resolved. It states what bar_map_again reads and what remains unread: a live mapping faulting, spawn's endowment, and the races.
  6. T14: run. At 2831e6e52, rc=0: OVER 02010000, MBOX 0000001d, and display_output_presents PASS. Reuse at adc35d850 is justified by the range-diff. Whether to boot again after main's sched: a thread runs only on the CPUs its process's mask names #1116 is the orchestrator's call.

Optional r2 items taken

  • Fence before touching the function: done. pcidev::claim decides from Machine::identities before the guard, driven and bring_up. The function is not reset.
  • Unknown bits: done. sys_device_claim answers InvalidArgument before demand_syscap. The expect in try_claim is reachable only through that syscall, which is its only caller, so no userland input reaches it.
  • AArch64 comment: done, and it is true.
  • ABI doc: done. It says the kernel checks only DEVICE.
  • revoke_handed(number) for PCI root bridges declared by acpiserver, device lifecycle and native hot-plug #934: correct form.
    • It takes the record under bound alone, with no MACHINE.
    • Handed::revoke documents thread context only and no MACHINE.
  • MBOX: accepting the bits the 2016 revision reserves is right. The driver relies only on bit 3, the T14 sets bit 4, and refusing would fail real firmware for a field nothing reads. The unit test holds it.

I found no userland path that panics the kernel. ASLS arithmetic is u64 from a u32. The window is page-aligned by construction, so aperture's assert holds. The user output is length-checked.

Required

  1. kernel/src/pcidev/mod.rs:1436-1439: the false departure clause is back.
    • "revoked: at the claim's last handle, and at its function's departure, which has no binding." Nothing calls it on a departure at this head. This is the clause r2 required 4 had deleted.
    • It also contradicts the BAR issue, which says a departing function's mapping outlives the departure until PCI root bridges declared by acpiserver, device lifecycle and native hot-plug #934.
    • Fix: state only the contract, for example "Every aperture function number's claim handed out, revoked. By number, for a caller with no binding." Keep the Handed::revoke terms.
    • This is a comment-only change and needs no rerun.

Optional

  1. kernel/src/object/device.rs:165: on_zero_handles revokes inside self.pci, under the claim's Held lock.
    • That breaks pci's stated contract at :99, "So f does not wait".
    • It is safe, because lock spinners poll shootdowns. The move paths, however, revoke outside every lock.
    • Either take inside and revoke outside, or narrow the pci doc to say what it actually forbids.
  2. sdk/abi/src/syscall.rs:2777: device_bar_map's ABI doc does not state the new contract.
    • The new contract: the object, and every mapping of it, is revoked when the claim ends or moves, after which a map answers Gone and a live mapping faults.
    • This covers a BAR handle sent alongside the claim in the same message, which the receiver may map before the sender's revoke lands.
    • That case is userland death by design, not a kernel fault, but a driver author needs one sentence saying so.
  3. kernel/src/syscall/device.rs:117: the doc still lists the selector as "a PCI function's arrival, an ISA set". The inline comment at :125 covers the second word. One clause in the doc would make it whole.
  4. Commit 477d0d18b's message is now false history. It says "(160)", "AArch64 has no such machine", and calls revoke_handed "one function" that runs on every move and that PCI root bridges declared by acpiserver, device lifecycle and native hot-plug #934 wires to departures. The last commit corrects only the number. Main keeps branch commits, so consider rewording it on this own branch.
  5. PR body (for the orchestrator).
    • "The lock order is bound, then the aperture's Held, then its spaces": bound is let go before the revoke, so no nesting exists.
    • "In metalcase, and the T14 row …" is garbled.

Tests and evidence

  • Host CI at adc35d850: exit 0, 109 steps. The earlier ENOSPC run is recorded as void.
  • bar_map_again (QEMU x86_64): PASS. The judge requires both new lines.
  • The move control: without the move's revoke, the test fails at the move assertion (r3, at 2831e6e52). It still applies, because the move code is identical at this head.
  • What the test does not read: a self-send cannot interleave, so the test checks that the move revokes, not that it revokes only the sender's list. The ordering fix rests on the inspection above, which I find sufficient. A spawn move and a live mapping faulting are unread. The issue records both.

SEND BACK (one comment-only fix, required 1)

Japabu and others added 4 commits November 2, 2026 06:39
… the signed image's row, and every PCI claim's mappings ending with it

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 <noreply@anthropic.com>
…nd 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 <noreply@anthropic.com>
…se's display row is granted its window, and the PSR restart point is the durable brief

Rebased onto main c81fc83. #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 <noreply@anthropic.com>
…, 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 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review r3, addressed at 577298fad.

  • Required: revoke_handed's doc states only its by-number contract: thread context only, and never with MACHINE held. The departure clause is gone.
  • Optional, taken:
    • device_bar_map's ABI doc says the 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's doc names the selector's window bits and their refusal.
    • The first commit's message says 164 and makes no departure claim, and the r3 commit drops "the first commit's message notwithstanding".
    • The PR body states the lock order as the code takes it, and the garbled metalcase sentence is fixed.
  • Checks: the trees of the message rewrites are byte-identical, and the code change is doc comments only. So host CI and bar_map_again at adc35d850 stand, and nothing was rerun.

🤖 Generated with Claude Code

@Japabu
Japabu marked this pull request as ready for review November 2, 2026 05:43
@Japabu
Japabu added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit a0e8f13 Nov 2, 2026
10 checks passed
@Japabu
Japabu deleted the firmware-window branch November 2, 2026 06:07
Japabu added a commit that referenced this pull request Nov 3, 2026
…d out

Resolutions:

- sdk/abi/src/syscall.rs: main's SYS_DEVICE_FIRMWARE_WINDOW keeps 164;
  SYS_DEVICE_SET is 165 and SYS_DEVICE_SET_READ 166, with 168 profile bins.
- issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md: main's,
  which names this PR's departure as what is left; the next commit wires it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 3, 2026
#1137 revokes what a claim handed out at its last handle and when it moves.
A function that leaves from behind a hot-plug slot while its claim is still
held kept its BAR mappings until that claim was released, and the next
arrival behind the port may be assigned the same addresses.

`slot::refresh`'s per-record loop, which consumes each departure, now takes
the departed claim's `Handed` there, under `REFRESHING`, where it can only
be the departed claim's: an arrival on the record is listed in a later
refresh, after §6.6.1's wait. It is revoked once `REFRESHING` is let go, on
the reader's thread with no `MACHINE` held, because the revoke's shootdown
waits on other CPUs and `REFRESHING` disables preemption. That is
`revoke_handed`'s take and revoke split around the lock; never the slot's
interrupt.

`pciholder` maps its BAR 0 before the pull and keeps the handle; once the
function has left, mapping that handle again answers `Gone`, which
`a_function_arrives_and_leaves` now requires (`kept=Some(Gone)`). The BAR
issue's departure item and the track's gap are closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant