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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4149 Β· 6 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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_testable
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4149

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.