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
20 changes: 20 additions & 0 deletions src/jsc/bindings/VectorSizeLimit.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include "root.h"
#include <wtf/Vector.h>

extern "C" size_t Bun__stringSyntheticAllocationLimit;

namespace Bun {

// The most elements a Vector<T> can hold, lowered by Bun__stringSyntheticAllocationLimit so tests reach it cheaply.
template<typename T>
size_t maxVectorSize()
{
constexpr size_t maxBytes = std::numeric_limits<unsigned>::max() >> 1;
static_assert(WTF::isValidCapacityForVector<T>(maxBytes / sizeof(T)));
static_assert(!WTF::isValidCapacityForVector<T>(maxBytes / sizeof(T) + 1));
return std::min(maxBytes, Bun__stringSyntheticAllocationLimit) / sizeof(T);
}

} // namespace Bun
13 changes: 10 additions & 3 deletions src/jsc/bindings/webcore/URLPatternTokenizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

#include "ExceptionOr.h"
#include "URLPatternParser.h"
#include "VectorSizeLimit.h"
#include <unicode/utf16.h>
#include <wtf/text/MakeString.h>

Expand Down Expand Up @@ -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<Token>() || !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
Expand Down Expand Up @@ -109,7 +112,7 @@ ExceptionOr<Vector<Token>> Tokenizer::tokenize()
{
ExceptionOr<void> 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();

Expand Down Expand Up @@ -266,6 +269,10 @@ ExceptionOr<Vector<Token>> 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);
}

Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/webcore/URLPatternTokenizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
54 changes: 53 additions & 1 deletion test/js/web/urlpattern/urlpattern.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
});
});
});