Skip to content

[websocket] Anchor LinkType<IElement> instantiation in websocket library to fix SIGSEGV on plugin unload: Stage 1#2136

Merged
MFransen69 merged 12 commits into
masterfrom
spec/move-commchannel-channelmap-cpp
Jun 3, 2026
Merged

[websocket] Anchor LinkType<IElement> instantiation in websocket library to fix SIGSEGV on plugin unload: Stage 1#2136
MFransen69 merged 12 commits into
masterfrom
spec/move-commchannel-channelmap-cpp

Conversation

@Karthick-Somasundaresan
Copy link
Copy Markdown
Contributor

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:

  • JSONRPCLink.h: extern template class LinkTypeCore::JSON::IElement;
  • JSONRPCLink.cpp: template class LinkTypeCore::JSON::IElement;

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

sramani-metro and others added 5 commits May 21, 2026 12:59
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.
Copilot AI review requested due to automatic review settings May 26, 2026 10:28
@MFransen69 MFransen69 requested a review from sebaszm May 26, 2026 10:30
MFransen69
MFransen69 previously approved these changes May 26, 2026
sramani-metro
sramani-metro previously approved these changes May 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 template declarations in JSONRPCLink.h and matching explicit instantiations in JSONRPCLink.cpp to 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.
MFransen69
MFransen69 previously approved these changes May 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

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
)
Copilot AI review requested due to automatic review settings May 28, 2026 10:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

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
)
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

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 MFransen69 merged commit 23301ce into master Jun 3, 2026
79 checks passed
@MFransen69 MFransen69 deleted the spec/move-commchannel-channelmap-cpp branch June 3, 2026 09:42
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants