# 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) and `return false`, leaving `*h` **untouched**.

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 a `construct` that then unconditionally
  `return 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)

1. `dc.c:626` `dc_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).
2. `dc.c:641` `dal_bios_parser_create(&bp_init_data, dc_version)` →
   `firmware_parser_create` first, **fall back to the legacy parser**
   `bios_parser_create` when it returns NULL
   (bios_parser_interface.c:41-45, comment "Fall back to old bios parser for
   older asics").
3. 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): requires `tbl_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).
4. `dal_bios_parser_init_cmd_tbl_helper(dce_version=12_0/DCN…)` → default →
   false, ignored → `bp->cmd_helper` stays NULL → construct returns true
   (bios_parser.c:2940).
5. `dc_construct` succeeds (resource pools exist for 12.0/DCN,
   dc_resource.c:138-146), driver comes up.
6. First BIOS command executed at mode set derefs the table, e.g.
   `bp->cmd_helper->assign_control_parameter(bp->cmd_helper, cntl, &params)`
   (command_table.c:204, also :219) — loads a function pointer from
   NULL+0x20 (member offset in `struct 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 49 `cmd_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;
  no `drm/amd` device 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 --check` passes against the pristine tree; never
  applied to `sys/`.

## 5. Negative results (classes hunted and killed)

- `assign_control_parameter` hub impl (command_table_helper.c:169-206) writes
  only fixed fields of the caller-stack `DIG_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 wires `assign_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_atom` default → 0 (:135-138) and
  `encoder_mode_bp_to_atom` default → 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.
