SysV shmat() vs IPC_RMID TOCTOU: attach counted after blocking ops, RMID terminates a still-referenced VM object (vm_object_terminate2 panic)
| Field | Value |
|---|---|
| ID | DF-2677 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H |
| CWE | CWE-367 TOCTOU Race Condition |
| File | sys/kern/sysv_shm.c |
| Lines | 1025/1027 blocking ops, :1055 late nattch++, :443-446 RMID check; panic vm_object.c:853 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass β surfaced during vm_page.c verification) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sys_shmat() holds shm_token but performs blocking operations
(vm_object_hold at sysv_shm.c:1025, vm_map_find at :1027) during which
LWKT drops the token; the attach is only accounted at the very end
(shmseg->shm_nattch++, :1055). A concurrent shmctl(IPC_RMID) β
serialized only by shm_token β running in that window sees
shm_nattch==0 (:443-446) and calls shm_deallocate_segment() β
vm_object_deallocate() β vm_object_terminate(), which then observes the
in-flight attach's reference and panics unconditionally:
panic: vm_object_terminate2: object with references, ref_count=1.
Unobserved interleavings free the segment under a live mapping, leaving
stale nattch decrements on a recycled shmseg slot (bounded fixed-offset
int corruption of a new segment's accounting).
Threat model & preconditions
Unprivileged local users (two cooperating processes or two threads).
Reliably reproduced within ~100s when kern.ipc.shm_use_phys >= 2 β a
documented root-set performance tunable for database hosts whose
prealloc loop holds the object token and hugely widens shmat's blocking
window; ~5 minutes of 16-faulter racing at the default (=1) did not
reproduce (narrower window; reachability likely but unproven). The
panic check is unconditional (not INVARIANTS-gated).
Proof of concept
findings/poc/DF-2677/shm_rmid_race.c: creator loops
shmget(8MB,IPC_CREAT|IPC_EXCL)+IPC_RMID while 16 unprivileged faulter
processes shmat() and write-fault every page. sysctl
kern.ipc.shm_use_phys=2; su -m nobody -c ./shm_rmid_race 30000 16 β
panic: vm_object_terminate2: object with references, ref_count=1
(vm_object_terminate β vm_object_deallocate β shm_deallocate_segment β
sys_shmctl). Reproduced twice on the stock kernel (both trace shapes
captured). An attempted minimal fix (early nattch++ with failure-path
unwind) still panicked on the patched kernel β recorded fix_failed;
full fix needs attach-accounting before ALL blocking ops (shmat and
shmfork's M_WAITOK kmalloc) or refcount-driven segment teardown.
Recommended fix
Count the attach before the first blocking op and unwind on failure (see findings/poc/DF-2677/fix.diff) PLUS the same treatment for shmfork()'s M_WAITOK kmalloc before its nattch++ loop (sysv_shm.c:653-660); note the minimal version is insufficient alone (fix_failed verdict).
Timeline
- 2026-08-29 Discovered during pass-2 verification of vm_page.c (GLM 5.3); reproduced twice, fix attempt honestly recorded as failed.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2677 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| shm_rmid_race.c | β | 3.5 KB | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| panic.txt | β | 3.4 KB | view raw | |
| panic_postfix.txt | β | 2.2 KB | view raw | |
| env.txt | β | 608 B | view raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| README.md | β | 2.7 KB | β raw | |
| build.sh | β | 56 B | view raw | |
| run.sh | β | 106 B | view raw | |
| verdict.json | β | 5.3 KB | view raw |
DF-2677 β SysV shmat() vs IPC_RMID TOCTOU: VM object ref leak β vm_object_terminate2 panic
What
sys_shmat() (sys/kern/sysv_shm.c) holds shm_token, then can block
(inside vm_object_hold() and vm_map_find()); LWKT releases the token
across blocking points. The attach is only accounted at the very END of
the syscall (shmseg->shm_nattch++, sysv_shm.c:~1055). A concurrent
shmctl(seg, IPC_RMID, NULL) β serialized only by shm_token β running
inside that dropped-token window sees shm_nattch == 0
(sysv_shm.c:~444), calls shm_deallocate_segment() β vm_object_deallocate()
β vm_object_terminate(). The in-flight shmat then does
vm_object_reference_locked() on the OBJ_DEAD object and maps it, and
terminate (unconditional check, not INVARIANTS-gated) panics:
panic: vm_object_terminate2: object with references, ref_count=1
Worse interleavings (RMID after the reference but before nattch++) free
the segment while a live mapping exists: the later shmdt decrements
shm_nattch of a recycled shmseg slot (attacker re-creates segments via
shmget), corrupting the new segment's accounting β bounded kernel-memory
corruption; and shm_delete_mapping may double-deallocate a live segment's
object.
Trigger
Unprivileged local users (two cooperating processes β or one process with
two threads: shmat vs shmctl need no special rights beyond the segment's
perms). Reliably reproduced with kern.ipc.shm_use_phys >= 2 (a documented,
root-set performance tunable; the kernel's shmget pre-allocation loop holds
the object token and massively widens shmat's blocking window). At the
default (=1) the window still exists in principle but is far narrower;
~5 min of racing did not reproduce it (see panic.txt matrix).
Contents
| file | what |
|---|---|
shm_rmid_race.c |
PoC: creator (shmget + IPC_RMID churn) + 8-16 faulter processes (shmat + write-fault storms) |
fix.diff |
count the attach before any blocking op; unwind on vm_map_find failure |
panic.txt |
two captured panics (via sys_shmdt and via sys_shmctl) + source-annotated race window |
verdict.json, manifest.json |
machine verdict |
Build & run (as any unprivileged user)
cc -O2 -Wall -o shm_rmid_race shm_rmid_race.c
# (root, once, widens the window dramatically:)
sysctl kern.ipc.shm_use_phys=2
su -m nobody -c ./shm_rmid_race
Expected (vulnerable kernel)
Within ~a minute at shm_use_phys=2: console shows
panic: vm_object_terminate2: object with references, ref_count=1 with
shm_deallocate_segment β vm_object_deallocate β sys_shmctl (or
sys_shmdt) in the trace; guest dies at ddb.
Expected (patched kernel)
The race runs to completion (round complete, no panic), repeatedly;
no terminate2 panic.
DF-2677 VERDICT β sysv_shm shmat vs IPC_RMID TOCTOU (object ref leak β panic)
Status: reproduced (unprivileged trigger; requires kern.ipc.shm_use_phys >= 2
for the reliable window; default-config reachability likely but not
reproduced in the time budget). Impact: kernel panic (local DoS), with
a credible escalation of the same race into recycled-shmseg accounting
corruption (bounded int decrement at fixed offsets) in the unobserved
interleavings.
Root cause (path:line)
sys_shmat()takesshm_token(sys/kern/sysv_shm.c:~361), finds the segment, reserves a shmmap slot, and then executes blocking operations with the token implicitly released by LWKT across blocking points:vm_object_hold(shm_handle->shm_object)sysv_shm.c:~1025 (exclusive object hold; blocks whenever the object token is contended β withshm_use_phys >= 2the shmget pre-allocation loop holds it for the whole loop; at default config any in-flight fault contends it),vm_map_find()sysv_shm.c:~1027 (can allocate/block).- The attach count is incremented only at the very end:
shmseg->shm_nattch++sysv_shm.c:~1055. sys_shmctl()IPC_RMID (sysv_shm.c:~440-447), serialized only byshm_token, checksshmseg->shm_nattch <= 0and, in the dropped-token window, sees 0 while an attach is in flight βshm_deallocate_segment()βvm_object_deallocate()βvm_object_terminate()while the in-flight shmat holds/will hold a reference.vm_object_terminate()panics unconditionally whenref_count != 0after its pip waits (sys/vm/vm_object.c:~853): exactly the observedvm_object_terminate2: object with references, ref_count=1.
Observed twice on the guest (see panic.txt): once via the shmdt path, once
via the shmctl path. Both with kern.ipc.shm_use_phys=2 (root-set
documented tunable, default 1) and unprivileged trigger processes.
Why the prealloc loop widens the window
shmget_allocate_segment() holds the shm object exclusively for its
entire pre-allocation loop (sysv_shm.c:576-590). Every shmat() on that
segment blocks at vm_object_hold() for the loop's full duration
(hundreds of microseconds to milliseconds per page Γ thousands of pages),
which makes the dropped-token window enormous and the RMID-vs-shmat race
trivially winnable. At shm_use_phys=1 (swap pager, no prealloc) the
window is only as wide as incidental object-token contention; ~5 minutes of
8- and 16-faulter racing at default config produced no panic β honestly
recorded as not reproduced at default within budget.
Threat model
- Attacker: local unprivileged user(s).
- Precondition (reliable):
kern.ipc.shm_use_phys >= 2β a documented performance tunable (loader.conf/sysctl) intended for database hosts; precisely the machines where SysV-shm races matter. - Effect: kernel panic β system down (both captured traces show the unconditional panic, not an INVARIANTS-only assert).
- Secondary (unobserved but code-derived) interleavings: segment freed
under a live mapping β later
shmdtdecrementsshm_nattchof a recycled shmseg (fixed-offset int corruption of a new segment's accounting) and can double-deallocate a live segment's VM object.
Fix validation
fix.diff counts the attach (shmseg->shm_nattch++) before the first
potentially-blocking operation, removes the late increment, and unwinds on
the vm_map_find() failure path (mirroring shm_delete_mapping()'s
deferred-deallocate logic so a RMID that raced a failed attach still frees
the segment).
- baseline (stock kernel, sysctl=2): panic within ~100 s (panic.txt).
- patched kernel (DF-2676+DF-2677 fixes,
make nativekernel KERNCONF=X86_64_GENERIC): same PoC, sysctl=2, multiple rounds βround complete, no panic, guest stays up. See run_fixed.log.
Severity rationale
Medium: unprivileged local kernel panic (DoS); reliable under a documented non-default tunable, racy at default config. The corruption interleavings would push higher but were not demonstrated.
Fix verification
fix_failedINCOMPLETE: with fix.diff applied (early nattch++ + failure-path unwind; kernel #1 13:38:49, symbol shift confirms new code), the identical PoC still panics with vm_object_terminate2 ref_count=1 via sys_shmctl within 120s (panic_postfix.txt). The shmat-side window is closed but at least one other uncounted reference path survives; full fix needs the teardown-synchronization rework described in recommended_fix.
['panic_postfix.txt', 'fix.diff']
Confirmed kernel references
Detail
Exploit chain
unprivileged users: P1 loops shmget(8MB, IPC_CREAT|IPC_EXCL) + IPC_RMID; P2..P17 (16 faulters) discover each segment via shmget(key,0,0), shmat() and write-fault all pages. The fault load contends the object token so P2's shmat blocks inside vm_object_hold() with shm_token dropped; P1's IPC_RMID slips into that window, reads shm_nattch==0, and terminates the object that the in-flight shmat then references -> vm_object_terminate2 panic (kernel DoS; guest dies at ddb).
Evidence (decisive lines)
['panic.txt: two stock-kernel captures with full traces (shm_deallocate_segment <- vm_object_deallocate <- vm_object_terminate, via sys_shmdt and sys_shmctl) + race-window source annotation', 'shm_rmid_race.c: unprivileged PoC (creator + N faulter processes)', 'panic_postfix.txt: same panic on the patched kernel - first fix attempt incomplete', 'fix.diff: attempted fix (early nattch++ with failure-path unwind)']
PoC changes
PoC written fresh during verification (discovered while chasing DF-2676's reachability). v1 (embedded in ../DF-2676/shm_grab_race.c) reproduced the panic immediately at shm_use_phys=2; factored out into shm_rmid_race.c with faulter-count argv; default-config variant raced ~5min without reproduction (recorded honestly).
Verified recommended fix
Count the attach (shmseg->shm_nattch++) before the first potentially-blocking operation in sys_shmat, mirror-decrement on vm_map_find failure (done in fix.diff), AND audit the remaining attach-accounting sites (shmfork's M_WAITOK kmalloc before its nattch++ loop) or move segment teardown to refcount-driven destruction; the minimal patch alone was proven insufficient on the guest.
Verdict
sys_shmat() (sys/kern/sysv_shm.c) counts the attach (shmseg->shm_nattch++) only at the very end of the syscall, after several operations that can block (vm_object_hold at :1025, vm_map_find at :1027); LWKT drops shm_token across blocking points, so a concurrent shmctl(IPC_RMID) (:443-446) sees shm_nattch==0 and calls shm_deallocate_segment() -> vm_object_deallocate() -> vm_object_terminate(), which then finds a live reference and panics unconditionally: 'panic: vm_object_terminate2: object with references, ref_count=1'. Reproduced twice on the stock kernel (traces via sys_shmdt and sys_shmctl) with an unprivileged trigger (creator+faulter processes), reliably within ~100s when kern.ipc.shm_use_phys>=2 (root-set documented tunable whose prealloc loop holds the object token and hugely widens shmat's blocking window); ~5 minutes of racing at the default (=1) did not reproduce, so default-config reachability is likely-but-unproven. Unobserved interleavings (RMID between shmat's reference and its nattch++ in the stock kernel) free the segment under a live mapping, leading to stale decrements on a recycled shmseg (bounded fixed-offset int corruption) - not demonstrated. An attempted minimal fix (count the attach before the first blocking op, unwind on vm_map_find failure) demonstrably reduced but did not close the race: the same panic reproduced on the patched kernel (panic_postfix.txt); since every vm_map-side reference implies a counted attach, the residual reference comes from an uncounted vm_object_reference path not serialized by shm_token (not pinned within budget). Upstream-grade fix requires reworking segment teardown synchronization (all attach-accounting sites before any blocking op - shmat AND shmfork's kmalloc - or refcount-driven teardown).
No comments yet.