Skip to content

mem_block_cache: fix two thread-safety bugs#271

Open
amaanq wants to merge 2 commits intoboostorg:developfrom
amaanq:fix-mem-block-cache
Open

mem_block_cache: fix two thread-safety bugs#271
amaanq wants to merge 2 commits intoboostorg:developfrom
amaanq:fix-mem-block-cache

Conversation

@amaanq
Copy link

@amaanq amaanq commented Feb 27, 2026

Problem

mem_block_cache has two thread-safety bugs that cause heap corruption and use-after-free when static const boost::regex objects are used from multiple threads. In my case, these were both observed as SIGSEGVs in the nix package manager's garbage collector (#198, #258).

1. TOCTOU race in lock-free destructor

The destructor loads each cache slot twice, once to check for non-null, and then again to pass to operator delete:

if (cache[i].load()) ::operator delete(cache[i].load());

Between the two loads, a concurrent get() can CAS the slot to null and hand the pointer to a caller, leaving the destructor to either double-free or free a live block.

2. Static destruction order fiasco

instance() uses a Meyers singleton (static local), which is destroyed during __cxa_atexit. Detached or late-joining threads that are still calling get()/put() at that point access a destroyed object.

Solution

The first commit replaces the two-load sequence with a single exchange(nullptr) that atomically claims the pointer for destruction.

The second commit switches from static to thread_local, giving each thread its own cache instance that is destroyed when that thread exits rather than at program shutdown. This follows the same pattern used by Beast (prng.ipp) and Asio (config.hpp), guarded by BOOST_NO_CXX11_THREAD_LOCAL for compilers that lack the keyword.

I've also added a regression test (concurrent_static_regex_test.cpp) that exercises regex_match, regex_search, and regex_replace from 24 threads simultaneously.

Note

I did use an LLM to assist me in debugging the issue and finding a way to test the bug.

The destructor loaded each cache slot twice: once to check for non-null,
then again to pass to `operator delete`. Between the two loads, a concurrent
`get()` could CAS the slot to null and hand the pointer to a caller, leaving
the destructor to either double-free or free a live block. This commit replaces
the two-load sequence with a single `exchange(nullptr)` that atomically claims
the pointer for destruction.
… fiasco

The Meyers singleton (`static` local in `instance()`) is destroyed during
`__cxa_atexit` while detached or late-joining threads may still be calling
`get()`/`put()`. Switching to `thread_local` gives each thread its own cache,
destroyed when that thread exits rather than at program shutdown. This matches
the pattern used by Beast (`prng.ipp`) and Asio (`config.hpp`), guarded by
`BOOST_NO_CXX11_THREAD_LOCAL` for compilers that lack the keyword.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Not Issue]boost regex_replace intermittent SIGARBT in boost::shared_ptr __cxa_pure_virtual

1 participant