ucNumDPMLevels==0 pplib state leaves num_levels==0: levels[-1] window writes 0 into the MSB of pi->requested_rps.ps_priv (non-canonical pointer deref -> panic) and feeds heap-garbage sclk into watermark programming
Summary
trinity_parse_pplib_clock_info sets num_levels=index+1 only when invoked; a state with ucNumDPMLevels==0 is parsed with num_levels 0 (kzalloc-zeroed ps_priv). When such a state is selected, apply_state_adjust_rules:1619 executes levels[num_levels-1].allow_gnb_slow = 0: levels[-1] spans ps-8..ps+3, so the write lands at ps-1 - the most-significant byte of requested_rps.ps_priv (the last member of struct radeon_ps). ps_priv holds a kernel heap pointer; zeroing its top byte yields a non-canonical address the very next dereference faults on. Additionally levels[num_levels-1].sclk reads return intra-object garbage that get_sclk feeds *10 into evergreen watermark MMIO. Reject-zero-level-states + num_levels guards fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4145 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| verdict.json | β | 5.8 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| fix.diff | β | 776 B | 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-4145 β ucNumDPMLevels==0 pplib state leaves num_levels==0: the :1619 levels[num_levels-1] store is a WILD byte-zero write at ps + 0xBFFFFFFFF (mechanism corrected by the rig), plus a same-offset OOB read
- File:
sys/dev/drm/radeon/trinity_dpm.c - Class: kernel out-of-bounds write (CWE-787) / OOB read (CWE-125)
- Verified via the verbatim-code rig (
rig/, 68 blocks byte-identical to the guest's/usr/src/sys). - IMPORTANT mechanism correction (rig-proven, see VERDICT.md): the
finding claims the write lands at
ps-1, the MSB ofpi->requested_rps.ps_priv. That is arithmetically impossible:ps->num_levelsisu32, sops->num_levels - 1is0xFFFFFFFF, and the array subscript scales bysizeof(struct trinity_pl) == 12(verbatim layout mode), putting the store atps + 4 + 0xFFFFFFFF*12 + 7 = ps + 0xBFFFFFFFFβ a wild 1-byte-zero store ~48 GiB past the object (gdb computes exactly the observed fault address:0x140047b1af == ps + 0xBFFFFFFFF). The trigger, the executing line and the OOB-write class of the finding are confirmed; the precise target and the "non-canonical ps_priv" consequence are corrected.trinity_dpm_get_sclkreadslevels[num_levels-1].sclkat the same wild offset (ps + 0xBFFFFFFF8) β an OOB read feeding the watermark path, not an intra-object garbage read.
What the rig demonstrates (baseline build = production code)
| mode | result |
|---|---|
4145-guard |
verbatim trinity_apply_state_adjust_rules on a num_levels==0 state β the :1619 store faults at exactly ps + 0xBFFFFFFFF (unmapped) β RESULT: FAULT addr=0x14004b4fff where ps=0x8004be000 |
4145-fullflow |
full production flow (init on the crafted image, state selection, verbatim trinity_dpm_pre_set_power_state): the :1603-:1618 block provably executes (Dpm0PgNbPsLo 1β2, DpmXNbPsLo 2β2), then the :1619 store lands in a canary page mapped at exactly ps+0xBFFFFFFFF: exactly 1 byte of 12288 changed β the byte at the computed wild offset written to 0x00; trinity_dpm_get_sclk then reads the same wild region (returns the canary bytes) |
Fixed build (this finding's fix.diff, A/B on the identical image)
The parser rejects states with zero usable DPM levels (k == 0 after the
idx loop) with a full unwind, and apply_state_adjust_rules guards the
write with if (ps->num_levels > 0). Fixed run: 4145-fullflow init
returns -EINVAL (state never parsed), 4145-guard never reaches the
write β no fault; good table unchanged.
Build / Run (on the DragonFly guest)
cd rig && sh ./build.sh /usr/src DF-4145 cd rig && sh ./run.sh
Verdict
Reproduced with corrected mechanism (verbatim-rig): the trigger and the
OOB store at trinity_dpm.c:1619 are real and attacker-controlled in
reachability; the write is a wild constant-zero byte at object+~48 GiB β in
kernel context an unmapped-address page fault (panic) or, where kernel VA
happens to be mapped there, a 1-byte zeroing of unrelated memory. The
finding's ps-1/pointer-MSB surgical-write claim is refuted by C unsigned
arithmetic and rig observation. See VERDICT.md, verdict.json.
DF-4145 β VERDICT
REPRODUCED, with the finding's mechanism corrected by the rig. Verbatim
fidelity as for DF-4143/4144 (68 blocks identical to the guest's
/usr/src/sys).
Confirmed part of the claim
- A pplib state with
ucNumDPMLevels == 0is parsed withps->num_levelsleft at its kzalloc zero:trinity_parse_pplib_clock_infosetsnum_levels = index + 1only when invoked (trinity_dpm.c:1726), and the rig's verbatim parse of the crafted two-state image (state0 BOOT with one level, state1 SDSTATE withucNumDPMLevels=0) leavesstate1 num_levels=0(run.log, mode4145-fullflow). - When that state is selected, the verbatim
trinity_dpm_pre_set_power_stateβtrinity_apply_state_adjust_rulespath executes the:1603-:1618block (proved:Dpm0PgNbPsLo1β2,DpmXNbPsLo2β2 β the:1604/:1606defaults overwritten at:1615/:1617, which immediately precede the store) and then executesps->levels[ps->num_levels - 1].allow_gnb_slow = 0;(:1619) β an out-of-bounds store. Reachability requirespi->sys_info.nb_dpm_enable(verbatim-parsed from the crafted IntegratedSystemInfo v1_7,ucNBDPMEnable=1) and an SDSTATE/HDSTATE/UI_BATTERY classification β all VBIOS-controlled, all set in the crafted image.
Corrected mechanism (the finding's ps-1 claim is wrong)
ps->num_levels is u32. ps->num_levels - 1 therefore evaluates to
0xFFFFFFFF (not β1), and C array subscripting scales by
sizeof(struct trinity_pl):
store address = ps + offsetof(trinity_ps, levels) (= 4)
+ 0xFFFFFFFF * 12 (= 0xBFFFFFFF4)
+ offsetof(trinity_pl, allow_gnb_slow)(= 7)
= ps + 0xBFFFFFFFF (~48 GiB past the object)
Rig proof, two independent ways:
4145-guard: the verbatim store faults; the reported address is0x14004bdfffwithps = 0x8004be000, i.e. exactlyps + 0xBFFFFFFFF(checked arithmetically in the mode output). gdb at the faulting store computes&ps->levels[ps->num_levels - 1].allow_gnb_slow == 0x140047b1af == ps + 0xBFFFFFFFFfor the fullflowps.4145-fullflow: a canary page mapped at exactlyps + 0xBFFFFFFFF(MAP_FIXED) receives the store:canary byte at target: 0xaa -> 0x00 (WRITTEN 0x00 BY THE VERBATIM STORE),canary region bytes changed: 1 (of 12288)β a single byte, at the computed wild offset, value 0.
The read side (trinity_dpm_get_sclk, :2102,
levels[requested_state->num_levels - 1].sclk) addresses
ps + 0xBFFFFFFF8 β the same wild region; the rig's
get_sclk returns the canary bytes placed there (0xaaaaaaaa),
demonstrating the read is satisfied by whatever memory (if any) exists
~48 GiB past the object β in-kernel an OOB read feeding the watermark
calculations, or a fault.
In-kernel consequence (corrected)
pi->requested_ps is embedded in the kzalloc'd struct trinity_power_info
(kernel heap). The store at heap_object + 0xBFFFFFFFF:
- most likely lands in unmapped KVA β page fault β panic at the first
power-state transition after the crafted state is selected (DoS,
reboot-loop);
- if the offset happens to fall inside mapped kernel VA (DMap is huge),
silently zeroes one byte of unrelated kernel memory ~48 GiB away.
There is no pointer-surgery capability: the offset is a compile-time
constant derived from the type layout, and the written value is always 0x00.
The finding's claimed "zero the MSB of ps_priv β non-canonical pointer β
next deref faults" chain does not occur (the store never touches ps-1).
Fix validation (A/B, identical image)
fix.diff: (1) parse-side rejection β after the idx loop, k == 0 states
are refused with a full unwind (power state %d has no dpm levels);
(2) defense-in-depth β if (ps->num_levels > 0) guards the :1619 store.
Fixed run: 4145-fullflow β init rc=-22 (RIG: power state 1 has no dpm
levels), apply never runs; 4145-guard β the guarded store cannot fault;
good unchanged. The checker proves the APPLY_DF4145_FIX lines are
exactly the fix.diff additions (build.log, 11 lines).
Honest limitations
- No AMD GPU on the audit guest: in-kernel execution impossible here; the 48 GiB offset makes silent corruption vs. panic depend on the target's KVA layout, which the rig cannot decide β the panic-class floor is certain, the corruption window speculative.
- The
layoutmode prints the verbatim struct offsets (48/40/12/7/4) that the corrected arithmetic rests on.
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 the identical crafted image: baseline executes the wild store (guard-mode fault at ps+0xBFFFFFFFF; fullflow canary byte written) and the wild read; fixed build rejects the zero-level state at parse (init rc=-22, 'power state 1 has no dpm levels') so :1619 is unreachable, and the defense-in-depth num_levels guard covers any other zero-level path. Good-table regression unchanged. 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 (init rc=-22) before the vulnerable site); build.log ('DF-4145 fix lines == fix.diff additions (11 lines)'); fix.diff
Confirmed kernel references
Detail
Exploit chain
Crafted VBIOS: IntegratedSystemInfo v1_7 with ucNBDPMEnable=1 (verbatim-parsed by trinity_parse_sys_info_table, enable_nbps_policy defaults true) + a pplib state carrying an SDSTATE/HDSTATE/UI_BATTERY classification and ucNumDPMLevels=0 -> parse leaves that state's num_levels==0 -> state selection (radeon_pm machinery) -> trinity_dpm_pre_set_power_state -> trinity_apply_state_adjust_rules:1619 -> wild byte-zero store at pi->requested_ps + 0xBFFFFFFFF and OOB read at +0xBFFFFFFF8 (get_sclk feeding watermark programming) -> kernel page fault -> panic, or 1-byte zero of unrelated kernel VA where mapped. No unprivileged-userland trigger; attacker data is the GPU ROM.
Evidence (decisive lines)
run.log (4145-guard: FAULT addr=0x14004bdfff == ps+0xBFFFFFFFF with the rig printing the computed wild offset; 4145-fullflow: canary byte 0xaa->0x00 written by the verbatim store, 1/12288 bytes changed, Dpm0/DpmX reachability proof, get_sclk returning canary bytes; FIXED: parse rejected rc=-22), run.2.log, build.log (68x identical + 'DF-4145 fix lines == fix.diff additions (11 lines)'), rig/, VERDICT.md (full corrected-arithmetic derivation), env.txt
PoC changes
Seed pack had no runnable PoC. Built the verbatim rig from scratch. Two rig-driven corrections vs the seed claim set: (1) the :1619 store target is ps+0xBFFFFFFFF (u32 index wrap x sizeof(trinity_pl)=12), not ps-1 -- proven by fault-address arithmetic, gdb inspection and a MAP_FIXED canary page catching exactly one byte; (2) the levels[num_levels-1].sclk reads are the SAME wild offset, not intra-object garbage. The trigger/reachability/OOB-write class of the finding stand unchanged.
Verified recommended fix
Reject pplib states whose idx loop yields k==0 (full unwind, -EINVAL) in trinity_parse_power_table and guard the :1619 store with ps->num_levels > 0 -- fix.diff, A/B validated.
Verdict
REPRODUCED with the finding's MECHANISM CORRECTED by the rig. Confirmed exactly as claimed: a pplib state with ucNumDPMLevels==0 parses with num_levels==0 (trinity_parse_pplib_clock_info:1726 never runs for it); when that state is selected, the verbatim trinity_dpm_pre_set_power_state -> trinity_apply_state_adjust_rules executes :1603-:1618 (proved by Dpm0PgNbPsLo 1->2 / DpmXNbPsLo 2->2 overwritten at :1615/:1617) and then the :1619 store ps->levels[ps->num_levels - 1].allow_gnb_slow = 0 executes OUT OF BOUNDS. Corrected mechanism: num_levels is u32, so the index is 0xFFFFFFFF and the store addresses ps + 4 + 0xFFFFFFFF*12 + 7 = ps + 0xBFFFFFFFF -- a WILD 1-byte-zero store ~48 GiB past the object, NOT the claimed ps-1 window write into the MSB of pi->requested_rps.ps_priv (C unsigned arithmetic; gdb computes the exact fault address 0x140047b1af == ps + 0xBFFFFFFFF, and a MAP_FIXED canary page at that exact offset receives precisely one changed byte, 0xaa->0x00, of 12288). The read side (trinity_dpm_get_sclk:2102 levels[num_levels-1].sclk) addresses ps + 0xBFFFFFFF8 -- same wild region (rig get_sclk returns the canary bytes), not an intra-object garbage read. In-kernel consequence: page fault -> panic at the first state transition (certain floor), or a silent 1-byte zeroing of unrelated kernel memory if the +48GiB offset happens to be mapped; NO pointer-surgery capability (constant offset, constant 0x00 value) -- the finding's non-canonical-ps_priv chain is refuted. fix.diff (reject zero-level states at parse + num_levels guard on the store) A/B-validated: fixed build rejects the crafted table at init (-22, 'power state 1 has no dpm levels'), the write is unreachable, good tables unaffected.
No comments yet.