From e1abd00d6ae9a47c077bf79b81f5aa8708d9ce6a Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 10:29:26 +0200 Subject: [PATCH 1/4] Record the shm map that races its region's last close Carried from the kernel audit's branch, where it was traced and not executed, so that the fix that follows closes a file this branch holds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...lose-publishes-a-mapping-after-teardown.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md diff --git a/issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md b/issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md new file mode 100644 index 00000000000..abdc2fe5570 --- /dev/null +++ b/issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md @@ -0,0 +1,38 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# A shared-memory map racing the last close publishes a mapping after teardown + +`sys_shm_map` (`kernel/src/syscall/ipc.rs:361`) resolves the handle and clones +the object's `Arc` under the process-data lock, releases the lock, and then +calls `SharedMemObject::map_into` (`kernel/src/object/shm.rs:132`). `map_into` +never checks whether the object has retired; it allocates a writable mapping and +appends it to `mapped_in`. + +A cloned `Arc` held by a syscall is not a handle and does not postpone +zero-handle retirement. If a sibling closes the last handle after the clone and +before `map_into`, the object retires and `on_zero_handles` +(`kernel/src/object/shm.rs:172`) runs once: it takes `mapped_in`, finds it empty +and returns, and its queued `Arc` drops. When `map_into` then appends the new +mapping, nothing will ever tear it down — `on_zero_handles` has already run and +will not run again. + +With debug assertions on (the guest/test profile sets `debug-assertions = true` +in `kernel/Cargo.toml`), the final `Arc` drop trips the `debug_assert!` in +`SharedMemObject::drop` (`:186`) and panics the kernel. Without them, the +region's owned pages return to the PMM while the page-table entries stay +present, so the surviving process can read or write pages after another process +or the kernel receives them. This is distinct from the release-latency recorded +in `issues/deferred-release-outlives-its-syscall.md`: there memory is reclaimed +safely but late; here a mapping is created *after* the one teardown, so the +pages are freed while still mapped. + +**Exit**: a `SYS_SHM_MAP` that races the last close of its handle either maps +nothing or is torn down with the object; no shm object's pages are present in +any page table after its zero-handle teardown has run, and the kernel does not +panic. **Traced, not executed.** No model compiles the object layer, and staging the +window needs an actuator the kernel does not have: one that holds `sys_shm_map` +between its clone and `map_into` until a sibling releases it. From 73fd889138efd544f5fd9da74be91c20a597f486 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 10:34:13 +0200 Subject: [PATCH 2/4] A shared region's last handle takes its list of mappings, and a map that finds none is refused SYS_SHM_MAP resolves its handle and clones the object out of the table, drops the process-data lock, and maps. A sibling thread closing the region's last handle in between retired the object: the zero-handle hook emptied the list of mappings, once, and returned. The map then installed a writable mapping and appended it to a list nothing would ever look at again. With debug assertions the last Arc's drop panicked the kernel; on a shipping kernel the pages went back to the allocator still present in the caller's page table. The handle count owns the mappings; the Arc count owns the pages. The first half was a convention: the list was a Lock, and an emptied Vec accepts a push. It is now the object layer's Held, which every other deferred object already keeps its released resource in: the hook takes the list out and nothing is left to push to, and a map takes the same lock to find it, so the map either lands before the take and is torn down by it, or finds nothing and answers Gone having touched no page table. Held gains the two verbs that needs, take and with_mut. The other objects with a zero-handle hook were read for the same shape. Pipes, connections, inboxes and device claims hold their resource in Held; a port checks closed and queues under one lock. The device calls have a different one, a slot index that outlives its claim, which is issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md. Closes issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...lose-publishes-a-mapping-after-teardown.md | 38 -------- kernel/src/object/mod.rs | 14 ++- kernel/src/object/shm.rs | 95 +++++++++++-------- toyos-abi/src/syscall.rs | 3 +- 4 files changed, 67 insertions(+), 83 deletions(-) delete mode 100644 issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md diff --git a/issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md b/issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md deleted file mode 100644 index abdc2fe5570..00000000000 --- a/issues/an-shm-map-racing-the-last-close-publishes-a-mapping-after-teardown.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-08 ---- - -# A shared-memory map racing the last close publishes a mapping after teardown - -`sys_shm_map` (`kernel/src/syscall/ipc.rs:361`) resolves the handle and clones -the object's `Arc` under the process-data lock, releases the lock, and then -calls `SharedMemObject::map_into` (`kernel/src/object/shm.rs:132`). `map_into` -never checks whether the object has retired; it allocates a writable mapping and -appends it to `mapped_in`. - -A cloned `Arc` held by a syscall is not a handle and does not postpone -zero-handle retirement. If a sibling closes the last handle after the clone and -before `map_into`, the object retires and `on_zero_handles` -(`kernel/src/object/shm.rs:172`) runs once: it takes `mapped_in`, finds it empty -and returns, and its queued `Arc` drops. When `map_into` then appends the new -mapping, nothing will ever tear it down — `on_zero_handles` has already run and -will not run again. - -With debug assertions on (the guest/test profile sets `debug-assertions = true` -in `kernel/Cargo.toml`), the final `Arc` drop trips the `debug_assert!` in -`SharedMemObject::drop` (`:186`) and panics the kernel. Without them, the -region's owned pages return to the PMM while the page-table entries stay -present, so the surviving process can read or write pages after another process -or the kernel receives them. This is distinct from the release-latency recorded -in `issues/deferred-release-outlives-its-syscall.md`: there memory is reclaimed -safely but late; here a mapping is created *after* the one teardown, so the -pages are freed while still mapped. - -**Exit**: a `SYS_SHM_MAP` that races the last close of its handle either maps -nothing or is torn down with the object; no shm object's pages are present in -any page table after its zero-handle teardown has run, and the kernel does not -panic. **Traced, not executed.** No model compiles the object layer, and staging the -window needs an actuator the kernel does not have: one that holds `sys_shm_map` -between its clone and `map_into` until a sibling releases it. diff --git a/kernel/src/object/mod.rs b/kernel/src/object/mod.rs index 205029806b9..da60f23bcb1 100644 --- a/kernel/src/object/mod.rs +++ b/kernel/src/object/mod.rs @@ -37,16 +37,26 @@ impl Held { Self(Lock::new(Some(value))) } + /// What is held, for a release that is more than a drop; `None` after the first. + pub(crate) fn take(&self) -> Option { + self.0.lock().take() + } + /// Drops what is held, outside the lock; `T`'s destructor must not re-enter it. pub(crate) fn release(&self) { - let taken = self.0.lock().take(); - drop(taken); + drop(self.take()); } /// `f` over what is held, under the lock, or `None` after the release. pub(crate) fn with(&self, f: impl FnOnce(&T) -> R) -> Option { self.0.lock().as_ref().map(f) } + + /// [`with`](Self::with) for an `f` that changes what is held: the change and the release + /// are one lock's, so nothing is added to what a release already took. + pub(crate) fn with_mut(&self, f: impl FnOnce(&mut T) -> R) -> Option { + self.0.lock().as_mut().map(f) + } } impl Held { diff --git a/kernel/src/object/shm.rs b/kernel/src/object/shm.rs index e19eda42b91..9d84684c4ca 100644 --- a/kernel/src/object/shm.rs +++ b/kernel/src/object/shm.rs @@ -1,8 +1,12 @@ //! A region of memory more than one process can see. //! //! Holding a handle allows mapping it; giving one away is `SYS_HANDLE_SEND`. -//! Mappings are torn down when the last handle goes; pages are freed when -//! the last `Arc` goes, always later since a handle holds an `Arc`. +//! The handle count owns the mappings and the `Arc` count owns the pages: the +//! last handle takes the list of mappings away for good and tears each one +//! down, and the pages are freed when the last `Arc` goes, always later since +//! a handle holds an `Arc`. An `Arc` a syscall cloned out of its table can +//! outlive the last handle, so a map through one finds no list and is refused +//! under the lock the teardown took it with. use alloc::sync::Arc; use alloc::vec::Vec; @@ -12,10 +16,9 @@ use toyos_abi::syscall::SyscallError; use crate::mm::policy::{CachePolicy, Prot}; use crate::mm::{align_2m_checked, pmm, Unmapped, PAGE_2M}; use crate::process::{PageTables, Pid}; -use crate::sync::Lock; use crate::{DirectMap, UserAddr}; -use super::{KObjectVariant, ObjectCore, ZeroHandles}; +use super::{Held, KObjectVariant, ObjectCore, ZeroHandles}; /// Physical pages a region keeps alive; behind an `Arc` since one page set /// can back several objects. @@ -52,8 +55,8 @@ impl Region { pub struct SharedMemObject { pub(super) core: ObjectCore, region: Region, - /// Where this region is mapped, per process; the zero-handle hook empties it. - mapped_in: Lock>, + /// Where this region is mapped, per process; released by the zero-handle hook. + mapped_in: Held>, } impl SharedMemObject { @@ -68,7 +71,7 @@ impl SharedMemObject { Arc::new(Self { core: Self::new_core(), region, - mapped_in: Lock::new(Vec::new()), + mapped_in: Held::new(Vec::new()), }) } @@ -119,7 +122,7 @@ impl SharedMemObject { /// could otherwise write the same bytes while the kernel is still initialising. pub fn phys_before_mapping(&self) -> DirectMap { assert!( - self.mapped_in.lock().is_empty(), + self.mapped_nowhere(), "shm koid {}: the region is mapped into a process already, so a kernel \ write through the direct map is not exclusive", self.core.koid().raw(), @@ -127,50 +130,58 @@ impl SharedMemObject { self.region.phys } + fn mapped_nowhere(&self) -> bool { + self.mapped_in.with(|mapped| mapped.is_empty()).unwrap_or(true) + } + /// Map into `pt`, or answer the address it is already mapped at; - /// idempotent per process. + /// idempotent per process. `Gone` once the last handle has gone. pub fn map_into(&self, pid: Pid, pt: &PageTables) -> Result { - let mut mapped = self.mapped_in.lock(); - if let Some((_, _, vaddr)) = mapped.iter().find(|(p, _, _)| *p == pid) { - return Ok(vaddr.raw()); - } - let (addr, _) = pt - .lock() - .alloc_and_map(self.region.phys.phys(), self.region.size, Prot::ReadWrite, self.region.cache) - .ok_or(SyscallError::ResourceExhausted)?; - // Logged only for a non-default policy: this process is the one - // paying for it. Read back the installed policy, not the request, - // so the line describes the mapping. - if self.region.cache != CachePolicy::Normal { - let installed = pt.lock().user_policy(addr).expect("shm: just mapped"); - crate::log!( - "shm: {:#x} mapped {:?} into pid {}", - self.region.phys.phys(), - installed, - pid - ); - } - mapped.push((pid, Arc::clone(pt), addr)); - Ok(addr.raw()) + let map = |mapped: &mut Vec<(Pid, PageTables, UserAddr)>| { + if let Some((_, _, vaddr)) = mapped.iter().find(|(p, _, _)| *p == pid) { + return Ok(vaddr.raw()); + } + let (addr, _) = pt + .lock() + .alloc_and_map(self.region.phys.phys(), self.region.size, Prot::ReadWrite, self.region.cache) + .ok_or(SyscallError::ResourceExhausted)?; + // Logged only for a non-default policy: this process is the one + // paying for it. Read back the installed policy, not the request, + // so the line describes the mapping. + if self.region.cache != CachePolicy::Normal { + let installed = pt.lock().user_policy(addr).expect("shm: just mapped"); + crate::log!( + "shm: {:#x} mapped {:?} into pid {}", + self.region.phys.phys(), + installed, + pid + ); + } + mapped.push((pid, Arc::clone(pt), addr)); + Ok(addr.raw()) + }; + self.mapped_in.with_mut(map).unwrap_or(Err(SyscallError::Gone)) } /// Take this process's mapping away, if it has one; the caller owes a /// shootdown before the freed address can be reissued. #[must_use = "the caller owes a shootdown before the address can be reissued"] pub fn unmap_from(&self, pid: Pid) -> Option> { - let mut mapped = self.mapped_in.lock(); - let pos = mapped.iter().position(|(p, _, _)| *p == pid)?; - let (_, pt, vaddr) = mapped.swap_remove(pos); - pt.lock().free_and_unmap(vaddr); - Some(Unmapped::new(())) + self.mapped_in.with_mut(|mapped| { + let pos = mapped.iter().position(|(p, _, _)| *p == pid)?; + let (_, pt, vaddr) = mapped.swap_remove(pos); + pt.lock().free_and_unmap(vaddr); + Some(Unmapped::new(())) + })? } } -/// Every mapping goes, flushed here; the pages do not, since a handle holds -/// an `Arc` and `Region::pages` frees them only when the last one drops. +/// Every mapping goes, flushed here, and with the list gone no later map can +/// add one; the pages do not, since a handle holds an `Arc` and +/// `Region::pages` frees them only when the last one drops. impl ZeroHandles for SharedMemObject { fn on_zero_handles(&self) { - let mapped = core::mem::take(&mut *self.mapped_in.lock()); + let mapped = self.mapped_in.take().expect("the zero-handle hook runs once"); if mapped.is_empty() { return; } @@ -184,9 +195,9 @@ impl ZeroHandles for SharedMemObject { impl Drop for SharedMemObject { fn drop(&mut self) { debug_assert!( - self.mapped_in.lock().is_empty(), - "shm koid {} freed with a live mapping: the zero-handle hook did \ - not run, so the pages go back to the PMM under somebody's window", + self.mapped_nowhere(), + "shm koid {} freed with a live mapping, so the pages go back to the \ + PMM under somebody's window", self.core.koid().raw(), ); } diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 3e3260d780e..c1c39f00255 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -1981,7 +1981,8 @@ pub fn shm_create(size: usize) -> Result { /// Map the region `shm` names into this process. Needs [`Rights::MAP`]. /// -/// Idempotent: a second call answers the first call's address. +/// Idempotent: a second call answers the first call's address. [`SyscallError::Gone`] +/// when the region's last handle was closed under the call: nothing is mapped. /// /// [`Rights::MAP`]: crate::handle::Rights::MAP /// From 58b768a030f1e6beab46fb09199ef663e8218f33 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 10:59:17 +0200 Subject: [PATCH 3/4] Two shapes the shm audit left traced: a poll in flight on an ISA claim, and a dead process's address space kept by a region The device-slot issue this branch filed was one subject with issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md, which main has; it is gone from the branch. Its untraced sentence, the ISA and ACPI row, is traced: a read and a write run whole under the lock of the table that holds the claim's one handle, so nothing releases the claim inside one; a poll clones the claim out and then answers from the row, and a handle moved on by a binder that ended lets the row be claimed again in between. The second file is the review's note: nothing takes a process's entry out of a region's list of mappings when the process ends, so the entry's Arc keeps the address space, and its PCID, until the region's last handle goes. Both are read from the code and not run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...im-is-registered-on-the-rows-next-claim.md | 48 +++++++++++++++++++ ...every-process-that-ended-with-it-mapped.md | 42 ++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md create mode 100644 issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md diff --git a/issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md b/issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md new file mode 100644 index 00000000000..9df2f91eb6e --- /dev/null +++ b/issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md @@ -0,0 +1,48 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# A poll in flight on an ISA or ACPI claim is registered on the row's next claim + +A poll submission resolves its handle and clones the object out of the table +(`resolve`, `kernel/src/inbox/mod.rs`), lets the process-data lock go, and then +`arm` asks the object whether it is ready and registers on its watch. For an +ISA or ACPI claim both answers are the row's, not the claim's: `ops::has_data` +reads `isa::has_irq(row)` and `ops::read_watch` hands out `isa::watch(row)` +(`kernel/src/object/ops.rs`), and neither asks whose the row is by then. + +The row is the next claim's by then on one path. A process reads the claim, +which binds the row's ports to it, moves the handle on to a second process and +ends: `isa::process_ends` takes the binding back while the claim stays minted +in the second process's table. A thread there submits a poll and is past +`resolve`; a sibling closes the handle, the zero-handle hook runs +`isa::release`, and `isa::claim_row` now finds the row neither minted nor bound +and mints the next claim. The first thread's `arm` then fires at once if the +next holder's function has an interrupt unread, or leaves its poll on the +row's watch, where the next holder's first interrupt completes it. A process +that holds no claim learns when another's device interrupted, once per race +won; on the i8042's row that is the time of a key transition. + +**The calls on the claim do not have this window.** A read and a write +(`read_device`, `write_device`) run whole under the process-data lock of the +table that holds the claim's one handle (`sys_read`, `sys_read_nonblock`, +`with_object_ref`), and a claim carries no `Rights::DUP`, so no close can +release the claim inside one; after the close the handle refuses the call. +Where the reader is the process that bound the row, the poll is safe too: +the binding ends only in `process_ends`, after the last thread has left, and +a thread inside a submission has not left, so `claim_row` answers `Owned`. + +**Read from the code, not run.** The interval is between two lock holds of +one submission and needs a close, the hook and a second claim inside it; no +actuator in this tree holds a thread there. + +**Exit condition**: a poll on an ISA or ACPI claim is answered and registered +for that claim or refused — and a test that holds a submission between +`resolve` and `arm`, across the claim's release and the row's next claim, sees +it answered as gone, with no completion at the next holder's interrupt. + +**Owner**: whoever holds `issues/every-driver-is-still-in-the-kernel.md`; the +PCI half of the shape is +`issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md`. diff --git a/issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md b/issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md new file mode 100644 index 00000000000..13673a447dc --- /dev/null +++ b/issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md @@ -0,0 +1,42 @@ +--- +status: open +kind: defect +opened: 2026-10-08 +--- + +# A region keeps the address space of every process that ended with it mapped + +`SharedMemObject::map_into` (`kernel/src/object/shm.rs`) records each mapping +as `(Pid, PageTables, UserAddr)`, and `PageTables` is the `Arc` of the +process's address space. An entry leaves the list in two places only: the +region's zero-handle hook, when the last handle to it goes anywhere, and +`unmap_from`, which nothing but an inbox's teardown calls +(`kernel/src/inbox/mod.rs`). A process's own teardown +(`teardown_resources`, `kernel/src/process.rs`) closes its handles and +touches no region's list. + +So a process that maps a region somebody else also holds, and ends, leaves +its entry behind, and the entry keeps that process's `AddressSpace` alive +until the region's last holder lets go: its page-table pages, whatever its +`pages` map still owns, and its user PCID, which returns to the pool only +when the space drops (`PcidGuard`, `kernel/src/arch/x86_64/paging.rs`). The +list grows by one entry per such process and nothing bounds it. A region +handle carries `DUP` and `TRANSFER`, so one process can keep a region and +have child after child map it and end; the pool holds +`kernel::pcid::MAX_USER_PCID` tags, and with every one held +`AddressSpace::new_user` answers `None` and every spawn on the machine is +refused `ResourceExhausted` (`kernel/src/loader/mod.rs`). + +**Read from the code, not run.** Pids are never reissued +(`kernel/pure/proclife/pids.rs`), so a later process is never answered a dead +one's address by the list's `find`. + +**Exit condition**: a process's teardown leaves no entry of it in any +region's list — and a test in which a child maps a region its parent keeps +and ends sees the child's address space freed, its PCID back in the pool, +while the parent still holds the handle. + +**Owner**: `kernel::object::shm`'s handle-driven mapping teardown, with +`issues/the-compositor-keeps-a-committed-regions-mapping-for-as-long-as-the-client-does.md`, +whose exit ends a mapping at its process's last handle and so reaches this +one only where the process still held a handle when it ended. From 933d291e798ad2606502c98201e44b41c7226142 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 11:17:26 +0200 Subject: [PATCH 4/4] Answer review round 2: the ISA poll issue goes, and the region issue names what the compositor's exit leaves The poll file was one subject with issues/a-poll-registered-racing-its-own-close-outlives-the-release.md, which main already carries and whose exit covers every claim's watch, not PCI's alone. What the trace added, that the ISA and ACPI rows have the window only where the binder moved the handle on and ended and that the calls on the claim do not have it, stays in the pull request's body, citing main's file. The region issue's owner line said the compositor issue's exit reaches it only where the process still held a handle when it ended. That exit unmaps at the close of the last handle, so it also reaches a process that closed before it ended; what it leaves is a handle moved on by SYS_HANDLE_SEND, which is no close. Nothing outside issues/ moves. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...im-is-registered-on-the-rows-next-claim.md | 48 ------------------- ...every-process-that-ended-with-it-mapped.md | 7 ++- 2 files changed, 5 insertions(+), 50 deletions(-) delete mode 100644 issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md diff --git a/issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md b/issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md deleted file mode 100644 index 9df2f91eb6e..00000000000 --- a/issues/a-poll-in-flight-on-an-isa-claim-is-registered-on-the-rows-next-claim.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-08 ---- - -# A poll in flight on an ISA or ACPI claim is registered on the row's next claim - -A poll submission resolves its handle and clones the object out of the table -(`resolve`, `kernel/src/inbox/mod.rs`), lets the process-data lock go, and then -`arm` asks the object whether it is ready and registers on its watch. For an -ISA or ACPI claim both answers are the row's, not the claim's: `ops::has_data` -reads `isa::has_irq(row)` and `ops::read_watch` hands out `isa::watch(row)` -(`kernel/src/object/ops.rs`), and neither asks whose the row is by then. - -The row is the next claim's by then on one path. A process reads the claim, -which binds the row's ports to it, moves the handle on to a second process and -ends: `isa::process_ends` takes the binding back while the claim stays minted -in the second process's table. A thread there submits a poll and is past -`resolve`; a sibling closes the handle, the zero-handle hook runs -`isa::release`, and `isa::claim_row` now finds the row neither minted nor bound -and mints the next claim. The first thread's `arm` then fires at once if the -next holder's function has an interrupt unread, or leaves its poll on the -row's watch, where the next holder's first interrupt completes it. A process -that holds no claim learns when another's device interrupted, once per race -won; on the i8042's row that is the time of a key transition. - -**The calls on the claim do not have this window.** A read and a write -(`read_device`, `write_device`) run whole under the process-data lock of the -table that holds the claim's one handle (`sys_read`, `sys_read_nonblock`, -`with_object_ref`), and a claim carries no `Rights::DUP`, so no close can -release the claim inside one; after the close the handle refuses the call. -Where the reader is the process that bound the row, the poll is safe too: -the binding ends only in `process_ends`, after the last thread has left, and -a thread inside a submission has not left, so `claim_row` answers `Owned`. - -**Read from the code, not run.** The interval is between two lock holds of -one submission and needs a close, the hook and a second claim inside it; no -actuator in this tree holds a thread there. - -**Exit condition**: a poll on an ISA or ACPI claim is answered and registered -for that claim or refused — and a test that holds a submission between -`resolve` and `arm`, across the claim's release and the row's next claim, sees -it answered as gone, with no completion at the next holder's interrupt. - -**Owner**: whoever holds `issues/every-driver-is-still-in-the-kernel.md`; the -PCI half of the shape is -`issues/a-pci-call-in-flight-names-its-function-by-a-slot-the-next-claim-reuses.md`. diff --git a/issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md b/issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md index 13673a447dc..41b360d430a 100644 --- a/issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md +++ b/issues/a-region-keeps-the-address-space-of-every-process-that-ended-with-it-mapped.md @@ -38,5 +38,8 @@ while the parent still holds the handle. **Owner**: `kernel::object::shm`'s handle-driven mapping teardown, with `issues/the-compositor-keeps-a-committed-regions-mapping-for-as-long-as-the-client-does.md`, -whose exit ends a mapping at its process's last handle and so reaches this -one only where the process still held a handle when it ended. +whose exit ends a mapping at the close of its process's last handle and so +reaches this one wherever the process closed that handle, before it ended or +in its teardown. It does not reach a process that mapped the region and moved +its handle on by `SYS_HANDLE_SEND` (`sys_handle_send`, +`kernel/src/syscall/ipc.rs`), which is no close.