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

dce110 hw_translate offset_to_id returns success for all eight DDC-family offsets without writing *id β€” dal_gpio_service_create_irq switches on an uninitialized gpio_id; garbage==GPIO_ID_HPD smuggles DDC-line en=6/7 into the 6-entry hpd_regs[] (OOB rodata read driving MMIO)

Summary

The 4th uninitialized-*id twin (DF-4943/45/46 family): verbatim harness proves all 8 DDC branches leave *id unwritten (poison preserved) while the controls DO write it; the GPIOPAD cross-domain producer is structurally absent on dce110 (register not defined) - so only the uninit path remains. VBIOS-fed; write-*id + consumer-guard fix (completes the family: all four ASIC generations now filed).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4965 Β· 14 files
FileTypeDescriptionSize
harness_dce110.c β€” 16.5 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 116 B view raw
b2.sh β€” 134 B view raw
r2.sh β€” 41 B view raw
build.log β€” 9 B view raw
run.log β€” 4.7 KB view raw
run.2.log β€” 2.4 KB view raw
fixrun.log β€” 2.3 KB view raw
env.txt β€” 232 B view raw
fix.diff β€” 1.9 KB view raw
VERDICT.md β€” 4.6 KB ↓ raw
README.md β€” 1.1 KB ↓ raw
verdict.json β€” 4.8 KB view raw

DF-4965 β€” dce110 hw_translate: DDC-family offset_to_id returns success without writing *id (4th uninitialized-id twin)

Reproduce

./build.sh          # gcc -O2 -Wall -Wextra (harness + -DFIXED variant)
./run.sh            # baseline run + fixed build run

Expected (baseline): all eight DDC-family offsets (0x4869..0x487d, 0x4881, 0x4899) return true with en set 0..7 and id UNWRITTEN (poison preserved) β†’ dal_gpio_service_create_irq (gpio_service.c:148) proceeds on garbage; garbage==GPIO_ID_HPD(3) β†’ define_hpd_registers(pin, 6/7) β†’ &hpd_regs[6..7] OOB past the 6-entry array (hw_factory_dce110.c:57-64).

Expected (fixed build): the eight branches write id=DDC_DATA(0); controls (GENERIC/HPD/SYNCA/rejects) unchanged.

Files

  • harness_dce110.c β€” verbatim dce110 offset_to_id/id_to_offset + real DCE11 constants; -DFIXED compiles the fixed branches
  • build.sh, run.sh, build.log, run.log, run.2.log, fixrun.log
  • env.txt β€” guest uname/compiler/no-AMD-hw proof
  • fix.diff β€” dce110 *id = GPIO_ID_DDC_DATA hunks + consumer hardening
  • VERDICT.md β€” full narrative
  • manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-4965 VERDICT

Status: reproduced (unit-level contract violation executed on the DF guest with the verbatim DCE110 functions); kernel consequence chain proven by code trace, probabilistic final stage. Identical defect family to DF-4943 (dce80) / DF-4945 (dce120) / DF-4946 (dcn10).

What was executed

harness_dce110.c carries verbatim copies of the dce110 offset_to_id (hw_translate_dce110.c:39-179) and id_to_offset (hw_translate_dce110.c:181-366) with the real DCE11 register constants taken from sys/dev/drm/amd/include/asic_reg/dce/dce_11_0_d.h and dce_11_0_sh_mask.h. The dce110 switch constants are absolute MMIO offsets (mmDC_GPIO_DDCVGA_A=0x4881, mmDC_GPIO_I2CPAD_A=0x4899, dce_11_0_d.h:1481,1504), which is exactly the domain the VBIOS stores in GPIO_Pin_LUT.usGpioPin_AIndex (bios_parser.c:1819-1820, v1 parser used for DCE110-era asics) and which get_hpd_gpio (dc_link.c:115-118) forwards into dal_gpio_service_create_irq. The mask arrives as 1 << ucGpioPinBitShift (bios_parser.c:1825-1826) β€” the DDC-family branches do not consult it at all.

Guest run (run.log, stable across run.2.log):

  • All eight DDC-family offsets (DDC1-0x4869 .. DDC6-0x487d, DDCVGA-0x4881, I2CPAD-0x4899) return true with *en set (0..7) and *id UNWRITTEN (poison 0xA11CE0D5 preserved through the call).
  • Controls prove the methodology: GENERIC/HPD/SYNCA branches do write *id; PWRSEQ/DEBUG/PAD_STRENGTH/unknown offsets are rejected with false.

Kernel consequence chain (code trace)

dal_gpio_service_create_irq (gpio_service.c:148) declares enum gpio_id id; uninitialized β†’ CWE-457. With garbage id:

  • garbage != GPIO_ID_HPD(3) && != GPIO_ID_GPIO_PAD(4) β†’ dal_gpio_create_irq (gpio_service.c:373-381) returns NULL β€” benign.
  • garbage == GPIO_ID_GPIO_PAD(4) β†’ dal_gpio_service_open rejects it in the switch default (gpio_service.c:271-274) β€” benign.
  • garbage == GPIO_ID_HPD(3) β†’ dal_gpio_create(HPD, en) β†’ dal_gpio_service_open (gpio_service.c:258-262) β†’ create_hpd + define_hpd_registers(pin, en) β†’ hpd->regs = &hpd_regs[en] (hw_factory_dce110.c:139,142). hpd_regs[] has exactly 6 entries (hw_factory_dce110.c:57-64; number_of_pins[GPIO_ID_HPD]=6, line 171), so en=6 (DDC_VGA) and en=7 (I2C_PAD) index 1 and 2 entries out of bounds β€” an OOB read of const rodata whose garbage int_status/toggle_filt_cntl descriptors then drive MMIO via REG_GET/REG_UPDATE (hw_hpd.c).

DCE110 differences vs the dce80 twin, both verified on the guest

  • (a) No mmGPIOPAD_A reverse-map case β€” the register is not even defined in dce_11_0_d.h; feeding the dce80 value 0x183 returns false (T[GPIOPAD]). The deterministic DF-4942 producer (GPIOPAD β†’ en up to 30 β†’ ddc arrays) is structurally absent on dce110. The uninit-id path is the only cross-domain route and is probabilistic (needs garbage == 3).
  • (b) No GPIO_ID_GPIO_PAD forward-map case in id_to_offset β†’ the DF-4944 mask-vs-index defect does not exist here (T: id_to_offset(GPIO_PAD,5) = false).
  • (c) ddc_data_regs[]/ddc_clk_regs[] have 8 entries (hw_factory_dce110.c:80-100), consistent with the DDC-line domain 0..7 β€” in-bounds (no finding).

Fix validation

fix.diff = dce110 hunks (8Γ— *id = GPIO_ID_DDC_DATA) + consumer hardening in dal_gpio_service_create_irq (init id = GPIO_ID_UNKNOWN, reject if still unknown). The -DFIXED build of the harness applies exactly the 8-branch edit: fixrun.log shows all eight DDC-family decodes now write id=DDC_DATA(0) (poison eliminated) with zero regressions on the controls; with the fix, dal_gpio_create_irq deterministically rejects DDC_DATA (default branch β†’ NULL) instead of switching on garbage.

Severity

Low (matches DF-4945/DF-4946): crafted-VBIOS precondition + stack-garbage==HPD final stage; worst case OOB rodata read β†’ wild MMIO β†’ panic/attach failure on Tonga/Fiji/Carrizo-class (DCE110) hardware. Not unprivileged-user-reachable on stock systems.

Guest

DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, 2026-07-02), gcc 8.3. amdgpu modules loaded: 0; AMD PCI devices: 0; /dev/dri absent β€” kernel-stage execution of the VBIOS-driven GPIO decode is impossible on this QEMU guest, so the verbatim translate functions were executed at unit level instead.

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 reproduced→ patch + rebuild →patched clean

fix.diff adds *id = GPIO_ID_DDC_DATA to all eight DDC-family branches of the dce110 offset_to_id plus a consumer guard in dal_gpio_service_create_irq (id = GPIO_ID_UNKNOWN init + reject). The -DFIXED harness build applies exactly the 8-branch edit: fixrun.log shows id=DDC_DATA(0) on every DDC decode (poison eliminated) with zero regressions on the GENERIC/HPD/SYNCA/reject controls; with the fix dal_gpio_create_irq deterministically rejects DDC_DATA (default -> NULL) instead of switching on garbage. No AMD hardware on the guest, so kernel-build validation is impossible; harness-level validation only (same standard as the DF-4945 family).

fix.diff (dce110 hunks + gpio_service.c create_irq guard); fixrun.log (fixed-build run, 8/8 branches write id, 0 regressions).
↓ fix.diffper-fix-DF-4965

Confirmed kernel references

Detail

Exploit chain

crafted VBIOS GPIO_Pin_LUT (usGpioPin_AIndex = mmDC_GPIO_DDCVGA_A=0x4881 or mmDC_GPIO_I2CPAD_A=0x4899, bios_parser.c:1819-1826) -> get_hpd_gpio (dc_link.c:115-118) -> dal_gpio_service_create_irq -> offset_to_id true with id UNWRITTEN -> garbage id==GPIO_ID_HPD(3) -> dal_gpio_service_open(HPD, en=6/7) -> define_hpd_registers -> hpd_regs[6..7] OOB rodata read -> REG_UPDATE/REG_GET MMIO at garbage register descriptors -> panic/attach failure on DCE110 hardware. Probabilistic final stage.

Evidence (decisive lines)

run.log lines 'T[DDCVGA] offset_to_id(0x4881,0x100)=true en=6 id=<poison> <== id UNWRITTEN' and 'T[I2CPAD] offset_to_id(0x4899,0x1)=true en=7 id=<poison> <== id UNWRITTEN'; T[GPIOPAD] 0x183->false; fixrun.log: FIXED build writes id=DDC_DATA(0) in all 8 branches, controls unchanged.

PoC changes

fresh harness (verbatim dce110 offset_to_id/id_to_offset copy, real DCE11 constants: absolute MMIO offsets from dce_11_0_d.h, field masks from dce_11_0_sh_mask.h, enums from gpio_types.h); fixed gcc-8 compile issue (enum gpio_sync needs HSYNC_B/VSYNC_B referenced by the verbatim body).

Verified recommended fix

Write id (GPIO_ID_DDC_DATA) in every successful DDC-family branch of dce110 offset_to_id and harden dal_gpio_service_create_irq against decoders that leave id unwritten.

Verdict

The DCE110 copy of offset_to_id (hw_translate_dce110.c:143-167) has the family defect of DF-4943/DF-4945/DF-4946: all eight DDC-family branches (DDC1-0x4869..DDC6-0x487d, DDCVGA-0x4881, I2CPAD-0x4899) return true after writing only en. dal_gpio_service_create_irq (gpio_service.c:148) then switches on the uninitialized id; garbage==GPIO_ID_HPD(3) with a DDC-line en of 6 (DDC_VGA) or 7 (I2C_PAD) reaches define_hpd_registers, which indexes the 6-entry hpd_regs[] (hw_factory_dce110.c:57-64,139,142) out of bounds -- garbage int_status/toggle_filt_cntl descriptors then drive MMIO (hw_hpd.c). Executed on the DF guest with a verbatim copy of the dce110 functions and real DCE11 absolute-offset constants (run.log: all 8 branches true with id untouched at poison 0xA11CE0D5; GENERIC/HPD/SYNCA controls do write *id). Unlike dce80, dce110 has no mmGPIOPAD_A case (register not even defined in dce_11_0_d.h; T[GPIOPAD]=0x183->false) so the deterministic DF-4942 producer is absent, and no GPIO_ID_GPIO_PAD forward-map case (no DF-4944 twin) -- the uninit-id path is the only cross-domain route and is probabilistic. Kernel-stage execution requires a DCE110 ASIC (Tonga/Fiji/Carrizo) + crafted VBIOS (guest has none).