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

offset_to_id returns success for all DDC-family offsets without ever writing *id β€” dal_gpio_service_create_irq then switches on an uninitialized gpio_id; garbage==GPIO_ID_HPD smuggles DDC-line en=6/7 into hpd_regs[6] (OOB)

Summary

All eight DDC branches write only *en; the consumer declares id uninitialized and switches on it - garbage==HPD(3) with a DDC en of 6/7 indexes hpd_regs[6..7] past the 6-entry array and programs MMIO from garbage. Poison-survival proven verbatim on guest (3 offsets); the same fix.diff as DF-4942 structurally eliminates the uninitialized use.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4943 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 2.0 KB ↓ raw
harness_dce80.c β€” 18.9 KB view raw
harness_fixed.c β€” 19.0 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 89 B view raw
build.log β€” 68 B view raw
run.log β€” 2.1 KB view raw
fixrun.log β€” 2.0 KB view raw
env.txt β€” 419 B view raw
fix.diff β€” 3.3 KB view raw
manifest.json β€” 770 B view raw
verdict.json β€” 3.7 KB view raw

DF-4943 β€” hw_translate_dce80.c: offset_to_id returns success for DDC-family offsets without writing *id β†’ consumer switches on an uninitialized gpio_id

File audited (pass 2): sys/dev/drm/amd/display/dc/gpio/dce80/hw_translate_dce80.c Lines: 172-196. Consumer: gpio_service.c:143-157 (dal_gpio_service_create_irq).

What to run

./build.sh && ./run.sh     # harness_dce80.c T1 section (run.log)

Expected (reproduced on the DF 6.5-DEVELOPMENT guest)

== T1: DDC/VGA/I2CPAD offsets return success WITHOUT writing *id ==
offset_to_id(mmDC_GPIO_DDCVGA_A,0x100) = true en=6(id==(other))
  -> BUG(DF-4943): success returned, *id NEVER WRITTEN; consumer switches on uninitialized id
offset_to_id(mmDC_GPIO_I2CPAD_A,0x1)  = true en=7(id==(other))
offset_to_id(mmDC_GPIO_DDC1_A,0x100)  = true en=0(id==(other))

(0x7B7B7B7B poison left in id β€” the function returns true having only written *en; the same holds for DDC2..DDC6.)

Kernel chain (code trace)

  1. VBIOS GPIO pin LUT entry (usGpioPin_AIndex, arbitrary u16 β€” bios_parser.c:1819-1820) that decodes to a DDC-family register offset is passed by get_hpd_gpio (dc_link.c:115-118) into dal_gpio_service_create_irq for a connector's hotplug pin.
  2. gpio_service.c:148-149 declares enum gpio_id id; uint32_t en; uninitialized; offset_to_id (line 151) returns true without writing id for DDC offsets (hw_translate_dce80.c:172-196).
  3. Line 156 passes the indeterminate id to dal_gpio_create_irq (gpio_service.c:366-391): only GPIO_ID_HPD(3) / GPIO_ID_GPIO_PAD(4) proceed; everything else is (correctly) rejected.
  4. If the garbage value is 3 (GPIO_ID_HPD), dal_gpio_create(service, HPD, en∈{0..7}, …) β†’ dal_gpio_service_open(HPD, en) β†’ define_hpd_registers( pin, en) (gpio_service.c:258-261) β†’ hpd->regs = &hpd_regs[en] (hw_factory_dce80.c:143) with hpd_regs[] sized 6 β†’ en 6/7 is an OOB const-array read; the garbage int_status/toggle_filt_cntl descriptors then drive REG_GET/REG_UPDATE_2 MMIO (hw_hpd.c:89,110). (id_to_offset itself would reject HPD enβ‰₯6 β€” proven in the harness T5 β€” but that check sits in a different function than the open path.)

Uninitialized-memory use is MSan-detectable and CWE-457 regardless of which garbage value lands in the slot; the OOB sub-chain additionally requires that value to equal 3 (per-build deterministic, unknown statically).

After fix.diff

T1 shows *id = GPIO_ID_DDC_DATA written on every DDC decode (fixrun.log: "-> FIXED: *id now written (GPIO_ID_DDC_DATA), consumer guard accepts"), eliminating the uninitialized read entirely.

Impact ceiling

Same VBIOS-craft precondition as DF-4942 plus a stack-garbage coin flip; worst case OOB rodata read β†’ wild MMIO (DoS). Rated Low because both stages are probabilistic relative to DF-4942's deterministic producer.

VERDICT.md
↓ download raw

DF-4943 VERDICT

Status: reproduced (unit-level contract violation executed on the DF guest); kernel consequence chain proven by code trace, probabilistic final stage.

  • harness_dce80.c (verbatim copy of offset_to_id from hw_translate_dce80.c:63-208) demonstrates on the guest that offset_to_id(mmDC_GPIO_DDCVGA_A|mmDC_GPIO_I2CPAD_A|mmDC_GPIO_DDC1_A, …) returns true while leaving the caller's id at its pre-call poison value β€” the function's success contract does not include writing *id for the entire DDC family (lines 172-196). This is exactly the situation dal_gpio_service_create_irq (gpio_service.c:148) creates with its uninitialized locals.
  • Consumer behavior traced: dal_gpio_create_irq (gpio_service.c:366-391) only proceeds for id == GPIO_ID_HPD(3) or GPIO_ID_GPIO_PAD(4). With garbage == 3 and a DDC-line en of 6/7, define_hpd_registers (gpio_service.c:261 β†’ hw_factory_dce80.c:143) computes &hpd_regs[6..7] past the 6-entry array; hw_hpd.c:89/110 then issues MMIO from the garbage descriptors. With garbage == 4 the open is rejected (default case, gpio_service.c:271-273) β€” harmless. Any other garbage β†’ NULL β†’ callers handle.
  • id_to_offset's own HPD enβ‰₯6 rejection (harness T5: id_to_offset(HPD,en=6)=false) does not protect this path β€” that check lives in a different function than the create/open flow.
  • Severity Low: crafted-VBIOS precondition plus stack-garbage value == 3; the deterministic sibling producer is DF-4942.
  • Fix (fix.diff) initializes *id on every DDC decode; harness_fixed shows the consumer now always observes GPIO_ID_DDC_DATA β€” the uninitialized use is structurally gone, 0 regressions.

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 sets id=GPIO_ID_DDC_DATA in every DDC-family branch of offset_to_id, so the consumer's id is always initialized on success (fixrun.log: '-> FIXED: id now written (GPIO_ID_DDC_DATA), consumer guard accepts'); dal_gpio_create_irq's switch then deterministically rejects DDC decodes; 0 regressions.

fixrun.log T1 section; fix.diff hunk 1 (eight '+		*id = GPIO_ID_DDC_DATA;' lines).
↓ fix.diffper-fix-DF-4943

Confirmed kernel references

Detail

Exploit chain

crafted VBIOS GPIO pin LUT (usGpioPin_AIndex = a DDC register offset, bios_parser.c:1819) -> get_hpd_gpio (dc_link.c:115) -> dal_gpio_service_create_irq -> offset_to_id true, id UNWRITTEN -> switch on garbage id -> garbage==GPIO_ID_HPD(3) -> dal_gpio_service_open(HPD, en=6/7) -> define_hpd_registers -> hpd_regs[6..7] OOB rodata read -> REG_UPDATE MMIO at garbage offsets -> panic/MCE. Probabilistic final stage (stack-garbage dependent).

Evidence (decisive lines)

run.log T1: three DDC offsets each return true with id left at the poison value ('*id NEVER WRITTEN'); T5: id_to_offset(HPD,en=6)=false proves the only route for HPD en>=6 into hpd_regs[] is the uninitialized-id path; fixrun.log: after fix, *id==GPIO_ID_DDC_DATA on all DDC decodes.

PoC changes

n/a β€” fresh harness (verbatim function copy, real constants).

Verified recommended fix

Write *id (GPIO_ID_DDC_DATA) in every successful offset_to_id DDC-family branch so no consumer can observe an uninitialized id.

Verdict

All eight DDC-family branches of offset_to_id (hw_translate_dce80.c:172-196) return true after writing only *en; dal_gpio_service_create_irq (gpio_service.c:148-156) then feeds the never-written id into dal_gpio_create_irq's switch β€” a use of an uninitialized stack variable (CWE-457). Reproduced on the DF guest with the verbatim function copy: a poisoned id survives a successful offset_to_id(mmDC_GPIO_DDCVGA_A/I2CPAD/DDC1) call (run.log T1). If the indeterminate slot holds GPIO_ID_HPD(3), a DDC-line en of 6/7 reaches define_hpd_registers, which indexes hpd_regs[6] β€” an OOB const-array read (array has 6 entries, hw_factory_dce80.c:61-68,143) whose garbage descriptors then drive MMIO (hw_hpd.c:89,110); id_to_offset's own en>=6 rejection (harness T5) sits off the open path and does not intercept it. Kernel-stage execution requires an AMD ASIC + crafted VBIOS (absent in QEMU); the uninitialized-write contract violation itself is proven by execution.