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

rs780_dpm: unchecked VBIOS pplib offsets/indices/strides in rs780_parse_power_table give wild OOB kernel reads past the kmalloc'd BIOS image

Summary

The PowerPlayInfo walk trusts only attacker-supplied fields: state array offset+i*entrySize (~64.8KB reach), nonClockInfoIndex*size (up to 65025B), clockInfoIndex*size (65025B). atom_parse_data_header validates nothing and the BIOS reader allocates exactly the ROM size - far indices page-fault, near indices silently expose adjacent kernel heap via printed power-state fields. Harness-verified with canary allocator: 16B-past redzone poison ingested into rps class/caps, far variant SIGSEGVs in a guard page. RS780 twin of DF-4190/DF-4146. Bounds-check-with-data_size fix (radeon.ko builds -Werror).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4492 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
harness.c β€” 48.7 KB view raw
build.sh β€” 65 B view raw
run.sh β€” 121 B view raw
build.log β€” 2.0 KB view raw
run.log β€” 7.0 KB view raw
env.txt β€” 390 B view raw
fix.diff β€” 4.5 KB view raw
radeon_fix_apply.log β€” 435 B view raw
radeon_module_build.log β€” 139.5 KB view raw
verdict.json β€” 3.9 KB view raw

DF-4492 β€” rs780_dpm.c: unchecked VBIOS pplib offsets/indices β†’ wild OOB kernel reads

What

rs780_parse_power_table() (sys/dev/drm/radeon/rs780_dpm.c:789-844) walks the VBIOS PowerPlayInfo table using only attacker-supplied fields β€” none are bounds-checked against the table or the BIOS image:

  • state array walk :812-816: bios + data_offset + usStateArrayOffset + i*ucStateEntrySize (u16 offset + up to 254Γ—255 stride β‡’ up to ~64 KB past the table start)
  • non-clock info :817-821: usNonClockInfoArrayOffset + ucNonClockStateIndex*ucNonClockSize (u8 index Γ— u8 stride β‡’ up to 65025 bytes)
  • clock info :822-827: usClockInfoArrayOffset + ucClockStateIndices[0]*ucClockInfoSize (up to 65025 bytes)

atom_parse_data_header() (atom.c:1366) validates nothing beyond a non-zero master-table pointer, and the radeon BIOS reader (radeon_bios.c:98) kzallocs exactly the ROM image size β€” so the reads run past the kmalloc'd BIOS buffer into adjacent kernel heap. The ingested bytes land in rps->class/class2/caps/vclk/dclk and ps->sclk_low/sclk_high/min_voltage and are (a) printed to the console / debugfs via rs780_dpm_print_power_state() (:944-948) and (b) programmed into PM registers.

Sumo twin of DF-4190, trinity twin of DF-4146.

Threat model

Crafted/malicious VBIOS image on an RS780/RS880 IGP (flashed ROM, replaced card, or a passed-through GPU in a VM). Far indices fault the kernel (page-fault panic = DoS); near indices silently ingest adjacent kernel-heap bytes into user-visible state (info leak class).

Reproduce (on the DragonFly audit guest, or any cc)

./build.sh          # cc -O2 -fno-delete-null-pointer-checks, baseline + -DFIX_ALL
./run.sh            # modes 4492 (redzone poison ingestion) and 4492g (guard page)

Expected baseline (see run.log):

  • mode 4492: three *** CANARY ... heap OOB access *** reports and rps[0].class = 0xa5a5 (== canary poison: OOB READ CONFIRMED), caps/vclk = 0xa5a5a5a5, exit=98
  • mode 4492g: *** SIGNAL 11 at address 0x8004c4280 *** β€” SIGSEGV exactly in the PROT_NONE guard page behind the 16 KB BIOS image replica, exit=99

Expected with harness_fixed: both modes print DRM_ERROR: ... walks past the table and exit=0.

Kernel-level reproduction is hardware-gated (no RS780-class GPU exists in the QEMU guest); the harness is a verbatim replica of the parse code β€” see VERDICT.md.

Fix

fix.diff (also in this directory): request the table size from atom_parse_data_header() and bound every walk by it; reject ucStateEntrySize < 2. Validated: applies to the guest's pristine /usr/src, radeon.ko builds under -Werror (radeon_module_build.log, MAKE_RC=0).

VERDICT.md
↓ download raw

DF-4492 β€” VERDICT

Finding: rs780_parse_power_table() trusts unvalidated VBIOS pplib offsets/indices/strides β†’ wild OOB kernel reads past the kmalloc'd BIOS image (kernleak class).

Status: REPRODUCED at logic level (verbatim replica), 2 attempts. Kernel-level trigger hardware-gated: the QEMU audit guest has no RS780/RS880-class ATI IGP, so the radeon driver never probes DPM there (family precedent DF-4190/DF-4146/DF-4124 were verified the same way).

How it was reproduced

harness.c carries the VERBATIM code of rs780_parse_power_table() (rs780_dpm.c:789-844), rs780_parse_pplib_non_clock_info() (:717-745) and rs780_parse_pplib_clock_info() (:747-787) with only MMIO stubbed; the kernel heap is emulated by a canary allocator (64-byte 0xA5 redzones both sides of every allocation). The VBIOS image is a crafted 16 KB buffer using the packed ATOM structure definitions from pptable.h (offsets faithful: ucNumStates@+5, entry sizes +6..+8, array offsets +9/+11/+13).

mode 4492 (near OOB β†’ poison ingestion, leak flavor)

Crafted table claims usNonClockInfoArrayOffset = 10336 with ucNonClockStateIndex = 250, ucNonClockSize = 24 β†’ the 24-byte _ATOM_PPLIB_NONCLOCK_INFO load lands at bios+16336, 16 bytes past the 16 KB image, inside the rear redzone. Observed (run.log):

*** CANARY: nonClockInfo[250] ... at 0x8004c4050 (len 24) is OUTSIDE every live allocation -> heap OOB access ***
*** CANARY: clockInfo[250] ... OUTSIDE every live allocation -> heap OOB access ***       (same landing via 250*20)
*** CANARY: state[7] of the ucStateEntrySize=254 walk ... OUTSIDE every live allocation *** (16318+7*254, ~1.7 KB past)
rs780_parse_power_table() = 0 num_ps=1
rps[0].class = 0xa5a5 (== canary poison: OOB READ CONFIRMED)
rps[0].caps  = 0xa5a5a5a5 (== canary poison)
rps[0].vclk  = 0xa5a5a5a5 (== canary poison)

In the kernel the same loads read whatever follows the kzalloc'd BIOS image in the kernel heap; class/caps/vclk/sclk flow to rs780_dpm_print_power_state() (:944-948, console/debugfs) β€” info leak class; far variants fault (DoS). Note the poisoned class = 0xa5a5 even carries bit0 (ATOM_PPLIB_CLASSIFICATION_BOOT), showing the OOB data also selects which state becomes boot_ps.

mode 4492g (far OOB β†’ deterministic fault)

BIOS image mapped as 16 KB writable + PROT_NONE guard page; ucClockStateIndices[0] = 66, ucClockInfoSize = 255 β†’ load at bios+17022 = 638 bytes past the image, inside the guard page. Observed:

clockInfo[66] (x255) lands at 0x8004c427e = 638 bytes PAST the image end (inside the guard page)
*** SIGNAL 11 at address 0x8004c4280 ***   exit=99

Maximum reach with u8 fields: 255Γ—255 = 65025 bytes past the table on the index walks, 254Γ—255 β‰ˆ 64.8 KB on the state walk, plus the u16 array offsets β€” comfortably past a 64 KB BIOS allocation.

Why this is real in the kernel

  • atom_parse_data_header() (sys/dev/drm/radeon/atom.c:1366-1385) returns the raw u16 master-table offset with no bounds validation of idx/table extent vs ctx->bios.
  • radeon_read_bios() (radeon_bios.c:78-110) allocates rdev->bios with exactly the ROM size; atom_context->bios points at it (atom.c:1283).
  • The rs780 parse (pass-1 sibling audits confirmed the same shape in sumo/trinity/kv/si) reads via those raw pointers with no table-extent math anywhere.

Fix validation

fix.diff requests the table size (&data_size) from atom_parse_data_header() and bounds every walk by it via rs780_pplib_walk_ok() (rejects with consistent cleanup). Applied to the guest's pristine /usr/src copy:

  • radeon_fix_apply.log: all 7 hunks applied (Plan A).
  • radeon_module_build.log: radeon.ko links under -Werror, MAKE_RC=0.
  • harness_fixed modes 4492/4492g: DRM_ERROR: ... walks past the table, exit=0, no canary violations.

Guest source reverted (md5 verified) and guest reset (vm.sh reset with-src) after the run.

Attempts

  1. First run: crafted landings fell inside the 16 KB image (intra-image overread only) β€” re-crafted offsets/strides to land in the redzone / guard page.
  2. Decisive run captured in run.log.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

harness_fixed 4492/4492g reject the crafted indices (DRM_ERROR, -EINVAL, exit 0); combined fix builds radeon.ko under -Werror (MAKE_RC=0, radeon_module_build.log)

run.log fixed sections 'DRM_ERROR: nonClockInfoIndex 250 walks past the table' / 'clockInfoIndex 66 walks past the table', exit=0; radeon_fix_apply.log hunks 1-7; radeon_module_build.log MAKE_RC=0
↓ fix.diffper-fix-DF-4492

Confirmed kernel references

Detail

Exploit chain

crafted VBIOS PowerPlayInfo (flashed ROM / replaced card / passed-through GPU) -> OOB read past BIOS allocation -> adjacent kernel-heap bytes ingested into rps->class/caps/vclk/sclk -> printed via rs780_dpm_print_power_state to console/debugfs (info leak); far indices -> kernel page fault (DoS); leaked BOOT-bit in class additionally steers boot_ps selection

Evidence (decisive lines)

run.log '===== mode 4492 =====': three CANARY heap-OOB reports + 'rps[0].class = 0xa5a5 (== canary poison: OOB READ CONFIRMED)', exit=98; '===== mode 4492g =====': 'SIGNAL 11 at address 0x8004c4280' inside the guard page, exit=99; fixed sections print 'DRM_ERROR: ... walks past the table', exit=0; radeon_module_build.log MAKE_RC=0 under -Werror

PoC changes

wrote harness from scratch (no seed): crafted bios image + verbatim parse replica; -fno-delete-null-pointer-checks used so NULL/wild derefs trap like the kernel instead of being optimized away

Verified recommended fix

request the table size from atom_parse_data_header and bounds-check every pplib walk (state array extent, nonClockInfoIndexucNonClockSize+entry, clockInfoIndexucClockInfoSize+entry) with consistent cleanup, as in fix.diff

Verdict

Reproduced at logic level with the verbatim replica (harness modes 4492/4492g): rs780_parse_power_table walks the VBIOS PowerPlayInfo using only attacker-supplied usStateArrayOffset+iucStateEntrySize (:812-816), ucNonClockStateIndexucNonClockSize (:817-821) and ucClockStateIndices[0]*ucClockInfoSize (:822-827) with no bounds checks anywhere (atom_parse_data_header does none); nonClockInfo[250] with a crafted u16 array offset lands 16 bytes past the 16 KB BIOS replica into the canary redzone and its poison is ingested verbatim into rps->class/caps/vclk (0xa5a5/0xa5a5a5a5) which print to console/debugfs via rs780_dpm_print_power_state (:944-948); the far variant (clockInfoIndex 66 x ucClockInfoSize 255) SIGSEGVs exactly in a guard page 638 bytes past the image (kernel: page-fault panic). Max reach ~65 KB past the kmalloc'd BIOS image per index walk, ~130 KB on the state walk. RS780 twin of DF-4190 (sumo) / DF-4146 (trinity). Kernel-level repro hardware-gated (no RS780 IGP in the QEMU guest). Fix proven: harness_fixed rejects the crafted indices; fix.diff builds radeon.ko under -Werror (MAKE_RC=0).