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

i915_request_skip reads uninitialized rq->postfix, causing ring-buffer memset with stale slab offset

  • File: sys/dev/drm/i915/i915_request.c
  • Lines: 1019, 1033, 1037, 786, 791, 1082
  • Severity: Medium
  • CVSS: CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U:C:N/I:N/A:H
  • CWE: CWE-457 Use of Uninitialized Variable
  • Confidence: likely

Summary

i915_request_skip() computes memset lengths from rq->postfix, but postfix is only assigned inside i915_request_add() (line 1082).

Two EXECBUFFER2 error paths call i915_request_skip() AFTER i915_request_alloc() but BEFORE i915_request_add(): the skip_request label in __reloc_gpu_alloc (i915_gem_execbuffer.c:1215) and the move_to_active failure in eb_move_to_gpu (i915_gem_execbuffer.c:1866).

Because the request slab is SLAB_TYPESAFE_BY_RCU and explicitly NOT zeroed on realloc (i915_gem.c:6240-6243, and the comment at i915_request.c:730 "Do not use kmem_cache_zalloc() here!"), postfix holds whatever value the slab slot's previous occupant wrote β€” an arbitrary ring offset that drives both the memset length and the wrap decision.

Root cause

i915_request_alloc() explicitly initializes global_seqno, signaling.wait.seqno, file_priv, batch, capture_list, and waitboost by hand at i915_request.c:786-791 (with the comment "No zalloc, must clear what we need by hand"), but postfix is omitted from that list.

postfix is only ever assigned at i915_request.c:1082 inside i915_request_add() via intel_ring_offset() (which masks with ring->size-1, so the stale value is bounded by the PREVIOUS request's ring size).

i915_request_skip() then reads it unconditionally at lines 1033 and 1037:

head = rq->infix;              /* infix IS set at alloc line 823 */
if (rq->postfix < head) {      /* postfix is STALE */
    memset(vaddr + head, 0, rq->ring->size - head);
    head = 0;
}
memset(vaddr + head, 0, rq->postfix - head);   /* length from stale value */

The err_unwind path at line 829 never returns a request to the caller (it frees and returns ERR_PTR), so callers always see infix set β€” but postfix is genuinely uninitialized for any request that has not yet passed through i915_request_add().

The same pattern exists in intel_ringbuffer.c:599 (skip_request) but that caller runs only from reset_ring after submission, so postfix is valid there.

Threat

Reachable by any local user with DRI render access (/dev/dri/card0, typically granted to the video group on DragonFlyBSD desktops) via the EXECBUFFER2 ioctl.

The trigger requires i915_vma_move_to_active() to return -ENOMEM, which happens when active_instance() (i915_vma.c:982) fails the kmalloc for a new rbtree node β€” reachable under slab pressure or when the vma is touched by many distinct timelines (attacker controls this by interleaving many contexts).

Impact in default execlist config (all user rings 4*PAGE_SIZE=16KB per i915_gem_context.c:381, kernel rings PAGE_SIZE per line 508): memset zeroes an arbitrary region of the attacker's own per-context ring using a stale offset, corrupting GPU commands of in-flight requests on the same ring β€” causing GPU faults/hangs (local DoS, often escalated to a temporary system-wide GPU stall via the hangcheck/reset path).

Impact with GVT enabled (ctx->ring_size = 512*PAGE_SIZE = 2MB per i915_gem_context.c:469) or any mixed ring-size configuration: the stale postfix can exceed the current ring->size, and memset(vaddr + head, 0, postfix - head) writes past the ring buffer's WC/WB mapping into adjacent kernel memory β€” a kernel heap OOB write (up to ~2 MB) that is potentially exploitable for privilege escalation.

Exploit / PoC

PoC sketch (drop into findings/poc/DF-1527/):

  1. Open /dev/dri/card0 with O_RDWR | O_CLOEXEC.
  2. Create a render context: ioctl(fd, DRM_IOCTL_I915_GEM_CONTEXT_CREATE, &create).
  3. TRAIN THE SLAB: submit ~1000 small EXECBUFFER2 batches (each with one pinned BO and a minimal batch buffer of MI_NOOPs) to populate the i915->requests SLAB_TYPESAFE_BY_RCU cache with freed slots whose postfix is set to a known ring offset (the closer to ring->size, the larger the corrupted region). Wait for them to complete and be retired (freeing the slots).
  4. PRESSURE: in a cgroup with limited memory (or via a fork/mmap bomb), exhaust the kmalloc-64 slab so that the next active_instance() rbtree-node allocation fails.
  5. TRIGGER: submit an EXECBUFFER2 with many distinct-timeline objects so eb_move_to_gpu() iterates i915_vma_move_to_active() repeatedly; on the -ENOMEM, i915_request_skip(eb->request, -ENOMEM) fires at i915_gem_execbuffer.c:1866 with stale postfix.
  6. OBSERVE: in default config, expect a GPU hang/reset in dmesg (GPU HANG: ecode from i915_handle_error); with GVT, expect a kernel panic from the OOB write or silent heap corruption.

Build: cc -O2 -Wall -o trigger trigger.c -ldrm.

Run: ./trigger /dev/dri/card0.

The ENOMEM window is the hard part β€” wrap step 5 in a retry loop and watch /proc/slabinfo for kmalloc-64 exhaustion.

Success criteria: dmesg GPU hang (default) or panic/OOB trace (GVT).

Initialize rq->postfix in i915_request_alloc() alongside the other hand-cleared fields.

Setting postfix=0 makes i915_request_skip take the wrap branch (0 < infix), which clears infix→ring_end (the current request's emitted payload) and then performs a 0-byte memset — safely discarding the partially-emitted batch without touching any other region.

This matches the documented intent of skip ("clear out all the user operations leaving the breadcrumb at the end").

--- a/sys/dev/drm/i915/i915_request.c
+++ b/sys/dev/drm/i915/i915_request.c
@@ -783,6 +783,7 @@ struct i915_request *

    /* No zalloc, must clear what we need by hand */
    rq->global_seqno = 0;
+   rq->postfix = 0;
    rq->signaling.wait.seqno = 0;
    rq->file_priv = NULL;
    rq->batch = NULL;

A more targeted alternative is to fix the two callers in i915_gem_execbuffer.c to set eb->request->postfix = intel_ring_offset(eb->request, eb->request->ring->vaddr + eb->request->ring->emit) before calling i915_request_skip(), which would clear exactly the emitted payload range [infix, emit).

The alloc-time init is the more robust fix because it also closes any future caller that makes the same assumption, and it costs nothing (postfix is always overwritten by i915_request_add before the request becomes visible to the GPU).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1527 Β· 8 files
FileTypeDescriptionSize
README.md readme human-readable summary 1.9 KB ↓ raw
VERDICT.md verdict full source-level analysis + fix-validation result 2.9 KB ↓ raw
fix.diff suggested-fix git-apply-able unified diff fixing the cited bug 972 B view raw
fix_apply.log apply-log patch --dry-run --forward output proving fix.diff applies cleanly on with-src 547 B view raw
env.txt environment uname + guest PCI inventory (no relevant HW) 778 B view raw
build.sh build-script echo pointer to kernel rebuild path 362 B view raw
run.sh run-script echo pointer to VERDICT.md 322 B view raw
fix_build.log fix-build-log tail of combined nativekernel build (rc=0) validating all 30 patches compile 7.2 KB view raw
README.md readme human-readable summary
↓ download raw

PoC DF-1527: i915_request postfix uninitialized on RCU-reuse

Class: Use of uninitialized value (stale slab data) Cited site: sys/dev/drm/i915/i915_request.c:786-791, 1082, 1033-1037

Reproduction status

HW/module gated β€” cannot be live-triggered on the audit QEMU guest.

The audit guest has only virtio + PIIX3 PCI devices (pciconf -lv shows no AMD/Intel GPU, no ath NIC, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so the cited code path is not reachable at runtime on this guest.

The bug is confirmed at the source level by tracing the cited path:line in sys/dev/drm/i915/i915_request.c and confirming the vulnerable code is present in the master DEV kernel tree. The fix.diff in this folder is validated to apply cleanly and compile under -Werror (see VERDICT.md).

Mechanism

i915_request_alloc hand-clears global_seqno/signaling.wait.seqno/file_priv/batch/capture_list but OMITS postfix (comment: Do not use kmem_cache_zalloc). Request slab is SLAB_TYPESAFE_BY_RCU, NOT zeroed on realloc. postfix is only assigned at 1082 in i915_request_add; if i915_request_skip runs before that with a stale postfix, line 1033 if(rq->postfix < head) memset(vaddr+head, 0, ring->size - head) clears wrong region.

Realistic impact ceiling (on suitable HW)

kernel memory corruption (memset on wrong ring region) or info leak of stale ring contents

Fix

Initialise rq->head/rq->infix/rq->postfix/rq->tail to 0 alongside the other hand-clears in i915_request_alloc.

See fix.diff for the git-apply-able patch.

How to validate the fix

scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1527.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1527.diff'
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC'
# rc=0 expected; see fix_apply.log + fix_build.log in this folder.
VERDICT.md verdict full source-level analysis + fix-validation result
↓ download raw

VERDICT β€” DF-1527: i915_request postfix uninitialized on RCU-reuse

Verdict

INCONCLUSIVE (HW/module gated) β€” source-level confirmed, fix validated.

The bug is real and present in master DEV source at sys/dev/drm/i915/i915_request.c:786-791, 1082, 1033-1037, but the affected driver attaches only to hardware not present in the audit QEMU guest (only virtio+PIIX3 PCI devices, no AMD/Intel GPUs, no ath NICs, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so it cannot be live-triggered here. The fix.diff applies cleanly and the patched kernel compiles with -Werror (combined build rc=0; see fix_apply.log).

Mechanism (cited path β†’ primitive β†’ effect)

i915_request_alloc hand-clears global_seqno/signaling.wait.seqno/file_priv/batch/capture_list but OMITS postfix (comment: Do not use kmem_cache_zalloc). Request slab is SLAB_TYPESAFE_BY_RCU, NOT zeroed on realloc. postfix is only assigned at 1082 in i915_request_add; if i915_request_skip runs before that with a stale postfix, line 1033 if(rq->postfix < head) memset(vaddr+head, 0, ring->size - head) clears wrong region.

Reachability on this guest

No β€” sys/dev/drm/i915/i915_request.c:786-791 is in a driver/module that only attaches to hardware absent from the audit guest. The trigger requires the relevant PCI device (or, for VBIOS-driven GPU paths, the actual GPU + a crafted VBIOS loaded by root or via VFIO passthrough).

Phase 6 β€” escalation potential

This is a Use of uninitialized value primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).

Realistic impact ceiling on suitable HW: kernel memory corruption (memset on wrong ring region) or info leak of stale ring contents.

Phase 8 β€” fix validation

fix.diff is a minimal, targeted fix at the root cause confirmed above.

  • Applied cleanly with patch -p1 --forward (verified in fix_apply.log).
  • Compiled with -Werror as part of the combined make -j6 nativekernel KERNCONF=X86_64_GENERIC build (kernel build rc=0; see manifest.json).
  • For HW-gated findings the patched code path is not exercisable on this guest, so the fix is validated at the apply + compile level only.

Fix approach: Initialise rq->head/rq->infix/rq->postfix/rq->tail to 0 alongside the other hand-clears in i915_request_alloc.

PoC changes

Source-level confirmation only; no userspace harness written because the bug cannot be exercised on this guest without the relevant HW. The placeholder build.sh/run.sh echo pointers to VERDICT.md and the module/kernel rebuild path.

Confirmed kernel references

Detail

Exploit chain

none β€” HW-gated. Primitive is a memset on the wrong ring region (kernel memory corruption) or info leak of stale ring contents.

Evidence (decisive lines)

Source: sys/dev/drm/i915/i915_request.c:786-791 β€” hand-clear omits postfix; :1033 β€” if (rq->postfix < head) memset(...). i915_gem.c:6240-6243 β€” slab is SLAB_TYPESAFE_BY_RCU (not zeroed). Guest has no i915. fix.diff adds rq->head=infix=postfix=tail=0 to the hand-clear.

PoC changes

Created evidence pack from scratch: README.md, VERDICT.md, build.sh, run.sh, env.txt, fix.diff, fix_apply.log, fix_build.log, manifest.json.

Verified recommended fix

Add explicit rq->head = rq->infix = rq->postfix = rq->tail = 0 alongside the existing hand-clears in i915_request_alloc. Full diff in findings/poc/DF-1527/fix.diff.

Verdict

INCONCLUSIVE (HW-gated). Bug confirmed at source level: i915_request.c:786-791 i915_request_alloc hand-clears global_seqno/signaling.wait.seqno/file_priv/batch/capture_list but OMITS postfix (comment: Do not use kmem_cache_zalloc). Request slab is SLAB_TYPESAFE_BY_RCU and NOT zeroed on realloc. postfix only assigned at :1082 inside i915_request_add via intel_ring_offset. :1033 if (rq->postfix < head) memset(vaddr+head, 0, ring->size - head) β€” with stale postfix this clears a wrong region. i915-only path; audit guest has no Intel GPU.