From ac270e0f4bc3031c3581ff54cdfff4095aed69bf Mon Sep 17 00:00:00 2001 From: Petri Savolainen Date: Fri, 17 Jul 2026 12:43:19 +0300 Subject: [PATCH] streamrproxyclient: re-initialize the library on API calls after cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proxyClientCleanupLibrary() destroys the LibProxyClientApi instance but nothing re-created it — the load-time constructor runs only once — so any API call after a cleanup dereferenced a null pointer and crashed. In practice this made it impossible to run two integration tests in one process: the fixture destructor cleans the library up after every test. CI masked the bug by running each test in its own process. getProxyClientApi() now re-initializes the library on demand, matching the header's documented contract that the library can be re-initialized after a cleanup. The regression test exercises use after cleanup both through the implicit lazy path and through an explicit proxyClientInitLibrary() call, in a single process. Co-Authored-By: Claude Fable 5 --- .../include/streamrproxyclient.h | 4 +- .../src/streamrproxyclient.cpp | 6 +++ .../integration/StreamrProxyClientTest.cpp | 41 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/streamr-libstreamrproxyclient/include/streamrproxyclient.h b/packages/streamr-libstreamrproxyclient/include/streamrproxyclient.h index 63bb408f..d25573b7 100644 --- a/packages/streamr-libstreamrproxyclient/include/streamrproxyclient.h +++ b/packages/streamr-libstreamrproxyclient/include/streamrproxyclient.h @@ -43,8 +43,10 @@ EXTERN_C SHARED_EXPORT void proxyClientInitLibrary(void); /** * @brief Cleanup the library. This function MUST be called before the program exits. * Can be safely called multiple times. This is needed because the standard dynamic library - * destructor (__attribute__((destructor))) is called after static variables have already been destroyed, + * destructor (__attribute__((destructor))) is called after static variables have already been destroyed, * which makes it impossible to clean up the other objects that depend on the static variables. + * The library may be used again after a cleanup: any API call re-initializes it automatically + * (an explicit proxyClientInitLibrary() call is also fine). */ // NOLINTNEXTLINE EXTERN_C SHARED_EXPORT void proxyClientCleanupLibrary(void); diff --git a/packages/streamr-libstreamrproxyclient/src/streamrproxyclient.cpp b/packages/streamr-libstreamrproxyclient/src/streamrproxyclient.cpp index fa819a1b..c9c589ee 100644 --- a/packages/streamr-libstreamrproxyclient/src/streamrproxyclient.cpp +++ b/packages/streamr-libstreamrproxyclient/src/streamrproxyclient.cpp @@ -1375,6 +1375,12 @@ void proxyClientCleanupLibrary() { // NOLINT } static LibProxyClientApi& getProxyClientApi() { // NOLINT + // proxyClientCleanupLibrary() destroys the instance and only the + // load-time constructor re-creates it, so an API call made after a + // cleanup would dereference a null pointer here. Re-initialize on + // demand instead — the header documents that the library can be + // re-initialized after a cleanup. + proxyClientInitLibrary(); return *libProxyClientApi; } diff --git a/packages/streamr-libstreamrproxyclient/test/integration/StreamrProxyClientTest.cpp b/packages/streamr-libstreamrproxyclient/test/integration/StreamrProxyClientTest.cpp index abfdd600..8da3fedf 100644 --- a/packages/streamr-libstreamrproxyclient/test/integration/StreamrProxyClientTest.cpp +++ b/packages/streamr-libstreamrproxyclient/test/integration/StreamrProxyClientTest.cpp @@ -61,6 +61,47 @@ TEST_F(StreamrProxyClientTest, CanCreateAndDeleteProxyClient) { SLogger::info("test finished"); } +// Regression test for use-after-cleanup: the fixture destructor calls +// proxyClientCleanupLibrary() after every test, so any test that runs +// after another one in the same process starts with the library torn +// down. API entry points must re-initialize the library on demand +// instead of dereferencing the destroyed instance (this crashed with +// SIGSEGV before the fix; CI masked it by running every test in its +// own process). +TEST_F(StreamrProxyClientTest, ApiIsUsableAfterCleanupLibrary) { + // Implicit re-initialization: call the API right after a cleanup. + proxyClientCleanupLibrary(); + + const ProxyResult* result = nullptr; + uint64_t clientHandle = + proxyClientNew(&result, validEthereumAddress, validStreamPartId); + EXPECT_EQ(result->numErrors, 0); + EXPECT_NE(clientHandle, 0); + proxyClientResultDelete(result); + + const ProxyResult* result2 = nullptr; + proxyClientDelete(&result2, clientHandle); + EXPECT_EQ(result2->numErrors, 0); + proxyClientResultDelete(result2); + + // Explicit re-initialization (the path documented in + // streamrproxyclient.h): cleanup, init, use. + proxyClientCleanupLibrary(); + proxyClientInitLibrary(); + + const ProxyResult* result3 = nullptr; + uint64_t clientHandle2 = + proxyClientNew(&result3, validEthereumAddress, validStreamPartId); + EXPECT_EQ(result3->numErrors, 0); + EXPECT_NE(clientHandle2, 0); + proxyClientResultDelete(result3); + + const ProxyResult* result4 = nullptr; + proxyClientDelete(&result4, clientHandle2); + EXPECT_EQ(result4->numErrors, 0); + proxyClientResultDelete(result4); +} + TEST_F(StreamrProxyClientTest, InvalidEthereumAddress) { const ProxyResult* result = nullptr;