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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3855 Β· 11 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + hardware reproduction notes
↓ download 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.
VERDICT.md verdict full narrative with path:line trace
↓ download raw

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

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

one-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
↓ fix.diffper-fix-DF-3855

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).