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

apple_gmux: second-instance attach clobbers the shared apple_gmux_softc_data global β†’ NULL-deref panic in vga_switcheroo dispatch (spontaneous at i915 load)

Summary

attach sets the single static global unconditionally; with two APP000B devices instance 2 overwrites it, its registration fails -EINVAL, and the error path NULLs the global while instance 1's handler remains registered - dispatching through NULL: gmux_switchto writes to NULL+0x60. Planted rig (independent of the length bug): i915's boot-time vga_switcheroo_force_migd call -> 'Fatal trap 12 fault VA 0x60, Stopped at gmux_switchto+0x10', guest wedged. NULL-guard + conditional-ownership fix validated (trigger returns 0, no panic, the surviving instance keeps serving). Fixed-value store at fixed offset - DoS ceiling.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5745 Β· 18 files
FileTypeDescriptionSize
README.md β€” 3.7 KB ↓ raw
VERDICT.md β€” 4.5 KB ↓ raw
make_ssdt.py β€” 3.7 KB view raw
ssdt_b.aml β€” 138 B ↓ download
ssdt_b2.aml β€” 138 B ↓ download
gmuxtrig.c β€” 2.2 KB view raw
Makefile β€” 142 B ↓ download
build.sh β€” 145 B view raw
run.sh β€” 171 B view raw
build.log β€” 204 B view raw
run.log β€” 1.5 KB view raw
panic.txt β€” 739 B view raw
panic.b2.txt β€” 624 B view raw
fix.diff β€” 1.6 KB view raw
fix_validation.log β€” 1.1 KB view raw
env.txt β€” 296 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.5 KB view raw

DF-5745 β€” apple_gmux: dual-instance attach clobbers the shared global softc pointer β†’ NULL-deref panic in vga_switcheroo dispatch

What

apple_gmux_softc_data is a single static global (apple-gmux.c:130). In apple_gmux_attach:

  • line 867: apple_gmux_softc_data = sc; β€” unconditional, even for a second gmux ACPI device;
  • lines 880-884: vga_switcheroo_register_handler() fails with -EINVAL for the second instance (a handler is already registered, sys/dev/video/vga/vga_switcheroo.c:242-245);
  • line 894 (error path): apple_gmux_softc_data = NULL; β€” also unconditional.

Net state after the second (failed) attach: the first instance's handler is still registered, but the global it dispatches through is NULL. Every vga_switcheroo dispatch then executes

gmux_switchto():  apple_gmux_softc_data->switch_state_ddc = id;   /* :511 */
gmux_switch_ddc(): ...->switch_state_ddc                          /* :524 */
gmux_set_power_state(): gmux_set_discrete_state(apple_gmux_softc_data, ...) /* :581 */

β†’ write to NULL+offset β†’ fatal page fault.

Natural trigger on affected systems: i915 load (sys/dev/drm/i915/i915_drv.c:742 β€” if (vga_switcheroo_handler_flags() & VGA_SWITCHEROO_CAN_SWITCH_DDC) vga_switcheroo_force_migd(); β†’ vgasr_priv->handler->switchto with no NULL guard, vga_switcheroo.c:1225-1239). Also /dev/vga_switcheroo writes (root-only, 0600) and any DRM EDID probe via vga_switcheroo_lock_ddc.

Precondition: two ACPI devices with _HID APP000B (crafted DSDT/SSDT, or duplicated gmux entries from a firmware bug).

Rig

  • ssdt_b.aml: AAA0 @ 0x8e + AAA2 @ 0x4cc, 4-port _CRS each (both reach full attach: version reads at base+4 hit readable QEMU ports 0x92 / ELCR 0x4d0-0x4d1, so the classic path is taken).
  • ssdt_b2.aml: same two devices with valid-length 0x78-port regions (AAA0 @ 0x4cc, AAA2 @ 0x8e) β€” proves the bug is independent of the DF-5744 length issue.
  • gmuxtrig.c β†’ gmuxtrig.ko: calls the same production entry point i915 uses at boot: vga_switcheroo_force_migd().

Build / run (guest, root)

cd /root/gmuxtrig_b && make
# boot QEMU with: -acpitable file=ssdt_b.aml
kldload apple_gmux                       # AAA0 attaches; AAA2 fails at -22
kldload /root/gmuxtrig_b/gmuxtrig.ko     # -> panic

Expected result (observed twice: 4-port rig and 0x78-length rig)

apple_gmux: Failed to register vga_switcheroo handler
device_probe_and_attach: apple_gmux1 attach returned -22
gmuxtrig: vga_switcheroo handler_flags = 0x1 (CAN_SWITCH_DDC=1)
gmuxtrig: calling vga_switcheroo_force_migd() ...
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x60
fault code = supervisor write data, page not present
Stopped at gmux_switchto+0x10: movl %edi,0x60(%rbx)      ; rbx == NULL global

Offset 0x60 == offsetof(struct apple_gmux_softc, switch_state_ddc) β€” the exact store at apple-gmux.c:511.

Fix validation (fix.diff, module-only rebuild)

  • Guard gmux_switchto/gmux_switch_ddc/gmux_set_power_state against a NULL global (panic β†’ -ENODEV).
  • Clear the global on error/detach only when this softc owns it.
  • Take ownership of the global only when it is still NULL (line 867).

Same 0x78-length rig, patched module, same trigger:

gmuxtrig: force_migd returned 0, NO PANIC     # dispatch drives the surviving instance

(Intermediate fix without the ownership hunk: returned -19, NO PANIC β€” no crash but dead handler; the ownership hunk restores function.)

Files

  • make_ssdt.py (shared), ssdt_b.aml, ssdt_b2.aml
  • gmuxtrig.c / Makefile β€” trigger KLD
  • panic.txt, panic.b2.txt β€” panic signatures (both rigs)
  • run.log β€” decisive run transcript
  • fix.diff β€” 6 hunks; fix_validation.log β€” baseline/patched comparison
VERDICT.md
↓ download raw

DF-5745 VERDICT β€” REPRODUCED (fatal page fault, production code, on-guest)

Status: reproduced Β· impact: panic (kernel NULL-deref DoS) Β· confidence: certain

Root cause (static, certain)

  • apple-gmux.c:130 β€” single static apple_gmux_softc_data.
  • apple-gmux.c:867 β€” unconditional apple_gmux_softc_data = sc; on every attach that reaches the registration stage.
  • apple-gmux.c:880-888 β€” second instance's vga_switcheroo_register_handler() returns -EINVAL (vga_switcheroo.c:242-245: one handler per system).
  • apple-gmux.c:892-894 β€” err_register_handler: path sets apple_gmux_softc_data = NULL unconditionally β€” destroying the live global owned by the first, still-attached instance.
  • Dispatches dereference the global with no NULL guard: gmux_switchto apple-gmux.c:511-513, gmux_switch_ddc :524/530/533, gmux_set_power_state :581.
  • Natural trigger: i915 boot path i915_drv.c:738-746 (handler_flags() & CAN_SWITCH_DDC β†’ vga_switcheroo_force_migd() β†’ handler->switchto() without checking the handler's softc); also root's /dev/vga_switcheroo (0600) writes and DRM EDID paths via vga_switcheroo_lock_ddc.

Reproduction (guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, QEMU)

Planted SSDT with two APP000B devices. Two independent rigs:

  1. 4-port _CRS rig (ssdt_b.aml, AAA0@0x8e, AAA2@0x4cc): both attach to the registration stage (version reads at base+4 hit readable QEMU ports 0x92 and ELCR 0x4d0/0x4d1 β†’ "classic"); AAA0 registers the handler fully; AAA2 fails at -22 after overwriting + NULLing the global.
  2. 0x78-length _CRS rig (ssdt_b2.aml) β€” same state machine with valid-length regions, proving independence from DF-5744.

Both rigs, unmodified shipped /boot/kernel/apple_gmux.ko, trigger KLD invoking the exact production entry point i915 uses:

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x60        <- offsetof(switch_state_ddc)
fault code = supervisor write data, page not present
Stopped at gmux_switchto+0x10: movl %edi,0x60(%rbx)   <- rbx = NULL global

(panic.txt / panic.b2.txt; ssh session died + guest in DDB, vm.sh status β‡’ down β€” reproduced/panic per the status table.)

handler_flags = 0x1 (CAN_SWITCH_DDC=1) confirms i915_drv.c:742 would take the same branch on this state, i.e. on an affected machine the panic occurs spontaneously at DRM load, no root action needed beyond module load order.

Threat model

Requires two APP000B ACPI devices β€” crafted/malicious DSDT/SSDT (root / firmware compromise / firmware bug). Not reachable by an unprivileged local user on properly-firmware'd hardware. Kernel NULL-deref β†’ panic (DoS), not memory corruption with controlled data (write value is an enum at a fixed small offset; NULL page not mappable on x86-64 without... the write is to fixed VA 0x60, kernel-mode, non-present β†’ always a trap). Low severity consistent with the crafted-firmware precondition; impact ceiling is reboot-panic of a machine at DRM load.

Fix validation (module-only rebuild, same 0x78 rig)

  • Step 1 (guards + conditional clear only): trigger β†’ force_migd returned -19, NO PANIC β€” crash eliminated, but line 867's unconditional set still let the second instance claim-then-clear the global (handler dead).
  • Step 2 (complete fix.diff, adds ownership: set global only if NULL): trigger β†’ force_migd returned 0, NO PANIC, guest stays up β€” the surviving instance keeps working.

Baseline reproduced; patched behavior: no panic and functional dispatch β‡’ fix_status: fixed. (Intermediate -19 result retained in fix_validation.log as evidence the guard alone is insufficient for correctness.)

Notes

  • Same-family lifetime issue as DF-2228 (ACPI notify-handler UAF) exists on the attach-error path (err_enable_gpe: β†’ AcpiRemoveNotifyHandler after AcpiEnableGpe failure, apple-gmux.c:897-900) β€” same root cause class (no-op AcpiOsWaitEventsComplete), not re-reported here.
  • vga_switcheroo.c has its own adjacent issues (e.g. force_migd lacks a handler NULL check; negative errno returned from d_write) β€” different file, noted for a future pass, not part of this finding.

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

Identical rig + trigger: unpatched module panics (gmux_switchto+0x10, fault va 0x60); module rebuilt with fix.diff returns cleanly ('force_migd returned 0, NO PANIC', guest up 18s+ at check). Intermediate guard-only variant returned -19 without panic, proving the ownership hunk is needed for a functional (not merely non-crashing) handler.

findings/poc/DF-5745/fix_validation.log; panic.b2.txt; fix.diff
↓ fix.diffper-fix-DF-5745

Confirmed kernel references

Detail

Exploit chain

crafted ACPI with two APP000B gmux devices -> kldload apple_gmux (or boot with modules) -> instance 1 attaches + registers vga_switcheroo handler -> instance 2 attaches, overwrites global at :867, register_handler returns -EINVAL, error path NULLs global at :894 -> any switcheroo dispatch (i915 load at i915_drv.c:742, /dev/vga_switcheroo write, DRM lock_ddc) -> gmux_switchto writes enum to NULL+0x60 -> fatal page fault -> kernel panic (DoS). Fixed offset/value store; not a controllable-memory-corruption primitive.

Evidence (decisive lines)

panic.txt + panic.b2.txt (identical signatures on both rigs: gmux_switchto+0x10 movl %edi,0x60(%rbx)); run.log (full decisive run incl. 'apple_gmux1 attach returned -22' then trap); fix_validation.log (baseline panic; step-1 fix -19 no panic; complete fix return 0 no panic, uptime shown)

PoC changes

Seed had no runnable rig. Built hand-encoded AML SSDT generator (two APP000B devices; readable non-0xff QEMU ports 0x92/0x4d0 chosen so the classic path is taken to full attach) and a trigger KLD invoking the production vga_switcheroo_force_migd() path i915 uses. Second rig with valid 0x78-length regions to decouple from DF-5744. Fixed a redundant-declaration build error in the trigger (header already declares force_migd); DDB-hung guest hard-kills lost un-synced .ko files once (rebuilt).

Verified recommended fix

Guard the three handler callbacks against a NULL apple_gmux_softc_data, take ownership of the global only if still NULL at :867, and clear it on error/detach only when this softc owns it (see fix.diff).

Verdict

REPRODUCED twice on-guest with production code: apple_gmux_attach sets the shared global apple_gmux_softc_data unconditionally (apple-gmux.c:867), so a second APP000B ACPI device overwrites it, then fails vga_switcheroo_register_handler with -EINVAL and its error path NULLs the global unconditionally (apple-gmux.c:894) while the first instance's handler remains registered. Any dispatch then writes through NULL in gmux_switchto (apple-gmux.c:511): observed 'Fatal trap 12, fault virtual address = 0x60, supervisor write data, Stopped at gmux_switchto+0x10: movl %edi,0x60(%rbx)' with the shipped /boot/kernel/apple_gmux.ko, on two independent planted-ACPI rigs (4-port and valid-length 0x78 _CRS). handler_flags=0x1 (CAN_SWITCH_DDC) proves i915's boot path (i915_drv.c:742 -> vga_switcheroo_force_migd -> handler->switchto) hits the same trap spontaneously at DRM load on an affected machine. Precondition is two APP000B devices (crafted/malicious ACPI or firmware bug), not unprivileged-reachable -> Low severity, panic/DoS impact. fix.diff (NULL guards + ownership of the global) validated on the same rig: 'force_migd returned 0, NO PANIC', guest stays up, surviving instance functional.