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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4145 Β· 19 files
FileTypeDescriptionSize
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 of pi->requested_rps.ps_priv. That is arithmetically impossible: ps->num_levels is u32, so ps->num_levels - 1 is 0xFFFFFFFF, and the array subscript scales by sizeof(struct trinity_pl) == 12 (verbatim layout mode), putting the store at ps + 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_sclk reads levels[num_levels-1].sclk at 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.

VERDICT.md
↓ download raw

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 == 0 is parsed with ps->num_levels left at its kzalloc zero: trinity_parse_pplib_clock_info sets num_levels = index + 1 only 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 with ucNumDPMLevels=0) leaves state1 num_levels=0 (run.log, mode 4145-fullflow).
  • When that state is selected, the verbatim trinity_dpm_pre_set_power_state β†’ trinity_apply_state_adjust_rules path executes the :1603-:1618 block (proved: Dpm0PgNbPsLo 1β†’2, DpmXNbPsLo 2β†’2 β€” the :1604/:1606 defaults overwritten at :1615/:1617, which immediately precede the store) and then executes ps->levels[ps->num_levels - 1].allow_gnb_slow = 0; (:1619) β€” an out-of-bounds store. Reachability requires pi->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:

  1. 4145-guard: the verbatim store faults; the reported address is 0x14004bdfff with ps = 0x8004be000, i.e. exactly ps + 0xBFFFFFFFF (checked arithmetically in the mode output). gdb at the faulting store computes &ps->levels[ps->num_levels - 1].allow_gnb_slow == 0x140047b1af == ps + 0xBFFFFFFFF for the fullflow ps.
  2. 4145-fullflow: a canary page mapped at exactly ps + 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 layout mode 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

fixed
baseline reproduced→ patch + rebuild →patched clean

A/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
↓ fix.diffper-fix-DF-4145

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.