[websocket] Anchor LinkType<IElement> instantiation in websocket library to fix SIGSEGV on plugin unload: Stage 1#2136
Merged
Conversation
spec: move channelMap to cpp to avoid SIGSEGV (#2040) Use explicit template specializations in `JSONRPCLink.cpp` and `extern template` declarations in the header so the channel map is owned by the websocket library TU and shared safely across plugins.
…time bug Stage 1 (short-term): anchor all LinkType<INTERFACE> symbols in the websocket library via whole-class explicit instantiation (extern template class in header, template class in JSONRPCLink.cpp). Fixes both the channelMap static and all INTERFACE-dependent vtables (CommunicationChannel, ChannelImpl, HandlerType) that ResourceMonitor accesses through IResource*. Stage 2 (long-term): separate socket ownership (WebSocketConnection, non-template, defined in .cpp) from protocol framing (ChannelImpl<INTERFACE>, thin adapter, in plugin DSO). ResourceMonitor holds IResource* into WebSocketConnection permanently. New IFramingCallback and IChannelClient interfaces decouple the framing and observer layers from INTERFACE. Zero per-type registration required. Artefacts updated: - proposal.md: 2-stage What Changes, full usage matrix, per-stage Impact - design.md: updated context (full vtable chain), 6 alternatives considered with drawbacks (including type-erasure base-class analysis), 2-stage Goals, Decisions, Risks, and Migration Plan - specs/channelmap-cpp-anchor/spec.md: corrected to explicit instantiation (not specialization); added vtable-stability and ResourceMonitor scenarios - specs/socket-framing-separation/spec.md: new spec for Stage 2 requirements - tasks.md: Stage 1 tasks (2 lines, nm -C verification) + Stage 2 tasks (interfaces, WebSocketConnection, non-template FactoryImpl/CommunicationChannel, ChannelImpl refactor, IChannelClient on LinkType) Tracks: #2040
This is a temporary fix for anchoring definition of LinkTypes to Websocket. By doing this, LinkType will not be bound to a plugins library rather, it will be bound to websocket library. The drawback of binding it to plugin library is that when the plugin is deinitialized, the static channels will also be unloaded leading to dangling references in ResourceMonitor. As a long term solution, we will be moving the CommunicationChannel out of LinkType making it agnostic of the INTERFACE that is passed.
MFransen69
previously approved these changes
May 26, 2026
sramani-metro
previously approved these changes
May 26, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
Anchors JSONRPC::LinkType<Core::JSON::IElement> (and IMessagePack) template instantiations in the Thunder WebSocket library via extern template + explicit instantiation to prevent SIGSEGV caused by dangling vtables/statics after the first-instantiating plugin DSO unloads.
Changes:
- Added
extern templatedeclarations inJSONRPCLink.hand matching explicit instantiations inJSONRPCLink.cppto force a single authoritative instantiation in the websocket shared library. - Added a new GTest file to exercise/validate the explicit-instantiation setup (and wired it into the unit test runner build).
- Added openspec documentation (proposal/design/spec/tasks) describing Stage 1 and the planned Stage 2 refactor.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/unit/core/test_linktype_instantiation.cpp | Adds a standalone (non-GTest) manual verification program for explicit instantiation/linking. |
| Tests/unit/core/test_jsonrpclink_dso.cpp | Adds unit tests intended to validate that LinkType symbols/vtables are anchored in the websocket library. |
| Tests/unit/core/CMakeLists.txt | Adds the new DSO-focused JSONRPCLink unit test to the core test runner build. |
| Source/websocket/JSONRPCLink.h | Adds extern template declarations to suppress per-consumer TU instantiation for LinkType<IElement> and LinkType<IMessagePack>. |
| Source/websocket/JSONRPCLink.cpp | Adds explicit template instantiations to emit the authoritative LinkType symbols in the websocket library TU. |
| openspec/changes/move-commchannel-channelmap-cpp/tasks.md | Adds staged task breakdown for Stage 1/Stage 2 work. |
| openspec/changes/move-commchannel-channelmap-cpp/specs/socket-framing-separation/spec.md | Adds Stage 2 requirements spec for socket/framing separation. |
| openspec/changes/move-commchannel-channelmap-cpp/specs/channelmap-cpp-anchor/spec.md | Adds Stage 1 requirements spec for channelMap/vtable anchoring via explicit instantiation. |
| openspec/changes/move-commchannel-channelmap-cpp/proposal.md | Adds the full problem statement + Stage 1/2 proposal write-up. |
| openspec/changes/move-commchannel-channelmap-cpp/design.md | Adds detailed design rationale and alternatives analysis for both stages. |
| openspec/changes/move-commchannel-channelmap-cpp/.openspec.yaml | Declares openspec metadata for this change set. |
Comment on lines
+120
to
+161
| * @brief Test that vtable pointers are in the websocket library | ||
| * | ||
| * This test verifies that when we create a LinkType object, its vtable | ||
| * pointer points into libThunderWebSocket, not into the test executable | ||
| * or some other DSO. | ||
| * | ||
| * This is the core verification that the explicit instantiation fix works. | ||
| */ | ||
| TEST(Core_JSONRPCLink_DSO, VtableInWebSocketLibrary) | ||
| { | ||
| // We need to set THUNDER_ACCESS for LinkType to work | ||
| // Use a dummy address - we won't actually connect | ||
| ::Thunder::Core::SystemInfo::SetEnvironment(_T("THUNDER_ACCESS"), _T("127.0.0.1:8080")); | ||
|
|
||
| // Create a LinkType instance | ||
| // Note: This will fail to connect, but that's fine - we just need | ||
| // the object to exist to check its vtable location | ||
| try { | ||
| // The constructor may throw or assert if connection fails | ||
| // We wrap in try-catch to handle that gracefully | ||
| JSONRPC::LinkType<::Thunder::Core::JSON::IElement> client("TestCallsign", false); | ||
|
|
||
| // Get the vtable pointer (first word of the object in most ABIs) | ||
| const void* vtablePtr = *reinterpret_cast<const void* const*>(&client); | ||
|
|
||
| std::string libraryName = FindLibraryForAddress(vtablePtr); | ||
|
|
||
| // The vtable should be in libThunderWebSocket | ||
| // (exact name may vary: libThunderWebSocket.so, libThunderWebSocket.so.1, etc.) | ||
| EXPECT_TRUE(libraryName.find("ThunderWebSocket") != std::string::npos) | ||
| << "Expected vtable to be in ThunderWebSocket library, but found in: " | ||
| << libraryName; | ||
| } | ||
| catch (...) { | ||
| // Connection failure is expected since there's no server | ||
| // The test passes if we got here without crashing - the explicit | ||
| // instantiation symbols were found at link time | ||
| SUCCEED() << "LinkType symbols resolved correctly (connection failed as expected)"; | ||
| } | ||
|
|
||
| // Clean up environment | ||
| ::Thunder::Core::SystemInfo::SetEnvironment(_T("THUNDER_ACCESS"), _T("")); |
Comment on lines
+142
to
+145
| // Get the vtable pointer (first word of the object in most ABIs) | ||
| const void* vtablePtr = *reinterpret_cast<const void* const*>(&client); | ||
|
|
||
| std::string libraryName = FindLibraryForAddress(vtablePtr); |
Comment on lines
+49
to
+52
| #ifdef __linux__ | ||
| #include <dlfcn.h> | ||
| #include <link.h> | ||
| #endif |
Comment on lines
+130
to
+133
| // We need to set THUNDER_ACCESS for LinkType to work | ||
| // Use a dummy address - we won't actually connect | ||
| ::Thunder::Core::SystemInfo::SetEnvironment(_T("THUNDER_ACCESS"), _T("127.0.0.1:8080")); | ||
|
|
| test_workerpool.cpp | ||
| test_xgetopt.cpp | ||
| test_jsonrpclink_dso.cpp | ||
| ) |
Comment on lines
+20
to
+39
| /** | ||
| * @file test_linktype_instantiation.cpp | ||
| * @brief Compile-time verification of LinkType explicit template instantiation | ||
| * | ||
| * This file verifies that the explicit template instantiation of | ||
| * LinkType<Core::JSON::IElement> and LinkType<Core::JSON::IMessagePack> | ||
| * in the websocket library works correctly. | ||
| * | ||
| * If this file compiles and links, it proves that: | ||
| * 1. The extern template declaration in JSONRPCLink.h suppresses local instantiation | ||
| * 2. The explicit instantiation in JSONRPCLink.cpp provides the symbols | ||
| * 3. The linker correctly resolves references to those symbols | ||
| * | ||
| * To build and run: | ||
| * g++ -std=c++11 -I<thunder_includes> -L<thunder_libs> \ | ||
| * test_linktype_instantiation.cpp \ | ||
| * -lThunderCore -lThunderWebSocket -lThunderCryptalgo -lpthread \ | ||
| * -o test_linktype_instantiation | ||
| * ./test_linktype_instantiation | ||
| */ |
There is a GCC issue with explicit template instantiation syntax. The issue is that EXTERNAL (visibility attribute) shouldn't be on explicit template instantiation lines. GCC with -Werror=attributes rejects this.
28e7b2a
MFransen69
previously approved these changes
May 27, 2026
Comment on lines
+137
to
+158
| try { | ||
| // The constructor may throw or assert if connection fails | ||
| // We wrap in try-catch to handle that gracefully | ||
| JSONRPC::LinkType<::Thunder::Core::JSON::IElement> client("TestCallsign", false); | ||
|
|
||
| // Get the vtable pointer (first word of the object in most ABIs) | ||
| const void* vtablePtr = *reinterpret_cast<const void* const*>(&client); | ||
|
|
||
| std::string libraryName = FindLibraryForAddress(vtablePtr); | ||
|
|
||
| // The vtable should be in libThunderWebSocket | ||
| // (exact name may vary: libThunderWebSocket.so, libThunderWebSocket.so.1, etc.) | ||
| EXPECT_TRUE(libraryName.find("ThunderWebSocket") != std::string::npos) | ||
| << "Expected vtable to be in ThunderWebSocket library, but found in: " | ||
| << libraryName; | ||
| } | ||
| catch (...) { | ||
| // Connection failure is expected since there's no server | ||
| // The test passes if we got here without crashing - the explicit | ||
| // instantiation symbols were found at link time | ||
| SUCCEED() << "LinkType symbols resolved correctly (connection failed as expected)"; | ||
| } |
| test_workerpool.cpp | ||
| test_xgetopt.cpp | ||
| test_jsonrpclink_dso.cpp | ||
| ) |
Comment on lines
+153
to
+195
| // We need to set THUNDER_ACCESS for LinkType to work | ||
| // Use a dummy address - we won't actually connect | ||
| ::Thunder::Core::SystemInfo::SetEnvironment(_T("THUNDER_ACCESS"), _T("127.0.0.1:8080")); | ||
|
|
||
| const bool isSharedBuild = IsWebSocketSharedLibraryLoaded(); | ||
|
|
||
| // Create a LinkType instance | ||
| // Note: This will fail to connect, but that's fine - we just need | ||
| // the object to exist to check its vtable location | ||
| try { | ||
| // The constructor may throw or assert if connection fails | ||
| // We wrap in try-catch to handle that gracefully | ||
| JSONRPC::LinkType<::Thunder::Core::JSON::IElement> client("TestCallsign", false); | ||
|
|
||
| // Get the vtable pointer (first word of the object in most ABIs) | ||
| const void* vtablePtr = *reinterpret_cast<const void* const*>(&client); | ||
|
|
||
| std::string libraryName = FindLibraryForAddress(vtablePtr); | ||
|
|
||
| if (isSharedBuild) { | ||
| // In shared library builds, vtable should be in libThunderWebSocket | ||
| // (exact name may vary: libThunderWebSocket.so, libThunderWebSocket.so.1, etc.) | ||
| EXPECT_TRUE(libraryName.find("ThunderWebSocket") != std::string::npos) | ||
| << "Expected vtable to be in ThunderWebSocket library, but found in: " | ||
| << libraryName; | ||
| } else { | ||
| // In static builds, vtable will be in the executable itself | ||
| // Just verify we got a valid library/executable name | ||
| EXPECT_FALSE(libraryName.empty()) | ||
| << "Could not determine vtable location"; | ||
| SUCCEED() << "Static build: vtable correctly linked into executable: " | ||
| << libraryName; | ||
| } | ||
| } | ||
| catch (...) { | ||
| // Connection failure is expected since there's no server | ||
| // The test passes if we got here without crashing - the explicit | ||
| // instantiation symbols were found at link time | ||
| SUCCEED() << "LinkType symbols resolved correctly (connection failed as expected)"; | ||
| } | ||
|
|
||
| // Clean up environment | ||
| ::Thunder::Core::SystemInfo::SetEnvironment(_T("THUNDER_ACCESS"), _T("")); |
| test_workerpool.cpp | ||
| test_xgetopt.cpp | ||
| test_jsonrpclink_dso.cpp | ||
| ) |
Comment on lines
+162
to
+192
| try { | ||
| // The constructor may throw or assert if connection fails | ||
| // We wrap in try-catch to handle that gracefully | ||
| JSONRPC::LinkType<::Thunder::Core::JSON::IElement> client("TestCallsign", false); | ||
|
|
||
| // Get the vtable pointer (first word of the object in most ABIs) | ||
| const void* vtablePtr = *reinterpret_cast<const void* const*>(&client); | ||
|
|
||
| std::string libraryName = FindLibraryForAddress(vtablePtr); | ||
|
|
||
| if (isSharedBuild) { | ||
| // In shared library builds, vtable should be in libThunderWebSocket | ||
| // (exact name may vary: libThunderWebSocket.so, libThunderWebSocket.so.1, etc.) | ||
| EXPECT_TRUE(libraryName.find("ThunderWebSocket") != std::string::npos) | ||
| << "Expected vtable to be in ThunderWebSocket library, but found in: " | ||
| << libraryName; | ||
| } else { | ||
| // In static builds, vtable will be in the executable itself | ||
| // Just verify we got a valid library/executable name | ||
| EXPECT_FALSE(libraryName.empty()) | ||
| << "Could not determine vtable location"; | ||
| SUCCEED() << "Static build: vtable correctly linked into executable: " | ||
| << libraryName; | ||
| } | ||
| } | ||
| catch (...) { | ||
| // Connection failure is expected since there's no server | ||
| // The test passes if we got here without crashing - the explicit | ||
| // instantiation symbols were found at link time | ||
| SUCCEED() << "LinkType symbols resolved correctly (connection failed as expected)"; | ||
| } |
Comment on lines
+159
to
+193
| // Create a LinkType instance | ||
| // Note: This will fail to connect, but that's fine - we just need | ||
| // the object to exist to check its vtable location | ||
| try { | ||
| // The constructor may throw or assert if connection fails | ||
| // We wrap in try-catch to handle that gracefully | ||
| JSONRPC::LinkType<::Thunder::Core::JSON::IElement> client("TestCallsign", false); | ||
|
|
||
| // Get the vtable pointer (first word of the object in most ABIs) | ||
| const void* vtablePtr = *reinterpret_cast<const void* const*>(&client); | ||
|
|
||
| std::string libraryName = FindLibraryForAddress(vtablePtr); | ||
|
|
||
| if (isSharedBuild) { | ||
| // In shared library builds, vtable should be in libThunderWebSocket | ||
| // (exact name may vary: libThunderWebSocket.so, libThunderWebSocket.so.1, etc.) | ||
| EXPECT_TRUE(libraryName.find("ThunderWebSocket") != std::string::npos) | ||
| << "Expected vtable to be in ThunderWebSocket library, but found in: " | ||
| << libraryName; | ||
| } else { | ||
| // In static builds, vtable will be in the executable itself | ||
| // Just verify we got a valid library/executable name | ||
| EXPECT_FALSE(libraryName.empty()) | ||
| << "Could not determine vtable location"; | ||
| SUCCEED() << "Static build: vtable correctly linked into executable: " | ||
| << libraryName; | ||
| } | ||
| } | ||
| catch (...) { | ||
| // Connection failure is expected since there's no server | ||
| // The test passes if we got here without crashing - the explicit | ||
| // instantiation symbols were found at link time | ||
| SUCCEED() << "LinkType symbols resolved correctly (connection failed as expected)"; | ||
| } | ||
|
|
Comment on lines
+20
to
+39
| /** | ||
| * @file test_linktype_instantiation.cpp | ||
| * @brief Compile-time verification of LinkType explicit template instantiation | ||
| * | ||
| * This file verifies that the explicit template instantiation of | ||
| * LinkType<Core::JSON::IElement> and LinkType<Core::JSON::IMessagePack> | ||
| * in the websocket library works correctly. | ||
| * | ||
| * If this file compiles and links, it proves that: | ||
| * 1. The extern template declaration in JSONRPCLink.h suppresses local instantiation | ||
| * 2. The explicit instantiation in JSONRPCLink.cpp provides the symbols | ||
| * 3. The linker correctly resolves references to those symbols | ||
| * | ||
| * To build and run: | ||
| * g++ -std=c++11 -I<thunder_includes> -L<thunder_libs> \ | ||
| * test_linktype_instantiation.cpp \ | ||
| * -lThunderCore -lThunderWebSocket -lThunderCryptalgo -lpthread \ | ||
| * -o test_linktype_instantiation | ||
| * ./test_linktype_instantiation | ||
| */ |
MFransen69
approved these changes
May 28, 2026
sramani-metro
approved these changes
May 29, 2026
sebaszm
approved these changes
Jun 1, 2026
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Fix SIGSEGV caused by dangling vtables when a plugin that first instantiated
LinkType is deactivated and its shared library is unloaded.
CommunicationChannel::Instance() holds channelMap as a function-local static
inside a header-only template. The nested ChannelImpl's vtable (via HandlerType
→ StreamJSONType → IResource) is instantiated in whichever plugin DSO first
triggers the template. ResourceMonitor holds IResource* into this object. When
the originating plugin unloads, both channelMap and all affected vtables become
dangling, causing SIGSEGV on subsequent channel operations.
Use explicit template instantiation to anchor all LinkType symbols —
including channelMap, CommunicationChannel's vtable, ChannelImpl's vtable, and
HandlerType's vtable — in the websocket library:
Plugin TUs see extern template and link to the websocket library's symbols
instead of emitting their own copies.
A long-term solution (Stage:2) is being worked upon as specified in the openspec/changes/move-commchannel-channelmap-cpp/proposal.md