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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4166 Β· 6 files
FileTypeDescriptionSize
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, so header->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.md
↓ download raw

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

Fix 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).
↓ fix.diffper-fix-DF-4166

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.