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

amdgpu df_v3_6_funcs is an incomplete vtable: .enable_ecc_force_par_wr_rmw left NULL (latent NULL function-pointer dereference for any Vega20 caller)

Summary

6-of-7 initializer on the VEGA20 table; the sole dispatch is VEGA10-gated (which implements it in v1_7) so dead today - a widened ECC gate on MI50/Radeon VII yields a boot-time NULL-deref. Upstream-inherited. Note the v1_7 sibling does NOT carry the DF-2202 class here (the clamp v1_7 lacks IS present in v3_6). NULL-guard one-liner.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5213 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
trigger.sh β€” 1.3 KB view raw
run.log β€” 1.1 KB view raw
fix.diff β€” 1.8 KB view raw
manifest.json β€” 923 B view raw
verdict.json β€” 3.6 KB view raw

DF-5213 β€” amdgpu df_v3_6_funcs incomplete vtable (NULL enable_ecc_force_par_wr_rmw)

What

struct amdgpu_df_funcs declares seven members (sys/dev/drm/amd/amdgpu/amdgpu.h:771-783), including enable_ecc_force_par_wr_rmw (amdgpu.h:781-782). df_v3_6_funcs (sys/dev/drm/amd/amdgpu/df_v3_6.c:108-116) β€” assigned for CHIP_VEGA20 in soc15.c:519-520 (device IDs incl. the consumer Radeon VII 0x66AF, amdgpu_drv.c:903-909) β€” initializes only six. C designated-initializer semantics zero the seventh: df_v3_6_funcs.enable_ecc_force_par_wr_rmw == NULL.

The sole caller, gmc_v9_0_late_init (sys/dev/drm/amd/amdgpu/gmc_v9_0.c:759), is today gated on adev->asic_type == CHIP_VEGA10 (gmc_v9_0.c:753), which uses df_v1_7_funcs β€” and df_v1_7 does implement the op (df_v1_7.c:105,119). So the NULL slot is never dispatched today; this is a latent hazard: any future Vega20-reachable caller (or a widening of the VEGA10 asic gate on this repeatedly-touched ECC path) turns it into a boot-time NULL function-pointer dereference (panic). DragonFly's port matches upstream Linux here β€” the gap is inherited, not introduced.

Info hardening; no demonstrated impact. The DF-2202 class (v1_7's missing table-bounds guard) does not carry over: v3_6 masks the register field to 4 bits (IntLvNumChan_MASK 0x3C, shift 2 β†’ max 15) and explicitly guards >= ARRAY_SIZE(df_v3_6_channel_number) (15 entries) at df_v3_6.c:67-68, clamping to index 0.

Build / Run

Nothing to build (no userspace trigger exists for an init-path vtable gap). trigger.sh re-derives the proof on any host with kernel source and probes for AMD hardware:

sh trigger.sh      # see run.log for this guest's output

Expected: the grep/sed sections show the 6-of-7 initializer and the VEGA10-gated sole caller; on this guest the hardware probe shows no AMD PCI device at all (AMD_GREP_RC=1, VEGA20_GREP_RC=1), so nothing can manifest at runtime β€” consistent with status=not_reproduced.

Fix

fix.diff β€” one NULL-guard at the sole call site (gmc_v9_0.c:759), the drm convention for optional vtable ops.

VERDICT.md
↓ download raw

DF-5213 β€” VERDICT

Status: not_reproduced (nothing can manifest β€” no AMD hardware on the guest, and the NULL slot has no live caller on any ASIC). Impact: none (latent NULL-deref panic if ever dispatched). Confidence in the code facts: certain.

The facts (source, line-accurate)

  1. struct amdgpu_df_funcs declares .enable_ecc_force_par_wr_rmw (sys/dev/drm/amd/amdgpu/amdgpu.h:781-782).
  2. df_v3_6_funcs (sys/dev/drm/amd/amdgpu/df_v3_6.c:108-116) initializes 6 of the 7 members; C99 designated initializers zero the rest β‡’ .enable_ecc_force_par_wr_rmw == NULL in this table.
  3. df_v3_6_funcs is selected only for CHIP_VEGA20 (soc15.c:519-520); Vega20 PCI IDs include the consumer Radeon VII (amdgpu_drv.c:903-909, 0x66AF), and df_v3_6.c is compiled (sys/dev/drm/amd/amdgpu/Makefile:80).
  4. The only call site of the op is gmc_v9_0_late_init (gmc_v9_0.c:759), gated asic_type == CHIP_VEGA10 && !amdgpu_sriov_vf (gmc_v9_0.c:753) β€” VEGA10 selects df_v1_7_funcs (soc15.c:521-522), which implements the op (df_v1_7.c:105,119). β‡’ the NULL slot is never dispatched on any currently-supported ASIC. Verified exhaustive: rg enable_ecc_force_par_wr_rmw over all of sys/dev/drm/amd/ returns exactly the four lines above.

Conclusion: latent hazard only β€” a widened asic gate or any future Vega20 caller converts it into a boot-time NULL function-pointer dereference (panic). Inherited unchanged from upstream Linux (same struct, same 6-of-7 initializer, same VEGA10-gated caller). Info-grade hardening; the one-line NULL guard in fix.diff is behavior-preserving.

Runtime probe (this guest)

run.log: no vendor-0x1002 PCI device at all (AMD_GREP_RC=1), no 0x66a* Vega20 device (VEGA20_GREP_RC=1), no drm/amd kmods loaded (KLD_GREP_RC=1). There is no runtime path to observe on this guest β€” hence not_reproduced / impact none, per the honest-classification table (expected marker absent, guest stays up).

Negative results (everything else hunted in df_v3_6.c, killed)

  • DF-2202 class (table index OOB): does NOT carry over. get_fb_channel_number masks IntLvNumChan to 4 bits (df_3_6_sh_mask.h:39,44 β€” MASK 0x3C, SHIFT 2 β‡’ max 15), the table has 15 entries (indices 0-14), and β€” unlike df_v1_7 β€” v3_6 guards fb_channel_number >= ARRAY_SIZE(...) and clamps to 0 (df_v3_6.c:67-68). Signed/unsigned in the comparison is harmless (value is a masked u32, never negative). Killed.
  • MGCG enum vs field width: MGCGMode_MASK 0xF (df_3_6_sh_mask.h:34); DF_V3_6_MGCG_ENABLE_15_CYCLE_DELAY = 13 and DISABLE = 0 (df_v3_6.h:29-36) both fit. Killed.
  • gmc/df interplay: get_hbm_channel_number feeds only gmc.vram_width = numchan * chansize (gmc_v9_0.c:812-813); a tripped clamp yields a wrong width (cosmetic), never memory unsafety. Killed.
  • vtable coverage (this finding): the other five ops v3_6 provides (init, enable_broadcast_mode, get_fb_channel_number, get_hbm_channel_number, update_medium_grain_clock_gating, get_clockgating_state) are all implemented; all external df_funcs-> call sites (soc15.c:998,1051, gmc_v9_0.c:759,812) were enumerated and resolve to implemented members except the one filed here. Killed (except DF-5213).
  • No attacker-controlled input reaches this file at all: every path is register R/W during GPU init/clock-gating with values read back from the device's own config space. Killed.

Fix validation

Not performed at runtime (nothing observable; no AMD hardware). The fix.diff NULL-guard is a pure conditional on an existing expression β€” not compile-validated because the full drm/amdgpu module build is disproportionate for an Info-grade latent hazard; patch -p1 --dry-run verified against the pristine tree.

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

No runtime behavior exists to A/B (slot never dispatched; guest has no AMD GPU). fix.diff is a pure NULL-guard on an existing expression, patch -p1 --dry-run clean against the pristine tree; full drm module build skipped as disproportionate for an Info-grade latent hazard.

fix.diff; run.log (AMD_GREP_RC=1 / VEGA20_GREP_RC=1 hardware absence)
↓ fix.diffper-fix-DF-5213

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (guest /usr/src grep showing the 6-of-7 initializer, the sole VEGA10-gated call site, the df_v1_7 implementation, and the no-AMD-hardware probe); VERDICT.md (line-accurate trace + five negative-result classes); fix.diff (one-line NULL guard, patch -p1 --dry-run clean); trigger.sh (re-derives everything on any DF host)

PoC changes

Authored fresh (no seed); trigger.sh is a source-trace + hardware-probe script because no userspace trigger exists for an init-path vtable gap.

Verified recommended fix

NULL-guard the sole indirect call: if (adev->df_funcs->enable_ecc_force_par_wr_rmw) adev->df_funcs->enable_ecc_force_par_wr_rmw(adev, false); at gmc_v9_0.c:759 (see fix.diff).

Verdict

Latent-hazard Info finding, code facts certain, nothing manifests. df_v3_6_funcs (sys/dev/drm/amd/amdgpu/df_v3_6.c:108-116, selected for CHIP_VEGA20 at soc15.c:519-520; compiled per amdgpu/Makefile:80) initializes 6 of the 7 members of struct amdgpu_df_funcs (amdgpu.h:771-783), leaving .enable_ecc_force_par_wr_rmw NULL by designated-initializer rules. The sole call site (gmc_v9_0.c:759) is gated on asic_type == CHIP_VEGA10 (gmc_v9_0.c:753), which uses df_v1_7_funcs where the op IS implemented (df_v1_7.c:105,119) - exhaustive grep confirms no other caller - so the slot is never dispatched today; a widened asic gate or future Vega20 caller converts it into a boot-time NULL function-pointer dereference (panic). Inherited unchanged from upstream Linux. Guest runtime probe: no vendor-0x1002 PCI device at all (run.log: AMD_GREP_RC=1, VEGA20_GREP_RC=1, no drm/amd kmods) - nothing observable, hence not_reproduced/impact none. All other classes hunted in df_v3_6.c are killed with line cites in VERDICT.md, notably: the DF-2202 table-index OOB class does NOT carry over (4-bit field mask 0x3C>>2 max 15 vs 15-entry table PLUS an explicit >= ARRAY_SIZE clamp at df_v3_6.c:67-68, which df_v1_7 lacked); MGCG enum values fit the 0xF field mask; get_hbm_channel_number feeds only vram_width (no memory-safety sink).