LibJS: Throw rather than crashing on a deep prototype-chain get

Problem: Converting an object with a pathologically-deep prototype chain
to a primitive was segfaulting.

Cause: Object::internal_get implements [[Get]] by recursing into the
prototype’s [[Get]] (parent->internal_get) when the property isn’t an
own property. For a sufficiently deep prototype chain, that C++
recursion exhausts the native stack, and segfaults. The bytecode
interpreter’s call-stack limit doesn’t cover this native recursion.

Fix: Before recursing into the prototype in Object::internal_get, check
VM::did_reach_stack_space_limit(), and throw a CallStackSizeExceeded
InternalError — the same way the interpreter and other recursive runtime
operations guard the native stack. The deep-chain get now throws a
catchable call-stack-size-exceeded error, rather than crashing.

Fixes https://github.com/LadybirdBrowser/ladybird/issues/3584
This commit is contained in:
sideshowbarker 2026-06-20 17:08:46 +09:00 committed by Shannon Booth
parent a237fbc24f
commit d7c08964cb
2 changed files with 15 additions and 0 deletions

View file

@ -1050,6 +1050,9 @@ ThrowCompletionOr<Value> Object::internal_get(PropertyKey const& property_key, V
return js_undefined();
// c. Return ? parent.[[Get]](P, Receiver).
// AD-HOC: Avoid a native stack overflow when walking a pathologically-deep prototype chain.
if (vm.did_reach_stack_space_limit()) [[unlikely]]
return vm.throw_completion<InternalError>(ErrorType::CallStackSizeExceeded);
return parent->internal_get(property_key, receiver, cacheable_metadata, PropertyLookupPhase::PrototypeChain);
}

View file

@ -0,0 +1,12 @@
// https://github.com/LadybirdBrowser/ladybird/issues/3584
// Converting a pathologically-deep-prototype-chain object to a primitive used to recurse Object::internal_get til the
// native stack overflowed. The code now throws a catchable "call stack size exceeded" error instead.
test("converting an object with a very deep prototype chain to a primitive does not crash", () => {
// Object.create() sets the prototype at creation without a cycle check — so the chain is built in linear time.
let object = {};
for (let i = 0; i < 1_000_000; ++i) object = Object.create(object);
expect(() => {
Number(object);
}).toThrowWithMessage(InternalError, "Call stack size limit exceeded");
});