diff --git a/.github/workflows/mingw-openssl-repro.yml b/.github/workflows/mingw-openssl-repro.yml new file mode 100644 index 0000000000..4eddabf938 --- /dev/null +++ b/.github/workflows/mingw-openssl-repro.yml @@ -0,0 +1,57 @@ +# Standalone reproducer for MinGW + OpenSSL heap corruption. +# No Glaze dependency — just ASIO + OpenSSL + MinGW. +# +# Copy this file to .github/workflows/ in any repo to reproduce. +# Or run locally on MSYS2 MINGW64: +# pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-gcc mingw-w64-x86_64-openssl +# cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -B build +# cmake --build build +# ./build/repro + +name: mingw-openssl-heap-repro + +on: + push: + branches: + - debug/* + - msys2-mingw-ssl + workflow_dispatch: + +jobs: + repro: + name: MinGW + OpenSSL heap corruption + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + + steps: + - uses: actions/checkout@v6 + + - uses: msys2/setup-msys2@v2 + with: + update: true + msystem: MINGW64 + install: >- + mingw-w64-x86_64-cmake + mingw-w64-x86_64-ninja + mingw-w64-x86_64-gcc + mingw-w64-x86_64-openssl + + - name: Build + working-directory: tests/networking_tests/mingw_ssl_diag/standalone_repro + run: | + cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -B build + cmake --build build + + - name: Run (attempt 1) + working-directory: tests/networking_tests/mingw_ssl_diag/standalone_repro/build + run: ./repro || echo "CRASHED with exit code $?" + + - name: Run (attempt 2) + working-directory: tests/networking_tests/mingw_ssl_diag/standalone_repro/build + run: ./repro || echo "CRASHED with exit code $?" + + - name: Run (attempt 3) + working-directory: tests/networking_tests/mingw_ssl_diag/standalone_repro/build + run: ./repro || echo "CRASHED with exit code $?" diff --git a/.github/workflows/msys2-ssl.yml b/.github/workflows/msys2-ssl.yml new file mode 100644 index 0000000000..9d4178c3f9 --- /dev/null +++ b/.github/workflows/msys2-ssl.yml @@ -0,0 +1,70 @@ +name: msys2-mingw-ssl + +# Tracks MinGW + OpenSSL heap corruption (see docs/networking/mingw-ssl-heap-corruption.md). +# Known issue: OpenSSL linked with MinGW/GCC causes intermittent heap corruption +# during ASIO worker thread shutdown. This is NOT a Glaze bug. +# This workflow exists to detect if the issue is resolved upstream. + +on: + push: + branches: + - main + - feature/* + - debug/* + paths-ignore: + - '**/*.md' + - 'docs/**' + pull_request: + branches: + - main + paths-ignore: + - '**/*.md' + - 'docs/**' + workflow_dispatch: + +jobs: + mingw-ssl: + name: MinGW64 + SSL + runs-on: windows-latest + # Known intermittent failure — don't block PRs + continue-on-error: true + defaults: + run: + shell: msys2 {0} + + steps: + - uses: actions/checkout@v6 + + - uses: msys2/setup-msys2@v2 + with: + update: true + msystem: MINGW64 + install: >- + mingw-w64-x86_64-cmake + mingw-w64-x86_64-ninja + mingw-w64-x86_64-gcc + mingw-w64-x86_64-openssl + + - name: Configure CMake + run: | + cmake -S . -B build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=23 \ + -Dglaze_ENABLE_SSL=ON + + - name: Build + run: | + cmake --build build -j $(nproc) --target \ + websocket_client_test \ + websocket_close_test \ + wss_test \ + shared_context_bug_test \ + utf8_test \ + https_test + + - name: Test + working-directory: build + run: | + ctest -C Release --output-on-failure -R \ + "^(websocket_client_test|websocket_close_test|wss_test|shared_context_bug_test|utf8_test|https_test)$" diff --git a/docs/networking/mingw-ssl-heap-corruption.md b/docs/networking/mingw-ssl-heap-corruption.md new file mode 100644 index 0000000000..32ea171fb1 --- /dev/null +++ b/docs/networking/mingw-ssl-heap-corruption.md @@ -0,0 +1,105 @@ +# MinGW + OpenSSL Heap Corruption + +**Issue**: [#2411](https://github.com/stephenberry/glaze/pull/2411), [#2448](https://github.com/stephenberry/glaze/pull/2448) +**Date**: 2026-04-07 through 2026-04-11 +**Status**: Root cause identified — OpenSSL + MinGW runtime interaction bug (not a Glaze bug) + +## Summary + +Heap corruption (`0xc0000374` STATUS_HEAP_CORRUPTION) occurs intermittently on MinGW/GCC 15.2.0 + Windows when OpenSSL is linked, during `http_server::stop()` → `thread.join()`. The crash happens after an ASIO worker thread has cleanly exited `io_context::run()`, during thread teardown. + +**This is not a Glaze bug.** The crash occurs purely from linking OpenSSL libraries — even when `GLZ_ENABLE_SSL` is not defined, no SSL headers are included, and no SSL code is compiled. MSVC builds are unaffected. + +## Root Cause + +OpenSSL's DLL initialization or its interaction with MinGW's threading/heap infrastructure corrupts the heap when ASIO worker threads exit after handling connections. + +### Evidence (A/B/C test) + +| Test | GLZ_ENABLE_SSL | OpenSSL linked | Crashes? | +|------|---------------|----------------|----------| +| ssl-enabled | YES | dynamic | YES | +| link-only | **NO** | dynamic | **YES** | +| static-ssl | YES | static | YES | +| msys2 (no OpenSSL) | NO | **not linked** | **NO** | + +The `link-only` test compiles with `glaze_ENABLE_SSL=OFF` at the CMake level — no SSL headers, no SSL types, no `GLZ_ENABLE_SSL` macro. The only difference from the passing `msys2` workflow is that OpenSSL libraries are linked. This is sufficient to cause the crash. + +### Crash characteristics + +- Occurs during `std::thread::join()` in `http_server::stop()` +- `HeapValidate(GetProcessHeap(), 0, NULL)` passes on the main thread immediately before `stop()` +- `HeapValidate` passes on the worker thread right after `io_context::run()` returns +- The corruption happens between the worker's function body returning and `join()` completing — during thread teardown (TLS destructors, CRT cleanup) +- Only occurs when the server has handled at least one HTTP connection +- Intermittent: ~50-70% reproduction rate per CI run +- GDB and Page Heap both mask the issue by changing timing/memory layout +- Build optimization level irrelevant (`-O0` still crashes) + +## Reproduction + +- **Platform**: Windows (GitHub Actions `windows-latest`), MSYS2 MINGW64 +- **Compiler**: GCC 15.2.0 (mingw-w64) +- **OpenSSL**: 3.6.2 (both dynamic and static) +- **ASIO**: 1.36.0 (standalone) + +### Minimal reproducer + +```cpp +// Link against OpenSSL (even without GLZ_ENABLE_SSL defined) +#include "glaze/net/http_server.hpp" + +int main() { + for (int round = 0; round < 20; ++round) { + glz::http_server<> server; + server.get("/ping", [](const glz::request&, glz::response& res) { + res.status(200).body("pong"); + }); + server.bind("127.0.0.1", 19300); + std::thread t([&]() { server.start(1); }); + + // Wait for server, then send one GET request with Connection: close + // ... + + server.stop(); // crash during thread.join() inside stop() + t.join(); + } +} +``` + +## Ruled out hypotheses + +| Hypothesis | Test | Result | +|---|---|---| +| `GLZ_ENABLE_SSL` macro / template changes | Built with `glaze_ENABLE_SSL=OFF` + OpenSSL linked | Still crashes | +| DLL boundary / CRT heap mismatch | Static OpenSSL linking | Still crashes | +| GCC optimizer miscompilation | Build with `-O0` | Still crashes | +| `http_server` struct layout change | Removed `ssl_context` member via base class | Still crashes | +| Pending handlers during io_context destruction | Added `restart()` + `poll()` drain | Still crashes | +| OpenSSL TLS cleanup on thread exit | Added `OPENSSL_thread_stop()` | Still crashes | +| Complex test infrastructure | Minimal reproducer (bare server + raw TCP) | Still crashes | +| ASIO misuse | Analyzed ASIO source — destruction semantics are correct | N/A | + +## Recommendations + +1. **Do not use OpenSSL with MinGW/GCC on Windows for production** until this is resolved upstream +2. **MSVC builds are unaffected** and should be used for Windows + SSL deployments +3. The MinGW SSL CI workflow (`msys2-ssl.yml`) documents and tracks this issue +4. Consider reporting to [MSYS2](https://github.com/msys2/MSYS2-packages/issues) and/or [OpenSSL](https://github.com/openssl/openssl/issues) + +## ASIO analysis (for reference) + +Analysis of ASIO 1.36.0 source confirmed that ASIO's io_context destruction semantics are correct: + +1. `shutdown_services()` closes all sockets via `close_for_destruction()` +2. Pending handlers are destroyed (not invoked) via `op->destroy()` +3. Services are deleted + +The drain pattern (`restart()` + `poll()` after joining threads) is still good practice to ensure clean shutdown, even though it doesn't fix this particular issue: + +```cpp +io_context->stop(); +join_worker_threads(); +io_context->restart(); +io_context->poll(); +``` diff --git a/include/glaze/net/http_client.hpp b/include/glaze/net/http_client.hpp index 199557b134..b5ba4bf7cc 100644 --- a/include/glaze/net/http_client.hpp +++ b/include/glaze/net/http_client.hpp @@ -1015,6 +1015,21 @@ namespace glz thread.join(); } } + + // Drain any pending completion handlers so they are destroyed now, + // while the io_context and connection_pool are still alive. + // Without this, pending handlers (holding shared_ptrs to sockets) + // are destroyed inside the io_context destructor. If a handler holds + // the last reference to an SSL socket, its destructor runs mid + // io_context teardown — undefined behavior that causes heap corruption + // on MinGW/GCC with Windows (0xc0000374). + async_io_context->restart(); + async_io_context->poll(); + + // Release pooled connections (and their sockets) while the io_context + // is still valid. Socket destructors may need to deregister from the + // io_context's reactor/IOCP. + connection_pool.reset(); } std::shared_ptr perform_stream_request( diff --git a/include/glaze/net/http_server.hpp b/include/glaze/net/http_server.hpp index d0febccdd0..d7f4cb48a8 100644 --- a/include/glaze/net/http_server.hpp +++ b/include/glaze/net/http_server.hpp @@ -519,9 +519,29 @@ namespace glz size_t max_request_body_size = http_default_max_body_size; }; + // Conditionally holds the SSL context only for TLS-enabled servers. + // Using a base class ensures http_server has no SSL members at all, + // which avoids instantiating unique_ptr destructor + // and prevents heap corruption on MinGW/GCC (#2411). +#ifdef GLZ_ENABLE_SSL + template + struct ssl_context_holder + {}; + + template <> + struct ssl_context_holder + { + std::unique_ptr ssl_context; + }; +#else + template + struct ssl_context_holder + {}; +#endif + // Server implementation using non-blocking asio with WebSocket support template - struct http_server + struct http_server : ssl_context_holder { // Socket type abstraction using socket_type = std::conditional_t(asio::ssl::context::tlsv12); - ssl_context->set_default_verify_paths(); + this->ssl_context = std::make_unique(asio::ssl::context::tlsv12); + this->ssl_context->set_default_verify_paths(); #else static_assert(!EnableTLS, "TLS support requires GLZ_ENABLE_SSL to be defined and OpenSSL to be available"); #endif @@ -796,7 +816,6 @@ namespace glz for (size_t i = 0; i < actual_threads; ++i) { threads.emplace_back([this] { io_context->run(); - // Don't report errors during shutdown }); } } @@ -1256,8 +1275,8 @@ namespace glz { if constexpr (EnableTLS) { #ifdef GLZ_ENABLE_SSL - ssl_context->use_certificate_chain_file(cert_file); - ssl_context->use_private_key_file(key_file, asio::ssl::context::pem); + this->ssl_context->use_certificate_chain_file(cert_file); + this->ssl_context->use_private_key_file(key_file, asio::ssl::context::pem); #endif } else { @@ -1277,7 +1296,7 @@ namespace glz { if constexpr (EnableTLS) { #ifdef GLZ_ENABLE_SSL - ssl_context->set_verify_mode(mode); + this->ssl_context->set_verify_mode(mode); #endif } return *this; @@ -1471,9 +1490,9 @@ namespace glz std::condition_variable shutdown_cv; std::mutex shutdown_mutex; -#ifdef GLZ_ENABLE_SSL - std::conditional_t, std::monostate> ssl_context; -#endif + + // ssl_context is inherited from ssl_context_holder + // and only exists when EnableTLS=true && GLZ_ENABLE_SSL is defined. inline void do_accept() { @@ -1488,7 +1507,7 @@ namespace glz if constexpr (EnableTLS) { #ifdef GLZ_ENABLE_SSL // For HTTPS: create connection eagerly, then perform SSL handshake - auto conn = std::make_shared(socket_type(std::move(socket), *ssl_context), + auto conn = std::make_shared(socket_type(std::move(socket), *this->ssl_context), remote_endpoint); conn->socket.async_handshake(asio::ssl::stream_base::server, [this, conn](std::error_code handshake_ec) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 796b82df90..9164f53732 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -76,6 +76,12 @@ else() endif() endif() +# ASIO requires Winsock libraries on Windows. +# MSVC auto-links via #pragma comment(lib, ...) but MinGW requires explicit linking. +if(WIN32) + target_link_libraries(glz_asio INTERFACE ws2_32 mswsock) +endif() + include(../cmake/code-coverage.cmake) add_code_coverage_all_targets() diff --git a/tests/networking_tests/CMakeLists.txt b/tests/networking_tests/CMakeLists.txt index 6467671d34..6a65db9aa9 100644 --- a/tests/networking_tests/CMakeLists.txt +++ b/tests/networking_tests/CMakeLists.txt @@ -29,3 +29,4 @@ add_subdirectory(registry_view_test) add_subdirectory(repe_to_jsonrpc_test) add_subdirectory(rest_test) add_subdirectory(websocket_test) +add_subdirectory(mingw_ssl_diag) diff --git a/tests/networking_tests/mingw_ssl_diag/CMakeLists.txt b/tests/networking_tests/mingw_ssl_diag/CMakeLists.txt new file mode 100644 index 0000000000..150a7aeb89 --- /dev/null +++ b/tests/networking_tests/mingw_ssl_diag/CMakeLists.txt @@ -0,0 +1,43 @@ +project(mingw_ssl_diag) + +find_package(OpenSSL QUIET) +if(OpenSSL_FOUND) + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_LIBRARIES OpenSSL::SSL OpenSSL::Crypto) + check_cxx_source_compiles(" + #include + int main() { return 0; } + " MINGW_SSL_DIAG_HEADERS_OK) + + if(MINGW_SSL_DIAG_HEADERS_OK) + # Test 1: Full SSL support (GLZ_ENABLE_SSL defined + OpenSSL linked) + add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp) + + target_link_libraries(${PROJECT_NAME} PRIVATE + glz_test_exceptions + OpenSSL::SSL + OpenSSL::Crypto + ) + + target_compile_definitions(${PROJECT_NAME} PRIVATE GLZ_ENABLE_SSL) + + add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) + + # Test 2: OpenSSL DLLs linked but GLZ_ENABLE_SSL NOT defined. + # Must be built with glaze_ENABLE_SSL=OFF at CMake level so the + # glaze::glaze interface doesn't propagate GLZ_ENABLE_SSL. + add_executable(mingw_link_only_test mingw_link_only_test.cpp) + + target_link_libraries(mingw_link_only_test PRIVATE + glz_test_exceptions + OpenSSL::SSL + OpenSSL::Crypto + ) + + add_test(NAME mingw_link_only_test COMMAND mingw_link_only_test) + else() + message(STATUS "mingw_ssl_diag skipped - OpenSSL headers not usable") + endif() +else() + message(STATUS "mingw_ssl_diag skipped - OpenSSL not found") +endif() diff --git a/tests/networking_tests/mingw_ssl_diag/mingw_link_only_test.cpp b/tests/networking_tests/mingw_ssl_diag/mingw_link_only_test.cpp new file mode 100644 index 0000000000..59835e241d --- /dev/null +++ b/tests/networking_tests/mingw_ssl_diag/mingw_link_only_test.cpp @@ -0,0 +1,108 @@ +// Test: link OpenSSL DLLs but do NOT define GLZ_ENABLE_SSL. +// If this crashes, the mere loading of OpenSSL DLLs causes the issue. +// If this passes, the GLZ_ENABLE_SSL macro (changing template instantiations) is the cause. +// +// This file must NOT include any SSL headers or define GLZ_ENABLE_SSL. + +#include +#include +#include +#include + +#include "glaze/net/http_client.hpp" +#include "glaze/net/http_server.hpp" +#include "ut/ut.hpp" + +#ifdef DELETE +#undef DELETE +#endif + +using namespace ut; + +#ifdef _WIN32 +#include +inline void check_heap_link(const char* label) +{ + BOOL ok = HeapValidate(GetProcessHeap(), 0, NULL); + std::fprintf(stderr, "[HEAP] %s: %s\n", label, ok ? "OK" : "CORRUPTED"); +} +#else +inline void check_heap_link(const char*) {} +#endif + +suite link_only_test = [] { + "server_get_stop_no_ssl_macro"_test = [] { + for (int round = 0; round < 20; ++round) { + std::fprintf(stderr, "[LINK_ONLY] round %d\n", round); + + glz::http_server<> server; + server.get("/ping", [](const glz::request&, glz::response& res) { + res.status(200).body("pong"); + }); + + uint16_t port = 0; + for (uint16_t p = 19400; p < 19500; ++p) { + try { + server.bind("127.0.0.1", p); + port = p; + break; + } + catch (...) { + continue; + } + } + expect(port > 0) << "Failed to bind"; + + std::thread server_thread([&]() { server.start(1); }); + + // Wait for server + for (int i = 0; i < 50; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + try { + asio::io_context io; + asio::ip::tcp::socket sock(io); + sock.connect({asio::ip::make_address("127.0.0.1"), port}); + sock.close(); + break; + } + catch (...) { + } + } + + // Make one GET request + { + asio::io_context io; + asio::ip::tcp::socket sock(io); + asio::ip::tcp::resolver resolver(io); + auto endpoints = resolver.resolve("127.0.0.1", std::to_string(port)); + asio::connect(sock, endpoints); + + std::string req = "GET /ping HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"; + asio::write(sock, asio::buffer(req)); + + asio::streambuf response_buf; + asio::error_code ec; + asio::read(sock, response_buf, asio::transfer_all(), ec); + + std::string response{std::istreambuf_iterator(&response_buf), std::istreambuf_iterator()}; + expect(response.find("pong") != std::string::npos) << "Response should contain 'pong'"; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + check_heap_link("before stop"); + server.stop(); + check_heap_link("after stop"); + + if (server_thread.joinable()) { + server_thread.join(); + } + } + std::fprintf(stderr, "[LINK_ONLY] all 20 rounds passed\n"); + }; +}; + +int main() +{ + std::cout << "Link-Only Test (OpenSSL linked, GLZ_ENABLE_SSL NOT defined)" << std::endl; + return 0; +} diff --git a/tests/networking_tests/mingw_ssl_diag/mingw_ssl_diag.cpp b/tests/networking_tests/mingw_ssl_diag/mingw_ssl_diag.cpp new file mode 100644 index 0000000000..7d6415ec68 --- /dev/null +++ b/tests/networking_tests/mingw_ssl_diag/mingw_ssl_diag.cpp @@ -0,0 +1,126 @@ +// Minimal reproducer for MinGW + SSL heap corruption (issue #2411) +// +// Reproduces heap corruption on MinGW/GCC 15.2.0 + Windows when +// GLZ_ENABLE_SSL is defined. The crash occurs during thread.join() +// in http_server::stop() after handling a connection. + +#ifndef GLZ_ENABLE_SSL +#define GLZ_ENABLE_SSL +#endif + +#include +#include +#include +#include + +#include + +#include "glaze/net/http_client.hpp" +#include "glaze/net/http_server.hpp" +#include "ut/ut.hpp" + +#ifdef DELETE +#undef DELETE +#endif + +using namespace ut; + +#ifdef _WIN32 +#include +inline void check_heap(const char* label) +{ + BOOL ok = HeapValidate(GetProcessHeap(), 0, NULL); + std::fprintf(stderr, "[HEAP] %s: %s\n", label, ok ? "OK" : "CORRUPTED"); +} +#else +inline void check_heap(const char*) {} +#endif + +suite mingw_ssl_repro = [] { + // Minimal reproducer: create server, handle one request, stop. + // This mirrors what http_client_test's basic_get_request does. + "minimal_server_get_stop"_test = [] { + for (int round = 0; round < 20; ++round) { + std::fprintf(stderr, "[REPRO] round %d\n", round); + + // Create and start server + glz::http_server<> server; + server.get("/ping", [](const glz::request&, glz::response& res) { + res.status(200).body("pong"); + }); + + uint16_t port = 0; + for (uint16_t p = 19300; p < 19400; ++p) { + try { + server.bind("127.0.0.1", p); + port = p; + break; + } + catch (...) { + continue; + } + } + expect(port > 0) << "Failed to bind"; + + std::thread server_thread([&]() { server.start(1); }); + + // Wait for server + for (int i = 0; i < 50; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + try { + asio::io_context io; + asio::ip::tcp::socket sock(io); + sock.connect({asio::ip::make_address("127.0.0.1"), port}); + sock.close(); + break; + } + catch (...) { + } + } + + // Make one GET request using raw TCP (same as simple_test_client) + { + asio::io_context io; + asio::ip::tcp::socket sock(io); + asio::ip::tcp::resolver resolver(io); + auto endpoints = resolver.resolve("127.0.0.1", std::to_string(port)); + asio::connect(sock, endpoints); + + std::string req = "GET /ping HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"; + asio::write(sock, asio::buffer(req)); + + asio::streambuf response_buf; + asio::error_code ec; + asio::read(sock, response_buf, asio::transfer_all(), ec); + + std::string response{std::istreambuf_iterator(&response_buf), std::istreambuf_iterator()}; + expect(response.find("pong") != std::string::npos) << "Response should contain 'pong'"; + } + + // Small delay then stop — this is where the crash happens + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + check_heap("before stop"); + server.stop(); + check_heap("after stop"); + + if (server_thread.joinable()) { + server_thread.join(); + } + check_heap("after server_thread join"); + } + std::fprintf(stderr, "[REPRO] all 20 rounds passed\n"); + }; +}; + +int main() +{ + std::cout << "MinGW + SSL Minimal Reproducer" << std::endl; +#ifdef __VERSION__ + std::cout << "Compiler: " << __VERSION__ << std::endl; +#elif defined(_MSC_VER) + std::cout << "Compiler: MSVC " << _MSC_VER << std::endl; +#endif + std::cout << "OpenSSL: " << OpenSSL_version(OPENSSL_VERSION) << std::endl; + std::cout << std::endl; + return 0; +} diff --git a/tests/networking_tests/mingw_ssl_diag/standalone_repro/CMakeLists.txt b/tests/networking_tests/mingw_ssl_diag/standalone_repro/CMakeLists.txt new file mode 100644 index 0000000000..f56dd17c66 --- /dev/null +++ b/tests/networking_tests/mingw_ssl_diag/standalone_repro/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.20) +project(mingw_openssl_heap_repro CXX) + +set(CMAKE_CXX_STANDARD 17) + +# Fetch standalone ASIO +include(FetchContent) +FetchContent_Declare( + asio + GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git + GIT_TAG asio-1-36-0 + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(asio) + +find_package(OpenSSL REQUIRED) + +add_executable(repro repro.cpp) +target_include_directories(repro PRIVATE ${asio_SOURCE_DIR}/asio/include) +target_link_libraries(repro PRIVATE OpenSSL::SSL OpenSSL::Crypto) + +if(WIN32) + target_link_libraries(repro PRIVATE ws2_32 mswsock) +endif() + +enable_testing() +add_test(NAME repro COMMAND repro) diff --git a/tests/networking_tests/mingw_ssl_diag/standalone_repro/repro.cpp b/tests/networking_tests/mingw_ssl_diag/standalone_repro/repro.cpp new file mode 100644 index 0000000000..6b0de37320 --- /dev/null +++ b/tests/networking_tests/mingw_ssl_diag/standalone_repro/repro.cpp @@ -0,0 +1,161 @@ +// Minimal reproducer: MinGW + OpenSSL heap corruption +// +// On MSYS2 MINGW64 with GCC 15 + OpenSSL 3.x, linking OpenSSL causes +// intermittent heap corruption (0xc0000374) during std::thread::join() +// after an ASIO worker thread handles a TCP connection. +// +// The crash occurs even though no SSL code is executed — merely linking +// the OpenSSL libraries is sufficient. +// +// Build: +// cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -B build . +// cmake --build build +// +// Run: +// ./build/repro (repeat if it passes — ~50% reproduction rate) + +#include +#include +#include +#include +#include +#include + +// A minimal async TCP server using ASIO +class mini_server +{ + public: + uint16_t start() + { + io_ctx_ = std::make_unique(); + // Port 0 = OS assigns a free port + acceptor_ = std::make_unique( + *io_ctx_, asio::ip::tcp::endpoint(asio::ip::make_address("127.0.0.1"), 0)); + uint16_t port = acceptor_->local_endpoint().port(); + + do_accept(); + worker_ = std::thread([this] { io_ctx_->run(); }); + + return port; + } + + void stop() + { + if (acceptor_) { + asio::error_code ec; + acceptor_->close(ec); + } + if (io_ctx_) io_ctx_->stop(); + if (worker_.joinable()) worker_.join(); // <-- crash here on MinGW + OpenSSL + } + + ~mini_server() { stop(); } + + private: + void do_accept() + { + acceptor_->async_accept([this](asio::error_code ec, asio::ip::tcp::socket socket) { + if (!ec) { + handle_connection(std::make_shared(std::move(socket))); + } + if (acceptor_->is_open()) { + do_accept(); + } + }); + } + + void handle_connection(std::shared_ptr sock) + { + auto buf = std::make_shared(); + asio::async_read_until( + *sock, *buf, "\r\n\r\n", [this, sock, buf](asio::error_code ec, std::size_t) { + if (ec) return; + auto response = std::make_shared( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n" + "\r\n" + "pong"); + asio::async_write(*sock, asio::buffer(*response), + [sock, response](asio::error_code, std::size_t) { + asio::error_code ec; + sock->shutdown(asio::ip::tcp::socket::shutdown_both, ec); + }); + }); + } + + std::unique_ptr io_ctx_; + std::unique_ptr acceptor_; + std::thread worker_; +}; + +// Send a simple HTTP GET and read the response +bool send_get(uint16_t port) +{ + try { + asio::io_context io; + asio::ip::tcp::socket sock(io); + sock.connect({asio::ip::make_address("127.0.0.1"), port}); + + std::string req = "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"; + asio::write(sock, asio::buffer(req)); + + asio::streambuf buf; + asio::error_code ec; + asio::read(sock, buf, asio::transfer_all(), ec); + + std::string response{std::istreambuf_iterator(&buf), std::istreambuf_iterator()}; + return response.find("pong") != std::string::npos; + } + catch (const std::exception& e) { + std::fprintf(stderr, "GET error: %s\n", e.what()); + return false; + } +} + +int main() +{ + std::fprintf(stderr, "MinGW + OpenSSL heap corruption reproducer\n"); + std::fprintf(stderr, "Repeat 20 rounds of: start server, GET, stop server\n\n"); + + for (int i = 0; i < 20; ++i) { + std::fprintf(stderr, "round %d... ", i); + + mini_server server; + uint16_t port = server.start(); + + // Wait for server to be ready + bool ready = false; + for (int attempt = 0; attempt < 50; ++attempt) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + try { + asio::io_context io; + asio::ip::tcp::socket sock(io); + sock.connect({asio::ip::make_address("127.0.0.1"), port}); + sock.close(); + ready = true; + break; + } + catch (...) { + } + } + + if (!ready) { + std::fprintf(stderr, "FAILED (server not ready on port %d)\n", port); + return 1; + } + + if (!send_get(port)) { + std::fprintf(stderr, "FAILED (GET failed on port %d)\n", port); + return 1; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + server.stop(); + + std::fprintf(stderr, "OK\n"); + } + + std::fprintf(stderr, "\nAll 20 rounds passed.\n"); + return 0; +} diff --git a/tests/networking_tests/mingw_ssl_diag/standalone_repro/repro.yml b/tests/networking_tests/mingw_ssl_diag/standalone_repro/repro.yml new file mode 100644 index 0000000000..3fbf98af4e --- /dev/null +++ b/tests/networking_tests/mingw_ssl_diag/standalone_repro/repro.yml @@ -0,0 +1,52 @@ +# Standalone reproducer for MinGW + OpenSSL heap corruption. +# No Glaze dependency — just ASIO + OpenSSL + MinGW. +# +# Copy this file to .github/workflows/ in any repo to reproduce. +# Or run locally on MSYS2 MINGW64: +# pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-gcc mingw-w64-x86_64-openssl +# cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -B build +# cmake --build build +# ./build/repro + +name: mingw-openssl-heap-repro + +on: workflow_dispatch + +jobs: + repro: + name: MinGW + OpenSSL heap corruption + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + + steps: + - uses: actions/checkout@v6 + + - uses: msys2/setup-msys2@v2 + with: + update: true + msystem: MINGW64 + install: >- + mingw-w64-x86_64-cmake + mingw-w64-x86_64-ninja + mingw-w64-x86_64-gcc + mingw-w64-x86_64-openssl + + - name: Build + working-directory: tests/networking_tests/mingw_ssl_diag/standalone_repro + run: | + cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -B build + cmake --build build + + - name: Run (attempt 1) + working-directory: tests/networking_tests/mingw_ssl_diag/standalone_repro/build + run: ./repro || echo "CRASHED with exit code $?" + + - name: Run (attempt 2) + working-directory: tests/networking_tests/mingw_ssl_diag/standalone_repro/build + run: ./repro || echo "CRASHED with exit code $?" + + - name: Run (attempt 3) + working-directory: tests/networking_tests/mingw_ssl_diag/standalone_repro/build + run: ./repro || echo "CRASHED with exit code $?"