Skip to content

fix(device): bypass foreign atfork handlers in reattach helper - #11

Merged
andrescera merged 2 commits into
mainfrom
fix/reattach-helper-raw-clone
Jul 30, 2026
Merged

fix(device): bypass foreign atfork handlers in reattach helper#11
andrescera merged 2 commits into
mainfrom
fix/reattach-helper-raw-clone

Conversation

@andrescera

Copy link
Copy Markdown
Member

What

Replace the reattach helper\047s fork() calls with raw clone(2) syscalls.

Why

glibc\047s fork() wrapper runs process-wide pthread_atfork() handlers. In the cerastream process, libsrt\047s child handler could lock a mutex held by a thread absent from the child, leaving the helper wedged with roughly 99 inherited descriptors and orphaning camera/audio resources. Raw clone(SIGCHLD, ...) preserves the required process topology while bypassing that handler chain.

How to verify

  • libuvc tests: 37/37 pass.
  • Red-first regression: reverting the raw-clone change produced the bounded ***Timeout 30.03 sec; restoring it produced Passed 0.11 sec.
  • Live RK3588 board proof: 3 real CeraUI start/stop cycles, zero orphans across all 3, including a cycle where the camera failed mid-test; the board remained at one clean cerastream process with no left-over-process journal entry.
  • Frozen board evidence: task-reattach-raw-clone-board-proof.md, SHA256 bdb689aa53eb82d9e1c13b34531a4d9d7f03b063d97f18d346c0b69575e9dec5.

Risks

Raw clone intentionally skips glibc post-fork repair, but the child path is already restricted to async-signal-safe operations and does not touch malloc, loader, or stdio state. The board evidence covers the production reattach path.

glibc's fork() runs every pthread_atfork() handler registered anywhere in
the process, in the child, on every call -- it does not matter which
library called fork() or why. cerastream links libuvc next to libsrt,
whose child handler (srt::CUDTUnited::cleanupAtFork) locks an SRT mutex.
A fork carries only the calling thread into the child, so whenever
another SRT thread held that mutex at the instant of the guard's fork it
was locked in the child with no thread left alive to release it.

gdb on a stuck child on an RK3588 board: pthread_mutex_lock <-
CUDT::closeInternal <- cleanupAllSockets <- cleanupAtFork <- fork <-
uvc_reattach_guard_create, with strace showing an untimed
FUTEX_WAIT_PRIVATE. helper_main() never ran, so nothing was reattached
AND nothing was closed -- the child kept the ~99 descriptors the fork had
copied, including the usbfs node and the ALSA capture device, and ignored
every SIGTERM. The next run then found its audio device busy and its
camera taken, and systemd logged a left-over process in the control group
on essentially every restart.

fork(2) is defined as clone(2) with flags of just SIGCHLD, and
__run_fork_handlers() is reached only from glibc's fork() wrapper, so
issuing that clone directly is the same process creation with none of the
handlers. Skipping glibc's own post-fork repairs costs nothing here: this
child is post-fork code in a multi-threaded process and was already
restricted to async-signal-safe calls that touch none of that state.

The double-fork topology, the waitpid() handshake and everything
helper_main() does are unchanged. The new regression case registers such
a handler in the victim and holds its mutex hostage on a second thread
across the fork; against the fork() code it fails on a bounded wait and
then times out, because the victim never reaches its own SIGKILL.

Board-Evidence-SHA256: bdb689aa53eb82d9e1c13b34531a4d9d7f03b063d97f18d346c0b69575e9dec5
@andrescera
andrescera merged commit 7a81419 into main Jul 30, 2026
3 checks passed
@andrescera
andrescera deleted the fix/reattach-helper-raw-clone branch July 30, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant