⬢ DragonFlyBSD Kernel Audit
← triage · dashboard
DF-4985

dce110 command-table helper leaves five vtable members NULL while six consumer call sites invoke them unguarded — latent NULL-function-pointer call under ASIC/VBIOS generation mismatch (generation-locked, unreachable on genuine hardware)

Summary

Live code (both parser dispatchers for Tonga/Fiji/Carrizo) with 5 NULL members vs 6 unguarded callers (command_table.c:204/219/507/638/678/2437, command_table2.c:722) - a cross-generation VBIOS pairing would call through NULL. Proven generation-locked-unreachable on genuine hw (the NULL-binding rev handlers exist only in DCE8-era firmware that selects the dce80 table implementing them; set_dce_clock wired only into dce112/120/dcn1). Reflash-gated; Info hardening. Reuse-shared-implementations fix (dce80 style).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4985 · 5 files
FileTypeDescriptionSize
README.md — 6.1 KB ↓ raw
VERDICT.md — 7.2 KB ↓ raw
verdict.json — 4.1 KB view raw
manifest.json — 998 B view raw
null_matrix_proof.log — 3.1 KB view raw
README.md
↓ download raw

DF-4985 — dce110 command-table helper: five NULL vtable members vs unguarded consumer call sites (latent NULL-func-call under ASIC/VBIOS generation mismatch); DF-2030 family attribution completed

File: sys/dev/drm/amd/display/dc/bios/dce110/command_table_helper_dce110.c (364 LOC) Severity: Info (hardware-gated latent; unreachable on genuine hardware) · Confidence: certain (dead shapes), likely (mismatch reachability) Pass 2 (GLM 5.3) · known pass-1 finding on this file: DF-2030 (dead return 0 in dig_encoder_sel_to_atom, lines 154-190) — not re-reported here Verdict: untested (Info hardening; guest has no AMD GPU — amdgpu never attaches; no hardware path exists on the audit guest)

Liveness (who consumes this table)

  • Compiled into the amdgpu kmod: sys/dev/drm/amd/amdgpu/Makefile:217 (SRCS+= command_table_helper_dce110.c).
  • Selected for DCE_VERSION_10_0 (Tonga/Fiji) and DCE_VERSION_11_0 (Carrizo) — sys/dev/drm/amd/display/dc/core/dc_resource.c:68,72-75.
  • Via both parsers: v1 command_table_helper.c:46,50 (old-VBIOS fallback) and v2 command_table_helper2.c:47,51 (firmware parser, selected first by bios_parser_interface.c:41-45; bios_parser2.c requires ROM header 2.2+ / object info 1.4+).

The latent matrix

command_table_helper_dce110.c:332-349 leaves five members of struct command_table_helper (command_table_helper_struct.h:34-64) NULL:

member dce110 dce80 dce112 unguarded consumer
assign_control_parameter NULL (:344) impl — command_table.c:204,219 (encoder_control_dig1_v1/dig2_v1)
clock_source_id_to_ref_clk_src NULL (:345) impl — command_table.c:678 (transmitter_control_v4)
transmitter_bp_to_atom NULL (:346) impl — command_table.c:507,638,764 (transmitter_control_v2/v3/v4)
dc_clock_type_to_atom NULL (implicit) — impl command_table.c:2437 and command_table2.c:722 (set_dce_clock_v2_1)
transmitter_color_depth_to_atom NULL (implicit) — impl (dce112+ paths only)

None of the six call sites NULL-checks the function pointer before calling → a generation-mismatched pairing (DCE10/11 ASIC + VBIOS exposing rev-2/3/4 transmitter tables, rev<3 DIG encoder tables, or a SetDCEClock table) would NULL-func-call → panic in display init.

Why it is unreachable on genuine hardware (the version matrix is coherent by design)

  • The rev-2/3/4 transmitter handlers that use transmitter_bp_to_atom / clock_source_id_to_ref_clk_src bind only from init_transmitter_control (command_table.c:367-396) when the card's VBIOS UNIPHYTransmitterControl crev is 2/3/4 — DCE8-generation firmware, which pairs with the dce80 table (implements both). DCE10/11 firmware uses crev 5/6 → transmitter_control_v1_5/v1_6, which use only implemented members (phy_id_to_atom, hpd_sel_to_atom, dig_encoder_sel_to_atom, clock_source_id_to_atom).
  • Same logic for assign_control_parameter: dig1/dig2_v1 bind for VBIOS DIG encoder control rev < 3 (DCE8-era); DCE10/11 use encoder_control_digx_v3+.
  • set_dce_clock (command_table.c:2437 / command_table2.c:722) is reached only from dce112_set_clock (dce/dce_clocks.c:292-340), which is wired exclusively into dce112_funcs/dce120_funcs/dcn1_funcs (dce_clocks.c:745,752,758). DCE10/11 resources wire dce110_funcs (dce_clocks.c:762-767, set_dispclk = dce_psr_set_clock → dce_set_clock → program_display_engine_pll, never set_dce_clock). DCE11.2+ (which does call it) uses the dce112 table, which implements dc_clock_type_to_atom.
  • The parser wrappers guard only their own cmd_tbl.set_dce_clock NULL (bios_parser.c:778, bios_parser2.c:1040), not the helper member — irrelevant given the above.

Residual trigger: a transplant/mismatched VBIOS (DCE10/11 card flashed with DCE8-era command tables, or a DCE10/11-class card whose dccg funcs got swapped) — i.e., an attacker who already controls the machine's firmware/hardware. Out of the local/remote attacker bar.

Family attribution checks requested by the orchestrator

  1. DF-2030 attribution: confirmed filed on this file (lines 154-189, return 0 discarding the computed DIGx_SEL mapping) — pass 1, not re-reported. Callers command_table.c:798,855, command_table2.c:213 feed the always-0 into AtomBIOS params (uint8_t field; no memory-safety consequence; upstream-intended per DF-4934 analysis).
  2. DF-4935 shape (dc_clock_type_to_atom uninit-output): does NOT exist in dce110 — this file has no dc_clock_type_to_atom implementation at all (implicit NULL vs dce112's implemented-but-uninit-in-default). Distinct shape, filed here as the NULL-member matrix.
  3. command_table_helper2_dce110? Does not exist — there is only the single v1-style command_table_helper_dce110.c shared by both parsers (dce112 is the only family with separate v1/v2 files; dce80 is v1-only).
  4. Other switches audited clean: phy_id_to_atom (default→UNIPHYA), signal_type_to_atom_dig_mode (default→DVI), clock_source_id_to_atom_phy_clk_src_id (default→P1PLL), hpd_sel_to_atom (default→0) all initialize their outputs on every path. clock_source_id_to_atom (:192-236) returns true without writing only when atom_pll_id == NULL — all callers pass valid stack pointers. engine_bp_to_atom (:238-281) omits ENGINE_ID_DACB/VGA but returns false, and both callers (command_table.c:1951,1990, command_table2.c:509) treat false as BP_RESULT_FAILURE. No uninitialized-output feed exists in this file.

Defense-in-depth only: either implement the three v2-vintage members by reusing the shared dal_cmd_table_helper_* implementations (as dce80 does) and add a dc_clock_type_to_atom that mirrors dce112's with an initialized default, or add if (!cmd->member) return BP_RESULT_BADINPUT; guards at the six call sites. Upstream parity is preserved either way.

Reproduce

Not reproducible on the audit guest (no AMD GPU; amdgpu cannot attach). Static reproduction of the matrix: see null_matrix_proof.log.

VERDICT.md
↓ download raw

DF-4985 VERDICT — latent NULL-function-pointer matrix, unreachable on genuine hardware (Info hardening)

Status: untested (Info severity, hardware-gated: requires an AMD DCE10/DCE11 GPU plus a generation-mismatched VBIOS; the audit guest has no AMD GPU so amdgpu never attaches — there is no runnable path). All line references verified in this tree on 2026-09-25.

1. What the file is and who consumes it

command_table_helper_dce110.c builds the single shared struct command_table_helper vtable (:332-349) returned by dal_cmd_tbl_helper_dce110_get_table() (:361-364). It is compiled into the amdgpu kmod (sys/dev/drm/amd/amdgpu/Makefile:217) and selected for DCE_VERSION_10_0 (Tonga/Fiji) and DCE_VERSION_11_0 (Carrizo) (sys/dev/drm/amd/display/dc/core/dc_resource.c:68,72-75) by BOTH helper dispatchers: v1 command_table_helper.c:46,50 and v2 command_table_helper2.c:47,51. The v2 parser is tried first (bios_parser_interface.c:41-45); it requires ROM header 2.2+ and object-info 1.4+ (bios_parser2.c:1937-1944), else the v1 parser (bios_parser.c) is the fallback.

2. The latent matrix (proven)

Five vtable members are NULL for dce110 — three explicitly (:344 .assign_control_parameter = NULL, :345 .clock_source_id_to_ref_clk_src = NULL, :346 .transmitter_bp_to_atom = NULL) and two implicitly via designated-initializer zero-fill (dc_clock_type_to_atom, transmitter_color_depth_to_atom; struct declared at command_table_helper_struct.h:34-64). Six consumer call sites invoke members without any NULL check:

  • command_table.c:204 and :219 — encoder_control_dig1_v1/dig2_v1 call bp->cmd_helper->assign_control_parameter(...).
  • command_table.c:507 — transmitter_control_v2; :638 — transmitter_control_v3; :764 — transmitter_control_v4; each calls cmd->transmitter_bp_to_atom(...).
  • command_table.c:678 — transmitter_control_v4 calls cmd->clock_source_id_to_ref_clk_src(...).
  • command_table.c:2437 (v1) and command_table2.c:722 (v2) — set_dce_clock_v2_1 calls cmd->dc_clock_type_to_atom(...).

Any of these with the dce110 table ⇒ call through NULL ⇒ immediate kernel panic in display bring-up. The parser wrappers only guard their own cmd_tbl.set_dce_clock NULL (bios_parser.c:778, bios_parser2.c:1040) — not the helper member.

3. Why genuine hardware never trips it (why this is Info, not High)

The VBIOS command-table revisions and the helper table are generation-locked:

  • init_transmitter_control (command_table.c:367-396) binds v2/v3/v4 handlers only when the card's VBIOS UNIPHYTransmitterControl crev is 2/3/4 — DCE8-generation firmware, whose ASICs (CI/KV, dc_resource.c:57-66) select the dce80 table — the only family member that implements transmitter_bp_to_atom and clock_source_id_to_ref_clk_src (dce80/command_table_helper_dce80.c:341-345). DCE10/11 firmware uses crev 5/6 → transmitter_control_v1_5/v1_6, which only use members dce110 implements.
  • encoder_control_dig1/dig2_v1 bind only for DIG encoder control rev < 3 (DCE8-era); DCE10/11 use encoder_control_digx_v3+ which do not call assign_control_parameter.
  • set_dce_clock is invoked only from dce112_set_clock (dce/dce_clocks.c:292-340), wired exclusively into dce112_funcs/dce120_funcs/dcn1_funcs (dce_clocks.c:745,752,758). DCE10/11 resources wire dce110_funcs (dce_clocks.c:762-767; dce110_resource.c:1267 → dce110_dccg_create), whose set_dispclk = dce_psr_set_clock → dce_set_clock uses program_display_engine_pll — never set_dce_clock. The ASICs that do call it (DCE11.2 Polaris, DCE12, DCN1) select dce112-family tables which implement dc_clock_type_to_atom (dce112/command_table_helper_dce112.c:401, dce112/command_table_helper2_dce112.c:401).

So the NULLs mirror upstream Linux's vintage design (the v1 paths for old-VBIOS revisions are dce80's job; the SetDCEClock path is dce112+'s job). The only way to pair dce110's table with a calling path is a cross-generation VBIOS transplant (DCE8-era command tables on a DCE10/11 card) — an attacker who already flashes the machine's firmware, i.e. outside the local/remote attacker model. Info hardening.

4. Attribution / family checks (orchestrator's checklist)

  • DF-2030 (dead return 0 in dig_encoder_sel_to_atom) is filed on this exact file, lines 154-189, pass 1 — correctly attributed; NOT re-reported here. Its consumers (command_table.c:798,855, command_table2.c:213) feed a uint8_t params field; no memory-safety consequence (per DF-2030/DF-4934 analysis; upstream codified always-0).
  • DF-4935 shape (implemented dc_clock_type_to_atom leaving *atom_clock_type uninitialized on default while returning true) does not exist in dce110: the file has no implementation at all — the member is implicitly NULL. The nearest analog here is the NULL member + set_dce_clock_v2_1 unguarded call (command_table.c:2437, command_table2.c:722), which is the subject of this finding.
  • command_table_helper2_dce110 does not exist — only the one shared file serves both parsers (dce112 is the only family with separate v1/v2 helper files).
  • Other switches in the file are clean (negative result, verified line-by-line): phy_id_to_atom :34-65 (default→UNIPHYA), signal_type_to_atom_dig_mode :67-95 (default→DVI), clock_source_id_to_atom_phy_clk_src_id :97-121 (default→P1PLL), hpd_sel_to_atom :123-152 (default→0) initialize outputs on every path. clock_source_id_to_atom :192-236 writes the output on every non-NULL-pointer path; the atom_pll_id == NULL early-true arm is never taken (all callers pass stack pointers — command_table.c:2436, command_table2.c:721). engine_bp_to_atom :238-281 omits ENGINE_ID_DACB/VGA but returns false and every caller (command_table.c:1951,1990, command_table2.c:509) downgrades to BP_RESULT_FAILURE. encoder_action_to_atom :283-306 / disp_power_gating_action_to_atom :308-329 default to 0.

5. Why no guest run

The audit QEMU guest has no AMD GPU (standard vga); amdgpu cannot attach, so neither the v1 nor v2 parser is ever constructed on it. A PoC would require DCE10/11 hardware with a cross-generation VBIOS image. Info severity → skipped per contract; status untested.

6. Fix (defense-in-depth, upstream-parity-safe)

Either give dce110 the shared implementations (.assign_control_parameter = dal_cmd_table_helper_assign_control_parameter, .clock_source_id_to_ref_clk_src = dal_cmd_table_helper_clock_source_id_to_ref_clk_src, .transmitter_bp_to_atom = dal_cmd_table_helper_transmitter_bp_to_atom — exactly what dce80 does at dce80/command_table_helper_dce80.c:341-345) plus a dc_clock_type_to_atom that initializes its output in the default arm, or add member NULL-guards at command_table.c:204,219,507,638,678,2437 and command_table2.c:722.

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable - requires AMD DCE10/11 hardware with mismatched VBIOS; audit guest has no AMD GPU

per-fix-DF-4985

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4985/VERDICT.md sections 2-4 (the five-member/six-call-site matrix and the generation-lock proof); README.md table; null_matrix_proof.log

PoC changes

not applicable - no PoC possible on the audit guest (no AMD GPU, amdgpu never attaches); matrix proven statically

Verified recommended fix

Defense-in-depth: reuse the shared dal_cmd_table_helper_* implementations for the three explicit-NULL members (as dce80 does) and add a dc_clock_type_to_atom with initialized default, or NULL-guard the six consumer call sites.

Verdict

Pass-2 depth on the dce110 command-table helper. The file is live code (compiled into the amdgpu kmod, amd/amdgpu/Makefile:217) and its vtable is selected for DCE 10.0/11.0 (Tonga/Fiji/Carrizo) by BOTH parser dispatchers (command_table_helper.c:46,50 v1; command_table_helper2.c:47,51 v2). Five vtable members are NULL (three explicit at :344-346, two implicit: dc_clock_type_to_atom, transmitter_color_depth_to_atom) while six consumer call sites invoke members unguarded (command_table.c:204,219,507,638,678,2437; command_table2.c:722) - a generation-mismatched ASIC/VBIOS pairing would call through NULL and panic display init. Unreachable on genuine hardware, proven by the version matrix: the rev-2/3/4 transmitter and rev<3 encoder handlers that use those members bind only from DCE8-generation VBIOS, whose ASICs select the dce80 table (the only one implementing them, dce80 :341-345); DCE10/11 firmware uses rev 5/6 handlers built only from implemented members; and set_dce_clock is called solely from dce112_set_clock, wired only into dce112/dce120/dcn1 funcs (dce_clocks.c:745,752,758) - DCE10/11 wire dce110_funcs (dce_clocks.c:762-767) which never calls it. Residual trigger requires reflashing the machine's own VBIOS (outside the attacker model). Family attribution: DF-2030 (dead return-0) correctly sits on THIS file (:154-189, not re-reported); the DF-4935 uninit-output shape does NOT exist here (no dc_clock_type_to_atom implementation at all - implicit NULL instead); command_table_helper2_dce110 does not exist; all other switches in the file initialize outputs on every path (negative result verified).