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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4178 Β· 8 files
FileTypeDescriptionSize
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
VERDICT.md verdict verification verdict + full path:line trace
↓ download 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)

  1. twe_ioctl TWEIO_GET_PARAM β†’ twe_get_param() (sys/dev/raid/twe/twe.c:770) with func == NULL uses twe_immediate_request() (twe.c:810).
  2. twe_immediate_request() (twe.c:981) sets tr->tr_status = TWE_CMD_BUSY, twe_map_request() posts the command β€” twe_start() (twe.c:1126-1158) sets BUSY and twe_enqueue_busy(tr) (twe_freebsd.c:1144 via twe_setup_data_dmamap β†’ twe_start).
  3. Poll loop (twe.c:1003-1006) waits at most 5000 ms, then falls out and returns 1 with the request still on twe_busy and still posted to the controller (twe.c:1010).
  4. 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).
  5. 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.
  6. 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_REMOVE on a request that is not on twe_busy: writes through the stale tr_link pointers (twevar.h:249-253) β€” kernel memory corruption; - twe_enqueue_complete(tr) β†’ twe_complete() dispatches the new owner's tr_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

inconclusive
baseline no→ patch + rebuild →patched clean

Compile-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'.
↓ fix.diffper-fix-DF-4178

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.