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)
PoC verification
Evidence pack
findings/poc/DF-5036 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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_testablen/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).
Confirmed kernel references
- sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:127
- sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:232
- sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:254
- sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:279
- sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:288
- sys/dev/drm/amd/display/dc/i2caux/dcn10/i2caux_dcn10.c:114
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.
No comments yet.