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.
Recommended fix
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.
Related findings
- DF-1512 (sibling):
lock_ddclock leak in same file. - DF-1513 (sibling): negative errno convention in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1511 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 β 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 infix_apply.log). - Compiled with
-Werroras part ofmake -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
-Werrorcflags β 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
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- v
- g
- a
- /
- v
- g
- a
- _
- s
- w
- i
- t
- c
- h
- e
- r
- o
- o
- .
- c
- :
- 1
- 2
- 2
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- v
- g
- a
- /
- v
- g
- a
- _
- s
- w
- i
- t
- c
- h
- e
- r
- o
- o
- .
- c
- :
- 1
- 2
- 3
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- v
- g
- a
- /
- v
- g
- a
- _
- s
- w
- i
- t
- c
- h
- e
- r
- o
- o
- .
- c
- :
- 1
- 2
- 3
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- v
- g
- a
- /
- v
- g
- a
- _
- s
- w
- i
- t
- c
- h
- e
- r
- o
- o
- .
- c
- :
- 2
- 6
- 4
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.
No comments yet.