Skip to content

Commit cdec41d

Browse files
committed
fix(sys): use THREAD_LOCAL TLS for mimalloc v3 on Apple
1 parent ec2fc80 commit cdec41d

1 file changed

Lines changed: 80 additions & 26 deletions

File tree

libmimalloc-sys/build.rs

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,82 @@ use std::env;
22

33
use cmake::Config;
44

5+
/// Patch the vendored mimalloc v3 `prim.h` so that Apple targets select
6+
/// `MI_TLS_MODEL_THREAD_LOCAL` with `MI_TLS_RECURSE_GUARD` instead of the
7+
/// upstream default `MI_TLS_MODEL_FIXED_SLOT`.
8+
///
9+
/// Background:
10+
/// - FIXED_SLOT (default) hardcodes the per-thread `theap` pointer at
11+
/// TCB[108]/[109]. When multiple statically-linked instances of
12+
/// mimalloc-safe live in the same process (e.g. multiple Node.js
13+
/// napi addons), every instance reads and writes the same slot on
14+
/// any thread that calls into more than one of them (the main JS
15+
/// thread in particular) — instances overwrite each other's `theap`
16+
/// pointers and corrupt each other's heaps.
17+
/// - The other obvious workaround, `MI_TLS_MODEL_DYNAMIC_PTHREADS`
18+
/// (activated by passing `-DMI_HAS_TLS_SLOT=0`), turned out to have
19+
/// poor macOS testing for long-lived non-tokio threads — notably
20+
/// rayon workers pulled in via `oxc_cfg → oxc_index → rayon` — and
21+
/// caused SIGABRT crashes in rolldown's CLI tests.
22+
///
23+
/// THREAD_LOCAL + RECURSE_GUARD avoids both failure modes:
24+
/// - `__thread` + `mi_decl_hidden` (already how upstream declares
25+
/// `__mi_theap_default` / `__mi_theap_cached` in `init.c`) gives
26+
/// per-image per-thread storage allocated by dyld's TLV system — no
27+
/// FIXED_SLOT sharing across instances.
28+
/// - RECURSE_GUARD uses a non-TLS `_mi_process_is_initialized` bool to
29+
/// short-circuit the fast path before process init completes, so a
30+
/// first `__thread` access triggering dyld's TLV malloc cannot
31+
/// recurse into mimalloc itself.
32+
/// - `MI_HAS_TLS_SLOT` stays at default 1, so the *thread-id* fast
33+
/// path (`prim.h` line ~304) still uses `mi_prim_tls_slot(0)`
34+
/// (Apple's system-defined thread-id TSD slot, shared semantically
35+
/// across all consumers — no conflict).
36+
///
37+
/// Idempotent: detects an already-patched file by a marker comment and
38+
/// returns early. Panics loudly if the upstream selector block can't be
39+
/// located — that means mimalloc changed `prim.h` and this patch needs
40+
/// to be updated. Leaves the submodule's working tree marked dirty
41+
/// after a fresh clone+build; reset with `git submodule update --force`
42+
/// if needed (next build will re-apply the patch).
43+
fn patch_apple_tls_model_for_v3() {
44+
let prim_h = "c_src/mimalloc3/include/mimalloc/prim.h";
45+
let content = std::fs::read_to_string(prim_h).unwrap_or_else(|e| {
46+
panic!(
47+
"could not read {}: {} — is the mimalloc3 submodule initialized?",
48+
prim_h, e
49+
)
50+
});
51+
52+
const MARKER: &str = "mimalloc-safe vendor patch: Apple uses THREAD_LOCAL";
53+
if content.contains(MARKER) {
54+
// Already patched — idempotent fast path.
55+
return;
56+
}
57+
58+
const OLD: &str = "#if defined(_WIN32)\n #define MI_TLS_MODEL_DYNAMIC_WIN32 1 \n#elif defined(__APPLE__) && MI_HAS_TLS_SLOT && !defined(__POWERPC__) // macOS on arm64 or x64\n // #define MI_TLS_MODEL_DYNAMIC_PTHREADS 1 // also works but a bit slower\n #define MI_TLS_MODEL_FIXED_SLOT 1\n #define MI_TLS_MODEL_FIXED_SLOT_DEFAULT 108 // seems unused. @apple: it would be great to get 2 official slots for custom allocators :-)\n #define MI_TLS_MODEL_FIXED_SLOT_CACHED 109\n // we used before __PTK_FRAMEWORK_OLDGC_KEY9 (89) but that seems used now.\n // see <https://github.com/rweichler/substrate/blob/master/include/pthread_machdep.h>\n#elif defined(__APPLE__) || defined(__OpenBSD__) || defined(__ANDROID__)\n #define MI_TLS_MODEL_DYNAMIC_PTHREADS 1\n // #define MI_TLS_MODEL_DYNAMIC_PTHREADS_DEFAULT_ENTRY_IS_NULL 1\n#else\n #define MI_TLS_MODEL_THREAD_LOCAL 1\n#endif";
59+
60+
const NEW: &str = "#if defined(_WIN32)\n #define MI_TLS_MODEL_DYNAMIC_WIN32 1\n// --- mimalloc-safe vendor patch: Apple uses THREAD_LOCAL + RECURSE_GUARD ---\n// See libmimalloc-sys/build.rs::patch_apple_tls_model_for_v3 for rationale.\n#elif defined(__APPLE__) && !defined(__POWERPC__) // macOS on arm64 or x64\n #define MI_TLS_MODEL_THREAD_LOCAL 1\n #ifndef MI_TLS_RECURSE_GUARD\n #define MI_TLS_RECURSE_GUARD 1\n #endif\n// --- end mimalloc-safe vendor patch ---\n#elif defined(__OpenBSD__) || defined(__ANDROID__)\n #define MI_TLS_MODEL_DYNAMIC_PTHREADS 1\n // #define MI_TLS_MODEL_DYNAMIC_PTHREADS_DEFAULT_ENTRY_IS_NULL 1\n#else\n #define MI_TLS_MODEL_THREAD_LOCAL 1\n#endif";
61+
62+
assert!(
63+
content.contains(OLD),
64+
"could not locate the Apple TLS_MODEL selector block in {}; \
65+
upstream mimalloc may have changed the file structure. \
66+
Update libmimalloc-sys/build.rs::patch_apple_tls_model_for_v3 to match.",
67+
prim_h
68+
);
69+
70+
let patched = content.replace(OLD, NEW);
71+
std::fs::write(prim_h, patched)
72+
.unwrap_or_else(|e| panic!("failed to write patched {}: {}", prim_h, e));
73+
74+
println!(
75+
"cargo:warning=patched mimalloc3 prim.h for Apple THREAD_LOCAL+RECURSE_GUARD \
76+
(multi-instance + rayon-worker safe)"
77+
);
78+
println!("cargo:rerun-if-changed={}", prim_h);
79+
}
80+
581
fn main() {
682
#[cfg(not(feature = "v3"))]
783
let mut cmake_config = Config::new("c_src/mimalloc");
@@ -76,33 +152,11 @@ fn main() {
76152
cmake_config.define("MI_LOCAL_DYNAMIC_TLS", "ON");
77153
}
78154

79-
// On Apple targets, mimalloc v3's `prim.h` selects
80-
// `MI_TLS_MODEL_FIXED_SLOT` and hardcodes the per-thread `theap`
81-
// pointer at TCB[108]/[109], reading/writing the slot directly
82-
// through `tpidrro_el0` (arm64) or `%gs:` (x86_64) rather than
83-
// through `pthread_setspecific`. The slot numbers are not allocated
84-
// via `pthread_key_create`; the same header notes the caveat:
85-
// "This goes wrong though if the OS or a library uses the same
86-
// fixed slot."
87-
//
88-
// When a process loads more than one image that statically links
89-
// mimalloc-safe (e.g. two napi addons in one Node.js process),
90-
// each image has its own mimalloc state but every image's
91-
// `_mi_theap_default()` reads and writes the same TCB[108] on any
92-
// given thread — the instances overwrite each other's pointers.
93-
//
94-
// `-DMI_HAS_TLS_SLOT=0` makes the cascade in `prim.h` skip the
95-
// FIXED_SLOT branch on Apple and fall through to
96-
// `MI_TLS_MODEL_DYNAMIC_PTHREADS`, whose accessors use
97-
// `pthread_{get,set}specific` with a key allocated per image via
98-
// `pthread_key_create`. Different images then use distinct keys and
99-
// no longer share TLS storage. `prim.h` notes this path is "a bit
100-
// slower"; the impact has not been measured here.
101-
//
102-
// Gated on the `v3` feature: v2 has a separate Apple fast path
103-
// controlled by `MI_TLS_SLOT` and is not affected by this define.
155+
// On Apple targets with v3, patch the submodule's prim.h to select
156+
// THREAD_LOCAL + RECURSE_GUARD instead of upstream's FIXED_SLOT.
157+
// See `patch_apple_tls_model_for_v3` above for full rationale.
104158
if target_os == "macos" && env::var_os("CARGO_FEATURE_V3").is_some() {
105-
cmake_config.cflag("-DMI_HAS_TLS_SLOT=0");
159+
patch_apple_tls_model_for_v3();
106160
}
107161

108162
if (target_os == "linux" || target_os == "android")

0 commit comments

Comments
 (0)