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

shm_deallocate_segment keeps the segment findable during its own blocking teardown: racing IPC_RMID double-deallocates the vm_object (guaranteed panic; kfree double-free door)

Field Value
ID DF-2857
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:H
CWE CWE-362 + CWE-415 Double Free
File sys/kern/sysv_shm.c
Lines 180-194 (find :161-178; RMID entry :439-448)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

shm_deallocate_segment() runs the blocking vm_object_deallocate()/kfree teardown FIRST and only clears shm_perm.mode=SHMSEG_FREE afterwards. vm_object_terminate frees every resident page with lwkt_user_yield() every 64 pages plus vm_page_sleep_busy, each yield forcing a release of shm_token. Until the mode clear, shm_find_segment_by_shmid() (with default shm_allow_removed=1) keeps returning the mid-teardown segment, so a second shmctl(shmid, IPC_RMID) by the unprivileged owner re-enters shm_deallocate_segment and calls vm_object_deallocate on the object whose ref_count is already 0 β†’ unconditional panic "vm_object_deallocate: object deallocated too many times". A concurrent shmat through the same window references the dying object (the DF-2677 sink via a separate, unfixed door), and OBJ_DEAD interleavings reach kfree(shm_handle) twice plus shm_committed/shm_nused double-decrement.

Threat model & preconditions

Unprivileged local user (segment owner; forked racers share the uid). Default configuration (kern.ipc.shm_use_phys=1, shm_allow_removed=1), unlike DF-2677 which needed a root-set tunable. Window scales with segment size (~768 forced token-release points for 192MB). Reliable kernel panic on first run; memory-corruption class with a literal double-free of the M_SHM shm_handle chunk in OBJ_DEAD interleavings.

Proof of contest

VERIFIED (findings/poc/DF-2857/shm_teardown_race.c, unpriv nobody, run 1 <60s): panic: vm_object_deallocate: object deallocated too many times: 5 with stacked vm_object_deallocate_locked frames (re-entrant teardown proof); guest frozen in DDB. Fix (hide the segment and disconnect the handle BEFORE the blocking teardown) validated on a rebuilt kernel: 400/400 race iterations clean, exit 0, racing RMIDs get EINVAL.

Validated fix.diff in findings/poc/DF-2857/.

References

  • DF-2677 (the sibling attach-side TOCTOU), DF-2795 (msgsnd seq β€” the IPC-family pattern)

Timeline

  • 2026-09-02 Discovered during pass-2 audit of sysv_shm.c (GLM 5.3); unpriv panic reproduced run 1 + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2857 Β· 11 files
FileTypeDescriptionSize
shm_teardown_race.c β€” 4.1 KB view raw
build.sh β€” 64 B view raw
run.sh β€” 375 B view raw
serial_full.log β€” 2.2 KB view raw
panic.txt β€” 790 B view raw
run.fixed.log β€” 21.3 KB view raw
fix_build.log.gz β€” 189.0 KB ↓ download
fix.diff β€” 1.0 KB view raw
env.txt β€” 333 B view raw
VERDICT.md β€” 4.3 KB ↓ raw
verdict.json β€” 4.9 KB view raw
VERDICT.md
↓ download raw

VERDICT β€” DF-2857

Status: REPRODUCED (panic, default config, unprivileged) β€” fix VALIDATED (fixed)

Baseline (stock INVARIANTS kernel #0, Thu Jul 2 06:02:54 UTC 2026)

  • Binary built in-guest: cc -O2 -Wall -o shm_teardown_race shm_teardown_race.c
  • Run as nobody: su -m nobody -c /tmp/shm_teardown_race rmid 400
  • Defaults verified before the run: kern.ipc.shm_use_phys=1, kern.ipc.shm_allow_removed=1 (no tunables touched, unlike DF-2677 which needed use_phys>=2 to widen shmat's window β€” this bug's window is on the teardown side and is wide on stock defaults).
  • Result: kernel panicked within the first run (< ~60 s):
panic: vm_object_deallocate: object deallocated too many times: 5
cpuid = 2
vm_object_deallocate_locked() at vm_object_deallocate_locked+0x272
vm_object_deallocate_locked() at vm_object_deallocate_locked+0x272
vm_object_deallocate() at vm_object_deallocate+0x85
shm_deallocate_segment() at shm_deallocate_segment+0x17
sys_shmctl() at sys_shmctl+0xd6
syscall2() at syscall2+0x11e
Debugger("panic")

The two stacked vm_object_deallocate_locked frames are direct proof of the mechanism: the second IPC_RMID re-entered shm_deallocate_segment() while the first teardown was still on the stack, and dropped a reference on an object whose ref_count was already 0 β†’ unconditional panic at sys/vm/vm_object.c:690.

Mechanism (all under default sysctls): 1. nobody creates a 192 MB IPC_PRIVATE segment, attaches, faults every page in, detaches β†’ segment alive with shm_nattch == 0. 2. Racer processes spam shmctl(shmid, IPC_RMID). First success enters sys_shmctl β†’ shm_deallocate_segment (sys/kern/sysv_shm.c:445). 3. Inside, vm_object_deallocate() β†’ vm_object_terminate() frees ~48k pages, calling lwkt_user_yield() every 64 pages (vm_object.c:932); the LWKT switch forces a release of shm_token. 4. Until line sysv_shm.c:193 (shm_perm.mode = SHMSEG_FREE), shm_find_segment_by_shmid() still returns the segment (SHMSEG_ALLOCATED set; SHMSEG_REMOVED ignored because shm_allow_removed=1). 5. The next racing IPC_RMID passes ipcperm(IPC_M) (owner), sees shm_nattch <= 0, calls shm_deallocate_segment() again β†’ vm_object_deallocate() on the ref-0 object β†’ panic.

Corruption ceiling beyond the panic (not needed for the verdict): in interleavings where a racing shmat re-raises ref_count (OBJ_DEAD already set), the second vm_object_deallocate does not re-terminate, returns, and the second pass then kfree()s the same shm_handle chunk again and double-decrements shm_committed/shm_nused β€” a classic double-free/ accounting-corruption door, pre-empted in the observed interleaving by the ref-0 panic. The shmat racer mode of the PoC exercises the sibling door (reference on the dying object β†’ vm_object_terminate(2): object with references), i.e. the DF-2677 sink reachable through this separate, unfixed find-during-teardown window.

Fix validation (kernel #1, Wed Sep 2 12:38:41 UTC 2026)

  • fix.diff applied to the guest's /usr/src (hunk at sysv_shm.c:184): clear shm_perm.mode = SHMSEG_FREE + shm_internal = NULL and do the committed/nused accounting before the blocking vm_object_deallocate()/kfree, so the segment is unfindable before the token can drop.
  • Kernel rebuilt (make -j6 nativekernel, rc=0, full log: fix_build.log.gz, includes the patched sysv_shm.c compile), installed, rebooted into #1.
  • Exact same PoC re-run as nobody, same defaults: 400/400 iterations completed, exit 0, guest stayed up (run.fixed.log). Second racing RMIDs now get EINVAL (segment hidden before teardown) instead of entering the teardown a second time.

Artifacts

file what
shm_teardown_race.c PoC source (rmid + shmat racer modes)
build.sh / run.sh exact build/run
serial_full.log full serial console (boot β†’ panic)
panic.txt panic + backtrace (baseline)
run.fixed.log patched-kernel run: 400/400 clean
fix_build.log.gz full untrimmed patched-kernel build log (rc=0)
fix.diff verified one-hunk reorder fix
env.txt guest/kernel/sysctl environment

Note: the first full build log was written to guest tmpfs and lost at reboot; it was re-captured by rebuilding (including a forced sysv_shm.o rebuild) to a persistent path β€” fix_build.log.gz.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

FIXED: baseline (stock #0) panicked on the first race run; identical PoC on the fix.diff kernel (#1) completed 400/400 iterations with exit 0 and the guest stayed healthy. Second RMIDs now fail with EINVAL because the segment is hidden before vm_object_deallocate can yield.

['run.fixed.log', 'fix_build.log.gz', 'panic.txt (baseline A vs patched B)']
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 12:38:41 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (patched sys/kern/sysv_shm.c hunk at :184)

Confirmed kernel references

Detail

Exploit chain

unpriv nobody: (1) shmget(IPC_PRIVATE, 192MB, 0600); (2) shmat + memset (fault ~48k pages) + shmdt -> nattch==0, segment alive; (3) two forked racers spam shmctl(shmid, IPC_RMID): first starts the multi-yield teardown, the next RMID landing before sysv_shm.c:193 re-enters shm_deallocate_segment -> vm_object_deallocate on ref-0 object -> panic. Corruption ceiling (char.): shmat racer re-raising ref_count under OBJ_DEAD turns the second pass into kfree(shm_handle) double-free + committed/nused double-decrement; uid0 chain not developed because the ref-0 panic deterministically pre-empts the double-free interleaving on this kernel.

Evidence (decisive lines)

["panic.txt: 'panic: vm_object_deallocate: object deallocated too many times: 5' with sys_shmctl -> shm_deallocate_segment -> vm_object_deallocate -> stacked vm_object_deallocate_locked frames (re-entrant teardown proof)", 'serial_full.log: full baseline serial console ending in DDB', "run.fixed.log: patched kernel #1, 400/400 iterations 'completed 400 iterations without panic', exit 0", 'fix_build.log.gz: full patched-kernel build (rc=0), includes patched sysv_shm.c compile', 'fix.diff: one-hunk reorder clearing shm_perm.mode/shm_internal before vm_object_deallocate']

PoC changes

n/a (written fresh for this finding; no seed)

Verified recommended fix

In shm_deallocate_segment(), set shmseg->shm_perm.mode = SHMSEG_FREE and shmseg->shm_internal = NULL (and adjust shm_committed/shm_nused) BEFORE calling vm_object_deallocate()/kfree, so the segment is unfindable for the whole blocking teardown.

Verdict

shm_deallocate_segment() (sys/kern/sysv_shm.c:180-194) only clears SHMSEG_ALLOCATED at line 193, AFTER the blocking vm_object_deallocate()/kfree teardown; vm_object_terminate() yields (lwkt_user_yield every 64 pages, vm_object.c:932) which releases shm_token, and with the default shm_allow_removed=1 shm_find_segment_by_shmid() keeps returning the mid-teardown segment. An unprivileged owner racing a second shmctl(IPC_RMID) re-enters shm_deallocate_segment and double-deallocates the vm_object (ref_count already 0) -> unconditional panic 'vm_object_deallocate: object deallocated too many times' (vm_object.c:690). Reproduced on the FIRST run, stock INVARIANTS kernel, default sysctls (no use_phys>=2 needed, unlike DF-2677), as user nobody; stack shows sys_shmctl -> shm_deallocate_segment -> vm_object_deallocate -> double vm_object_deallocate_locked frame. A racing shmat through the same window references the dying object (the DF-2677 panic sink via a separate, unfixed door), and OBJ_DEAD interleavings reach a literal kfree() double-free of shm_handle plus shm_committed/shm_nused double-decrement. fix.diff (hide segment before blocking teardown) validated: patched kernel #1 completes 400/400 race iterations cleanly where baseline panicked immediately.