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

drm_clients_info() walks dev->filelist under the wrong lock with a sleeping kmalloc between a count pass and an unbounded copy pass: concurrent open()/close() yields a kernel heap OOB write (or UAF read)

Summary

The world-readable hw.dri.N.clients sysctl counts dev->filelist under dev->struct_mutex, then does a SLEEPING kmalloc sized by that count, then re-walks copying whole drm_file entries with no bound against the stale count - but every list mutation runs under dev->filelist_mutex (open list_add, close list_del, kfree under neither). Racing open/close against the sysctl read writes (growth x sizeof(drm_file)) bytes past an attacker-sized kmalloc - content partially attacker-controlled (pid/magic/caps) plus kernel heap pointers - or UAF-reads freed nodes. Video-group + sysctl only: no auth, no master, no ioctls. Logic half proven 3/3 on guest (OOB writes detected past the kmalloc'd buffer every run); hardware-gated for the live race. filelist_mutex + never-trust-stale-count fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4531 Β· 8 files
FileTypeDescriptionSize
trigger.c β€” 3.9 KB view raw
logicproof.c β€” 4.9 KB view raw
build.log β€” 172 B view raw
run.log β€” 1.7 KB view raw
fix.diff β€” 1.1 KB view raw
fix_build.log β€” 1.2 KB view raw
VERDICT.md β€” 4.5 KB ↓ raw
verdict.json β€” 4.3 KB view raw
VERDICT.md
↓ download raw

DF-4531 VERDICT

Classification: REAL BUG β€” inconclusive at runtime only because the QEMU audit guest has no DRM hardware. Source-level certainty is certain (every lock edge and every line of the control flow verified in-tree, plus a deterministic algorithmic proof of the overflow run on the guest 3/3).

Root cause chain (path:line)

  1. sys/dev/drm/drm_sysctl.c:268 β€” DRM_LOCK(dev) = lockmgr(&dev->struct_mutex, LK_EXCLUSIVE) (drmP.h:114). This is the ONLY synchronization the handler has.
  2. drm_sysctl.c:271-272 β€” pass 1 counts privcount entries of dev->filelist.
  3. drm_sysctl.c:274-275 β€” kmalloc(..., M_WAITOK|M_NULLOK): sleeps, unbounded duration under memory pressure; even without sleeping, holding a lockmgr exclusive lock does not stop other CPUs.
  4. drm_sysctl.c:281-282 β€” pass 2 copies *priv into the count-sized buffer with no bound check β€” list_for_each_entry simply runs to the end of the list.
  5. sys/dev/drm/drm_file.c:514-516 β€” drm_open_helper() adds entries under mutex_lock(&dev->filelist_mutex) (mutex_lock = lockmgr(LK_EXCLUSIVE), include/linux/mutex.h:38) β€” a different lock object than struct_mutex. Nothing serializes it against steps 2-4.
  6. sys/dev/drm/drm_file.c:380-382 + :430 β€” drm_close() removes under filelist_mutex and then kfree(file_priv) under no list lock at all (only drm_global_mutex): a shrinking/deleting race turns the walker into a use-after-free read.

Growth β‡’ OOB write of growth Γ— sizeof(struct drm_file) (β‰ˆ 0x3c0+ bytes per entry on x86_64) past a kmalloc'd region whose size the attacker chose via the entry count at pass-1 time. The attacker fully controls:

  • the base count N (open N fds before reading the sysctl),
  • the growth (open fds from another thread during the read),
  • the repeat rate (loop the sysctl read),
  • part of the written content (pid, magic, capability bitfields), the rest being kernel heap pointers (idr roots, list heads) β€” useful fake-object material on a guest with no SMAP/SMEP/KASLR.

Shrink β‡’ UAF read (not directly user-visible; secondary).

Why High severity

Kernel heap buffer overflow, write primitive, reachable by a local unprivileged user in the video group (default desktop membership), with no DRM authentication and no master rights β€” just open(2) and sysctl(3). On the standard audit-guest configuration (no SMAP/SMEP/KASLR) a slab-groom β†’ victim-object overwrite chain toward uid=0 is the expected escalation route; that end-to-end chain could not be developed here only because the guest lacks a DRM device (QEMU std-VGA, no i915/amdgpu, no virtio-gpu driver in the tree β€” verified: pciconf -l shows only vgapci0 with no drm child; /dev/dri absent; sysctl hw.dri ENOENT).

What was run (honest ledger)

Step Result
logicproof.c on guest (x3) OOB WRITES DETECTED 3/3 (11.9MB / 26.8MB / 29.0MB past-buffer writes in the modelled window)
trigger.c build on guest OK (-Wall -Wextra clean except 2 unused-param warnings)
trigger.c run on guest hardware-gated: /dev/dri/card0: No such file or directory; sysctl hw.dri.0.clients β†’ unknown oid
fix.diff applies to pristine sys/dev/drm/drm_sysctl.c verified with patch --dry-run on a scratch copy
patched drm_sysctl.c compiled with exact kernel CFLAGS (X86_64_GENERIC obj dir) RC=0, drm_sysctl_patched.o produced (fix_build.log)
guest /usr/src/sys restored md5s match the repo after restore; guest otherwise untouched

Fix

fix.diff: hold dev->filelist_mutex (the lock all mutators already use) across both passes, release on the ENOMEM path, and bound pass 2 by the count with privcount = i after the copy so the print loop can never read uninitialized slots. Lock order is safe: no code path takes filelist_mutex before struct_mutex, so struct_mutex β†’ filelist_mutex (the patched order) introduces no inversion. The M_WAITOK sleep under filelist_mutex is legal (lockmgr-based, sleepable) and bounded. fix_status: not_testable β€” runtime validation needs a DRM device; the patch compiles clean under the real kernel flags and makes the overflow unreachable by construction.

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

Patch compiles clean (RC=0) as a translation unit against the pristine guest kernel tree with the exact X86_64_GENERIC CFLAGS (fix_build.log); git-apply verified on a scratch copy. Runtime before/after comparison impossible on this guest (no DRM device), so fix_status=not_testable; the fix makes both the OOB write and the UAF read unreachable by construction (single lock domain + hard bound).

findings/poc/DF-4531/fix.diff; findings/poc/DF-4531/fix_build.log
↓ fix.diffper-fix-DF-4531

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (logicproof 3/3 OOB WRITES DETECTED + honest hw-gated trigger output); VERDICT.md (full path:line lock analysis); fix_build.log (patched drm_sysctl.c compiles RC=0 with exact kernel CFLAGS on guest); fix.diff (applies clean, verified via patch --dry-run on scratch copy)

PoC changes

Wrote from scratch (no seed): trigger.c (sysctl-reader + open/close-grower threads using DFly sysctl(3)); logicproof.c (deterministic userland reproduction of the exact count->sleeping-alloc->unbounded-copy control flow with an OOB probe region).

Verified recommended fix

Serialize both walks of dev->filelist under dev->filelist_mutex (the lock every mutator already uses) and never let the copy pass exceed the count: see fix.diff.

Verdict

REAL BUG, runtime-gated on absent hardware. drm_clients_info() (the world-readable hw.dri.N.clients sysctl, drm_sysctl.c:260-301) walks dev->filelist twice under dev->struct_mutex (DRM_LOCK, drmP.h:114): a count pass (271-272), a SLEEPING kmalloc(M_WAITOK) sized by that stale count (274-275), then an unbounded copy pass (281-282). Every list mutation runs under dev->filelist_mutex instead: drm_open_helper list_add (drm_file.c:514-516), drm_close list_del (drm_file.c:380-382), and the kfree(file_priv) (drm_file.c:430) under neither. A local user in the video group (card nodes are 0660 root:video, drmP.h:108-110) racing open()/close() against the sysctl read gets: growth-between-passes -> (growth x sizeof(struct drm_file)) heap OOB WRITE past an attacker-sized kmalloc with partially controlled content (pid/magic/bitfields plus kernel heap pointers), or shrink-between-passes -> use-after-free read of freed drm_file nodes. Algorithmic half proven deterministically on the guest 3/3 runs (logicproof.c: OOB WRITES DETECTED every run, 11.9-29.0MB modelled overflow); the kernel race itself could not run because the QEMU guest has no DRM device (/dev/dri absent, sysctl hw.dri ENOENT, vgapci0 with no drm driver attached; the tree has no virtio-gpu). End-to-end uid=0 chain (slab groom -> victim overwrite) was not developable for the same hardware reason; on the standard no-SMAP/SMEP/KASLR target this primitive is the expected starting point.