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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4311 Β· 13 files
FileTypeDescriptionSize
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.md
↓ download raw

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):

  1. 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.
  2. 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_switcheroo is 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-4311

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.