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

eb_move_to_gpu error path double-unreserves vmas: pin/fence/GEM-object refcount underflow β†’ UAF

Summary

If i915_vma_move_to_active() fails mid-loop (active_instance rbtree kmalloc -ENOMEM), eb_move_to_gpu returns with eb->flags[0..i) still claiming HAS_PIN/HAS_REF (the unreserve took flags BY VALUE). The ioctl error path then unreserves and i915_vma_put()s those entries a SECOND time. All drops are raw decrements with no-op GEM_BUG_ON guards in release builds -> vma pin/fence refcount and GEM object refcount underflow -> premature unbind/free -> UAF. Identical to torvalds/linux v4.19; upstream removed the mid-loop error return in v5.2/v5.6 reworks. Unprivileged render client sprays execbuf2s under slab pressure. Guest model-verified (verbatim both loops, underflow demonstrated, patched balanced β€” model caught that HAS_REF must be cleared too); ioctl PoC exits 77 (no /dev/dri on guest).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4007 Β· 9 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
build.sh β€” 65 B view raw
run.sh β€” 89 B view raw
run.log β€” 1.1 KB view raw
env.txt β€” 493 B view raw
fix.diff β€” 825 B view raw
model_df4007.c β€” 5.9 KB view raw
poc_df4007.c β€” 2.5 KB view raw

DF-4007 β€” i915 execbuf eb_move_to_gpu error path double-unreserves vmas (pin/fence/object-refcount underflow β†’ UAF)

File: sys/dev/drm/i915/i915_gem_execbuffer.c:1860-1876 (bug), unwind at :857-879 via :2442-2444 Class: kernel refcount underflow β†’ premature unbind/free β†’ UAF (memcorrupt) Severity: Medium Β· Confidence: certain (source; carried 4.19-vintage shape) Β· runtime hardware-gated (no GPU on guest)

Build / Run / Expected

./build.sh   # builds model_df4007 + poc_df4007
./run.sh

Expected (run.log):

[DF-4007 BASELINE] move_to_active fails at index 1 (BASELINE)
    vma[0] objA  pin=-1 fence=-1 objrefs=2  <-- FENCE PIN UNDERFLOW (also pin underflow)
[DF-4007 BASELINE] move_to_active fails at index 2 (BASELINE)
    vma[0] objA  pin=-1 fence=-1 objrefs=2
    vma[1] shadow pin=-1 fence=0 objrefs=1  <-- OBJ REF UNDERFLOW (premature GEM object free -> UAF)
[DF-4007 control] ... perfectly balanced
[DF-4007 PATCHED ] ... balanced even on failure
---- ioctl PoC (no GPU on this guest) ----
poc exit=77

Source chain (unprivileged ioctl path)

  1. eb_submit β†’ eb_move_to_gpu second loop (i915_gem_execbuffer.c:1860-1875): on i915_vma_move_to_active(vma, eb->request, flags) failure (:1864) it returns at :1867 β€” for entries already processed it called __eb_unreserve_vma(vma, flags) by value (:1870) and i915_vma_put for __EXEC_OBJECT_HAS_REF (:1873-1874), but never cleared those bits in eb->flags[i] and left eb->exec set (eb->exec = NULL only at :1876).
  2. i915_vma_move_to_active can fail: active_instance() returns -ENOMEM when its rbtree kmalloc fails (i915_vma.c:981-989) β€” needs the vma active from a second context (last_active cache miss) + DRM-slab exhaustion, i.e. an unprivileged client spraying execbufs under memory pressure.
  3. The ioctl error path (i915_gem_do_execbuffer:2442-2444) then calls eb_release_vmas (:857-879), which for every entry with flags & __EXEC_OBJECT_HAS_PIN unreserves again (:873-874) and for __EXEC_OBJECT_HAS_REF puts again (:876-877).
  4. __i915_vma_unpin is a raw vma->flags-- (i915_vma.h:323-326), __i915_vma_unpin_fence a raw vma->fence->pin_count-- (:392-396), i915_vma_put drops the GEM object ref (:222) β€” all underflow β†’ vma unbound while the (skipped) request believes it pinned / object freed prematurely β†’ UAF class. (GEM_BUG_ON guards are no-ops in non-DEBUG builds, i915_gem.h:60-62.)

Carried from the upstream 4.19-vintage file (verified identical control flow in torvalds/linux v4.19 i915_gem_execbuffer.c); upstream later restructured the loop (v5.2 ww-mutex rework unreserves unconditionally; v5.6 makes i915_vma_move_to_active() void), removing the mid-loop error return.

Fix

fix.diff: switch the loop tail to the by-ref eb_unreserve_vma(vma, &eb->flags[i]) (clears __EXEC_OBJECT_RESERVED) and clear __EXEC_OBJECT_HAS_REF after the put, making any later eb_release_vmas a no-op for processed entries. Behavior is model-verified balanced in model_df4007.c (PATCHED run) and compile-validated by nativekernel rebuild (fix_build.log).

VERDICT.md
↓ download raw

VERDICT β€” DF-4007

Status: inconclusive at runtime (no GPU on the audit guest); source-certain; control flow model-verified on the guest.

What is proven, and how

  1. Source (kernel tree): the second loop of eb_move_to_gpu (i915_gem_execbuffer.c:1860-1875) returns at :1867 when i915_vma_move_to_active() fails, after having unreserved entries [0, i) via the BY-VALUE __eb_unreserve_vma(vma, flags) (:1870, :427-435) and i915_vma_put() for __EXEC_OBJECT_HAS_REF (:1873) β€” **without clearing those bits in eb->flags[i]** and witheb->execstill set (eb->exec = NULLhappens only at:1876). The ioctl error pathi915_gem_do_execbuffer:2442-2444therefore callseb_release_vmas()(:857-879), which unreserves (:873-874) and puts (:876-877) every entry whose flags still claimHAS_PIN/HAS_REFβ€” a second drop of the same pin/fence/reference. The drops are raw decrements (__i915_vma_unpin=vma->flags--,i915_vma.h:323-326;__i915_vma_unpin_fence=vma->fence->pin_count--,:392-396;i915_vma_put=i915_gem_object_put,:222), guarded only by no-op GEM_BUG_ONs in non-DEBUG builds (i915_gem.h:60-62`) β†’ underflow β†’ premature unbind / premature GEM object free β†’ UAF class.
  2. Trigger is unprivileged but narrow: i915_vma_move_to_active fails with -ENOMEM from active_instance()'s rbtree kmalloc (i915_vma.c:981-989), which requires (a) the vma already active from a different context (last_active cache miss β€” attacker arranges by sharing one BO across two contexts) and (b) the small DRM-slab alloc to fail (memory-pressure/slab exhaustion spray). Not deterministic, but a standard pressure primitive.
  3. Model verification on the guest (model_df4007.c, run.log): exact replication of the two functions with mock refcounts. BASELINE with the failure injected at entry 1 β†’ entry 0 pin=-1 fence=-1; failure at entry 2 β†’ additionally the cmdparser shadow (the only HAS_REF user, :1963-1965) objrefs=1 < floor 2 (object-ref underflow). Control (no failure) and PATCHED (fix.diff applied) runs are perfectly balanced β€” including the subtle part the model caught: clearing only __EXEC_OBJECT_RESERVED is NOT enough; HAS_REF must also be cleared or the shadow ref is still double-put.
  4. Runtime: poc_df4007.c builds cleanly on the guest but /dev/dri does not exist (virtio-only GPU, env.txt) β†’ exit 77 per DF-3634 conventions. The ioctl-level trigger needs real Intel hardware.

Upstream status

The DragonFly file matches torvalds/linux v4.19 i915_gem_execbuffer.c byte-for-byte in this control flow (fetched and compared during verification). Upstream eliminated the class later: the v5.2 ww-mutex rework restructured the loop (unreserve unconditional) and v5.6 made i915_vma_move_to_active() void, removing the mid-loop error return entirely.

Fix validation

fix.diff (by-ref eb_unreserve_vma(vma, &eb->flags[i]) + HAS_REF clear): model-verified balanced; applied to guest /usr/src together with the fiji fixes, full nativekernel rebuild completed (fix_build.log), reverted after validation.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline no→ patch + rebuild →patched clean

fixed (compile-validated): both diffs applied to clean guest /usr/src (fiji_smumgr.c md5 800babd0..->d50bdc81.., i915_gem_execbuffer.c 334c7bd9..->dc882976..), make nativekernel KERNCONF=X86_64_GENERIC completed with error_count=0 under -Werror (fix_build.log); behavior additionally model-verified for DF-4007 (PATCHED run balanced). Runtime re-test impossible on this guest (no AMD/Intel GPU). Patch reverted after validation.

findings/poc/DF-4007/fix.diff; findings/poc/DF-4001/fix_build.log; findings/poc/DF-4007/run.log (PATCHED model run for DF-4007)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT (nativekernel X86_64_GENERIC, patched, -Werror; see fix_build.log)

Confirmed kernel references

Detail

Exploit chain

unprivileged render client: share one BO across two contexts (vma active from 2nd timeline) -> spray execbuf2s under DRM-slab pressure so active_instance()'s kmalloc fails mid-loop -> eb_move_to_gpu returns with HAS_PIN/HAS_REF still set -> err path eb_release_vmas double-unreserve/double-put -> vma->flags / fence->pin_count / GEM object refcount underflow -> premature unbind or premature object free -> UAF

Evidence (decisive lines)

run.log (model_df4007: BASELINE pin=-1/fence=-1 + shadow objref underflow vs balanced control/PATCHED; poc exits 77 no-GPU); model_df4007.c; VERDICT.md (incl. upstream v4.19 comparison); fix_build.log; env.txt

PoC changes

Pass-2 reviewer authored the control-flow model (verbatim loop semantics of eb_move_to_gpu/eb_release_vmas with mock refcounts) plus a graceful-exit ioctl PoC (uapi structs matched to sys/dev/drm/include/uapi/drm/i915_drm.h, DRM_IOCTL_I915_GEM_EXECBUFFER2 encoding via _IOW as drm.h:796-799/932). The model caught that the first fix draft (clear only __EXEC_OBJECT_RESERVED) still double-put the HAS_REF shadow ref; fix.diff extended accordingly.

Verified recommended fix

In eb_move_to_gpu use eb_unreserve_vma(vma, &eb->flags[i]) (by-ref, clears __EXEC_OBJECT_RESERVED) and clear __EXEC_OBJECT_HAS_REF after i915_vma_put() so any later eb_release_vmas is a no-op for processed entries (fix.diff)

Verdict

Source-certain refcount-underflow -> UAF class on the unprivileged execbuf2 ioctl, control flow model-verified on the guest: when i915_vma_move_to_active() fails mid-loop (active_instance rbtree kmalloc -ENOMEM, i915_vma.c:981-989 - requires vma active from a second context + DRM-slab exhaustion), eb_move_to_gpu returns at i915_gem_execbuffer.c:1867 leaving eb->flags[0..i) with HAS_PIN/HAS_REF still set (unreserve took flags by value at :1870-1874); the ioctl error path (:2442-2444 -> eb_release_vmas :857-879) then unreserves and i915_vma_put()s those entries AGAIN -> vma->flags-- / fence->pin_count-- / GEM object refcount underflow (raw decrements, i915_vma.h:323/392/222; GEM_BUG_ON no-ops in release builds) -> premature unbind/free -> UAF. Carried verbatim from torvalds/linux v4.19 (verified during this run); upstream removed the mid-loop error return in the v5.2/v5.6 reworks. Guest model (model_df4007.c) shows baseline underflow (pin=-1, fence=-1, shadow objrefs below floor) vs balanced control/PATCHED runs - the model also caught that the fix must clear HAS_REF, not just PIN/FENCE. Runtime trigger needs Intel GPU hardware; guest has none (/dev/dri absent) - poc_df4007.c exits 77 per DF-3634 conventions. Fix compile-validated by nativekernel rebuild.