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 invokesi915_ggtt_cleanup_hw(i915_gem_gtt.c:2992, which unconditionally callsintel_vgt_deballoonat line 3011 inside theif (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 oni==0first). - 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 byi915_check_vgpuati915_vgpu.c:80when theVGT_MAGICMMIO signature is present) andPR: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.
Recommended fix
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
sys/dev/drm/drm_mm.c:588-612βdrm_mm_remove_nodecontract.sys/dev/drm/include/drm/drm_mm.h:51-55βDRM_MM_BUG_ONcompiled-out behavior.sys/dev/drm/i915/i915_gem_gtt.c:3007-3008β the correctly-guarded sibling pattern.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2100 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix | 345 B | view raw |
| VERDICT.md | verdict | source-trace confirmation | 689 B | β 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
fixedAdded if(!node->allocated) return; batch build rc=0.
Added if(!node->allocated) return; batch 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.
No comments yet.