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

twa(4) deferred-busdma passthru error path returns the request to the free queue twice β€” queue corruption and cross-thread request reuse (latent UAF/double-free; sibling of twe DF-4178)

Summary

When bus_dmamap_load defers with EINPROGRESS and the deferred callback's CL submission then fails, the callback path wakes the sleeping passthru owner (IN_PROGRESS set) AND ALSO inserts the request into the free queue; the woken owner unwinds and inserts the SAME request again. Double insertion self-rings the queue; remove_head hands the same context to two threads, and the first owner's kfree can free a buffer the second owner re-assigned - UAF/double-free. Latent on current x86-64 defaults (these tags never bounce); goes live under any busdma config that bounces. Exclude-passthru-from-callback-insert fix (the CAM async path genuinely needs it).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4274 Β· 5 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 5.6 KB ↓ raw
fix.diff β€” 962 B view raw
manifest.json β€” 954 B view raw
verdict.json β€” 3.5 KB view raw

DF-4274 β€” twa(4) deferred-busdma error path returns a passthrough request to the free queue twice

File: sys/dev/raid/twa/tw_osl_freebsd.c (pass 2)

What this pack contains

  • VERDICT.md β€” full code-path proof of the double free-queue insertion and the platform-reachability analysis (latent on current x86-64 defaults).
  • fix.diff β€” minimal conditional fix.
  • manifest.json, verdict.json.
  • No runnable PoC exists: triggering requires a 3ware 9000 HBA (absent from the audit guest β€” see ../DF-4273/env.txt) and a bus_dmamap_load() deferral (EINPROGRESS), which the driver's DMA tags cannot produce on default-configured x86-64 (analysis in VERDICT.md).

The defect (code-only proof)

tw_osli_map_request() (tw_osl_freebsd.c:1390-1401) handles bus_dmamap_load() == EINPROGRESS by marking the request TW_OSLI_REQ_FLAGS_IN_PROGRESS and letting the originator sleep. When the deferred load later completes and CL submission fails, twa_map_load_data_callback() (:1273-1284) calls the completion callback (tw_osl_complete_passthru, which wakes the sleeping ioctl thread β€” the originator always unwinds through fw_passthru_err at :1040-1049 and inserts the request into TW_OSLI_FREE_Q itself) and then also inserts the same request into TW_OSLI_FREE_Q again (:1282-1283). Two unconditional insertions of one tw_cl_link corrupt the free queue (self-referential ring) and hand the same request context to two threads; the second owner's req->data can be freed by the first owner's error path β€” double-free/UAF class.

VERDICT.md
↓ download raw

DF-4274 VERDICT β€” double free-queue insertion in the deferred-busdma passthru error path (latent UAF; currently unreachable on default x86-64)

Bottom line

A passthrough request whose bus_dmamap_load() defers (EINPROGRESS) and whose deferred CL submission subsequently fails is inserted into TW_OSLI_FREE_Q twice β€” once by twa_map_load_data_callback()'s error path and once by the ioctl thread's own unwind β€” corrupting the request queue and enabling cross-thread reuse of one tw_osli_req_context (double kfree of req->data / use-after-free). The defect is deterministic once its two preconditions hold; on this DragonFly/x86-64 build the deferral precondition cannot currently be produced by the twa tags, making this a latent bug that turns live the day busdma bouncing applies to these loads.

Proof (code path, all in sys/dev/raid/twa/tw_osl_freebsd.c)

  1. tw_osli_fw_passthru() calls tw_osli_map_request(req) (:953).
  2. tw_osli_map_request() loads the shared sc->ioctl_map with BUS_DMA_WAITOK (:1371-1374). DragonFly bus_dmamap_load() queues the map and returns EINPROGRESS when the bounce zone cannot satisfy map->pagesneeded (sys/platform/pc64/x86_64/busdma_machdep.c:640-657, 793-805). The driver marks the request TW_OSLI_REQ_FLAGS_IN_PROGRESS (:1397-1398), reports success, and the ioctl thread enters its wait loop (:957).
  3. The deferred load is resumed from busdma_swi() (softint context) which re-invokes the stored callback twa_map_load_data_callback (busdma_machdep.c:1436-1448) β€” note: outside the driver's io_lock, on the shared map.
  4. If tw_cl_fw_passthru() returns an error synchronously inside that callback (tw_cl_io.c:283 EBUSY when the CL request pool is exhausted; tw_cl_io.c:341-348 submit failure β€” both reachable under load/controller fault), the out: error path (:1273-1284) runs: - req_pkt->tw_osl_callback() β†’ tw_osl_complete_passthru() (:1064): sets state = COMPLETE, removes from TW_OSLI_BUSY_Q, unmaps, and β€” because TW_OSLI_REQ_FLAGS_IN_PROGRESS is set, the early-return at :1098 does not fire β€” wakes the sleeping originator (:1101-1107). - then if (req->flags & TW_OSLI_REQ_FLAGS_IN_PROGRESS) tw_osli_req_q_insert_tail(req, TW_OSLI_FREE_Q); (:1282-1283) β€” first insertion.
  5. The woken ioctl thread sees state == TW_OSLI_REQ_STATE_COMPLETE, exits the loop (:957), runs its copyout and then unwinds through fw_passthru_err: :1040-1049 β†’ tw_osli_req_q_insert_tail(req, TW_OSLI_FREE_Q) β€” second insertion of the same link.

Effect of inserting an already-queued tw_cl_link: with the request as the only element, the second INSERT_TAIL sets req->link.prev = &req->link (self-ring); subsequent remove_head operations then hand out the same request to two concurrent ioctl/CAM threads while the first owner still executes its error path (kfree(req->data) at :1047-1048 may free a buffer the second owner has already re-assigned). Class: queue corruption β†’ request-context UAF / double-free.

The synchronous (non-deferred) failure path is not affected: IN_PROGRESS is clear there, tw_osl_complete_passthru early-returns at :1098 without waking, and only the ioctl thread inserts.

Reachability (why it is latent here)

For EINPROGRESS, _bus_dmamap_load_buffer must count bounce pages (busdma_machdep.c:617-632), which requires addr_needs_bounce() to fire (:186-193): either paddr > lowaddr (the twa tags use lowaddr = highaddr = BUS_SPACE_MAXADDR, so never) or the global bounce_alignment with paddr & (alignment-1) β€” the tags' alignment is sc->alignment = TWA_ALIGNMENT() = 4, and the bounce test is applied at page granularity, so a page-aligned physical address can never be misaligned mod 4. Hence pagesneeded == 0 always for twa loads on default x86-64 (verified hw.busdma.bounce_alignment: 1, max_bpages: 1024 on the guest β€” see ../DF-4273/env.txt), and the EINPROGRESS path is dead in this configuration. It becomes live under any future busdma change that makes these tags bounce (lowaddr constraints, sub-page alignment checks) or on platforms with real DMA restrictions. The double-insert itself is unconditional code that will execute the moment deferral occurs, and the io_lock serialization that normally protects the shared ioctl_map (:1368-1375, :1463-1486) is also bypassed in the SWI-resumed path β€” the map can be loading for two requests at once.

Verification

No runtime PoC is possible on the audit guest: it needs the 3ware HBA (absent; /dev/twa0 does not exist) and a busdma deferral this platform cannot generate for these tags. The finding is a static proof; status untested, severity Low (latent, hardware-gated, root-gated device).

Fix

fix.diff: in twa_map_load_data_callback()'s error path, only return the request to the free queue for asynchronous owners (CAM I/O, whose originator twa_action has already returned β€” tw_osl_cam.c:335-342 confirmed); a PASSTHRU request's originator is still blocked in tw_osli_fw_passthru() and always returns it itself at fw_passthru_err: (:1049). One-line conditional; not build-validated (driver cannot attach on this guest) β†’ 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 authored from line-accurate root cause; not build/boot-validated - the driver cannot attach on the audit guest and the triggering deferral cannot be generated on this platform

findings/poc/DF-4274/fix.diff (conditional excludes TW_OSLI_REQ_FLAGS_PASSTHRU from the callback-side reinsert at tw_osl_freebsd.c:1282)
↓ fix.diffper-fix-DF-4274

Confirmed kernel references

Detail

Exploit chain

concurrent root ioctls on /dev/twa0 with data buffers -> EINPROGRESS deferral (bounce pressure) -> CL submit failure (request-pool exhaustion / controller fault) -> request double-inserted into free queue -> two threads pop the same request -> double kfree(req->data) / UAF (kernel heap corruption potential, race-dependent)

Evidence (decisive lines)

VERDICT.md (step-by-step trace with path:line for both insertions, the wake ordering, the sync-path counterexample, and the platform-reachability analysis); fix.diff

PoC changes

n/a (finding originated in this run; no PoC exists - hardware-gated)

Verified recommended fix

In twa_map_load_data_callback's error path, skip the free-queue insertion for PASSTHRU requests - the sleeping originator in tw_osli_fw_passthru returns the request itself (see fix.diff)

Verdict

Deterministic code-path defect (proven by trace, not runtime): a passthrough request whose bus_dmamap_load defers with EINPROGRESS and whose deferred CL submission then fails is inserted into TW_OSLI_FREE_Q twice - once by twa_map_load_data_callback's error path (tw_osl_freebsd.c:1282-1283) and once by the woken ioctl thread unwinding at fw_passthru_err (tw_osl_freebsd.c:1049) - corrupting the request queue (self-ring link) and allowing the same tw_osli_req_context to be owned by two threads (second owner's req->data freeable by the first owner's kfree at :1047-1048). Latent on current x86-64 defaults: EINPROGRESS requires addr_needs_bounce() to fire, which the twa tags (lowaddr=highaddr=BUS_SPACE_MAXADDR, alignment=4 < PAGE_SIZE, page-granular bounce test) can never trigger; it goes live under any busdma config that bounces these loads. Could not be exercised at runtime: /dev/twa0 is 0600 root-only and the audit guest has no 3ware HBA.