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

Coredump each_segment traverses the vm_map RB-tree unlocked while a pre-counted-'stopped' sibling LWP can still be mutating it (UAF read race)

Field Value
ID DF-2719
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:H
CWE CWE-362 / CWE-416
File sys/kern/imgact_elf.c
Lines 1193-1259 (unlocked RB_FOREACH)
Area kern
Confidence likely
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

generic_elf_coredump walks the victim's map four times via each_segment's RB_FOREACH with no vm_map lock. sigexit's proc_stop(SCORE) pre-counts sleeping LWPs as stopped and only AST-notifies running ones β€” both stop at the next userret β€” so a sibling inside one long munmap/mmap keeps unlinking/freeing vm_map_entry objects throughout the dump's unlocked traversals. Mid-teardown core files (8.5k of 20k entries present) prove the overlap occurred across thousands of children; no crash in ~13,400 attempts on the INVARIANTS guest (freed-but-unreused entries stay readable). Assessed ceiling: kernel panic (local DoS); speculative cross-process fhandle leak into the core if a freed entry is reused mid-walk.

Threat model & preconditions

Unprivileged threaded program: worker performs a single ~11ms munmap of a 20k-100k-entry mapping while the main thread takes a fatal signal.

Proof of concept

findings/poc/DF-2719/racedump.c: delay sweep calibrated to the measured 11.14ms munmap; race entry proven by mid-teardown cores (run.log); panic not achieved in ~13.4k attempts.

Hold the map lock across each pass (callbacks perform no topology changes) β€” or snapshot the entry list under the lock first: vm_map_lock(map); RB_FOREACH(...); vm_map_unlock(map);

Timeline

  • 2026-08-30 Discovered during pass-2 audit of imgact_elf.c (GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2719 Β· 9 files
FileTypeDescriptionSize
racedump.c β€” 3.1 KB view raw
build.sh β€” 151 B view raw
run.sh β€” 139 B view raw
run.log β€” 1.4 KB view raw
fix.diff β€” 598 B view raw
verdict.json β€” 3.0 KB view raw
VERDICT.md β€” 2.3 KB ↓ raw
README.md β€” 3.3 KB ↓ raw
env.txt β€” 268 B view raw

DF-2719 β€” coredump each_segment iterates the vm_map RB-tree with no lock while a "stopped" sibling LWP can still be mutating it

  • File: sys/kern/imgact_elf.c
  • Severity: Medium (race/UAF; kernel panic at minimum when it manifests)
  • Confidence: likely β€” code-certain race; crash not reproduced in ~13,400 attempts
  • Class: memcorrupt (use-after-free read) / CWE-362 + CWE-416

Root cause

generic_elf_coredump (sys/kern/imgact_elf.c:988) walks the victim's map four times (count :1004, dry fp-count via elf_puttextvp :1656/1660, cb_put_phdr :1393, cb_put_fp :1656) through:

sigexit (sys/kern/kern_sig.c:2394-2398) stops LWPs via proc_stop(SCORE) + proc_stopwait, but proc_stop pre-counts a sleeping LWP as stopped (kern_sig.c:1598-1612: "We're sleeping, but we will stop before returning to userspace, so count us as stopped") and only notifies running LWPs (kern_sig.c:1614-1620) β€” they stop at the next userret. Therefore an LWP that is inside a map-mutating syscall (mmap/munmap/mprotect) when the fatal signal hits completes that syscall β€” mutating the RB tree β€” while the coredump's unlocked RB_FOREACH is traversing it. Entries are unlinked and freed (vm_map_entry_unlink/vm_map_entry_free) under the reader.

The p_token held by sigexit does not help: mmap/munmap run under the vm_map lock, not p_token (which only guards the LWP list).

Reproduce (stress)

cc -O2 -D__BSD_VISIBLE=1 -include sys/resource.h -include sys/time.h \
   -o racedump racedump.c -lpthread
./racedump 2500 40000 500 10000        # parent map of N mappings; child:
                                       #  worker: ONE munmap() of all N entries
                                       #  main: usleep(delay) then *(int*)0=0

Calibration: munmap(40000 entries) β‰ˆ 11.1 ms in-guest β€” the delay sweep aims the fatal signal into the middle of that syscall so the dump's traversals run concurrently with the unlinks.

Observed (evidence the race window is entered): core files written with mid-teardown maps β€” e.g. racedump.core of 1019904 bytes β‰ˆ 8.5k vn_hdrs when the process had 20,000 file mappings: ~11.5k entries were unlinked while the dump was running (pass-2/pass-4 traverse MAP_NOCORE entries since elf_puttextvp uses writable=0).

Result: no panic in ~13,400 attempts (3 configs: 20k maps serialized, 40k maps Γ—3 parallel racers with per-pid core files, 100k maps Γ—2 racers) on the INVARIANTS guest β€” freed-but-not-yet-reused vm_map_entry contents remain readable and RB traversal over stale fragments stays self-consistent.

Impact if it manifests

Reader dereferences freed/reused vm_map_entry (entry->ba.object, rb-node links mid-rotation) β†’ wild pointer or torn object pointer β†’ kernel panic; theoretically foreign-map entries reused into this traversal can write other processes' file handles into the core file (fhandle leak).

Fix

Take the vm_map lock around the whole each_segment pass set (see fix.diff β€” vm_map_lock/unlock around the RB_FOREACH; callbacks do no sleeping VFS ops beyond VFS_VPTOFH, which is safe under vm_map_lock's lockmgr semantics) β€” or re-verify the hardening: snapshot entry list under lock first.

VERDICT.md
↓ download raw

DF-2719 VERDICT

Status: not_reproduced (race confirmed entered; no crash manifestation) β€” impact: none observed (assessed panic/DoS if it lands) β€” confidence: likely.

Narrative

The code path is certain:

  • sigexit β†’ proc_stop(SCORE)/proc_stopwait (sys/kern/kern_sig.c:2394-2398) returns while a sibling LWP mid-syscall is still running: kern_sig.c:1598-1612 pre-counts LSSLEEP lwps as stopped; kern_sig.c:1614-1620 only queues an AST for LSRUN lwps β€” both finish their current syscall before stopping at userret.
  • If that syscall is a long munmap, it unlinks+frees vm_map_entry objects (under the vm_map lock, which the reader never takes) for its whole duration.
  • Concurrently generic_elf_coredump runs each_segment (sys/kern/imgact_elf.c:1199 RB_FOREACH, no lock) four times.

Stress evidence (see README.md, race logs): core files demonstrably written from a partially-demolished map (~8.5k of 20k entries at dump time), proving the dump traversals and the munmap overlapped in wall-clock time across thousands of children.

What did NOT happen in ~13,400 attempts on the INVARIANTS guest: no panic, no wedged guest, no malformed core observed. Freed vm_map_entry objects remain readable until objcache reuse, and an RB walk over stale-but-intact fragments does not fault. A crash needs a torn read (entry reused or rb links mid-rotation exactly under the reader), which these attempts did not land.

Why not false_positive

The overlap itself is proven by the mid-teardown core files; the unlocked traversal is proven by reading imgact_elf.c:1199 against vm_map's locking rules (every other RB_FOREACH over a vm_map in sys/vm/vm_map.c is performed under vm_map_lock). The defect is real; only the catastrophic manifestation is probabilistic and did not land within the attempt budget.

Exploit chain

none demonstrated (read-side UAF of a dedicated-objcache object; realistic ceiling is a kernel panic / local DoS, with a speculative cross-process fhandle leak into the core file if a freed entry is reused by another process's map during traversal).

Fix

vm_map_lock/vm_map_unlock around each each_segment pass (fix.diff). Validating the fix build is not meaningful while the baseline crash never reproduced; fix_status = not_testable for that reason.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Baseline crash never reproduced, so a patched-kernel run cannot demonstrate a delta; fix.diff provided for review.

[]
↓ fix.diffper-fix-DF-2719

Confirmed kernel references

Detail

Evidence (decisive lines)

['findings/poc/DF-2719/run.log β€” calibration (munmap 40000 entries = 11.14 ms), run configs, mid-teardown core size evidence, 0 panics in ~13,400 attempts', 'findings/poc/DF-2719/racedump.c β€” stress harness (parent map of N PROT_READ MAP_NOCORE file mappings; child worker = one whole-range munmap; main = usleep(delay) + SIGSEGV)', 'VERDICT.md β€” narrative incl. why not false_positive']

PoC changes

Stress harness written from scratch; delay sweep calibrated to the measured 11.1ms munmap duration; per-pid core files (kern.corefile='%N.%P.core') to avoid flock serialization of dumps; 20k/40k/100k-entry map configurations with parallel racers

Verified recommended fix

Hold the vm_map lock across each each_segment RB_FOREACH in generic_elf_coredump's passes (or snapshot the entry list under the lock before walking)

Verdict

Code-certain race, crash not observed. generic_elf_coredump's each_segment (imgact_elf.c:1199 RB_FOREACH) traverses the vm_map rb-tree with no vm_map lock, while proc_stop (kern_sig.c:1598-1612) pre-counts sleeping siblings as stopped and AST-stops running ones only at userret - so a sibling inside one long munmap keeps unlinking/freeing vm_map_entry objects throughout the dump's traversals. Mid-teardown core files (8.5k of 20k entries present at dump time) prove the overlap occurred across thousands of children, but ~13,400 attempts across 3 stress configurations produced no panic on the INVARIANTS guest: freed-but-unreused entries stay readable and RB walks over stale fragments remain self-consistent. Assessed impact if it lands: kernel panic (local DoS); speculative cross-process fhandle leak into the core via reused entries.