diff --git a/Libraries/LibJS/Runtime/Object.cpp b/Libraries/LibJS/Runtime/Object.cpp index 7c47ef7112..054f3ddb66 100644 --- a/Libraries/LibJS/Runtime/Object.cpp +++ b/Libraries/LibJS/Runtime/Object.cpp @@ -1050,6 +1050,9 @@ ThrowCompletionOr 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(ErrorType::CallStackSizeExceeded); return parent->internal_get(property_key, receiver, cacheable_metadata, PropertyLookupPhase::PrototypeChain); } diff --git a/Tests/LibJS/Runtime/regress/long-prototype-chain-to-primitive.js b/Tests/LibJS/Runtime/regress/long-prototype-chain-to-primitive.js new file mode 100644 index 0000000000..74ac4612d8 --- /dev/null +++ b/Tests/LibJS/Runtime/regress/long-prototype-chain-to-primitive.js @@ -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"); +});