[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

[email protected]
Newsgroups gmane.os.freebsd.bugs
Message-ID <[email protected]/bugzilla/>
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.