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

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)

No comments yet.

PoC verification

Evidence pack

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

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)

  1. drm_dp_send_link_address() queues txmsg, waits in drm_dp_mst_wait_tx_reply() for up to 4 * HZ (:808-810). On timeout with state == DRM_DP_SIDEBAND_TX_SENT it clears mstb->tx_slots[txmsg->seqno] (:829-832) under qlock, returns -EIO, and the caller immediately kfree(txmsg) (:1649). SENT means: fully transmitted, off tx_msg_downq (list_del at :1563), referenced only by the slot β€” which the timeout path just cleared. Nothing else keeps it alive.
  2. Concurrently, the HPD path (drm_dp_mst_hpd_irq β†’ drm_dp_mst_handle_down_rep) reads txmsg = mstb->tx_slots[slot] under qlock (:2336-2339), then drops the lock, does drm_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 writes txmsg->state = DRM_DP_SIDEBAND_TX_RX (:2362) and reads mstb->tx_slots[slot] (:2363) from an mstb reference already dropped at :2359.
  3. Interleaving: sender's timeout fires inside the handler's unlocked window β†’ sender's kfree(txmsg) precedes the handler's :2353 / :2362 writes β†’ 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_testable
baseline no→ patch + rebuild →patched clean

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

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.