DF-2857 / verdict.json
{ "finding_id": "DF-2857", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "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.", "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": [ "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" ], "kernel_refs": [ "sys/kern/sysv_shm.c:180", "sys/kern/sysv_shm.c:187", "sys/kern/sysv_shm.c:193", "sys/kern/sysv_shm.c:161", "sys/kern/sysv_shm.c:171", "sys/kern/sysv_shm.c:439", "sys/kern/sysv_shm.c:444", "sys/vm/vm_object.c:690", "sys/vm/vm_object.c:884", "sys/vm/vm_object.c:932" ], "poc_changes": "n/a (written fresh for this finding; no seed)", "attempts": 2, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 60, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -o shm_teardown_race shm_teardown_race.c", "run_cmd": "su -m nobody -c /tmp/shm_teardown_race rmid 400 (defaults: kern.ipc.shm_use_phys=1, kern.ipc.shm_allow_removed=1)", "code_hash": "4bc42fda338bbd9b14573bde923669f026e275c88d3cdd678fb36d6217be5d97", "notes": "Default-config reproduction (no root-set tunable, unlike DF-2677 which needed use_phys>=2). Window is on the deallocation side: ~768 forced token-release points for a 192MB segment. Distinct root cause from DF-2677 (find-during-teardown vs late-nattch++); DF-2677's early-attach-accounting fix would NOT close this window - only hiding the segment before the blocking teardown does. Guest reset to clean with-src snapshot after validation.", "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.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly 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)", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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.", "fix_evidence": [ "run.fixed.log", "fix_build.log.gz", "panic.txt (baseline A vs patched B)" ] } |