Uninitialized kernel stack variable leaked to userspace by i915_gem_mmap_ioctl() when size==0 (DragonFly-only path)
Summary
In the DragonFly port of i915_gem_mmap_ioctl(), a zero-length mmap request takes an early 'goto out' that bypasses every assignment of the local 'unsigned long addr'; the out: tail then does 'args->addr_ptr = (uint64_t)addr' with error==0, returning 8 bytes of stale kernel stack to unprivileged userspace on every call. The #ifdef __DragonFly__ early-out is port glue - the Linux original assigns addr unconditionally. Unprivileged local user with /dev/dri render-node access (both GEM_CREATE and MMAP are DRM_RENDER_ALLOW): loop the ioctl with size=0, optionally interleaving syscalls to seed the stack, and harvest stale stack contents (kernel pointers -> KASLR defeat / stack-oracle). No auth, no race, unlimited repetitions. PoC compiles clean on the guest; runtime unreachable (virtio-only, no /dev/dri) - honest inconclusive / source-certain. On hardware this pairs with DF-1146 to turn address disclosure into full compromise.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3634 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_df3634.c | β | 2.9 KB | view raw | |
| build.sh | β | 94 B | view raw | |
| run.sh | β | 108 B | view raw | |
| build.log | β | 216 B | view raw | |
| run.log | β | 365 B | view raw | |
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| fix.diff | β | 331 B | view raw | |
| manifest.json | β | 854 B | view raw | |
| verdict.json | β | 3.8 KB | view raw | |
| env.txt | β | 357 B | view raw |
DF-3634 β Uninitialized kernel-stack variable leaked to userspace via i915_gem_mmap_ioctl (size==0)
What
i915_gem_mmap_ioctl() (sys/dev/drm/i915/i915_gem.c:1917) declares
unsigned long addr; at line 1923 and β on the DragonFly-specific
fast-out for zero-length mappings β jumps past every assignment of
addr:
1960 #ifdef __DragonFly__
1961 if (args->size == 0)
1962 goto out;
The out: path then uses the uninitialized variable:
2056 out:
2057 i915_gem_object_put(obj);
2058 if (IS_ERR((void *)addr)) { /* uninit read #1 */
2059 DRM_DEBUG("PTR_ERR=%ld\n", PTR_ERR((void *)addr));
2060 return addr; /* garbage error code */
2061 }
2063 args->addr_ptr = (uint64_t) addr; /* uninit read #2: 8 bytes of
stale kernel stack copied to
the ioctl args */
2067 return error; /* error == 0 here */
The ioctl returns success and hands 8 bytes of stale kernel stack back to
userspace in args->addr_ptr. The first real assignment of addr is at
line 1977 (addr = round_page((vm_offset_t)p->p_vmspace->vm_taddr + maxtsiz + maxdsiz);),
after the goto out.
Reachability
- Ioctl
DRM_I915_GEM_MMAPis registeredDRM_RENDER_ALLOW(sys/dev/drm/i915/i915_drv.c:3246) β reachable from any unprivileged user with access to a render node / card node. - Preconditions: an open DRM fd, one GEM handle (
DRM_I915_GEM_CREATE, also RENDER_ALLOW),flags=0,size=0, any valid handle. - No DRM_AUTH, no master, no root required.
- Repeatable without limit; the leaked slot contains whatever previous kernel frames left at that stack offset (kernel pointers, user pointers, lengths). Classic KASLR-defeat / stack-content oracle.
Impact
Info leak of limited kernel memory (8 bytes of kernel stack per call,
unlimited calls) β Medium per the project rubric. The secondary
return addr; path can also turn garbage into an ioctl return code.
Reproduce (on hardware with an Intel GPU)
cc -O2 -Wall -o poc_df3634 poc_df3634.c ./poc_df3634 /dev/dri/card0 # or renderD128 # expected on vulnerable hw: "ioctl ret=0 addr_ptr=0x...." non-zero, # varying/pointing at stale stack contents across calls with # different preceding syscalls
On this audit guest the PoC builds cleanly but cannot execute: the
QEMU guest is virtio-only (vgapci0 ... chip=0x11111234 virtio-gpu,
no /dev/dri, no drm modules) and i915 is a PCI-driver that cannot
attach to non-Intel hardware. Verdict is therefore inconclusive at
runtime, source-certain β the uninit path is directly visible in the
code and requires no race.
Fix
fix.diff in this directory (reject size==0 with -EINVAL, which also
restores the intended semantics; alternative minimal fix is to
initialize addr at declaration).
DF-3634 VERDICT
Status: inconclusive (runtime) / certain (source). Impact if reachable: kernel stack info leak (8 bytes/call, unlimited).
Why the bug is certain from source
sys/dev/drm/i915/i915_gem.c:1923βunsigned long addr;is a plain uninitialized local.sys/dev/drm/i915/i915_gem.c:1961-1962β the DragonFly-onlyif (args->size == 0) goto out;bypasses the first assignment ofaddrat:1977(addr = round_page((vm_offset_t)p->p_vmspace->vm_taddr + maxtsiz + maxdsiz)).sys/dev/drm/i915/i915_gem.c:2056-2067β theout:tail performsif (IS_ERR((void *)addr))andargs->addr_ptr = (uint64_t) addr;witherror == 0on this path, i.e. success + stale stack bytes copied into the ioctl out-argument.- Nothing between (2) and (3) assigns
addron this path (DRM_DEBUGat :1949 only prints args/obj). - The ioctl is unprivileged-reachable:
DRM_IOCTL_DEF_DRV(I915_GEM_MMAP, i915_gem_mmap_ioctl, DRM_RENDER_ALLOW)βsys/dev/drm/i915/i915_drv.c:3246; the only precondition is a valid GEM handle, creatable viaDRM_IOCTL_I915_GEM_CREATE(also RENDER_ALLOW,i915_drv.c:3243).
CWE-457 (use of uninitialized variable) / CWE-909. Comparable class: the historic DRM_INFO-leak family of uninit ioctls.
Verification performed
- PoC written with uapi structs/ioctl numbers matched 1:1 against
sys/dev/drm/include/uapi/drm/i915_drm.h(DRM_I915_GEM_CREATE=0x1b,DRM_I915_GEM_MMAP=0x1e,DRM_COMMAND_BASE+nr, struct layouts verified at i915_drm.h:638 and i915_drm.h:686). - Built on the DragonFly audit guest:
BUILD-OKwithcc -O2 -Wall, zero warnings (build.log). - Execution impossible on this guest:
open("/dev/dri/card0")β ENOENT; guest GPU is virtio (chip=0x11111234), no drm klds, i915 cannot probe (run.log). No Intel GPU emulation exists for this QEMU setup. - No runtime divergence attempted via i915 module load: i915 is a PCI
driver; without an Intel VGA PCI id it never attaches and creates no
/dev/dri node (checked
pciconf -l).
Exploit chain (on real hardware)
unprivileged open("/dev/dri/renderD128") β DRM_IOCTL_I915_GEM_CREATE
(handle) β loop: interleave arbitrary syscalls (to seed the thread's
kernel stack with interesting values) with DRM_IOCTL_I915_GEM_MMAP
{handle, size=0} β read args->addr_ptr = 8 bytes stale kernel stack.
Use for pointer disclosure / KASLR defeat; combine with any memory
corruption primitive (e.g. DF-1146 in the same driver) for full
compromise.
Fix validation
fix.diff authored against the read-only sys/ tree (reject size==0 with -EINVAL before the reference-counted map is attempted; also prevents the pointless object lookup/reference churn). Not applicable to build on this guest (no i915 device to test against), so fix_status=not_testable; the fix makes the uninit read unreachable by construction (early return).
Kernel references
- sys/dev/drm/i915/i915_gem.c:1923 (declaration, uninit)
- sys/dev/drm/i915/i915_gem.c:1961-1962 (DFly early-out bypassing init)
- sys/dev/drm/i915/i915_gem.c:1977 (first assignment, skipped)
- sys/dev/drm/i915/i915_gem.c:2056-2067 (uninit use β userspace leak)
- sys/dev/drm/i915/i915_drv.c:3246 (DRM_RENDER_ALLOW registration)
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 returns -EINVAL for size==0 (drops the object reference first), removing the goto-out path entirely so addr can never be read uninitialized. Cannot be kernel-built/run against this finding on the audit guest: i915 does not attach without Intel hardware, so no baseline or patched behavior is observable; the fix is correct by construction (the uninit use site becomes dead code on that path).
fix.diff hunk at i915_gem.c:1958-1967; source trace in VERDICT.md.
Confirmed kernel references
Detail
Exploit chain
open /dev/dri/renderD128 -> DRM_IOCTL_I915_GEM_CREATE (handle) -> loop { seed thread kernel stack via arbitrary syscalls; DRM_IOCTL_I915_GEM_MMAP {handle, size=0} } -> read args->addr_ptr = 8 bytes stale kernel stack; use disclosed pointers to defeat KASLR / locate objects in combination with a memory-corruption primitive such as DF-1146 in the same driver.
Evidence (decisive lines)
VERDICT.md (full source trace); poc_df3634.c (uapi structs matched 1:1 to sys/dev/drm/include/uapi/drm/i915_drm.h); build.log (BUILD-OK on DragonFly guest); run.log (open ENOENT, no /dev/dri, virtio GPU, no drm klds). Decisive lines: i915_gem.c:1961-1962 goto out before :1977 assignment; :2063 args->addr_ptr = (uint64_t)addr.
PoC changes
Rewrote embedded uapi (initial draft had wrong ioctl numbers 0x11/0x18 and a malformed drm_i915_gem_create); now matches sys/dev/drm/include/uapi/drm/i915_drm.h exactly: DRM_I915_GEM_CREATE=0x1b, DRM_I915_GEM_MMAP=0x1e, DRM_COMMAND_BASE+nr, correct struct layouts. Added graceful no-device exit so the absent-hardware case is explicit.
Verified recommended fix
Reject size==0 with -EINVAL before the mapping attempt (fix.diff), making the uninitialized read unreachable; belt-and-braces: initialize addr at declaration.
Verdict
Source-certain uninitialized-stack leak, runtime-unverifiable on this guest. i915_gem_mmap_ioctl() (sys/dev/drm/i915/i915_gem.c:1917) declares 'unsigned long addr' at :1923; the DragonFly-only 'if (args->size == 0) goto out;' at :1961-1962 skips the first assignment at :1977, and the out: tail at :2056-2067 does 'args->addr_ptr = (uint64_t)addr' with error==0, copying 8 bytes of stale kernel stack to userspace on a DRM_RENDER_ALLOW ioctl (i915_drv.c:3246), reachable unprivileged with just a GEM handle. PoC built cleanly on the audit guest (BUILD-OK, build.log) but /dev/dri does not exist there (virtio-only GPU chip=0x11111234, no drm klds, run.log), so the ioctl cannot be exercised: no Intel-GPU emulation exists for this QEMU setup. On real Intel hardware the leak is repeatable per call and is a stack-contents oracle (kernel pointers -> KASLR defeat).
No comments yet.