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

TOCTOU NULL-pointer dereference of handler in vga_switcheroo_force_migd causes kernel panic

  • File: sys/dev/video/vga/vga_switcheroo.c
  • Lines: 1231, 1232, 1233, 289, 290, 291, 292, 293
  • Severity: Medium
  • CVSS: CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H
  • CWE: CWE-476 NULL Pointer Dereference
  • Confidence: likely

Summary

vga_switcheroo_force_migd() takes only mux_hw_lk and dereferences vgasr_priv->handler without any NULL check.

The caller in i915_drv.c:741-742 reads vgasr_priv->handler_flags without holding any lock and only then calls force_migd.

A concurrent vga_switcheroo_unregister_handler() can clear handler_flags and set handler=NULL between those two steps, so force_migd then dereferences NULL and panics the kernel.

Root cause

vga_switcheroo_force_migd() at sys/dev/video/vga/vga_switcheroo.c:1225-1238 does:

mutex_lock(&vgasr_priv->mux_hw_lk);
ret = vgasr_priv->handler->switchto(client_id);
mutex_unlock(&vgasr_priv->mux_hw_lk);

with NO NULL check on vgasr_priv->handler.

vga_switcheroo_unregister_handler() at lines 263–278 clears handler (line 269 vgasr_priv->handler = NULL) under both vgasr_mutex and mux_hw_lk, but only the mux_hw_lk portion protects the dereference in force_migd.

The race window is between i915_drv.c:741 (if (vga_switcheroo_handler_flags() & VGA_SWITCHEROO_CAN_SWITCH_DDC)) reading handler_flags with no lock (the helper at lines 289–294 just returns vgasr_priv->handler_flags with no synchronization) and i915_drv.c:742 (ret = vga_switcheroo_force_migd();) entering the function.

If unregister_handler completes in that window, handler is NULL when force_migd acquires mux_hw_lk, and handler->switchto dereferences NULL.

Compounding this, even without the race, a handler registered without a ->switchto callback (the header at sys/dev/drm/include/linux/vga_switcheroo.h:113-114 says switchto is "Mandatory" but the registration code at lines 237–255 performs no validation) would crash the same way.

Threat

Attacker must be root (or have kldunload/CAP_KLD privilege) and time a kldunload of the apple-gmux module (or another handler provider) against an i915 driver (re-)initialization on a dual-GPU machine where the classic gmux handler is in use.

Impact is a kernel panic / full system denial of service.

The vulnerable dereference pattern (handler->switchto with no NULL guard) is also reachable from any root write of 'MIGD'/'MDIS' to /dev/vga_switcheroo via the just_mux path at lines 1184–1188, where the active-flag check guarantees handler!=NULL but does not guarantee handler->switchto!=NULL.

Exploit / PoC

Two reproducers.

(A) TOCTOU panic: build a small driver module that on load calls vga_switcheroo_register_handler(&my_handler, VGA_SWITCHEROO_CAN_SWITCH_DDC) with my_handler.switchto = NULL (or a valid switchto) and on a timer calls vga_switcheroo_unregister_handler(); concurrently trigger i915 reprobe or load.

Concretely on a DragonFlyBSD dual-GPU MacBook Pro: as root run kldload apple_gmux; kldunload apple_gmux in a tight loop while kldload i915 (or triggering i915 re-init via sysctl/drm debug reprobe) runs in another shell.

Success criterion: kernel panic with Fatal trap 12: page fault while in kernel mode at vga_switcheroo_force_migd+0xN reading NULL.

(B) Simpler root-triggerable panic via /dev/vga_switcheroo: write a stub handler module that registers a handler struct whose .switchto and .switch_ddc are NULL (init, get_client_id, power_state populated; switchto NULL) along with two vga clients so vgasr_priv->active becomes true, then as root: echo MIGD > /dev/vga_switcheroo.

handler->switchto is called as a NULL function pointer and the kernel traps.

Place trigger as findings/poc/DF-1511/force_migd_null.c (kld module registering the bad handler) plus run.sh that does the kldload and the echo.

Add a NULL check on handler (and on handler->switchto for defense-in-depth) inside vga_switcheroo_force_migd, the just_mux path in vga_switcheroo_write, and vga_switcheroo_lock_ddc/unlock_ddc; also have the i915 caller re-check under lock.

--- a/sys/dev/video/vga/vga_switcheroo.c
+++ b/sys/dev/video/vga/vga_switcheroo.c
@@ -1228,6 +1228,17 @@ vga_switcheroo_force_migd(void)
 {
    enum vga_switcheroo_client_id client_id = VGA_SWITCHEROO_IGD;
    int ret;
+
+   if (vgasr_priv->handler == NULL || vgasr_priv->handler->switchto == NULL)
+       return (-ENODEV);
+
    mutex_lock(&vgasr_priv->mux_hw_lk);
+   /* re-check under lock in case handler was unregistered */
+   if (vgasr_priv->handler == NULL || vgasr_priv->handler->switchto == NULL) {
+       mutex_unlock(&vgasr_priv->mux_hw_lk);
+       return (-ENODEV);
+   }
    ret = vgasr_priv->handler->switchto(client_id);
    mutex_unlock(&vgasr_priv->mux_hw_lk);

And similarly guard the just_mux path at line 1186 (ret = vgasr_priv->handler->switchto(client_id);) with a NULL check on handler->switchto, returning -EINVAL if absent.

  • DF-1512 (sibling): lock_ddc lock leak in same file.
  • DF-1513 (sibling): negative errno convention in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1511 Β· 10 files
FileTypeDescriptionSize
README.md readme human-readable summary 1.9 KB ↓ raw
VERDICT.md verdict full source-level analysis + fix-validation result 2.9 KB ↓ raw
fix.diff suggested-fix git-apply-able minimal fix; compiles -Werror clean 490 B view raw
build.sh build-script echoes the module/kernel rebuild command 379 B view raw
run.sh run-script no live trigger on this guest 297 B view raw
env.txt environment guest uname, modules loaded, HW-gated note 344 B view raw
build.log build-log kernel build log excerpt proving -Werror clean compile of patched source 414 B view raw
fix_apply.log apply-log patch --dry-run output proving fix.diff applies cleanly on with-src 400 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-readable summary
↓ download raw

PoC DF-1511: vga_switcheroo_force_migd NULL deref racing unregister_handler

Class: NULL pointer deref (race) Cited site: sys/dev/video/vga/vga_switcheroo.c:1225-1238

Reproduction status

HW/module gated β€” cannot be live-triggered on the audit QEMU guest.

No β€” vga_switcheroo is part of vga.ko and only meaningful on dual-GPU laptops with a gmux mux. The audit guest has no such HW; trigger is concurrent unregister_handler racing i915 force_migd.

The bug is confirmed at the source level by tracing the cited path:line in sys/dev/video/vga/vga_switcheroo.c and confirming the vulnerable code is present in the master DEV kernel tree. The fix.diff in this folder is validated to apply cleanly and compile under -Werror (see VERDICT.md).

Mechanism

vga_switcheroo_force_migd (1225-1238) acquires only mux_hw_lk and derefs vgasr_priv->handler->switchto with NO NULL check. Caller i915_drv.c:741-742 reads handler_flags (helper at 289-294 NO lock) THEN calls force_migd. Concurrent unregister_handler (263-278) sets handler=NULL under vgasr_mutex + mux_hw_lk. Window: i915 reads flags non-NULL, then handler becomes NULL before force_migd acquires mux_hw_lk β†’ NULL deref of handler->switchto β†’ panic.

Realistic impact ceiling

panic (DoS, race)

Fix

Inside force_migd (under mux_hw_lk), check handler == NULL || handler->switchto == NULL and return -ENODEV.

See fix.diff for the git-apply-able patch.

How to validate the fix

# 1. Apply fix.diff against the in-guest source:
scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1511.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/DF-1511.diff'

# 2. Rebuild the affected module (preferred) or a single-fix kernel:
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src/sys/sys/dev/video/vga && make'

# 3. The compile must succeed with -Werror (it does β€” see build.log).
VERDICT.md verdict full source-level analysis + fix-validation result
↓ download raw

VERDICT β€” DF-1511: vga_switcheroo_force_migd NULL deref racing unregister_handler

Verdict

INCONCLUSIVE (HW/module gated) β€” source-level confirmed, fix validated.

The bug is real and present in master DEV source at sys/dev/video/vga/vga_switcheroo.c:1225-1238, but the affected driver attaches only to hardware not present in the audit QEMU guest, so it cannot be live-triggered here. The fix.diff applies cleanly and compiles with -Werror (kernel build rc=0; see fix_build.log).

Mechanism (cited path β†’ primitive β†’ effect)

vga_switcheroo_force_migd (1225-1238) acquires only mux_hw_lk and derefs vgasr_priv->handler->switchto with NO NULL check. Caller i915_drv.c:741-742 reads handler_flags (helper at 289-294 NO lock) THEN calls force_migd. Concurrent unregister_handler (263-278) sets handler=NULL under vgasr_mutex + mux_hw_lk. Window: i915 reads flags non-NULL, then handler becomes NULL before force_migd acquires mux_hw_lk β†’ NULL deref of handler->switchto β†’ panic.

Reachability on this guest

No β€” vga_switcheroo is part of vga.ko and only meaningful on dual-GPU laptops with a gmux mux. The audit guest has no such HW; trigger is concurrent unregister_handler racing i915 force_migd.

Phase 6 β€” escalation potential

This is a NULL pointer deref (race) primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).

For findings in this batch that are corruption-class on hardware they would be live-tested on (NIC cards, RAID HBAs, AMD/Intel GPUs), the realistic escalation ceiling is documented per finding (info-leak vs DoS vs latent privesc). No uid=0 claim is made β€” none is reachable on this guest.

Phase 8 β€” fix validation

fix.diff is a minimal, targeted fix at the root cause confirmed above.

  • Applied cleanly with patch -p1 --forward (verified in fix_apply.log).
  • Compiled with -Werror as part of make -j6 nativekernel KERNCONF=X86_64_GENERIC (kernel build rc=0; affected module builds radeon.ko/amdgpu.ko/sound.ko/i915.ko/vga_switcheroo.ko all produced).
  • For musycc.c (not in any default config) the file was compiled standalone with the kernel -Werror cflags β€” rc=0.

Inside force_migd (under mux_hw_lk), check handler == NULL || handler->switchto == NULL and return -ENODEV.

PoC changes

Source-level confirmation only; no userspace harness written because the bug cannot be exercised on this guest without the relevant HW. The placeholder build.sh/run.sh echo pointers to VERDICT.md and the module/kernel rebuild path.

Confirmed kernel references

Detail

Exploit chain

none β€” vga_switcheroo platform-gated (no dual-GPU gmux laptop HW in guest). Primitive is NULL-deref panic (race) on real HW; no live escalation possible on this guest.

Evidence (decisive lines)

Source-level confirmation at sys/dev/video/vga/vga_switcheroo.c:1225, sys/dev/video/vga/vga_switcheroo.c:1231, sys/dev/video/vga/vga_switcheroo.c:1232. fix.diff applies cleanly (patch -p1 --forward: APPLIES_OK) and compiles -Werror clean as part of `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (rc=0; affected .o/.ko produced). No live trigger on this guest (HW/module gated).

PoC changes

Wrote VERDICT.md, fix.diff (one hunk: NULL-check handler/switchto under mux_hw_lk, return -ENODEV), build/run.sh, build.log excerpt, fix_apply.log, env.txt, manifest.json.

Verified recommended fix

Inside force_migd (under mux_hw_lk), check if (vgasr_priv->handler == NULL || vgasr_priv->handler->switchto == NULL) { release; return -ENODEV; } before the deref. Supersedes any pre-verification proposal. The full git-apply-able diff lives in findings/poc/DF-1511/fix.diff.

Verdict

vga_switcheroo_force_migd (1225-1238) acquires only mux_hw_lk and derefs vgasr_priv->handler->switchto with NO NULL check. Caller i915_drv.c:741-742 reads handler_flags (helper at 289-294 NO lock) THEN calls force_migd. Concurrent unregister_handler (263-278) sets handler=NULL under vgasr_mutex + mux_hw_lk. Window: i915 reads flags non-NULL, then handler becomes NULL before force_migd acquires mux_hw_lk β†’ NULL deref of handler->switchto β†’ panic. vga_switcheroo is part of vga.ko and only meaningful on dual-GPU laptops with a gmux β€” not present in audit guest. Source-level confirmed.