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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3881 Β· 11 files
FileTypeDescriptionSize
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.md
↓ download raw

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)

  1. drm_dp_mst_topology_mgr_set_mst() creates mst_primary with lct = 1 (drm_dp_add_mst_branch_device(1, NULL), :2140).
  2. Each LINK_ADDRESS reply enumerates ports; for a port with peer_device_type == DP_PEER_DEVICE_MST_BRANCHING, drm_dp_add_port() (:1140) calls drm_dp_port_setup_pdt() (:1196).
  3. drm_dp_port_setup_pdt() (:1071) declares u8 rad[6] and calls drm_dp_calculate_rad(port, rad) (:1083), which computes idx = (parent_lct - 1) / 2, does memcpy(rad, port->parent->rad, idx + 1) and rad[idx] |= port->port_num << shift and returns parent_lct + 1 (:1052-1066). parent->rad is u8 rad[8] (drm_dp_mst_helper.h:125). For parent_lct β‰₯ 13, idx β‰₯ 6: - memcpy(rad, parent->rad, 7..8) writes 1-2 bytes past rad[6]; - rad[6..127] |= port_num << shift writes a 4-bit hub-chosen value at a hub-chosen stack offset (offset = depth).
  4. The returned lct = parent+1 is passed to drm_dp_add_mst_branch_device() (:1085), which does memcpy(mstb->rad, rad, lct / 2) into the heap object's u8 rad[8] (:848-850): for lct β‰₯ 17 this copies β‰₯ 9 bytes (up to 127), a linear heap overflow of the freshly allocated drm_dp_mst_branch (~200-byte kmalloc zone).
  5. 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 stack drm_dp_sideband_msg_hdr.rad[8] β€” same β‰₯17 overflow of the caller's frame (process_single_tx_qlock(), :1496-1501).
  6. 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_PAGES default 4); at ~300 bytes/level the chain reaches the guard page around depth 54 β€” a hub only needs 54 fabricated replies.
  7. drm_dp_mst_dump_mstb() (:2954-2964) writes prefix[i] = '\t' for i < mstb->lct into char 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_testable
baseline no→ patch + rebuild →patched clean

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

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< 1-byte heap overflow past drm_dp_mst_branch.rad[8] (adjacent lct/num_ports/ports list_head/mgr/tx_slots/guid); ~54 levels -> deterministic kernel stack exhaustion (DoS).

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