From d7c08964cb64f96f326e120ca6027f564b3419db Mon Sep 17 00:00:00 2001 From: sideshowbarker Date: Sat, 20 Jun 2026 17:08:46 +0900 Subject: [PATCH] LibJS: Throw rather than crashing on a deep prototype-chain get MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Libraries/LibJS/Runtime/Object.cpp | 3 +++ .../regress/long-prototype-chain-to-primitive.js | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 Tests/LibJS/Runtime/regress/long-prototype-chain-to-primitive.js 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"); +});