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:
parent
a237fbc24f
commit
d7c08964cb
2 changed files with 15 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
Loading…
Reference in a new issue