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)
PoC verification
Evidence pack
findings/poc/DF-4985 · 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
amdgpukmod: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 v2command_table_helper2.c:47,51(firmware parser, selected first bybios_parser_interface.c:41-45;bios_parser2.crequires 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_srcbind only frominit_transmitter_control(command_table.c:367-396) when the card's VBIOSUNIPHYTransmitterControlcrev 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_v1bind for VBIOS DIG encoder control rev < 3 (DCE8-era); DCE10/11 useencoder_control_digx_v3+. set_dce_clock(command_table.c:2437/command_table2.c:722) is reached only fromdce112_set_clock(dce/dce_clocks.c:292-340), which is wired exclusively intodce112_funcs/dce120_funcs/dcn1_funcs(dce_clocks.c:745,752,758). DCE10/11 resources wiredce110_funcs(dce_clocks.c:762-767,set_dispclk = dce_psr_set_clock→dce_set_clock→program_display_engine_pll, neverset_dce_clock). DCE11.2+ (which does call it) uses the dce112 table, which implementsdc_clock_type_to_atom.- The parser wrappers guard only their own
cmd_tbl.set_dce_clockNULL (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
- DF-2030 attribution: confirmed filed on this file (lines 154-189,
return 0discarding the computed DIGx_SEL mapping) — pass 1, not re-reported. Callerscommand_table.c:798,855,command_table2.c:213feed the always-0 into AtomBIOS params (uint8_t field; no memory-safety consequence; upstream-intended per DF-4934 analysis). - DF-4935 shape (
dc_clock_type_to_atomuninit-output): does NOT exist in dce110 — this file has nodc_clock_type_to_atomimplementation at all (implicit NULL vs dce112's implemented-but-uninit-in-default). Distinct shape, filed here as the NULL-member matrix. command_table_helper2_dce110? Does not exist — there is only the single v1-stylecommand_table_helper_dce110.cshared by both parsers (dce112 is the only family with separate v1/v2 files; dce80 is v1-only).- 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) returnstruewithout writing only whenatom_pll_id == NULL— all callers pass valid stack pointers.engine_bp_to_atom(:238-281) omits ENGINE_ID_DACB/VGA but returnsfalse, and both callers (command_table.c:1951,1990,command_table2.c:509) treatfalseasBP_RESULT_FAILURE. No uninitialized-output feed exists in this file.
Recommended fix
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.
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:204and:219—encoder_control_dig1_v1/dig2_v1callbp->cmd_helper->assign_control_parameter(...).command_table.c:507—transmitter_control_v2;:638—transmitter_control_v3;:764—transmitter_control_v4; each callscmd->transmitter_bp_to_atom(...).command_table.c:678—transmitter_control_v4callscmd->clock_source_id_to_ref_clk_src(...).command_table.c:2437(v1) andcommand_table2.c:722(v2) —set_dce_clock_v2_1callscmd->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 VBIOSUNIPHYTransmitterControlcrev 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 implementstransmitter_bp_to_atomandclock_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_v1bind only for DIG encoder control rev < 3 (DCE8-era); DCE10/11 useencoder_control_digx_v3+ which do not callassign_control_parameter.set_dce_clockis invoked only fromdce112_set_clock(dce/dce_clocks.c:292-340), wired exclusively intodce112_funcs/dce120_funcs/dcn1_funcs(dce_clocks.c:745,752,758). DCE10/11 resources wiredce110_funcs(dce_clocks.c:762-767;dce110_resource.c:1267→dce110_dccg_create), whoseset_dispclk = dce_psr_set_clock→dce_set_clockusesprogram_display_engine_pll— neverset_dce_clock. The ASICs that do call it (DCE11.2 Polaris, DCE12, DCN1) select dce112-family tables which implementdc_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 0indig_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_atomleaving*atom_clock_typeuninitialized 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_1unguarded 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; theatom_pll_id == NULLearly-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_testablenot_testable - requires AMD DCE10/11 hardware with mismatched VBIOS; audit guest has no AMD GPU
Confirmed kernel references
- sys/dev/drm/amd/display/dc/bios/dce110/command_table_helper_dce110.c:332-349
- sys/dev/drm/amd/display/dc/bios/dce110/command_table_helper_dce110.c:344-346
- sys/dev/drm/amd/display/dc/bios/command_table_helper_struct.h:34-64
- sys/dev/drm/amd/display/dc/bios/command_table.c:204
- sys/dev/drm/amd/display/dc/bios/command_table.c:219
- sys/dev/drm/amd/display/dc/bios/command_table.c:507
- sys/dev/drm/amd/display/dc/bios/command_table.c:638
- sys/dev/drm/amd/display/dc/bios/command_table.c:678
- sys/dev/drm/amd/display/dc/bios/command_table.c:2437
- sys/dev/drm/amd/display/dc/bios/command_table2.c:722
- sys/dev/drm/amd/display/dc/bios/command_table.c:367-396
- sys/dev/drm/amd/display/dc/bios/dce80/command_table_helper_dce80.c:341-345
- sys/dev/drm/amd/display/dc/bios/command_table_helper.c:46,50
- sys/dev/drm/amd/display/dc/bios/command_table_helper2.c:47,51
- sys/dev/drm/amd/display/dc/bios/bios_parser_interface.c:41-45
- sys/dev/drm/amd/display/dc/dce/dce_clocks.c:745-771
- sys/dev/drm/amd/display/dc/core/dc_resource.c:68-75
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).
No comments yet.