Skip to content

Commit e6104b4

Browse files
ViralBShahclaude
andauthored
Add optional compile-time thread-safety for the forwarding API (#179)
* Add optional compile-time thread-safety for the forwarding API The `lbt_*` mutation API (`lbt_forward()`, `lbt_set_forward()`, `lbt_set_forward_by_index()`) is documented as thread-unsafe: it mutates global forwarding tables with no locking. This adds an opt-in process-global lock, enabled with `make LBT_THREADSAFE=1`, that serializes those mutators so racing initializers cannot corrupt the tables. - New `lbt_lock()`/`lbt_unlock()` helpers: a `pthread_mutex_t` (static initializer) elsewhere, a `CRITICAL_SECTION` (initialized in `DllMain`) on Windows. When `LBT_THREADSAFE` is undefined they compile to no-ops, so the default build is byte-for-byte equivalent in behavior to before. - The public mutators now wrap unlocked `*_impl` workers; internal callers (`lbt_forward_impl`, the constructor) call the workers directly to avoid re-entrant locking. - `Make.inc` gains the `LBT_THREADSAFE` toggle (default 0), adding `-pthread` on non-Windows when enabled. Scope note: only the mutators are locked; read-only accessors (`lbt_get_config()`, `lbt_get_forward()`) remain unlocked, so callers must still avoid racing reads against concurrent reconfiguration. Verified: builds with and without the flag; public symbols still exported and the `*_impl` workers stay internal; a dlopen smoke test exercises the lock path without deadlock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Exercise the LBT_THREADSAFE build in the test suite The locking added in this PR was previously never compiled or run by the tests: `build_libblastrampoline()` always built the default, no-op build. Make the harness build with the optional internal locking compiled in by default, so every backend's tests run against a `-DLBT_THREADSAFE` library and each `lbt_forward()`/`lbt_set_forward()` goes through `lbt_lock()`/`lbt_unlock()` across the whole existing CI matrix (all OSes/interfaces). The value is read from the `LBT_THREADSAFE` env var, so `LBT_THREADSAFE=0` still tests the plain build that ships by default. Verified locally: the `direct` backend passes (74/74) with the library built `-DLBT_THREADSAFE -pthread`, both with the env var set and via the new default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * README: document the LBT_THREADSAFE build option Mention `make LBT_THREADSAFE=1` in the thread-safety note: it compiles in a process-global lock around the mutating API (pthread mutex / CRITICAL_SECTION), is off by default, and guards mutators only (readers and call forwarding stay lock-free). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * README: make the threading note a concise section Promote the thread-safety note to a `### Threading` section and trim it to the essentials: thread-unsafe by default, `make LBT_THREADSAFE=1` adds a lock around the mutating API, readers/forwarding stay lock-free. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7bb5f53 commit e6104b4

4 files changed

Lines changed: 83 additions & 10 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ This support is only available on the `x86_64` and `i686` architectures, however
5353
Vendor-specific APIs such as `openblas_get_num_threads()` are not included in header files or exported from the library.
5454
See the [public header file](src/libblastrampoline.h) for the most up-to-date documentation on the `libblastrampoline` API.
5555

56-
**Note**: all `lbt_*` functions should be considered thread-unsafe.
57-
Do not attempt to load two BLAS libraries on two different threads at the same time.
56+
### Threading
57+
58+
By default, all `lbt_*` functions are thread-unsafe; do not reconfigure forwards from multiple threads at once.
59+
Building with `make LBT_THREADSAFE=1` adds a process-global lock around the mutating API (`lbt_forward()`, `lbt_set_forward()`, `lbt_set_forward_by_index()`).
60+
Readers and the BLAS/LAPACK call forwarding itself stay lock-free, so the model remains "configure under the lock, then use".
5861

5962
### Limitations
6063

src/Make.inc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,18 @@ ifeq ($(OS), Darwin)
104104
SYMBOL_TRIMMING := 1
105105
endif
106106

107+
# Optional internal locking around the (otherwise thread-unsafe) library-loading and
108+
# forwarding API. Off by default to preserve historical behavior; enable with
109+
# `make LBT_THREADSAFE=1`.
110+
LBT_THREADSAFE ?= 0
111+
ifeq ($(LBT_THREADSAFE),1)
112+
LBT_CFLAGS += -DLBT_THREADSAFE
113+
ifneq ($(OS),WINNT)
114+
LBT_CFLAGS += -pthread
115+
LBT_LDFLAGS += -pthread
116+
endif
117+
endif
118+
107119
ifeq ($(VERBOSE),0)
108120
ENDCOLOR := "\033[0m"
109121
CCCOLOR := "\033[34m"

src/libblastrampoline.c

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,35 @@
99
#define DEEPBINDLESS_INTERFACE_ILP64_LOADED 0x02
1010
uint8_t deepbindless_interfaces_loaded = 0x00;
1111

12+
/*
13+
* Optional process-global lock around the state-mutating API (`lbt_forward()`,
14+
* `lbt_set_forward()`, `lbt_set_forward_by_index()`). Historically these are
15+
* documented as thread-unsafe; defining `LBT_THREADSAFE` at compile time wraps
16+
* them in a single lock so that concurrent initializers cannot corrupt the
17+
* forwarding tables. When disabled, `lbt_lock()`/`lbt_unlock()` compile to
18+
* no-ops, preserving the previous behavior exactly.
19+
*
20+
* NB: this guards the *mutators* only. The read-only accessors (e.g.
21+
* `lbt_get_config()`, `lbt_get_forward()`) are intentionally left unlocked,
22+
* so callers must still avoid racing reads against concurrent reconfiguration.
23+
*/
24+
#ifdef LBT_THREADSAFE
25+
#ifdef _OS_WINDOWS_
26+
static CRITICAL_SECTION lbt_global_lock;
27+
// Initialized in `DllMain()` (DLL_PROCESS_ATTACH) before any forwarding happens.
28+
static inline void lbt_lock(void) { EnterCriticalSection(&lbt_global_lock); }
29+
static inline void lbt_unlock(void) { LeaveCriticalSection(&lbt_global_lock); }
30+
#else
31+
#include <pthread.h>
32+
static pthread_mutex_t lbt_global_lock = PTHREAD_MUTEX_INITIALIZER;
33+
static inline void lbt_lock(void) { pthread_mutex_lock(&lbt_global_lock); }
34+
static inline void lbt_unlock(void) { pthread_mutex_unlock(&lbt_global_lock); }
35+
#endif
36+
#else
37+
static inline void lbt_lock(void) { }
38+
static inline void lbt_unlock(void) { }
39+
#endif
40+
1241

1342
int32_t find_symbol_idx(const char * name) {
1443
for (int32_t symbol_idx=0; exported_func_names[symbol_idx] != NULL; ++symbol_idx) {
@@ -80,9 +109,10 @@ LBT_DLLEXPORT void lbt_set_default_func(const void * addr) {
80109
}
81110

82111
/*
83-
* Force a forward to a particular value.
112+
* Force a forward to a particular value. Internal, unlocked worker; callers that
113+
* already hold (or intentionally forgo) `lbt_global_lock` use this directly.
84114
*/
85-
LBT_DLLEXPORT int32_t lbt_set_forward_by_index(int32_t symbol_idx, const void * addr, int32_t interface, int32_t complex_retstyle, int32_t f2c, int32_t verbose) {
115+
static int32_t set_forward_by_index_impl(int32_t symbol_idx, const void * addr, int32_t interface, int32_t complex_retstyle, int32_t f2c, int32_t verbose) {
86116
// Quit out immediately if this is not a interface setting
87117
if (interface != LBT_INTERFACE_LP64 && interface != LBT_INTERFACE_ILP64) {
88118
return -1;
@@ -163,6 +193,13 @@ LBT_DLLEXPORT int32_t lbt_set_forward_by_index(int32_t symbol_idx, const void *
163193
return 0;
164194
}
165195

196+
LBT_DLLEXPORT int32_t lbt_set_forward_by_index(int32_t symbol_idx, const void * addr, int32_t interface, int32_t complex_retstyle, int32_t f2c, int32_t verbose) {
197+
lbt_lock();
198+
int32_t ret = set_forward_by_index_impl(symbol_idx, addr, interface, complex_retstyle, f2c, verbose);
199+
lbt_unlock();
200+
return ret;
201+
}
202+
166203
LBT_DLLEXPORT const void * lbt_get_forward(const char * symbol_name, int32_t interface, int32_t f2c) {
167204
// Search symbol list for `symbol_name``
168205
int32_t symbol_idx = find_symbol_idx(symbol_name);
@@ -202,22 +239,25 @@ LBT_DLLEXPORT const void * lbt_get_forward(const char * symbol_name, int32_t int
202239
}
203240

204241
LBT_DLLEXPORT int32_t lbt_set_forward(const char * symbol_name, const void * addr, int32_t interface, int32_t complex_retstyle, int32_t f2c, int32_t verbose) {
205-
// Search symbol list for `symbol_name`, then sub off to `set_forward_by_index()`
242+
// Search symbol list for `symbol_name`, then sub off to `set_forward_by_index_impl()`
206243
int32_t symbol_idx = find_symbol_idx(symbol_name);
207244
if (symbol_idx == -1)
208245
return -1;
209246

210-
int32_t ret = lbt_set_forward_by_index(symbol_idx, addr, interface, complex_retstyle, f2c, verbose);
247+
lbt_lock();
248+
int32_t ret = set_forward_by_index_impl(symbol_idx, addr, interface, complex_retstyle, f2c, verbose);
211249
if (ret == 0) {
212250
// Un-mark this symbol as being provided by any of our libraries;
213251
// if you use the footgun API, you can keep track of who is providing what.
214252
clear_forwarding_mark(symbol_idx, interface);
215253
}
254+
lbt_unlock();
216255
return ret;
217256
}
218257

219258
// Load `libname`, clearing previous mappings if `clear` is set.
220-
LBT_DLLEXPORT int32_t lbt_forward(const char * libname, int32_t clear, int32_t verbose, const char * suffix_hint) {
259+
// Internal, unlocked worker; the public `lbt_forward()` wraps this under `lbt_global_lock`.
260+
static int32_t lbt_forward_impl(const char * libname, int32_t clear, int32_t verbose, const char * suffix_hint) {
221261
if (verbose) {
222262
printf("Generating forwards to %s (clear: %d, verbose: %d, suffix_hint: '%s')\n", libname, clear, verbose, suffix_hint);
223263
}
@@ -406,7 +446,7 @@ LBT_DLLEXPORT int32_t lbt_forward(const char * libname, int32_t clear, int32_t v
406446
}
407447

408448
if (addr != NULL && addr != self_symbol_addr) {
409-
lbt_set_forward_by_index(symbol_idx, addr, interface, complex_retstyle, f2c, verbose);
449+
set_forward_by_index_impl(symbol_idx, addr, interface, complex_retstyle, f2c, verbose);
410450
LBT_BITFIELD_SET(forwards, symbol_idx);
411451
nforwards++;
412452
}
@@ -443,6 +483,13 @@ LBT_DLLEXPORT int32_t lbt_forward(const char * libname, int32_t clear, int32_t v
443483
return nforwards;
444484
}
445485

486+
LBT_DLLEXPORT int32_t lbt_forward(const char * libname, int32_t clear, int32_t verbose, const char * suffix_hint) {
487+
lbt_lock();
488+
int32_t ret = lbt_forward_impl(libname, clear, verbose, suffix_hint);
489+
lbt_unlock();
490+
return ret;
491+
}
492+
446493
/*
447494
* On windows it's surprisingly difficult to get a handle to ourselves,
448495
* and that's because they give it to you in `DllMain()`. ;)
@@ -452,6 +499,10 @@ void * _win32_self_handle;
452499
BOOL APIENTRY DllMain(HINSTANCE hModule, DWORD code, void *reserved) {
453500
if (code == DLL_PROCESS_ATTACH) {
454501
_win32_self_handle = (void *)hModule;
502+
#ifdef LBT_THREADSAFE
503+
// Set up the global lock before anyone can call into the forwarding API.
504+
InitializeCriticalSection(&lbt_global_lock);
505+
#endif
455506
} else {
456507
// We do not want to run our initialization more than once per process.
457508
return TRUE;
@@ -537,7 +588,9 @@ __attribute__((constructor)) void init(void) {
537588
curr_lib_start++;
538589

539590
// Load functions from this library, clearing only the first time.
540-
lbt_forward(curr_lib, clear, verbose, suffix_hint);
591+
// We run inside the constructor (single-threaded), so call the unlocked
592+
// worker directly rather than re-entering the public locking wrapper.
593+
lbt_forward_impl(curr_lib, clear, verbose, suffix_hint);
541594
clear = 0;
542595
}
543596
}

test/utils.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,16 @@ function build_libblastrampoline()
9595
end
9696

9797
cflags_add = "-Werror" * (needs_m32() ? " -m32" : "")
98+
# Build with the optional internal locking compiled in by default, so the
99+
# `LBT_THREADSAFE` code paths (pthread / CRITICAL_SECTION) are exercised across the
100+
# whole test matrix. Set `LBT_THREADSAFE=0` in the environment to instead test the
101+
# plain (no-op lock) build that ships by default.
102+
threadsafe = get(ENV, "LBT_THREADSAFE", "1")
98103
dir = mktempdir()
99104
srcdir = joinpath(dirname(@__DIR__), "src")
100105
global blastrampoline_build_dir = joinpath(dir, "output")
101106
run(`$(make) -sC $(pathesc(srcdir)) CFLAGS="$(cflags_add)" ARCH=$(Sys.ARCH) clean`)
102-
run(`$(make) -sC $(pathesc(srcdir)) CFLAGS="$(cflags_add)" ARCH=$(Sys.ARCH) install builddir=$(pathesc(dir))/build prefix=$(pathesc(blastrampoline_build_dir))`)
107+
run(`$(make) -sC $(pathesc(srcdir)) CFLAGS="$(cflags_add)" ARCH=$(Sys.ARCH) LBT_THREADSAFE=$(threadsafe) install builddir=$(pathesc(dir))/build prefix=$(pathesc(blastrampoline_build_dir))`)
103108

104109
link_name = blastrampoline_dev_link_name
105110
cp(

0 commit comments

Comments
 (0)