ext-TMDS init parser desynchronisation: non-IGP case-0 record missing 'index += 4' β value/terminator bytes re-executed as MMIO commands
Summary
In the non-IGP branch, every command case advances 'index' past its operands except case 0, which reads val = RBIOS32(index) and calls WREG32(reg, val) without skipping the 4 value bytes - unlike the IGP branch of the same function. After the first plain register write, the low half of the just-written VALUE is re-read as the next command id, the 0xffff terminator is swallowed as operand bytes, and the walk continues past the record stream executing data as commands. HARNESS on a well-formed stream: buggy walk executes an unintended case-2 RMW (reg built from terminator+data) and terminates at +0x38 instead of the terminator at +0x30. Inherited verbatim from upstream. Corrupt/truncated VBIOS ext-TMDS init table: every modeset executes unintended MMIO writes - GPU register corruption/hang. Crafted-VBIOS attackers gain nothing beyond DF-1251; hence Low. One-line fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3855 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | walks a well-formed ext-TMDS record stream with the kernel loop (buggy) and the fixed loop; shows the value/terminator re-executed as commands | 5.0 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 65 B | view raw |
| run.sh | run-script | ./harness (crash-tolerant wrapper for the SIGFPE/SIGSEGV harnesses) | 89 B | view raw |
| build.log | build-log | full compiler output (guest) | 65 B | view raw |
| run.log | run-log | full runtime output (guest) - decisive | 673 B | view raw |
| fix.diff | suggested-fix | ext-TMDS init parser desync: non-IGP case-0 record missing index += 4 - git/patch-applyable vs sys/ | 246 B | view raw |
| fix_apply.log | apply-log | patch --dry-run -p1 -d /usr/src on the guest: clean apply | 314 B | view raw |
| VERDICT.md | verdict | full narrative with path:line trace | 3.4 KB | β raw |
| README.md | readme | build/run/expected + hardware reproduction notes | 1.8 KB | β raw |
| env.txt | environment | guest uname + toolchain + 'dmesg radeon lines: 0' | 210 B | view raw |
| verdict.json | verdict-json | machine verdict (persist_poc.py input) | 3.4 KB | view raw |
DF-3855 β ext-TMDS init parser desync: non-IGP case 0 record missing index += 4
Verdict (one line)
CONFIRMED REAL by source trace + harness (well-formed stream misparsed);
security impact bounded (crafted-VBIOS attacker already controls the table);
NOT runnable on the audit guest (no radeon GPU) β inconclusive.
Build / run
./build.sh # cc -O2 -Wall -o harness harness.c ./run.sh # walks the same stream with buggy and fixed parsers
Expected output
The stream is one well-formed case-0 record (write reg 4, value 0x55555555) followed by the 0xffff terminator. The buggy kernel walk re-reads the value's low half (0x5555) as the next command id β executes an unintended second command, consumes the terminator as operand bytes, and runs past the end of the record stream (feeding whatever follows β data or out-of-table bytes β into WREG32):
[non-IGP, buggy] ...
case 0 (direct write): consumes 4-byte value but DOES NOT advance index
WREG32(0x0004, 0x55555555)
case 0 (direct write) ... <- 0x5555 re-read as id: unintended
WREG32(0x1554, 0xaaaaaaaa) <- terminator consumed as operand
buggy walk executed 2 MMIO write(s) and ran PAST the terminator ...
[with index += 4 fix]:
WREG32(0x0004, 0x55555555)
fixed walk executed 1 MMIO write(s) and stopped at the terminator
Notes
- Same-function contrast: the IGP branch of
radeon_combios_external_tmds_setup()DOES advance (radeon_combios.c:2903). - Upstream parity: Linux v5.10 radeon_combios.c:2965-2969 has the same missing advance β inherited, not a port regression.
- The out-of-table walk this desync causes is the DF-1251 family (no bounds vs bios allocation); THIS finding is the desync itself (in-bounds data executed as commands). Fixing it does not fix DF-1251 and vice versa.
DF-3855 β VERDICT
Status: inconclusive (parser desync proven deterministically by harness on a well-formed stream; kernel run hardware-gated)
Full narrative
The bug
radeon_combios_external_tmds_setup(), non-IGP branch
(sys/dev/drm/radeon/radeon_combios.c:2948-2998), walks the external-TMDS
init record stream:
index = offset + 10;
id = RBIOS16(index);
while (id != 0xffff) {
index += 2; /* skip the id word */
switch (id >> 13) {
case 0:
reg = (id & 0x1fff) * 4;
val = RBIOS32(index);
WREG32(reg, val); /* index NOT advanced past val */
break;
case 2: ... index += 4; ... index += 4; ... /* advanced */
case 4: ... index += 2; ... /* advanced */
case 5: ... index += 4; ... /* advanced */
case 6: ... index += 1; ... /* advanced */
}
id = RBIOS16(index); /* re-reads the value low half */
}
Every case advances index past its operands except case 0. After a
plain register write the low 16 bits of the just-written value are
interpreted as the next command id, the intended 0xffff terminator is
swallowed as operand bytes, and the walk continues past the end of the
record stream β executing data words (and, once past the table, whatever
bytes follow, i.e. the DF-1251 unbounded-walk family) as MMIO/PLL/I2C
commands.
The IGP branch of the same function advances correctly
(radeon_combios.c:2900-2905: val = RBIOS32(index); index += 4; WREG32(...))
β the omission is plainly an oversight, also present upstream (verified
against Linux v5.10 radeon_combios.c:2965-2969).
Caller / trigger
radeon_legacy_encoders.c:985, from the external-TMDS encoder mode-set
path β executes on every modeset on boards with an external TMDS transmitter
(sil164-class) whose VBIOS uses this table form (non-IGP).
Impact assessment (honest)
Two scenarios: 1. Malicious VBIOS: the attacker already authors the command stream, so the desync adds nothing they don't already have (DF-1251 territory). 2. Benign-but-corrupt / bit-flipped VBIOS: a single flipped bit in a shift count or a truncated table turns a valid init sequence into unintended MMIO writes β register corruption on an otherwise healthy card, GPU hang class.
That bounds it at Low: real defect, marginal security delta, hardware-gated (2 executors above), trivially fixable. The harness proves the misparse on a well-formed stream (buggy walk: 2 writes, runs past terminator; fixed walk: 1 write, stops at terminator).
Fix
fix.diff: add index += 4; after the case-0 value read (matches the IGP
branch). Apply-verified (fix_apply.log); runtime fix validation not
possible on this guest: fix_status = not_testable.
kernel_refs
- sys/dev/drm/radeon/radeon_combios.c:2950-2959 (buggy case 0)
- sys/dev/drm/radeon/radeon_combios.c:2900-2905 (IGP branch, correct)
- sys/dev/drm/radeon/radeon_legacy_encoders.c:985 (caller)
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_testableone-line fix.diff applies cleanly to pristine /usr/src (fix_apply.log); runtime fix validation impossible on this guest (no hardware)
fix.diff; fix_apply.log
Confirmed kernel references
Detail
Exploit chain
corrupt/truncated ext-TMDS init table (or crafted VBIOS) on a board with a sil164-class external TMDS transmitter -> every modeset runs the desynced walk -> value/terminator bytes executed as commands -> unintended WREG32/WREG32_PLL writes -> GPU register corruption/hang
Evidence (decisive lines)
run.log: buggy walk '[case 0 ... DOES NOT advance index] WREG32(0x0004, 0x55555555)' then unintended 'case 2 (RMW): reg 0x5554 ... WREG32(0x5554, 0xaaaa0001)' then 'terminated at index 0x0038' vs fixed walk 'WREG32(0x0004, 0x55555555) terminated at 0x0030'; fix_apply.log: hunk #1 succeeded at 2955
PoC changes
harness written from scratch (no seed); first build failed - the header comment contained nested C comment markers copied from kernel code and em-dashes (gcc: 'stray \342', '-Wcomment'); rewritten with bracket-style comment text
Verified recommended fix
Add index += 4 after the case-0 value read in the non-IGP branch, matching the IGP branch (fix.diff)
Verdict
Source-certain parser desynchronisation: the non-IGP branch of radeon_combios_external_tmds_setup() walks the ext-TMDS init stream and every command case advances index past its operands EXCEPT case 0, which reads val = RBIOS32(index) and writes WREG32(reg, val) without index += 4 (radeon_combios.c:2955-2959), unlike the IGP branch of the same function (radeon_combios.c:2900-2905). After the first plain register write, the low half of the just-written VALUE is re-read as the next command id, the 0xffff terminator is swallowed as operand bytes, and the walk continues past the record stream executing data as MMIO/PLL/I2C commands. Harness proves it on a well-formed stream: buggy walk executes 2 MMIO writes (second one an unintended case-2 RMW at reg 0x5554 built from the terminator and following data) and terminates only at +0x38 vs the fixed walk's single write terminating at the terminator (+0x30). Inherited verbatim from upstream (verified Linux v5.10:2965-2969). Security delta is bounded: a malicious-VBIOS author already controls the stream (DF-1251 family), so this matters mainly for benign-but-corrupt images turning valid init sequences into unintended MMIO writes (GPU register corruption/hang). Kernel-side run not possible on the virtio-only guest (no radeon device).
No comments yet.