mem_block_cache: fix two thread-safety bugs#271
Open
amaanq wants to merge 2 commits intoboostorg:developfrom
Open
mem_block_cache: fix two thread-safety bugs#271amaanq wants to merge 2 commits intoboostorg:developfrom
amaanq wants to merge 2 commits intoboostorg:developfrom
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
mem_block_cachehas two thread-safety bugs that cause heap corruption and use-after-free whenstatic const boost::regexobjects 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: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 (staticlocal), which is destroyed during__cxa_atexit. Detached or late-joining threads that are still callingget()/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
statictothread_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 byBOOST_NO_CXX11_THREAD_LOCALfor compilers that lack the keyword.I've also added a regression test (
concurrent_static_regex_test.cpp) that exercisesregex_match,regex_search, andregex_replacefrom 24 threads simultaneously.Note
I did use an LLM to assist me in debugging the issue and finding a way to test the bug.