β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4495

dce100_link_encoder_create indexes 7/6/6-entry const register tables with unvalidated VBIOS-derived transmitter/channel/hpd_source β€” OOB const reads become the encoder's MMIO register bases (DF-4312/4319/4352/4375 family twin)

Summary

The link-encoder regs/aux/hpd table lookups are formed directly from encoder_init_data fields originating in the VBIOS object tables with no range validation anywhere on the path; producers emit -1/7/8/9 transmitters, unknown/7/8 channels, and unknown=7 hpd - 8/27 reachable enum values OOB and a craftable all-three-OOB triple. Guest model verified over every producer-reachable value; kernel path hardware-gated (no AMD device). First encoder register access issues wild MMIO at VBIOS-chosen offsets. Bounds-check-and-fail fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4495 Β· 10 files
FileTypeDescriptionSize
df4495.c β€” 7.8 KB view raw
build.sh β€” 86 B view raw
run.sh β€” 89 B view raw
build.log β€” 678 B view raw
run.log β€” 1.8 KB view raw
run.2.log β€” 478 B view raw
env.txt β€” 266 B view raw
fix.diff β€” 1.0 KB view raw
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 4.5 KB ↓ raw

DF-4495 β€” dce100_link_encoder_create OOB const-table reads (VBIOS-indexed)

File: sys/dev/drm/amd/display/dc/dce100/dce100_resource.c:560-577 Family: DF-4312 (dcn10) / DF-4319 (dce80) / DF-4352 (dce110) / DF-4375 (dce112) twin.

What

dce100_link_encoder_create() indexes three static const register tables with values taken straight from the GPU VBIOS object tables, with no validation anywhere on the path:

dce110_link_encoder_construct(enc110, enc_init_data, &link_enc_feature,
        &link_enc_regs[enc_init_data->transmitter],      /* :573 β€” 7 entries */
        &link_enc_aux_regs[enc_init_data->channel - 1],  /* :574 β€” 6 entries */
        &link_enc_hpd_regs[enc_init_data->hpd_source]);  /* :575 β€” 6 entries */

The values are produced by (all in sys/dev/drm/amd/display/dc/core/dc_link.c):

  • translate_encoder_to_transmitter() (:1006-1069): emits TRANSMITTER_UNKNOWN (-1) for unknown encoder ids/enum_ids, and 7/8/9 (TRAVIS_CRT/TRAVIS_LCD/UNIPHY_G) for external TRAVIS / UNIPHY3 encoder objects β€” -1, 7, 8, 9 are all OOB for link_enc_regs[7].
  • get_ddc_line() (:964-1004): emits CHANNEL_ID_UNKNOWN (0), CHANNEL_ID_DDC_VGA (7), CHANNEL_ID_I2C_PAD (8) β€” after the - 1 these give indices -1, 6, 7, all OOB for link_enc_aux_regs[6].
  • get_hpd_line() (:925-962): returns HPD_SOURCEID_UNKNOWN (7) for any connector without a mapped HPD gpio (default arm) β€” OOB for link_enc_hpd_regs[6].

The OOB-read garbage is stored as the encoder's register base set; every subsequent register access on that link encoder issues MMIO at VBIOS-steerable wild offsets (bring-up hang / machine check during amdgpu bind / link detection). No userspace-readable leak; no unprivileged or remote trigger β€” the attacker model is crafted/flashed VBIOS or a malicious PCIe device.

Reproduce (guest-side model β€” no AMD GPU required)

cc -O2 -Wall -o df4495 df4495.c   # build.sh
./df4495                          # run.sh

Expected decisive output (run.log, re-confirmed in run.2.log):

CONST-TABLE OOB READ CONFIRMED (model): dce100_resource.c:573-575 forms
&link_enc_regs[tx], &link_enc_aux_regs[ch-1], &link_enc_hpd_regs[hpd] with
unvalidated VBIOS-derived indexes β€” garbage becomes the encoder's MMIO
register bases.

with the per-table tally: 4/11 transmitter, 3/9 channel, 1/7 hpd_source reachable values OOB (8/27), and a concrete craftable triple (TRAVIS_CRT + DDC_VGA + no-HPD β†’ indices 7/6/7, all three OOB at once).

Why not a kernel PoC on the audit guest

env.txt: the guest has 0 AMD/ATI PCI devices; the amdgpu/dce100 path never executes. Same situation as the accepted family twins (DF-4352/DF-4375). The defect is source-certain; runtime impact is hardware-gated.

Fix

fix.diff (git-apply clean against this tree): bounds-check the three values in dce100_link_encoder_create and return NULL β€” the caller (dc_link.c:1184-1190) already handles a NULL link encoder by failing just that link.

VERDICT.md
↓ download raw

DF-4495 VERDICT

Status: untested (kernel-level) β€” model-confirmed. Impact: none on this guest (hardware-gated). Confidence: certain (source).

Reproduced?

At the kernel level: no β€” and it cannot be on this guest. env.txt records 0 AMD/ATI PCI devices; dce100_create_resource_pool and dce100_link_encoder_create only execute during amdgpu bind on a DCE10 (Tonga/Topaz) ASIC, which the QEMU guest does not have. This is the same verification ceiling as the accepted family twins DF-4352 (dce110) and DF-4375 (dce112).

What was verified:

  1. Line-accurate source trace. The three index expressions are at dce100_resource.c:573-575, formed from enc_init_data fields filled at core/dc_link.c:1177-1183 by get_hpd_line() (:925-962), get_ddc_line() (:964-1004) and translate_encoder_to_transmitter() (:1006-1069) β€” all reading the VBIOS object tables via the bios parser, none validating range. Enum domains from include/grph_object_defs.h:45-94; table extents 7/6/6 at dce100_resource.c:232-240, :204-211, :218-225.
  2. Guest-side index-domain model (df4495.c, compiled with the guest cc 8.3, full output in run.log/run.2.log): the verbatim index expressions evaluated over every value the three producers can emit β€” 4/11 transmitter (UNKNOWN(-1), TRAVIS_CRT(7), TRAVIS_LCD(8), UNIPHY_G(9)), 3/9 channel (UNKNOWN(0)β†’-1, DDC_VGA(7)β†’6, I2C_PAD(8)β†’7), 1/7 hpd (UNKNOWN(7)) are OOB β€” 8/27 reachable values; the concrete craftable triple TRAVIS_CRT+DDC_VGA+no-HPD makes all three lookups OOB simultaneously (indices 7/6/7).
  3. Consequence path: dce110_link_encoder_construct stores the garbage pointers as enc110->link_regs/aux_regs/hpd_regs; every later REG_* access on that encoder (link detection, AUX/HPD setup at bind) dereferences them β†’ wild MMIO (hang/machine-check), not a readable leak.

Exploit chain

Crafted/flashed VBIOS (or malicious PCIe GPU) on a DCE10 board declares an external TRAVIS encoder object, a VGA/I2C-pad DDC line, or a connector without HPD mapping β†’ amdgpu bind β†’ dc_link_construct fills enc_init_data with UNKNOWN/out-of-range enum values β†’ dce100_link_encoder_create performs the OOB const-table reads β†’ bogus register bases stored β†’ first encoder register access issues MMIO at VBIOS-chosen offsets. Requires firmware/hardware control; no unprivileged or remote path. Severity Low (family precedent).

Fix validation

fix.diff bounds-checks the three values (identical shape to the accepted DF-4352 fix) and returns NULL; the caller dc_link.c:1184-1190 already fails that single link cleanly on NULL, so the driver continues with the remaining links. git apply --check passes against the read-only tree. Fix not kernel-testable on this guest (no AMD device): fix_status: not_testable.

Negative checks performed on this file (pass-2 depth)

  • All other register-table lookups are bounded: dce100_tg_offsets[6] indexed i < pipe_count(6) (:977-982); mi/ipp/xfm/opp/aux/i2c regs[6] indexed i < pipe_count / i < num_ddc(6) (:989-1036); clk_src_regs[3] with clk_src_count ∈ {2,3} (:884-906); audio_regs[7] with inst ≀ min(pipe_count, num_audio)=6 (dc_resource.c:232-247); stream_enc_regs[7] with eng_id ≀ num_stream_encoder=6 (dc_resource.c:252-257). Only the three link-encoder lookups are VBIOS-indexed.
  • destruct() (:678-745) loops pipe_count/num_ddc/stream_enc_count/ clk_src_count/audio_count, all set before the allocations they bound and all NULL-guarded β€” no double-free/UAF on the unwind path.
  • The i2c 1-based-regs vs 0-based-slot pairing (:614-621 vs :1030) is the upstream-intended DC_I2C engine↔DDC-select mapping (dce_i2c_hw.c:282), not an off-by-one.
  • dce100_validate_global/validate_surface_sets (:789-818): stream_status[i] is maintained in lockstep with streams[i] by the dc core; plane_states[0] is only read when plane_count == 1. No join arithmetic exists for DCE10 (no underlay/MPC joins).
  • populate_dml does not exist for DCE10 (no DML); dce100_validate_bandwidth hardcodes clocks (:777-784) β€” functional TODO only.

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_testable
baseline no→ patch + rebuild →patched clean

fix.diff git-apply-clean against the read-only sys/ tree; adds the three-range check before dce110_link_encoder_construct and frees enc110 on violation; caller dc_link.c:1184-1190 already fails only that link on NULL, matching the accepted DF-4352 fix shape. Kernel-level validation impossible on this guest (0 AMD PCI devices).

fix.diff; git apply --check output 'DF4495_APPLY_OK'; VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-4495

Confirmed kernel references

Detail

Exploit chain

crafted or foreign VBIOS on a Tonga/Topaz (DCE10) ASIC declares an external TRAVIS encoder, a VGA/I2C-pad DDC line, or a connector without HPD mapping -> amdgpu bind -> dc_link_construct fills enc_init_data with UNKNOWN/out-of-range enum values -> dce100_link_encoder_create performs the OOB const-table reads -> stored bogus register bases -> subsequent encoder programming issues MMIO at VBIOS-chosen offsets (hang/machine-check). Requires firmware/hardware control; no unprivileged or remote path.

Evidence (decisive lines)

run.log + run.2.log ('CONST-TABLE OOB READ CONFIRMED (model)', 8/27 reachable values incl. -1/7/8/9 transmitter and 7/6/7 triple); VERDICT.md (line-accurate domain proof + negative checks); env.txt (0 AMD GPU on guest); fix.diff (git apply --check clean, caller dc_link.c:1187-1190 handles NULL)

PoC changes

No executable kernel PoC possible on this guest (no AMD device). Authored df4495.c: replica of the three const-table extents with the verbatim :573-575 index expressions evaluated over the full producer-reachable enum domains from grph_object_defs.h.

Verified recommended fix

Bounds-check transmitter in [UNIPHY_A, NUTMEG_CRT], channel in [DDC1, DDC6], hpd_source in [HPD_SOURCEID1, HPD_SOURCEID6] in dce100_link_encoder_create and return NULL on violation (see fix.diff).

Verdict

Source-certain, hardware-gated (DF-4312/4319/4352/4375 family twin at the DCE10 instance). dce100_link_encoder_create (dce100_resource.c:560-577) indexes link_enc_regs[7] (:232-240) with transmitter (reachable -1 via TRANSMITTER_UNKNOWN from dc_link.c:1006-1069 default arms, and 7/8/9 via TRAVIS_CRT/TRAVIS_LCD/UNIPHY_G), link_enc_aux_regs[6] (:204-211) with channel-1 (reachable -1 via CHANNEL_ID_UNKNOWN default from dc_link.c:964-1004, and 6/7 via DDC_VGA/I2C_PAD), and link_enc_hpd_regs[6] (:218-225) with hpd_source (reachable 7 via HPD_SOURCEID_UNKNOWN default from dc_link.c:925-962) - all from the GPU VBIOS object tables, unvalidated. The OOB const reads become the link encoder's MMIO register bases -> VBIOS-steerable wild MMIO on every later encoder register access (bind-time hang/machine check); no userspace-readable leak. Kernel path untestable on the guest (pciconf: 0 AMD/ATI devices, amdgpu module present but never binds). Guest model (cc 8.3) reproduces the verbatim index expressions over the real enum domains: 8/27 reachable values OOB incl. the all-three-OOB craftable triple TRAVIS_CRT+DDC_VGA+no-HPD (7/6/7).