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)
PoC verification
Evidence pack
findings/poc/DF-4556 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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.
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:
- 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).
- Answer every 16-byte EDID-segment / DPCD read with: status=ACK, 8 data bytes, then let the master retry the remainder.
-
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_testableFix 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
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.
No comments yet.