Repository navigation
A claim keeps where its BARs are and no object over them: a BAR asked for again after its handle closed no longer panics the kernel, and two answers held at once map apart - #761
Conversation
… red on a kernel panic A guest job claims QEMU's virtio NIC on tests/testcases, asks for one of its memory BARs, closes the handle it is given and asks for the same BAR again on the same live claim. The kernel owes it a handle or a refusal. At b432ed2 it panics instead, `a handle to a retired SharedMem`, at kernel/src/object/handle.rs's constructor: the claim's cached BAR object was retired when its only handle closed, and the second request installs it again. `cargo test --test toyos-build -- bar_map_again` exits 1 on that panic. The test is red by design and is deleted by a later commit of this branch, with its issue naming the revert that restores it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
… asked for as often as its holder likes `pcidev::bar_object` made one `SharedMemObject` per memory BAR on the first `SYS_DEVICE_BAR_MAP` and kept it in the claim's `Bound`, to answer every later ask with "the same object". An object's life is its handles': the last one to close retires it, and `HandleEntry::new` asserts nobody installs it again. The claim outlived that, so ask, close, ask handed the retired object to `ops::install` and panicked the kernel from an unprivileged holder of a live claim. An ask a full handle table refused retired the object the same way without a close. The claim now keeps only what it already kept, `bar_at` and `bar_bytes`, and every ask makes an object of its own over that window, the rule `device::Screen` already states for the framebuffer's buffers. The second ask answers a fresh mapping and not a refusal: the claim handle is the authority over the window, a `SharedMem` handle derived from it is closed without spending that authority, and a refusal would be a per-BAR "was asked" bit kept only to say no. Two live objects over one window are two uncacheable mappings of registers the holder already drives. `bar_map_again` comes back and now reads the function's `device_feature` through each answer's mapping: after a close, and after an ask refused at a full table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Found reading `pcidev::tear_down` for the BAR object's owner: read, not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Negative-control patches, applied onto Control 1 — the whole kernel change reverted ( diff --git a/kernel/src/object/mod.rs b/kernel/src/object/mod.rs
index 50f57f508..205029806 100644
--- a/kernel/src/object/mod.rs
+++ b/kernel/src/object/mod.rs
@@ -2,8 +2,6 @@
//!
//! Objects are plain `Arc<T>`: no custom refcounting, no `Weak`, no `dyn` hierarchy.
//!
-//! An object ends with its last handle and is never handed out again: a holder that outlives the handles it answers with keeps what an object is made over (`device::Screen`, a claim's BAR windows) and makes a fresh object for each.
-//!
//! `handle_count`, not the Arc strong count, is what userland-visible lifecycle rides: a syscall's `Arc` can be stranded on a killed thread's kernel stack, so release is deferred through [`ZERO_QUEUE`] — see `issues/deferred-release-outlives-its-syscall.md`.
// `warn` here gates via CI's `-D warnings`; the rest of the kernel is not yet swept.
diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs
index f6de66efc..1590f7a85 100644
--- a/kernel/src/pcidev/mod.rs
+++ b/kernel/src/pcidev/mod.rs
@@ -238,6 +238,9 @@ struct Bound {
/// advertises; 0 bytes is a slot with no BAR this claim may map.
bar_at: [u64; BARS],
bar_bytes: [u64; BARS],
+ /// Minted on the first map of that BAR, so a second answers the same
+ /// object rather than a second handle to one window.
+ bars: [Option<Arc<SharedMemObject>>; BARS],
grants: Vec<Grant>,
/// The [`RESIDUE`] this claim took over and has placed no grant at: mapped
/// to nothing and counted against nothing, and where a grant of a range's
@@ -804,6 +807,7 @@ fn bring_up(pci: PciDevice, id: PciId, slot: usize) -> Result<Bound, Refusal> {
id,
bar_at,
bar_bytes,
+ bars: [const { None }; BARS],
grants: Vec::new(),
residue: Vec::new(),
mastering: false,
@@ -1493,25 +1497,26 @@ fn with_bound<T>(
f(bound)
}
-/// One memory BAR as an object to map, a fresh one for every ask.
-///
-/// **The claim keeps where the window is and never an object over it**: an
-/// object ends with its last handle, which its holder closes whenever it
-/// likes, and the claim outlives that.
+/// One memory BAR as an object to map; the same object every time.
pub fn bar_object(slot: usize, index: u64) -> Result<Arc<SharedMemObject>, SyscallError> {
with_bound(slot, |bound| {
let index = usize::try_from(index).map_err(|_| SyscallError::InvalidArgument)?;
if index >= BARS || bound.bar_bytes[index] == 0 {
return Err(SyscallError::InvalidArgument);
}
- Ok(SharedMemObject::over(Region {
+ if let Some(object) = bound.bars[index].as_ref() {
+ return Ok(Arc::clone(object));
+ }
+ let object = SharedMemObject::over(Region {
phys: DirectMap::from_phys(bound.bar_at[index]),
size: align_2m(bound.bar_bytes[index] as usize) as u64,
// Registers, and the memory type is not firmware's to decide.
cache: CachePolicy::Uncacheable,
// The kernel owns no pages here: this is a device's aperture.
pages: None,
- }))
+ });
+ bound.bars[index] = Some(Arc::clone(&object));
+ Ok(object)
})
}
diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs
index 53ca1e35f..3e3260d78 100644
--- a/toyos-abi/src/syscall.rs
+++ b/toyos-abi/src/syscall.rs
@@ -2312,8 +2312,7 @@ pub fn pipe_map(handle: RawHandle) -> Result<*mut u8, SyscallError> {
/// kernel's, and a process that could write it could aim the device's interrupt
/// at any address the LAPIC decodes.
///
-/// Every call answers an object of its own over the same window, so a BAR
-/// whose handle was closed can be asked for again.
+/// Idempotent per BAR: a second call answers the same object.
///
/// A window alone drives nothing: the function masters the bus from its first
/// [`device_dma_alloc`] and not before.Control 2 — applied on top of control 1: the job's close-then-ask arm removed, so the first ask is the one a full handle table refuses ( diff --git a/tests/toyos-rust-tests/src/bin/bar_map_again.rs b/tests/toyos-rust-tests/src/bin/bar_map_again.rs
index 5823bed50..a92c76f64 100644
--- a/tests/toyos-rust-tests/src/bin/bar_map_again.rs
+++ b/tests/toyos-rust-tests/src/bin/bar_map_again.rs
@@ -50,10 +50,7 @@ fn main() {
let bytes = info.bar_bytes[bar];
let bar = bar as u32;
- let first = ask(&nic, bar, bytes);
println!("bar_map_again: BAR {bar} asked for and its handle closed; asking again");
- let again = ask(&nic, bar, bytes);
- assert_eq!(again, first, "bar_map_again: the second ask's mapping is another window");
println!("bar_map_again: answered with a handle that maps");
// No room for the handle: the ask is refused, and the object made for it
@@ -78,7 +75,6 @@ fn main() {
"bar_map_again: an ask from a full table was not refused for room"
);
println!("bar_map_again: an ask from a full handle table refused; asking again");
- let after = ask(&nic, bar, bytes);
- assert_eq!(after, first, "bar_map_again: the ask after the refusal maps another window");
+ let _ = ask(&nic, bar, bytes);
println!("bar_map_again: answered after the refusal with a handle that maps");
} |
…he kernel Its exit is met. A BAR asked for again on a live claim, after every handle to an earlier answer has gone, is answered with a handle that maps, and `bar_map_again` is green and reads both arms: the close, and the install a full handle table refused, which the issue had read and not run. With the kernel change reverted the test is red on the recorded panic, `a handle to a retired SharedMem (koid 197)` at `src/object/handle.rs:108:9`, on either arm. The rule it carried is in `kernel/src/object/mod.rs`'s header: an object ends with its last handle and is never handed out again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Review round 1 of Net lines ( BLOCKER
NOTE
What the brief asked to be held hardest
Whole guest suite: not run at this head, as the body says. I do not hold that as its own BLOCKER: the function's only behavioural change is the second ask, and each QEMU-reachable caller's first ask is measured. SEND BACK |
…r outlives the earlier Review round 1 of #761. The fix makes one kernel state newly reachable — two live objects over one BAR window in one address space — and no arm of the job held two answers together: every ask dropped its mapping before the next. The job now opens with that arm: two `map_bar` answers held, their addresses unequal, the register read through both, the earlier dropped, the register read through the later again. The object header's sentence named every holder that outlives its handles; `DeviceInfo` is one that keeps objects and is sound because it answers once. The sentence now names the holder that answers more than once. The issue on a BAR mapping outliving its claim says what the fix changes for its exit: the claim holds no reference to what it minted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
Round 1 controls and logs, all at The new control: the whole kernel change reverted, the job as it stands (
|
|
Round 2 gates at the merged head
Control, two answers held ( Control 1, close then ask ( Control 2, refused then ask ( Not run at this head: |
|
Review round 2 of Net lines ( Round 1's BLOCKERs
BLOCKER
NOTENone. What the brief asked to be ruled on
What the landing rests on
SEND BACK |
Review, round 3 — head
|
…ted per ask One conflict, pcidev::bar_object: #761's body and doc, an object per ask and no cache, under this branch's signature, `binding: &Binding` and `with_bound(binding, ..)`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Changes no byte: the branch already carried #761's content, and `git diff 5d14696 HEAD` is empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
The defect
A process holding a live PCI claim panicked the kernel with three ordinary calls:
SYS_DEVICE_BAR_MAP, close the handle,SYS_DEVICE_BAR_MAPagain.pcidev::bar_objectmade oneSharedMemObjectper memory BAR on the first ask and kept it in the claim'sBound(bars) to answer every later ask with the same object. A kernel object's life is its handles': the last one to close retires it (HandleEntry's drop), andHandleEntry::newasserts that nobody installs a retired object again. The claim outlived the object it cached, so the second ask handed the retired object toops::install. An ask refused for a full handle table retired the cached object the same way with no close at all: the entry built for it is dropped.The fix, and what the second ask answers
The object's lifetime belongs to each ask, not to the claim. The claim keeps what it already kept,
bar_atandbar_bytes, andbar_objectmakes a freshSharedMemObjectover that window every time;Bound::barsis gone. The panic site is untouched: its assertion was right.The second ask answers a fresh mapping, not a refusal.
device::Screen"storesRegions rather thanSharedMemObjects because each claim mints its own object, and aSharedMemObjectis retired once its handle count reaches zero". The BAR cache was the one site that broke it.pci_slot: "the handle is the authority and the slot is what it names"). ASharedMemhandle derived from it is closed without spending that authority, as aSYS_DEVICE_DMA_ALLOCafter a closed grant handle is still answered.SharedMemhandle carriesTRANSFERand its last close can run under another process's lock.What changes for a caller: two asks while the first handle is still held are now two objects and two mappings of the same uncacheable window, where they were two handles to one object. The holder already drives those registers, and each object costs it a handle slot. No caller in the tree asks twice (
netstack's two drivers anddiskserver's NVMe each callmap_baronce). The ABI's doc comment said "Idempotent per BAR: a second call answers the same object" and now says what is true; a comment undertoyos-abi/srcchanges no identity.Production:
kernel/andtoyos-abi/are 11 insertions, 13 deletions.The same shape, checked
Every site that turns an object into a handle (
HandleEntry::new,ops::install,install_all), and every kernel-side holder of anArcto a handle-counted object:syscall/device.rssys_device_bar_mapsyscall/device.rssys_device_dma_allocdma_alloc's fresh region; the claim'sGrant::memorykeeps theArcfor the pages and never hands it out againpcidev::dma_mapobject/device.rsDeviceInfo::mint(framebuffer, HDA, virtio-sound buffers)described.bytes;install_allreserves room before anyHandleEntry::new, so a refusal retires nothing;remintis given fresh objects fromdevice::framebuffer_infodevice::Screen,drivers::hda::info,drivers::virtio_sound::infoRegions, a fresh object per claimobject/namespace.rsArc<Connector>s and never installs one:sys_namespace_openmakes a newConnectionprocess.rsProcessEntry::objectloader/start.rs, as the object's first handle and a second made while the first is heldinbox/mod.rsCompletions::shm,file_backing.rsSharedImage::objectsyscall/ipc.rs(pipe, join, port create, port mint, namespace build, connect, accept, shm create, inbox setup),inbox/mod.rsaccept,loader/mod.rssupervisor's console andSysCap,loader/start.rsminted consolessys_handle_recv,HandleTable::transfer/duplicateInterruptandIrqWatchper slot, read through the claim handleFound on the way and filed, not fixed:
issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.md(read from the code, not run). It was true before this change and is not made worse by it.The test
bar_map_againis restored fromef41f0ad1and strengthened to the decision: it no longer accepts a refusal. The job claims QEMU's virtio NIC ontests/testcasesand reads the function'sdevice_featuredword through each answer's mapping:ResourceExhausted; empty it, ask again, map, read: the same dword. This is the arm the issue had read and not run.Why a guest test: the behaviour is a syscall sequence against a live claim on a real PCI function: a handle table, an object's retirement through the deferred queue, a BAR window mapped into a process. No type reaches it without changing the object model (
HandleEntry::newtaking a not-yet-retired witness is a larger design than this defect), the kernel library's host tests have no handle table over a claimed function, and the T14's one claimable function is its I219, where a red is the bench's machine in a panic.Checks (high-risk: devices, a capability boundary)
The head is
00b7a0c74:8a6dc0be3withorigin/mainatfb00abfa5merged in (#749, #757, #759, #762; a clean merge, #762's hunks inkernel/src/object/mod.rsandtoyos-abi/src/syscall.rsdisjoint from this branch's). Every row with an exit is measured at that head, by one script the orchestrator ran alone and serially from a clean tree; exit codes are the commands' own. Logs under the orchestrator's scratchpad,orch/barremap-r2/, their deciding lines in the round 2 comment below; the three control patches are byte for byte the ones in the round 1 comment, which apply to the merged head unchanged. Each control therefore reverts this branch's whole kernel change onto the base the green arm ran on, #762 included.cargo test --test toyos-build -- bar_map_again(green.log)control-kernel.patch), the job as it standscontrol-held.log)assert_ne!atbar_map_again.rs:63:two answers held at once are one mapping, both0xfffec00000; no kernel paniccontrol-heldarm.patch) so the ask after a close is reachedcontrol-1.log)PANIC: panicked at src/object/handle.rs:108:9: a handle to a retired SharedMem (koid 197), throughops::installfromsys_device_bar_mapcontrol-arm2.patch) so the first ask is the one a full table refusescontrol-2.log)cargo test --test toyos-build -- iommu_virtio_platform(guest-iommu.log)cargo run -- --ci host00b7a0c74:hostsuccess (job 113260522116),[ci] Host: 78 step(s), all greencargo run -- --ci guestguest / suitesuccess (job 113261645563),test result: ok. 32 passed, 32 total,PASS bar_map_again,[ci] Guest: 5 step(s), all green;toolchain / buildsuccess (113260522448)The script applied each control with
git apply --check, stacked, ran the test, and reversed each withgit apply -R(each exit 0;git status --porcelain --ignore-submodules=noneempty after). The first comment's patches were against72a18fa81and are superseded.Where this meets #762
#762 made a region's list of mappings a
Heldits zero-handle hook takes for good, so a retired region can never be mapped again and must never be handed out twice.pcidev::bar_objectmakes an object per ask andBoundkeeps none;sys_device_bar_mapis its only caller. The two fixes do not replace each other: the controls above run #762's kernel with this branch's change reverted, and still panic.What a green
bar_map_againshows of the two together: two objects over one frame in one address space map apart and both read the device; #762's hook (take().expect("the zero-handle hook runs once")) runs on the earlier object while the later is mapped, and on an object that was never mapped, the one the full table refused; and no object reachesSharedMemObject's drop with a mapping left (debug_assert!, on in the kernel's one profile). Any of those failing is a kernel panic, which the harness reads as red.Independent oracle: the recorded real failure. The audit measured this panic at
06bbc236dwith a test it wrote before any fix existed, and control 1 reproduces it message for message, koid included. The value read through each mapping is the device's own register, and the kernel's placement probe reads the same dword (toyos_pci::probe::reference) to prove the BAR decodes where it was put.The issue
issues/a-claims-bar-asked-for-again-after-its-handle-closed-panics-the-kernel.mdis deleted: its exit is met on both arms it names. Nothing else in the tree cited its slug (git grepat the head). Its durable rule is one sentence inkernel/src/object/mod.rs's header: an object ends with its last handle and is never handed out again, so a holder that answers more than once keeps what the object is made over. (DeviceInfokeeps objects and is sound because it answers once per claim.)issues/a-bars-mapping-outlives-the-claim-it-was-asked-on.mdsays what this fix changes for its exit: the claim holds no reference to an object it minted, so taking a window back needs the claim to learn what it handed out.Not run, and unsure of
bar_object's first ask are the ones that boot netstack on the I219 (userland/netstack/src/i219.rs):lan_lease_report,lan_dhcp_leaseandlan_message_delivery. That path's behaviour is unchanged by reading (one object over the same region either way); it is not measured on the T14 here.cargo run -- --ci hostand the whole guest suite were not run locally; both are read from CI run 37762030186 at this head (the rows above). That run merged this head intomainas it was before One workspace, one lock: the root, the kernel, the loader, userland and the SDK resolve together #746 landed; the merge queue runs both again on the treemainbecomes.syscall/dispatch.rs) unless another CPU's drain took the batch first (drain_zero_handles), in which case the read can precede the unmap; the job does not wait on that, and has no event to wait on. What it rests on by reading: the list the hook takes is its own object's field (self.mapped_in.take()),free_and_unmapgoes by virtual address, and a BAR's region haspages: None, so no frame is freed under the other window.Arcthat outlived its last handle answeringGoneis A shared region's last handle takes its list of mappings, and a map that finds none is refused #762's, held by its type; this job does not reach it.🤖 Generated with Claude Code
https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A