get_gpio_i2c_info off-by-one (count < table_index) reads gpio_pin[count] one entry past the GPIO pin LUT
Summary
table_index = record->i2c_id & 0x0f (0..15). The guard redirecting to a linear search is 'count < table_index' - when count == table_index the search is skipped and gpio_pin[table_index] is read with table_index == count, exactly one 8-byte entry past the table region declared by structuresize. Distinct root cause from DF-1415: fires even with a perfectly consistent, small table (count=1, lane-mux 1 reads gpio_pin[1]). Values become the DDC/I2C GPIO MMIO register index and bit shift (wrong-register MMIO programming). '<=' fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4166 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.6 KB | β raw | |
| VERDICT.md | β | 1.6 KB | β raw | |
| verdict.json | β | 2.8 KB | view raw | |
| manifest.json | β | 789 B | view raw | |
| env.txt | β | 847 B | view raw | |
| fix.diff | β | 391 B | view raw |
DF-4166 β get_gpio_i2c_info off-by-one lets table_index == count index gpio_pin[] past the table
- File: sys/dev/drm/amd/display/dc/bios/bios_parser2.c:367-403
- Class: OOB read (off-by-one), CWE-125. Hardware-gated (DCE12+ ASIC, crafted VBIOS).
- Distinct from DF-1415 (count derived from structuresize exceeding the declared
gpio_pin[8]): here the table can be perfectly consistent (count == real entries,
<= 8) and the guard
if (count < table_index)still fails to catch table_index == count, soheader->gpio_pin[table_index]reads ONE ENTRY PAST the table's declared end (gpio_pin[count]) at lines 374 and 403.
Trigger math
table_index = record->i2c_id & I2C_HW_LANE_MUX; // 0x0f -> 0..15 count = (structuresize - 4) / 8; // entries actually in table if (count < table_index) { / search loop reassigns table_index in [0,count) / } // count == table_index -> loop SKIPPED -> gpio_pin[count] read (OOB by 1 entry, 8 bytes)
E.g. a GPIO pin LUT with exactly 1 entry (count=1) and an I2C record whose lane mux nibble is 1 -> reads gpio_pin[1], 8 bytes past the table data.
Impact
Reads data_a_reg_index (MMIO register index) + gpio_bitshift from whatever follows the table in the BIOS image (or past the image buffer if the table is at its end). Values feed DC's GPIO/DDC programming (clk_a_register_index) -> wrong-register MMIO access. Info leak ceiling: none direct (not copied to userspace).
Build/run: not runnable here (no AMD GPU in audit guest, see env.txt).
Source-level certainty: comparison-operator bug, provable by inspection.
VERDICT β DF-4166
status: untested (hardware-gated; guest has no AMD GPU β env.txt) expected impact: small OOB read (8 bytes) past the gpio_pin_lut table in the VBIOS image heap buffer; derived MMIO register index/bitshift misprogram DDC/GPIO
Root cause
bios_parser2.c:367 table_index = record->i2c_id & I2C_HW_LANE_MUX (I2C_HW_LANE_MUX
= 0x0f, atomfirmware.h:597). The guard at :369 is count < table_index; the
skip-search case count == table_index falls through to header->gpio_pin[table_index]
at :374 (search loop) β unreachable there because the loop is skipped β and at
:401-405 (info->gpio_info.clk_a_register_index / clk_a_shift), which is executed
unconditionally. gpio_pin is the flexible tail of atom_gpio_pin_lut_v2_1
(atomfirmware.h:619-624, declared gpio_pin[8]); count entries are covered by
structuresize. Index == count reads 8 bytes past the covered region.
Fix: count <= table_index (fix.diff). Upstream Linux carries the same operator
in this era of the DAL code; the clamp is a one-character-class fix.
Why untested
Guest pciconf: only bochs SVGA 0x1234:1111; no AMD device; dev.drm absent. The DC bios parser never runs. Fix.diff authored from source; not build-validated.
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_testableFix authored from source analysis only: Change the guard to count <= table_index so index==count enters the search loop. Baseline/patched kernels could not be exercised on the audit guest (no AMD GPU), so fix validation is not_testable rather than fixed.
fix.diff in this pack (unified diff generated against the pristine sys/ tree).
Confirmed kernel references
Detail
Exploit chain
Hardware attacker model only: crafted VBIOS field on AMD hardware. No userland-triggerable chain on the audit guest - path hardware-gated.
Evidence (decisive lines)
VERDICT.md (root cause, line-accurate), README.md (trigger math), env.txt (guest hardware-gating proof: pciconf/sysctl/uname), fix.diff (source-authored, git-apply-able, not build-validated).
PoC changes
None - no seeded PoC sources existed for these pass-2 findings; packs built fresh.
Verified recommended fix
Change the guard to count <= table_index so index==count enters the search loop.
Verdict
Off-by-one in get_gpio_i2c_info: the guard at bios_parser2.c:369 is count < table_index, so table_index == count skips the search loop and gpio_pin[count] is read at :401-405 - one 8-byte entry past the table region declared by structuresize. Distinct root cause from DF-1415 (which is about count exceeding the declared gpio_pin[8]); this triggers even with a perfectly consistent table. Impact is a small OOB read whose bytes become the DDC/GPIO MMIO register index and bitshift.
No comments yet.