Signed int16_t table_id passes the only bounds check in vega20_copy_table_from/to_smc: uint16 API values 0x8000..0xFFFF index smu_tables.entry[] out of bounds (heap OOB read AND write) β DF-2021/DF-2034/DF-2072 family twin pass 1 missed for vega20
Summary
The internal functions take int16_t while the whole public API is uint16_t; the sole 'table_id < TABLE_COUNT' guard is a signed compare, so API values 0x8000..0xFFFF convert negative and pass - entry[] sits at offset 0 of the backend, so negative indexes consume version/size/table from the preceding heap object and memcpy reads/writes through the OOB pointer, returning success. PROVEN in-kernel via verbatim-function KLD: groomed entry[-1] yields a controlled 8-byte kernel write (rc=0) + OOB read; 0x8000 reaches the full 32KB pre-object window; fixed variant rejects all. Latent today (all callers pass constants) - same rationale as DF-2072. uint16_t-widen fix (identical to the verified family fixes).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4792 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| vega20_oob_kld.c | trigger | KLD harness: verbatim-extracted vega20_copy_table_from/to_smc + vega20_smc_table_manager + struct layout; groomed entry[-1] proves OOB write/read | 10.3 KB | view raw |
| Makefile | build | DragonFly kmod makefile | 60 B | β download |
| build.sh | build-script | builds baseline (int16_t) and fixed (uint16_t) variants | 150 B | view raw |
| run.sh | run-script | loads both variants, unloads after each | 181 B | view raw |
| build.log | build-log | harness build output (both variants, -Werror) | 2.0 KB | view raw |
| run.log | run-log | final decisive run + full dmesg: CONTROLLED KERNEL WRITE / OOB KERNEL READ (baseline) and -EINVAL rejections (fixed) | 1.2 KB | view raw |
| fix.diff | suggested-fix | git-apply-able: int16_t -> uint16_t on both copy functions' table_id | 738 B | view raw |
| fix_build.log | fix-build-log | patched-tree nativekernel: 35829 lines, 0 error: lines, vega20_smumgr.o compiled -Werror clean, RC=0 | 4.6 KB | view raw |
| fix_run.log | fix-run-log | patched (uint16_t) variant: all negative ids rejected -EINVAL, victim untouched | 835 B | view raw |
| env.txt | env | guest uname, cc version, kldstat | 375 B | view raw |
| VERDICT.md | verdict | full narrative: source trace, reachability, reproduction, fix validation, harness-deviation notes | 7.0 KB | β raw |
| README.md | readme | human-readable build/run/expected | 3.2 KB | β raw |
| verdict.json | verdict-json | machine verdict consumed by persist_poc.py/persist_fix.py | 5.6 KB | view raw |
DF-4792 β vega20_smumgr signed table_id guard bypass (entry[] OOB read/write)
What
vega20_copy_table_from_smc() / vega20_copy_table_to_smc() in
sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c take int16_t table_id
but the whole public API surface (hwmgr.h:218, smumgr.h:114,
vega20_smc_table_manager at vega20_smumgr.c:563) is uint16_t. The only
bounds check is a signed comparison:
PP_ASSERT_WITH_CODE(table_id < TABLE_COUNT, "Invalid SMU Table ID!", return -EINVAL);
Any API value in 0x8000..0xFFFF narrows to a negative int16_t, passes the
guard, and indexes priv->smu_tables.entry[table_id] out of bounds (before
the array β struct vega20_smumgr puts smu_tables.entry[] at offset 0, so
entry[-1] reads the preceding heap object). The OOB .table pointer and
.size then feed a memcpy in both directions = heap OOB read and write
whose target/length come from adjacent kernel memory.
Pass-1 missed vega20; this is the exact family twin of DF-2021 (vega12), DF-2034 (vega10), DF-2072 (smu10) β all fixed the same way.
Reachability (honest)
Latent. Every in-tree vega20 caller passes a compile-time constant
(vega20_hwmgr.c:760,1106,1234,1253,1310,1924,1958,2726,2735,2961 β
TABLE_PPTABLE / TABLE_OVERDRIVE / TABLE_SMU_METRICS / TABLE_WATERMARKS).
No sysfs/ioctl path forwards a user-controlled table_id. The bug becomes
memory corruption the day any caller routes a 16-bit selector through
smum_smc_table_manager() β exactly how the twins were rated.
Reproduce (inside the guest, after ./build.sh)
./build.sh # builds df4792.baseline.ko (verbatim int16_t) + df4792.fixed.ko (fix.diff's uint16_t) ./run.sh # kldload baseline -> kldunload -> kldload fixed -> kldunload
Expected console (dmesg) β baseline:
DF-4792 harness loaded (param type: int16_t) DF-4792: in-bounds id=9 (unalloc) rc=-22 (want -EINVAL) DF-4792: to_smc(0xFFFF) rc=0 victim[0..7]=4141414141414141 << CONTROLLED KERNEL WRITE DF-4792: from_smc(0xFFFF) rc=0 victim[0..7]=4141414141414141 << OOB KERNEL READ DF-4792: from_smc(0x8000 -> entry[-32768], far OOB) rc=-22
fixed (int16_t -> uint16_t, exactly fix.diff):
DF-4792 harness loaded (param type: uint16_t) PP: Invalid SMU Table ID! DF-4792: to_smc(0xFFFF) rc=-22 victim[0..7]=0000000000000000 (untouched) ...
The harness is a DragonFly KLD that compiles the verbatim function bodies
(vega20_smumgr.c:163-236, 563-574) and struct layout (vega20_smumgr.h:29-43)
with only the MMIO SMC-message sender stubbed to return 0 β see the header
comment of vega20_oob_kld.c for the exact substitution contract. A groomed
entry[-1] (32 bytes before the allocation, as on a real
kzalloc(sizeof(struct vega20_smumgr))) supplies version=1, size=8,
table=victim, so the OOB write/read land on a module-owned buffer β fully
deterministic, no wild writes, no panic.
Fix
fix.diff β widen the two internal table_id parameters from int16_t to
uint16_t (matching the public API), making the existing < TABLE_COUNT
guard an unsigned comparison. Validated: harness-fixed variant rejects
0xFFFF/0x8000 with -EINVAL, and the patched TU compiles clean in the
default -Werror kernel build (fix_build.log).
DF-4792 β VERDICT
Status: REPRODUCED (kernel-context harness; conditional primitive proven, latent reachability).
Impact: none as shipped β no in-tree caller forwards an attacker-controlled
table_id; the conditional primitive (any future caller passing a 16-bit
selector) is a kernel heap OOB read and write, proven live in-kernel below.
Confidence: certain.
1. The bug (source trace)
sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:163-164βvega20_copy_table_from_smc(struct pp_hwmgr *hwmgr, uint8_t *table, int16_t table_id)(signed)sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:170-171β the only bound check:PP_ASSERT_WITH_CODE(table_id < TABLE_COUNT, "Invalid SMU Table ID!", return -EINVAL);sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:203-204, 210-211β same invega20_copy_table_to_smcPP_ASSERT_WITH_CODE(c,m,k)=if (!(c)) { pr_warn; k; }(inc/pp_debug.h:29-36) β a signed compare, sotable_id == -1passes.sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:172-175 / 212-215βpriv->smu_tables.entry[table_id].version/.sizeare then read OOB;:179/184use the OOB.mc_addr;:192-193/:217-218memcpywith the OOB.tablepointer and.sizelength β heap OOB read (from_smc) and write (to_smc).- The narrowing entry point is in-tree:
vega20_smc_table_manager(..., uint16_t table_id, ...)(vega20_smumgr.c:563-565) calls both copy functions with the uint16 β int16 implicit conversion (:569/:571), and the public API contract is uint16_t (inc/hwmgr.h:218, inc/smumgr.h:114). struct vega20_smumgr { struct smu_table_array smu_tables; }withentry[]the first member (vega20_smumgr.h:37-43),TABLE_COUNT=10(inc/smu11_driver_if.h:837) β negative indexes land in the heap chunk preceding thekzalloc'd backend (vega20_smumgr.c:386).
Family: identical pattern already filed for vega12 (DF-2021), vega10 (DF-2034), smu10 (DF-2072) β all with the same int16_tβuint16_t fix. Pass 1 missed vega20; found in this pass-2 audit.
2. Reachability (why impact=none today)
All vega20 smum_smc_table_manager callers pass constants:
vega20_hwmgr.c:760 (TABLE_PPTABLE), :1106/:1234/:1253/:1310/:2726/:2735
(TABLE_OVERDRIVE), :1924/:1958 (TABLE_SMU_METRICS), :2961 (TABLE_WATERMARKS).
vega20_set/get_activity_monitor_coeff (vega20_smumgr.c:238-293) index with
the constant TABLE_ACTIVITY_MONITOR_COEFF; workload_type only rides inside
the SMC message parameter (| (workload_type << 16)), never a kernel index.
No sysfs/ioctl/debugfs path forwards a user-controlled 16-bit table_id.
So: latent defense-in-depth-against-future-callers β same rating as DF-2072
(Medium/memcorrupt).
3. Reproduction (guest, DragonFly 6.5-DEVELOPMENT, cc 8.3)
KLD harness vega20_oob_kld.c (see README for the verbatim-extraction
contract): allocates a vega20_smumgr with 32 groomed bytes before it
(modeling the preceding slab object), arms entry[-1] = {version=1, size=8,
table=victim}, then calls the extracted vega20_smc_table_manager with the
runtime value 0xFFFF (volatile, as it would arrive via the uint16 API).
Baseline (verbatim int16_t) β decisive lines from run.log:
DF-4792 harness loaded (param type: int16_t) DF-4792: in-bounds id=9 (unalloc) rc=-22 (want -EINVAL) <- guard fine for valid ids DF-4792: to_smc(0xFFFF) rc=0 victim[0..7]=4141414141414141 << CONTROLLED KERNEL WRITE DF-4792: from_smc(0xFFFF) rc=0 victim[0..7]=4141414141414141 << OOB KERNEL READ DF-4792: from_smc(0x8000 -> entry[-32768], far OOB) rc=-22 <- far-OOB window exists too
0xFFFFβint16_t -1β-1 < 10true βentry[-1]consumed; the groomed.table=victim,.size=8produce a controlled 8-byte kernel write of0x41bytes, and the function returns 0 (success).- Read path symmetrically copies 8 OOB-sourced bytes out.
0x8000(β β32768) shows the window is the whole 32 KB before the object, not just one entry (here it landed on zeroed memory and was rejected by the version check β on a live system that is adjacent-kernel-memory-dependent).
Fixed variant (build.sh seds the harness's TID_T to uint16_t, i.e.
exactly fix.diff):
DF-4792 harness loaded (param type: uint16_t) PP: Invalid SMU Table ID! DF-4792: to_smc(0xFFFF) rc=-22 victim[0..7]=0000000000000000 (untouched) DF-4792: from_smc(0xFFFF) rc=-22 victim[0..7]=0000000000000000 (untouched) DF-4792: from_smc(0x8000 -> entry[-32768], far OOB) rc=-22
All negative ids now rejected before any array access; in-bounds behavior unchanged. Baseline reproduced on 4 independent loads (see run.log history).
4. Why no uid=0 chain
The primitive triggers only on table_id β 0x8000..0xFFFF reaching the copy
functions; every in-tree vega20 caller passes constants. There is no
unprivileged (or even privileged) syscall/sysfs path that forwards a
user-chosen table id, so the escalation chain has no first link on a stock
system. If such a caller ever appears, the chain would be: groom the heap
chunk preceding hwmgr->smu_backend (kzalloc'd at vega20_smumgr.c:386,
stable for the device's lifetime) β entry[-1].table = target,
.size = length β copy_table_to_smc = arbitrary kernel write returning
success.
5. Fix validation
fix.diff:int16_tβuint16_ton both copy functions'table_id(vega20_smumgr.c:164, :204) β the guard becomes an unsigned compare.- Harness-level: fixed variant rejects all negative ids (above).
- Kernel-level: applied to the guest's /usr/src copy and full
make -j6 nativekernel KERNCONF=X86_64_GENERICcompleted rc=0 with 0error:lines in the default-Werrorbuild (see fix_build.log); the patchedvega20_smumgr.ccompiled clean into the drm modules. Guest then reset (vm.sh reset with-src) β source tree reverted.
6. Honest notes / deviations
- Two early harness iterations panicked the guest β harness bugs, not the
target code: (a) DragonFly's
DECLARE_MODULEtakes amoduledata_tstruct, not a handler (garbage module name β strcmp GPF during kldload); (b) an uninitialized localhwmgr.smu_backendin the sanity probe. Both fixed; the final harness runs deterministically with no guest instability. - GCC 8.3's
-Warray-boundsindependently flagged the exact OOB index during harness development when the trigger id was a compile-time constant (error: array subscript -32768 is below array bounds of 'struct smu_table_entry[10]') β the production TU escapes it only becausetable_idis a runtime parameter. - No Vega20 hardware on the audit guest (QEMU) β the real driver never binds; hence the verbatim-extraction harness rather than a live-driver PoC.
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
fixedfix.diff (int16_t -> uint16_t on vega20_copy_table_from_smc/_to_smc table_id) applied to the guest /usr/src: (a) harness-level - the uint16_t-built variant rejects 0xFFFF and 0x8000 with 'Invalid SMU Table ID!' -EINVAL and leaves the victim buffer untouched, while in-bounds behavior (unallocated slot -> version-check -EINVAL) is unchanged; (b) kernel-level - full 'make -j6 nativekernel KERNCONF=X86_64_GENERIC' completed RC=0 with 0 error: lines in 35829 (default -Werror), vega20_smumgr.o compiled clean into the amdgpu module. Baseline (unpatched) behavior reproduced 4x before the fix. Bad behavior GONE on the patched build.
fix_run.log (patched-variant rejection output); fix_build.log (compile line for vega20_smumgr.c with -Werror, --- vega20_smumgr.o --- marker, 'Kernel build for X86_64_GENERIC completed', RC=0, 0 errors); run.log baseline sections for contrast
Confirmed kernel references
- sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:163
- sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:170
- sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:192
- sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:203
- sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:210
- sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:217
- sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:563
- sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.c:386
- sys/dev/drm/amd/powerplay/smumgr/vega20_smumgr.h:37
- sys/dev/drm/amd/powerplay/inc/smu11_driver_if.h:837
- sys/dev/drm/amd/powerplay/inc/hwmgr.h:218
- sys/dev/drm/amd/powerplay/inc/smumgr.h:114
Detail
Exploit chain
Conditional only (no trigger on stock system): if a future caller forwards a user-controlled 16-bit table_id -> groom heap chunk preceding hwmgr->smu_backend (kzalloc vega20_smumgr.c:386, device-lifetime stable) -> entry[-1].table=target, .size=len -> vega20_copy_table_to_smc = arbitrary kernel write returning 0 (success). No unprivileged trigger exists today, so no uid=0 chain.
Evidence (decisive lines)
run.log decisive lines: 'to_smc(0xFFFF) rc=0 victim[0..7]=4141414141414141 << CONTROLLED KERNEL WRITE' and 'from_smc(0xFFFF) rc=0 ... << OOB KERNEL READ' (baseline, param type: int16_t); 'PP: Invalid SMU Table ID!' + 'untouched' for all probes on the uint16_t fixed variant; fix_build.log: patched nativekernel 35829 lines 0 errors RC=0 with vega20_smumgr.o compiled -Werror clean
PoC changes
PoC authored fresh (no seed): verbatim extraction of vega20_smumgr.c:163-236,563-574 + vega20_smumgr.h:29-43 + pp_debug.h PP_ASSERT into a DragonFly KLD; only the MMIO SMC-message sender stubbed to return 0. Two harness-only panics during development (DFly DECLARE_MODULE takes moduledata_t struct, not a handler; uninitialized local hwmgr.smu_backend in the sanity probe) - both fixed, neither a property of the target code. Trigger ids passed via volatile uint16_t (as they arrive via the public API) after GCC -Warray-bounds statically flagged the constant-propagated OOB index.
Verified recommended fix
Widen both internal copy functions' table_id from int16_t to uint16_t (matching the uint16_t public API) so the existing < TABLE_COUNT guard compares unsigned - identical to the applied-and-verified DF-2021/DF-2034/DF-2072 fixes.
Verdict
Signed int16_t table_id in vega20_copy_table_from_smc/_to_smc passes the only bounds check (table_id < TABLE_COUNT) for every API value 0x8000..0xFFFF, indexing priv->smu_tables.entry[] out of bounds before the array (struct vega20_smumgr has entry[] at offset 0, so negative indexes hit the preceding heap object of the kzalloc'd backend). Proven in-kernel with a verbatim-extraction KLD harness: a groomed entry[-1] (version=1,size=8,table=victim) yields a controlled 8-byte kernel write (to_smc, rc=0 'success') and an OOB kernel read (from_smc, rc=0); 0x8000->entry[-32768] shows a 32KB pre-object read window. Impact 'none' as shipped because every in-tree vega20 caller (vega20_hwmgr.c:760..2961) passes compile-time constant table ids and no sysfs/ioctl forwards a user-controlled 16-bit selector - the DF-2021/DF-2034/DF-2072 family twin that pass 1 missed for vega20; latent memcorrupt primitive, Medium.
No comments yet.