Compare commits

...

4 Commits

Author SHA1 Message Date
yhirose 811dd0b6f2 Release v0.44.0 2026-05-10 21:46:24 +09:00
yhirose e8e652824b Add --minor flag to release.sh for forced minor bumps
Allows forcing a minor version bump even when abidiff passes,
for behavioral breaking changes that don't break ABI.
2026-05-10 21:28:38 +09:00
yhirose fbb031ed85 Stop percent-decoding HTTP request header values
parse_header() applied decode_path_component() to every header value
except Location and Referer, after is_field_value() validation. Wire
sequences like %0D%0A passed the check and expanded into literal CR/LF
inside stored values, enabling response splitting, log injection, and
proxy smuggling. %3D/%2C/%3B also flipped Cookie and X-Forwarded-For
boundaries against WAFs inspecting the wire form.

RFC 9110 §5.5 specifies header values as opaque octets. Drop the
decoding and the Location/Referer special case (originally workarounds
for the same auto-decode misbehavior; redundant once decoding stops).
Applications that need URI semantics should call decode_uri_component()
or decode_path_component() on the result explicitly.

Add regression tests covering CRLF injection, %3D/%2C/%3B boundary
characters, UTF-8 and %uXXXX sequences, browser-style Referer URLs
containing %0A (issue #2033), and the explicit-decode migration
pattern.
2026-05-10 12:59:29 +09:00
yhirose 7d5082cc0e Make ThreadPool ctor exception-safe on partial thread creation (#2445)
* Make ThreadPool ctor exception-safe on partial thread creation

If std::thread construction throws partway through the ThreadPool
constructor (e.g., pthread_create returns EAGAIN under thread-resource
pressure), the partially-built threads_ vector would destruct joinable
std::thread objects, calling std::terminate(). Wrap the spawn loop and,
on failure, signal shutdown to the workers already created, join them,
and rethrow.

Adds a reproducer test in test_thread_pool.cc that interposes
pthread_create at link time to deterministically fail the second call,
gated to POSIX + exceptions-enabled builds.

Fix #2444

* Strip ASAN from test_thread_pool to coexist with pthread_create override

Linux libasan installs its own pthread_create interceptor; our in-binary
symbol override sits on top of it and corrupts ASAN's thread bookkeeping,
which surfaces as "Joining already joined thread" on the very first test.
Disable ASAN for this small unit-test binary -- ThreadPool memory behavior
is still exercised under ASAN by the main `test` binary.
2026-05-09 21:13:40 -04:00
6 changed files with 254 additions and 26 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.43.4"
version = "0.44.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+29 -10
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.43.4"
#define CPPHTTPLIB_VERSION_NUM "0x002b04"
#define CPPHTTPLIB_VERSION "0.44.0"
#define CPPHTTPLIB_VERSION_NUM "0x002c00"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -5016,12 +5016,11 @@ inline bool parse_header(const char *beg, const char *end, T fn) {
if (!detail::fields::is_field_value(val)) { return false; }
if (case_ignore::equal(key, "Location") ||
case_ignore::equal(key, "Referer")) {
fn(key, val);
} else {
fn(key, decode_path_component(val));
}
// RFC 9110 §5.5: header field values are opaque octets and MUST NOT be
// percent-decoded by the recipient. Applications that need to interpret a
// value as a URI component should call httplib::decode_uri_component()
// (or decode_path_component()) explicitly.
fn(key, val);
return true;
}
@@ -10047,9 +10046,29 @@ inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr)
#endif
max_thread_count_ = max_n == 0 ? n : max_n;
threads_.reserve(base_thread_count_);
for (size_t i = 0; i < base_thread_count_; i++) {
threads_.emplace_back(std::thread([this]() { worker(false); }));
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
try {
#endif
for (size_t i = 0; i < base_thread_count_; i++) {
threads_.emplace_back(std::thread([this]() { worker(false); }));
}
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
} catch (...) {
// If thread creation fails partway (e.g., pthread_create returns EAGAIN),
// signal the workers we already spawned to exit and join them so the
// vector destructor does not see joinable threads (which would call
// std::terminate). Then rethrow so the caller learns of the failure.
{
std::unique_lock<std::mutex> lock(mutex_);
shutdown_ = true;
}
cond_.notify_all();
for (auto &t : threads_) {
if (t.joinable()) { t.join(); }
}
throw;
}
#endif
}
inline bool ThreadPool::enqueue(std::function<void()> fn) {
+33 -14
View File
@@ -2,10 +2,12 @@
#
# Release a new version of cpp-httplib.
#
# Usage: ./release.sh [--run]
# Usage: ./release.sh [--run] [--minor]
#
# By default, runs in dry-run mode (no changes made).
# Pass --run to actually update files, commit, tag, and push.
# Pass --minor to force a minor bump even when ABI is unchanged
# (use this for behavioral breaking changes that don't break ABI).
#
# This script:
# 1. Reads the current version from httplib.h
@@ -14,21 +16,30 @@
# 4. Determines the next version automatically:
# - abidiff passed → patch bump (e.g., 0.38.0 → 0.38.1)
# - abidiff failed → minor bump (e.g., 0.38.1 → 0.39.0)
# - --minor passed → forces minor bump regardless of abidiff
# 5. Updates httplib.h and docs-src/config.toml
# 6. Commits, tags (vX.Y.Z), and pushes
set -euo pipefail
DRY_RUN=1
if [ "${1:-}" = "--run" ]; then
DRY_RUN=0
shift
fi
if [ $# -ne 0 ]; then
echo "Usage: $0 [--run]"
exit 1
fi
FORCE_MINOR=0
while [ $# -gt 0 ]; do
case "$1" in
--run)
DRY_RUN=0
shift
;;
--minor)
FORCE_MINOR=1
shift
;;
*)
echo "Usage: $0 [--run] [--minor]"
exit 1
;;
esac
done
# --- Step 1: Read current version from httplib.h ---
CURRENT_VERSION=$(sed -n 's/^#define CPPHTTPLIB_VERSION "\([^"]*\)"/\1/p' httplib.h)
@@ -51,8 +62,7 @@ HEAD_SHORT=$(git rev-parse --short HEAD)
echo " Latest commit: $HEAD_SHORT"
# Fetch all workflow runs for the HEAD commit
RUNS=$(gh run list --json name,conclusion,headSha \
--jq "[.[] | select(.headSha == \"$HEAD_SHA\")]")
RUNS=$(gh run list --commit "$HEAD_SHA" --json name,conclusion,headSha)
NUM_RUNS=$(echo "$RUNS" | jq 'length')
@@ -95,7 +105,12 @@ fi
echo " All non-abidiff CI checks passed."
# --- Step 4: Determine new version ---
if [ "$ABIDIFF_PASSED" -eq 1 ]; then
if [ "$FORCE_MINOR" -eq 1 ] && [ "$ABIDIFF_PASSED" -eq 1 ]; then
NEW_MINOR=$((V_MINOR + 1))
NEW_VERSION="$V_MAJOR.$NEW_MINOR.0"
echo ""
echo "==> abidiff passed but --minor specified → forced minor bump"
elif [ "$ABIDIFF_PASSED" -eq 1 ]; then
NEW_PATCH=$((V_PATCH + 1))
NEW_VERSION="$V_MAJOR.$V_MINOR.$NEW_PATCH"
echo ""
@@ -104,7 +119,11 @@ else
NEW_MINOR=$((V_MINOR + 1))
NEW_VERSION="$V_MAJOR.$NEW_MINOR.0"
echo ""
echo "==> abidiff failed → minor bump"
if [ "$FORCE_MINOR" -eq 1 ]; then
echo "==> abidiff failed → minor bump (--minor also specified)"
else
echo "==> abidiff failed → minor bump"
fi
fi
VERSION_HEX=$(printf "0x%02x%02x%02x" "${NEW_VERSION%%.*}" "$(echo "$NEW_VERSION" | cut -d. -f2)" "${NEW_VERSION##*.}")
+18 -1
View File
@@ -202,8 +202,25 @@ test_split_no_tls : test.cc ../httplib.h httplib.cc Makefile
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_NO_TLS)
# ThreadPool unit tests (no TLS, no compression needed)
#
# The constructor-exception-safety reproducer test interposes pthread_create
# at link time. The link flags below enable that interposition. ASAN is also
# stripped from this target because libasan installs its own pthread_create
# interceptor; layering our override on top corrupts ASAN's thread bookkeeping
# and trips "Joining already joined thread" on Linux. ThreadPool memory
# behavior is still covered by the ASAN-instrumented `test` binary.
ifneq ($(OS), Windows_NT)
ifeq ($(shell uname -s), Darwin)
THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,-flat_namespace
else
THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,--export-dynamic
endif
endif
THREAD_POOL_CXXFLAGS := $(filter-out -fsanitize=address,$(CXXFLAGS))
test_thread_pool : test_thread_pool.cc ../httplib.h Makefile
$(CXX) -o $@ -I.. $(CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread
$(CXX) -o $@ -I.. $(THREAD_POOL_CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread $(THREAD_POOL_INTERPOSE_LDFLAGS)
check_abi:
@./check-shared-library-abi-compatibility.sh
+116
View File
@@ -7441,6 +7441,122 @@ TEST(ServerRequestParsingTest, EmptyFieldValue) {
EXPECT_EQ("HTTP/1.1 200 OK", out.substr(0, 15));
}
TEST(ServerRequestParsingTest, HeaderValueNotPercentDecoded) {
Server svr;
std::string x_custom;
std::string cookie;
std::string xff;
std::string x_unicode;
std::string x_iis;
svr.Get("/check", [&](const Request &req, Response &res) {
x_custom = req.get_header_value("X-Custom");
cookie = req.get_header_value("Cookie");
xff = req.get_header_value("X-Forwarded-For");
x_unicode = req.get_header_value("X-Unicode");
x_iis = req.get_header_value("X-IIS");
res.set_content("ok", "text/plain");
});
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
const std::string req = "GET /check HTTP/1.1\r\n"
"Host: localhost\r\n"
"X-Custom: a%0D%0AInjected: b\r\n"
"Cookie: session%3Dvictim%3B%20admin%3Dyes\r\n"
"X-Forwarded-For: 1.2.3.4%2C5.6.7.8\r\n"
"X-Unicode: %E3%81%82\r\n"
"X-IIS: %u00E9\r\n"
"Connection: close\r\n"
"\r\n";
std::string res;
ASSERT_TRUE(send_request(5, req, &res));
EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
// Every value must be returned verbatim (wire form), with no decoding.
EXPECT_EQ("a%0D%0AInjected: b", x_custom);
EXPECT_EQ("session%3Dvictim%3B%20admin%3Dyes", cookie);
EXPECT_EQ("1.2.3.4%2C5.6.7.8", xff);
EXPECT_EQ("%E3%81%82", x_unicode);
EXPECT_EQ("%u00E9", x_iis);
}
// Applications that previously relied on automatic percent-decoding can
// reproduce the old behavior by explicitly calling decode_path_component()
// or, for RFC 3986 conformance, decode_uri_component().
TEST(ServerRequestParsingTest, HeaderValueExplicitDecodingByApplication) {
Server svr;
std::string decoded;
svr.Get("/check", [&](const Request &req, Response &res) {
decoded = decode_uri_component(req.get_header_value("X-Custom"));
res.set_content("ok", "text/plain");
});
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
const std::string req = "GET /check HTTP/1.1\r\n"
"Host: localhost\r\n"
"X-Custom: hello%20world\r\n"
"Connection: close\r\n"
"\r\n";
std::string res;
ASSERT_TRUE(send_request(5, req, &res));
EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
EXPECT_EQ("hello world", decoded);
}
// Regression test for #2033. Browsers send Referer values that include
// percent-encoded characters such as %0A inside the URL. Decoding the
// header value would either trip the post-decode CR/LF/NUL guard (the
// original bug, returning 400) or, after that guard was relaxed, silently
// store a literal LF — both unacceptable. The wire form must round-trip.
TEST(ServerRequestParsingTest, RefererWithPercentEncodedNewline) {
Server svr;
std::string referer;
svr.Get("/check", [&](const Request &req, Response &res) {
referer = req.get_header_value("Referer");
res.set_content("ok", "text/plain");
});
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
const std::string req = "GET /check HTTP/1.1\r\n"
"Host: localhost\r\n"
"Referer: http://localhost:1111/?q=Hello%0A\r\n"
"Connection: close\r\n"
"\r\n";
std::string res;
ASSERT_TRUE(send_request(5, req, &res));
EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
EXPECT_EQ("http://localhost:1111/?q=Hello%0A", referer);
}
TEST(ServerStopTest, StopServerWithChunkedTransmission) {
Server svr;
+57
View File
@@ -198,6 +198,63 @@ TEST(ThreadPoolTest, InvalidMaxThreadsThrows) {
}
#endif
// Issue #2444: ThreadPool constructor must be exception-safe when std::thread
// construction fails partway (e.g., pthread_create returns EAGAIN under thread
// resource pressure). Without proper handling, the partially-built threads_
// vector destroys joinable std::thread objects, calling std::terminate().
//
// We reproduce the failure portably by interposing pthread_create at link
// time: while the counter is armed, the first N calls succeed, the rest
// return EAGAIN. This is gated to POSIX + exceptions-enabled builds.
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
#if defined(__unix__) || defined(__APPLE__)
#include <dlfcn.h>
#include <errno.h>
#include <pthread.h>
namespace {
// -1 = pass-through (default). >= 0 = number of remaining successful calls
// before EAGAIN is returned. Reset to -1 after each test that arms it.
std::atomic<int> g_pthread_create_remaining{-1};
} // namespace
extern "C" int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg) {
using fn_t =
int (*)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *);
static fn_t real = reinterpret_cast<fn_t>(dlsym(RTLD_NEXT, "pthread_create"));
int n = g_pthread_create_remaining.load(std::memory_order_relaxed);
if (n == 0) { return EAGAIN; }
if (n > 0) {
g_pthread_create_remaining.fetch_sub(1, std::memory_order_relaxed);
}
return real(thread, attr, start_routine, arg);
}
TEST(ThreadPoolTest, ConstructorRecoversWhenThreadCreationFails) {
// Allow only the first thread to spawn; subsequent pthread_create calls
// return EAGAIN, causing std::thread() to throw std::system_error mid-loop.
g_pthread_create_remaining.store(1);
bool caught = false;
try {
ThreadPool pool(/*n=*/4);
(void)pool;
} catch (const std::system_error &) { caught = true; } catch (...) {
caught = true;
}
// Disarm before any further test runs.
g_pthread_create_remaining.store(-1);
EXPECT_TRUE(caught);
}
#endif // POSIX
#endif // CPPHTTPLIB_NO_EXCEPTIONS
TEST(ThreadPoolTest, EnqueueAfterShutdownReturnsFalse) {
ThreadPool pool(2);
pool.shutdown();