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

Dead FPGA-diagnostics hw_translate installs an all-NULL function table: activating the MAXIMUS env would turn every GPIO id/offset translation into an immediate NULL function-pointer call (kernel panic)

Summary

Both funcs members explicitly NULL while init reports success; every consumer calls them unconditionally (gpio_service.c:151/:418, gpio_base.c:144) - first GPIO op = call through NULL = panic. Provably dead on this tree (sole gate IS_FPGA_MAXIMUS_DC; only dce_environment assignment tree-wide is the hardcoded DCE_ENV_PRODUCTION_DRV at amdgpu_dm.c:424; no sysctl/ioctl/modpath). Family-consistent with DF-5231 (hw_factory_diag). False-returning stub fix; closes the gpio/ family.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5236 Β· 5 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
verdict.json β€” 4.1 KB view raw
fix.diff β€” 1.1 KB view raw
manifest.json β€” 1014 B view raw

DF-5236 β€” hw_translate_diag.c all-NULL function table (Info, latent, FPGA-gated)

File: sys/dev/drm/amd/display/dc/gpio/diagnostics/hw_translate_diag.c (pass 2)

What

funcs (hw_translate_diag.c:34-37) is a struct hw_translate_funcs with both members explicitly NULL (offset_to_id = NULL, id_to_offset = NULL). dal_hw_translate_diag_fpga_init() (hw_translate_diag.c:39-42) installs this table and returns β€” reporting success β€” with no functional translator whatsoever.

Why it is only Info (unreachable on this tree)

The sole caller is dal_hw_translate_init() (hw_translate.c:61-63), which installs the diag table only when IS_FPGA_MAXIMUS_DC(dce_environment) is true (dc_types.h:58-59, i.e. dce_environment == DCE_ENV_FPGA_MAXIMUS).

enum dce_environment (dc_types.h:46-55) has DCE_ENV_PRODUCTION_DRV = 0. The only assignment in the entire driver is hardcoded:

sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:424: init_data.dce_environment = DCE_ENV_PRODUCTION_DRV;

copied verbatim into the dc context at core/dc.c:624 and passed to dal_gpio_service_create() at core/dc.c:661-663. There is no module parameter, sysctl, ioctl, debugfs entry, or device property in this tree that can select DCE_ENV_FPGA_MAXIMUS. Zero-initialization is also safe (0 == production). dal_hw_translate_diag_fpga_init is therefore dead code on DragonFlyBSD β€” same proof shape as the i2caux diag twin and DF-5231 (hw_factory_diag.c, Info).

Impact if ever activated (e.g. someone flips the env or imports the diag env)

First GPIO operation would be an immediate NULL function-pointer call β€” kernel panic at boot/DC-init:

gpio_service.c:151 service->translate.funcs->offset_to_id(...) (create_irq) gpio_service.c:418 service->translate.funcs->offset_to_id(...) (create_ddc) gpio_base.c:144 gpio->service->translate.funcs->id_to_offset(...) (get_pin_info)

Not attacker-steerable: the table is a compile-time constant of NULLs; there is no write primitive, only call-through-NULL. Panic-only, latent, gated off.

Negative results (why nothing worse hides here)

  • Unknown-ASIC path cannot leak the NULL table: dal_hw_translate_init() default case returns false (hw_translate.c:92-94) and dal_gpio_service_create() handles it (gpio_service.c:69-73: BREAK_TO_DEBUGGER; goto failure_1; kfree; return NULL) β€” production always gets a fully-populated ASIC table.
  • No other caller of dal_hw_translate_diag_fpga_init exists (tree-wide grep).
  • Nothing else in the 42-line file: no state, no allocation, no locking concerns.

PoC status

untested β€” static-analysis-only Info/hardening finding. The trigger requires a source change (forcing DCE_ENV_FPGA_MAXIMUS), i.e. it is not reachable on a shipped kernel; per audit contract Phase V is not run for Info findings. If a maintainer wants a live demonstration, the 2-line patch in fix.diff applied together with init_data.dce_environment = DCE_ENV_FPGA_MAXIMUS; panics at dal_gpio_service_create_irq on any amdgpu ASIC (expected call 0x0 trap).

Fix

See fix.diff: replace the NULL entries with explicit false-returning stubs (or delete the diagnostics twins from the build β€” Makefile:252 builds them for nothing). Family-consistent with DF-5231's recommendation.

VERDICT.md
↓ download raw

DF-5236 β€” VERDICT

Status: untested (static-analysis-only Info finding; Phase V legitimately skipped for Info hardening findings per audit contract). Reproduced: no. Impact: none as shipped. Confidence: certain (reachability proven dead by exhaustive grep + constant-flow analysis).

The finding

sys/dev/drm/amd/display/dc/gpio/diagnostics/hw_translate_diag.c:34-37 installs a struct hw_translate_funcs whose both members (offset_to_id, id_to_offset) are explicitly NULL, and dal_hw_translate_diag_fpga_init() (hw_translate_diag.c:39-42) reports success while installing nothing functional. Every consumer calls these pointers unconditionally:

β†’ immediate call-through-NULL β†’ kernel panic.

Reachability β€” proven dead on this tree

  1. Sole installer entry: dal_hw_translate_init() (hw_translate.c:61-64) takes the diag branch only when IS_FPGA_MAXIMUS_DC(dce_environment) (dc_types.h:58-59 β†’ dce_environment == DCE_ENV_FPGA_MAXIMUS).
  2. enum dce_environment (dc_types.h:46-55): DCE_ENV_PRODUCTION_DRV = 0, DCE_ENV_FPGA_MAXIMUS = 1, DCE_ENV_DIAG = 2. Zero-init is production β€” safe.
  3. Tree-wide grep for dce_environment = / DCE_ENV_FPGA_MAXIMUS: the only value assignment is sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:424 init_data.dce_environment = DCE_ENV_PRODUCTION_DRV; (hardcoded constant). It is copied at core/dc.c:624 and consumed at core/dc.c:661-663 (dal_gpio_service_create(dc_version, dc_ctx->dce_environment, ctx)). No sysctl, module parameter, ioctl, or firmware path can change it.
  4. Failure path is handled: for an ASIC version missing from the switch, dal_hw_translate_init() returns false (hw_translate.c:92-95) and dal_gpio_service_create() frees and returns NULL (gpio_service.c:69-73, 137-140) β€” a NULL table can never reach production via the version switch either.

Conclusion: dal_hw_translate_diag_fpga_init is dead code; the NULL-call hazard is latent only, exactly like its twin DF-5231 (hw_factory_diag.c, Info) and the i2caux diag twin (unreachable-proven). Family treatment consistent: Info.

Why no exploit chain exists

The hazard is a call through a compile-time-constant NULL in a static const table. There is no attacker-controllable pointer value, no adjacent state to corrupt, and no reachable path in a shipped kernel. Worst case (developer flips the env constant): instant panic at amdgpu DC init β€” a DoS you did to your own rebuilt kernel, not a vulnerability.

What was hunted and killed (pass-2 depth for this 42-LOC file)

  • All-NULL table walk-in: consumers enumerated above; all gated behind the dead env.
  • Version-switch default leak: killed (gpio_service.c:69-73 handles false).
  • hw_translate.funcs left dangling on destroy: dal_gpio_service_destroy() frees only the busyness slots (gpio_service.c:159-175); translate is by-value inside the service β€” no dangling pointer.
  • Build inclusion: SRCS+= hw_translate_diag.c hw_factory_diag.c (sys/dev/drm/amd/amdgpu/Makefile:252) β€” compiled into the amdgpu kernel module, so the dead code ships but never executes.
  • Race/refcount/lock surfaces: none β€” the file has no state, no allocation, no locks; it is a 4-line initializer.

Fix

fix.diff in this directory replaces the NULL entries with explicit false-returning stubs, making the table safe to call even if the MAXIMUS env is ever enabled (family-consistent with DF-5231's recommendation). Alternative: drop the two diagnostics objects from the build entirely.

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 not built/tested: the finding is Info and unreachable on shipped kernels (hardcoded DCE_ENV_PRODUCTION_DRV at amdgpu_dm.c:424), so there is no observable baseline behavior to A/B on the guest. fix.diff is a defensive compile-level change (NULL entries -> false-returning stubs) authored against the read-only sys/ tree and never applied there.

findings/poc/DF-5236/fix.diff
↓ fix.diffper-fix-DF-5236

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5236/VERDICT.md (full reachability proof); hw_translate_diag.c:34-42 (all-NULL table + init); hw_translate.c:61-64 (sole caller, FPGA gate); dc_types.h:46-59 (enum + IS_FPGA_MAXIMUS_DC); amdgpu_dm.c:424 (hardcoded production env); gpio_service.c:151,418 and gpio_base.c:144 (unconditional NULL-call sites); gpio_service.c:69-73 (unknown-ASIC failure handled, NULL table cannot leak); Makefile:252 (compiled but dead).

PoC changes

No PoC authored: the trigger requires a source-level change (forcing DCE_ENV_FPGA_MAXIMUS at amdgpu_dm.c:424), i.e. not reachable on a shipped kernel; recorded honestly as untested rather than fabricating a runnable trigger.

Verified recommended fix

Replace the two NULL entries in hw_translate_diag.c's funcs table with explicit false-returning stubs (see findings/poc/DF-5236/fix.diff), or drop hw_translate_diag.c/hw_factory_diag.c from the amdgpu build (Makefile:252).

Verdict

Info (latent, unreachable). hw_translate_diag.c:34-37 installs an all-NULL struct hw_translate_funcs and dal_hw_translate_diag_fpga_init (hw_translate_diag.c:39-42) reports success with no functional translator; the first GPIO operation (gpio_service.c:151 offset_to_id, gpio_service.c:418 offset_to_id, gpio_base.c:144 id_to_offset) would be an immediate NULL function-pointer call => kernel panic. Reachability is proven dead on this tree: the sole entry (hw_translate.c:61-64) is gated on IS_FPGA_MAXIMUS_DC (dc_types.h:58-59) and the only dce_environment assignment anywhere is the hardcoded DCE_ENV_PRODUCTION_DRV at amdgpu_dm.c:424 (copied at core/dc.c:624, consumed at core/dc.c:661-663); no sysctl/ioctl/module parameter can select DCE_ENV_FPGA_MAXIMUS, and enum zero-init is production (dc_types.h:47). Family-consistent with DF-5231 (hw_factory_diag, Info) and the unreachable i2caux diag twin. Panic-only even if activated; the NULLs are compile-time constants, so there is no attacker-steerable pointer and no exploit chain. Phase V skipped per contract for Info hardening findings.