Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
c1b80836e0 Add stack overflow checks to URL pathname and string conversion
This prevents crashes when setting URL properties (like pathname) to objects
with deeply recursive toString() methods.

The fix adds stack safety checks in two key locations:
1. URL pathname setter - checks stack before attempting string conversion
2. String conversion functions (valueToUSVString, valueToByteString, etc.) -
   checks stack before calling toWTFString/toString

When the stack is near its limit, a RangeError is thrown instead of crashing
with an ASAN assertion failure or stack overflow.

Tests verify that:
- Limited recursion (100 calls) throws a proper error instead of crashing
- toString methods that modify URLs work correctly
- Deep recursion (10000 calls) either succeeds or throws RangeError

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 19:58:29 +00:00
3 changed files with 115 additions and 0 deletions

View File

@@ -23,6 +23,7 @@
#include "JSDOMConvertStrings.h"
#include "JSDOMExceptionHandling.h"
#include <JavaScriptCore/ExceptionHelpers.h>
#include <JavaScriptCore/HeapInlines.h>
#include <JavaScriptCore/JSCJSValueInlines.h>
#include <wtf/text/StringBuilder.h>
@@ -68,6 +69,12 @@ String valueToByteString(JSGlobalObject& lexicalGlobalObject, JSValue value)
VM& vm = lexicalGlobalObject.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// Check for stack overflow before attempting string conversion
if (!vm.isSafeToRecurseSoft()) [[unlikely]] {
scope.throwException(&lexicalGlobalObject, createStackOverflowError(&lexicalGlobalObject));
return {};
}
auto string = value.toWTFString(&lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
@@ -81,6 +88,12 @@ AtomString valueToByteAtomString(JSC::JSGlobalObject& lexicalGlobalObject, JSC::
VM& vm = lexicalGlobalObject.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// Check for stack overflow before attempting string conversion
if (!vm.isSafeToRecurseSoft()) [[unlikely]] {
scope.throwException(&lexicalGlobalObject, createStackOverflowError(&lexicalGlobalObject));
return nullAtom();
}
AtomString string = value.toString(&lexicalGlobalObject)->toAtomString(&lexicalGlobalObject).data;
RETURN_IF_EXCEPTION(scope, {});
@@ -100,6 +113,13 @@ String valueToUSVString(JSGlobalObject& lexicalGlobalObject, JSValue value)
VM& vm = lexicalGlobalObject.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// Check for stack overflow before attempting string conversion
// This prevents crashes from deeply recursive toString() calls
if (!vm.isSafeToRecurseSoft()) [[unlikely]] {
scope.throwException(&lexicalGlobalObject, createStackOverflowError(&lexicalGlobalObject));
return {};
}
auto string = value.toWTFString(&lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
@@ -111,6 +131,12 @@ AtomString valueToUSVAtomString(JSGlobalObject& lexicalGlobalObject, JSValue val
VM& vm = lexicalGlobalObject.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// Check for stack overflow before attempting string conversion
if (!vm.isSafeToRecurseSoft()) [[unlikely]] {
scope.throwException(&lexicalGlobalObject, createStackOverflowError(&lexicalGlobalObject));
return nullAtom();
}
auto string = value.toString(&lexicalGlobalObject)->toAtomString(&lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});

View File

@@ -533,6 +533,14 @@ static inline bool setJSDOMURL_pathnameSetter(JSGlobalObject& lexicalGlobalObjec
auto& vm = JSC::getVM(&lexicalGlobalObject);
UNUSED_PARAM(vm);
auto throwScope = DECLARE_THROW_SCOPE(vm);
// Check for potential stack overflow before string conversion
// This prevents crashes from deeply recursive toString() calls
if (!vm.isSafeToRecurseSoft()) [[unlikely]] {
throwScope.throwException(&lexicalGlobalObject, JSC::createStackOverflowError(&lexicalGlobalObject));
return false;
}
auto& impl = thisObject.wrapped();
auto nativeValue = convert<IDLUSVString>(lexicalGlobalObject, value);
RETURN_IF_EXCEPTION(throwScope, false);

View File

@@ -0,0 +1,81 @@
import { expect, test } from "bun:test";
test("URL pathname should handle limited recursive toString without crashing", () => {
// This test verifies that setting a URL pathname to an object with a
// recursive toString() function that has reasonable depth limits
// doesn't cause a crash.
let callCount = 0;
const maxCalls = 100;
function createRecursiveToString() {
callCount++;
if (callCount > maxCalls) {
throw new Error(`Recursion limit reached: ${maxCalls}`);
}
const url = new URL("https://example.com/");
const blob = new Blob();
const stream = blob.stream().cancel(blob);
stream.toString = createRecursiveToString;
url.pathname = stream;
return createRecursiveToString;
}
// This should throw an error when the limit is reached, not crash
expect(() => createRecursiveToString()).toThrow("Recursion limit reached");
});
test("URL pathname with toString that modifies URL", () => {
// Test a simpler case where toString causes recursion
const url = new URL("https://example.com/");
let toStringCalled = false;
const obj = {
toString() {
toStringCalled = true;
// Try to access the URL again during toString conversion
url.pathname = "/nested";
return "test";
},
};
// This should not crash and should handle the recursion gracefully
expect(() => {
url.pathname = obj;
}).not.toThrow();
// The pathname should be set to the result of toString()
expect(url.pathname).toBe("/test");
expect(toStringCalled).toBe(true);
});
test("URL pathname with deep toString recursion should eventually throw", () => {
// Test that a custom toString that recurses very deeply
// eventually hits a stack limit
let callCount = 0;
const maxCalls = 10000;
const obj = {
toString() {
callCount++;
if (callCount < maxCalls) {
// Recurse deeply
return obj.toString();
}
return "done";
},
};
const url = new URL("https://example.com/");
// Should either succeed (if stack is deep enough) or throw RangeError
try {
url.pathname = obj;
// If we get here, the stack was deep enough
expect(callCount).toBe(maxCalls);
expect(url.pathname).toBe("/done");
} catch (e) {
// Should be a stack overflow error
expect(e.name).toBe("RangeError");
expect(e.message).toContain("call stack");
}
});