/dev/lpmap fault races lwp teardown: orphaned lpmap allocation leaks 4KB M_UPMAP kernel heap per hit (unprivileged kmem exhaustion)
| Field | Value |
|---|---|
| ID | DF-2839 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-362 β CWE-401 |
| File | sys/kern/kern_memio.c |
| Lines | 801-824 (fault handler); teardown kern_proc.c:1356-1381; WEXIT kern_exit.c:688 vs :695 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
user_kernel_mapping(), the UKSMAPOP_FAULT handler for /dev/lpmap (minor 7, mode 0666), reads lp->lwp_lpmap and calls lwp_usermap(lp,-1) without lifetime synchronization against the mapping's LWP exiting. lwp_userunmap() running from lwp_exit() NULLs lwp_lpmap then sleeps for milliseconds in its vm_map_remove drain with LWP_MP_WEXIT not yet set (only set after return). A fault on any not-yet-removed page of a mapping the exiting thread created sees lwp_lpmap==NULL, passes lwp_usermap's WEXIT guard, and installs a brand-new lpmap into the exiting LWP; the drain already saved/freed the old one and no other free site exists β the LWP is reaped with the allocation installed. Narrow same-window edges (pmap_enter landing after the drain's pmap_remove; pmap_kextract of the freed page) could expose a freed kernel heap page RW to userspace β source-proven only, speculative. Same-window AB-BA deadlock disproved (LWKT tokens re-acquired across tsleep).
Threat model & preconditions
Any local unprivileged user (lpmap is 0666) permanently consumes kernel heap at ~150 pages/s per 4 processes β M_UPMAP cap (390M) exhaustion / kmem denial within minutes, plus per-hit orphaned kernel page-table mappings.
Proof of contest
VERIFIED (findings/poc/DF-2839/lpdeadlock3.c, unpriv uid 1001): 6 victim threads (mmap 6 lpmap regions, fault, exit) + 6 storm threads (drain-window stretching) + 3 touchers (shuffled jittered faults) β upmap live count +7.0K/+6.8K allocations (+28.1MB/+27.6MB) per 45s run on stock with live-process count constant; fresh-boot baseline 37. Fix (set LWP_MP_WEXIT BEFORE lwp_userunmap in lwp_exit so lwp_usermap's post-allocation guard refuses late installs) authored.
Recommended fix
See findings/poc/DF-2839/fix.diff (ordering change in kern_exit.c).
Timeline
- 2026-08-31 Discovered during pass-2 audit of kern_memio.c (GLM 5.3); unpriv kmem leak quantified monotonic across runs same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2839 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 5.5 KB | β raw | |
| lpdeadlock.c | β | 6.4 KB | view raw | |
| lpdeadlock2.c | β | 6.4 KB | view raw | |
| lpdeadlock3.c | β | 5.7 KB | view raw | |
| build.sh | β | 141 B | view raw | |
| run.sh | β | 382 B | view raw | |
| run.log | β | 74 B | view raw | |
| run.2.log | β | 68 B | view raw | |
| run.3.log | β | 329 B | view raw | |
| leak_sample.txt | β | 460 B | view raw | |
| env.txt | β | 216 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| fix.build.log | β | 906 B | view raw | |
| fix.run.log | β | 362 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 5.3 KB | view raw |
DF-2839 β /dev/lpmap fault vs lwp-exit race: orphaned lpmap allocation (unprivileged kernel heap leak)
Build
On the DragonFly guest (any user; binary drops privileges itself):
cc -O -o /tmp/lpdeadlock3 /tmp/lpdeadlock3.c -lpthread
Run
vmstat -m | grep -w upmap # baseline /tmp/lpdeadlock3 4 45 # 4 unprivileged children, 45s vmstat -m | grep -w upmap # sample again /tmp/lpdeadlock3 4 45 vmstat -m | grep -w upmap # and again
Expected (stock kernel β reproduced)
upmap live allocation count / MemUse grows monotonically by ~7,000
allocations (~28 MB) per 45 s run while the live process count stays
constant. Observed 2026-09-02 on DragonFly dfbsd 6.5-DEVELOPMENT #0
Thu Jul 2 06:02:54 UTC 2026 X86_64_GENERIC:
15.4K / 61.4M -> 22.4K / 89.5M -> 29.2K / 117M
The M_UPMAP pool is capped (390M here) β sustained unprivileged racing exhausts it in minutes and permanently consumes kernel heap.
Expected (kernel with fix.diff β validated)
Allocation count returns to its pre-run baseline (growth 0) for the same
workload: LWP_MP_WEXIT is set before lwp_userunmap() in lwp_exit(),
so lwp_usermap() refuses to install an lpmap into an LWP whose teardown
already ran.
Files
lpdeadlock.c v1 (deadlock probe β disproved: LWKT tokens are
re-acquired by the scheduler across tsleep)
lpdeadlock2.c v2 (storm variant of v1)
lpdeadlock3.c v3 β leak reproducer (this is the PoC of record)
fix.diff verified one-hunk fix (sys/kern/kern_exit.c)
run.log v1 run (no deadlock β negative result)
run.2.log v2 run (no deadlock β negative result)
run.3.log v3 leak reproduction, 3 samples, 2 runs (decisive)
leak_sample.txt the three samples
env.txt guest uname / sysctl context
VERDICT β DF-2839
Status: reproduced (impact: dos β unprivileged kernel heap exhaustion via orphaned M_UPMAP allocations). Confidence: certain (for the leak); the freed-page stray-PTE edge is speculative.
What the finding is
user_kernel_mapping() (sys/kern/kern_memio.c:801-824) β the
UKSMAPOP_FAULT handler for /dev/lpmap (minor 7, mode 0666, open to any
user) β reads lp->lwp_lpmap and, when NULL, calls lwp_usermap(lp, -1)
(kern_memio.c:813) without any lifetime synchronization against the lwp
leaving the map other than the vm_map read lock held by vm_fault().
The teardown side, lwp_userunmap() (sys/kern/kern_proc.c:1356-1384),
runs from lwp_exit() (sys/kern/kern_exit.c:688) and:
lwkt_gettoken(&lp->lwp_token)(kern_proc.c:1363) β released whenever the thread sleeps (LWKT tokens are re-acquired by the scheduler,lwkt_getalltokens()inlwkt_switch),- saves and NULLs
lp->lwp_lpmap(1365-1366), - drains every lpmap backing with
vm_map_remove()(1374) β this sleeps for the map write lock, for milliseconds under mapping churn, with LWP_MP_WEXIT not yet set (it is only set afterlwp_userunmap()returns, kern_exit.c:695), - frees the saved lpmap (1381) β the only
lwp_lpmapfree site in the kernel (verified by grep).
A page fault on a mapping created by the exiting thread (any other thread of the same process β the map is process-wide) that lands in the drain window:
lp->lwp_lpmapreads NULL (step 2 already ran),lwp_usermap()acquireslwp_token(free β the exiter released it to sleep), passes theLWP_MP_WEXITguard (kern_proc.c:1326 β flag not set yet), and installs a brand-new lpmap into the exiting lwp,- the fault even succeeds (
pmap_kextractof the fresh page, kern_memio.c:821), - the exiter wakes, finishes
lwp_userunmap()having already saved the OLD pointer, and the LWP is reaped with the NEW allocation still installed.
Nothing ever frees it: 4096 bytes of M_UPMAP kernel heap per hit, triggerable by an unprivileged user at will.
How it was proven (run.3.log, leak_sample.txt)
Stock kernel DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC
2026 X86_64_GENERIC, unprivileged uid 1001, /dev/lpmap crw-rw-rw-:
upmap 15.4K / 61.4M (residual of earlier probe runs) +45 s 22.4K / 89.5M (+7.0K allocs, +28.1 MB) +45 s 29.2K / 117M (+6.8K allocs, +27.6 MB) ps ax | wc -l == 196 at every sample
Monotonic growth across two independent runs with constant live-process count = orphaned allocations, ~150 pages/s from four unprivileged processes. M_UPMAP is capped at 390M on this guest; sustained racing exhausts the cap in minutes and permanently consumes kernel heap (local DoS; each wedged page also pins a kernel page table mapping).
Why this is not the deadlock I first suspected (negative results)
v1/v2 (run.log, run.2.log) probed an AB-BA deadlock
(map-read-lock held while lwkt_gettoken(lwp_token) vs
lwp_token-held vm_map_remove). It cannot deadlock: DFly LWKT tokens
are per-thread and are re-acquired by the scheduler when the thread is
rescheduled (lwkt_getalltokens, sys/kern/lwkt_thread.c:707/755), so the
exiting thread does not hold lwp_token while sleeping in the drain. The
lockmgr is also reader/excl fair (lockmgr_shared, sys/kern/kern_lock.c:107:
new shared acquisitions block behind LKC_EXREQ). Both probes ran 90 s
with zero stalls. The leak above is what the same interleaving actually
produces.
Impact ceiling (speculative, source-proven only)
Two narrow edges of the same unsynchronized window:
- the fault's
pmap_enter()(vm_fault.c:577) can land after the drain'spmap_remove()of the entry range, leaving a stray user PTE to a page freed at kern_proc.c:1381 (RW window onto recycled kernel heap) β needs the faulting thread to stall between callback return andpmap_enteracross the whole drain: not demonstrated; - with a real lpmap (victim faults one page first), the pre-NULL window
allows
pmap_kextractof the page freed at 1381 β same stall caveat.
Reported as Medium/dos on the strength of the demonstrated leak; the memcorrupt edges are documented for triage, not claimed.
Fix validation (fix.diff β verified on this guest)
LWP_MP_WEXIT is now set in lwp_exit() before lwp_userunmap()
(kern_exit.c). This is airtight for the leak: lwp_usermap()'s check is a
token-serialized check-and-store evaluated after its (sleeping) kmalloc,
and lwp_userunmap()'s save happens under the same token β so any store
that slips in before the save is freed by the drain, and any store after
the save is refused by the flag. It also closes the stray-pmap_enter
edge, because faults in the window now fail with EINVAL (allocation
refused) instead of succeeding.
Rebuilt with make -j6 nativekernel + make installkernel, rebooted;
identical PoC workload ran twice with zero M_UPMAP growth
(see verdict.json fix_* fields and fix run log).
Kernel references
- sys/kern/kern_memio.c:801-824 (user_kernel_mapping, minor 7)
- sys/kern/kern_memio.c:813 (lwp_usermap call, unsynchronized)
- sys/kern/kern_proc.c:1318-1345 (lwp_usermap, WEXIT guard)
- sys/kern/kern_proc.c:1356-1384 (lwp_userunmap, save/drain/free)
- sys/kern/kern_exit.c:688,695 (WEXIT set after lwp_userunmap β the bug)
- sys/kern/kern_exit.c:1269 (proc_userunmap at reap β upmap side ordered safely)
- sys/vm/vm_fault.c:562-580, 467 (uksmap fault under map read lock)
- sys/vm/vm_map.c:1660-1665 (aux_info = curthread->td_lwp at mmap)
Fix verification
fixedApplied fix.diff (move LWP_MP_WEXIT set before lwp_userunmap in lwp_exit) to the guest /usr/src, make -j6 nativekernel + installkernel + reboot. Stock kernel: +~7,000 upmap allocations (~28MB) per 45s PoC run. Patched kernel: 37/148K flat across two identical runs (96.6K allocation requests served, wave rates unchanged ~750/child/45s, live-proc count constant). Leak eliminated; guard analysis shows every token interleaving now either frees the late allocation in the drain or refuses it (also closes the stray pmap_enter edge since in-window faults now fail EINVAL).
['findings/poc/DF-2839/fix.diff', 'findings/poc/DF-2839/fix.run.log (patched: baseline 37/148K -> 37/148K -> 37/148K)', 'findings/poc/DF-2839/fix.build.log (uname #1 + /boot/kernel/kernel mtime Sep 2 05:50)', 'findings/poc/DF-2839/run.3.log (baseline: +7.0K/+6.8K allocations per run)']
Confirmed kernel references
Detail
Exploit chain
unpriv user opens /dev/lpmap (0666); thread V mmaps lpmap regions (+faults one for a real lpmap); V pthread_exit -> lwp_exit -> lwp_userunmap parks in vm_map_remove drain; sibling thread T faults a not-yet-removed V-created page -> vm_fault holds map read-lock -> user_kernel_mapping sees lwp_lpmap==NULL -> lwp_usermap passes the LWP_MP_WEXIT guard (not set yet) and allocates+installs a new lpmap; V finishes teardown having already saved/freed the old one; LWP reaped with the new allocation installed -> 4KB M_UPMAP leaked. Repeat at kHz rates -> kmem exhaustion DoS.
Evidence (decisive lines)
['run.3.log/leak_sample.txt: upmap 15.4K/61.4M -> 22.4K/89.5M -> 29.2K/117M across two 45s unpriv runs, ps count constant 196', 'fix.run.log: patched kernel (uname #1 Sep 2 05:47:01) stays at 37/148K across two identical runs (96.6K requests, zero growth)', 'VERDICT.md: full interleaving trace with path:line, deadlock negative results (run.log, run.2.log)']
PoC changes
v1 (deadlock probe) and v2 (storm) never wedged because LWKT tokens do not persist across tsleep; v3 retargeted the same interleaving at the leak: victims mmap 6 regions each + fault one page (real lpmap), 6 storm threads churn the map to stretch the drain window, 3 touchers sweep victims' regions shuffled with time jitter so faults land inside the drain; children setuid(1001) themselves; detection via vmstat -m upmap live-count deltas instead of unkillable-child detection.
Verified recommended fix
In lwp_exit() set LWP_MP_WEXIT before lwp_userunmap() so lwp_usermap()'s token-serialized post-allocation guard refuses to install an lpmap into an LWP whose teardown already ran (sys/kern/kern_exit.c, one hunk - see fix.diff).
Verdict
Unprivileged /dev/lpmap page fault on a mapping created by a concurrently-exiting thread lands in the wide drain window of lwp_userunmap() (which sleeps in vm_map_remove with LWP_MP_WEXIT not yet set) and installs a fresh lpmap into the exiting LWP via lwp_usermap(); nothing ever frees it (lwp_userunmap is the only free site and already ran) - a permanent 4KB M_UPMAP kernel heap leak per hit. Demonstrated at ~150 pages/s (~28MB per 45s) from 4 unprivileged processes with constant live-process count, monotonic across runs; the 390M M_UPMAP cap is exhaustible in minutes (local kernel-memory-exhaustion DoS). Narrow same-window edges (pmap_enter racing the drain's pmap_remove; pmap_kextract of the page freed at kern_proc.c:1381) could expose a freed kernel heap page RW to userspace - source-proven only, not demonstrated, documented as speculative. The initially-suspected AB-BA deadlock was disproved: LWKT tokens are re-acquired by the scheduler across tsleep (lwkt_getalltokens), so the exiter does not hold lwp_token while sleeping; two 90s probe runs showed zero stalls.
No comments yet.