# 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`):

```c
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`):

```c
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.
