Signedness bug in copy_batch min_t causes kernel heap overflow with large batch_len
- File:
sys/dev/drm/i915/i915_cmd_parser.c - Lines: 1104, 1099, 1109
- Severity: Medium
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:N/I:N/A:H - CWE: CWE-197 Integer Truncation
- Confidence: likely
Summary
In copy_batch(), the kmap fallback loop computes
int len = min_t(int, batch_len, PAGE_SIZE - offset).
Since batch_len is u32 and an unprivileged DRI client can submit
batch_len >= 0x80000000 (2 GiB) via EXECBUFFER2 with a large enough GEM
object, the (int) cast inside min_t produces a negative value.
This negative int is then passed to memcpy() as the size parameter, where it
is implicitly promoted to size_t (~2^63 on 64-bit), causing a massive write
past the end of the shadow batch buffer's WB vmap into kernel vmalloc space.
This reliably crashes the kernel (local DoS).
Root cause
The variable batch_len is u32 (copy_batch signature, line 1052).
The caller (intel_engine_cmd_parser line 1268, via eb_parse at
i915_gem_execbuffer.c:1944) passes eb->batch_len directly from the
user-supplied args->batch_len (u32, set at i915_gem_execbuffer.c:2248) with
only the constraint batch_len <= batch->size - batch_start_offset (line 2320).
Object creation allows sizes up to INT_MAX << PAGE_SHIFT β 8 TiB
(i915_gem.c:5174), so a 2+ GiB object is valid.
At line 1098-1100, when dst_needs_clflush has CLFLUSH_BEFORE set (common for
reused batch-pool shadow objects that were previously WB-written), batch_len is
reassigned via roundup(batch_len, x86_clflush_size) which keeps values
>= 0x80000000 unchanged.
Then at line 1104:
int len = min_t(int, batch_len, PAGE_SIZE - offset).
The DragonFlyBSD min_t macro (sys/dev/drm/include/linux/kernel.h:82) expands
to ((int)batch_len < (int)(PAGE_SIZE - offset) ? (int)batch_len :
(int)(PAGE_SIZE - offset)).
For batch_len=0x80000000, (int)0x80000000 = INT_MIN = -2147483648, which is
less than the positive PAGE_SIZE - offset, so len becomes -2147483648.
At line 1109, memcpy(ptr, src + offset, len) promotes len to size_t: on
LP64 this is 0xFFFFFFFF80000000, an astronomically large write originating from
attacker-controlled source data (the user's GEM object pages).
Threat
An unprivileged local user with DRI render-node access on gen7 hardware (Intel
Ivy Bridge / Haswell, IS_GEN7 check at line 868 β the parser is unconditionally
enabled for all gen7 engines) can trigger a kernel panic (denial of service) by:
- creating a
>= 2 GiBGEM object viaI915_GEM_CREATE(no special privileges needed), - submitting a prior small batch to dirty a batch-pool shadow object (setting
CLFLUSH_BEFOREon the pool entry), - submitting
EXECBUFFER2withbatch_len >= 0x80000000andbatch_start_offset=0on the large object.
The WC fast-path bypass is forced when src_needs_clflush is false (object never
CPU-written) or when the CPU lacks SSE4.1 / runs under a hypervisor
(i915_memcpy_from_wc disabled per i915_memcpy.c:103-105).
The resulting memcpy writes ~2^63 bytes from the user's GEM object contents
into kernel vmalloc space past the shadow object, immediately hitting a guard
page and panicking.
On 32-bit kernels (size_t truncation to 0x80000000), the overflow length is
2 GiB which could corrupt significant kernel state, potentially enabling code
execution, though gen7+32-bit is rare.
Impact on default 64-bit config: reliable local DoS (kernel panic).
Exploit / PoC
Build a standalone C program using libdrm or direct ioctl on /dev/dri/card0
(or render node /dev/dri/renderD128).
Steps:
- Open the DRI device and obtain DRM auth (or use render node which needs no auth for unprivileged submit on many configs).
ioctl(fd, DRM_IOCTL_I915_GEM_CREATE, {.size = 0x80001000})to create a ~2 GiB object (requires available memory/swap).- Submit a tiny 4 KiB batch first via
DRM_IOCTL_I915_GEM_EXECBUFFER2with a validMI_NOOP+MI_BATCH_BUFFER_ENDto prime the batch pool and dirty a shadow. - Submit the large object as a batch:
exec2.batch_start_offset = 0,exec2.batch_len = 0x80001000, with the large object as the batch buffer, targeting the RCS engine (I915_EXEC_RENDER) on gen7.
The kernel enters copy_batch, hits the min_t signedness bug, and panics with
a page fault in the memcpy.
Expected result: kernel panic / reset.
The PoC does not need to control batch contents (the overflow size is fixed at
INT_MIN regardless), so a freshly-created zero-filled object suffices.
Success criterion: system hangs / reboots / drops to ddb> prompt with a fatal
page fault in i915_cmd_parser:copy_batch.
Recommended fix
Change the min_t comparison to use an unsigned type so that
batch_len >= 0x80000000 is treated as the large positive value it actually is,
causing min() to correctly return PAGE_SIZE - offset.
--- a/sys/dev/drm/i915/i915_cmd_parser.c
+++ b/sys/dev/drm/i915/i915_cmd_parser.c
@@ -1101,7 +1101,7 @@ static u32 *copy_batch(struct drm_i915_gem_object *dst_obj,
ptr = dst;
for (n = batch_start_offset >> PAGE_SHIFT; batch_len; n++) {
- int len = min_t(int, batch_len, PAGE_SIZE - offset);
+ unsigned int len = min_t(unsigned int, batch_len,
+ (unsigned int)PAGE_SIZE - offset);
src = kmap_atomic(i915_gem_object_get_page(src_obj, n));
if (src_needs_clflush)
This ensures batch_len is compared as unsigned int: for
batch_len >= 0x80000000, min(unsigned int, large, PAGE_SIZE) correctly returns
PAGE_SIZE (a small positive value), and the loop processes one page at a time as
intended.
Additionally, consider adding an upper-bound check on batch_len early in
intel_engine_cmd_parser (e.g., reject batch_len > SZ_256M or similar sane
limit for gen7 command buffers) as defense-in-depth, since multi-gigabyte batch
buffers are not a legitimate use case on this hardware generation.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1559 Β· 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 | 796 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 | 316 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-1559: i915_cmd_parser signed-int min_t -> giant memcpy
Class: Heap overflow write via signedness confusion
Cited site: sys/dev/drm/i915/i915_cmd_parser.c:1104,1109, 2228
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_cmd_parser.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_cmd_parser copy-batch loop: int len = min_t(int, batch_len, PAGE_SIZE-offset). batch_len is u32 from user EXECBUFFER2 args->batch_len, bounded only by batch->size-batch_start_offset (object creation allows sizes up to INT_MAX<
Realistic impact ceiling (on suitable HW)
massive kernel memory corruption -> panic or potential privilege escalation
Fix
Reject batch_len > INT_MAX before the min_t(int, ...) loop body.
See fix.diff for the git-apply-able patch.
How to validate the fix
scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1559.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1559.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-1559: i915_cmd_parser signed-int min_t -> giant memcpy
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_cmd_parser.c:1104,1109, 2228, 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_cmd_parser copy-batch loop: int len = min_t(int, batch_len, PAGE_SIZE-offset). batch_len is u32 from user EXECBUFFER2 args->batch_len, bounded only by batch->size-batch_start_offset (object creation allows sizes up to INT_MAX<
Reachability on this guest
No β sys/dev/drm/i915/i915_cmd_parser.c:1104 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 Heap overflow write via signedness confusion 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: massive kernel memory corruption -> panic or potential privilege escalation.
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: Reject batch_len > INT_MAX before the min_t(int, ...) loop body.
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
- _
- c
- m
- d
- _
- p
- a
- r
- s
- e
- r
- .
- c
- :
- 1
- 1
- 0
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- 9
- 1
- 5
- /
- i
- 9
- 1
- 5
- _
- c
- m
- d
- _
- p
- a
- r
- s
- e
- r
- .
- c
- :
- 1
- 1
- 0
- 9
Detail
Exploit chain
none β HW-gated. Primitive is a massive kernel memory corruption (potentially exploitable for privesc) via a crafted EXECBUFFER2 batch_len.
Evidence (decisive lines)
Source: sys/dev/drm/i915/i915_cmd_parser.c:1104 β int len = min_t(int, batch_len, PAGE_SIZE-offset); :1109 β memcpy(ptr, src+offset, len). Guest has no i915 GPU. fix.diff adds `if (batch_len > INT_MAX) return -EINVAL;` before the loop.
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
Reject batch_len > INT_MAX before the min_t(int, ...) loop body in i915_parse_commands. Full diff in findings/poc/DF-1559/fix.diff.
Verdict
INCONCLUSIVE (HW-gated). Bug confirmed at source level: i915_cmd_parser.c:1104 int len = min_t(int, batch_len, PAGE_SIZE-offset). batch_len is u32 from user EXECBUFFER2 args->batch_len, bounded only by batch->size-batch_start_offset; object creation allows sizes up to INT_MAX<
No comments yet.