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

Integer underflow in mclk_latency loop in vega12_apply_clocks_adjust_rules (count-1 wraps when count==0)

Summary

vega12_apply_clocks_adjust_rules at vega12_hwmgr.c:1973: for(i=0;i<mclk_latency_table.count-1;i++). count is uint32, defaults 0 (kzalloc). count==0 -> count-1 wraps to 0xFFFFFFFF. Loop reads entries[i] (16-entry) and dpm_levels[i] OOB. Reached when disable_mclk_switching=true (multi-monitor). vega10 uses i<count (safe). vega20 has identical bug. Sibling of DF-1179/DF-1253. Fix: i<count && i<dpm_table->count.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1354 Β· 11 files
FileTypeDescriptionSize
trigger.c trigger-source function-level harness: uint32 count==0 -> count-1 wraps -> OOB loop 1.6 KB view raw
fix.diff suggested-fix git-apply-able diff that adds the guard verified at the function level 1.2 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 218 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 1.8 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
README.md readme human-facing reproduce instructions
↓ download raw

DF-1354 β€” vega12_apply_clocks_adjust_rules mclk_latency count uint32 underflow

Summary

Guard count != 0 and bound loop by i < count && i < dpm_table->count && i < MAX_REGULAR_DPM_NUMBER-1.

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.diff cleanly 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

panic β€” 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 against sys/.
  • 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).

VERDICT.md verdict narrative analysis: mechanism, why not live, fix
↓ download raw

DF-1354 β€” VERDICT

REPRODUCED at the function level (impact: panic).

Mechanism

vega12_apply_clocks_adjust_rules() at vega12_hwmgr.c:1973: 'for (i = 0; i < data->mclk_latency_table.count - 1; i++)'. count is uint32, defaults to 0 (kzalloc'd struct). count == 0 -> count-1 wraps to 0xFFFFFFFF, loop reads mclk_latency_table.entries[i] (16-entry) and dpm_table.dpm_levels[i] OOB. Reached when disable_mclk_switching is true (multi-monitor config). vega10 uses 'i < count' (safe); vega20 has the same bug.

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 at display-config change with multi-monitor.

In vega12_apply_clocks_adjust_rules, check 'if (dpm_table->count == 0) return -EINVAL;' and change loop bound to 'for (i = 0; i < data->mclk_latency_table.count && i < dpm_table->count && i < MAX_REGULAR_DPM_NUMBER - 1; i++)'.

Kernel references (confirmed during verification)

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 β€” see fix_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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via batched single-fix kernel build: vega12_hwmgr.c compiles cleanly with the fix (amdgpu module rc=0; both DF-1353 and DF-1354 patches co-exist in the same file). Harness BEFORE-FIX shows 84 OOB reads in 100 iters; AFTER-FIX returns -EINVAL.

baseline #0 BEFORE-FIX: count=0, count-1=0xFFFFFFFF, 84 OOB reads. patched #1 cc6aa06b AFTER-FIX: count==0 -> -EINVAL; amdgpu module rc=0.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 22:25:35 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

none β€” uint32 underflow drives massive OOB read of two 16-entry arrays into adjacent powerplay softc state; on a live system this would likely panic (kASAN/slab redzone) or corrupt neighboring DPM tables. Not a clean primitive for escalation; primarily a DoS/corruption panic.

Evidence (decisive lines)

BEFORE-FIX (vega12_underflow.c): count=0, count-1 wraps to 4294967295, loop reads entries[i]/dpm_levels[i] OOB (OOB reads in first 100 iters: 84). AFTER-FIX: count==0 -> return -EINVAL, no OOB reads. Patched-kernel build rc=0. See findings/poc/DF-1354/run.log and fix_build.log.

PoC changes

Wrote trigger.c (vega12_underflow.c) harness demonstrating the count-1 wrap.

Verified recommended fix

fix.diff adds 'if (dpm_table->count == 0) return -EINVAL;' and changes loop bound to 'i < count && i < dpm_table->count && i < MAX_REGULAR_DPM_NUMBER - 1'. Matches finding proposal. Full diff in findings/poc/DF-1354/fix.diff.

Verdict

REPRODUCED at function level. vega12_apply_clocks_adjust_rules() at vega12_hwmgr.c:1973: 'for (i = 0; i < data->mclk_latency_table.count - 1; i++)'. count is uint32, defaults to 0 (kzalloc'd struct). count==0 -> count-1 wraps to 0xFFFFFFFF, loop reads mclk_latency_table.entries[i] (16-entry) and dpm_table.dpm_levels[i] OOB. Reached when disable_mclk_switching=true (multi-monitor). Harness vega12_underflow.c demonstrates 84 OOB reads in first 100 iterations before fix; fixed path returns -EINVAL when count==0. AMD Vega10/12 GPU absent from guest.