vga_switcheroo MOD_UNLOAD/MOD_SHUTDOWN frees module state and lets the module unload while DRM modules hold live symbol relocations and (on hybrid hardware) registered clients β use-after-unload panic / heap UAF
Summary
The module event handler shares one body for MOD_UNLOAD and MOD_SHUTDOWN: destroy_dev + kfrees with no check of registered_clients/handler, no veto, no global NULLing, no lockuninit. Independently, the linker resolves this module's exported symbols for drm.ko/i915.ko/amdgpu.ko via a global search that records NO dependency - so kldunload vga_switcheroo is not blocked while the DRM stack is loaded and calling in. Root-triggered kernel UAF: REPRODUCED on the guest with a dependent KLD (kldunload succeeds while dependent loaded; next call-in -> Fatal trap 12 with IP inside the unloaded module's text; guest down); fix validated on the same guest (EBUSY veto + MOD_SHUTDOWN split + MODULE_DEPEND x3 - unload only drops a reference, panic-op returns -19, real unload chains after the last dependent). Guarded-teardown + deps fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4311 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| uaftrig.c | β | 2.4 KB | view raw | |
| Makefile | β | 174 B | β download | |
| run.sh | β | 222 B | view raw | |
| run.log | β | 3.2 KB | view raw | |
| panic.txt | β | 1.5 KB | view raw | |
| panic-full.log | β | 2.3 KB | view raw | |
| fix.log | β | 2.3 KB | view raw | |
| fix.diff | β | 2.3 KB | view raw | |
| env.txt | β | 545 B | view raw | |
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| verdict.json | β | 4.0 KB | view raw | |
| manifest.json | β | 1.5 KB | view raw |
DF-4311 β vga_switcheroo unsafe module teardown (use-after-unload)
What: sys/dev/video/vga/vga_switcheroo.c MOD_UNLOAD/MOD_SHUTDOWN handler (free case at vga_switcheroo.c:1022-1027) tears down and frees the module's global state and lets the module unload even while other loaded kernel modules (drm.ko, i915.ko, amdgpu.ko) hold direct relocations to its exported symbols β and, on real hybrid-GPU systems, while DRM clients are registered in vgasr_priv->clients.
Impact: kldunload(8) of vga_switcheroo.ko followed by any vga_switcheroo_*() call from the DRM stack = call into freed module text + operation on freed heap state -> kernel panic (demonstrated) / heap UAF.
Privilege: kldunload requires root (and securelevel must allow it). This is a privileged-user UAF / robustness defect, NOT an unpriv escalation.
Files:
uaftrig.c PoC module: stands in for drm.ko's live call-ins
(drm_edid.c:1770 drm_get_edid_switcheroo ->
vga_switcheroo_lock_ddc/unlock_ddc; i915_drv.c:742).
Writing /dev/uaftrig performs exactly that call pair.
Makefile builds uaftrig.ko (needs SYSDIR=/usr/src/sys)
run.sh full run recipe (phases actually executed via vm.sh run_root)
run.log baseline (unpatched) session: parser exercise + panic
panic.txt serial-console panic (Fatal trap 12, IP in unloaded text)
panic-full.log full console capture
fix.log post-fix validation run (same guest, patched modules)
fix.diff git-apply-able fix (passes git apply --check on sys/)
env.txt guest environment
Build (in guest, as root): cd /root/uaftrig && make SYSDIR=/usr/src/sys
Run (in guest, as root): kldload vga_switcheroo kldload ./uaftrig.ko echo trigger > /dev/uaftrig # baseline: -19/-19 kldunload vga_switcheroo # SUCCEEDS (no dependency guard) echo trigger-after-unload > /dev/uaftrig # ==> kernel panic
Expected (unpatched): Fatal trap 12, instruction fetch fault at the unloaded module's text address; guest down with db> prompt. Expected (patched + MODULE_DEPEND declared in the dependent module): kldunload reports success but only drops a reference (module stays resident); trigger returns -19/-19; no panic.
VERDICT β DF-4311 vga_switcheroo unsafe module teardown UAF
STATUS: reproduced (kernel panic, root-triggered use-after-unload) IMPACT: panic β privileged local (kldunload requires root). Not an unprivileged escalation; classified Low severity for that reason, but the memory-corruption class is real: the crash is an instruction fetch at freed/unmapped module text, and on a system where the freed module VA range got reused, this is an arbitrary-code-execution primitive at a time of the admin's choosing; the vgasr_priv heap free additionally leaves lockmgr()-on-freed-memory writes for any later caller if the module text happened to remain mapped.
ROOT CAUSE (two independent halves, both required for the bug):
-
vga_switcheroo.c:1022-1027 β the module event handler runs
destroy_dev(); kfree(vga_switcheroo_buf); kfree(vgasr_priv);for BOTH MOD_UNLOAD and MOD_SHUTDOWN (shared case labels), with:- no check that vgasr_priv->registered_clients == 0 or that vgasr_priv->handler == NULL,
- no veto (always returns 0, so the unload always proceeds),
- globals not NULLed, mux_hw_lk never lockuninit()ed.
-
No module declares a dependency on vga_switcheroo, although three modules in this tree hold direct relocations to its symbols:
- drm.ko: drm_edid.c:1770-1772 vga_switcheroo_lock_ddc()/_unlock_ddc()
- i915.ko: i915_drv.c:741-742 vga_switcheroo_handler_flags() / vga_switcheroo_force_migd() (live, #ifdef DragonFly)
- amdgpu.ko: amdgpu_kms.c:950 vga_switcheroo_process_delayed_switch()
sys/kern/kern_linker.c linker_file_lookup_symbol() (kern_linker.c:605-660)
resolves such cross-file symbols with a GLOBAL search across all loaded
files and records nothing; linker_file_add_dependancy() (kern_linker.c:575)
is only invoked for DECLARED MODULE_DEPEND metadata
(linker_load_dependencies, kern_linker.c:1583+). Consequently
kldunload vga_switcheroois not blocked while the DRM stack is loaded β verified live: the unload succeeded while the dependent module was resident, then the dependent's call faulted.
REPRODUCTION (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC guest, QEMU): * uaftrig.ko is a stand-in for drm.ko: it references vga_switcheroo_lock_ddc()/unlock_ddc() exactly like drm_get_edid_switcheroo() does, and /dev/uaftrig invokes them. * kldload vga_switcheroo; kldload uaftrig.ko; baseline trigger: lock_ddc = -19 (-ENODEV, no handler), unlock_ddc = -19. * kldunload vga_switcheroo -> "vga_switcheroo device unloaded", rc=0 (module freed and unmapped while uaftrig still resident). * trigger -> Fatal trap 12: page fault in kernel mode, fault VA = 0xffffffff82601040 = instruction pointer, i.e. the CPU jumped into the unloaded module's text (module was mapped at 0xffffffff82600000 + 0x2000). Guest down at db>. * Secondary empirical results captured in run.log: the /dev write parser is bounded (255B/1024B/appends safe), all commands inert (module can never become active in this tree: every registration call site is compiled out), and unprivileged access is denied (0600).
FIX VALIDATION (see fix.log): * fix.diff applied to guest /usr/src; vga_switcheroo.ko rebuilt with the MOD_UNLOAD EBUSY guard + separate MOD_SHUTDOWN case; uaftrig.ko rebuilt with MODULE_DEPEND(uaftrig, vga_switcheroo, 1, 1, 1) modeling the drm/i915/amdgpu dependency declarations in fix.diff. * Same sequence: baseline -19/-19; kldunload vga_switcheroo now only DROPS A REFERENCE (kldstat shows the module still resident); the post-unload trigger returns -19/-19 with the guest alive β the exact operation that panicked the unpatched kernel. * After kldunload uaftrig, the linker chained the real vga_switcheroo unload automatically ("vga_switcheroo device unloaded", module gone). * Baseline reproduced pre-fix; patched behavior correct post-fix: fix_status = fixed.
PRIVILEGE / THREAT honesty: * Triggering requires root (kldunload). On securelevel>0 systems kldunload is refused entirely. So: Low severity, privileged-user UAF + shutdown-path state free. Filed because the teardown contract is wrong (a module must not free state its live callers depend on, and must not unload while exported-symbol callers hold relocations), and because the same handler frees live state at MOD_SHUTDOWN, which fires on every reboot with hybrid-GPU drivers mid-shutdown. * No unprivileged route exists to the write parser (0600 root:wheel, verified) β the pass-2 hunt of the parser itself was negative.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff (teardown guard + MOD_SHUTDOWN split + MODULE_DEPEND declarations in drm/i915/amdgpu) applied to guest /usr/src and validated: pre-fix the trigger panics; post-fix kldunload only drops a reference, module stays resident, trigger returns -19/-19, and the true unload chains after the last dependent unloads. Clean guest afterwards.
findings/poc/DF-4311/fix.log (patched-run console transcript), findings/poc/DF-4311/fix.diff (git apply --check clean against the audit sys/ tree)
Confirmed kernel references
Detail
Exploit chain
root: kldload vga_switcheroo; kldload dependent module holding direct relocation to vga_switcheroo_lock_ddc (drm.ko / i915.ko / amdgpu.ko in-tree, uaftrig.ko in PoC); kldunload vga_switcheroo (module text unmapped + vgasr_priv/vga_switcheroo_buf kfree'd); any subsequent DRM switcheroo call-in (EDID probe via drm_get_edid_switcheroo, i915 load-time force_migd, amdgpu lastclose) -> instruction fetch in freed module text -> kernel panic; if the VA range is reused before the call, execution lands in attacker-influenced kernel heap instead.
Evidence (decisive lines)
run.log (baseline phases + unload + panic), panic.txt (Fatal trap 12, instruction pointer == fault VA inside freed module text 0xffffffff82601040), fix.log (patched run: module stays resident, trigger -19/-19, chained clean unload), fix.diff (git apply --check clean against sys/)
PoC changes
Authored from scratch (no seed): uaftrig.ko is a minimal dev_ops module that calls vga_switcheroo_lock_ddc/unlock_ddc(NULL) on write, mirroring drm_edid.c's live call pattern; had to add pci_if.h to SRCS and the full DFly include set (sys/bus.h, sys/device.h) to build; MODULE_DEPEND added for the fix-validation build.
Verdict
Reproduced: kldunload vga_switcheroo.ko succeeds while a dependent module (uaftrig.ko standing in for drm.ko's drm_edid.c:1770 call-ins) is loaded, because kern_linker.c records no dependency for globally-resolved cross-module symbols; the next vga_switcheroo_lock_ddc() call faults at the unloaded module's text (Fatal trap 12, IP=fault VA=0xffffffff82601040) and the guest dies. The same MOD_UNLOAD body (shared with MOD_SHUTDOWN) also kfree()s vgasr_priv without checking registered_clients/handler. Trigger requires root (kldunload), so this is a privileged-user UAF/panic (Low), not an unpriv escalation. Fix validated on the same guest: with the teardown guard + declared MODULE_DEPEND, the unload only drops a reference, the module stays resident, and the panic-op returns -19/-19 with the system alive.
No comments yet.