vmalloc()/vzalloc()/vmalloc_user() implemented over plain kmalloc violate the Linux vmalloc contract (page alignment, whole-page ownership) that the legacy DRM _DRM_SHM userspace-mapping path is written against
Summary
The only consumer of the contract (drm_bufs.c:244 _DRM_SHM + drm_vm.c:130 vtophys per-page mapping) is dead code on this tree (byte-offset vs page-number hash-key mismatch, corroborating DF-5122) - benign today, an intra-page heap disclosure the day the dispatch is revived or a new consumer maps vmalloc_user memory page-wise. kmem-backed + registry fix. BONUS: the kvmalloc SIZE_MAX/size div0 claim was empirically killed on the guest (ZERO_LENGTH_PTR return, mul/jo not divq in disassembly).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5190 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 917 B | β raw | |
| VERDICT.md | β | 5.2 KB | β raw | |
| negative-kvmalloc-div0.txt | β | 3.6 KB | view raw | |
| fix.diff | β | 2.7 KB | view raw | |
| verdict.json | β | 3.4 KB | view raw | |
| env.txt | β | 264 B | view raw |
DF-5190 β vmalloc()/vzalloc()/vmalloc_user() implemented over kmalloc violates the Linux vmalloc contract
Status
Info / speculative β no runnable PoC (verification skipped per reviewer contract: Info-tier hardening finding). The pack documents the code-trace analysis that establishes (a) the contract violation, (b) why it is not exploitable in this tree today, and (c) what changes would make it live.
Additionally, this pack carries the pass-2 negative evidence for the sibling
candidate "kvmalloc_array divide-by-zero on element size 0"
(negative-kvmalloc-div0.txt): empirically refuted on the shipped kernel binary.
What to look at
VERDICT.mdβ full analysisnegative-kvmalloc-div0.txtβ guest experiment disproving the kvmalloc_array #DE claimfix.diffβ proposed kmem-backed reimplementation of the vmalloc family
Nothing to build or run for the finding itself.
DF-5190 VERDICT β vmalloc family contract violation (Info / speculative)
File: sys/dev/drm/linux_vmalloc.c (pass 2, GLM 5.3)
Lines: 107β133 (vmalloc, vzalloc, vmalloc_user, vfree)
The defect
The LinuxKPI vmalloc shim implements the entire vmalloc family as plain
kmalloc(M_DRM):
vmalloc()βkmalloc(size, M_DRM, M_WAITOK)(linux_vmalloc.c:107-111)vzalloc()βkmalloc(size, M_DRM, M_WAITOK|M_ZERO)(linux_vmalloc.c:113-117)vmalloc_user()βkmalloc(size, M_DRM, M_WAITOK|M_ZERO)(linux_vmalloc.c:119-124)
Linux guarantees for these APIs (Documented/core-api/mm-api.rst, include/linux/vmalloc.h) that DragonFly's kmalloc does not provide:
- page-aligned, virtually-contiguous, whole-page allocations;
- that every page belonging to the allocation is exclusively owned by it and
zeroed β the property that makes
vmalloc_user()memory safe to expose to userspace page-by-page; - sizes rounded up to page granularity.
The only in-tree consumer of the dangerous property
drm_addmap_core() for _DRM_SHM maps (sys/dev/drm/drm_bufs.c:244):
map->handle = vmalloc_user(map->size); /* drm_bufs.c:244 */
...
map->offset = (unsigned long)map->handle; /* drm_bufs.c:251 */
and the userspace mapping of such a handle goes through the legacy d_mmap
handler drm_mmap() (sys/dev/drm/drm_vm.c:129-131):
case _DRM_SHM:
phys = vtophys((char *)map->handle + offset); /* drm_vm.c:130 */
i.e. the page containing (any part of) the kmalloc chunk would be mapped wholesale into the calling process. With a non-page-aligned chunk that shares physical pages with neighbouring M_DRM objects, this would disclose kernel heap memory beyond the allocation.
Why it is NOT exploitable in this tree today (verified)
- The allocation size is page-aligned before the call.
drm_addmap_core()doesmap->size = PAGE_ALIGN(map->size)for_DRM_SHM(drm_bufs.c:173-174), sovmalloc_user()always receives a page multiple, andM_ZEROzeroes exactly that many bytes β no uninitialized tail. - The legacy mmap sink is dead code on DragonFly.
drm_mmap()looks the map up withdrm_ht_find_item(&dev->map_hash, offset)using the raw byte offset (drm_vm.c:91), while map-hash keys are page numbers (drm_bufs.c:98,hash->key = user_token >> PAGE_SHIFT) or 21-bit hashed keys for_DRM_SHM(drm_bufs.c:125-127). An absolute byte offset can essentially never equal a key, so the lookup fails and the handler returns-EINVALbefore reachingvtophys(). This is the same dead-path conclusion already on record as DF-5122 ("unreachable on every shipped driver"), which this pass independently re-derived before finding DF-5122 in the DB. - Other users are unaffected.
vmalloc_to_page()(include/linux/mm.h:165) ispmap_kextract-based and works for any kernel address; thevfree/kfreepairing is allocator-consistent (the DRMkfreeshim NULL-guards, include/linux/slab.h:39-42).
Net: contract divergence with real landmine potential (any future fix that revives the legacy SHM mmap dispatch, or any newly ported driver that mmaps a vmalloc_user buffer page-wise, silently turns this into an intra-page kernel heap disclosure), but no demonstrable impact on the current tree. Filed as Info/speculative to document the hazard and its preconditions.
Fix
fix.diff in this pack re-implements vmalloc/vzalloc/vmalloc_user over
kmem_alloc3() (page-aligned, wired kernel-object memory, explicitly zeroed)
with a small size registry under the existing vmap_lock, and makes vfree()
free through that registry (falling back to kfree for pointers that do not
belong to it, which keeps the kvfree()==kfree pairing for kvmalloc-family
memory intact).
Sibling candidate killed in this pass (negative result)
"kvmalloc_array(n, 0, flags) executes SIZE_MAX / 0 β kernel #DE"
(linux_vmalloc.c:141). Source-level the division exists, and C makes it UB for
size == 0 β but the shipped drm.ko compiled by cc 8.3 -O2 transforms
n > SIZE_MAX / size into a mul/jo overflow check: there is no divq
in kvmalloc_array and the call cannot fault. Verified live on the audit
guest by loading a kld that calls kvmalloc_array(1, 0, GFP_KERNEL) through
the loaded drm.ko β it returned 0xfffffffffffffff8 (ZERO_LENGTH_PTR) without
faulting; disassembly of the shipped drm.ko confirms. No in-tree caller passes
a zero element size anyway (all pass sizeof constants). Full transcript in
negative-kvmalloc-div0.txt. Not filed.
Guest
DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 β¦
X86_64_GENERIC, no /dev/dri (virtio-vga unattached β no DRM device), so even
the dead legacy path has no runtime surface on this guest. Verification:
skipped (Info tier). Guest was reset to the clean-source snapshot after the
kvmalloc experiment.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableFix not validated on the guest: the finding has no runnable trigger (sink path dead, no DRM device on guest), so no baseline exists to compare a patched kernel against. fix.diff is a compile-reviewed proposal only.
n/a
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (contract analysis + why the drm_vm.c:130 sink is dead); negative-kvmalloc-div0.txt (guest transcript + objdump of shipped drm.ko kvmalloc_array showing mul/jo instead of divq); fix.diff (kmem-backed reimplementation)
PoC changes
n/a - no PoC seed existed; this is a code-trace finding. The kvmalloc_array guest experiment (not a finding) used a 30-line kld built in-guest against /usr/src.
Verified recommended fix
Back the vmalloc family with page-aligned wired kmem (kmem_alloc3 + explicit zeroing + size registry for vfree) instead of kmalloc - see fix.diff
Verdict
vmalloc()/vzalloc()/vmalloc_user() (sys/dev/drm/linux_vmalloc.c:107-124) are implemented as plain kmalloc(M_DRM), violating the Linux vmalloc contract (page-aligned, whole-page, exclusively-owned zeroed pages). The only in-tree consumer of the user-mappable property is the legacy _DRM_SHM path (drm_bufs.c:244 handle = vmalloc_user(size), mmap sink drm_vm.c:130 vtophys(handle+offset)), and that sink is unreachable: drm_mmap() looks maps up by raw byte offset (drm_vm.c:91) while map-hash keys are page numbers (drm_bufs.c:98), so the lookup fails before vtophys() is ever reached (independently re-derived here; corroborates pass-1 DF-5122 'unreachable on every shipped driver'). Additionally drm_bufs.c:173-174 PAGE_ALIGNs _DRM_SHM sizes and M_ZERO zeroes the full (page-multiple) chunk, so even a revived sink would not disclose intra-chunk slack. No demonstrable impact today; filed as Info/speculative to document the hazard for any future revival of the legacy mmap dispatch or newly ported code that mmaps vmalloc_user() memory page-wise. Verification skipped per reviewer contract for Info-tier findings; guest has no /dev/dri anyway. Sibling candidate 'kvmalloc_array SIZE_MAX/0 divide-by-zero' was empirically refuted on the shipped drm.ko (kld call returned ZERO_LENGTH_PTR, no divq in the disassembly) and is recorded in negative-kvmalloc-div0.txt, not filed.
No comments yet.