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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3886 Β· 11 files
FileTypeDescriptionSize
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_hdr lets the hub desynchronize the message assembler mid-stream (forge initial_hdr.lct /rad used by drm_dp_mst_handle_down_rep():2324 device lookup, force premature eomt on truncated bodies feeding the parsers of DF-1265, etc.).
  • Data smuggling into up_req_recv pre-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_mgr allocation (embedded in the driver's intel_dp/radeon_connector heap 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.md
↓ download raw

VERDICT β€” DF-3886

status: not_reproduced (hardware-gated) / impact: none demonstrated / confidence: certain (source-level)

Trace (attacker input β†’ sink)

  1. Hub raises DP_DOWN_REP_MSG_RDY (ESI bit); driver calls drm_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 to drm_dp_sideband_msg_build() (:2278, :2298).
  2. Each header chunk sets msg->curchunk_len = recv_hdr.msg_len (buf & 0x3f, ≀ 63 β€” :190, :349). Continuation chunks (hdr=false) append into msg->chunk[] until curchunk_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).
  3. msg->curlen (u8) is never compared to 256 anywhere in the file. A 333-byte LINK_ADDRESS ACK (15 ports, all fields present β€” exactly what drm_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 past msg[256], content = hub reply bytes, destination = curchunk_len/curchunk_idx/curchunk_hdrlen/curlen/have_somt/have_eomt/ initial_hdr + first bytes of up_req_recv (struct order in drm_dp_mst_helper.h:215-226 and :464-470).
  4. Both receivers are zeroed between messages (memset at :2315, :2330, :2358, :2408, :2434) but the overwrite happens during assembly, before any zeroing β€” and the corrupted curlen/have_eomt then 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 bounding curchunk_idx.
  • DF-1265: parser reads past curlen β€” read-side, in drm_dp_sideband_parse_*.
  • DF-3886 (this): write-side overrun of the msg[256] accumulation buffer (:375), fixed by bounding curlen + curchunk_len - 1 before 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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3886

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).