drm_dp_mst_handle_down_rep txmsg use-after-free race vs drm_dp_mst_wait_tx_reply timeout kfree
Summary
handle_down_rep captures txmsg from mstb->tx_slots[slot] under qlock, drops the lock, then writes up to ~700 bytes of hub-controlled reply data into txmsg->reply and txmsg->state, and reads the slot after its mstb ref was put. If the sender's 4*HZ wait expires inside that window, wait_tx_reply clears the slot and the caller kfrees txmsg - the handler's writes land in freed memory. txmsg has no refcount anywhere in the file. Malicious hub times the DOWN_REP_MSG_RDY release at the sender's 4s deadline and retries indefinitely on an SMP host; ~1KB kmalloc freed then written with hub content at fixed offset - groom-and-overwrite primitive (the hub's own hotplug churn grooms it). Interleave model: 3/3 deterministic SIGSEGV. Stack-local parse + revalidate-slot fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3882 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| txmsg_uaf_race_model.c | β | 4.9 KB | view raw | |
| build.sh | β | 123 B | view raw | |
| run.sh | β | 110 B | view raw | |
| build.log | β | 156 B | view raw | |
| run.log | β | 339 B | view raw | |
| env.txt | β | 482 B | view raw | |
| fix.diff | β | 1.9 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-3882 β drm_dp_mst_handle_down_rep() txmsg use-after-free race vs drm_dp_mst_wait_tx_reply() timeout
File: sys/dev/drm/drm_dp_mst_topology.c:2336-2364 vs :802-838 (pass 2)
Class: UAF write (CWE-416) on kmalloc'd drm_dp_sideband_msg_tx (~1KB object)
Severity: High / confidence: likely (race is source-certain; window timing attacker-steerable)
The interleave
IRQ/HPD context β drm_dp_mst_handle_down_rep():
2336: mutex_lock(&mgr->qlock);
2337: txmsg = mstb->tx_slots[slot]; /* pointer captured */
2339: mutex_unlock(&mgr->qlock);
... ~Β΅s of unlocked window ...
2353: drm_dp_sideband_parse_reply(&mgr->down_rep_recv, &txmsg->reply); /* up to ~700 B of hub-controlled data written into txmsg */
2358: memset(&mgr->down_rep_recv, ...);
2359: drm_dp_put_mst_branch_device(mstb);
2361: mutex_lock(&mgr->qlock);
2362: txmsg->state = DRM_DP_SIDEBAND_TX_RX; /* 4-byte write into txmsg */
2363: mstb->tx_slots[slot] = NULL;
2364: mutex_unlock(&mgr->qlock);
Sender thread (topology worker, e.g. drm_dp_send_link_address() parked
in drm_dp_mst_wait_tx_reply()) whose wait_event_timeout(..., 4*HZ):
824: if (txmsg->state == QUEUED || START_SEND) list_del(...)
829: if (txmsg->state == START_SEND || SENT)
831: mstb->tx_slots[txmsg->seqno] = NULL;
/* ret = -EIO; caller at :1649 does kfree(txmsg) */
If the 4-second timeout expires inside the handler's unlocked window, the
sender NULLs the slot and kfrees txmsg; the handler then (a) writes up to
~700 bytes of hub-controlled reply data into the freed object via
drm_dp_sideband_parse_reply() and (b) writes state=3 at :2362, and (c)
reads mstb->tx_slots[slot] from an mstb whose reference it dropped at
:2359.
Nothing takes a reference on txmsg across the window; the sender's timeout
path holds no marker the handler checks (the handler only checked
tx_slots[slot] != NULL, before the sender NULLs it).
Why the race is winnable
The hub chooses when the reply becomes visible: it can park DOWN_REP_MSG_RDY until t β 3.99 s and release it at the deadline, so the reply processing lands exactly at the sender's timeout with per-attempt probability set by the Β΅s-scale window vs. scheduling jitter β and the hub can re-arm the race on every sideband transaction (it sends thousands of them during topology churn). Multi-CPU (HPD work vs. topology worker) makes the interleave real, not theoretical.
PoC
Hardware-gated kernel path (no GPU on the guest, env.txt).
txmsg_uaf_race_model.c reproduces the exact interleaving with two pthreads
using the same lock structure β the "kfree" is munmap, so the post-window
write faults deterministically: 3/3 runs exit 42 with the SIGSEGV marker
(run.log). That proves the interleaving is schedulable; it is a model, not
the kernel path.
Exploitability
Freed object is a ~1KB kmalloc (sizeof drm_dp_sideband_msg_tx: msg[256] +
chunk[48] + reply body ~700B). The race write puts ~700 bytes of
hub-controlled data at a fixed offset into that freed chunk β a
groom-and-overwrite primitive against whatever reoccupies the zone
(the hub itself churns the same zone with every hotplug cycle: txmsg
allocations are its native allocation). Full uid=0-from-display-cable
exploitation is beyond this audit's hardware, but the primitive is a
textbook UAF-write with attacker content and unlimited retries.
Fix
fix.diff β parse into a stack-local reply under the captured-pointer rules,
and only publish into txmsg while re-validating the slot under qlock
(state/slot update is skipped if the sender already took the timeout path);
also move drm_dp_put_mst_branch_device() after the last mstb use, fixing
the adjacent mstb-after-put read at :2363.
VERDICT β DF-3882
status: not_reproduced (kernel path hardware-gated) / impact: none demonstrated / confidence: likely (source-certain race, attacker-steerable window, model-demonstrated interleave)
Source trace (why the UAF is real)
drm_dp_send_link_address()queues txmsg, waits indrm_dp_mst_wait_tx_reply()for up to4 * HZ(:808-810). On timeout withstate == DRM_DP_SIDEBAND_TX_SENTit clearsmstb->tx_slots[txmsg->seqno](:829-832) underqlock, returns-EIO, and the caller immediatelykfree(txmsg)(:1649). SENT means: fully transmitted, offtx_msg_downq(list_del at :1563), referenced only by the slot β which the timeout path just cleared. Nothing else keeps it alive.- Concurrently, the HPD path (
drm_dp_mst_hpd_irqβdrm_dp_mst_handle_down_rep) readstxmsg = mstb->tx_slots[slot]underqlock(:2336-2339), then drops the lock, doesdrm_dp_sideband_parse_reply(&mgr->down_rep_recv, &txmsg->reply)(:2353) β a union write of up to ~700 bytes (link_addr: 16 ports Γ ~40B) whose content is the hub's reply β then re-locks and writestxmsg->state = DRM_DP_SIDEBAND_TX_RX(:2362) and readsmstb->tx_slots[slot](:2363) from an mstb reference already dropped at :2359. - Interleaving: sender's timeout fires inside the handler's unlocked window
β sender's
kfree(txmsg)precedes the handler's:2353/:2362writes β both writes land in freed memory. The handler never re-checks the slot before using txmsg; there is no txmsg refcount anywhere in this file (kzalloc'ed, owned by the sender, no kref).
Reachability prerequisites: DP MST active (malicious hub), SMP, and the reply released at the 4 s deadline β all controlled by the hub; retry unlimited.
What was executed
txmsg_uaf_race_model.c on the DragonFly guest (cc 8.3, pthreads): sender
thread times out at a 200 ms deadline and munmap()s the txmsg page (models
kfree); handler thread captures the slot 1 ms before the deadline, sleeps
through the "parse", then writes state β SIGSEGV in 3/3 runs because
the page is gone (run.log; exit 42 is the model's UAF marker). This
demonstrates the interleaving is schedulable, not the kernel bug.
Why not reproduced
drm_dp_mst_handle_down_rep() requires a DP MST-capable GPU raising
DOWN_REP_MSG_RDY; the QEMU guest has none (/dev/dri absent, no drm kld β
env.txt). No software-only entry exists. Classification: not_reproduced,
hardware-gated.
Fix
fix.diff:
- parse into a local struct drm_dp_sideband_msg_reply_body before touching
txmsg;
- under qlock, publish into txmsg only if mstb->tx_slots[slot] ==
txmsg still holds (i.e. the sender's timeout path hasn't taken it);
- move drm_dp_put_mst_branch_device(mstb) after the last mstb access,
fixing the mstb-after-put read at :2363 as well.
Kernel-side fix validation impossible on this guest (no DP hardware):
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 restructures the handoff (local reply + slot re-validation under qlock + mstb put moved after last use); applies cleanly via patch --dry-run to the pristine tree; kernel-side validation impossible on this guest (no DP hardware), so fix_status=not_testable.
findings/poc/DF-3882/fix.diff
Confirmed kernel references
Detail
Exploit chain
malicious hub parks DOWN_REP_MSG_RDY until the 4s tx timeout boundary -> sender kfree(txmsg) races the unlocked parse window -> up to 700 bytes of hub reply data written at fixed offset into freed ~1KB kmalloc zone + 4-byte state write; the hub's own hotplug churn grooms the same zone -> classic groom-and-overwrite of whatever reoccupies the chunk.
Evidence (decisive lines)
run.log (3/3 SIGSEGV UAF demonstrations with interleaving printout), VERDICT.md (line-numbered both-sides trace), fix.diff (validated: applies cleanly to pristine sys/dev/drm/drm_dp_mst_topology.c)
PoC changes
no seed; interleave model authored from scratch (munmap models kfree so the UAF write faults visibly)
Verified recommended fix
Parse the reply into a stack-local drm_dp_sideband_msg_reply_body and only publish it into txmsg under qlock after re-validating mstb->tx_slots[slot]==txmsg; hold the mstb ref until after the last slot access
Verdict
Source-certain race, kernel path hardware-gated: drm_dp_mst_handle_down_rep captures txmsg=mstb->tx_slots[slot] under qlock (drm_dp_mst_topology.c:2337), drops the lock (:2339), then writes ~700B of hub-controlled reply data via drm_dp_sideband_parse_reply (:2353) and txmsg->state (:2362) plus reads mstb after its ref was dropped (:2359/:2363). If the sender's 4*HZ wait in drm_dp_mst_wait_tx_reply expires inside that window it clears the slot (:831) and the caller kfrees txmsg (:1649) -- both handler writes then land in freed memory. The hub times the reply release at the deadline and can retry indefinitely. The pthread/mmap interleave model demonstrates the use-after-free deterministically (SIGSEGV into munmap'd txmsg, 3/3 runs, run.log). Guest has no GPU (/dev/dri absent, env.txt), so the kernel path itself is not_reproduced here.
No comments yet.