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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3856 Β· 11 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + hardware reproduction notes
↓ download 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-3108 combios_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,2760 voltage gpio mask = (1 << tmp) β†’ r100_pm_misc() RMW (see DF-3852 for the same record's register injection).
  • radeon_combios.c:654-658 i2c gpiopad masks (1 << clk), (1 << data).
  • radeon_combios.c:2673-2687 thermal-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.

VERDICT.md verdict full narrative with path:line trace
↓ download raw

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:

  1. 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 by radeon_combios_asic_init() at driver init/resume from the PLL INIT / DYN CLK tables (radeon_combios.c:3329-3332, 3407-3410).

  2. voltage gpio mask β€” radeon_combios.c:2751-2752, 2759-2760: mask = (1 << tmp) with tmp = RBIOS8(...): consumed by the r100_pm_misc() RMW (see DF-3852).

  3. i2c gpiopad masks β€” radeon_combios.c:654-658: combios_setup_i2c_bus(rdev, DDC_MONID, (1 << clk), (1 << data)) β€” the masks become i2c.mask_*_mask written to GPIO registers during DDC transfers.

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

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

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.