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

dce110 id_to_offset DDC_DATA/I2C_PAD translation carries the shared DDC6 DATA mask 0x100, but DC_GPIO_I2CPAD_A defines only SCL=bit0 (0x1) and SDA=bit1 (0x2) β€” reserved-bit MMIO write and dead SDA bit on the I2C pad

Summary

Shared-mask shortcut ignores the pad register's different layout: DDC_DATA on the I2C pad selects nonexistent bit 8 while never touching real SDA (CLOCK correct by coincidence). Deterministic constant-arithmetic PoC on guest; the dcn10 twin was DF-4947. SDA-mask override fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4966 Β· 10 files
FileTypeDescriptionSize
mask_check.c β€” 2.5 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 116 B view raw
build.log β€” 9 B view raw
run.log β€” 433 B view raw
env.txt β€” 192 B view raw
fix.diff β€” 473 B view raw
VERDICT.md β€” 3.3 KB ↓ raw
README.md β€” 819 B ↓ raw
verdict.json β€” 3.3 KB view raw

DF-4966 β€” dce110 hw_translate id_to_offset: I2C-pad DDC_DATA uses the shared DDC6 DATA mask 0x100; the pad's SDA bit is 0x2

Reproduce

./build.sh          # gcc -O2 -Wall -Wextra -o mask_check mask_check.c
./run.sh

Expected: id_to_offset(DDC_DATA, I2C_PAD) mask = 0x100 -> WRONG: selects bit 8; SDA is bit 1 while the DDC1 control and the DDC_CLOCK/I2C_PAD case come out correct (shared mask valid for real DDC lines; CLK bit == SCL bit).

Files

  • mask_check.c β€” evaluates the translation against the real DC_GPIO_I2CPAD_A field constants (dce_11_0_sh_mask.h:4203-4206)
  • build.sh, run.sh, build.log, run.log, env.txt
  • fix.diff β€” override the mask in the DDC_DATA/I2C_PAD case with DC_GPIO_I2CPAD_A__DC_GPIO_SDA_A_MASK
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-4966 VERDICT

Status: reproduced (the wrong-bit translation is deterministic and was executed on the DF guest against the real DCE11 register field constants). No memory-safety consequence β€” functional MMIO misprogramming only.

The defect

id_to_offset in sys/dev/drm/amd/display/dc/gpio/dce110/hw_translate_dce110.c assigns the shared DDC data mask to every GPIO_ID_DDC_DATA line:

  • hw_translate_dce110.c:189-190: info->mask = DC_GPIO_DDC6_A__DC_GPIO_DDC6DATA_A_MASK;
  • hw_translate_dce110.c:213-215: case GPIO_DDC_LINE_I2C_PAD: info->offset = mmDC_GPIO_I2CPAD_A; β€” the offset changes but the mask stays 0x100.

On DCE11, the real DC_GPIO_I2CPAD_A (0x4899) field layout is (dce_11_0_sh_mask.h:4203-4206):

  • DC_GPIO_I2CPAD_A__DC_GPIO_SCL_A_MASK = 0x1 (bit 0)
  • DC_GPIO_I2CPAD_A__DC_GPIO_SDA_A_MASK = 0x2 (bit 1)

unlike the DDCx_A/DDCVGA_A registers where CLK=bit0 / DATA=bit8 (0x100), which is why the shared mask is correct for DDC1..6 and VGA (verified controls: DDC1DATA=0x100, DDC6DATA=0x100, VGADATA=0x100).

So id_to_offset(GPIO_ID_DDC_DATA, GPIO_DDC_LINE_I2C_PAD) returns offset=0x4899 with mask=0x100 β€” a bit that does not exist in that register (SDA is bit 1). The DDC_CLOCK twin is correct only by coincidence (DDC CLK bit0 == pad SCL bit0).

Manifestation (run.log, guest)

  • id_to_offset(DDC_DATA, I2C_PAD) mask = 0x100 -> WRONG: selects bit 8
  • id_to_offset(DDC_CLOCK, I2C_PAD) mask = 0x1 -> correct (by coincidence)
  • control id_to_offset(DDC_DATA, DDC1) = 0x100 == DDC1DATA_A_MASK -> correct

Consequence / impact ceiling

The hw_gpio/hw_ddc code then does REG_UPDATE/REG_GET on 0x4899 with mask 0x100: read-modify-write sets/reads an undefined bit while never touching the real SDA bit (0x2). Effects: the I2C-pad GPIO DDC line cannot work (reads always 0; writes hit a reserved bit) β€” a functional defect; on hardware where bit 8 of that register aliases something else, unintended MMIO state change. No kernel memory-safety impact; upstream-shared code (AMD DAL dce110 hw_translate is verbatim from the amdgpu display-core).

Reached from dal_gpio_create_ddc when the VBIOS I2C record points the DDC data line at the I2C pad (dc_link_ddc.c:272-276, offset/mask from get_gpio_i2c_info), i.e. crafted-VBIOS or real boards that wire the pad that way. Severity: Info (hardening / correctness; no demonstrated security impact).

Fix

fix.diff: override the mask in the DDC_DATA I2C_PAD case with the real SDA bit (DC_GPIO_I2CPAD_A__DC_GPIO_SDA_A_MASK). Validated at harness level β€” the wrong-bit is structurally eliminated; all other lines keep the shared mask.

Guest

DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, 2026-07-02), gcc 8.3. No AMD hardware (amdgpu modules: 0, vendor-0x1002 PCI devices: 0), so the register constants were verified against the tree's generated headers and the translation executed at unit level β€” the defect itself is pure constant arithmetic, deterministic on any host.

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 overrides the mask in the DDC_DATA/I2C_PAD case with DC_GPIO_I2CPAD_A__DC_GPIO_SDA_A_MASK (0x2) -- the defect is pure constant selection, structurally eliminated by the one-line override; all other lines keep the shared mask (harness controls). No kernel-build validation possible on this guest (no AMD hardware); Info-severity functional fix.

fix.diff (2-line hunk, hw_translate_dce110.c DDC_DATA/I2C_PAD case); run.log controls demonstrate shared-mask correctness for the other seven lines.
↓ fix.diffper-fix-DF-4966

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: 'id_to_offset(DDC_DATA, I2C_PAD) mask = 0x100 -> WRONG' vs real SCL=0x1/SDA=0x2 field layout; controls: DDC_CLOCK/I2C_PAD mask 0x1 == SCL (correct by coincidence), DDC_DATA/DDC1 mask 0x100 == DDC1DATA (shared mask valid for real DDC lines).

PoC changes

fresh minimal constant-arithmetic harness; fixed a nested-comment gcc error from the seed draft.

Verified recommended fix

Set info->mask = DC_GPIO_I2CPAD_A__DC_GPIO_SDA_A_MASK in the GPIO_ID_DDC_DATA / GPIO_DDC_LINE_I2C_PAD case.

Verdict

id_to_offset in hw_translate_dce110.c assigns the shared DDC data mask DC_GPIO_DDC6_A__DC_GPIO_DDC6DATA_A_MASK (0x100) to every GPIO_ID_DDC_DATA line (line 190) and merely swaps the offset to mmDC_GPIO_I2CPAD_A (0x4899) for GPIO_DDC_LINE_I2C_PAD (lines 213-215). The DCE11 register DC_GPIO_I2CPAD_A defines only SCL=bit0 (0x1) and SDA=bit1 (0x2) (dce_11_0_sh_mask.h:4203-4206), so the DDC_DATA(SDA) translation carries a mask for a bit that does not exist in the target register. Deterministic (constant arithmetic), executed on the DF guest: mask_check run.log shows 'id_to_offset(DDC_DATA, I2C_PAD) mask = 0x100 -> WRONG: selects bit 8; SDA is bit 1', with the DDC1 control and the DDC_CLOCK/I2C_PAD case (0x1 == SCL by coincidence) correct. Consequence: REG_UPDATE/REG_GET on 0x4899 read-modify-write an undefined bit and never touch the real SDA bit -- functional defect (I2C-pad DDC line dead / reserved-bit MMIO write), upstream-shared AMD DAL code, no kernel memory-safety impact. Info severity.