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)
PoC verification
Evidence pack
findings/poc/DF-2202 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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(fullamdgpu.kolink, 3.7 MB) β rc=0, zero warnings/errors,df_v1_7.ois 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 mirroringdf_v3_6's bounds checkbuild.shβ exact build command for the triggerrun.shβ exact run command for the triggerbuild.logβ trigger-stub build outputrun.logβ trigger-stub run output (the OOB demonstration)fix_build.logβ full kernel-module build output (Phase 8), rc=0env.txtβ guest environment (uname, cc version)VERDICT.mdβ full narrative verdictmanifest.jsonβ machine-readable catalog
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(fullamdgpu.ko, 3.7 MB, includesdf_v1_7.oon 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_widthand used for VM sizing. Limited info disclosure + memory-miscomputation DoS / follow-on GMC corruption. NOT a write primitive, NOT escalatable touid=0(read-only primitive; the only sink isadev->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.diffbuilds cleanly with-Werrorand closes the OOB index by clamping to a safe in-bounds value (0), mirroring the siblingdf_v3_6hardening.
Fix verification
fixedVALIDATED module build rc=0 -Werror.
amdgpu.ko rc=0.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- d
- f
- _
- v
- 1
- _
- 7
- .
- c
- :
- 6
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- d
- f
- _
- v
- 1
- _
- 7
- .
- c
- :
- 6
- 5
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.
No comments yet.