DF-4521 / verdict.json
{ "finding_id": "DF-4521", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "drm_mm_for_each_node_in_range() hands the unallocated head_node sentinel to the callers loop body and never terminates when end > mm end; reproduced live with the real kernel drm_mm.c in a kld: a top-of-GTT eviction walk (i915_gem_evict_for_nodes exact parameters) yielded allocated=0 head sentinel at visit 2 and wrapped forever (visited=8 capped, non_terminating=1). The downstream i915 arm (container_of fake i915_vma -> i915_vma_unbind -> drm_mm_remove_node(sentinel)) was simulated at the drm_mm level and demonstrably wipes the interval tree (range query over an allocated nodes own range returns zero nodes) and plants a size-underflowed hole (0xffffffff10000000) into the hole trees. Full unprivileged execbuf2 (EXEC_OBJECT_PINNED) trigger is source-traced hop-by-hop but hardware-gated: guest has no Intel GPU.", "exploit_chain": "execbuf2 EXEC_OBJECT_PINNED at offset=vm.total-size (no gen gate, execbuffer.c:461) -> i915_vma_pin PIN_OFFSET_FIXED -> i915_gem_gtt_reserve (gtt.c:3949) -> drm_mm_reserve_node -ENOSPC -> i915_gem_evict_for_node with check_color end+=4K (evict.c:296) -> drm_mm_for_each_node_in_range walks past last node onto head sentinel -> (a) livelock under struct_mutex (DoS), or (b) container_of(head) fake vma (GEM_BUG_ON compiled out) -> pin/list_add -> i915_vma_unbind -> drm_mm_remove_node(head_node) -> interval tree root wiped (RB_REMOVE of never-inserted node) + underflowed hole -> blind overlap checks -> GTT aliasing / list corruption on wrap passes", "evidence": "run.log visits 1-8 (HEAD SENTINEL at visit 2, RESULT non_terminating=1); corrupt.log probes (mid node invisible to its own range query; bogus hole [0x100000000,0x10000000) size ffffffff10000000); run.fixed.log (visited=1, no sentinel, terminates); dmesg.txt full console", "kernel_refs": [ "sys/dev/drm/drm_mm.c:171-176", "sys/dev/drm/include/drm/drm_mm.h:483-502", "sys/dev/drm/i915/i915_gem_evict.c:292-336", "sys/dev/drm/i915/i915_gem_gtt.c:3949-3975", "sys/dev/drm/i915/i915_gem_gtt.c:3638-3639", "sys/dev/drm/i915/i915_gem_execbuffer.c:461-463", "sys/dev/drm/i915/i915_gem_execbuffer.c:614-615", "sys/dev/drm/i915/i915_vma.c:687-695", "sys/dev/drm/i915/i915_gem.h:60", "sys/dev/drm/include/linux/bug.h:45", "sys/dev/drm/include/linux/rbtree.h:101-102" ], "poc_changes": "PoC authored fresh (no seed): kld embeds unmodified /usr/src/sys/dev/drm/drm_mm.c + real drm_mm.h macros; local RB_GENERATE(linux_root)/panic_cmp/drm_printf stubs were needed because those live in the not-loaded drm.ko; sysctl-triggered scenarios with iteration cap to make non-termination observable without hanging the guest", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 2400, "guest_dirty": 1, "build_cmd": "cd /tmp/DF-4521 && cp /usr/src/sys/dev/drm/drm_mm.c . && make", "run_cmd": "kldload ./dfmm_poc.ko && sysctl dev.dfmm.run=1 (scenario 2: sysctl dev.dfmm.corrupt=1)", "code_hash": "9b8283d18d06a376806e0ce586ee24eff27028453f77b7202fabee677ae96b77", "notes": "Guest lacks Intel GPU (Bochs vga chip 0x11111234) so the end-to-end ioctl trigger is hardware-gated; drm_mm-level defects (sentinel yield, non-termination, interval-tree wipe, underflow hole) all executed live with real kernel code. No panic: kernel survived sentinel remove with corrupted allocator state (proof in corrupt.log). Inherited from Linux 4.12-era code; i915 no longer uses this path with unclamped end in modern upstream. Guest left dirty (kld + /usr/src patch) and reset with-src afterwards.", "recommended_fix": "Stop drm_mm_for_each_node_in_range at the head_node sentinel (drm_mm.h) and clamp i915_gem_evict_for_nodes color-expanded end to vm->total", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to guest /usr/src/sys (drm_mm.h iterator sentinel stop + i915 evict end clamp), kld rebuilt from same real sources, identical PoC re-run: baseline visited=8/head_sentinel_yielded=1/non_terminating=1 vs patched visited=1/head_sentinel_yielded=0/non_terminating=0 (run.fixed.log). Bad behavior gone; in-range iteration semantics unchanged.", "fix_evidence": "run.fixed.log last lines: visit 1: start=fffff000 ... allocated=1 then RESULT: visited=1 head_sentinel_yielded=0 non_terminating=0; patch transcript in session (patch -p0: Hunk #1 succeeded at 492 / Hunk #1 succeeded at 308)" } |