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

dce_aux: partial AUX reads reassembled at buffer base instead of running offset β€” kernel-assembled DPCD/EDID corruption reported as SUCCEEDED

Summary

The read loop advances offset and shrinks current_read_length on DP-legal short reads, but process_read_request pins the reply destination to the buffer base while the loop's memset zeroes the tail - each subsequent chunk overwrites chunk 1, yielding [chunk2|zeros|stale] with SUCCEEDED status. Multi-byte DPCD/EDID reads silently corrupted for link training/mode selection. Bounded copy, no memory unsafety; distinct from DF-1694 (write path). Hostile-dongle threat model. Carry-the-offset fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4556 Β· 9 files
FileTypeDescriptionSize
README.md β€” 851 B ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
trigger.md β€” 1.5 KB ↓ raw
run.sh β€” 89 B view raw
env.txt β€” 472 B view raw
leak_sample.txt β€” 58 B view raw
fix.diff β€” 769 B view raw
manifest.json β€” 817 B view raw
verdict.json β€” 2.5 KB view raw

DF-4556 β€” dce_aux partial AUX read reassembly at wrong offset

Build

No code: the trigger is DP-sink behavior (short-read ACK). See trigger.md.

Run

Follow trigger.md on an AMD-GPU host with a programmable DP sink / aux emulator. The audit guest has Bochs VGA only β€” nothing to run (env.txt).

Expected

A >16-byte DPCD/EDID read whose chunks are ACKed short returns SUCCEEDED with buffer content [chunk2 | zeros | stale] instead of [chunk1 | chunk2] β€” kernel-assembled data corruption the driver then uses for link training / mode selection. Data-integrity bug, no overflow (copy bounded by returned_byte <= current_read_length <= payload.length).

Success criterion

On affected hardware: assembled read buffer contains chunk-2 at offset 0. After fix.diff (reply.data = ctx->buffer + ctx->offset): chunks concatenate in order.

VERDICT.md
↓ download raw

DF-4556 β€” dce_aux: partial AUX reads reassembled at the wrong offset (kernel-assembled DPCD/EDID data corruption)

What

read_command() (sys/dev/drm/amd/display/dc/dce/dce_aux.c:542-604) loops to assemble a multi-transaction AUX read. On a short read (sink ACKs with fewer bytes than requested β€” legal per DP 1.2a), process_read_reply() (dce_aux.c:426-447) advances ctx->offset += ctx->returned_byte and reduces current_read_length, and the loop re-requests the remainder (request.data = ctx->buffer + ctx.offset at dce_aux.c:583).

But the reply destination is never offset: process_read_request() sets

    ctx->reply.length = ctx->returned_byte;
    ctx->reply.data = ctx->buffer;          /* dce_aux.c:501-502 β€” base, not + offset */

so read_channel_reply() (dce_aux.c:288-294) copies the second chunk over buffer[0..R2-1] β€” destroying the first chunk β€” while the tail region buffer[offset..] was just zeroed by the loop's memset() (dce_aux.c:581) and is never filled.

Result: the caller's DPCD register block / EDID buffer contains [chunk2 | zeros | stale-bytes] instead of [chunk1 | chunk2], while the transaction is reported SUCCEEDED.

Who is affected

Callers of aux_engine->funcs->submit_request with payload.length > one AUX transaction (16 B), i.e. the i2caux path (dal_i2caux_submit_aux_command, i2caux/i2caux.c:196-267) used for I2C-over-AUX EDID segment reads and multi-byte DPCD reads. A DisplayPort sink that ACKs a 16-byte native read with, say, 8 bytes forces the partial path; the reassembly then silently corrupts the data the kernel subsequently uses for link training decisions and mode selection.

Upstream status: the whole read_command/process_read_* machinery was replaced in later DC releases (dce_aux_transfer_raw), so there is no upstream "fix commit" to point at β€” the reassembly logic was discarded. Within this DC version the ctx->buffer vs ctx->buffer + ctx->offset mismatch is provable from the code alone.

Impact

Data integrity only β€” not a memory-safety bug (the copy itself is bounded by returned_byte <= current_read_length <= payload.length). A hostile or marginal DP sink can make the driver train the link with garbage DPCD values or parse a scrambled EDID; the worst downstream outcome is a misconfigured link / rejected EDID (drm_edid re-checksums), plus repeated retries. This is a correctness defect with attacker-influence, filed at Low severity.

Reproduction

Hardware-gated (AMD GPU + DP sink returning short reads). The audit guest has Bochs VGA only (see ../env.txt), so the path cannot execute there. trigger.md documents the DP-sink behavior that forces the partial path.

Fix

See fix.diff β€” carry the offset into the reply destination.

Class considered and killed (why this is not DF-1694)

DF-1694 is the write path copying bytes_replied (re-read from HW, ≀31 via the 5-bit AUX_SW_REPLY_BYTE_COUNT field) into the fixed 16-byte write_command_context.reply_data[]. This finding is the read path writing a correctly-bounded number of bytes to a correctly-sized caller buffer at the wrong offset β€” no overflow, distinct defect in a different function.

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.

trigger.md
↓ download raw

DF-4556 trigger (hardware recipe)

The bug fires when a native/I2C-over-AUX read of >16 bytes receives an ACK carrying fewer data bytes than requested (short read). The DP 1.2a spec allows a sink to do this.

Force it on a machine with an AMD GPU (amdgpu DC) + DisplayPort:

  1. Use a programmable DP sink (or an FPD-Link/USB-C DP alt-mode test dongle with controllable firmware, e.g. an aux emulator built on an FT232H + DP plug, or a "bad DP sink" board).
  2. Answer every 16-byte EDID-segment / DPCD read with: status=ACK, 8 data bytes, then let the master retry the remainder.
  3. Observe on the host:

    dmesg | grep -i aux

and read back the assembled EDID/DPCD: the first 8 bytes will be the second chunk, bytes 8..15 zeroed β€” while the driver reports the read as SUCCEEDED.

Minimal aux emulator answer bytes for a short read (native AUX reply header 0x00 = ACK-with-8-bytes is 0x0 nibble + count 9 total bytes on the wire: 1 header + 8 data). The kernel path exercised:

read_command()                 dce_aux.c:542
  -> process_read_request()    dce_aux.c:480
  -> process_read_reply()      dce_aux.c:417  (offset += 8, len -= 8)
  -> [2nd chunk ACKed 8 bytes]
  -> read_channel_reply()      dce_aux.c:250  (copies to buffer[0..7])
  -> returned buffer = [chunk2 | zeros]

The audit guest has no AMD GPU (Bochs VGA 0x1234:0x1111 β€” env.txt), so this recipe could not be executed there; the defect is proven from source.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix not validated on guest: baseline cannot run (no AMD GPU). fix.diff is a one-line, line-accurate change; validated by inspection only.

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

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (offset-vs-base proof with path:line); trigger.md (DP sink short-read recipe); fix.diff (one-line offset fix mirroring request side at dce_aux.c:583)

PoC changes

Seed replaced: no userspace trigger exists (hardware behavior required); trigger.md documents the sink-side recipe.

Verified recommended fix

ctx->reply.data = ctx->buffer + ctx->offset in process_read_request() (dce_aux.c:502), mirroring the request side at :583.

Verdict

Code-provable reassembly defect: read_command() advances ctx->offset on short (partial-ACK) reads (dce_aux.c:426-433) and re-requests the remainder, but process_read_request() pins the reply destination to the buffer base (ctx->reply.data = ctx->buffer, dce_aux.c:502), so every subsequent chunk overwrites buffer[0..] while the tail stays zeroed by the loop memset (dce_aux.c:581). The driver then reports SUCCEEDED with corrupted DPCD/EDID content. The copy itself is bounded (returned_byte <= current_read_length <= payload.length), so this is data corruption, not memory unsafety β€” a DP-spec-legal short read from a hostile/marginal sink poisons link-training inputs. Not reproduced on the audit guest: no AMD GPU / no DP sink (env.txt); trigger requires a programmable sink per trigger.md. Distinct from DF-1694 (write-path overflow into fixed reply_data[16]); this is the read path, correct bounds, wrong offset.