From 4409a68c5abaaa82aac7bb04b1caa8a9fadf2ba7 Mon Sep 17 00:00:00 2001 From: Florian Albrechtskirchinger Date: Thu, 13 Mar 2025 07:50:18 +0100 Subject: [PATCH 1/4] Make random_string() thread-safe By making the random engine thread_local, each thread now has its own independent random sequence, ensuring safe concurrent access. Additionally, using an immediately invoked lambda expression to initialize the engine eliminates the need for separate static seed variables. --- httplib.h | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/httplib.h b/httplib.h index 0949e1d2f2..3f2bdaccfd 100644 --- a/httplib.h +++ b/httplib.h @@ -5100,16 +5100,15 @@ inline std::string random_string(size_t length) { constexpr const char data[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - // std::random_device might actually be deterministic on some - // platforms, but due to lack of support in the c++ standard library, - // doing better requires either some ugly hacks or breaking portability. - static std::random_device seed_gen; - - // Request 128 bits of entropy for initialization - static std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), - seed_gen()}; - - static std::mt19937 engine(seed_sequence); + static thread_local std::mt19937 engine([]() { + // std::random_device might actually be deterministic on some + // platforms, but due to lack of support in the c++ standard library, + // doing better requires either some ugly hacks or breaking portability. + std::random_device seed_gen; + // Request 128 bits of entropy for initialization + std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()}; + return std::mt19937(seed_sequence); + }()); std::string result; for (size_t i = 0; i < length; i++) { From b6ec06b3bcbbe934644d8907babb7eead95ddf2b Mon Sep 17 00:00:00 2001 From: Florian Albrechtskirchinger Date: Wed, 12 Mar 2025 10:58:23 +0100 Subject: [PATCH 2/4] Add macro for static variable definition Introduce macro CPPHTTPLIB_DEFINE_STATIC to define static variables, with optional dynamic allocation to avoid exit-time destructors and race conditions with atexit handlers. --- httplib.h | 54 ++++++++++++++++++++++++++++++++++------------------ test/test.cc | 10 ++++++---- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/httplib.h b/httplib.h index 3f2bdaccfd..28d0ca0d11 100644 --- a/httplib.h +++ b/httplib.h @@ -145,6 +145,16 @@ #define CPPHTTPLIB_LISTEN_BACKLOG 5 #endif +#ifndef CPPHTTPLIB_DEFINE_STATIC +#ifdef CPPHTTPLIB_NO_EXIT_TIME_DESTRUCTORS +#define CPPHTTPLIB_DEFINE_STATIC(var_type, var, init) \ + static var_type &var = *new typename std::remove_cv::type init +#else +#define CPPHTTPLIB_DEFINE_STATIC(var_type, var, init) \ + static var_type var = typename std::remove_cv::type init +#endif +#endif + /* * Headers */ @@ -2919,7 +2929,7 @@ inline std::string decode_url(const std::string &s, inline std::string file_extension(const std::string &path) { std::smatch m; - static auto re = std::regex("\\.([a-zA-Z0-9]+)$"); + CPPHTTPLIB_DEFINE_STATIC(const std::regex, re, ("\\.([a-zA-Z0-9]+)$")); if (std::regex_search(path, m, re)) { return m[1].str(); } return std::string(); } @@ -4912,9 +4922,10 @@ class MultipartFormDataParser { file_.content_type = trim_copy(header.substr(str_len(header_content_type))); } else { - static const std::regex re_content_disposition( - R"~(^Content-Disposition:\s*form-data;\s*(.*)$)~", - std::regex_constants::icase); + CPPHTTPLIB_DEFINE_STATIC( + const std::regex, re_content_disposition, + (R"~(^Content-Disposition:\s*form-data;\s*(.*)$)~", + std::regex_constants::icase)); std::smatch m; if (std::regex_match(header, m, re_content_disposition)) { @@ -4935,8 +4946,9 @@ class MultipartFormDataParser { it = params.find("filename*"); if (it != params.end()) { // Only allow UTF-8 encoding... - static const std::regex re_rfc5987_encoding( - R"~(^UTF-8''(.+?)$)~", std::regex_constants::icase); + CPPHTTPLIB_DEFINE_STATIC( + const std::regex, re_rfc5987_encoding, + (R"~(^UTF-8''(.+?)$)~", std::regex_constants::icase)); std::smatch m2; if (std::regex_match(it->second, m2, re_rfc5987_encoding)) { @@ -5614,7 +5626,7 @@ class WSInit { bool is_valid_ = false; }; -static WSInit wsinit_; +CPPHTTPLIB_DEFINE_STATIC(WSInit, wsinit_, ()); #endif inline bool parse_www_authenticate(const Response &res, @@ -5622,7 +5634,8 @@ inline bool parse_www_authenticate(const Response &res, bool is_proxy) { auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; if (res.has_header(auth_key)) { - static auto re = std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"); + CPPHTTPLIB_DEFINE_STATIC(const std::regex, re, + (R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~")); auto s = res.get_header_value(auth_key); auto pos = s.find(' '); if (pos != std::string::npos) { @@ -5706,7 +5719,7 @@ inline void hosted_at(const std::string &hostname, inline std::string append_query_params(const std::string &path, const Params ¶ms) { std::string path_with_query = path; - const static std::regex re("[^?]+\\?.*"); + CPPHTTPLIB_DEFINE_STATIC(const std::regex, re, ("[^?]+\\?.*")); auto delm = std::regex_match(path, re) ? '&' : '?'; path_with_query += delm + detail::params_to_query_str(params); return path_with_query; @@ -6480,9 +6493,9 @@ inline bool Server::parse_request_line(const char *s, Request &req) const { if (count != 3) { return false; } } - static const std::set methods{ - "GET", "HEAD", "POST", "PUT", "DELETE", - "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; + CPPHTTPLIB_DEFINE_STATIC(std::set, methods, + ({"GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", + "OPTIONS", "TRACE", "PATCH", "PRI"})); if (methods.find(req.method) == methods.end()) { return false; } @@ -7469,9 +7482,11 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, if (!line_reader.getline()) { return false; } #ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR - const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n"); + CPPTHTTPLIB_DEFINE_STATIC(std::regex, re, + ("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n")); #else - const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n"); + CPPHTTPLIB_DEFINE_STATIC(const std::regex, re, + ("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n")); #endif std::cmatch m; @@ -7703,8 +7718,9 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) { auto location = res.get_header_value("location"); if (location.empty()) { return false; } - const static std::regex re( - R"((?:(https?):)?(?://(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)"); + CPPHTTPLIB_DEFINE_STATIC( + const std::regex, re, + (R"((?:(https?):)?(?://(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)")); std::smatch m; if (!std::regex_match(location, m, re)) { return false; } @@ -9787,8 +9803,10 @@ inline Client::Client(const std::string &scheme_host_port) inline Client::Client(const std::string &scheme_host_port, const std::string &client_cert_path, const std::string &client_key_path) { - const static std::regex re( - R"((?:([a-z]+):\/\/)?(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)"); + + CPPHTTPLIB_DEFINE_STATIC( + const std::regex, re, + (R"((?:([a-z]+):\/\/)?(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)")); std::smatch m; if (std::regex_match(scheme_host_port, m, re)) { diff --git a/test/test.cc b/test/test.cc index 81a5e33a6c..3508834b37 100644 --- a/test/test.cc +++ b/test/test.cc @@ -40,12 +40,14 @@ using namespace httplib; const char *HOST = "localhost"; const int PORT = 1234; -const string LONG_QUERY_VALUE = string(25000, '@'); -const string LONG_QUERY_URL = "/long-query-value?key=" + LONG_QUERY_VALUE; +CPPHTTPLIB_DEFINE_STATIC(const string, LONG_QUERY_VALUE, (25000, '@')); +CPPHTTPLIB_DEFINE_STATIC(const string, LONG_QUERY_URL, + ("/long-query-value?key=" + LONG_QUERY_VALUE)); -const std::string JSON_DATA = "{\"hello\":\"world\"}"; +CPPHTTPLIB_DEFINE_STATIC(const string, JSON_DATA, ("{\"hello\":\"world\"}")); -const string LARGE_DATA = string(1024 * 1024 * 100, '@'); // 100MB +CPPHTTPLIB_DEFINE_STATIC(const string, LARGE_DATA, + (1024 * 1024 * 100, '@')); // 100MB MultipartFormData &get_file_value(MultipartFormDataItems &files, const char *key) { From 736203f0540a32e48d1de1a9f1bd4af13d342c3d Mon Sep 17 00:00:00 2001 From: Florian Albrechtskirchinger Date: Thu, 13 Mar 2025 09:34:25 +0100 Subject: [PATCH 3/4] Add exit-time destructors test --- .gitignore | 1 + test/Makefile | 23 +++++++++++----- test/test.cc | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 1ae8acf708..83129ed40d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ test/httplib.cc test/httplib.h test/test test/server_fuzzer +test/test_no_exit_time_dtors test/test_proxy test/test_split test/test.xcodeproj/xcuser* diff --git a/test/Makefile b/test/Makefile index 48cd3abb2e..7c75010260 100644 --- a/test/Makefile +++ b/test/Makefile @@ -18,7 +18,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz BROTLI_DIR = $(PREFIX)/opt/brotli BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec -TEST_ARGS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) -pthread -lcurl +TEST_ARGS = gtest_main.o gtest-all.o -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) -pthread -lcurl # By default, use standalone_fuzz_target_runner. # This runner does no fuzzing, but simply executes the inputs @@ -33,19 +33,30 @@ REALPATH = $(shell which grealpath 2>/dev/null || which realpath 2>/dev/null) STYLE_CHECK_FILES = $(filter-out httplib.h httplib.cc, \ $(wildcard example/*.h example/*.cc fuzzing/*.h fuzzing/*.cc *.h *.cc ../httplib.h)) -all : test test_split +all : test test_no_exit_time_dtors test_split ./test + GTEST_FILTER="ExitTimeDtorsTest.*" ./test_no_exit_time_dtors proxy : test_proxy ./test_proxy -test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem +gtest-all.o : gtest/src/gtest-all.cc + $(CXX) -c -o $@ $(CXXFLAGS) -Igtest -Igtest/include $< + +gtest_main.o : gtest/src/gtest_main.cc + $(CXX) -c -o $@ $(CXXFLAGS) -Igtest -Igtest/include $< + +test : gtest_main.o gtest-all.o test.cc include_httplib.cc ../httplib.h Makefile cert.pem $(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS) @file $@ +test_no_exit_time_dtors : gtest_main.o gtest-all.o test.cc ../httplib.h Makefile cert.pem + $(CXX) -o $@ -I.. $(CXXFLAGS) -DCPPHTTPLIB_NO_EXIT_TIME_DESTRUCTORS \ + $(if $(findstring clang,$(CXX)),-Wexit-time-destructors -Werror=exit-time-destructors) test.cc $(TEST_ARGS) + # Note: The intention of test_split is to verify that it works to compile and # link the split httplib.h, so there is normally no need to execute it. -test_split : test.cc ../httplib.h httplib.cc Makefile cert.pem +test_split : gtest_main.o gtest-all.o test.cc ../httplib.h httplib.cc Makefile cert.pem $(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS) check_abi: @@ -73,7 +84,7 @@ style_check: $(STYLE_CHECK_FILES) echo "All files are properly formatted."; \ fi -test_proxy : test_proxy.cc ../httplib.h Makefile cert.pem +test_proxy : gtest_main.o gtest-all.o test_proxy.cc ../httplib.h Makefile cert.pem $(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS) # Runs server_fuzzer.cc based on value of $(LIB_FUZZING_ENGINE). @@ -98,5 +109,5 @@ cert.pem: ./gen-certs.sh clean: - rm -rf test test_split test_proxy server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM + rm -rf test test_no_exit_time_dtors test_split test_proxy server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM diff --git a/test/test.cc b/test/test.cc index 3508834b37..6e9a174e58 100644 --- a/test/test.cc +++ b/test/test.cc @@ -8470,3 +8470,75 @@ TEST(ClientInThreadTest, Issue2068) { t.join(); } } + +#if defined(__SANITIZE_ADDRESS__) +#define ASAN_ENABLED 1 +#else +#if defined(__has_feature) +#if __has_feature(address_sanitizer) +#define ASAN_ENABLED 1 +#else +#define ASAN_ENABLED 0 +#endif +#else +#define ASAN_ENABLED 0 +#endif +#endif + +// No death tests on Windows +#ifndef _WIN32 +bool KilledByAbortOrSegfault(int exit_status) { + return +#if ASAN_ENABLED + // For ASan in some environments + (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 1) || +#endif + (WIFSIGNALED(exit_status) && + (WTERMSIG(exit_status) == SIGABRT || WTERMSIG(exit_status) == SIGSEGV)); +} + +Server *issue2097_svr = nullptr; +std::thread *issue2097_svr_thread = nullptr; + +TEST(ExitTimeDtorsTest, Issue2097) { + GTEST_FLAG_SET(death_test_style, "threadsafe"); + ASSERT_EXIT( + { + issue2097_svr = new Server(); + std::atexit([]() { + // Wait a bit before stopping server to simulate delayed exit + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + issue2097_svr->stop(); + issue2097_svr_thread->join(); + }); + + issue2097_svr_thread = new std::thread([]() { + issue2097_svr->Get( + "/hi", [](const Request & /*req*/, httplib::Response &res) { + res.set_content("Quack", "text/plain"); + }); + + issue2097_svr->listen(HOST, PORT); + }); + + std::thread cli_thread([]() { + Client cli(HOST, PORT); + while (true) { + auto res = cli.Get("/hi"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }); + + std::thread([]() { + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + std::exit(42); + }).join(); + }, +#ifdef CPPHTTPLIB_NO_EXIT_TIME_DESTRUCTORS + ::testing::ExitedWithCode(42), +#else + KilledByAbortOrSegfault, +#endif + ""); +} +#endif From 6c11aa5f55d081a37b05cfc0c9b14c8ee0394aca Mon Sep 17 00:00:00 2001 From: Florian Albrechtskirchinger Date: Thu, 13 Mar 2025 11:47:01 +0100 Subject: [PATCH 4/4] Document exit-time destructors --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index aed19ca6c4..e4c30907a9 100644 --- a/README.md +++ b/README.md @@ -963,6 +963,16 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32 > [!NOTE] > Windows 8 or lower, Visual Studio 2015 or lower, and Cygwin and MSYS2 including MinGW are neither supported nor tested. +### Exit-time destructors + +By default, the library relies on exit-time destructors for the cleanup of its static objects when the program terminates. To disable these exit-time destructors, define the preprocessor macro `CPPHTTPLIB_NO_EXIT_TIME_DESTRUCTORS` before including `httplib.h`. + +> [!NOTE] +> When exit-time destructors are disabled, all static variables are allocated on the heap and are not deleted, to prevent their destructors from being called at exit time. This results in purposeful memory leaks, but since the program is exiting, it typically does not affect the application's behavior. + +> [!NOTE] +> If you use `std::atexit()` to register a function that accesses client or server objects from this library, it is recommended to disable exit-time destructors. This ensures that the objects remain valid when your registered function runs, avoiding potential issues with destructors being called before your function executes. + License -------