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

dce80 command-table helper leaves 2 of 17 vtable members NULL (dc_clock_type_to_atom, transmitter_color_depth_to_atom); the SetPixelClock-V7 rev handlers call transmitter_color_depth_to_atom unguarded β€” latent NULL-function-pointer kernel panic under cross-generation VBIOS pairing on DCE8 silicon

Summary

The designated initializer omits the last two members while set_pixel_clock_v7 (wired purely from the ROM's SetPixelClock revision) calls one unguarded at command_table.c:1188/command_table2.c:308. Genuine DCE8 VBIOSes carry V6 (radeon-proven - no V7 path exists there), so the trigger requires a cross-flashed/crafted VBIOS (reflash-gated, like DF-4985). Completes/corrects the DF-4985 matrix (its count missed these two call sites; its dce80-implements-them premise held only for 3 members). Semantics demo on guest (SIGSEGV at +0x80); vtable-completion fix (with the DF-4935 default fixed in the copy).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5000 Β· 10 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 7.5 KB ↓ raw
trigger_nullcall.c β€” 3.1 KB view raw
build.sh β€” 106 B view raw
run.sh β€” 108 B view raw
build.log β€” 32 B view raw
run.log β€” 352 B view raw
null_member_matrix.log β€” 4.9 KB view raw
env.txt β€” 163 B view raw
fix.diff β€” 2.8 KB view raw

DF-5000 β€” dce80 command-table helper: 2 of 17 vtable members NULL, V7 rev handlers call one of them unguarded

Severity: Info (hardening, DF-4985 family shape) Β· CWE-476 Β· confidence: certain PoC status: untested (hardware-gated: needs AMD DCE8 silicon paired with a SetPixelClock-V7-generation VBIOS; the audit guest has no AMD GPU β€” verified in env.txt β€” so the kernel path cannot attach, let alone fault). A guest userspace semantics demo of the exact C pattern (designated-initializer zero-fill + unguarded indirect call) is included and passes.

The defect (all statically proven, see null_member_matrix.log)

  • struct command_table_helper declares 17 function-pointer members (sys/dev/drm/amd/display/dc/bios/command_table_helper_struct.h:33-63).
  • The dce80 initializer (sys/dev/drm/amd/display/dc/bios/dce80/command_table_helper_dce80.c:329-349) assigns 15. dc_clock_type_to_atom and transmitter_color_depth_to_atom are absent β‡’ NULL by C99 6.7.8p10 zero-fill.
  • set_pixel_clock_v7 calls bp->cmd_helper->transmitter_color_depth_to_atom(...) with no NULL check at command_table.c:1188 and command_table2.c:308-311.
  • The V7 handler is wired purely from the ROM's SetPixelClock parameter revision (command_table.c:918-939, command_table2.c:244-256); the parser wrapper guards only the rev-handler pointer (bios_parser.c:766), never the helper member.

Reachability

command_table_helper.c:39-43 and command_table_helper2.c:40-44 select the dce80 table for DCE_VERSION_8_0/8_1/8_3 (KV/KB/BV Sea Islands; dc_resource.c:57-65). Genuine DCE8 VBIOSes carry SetPixelClock V6 β€” proven by radeon driving the same ROMs with only V5/V6 handlers (sys/dev/drm/radeon/atombios_crtc.c:760-766,916; no V7 path exists there) β€” so genuine pairings take set_pixel_clock_v6, which never touches the NULL member. Trigger: DCE8 ASIC + cross-generation/malicious ROM reporting SetPixelClock rev 7 (physical reflash or crafted add-in card) β†’ first mode set β†’ call through NULL+0x80 β†’ kernel panic in display bring-up.

dc_clock_type_to_atom (the other NULL member) is triply dead on DCE8 (no DC-side caller β€” dce/dce_clocks.c:298-334 is wired only for dce110/112/120/dcn10 clock managers; ROM must carry the DCE11.2-era SetDCEClock command; bios_parser.c:778 NULL-checks the rev handler) β€” hardening-only, fixed by the same diff.

This also refines DF-4985's premise ("the dce80 table implementing them"): dce80 implements the three members dce110 NULLs explicitly, but not these two; the set_pixel_clock_v7 call sites (command_table.c:1188 / command_table2.c:310) were missing from DF-4985's six-site matrix.

Reproduce the demo

# on the DragonFly audit guest (or any POSIX box with a C compiler):
cc -O2 -Wall -o trigger_nullcall trigger_nullcall.c
./trigger_nullcall; echo $?
# expected:
#   transmitter_color_depth_to_atom= 0x0  <- must be NULL (C99 6.7.8p10)
#   calling set_pixel_clock_v7 line-1188 shape...
#   FAULT: called through NULL member at address 0x0 (offsetof ... = 0x80)
#   SEMANTICS DEMO PASSED ...
#   exit=42

See build.log / run.log for the captured guest run (DragonFly 6.5-DEVELOPMENT, x86_64). Full analysis: VERDICT.md. Fix: fix.diff (git apply -p0, check-verified).

VERDICT.md
↓ download raw

DF-5000 VERDICT β€” dce80 helper leaves 2/17 vtable members NULL; the SetPixelClock-V7 rev handlers call transmitter_color_depth_to_atom unguarded

Status: untested (Info severity, hardware-gated). The kernel path requires AMD DCE8 silicon (Sea Islands: Bonaire/Kaveri/Kabini/Mullins) paired with a VBIOS whose SetPixelClock command-table parameter revision is 7 β€” a cross-generation pairing. The audit QEMU guest has no AMD GPU at all (env.txt: NO_AMD_GPU), so amdgpu never attaches and the path cannot be entered, let alone faulted. All structural claims below are proven statically in this tree (null_member_matrix.log), and the C semantics (designated-initializer zero-fill + unguarded indirect call β‡’ fault at member offset) were demonstrated as a userspace harness on the DragonFly guest (run.log, exit 42). Line references verified 2026-09-25.

1. What the file is and who reaches it

command_table_helper_dce80.c builds the single shared struct command_table_helper vtable (:329-349) returned by dal_cmd_tbl_helper_dce80_get_table() (:351-354). It is compiled into the amdgpu kmod (sys/dev/drm/amd/amdgpu/Makefile:214) and is selected for DCE_VERSION_8_0/8_1/8_3 (core/dc_resource.c:57,63,65) by both parser dispatchers: v1 command_table_helper.c:39-43 and v2 command_table_helper2.c:40-44. Parser choice is firmware_parser_create first, v1 fallback (bios_parser_interface.c:41-45); both construct for DCE8 and both ignore the dispatcher's return value (bios_parser2.c:1987, bios_parser.c:2936).

2. The NULL members (proven)

The struct declares 17 members (command_table_helper_struct.h:33-63). The dce80 initializer lists 15 designators (:330-348). Missing:

member consumer call sites guard at call site
transmitter_color_depth_to_atom command_table.c:1188 (v1), command_table2.c:308-311 (v2) β€” both inside set_pixel_clock_v7 none
dc_clock_type_to_atom command_table.c:2437 (v1), command_table2.c:722 (v2) β€” inside set_dce_clock_v1 bool-return checked, but the call itself goes through the pointer

C99 6.7.8p10 zero-fills unnamed members of a designated-initializer aggregate β‡’ both are NULL. Contrast: dce112 β€” the only table of the DCE11.2 (Polaris) generation that V7 pixel-clock ROMs genuinely pair with β€” implements both (command_table_helper_dce112.c:358-376,401-402); dce110 also leaves both NULL (counted in DF-4985's five, but with no call site listed for them there).

3. The panic chain (transmitter_color_depth_to_atom)

  1. DCE8 ASIC, amdgpu DC attaches; dce80 helper selected.
  2. ROM reports SetPixelClock parameter revision 7 β‡’ init_set_pixel_clock wires set_pixel_clock_v7 (command_table.c:930-931; v2 twin command_table2.c:247-248). Other revisions wire v3/5/6 or leave NULL (graceful BP_RESULT_FAILURE via bios_parser.c:766-767).
  3. First display enable / mode set β‡’ bios_parser_set_pixel_clock β‡’ set_pixel_clock_v7.
  4. :1159 clock_source_id_to_atom (implemented, returns true), :1160 controller_id_to_atom (implemented) β‡’ body entered.
  5. :1188 bp->cmd_helper->transmitter_color_depth_to_atom(color_depth) β€” call through NULL+0x80 β‡’ immediate kernel page fault / panic in display bring-up. No check exists anywhere between the dispatcher and the call.

4. Why genuine hardware is safe (and what the real trigger is)

Genuine DCE8 VBIOSes are SetPixelClock V6: radeon drives the same Sea Islands ROMs (the same AtomBIOS command table) with a V5/V6-only implementation (sys/dev/drm/radeon/atombios_crtc.c:760-766 union, :916 case 6:), and has no V7 path at all β€” if any genuine DCE8 ROM reported rev 7, radeon CIK modesetting would have no handler and would fail visibly. The V7 parameter revision belongs to the DCE11.2+ (Polaris/DCE12) ROM generation, whose ASICs select the dce112 table that implements the member. Hence:

  • Genuine pairing: DCE8 ROM (V6) β†’ set_pixel_clock_v6 (command_table.c:1070-1146) never touches the member. No crash β€” consistent with CIK DC working upstream.
  • Attack pairing: DCE8 silicon + ROM built for a V7 generation (physical reflash of the card's own VBIOS, or a malicious/crafted add-in card / eGPU ROM). Then step 2-5 above fire on the first mode set.

dc_clock_type_to_atom is triply dead on DCE8: (a) its only DC-side callers (dce/dce_clocks.c:298-334) are constructed solely for dce110/112/120/dcn10 display-clock managers β€” dce80_resource.c builds no such clock manager; (b) the set_dce_clock rev handler is wired only if the ROM carries the SetDCEClock command (DCE11.2-era); (c) bios_parser_set_dce_clock NULL-checks the rev handler (bios_parser.c:778-779). It is fixed by the same vtable completion for defense-in-depth.

5. What is NOT wrong here (negative results, pass-2 depth)

  • dig_encoder_sel_to_atom (:239-271): default maps to DIGA_SEL and the function returns the computed variable β€” the DF-2030/DF-4934/DF-4936 dead-return-0 shape does not exist in dce80.
  • engine_bp_to_atom (:61-104) / clock_source_id_to_atom (:106-150): default leaves the output unwritten but returns false, and every consumer checks the bool before use (command_table.c:1159,1951-1955,1990-1994; command_table2.c:271-273,509) β€” the DF-4935 uninit-output shape does not exist here (unlike dce112's dc_clock_type_to_atom, dce80's clock_source function correctly returns false).
  • encoder_action_to_atom / disp_power_gating_action_to_atom defaults β†’ BREAK_TO_DEBUGGER + 0: byte-identical family shape (dce110 :300-302 etc.), not a per-file defect.
  • clock_source_id_to_atom_phy_clk_src_id >>2 (:175) and hpd_sel_to_atom >>4 (:236): family-identical (dce110 :120,151; dce112 :117); constants sane (ATOM_TRANSMITTER_CONFIG_V5_P0PLL=0x08, HPD1_SEL=0x10, atombios.h:1543,1550).
  • All 5 shared dal_cmd_table_helper_* bindings resolve to the shared command_table_helper.c implementations (pass-1 audited clean).

6. Family attribution

  • DF-4985 (dce110, pass-2): same family; its five-NULL-member count included these two members but its six-site unguarded-caller matrix omitted the set_pixel_clock_v7 sites (command_table.c:1188, command_table2.c:308-311) β€” this finding completes that matrix and corrects its premise that "the dce80 table implementing them" covers everything dce110 NULLs (true only for the three explicitly-NULLed members).
  • DF-2030/DF-4934/DF-4936 (dead return-0): shape absent in dce80 (see Β§5).
  • DF-4935/DF-4937 (uninit output + return true): shape absent in dce80 (Β§5); note the copy in fix.diff deliberately fixes that defect in the implanted dc_clock_type_to_atom (default β†’ output written, ret_code = false).

7. Fix

fix.diff (git-apply-able, git apply -p0, check-verified against the pristine tree): implant transmitter_color_depth_to_atom (dce112 :358-376 semantics, dce80's BREAK_TO_DEBUGGER style) and dc_clock_type_to_atom (dce112 :333-355 semantics with the DF-4935 defect fixed) and bind both in the vtable. After the fix no dce80 member is NULL and the four consumer call sites are unconditionally safe. Fix validation on hardware is impossible on this guest (fix_status: not_testable).

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

not_testable - requires AMD DCE8 hardware with a generation-mismatched (SetPixelClock-V7) VBIOS; the audit guest has no AMD GPU so neither baseline nor patched kernel path can execute. fix.diff is git-apply check-verified against the pristine sys/ tree and mirrors the only in-tree implementation of the member (dce112).

findings/poc/DF-5000/fix.diff (git apply -p0 --check passes; never applied to sys/)
↓ fix.diffper-fix-DF-5000

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5000/VERDICT.md sections 2-4 (the 17-vs-15 vtable proof, the unguarded call sites, the radeon V6 generation-lock proof) and section 5 (negative results); null_member_matrix.log (static proof matrix); run.log (guest semantics demo: NULL member + fault at 0x80, exit 42); env.txt (guest has no AMD GPU); fix.diff (git apply -p0 check-verified)

PoC changes

n/a - evidence pack authored fresh in pass 2 (no seed). trigger_nullcall.c is a guest/userspace semantics harness, not a kernel PoC: no AMD GPU exists on the audit guest so no kernel path can attach.

Verified recommended fix

Complete the vtable: implant transmitter_color_depth_to_atom (dce112 :358-376 semantics) and dc_clock_type_to_atom (dce112 :333-355 semantics with the DF-4935 defect fixed) and bind both in dce80's initializer - see fix.diff; alternatively NULL-guard command_table.c:1188 and command_table2.c:308.

Verdict

Pass-2 depth on the dce80 command-table helper (pass-1: clean; this is the family's last helper). The file is live code compiled into the amdgpu kmod (Makefile:214) and its vtable is selected for DCE_VERSION_8_0/8_1/8_3 (Sea Islands) by BOTH parser dispatchers (command_table_helper.c:39-43, command_table_helper2.c:40-44). The struct declares 17 members (command_table_helper_struct.h:33-63) but the dce80 initializer (:329-349) assigns only 15: dc_clock_type_to_atom and transmitter_color_depth_to_atom are NULL via C99 designated-initializer zero-fill. set_pixel_clock_v7 calls transmitter_color_depth_to_atom UNGUARDED at command_table.c:1188 and command_table2.c:308-311, and the V7 handler is wired purely from the ROM's SetPixelClock parameter revision (command_table.c:918-939, command_table2.c:244-256); the parser wrapper guards only the rev handler, not the helper member (bios_parser.c:766). Chain: DCE8 ASIC + ROM reporting SetPixelClock rev 7 -> first mode set -> call through NULL+0x80 -> kernel panic in display bring-up. Genuine DCE8 VBIOSes are SetPixelClock V6 - proven by radeon driving the same ROMs with a V5/V6-only implementation (atombios_crtc.c:760-766, case 6 at :916, no V7 path exists) - so genuine pairings take set_pixel_clock_v6 which never touches the member; V7 is the DCE11.2+ (Polaris) generation, whose ASICs select dce112, the only table implementing it (command_table_helper_dce112.c:358-376,402). Trigger therefore requires a cross-generation/malicious VBIOS on DCE8 silicon (physical reflash or crafted add-in card) - same threat class as DF-4985 (Info, reflash-gated). dc_clock_type_to_atom (second NULL member) is triply dead on DCE8: no DC-side caller (dce_clocks.c:298-334 wired only for dce110/112/120/dcn10; dce80_resource.c builds none), ROM must carry the DCE11.2-era SetDCEClock command, and bios_parser.c:778 NULL-checks the rev handler - fixed by the same vtable completion. This finding also completes/corrects DF-4985's matrix: its five-NULL count included these two members but its six unguarded call sites omitted command_table.c:1188 / command_table2.c:310, and its premise 'dce80 implements them' holds only for the three members dce110 explicitly NULLs. Negative results (verified): no DF-2030 dead-return-0 shape (dig_encoder_sel default maps DIGA_SEL and returns the variable, :265-270); no DF-4935 uninit-output shape (engine_bp_to_atom/clock_source_id_to_atom return false on default and all consumers check the bool - command_table.c:1159,1951,1990; command_table2.c:271,509); >>2/>>4 shift helpers byte-identical to dce110:120,151 and dce112:117 (family norm); BREAK_TO_DEBUGGER defaults family-identical. Guest userspace semantics demo of the exact C pattern (zero-filled member + unguarded indirect call -> fault at offset 0x80) ran on the DragonFly guest, exit 42 (run.log); kernel-path reproduction impossible on this guest - no AMD GPU (env.txt: NO_AMD_GPU).