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

offset_to_id accepts mmGPIOPAD_A with en up to 30; the DDC consumer dal_gpio_create_ddc ignores the id output and hands en to define_ddc_registers, indexing ddc_data_regs[8]/ddc_clk_regs[8] out of bounds β€” OOB const-array read whose garbage register descriptors then drive MMIO programming

Summary

The GPIOPAD branch validates en only against its own domain (<=30) and returns true, but the consumer discards the id output (the in-file comment admits it) and treats en as a ddc_line - ddc_data_regs[en] with en 8..30 is a deterministic OOB rodata read feeding REG_UPDATE MMIO at garbage offsets. VBIOS-controlled input (usClkA_RegisterIndex). Verbatim-function harness on guest proves the decode; consumer+fix traced/validated (reject-at-consumer, 0 regressions). Write-*id-in-DDC-branches + domain-guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4942 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 4.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 β€” 918 B view raw
verdict.json β€” 4.0 KB view raw

DF-4942 β€” hw_translate_dce80.c: GPIOPAD decode leaks en≀30 into the 8-entry DDC register arrays (OOB read β†’ wild MMIO programming)

File audited (pass 2): sys/dev/drm/amd/display/dc/gpio/dce80/hw_translate_dce80.c Lines: 164-167 (mmGPIOPAD_A case), 172-196 (DDC cases with no *id), consumer chain in gpio_service.c:408-457, hw_factory_dce80.c:84-104,114-137.

What to run

./build.sh          # compiles harness_dce80.c (verbatim copy of offset_to_id/id_to_offset,
                    #  real DCE8 register constants) and the fixed variant
./run.sh            # runs both; see run.log (baseline) and fixrun.log (after fix.diff)

Expected (baseline, reproduced on the DF 6.5-DEVELOPMENT guest, gcc 8.3)

== T2: GPIOPAD offset succeeds with en up to 30 (DF-4942 producer) ==
offset_to_id(mmGPIOPAD_A,1<<20) = true id=GPIO_ID_GPIO_PAD(4) en=20
  -> BUG(DF-4942): success with en=20 >= 8 entries: define_ddc_registers reads
     ddc_data_regs[20] OOB (hw_factory_dce80.c:122), regs then drive MMIO

T3 (negative control) in the same output proves the GPIOPAD bound inside offset_to_id is sound (31/UNKNOWN=32/multi-bit rejected) β€” the defect is the cross-domain hand-off, not the internal bound.

Expected (after fix.diff)

  -> FIXED: translate still decodes en=20, but fixed consumer guard
     (id != GPIO_ID_DDC_DATA) rejects: accepted=0 -> no ddc_data_regs[20] OOB
0 regressions after fix (expect 0)

Full kernel chain (code-trace; not executable in QEMU β€” no AMD ASIC)

  1. VBIOS ATOM_GPIO_I2C_INFO.asGPIO_Info[line].usClkA_RegisterIndex (arbitrary u16, bios_parser.c:1881-1882) β†’ dal_gpio_create_ddc(svc, 0x183 /*mmGPIOPAD_A*/, 1<<shift, …) (dc_link_ddc.c:272-276).
  2. offset_to_id(0x183, 1<<shift) β†’ true, *id=GPIO_ID_GPIO_PAD, *en=shift∈[0..30] (hw_translate_dce80.c:164-167).
  3. dal_gpio_create_ddc ignores id and creates GPIO_ID_DDC_DATA/CLOCK gpio objects with en (gpio_service.c:418-437).
  4. First DDC use β†’ dal_gpio_service_open β†’ define_ddc_registers(pin, en) β†’ ddc_data_regs[en] / ddc_clk_regs[en] β€” arrays of 8 entries (hw_factory_dce80.c:84-104) β†’ for en 8..30 an OOB read of const .rodata, pointer-stored into ddc->regs/ddc->base.regs (hw_factory_dce80.c:122-127).
  5. pin->funcs->open/set_config β†’ REG_UPDATE/REG_GET on regs->… (hw_ddc.c/hw_gpio.c reg_helper) β†’ MMIO R/W at garbage register offsets β†’ panic/MCE or wrong-register corruption of display-block state.

Impact ceiling

Deterministic OOB (no uninitialized-stack dependency) given one crafted u16 in the VBIOS image at amdgpu attach. Not reachable by an unprivileged user on stock hardware (VBIOS is GPU firmware); realistic preconditions: re-flashed VBIOS, corrupted firmware, or a host-supplied ROM in GPU-passthrough setups. Impact: kernel panic / device-state corruption (DoS class). No RAM write primitive (addresses land in MMIO space).

VERDICT.md
↓ download raw

DF-4942 VERDICT

Status: reproduced (unit-level, verbatim function copy executed on the DF guest); kernel chain proven by code trace β€” full execution requires an AMD ASIC + crafted VBIOS, which the QEMU guest does not have.

What was proven, and how

  • The audited functions index_from_vector, offset_to_id, id_to_offset were copied verbatim from sys/dev/drm/amd/display/dc/gpio/dce80/hw_translate_dce80.c:44-400 into harness_dce80.c, with the real register constants from dce_8_0_d.h/dce_8_0_sh_mask.h/smu_7_0_1_d.h and the real enum values from gpio_types.h. Compiled with gcc 8.3 (-O2 -Wall -Wextra, zero warnings) and executed on the DragonFly 6.5-DEVELOPMENT guest.
  • Output (run.log): offset_to_id(mmGPIOPAD_A, 1<<20) returns true with id=GPIO_ID_GPIO_PAD(4), en=20. 20 β‰₯ 8 = the number of entries in ddc_data_regs[]/ddc_clk_regs[] (hw_factory_dce80.c:84-104). That is the exact decode result the kernel consumer receives.
  • Negative controls (T3): 1<<31 β†’ false (31 > GPIO_GPIO_PAD_MAX=30), multi-bit mask β†’ false (GPIO_ENUM_UNKNOWN=32 > 30), zero β†’ false. The internal GPIOPAD bound is sound; the defect is that success is reported in a call context whose only consumer (dal_gpio_create_ddc, gpio_service.c:408-457) discards *id and interprets en in the gpio_ddc_line domain (8 entries).

Kernel chain (traced, path:line)

bios_parser.c:1881 (usClkA_RegisterIndex, arbitrary VBIOS u16; equals mmGPIOPAD_A=0x183 on the DCE8 input path β€” smu_7_0_1_d.h:138) β†’ dc_link_ddc.c:272-276 (dal_gpio_create_ddc(gpio_service, 0x183, 1<<shift, …)) β†’ gpio_service.c:418-419 offset_to_id success, 428-437 creates DDC_DATA/DDC_CLOCK gpio objects with en (id output ignored) β†’ first DDC use gpio_base.c:59-72 dal_gpio_open_ex β†’ gpio_service.c:244-253 create_ddc_data + define_ddc_registers(pin, en) β†’ hw_factory_dce80.c:122 ddc->regs = &ddc_data_regs[en] β€” OOB const-array read for en ∈ [8..30] β€” β†’ hw_ddc.c/hw_gpio.c use regs->… addresses for REG_UPDATE/REG_GET MMIO (garbage offsets from adjacent .rodata: likely ddc_clk_regs[0..] for en 8..15 β€” wrong-but-in-layout register descriptors; beyond that, shift/mask constants reinterpreted as register addresses β†’ unmapped MMIO β†’ panic/MCE).

Related already-filed finding (consumer side, different array): DF-4827 (dce_i2c_sw.c sw_i2cs[MAX_PIPES=6] vs GPIO_DDC_LINE_COUNT=8). DF-4942 is the translate-side producer defect for a different consumer array pair (ddc_data_regs/ddc_clk_regs, 8 entries) with the GPIOPAD domain (en≀30) as the OOB source β€” not a re-report.

Why not executed end-to-end

Guest has no AMD GPU (pciconf: 0 devices vendor 0x1002; amdgpu module not loaded — env.txt). The VBIOS→GPIO path only executes on real ASICs at driver attach. The deterministic producer stage was executed verbatim instead; the consumer stage is a four-hop straight-line trace with no branches that could change the outcome (en is passed unmodified: create_ddc_data(ctx,id,en) → define_ddc_registers(pin,en) → array index).

Fix validation

fix.diff (3 files: dce80 translate sets *id=GPIO_ID_DDC_DATA on all 8 DDC cases; dce120 same; gpio_service.c:dal_gpio_create_ddc rejects id != GPIO_ID_DDC_DATA) β€” git apply --check clean; harness_fixed.c models the fixed translate + consumer guard: en=20 decode now rejected at the consumer (accepted=0), 0 regressions, T4/T5 behaviors preserved (fixrun.log).

Impact

Medium. DoS-class (panic/wrong-register MMIO) at driver attach with a crafted/corrupt VBIOS; not unprivileged-user-reachable on stock hardware.

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 (translate sets *id=GPIO_ID_DDC_DATA on all DDC cases + dal_gpio_create_ddc rejects id != GPIO_ID_DDC_DATA) applied to the harness model: the en=20 GPIOPAD decode is rejected at the consumer (accepted=0), 0 regressions across all test classes (fixrun.log); git apply --check clean against sys/.

fixrun.log lines: '-> FIXED: translate still decodes en=20, but fixed consumer guard (id != GPIO_ID_DDC_DATA) rejects: accepted=0 -> no ddc_data_regs[20] OOB' and '0 regressions after fix (expect 0)'; fix.diff in pack.
↓ fix.diffper-fix-DF-4942

Confirmed kernel references

Detail

Exploit chain

crafted VBIOS GPIO_I2C record (usClkA_RegisterIndex=0x183 mmGPIOPAD_A, ucClkA_Shift in 8..30) -> dc_link_ddc.c:272 dal_gpio_create_ddc -> offset_to_id success with en=8..30 -> gpio_service.c:428-437 DDC gpio objects with foreign-domain en -> first DDC open -> define_ddc_registers -> ddc_data_regs[en] OOB rodata read -> REG_UPDATE/REG_GET MMIO at garbage offsets -> panic/MCE or display-block state corruption. (Same input family as DF-4827 but different consumer array: ddc_data_regs/ddc_clk_regs sized 8 vs the GPIOPAD domain 0..30.)

Evidence (decisive lines)

run.log T2 section: 'offset_to_id(mmGPIOPAD_A,1<<20) = true id=GPIO_ID_GPIO_PAD(4) en=20 -> BUG(DF-4942) ... ddc_data_regs[20] OOB (hw_factory_dce80.c:122)'; T3 negative controls prove the internal GPIOPAD bound is sound; fixrun.log: consumer guard rejects the decode, 0 regressions; harness_dce80.c is a verbatim copy of hw_translate_dce80.c:44-400 with real header constants.

PoC changes

n/a β€” harness written fresh (verbatim function copy; BREAK_TO_DEBUGGER mapped to printf; real DCE8 register constants from dce_8_0_d.h/dce_8_0_sh_mask.h/smu_7_0_1_d.h).

Verified recommended fix

Set *id=GPIO_ID_DDC_DATA in all DDC-family offset_to_id cases and make dal_gpio_create_ddc reject decodes whose id is not GPIO_ID_DDC_DATA.

Verdict

offset_to_id's mmGPIOPAD_A case (hw_translate_dce80.c:164-167) reports success with en in [0..30] while the only reverse-map consumer, dal_gpio_create_ddc (gpio_service.c:408-457), discards the id output and treats en as a gpio_ddc_line, letting define_ddc_registers index ddc_data_regs[8]/ddc_clk_regs[8] (hw_factory_dce80.c:122) β€” for en 8..30 this is a deterministic OOB const-array read whose register descriptors then drive MMIO programming. The verbatim function copy executed on the DragonFly guest reproduces the decode (offset_to_id(mmGPIOPAD_A,1<<20) == true, en=20); the kernel stages are a straight-line code trace (four hops, en unmodified). End-to-end kernel execution requires an AMD ASIC with a crafted VBIOS (usClkA_RegisterIndex == 0x183), absent from the QEMU guest. DoS-class (panic/wrong-register MMIO at driver attach); no unprivileged-user reach on stock hardware.