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.3 KB | β raw | |
| README.md | β | 2.7 KB | β raw | |
| build.sh | β | 100 B | view raw | |
| run.sh | β | 170 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 status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
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.