[Bug 296291] cuse(3): cuse_server_free( ) busy-waits unkillably (pause("W")) when a c lient ref is not released on virtual_oss te ardown — process stuck in D, cuse.ko pinned , reboot required
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=296291
Bug ID: 296291
Summary: cuse(3): cuse_server_free() busy-waits unkillably
(pause("W")) when a client ref is not released on
virtual_oss teardown — process stuck in D, cuse.ko
pinned, reboot required
Product: Base System
Version: 15.1-RELEASE
Hardware: Any
OS: Any
Status: New
Severity: Affects Only Me
Priority: ---
Component: kern
Assignee: [email protected]
Reporter: [email protected]
Every time virtual_oss(8) is stopped, its exit path busy-waits in the kernel
for
its cuse clients to drop their references — a routine multi-second stall.
INTERMITTENTLY a client reference is never released, so the wait never ends:
the
exiting process is left wedged forever in an uninterruptible, SIGKILL-immune
state, cuse.ko becomes pinned (cannot be unloaded), and no new virtual_oss
can
recreate the devices. Only a reboot recovers the machine. The proximate
severity
is that the kernel wait is unbounded and uninterruptible (a plain
pause("W")), so
any reference leak becomes permanent.
ENVIRONMENT
FreeBSD 15.1-RELEASE releng/15.1-n283562-96841ea08dcf GENERIC amd64
Base components: sys/fs/cuse/cuse.c (cuse.ko) and usr.sbin/virtual_oss.
Setup: a loopback audio chain.
virtual_oss -i 8 -C 2 -c 2 -b 32 -s 200ms -f /dev/null -a 0 \
-d dsp.play -L dsp.loop
MPD opens /dev/dsp.play (cuse client); BruteFIR opens /dev/dsp.loop (cuse
client) and writes the USB DAC.
OBSERVED
$ ps -o pid,ppid,stat,wchan,mwchan,command -p 5992
PID PPID STAT WCHAN MWCHAN COMMAND
5992 5988 D<E W W virtual_oss ... -d dsp.play -L dsp.loop
- D = uninterruptible kernel sleep, < = realtime prio (-i 8), E = exiting.
- kill -TERM and kill -9 have NO effect (thread is past signal delivery).
- cuse.ko is pinned; "kldunload cuse" then also hangs in D.
- A fresh virtual_oss dies with "Could not create CUSE DSP device".
- No userland process holds /dev/dsp.play or /dev/dsp.loop open at this
point
(procstat -f sweep) — the leaked reference is purely kernel-side.
- Only a reboot clears it; every virtual_oss stop/restart is a reboot risk.
ROOT CAUSE (kernel)
MWCHAN "W" pinpoints cuse_server_free(), the cdevpriv destructor for the
daemon's /dev/cuse fd, run when the daemon exits (sys/fs/cuse/cuse.c:765):
static void
cuse_server_free(void *arg)
{
struct cuse_server *pcs = arg;
while (cuse_server_do_close(pcs) != 1) /* returns pcs->refs */
pause("W", hz); /* <-- MWCHAN "W" */
cuse_server_unref(pcs);
}
The loop busy-waits (1s ticks) until pcs->refs == 1, i.e. until every
client
handle is released. pcs->refs is ++'d per client open (cuse.c:1506,
cuse_client_open) and dropped in the client cdevpriv destructor
cuse_client_free() -> cuse_server_unref() (cuse.c:1480). If a client ref is
never dropped, this loop never terminates and is not interruptible (plain
pause(), no signal/timeout escape) -> the exiting process is stuck in D
forever.
A mitigation exists but is escaped: cuse_server_do_close() sets
pcs->is_closing
and wakes all clients (cuse.c:752-755), and
cuse_client_receive_command_locked()
returns CUSE_ERR_OTHER when is_closing is set (cuse.c:631). The comment
above
the loop ("the client cdevpriv destructor, which cannot destroy itself")
points
at the self-referential case, exactly the loopback (-L/-l) device, where
virtual_oss is both server and client of the same cuse server.
EMPIRICAL EVIDENCE (instrumented cuse.ko + DTrace)
A diagnostic cuse.ko logging pcs->refs inside the loop, over many
start/stop
cycles of the real chain (MPD + BruteFIR clients):
1. EVERY teardown enters the loop with refs > 1 (it is the normal stop
path):
cuse: server pid 11027 exit stuck: refs=3 (want 1) after 1 s
cuse: server pid 11036 exit stuck: refs=2 (want 1) after 1 s
refs=3 = server + BruteFIR(dsp.loop) + MPD(dsp.play); refs=2 = server +
1.
2. DTrace lifecycle (fbt:cuse:cuse_client_open/free, cuse_server_free, by
execname/pid):
musicpd[2278] OPEN
brutefir[9705] OPEN
virtual_oss[9697] SERVER_FREE begin (teardown wait)
... loop spins ...
brutefir[9705] FREE (cuse_client_free -> cuse_server_unref)
musicpd[2278] FREE
(refs reaches 1, server exits)
3. USUAL outcome: clients close within ~1-6 s and the server RECOVERS — a
routine multi-second busy-wait on every stop (e.g. pid 11036 logged
"after 1 s" then "after 6 s" before recovering).
4. SEVERE/INTERMITTENT outcome: a client ref never drops, the loop spins
forever, virtual_oss is stuck D<E (SIGKILL-immune), cuse.ko pinned,
reboot-only (pid 5992).
So is_closing DOES release clients in the common case; the permanent hang
is an
unlucky-timing race where a client close cannot be serviced/completed. This
is
consistent with virtual_oss's teardown: usr.sbin/virtual_oss/.../main.c
destroy_threads() (main.c:2498) pthread_cancel()s the cuse worker threads
without pthread_join(), the workers block in cuse_wait_and_process() (an
ioctl(2), NOT a POSIX cancellation point, main.c:1990), and only the CTL
device
is cuse_dev_destroy()'d on exit (main.c:2630) — the DSP/WAV/loopback
devices
(created at main.c:1918/1935) are never destroyed. On unlucky timing the
close
command for a client thus has no server thread left to process it, and the
unbounded uninterruptible pause("W") turns that transient leak into a
reboot-only hang.
REPRODUCE
1. virtual_oss -i 8 -C 2 -c 2 -b 32 -s 200ms -f /dev/null -a 0 \
-d dsp.play -L dsp.loop
2. Open clients on the devices (BruteFIR reading /dev/dsp.loop S32_LE 2ch,
and/or MPD playing to /dev/dsp.play).
3. Stop virtual_oss (kill, killall, or stop consumers then virtual_oss).
4. Intermittently it never exits: ps shows "D<E W"; kill -9 no effect;
cuse.ko
cannot be unloaded; a fresh virtual_oss cannot recreate the devices.
Frequency rises with rapid start/stop churn and a client attached at stop
time.
IMPACT
Unkillable process; pinned kernel module; reboot is the only recovery. Any
virtual_oss-based audio routing can be bricked until reboot by a normal
stop/restart (e.g. a sample-rate change).
SUGGESTED FIX
Kernel (sys/fs/cuse/cuse.c): cuse_server_free() must not busy-wait
unboundedly/uninterruptibly. Bound the wait (timeout + forced reclamation),
or
restructure teardown so a closing server forcibly invalidates client refs
that
can no longer be serviced (the is_closing wakeup does not cover the stuck
case,
notably the loopback/self-client).
virtual_oss (usr.sbin/virtual_oss/.../main.c): on exit, cuse_dev_destroy()
ALL
created devices (DSP, WAV, loopback), not just CTL, before tearing down
threads;
and pthread_join() the workers instead of pthread_cancel()+free(). A
candidate
userland patch doing this (store each cuse_dev handle in the profile;
destroy
all devices before exit) has been written and compile-tested — it makes the
client refs drop before process exit so cuse_server_free() finds refs==1
and
never busy-waits. That fixes the trigger; the kernel pause("W") hardening
is
still wanted so a future ref leak cannot become an unrecoverable hang.
--
You are receiving this mail because:
You are the assignee for the bug.
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.