# DF-5036 — `dal_i2caux_dce110_construct` indexes callee-static 6-entry `hw_ddc_lines[]`/`hw_aux_lines[]` with the caller-supplied `num_i2caux_inst`: latent OOB read → garbage line index → OOB write into the engine arrays (DF-4827 pattern-class)

- File: `sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c`
- Lines: 127-143 (static line tables), 231-232 / 278-279 (indexed by `i < num_i2caux_inst`)
- Severity: Info (latent — no in-tree caller exceeds the bound today).
  Confidence: certain (about the code shape / API contract).

## What the code does

`dal_i2caux_dce110_construct()` is the SHARED family constructor — it is
the engine factory for **five** ASIC generations:

| caller | num passed | aux_regs[] passed | hw_engine_regs[] passed |
|---|---|---|---|
| dce110 (`i2caux_dce110.c:321-327`) | 6 | dce110 (6) | dce110 (6) |
| dce100 (`i2caux_dce100.c:100-106`) | 6 | dce100 (6) | dce100 (6) |
| dce112 (`i2caux_dce112.c:94-100`)  | 6 | dce112 (6) | dce112 (6) |
| dce120 (`i2caux_dce120.c:114-120`) | 6 | dce120 (6) | dce120 (6) |
| dcn10 (`i2caux_dcn10.c:114-120`)   | 6 | dcn10 (6) | dcn10 (6) |

Its signature invites callers to bring their own counts and register
tables (`num_i2caux_inst`, `aux_regs[]`, `i2c_hw_engine_regs[]` are all
parameters, i2caux_dce110.c:202-209) — but the DDC/AUX *line* tables it
indexes are **file-static, fixed at 6 entries**:

```c
static const enum gpio_ddc_line hw_ddc_lines[] = { DDC1..DDC6 };  /* :127 */
static const enum gpio_ddc_line hw_aux_lines[]  = { DDC1..DDC6 };  /* :136 */

	do {
		enum gpio_ddc_line line_id = hw_ddc_lines[i];       /* :232  i unbounded by 6 */
		...
		base->i2c_hw_engines[line_id] = ...;                /* :254 */
	} while (i < num_i2caux_inst);                          /* :271 caller's count */
```

The register-array parameters are likewise indexed with the caller count
(`&i2c_hw_engine_regs[i]` :250, `&aux_regs[i]` :286) — for those, the
caller controls both sides. For the line tables, the callee owns the array
and the caller owns the bound: **an asymmetric ownership contract**. Any
future ASIC whose `*_create()` passes `num_i2caux_inst > 6` (a natural
copy-paste from the register tables, whose size legitimately grows with
the ASIC) reads past the static const arrays, pulls a garbage `line_id`
out of adjacent rodata (function pointers live nearby — `i2caux_funcs`
at :146), and then performs an **OOB write** into
`base->i2c_hw_engines[line_id]` / `base->aux_engines[line_id]`
(struct fields sized `GPIO_DDC_LINE_COUNT` = 8).

This is precisely the bug class already proven exploitable-adjacent in
this family: DF-4827 (High, KLD-proven) — a caller validated a DDC line
against `GPIO_DDC_LINE_COUNT`(8) while the array it indexed was sized
`MAX_PIPES`(6), producing an OOB read of an overlaying scalar reinterpreted
as an engine pointer. Here the mismatch is count-vs-static-array rather
than COUNT-vs-array, and no current caller trips it — hence Info/latent,
but the guard is one line and the family history shows the pattern bites.

## Fix

Clamp once, at the top of the constructor:

```diff
 	uint32_t i = 0;
+	/* the line tables are callee-static (6 entries): never walk past them */
+	if (num_i2caux_inst > ARRAY_SIZE(hw_ddc_lines))
+		num_i2caux_inst = ARRAY_SIZE(hw_ddc_lines);
```

(or make the line tables parameters like the reg arrays, restoring
symmetric ownership).

## Reproduce / evidence

No runtime PoC: the trigger requires a (future) caller passing count > 6;
all five in-tree callers pass 6 (verified by `analysis.sh`). The construct
path additionally only executes on real AMD ASICs, absent from the guest.
