Skip to content

Commit f8b4e43

Browse files
committed
Reapply "fix(windows): release thread heaps from a TLS detach callback"
This reverts commit f843db8, which removed the #581 fix on a false attribution. The arm64 shards were crashing because of an unaligned SQLite page-cache slab (fixed in #1274, arm64 shard 2/2 green since); removing this callback never changed those crashes, which is what exonerated it. The Windows soak then failed for the obvious reason: the leak was back. Static MinGW links have no DllMain and register no TLS callback, so mi_thread_done never runs and every thread leaks its heap -- 607 heaps after 300 requests holding 170 MiB against a ~300 KiB live set. With the callback the 10-minute Windows soak measured 1.1x against the original 53x. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent 00495a7 commit f8b4e43

1 file changed

Lines changed: 48 additions & 0 deletions

File tree

src/foundation/compat_thread.c

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
#include "foundation/constants.h"
88
#include "foundation/compat_thread.h"
99

10+
#include "foundation/platform.h"
11+
12+
#include <mimalloc.h> /* mi_thread_done at thread exit */
13+
1014
#include <pthread.h>
1115
#include <stdlib.h>
1216

@@ -24,6 +28,50 @@ typedef struct {
2428
void *arg;
2529
} win_thread_arg_t;
2630

31+
/* Release each thread's allocator heap at DLL_THREAD_DETACH.
32+
*
33+
* Doing this from the thread wrapper instead crashes: the wrapper returns
34+
* before the CRT's own thread teardown, and abandoning the heap there raced
35+
* with frees still in flight (daemon_ipc_wait_forever_is_interruptible
36+
* segfaulted, rc=139, reproducibly and only with the release enabled). A TLS
37+
* callback is the mechanism mimalloc itself uses under MSVC and runs after all
38+
* other thread cleanup, which is the only point where the heap is genuinely
39+
* unreferenced.
40+
*
41+
* Without any of this, a static MinGW link -- no DllMain, no TLS callback --
42+
* never releases a thread heap at all: 607 heaps after 300 requests, 170 MiB
43+
* held against a ~300 KiB live set, growing without bound (#581). POSIX gets
44+
* this from a pthread TSD destructor, which is why only Windows leaked.
45+
*
46+
* CBM_MI_THREAD_DONE=0 disables it, so one binary can demonstrate both
47+
* behaviours rather than requiring a rebuild to establish causality. */
48+
static bool thread_release_heap_enabled(void) {
49+
static int state = -1;
50+
if (state < 0) {
51+
char buf[CBM_SZ_16];
52+
state =
53+
(cbm_safe_getenv("CBM_MI_THREAD_DONE", buf, sizeof(buf), NULL) != NULL && buf[0] == '0')
54+
? 0
55+
: 1;
56+
}
57+
return state == 1;
58+
}
59+
60+
static void NTAPI cbm_thread_detach_callback(PVOID handle, DWORD reason, PVOID reserved) {
61+
(void)handle;
62+
(void)reserved;
63+
if (reason == DLL_THREAD_DETACH && thread_release_heap_enabled()) {
64+
mi_thread_done();
65+
}
66+
}
67+
68+
/* Park the callback in .CRT$XLB, the table the loader walks. The linker only
69+
* emits a TLS directory when _tls_used is referenced, so anchor it. */
70+
extern const IMAGE_TLS_DIRECTORY64 _tls_used;
71+
static const void *const cbm_tls_anchor __attribute__((used)) = &_tls_used;
72+
__attribute__((section(".CRT$XLB"), used)) PIMAGE_TLS_CALLBACK cbm_thread_detach_tls_cb =
73+
cbm_thread_detach_callback;
74+
2775
static DWORD WINAPI win_thread_wrapper(LPVOID lpParam) {
2876
win_thread_arg_t *a = (win_thread_arg_t *)lpParam;
2977
void *(*fn)(void *) = a->fn;

0 commit comments

Comments
 (0)