dal_i2caux_release_engine(): unsynchronized engine state — engine->ddc NULL-wipe race ends in dal_ddc_close(NULL) NULL-deref panic (DAL2 mutex dropped) + wrong-owner pin close
Summary
The acquire->submit->release critical section runs with no lock (the DAL2 mutex was dropped in this port - admitted in the header) - a concurrent acquirer of the same engine can pass the pin double-open guard in the window between close and the NULL store, so T1 wipes T2's fresh ddc (immediate NULL deref in submit or dal_ddc_close(NULL), which derefs without guard) or closes T2's pins mid-transaction. HPD-detect vs mode-set on one connector; narrow-window DoS. Guard-the-close + restore-the-mutex fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4878 · 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | — | 3.6 KB | ↓ raw | |
| VERDICT.md | — | 3.2 KB | ↓ raw | |
| env.txt | — | 195 B | view raw | |
| fix.diff | — | 663 B | view raw | |
| manifest.json | — | 665 B | view raw | |
| verdict.json | — | 3.0 KB | view raw |
DF-4878 — i2caux engine acquire/release is unsynchronized: engine->ddc
NULL-wipe race ends in dal_ddc_close(NULL) NULL-deref
sys/dev/drm/amd/display/dc/i2caux/i2caux.c:420-429:
void dal_i2caux_release_engine(
struct i2caux *i2caux,
struct engine *engine)
{
engine->funcs->release_engine(engine);
dal_ddc_close(engine->ddc); /* :426 — no NULL guard */
engine->ddc = NULL; /* :428 — wipes concurrent acquirer's ddc */
}
The whole acquire→submit→release engine critical section runs without any lock — the header even documents the omission (sys/dev/drm/amd/display/dc/i2caux/i2caux.h:91: "// [anaumov] in DAL2, there is a Mutex"). DAL2's mutex was dropped in this port.
Race (two threads, same DDC line / same engine)
Engines are per-line on all current ASICs (i2c_sw_engines[] /
aux_engines[], i2caux.c:385/409; the "generic" engines are never created
— dce80/dce110 constructs end with "TODO Generic I2C SW and HW"). Two
concurrent transactions on one connector (e.g. HPD-workqueue EDID read
racing a DRM-ioctl AUX transfer) both target the same engine object:
- T1 in
dal_i2caux_release_engine:dal_ddc_close(engine->ddc)done (pins closed,gpio->pin = NULL— gpio_base.c open/close state machine), but T1 has not yet executedengine->ddc = NULL. - T2 calls
engine->funcs->acquire()→dal_aux_engine_acquire()(aux_engine.c:48-77) /dal_i2caux_i2c_sw_engine_acquire_engine()(i2c_sw_engine.c:462-476): the pin double-open guard (dal_gpio_open_ex, gpio_base.c:59-70) now passes (T1 already closed), T2 opens the pins and storesengine->ddc = ddc(aux_engine.c:75 / i2c_sw_engine.c:473). - T1 resumes:
engine->ddc = NULL— wipes T2's ddc pointer. - T2 submits: for the SW i2c path
dal_i2c_sw_engine_submit_channel_request()immediately dereferencesengine->base.ddc(i2c_sw_engine.c:491start_sync(ctx, ddc, ...)) → NULL deref; for the AUX path T2 survives to its own release and callsdal_ddc_close(NULL)(i2caux.c:426) →dal_gpio_close(ddc->pin_clock)on NULL (gpio_service.c:587-590, which has no NULL guard —dal_gpio_closeguards its argument butdal_ddc_closedereferencesddcitself) → fatal trap.
Secondary hazard in the same window: if T2's acquire completes before
T1's dal_ddc_close runs, T1 closes T2's pins mid-transaction (wrong
owner close), corrupting the gpio pin state machine for the running
bit-banged/AUX sequence.
Reachability: dal_i2caux_submit_i2c_command / submit_aux_command
(i2caux.c:109-267) are the EDID/DDC/AUX datapath of the amdgpu display
stack (callers: dc_link_ddc.c:598 dal_i2caux_submit_aux_command and the
i2c-algo path), driven by HPD detect, DP AUX, and DRM ioctls. Root is
needed for DRM master ioctls, but HPD-driven detection runs concurrently
with them by design, so unprivileged-triggerable events (hotplug) can
supply the second thread.
Status: untested (in situ) — no AMD GPU on the audit guest
amdgpu.ko is present on the guest but the only VGA is QEMU's std VGA
(vgapci0 8086:1111 — env.txt), so the amdgpu driver never attaches and the
DC i2caux object is never constructed. This is a hardware-availability
blocker; the interleaving above is established purely from the lock-free
code layout (two adjacent stores at i2caux.c:426/428 vs. the store in the
acquire path at aux_engine.c:75).
Fix
fix.diff (hardening sink): guard if (engine->ddc) dal_ddc_close(...)
in dal_i2caux_release_engine, killing the NULL-deref sink. The complete
fix is to restore the DAL2 mutex (serialize acquire→submit→release per
i2caux) — see recommended_fix in the finding.
DF-4878 VERDICT — unsynchronized i2caux engine release NULL-wipe → dal_ddc_close(NULL)
Status: untested in situ (no AMD GPU on guest); race established by line trace
Root cause
dal_i2caux_release_engine() (sys/dev/drm/amd/display/dc/i2caux/i2caux.c:420-429)
performs, with no synchronization whatsoever:
engine->funcs->release_engine(engine)— for aux engines just a register update (aux_engine_dce110.c:73-78), for SW i2c engines a no-op (i2c_sw_engine.c:559-561);dal_ddc_close(engine->ddc)— closes both DDC pins;engine->ddc = NULL.
The acquire path (dal_aux_engine_acquire aux_engine.c:48-77;
dal_i2caux_i2c_sw_engine_acquire_engine i2c_sw_engine.c:462-476) opens
the pins and stores engine->ddc = ddc (aux_engine.c:75,
i2c_sw_engine.c:473). The pin double-open guard lives in
dal_gpio_open_ex (gpio->pin != NULL → GPIO_RESULT_ALREADY_OPENED,
gpio_base.c:59-70) — which re-opens the instant T1's dal_ddc_close
completes, i.e. between steps 2 and 3 of a concurrent release. T1's step 3
then NULLs the ddc pointer T2 just installed:
- SW i2c path: T2's
dal_i2c_sw_engine_submit_channel_request()usesstruct ddc *ddc = engine->base.ddc(i2c_sw_engine.c:491) → NULL deref instart_sync. - AUX path: T2 completes the transaction and its own release calls
dal_ddc_close(NULL)(i2caux.c:426) →ddc->pin_clockon NULL (gpio_service.c:587-590 —dal_ddc_closehas no NULL guard, unlikedal_gpio_closeat gpio_base.c:220-227) → fatal trap.
The port itself flags the missing lock: i2caux.h:91
/* [anaumov] in DAL2, there is a Mutex */ — DAL2 serialized
acquire→submit→release; this tree does not. Related known family:
DF-4787/DF-4788 (gpio_service open path), DF-4808 (hw-engine accounting) —
all different files; this file's defect is the release-side wipe.
Converse interleaving (T2 acquires before T1's close): T1 closes T2's pins mid-transaction — wrong-owner close corrupting the gpio state machine.
Why untested
The guest's only display device is QEMU std VGA (env.txt: vgapci0
8086:1111); amdgpu.ko never attaches, the DC i2caux object is never
constructed, and its datapath (dc_link_ddc.c:598 → i2caux.c:109/196)
cannot be entered. Hardware blocker, not a refutation — every store/load
in the interleaving was verified at the cited lines.
Impact
Narrow-window kernel NULL-deref panic (or gpio state corruption) inside the display DDC/AUX path, triggered by concurrent hotplug-detect vs mode-set transactions on one connector. DoS-class; not a corruption primitive (the wiped value is NULL, not attacker data).
Fix validation
fix_status: not_testable (hardware absent). fix.diff guards the close —
if (engine->ddc) dal_ddc_close(engine->ddc); — removing the panic sink
(the wrong-owner close and full serialization need the DAL2 mutex;
recommended in the finding text).
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
not_testableNo AMD GPU on guest; the guarded close removes the panic sink by construction; full serialization needs the mutex (recommended in finding)
fix.diff (git apply --check OK)
Confirmed kernel references
- sys/dev/drm/amd/display/dc/i2caux/i2caux.c:420
- sys/dev/drm/amd/display/dc/i2caux/i2caux.c:426
- sys/dev/drm/amd/display/dc/i2caux/i2caux.c:428
- sys/dev/drm/amd/display/dc/i2caux/i2caux.h:91
- sys/dev/drm/amd/display/dc/i2caux/aux_engine.c:75
- sys/dev/drm/amd/display/dc/i2caux/i2c_sw_engine.c:473
- sys/dev/drm/amd/display/dc/i2caux/i2c_sw_engine.c:491
- sys/dev/drm/amd/display/dc/gpio/gpio_service.c:587
- sys/dev/drm/amd/display/dc/gpio/gpio_base.c:59
Detail
Exploit chain
T1 release between dal_ddc_close (i2caux.c:426) and engine->ddc=NULL (:428) while T2 acquire passes the now-clear pin-open guard (gpio_base.c:59-70) and sets engine->ddc=ddc (aux_engine.c:75 / i2c_sw_engine.c:473) -> T1 wipes it -> T2 submits (SW: i2c_sw_engine.c:491 NULL ddc) or T2 releases (i2caux.c:426 dal_ddc_close(NULL) -> gpio_service.c:590 NULL deref) -> panic
Evidence (decisive lines)
VERDICT.md (interleaving with every load/store cited); README.md; env.txt (vgapci0 8086:1111 std VGA, amdgpu.ko unloaded)
PoC changes
n/a — no AMD GPU on guest (hardware blocker); race proven by code layout
Verified recommended fix
Guard dal_ddc_close(engine->ddc) against NULL (sink fix, fix.diff) and restore the DAL2 per-i2caux mutex across acquire->submit->release (root fix)
Verdict
dal_i2caux_release_engine() runs unsynchronized (the DAL2 mutex was dropped — i2caux.h:91) and stores engine->ddc = NULL two statements after dal_ddc_close(); a concurrent acquirer of the same per-line engine can open the just-closed pins and install its ddc pointer inside that window, which the release then wipes — the SW-i2c submit path dereferences the NULL ddc immediately, and the AUX path ends in dal_ddc_close(NULL) whose callee dereferences ddc->pin_clock without a NULL guard (gpio_service.c:587-590) -> fatal trap. Narrow-window display-path panic; not runnable on the audit guest because its only VGA is QEMU std VGA so amdgpu never attaches.
No comments yet.