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

Stack buffer overflow in radeon_atom_get_mclk_range_table: uncapped (u8) num_entries fill of mclk[16] into a caller stack local

Summary

num_entries = (u8)((usModuleSize - 32)/24) is computed from the BIOS VRAM module's u16 usModuleSize and the copy loop writes num_entries BIOS-chosen u32s into mclk[MAX_AC_TIMING_ENTRIES=16] with no bound; usModuleSize=0xFFF0 -> (u8)2736 = 168 entries -> 152 OOB u32s (608 B). The only caller (cypress_get_memory_clock_range_table) passes a stack local -> kernel-stack overwrite; cypress_retrieve_ac_timing_for_all_ranges then OOB-reads the same range. usModuleSize < 32 underflow yields arbitrary counts too. HARNESS: verbatim arithmetic on the guest - 152 OOB u32 writes of VBIOS data, first OOB value exactly at the 68-byte struct end; './mclk_bin stack' performs the overflow on the harness frame. Crafted VRAM_Info on Cypress/Juniper-era GPUs: BIOS-controlled 608+ bytes over a kernel stack frame at DPM init - crash to potential control-flow hijack.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3740 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 2.9 KB ↓ raw
build.log β€” 359 B view raw
build.sh β€” 65 B view raw
env.txt β€” 375 B view raw
harness_mclk.c β€” 5.0 KB view raw
run.log β€” 570 B view raw
run.sh β€” 181 B view raw
run.stack.log β€” 145 B view raw
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 2.9 KB ↓ raw
manifest.json β€” 993 B view raw
verdict.json β€” 3.3 KB view raw
fix.diff β€” 642 B view raw

DF-3740 β€” Stack buffer overflow in radeon_atom_get_mclk_range_table via uncapped num_entries fill of mclk[16]

Severity: High Β· CWE: CWE-787 / CWE-121 Out-of-bounds Write (stack) Β· File: sys/dev/drm/radeon/radeon_atombios.c:3938-3946

What this is

Source-certain kernel-stack overflow. radeon_atom_get_mclk_range_table() computes num_entries=(u8)((usModuleSize-32)/24) from the BIOS VRAM module's u16 usModuleSize and copies num_entries BIOS u32s into mclk[MAX_AC_TIMING_ENTRIES=16] (radeon_mode.h:637-649) with NO bound between the u8 count (up to 255 after truncation) and the 16-slot array. The ONLY caller (cypress_dpm.c:1075-1089, cypress_get_memory_clock_range_table) passes a stack local 'struct atom_memory_clock_range_table range_table = {0}' β€” so up to 239 BIOS-chosen u32s (956 B) are written past a 68-byte stack object (return address / caller frame). Harness on guest: usModuleSize=0xFFF0 -> num_entries=168 -> 152 OOB u32 writes (608 B past struct end). A truncation underflow (usModuleSize < 32) yields arbitrary num_entries as well. Downstream cypress_retrieve_ac_timing_for_all_ranges() also READS mclk[i] for i<num_entries (OOB stack reads). Not executable in-kernel on this guest (no AMD GPU).

Reproduce

./build.sh    # cc -O1 -g -Wall -o <bin> harness_mclk.c   (DragonFly guest, cc 8.3)
./run.sh      # expected: num_entries=168 vs mclk[16]; 152 OOB u32 writes (608 bytes) of VBIOS-chosen data; exit_code=0

The harness replicates the kernel parse arithmetic VERBATIM (cited path:line) against a synthetic crafted-VBIOS buffer with canary-guarded destinations.

Guest / environment

Guest has NO AMD/radeon GPU (virtio-only); /boot/kernel/radeon.ko exists (code shipped in the default install) but cannot attach, so the kernel code path is unreachable at runtime on this guest. Evidence is source-level certainty (path:line traced) plus a verbatim-arithmetic userland harness compiled and executed on the guest (build.log/run.log). See env.txt (uname, cc, kldstat, /boot/kernel/radeon.ko presence).

Fix

reject/clamp num_entries to MAX_AC_TIMING_ENTRIES β€” see fix.diff (authored after verification; never applied to sys/).

DF-3740 β€” Stack buffer overflow in radeon_atom_get_mclk_range_table via uncapped num_entries fill of mclk[16]

Severity: High Β· CWE: CWE-787 / CWE-121 Out-of-bounds Write (stack) Β· File: sys/dev/drm/radeon/radeon_atombios.c:3938-3946

What this is

Source-certain kernel-stack overflow. radeon_atom_get_mclk_range_table() computes num_entries=(u8)((usModuleSize-32)/24) from the BIOS VRAM module's u16 usModuleSize and copies num_entries BIOS u32s into mclk[MAX_AC_TIMING_ENTRIES=16] (radeon_mode.h:637-649) with NO bound between the u8 count (up to 255 after truncation) and the 16-slot array. The ONLY caller (cypress_dpm.c:1075-1089, cypress_get_memory_clock_range_table) passes a stack local 'struct atom_memory_clock_range_table range_table = {0}' β€” so up to 239 BIOS-chosen u32s (956 B) are written past a 68-byte stack object (return address / caller frame). Harness on guest: usModuleSize=0xFFF0 -> num_entries=168 -> 152 OOB u32 writes (608 B past struct end). A truncation underflow (usModuleSize < 32) yields arbitrary num_entries as well. Downstream cypress_retrieve_ac_timing_for_all_ranges() also READS mclk[i] for i<num_entries (OOB stack reads). Not executable in-kernel on this guest (no AMD GPU).

Reproduce

./build.sh    # cc -O1 -g -Wall -o <bin> harness_mclk.c   (DragonFly guest, cc 8.3)
./run.sh      # expected: num_entries=168 vs mclk[16]; 152 OOB u32 writes (608 bytes) of VBIOS-chosen data; exit_code=0

The harness replicates the kernel parse arithmetic VERBATIM (cited path:line) against a synthetic crafted-VBIOS buffer with canary-guarded destinations.

Guest / environment

Guest has NO AMD/radeon GPU (virtio-only); /boot/kernel/radeon.ko exists (code shipped in the default install) but cannot attach, so the kernel code path is unreachable at runtime on this guest. Evidence is source-level certainty (path:line traced) plus a verbatim-arithmetic userland harness compiled and executed on the guest (build.log/run.log). See env.txt (uname, cc, kldstat, /boot/kernel/radeon.ko presence).

Fix

reject/clamp num_entries to MAX_AC_TIMING_ENTRIES β€” see fix.diff (authored after verification; never applied to sys/).

VERDICT.md
↓ download raw

VERDICT β€” DF-3740

Status: inconclusive (source-certain; harness-reproduced arithmetic; kernel path unreachable on this guest) reproduced: 0 (in-kernel) β€” harness demonstration: YES Impact on real hardware: High β€” CWE-787 / CWE-121 Out-of-bounds Write (stack)

Finding

Source-certain kernel-stack overflow. radeon_atom_get_mclk_range_table() computes num_entries=(u8)((usModuleSize-32)/24) from the BIOS VRAM module's u16 usModuleSize and copies num_entries BIOS u32s into mclk[MAX_AC_TIMING_ENTRIES=16] (radeon_mode.h:637-649) with NO bound between the u8 count (up to 255 after truncation) and the 16-slot array. The ONLY caller (cypress_dpm.c:1075-1089, cypress_get_memory_clock_range_table) passes a stack local 'struct atom_memory_clock_range_table range_table = {0}' β€” so up to 239 BIOS-chosen u32s (956 B) are written past a 68-byte stack object (return address / caller frame). Harness on guest: usModuleSize=0xFFF0 -> num_entries=168 -> 152 OOB u32 writes (608 B past struct end). A truncation underflow (usModuleSize < 32) yields arbitrary num_entries as well. Downstream cypress_retrieve_ac_timing_for_all_ranges() also READS mclk[i] for i<num_entries (OOB stack reads). Not executable in-kernel on this guest (no AMD GPU).

Why not reproduced in-kernel

The QEMU guest is virtio-only (no AMD GPU): pciconf -l shows no 1002 device; kldstat shows radeon not loaded. /boot/kernel/radeon.ko exists (the vulnerable code ships in the default install) but cannot attach, so radeon_atombios.c parse paths never execute here. All conclusions are source-level (path:line above) plus the on-guest harness (harness_mclk.c).

Harness result

Command: ./run.sh (after ./build.sh); optional: ./mclk_bin stack (performs the overflow on the harness frame) Expected/observed: num_entries=168 vs mclk[16]; 152 OOB u32 writes (608 bytes) of VBIOS-chosen data; exit_code=0 Full logs: build.log, run.log, run.stack.log.

Impact / exploit chain

crafted VRAM_Info table (frev1/crev4) with usModuleSize=0xFFF0 on a Cypress/Juniper GPU -> 168-entry fill into 16-slot stack array at DPM init -> overwrites caller frame with BIOS u32s -> crash/controlled-stack-corruption (BIOS-gated privesc-grade primitive)

Fix

reject/clamp num_entries to MAX_AC_TIMING_ENTRIES β€” fix.diff.

Honest notes

  • DF-3740 does NOT re-report pass-1 findings DF-1198/1199/1200 (power-table state_index mismatch, asic_ss_info underflow, power-table indices) β€” different functions/tables/fields.
  • fix validation not possible on this guest (no hardware); fix.diff is source-authored only.

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.

VERDICT.md
↓ download raw

VERDICT β€” DF-3740

Status: inconclusive (source-certain; harness-reproduced arithmetic; kernel path unreachable on this guest) reproduced: 0 (in-kernel) β€” harness demonstration: YES Impact on real hardware: High β€” CWE-787 / CWE-121 Out-of-bounds Write (stack)

Finding

Source-certain kernel-stack overflow. radeon_atom_get_mclk_range_table() computes num_entries=(u8)((usModuleSize-32)/24) from the BIOS VRAM module's u16 usModuleSize and copies num_entries BIOS u32s into mclk[MAX_AC_TIMING_ENTRIES=16] (radeon_mode.h:637-649) with NO bound between the u8 count (up to 255 after truncation) and the 16-slot array. The ONLY caller (cypress_dpm.c:1075-1089, cypress_get_memory_clock_range_table) passes a stack local 'struct atom_memory_clock_range_table range_table = {0}' β€” so up to 239 BIOS-chosen u32s (956 B) are written past a 68-byte stack object (return address / caller frame). Harness on guest: usModuleSize=0xFFF0 -> num_entries=168 -> 152 OOB u32 writes (608 B past struct end). A truncation underflow (usModuleSize < 32) yields arbitrary num_entries as well. Downstream cypress_retrieve_ac_timing_for_all_ranges() also READS mclk[i] for i<num_entries (OOB stack reads). Not executable in-kernel on this guest (no AMD GPU).

Why not reproduced in-kernel

The QEMU guest is virtio-only (no AMD GPU): pciconf -l shows no 1002 device; kldstat shows radeon not loaded. /boot/kernel/radeon.ko exists (the vulnerable code ships in the default install) but cannot attach, so radeon_atombios.c parse paths never execute here. All conclusions are source-level (path:line above) plus the on-guest harness (harness_mclk.c).

Harness result

Command: ./run.sh (after ./build.sh); optional: ./mclk_bin stack (performs the overflow on the harness frame) Expected/observed: num_entries=168 vs mclk[16]; 152 OOB u32 writes (608 bytes) of VBIOS-chosen data; exit_code=0 Full logs: build.log, run.log, run.stack.log.

Impact / exploit chain

crafted VRAM_Info table (frev1/crev4) with usModuleSize=0xFFF0 on a Cypress/Juniper GPU -> 168-entry fill into 16-slot stack array at DPM init -> overwrites caller frame with BIOS u32s -> crash/controlled-stack-corruption (BIOS-gated privesc-grade primitive)

Fix

reject/clamp num_entries to MAX_AC_TIMING_ENTRIES β€” fix.diff.

Honest notes

  • DF-3740 does NOT re-report pass-1 findings DF-1198/1199/1200 (power-table state_index mismatch, asic_ss_info underflow, power-table indices) β€” different functions/tables/fields.
  • fix validation not possible on this guest (no hardware); fix.diff is source-authored only.

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored after source-level verification; cannot be validated on this guest (no AMD GPU; radeon.ko cannot attach). Compiling the module or booting a patched kernel would not exercise the path without hardware.

fix.diff (unified, against read-only sys/ tree; never applied)
↓ fix.diffper-fix-DF-3740

Confirmed kernel references

Detail

Exploit chain

crafted VRAM_Info table (frev1/crev4) with usModuleSize=0xFFF0 on a Cypress/Juniper GPU -> 168-entry fill into 16-slot stack array at DPM init -> overwrites caller frame with BIOS u32s -> crash/controlled-stack-corruption (BIOS-gated privesc-grade primitive)

Evidence (decisive lines)

harness_mclk.c + build.log + run.log + run.stack.log (direct-stack mode); VERDICT.md; kernel refs sys/dev/drm/radeon/radeon_atombios.c:3925-3946

PoC changes

harness written from scratch (no seed PoC): verbatim kernel arithmetic replica with canary-guarded destination model, built and executed on the DragonFly guest (cc 8.3); fixed nested-comment, stddef/stdint includes and volatile divisors during bring-up

Verified recommended fix

reject/clamp num_entries to MAX_AC_TIMING_ENTRIES

Verdict

Source-certain kernel-stack overflow. radeon_atom_get_mclk_range_table() computes num_entries=(u8)((usModuleSize-32)/24) from the BIOS VRAM module's u16 usModuleSize and copies num_entries BIOS u32s into mclk[MAX_AC_TIMING_ENTRIES=16] (radeon_mode.h:637-649) with NO bound between the u8 count (up to 255 after truncation) and the 16-slot array. The ONLY caller (cypress_dpm.c:1075-1089, cypress_get_memory_clock_range_table) passes a stack local 'struct atom_memory_clock_range_table range_table = {0}' β€” so up to 239 BIOS-chosen u32s (956 B) are written past a 68-byte stack object (return address / caller frame). Harness on guest: usModuleSize=0xFFF0 -> num_entries=168 -> 152 OOB u32 writes (608 B past struct end). A truncation underflow (usModuleSize < 32) yields arbitrary num_entries as well. Downstream cypress_retrieve_ac_timing_for_all_ranges() also READS mclk[i] for i<num_entries (OOB stack reads). Not executable in-kernel on this guest (no AMD GPU).