# DF-5035 — i2caux_dce110 `i2c_hw_buffer_in_use`: unsynchronized check-then-set lets concurrent acquires share the single DCE11 DC_I2C HW engine (acquire-side twin of DF-4878's release-side race)

- File: `sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c`
- Lines: 105-109 (acquire), 120-122 (release)
- Severity: Low. Confidence: speculative (race window is real, concurrency
  prerequisite unproven on this stack).

## What the code does

The whole `struct i2caux` carries no lock — the port's own header admits it:

```c
	/* [anaumov] in DAL2, there is a Mutex */
	uint32_t aux_timeout_period;              /* i2caux.h:91-93 */
```

`acquire_i2c_hw_engine()` guards the chip-wide single DC_I2C transaction
buffer with a plain, lock-free flag:

```c
	if (!i2caux_dce110->i2c_hw_buffer_in_use &&              /* :105 */
		engine->base.funcs->acquire(&engine->base, ddc)) {
		i2caux_dce110->i2c_hw_buffer_in_use = true;      /* :107 */
		return engine;
	}
```

and `release_engine()` clears it:

```c
	if (engine->funcs->get_engine_type(engine) == I2CAUX_ENGINE_TYPE_I2C_DDC_HW)
		i2caux_dce110->i2c_hw_buffer_in_use = false;     /* :120-122 */
```

## The race

Thread A (connector DDC1) and thread B (connector DDC2) both in
`dal_i2caux_submit_i2c_command()` (`i2caux.c:109-194`, itself lock-free):

1. A reads `i2c_hw_buffer_in_use == false`
2. B reads `i2c_hw_buffer_in_use == false`
3. A's `engine->base.funcs->acquire()` takes DDC1 pins — succeeds
4. B's `engine->base.funcs->acquire()` takes DDC2 pins — succeeds
   (different `struct ddc`, different gpio pins — the per-engine acquire
   does NOT serialize cross-line)
5. Both set the flag true, both get an engine, both drive the single shared
   DC_I2C HW block (per-transaction buffer `DC_I2C_*` registers,
   `i2c_hw_engine_dce110.c` `submit_request`/`process_transaction`) with
   interleaved state.

Effect: corrupted DDC/EDID/AUX-over-I2C transactions (wrong bytes, bogus
NACKs, wedged engine needing the reset path at
`i2c_hw_engine_dce110.c:123-139`), i.e. functional corruption of display
detection — not kernel memory corruption. Kernel memory stays untouched;
the flag itself is a plain uint32-ish bool so no torn access.

Realistic triggers: HPD hotplug work (EDID read on one connector) racing a
DRM ioctl path reading EDID on another connector. On real AMD DCE11 ASICs
only — the QEMU guest has no AMD GPU. Whether two i2caux submits can
actually run concurrently on this DragonFly port (single dm hotplug thread
vs ioctl context) was not proven; that is why confidence is speculative.
The release-side sibling of this missing-lock problem (engine->ddc NULL-wipe
race ending in `dal_ddc_close(NULL)`) is already filed as DF-4878 on
`i2caux.c`; this is the dce110-private acquire-side instance (different
field, different file).

## Fix

Serialize acquire/release with a lock in `struct i2caux` (restoring the
DAL2 mutex the comment references), e.g. `lockmgr`-style or the drm
`mutex` shim already used elsewhere in this port; check-then-acquire-then-set
and the release-side clear both inside it. Minimal version: make
`i2c_hw_buffer_in_use` an atomic test-and-set, though that only closes the
double-acquire, not DF-4878's release-side interleavings — one mutex closes
both.

## Reproduce / evidence

No PoC run: needs an AMD DCE11-class ASIC with two connectors and a
concurrent HPD+ioctl workload; the test guest has only a virtio GPU and no
drm KLD. Run `./analysis.sh` to print the cited lines.
