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)
PoC verification
Evidence pack
findings/poc/DF-5000 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_helperdeclares 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_atomandtransmitter_color_depth_to_atomare absent β NULL by C99 6.7.8p10 zero-fill. set_pixel_clock_v7callsbp->cmd_helper->transmitter_color_depth_to_atom(...)with no NULL check atcommand_table.c:1188andcommand_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).
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)
- DCE8 ASIC, amdgpu DC attaches; dce80 helper selected.
- ROM reports SetPixelClock parameter revision 7 β
init_set_pixel_clockwiresset_pixel_clock_v7(command_table.c:930-931; v2 twincommand_table2.c:247-248). Other revisions wire v3/5/6 or leave NULL (gracefulBP_RESULT_FAILUREviabios_parser.c:766-767). - First display enable / mode set β
bios_parser_set_pixel_clockβset_pixel_clock_v7. :1159clock_source_id_to_atom(implemented, returns true),:1160controller_id_to_atom(implemented) β body entered.:1188bp->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 toDIGA_SELand 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'sdc_clock_type_to_atom, dce80's clock_source function correctly returns false).encoder_action_to_atom/disp_power_gating_action_to_atomdefaults β BREAK_TO_DEBUGGER + 0: byte-identical family shape (dce110:300-302etc.), not a per-file defect.clock_source_id_to_atom_phy_clk_src_id>>2(:175) andhpd_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 sharedcommand_table_helper.cimplementations (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_v7sites (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.diffdeliberately fixes that defect in the implanteddc_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_testablenot_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/)
Confirmed kernel references
- sys/dev/drm/amd/display/dc/bios/dce80/command_table_helper_dce80.c:329-349
- sys/dev/drm/amd/display/dc/bios/command_table_helper_struct.h:33-63
- sys/dev/drm/amd/display/dc/bios/command_table.c:1148-1188
- sys/dev/drm/amd/display/dc/bios/command_table.c:918-939
- sys/dev/drm/amd/display/dc/bios/command_table2.c:260-311
- sys/dev/drm/amd/display/dc/bios/command_table2.c:244-256
- 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/bios_parser.c:761-782
- sys/dev/drm/amd/display/dc/bios/command_table_helper.c:39-43
- sys/dev/drm/amd/display/dc/bios/command_table_helper2.c:40-44
- sys/dev/drm/amd/display/dc/core/dc_resource.c:57-65
- sys/dev/drm/amd/display/dc/bios/dce112/command_table_helper_dce112.c:358-376
- sys/dev/drm/amd/display/dc/dce/dce_clocks.c:298-334
- sys/dev/drm/radeon/atombios_crtc.c:760-766
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).
No comments yet.