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)
PoC verification
Evidence pack
findings/poc/DF-4531 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
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.drm_sysctl.c:271-272β pass 1 countsprivcountentries ofdev->filelist.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.drm_sysctl.c:281-282β pass 2 copies*privinto the count-sized buffer with no bound check βlist_for_each_entrysimply runs to the end of the list.sys/dev/drm/drm_file.c:514-516βdrm_open_helper()adds entries undermutex_lock(&dev->filelist_mutex)(mutex_lock=lockmgr(LK_EXCLUSIVE),include/linux/mutex.h:38) β a different lock object thanstruct_mutex. Nothing serializes it against steps 2-4.sys/dev/drm/drm_file.c:380-382+:430βdrm_close()removes underfilelist_mutexand thenkfree(file_priv)under no list lock at all (onlydrm_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_testablePatch 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
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.
No comments yet.