diff --git a/src/jsc/bindings/VectorSizeLimit.h b/src/jsc/bindings/VectorSizeLimit.h new file mode 100644 index 000000000000..cb2b4686f9c5 --- /dev/null +++ b/src/jsc/bindings/VectorSizeLimit.h @@ -0,0 +1,20 @@ +#pragma once + +#include "root.h" +#include + +extern "C" size_t Bun__stringSyntheticAllocationLimit; + +namespace Bun { + +// The most elements a Vector can hold, lowered by Bun__stringSyntheticAllocationLimit so tests reach it cheaply. +template +size_t maxVectorSize() +{ + constexpr size_t maxBytes = std::numeric_limits::max() >> 1; + static_assert(WTF::isValidCapacityForVector(maxBytes / sizeof(T))); + static_assert(!WTF::isValidCapacityForVector(maxBytes / sizeof(T) + 1)); + return std::min(maxBytes, Bun__stringSyntheticAllocationLimit) / sizeof(T); +} + +} // namespace Bun diff --git a/src/jsc/bindings/webcore/URLPatternTokenizer.cpp b/src/jsc/bindings/webcore/URLPatternTokenizer.cpp index 4c8d87700c7e..efe491360155 100644 --- a/src/jsc/bindings/webcore/URLPatternTokenizer.cpp +++ b/src/jsc/bindings/webcore/URLPatternTokenizer.cpp @@ -28,6 +28,7 @@ #include "ExceptionOr.h" #include "URLPatternParser.h" +#include "VectorSizeLimit.h" #include #include @@ -69,8 +70,10 @@ void Tokenizer::seekNextCodePoint(size_t index) // https://urlpattern.spec.whatwg.org/#add-a-token void Tokenizer::addToken(TokenType currentType, size_t nextPosition, size_t valuePosition, size_t valueLength) { - m_tokenList.append(Token { currentType, m_index, m_input.substring(valuePosition, valueLength) }); - m_index = nextPosition; + if (m_tokenList.size() >= Bun::maxVectorSize() || !m_tokenList.tryAppend(Token { currentType, m_index, m_input.substring(valuePosition, valueLength) })) + m_tokenAppendFailure = true; + else + m_index = nextPosition; } // https://urlpattern.spec.whatwg.org/#add-a-token-with-default-length @@ -109,7 +112,7 @@ ExceptionOr> Tokenizer::tokenize() { ExceptionOr maybeException; - while (m_index < m_input.length()) { + while (m_index < m_input.length() && !m_tokenAppendFailure) { if (m_policy == TokenizePolicy::Strict && maybeException.hasException()) return maybeException.releaseException(); @@ -266,6 +269,10 @@ ExceptionOr> Tokenizer::tokenize() } addToken(TokenType::End, m_index, m_index); + + if (m_tokenAppendFailure) + return Exception { ExceptionCode::TypeError, "URLPattern constructor: Failed to create URLPattern (from input string)"_s }; + return WTF::move(m_tokenList); } diff --git a/src/jsc/bindings/webcore/URLPatternTokenizer.h b/src/jsc/bindings/webcore/URLPatternTokenizer.h index fb5a79061687..576b5153883f 100644 --- a/src/jsc/bindings/webcore/URLPatternTokenizer.h +++ b/src/jsc/bindings/webcore/URLPatternTokenizer.h @@ -67,6 +67,7 @@ class Tokenizer { size_t m_index { 0 }; size_t m_nextIndex { 0 }; char32_t m_codepoint; + bool m_tokenAppendFailure { false }; void getNextCodePoint(); void seekNextCodePoint(size_t index); diff --git a/test/js/web/urlpattern/urlpattern.test.ts b/test/js/web/urlpattern/urlpattern.test.ts index c2a727ab5b15..2edef8129210 100644 --- a/test/js/web/urlpattern/urlpattern.test.ts +++ b/test/js/web/urlpattern/urlpattern.test.ts @@ -1,6 +1,7 @@ // Test data from Web Platform Tests // https://github.com/web-platform-tests/wpt/blob/master/LICENSE.md -import { describe, expect, test } from "bun:test"; +import { setSyntheticAllocationLimitForTesting } from "bun:internal-for-testing"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import testData from "./urlpatterntestdata.json"; const kComponents = ["protocol", "username", "password", "hostname", "port", "pathname", "search", "hash"] as const; @@ -206,4 +207,55 @@ describe("URLPattern", () => { expect(new URLPattern({ pathname: "/a/:foo/:baz([a-z]+)?/b/*" }).hasRegExpGroups).toBe(true); }); }); + + // The tokenizer keeps one token (40 bytes or more) per code point in a + // WTF::Vector, which holds at most 2^31 - 1 bytes. A pattern near 52 million + // characters used to abort the process when that Vector could not grow. The + // bound follows the synthetic allocation limit, so 1 MiB puts it below 64 Ki. + describe("a pattern with more tokens than the token list can hold throws", () => { + const dots = (length: number) => Buffer.alloc(length, ".").toString(); + const message = "URLPattern constructor: Failed to create URLPattern (from input string)"; + const tooLong = 64 * 1024; + const fits = 8 * 1024; + + let originalLimit: number; + beforeAll(() => { + originalLimit = setSyntheticAllocationLimitForTesting(1024 * 1024); + }); + afterAll(() => { + setSyntheticAllocationLimitForTesting(originalLimit); + }); + + test.each(["username", "password", "hostname", "pathname", "search", "hash"] as const)("in %s", component => { + expect(() => new URLPattern({ [component]: dots(tooLong) })).toThrow(message); + }); + + test("in a constructor string", () => { + expect(() => new URLPattern("https://example.com/" + dots(tooLong))).toThrow(message); + expect(new URLPattern("https://example.com/" + dots(fits)).test("https://example.com/" + dots(fits))).toBe(true); + }); + + test("in a pathname inherited from baseURL", () => { + expect(() => new URLPattern({ search: "a", baseURL: "https://example.com/" + dots(tooLong) })).toThrow(message); + }); + + // Find the longest pathname that parses. It must come back whole: the + // End token counts against the bound too, and without it the parser + // would drop the pending text instead of throwing. + test("at the exact bound, and one code point below it parses whole", () => { + let low = fits; + let high = tooLong; + while (low + 1 < high) { + const length = (low + high) >>> 1; + try { + new URLPattern({ pathname: dots(length) }); + low = length; + } catch { + high = length; + } + } + expect(new URLPattern({ pathname: dots(low) }).pathname).toBe(dots(low)); + expect(() => new URLPattern({ pathname: dots(high) })).toThrow(message); + }); + }); });