drm_ioctl() dispatches on cmd&0xff only β never validates the user ioctl command (size/dir/group) against the canonical table entry; port dropped upstream's cmd=ioctl->cmd + canonical-size guard: kernel stack OOB write via DRM_IOCTL_GET_STATS and kernel-stack disclosure oracle via any 0-returning unauthenticated ioctl
Summary
mapped_ioctl sizes the argument buffer from the USER-controlled size/dir bits while drm_ioctl selects the handler on nr alone and never compares cmd with the canonical ioctl->cmd - the port deleted upstream v4.7's protection. GET_STATS (no auth) memsets sizeof(drm_stats)==248 into the <=128-byte stack buffer - deterministic RIP=0 panic; GET_MAGIC issued as IOC_VOID|IOC_OUT with size up to 8191 leaks ~8KB of kernel stack (return addresses, live pointers) per call. REPLICATED on guest with an exact-semantics KLD: leak at 2048/8191 bytes reproduced 3x with varying kernel pointers (unpriv), smash reproduced fatal trap 12 at RIP=0 from uid=1001. Desktop video-group user is the reach; content zeros-only on this table so privesc not claimed. Require-canonical-cmd fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4569 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 5.8 KB | β raw | |
| dfioctl_rep.c | β | 3.7 KB | view raw | |
| df_primitive.c | β | 4.1 KB | view raw | |
| drm_size_confusion.c | β | 3.4 KB | view raw | |
| Makefile | β | 166 B | β download | |
| build.sh | β | 201 B | view raw | |
| run.sh | β | 233 B | view raw | |
| run.log | β | 4.5 KB | view raw | |
| panic.txt | β | 598 B | view raw | |
| leak_sample.txt | β | 2.7 KB | view raw | |
| env.txt | β | 222 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw |
DF-4569 β drm_ioctl.c dispatch never validates the user ioctl command
DragonFly drm_ioctl() (sys/dev/drm/drm_ioctl.c:801) dispatches purely on
nr = DRM_IOCTL_NR(cmd) = cmd & 0xff. It never compares the user-supplied
command word against the canonical ioctl->cmd from the dispatch table β
neither the group byte, nor the IOC direction bits, nor the IOCPARM_LEN size
bits. On DragonFly the kernel argument buffer is allocated by
mapped_ioctl() (sys/kern/sys_generic.c:668, 674-680) from the user's size
bits (kmalloc(size) when size > 128, else a 128-byte on-stack buffer;
IOC_VOID commands always get the stack buffer), and the final copyout ships
size bytes back (sys_generic.c:729-730). Every DRM handler interprets
data as the full compile-time structure, so a crafted command word turns
handlers into OOB accesses and leak oracles.
Upstream Linux v4.7 (the generation this port derives from) contains the
exact protection the port dropped: drv_size = _IOC_SIZE(ioctl->cmd);
usize = _IOC_SIZE(cmd); asize = max(usize, drv_size); cmd = ioctl->cmd;
with zero-extension between usize and asize.
Impact
- Kernel stack buffer overflow (write) β
DRM_IOCTL_GET_STATS(nr 0x06, flagsDRM_UNLOCKEDonly, no DRM_AUTH):drm_getstats()doesmemset(stats, 0, sizeof(struct drm_stats))= 248 bytes (drm_ioctl.c:215-224, struct at uapi drm.h:262-268). With a user size β€ 128 that memsets 120 bytes past mapped_ioctl()'s 128-byteubuf.stkbuf, zeroing saved registers and the return address β deterministic panic (proven on the guest kernel via exact-semantics replica: RIP=0). Reachable by any user who can open/dev/dri/cardN(root:video 0660, drm_drv.c:1178 + drmP.h:108-110 β the standard desktop session user). - Kernel stack disclosure (read) β
DRM_IOCTL_GET_MAGIC(nr 0x02, no auth):drm_getmagic()writes 4 bytes and returns 0 (drm_auth.c:58-76). Issued as IOC_VOID|IOC_OUT with size 1024..8191, the trailing copyout leaks up to ~8 KB of kernel stack (return addresses, pointers, trapframe data) to userspace. (The over-large copyout itself is the sys/kern/sys_generic.c IOC_VOID hole β related to pass-1 DF-2734 β but DRM is what makes it trivially reachable: any 0-returning unauthenticated DRM ioctl is an oracle.) - All other
_IOWRcore handlers (drm_version, drm_getcap, drm_getclient, mode-, gem-, syncobj-*, and every driver ioctl dispatched at drm_ioctl.c:819-825) read/write stale stack or out-of-region heap fields when given a short size; on this table onlystruct drm_stats(248) and the heap path (size 129..247, contained by the 256-byte kmalloc bucket) matter for corruption β the stack path is the killer.
Reproduce (guest has no GPU β replica proves the kernel machinery)
# see build.sh / run.sh; on a machine WITH /dev/dri/card0: cc -O2 -o drm_size_confusion drm_size_confusion.c ./drm_size_confusion /dev/dri/card0 leak 2048 # kernel stack disclosure ./drm_size_confusion /dev/dri/card0 smash # panic: RIP=0 # on the QEMU guest (no DRM device): dfioctl_rep.ko replicates the dispatch make && kldload ./dfioctl_rep.ko /tmp/df_primitive leak 2048 ; /tmp/df_primitive leak 8191 /tmp/df_primitive smash 8 # panic (panic.txt)
Expected: leak β 27-30 kernel pointer qwords, different every run;
smash β Fatal trap 12 ... instruction pointer = 0x8:0x0.
Fix
fix.diff β require the exact canonical command word in drm_ioctl()
(port-side equivalent of upstream's cmd = ioctl->cmd). The
sys/kern/sys_generic.c copyout hole should also be bounded (recommend:
for IOC_VOID commands never copy out more than a pointer word) β that part
belongs to a separate kern fix.
DF-4569 VERDICT
What the finding claims
drm_ioctl() (sys/dev/drm/drm_ioctl.c:801-874) validates NOTHING about the
user's ioctl command word except the low byte (nr = DRM_IOCTL_NR(cmd),
line 809; table selection at 819-831). In particular it never compares
cmd with the canonical ioctl->cmd stored in the same table entry, so the
kernel argument buffer it hands to handlers is whatever mapped_ioctl()
(sys/kern/sys_generic.c) allocated from the user-controlled size/dir bits:
kmalloc(IOCPARM_LEN(cmd)) for size>128 non-VOID, a 128-byte stack buffer
otherwise, with a trailing copyout(data, uspc_data, size) on IOC_OUT
(sys_generic.c:729-730). Handlers assume the full struct β OOB + leak.
Upstream Linux v4.7 drm_ioctl.c has the protection: drv_size =
_IOC_SIZE(ioctl->cmd); usize = _IOC_SIZE(cmd); asize = max(usize, drv_size);
cmd = ioctl->cmd; plus zero-extension β verified from kernel.org (fetched
during this run, quoted in README/manifest). The DragonFly port moved the
copy to mapped_ioctl and deleted this logic. Port-introduced regression.
Why the QEMU guest cannot run the real DRM path β and what was done instead
The guest's only display device is QEMU's std VGA (pciconf: vgapci0
pci0:0:2:0 chip=0x11111234); i915 does not bind, no drm kld is loaded,
/dev/dri/ does not exist, and X86_64_GENERIC does not compile DRM in.
So drm_ioctl() itself cannot execute there. Per the audit's established
practice (DF-3715 protocol-replica module), the kernel-side machinery was
proven with a replica kld (dfioctl_rep.c) that reproduces the two dispatch
properties under test byte-for-byte:
* dispatch on cmd & 0xff only β the group/direction/size bits of the
user command are never examined (drm_ioctl.c:809, 847, 854);
* handlers identical in behavior to drm_getmagic (4-byte write at
offset 0, return 0 β drm_auth.c:58-76) and drm_getstats
(memset(data, 0, 248) β drm_ioctl.c:215-224,
sizeof(struct drm_stats)=248 on LP64: uapi drm.h:262-268).
What was reproduced on the guest (uid=1001, /dev/dfioctl 0666)
- Kernel stack disclosure β
ioctl(fd, IOC_VOID|IOC_OUT|(2048<<16)| ('d'<<8)|0x02, buf)(nr 0x02 = DRM_IOCTL_GET_MAGIC): returned 0 with 2048 bytes of kernel stack containing 30 qword kernel pointers (0xffff810cβ¦, 0xfffff801β¦, 0xffffffff80β¦), varying across 3 runs (md5s in run.log). At size=8191 (IOCPARM_MAX) the copyout still returns 0 and ships ~8 KB β the read runs far past the 128-byteubuf.stkbuf. The identical command word on a real /dev/dri/card0 reaches drm_getmagic via drm_ioctl.c:573 (flags DRM_UNLOCKED only β no DRM_AUTH) and behaves the same: it writes 4 bytes, returns 0. - Kernel stack buffer overflow β deterministic panic β
ioctl(fd, IOC_OUT|(8<<16)|('d'<<8)|0x06, buf)(nr 0x06 = DRM_IOCTL_GET_STATS, flags DRM_UNLOCKED only): handler memsets 248 bytes over the 128-byte stack buffer; return address zeroed; guest died withFatal trap 12: page fault while in kernel mode β¦ instruction pointer = 0x8:0x0from unpriv process 1059 (panic.txt). Identical arithmetic on real DRM: drm_getstats memsets sizeof(struct drm_stats)=248 regardless of the buffer mapped_ioctl sized from the user's size bits.
Exploitability assessment (the honest ceiling)
- The smash primitive is a zero-filled 120-byte overwrite past a 128-byte kernel stack array in mapped_ioctl's frame (zero content because the memset value is 0). Direct RIP control needs non-zero content; zeroing saved registers/return address gives a reliable local DoS/panic. A non-zero-content variant would need a handler writing attacker-influenced data past offset 128 β none exists in the core table (drm_stats is the only struct >128 bytes); driver tables (i915 etc.) were sized-checked (all β€ ~72 bytes), and heap sizes 129..247 are contained by the 256-byte kmalloc bucket. So: unpriv β reliable kernel panic on any DRM box; privesc not demonstrated from this table.
- The leak primitive (via the sys_generic IOC_VOID copyout hole, exposed by any 0-returning unauth DRM ioctl) is a full kernel-stack disclosure β on this guest 27-30 kernel pointers per call β defeating any future KASLR and handing out live kernel pointers (pointer, heap-layout and β with stack grooming β potentially key/credential adjacency information).
Status reasoning
The DRM dispatch semantics and both kernel-side primitives are proven on
the guest kernel (replica module + identical crafted commands + the exact
memset/write/return-0 handler behaviors); the DRM-specific last hop
(nrβdrm_getstats/drm_getmagic through the real table) is proven statically
(drm_ioctl.c:215-224, 573, 577; drm_auth.c:58-76) but could not execute
because the guest has no DRM device. Overall: mechanisms reproduced,
DRM-path execution blocked by missing hardware β status inconclusive
with reproduced=0 (strict honesty), impact ceiling panic
(memcorrupt family + info leak), root cause certain.
Fix
fix.diff adds the canonical-command check to drm_ioctl() (equivalent of
upstream's cmd = ioctl->cmd). Validation: applied cleanly against the
guest's byte-identical /usr/src copy (patch -p0 RC=0; GNU patch dry-run
RC=0 on host). Runtime before/after on real DRM hardware was not possible
on this guest (no DRM device; DRM not in X86_64_GENERIC) β
fix_status=not_testable. Recommend additionally bounding the sys_generic.c
IOC_VOID copyout (separate kern finding).
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.diff authored and verified to apply cleanly to the guest's byte-identical /usr/src/sys/dev/drm/drm_ioctl.c (patch -p0 RC=0; GNU patch dry-run RC=0). Runtime before/after validation impossible on this guest: no DRM device and DRM is not compiled into X86_64_GENERIC, so the patched dispatch cannot be exercised. Baseline (unpatched) behavior was reproduced via the exact-semantics replica module (leak + RIP=0 panic).
findings/poc/DF-4569/fix.diff; VERDICT.md 'Fix' section; guest apply-check transcript in run.log notes (patch -p0 RC=0, grep -c DF-4569 == 1)
Confirmed kernel references
- sys/dev/drm/drm_ioctl.c:801
- sys/dev/drm/drm_ioctl.c:809
- sys/dev/drm/drm_ioctl.c:819-831
- sys/dev/drm/drm_ioctl.c:847
- sys/dev/drm/drm_ioctl.c:854
- sys/dev/drm/drm_ioctl.c:215-224
- sys/dev/drm/drm_ioctl.c:573
- sys/dev/drm/drm_ioctl.c:577
- sys/dev/drm/drm_auth.c:58-76
- sys/kern/sys_generic.c:668-680
- sys/kern/sys_generic.c:729-730
- sys/dev/drm/include/uapi/drm/drm.h:262-268
- sys/dev/drm/drm_drv.c:1178
- sys/dev/drm/include/drm/drmP.h:108-110
- linux v4.7 drivers/gpu/drm/drm_ioctl.c (drv_size/usize/asize + cmd=ioctl->cmd)
Detail
Exploit chain
open /dev/dri/cardN (root:video 0660; standard desktop session user) -> ioctl(fd, IOC_OUT|(8<<16)|('d'<<8)|0x06) -> mapped_ioctl hands drm_ioctl the 128-byte stack buffer -> drm_getstats memsets sizeof(struct drm_stats)=248 -> 120-byte kernel stack overflow, RIP=0 panic. Leak variant: ioctl(fd, IOC_VOID|IOC_OUT|(size<<16)|0x02) -> drm_getmagic returns 0 -> mapped_ioctl copyouts `size' bytes from the 128-byte stack buffer -> up to ~8KB kernel stack disclosure (pointers, trapframe data). Privesc not demonstrated: the overwrite content is zeros-only on this table (drm_stats is the only >128B core struct; driver structs <=72B; heap sizes 129..247 contained by the 256B kmalloc bucket).
Evidence (decisive lines)
run.log [1]-[4] (full transcripts: leak hexdump with kernel pointers, 3-run md5 variance, 8191-byte probe, smash console output), panic.txt (Fatal trap 12, instruction pointer = 0x8:0x0, unpriv process 1059), leak_sample.txt, dfioctl_rep.c (replica of drm_ioctl dispatch semantics + drm_getmagic/drm_getstats behaviors), df_primitive.c (unpriv trigger), drm_size_confusion.c (real-DRM trigger for hardware boxes), fix.diff (canonical-cmd check, applied cleanly to guest /usr/src)
PoC changes
The finding seed had no runnable PoC; everything was authored in this run. Guest build required the kmod.mk route (hand-linked .ko panicked the linker with 'lost base for relatab'); replica module needed DFly-specific dev_ops/dev_ioctl_args fixes (sys/device.h + sys/conf.h, cdev_t). df_primitive adds the VOID|OUT leak mode and the smash mode with hand-encoded BSD ioctl command words.
Verified recommended fix
In drm_ioctl(), require (unsigned int)cmd == ioctl->cmd (port-side equivalent of upstream cmd=ioctl->cmd) before permit/dispatch; independently bound mapped_ioctl()'s copyout for IOC_VOID commands.
Verdict
Root cause certain and kernel-side machinery reproduced on the guest: drm_ioctl() dispatches on cmd&0xff only and never validates the user command against the canonical ioctl->cmd (upstream v4.7's drv_size/usize/asize + cmd=ioctl->cmd logic was dropped by the port), while mapped_ioctl() sizes the handler buffer from the user's IOCPARM_LEN/dir bits. Via an exact-semantics replica module, an unprivileged user demonstrated (1) drm_getstats-style memset(248) over the 128-byte on-stack buffer -> return address zeroed -> deterministic fatal kernel-mode page fault at RIP=0 (panic.txt), and (2) drm_getmagic-style 4-byte-write+return-0 handler turning the IOC_VOID|IOC_OUT cmd into an up-to-8KB kernel stack disclosure (27-30 kernel pointer qwords per run, varying across runs). The final DRM-specific hop (nr -> drm_getstats/drm_getmagic through the real table) is proven statically (drm_ioctl.c:215-224, 573, 577; drm_auth.c:58-76) but could not execute on this guest because it has no DRM device (QEMU std VGA, no /dev/dri, DRM not built into X86_64_GENERIC) -- hence inconclusive rather than reproduced.
No comments yet.