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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2719 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- sys/kern/imgact_elf.c:1199
RB_FOREACH(entry, vm_map_rb_tree, &map->rb_root)β no vm_map lock held.
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.
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_coredumprunseach_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_testableBaseline crash never reproduced, so a patched-kernel run cannot demonstrate a delta; fix.diff provided for review.
[]
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.
No comments yet.