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)
PoC verification
Evidence pack
findings/poc/DF-4942 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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)
- 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). offset_to_id(0x183, 1<<shift)β true,*id=GPIO_ID_GPIO_PAD,*en=shiftβ[0..30](hw_translate_dce80.c:164-167).dal_gpio_create_ddcignoresidand createsGPIO_ID_DDC_DATA/CLOCKgpio objects withen(gpio_service.c:418-437).- 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 intoddc->regs/ddc->base.regs(hw_factory_dce80.c:122-127). pin->funcs->open/set_configβREG_UPDATE/REG_GETonregs->β¦(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).
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_offsetwere copied verbatim fromsys/dev/drm/amd/display/dc/gpio/dce80/hw_translate_dce80.c:44-400intoharness_dce80.c, with the real register constants fromdce_8_0_d.h/dce_8_0_sh_mask.h/smu_7_0_1_d.hand the real enum values fromgpio_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 withid=GPIO_ID_GPIO_PAD(4),en=20. 20 β₯ 8 = the number of entries inddc_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*idand interpretsenin thegpio_ddc_linedomain (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_testablefix.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.
Confirmed kernel references
- sys/dev/drm/amd/display/dc/gpio/dce80/hw_translate_dce80.c:164
- sys/dev/drm/amd/display/dc/gpio/dce80/hw_translate_dce80.c:172
- sys/dev/drm/amd/display/dc/gpio/gpio_service.c:418
- sys/dev/drm/amd/display/dc/gpio/dce80/hw_factory_dce80.c:84
- sys/dev/drm/amd/display/dc/gpio/dce80/hw_factory_dce80.c:122
- sys/dev/drm/amd/display/dc/core/dc_link_ddc.c:272
- sys/dev/drm/amd/display/dc/bios/bios_parser.c:1881
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.
No comments yet.