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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2857 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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 neededuse_phys>=2to 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.diffapplied to the guest's/usr/src(hunk at sysv_shm.c:184): clearshm_perm.mode = SHMSEG_FREE+shm_internal = NULLand do the committed/nused accounting before the blockingvm_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 patchedsysv_shm.ccompile), 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 getEINVAL(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
fixedFIXED: 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)']
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.
No comments yet.