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/):
- Open
/dev/dri/card0withO_RDWR | O_CLOEXEC. - Create a render context:
ioctl(fd, DRM_IOCTL_I915_GEM_CONTEXT_CREATE, &create). - TRAIN THE SLAB: submit ~1000 small
EXECBUFFER2batches (each with one pinned BO and a minimal batch buffer ofMI_NOOPs) to populate thei915->requestsSLAB_TYPESAFE_BY_RCUcache with freed slots whosepostfixis set to a known ring offset (the closer toring->size, the larger the corrupted region). Wait for them to complete and be retired (freeing the slots). - PRESSURE: in a cgroup with limited memory (or via a fork/mmap bomb),
exhaust the
kmalloc-64slab so that the nextactive_instance()rbtree-node allocation fails. - TRIGGER: submit an
EXECBUFFER2with many distinct-timeline objects soeb_move_to_gpu()iteratesi915_vma_move_to_active()repeatedly; on the-ENOMEM,i915_request_skip(eb->request, -ENOMEM)fires ati915_gem_execbuffer.c:1866with stalepostfix. - OBSERVE: in default config, expect a GPU hang/reset in
dmesg(GPU HANG: ecodefromi915_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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-1527 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 β 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 infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.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
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- 9
- 1
- 5
- /
- i
- 9
- 1
- 5
- _
- r
- e
- q
- u
- e
- s
- t
- .
- c
- :
- 7
- 8
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- 9
- 1
- 5
- /
- i
- 9
- 1
- 5
- _
- r
- e
- q
- u
- e
- s
- t
- .
- c
- :
- 7
- 9
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- 9
- 1
- 5
- /
- i
- 9
- 1
- 5
- _
- r
- e
- q
- u
- e
- s
- t
- .
- c
- :
- 1
- 0
- 3
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- 9
- 1
- 5
- /
- i
- 9
- 1
- 5
- _
- r
- e
- q
- u
- e
- s
- t
- .
- c
- :
- 1
- 0
- 8
- 2
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.
No comments yet.