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

trinity_parse_power_table trusts unvalidated VBIOS u16 table offsets, per-state stride walk, and indexes rdev->pm.power_state[] by StateArray ucNumEntries while it is allocated for pplib ucNumStates β€” heap OOB reads of the BIOS image and the power_state array

Summary

Three independent trust violations: (1) state/clock/non-clock array base pointers are bios+data_offset+unvalidated u16 offsets - atom_parse_data_header performs no extent validation; (2) the state walk advances by 2+ucNumDPMLevels per entry and reads idx[j] for every j with no cumulative bound - up to 255 bytes per entry past the states[] array and ultimately past the BIOS allocation; (3) the loop bound is state_array->ucNumEntries while power_state[] was allocated for pplib ucNumStates (two different VBIOS-controlled counts) - ucNumStates=1 with ucNumEntries=255 reads pointers up to ~25KB past a 1-entry allocation. Read values flow into checked indices and NULL-tests (limiting exfiltration) but a NULL OOB word aborts dpm init whose swallowed return leads into the DF-4143 panic chain. Validate-offsets/counts fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4146 Β· 19 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 5.8 KB ↓ raw
verdict.json β€” 6.5 KB view raw
manifest.json β€” 1.2 KB view raw
fix.diff β€” 1.6 KB view raw
build.log β€” 8.6 KB view raw
run.log β€” 12.5 KB view raw
run.2.log β€” 12.5 KB view raw
env.txt β€” 1.0 KB view raw
rig/gen_rig.sh β€” 15.5 KB view raw
rig/check_verbatim.sh β€” 3.0 KB view raw
rig/Makefile β€” 690 B ↓ download
rig/build.sh β€” 502 B view raw
rig/run.sh β€” 834 B view raw
rig/trinity_slice_verbatim.c β€” 54.8 KB view raw
rig/trinity_rig.c β€” 35.2 KB view raw
rig/rig_shims.h β€” 7.8 KB view raw
rig/radeon_structs_verbatim.h β€” 7.1 KB view raw
rig/atom_defs_verbatim.h β€” 20.9 KB view raw

DF-4146 β€” trinity_parse_power_table trusts unvalidated VBIOS u16 array offsets, an unbounded per-state stride walk, and state_array->ucNumEntries against a power_state[] allocation sized by a different VBIOS count β€” heap OOB reads of the BIOS image and the legacy power_state array

  • File: sys/dev/drm/radeon/trinity_dpm.c
  • Class: kernel out-of-bounds reads (CWE-125) driven by crafted VBIOS
  • Verified via the verbatim-code rig (rig/, 68 blocks byte-identical to the guest's /usr/src/sys), including the verbatim atom_parse_data_header (atom.c:1366-1385) β€” which demonstrably performs no extent validation.

What the rig demonstrates (baseline build = production code)

mode claim result
4146-offset (1) unvalidated u16 array offsets usStateArrayOffset=0x1000 with a 0x1000-byte image β†’ state_array = bios+0x1500 β†’ the :1767 read of ucNumEntries faults past the BIOS image guard page (FAULT addr=bios+0x1500)
4146-stride (2) unbounded stride walk a state with ucNumDPMLevels=255 placed at the image end β†’ the :1788 idx[j] reads walk past the image and fault at exactly bios+0x2000 (the first byte past the image)
4146-canary (2) data flow the same 255-byte walk with a readable tail: levels are parsed from bytes beyond the state entry β€” idx[1..3] (image bytes past the entry) select clock entries 5/6/7 (sclk 5777/6777/7777), and idx[253] reads the non-clock array's own header byte (ucNumEntries=3 at state+254) as a clock index (sclk 3777) β€” the walk crosses into the adjacent table
4146-powerguard (3) ucNumEntries vs ucNumStates sizing frev-4/5-shaped image: legacy pre-parse (radeon_atombios.c:2592) sized power_state[] for ucNumStates=1 (48-byte entries); the trinity loop runs to ucNumEntries=255 β†’ power_state[1].clock_info read at allocation_end+8 faults in the guard page (FAULT addr=0x...c008 = arr+56); the claim's read reach is 255Γ—48 β‰ˆ 12.2 KB past a 1-entry allocation (measured sizeof = 48)
4146-nullabort (3) NULL-word abort β†’ swallowed-error chain ucNumEntries=4 with only entry 0 filled β†’ the :1777 OOB/NULL power_state[1].clock_info read aborts parse with -EINVAL β†’ the rig simulates radeon_pm.c:1448 discarding it β†’ enable β†’ the DF-4143 NULL-boot_ps fault at 0x28

Fixed build (this finding's fix.diff, A/B on identical images)

fix.diff validates all three array offsets against the PowerPlayInfo table size (captured via a second verbatim atom_parse_data_header call), bounds the per-state walk (state + 2 + ucNumDPMLevels within the table) with full unwind, and stops the loop when i >= rdev->pm.num_power_states. Fixed run: 4146-offset/stride/powerguard/nullabort all rejected cleanly (init rc=-22, enable guarded, no fault); 4146-canary (walk within table bounds by construction) unchanged; good unchanged.

Build / Run (on the DragonFly guest)

cd rig && sh ./build.sh /usr/src DF-4146
cd rig && sh ./run.sh

Verdict

Reproduced (verbatim-rig): all three trust violations produce demonstrated OOB reads at exact, predicted addresses, plus the canary data-flow proof and the swallowed-abort β†’ DF-4143 panic chain. Note (measurement): with the measured sizeof(struct radeon_power_state) == 48, claim (3)'s reach is ~12.2 KB past a 1-entry allocation (the finding estimated ~25 KB). See VERDICT.md, verdict.json.

VERDICT.md
↓ download raw

DF-4146 β€” VERDICT

REPRODUCED via the verbatim-code rig (68 blocks identical to the guest's /usr/src/sys; the driven chain includes the verbatim atom_parse_data_header (atom.c:1366-1385) and the verbatim master-table index macro, so the crafted images go through the same dispatch the kernel uses).

Claim (1) β€” unvalidated u16 array base offsets: CONFIRMED

atom_parse_data_header resolves data_start = idx (a raw u16 from the master data table) and performs no extent check (verbatim block, atom.c:1366-1385). trinity_parse_power_table then computes bios + data_offset + usStateArrayOffset (trinity_dpm.c:1756-1758) with the raw u16 and dereferences state_array->ucNumEntries at :1767.

Rig (4146-offset): image of 0x1000 bytes (guard page after); usStateArrayOffset = 0x1000, PowerPlayInfo at bios+0x500 β†’ state_array = bios+0x1500 β†’ RESULT: FAULT addr=0x800479500 (bios base 0x800478000 + 0x1500) β€” a read past the BIOS image. The same trust applies to usClockInfoArrayOffset / usNonClockInfoArrayOffset (identical code shape); a u16 allows reads up to 64 KiB past the image start.

Claim (2) β€” unbounded per-state stride walk: CONFIRMED (memory + data flow)

The walk advances power_state_offset += 2 + ucNumDPMLevels (:1804) and the j-loop reads idx[j] for every j < ucNumDPMLevels (:1787-1788) with no cumulative bound against the states array or the image.

Rig (4146-stride): one state with ucNumDPMLevels=255 placed so clockInfoIndex[0] is the last image byte β†’ RESULT: FAULT addr=bios+0x2000 β€” the very first byte past the image, at j=1.

Rig (4146-canary): with a readable tail the OOB bytes drive parsing: state entry idx[0]=0 β†’ level0 sclk 777 (clock entry 0, in-entry byte); idx[1..3] = 5,6,7 β€” image bytes past the state entry β€” select clock entries 5/6/7 (sclk 5777/6777/7777); idx[253] (state+254) reads the non-clock array's ucNumEntries byte (3) as a clock index β†’ level4 sclk 3777 (clock entry 3). num_levels=5 from a 1-entry array: the walk consumed 254 bytes beyond it, crossing into the adjacent table inside the image, exactly the "no cumulative bound" claim; the guard variant shows the same walk escaping the image entirely.

Claim (3) β€” ucNumEntries vs ucNumStates allocation mismatch: CONFIRMED

The legacy pre-parse sizes rdev->pm.power_state[] by: - pplib.ucNumStates for frev 4/5 tables (radeon_atombios.c:2592), - ucNumOfPowerModeEntries for frev 1-3 (:2120), - state_array->ucNumEntries only for frev 6 (:2693), while the DPM loop indexes power_state[i] for i < ucNumEntries (trinity_dpm.c:1771/1777) β€” two independent VBIOS-controlled counts.

Rig (4146-powerguard): frev-4 image, ucNumStates=1 β†’ mock power_state allocation of 48 bytes (measured sizeof(struct radeon_power_state)=48) placed at a page end with [0].clock_info set; ucNumEntries=255 β†’ power_state[1].clock_info read at arr+56 faults in the guard page (RESULT: FAULT addr=0x80047c008 with arr=0x80047bfa0) β€” 8 bytes past the 1-entry allocation, and the loop would keep reading up to 254*48+8 β‰ˆ 12.2 KB past it. (The finding's "~25 KB" estimate assumed a larger entry; measured reach is ~12.2 KB β€” same class, same mechanism.)

Rig (4146-nullabort): ucNumEntries=4 with only [0] filled β†’ the :1777 read of power_state[1].clock_info yields NULL β†’ parse aborts -EINVAL β†’ radeon_pm.c:1448 discards it (simulated, cited) β†’ enable β†’ the DF-4143 NULL-boot_ps fault at 0x28 β€” the swallowed-abort panic chain, end to end.

Fix validation (A/B, identical images)

fix.diff: capture pp_size via a second verbatim atom_parse_data_header call; validate the three array offsets + minimal headers against pp_size; bound the walk (state + 2 + ucNumDPMLevels within the table, full unwind on violation); stop at i >= rdev->pm.num_power_states. Fixed build: 4146-offset, 4146-stride, 4146-powerguard rejected at parse (init rc=-22, no fault); 4146-nullabort rejected + enable guarded; 4146-canary (walk within table bounds by construction) and good unchanged β€” the fix does not over-reject legal tables whose arrays lie within the declared table size. Checker proves the APPLY_DF4146_FIX lines are exactly the fix.diff additions (build.log, 21 lines).

Honest limitations / observations

  • No AMD GPU on the audit guest: in-kernel execution impossible here; the demonstrated reads are the exact production code on the exact production struct layouts, driven by images equivalent to what the ROM parser sees.
  • Exfiltration ceiling stays low, as the finding states: OOB-derived bytes feed clock_array_index comparisons and NULL tests. The demonstrated impact is the parse-abort β†’ swallowed-error β†’ DF-4143 panic chain plus OOB reads of up to ~12 KB of adjacent kernel heap (information used only as indices).
  • Incidental observation (not part of this finding): the VCE loop at :1809-1818 indexes clockInfo[clock_array_index * ucEntrySize] with an index imported from r600_parse_extended_power_table without a ucNumEntries check β€” same family, fixable by the same bound; noted for the orchestrator, no new finding filed here.
  • pp_size itself is VBIOS-controlled (it bounds within the declared table, not the mapped image). It closes the demonstrated image-escape modes because a table declaring offsets beyond pp_size is rejected; a fully robust fix would also carry the BIOS image length through atom_context (cross-file change, out of scope here).

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

A/B validated in the verbatim rig on identical crafted images: baseline faults at bios+0x1500 (offset), bios+0x2000 (stride), power_state end+8 (count mismatch) and aborts into the DF-4143 chain (NULL word); fixed build rejects all four at parse (init rc=-22) with clean unwinding and the enable guard -- no fault; canary and good-table modes unchanged (no over-rejection of in-table layouts). Kernel-rebuild validation not performed (AMD-only path, guest has no AMD GPU); validated in the rig per the established protocol.

run.log FIXED sections (RESULT: OK (fixed) parse rejected the crafted table ... / count-bound reject + enable guard, no fault); build.log ('DF-4146 fix lines == fix.diff additions (21 lines)'); fix.diff
↓ fix.diffper-fix-DF-4146

Confirmed kernel references

Detail

Exploit chain

Crafted VBIOS on a Trinity/Richland APU -> radeon_atombios_get_power_modes legacy pre-parse sizes power_state[] by pplib.ucNumStates (frev 4/5, radeon_atombios.c:2592) or ucNumOfPowerModeEntries (frev 1-3, :2120) -> trinity_dpm_init -> trinity_parse_power_table dereferences bios+data_offset+unvalidated u16 offsets (:1756-:1767), walks states with an unbounded 2+ucNumDPMLevels stride reading idx[j] past the states array and the image (:1787-:1804), and indexes power_state[i] for iucNumEntries (:1771/:1777) -> OOB reads of the BIOS image and up to ~12.2 KB past the legacy power_state allocation; a NULL/OOB word aborts parse with -EINVAL -> radeon_pm.c:1448 discards it -> enable -> DF-4143 NULL-boot_ps page fault (demonstrated chain). No unprivileged-userland trigger: attacker data is the GPU ROM.

Evidence (decisive lines)

run.log (4146-offset: FAULT at bios+0x1500; 4146-stride: FAULT at bios+0x2000 exactly; 4146-canary: 5 levels parsed from OOB bytes incl. the non-clock array header byte; 4146-powerguard: FAULT at power_state allocation end+8; 4146-nullabort: -EINVAL abort -> swallowed -> enable fault 0x28; FIXED: all rejected rc=-22, canary/good unchanged), run.2.log, build.log (68x identical + 'DF-4146 fix lines == fix.diff additions (21 lines)'), rig/, VERDICT.md, env.txt

PoC changes

Seed pack had no runnable PoC. Built the verbatim rig from scratch (shared with DF-4143/4144/4145). Measurement corrections vs the seed: sizeof(struct radeon_power_state)=48 on x86_64, so claim (3)'s read reach is ~12.2 KB (not ~25 KB) past a 1-entry allocation; also demonstrated that the stride walk crosses into the ADJACENT non-clock array within the image (idx[253] reads its header byte), a stronger data-flow proof than the seed's. Incidental observation recorded for the orchestrator (no new finding filed): the VCE loop :1809-:1818 indexes clockInfo[] without a ucNumEntries check.

Verified recommended fix

Validate usStateArrayOffset/usClockInfoArrayOffset/usNonClockInfoArrayOffset (+minimal headers) against the PowerPlayInfo table size, bound the per-state walk (state+2+ucNumDPMLevels within the table) with unwind, and stop the loop at rdev->pm.num_power_states -- fix.diff, A/B validated.

Verdict

REPRODUCED via the verbatim-code rig (68 blocks byte-identical to the guest's /usr/src/sys, including the verbatim atom_parse_data_header and master-table dispatch). All three trust violations demonstrated at exact predicted addresses. (1) Unvalidated u16 array offsets: usStateArrayOffset=0x1000 on a 0x1000-byte image -> state_array=bios+0x1500 -> the :1767 ucNumEntries read faults past the BIOS image guard page; atom_parse_data_header demonstrably performs no extent validation. (2) Unbounded stride walk: ucNumDPMLevels=255 state at the image end -> :1788 idx[j] faults at exactly bios+image_end (j=1); with a readable tail the OOB bytes DRIVE PARSING -- idx[1..3] read past the state entry select clock entries 5/6/7 (sclk 5777/6777/7777) and idx[253] (state+254) reads the NON-CLOCK ARRAY's own ucNumEntries byte as a clock index (sclk 3777), num_levels=5 from a 1-entry state array. (3) ucNumEntries vs ucNumStates sizing mismatch: frev-4 image sizes power_state[] for ucNumStates=1 (radeon_atombios.c:2592, 48-byte entries measured) while the trinity loop runs to ucNumEntries=255 -> power_state[1].clock_info read at allocation_end+8 faults in the guard page; measured worst-case read reach 254*48+8 ~ 12.2 KB past a 1-entry allocation (finding estimated ~25 KB -- same class). The NULL-word variant (:1777 NULL power_state[1].clock_info) aborts parse with -EINVAL whose simulated swallow per radeon_pm.c:1448-1453 leads into the demonstrated DF-4143 NULL-boot_ps fault at 0x28. Gating: no AMD GPU on the audit guest (env.txt); kernel-context consequence is the panic chain above plus OOB heap reads used only as indices (exfiltration stays limited, as the finding itself states). fix.diff (offset validation against the table size captured by a second verbatim header parse + walk bound with unwind + i>=num_power_states loop bound) A/B-validated: every 4146 fault mode rejected cleanly at parse, canary/good unchanged.