# 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`):

```c
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):

```c
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`):
```c
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`:

```c
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:

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