Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/traffic_layout/info.cc
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,25 @@ produce_features(bool json)
print_feature("TS_IP_TRANSPARENT", TS_IP_TRANSPARENT, json);
print_feature("TS_HAS_128BIT_CAS", TS_HAS_128BIT_CAS, json);
print_feature("TS_HAS_TESTS", TS_HAS_TESTS, json);
// Whether PCRE2 can run a pattern on the just-in-time engine. This is a property of the
// PCRE2 that ATS is linked against, not of ATS, and it decides which resource limit a
// pathological pattern reaches: the JIT stack, or the interpreter's far larger match,
// depth and heap limits, because the interpreter keeps its backtracking frames on the
// heap. Tests that assert on one of those limits need to know.
//
// PCRE2_JIT_TEST_ALLOC (PCRE2 10.45) also confirms the JIT can allocate executable
// memory. PCRE2_CONFIG_JIT does not, and reports success on a hardened runtime where
// every JIT compile then fails, which is the direction that misleads a test gate.
{
uint32_t has_jit = 0;

#ifdef PCRE2_JIT_TEST_ALLOC
has_jit = pcre2_jit_compile(nullptr, PCRE2_JIT_TEST_ALLOC) == 0;
#else
pcre2_config(PCRE2_CONFIG_JIT, &has_jit);
#endif
print_feature("TS_HAS_PCRE2_JIT", has_jit != 0, json);
}
print_feature("TS_MAX_THREADS_IN_EACH_THREAD_TYPE", TS_MAX_THREADS_IN_EACH_THREAD_TYPE, json);
print_feature("TS_MAX_NUMBER_EVENT_THREADS", TS_MAX_NUMBER_EVENT_THREADS, json);
print_feature("TS_MAX_HOST_NAME_LEN", TS_MAX_HOST_NAME_LEN, json);
Expand Down
81 changes: 73 additions & 8 deletions src/tsutil/Regex.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>
#include <pthread.h>

#include <array>
#include <vector>
Expand Down Expand Up @@ -79,6 +80,63 @@ my_free(void *ptr, void * /*caller*/)
free(ptr);
}

//----------------------------------------------------------------------------
// One match context is shared by every thread that matches through it, and PCRE2
// requires a distinct JIT stack per thread, so the stack comes from a callback
// invoked at match time rather than a pointer baked in when the context is built.
//
// The per thread stack is held in a pthread key rather than a thread_local. A
// thread_local with a destructor registers it through __cxa_thread_atexit, which
// takes the dynamic loader lock; doing that from a match would invert lock order
// against a dlopen caller running a plugin's static initialization. See the same
// hazard described at Diags::tag_activated. A pthread key registers its destructor
// once, at key creation, and never from the matching path.
pthread_key_t jit_stack_key;
bool jit_stack_key_valid = false;
pthread_once_t jit_stack_key_once = PTHREAD_ONCE_INIT;

void
destroy_jit_stack(void *stack)
{
if (stack != nullptr) {
pcre2_jit_stack_free(static_cast<pcre2_jit_stack *>(stack));
}
}

void
make_jit_stack_key()
{
jit_stack_key_valid = pthread_key_create(&jit_stack_key, destroy_jit_stack) == 0;
}

pcre2_jit_stack *
jit_stack_for_this_thread(void *)
{
pthread_once(&jit_stack_key_once, make_jit_stack_key);
if (!jit_stack_key_valid) {
// Without a key there is nowhere to keep a stack, and jit_stack_key holds a
// default value that may name an unrelated key. Returning null tells PCRE2 to
// use its own default stack, which pcre2jit documents as thread safe.
return nullptr;
}

auto *stack = static_cast<pcre2_jit_stack *>(pthread_getspecific(jit_stack_key));
if (stack == nullptr) {
// One page to start, one mebibyte at most. The maximum is address space reserved at
// creation and made resident only as deep as a match actually goes, so a larger one
// costs nothing per match, and a mebibyte already resolves a longer subject than
// proxy.config.http.request_header_max_size lets a client send.
stack = pcre2_jit_stack_create(4096, 1024 * 1024, nullptr);
if (pthread_setspecific(jit_stack_key, stack) != 0) {
// Nothing holds the stack now, so it would leak once per match. Give it back and
// let PCRE2 use its own default stack for this call.
pcre2_jit_stack_free(stack);
return nullptr;
}
}
return stack;
}

//----------------------------------------------------------------------------
class RegexContext
{
Expand All @@ -100,9 +158,6 @@ class RegexContext
if (_match_context != nullptr) {
pcre2_match_context_free(_match_context);
}
if (_jit_stack != nullptr) {
pcre2_jit_stack_free(_jit_stack);
}
}
pcre2_general_context *
get_general_context()
Expand All @@ -126,13 +181,11 @@ class RegexContext
_general_context = pcre2_general_context_create(my_malloc, my_free, nullptr);
_compile_context = pcre2_compile_context_create(_general_context);
_match_context = pcre2_match_context_create(_general_context);
_jit_stack = pcre2_jit_stack_create(4096, 1024 * 1024, nullptr); // 1 page min and 1MB max
pcre2_jit_stack_assign(_match_context, nullptr, _jit_stack);
pcre2_jit_stack_assign(_match_context, jit_stack_for_this_thread, nullptr);
}
pcre2_general_context *_general_context = nullptr;
pcre2_compile_context *_compile_context = nullptr;
pcre2_match_context *_match_context = nullptr;
pcre2_jit_stack *_jit_stack = nullptr;
};

} // namespace
Expand Down Expand Up @@ -257,8 +310,20 @@ struct RegexMatchContext::_MatchContext {
//----------------------------------------------------------------------------
RegexMatchContext::RegexMatchContext()
{
auto ctx = pcre2_match_context_create(nullptr);
debug_assert_message(ctx, "Failed to allocate custom pcre2 match context");
// Copy the shared context rather than building a blank one. A blank context
// silently drops everything the shared context configures, which is how this
// type came to run with PCRE2's fallback 32KiB JIT stack instead of the 1MiB
// one. Callers override only the fields they mean to.
//
// pcre2_match_context_copy dereferences its argument rather than returning null
// for one, and RegexContext's constructor does not check its allocations, so a
// shared context that failed to allocate would crash here. Fall back to a blank
// context, which is what this constructor built before and which no reader of
// _match_context dereferences unchecked.
auto *shared = RegexContext::get_instance()->get_match_context();
auto *ctx = shared != nullptr ? pcre2_match_context_copy(shared) : pcre2_match_context_create(nullptr);
Comment on lines +323 to +324

debug_assert_message(ctx, "Failed to obtain a pcre2 match context");
_MatchContext::set(_match_context, ctx);
}

Expand Down
Loading