Unchecked BIOS shift counts >= 32 (C99 6.5.7p3 UB) used to build PLL/GPIO write masks at four parser sites
Summary
Four sites use a raw BIOS byte as a 32-bit shift count: the PLL-table case-1 shift (up to 2040) before WREG32_PLL (executed at init/resume from the PLL INIT/DYN CLK tables); voltage-GPIO masks feeding the r100_pm_misc RMW (DF-3852's record); i2c gpiopad masks; thermal-controller masks. Counts >= 32 are UB. HARNESS shows the concrete misbehaviour on the audit toolchain: counts silently taken mod 32 - shift byte 4 yields the wrong byte lane; 1<<32 gives bit 0. Wrong PLL/GPIO programming with no trap and no runtime warning; compiler-dependent. Low standalone; force-multiplier for the MMIO-injection findings. Bound-every-shift fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3856 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | demonstrates mod-32 shift-count masking for pll case-1 masks and (1 << n) gpio masks on DragonFly cc 8.3 | 3.1 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 | 636 B | view raw |
| fix.diff | suggested-fix | unchecked BIOS shift counts >= 32 (C99 UB) build PLL/GPIO write masks - git/patch-applyable vs sys/ | 1.3 KB | view raw |
| fix_apply.log | apply-log | patch --dry-run -p1 -d /usr/src on the guest: clean apply | 367 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.7 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.6 KB | view raw |
DF-3856 β BIOS-controlled shift counts β₯ 32 (undefined behavior) build GPIO/PLL write masks
Verdict (one line)
CONFIRMED REAL by source trace; harness shows the concrete misbehaviour on
the audit toolchain (count silently taken mod 32 β wrong masks). Kernel-side
effect hardware-gated β inconclusive.
Build / run
./build.sh # cc -O2 -Wall -o harness harness.c ./run.sh # prints correct vs UB shift results
Expected output
[pll case1] shift_byte=3 -> shift=24: and_mask=0xffffff00 (correct byte lane) [pll case1] shift_byte=4 -> shift=32: and_mask=0x0000ffff (UB! ... mod 32 -> count 0) [gpio mask] tmp=9 -> 1<<tmp = 0x00000200 (intended single bit) [gpio mask] tmp=32 -> 1<<tmp = 0x00000001 (UB! mod-32 -> bit 0; ...) [gpio mask] tmp=200 -> 1<<tmp = 0x00000100 (UB! mod-32 -> bit 8)
(Exact UB output varies by compiler/arch β that is the point; C99 6.5.7p3.)
Sites (all take an unchecked BIOS byte as shift count)
radeon_combios.c:3103-3108combios_parse_pll_table()case 1:shift = RBIOS8(offset) * 8(0..2040) βx << shift,~(0xff << shift)βWREG32_PLL(addr, tmp)with a garbage-modified value.radeon_combios.c:2752,2760voltage gpiomask = (1 << tmp)β r100_pm_misc() RMW (see DF-3852 for the same record's register injection).radeon_combios.c:654-658i2c gpiopad masks(1 << clk),(1 << data).radeon_combios.c:2673-2687thermal-controller(1 << clk_bit),(1 << data_bit).
Impact: silently-wrong hardware programming (PLL read-modify-writes and GPIO masks built from garbage); no trap, no runtime warning. Low severity on its own; force-multiplier for the MMIO-injection findings above.
DF-3856 β VERDICT
Status: inconclusive (UB proven by harness on audit toolchain; kernel write sinks hardware-gated, no radeon GPU on guest)
Full narrative
The bug family
Four parser sites in radeon_combios.c use a raw BIOS byte as a shift
count for 32-bit values. C99 6.5.7p3: shifting a 32-bit value by β₯ 32 (or by
a negative count) is undefined behavior. Nothing bounds these bytes:
-
combios_parse_pll_table()case 1 βsys/dev/drm/radeon/radeon_combios.c:3102-3113:c shift = RBIOS8(offset) * 8; /* 0..2040 */ offset++; and_mask = RBIOS8(offset) << shift; /* UB for byte >= 4 */ and_mask |= ~(0xff << shift); /* UB */ or_mask = RBIOS8(offset) << shift; /* UB */ tmp = RREG32_PLL(addr); tmp &= and_mask; tmp |= or_mask; WREG32_PLL(addr, tmp); /* PLL register rewritten */Executed byradeon_combios_asic_init()at driver init/resume from the PLL INIT / DYN CLK tables (radeon_combios.c:3329-3332, 3407-3410). -
voltage gpio mask β
radeon_combios.c:2751-2752, 2759-2760:mask = (1 << tmp)withtmp = RBIOS8(...): consumed by ther100_pm_misc()RMW (see DF-3852). -
i2c gpiopad masks β
radeon_combios.c:654-658:combios_setup_i2c_bus(rdev, DDC_MONID, (1 << clk), (1 << data))β the masks becomei2c.mask_*_maskwritten to GPIO registers during DDC transfers. -
thermal-controller gpio bits β
radeon_combios.c:2669-2687:combios_setup_i2c_bus(rdev, gpio, 1 << clk_bit, 1 << data_bit).
Observed behaviour (audit toolchain: DragonFly cc 8.3, x86-64)
harness.c (volatile operands, so no constant folding) shows gcc silently
taking the count mod 32: shift=32 behaves as shift=0, 1<<200 yields
0x100. The PLL read-modify-write therefore applies an
and_mask/or_mask pair built for the wrong byte lane β e.g. the sample
shows the intended ~0xff000000-style mask coming out as 0x0000ffff β
and writes it to a PLL register. No trap, no warning; other compilers are
entitled to different (or no) results β the guarantee is simply gone.
Impact (honest)
Wrong PLL/GPIO programming from a crafted (or corrupted) VBIOS: GPU clock misconfiguration, failed posting, hardware hangs; a force-multiplier for DF-3852 (mask) rather than an independent memory-safety bug. Low severity.
Fix
fix.diff: pll-table case 1 validates the shift byte (β€3) and skips the
record otherwise (consuming its 2 operand bytes to stay aligned); i2c table
and thermal-controller sites only accept bit numbers < 32. (The voltage-mask
site is covered by DF-3852's fix.diff; the two diffs are independent.)
Apply-verified (fix_apply.log); runtime fix validation not possible:
fix_status = not_testable.
kernel_refs
- sys/dev/drm/radeon/radeon_combios.c:3102-3113 (pll table case 1)
- sys/dev/drm/radeon/radeon_combios.c:2751-2760 (voltage masks)
- sys/dev/drm/radeon/radeon_combios.c:650-658 (i2c table masks)
- sys/dev/drm/radeon/radeon_combios.c:2669-2687 (thermal masks)
- sys/dev/drm/radeon/radeon_combios.c:3329-3340, 3407-3410 (executors)
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.diff applies cleanly to pristine /usr/src (fix_apply.log, hunks at 654/2686/3103); runtime fix validation impossible on this guest (no hardware)
fix.diff; fix_apply.log
Confirmed kernel references
Detail
Exploit chain
crafted or bit-flipped VBIOS -> shift byte >= 4 (pll table) or >= 32 (mask sites) -> UB shifts silently mis-build the mask pair -> WREG32_PLL / r100_pm_misc / GPIO set_masks write wrongly-masked values -> GPU clock/GPIO misprogramming (hang / failed posting)
Evidence (decisive lines)
run.log decisive lines: '[pll case1] shift_byte=4 -> shift=32: and_mask=0xffffff0f (UB! count taken mod 32 -> lane 0 ... expected 0x0fffffff)', '[gpio mask] tmp=32 -> 1<<tmp = 0x00000001 (UB! ...)', '[gpio mask] tmp=200 -> 1<<tmp = 0x00000100 (UB! ...)'; fix_apply.log: 3 hunks apply to pristine /usr/src (654, 2686, 3103)
PoC changes
harness written from scratch (no seed); first build failed (nested C comment markers + em-dashes in header comment - gcc 'stray' errors); rewritten; demo bytes changed from 0xff to 0x0f so the wrong byte-lane is visible in and_mask
Verified recommended fix
Bound every BIOS-sourced shift count: pll-table case 1 accepts shift bytes <= 3 (skip record otherwise, staying aligned); mask sites accept bit numbers < 32 (fix.diff)
Verdict
Source-certain undefined behavior: four combios parser sites use an unchecked BIOS byte as a 32-bit shift count. combios_parse_pll_table() case 1 computes shift = RBIOS8(offset)*8 (0..2040) and shifts with it (radeon_combios.c:3103-3108), feeding WREG32_PLL a value masked with a garbage-derived and/or mask pair at driver init/resume from the PLL INIT / DYN CLK tables (radeon_combios.c:3329-3340, 3407-3410). Voltage-GPIO masks (1 << tmp) at :2752/:2760 (consumed by the r100_pm_misc RMW - see DF-3852), i2c gpiopad masks (1 << clk)/(1 << data) at :654-658, and thermal-controller masks (1 << clk_bit) at :2687 share the defect. C99 6.5.7p3 makes counts >= 32 UB. Harness demonstrates the concrete misbehaviour on the audit toolchain (DragonFly cc 8.3, x86-64): counts silently taken mod 32 - pll case 1 with shift byte 4 yields and_mask 0xffffff0f (byte lane 0) where lane 3 (0x0fffffff) was programmed; 1<<32 gives bit 0; 1<<200 gives bit 8 - wrong PLL/GPIO programming with no trap and no runtime warning. Kernel-side write sinks hardware-gated (no radeon device on guest). Low severity standalone; force-multiplier for the MMIO-injection findings.
No comments yet.