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)
PoC verification
Evidence pack
findings/poc/DF-4146 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 verbatimatom_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.
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_indexcomparisons 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 fromr600_parse_extended_power_tablewithout aucNumEntriescheck β same family, fixable by the same bound; noted for the orchestrator, no new finding filed here. pp_sizeitself 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 beyondpp_sizeis 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
fixedA/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
Confirmed kernel references
- sys/dev/drm/radeon/trinity_dpm.c:1756
- sys/dev/drm/radeon/trinity_dpm.c:1767
- sys/dev/drm/radeon/trinity_dpm.c:1771
- sys/dev/drm/radeon/trinity_dpm.c:1777
- sys/dev/drm/radeon/trinity_dpm.c:1787
- sys/dev/drm/radeon/trinity_dpm.c:1804
- sys/dev/drm/radeon/atom.c:1366
- sys/dev/drm/radeon/radeon_atombios.c:2592
- sys/dev/drm/radeon/radeon_atombios.c:2693
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 i
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.
No comments yet.