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