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)
PoC verification
Evidence pack
findings/poc/DF-4007 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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)
eb_submitβeb_move_to_gpusecond loop (i915_gem_execbuffer.c:1860-1875): oni915_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) andi915_vma_putfor__EXEC_OBJECT_HAS_REF(:1873-1874), but never cleared those bits ineb->flags[i]and lefteb->execset (eb->exec = NULLonly at:1876).i915_vma_move_to_activecan fail:active_instance()returns-ENOMEMwhen its rbtreekmallocfails (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.- The ioctl error path (
i915_gem_do_execbuffer:2442-2444) then callseb_release_vmas(:857-879), which for every entry withflags & __EXEC_OBJECT_HAS_PINunreserves again (:873-874) and for__EXEC_OBJECT_HAS_REFputs again (:876-877). __i915_vma_unpinis a rawvma->flags--(i915_vma.h:323-326),__i915_vma_unpin_fencea rawvma->fence->pin_count--(:392-396),i915_vma_putdrops 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 β 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
- Source (kernel tree): the second loop of
eb_move_to_gpu(i915_gem_execbuffer.c:1860-1875) returns at:1867wheni915_vma_move_to_active()fails, after having unreserved entries [0, i) via the BY-VALUE__eb_unreserve_vma(vma, flags)(:1870,:427-435) andi915_vma_put()for__EXEC_OBJECT_HAS_REF(:1873) β **without clearing those bits ineb->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. - Trigger is unprivileged but narrow:
i915_vma_move_to_activefails with-ENOMEMfromactive_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. - 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 0pin=-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_RESERVEDis NOT enough;HAS_REFmust also be cleared or the shadow ref is still double-put. - Runtime:
poc_df4007.cbuilds cleanly on the guest but/dev/dridoes 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
fixedfixed (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)
Confirmed kernel references
- sys/dev/drm/i915/i915_gem_execbuffer.c:1860
- sys/dev/drm/i915/i915_gem_execbuffer.c:1864
- sys/dev/drm/i915/i915_gem_execbuffer.c:1870
- sys/dev/drm/i915/i915_gem_execbuffer.c:1873
- sys/dev/drm/i915/i915_gem_execbuffer.c:1876
- sys/dev/drm/i915/i915_gem_execbuffer.c:2442
- sys/dev/drm/i915/i915_gem_execbuffer.c:857
- sys/dev/drm/i915/i915_gem_execbuffer.c:873
- sys/dev/drm/i915/i915_gem_execbuffer.c:876
- sys/dev/drm/i915/i915_vma.c:981
- sys/dev/drm/i915/i915_vma.c:1033
- sys/dev/drm/i915/i915_vma.h:323
- sys/dev/drm/i915/i915_vma.h:392
- sys/dev/drm/i915/i915_vma.h:222
- sys/dev/drm/i915/i915_gem.h:60
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.
No comments yet.