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)
PoC verification
Evidence pack
findings/poc/DF-5745 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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-EINVALfor 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_CRSeach (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_stateagainst 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.amlgmuxtrig.c/Makefileβ trigger KLDpanic.txt,panic.b2.txtβ panic signatures (both rigs)run.logβ decisive run transcriptfix.diffβ 6 hunks;fix_validation.logβ baseline/patched comparison
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 staticapple_gmux_softc_data.apple-gmux.c:867β unconditionalapple_gmux_softc_data = sc;on every attach that reaches the registration stage.apple-gmux.c:880-888β second instance'svga_switcheroo_register_handler()returns-EINVAL(vga_switcheroo.c:242-245: one handler per system).apple-gmux.c:892-894βerr_register_handler:path setsapple_gmux_softc_data = NULLunconditionally β destroying the live global owned by the first, still-attached instance.- Dispatches dereference the global with no NULL guard:
gmux_switchtoapple-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 viavga_switcheroo_lock_ddc.
Reproduction (guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, QEMU)
Planted SSDT with two APP000B devices. Two independent rigs:
- 4-port
_CRSrig (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-22after overwriting + NULLing the global. - 0x78-length
_CRSrig (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:βAcpiRemoveNotifyHandlerafterAcpiEnableGpefailure, apple-gmux.c:897-900) β same root cause class (no-opAcpiOsWaitEventsComplete), not re-reported here. vga_switcheroo.chas its own adjacent issues (e.g.force_migdlacks 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
fixedIdentical 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
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.
No comments yet.