drm_dp_sideband_msg_build msg[256] accumulation overrun: up to 61 hub-controlled bytes past msg_rx->msg (distinct sink from DF-1264)
Summary
Each completed sideband chunk appends curchunk_len-1 bytes at msg->msg[curlen] where curlen (u8) is never validated against 256. A spec-shaped reply longer than 256 bytes (15-port LINK_ADDRESS ACK = 333 bytes, 6 chunks) walks the append offset past the array and writes up to 61 (measured 54) hub-controlled bytes over the parse-state fields and into the adjacent up_req_recv of the same topology manager. Malicious hub, no malformed fields needed (fully spec-conformant long reply). Corrupted have_eomt/curlen/initial_hdr steer downstream parsing and device lookup; smuggled bytes pre-seed the up-request channel's parse buffer. Model PoC on the guest: chunk 5 appends 62 bytes at &msg[248] -> 54 bytes past msg[256] incl. up_req_recv.chunk[0]. Pre-append bound fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3886 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| sbmsg_accum_model.c | β | 3.3 KB | view raw | |
| build.sh | β | 108 B | view raw | |
| run.sh | β | 100 B | view raw | |
| build.log | β | 141 B | view raw | |
| run.log | β | 787 B | view raw | |
| env.txt | β | 482 B | view raw | |
| fix.diff | β | 958 B | view raw | |
| manifest.json | β | 1004 B | view raw | |
| verdict.json | β | 2.6 KB | view raw |
DF-3886 β drm_dp_sideband_msg_build() accumulates curlen into msg[256] without bound β 54-byte hub-controlled OOB write
File: sys/dev/drm/drm_dp_mst_topology.c:371-377 (pass 2)
Class: kernel memory corruption (CWE-787), malicious MST hub
Severity: High / confidence: certain (source-level; hardware-gated)
What
Distinct sink from the two pass-1 knowns:
- DF-1264 is the
chunk[48]array overrun in the non-header continuation branch (memcpy(&msg->chunk[msg->curchunk_idx], replybuf, replybuflen), line 367). - DF-1265 is the reply parsers' OOB reads past
curlen.
This finding is the completed-chunk accumulation into msg->msg[256]:
371: if (msg->curchunk_idx >= msg->curchunk_len) {
372: /* do CRC */
373: crc4 = drm_dp_msg_data_crc4(msg->chunk, msg->curchunk_len - 1);
374: /* copy chunk into bigger msg */
375: memcpy(&msg->msg[msg->curlen], msg->chunk, msg->curchunk_len - 1);
376: msg->curlen += msg->curchunk_len - 1;
377: }
curchunk_len is the hub-controlled 6-bit msg_len from the sideband header
(hdr->msg_len = buf[idx] & 0x3f, line 190 β up to 63, so up to 62 bytes
appended per completed chunk). curlen is a u8 never validated against
sizeof(msg->msg) == 256. A reply whose body exceeds 256 bytes β trivially
a crafted LINK_ADDRESS ACK with 15 ports: 18 + 15Γ21 = 333 bytes β walks the
append offset past 256 and copies hub-controlled reply bytes past the array.
Bounds of the primitive (exact)
dst = curlen β€ 255, len = curchunk_len-1 β€ 62 β max end offset 317 β
up to 61 bytes past msg[256] (u8 wrap keeps it bounded; measured 54 for
the natural 62-byte chunk cadence β run.log). The bytes past the array land
on, in order (drm_dp_mst_helper.h:215-226): curchunk_len,
curchunk_idx, curchunk_hdrlen, curlen, have_somt, have_eomt,
initial_hdr (24B: lct, lcr, rad[8], flags), and β because
down_rep_recv is immediately followed by up_req_recv in
struct drm_dp_mst_topology_mgr (drm_dp_mst_helper.h:464-470) β the first
bytes of up_req_recv.chunk[48], the parse buffer of the other
sideband channel.
Consequences
- Hub-controlled overwrite of
have_eomt/curlen/initial_hdrlets the hub desynchronize the message assembler mid-stream (forgeinitial_hdr.lct/radused bydrm_dp_mst_handle_down_rep():2324device lookup, force premature eomt on truncated bodies feeding the parsers of DF-1265, etc.). - Data smuggling into
up_req_recvpre-seeds the up-request parser with bytes the hub never sent as an up-request, compounding parser confusion. - All writes stay inside the
drm_dp_mst_topology_mgrallocation (embedded in the driver'sintel_dp/radeon_connectorheap object) β type-stable corruption of scaler fields, not a pointer smash; still a deterministic, fully content-controlled OOB write from an unprivileged physical attacker.
PoC
Hardware-gated (same reason as DF-3881). sbmsg_accum_model.c models the
assembler arithmetic faithfully (same field order); build ./build.sh, run
./run.sh. Expected output: first OOB append at chunk 5
(memcpy(&msg[248], chunk, 62) β 54 bytes past msg[256]), and the
clobbered-field dump showing 0xA5 reply bytes in curchunk_len, curlen,
have_somt, have_eomt, initial_hdr[0], up_req_recv.chunk[0].
Fix
fix.diff: validate the accumulated length before the append
(if ((int)msg->curlen + (int)msg->curchunk_len - 1 > (int)sizeof(msg->msg)) return false;)
β mirrors the bound upstream added when it reworked this builder.
VERDICT β DF-3886
status: not_reproduced (hardware-gated) / impact: none demonstrated / confidence: certain (source-level)
Trace (attacker input β sink)
- Hub raises
DP_DOWN_REP_MSG_RDY(ESI bit); driver callsdrm_dp_mst_hpd_irq()(:2450) βdrm_dp_mst_handle_down_rep()(:2310) βdrm_dp_get_one_sb_msg()(:2261), which reads up to 16 bytes per DPCD transaction from the hub and feeds them todrm_dp_sideband_msg_build()(:2278, :2298). - Each header chunk sets
msg->curchunk_len = recv_hdr.msg_len(buf & 0x3f, β€ 63 β :190, :349). Continuation chunks (hdr=false) append intomsg->chunk[]untilcurchunk_idx >= curchunk_len, at which point the completed body is appended to the whole-message buffer (:371-377):memcpy(&msg->msg[msg->curlen], msg->chunk, msg->curchunk_len - 1). msg->curlen(u8) is never compared to 256 anywhere in the file. A 333-byte LINK_ADDRESS ACK (15 ports, all fields present β exactly whatdrm_dp_sideband_parse_link_address()is prepared to consume) needs 6 chunks of β€62 body bytes; the 5th append starts at offset 248 and runs to 309 β 54 bytes pastmsg[256], content = hub reply bytes, destination =curchunk_len/curchunk_idx/curchunk_hdrlen/curlen/have_somt/have_eomt/ initial_hdr+ first bytes ofup_req_recv(struct order indrm_dp_mst_helper.h:215-226and:464-470).- Both receivers are zeroed between messages (
memsetat :2315, :2330, :2358, :2408, :2434) but the overwrite happens during assembly, before any zeroing β and the corruptedcurlen/have_eomtthen steer the very parse of that message.
Reachability is the same as DF-1264's (malicious hub; no host privilege); the trigger is easier than DF-1264 because every value used is in-spec shaped β no malformed lengths needed, just a legitimately-sized long reply.
Differentiation from knowns
- DF-1264:
chunk[48]overrun in the continuation branch (:367) β different array, different branch, fixed by boundingcurchunk_idx. - DF-1265: parser reads past
curlenβ read-side, indrm_dp_sideband_parse_*. - DF-3886 (this): write-side overrun of the
msg[256]accumulation buffer (:375), fixed by boundingcurlen + curchunk_len - 1before the append. A fix for DF-1264 does not touch this line.
What was executed on the guest
sbmsg_accum_model.c (built with guest cc 8.3, run unprivileged):
reproduces the assembler arithmetic and prints the exact overrun β first OOB
append at chunk 5 (&msg[248], 62 bytes, 54 past the array), max spill 54,
clobbered-field dump all 0xa5 including up_req_recv.chunk[0] (run.log).
Why not reproduced on the kernel path
No GPU / no /dev/dri / no drm kld on the QEMU guest (env.txt);
drm_dp_sideband_msg_build() is only reachable from the DP MST IRQ path.
Classification: not_reproduced, hardware-gated, source-certain.
Fix
fix.diff β pre-append bound check in drm_dp_sideband_msg_build():
if (msg->curchunk_idx >= msg->curchunk_len) {
/* do CRC */
crc4 = drm_dp_msg_data_crc4(msg->chunk, msg->curchunk_len - 1);
+ if ((int)msg->curlen + (int)msg->curchunk_len - 1 >
+ (int)sizeof(msg->msg)) {
+ DRM_DEBUG_KMS("sideband msg overflows msg buffer\n");
+ return false;
+ }
/* copy chunk into bigger msg */
memcpy(&msg->msg[msg->curlen], msg->chunk, msg->curchunk_len - 1);
Kernel-side fix validation is impossible on this guest (no DP hardware), so
fix_status: not_testable.
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_testablefix.diff adds the pre-append bound (return false on overflow); kernel-side validation impossible on this guest (no DP hardware), so fix_status=not_testable.
findings/poc/DF-3886/fix.diff
Confirmed kernel references
Detail
Exploit chain
malicious hub sends a long but spec-shaped down-reply (no malformed fields needed) -> 5th 62-byte append starts at &msg[248] -> 54 bytes of hub data past msg[256] corrupt have_eomt/curlen/initial_hdr (steering downstream parse + device lookup at :2324) and pre-seed up_req_recv.chunk with smuggled bytes.
Evidence (decisive lines)
run.log (chunk-by-chunk offsets + clobbered-field dump), VERDICT.md (attacker-input-to-sink trace and known-finding differentiation), env.txt
PoC changes
no seed; model authored from scratch mirroring struct field order
Verified recommended fix
Pre-append bound check: reject when curlen + curchunk_len - 1 exceeds sizeof(msg->msg) in drm_dp_sideband_msg_build
Verdict
Hardware-gated, source-certain: drm_dp_sideband_msg_build appends each completed chunk with memcpy(&msg->msg[curlen], chunk, curchunk_len-1) where curchunk_len is the hub's 6-bit header msg_len (<=63) and curlen (u8) is never validated against 256 (drm_dp_mst_topology.c:371-377). A >256-byte reply (15-port LINK_ADDRESS ACK = 333 bytes, 6 chunks) walks the append offset past the array: up to 61 hub-controlled bytes overwrite curlen/have_somt/have_eomt/initial_hdr and the adjacent up_req_recv of the same mgr (helper.h:215-226,464-470). Distinct sink from DF-1264 (chunk[48] continuation branch, line 367) and DF-1265 (parser reads): different array, different branch, different fix. Guest has no GPU (env.txt); the compiled assembler model reproduces the exact overrun (chunk 5, 54 bytes past, run.log).
No comments yet.