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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4569 Β· 14 files
FileTypeDescriptionSize
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, flags DRM_UNLOCKED only, no DRM_AUTH): drm_getstats() does memset(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-byte ubuf.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 _IOWR core 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 only struct 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.

VERDICT.md
↓ download raw

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)

  1. 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-byte ubuf.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.
  2. 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 with Fatal trap 12: page fault while in kernel mode … instruction pointer = 0x8:0x0 from 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_testable
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-4569

Confirmed kernel references

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.