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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5190 Β· 6 files
FileTypeDescriptionSize
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 analysis
  • negative-kvmalloc-div0.txt β€” guest experiment disproving the kvmalloc_array #DE claim
  • fix.diff β€” proposed kmem-backed reimplementation of the vmalloc family

Nothing to build or run for the finding itself.

VERDICT.md
↓ download raw

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:

  1. page-aligned, virtually-contiguous, whole-page allocations;
  2. 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;
  3. 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)

  1. The allocation size is page-aligned before the call. drm_addmap_core() does map->size = PAGE_ALIGN(map->size) for _DRM_SHM (drm_bufs.c:173-174), so vmalloc_user() always receives a page multiple, and M_ZERO zeroes exactly that many bytes β€” no uninitialized tail.
  2. The legacy mmap sink is dead code on DragonFly. drm_mmap() looks the map up with drm_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 -EINVAL before reaching vtophys(). 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.
  3. Other users are unaffected. vmalloc_to_page() (include/linux/mm.h:165) is pmap_kextract-based and works for any kernel address; the vfree/ kfree pairing is allocator-consistent (the DRM kfree shim 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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-5190

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.