Zero-length iovec sends cuse_client_read/write into an infinite command loop; the looping reader becomes unkillable (survives SIGKILL)
Summary
The read/write clients iterate 'while (uio_resid != 0)' using iov_len directly; for an empty first iov a server reply of 0 advances nothing, so the loop re-issues 0-length commands forever. Because receive_command switches to uninterruptible cv_wait after the first signal, SIGKILL is never delivered while the server replies; a fork-inherited /dev/cuse fd makes the D-state permanent until reboot, pinning kernel allocations. Any unprivileged user with read access to a cuse node. REPRODUCED FIRST TRY: nobody-uid client with readv zero-len first iov 'SURVIVED SIGKILL (unkillable, stuck in cuse_client_read loop)'. Skip-zero-length-iov fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4149 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| cuse_zerolen.c | β | 3.9 KB | view raw | |
| build.sh | β | 110 B | view raw | |
| run.log | β | 331 B | view raw | |
| fix.diff | β | 938 B | view raw | |
| VERDICT.md | β | 1.9 KB | β raw | |
| verdict.json | β | 2.6 KB | view raw |
DF-4149 β zero-length iovec β unkillable read loop in cuse_client_read/write
What (reproduced)
cuse_client_read() (cuse.c:1687-1741) and cuse_client_write()
(cuse.c:1778-1832) walk the uio directly:
while (uio->uio_resid != 0) {
len = uio->uio_iov->iov_len; /* can be 0 (empty first iov) */
... send CUSE_CMD_READ, wait for server ...
else if (error == len) /* 0 == 0 */
error = uiomove(NULL, 0, uio); /* no progress */
uiomove() with count 0 never advances uio_iov past the empty iov, so
the loop re-sends a 0-length READ/WRITE command forever. readv(fd,
[{NULL,0},{buf,8}], 2) from any user able to open the cuse device node
triggers it (DFly's iovec_copyin does not drop empty iovs).
Worse, after the first signal cuse_client_receive_command_locked
(cuse.c:622-635) switches from cv_wait_sig to the uninterruptible
cv_wait, and each loop iteration blocks again waiting for the next
server round-trip, so SIGKILL is never delivered for as long as the
server keeps replying: the reader is an unkillable process.
Reproduced (run.log): client (uid 65534) survives kill -9; it is only
released when the server disappears (is_closing). If the stuck client
holds an inherited/duplicated /dev/cuse fd (trivially arranged β observed
accidentally during DF-4147 harness work), is_closing is never set and
the process is permanently unkillable (D-state) until reboot, pinning
the cuse client (~12 KB) and server objects forever.
Fix
fix.diff: skip leading zero-length iovs exactly like uiomove() does,
in both readers.
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 (skip leading zero-length iovs in both loops, mirroring uiomove); not rebuilt on the guest - the only guest mutation budget was spent on the DF-4147 memory-corruption fix, and this Low-severity DoS fix is a two-loop mechanical change reviewed at logic level.
fix.diff; logic-level review only
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: '[+] client uid=65534 entering readv(iov[0].len=0, iov[1].len=8)' then '[+] client SURVIVED SIGKILL (unkillable, stuck in cuse_client_read loop) - DoS confirmed'; post-run ps showed pid 1268 in state D0 (wchan cuse-cli) persisting after the server process exited.
PoC changes
none (first-try reproduction)
Verified recommended fix
Skip zero-length iovs (advance uio_iov/uio_iovcnt) at the top of the read/write loops like uiomove() does (see fix.diff).
Verdict
cuse_client_read/write (cuse.c:1687-1741/1778-1832) walk uio->uio_iov directly without skipping zero-length iovs; uiomove(NULL, 0, uio) makes no progress so the loop re-issues a 0-length READ/WRITE command forever. Triggered from an unprivileged client (uid 65534) with readv(fd, [{NULL,0},{buf,8}], 2) on a world-accessible cuse device whose server replies 0. After the first signal the receive path uses uninterruptible cv_wait per round-trip, so the looping reader SURVIVES kill -9 (demonstrated: 'client SURVIVED SIGKILL'). It is only released when the server exits (is_closing); if the stuck client also holds a /dev/cuse fd (e.g. forked from the server without O_CLOEXEC hygiene - observed during DF-4147 harness work) it stays unkillable in D-state until reboot, permanently pinning the cuse client+server kernel allocations.
No comments yet.