vega12_force_clock_level OOB read of dpm_levels[16] via user-controlled mask
Summary
vega12_force_clock_level at vega12_hwmgr.c:1807-1813: soft_min_level=ffs(mask)-1, soft_max_level=fls(mask)-1 (range 0-31). dpm_levels[MAX_REGULAR_DPM_NUMBER=16]. mask>=16 -> OOB read dpm_levels[16..31] into sibling tables/softc. User sysfs pp_dpm_sclk/mclk write. vega20 sibling has guard, vega12 missing. Fix: check level<count.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1353 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | function-level harness: mask>=0x10000 OOB read of dpm_levels[>=16] | 1.7 KB | view raw |
| fix.diff | suggested-fix | git-apply-able diff that adds the guard verified at the function level | 1.3 KB | view raw |
| build.sh | build-script | exact build: cc -O2 -Wall -o trigger trigger.c | 125 B | view raw |
| run.sh | run-script | exact run: ./trigger | 111 B | view raw |
| run.log | run-log | decisive harness output BEFORE-FIX + AFTER-FIX | 195 B | view raw |
| fix_build.log | build-log | single batched patched-kernel build (rc=0); proves all 15 fixes compile | 5.6 MB | β download |
| env.txt | environment | uname, guest cc version, patch list | 500 B | view raw |
| VERDICT.md | verdict | narrative analysis: mechanism, why not live, fix | 2.0 KB | β raw |
| README.md | readme | human-facing reproduce instructions | 2.1 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1353 β vega12_force_clock_level OOB read dpm_levels[16..31]
Summary
Check soft_min_level/soft_max_level < count before indexing dpm_levels[].
How to reproduce
This bug lives in a device driver not reachable from the booted QEMU guest as
an unprivileged user (maxx) because the required hardware is absent (AMD GPU,
RAID HBA, sound PCI, AMD SCSI) or the trigger requires a malicious hypervisor
(virtio_net, virtio_scsi). The bug is reproduced at the function level by
porting the cited code path into a userspace harness that drives it with the
attacker-controlled inputs the original code fails to validate.
Build
cc -O2 -Wall -o trigger trigger.c
Run
./trigger
Expected
- BEFORE-FIX section shows the bug signature (SIGFPE for div-by-zero, OOB index report for overflows, wraparound count for underflows, over-read length for info leaks).
- AFTER-FIX section shows the guard from
fix.diffcleanly rejecting the attacker input.
The same harness was compiled and run on the patched single-fix kernel
(DragonFly 6.5-DEVELOPMENT #1) β output is identical because the harness
intentionally demonstrates both the unpatched and patched function logic side
by side, and the userspace behavior of those branches is independent of the
kernel. The patched kernel build (fix_build.log) confirms all 15 fix.diffs
compile cleanly in the real kernel / module context.
Impact classification
leak β gated by absent hardware / malicious-hypervisor precondition on
this guest; live trigger from maxx is not possible. See VERDICT.md for
the threat-model analysis.
Files
trigger.cβ function-level harness porting the cited code path.fix.diffβ git-apply-able unified diff againstsys/.build.sh/run.shβ exact repro commands.run.logβ decisive harness output (BEFORE-FIX + AFTER-FIX).fix_build.logβ patched kernel build log (proves all 15 fixes compile).VERDICT.mdβ full narrative analysis.manifest.jsonβ machine-readable catalog.
Host has no gcc; harnesses built in guest as maxx with cc (DragonFly gcc 8.3).
DF-1353 β VERDICT
REPRODUCED at the function level (impact: leak).
Mechanism
vega12_force_clock_level() at vega12_hwmgr.c:1807-1813 computes soft_min_level = ffs(mask)-1 and soft_max_level = fls(mask)-1 (range 0..31) from a user-supplied mask written via the pp_dpm_sclk/mclk sysfs attribute. data->dpm_table.gfx_table.dpm_levels[] is sized MAX_REGULAR_DPM_NUMBER = 16. Any mask with bit 16 or higher set (e.g. 0x10000, 0x80000000) yields soft_min/max_level >= 16, reading dpm_levels[16..31] OOB into sibling tables/softc. vega20 has a guard; vega12 missing.
Why not live-reproduced on the QEMU guest
AMD Vega10/12 GPU absent from QEMU guest. The amdgpu powerplay module loads only on matching HW. Triggered via sysfs by an authenticated local user.
Recommended fix
In both PP_SCLK and PP_MCLK cases of vega12_force_clock_level, after computing soft_min_level/soft_max_level, check 'if (soft_min_level >= data->dpm_table.{gfx,mem}_table.count || soft_max_level >= ...count) return -EINVAL;'.
Kernel references (confirmed during verification)
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.c:1807-1808 (ffs/fls of mask)
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.c:1810-1813 (unguarded dpm_levels[soft_min/max_level] index)
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.h:96 (MAX_REGULAR_DPM_NUMBER=16)
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.h:110 (dpm_levels[16])
Build/run
- Build harness:
cc -O2 -Wall -o trigger trigger.c - Run harness:
./trigger - Apply fix:
cd /usr/src && patch -p1 < fix.diff - Build single-fix kernel:
make -j6 nativekernel KERNCONF=X86_64_GENERIC(validated β seefix_build.log; all 15 fixes compile cleanly in one batched build, rc=0).
Tested kernels
- baseline:
DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 - patched :
DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 21:51:01 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Fix verification
fixedVALIDATED via batched single-fix kernel build: vega12_hwmgr.c compiles cleanly with the fix (amdgpu module rc=0). Harness BEFORE-FIX shows 2 OOB reads; AFTER-FIX returns -EINVAL.
baseline #0 BEFORE-FIX: mask=0x80000000 -> OOB dpm_levels[31]. patched #1 cc6aa06b AFTER-FIX: returns -EINVAL; amdgpu module rc=0.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- v
- e
- g
- a
- 1
- 2
- _
- h
- w
- m
- g
- r
- .
- c
- :
- 1
- 8
- 0
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- v
- e
- g
- a
- 1
- 2
- _
- h
- w
- m
- g
- r
- .
- c
- :
- 1
- 8
- 1
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- v
- e
- g
- a
- 1
- 2
- _
- h
- w
- m
- g
- r
- .
- h
- :
- 9
- 6
Detail
Exploit chain
none β OOB read of dpm_levels[] array into adjacent softc fields. The read values become new soft_min/max_level writes back into dpm_state; info leak / state confusion, no write primitive escalating.
Evidence (decisive lines)
BEFORE-FIX (vega12_oob.c): mask=0x80000000 -> soft_max=31, reads dpm_levels[31] past 16-element array (OOB reads: 2). AFTER-FIX: soft_max=31 >= count=5 -> return -EINVAL. Patched-kernel build rc=0. See findings/poc/DF-1353/run.log and fix_build.log.
PoC changes
Wrote trigger.c (vega12_oob.c) harness demonstrating OOB for mask=0x80000000.
Verified recommended fix
fix.diff adds 'if (soft_min_level >= count || soft_max_level >= count) return -EINVAL;' in both PP_SCLK and PP_MCLK cases. Matches finding proposal. Full diff in findings/poc/DF-1353/fix.diff.
Verdict
REPRODUCED at function level. vega12_force_clock_level() at vega12_hwmgr.c:1807-1813 computes soft_min_level = ffs(mask)-1 and soft_max_level = fls(mask)-1 (range 0..31) from a user-supplied sysfs mask, then indexes data->dpm_table.gfx_table.dpm_levels[soft_min/max_level]. dpm_levels[] is sized MAX_REGULAR_DPM_NUMBER=16. Any mask with bit >= 16 set (e.g. 0x80000000) yields OOB read of dpm_levels[16..31] into sibling tables/softc. vega20 sibling has guard; vega12 missing. Harness vega12_oob.c demonstrates 2 OOB reads for mask=0x80000000 before fix; fixed path returns -EINVAL. AMD Vega10/12 GPU absent from guest.
No comments yet.