Pin native runtime and app libraries to fix residual worker-teardown SIGSEGV - #491
Closed
Saúl Ponce (GalaxiasKyklos) wants to merge 1 commit into
Closed
Conversation
…SIGSEGV The microsoft#487 host-module pin only keeps node-api-dotnet's own module mapped. On Linux/macOS the .NET runtime native libraries (libcoreclr, libclrjit, libhostpolicy, libSystem.Native, the OpenSSL crypto libs) and any app-loaded native dependency register their own pthread_key TLS destructors. When such a library is unloaded (dlclose) while a worker thread still holds a key value, glibc's __nptl_deallocate_tsd calls a dangling destructor pointer as the thread exits, crashing the process with SIGSEGV (exit 139). Re-open already-loaded libraries with RTLD_NODELETE so they stay mapped for the process lifetime and their destructors never dangle: - NativeLibraryPinning: enumerates loaded modules (dl_iterate_phdr on Linux, dyld on macOS) and pins the runtime native libraries. Names are collected during iteration and pinned afterward, since dl_iterate_phdr holds the loader lock and calling dlopen inside the callback would deadlock. - Auto-pin the runtime libraries at host init in both the AOT path (NativeHost.PreventModuleUnload) and the hosted/npm path (ManagedHost.InitializeModule). - NodeApiNativeLibrary.PreventUnload lets applications pin their own native dependencies that register TLS destructors; PreventRuntimeLibrariesUnload exposes the runtime-library pin. No-op on Windows and on net472/netstandard. Adds NativeLibraryPinningTests covering the API contract and the RTLD_NODELETE pin against a loaded runtime library. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 144c87db-8d78-474f-bff9-21031a23e3e7
Copilot started reviewing on behalf of
Saúl Ponce (GalaxiasKyklos)
August 10, 2026 20:54
View session
There was a problem hiding this comment.
Pull request overview
Adds native-library pinning to prevent worker teardown crashes caused by unloaded TLS destructors.
Changes:
- Adds public and internal native-library pinning APIs.
- Automatically pins runtime libraries during host initialization.
- Adds cross-platform API tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
test/NativeLibraryPinningTests.cs |
Tests pinning API behavior. |
src/NodeApi/NodeApiNativeLibrary.cs |
Exposes public pinning APIs. |
src/NodeApi/DotNetHost/NativeLibraryPinning.cs |
Implements library discovery and pinning. |
src/NodeApi/DotNetHost/NativeHost.cs |
Pins runtime libraries from the native host. |
src/NodeApi.DotNetHost/ManagedHost.cs |
Pins runtime libraries during managed initialization. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+67
to
+71
| public void PreventRuntimeLibrariesUnload_DoesNotThrow_AndIsIdempotent() | ||
| { | ||
| // Best-effort and idempotent on every platform: it pins the runtime's native libraries on | ||
| // Linux/macOS and is a no-op on Windows. It must never throw. | ||
| NodeApiNativeLibrary.PreventRuntimeLibrariesUnload(); |
Comment on lines
+66
to
+68
| if (s_runtimeLibrariesPinned || !IsSupported) | ||
| { | ||
| return; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #487. That fix pinned node-api-dotnet's own host module so it isn't unloaded when a
worker_threadsWorker is torn down. However, a residual crash still reproduces on Linux (exit 139/ SIGSEGV in__nptl_deallocate_tsd) because other native libraries in the process register their ownpthread_keyTLS destructors and are not covered by the host-module pin.This PR pins those additional libraries — the .NET runtime natives, plus an opt-in API for an application's own native dependencies — so their destructors can never dangle at worker-thread teardown.
Issue: #490
Root cause
A native library can register per-thread cleanup with the OS via
pthread_key_create(&key, destructor). glibc's__nptl_deallocate_tsdinvokes that destructor when a thread exits.If the library that owns the destructor is unloaded (
dlclose) while a worker thread still holds a value for the key — as happens when a Worker that used native code is terminated — the destructor pointer dangles into unmapped memory and the process crashes with SIGSEGV as the worker thread exits:#487 pins only node-api-dotnet's host module. The .NET runtime native libraries (
libcoreclr,libclrjit,libhostpolicy,libSystem.Native, and the OpenSSL crypto libraries) and any app-loaded native dependency register their own TLS destructors and remain exposed.Fix
Re-open already-loaded libraries with
RTLD_NODELETEso they stay mapped for the lifetime of the process; the destructor pointers then always stay valid.NativeLibraryPinning(new, internal): enumerates the loaded modules (dl_iterate_phdron Linux, dyld on macOS) and pins the .NET runtime native libraries. Module names are collected during iteration and pinned afterward, becausedl_iterate_phdrholds the loader lock and callingdlopeninside the callback would deadlock.NativeHost.PreventModuleUnload()ManagedHost.InitializeModule()NodeApiNativeLibrary(new, public API):PreventUnload(string libraryNameOrPath)— lets an application pin its own already-loaded native dependency that registers a TLS destructor (e.g. a native auth/crypto library).PreventRuntimeLibrariesUnload()— public wrapper for the runtime-library pin.No-op on Windows and on
net472/netstandard2.0(the crash is specific to the Linux/macOS hosted-runtime scenario). The pin is idempotent and best-effort — failures are traced, not thrown.Public API
Applications that load a native dependency registering a TLS destructor should call
PreventUnloadonce during startup, from the .NET assembly that owns that dependency:Testing
NativeLibraryPinningTestscovering the API contract (null/empty, not-loaded, Windows no-op) and the realRTLD_NODELETEpin against a loaded runtime library. 4/4 pass on both Windows and Linux (net10.0).exit 139→exit 0once the offending library is pinned; unaffected pure-managed workloads are unchanged (no regression).Notes for reviewers
RTLD_NODELETEpath; validated on Linux, macOS path is untested.Fixes the residual crash tracked in the follow-up issue to #487.