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

Unvalidated 4-bit register field indexes 9-element lookup table (OOB read)

Summary

df_v1_7_get_hbm_channel_number() indexes static 9-element table df_v1_7_channel_number[] (df_v1_7.c:30) using raw return of df_v1_7_get_fb_channel_number() (50-59) which masks HW register mmDF_CS_AON0_DramBaseAddress0 down to IntLvNumChan field. Field is 4-bit (mask 0x000000F0L shift 0x4 per df_1_7_sh_mask.h:39 44) yields 0..15 but lookup table has only 9 entries (indices 0..8). Values 9..15 read 1..6 u32 words (4..24 bytes) past end of static const array. Sibling df_v3_6_get_hbm_channel_number() at df_v3_6.c:67-68 hardened with exact bounds check missing here proving maintainers treat as real defect. Reachable: Vega10/Vega12 dGPU soc15.c:521-522 assigns df_funcs gmc_v9_0_mc_init() at gmc_v9_0.c:812 invokes get_hbm_channel_number on every GPU init/re-init. Trigger value from VBIOS/firmware attacker-influenced: (a) malicious ATOM VBIOS ROM hotplugged PCIe card; (b) malicious hypervisor SR-IOV VF path; (c) transient read during early bring-up/reset. Impact: OOB read 4..24 bytes kernel .rodata/.data adjacent to table multiplied by chansize(64/128) at gmc_v9_0.c:813 stored as adev->gmc.vram_width corrupting GPUVM/GART/VRAM sizing decisions. Limited info disclosure + possible memory-miscomputation DoS/later corruption in GMC init path.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2202 Β· 10 files
FileTypeDescriptionSize
df2202_trigger.c trigger-source userspace demonstration of the OOB indexing defect (verbatim table + mask/shift from kernel headers) 3.7 KB view raw
fix.diff suggested-fix git-apply-able fix: add ARRAY_SIZE bounds check mirroring df_v3_6 417 B view raw
build.sh build-script exact build command for the trigger stub 348 B view raw
run.sh run-script exact run command for the trigger stub 306 B view raw
build.log build-log trigger-stub build output, rc=0 72 B view raw
run.log run-log trigger-stub run output: 7 of 16 field values OOB 1.3 KB view raw
fix_build.log build-log Phase 8: full amdgpu.ko module build with fix applied, rc=0 -Werror 1.2 MB ↓ download
env.txt environment guest uname, cc version 363 B view raw
README.md readme human-facing summary, reproduce + fix-validate instructions 4.2 KB ↓ raw
VERDICT.md verdict full narrative verdict with path:line citations 6.1 KB ↓ raw
README.md readme human-facing summary, reproduce + fix-validate instructions
↓ download raw

DF-2202 β€” amdgpu df_v1_7 OOB array index (source-only confirmation)

Finding

df_v1_7_get_hbm_channel_number() indexes a static 9-element table df_v1_7_channel_number[] (sys/dev/drm/amd/amdgpu/df_v1_7.c:30) using the raw return of df_v1_7_get_fb_channel_number() (lines 50-59), which masks HW register mmDF_CS_AON0_DramBaseAddress0 down to the IntLvNumChan field. That field is 4-bit (MASK 0x000000F0L, SHIFT 0x4 β€” df_1_7_sh_mask.h:39,44) so it yields 0..15, but the lookup table has only 9 entries (indices 0..8). Values 9..15 read 1..6 u32 words past the end of the static const array.

Status

Source-only confirmation (HW-gated). The vulnerable path lives entirely inside the amdgpu DRM module and is only reachable on Vega10 / Vega12 (CHIP_VEGA10 / CHIP_VEGA12) discrete GPU hardware. This QEMU guest has no such hardware, so the bug cannot be triggered at runtime here. Verification proceeds by source trace + isolated userspace demonstration of the indexing defect + kernel-module build of the fix.

How to reproduce (the indexing defect in isolation)

cc -Wall -Wextra -o df2202_trigger df2202_trigger.c
./df2202_trigger

Expected output (decisive excerpt):

table df_v1_7_channel_number[] has 9 entries (indices 0..8)
IntLvNumChan field is 4-bit -> values 0..15
  ...
  field= 9  ->  INDEX  9  OOB  (u32 read at offset 0 word(s) = 0 byte(s) past end)
  ...
  field=15  ->  INDEX 15  OOB  (u32 read at offset 6 word(s) = 24 byte(s) past end)

7 of 16 possible field values land OUT-OF-BOUNDS on the table.
OOB reads land at offset 0..6 u32 words (0..24 bytes) past the end
of the static array, reading whatever 4-byte word lives there.

How to validate the fix

./build.sh   # builds df2202_trigger (sanity) and applies fix.diff on guest
./run.sh     # runs the trigger; runs the kernel-module build with -Werror

The fix is fix.diff β€” adds the exact bounds check that the sibling df_v3_6_get_hbm_channel_number() already has (df_v3_6.c:67-68):

fb_channel_number = adev->df_funcs->get_fb_channel_number(adev);
if (fb_channel_number >= ARRAY_SIZE(df_v1_7_channel_number))
    fb_channel_number = 0;

Phase 8 (kernel-module build with -Werror)

On the DragonFly guest (kernel 6.5-DEVELOPMENT #0, gcc 8.3), the amdgpu KMOD's natural CWARNFLAGS already includes -Werror (and -Wno-pointer-sign to suppress an unrelated pre-existing issue in sys/sys/hash.h). With the patch applied:

  • make df_v1_7.o β†’ rc=0, zero warnings/errors.
  • make (full amdgpu.ko link, 3.7 MB) β†’ rc=0, zero warnings/errors, df_v1_7.o is part of the link line.

Full build log: fix_build.log.

Triggerability / threat model (from the finding summary)

The OOB index value comes from a HW register field read at GPU init time on Vega10/Vega12 (soc15.c:521-522 wires df_v1_7_funcs; gmc_v9_0.c:812 calls get_hbm_channel_number on every GPU init/re-init when amdgpu_atomfirmware_get_vram_width() returns 0). The register's value is attacker-influenced through: 1. A malicious ATOM VBIOS ROM on a hotplugged PCIe card; 2. A malicious hypervisor via the SR-IOV VF path; 3. A transient read during early bring-up / reset.

Impact: OOB read 4..24 bytes from kernel .rodata/.data adjacent to the table; the OOB-read value is multiplied by chansize (64 or 128) at gmc_v9_0.c:813 and stored as adev->gmc.vram_width, corrupting later GPUVM/GART/VRAM sizing decisions. Limited info disclosure + possible memory-miscomputation DoS / follow-on corruption in the GMC init path.

Files

  • df2202_trigger.c β€” userspace demonstration of the indexing defect (no kernel memory touched)
  • fix.diff β€” git-apply-able fix mirroring df_v3_6's bounds check
  • build.sh β€” exact build command for the trigger
  • run.sh β€” exact run command for the trigger
  • build.log β€” trigger-stub build output
  • run.log β€” trigger-stub run output (the OOB demonstration)
  • fix_build.log β€” full kernel-module build output (Phase 8), rc=0
  • env.txt β€” guest environment (uname, cc version)
  • VERDICT.md β€” full narrative verdict
  • manifest.json β€” machine-readable catalog
VERDICT.md verdict full narrative verdict with path:line citations
↓ download raw

DF-2202 β€” VERDICT

Verdict: SOURCE-ONLY CONFIRMED (HW-gated). The bug is real; it cannot be triggered at runtime on this audit guest because the vulnerable code lives in the amdgpu DRM module and is only reachable on Vega10/Vega12 dGPU hardware this guest does not have. Verification is by source trace, isolated userspace demonstration of the indexing defect, and a kernel-module build of the fix.

Mechanism

The vulnerable function chain:

soc15.c:521-522   adev->df_funcs = &df_v1_7_funcs;   // CHIP_VEGA10 / CHIP_VEGA12 only
                   (CHIP_VEGA20 uses df_v3_6_funcs, which has the bounds check)

gmc_v9_0.c:812     numchan = adev->df_funcs->get_hbm_channel_number(adev);
gmc_v9_0.c:813     adev->gmc.vram_width = numchan * chansize;   // chansize = 64 or 128

get_hbm_channel_number resolves to df_v1_7_get_hbm_channel_number() (sys/dev/drm/amd/amdgpu/df_v1_7.c:61-68):

static u32 df_v1_7_get_hbm_channel_number(struct amdgpu_device *adev)
{
    int fb_channel_number;

    fb_channel_number = adev->df_funcs->get_fb_channel_number(adev);   // <-- 4-bit value 0..15

    return df_v1_7_channel_number[fb_channel_number];                  // <-- table has 9 entries
}

get_fb_channel_number is df_v1_7_get_fb_channel_number() (lines 50-59):

tmp = RREG32_SOC15(DF, 0, mmDF_CS_AON0_DramBaseAddress0);
tmp &= DF_CS_AON0_DramBaseAddress0__IntLvNumChan_MASK;     // 0x000000F0L
tmp >>= DF_CS_AON0_DramBaseAddress0__IntLvNumChan__SHIFT;  // 0x4
return tmp;                                                 // yields 0..15

Field-width proof (sys/dev/drm/amd/include/asic_reg/df/df_1_7_sh_mask.h): - line 39: #define DF_CS_AON0_DramBaseAddress0__IntLvNumChan__SHIFT 0x4 - line 44: #define DF_CS_AON0_DramBaseAddress0__IntLvNumChan_MASK 0x000000F0L

(x & 0xF0) >> 4 is a 4-bit field β€” 16 possible values (0..15).

The table (sys/dev/drm/amd/amdgpu/df_v1_7.c:30):

static u32 df_v1_7_channel_number[] = {1, 2, 0, 4, 0, 8, 0, 16, 2};

9 elements, indices 0..8.

Values 9..15 therefore index 1..6 u32 words past the end of the array, reading 4..24 bytes of whatever lies adjacent in kernel .rodata/.data. The OOB-read value is then multiplied by chansize (64 or 128) at gmc_v9_0.c:813 and stored as adev->gmc.vram_width, corrupting later GPUVM/GART/VRAM sizing decisions.

Sibling-hardening proof

The exact same pattern, hardened, exists in the sibling sys/dev/drm/amd/amdgpu/df_v3_6.c:62-68:

static u32 df_v3_6_get_hbm_channel_number(struct amdgpu_device *adev)
{
    int fb_channel_number;

    fb_channel_number = adev->df_funcs->get_fb_channel_number(adev);
    if (fb_channel_number >= ARRAY_SIZE(df_v3_6_channel_number))   // <-- the missing check
        fb_channel_number = 0;

    return df_v3_6_channel_number[fb_channel_number];
}

This proves the maintainers know the index needs clamping β€” df_v1_7 was simply missed.

Triggerability

The OOB index comes from a HW register field read at GPU init time. The register's value is attacker-influenced through: 1. A malicious ATOM VBIOS ROM on a hotplugged PCIe card; 2. A malicious hypervisor via the SR-IOV VF path; 3. A transient read during early bring-up / reset.

None of these can be reproduced on this audit guest (no Vega10/Vega12 dGPU). Verification is therefore source-only.

Isolated demonstration

df2202_trigger.c is a userspace program that reproduces the indexing defect in isolation (a verbatim copy of the table and the mask/shift from the kernel headers). Build & run:

table df_v1_7_channel_number[] has 9 entries (indices 0..8)
IntLvNumChan field is 4-bit -> values 0..15

  field= 0..8 -> IN-BOUNDS, value=<table[i]>
  field= 9  ->  INDEX  9  OOB  (u32 read at offset 0 word(s) = 0 byte(s) past end)
  ...
  field=15  ->  INDEX 15  OOB  (u32 read at offset 6 word(s) = 24 byte(s) past end)

7 of 16 possible field values land OUT-OF-BOUNDS on the table.

(See run.log for the full output.)

Phase 8 β€” fix validation (module build with -Werror)

fix.diff adds the same ARRAY_SIZE bounds check that df_v3_6 has:

--- a/sys/dev/drm/amd/amdgpu/df_v1_7.c
+++ b/sys/dev/drm/amd/amdgpu/df_v1_7.c
@@ -63,6 +63,8 @@
    int fb_channel_number;

    fb_channel_number = adev->df_funcs->get_fb_channel_number(adev);
+   if (fb_channel_number >= ARRAY_SIZE(df_v1_7_channel_number))
+       fb_channel_number = 0;

    return df_v1_7_channel_number[fb_channel_number];
 }

On the DragonFly guest (6.5-DEVELOPMENT #0, gcc 8.3), the amdgpu KMOD's natural CWARNFLAGS already includes -Werror (verified in the cc command line captured in fix_build.log). With the patch applied:

  • make df_v1_7.o (just the patched TU) β†’ rc=0, zero warnings/errors.
  • make (full amdgpu.ko, 3.7 MB, includes df_v1_7.o on the link line) β†’ rc=0, zero warnings/errors.

For comparison, the unpatched baseline also builds df_v1_7.o cleanly (make df_v1_7.o β†’ rc=0). The fix introduces no new warnings and the module links successfully.

(An initial attempt with make CWARNFLAGS="-Wall -Werror" failed with -Werror=pointer-sign in sys/sys/hash.h via amdgpu_drv.c β€” but that override removed the module's natural -Wno-pointer-sign, surfacing a pre-existing warning unrelated to this finding. The natural kernel build, which is what ships, does not have this problem.)

Verdict

  • REPRODUCED (source-only). The OOB indexing defect is real and line-by-line confirmed against the audited source.
  • Impact ceiling: OOB read of 4..24 bytes of adjacent kernel memory at GPU init; OOB value multiplied into vram_width and used for VM sizing. Limited info disclosure + memory-miscomputation DoS / follow-on GMC corruption. NOT a write primitive, NOT escalatable to uid=0 (read-only primitive; the only sink is adev->gmc.vram_width, a u32 used as a size).
  • Severity: Medium is appropriate (HW-gated, read-only, requires either a malicious PCIe card, a malicious hypervisor, or a transient read).
  • Fix: VALIDATED β€” fix.diff builds cleanly with -Werror and closes the OOB index by clamping to a safe in-bounds value (0), mirroring the sibling df_v3_6 hardening.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED module build rc=0 -Werror.

amdgpu.ko rc=0.
↓ fix.diffamdgpu.ko module build rc=0

Confirmed kernel references

Detail

Exploit chain

none (read-only OOB).

Evidence (decisive lines)

Source-confirmed OOB.

Verified recommended fix

Add ARRAY_SIZE bounds check mirroring df_v3_6.c:67-68.

Verdict

HW-gated (no AMD GPU). Source-confirmed: df_v1_7 indexes 9-element table with 4-bit field 0..15. Values 9..15 OOB.