twe_immediate_request timeout releases a request still posted to the controller and linked on twe_busy β late completion double-processes the reused request (TAILQ corruption / biodone of in-flight bio)
Summary
twe_immediate_request polls a submitted command for at most 5s and returns 1 on timeout with the request still on the busy queue and still posted. Every caller (get_param/set_param err paths, dump_blocks, init_connection) then calls twe_release_request - double-linked busy+free. The free list turns over in microseconds; when the controller completes the forgotten command, twe_done prints 'nonbusy command' and FALLS THROUGH: clobbers tr_status, runs twe_remove_busy on a request no longer on the busy queue (TAILQ_REMOVE writes through stale tr_link pointers), then dispatches the new owner's tr_complete (biodone on a bio still in flight, completed twice). Root + controller stalling >5s. Abort-on-timeout + drop-stale fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4178 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | verification verdict + full path:line trace | 4.2 KB | β raw |
| twe_getparam_timeout.c | trigger | TWEIO_GET_PARAM driver for the timeout/late-completion race | 3.4 KB | view raw |
| build.sh | build | build command | 113 B | view raw |
| run.sh | run | run command | 173 B | view raw |
| run.log | runlog | guest run: open /dev/twe0 ENOENT (no 3ware HW) | 165 B | view raw |
| env.txt | env | guest environment | 621 B | view raw |
| fix.diff | suggested-fix | timeout dequeue + stale-response drop (git-apply-able) | 2.3 KB | view raw |
| verdict.json | verdict | machine verdict | 3.4 KB | view raw |
DF-4178 β Verification Verdict
Status: inconclusive (hardware-gated: no 3ware Escalade controller in the QEMU guest) Impact: none on this guest (source-certain kernel memory corruption on affected hardware) Confidence: likely (control flow source-certain; trigger needs a controller that completes a command after the 5 s poll window)
Mechanism (source trace)
twe_ioctlTWEIO_GET_PARAM βtwe_get_param()(sys/dev/raid/twe/twe.c:770) withfunc == NULLusestwe_immediate_request()(twe.c:810).twe_immediate_request()(twe.c:981) setstr->tr_status = TWE_CMD_BUSY,twe_map_request()posts the command βtwe_start()(twe.c:1126-1158) sets BUSY andtwe_enqueue_busy(tr)(twe_freebsd.c:1144 via twe_setup_data_dmamap β twe_start).- Poll loop (twe.c:1003-1006) waits at most 5000 ms, then falls out and
returns 1 with the request still on
twe_busyand still posted to the controller (twe.c:1010). - Caller error path
twe_get_param err:(twe.c:826-833) βtwe_release_request(tr)βtwe_enqueue_free(tr)(twe.c:1623-1631). The request is now double-linked (busy + free). - The free list turns over in microseconds under I/O; another consumer
pops the request (
twe_get_request, twe.c:1594) and re-submits it. - The controller eventually completes the old command:
twe_done()(twe.c:1177) βtr = sc->twe_lookup[rq.u.response_id](twe.c:1199).tr->tr_status != TWE_CMD_BUSYβ only prints "completion event for nonbusy command" (twe.c:1203-1204) and then falls through: -tr->tr_status = TWE_CMD_COMPLETE(twe.c:1205) β clobbers the new owner's SETUP/BUSY state; -twe_remove_busy(tr)(twe.c:1209) βTAILQ_REMOVEon a request that is not ontwe_busy: writes through the staletr_linkpointers (twevar.h:249-253) β kernel memory corruption; -twe_enqueue_complete(tr)βtwe_complete()dispatches the new owner'str_complete(e.g.twe_completeio, twe.c:1017 βtwed_intr(bio)βbiodone()on a bio still in flight).
Identical release-while-busy patterns: twe_set_param out: (twe.c:909-914),
twe_dump_blocks (twe.c:506-510), twe_init_connection (twe.c:946-947).
Related but distinct: twe_wait_request() (twe.c:960-973, TWEIO_COMMAND)
has no timeout at all β a never-completing controller hangs the ioctl
forever, each such ioctl permanently consuming one of the 255 requests.
Why it cannot be reproduced on this guest
The twe driver binds to PCI vendor 0x13c1 devices only
(twe_freebsd.c:172-174). The QEMU guest has no 3ware controller
(pciconf -l shows no 13c1 device, no /dev/twe* node exists), so the
trigger path cannot be exercised. PoC builds and runs; open("/dev/twe0")
fails with ENOENT (see run.log).
Phase V: escalation assessment
The primitive (once a late completion arrives) is a write-what-where-ish
TAILQ corruption inside the driver's request queues plus a forced
completion callback with attacker-influenced tr_private/tr_data β
enough for kernel RCE in principle on affected hardware. On this guest the
path is not exercisable at all (device absent), so no chain can be
developed here. Gate: root-only control device (0600 root:operator,
twe_freebsd.c:339-342) + a controller that completes commands late.
Fix validation
fix.diff (two hunks: timeout path unmaps + dequeues + marks ERROR;
twe_done consumes-and-drops stale responses instead of falling through)
applies cleanly to the guest's /usr/src and was batch-compiled with the
other four fixes of this batch into X86_64_GENERIC β see fix_build.log /
fix_verdict in verdict.json.
Residual (documented in fix.diff header): the caller still kfree()s the 512-byte parameter buffer after a timeout while the controller may late-write it; a full fix needs a controller-side abort (TWE_OP_ABORT) before freeing.
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
inconclusiveCompile-validated only: fix.diff (two hunks) applied cleanly to the guest /usr/src and batch-built into X86_64_GENERIC with -Werror (twe.c both in-kernel and as module), rc=0, zero errors - see fix_build.log. Behavioral validation impossible on this guest (no 3ware controller).
fix_build.log (excerpt; full log findings/poc/_batch4178-4182/fix_build.log): twe.c compiled with -Werror in-kernel and KLD_MODULE; 'Kernel build for X86_64_GENERIC completed'.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (step-by-step path:line trace); twe_getparam_timeout.c (trigger); run.log (hardware absent, build OK); fix.diff + fix_build.log (fix compiles -Werror in kernel and module)
PoC changes
n/a (first-pass PoC authored in this run; self-contained ioctl definitions, builds clean with -Wall on the guest)
Verified recommended fix
On timeout, twe_immediate_request must unmap + dequeue (twe_remove_busy) + mark TWE_CMD_ERROR the request before returning, and twe_done must consume-and-drop stale responses for nonbusy requests instead of falling through.
Verdict
Source-certain use-after-release/double-queue in the twe request state machine: twe_immediate_request (twe.c:981) returns after its 5s poll with the request still posted to the controller and linked on twe_busy; every caller (twe_get_param twe.c:826, twe_set_param twe.c:909, twe_dump_blocks twe.c:506, twe_init_connection twe.c:946) then calls twe_release_request, double-linking the request (busy+free). A late completion twe_done (twe.c:1199) falls through the 'nonbusy' print: it clobbers the reused request's status, executes twe_remove_busy (twe.c:1209) on a request not on the busy queue (TAILQ writes through stale linkage pointers) and can fire the new owner's completion callback (biodone on an in-flight bio). Not exercisable on this guest: no 3ware Escalade controller (no /dev/tweN; PoC open() = ENOENT, see run.log); the control node is additionally 0600 root:operator (twe_freebsd.c:339). Trigger on affected HW: any parameter/init/dump command that exceeds the 5s poll (controller busy rebuilding / faulted) and completes afterwards.
No comments yet.