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

Fake-EDID record: kmalloc(128) with memcpy of only ucFakeEDIDLength bytes leaves an uninitialized heap tail that (a) drives an OOB multi-block walk in drm_edid_is_valid via uninit extensions byte and (b) on a ~1/256 checksum pass is exposed to unprivileged users via the EDID ioctl (kernel heap info leak)

Summary

The EDID buffer is kmalloc(max(128,len)) but only len bytes are copied. For len<128 the tail is uninitialized heap: drm_edid_is_valid reads the uninit extensions byte and walks up to 255 extra 128-byte blocks past the allocation; if the block-0 checksum passes (~1/256), the tainted 128-byte EDID is cached and copied wholesale to userspace via GETCONNECTOR - probabilistic kernel heap disclosure (<=127 bytes) to any local user. kzalloc fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4105 Β· 4 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 2.3 KB ↓ raw
verdict.json β€” 2.2 KB view raw
fix.diff β€” 539 B view raw

DF-4105 β€” amdgpu atombios_encoders: fake-EDID record leaves uninitialized kmalloc tail that can be read back by unprivileged users (CWE-457), plus uninit extensions byte drives an OOB walk in drm_edid_is_valid

File: sys/dev/drm/amd/amdgpu/atombios_encoders.c (pass 2) Class: uninitialized kernel heap memory disclosure (CWE-457/908) + OOB read Hardware gate: AMD GPU, LCD encoder with a fake-EDID patch record in VBIOS. Source-level certainty. Related: DF-1380 (same record loop β€” bounds of the walk and the OOB memcpy source; this finding is the uninitialized tail of the destination β€” different read source, different fix). radeon twin: DF-3744 mentions the leak conditionally; here it is isolated with the exact gate.

The defect

atombios_encoders.c:2167-2183:

case LCD_FAKE_EDID_PATCH_RECORD_TYPE:
    fake_edid_record = (ATOM_FAKE_EDID_PATCH_RECORD *)record;
    if (fake_edid_record->ucFakeEDIDLength) {          /* u8, 1..255 */
        struct edid *edid;
        int edid_size = max((int)EDID_LENGTH, (int)fake_edid_record->ucFakeEDIDLength);  /* >= 128 */
        edid = kmalloc(edid_size, M_DRM, GFP_KERNEL);  /* NOT zeroed */
        if (edid) {
            memcpy((u8 *)edid, (u8 *)&fake_edid_record->ucFakeEDIDString[0],
                   fake_edid_record->ucFakeEDIDLength);   /* only `len` bytes */
            if (drm_edid_is_valid(edid)) {
                adev->mode_info.bios_hardcoded_edid = edid;
                adev->mode_info.bios_hardcoded_edid_size = edid_size;

When 1 <= ucFakeEDIDLength < 128, bytes [len, 128) of the 128-byte kmalloc chunk are uninitialized heap. Two consequences:

  1. drm_edid_is_valid (drm_edid.c:1484-1497) loops for (i = 0; i <= edid->extensions; i++) drm_edid_block_valid(raw + i*128, ...). edid->extensions is byte 126 β€” uninitialized when len < 127. A garbage non-zero value makes the loop read up to 255 additional 128-byte blocks past the 128-byte allocation (OOB heap read; each block is fully read before its checksum rejects it).
  2. If the garbage happens to pass the block-0 checksum (~1/256 per boot for a random tail), the EDID is cached with the uninit tail included, and amdgpu_connector_get_edid (amdgpu_connectors.c:269-274) copies the full bios_hardcoded_edid_size (128) bytes into the connector's EDID, which any unprivileged user can read back with DRM_IOCTL_MODE_GETCONNECTOR (hw.backlight EDID property) β†’ kernel heap info leak of up to 127-len bytes.

Build / Run

Hardware-gated; audit guest has no AMD GPU. See VERDICT.md.

Fix

Zero the allocation (kzalloc) β€” one line. See fix.diff.

VERDICT.md
↓ download raw

DF-4105 VERDICT

Status: untested (hardware-gated β€” no AMD GPU on the audit guest; source-level certainty certain) Impact: conditional kernel-heap info leak to unprivileged userspace (≀127 bytes, ~1/256 per boot) + up-to-32KB OOB heap read probe during validation.

Trace

  • amdgpu_atombios_encoder_get_lcd_info record walk (atombios_encoders.c:2156) reaches a LCD_FAKE_EDID_PATCH_RECORD_TYPE record (2167).
  • edid = kmalloc(max(128, len)) at 2173 β€” plain kmalloc, no zeroing.
  • memcpy(edid, ..., len) at 2175-2176 initializes only len bytes.
  • drm_edid_is_valid(edid) at 2178 (drm_edid.c:1484): reads edid->extensions = byte 126 (uninit when len ≀ 126) and walks raw + i*128 for i in [0, extensions] β€” each block fully read (header + checksum) before rejection β†’ OOB read past the chunk when garbage extensions != 0 (likely for random heap: ~99.6%).
  • On block-0 checksum pass (~1/256 with random tail): cached as bios_hardcoded_edid (2179-2180).
  • Userspace readback: amdgpu_connector_get_edid (amdgpu_connectors.c:269-274) duplicates the full 128-byte buffer (tail included) into the connector EDID β†’ returned via the standard GETCONNECTOR ioctl / debugfs EDID node, readable by any local user.

Reachability gate: crafted VBIOS with a fake-EDID record whose declared length is < 128 (or ≀ 126 for the extensions variant). Same threat model as DF-1380 (root-flashed VBIOS / malicious option ROM) β€” hence Low severity, consistent with DF-1380's rating.

Not DF-1380: DF-1380's fix (bound the record pointer vs bios_end before the walk and before the memcpy) does NOT touch this β€” the leak source is the destination buffer's uninitialized tail, fixed by kzalloc.

Why untested

Guest has no AMD GPU (bochs 1234:1111 only); the record walk never executes. Source trace is line-exact.

Fix

edid = kzalloc(edid_size, M_DRM, GFP_KERNEL); β€” zero-initialize so the tail cannot leak heap contents and edid->extensions is deterministic (0 β†’ single-block validation only).

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

One-line kzalloc fix removes both the uninit-tail leak and the uninit extensions-byte walk; not buildable/testable on guest (no AMD GPU).

findings/poc/DF-4105/fix.diff
↓ fix.diffper-fix-DF-4105

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md: the three code excerpts (atombios_encoders.c:2167-2183, drm_edid.c:1484-1497, amdgpu_connectors.c:269-274) and the 1/256 checksum-gate analysis.

PoC changes

No runnable PoC (hardware absent); pack is the source trace + one-line kzalloc fix.

Verified recommended fix

kzalloc instead of kmalloc for the EDID buffer (fix.diff).

Verdict

Source-level certain: the fake-EDID record handler kmalloc()s max(128,len) bytes but memcpy()s only ucFakeEDIDLength of them (atombios_encoders.c:2173-2176); for len<128 the tail is uninitialized heap. drm_edid_is_valid (drm_edid.c:1484-1497) then reads the uninit byte126 (extensions) and walks up to 255 extra 128-byte blocks past the chunk (OOB heap read), and on a ~1/256 checksum pass the tainted 128-byte EDID is cached as bios_hardcoded_edid and copied wholesale to userspace via amdgpu_connector_get_edid (amdgpu_connectors.c:269-274) + GETCONNECTOR ioctl - kernel heap info leak. Distinct from DF-1380 (walk/memcpy source bounds; this is the destination's uninit tail, fixed by kzalloc). Hardware absent on guest.