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)
PoC verification
Evidence pack
findings/poc/DF-4274 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 abus_dmamap_load()deferral (EINPROGRESS), which the driver's DMA tags cannot produce on default-configured x86-64 (analysis inVERDICT.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.
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)
tw_osli_fw_passthru()callstw_osli_map_request(req)(:953).tw_osli_map_request()loads the sharedsc->ioctl_mapwithBUS_DMA_WAITOK(:1371-1374). DragonFlybus_dmamap_load()queues the map and returnsEINPROGRESSwhen the bounce zone cannot satisfymap->pagesneeded(sys/platform/pc64/x86_64/busdma_machdep.c:640-657, 793-805). The driver marks the requestTW_OSLI_REQ_FLAGS_IN_PROGRESS(:1397-1398), reports success, and the ioctl thread enters its wait loop (:957).- The deferred load is resumed from
busdma_swi()(softint context) which re-invokes the stored callbacktwa_map_load_data_callback(busdma_machdep.c:1436-1448) β note: outside the driver'sio_lock, on the shared map. - If
tw_cl_fw_passthru()returns an error synchronously inside that callback (tw_cl_io.c:283EBUSY when the CL request pool is exhausted;tw_cl_io.c:341-348submit failure β both reachable under load/controller fault), theout:error path (:1273-1284) runs: -req_pkt->tw_osl_callback()βtw_osl_complete_passthru()(:1064): setsstate = COMPLETE, removes fromTW_OSLI_BUSY_Q, unmaps, and β becauseTW_OSLI_REQ_FLAGS_IN_PROGRESSis set, the early-return at:1098does not fire β wakes the sleeping originator (:1101-1107). - thenif (req->flags & TW_OSLI_REQ_FLAGS_IN_PROGRESS) tw_osli_req_q_insert_tail(req, TW_OSLI_FREE_Q);(:1282-1283) β first insertion. - The woken ioctl thread sees
state == TW_OSLI_REQ_STATE_COMPLETE, exits the loop (:957), runs itscopyoutand then unwinds throughfw_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_testablefix.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)
Confirmed kernel references
- sys/dev/raid/twa/tw_osl_freebsd.c:1273
- sys/dev/raid/twa/tw_osl_freebsd.c:1282
- sys/dev/raid/twa/tw_osl_freebsd.c:1390
- sys/dev/raid/twa/tw_osl_freebsd.c:1397
- sys/dev/raid/twa/tw_osl_freebsd.c:1040
- sys/dev/raid/twa/tw_osl_freebsd.c:1049
- sys/dev/raid/twa/tw_osl_freebsd.c:1098
- sys/dev/raid/twa/tw_osl_freebsd.c:1101
- sys/platform/pc64/x86_64/busdma_machdep.c:640
- sys/platform/pc64/x86_64/busdma_machdep.c:793
- sys/dev/raid/twa/tw_cl_io.c:283
- sys/dev/raid/twa/tw_osl_cam.c:335
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.
No comments yet.