tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 78d20156e19eed76b343d45d73b8ba4ee6e2c515
parent 90d6a6333a62ea7f8d86037bb4d9a8a6ba7c6bbf
Author: André Bargull <andre.bargull@gmail.com>
Date:   Mon, 20 Oct 2025 12:14:52 +0000

Bug 1992107: Handle near overrecursion in RegExpDepthCheck. r=iain

`AutoCheckRecursionLimit` checks for overrecursion by comparing against the
base-pointer (`__builtin_frame_address(0)`). This can lead to issues when we're
near the recursion limit and the RegExp doesn't contain any nested structures:
`uint8_t padding[FRAME_PADDING]` increases the stack-pointer, but because recursion
checks are performed against the base-pointer, adding the padding array is a
no-op for `RegExpNode` elements without child nodes (the `LEAF_DEPTH` case).

Add `AutoCheckRecursionLimit::checkWithExtraDontReport` so we can add the frame
padding directly to the base-pointer address, so we can also check for over-
recursion on leaf RegExp nodes.

Differential Revision: https://phabricator.services.mozilla.com/D268720

Diffstat:
Mjs/public/friend/StackLimits.h | 17++++++++++++-----
Mjs/src/irregexp/RegExpAPI.cpp | 35++++++++++++++---------------------
Ajs/src/jit-test/tests/regexp/over-recursion-with-leaf-nodes.js | 25+++++++++++++++++++++++++
3 files changed, 51 insertions(+), 26 deletions(-)

diff --git a/js/public/friend/StackLimits.h b/js/public/friend/StackLimits.h @@ -163,6 +163,8 @@ class MOZ_RAII AutoCheckRecursionLimit { FrontendContext* fc) const; [[nodiscard]] MOZ_ALWAYS_INLINE bool checkWithExtra(JSContext* cx, size_t extra) const; + [[nodiscard]] MOZ_ALWAYS_INLINE bool checkWithExtraDontReport( + JSContext* cx, size_t extra) const; [[nodiscard]] MOZ_ALWAYS_INLINE bool checkWithStackPointerDontReport( JSContext* cx, void* sp) const; [[nodiscard]] MOZ_ALWAYS_INLINE bool checkWithStackPointerDontReport( @@ -285,17 +287,22 @@ MOZ_ALWAYS_INLINE bool AutoCheckRecursionLimit::checkWithStackPointerDontReport( MOZ_ALWAYS_INLINE bool AutoCheckRecursionLimit::checkWithExtra( JSContext* cx, size_t extra) const { + if (MOZ_UNLIKELY(!checkWithExtraDontReport(cx, extra))) { + ReportOverRecursed(cx); + return false; + } + return true; +} + +MOZ_ALWAYS_INLINE bool AutoCheckRecursionLimit::checkWithExtraDontReport( + JSContext* cx, size_t extra) const { char* sp = reinterpret_cast<char*>(__builtin_frame_address(0)); #if JS_STACK_GROWTH_DIRECTION > 0 sp += extra; #else sp -= extra; #endif - if (MOZ_UNLIKELY(!checkWithStackPointerDontReport(cx, sp))) { - ReportOverRecursed(cx); - return false; - } - return true; + return checkWithStackPointerDontReport(cx, sp); } MOZ_ALWAYS_INLINE bool AutoCheckRecursionLimit::checkSystem( diff --git a/js/src/irregexp/RegExpAPI.cpp b/js/src/irregexp/RegExpAPI.cpp @@ -417,12 +417,10 @@ class RegExpDepthCheck final : public v8::internal::RegExpVisitor { } // Leaf nodes with no children -#define LEAF_DEPTH(Kind) \ - void* Visit##Kind(v8::internal::RegExp##Kind* node, void*) override { \ - uint8_t padding[FRAME_PADDING]; \ - dummy_ = padding; /* Prevent padding from being optimized away.*/ \ - AutoCheckRecursionLimit recursion(cx_); \ - return (void*)recursion.checkDontReport(cx_); \ +#define LEAF_DEPTH(Kind) \ + void* Visit##Kind(v8::internal::RegExp##Kind* node, void*) override { \ + AutoCheckRecursionLimit recursion(cx_); \ + return (void*)recursion.checkWithExtraDontReport(cx_, FRAME_PADDING); \ } LEAF_DEPTH(Assertion) @@ -437,10 +435,8 @@ class RegExpDepthCheck final : public v8::internal::RegExpVisitor { // Wrapper nodes with one child #define WRAPPER_DEPTH(Kind) \ void* Visit##Kind(v8::internal::RegExp##Kind* node, void*) override { \ - uint8_t padding[FRAME_PADDING]; \ - dummy_ = padding; /* Prevent padding from being optimized away.*/ \ AutoCheckRecursionLimit recursion(cx_); \ - if (!recursion.checkDontReport(cx_)) { \ + if (!recursion.checkWithExtraDontReport(cx_, FRAME_PADDING)) { \ return nullptr; \ } \ return node->body()->Accept(this, nullptr); \ @@ -454,10 +450,8 @@ class RegExpDepthCheck final : public v8::internal::RegExpVisitor { void* VisitAlternative(v8::internal::RegExpAlternative* node, void*) override { - uint8_t padding[FRAME_PADDING]; - dummy_ = padding; /* Prevent padding from being optimized away.*/ AutoCheckRecursionLimit recursion(cx_); - if (!recursion.checkDontReport(cx_)) { + if (!recursion.checkWithExtraDontReport(cx_, FRAME_PADDING)) { return nullptr; } for (auto* child : *node->nodes()) { @@ -469,10 +463,8 @@ class RegExpDepthCheck final : public v8::internal::RegExpVisitor { } void* VisitDisjunction(v8::internal::RegExpDisjunction* node, void*) override { - uint8_t padding[FRAME_PADDING]; - dummy_ = padding; /* Prevent padding from being optimized away.*/ AutoCheckRecursionLimit recursion(cx_); - if (!recursion.checkDontReport(cx_)) { + if (!recursion.checkWithExtraDontReport(cx_, FRAME_PADDING)) { return nullptr; } for (auto* child : *node->alternatives()) { @@ -484,10 +476,8 @@ class RegExpDepthCheck final : public v8::internal::RegExpVisitor { } void* VisitClassSetExpression(v8::internal::RegExpClassSetExpression* node, void*) override { - uint8_t padding[FRAME_PADDING]; - dummy_ = padding; /* Prevent padding from being optimized away.*/ AutoCheckRecursionLimit recursion(cx_); - if (!recursion.checkDontReport(cx_)) { + if (!recursion.checkWithExtraDontReport(cx_, FRAME_PADDING)) { return nullptr; } for (auto* child : *node->operands()) { @@ -500,11 +490,15 @@ class RegExpDepthCheck final : public v8::internal::RegExpVisitor { private: JSContext* cx_; - void* dummy_ = nullptr; // This size is picked to be comfortably larger than any // RegExp*::ToNode stack frame. +#ifndef DEBUG static const size_t FRAME_PADDING = 256; +#else + // Use a slightly larger padding for debug builds. + static const size_t FRAME_PADDING = 256 * 2; +#endif }; enum class AssembleResult { @@ -794,8 +788,7 @@ bool CompilePattern(JSContext* cx, MutableHandleRegExpShared re, // Avoid stack overflow while recursively walking the AST. RegExpDepthCheck depthCheck(cx); if (!depthCheck.check(data.tree)) { - JS_ReportErrorASCII(cx, "regexp too big"); - cx->reportResourceExhaustion(); + ReportOverRecursed(cx); return false; } diff --git a/js/src/jit-test/tests/regexp/over-recursion-with-leaf-nodes.js b/js/src/jit-test/tests/regexp/over-recursion-with-leaf-nodes.js @@ -0,0 +1,25 @@ +function hasChildNodes() { + // Recurse so we get close to the stack limit. + try { + hasChildNodes(); + } catch { + // Ignore over-recursion error. + } + + // RegExp with child nodes. + return RegExp(".|.").test(""); +} +hasChildNodes(); + +function hasLeafNode() { + // Recurse so we get close to the stack limit. + try { + hasLeafNode(); + } catch { + // Ignore over-recursion error. + } + + // RegExp consisting of a single leaf node. + return RegExp(".").test(""); +} +hasLeafNode();