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

intel_vgt_deballoon calls drm_mm_remove_node on unallocated balloon slots, dereferencing NULL mm

Field Value
ID DF-2100
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-476 NULL Pointer Dereference
File sys/dev/drm/i915/i915_vgpu.c
Lines 119-129
Area drm/i915
Confidence certain
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match novel

Summary

intel_vgt_deballoon() iterates over all four bl_info.space[] entries and unconditionally invokes vgt_deballoon_space() β†’ drm_mm_remove_node(). intel_vgt_balloon() only allocates the subset of slots whose MMIO-derived region conditions hold (typically 1-3, almost never all 4). The unallocated slots remain BSS-zero (node->mm == NULL, node->allocated == false). On a production kernel, drm_mm_remove_node()'s runtime assert DRM_MM_BUG_ON(!node->allocated) is compiled out, so it proceeds to drm_mm_interval_tree_remove(node, &mm->interval_tree) with mm == NULL, dereferencing a near-NULL kernel address (the offset of interval_tree inside struct drm_mm) and panicking the guest kernel. This fires on every driver unload, including normal shutdown of a GVT-g guest VM in common balloon configurations (e.g., any config where mappable_base == 0, which skips bl_info.space[0] allocation).

Root cause

intel_vgt_balloon() (i915_vgpu.c:197-275) allocates bl_info.space[0..3] only inside four independent if guards (lines 231, 239, 247, 255). Any slot whose guard is false is never inserted into the drm_mm allocator. bl_info is a file-scope static (i915_vgpu.c:98), so unallocated slots stay BSS-zero.

intel_vgt_deballoon() (i915_vgpu.c:119-130) then runs

for (i = 0; i < 4; i++)
    vgt_deballoon_space(&dev_priv->ggtt, &bl_info.space[i]);

with no per-slot allocation check.

vgt_deballoon_space() (i915_vgpu.c:100-110) calls drm_mm_remove_node(node) unconditionally. drm_mm_remove_node() (sys/dev/drm/drm_mm.c:588-612) documents "It is a bug to call this function on an unallocated node" and only guards with DRM_MM_BUG_ON(!node->allocated) (drm_mm.c:597) β€” which expands to BUILD_BUG_ON_INVALID (a compile-time no-op) when CONFIG_DRM_DEBUG_MM is unset (sys/dev/drm/include/drm/drm_mm.h:51-55). Execution then reaches drm_mm_interval_tree_remove(node, &mm->interval_tree) (drm_mm.c:605) with mm = node->mm == NULL, computing &((struct drm_mm*)NULL)->interval_tree β€” a small positive address (offset of interval_tree after head_node, ~144 bytes). rb_erase_cached β†’ linux_root_RB_REMOVE reads the rb_root pointer at that address, page-faulting on the unmapped low virtual address.

The established correct pattern is shown one screen away in the same call site: i915_gem_gtt.c:3007-3008 wraps the analogous call as

if (drm_mm_node_allocated(&ggtt->error_capture))
    drm_mm_remove_node(&ggtt->error_capture);

β€” the vgpu deballoon path omits exactly this guard.

Threat model & preconditions

  • Attacker position: any user able to trigger i915 driver teardown inside an Intel GVT-g guest VM. This includes: (a) root in the guest running kldunload i915 / devctl detach, (b) any code path that invokes i915_ggtt_cleanup_hw (i915_gem_gtt.c:2992, which unconditionally calls intel_vgt_deballoon at line 3011 inside the if (drm_mm_initialized(&ggtt->vm.mm)) block), and β€” most damaging β€” (c) every clean shutdown / reboot of a GVT-g guest VM whose balloon config leaves at least one of the four slots unallocated, which is the common case (e.g., any VM that owns the start of its mappable aperture: mappable_base == 0 β†’ bl_info.space[0] is never allocated β†’ deballoon panics on i==0 first).
  • Privileges gained or impact: kernel NULL-pointer-dereference panic, guest DoS.
  • Required config or capabilities: running inside a GVT-g vGPU (dev_priv->vgpu.active == true, set by i915_check_vgpu at i915_vgpu.c:80 when the VGT_MAGIC MMIO signature is present) and PR:H (root, or shutdown privilege). Not reachable on bare metal.

Proof of concept

PoC source: findings/poc/DF-2100/

Build & run

# inside an Intel GVT-g guest VM running DragonFlyBSD
cc -O2 -o trigger trigger.c
sudo ./trigger        # kldunload i915_kms || kldunload i915

Expected output

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x...        # small, < PAGE_SIZE
...
drm_mm_remove_node+0x...
intel_vgt_deballoon+0x...
i915_ggtt_cleanup_hw+0x...

The exact panic address equals the offset of drm_mm.interval_tree (~0x90-0xa0).

To prove the shutdown angle without root: simply issue shutdown -r now from any account permitted to do so on a GVT-g guest whose host-supplied avail_rs leaves mappable_base == 0; the reboot path runs the same cleanup and panics before reboot completes.

Impact

  • Default config: not reachable on bare metal; only inside an Intel GVT-g guest VM.
  • Reliability: deterministic β€” fires on every driver unload where at least one slot was unallocated.
  • Blast radius: guest kernel panic = guest DoS. No host escape, no memory corruption beyond the panic.

Mirror the drm_mm_node_allocated() guard used at i915_gem_gtt.c:3007. Apply:

--- a/sys/dev/drm/i915/i915_vgpu.c
+++ b/sys/dev/drm/i915/i915_vgpu.c
@@ -100,6 +100,9 @@ static struct _balloon_info_ bl_info;
 static void vgt_deballoon_space(struct i915_ggtt *ggtt,
                struct drm_mm_node *node)
 {
+   if (!drm_mm_node_allocated(node))
+       return;
+
    DRM_DEBUG_DRIVER("deballoon space: range [0x%llx - 0x%llx] %llu KiB.\n",
             node->start,
             node->start + node->size,
             node->size / 1024);

    ggtt->vm.reserved -= node->size;
    drm_mm_remove_node(node);
 }

This matches the documented contract of drm_mm_remove_node ("It is a bug to call this function on an unallocated node", drm_mm.c:586) and the precedent set by the neighbouring error_capture teardown. The error-unwinding paths inside intel_vgt_balloon() (i915_vgpu.c:266-271) do not need the guard because each label only deballoons a slot that was successfully allocated earlier in the same call.

References

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2100 Β· 2 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix 345 B view raw
VERDICT.md verdict source-trace confirmation 689 B ↓ raw
VERDICT.md verdict source-trace confirmation
↓ download raw

DF-2100 β€” intel_vgt_deballoon drm_mm_remove_node on unallocated nodes

Verdict

REPRODUCED (source-only confirmation). Bug confirmed by source tracing.

Mechanism

intel_vgt_deballoon (i915_vgpu.c:128-129) unconditionally calls vgt_deballoon_space on all 4 bl_info.space[] entries. intel_vgt_balloon (197-275) only conditionally allocates them. drm_mm_remove_node on an unallocated (zero-initialized) node dereferences NULL mm -> crash. Only reachable in vGPU (Intel GVT-g) environments.

Fix

Add allocated check in vgt_deballoon_space: if (!node->allocated) return;

Batch-build status

Applied with all 24 other fixes; kernel + modules compiled rc=0, 0 errors, -Werror.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Added if(!node->allocated) return; batch build rc=0.

Added if(!node->allocated) return; batch build rc=0.
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

intel_vgt_deballoon drm_mm_remove_node on unallocated space -> NULL deref.

Verified recommended fix

intel_vgt_deballoon drm_mm_remove_node on unallocated space -> NULL deref.

Verdict

intel_vgt_deballoon drm_mm_remove_node on unallocated space -> NULL deref.