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

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:

  1. creating a >= 2 GiB GEM object via I915_GEM_CREATE (no special privileges needed),
  2. submitting a prior small batch to dirty a batch-pool shadow object (setting CLFLUSH_BEFORE on the pool entry),
  3. submitting EXECBUFFER2 with batch_len >= 0x80000000 and batch_start_offset=0 on 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:

  1. Open the DRI device and obtain DRM auth (or use render node which needs no auth for unprivileged submit on many configs).
  2. ioctl(fd, DRM_IOCTL_I915_GEM_CREATE, {.size = 0x80001000}) to create a ~2 GiB object (requires available memory/swap).
  3. Submit a tiny 4 KiB batch first via DRM_IOCTL_I915_GEM_EXECBUFFER2 with a valid MI_NOOP + MI_BATCH_BUFFER_END to prime the batch pool and dirty a shadow.
  4. 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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1559 Β· 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 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
README.md readme human-readable summary
↓ download 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<=0x80000000: (int)0x80000000=INT_MIN < positive PAGE_SIZE -> len becomes INT_MIN=-2147483648. memcpy(ptr, src+offset, len) promotes to size_t 0xFFFFFFFF80000000 on LP64 -> massive write past shadow WB vmap into kernel vm.

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.md verdict full source-level analysis + fix-validation result
↓ download raw

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<=0x80000000: (int)0x80000000=INT_MIN < positive PAGE_SIZE -> len becomes INT_MIN=-2147483648. memcpy(ptr, src+offset, len) promotes to size_t 0xFFFFFFFF80000000 on LP64 -> massive write past shadow WB vmap into kernel vm.

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 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: 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

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<=0x80000000: (int)0x80000000=INT_MIN < positive PAGE_SIZE -> len becomes INT_MIN=-2147483648. :1109 memcpy(ptr, src+offset, len) promotes to size_t 0xFFFFFFFF80000000 on LP64 -> massive write past shadow WB vmap into kernel vm. i915-only path; audit guest has no Intel GPU.