Skip to content

Pin native runtime and app libraries to fix residual worker-teardown SIGSEGV - #491

Closed
Saúl Ponce (GalaxiasKyklos) wants to merge 1 commit into
microsoft:mainfrom
GalaxiasKyklos:fix/pin-native-tls-destructor-libs
Closed

Pin native runtime and app libraries to fix residual worker-teardown SIGSEGV#491
Saúl Ponce (GalaxiasKyklos) wants to merge 1 commit into
microsoft:mainfrom
GalaxiasKyklos:fix/pin-native-tls-destructor-libs

Conversation

@GalaxiasKyklos

@GalaxiasKyklos Saúl Ponce (GalaxiasKyklos) commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #487. That fix pinned node-api-dotnet's own host module so it isn't unloaded when a worker_threads Worker 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 own pthread_key TLS 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_tsd invokes 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:

#0  0x0000...  ?? ()                         <- unmapped: dlclosed library's destructor
#1  __GI___nptl_deallocate_tsd (nptl_deallocate_tsd.c:73)
#2  __GI___nptl_deallocate_tsd (nptl_deallocate_tsd.c:22)
#3  start_thread (pthread_create.c:454)
#4  clone3 (clone3.S:91)

#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_NODELETE so 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_phdr on Linux, dyld on macOS) and pins the .NET runtime native libraries. Module names are collected during iteration and pinned afterward, because dl_iterate_phdr holds the loader lock and calling dlopen inside the callback would deadlock.
  • Auto-pin at host init in both entry paths:
    • AOT path — NativeHost.PreventModuleUnload()
    • hosted / npm path — 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

namespace Microsoft.JavaScript.NodeApi;

public static class NodeApiNativeLibrary
{
    // Pin an application's own native dependency (must already be loaded).
    public static bool PreventUnload(string libraryNameOrPath);

    // Pin the .NET runtime's native libraries (called automatically at host init).
    public static void PreventRuntimeLibrariesUnload();
}

Applications that load a native dependency registering a TLS destructor should call PreventUnload once during startup, from the .NET assembly that owns that dependency:

NodeApiNativeLibrary.PreventUnload("libmy-native-auth.so");

Testing

  • Adds NativeLibraryPinningTests covering the API contract (null/empty, not-loaded, Windows no-op) and the real RTLD_NODELETE pin against a loaded runtime library. 4/4 pass on both Windows and Linux (net10.0).
  • Full solution builds clean across all TFMs (net10.0 / net9.0 / net8.0 / netstandard2.0 / net472), 0 warnings.
  • Verified end-to-end against a deterministic native-dependency repro: worker-teardown crash goes from exit 139exit 0 once the offending library is pinned; unaffected pure-managed workloads are unchanged (no regression).

Notes for reviewers

  • Should the runtime-library auto-pin be on by default? It pins libraries that in practice never unload, so the risk is low, but it's called out here in case you'd prefer it gated behind an env var / opt-in.
  • macOS uses the dyld enumeration + RTLD_NODELETE path; validated on Linux, macOS path is untested.

Fixes the residual crash tracked in the follow-up issue to #487.

…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 AI balanced review requested due to automatic review settings August 10, 2026 20:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
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.

2 participants