Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
83b33e9
msys2-mingw-ssl error replication test
stephenberry Apr 8, 2026
5a56229
Update CMakeLists.txt
stephenberry Apr 8, 2026
f63fbff
Minimal diagnostic test for MinGW + SSL heap corruption
stephenberry Apr 8, 2026
73d1100
Update msys2-ssl.yml
stephenberry Apr 8, 2026
0ad8abc
Update msys2-ssl.yml
stephenberry Apr 8, 2026
b08b8ba
more stress testing
stephenberry Apr 8, 2026
bb44ab5
On job 20 iterations to tease out the issue
stephenberry Apr 8, 2026
894cdf5
Update msys2-ssl.yml
stephenberry Apr 8, 2026
85a3f55
Update msys2-ssl.yml
stephenberry Apr 8, 2026
2b71889
Update msys2-ssl.yml
stephenberry Apr 8, 2026
9ee4791
updates
stephenberry Apr 8, 2026
51e3846
Update msys2-ssl.yml
stephenberry Apr 8, 2026
a2d70b7
better diagnostics
stephenberry Apr 9, 2026
4682dda
Update msys2-ssl.yml
stephenberry Apr 9, 2026
43a5eae
Update msys2-ssl.yml
stephenberry Apr 9, 2026
4ad07d8
Update msys2-ssl.yml
stephenberry Apr 9, 2026
a87c467
Update http_client_test.cpp
stephenberry Apr 9, 2026
499ec41
Update http_client_test.cpp
stephenberry Apr 9, 2026
e036476
Update http_client_test.cpp
stephenberry Apr 9, 2026
7912f79
Update http_client_test.cpp
stephenberry Apr 9, 2026
7916a5f
Update http_client_test.cpp
stephenberry Apr 9, 2026
952ad64
Update http_server.hpp
stephenberry Apr 9, 2026
c980c12
http_server fix
stephenberry Apr 9, 2026
aeccc6a
Update http_server.hpp
stephenberry Apr 10, 2026
f5ecf5c
updates
stephenberry Apr 10, 2026
73ce111
Close all tracked sockets to cancel pending I/O
stephenberry Apr 10, 2026
59688bf
Update http_server.hpp
stephenberry Apr 10, 2026
d49405b
Update http_server.hpp
stephenberry Apr 10, 2026
dcf485a
Update http_client_test.cpp
stephenberry Apr 10, 2026
26eddac
Update http_server.hpp
stephenberry Apr 10, 2026
bcc59c3
Update http_server.hpp
stephenberry Apr 10, 2026
ace39a9
Update mingw_ssl_diag.cpp
stephenberry Apr 10, 2026
051e5ec
Run minimal reproducer before http_client_test in CI
stephenberry Apr 10, 2026
3e09b9d
Add A/B/C tests to isolate MinGW SSL heap corruption cause
stephenberry Apr 10, 2026
3b94e27
Remove ssl_context member from http_server to test crash hypothesis
stephenberry Apr 11, 2026
a6c957f
Move ssl_context to conditional base class for http_server
stephenberry Apr 11, 2026
0c13f3b
Fix link-only test to use glaze_ENABLE_SSL=OFF at CMake level
stephenberry Apr 11, 2026
298938a
Clean up MinGW SSL investigation, document as upstream issue
stephenberry Apr 11, 2026
b13a078
Add standalone MinGW + OpenSSL heap corruption reproducer
stephenberry Apr 13, 2026
74e166a
Add standalone reproducer workflow to .github/workflows/
stephenberry Apr 13, 2026
8ec4c8b
Fix standalone reproducer: use OS-assigned port
stephenberry Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/mingw-openssl-repro.yml
Original file line number Diff line number Diff line change
@@ -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 $?"
70 changes: 70 additions & 0 deletions .github/workflows/msys2-ssl.yml
Original file line number Diff line number Diff line change
@@ -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)$"
105 changes: 105 additions & 0 deletions docs/networking/mingw-ssl-heap-corruption.md
Original file line number Diff line number Diff line change
@@ -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();
```
15 changes: 15 additions & 0 deletions include/glaze/net/http_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<http_stream_connection> perform_stream_request(
Expand Down
41 changes: 30 additions & 11 deletions include/glaze/net/http_server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<false> has no SSL members at all,
// which avoids instantiating unique_ptr<asio::ssl::context> destructor
// and prevents heap corruption on MinGW/GCC (#2411).
#ifdef GLZ_ENABLE_SSL
template <bool EnableTLS>
struct ssl_context_holder
{};

template <>
struct ssl_context_holder<true>
{
std::unique_ptr<asio::ssl::context> ssl_context;
};
#else
template <bool>
struct ssl_context_holder
{};
#endif

// Server implementation using non-blocking asio with WebSocket support
template <bool EnableTLS = false>
struct http_server
struct http_server : ssl_context_holder<EnableTLS>
{
// Socket type abstraction
using socket_type = std::conditional_t<EnableTLS,
Expand Down Expand Up @@ -605,8 +625,8 @@ namespace glz
// Initialize SSL context for TLS-enabled servers
if constexpr (EnableTLS) {
#ifdef GLZ_ENABLE_SSL
ssl_context = std::make_unique<asio::ssl::context>(asio::ssl::context::tlsv12);
ssl_context->set_default_verify_paths();
this->ssl_context = std::make_unique<asio::ssl::context>(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
Expand Down Expand Up @@ -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
});
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -1471,9 +1490,9 @@ namespace glz
std::condition_variable shutdown_cv;
std::mutex shutdown_mutex;

#ifdef GLZ_ENABLE_SSL
std::conditional_t<EnableTLS, std::unique_ptr<asio::ssl::context>, std::monostate> ssl_context;
#endif

// ssl_context is inherited from ssl_context_holder<EnableTLS>
// and only exists when EnableTLS=true && GLZ_ENABLE_SSL is defined.

inline void do_accept()
{
Expand All @@ -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<connection_state>(socket_type(std::move(socket), *ssl_context),
auto conn = std::make_shared<connection_state>(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) {
Expand Down
6 changes: 6 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions tests/networking_tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading
Loading