β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2677

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2677 Β· 10 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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() takes shm_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 β€” with shm_use_phys >= 2 the 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 by shm_token, checks shmseg->shm_nattch <= 0 and, 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 when ref_count != 0 after its pip waits (sys/vm/vm_object.c:~853): exactly the observed vm_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 shmdt decrements shm_nattch of 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_failed
baseline reproduced→ patch + rebuild →patched reproduced

INCOMPLETE: 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']
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Aug 30 13:38:49 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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).