Unbounded MST topology depth (LCT) -> OOB stack/heap writes and unbounded kernel recursion from malicious hub
Summary
Nothing bounds mstb->lct: drm_dp_calculate_rad returns parent_lct+1 per nested branch claimed by the hub, and each claimed MST_BRANCHING port triggers a nested link-address exchange. Every RAD-indexed buffer is fixed-size: stack rad[6] overflows from parent_lct>=13, heap mstb->rad[8] overflows from lct>=17, stack hdr->rad[8] the same way, dump prefix[10] overflows for lct>=10 (root-only debugfs), and the synchronous recursion exhausts the 16KB kernel stack around depth ~54. Malicious/compromised DP hub, dock or monitor: costs one fabricated LINK_ADDRESS reply per level, no host privilege, automatic on HPD. 12-deep corrupts the setup_pdt stack frame; 16-deep overruns the branch heap object (adjacent ports list_head/mgr/tx_slots/guid - pointer-graft potential); ~54-deep deterministically panics via stack exhaustion. Model PoC on the guest reproduces every OOB offset. Cap-the-chain fix validated patch-clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3881 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| mst_lct_model.c | β | 4.5 KB | view raw | |
| build.sh | β | 100 B | view raw | |
| run.sh | β | 96 B | view raw | |
| build.log | β | 133 B | view raw | |
| run.log | β | 3.5 KB | view raw | |
| env.txt | β | 482 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 2.9 KB | view raw |
DF-3881 β unbounded MST topology depth (LCT) β OOB stack/heap writes + kernel-stack exhaustion
File: sys/dev/drm/drm_dp_mst_topology.c (pass 2)
Class: kernel memory corruption (CWE-787) driven by a malicious DisplayPort MST hub
Severity: High / confidence: certain (source-level; hardware-gated on this guest)
What
Nothing in drm_dp_mst_topology.c bounds the Link Count Total (lct) of a
discovered MST branch chain. lct grows by exactly +1 per nested branch
device (drm_dp_calculate_rad() returns parent_lct + 1,
drm_dp_port_setup_pdt() installs that as the child's lct), and each newly
discovered MST_BRANCHING port triggers a nested drm_dp_send_link_address()
(drm_dp_add_port() β drm_dp_port_setup_pdt() β drm_dp_send_link_address(port->mstb)),
so a hub that claims to be a chain of N nested branches drives lct to N+1
with only N sideband round trips. Every buffer indexed off lct is fixed-size:
| sink | buffer | first OOB | what is written |
|---|---|---|---|
drm_dp_calculate_rad() :1052-1066, called from drm_dp_port_setup_pdt() :1074 |
stack u8 rad[6] |
parent_lct β₯ 13 | memcpy(rad, parent->rad, idx+1) with idx+1 = 7..8 (1-2 bytes past) and rad[idx] |= port_num << shift with idx up to 127 (4-bit port number at attacker-chosen stack offset) |
drm_dp_add_mst_branch_device() :848-850 |
heap mstb->rad[8] (drm_dp_mst_helper.h:125) |
lct β₯ 17 | memcpy(mstb->rad, rad, lct/2) β up to 127 bytes into a freshly kzmalloc'd drm_dp_mst_branch (heap overflow with stack-garbage + hub-derived bytes) |
set_hdr_from_dst_qlock() :1484-1485 |
stack hdr->rad[8] (drm_dp_mst_helper.h:150) |
lct β₯ 17 | memcpy(hdr->rad, mstb->rad, lct/2) on every down-req to the deep device |
drm_dp_mst_dump_mstb() :2958-2964 |
stack char prefix[10] |
lct β₯ 10 | prefix[i]='\t' for i < lct (root-only debugfs sink, radeon_dp_mst.c:775) |
drm_dp_send_link_address() :1639 β drm_dp_add_port() :1196-1198 |
kernel thread stack | depth β 54 | unbounded synchronous recursion, one send_link_address+add_port frame-set per level, 16KB DFBSD kernel stack |
Threat model
Attacker = malicious/compromised DP hub, dock or monitor firmware (same actor
as DF-1264/DF-1265). No host privilege required; the topology is enumerated
automatically on HPD. Each level costs the hub one LINK_ADDRESS reply it
fabricates locally, so a single $ device can simulate an arbitrarily deep
chain. A 12-hub-deep claim already corrupts the drm_dp_port_setup_pdt()
stack frame; ~16-hub-deep corrupts the heap object; ~54-deep exhausts the
kernel stack (each level's wait is synchronous in the topology worker).
PoC
Hardware-gated: the QEMU guest has no GPU (/dev/dri absent, no drm module
loaded β see env.txt), so the kernel path cannot be driven here.
mst_lct_model.c is a faithful arithmetic model of the four sinks; build with
./build.sh, run with ./run.sh (as unprivileged user). Expected: the table
of first-OOB depths (13 / 17 / 17 / 10) and the stack-exhaustion depth
estimate β see run.log.
Fix
Bound the topology depth at the point where it is computed and where the RAD
copies happen (fix.diff): reject lct > 16 in drm_dp_add_mst_branch_device()
(which also stops the recursion, because port_setup_pdt() only recurses when
port->mstb was created), refuse to compute a RAD for parent_lct > 12 in
drm_dp_calculate_rad() (rad[6] holds at most 12 nibbles), and size
prefix[] for the maximum legal lct in drm_dp_mst_dump_mstb().
VERDICT β DF-3881
status: not_reproduced (hardware-gated) / impact: none demonstrated / confidence: certain (source-level)
Why the bug is real (kernel path, line by line)
drm_dp_mst_topology_mgr_set_mst()createsmst_primarywith lct = 1 (drm_dp_add_mst_branch_device(1, NULL), :2140).- Each LINK_ADDRESS reply enumerates ports; for a port with
peer_device_type == DP_PEER_DEVICE_MST_BRANCHING,drm_dp_add_port()(:1140) callsdrm_dp_port_setup_pdt()(:1196). drm_dp_port_setup_pdt()(:1071) declaresu8 rad[6]and callsdrm_dp_calculate_rad(port, rad)(:1083), which computesidx = (parent_lct - 1) / 2, doesmemcpy(rad, port->parent->rad, idx + 1)andrad[idx] |= port->port_num << shiftand returnsparent_lct + 1(:1052-1066).parent->radisu8 rad[8](drm_dp_mst_helper.h:125). Forparent_lct β₯ 13,idx β₯ 6: -memcpy(rad, parent->rad, 7..8)writes 1-2 bytes pastrad[6]; -rad[6..127] |= port_num << shiftwrites a 4-bit hub-chosen value at a hub-chosen stack offset (offset = depth).- The returned lct = parent+1 is passed to
drm_dp_add_mst_branch_device()(:1085), which doesmemcpy(mstb->rad, rad, lct / 2)into the heap object'su8 rad[8](:848-850): for lct β₯ 17 this copies β₯ 9 bytes (up to 127), a linear heap overflow of the freshly allocateddrm_dp_mst_branch(~200-byte kmalloc zone). - Every subsequent down-request to that device runs
set_hdr_from_dst_qlock()(:1484-1485)memcpy(hdr->rad, mstb->rad, mstb->lct / 2)into the stackdrm_dp_sideband_msg_hdr.rad[8]β same β₯17 overflow of the caller's frame (process_single_tx_qlock(), :1496-1501). - The enumeration is recursively unbounded:
drm_dp_send_link_address()reply loop (:1639-1641) βdrm_dp_add_port()βdrm_dp_send_link_address(port->mstb)(:1198) β one nested frame-set per claimed hub level, executed synchronously inside the topology/probe worker. DFBSD kernel stacks are 16KB (KSTACK_PAGESdefault 4); at ~300 bytes/level the chain reaches the guard page around depth 54 β a hub only needs 54 fabricated replies. drm_dp_mst_dump_mstb()(:2954-2964) writesprefix[i] = '\t'fori < mstb->lctintochar prefix[10]β OOB for lct β₯ 10; reachable from root-only radeon debugfs (radeon_dp_mst.c:775).
No DP_MAX_LCT bound, no depth check, no rad-size validation exists anywhere
in this file (verified: only three lct comparisons exist β :849, :1058,
:1484 β all > 1 tests). Upstream Linux later capped this class of growth;
this tree has no such cap.
What was executed on the guest
mst_lct_model.c β exact arithmetic model of sinks 3/4/5/6 (built with the
guest's cc 8.3, run as unprivileged maxx): first stack-OOB at
parent_lct=13, heap/hdr OOB at parent_lct=17, prefix OOB at lct=10,
stack-exhaustion estimate β depth 54 (full output in run.log).
Why not reproduced
The sink lives in the DisplayPort MST driver path, which requires a GPU with
DP 1.2 MST that enumerates through drm. The QEMU guest has no GPU:
/dev/dri, /dev/fb, /dev/iic all absent, no drm/i915/radeon kld loaded
(env.txt). There is no software-only entry into
drm_dp_calculate_rad()/drm_dp_add_mst_branch_device() (all callers descend
from drm_dp_mst_topology_mgr_set_mst(), invoked by the GPU driver's HPD
handler). Classification: not_reproduced, hardware-gated, source-certain.
Exploit chain (for the maintainer's threat model)
physical malicious hub β 12-deep claimed topology β 1-2 byte stack overwrite
in drm_dp_port_setup_pdt() with rad[idx] |= port_num<<shift
(semi-controlled); 16-deep β 1-byte heap overwrite past mstb->rad[8]
inside drm_dp_mst_branch (adjacent fields: lct, num_ports, msg_slots,
list_head, port_parent, mgr, tx_slots[2], guid[16] β corrupting the list_head
or tx_slots of the next allocation yields pointer-graft primitives); 54-deep β
kernel stack exhaustion (guard-page panic, deterministic DoS). Grooming the
drm_dp_mst_branch kmalloc zone is straightforward for a device that also
speaks i2c/EDID (same allocation cache churns on every hotplug cycle).
Fix validation
fix.diff authored against the read-only sys/ tree (never applied): caps lct at
16 in drm_dp_add_mst_branch_device() (kfree on reject), refuses RAD
computation for parent_lct > 12 in drm_dp_calculate_rad(), sizes
prefix[18] in drm_dp_mst_dump_mstb(). Cannot be kernel-built/validated on
this guest (no GPU to boot a DRM console against; kernel build of the drm
module itself is possible but the fixed path is unreachable β recorded as
fix_status: not_testable per the honest-classification rules for
hardware-gated findings).
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 authored against read-only sys/ tree (lct>16 reject + parent_lct>12 RAD guard + prefix[18]); kernel validation impossible on this guest (no GPU: the fixed path cannot be exercised even with a rebuilt kernel), so fix_status=not_testable rather than fixed.
findings/poc/DF-3881/fix.diff
Confirmed kernel references
Detail
Exploit chain
malicious DP hub claims 12+ nested branch levels via LINK_ADDRESS replies -> 1-2-byte stack overwrite in drm_dp_port_setup_pdt (rad[6]|=port_num<
Evidence (decisive lines)
run.log (first-OOB table: 13/17/17/10), VERDICT.md (line-by-line trace), env.txt (no /dev/dri, no drm kld)
PoC changes
no seed existed; model authored from scratch to mirror the four sink buffers exactly
Verified recommended fix
Cap lct at 16 in drm_dp_add_mst_branch_device (kfree on reject), refuse RAD computation for parent_lct>12 in drm_dp_calculate_rad, size dump prefix[18]
Verdict
Hardware-gated, source-certain: nothing bounds mstb->lct, which grows +1 per nested branch claimed by the hub (drm_dp_calculate_rad:1065 returns parent_lct+1). From parent_lct>=13 drm_dp_calculate_rad memcpy/rad[idx]|= writes past the u8 rad[6] stack array of drm_dp_port_setup_pdt:1074; from lct>=17 drm_dp_add_mst_branch_device:850 overruns heap mstb->rad[8] and set_hdr_from_dst_qlock:1485 overruns stack hdr->rad[8]; drm_dp_mst_dump_mstb:2962 overruns prefix[10] for lct>=10; the LINK_ADDRESS->add_port->send_link_address recursion (:1639->:1198) is likewise unbounded (~300B kernel stack per level, 16KB stack exhausted ~depth 54). Guest has no GPU (/dev/dri absent, env.txt) so the kernel path is not executable here; the compiled arithmetic model reproduces every OOB offset (run.log).
No comments yet.