Skip to content

deps: uv: restore prior signal disposition instead of SIG_DFL (DCP-4748)#4

Open
philippe-distributive wants to merge 1 commit into
dcp/releasefrom
dcp-4748-uv-signal-restore-prev-handler
Open

deps: uv: restore prior signal disposition instead of SIG_DFL (DCP-4748)#4
philippe-distributive wants to merge 1 commit into
dcp/releasefrom
dcp-4748-uv-signal-restore-prev-handler

Conversation

@philippe-distributive
Copy link
Copy Markdown

@philippe-distributive philippe-distributive commented Jun 3, 2026

Summary

When a Node environment is torn down inside an Android app process
(node::FreeEnvironment()uv_close() on a UV_SIGNAL handle), libuv
resets the signal's disposition to SIG_DFL. On Android that sigaction
call is intercepted by ART's libsigchain, which logs an ERROR-level
stack trace on every worker stop:

  E libsigchain: Setting SIGSEGV to SIG_DFL
  E libsigchain:   ... uv_close ... node::Environment::CleanupHandles() ...

ART claims SIGSEGV/SIGBUS/SIGFPE/SIGILL/SIGTRAP/SIGABRT for implicit
null-checks, stack-overflow detection, etc., so resetting one to SIG_DFL
is also semantically wrong, not just noisy.

Root cause

uv__signal_unregister_handler() unconditionally installs SIG_DFL once
the last watcher for a signal is removed. libuv never recorded what was
installed before it added its multiplexer — there's a long-standing
/* XXX save old action so we can restore it later on? */ TODO at the
install site to prove it.

Fix

Resolve that TODO. uv__signal_register_handler() now captures the
previous struct sigaction (straight into a per-signum slot) on the
genuine "no libuv handler → installed" transition, gated by a save
flag so the oneshot↔regular re-registration paths never overwrite it
with libuv's own handler. uv__signal_unregister_handler() restores that
saved disposition instead of forcing SIG_DFL, falling back to SIG_DFL

Platform-agnostic and strictly more correct than a blind reset; on Android
the restored handler is ART's (kept by libsigchain), so no SIG_DFL reset
— and no libsigchain report — occurs. Zero hot-path cost (only runs on the
first register / last unregister per signum); the saved table is touched
only under the existing global signal lock.

Alternatives rejected

  • Skip the reset under #ifdef __ANDROID__ — Android-specific and
    leaves libuv's multiplexer chained behind ART forever.
  • Raw rt_sigaction(SIG_DFL) syscall to bypass libsigchainunsafe:
    it overwrites libsigchain's kernel-level handler (the one ART chains
    behind), silencing the log by breaking ART's signal handling.

Testing

  • libuv signal suite built for the host: 10/10 pass, including
    we_get_signals_mixed (the oneshot↔regular path the save gate
    guards), signal_pending_on_close, signal_close_loop_alive.
  • Cross-compiled libnode.so for all four Android ABIs (arm64-v8a,
    x86_64, armeabi-v7a, x86).
  • On an x86_64 emulator (API 35): worker start→stop, libsigchain "Setting SIGSEGV to SIG_DFL" count 1 → 0, with the cleanup path
    confirmed exercised (FreeEnvironment runs, libuv loop closed cleanly).

Upstreaming

deps/uv here is byte-identical to upstream libuv v1.51.0, so this is a
clean candidate for an upstream libuv PR (any embedder benefits).

Rollout

Cut a tag (e.g. dcp/3.2.1) → bump node-build's GIT_TAG → rebuild the
Android node_api zips → bump the URL/SHA in
dcp-native/externals/node_api/CMakeLists.txt → drop the rebuilt
libnode.so into android-worker jnilibs/.

libuv's uv__signal_unregister_handler() reset the disposition of a signal
to SIG_DFL once its last watcher was removed. On Android this is intercepted
by ART's libsigchain, which logs an ERROR-level stack trace ("Setting SIGSEGV
to SIG_DFL ...") and can destabilise ART's signal handling, because ART claims
SIGSEGV/SIGBUS/SIGFPE/SIGILL/SIGTRAP/SIGABRT for implicit null checks, stack
overflow detection, etc. On an embedded worker this fires on every
node::FreeEnvironment() during CleanupHandles -> uv_close on a UV_SIGNAL
handle (DCP-4748).

Resolve libuv's own long-standing "XXX save old action so we can restore it
later on?" TODO: uv__signal_register_handler() now captures the previous
disposition on the genuine none->installed transition (gated by a `save`
flag so the oneshot re-registration paths don't clobber it), and
uv__signal_unregister_handler() restores that instead of forcing SIG_DFL,
falling back to SIG_DFL only when nothing was captured. Platform-agnostic and
strictly more correct than a blind reset; on Android it restores ART's handler
so no SIG_DFL reset (and no libsigchain report) occurs.

Signed-off-by: Philippe Laporte <philippelaporte1@gmail.com>
@philippe-distributive philippe-distributive self-assigned this Jun 3, 2026
@philippe-distributive philippe-distributive added the bug Something isn't working label Jun 3, 2026
@philippe-distributive
Copy link
Copy Markdown
Author

fixed upstream in libuv/libuv#5157

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants