v1 command-table helper hub leaves bp->cmd_helper NULL for DCE12/DCN generations on the legacy BIOS parser; construct ignores the init failure β whole-vtable NULL dereference at the first BIOS command
Summary
The parsers partition on VBIOS header rev (not ASIC gen) with a parser1 fallback, so a DCE12/DCN ASIC with a legacy-format VBIOS gets parser1 with cmd_helper==NULL (the init failure is ignored) - the first BIOS command loads a function pointer from NULL+0x20. The sibling helper2 hub serves exactly these generations (the coverage gap). Family NULL-matrix completed: dce80 2, dce110 5, dce112 3, dce112-2 0. Userspace semantics harness SIGSEGV; hardware-gated. Mirror-helper2 + honor-the-failure fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5075 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.7 KB | β raw | |
| VERDICT.md | β | 8.4 KB | β raw | |
| trigger_nullhelper.c | β | 4.3 KB | view raw | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 126 B | view raw | |
| run.log | β | 399 B | view raw | |
| env.txt | β | 252 B | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 5.4 KB | view raw |
DF-5075 evidence pack
What
sys/dev/drm/amd/display/dc/bios/command_table_helper.c β the v1 DC BIOS
command-table helper hub leaves *h NULL (returns false, value ignored) for
DCE_VERSION_12_0 / DCN_VERSION_1_0 / DCN_VERSION_1_01 / UNKNOWN, while
the legacy-parser fallback (old-format VBIOS header on DCE12/DCN silicon) can
still construct parser1 on exactly those ASICs. Every BIOS command then calls
through a NULL vtable pointer (bp->cmd_helper->β¦, e.g. command_table.c:204)
β kernel page-fault panic at display bring-up.
Build / run (guest, unprivileged)
sh build.sh # cc -O2 -Wall -o trigger_nullhelper trigger_nullhelper.c sh run.sh # expect: "[+] child died SIGSEGV ..." and exit=42
The harness is a userspace semantics demo of the defect chain (kzalloc-NULL β ignored init failure β unguarded indirect call), not a kernel PoC: the kernel path needs AMD DCE12/DCN silicon with a legacy-format VBIOS, and the audit guest has no AMD GPU at all (env.txt).
Expected output
[*] kzalloc'd bios_parser: bp->cmd_helper = 0x0 (NULL) [*] init_cmd_tbl_helper(dce_version=1200) returned; return value ignored, cmd_helper = 0x0 [*] calling bp->cmd_helper->assign_control_parameter() (member at +0x20)... [+] child died SIGSEGV at the indirect call through the NULL vtable -- semantics chain (1)-(4) confirmed exit=42
Contents
VERDICT.mdβ full narrative, reachability chain, negative results, matrixtrigger_nullhelper.c/build.sh/run.sh/build.log/run.logenv.txtβ guest evidence (uname, no AMD GPU, no class-0c:07 device)fix.diffβ hub coverage + caller return-check;git apply --checkverifiedmanifest.json,verdict.json
DF-5075 β dal_bios_parser_init_cmd_tbl_helper leaves bp->cmd_helper NULL for DCE12/DCN ASICs on the legacy BIOS parser; the sole caller ignores the failure β whole-vtable NULL dereference at the first BIOS command
File: sys/dev/drm/amd/display/dc/bios/command_table_helper.c (pass 2, GLM 5.3)
Severity: Low Β· CWE-476 (NULL Pointer Dereference) Β· confidence: certain (code chain), latent (VBIOS/ASIC-mismatch-gated)
Status: untested for the kernel path (guest has no AMD GPU); the C-semantics
chain was demonstrated on the guest (run.log, exit 42).
1. Root cause
The v1 command-table helper hub dispatches per ASIC dce_version:
sys/dev/drm/amd/display/dc/bios/command_table_helper.c:34-63β handled: DCE 8.0/8.1/8.3 β dce80 table, 10.0/11.0 β dce110, 11.2/11.22 β dce112.command_table_helper.c:58-61β default:BREAK_TO_DEBUGGER()(a no-op in release builds) andreturn false, leaving*huntouched.
The unhandled set is exactly {DCE_VERSION_12_0, DCN_VERSION_1_0,
DCN_VERSION_1_01, DCE_VERSION_UNKNOWN, DCE_VERSION_MAX, β¦} β i.e. every
generation that has a resource pool (dc_resource.c:138-146 wires dce120 for
DCE_VERSION_12_0 and dcn10 for DCN 1.0/1.01) but no v1 helper table.
The contract violation is caller-side: bios_parser_construct ignores the
return value β
sys/dev/drm/amd/display/dc/bios/bios_parser.c:2936:dal_bios_parser_init_cmd_tbl_helper(&bp->cmd_helper, dce_version);(no check), inside aconstructthat then unconditionallyreturn true(bios_parser.c:2940).
bp is kzalloc'd (bios_parser.c:100), so an ignored failure leaves
bp->cmd_helper == NULL (not wild) β a NULL table pointer, distinct from
the NULL-member class of DF-4047 / DF-4985 / DF-5000 (explicitly
cross-checked; those findings cover initialized tables with missing members).
2. Reachability chain (why parser1 can run on DCE12/DCN silicon)
dc.c:626dc_version = resource_parse_asic_id(asic_id)β FAMILY_AI βDCE_VERSION_12_0, FAMILY_RV βDCN_VERSION_1_0/1_01(dc_resource.c:84-95).dc.c:641dal_bios_parser_create(&bp_init_data, dc_version)βfirmware_parser_createfirst, fall back to the legacy parserbios_parser_createwhen it returns NULL (bios_parser_interface.c:41-45, comment "Fall back to old bios parser for older asics").- The two parsers partition on the VBIOS ROM header table revision, not
on ASIC generation:
- parser2 (bios_parser2.c:1948): requires
tbl_rev >= 2.2, else fail; - parser1 (bios_parser.c:2895): requirestbl_rev < 2.2, else fail. So a DCE12/DCN ASIC whose VBIOS ROM header is missing, corrupt, or old-format lands on parser1, whose construct then validates only the legacy ATOM tables (master data table, object info β bios_parser.c:2897-2932). dal_bios_parser_init_cmd_tbl_helper(dce_version=12_0/DCNβ¦)β default β false, ignored βbp->cmd_helperstays NULL β construct returns true (bios_parser.c:2940).dc_constructsucceeds (resource pools exist for 12.0/DCN, dc_resource.c:138-146), driver comes up.- First BIOS command executed at mode set derefs the table, e.g.
bp->cmd_helper->assign_control_parameter(bp->cmd_helper, cntl, ¶ms)(command_table.c:204, also :219) β loads a function pointer from NULL+0x20 (member offset instruct command_table_helper, command_table_helper_struct.h:34-64) β kernel page fault β panic. Other first-touch sites: command_table.c:507 (transmitter_bp_to_atom), :1188, and every one of the 49cmd_helper->uses in command_table.c.
Note the sibling hub handles exactly this: dal_bios_parser_init_cmd_tbl_helper2
(command_table_helper2.c:63-75) serves dal_cmd_tbl_helper_dce112_get_table2()
for DCE_VERSION_12_0 / DCN 1.0 / 1.01 β the v1 hub's omission is not a design
decision, it is a coverage gap.
3. Threat model
Genuine Vega10/Raven boards ship atomfirmware-format VBIOSes (header β₯ 2.2) β parser2 + helper2 β the hole requires a cross-generation pairing: legacy or corrupted VBIOS header on DCE12/DCN silicon (SPI bitrot, bad vendor flash, reflashed card, or a deliberately crafted image β the same threat class as DF-3942 ("corrupt VBIOS β NULL-page panic at attach", Low) and DF-4985/DF-5000 ("cross-generation VBIOS pairing")). Impact ceiling: kernel NULL-deref panic (display bring-up DoS), not memory corruption β the dereferenced value is NULL, not attacker-controlled data. Physical/trusted-platform prerequisite β Low.
4. Verification performed
- Kernel path: not testable on this guest β no AMD GPU
(env.txt: only
vgapci0@pci0:0:2:0 chip=0x11111234, the QEMU virtual VGA; nodrm/amddevice attaches). Same treatment as DF-4985/DF-5000. - C-semantics harness (
trigger_nullhelper.c) ran on the DragonFly guest as an unprivileged user: replicates steps (1)-(4) β zero-filled storage, ignored init failure, unguarded indirect call through the NULL vtable β child dies SIGSEGV, harness exits 42 (run.log). This proves the primitive exists at the language level; it is not a kernel PoC. fix.diff:git apply --checkpasses against the pristine tree; never applied tosys/.
5. Negative results (classes hunted and killed)
assign_control_parameterhub impl (command_table_helper.c:169-206) writes only fixed fields of the caller-stackDIG_ENCODER_CONTROL_PARAMETERS_V2(command_table.c:197/212 allocate the exact struct) β no size/type issue; the three vtable members it derefs (transmitter_bp_to_atom, encoder_action_to_atom, encoder_mode_bp_to_atom) are non-NULL in every table that wiresassign_control_parameter(only dce80 wires it, :341-343 of dce80's initializer; dce110/dce112 leave the member NULL β that member gap is DF-4047/DF-4985, not re-reported).usPixelClock = cpu_to_le16((uint16_t)(pixel_clock / 10))(:201) truncates above 655.35 MHz β inherent ATOM V2 field width (u16 in the interface definition), upstream-inherited display-glitch class, not kernel memory safety; killed.transmitter_bp_to_atomdefault β 0 (:135-138) andencoder_mode_bp_to_atomdefault β CRT (:164-166): silent fallbacks, upstream-inherited; no enum value that maps wrong on supported silicon (UNIPHY_A..F + TRAVIS_LCD covered); killed as Info-noise.- Output-pointer helpers (
controller_id_to_atom:71-74,clock_source_id_to_ref_clk_src:212-215) NULL-check their out-params and return false on default β clean. - No allocation, no locks, no refcounts, no copyin/copyout in this file β the entire attack surface is the dispatch contract.
- Cross-hub type confusion ruled out: helper2 reuses the same
struct command_table_helper(command_table_helper2.h:29-32) so the dce80 table served via helper2 (command_table_helper2.c:40-44) is layout- compatible; the NULL members there are DF-5000's, already filed. - Family matrix completed (the prompt's DF-5000 completion ask): | member | dce80 | dce110 | dce112 | dce112-2 (parser2) | |---|---|---|---|---| | controller_id_to_atom | β | β | β | β | | encoder_action_to_atom | β | β | β | β | | encoder_mode_bp_to_atom | β | β | β | β | | engine_bp_to_atom | β | β | β | β | | assign_control_parameter | hub | NULL | NULL | hub2 | | clock_source_id_to_atom | β | β | β | β | | clock_source_id_to_atom_phyβ¦ | β | β | β | β | | clock_source_id_to_ref_clk_src | hub | NULL | NULL | hub2 | | transmitter_bp_to_atom | hub | NULL | NULL | hub2 | | encoder_id_to_atom | hub | hub | hub | hub2 | | signal_type_to_atom_dig_mode | β | β | β | β | | hpd_sel_to_atom | β | β | β | β | | dig_encoder_sel_to_atom | β(dead0=DF-2030 sibling) | β | β(DF-4936) | β | | phy_id_to_atom | β | β | β | β | | disp_power_gating_actionβ¦ | β | β | β | β | | dc_clock_type_to_atom | NULL (DF-5000) | NULL (DF-4985) | β | β(DF-4935) | | transmitter_color_depthβ¦ | NULL (DF-5000) | NULL (DF-4985) | β | β | (this hub file adds no new NULL members β its gap is the version dispatch, filed here as DF-5075.)
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 DCE12/DCN hardware with a legacy-format 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; it mirrors the only in-tree handling of these generations (command_table_helper2.c:63-75) plus the caller-side return check.
findings/poc/DF-5075/fix.diff (git apply --check passes; never applied to sys/)
Confirmed kernel references
- sys/dev/drm/amd/display/dc/bios/command_table_helper.c:34-63
- sys/dev/drm/amd/display/dc/bios/command_table_helper.c:58-61
- sys/dev/drm/amd/display/dc/bios/bios_parser.c:2936
- sys/dev/drm/amd/display/dc/bios/bios_parser.c:100
- sys/dev/drm/amd/display/dc/bios/bios_parser.c:2895
- sys/dev/drm/amd/display/dc/bios/bios_parser2.c:1948
- sys/dev/drm/amd/display/dc/bios/bios_parser_interface.c:41-45
- sys/dev/drm/amd/display/dc/core/dc.c:626
- sys/dev/drm/amd/display/dc/core/dc.c:641
- sys/dev/drm/amd/display/dc/core/dc_resource.c:84-95
- sys/dev/drm/amd/display/dc/core/dc_resource.c:138-146
- sys/dev/drm/amd/display/dc/bios/command_table_helper2.c:63-75
- 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
Detail
Evidence (decisive lines)
VERDICT.md sections 1-3 (hub default path :58-61, caller ignoring at bios_parser.c:2936, the ROM-header-revision parser partition bios_parser.c:2895 / bios_parser2.c:1948, fallback bios_parser_interface.c:41-45, pool support dc_resource.c:138-146); run.log (guest userspace semantics demo, exit 42); env.txt (guest has no AMD GPU); fix.diff (git apply --check passes)
PoC changes
evidence pack authored fresh in pass 2 (no seed). trigger_nullhelper.c is a guest userspace semantics harness mirroring steps (1)-(4) of the kernel chain, not a kernel PoC: no AMD GPU exists on the audit guest.
Verified recommended fix
Mirror helper2 coverage: serve dal_cmd_tbl_helper_dce112_get_table2() for DCE_VERSION_12_0/DCN1_0/1_01 in the v1 hub, and make bios_parser.c:2936 check the return and fail construct -- see fix.diff.
Verdict
Pass-2 audit of the v1 command-table helper hub (command_table_helper.c). The hub covers only DCE 8.0/8.1/8.3/10.0/11.0/11.2/11.22 (:38-56); for DCE_VERSION_12_0, DCN_VERSION_1_0, DCN_VERSION_1_01 and DCE_VERSION_UNKNOWN the default path (:58-61) returns false leaving h untouched. The sole caller, bios_parser_construct, IGNORES the return (bios_parser.c:2936) inside a construct whose bp is kzalloc'd (bios_parser.c:100) -- so the failure leaves bp->cmd_helper == NULL while construct returns true. Reachability: the parser choice partitions on the VBIOS ROM header revision (parser2 requires >=2.2, bios_parser2.c:1948; parser1 requires <2.2, bios_parser.c:2895), NOT on ASIC generation, and dal_bios_parser_create falls back to parser1 whenever firmware_parser_create fails (bios_parser_interface.c:41-45). A DCE12/DCN ASIC (dc.c:626, dc_resource.c:84-95) with a legacy-format or header-corrupted VBIOS therefore gets parser1, a NULL cmd_helper, a SUCCESSFUL dc_construct (pools exist for 12.0/DCN, dc_resource.c:138-146), and then a whole-vtable NULL dereference at the first BIOS command -- e.g. bp->cmd_helper->assign_control_parameter at command_table.c:204/219 loads a function pointer from NULL+0x20 -- a kernel page-fault panic at display bring-up. The sibling hub proves the omission is a gap, not a design choice: command_table_helper2.c:63-75 serves the dce112 helper2 table for exactly DCE_VERSION_12_0/DCN1_0/1_01. Distinct from DF-4047/DF-4985/DF-5000 (initialized tables with NULL members*): here the table pointer itself is NULL. Threat model equals DF-3942/DF-4985/DF-5000 (crafted/corrupt or cross-generation-paired VBIOS on modern silicon; physical prerequisite) -> Low. Kernel path untestable on this guest (no AMD GPU, env.txt); the C-semantics chain was demonstrated on the guest as an unprivileged user (trigger_nullhelper.c: ignored init failure -> NULL vtable -> SIGSEGV at the indirect call, exit 42, run.log). fix.diff (hub coverage mirroring helper2 + caller return-check) passes git apply --check; not applied to sys/.
No comments yet.