From 933721d578482857c7e630418e61de06a2ff5d25 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Feb 2026 12:30:10 -0500 Subject: [PATCH 1/2] mem_block_cache: fix TOCTOU race in lock-free destructor 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. --- include/boost/regex/v5/mem_block_cache.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/boost/regex/v5/mem_block_cache.hpp b/include/boost/regex/v5/mem_block_cache.hpp index 18116fb43..9ec82dcb2 100644 --- a/include/boost/regex/v5/mem_block_cache.hpp +++ b/include/boost/regex/v5/mem_block_cache.hpp @@ -47,7 +47,8 @@ struct mem_block_cache ~mem_block_cache() { for (size_t i = 0;i < BOOST_REGEX_MAX_CACHE_BLOCKS; ++i) { - if (cache[i].load()) ::operator delete(cache[i].load()); + void* p = cache[i].exchange(nullptr); + if (p) ::operator delete(p); } } void* get() From aab29ebf4432dabef22717e752da50bf0f8f4423 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Feb 2026 12:50:35 -0500 Subject: [PATCH 2/2] mem_block_cache: use thread_local to prevent static destruction order 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. --- include/boost/regex/v5/mem_block_cache.hpp | 8 ++ test/Jamfile.v2 | 1 + .../concurrent_static_regex_test.cpp | 114 ++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 test/pathology/concurrent_static_regex_test.cpp diff --git a/include/boost/regex/v5/mem_block_cache.hpp b/include/boost/regex/v5/mem_block_cache.hpp index 9ec82dcb2..e5862a2f8 100644 --- a/include/boost/regex/v5/mem_block_cache.hpp +++ b/include/boost/regex/v5/mem_block_cache.hpp @@ -74,7 +74,11 @@ struct mem_block_cache static mem_block_cache& instance() { +#ifdef BOOST_NO_CXX11_THREAD_LOCAL static mem_block_cache block_cache = { { {nullptr} } }; +#else + thread_local mem_block_cache block_cache = { { {nullptr} } }; +#endif return block_cache; } }; @@ -139,7 +143,11 @@ struct mem_block_cache } static mem_block_cache& instance() { +#ifdef BOOST_NO_CXX11_THREAD_LOCAL static mem_block_cache block_cache; +#else + thread_local mem_block_cache block_cache; +#endif return block_cache; } }; diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 17e813ff1..dadd11208 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -95,6 +95,7 @@ regex-test posix_api_check_cpp : c_compiler_checks/posix_api_check.cpp ; regex-test wide_posix_api_check_cpp : c_compiler_checks/wide_posix_api_check.cpp ; run pathology/bad_expression_test.cpp ; run pathology/recursion_test.cpp ; +run pathology/concurrent_static_regex_test.cpp : : : multi ; run named_subexpressions/named_subexpressions_test.cpp ; run unicode/unicode_iterator_test.cpp : : : release TEST_UTF8 : unicode_iterator_test_utf8 ; run unicode/unicode_iterator_test.cpp : : : release TEST_UTF16 : unicode_iterator_test_utf16 ; diff --git a/test/pathology/concurrent_static_regex_test.cpp b/test/pathology/concurrent_static_regex_test.cpp new file mode 100644 index 000000000..7900d0612 --- /dev/null +++ b/test/pathology/concurrent_static_regex_test.cpp @@ -0,0 +1,114 @@ +/* + * + * Copyright (c) 2026 + * Amaan Qureshi + * + * Use, modification and distribution are subject to the + * Boost Software License, Version 1.0. (See accompanying file + * LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * + */ + +/* + * LOCATION: see http://www.boost.org for most recent version. + * FILE: concurrent_static_regex_test.cpp + * VERSION: see + * DESCRIPTION: Test for concurrent use of static regex objects. + * See https://github.com/boostorg/regex/issues/198 + */ + +#include "../test_macros.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +std::atomic match_count{0}; +std::atomic search_count{0}; +std::atomic replace_count{0}; + +static const boost::regex digits_regex(R"(^\d+$)"); +static const boost::regex + map_regex(R"(^\s*\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/\S+)\s*$)"); +static const boost::regex special_regex(R"([.^$\\*+?()\[\]{}|])"); + +void thread_regex_match(unsigned iterations) { + const char *samples[] = {"12345", "hello", "99999", "0", "abc123", "42"}; + const unsigned n_samples = sizeof(samples) / sizeof(samples[0]); + + for (unsigned i = 0; i < iterations; ++i) { + const char *s = samples[i % n_samples]; + if (boost::regex_match(s, digits_regex)) + match_count.fetch_add(1, std::memory_order_relaxed); + } +} + +void thread_regex_search(unsigned iterations) { + const std::string lines[] = { + "08:00-17:00 r--p 00000000 08:01 12345 /usr/lib/libc.so.6", + "7f8a1000-7f8a2000 rw-p 00001000 08:01 67890 /usr/lib/ld-linux.so.2", + "this line does not match the pattern at all", + "00400000-00401000 r-xp 00000000 08:02 54321 /usr/bin/test", + }; + const unsigned n_lines = sizeof(lines) / sizeof(lines[0]); + + boost::smatch what; + for (unsigned i = 0; i < iterations; ++i) { + const std::string &line = lines[i % n_lines]; + if (boost::regex_search(line, what, map_regex)) + search_count.fetch_add(1, std::memory_order_relaxed); + } +} + +void thread_regex_replace(unsigned iterations) { + const std::string inputs[] = { + "/nix/store/abc123", + "path.with" + "[brackets]", + "no+specials*here?", + "plain", + }; + const unsigned n_inputs = sizeof(inputs) / sizeof(inputs[0]); + + for (unsigned i = 0; i < iterations; ++i) { + const std::string &input = inputs[i % n_inputs]; + std::string result = boost::regex_replace(input, special_regex, R"(\\$&)"); + if (result != input) + replace_count.fetch_add(1, std::memory_order_relaxed); + } +} + +} // anonymous namespace + +int cpp_main(int, char *[]) { + const unsigned n_threads = 8; + const unsigned iterations = 50000; + + for (unsigned round = 0; round < 3; ++round) { + match_count = 0; + search_count = 0; + replace_count = 0; + + std::vector threads; + threads.reserve(n_threads * 3); + + for (unsigned t = 0; t < n_threads; ++t) { + threads.emplace_back(thread_regex_match, iterations); + threads.emplace_back(thread_regex_search, iterations); + threads.emplace_back(thread_regex_replace, iterations); + } + + for (auto &th : threads) + th.join(); + + BOOST_CHECK(match_count > 0); + BOOST_CHECK(search_count > 0); + BOOST_CHECK(replace_count > 0); + } + + return 0; +}