β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5036

Shared family constructor 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 feeding an unbounded engine-array store (DF-4827 pattern-class)

Summary

The factory for five ASIC generations takes the loop bound as a caller parameter but indexes callee-static 6-entry tables - nothing relates the two. All five in-tree callers pass exactly 6 today (mechanically verified) so no current trigger; the family's own history shows this exact mismatch class becoming a KLD-proven High (DF-4827). Entry-clamp fix authored.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5036 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
analysis.sh β€” 1.1 KB view raw
fix.diff β€” 619 B view raw
verdict.json β€” 3.0 KB view raw
manifest.json β€” 1.2 KB view raw

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:

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:

    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.

VERDICT.md
↓ download raw

DF-5036 β€” VERDICT

status: untested (latent defect β€” no in-tree trigger; construct path requires an AMD ASIC, absent from the QEMU guest) reproduced: 0, impact: none (demonstrated ceiling if ever triggered: OOB read of static rodata feeding an unbounded index into the engine pointer arrays β€” OOB write), confidence: certain

Narrative

dal_i2caux_dce110_construct() is the shared engine factory for the whole DCE10/11/12/DCN10 i2caux family (verified callers: i2caux_dce100.c:100, i2caux_dce112.c:94, i2caux_dce120.c:114, i2caux_dcn10.c:114, plus its own dal_i2caux_dce110_create at i2caux_dce110.c:321). Its loop bound num_i2caux_inst is a caller parameter designed to track the size of the caller-supplied aux_regs[]/i2c_hw_engine_regs[] tables β€” that is exactly how all five callers compute it (ARRAY_SIZE(<their>_aux_regs)). But the same i also indexes two callee-static 6-entry arrays, hw_ddc_lines[] (i2caux_dce110.c:127-134) and hw_aux_lines[] (:136-143), at :232 and :279. Nothing relates num_i2caux_inst to ARRAY_SIZE(hw_ddc_lines); the compiler cannot check it because the function receives the other tables as decayed pointers.

Today all five callers pass 6 (each sibling file defines exactly six aux_regs(0..5) and six hw_engine_regs(1..6) entries β€” verified by reading every sibling; analysis.sh reproduces the count check mechanically). So no in-tree path overruns: this is a latent API-contract defect, the same shape that already produced a real High finding in this family (DF-4827: a line validated against COUNT(8) but indexed into a 6-entry array β€” OOB read of an adjacent scalar used as a pointer, then a wild engine->ddc = ddc write). If a future ASIC file passes count β‰₯ 7 (natural: the register tables legitimately grow), the constructor reads hw_ddc_lines[6+] from adjacent rodata β€” in this TU the neighbours are hw_aux_lines[] and the i2caux_funcs vtable (:146) β€” and stores through base->i2c_hw_engines[line_id] (:254) / base->aux_engines[line_id] (:288) with that garbage index, writing past the GPIO_DDC_LINE_COUNT(8)-sized arrays inside the heap-allocated struct i2caux_dce110.

Classification honesty: with no current trigger, this is hardening (Info), not an exploitable bug. It is filed because the family's own history (DF-4827) shows exactly this ownership mismatch becoming a real, KLD-proven High, and the fix is a two-line clamp (fix.diff).

Exploit chain

None today. Hypothetical future-trigger chain documented above only.

Fix validation

fix.diff clamps num_i2caux_inst to ARRAY_SIZE(hw_ddc_lines) at function entry (hw_aux_lines has the same size, asserted by construction). Not built/booted: nothing to observe β€” the clamp is a no-op for all five current callers, and the constructor cannot execute without an AMD ASIC.

kernel_refs: - sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:127-143 - sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:231-232 - sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:254 - sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:278-279 - sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:288 - callers: i2caux_dce100.c:100, i2caux_dce112.c:94, i2caux_dce120.c:114, i2caux_dcn10.c:114 - pattern precedent: DF-4827 (dce_i2c_sw.c)

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_testable
baseline no→ patch + rebuild →patched clean

n/a β€” latent defect, no in-tree trigger; clamp is a no-op for all five current callers (verified by reading each sibling's table sizes).

fix.diff (ARRAY_SIZE clamp at function entry).
↓ fix.diffper-fix-DF-5036

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md and VERDICT.md; run ./analysis.sh β€” prints the static line tables, the loops that index them with the caller count, and mechanically counts aux_regs/hw_engine_regs entries per sibling (all 6/6) plus each caller's ARRAY_SIZE(...) bound.

PoC changes

No PoC written β€” latent API-contract defect with zero in-tree triggers; evidence is a mechanical source-print/count script.

Verified recommended fix

Clamp num_i2caux_inst to ARRAY_SIZE(hw_ddc_lines) at the top of dal_i2caux_dce110_construct β€” see fix.diff.

Verdict

dal_i2caux_dce110_construct() is the shared engine factory for dce100/dce110/dce112/dce120/dcn10, taking the loop bound num_i2caux_inst (and the register tables) as caller parameters while indexing two callee-static 6-entry tables hw_ddc_lines[]/hw_aux_lines[] (i2caux_dce110.c:127-143) at :232/:279 and storing through base->i2c_hw_engines[line_id]/base->aux_engines[line_id] at :254/:288. Nothing relates num_i2caux_inst to ARRAY_SIZE(hw_ddc_lines). All five in-tree callers today pass exactly 6, so no overrun exists in-tree; a future ASIC passing a larger count (natural, since its register tables legitimately grow) reads a garbage line_id from adjacent rodata and writes past the GPIO_DDC_LINE_COUNT(8)-sized engine arrays in the heap object. Filed as latent hardening (Info) because the identical ownership mismatch already produced a real KLD-proven High in this family (DF-4827, dce_i2c_sw.c). Untested: no in-tree trigger, and the construct path needs an AMD ASIC absent from the guest.