β¬’ 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.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.

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