# 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`:

```c
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 executed `engine->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 stores `engine->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 dereferences `engine->base.ddc` (i2c_sw_engine.c:491
  `start_sync(ctx, ddc, ...)`) → NULL deref; for the AUX path T2 survives
  to its own release and calls `dal_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_close`
  guards its argument but `dal_ddc_close` dereferences `ddc` itself)
  → 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.
