From a427c978a35592376274ec019571d851cc8cac9d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:04:33 +0000 Subject: [PATCH] ErrorInstance: keep the captured stack frames alive until the error info is materialized ErrorInstance held its captured frames weakly. When a callee or code block in the trace died before the first .stack read, reconcileWeakReferencesAtGCEnd pre-rendered the stack string from the GC end phase through VM::onComputeErrorInfo. That callback gets no error instance and cannot run JS, so the cached string had a bare "Error" header with no message, Error.prepareStackTrace never ran, and no call sites existed. The first .stack read then served that string. Under USE(BUN_JSC_ADDITIONS), ErrorInstance::visitChildren now visits every frame's callee and code block, the way Exception already does. It holds the cell lock because the mutator replaces the vector under that lock. The frames stay alive until materializeErrorInfoIfNeeded drops them or the error dies, which is what V8 does with CallSiteInfo. The first .stack read then always takes the normal path with live frames. The weak reconciliation loop stays as a fallback for a frame stored without a write barrier and asserts in debug builds. Needed for oven-sh/bun#34398. --- .../JavaScriptCore/runtime/ErrorInstance.cpp | 29 +++++++++++++++++++ Source/JavaScriptCore/runtime/ErrorInstance.h | 4 +++ 2 files changed, 33 insertions(+) diff --git a/Source/JavaScriptCore/runtime/ErrorInstance.cpp b/Source/JavaScriptCore/runtime/ErrorInstance.cpp index 7e1dbf9d933e..98a15017d10a 100644 --- a/Source/JavaScriptCore/runtime/ErrorInstance.cpp +++ b/Source/JavaScriptCore/runtime/ErrorInstance.cpp @@ -136,6 +136,30 @@ size_t ErrorInstance::estimatedSize(JSCell* cell, VM& vm) return Base::estimatedSize(cell, vm) + size; } +#if USE(BUN_JSC_ADDITIONS) +// The captured frames stay reachable until the error info is materialized or the error dies, +// like V8's CallSiteInfo. The first .stack read then always sees live callees and code blocks: +// the embedder's onComputeErrorInfoJSValue callback can build call sites and call +// Error.prepareStackTrace on them, and the header is computed from the current name and message. +// Upstream keeps the frames weak and pre-renders a frames-only string in +// reconcileWeakReferencesAtGCEnd when one of them dies, which drops all of that. +template +void ErrorInstance::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + ErrorInstance* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + + Locker locker { thisObject->cellLock() }; + if (thisObject->m_stackTrace) { + for (StackFrame& frame : *thisObject->m_stackTrace) + frame.visitAggregate(visitor); + } +} + +DEFINE_VISIT_CHILDREN(ErrorInstance); +#endif + void ErrorInstance::captureStackTrace(VM& vm, JSGlobalObject* globalObject, size_t framesToSkip, bool append) { { @@ -385,8 +409,13 @@ void ErrorInstance::reconcileWeakReferencesAtGCEnd(VM& vm, CollectionScope) // get caught in a trace. // Since the frames are weak, a dead one means the trace can no longer be reconstructed, so // materialize it into strings while it is still readable. + // With USE(BUN_JSC_ADDITIONS), visitChildren keeps the frames of a live error marked, so a dead + // frame here means it was stored without a write barrier. for (const auto& frame : *m_stackTrace.get()) { if (!frame.isMarked(vm)) { +#if USE(BUN_JSC_ADDITIONS) + ASSERT_NOT_REACHED(); +#endif computeErrorInfo(vm, false); return; } diff --git a/Source/JavaScriptCore/runtime/ErrorInstance.h b/Source/JavaScriptCore/runtime/ErrorInstance.h index e7524ea619a8..94cef67caf3e 100644 --- a/Source/JavaScriptCore/runtime/ErrorInstance.h +++ b/Source/JavaScriptCore/runtime/ErrorInstance.h @@ -52,6 +52,10 @@ class ErrorInstance : public JSNonFinalObject { DECLARE_EXPORT_INFO; +#if USE(BUN_JSC_ADDITIONS) + DECLARE_VISIT_CHILDREN; +#endif + static size_t estimatedSize(JSCell*, VM&); inline static Structure* createStructure(VM&, JSGlobalObject*, JSValue);