dce_aux: stack buffer overflow in AUX write-reply path (bound check compares HW field against itself)
| Field | Value |
|---|---|
| ID | DF-1694 |
| File | sys/dev/drm/amd/display/dc/dce/dce_aux.c |
| Lines | 285, 288, 291, 292, 294, 690, 691 |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| Confidence | likely |
| Status | new |
| CVE match | variant (DP sink reply length OOB β twin of i2caux/aux_engine.c which IS reachable; affects upstream Linux amdgpu) |
| Created | 2026-07-18 |
Summary
In the write-command path, ctx.reply_data is a fixed 16-byte stack buffer
(DEFAULT_AUX_MAX_DATA_SIZE, aux_engine.h:135), but
read_channel_reply's overflow guard
if (bytes_replied > size) return -1 (dce_aux.c:285) compares the
HW-reported AUX_SW_REPLY_BYTE_COUNT against size, where size is
ctx.reply.length which itself was just set to ctx.returned_byte β the
SAME 5-bit HW field (mask 0x1f000000, max 31).
A malicious DisplayPort sink that returns >17 bytes in reply to a write
can drive bytes_replied up to 30, pass the meaningless self-comparison,
and overflow ctx.reply_data on the kernel stack by up to 14 bytes.
Root cause
process_write_request (dce_aux.c:690-691) sets:
ctx->reply.length = ctx->returned_byte;
ctx->reply.data = ctx->reply_data;
where ctx->returned_byte (uint8_t, set by get_channel_status at
dce_aux.c:398-407 from AUX_SW_STATUS.AUX_SW_REPLY_BYTE_COUNT, masked
to 5 bits β 0..31, then decremented by 1 β 0..30) is in NO way bounded
against sizeof(ctx->reply_data) == 16.
read_channel_reply (dce_aux.c:250-300) then re-reads the SAME HW
field into a local bytes_replied (line 258), decrements it (line 282),
and gates the write loop with if (bytes_replied > size) return -1
(line 285). Because both size and bytes_replied are derived from
AUX_SW_REPLY_BYTE_COUNT, an attacker who makes the sink emit a
consistent oversized reply (count = N, 18 β€ N β€ 31) gets
size = N-1 and bytes_replied = N-1, so the guard reduces to
N-1 > N-1 = false, and the loop at lines 288-294 writes N-1 bytes
(17..30) into the 16-byte ctx.reply_data.
The correct bound should be DEFAULT_AUX_MAX_DATA_SIZE (16), NOT a
re-read of the attacker-influenced HW register.
Threat model
Attacker position: a malicious DisplayPort sink (rogue monitor, hostile USB-C dock/DP-HDMI dongle, or compromised MST hub) plugged into the target machine. The sink responds to an AUX write transaction (e.g. DPCD write during link training, which the kernel issues automatically on hotplug) with an AUX reply carrying 18 or more bytes instead of the spec-mandated single status byte.
The DP AUX standard caps native replies at 1 byte for writes / 17 bytes
for reads, but the AMD HW register field AUX_SW_REPLY_BYTE_COUNT is 5
bits wide (dce_11_0_sh_mask.h:8677, mask 0x1f000000) and will
faithfully count any extra bytes the sink emits.
Impact: kernel stack overflow of up to 14 bytes past ctx.reply_data in
write_command()'s stack frame, overwriting transaction_complete and
operation_succeeded and continuing into the stack canary / saved frame.
With -fstack-protector this becomes a reliable kernel panic (local DoS
from physically-adjacent attacker); without the canary, or if the canary
check is somehow bypassed, it is a kernel-RCE primitive from a plugged-in
device.
IMPORTANT REACHABILITY CAVEAT for this specific file: the dce_aux.c
submit_request function (registered at line 908 in aux_engine_funcs)
is NOT invoked by any in-tree caller on a DC res_pool engine β the only
consumer of these engines is dc_link_aux_transfer (dc_link_ddc.c:655),
which bypasses submit_request and calls
submit_channel_request/get_channel_status/read_channel_reply directly
with caller-bounded size <= 16, which is safe.
HOWEVER the IDENTICAL bug pattern is present and IS reachable in the
parallel i2caux implementation
(sys/dev/drm/amd/display/dc/i2caux/aux_engine.c:309-332 +
i2caux/dce110/aux_engine_dce110.c, whose submit_request IS called via
dal_i2caux_submit_aux_command from dc_link_ddc.c:598), so the bug
class is live in the same kernel image; fixing it here is
defense-in-depth against future refactoring that wires DC engines through
submit_request.
PoC
findings/poc/DF-1694/:
Triggering the dce_aux.c instance directly requires submitting a
transaction through this engine's submit_request (currently uncalled).
To prove the bug in isolation, build a small kernel module that:
- obtains an
aux_enginefromdc->res_pool->engines[i](dc_link_ddc.c:655pattern) - calls
engine->funcs->acquire(engine, ddc_pin) - constructs an
i2caux_transaction_requestwithoperation=I2CAUX_TRANSACTION_WRITE,payload.address_space=I2CAUX_TRANSACTION_ADDRESS_SPACE_DPCD,payload.length=16,payload.data=scratch_buf - attaches a malicious DP sink (e.g. a FPCA/FPGA DP source emulator, or a Linux machine with a USB-C DP-alt-mode controller reprogrammed via its AUX state machine) that responds to AUX native writes with 20+ reply bytes
- calls
engine->funcs->submit_request(engine, &request, true)
Success criterion: kernel panic with stack-protector failure
(__stack_chk_fail) or, with CONFIG_CC_STACKPROTECTOR_NONE,
unexplained corruption/panic in or near write_command().
For the actually-reachable twin bug in the i2caux file, no module is needed: just plug the malicious sink into a running DF/amdgpu system; the overflow fires from the kernel's own DPCD writes during link training.
The PoC source for the malicious-sink side is an Arduino/FPGA DP AUX
responder that, on receiving any AUX_NATIVE_WRITE request, returns a
reply packet whose body length field is set to e.g. 20 and prepends 20
junk bytes after the 4-bit ACK status; the host's
AUX_SW_REPLY_BYTE_COUNT then reads back 21, ctx.reply_data overflows
by 4 bytes.
Place PoC sources under findings/poc/DF-1694/ as:
malicious_sink.ino(oremulate_dp_sink.pyusing a USB-C DP-alt-mode SINK eval board)trigger_write.c(kernel module that drivessubmit_requestonres_pool->engines[0])build.sh(kldload/cc)run.shexpected_panic.txtcontaining the stack-protector signature
Recommended fix
The bound check must compare against the actual buffer capacity, not against a re-read of the same attacker-influenced HW field.
The actual fix belongs in process_write_request, which must cap
ctx->reply.length at DEFAULT_AUX_MAX_DATA_SIZE before assigning it to
ctx->reply.length, because ctx->reply.data is only
DEFAULT_AUX_MAX_DATA_SIZE bytes:
--- a/sys/dev/drm/amd/display/dc/dce/dce_aux.c
+++ b/sys/dev/drm/amd/display/dc/dce/dce_aux.c
@@ -687,8 +687,14 @@ static void process_write_request(
ctx->timed_out_retry_aux = 0;
ctx->invalid_reply_retry_aux = 0;
- ctx->reply.length = ctx->returned_byte;
+ /* ctx->reply_data is DEFAULT_AUX_MAX_DATA_SIZE (16) bytes; the HW
+ * AUX_SW_REPLY_BYTE_COUNT field is 5 bits wide and a malicious sink
+ * can drive it up to 31. Cap the reply length we feed to
+ * read_channel_reply at the actual buffer capacity. */
+ ctx->reply.length = min_t(uint32_t, ctx->returned_byte,
+ DEFAULT_AUX_MAX_DATA_SIZE);
ctx->reply.data = ctx->reply_data;
Apply the analogous fix to the reachable twin in
sys/dev/drm/amd/display/dc/i2caux/aux_engine.c
(process_write_request, around line ~453 where
ctx->reply.length = ctx->returned_byte is set), which is the
actually-reachable instance of the same bug.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1694 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Fix for dce_aux write reply buffer overflow | 457 B | view raw |
| VERDICT.md | verdict | Source-only verification verdict | 824 B | β raw |
| build.sh | build-script | No-op (source-only) | 109 B | view raw |
| run.sh | run-script | No-op (source-only) | 107 B | view raw |
VERDICT DF-1694: dce_aux write reply buffer overflow
Verdict
REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.
Mechanism
ctx->returned_byte (5-bit HW field max 31) set as reply.length; reply_data is 16 bytes -> OOB.
Source reference: sys/dev/drm/amd/display/dc/dce/dce_aux.c:690-691.
Reproduction
Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed.
The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present
on the QEMU/virtio guest. The finding is HW-gated.
Fix
Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with
make -j6 nativekernel KERNCONF=X86_64_GENERIC β rc=0, -Werror clean.
See fix.diff for the git-apply-able patch.
Fix verification
fixedCombined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.
'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- d
- i
- s
- p
- l
- a
- y
- /
- d
- c
- /
- d
- c
- e
- /
- d
- c
- e
- _
- a
- u
- x
- .
- c
- :
- 6
- 9
- 0
Detail
Exploit chain
none
Evidence (decisive lines)
Source confirmed: sys/dev/drm/amd/display/dc/dce/dce_aux.c:690. Combined 41-fix kernel build rc=0 -Werror clean.
PoC changes
fix.diff authored; validated by combined kernel build.
Verified recommended fix
Cap at DEFAULT_AUX_MAX_DATA_SIZE. Matches finding.
Verdict
REPRODUCED (source-confirmed). returned_byte (max 31) as reply.length; buffer is 16 -> OOB. Cited path verified at sys/dev/drm/amd/display/dc/dce/dce_aux.c:690. HW/module-gated on QEMU guest.
No comments yet.