Skip to content

deps: uv: restore prior signal disposition on Android (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 on Android (DCP-4748)#4
philippe-distributive wants to merge 1 commit into
dcp/releasefrom
dcp-4748-uv-signal-restore-prev-handler

Conversation

@philippe-distributive

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

Copy link
Copy Markdown

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
also clobbers ART's handler — 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.

Fix (gated to __ANDROID__)

On Android, uv__signal_register_handler() captures the previous
struct sigaction (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() then restores that saved
disposition instead of forcing SIG_DFL, falling back to SIG_DFL only when
nothing was captured.

The whole save/restore path is #ifdef __ANDROID__; off Android the code
path and behaviour are byte-for-byte unchanged
. 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 the first register /
last unregister per signum); the saved table is touched only under the
existing global signal lock.

Why Android-only

A cross-platform version of this was tried upstream (libuv nodejs#4216) and
reverted (nodejs#4302, refs nodejs#4299) because macOS doesn't propagate
SA_RESETHAND in the old action returned by sigaction()
— so the captured
disposition is wrong there. bionic (Android) round-trips the flags correctly,
so gating to __ANDROID__ keeps the fix where it's both needed and correct
and leaves macOS/Linux/Windows on stock upstream behaviour. See the upstream
discussion in libuv#5157 (this change floated cross-platform) and libuv issue
nodejs#4299.

Alternatives rejected

Testing

  • libuv signal suite built for the host: 10/10 pass (incl.
    we_get_signals_mixed — the oneshot↔regular path the save gate guards —
    signal_pending_on_close, signal_close_loop_alive); non-Android compiles
    warning-free with stock SIG_DFL behaviour preserved.
  • Cross-compiled libnode.so for all four Android ABIs (arm64-v8a, x86_64,
    armeabi-v7a, x86); the 32-bit pair via the ia32 build-host path.
  • 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

The cross-platform variant is not currently acceptable upstream — it hits
the macOS oldact bug (nodejs#4299) that reverted nodejs#4216. Tracked in libuv#5157,
where an __ANDROID__-gated version is proposed; the door reopens if a
supported macOS (≥ 11) starts round-tripping the flags.

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. (The four android-worker
jnilibs/*.so have already been rebuilt from this branch and validated on the
emulator.)

@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

On Android, ART claims SIGSEGV/SIGBUS/SIGFPE/SIGILL/SIGTRAP/SIGABRT for
implicit null-checks and stack-overflow detection, routed through
libsigchain. libuv's uv__signal_unregister_handler() resets a signal to
SIG_DFL once its last watcher is removed; on Android that clobbers ART's
handler and emits an ERROR-level libsigchain stack trace on every embedded
node::FreeEnvironment() (DCP-4748).

Gated to __ANDROID__: uv__signal_register_handler() captures the previous
disposition on the no-handler -> handler transition and
uv__signal_unregister_handler() restores it instead of forcing SIG_DFL.
bionic round-trips the sigaction flags correctly, so this is safe; it is
deliberately NOT applied to other platforms (notably macOS, which drops
SA_RESETHAND in the old action -- see upstream libuv nodejs#4299, which caused
PR nodejs#4216 to be reverted). Off Android the code path and behaviour are
unchanged. The saved table is accessed only under the global signal lock.
@philippe-distributive philippe-distributive force-pushed the dcp-4748-uv-signal-restore-prev-handler branch from 208331f to dcededb Compare June 4, 2026 17:10
@philippe-distributive philippe-distributive changed the title deps: uv: restore prior signal disposition instead of SIG_DFL (DCP-4748) deps: uv: restore prior signal disposition on Android (DCP-4748) Jun 4, 2026
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