From 7c8a63898c82893a2cf346f289d319ec98f7c112 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:43:42 +0000 Subject: [PATCH 1/2] ErrorInstance: keep a lock that the error info callback adds to the error materializeErrorInfoIfNeeded() calls VM::onComputeErrorInfoJSValue() and then stores "line", "column", "sourceURL" and "stack" with putDirect(). In Bun the callback runs Error.prepareStackTrace, so user code runs between the two steps. putDirect() checks nothing. It added the properties to an error that the callback froze, and it replaced a "stack" that the callback made non-configurable. Skip each write that an ordinary [[DefineOwnProperty]] would reject: the property exists and is non-configurable, or the property is absent and the error became non-extensible while the callback ran. An error that is non-extensible before the callback still gets the properties, because they count as present from creation. --- .../JavaScriptCore/runtime/ErrorInstance.cpp | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/Source/JavaScriptCore/runtime/ErrorInstance.cpp b/Source/JavaScriptCore/runtime/ErrorInstance.cpp index 7e1dbf9d933eb..4e2054075af33 100644 --- a/Source/JavaScriptCore/runtime/ErrorInstance.cpp +++ b/Source/JavaScriptCore/runtime/ErrorInstance.cpp @@ -441,6 +441,10 @@ bool ErrorInstance::materializeErrorInfoIfNeeded(VM& vm) m_errorInfoMaterialized = true; DeferGCForAWhile deferGC(vm); + // The lazy properties count as present from creation: an error that is already + // non-extensible here still gets them. + bool wasExtensible = isStructureExtensible(); + JSValue stack; if (!m_stackPropertyAlreadyMaterialized) stack = fn(vm, *m_stackTrace.get(), m_lineColumn.line, m_lineColumn.column, m_sourceURL, this, this->bunErrorData()); @@ -452,15 +456,27 @@ bool ErrorInstance::materializeErrorInfoIfNeeded(VM& vm) m_stackString = String(); } - auto attributes = static_cast(PropertyAttribute::DontEnum); - - putDirect(vm, vm.propertyNames->line, jsNumber(m_lineColumn.line), attributes); - putDirect(vm, vm.propertyNames->column, jsNumber(m_lineColumn.column), attributes); + // fn can run user code (Error.prepareStackTrace), which can freeze this error or make one + // of these properties non-configurable. putDirect() checks nothing, so skip each write + // that an ordinary [[DefineOwnProperty]] would reject at this point. + bool becameNonExtensible = wasExtensible && !isStructureExtensible(); + auto putUnlessLocked = [&](PropertyName propertyName, JSValue value) { + unsigned currentAttributes; + if (isValidOffset(structure()->get(vm, propertyName, currentAttributes))) { + if (currentAttributes & PropertyAttribute::DontDelete) + return; + } else if (becameNonExtensible) + return; + putDirect(vm, propertyName, value, static_cast(PropertyAttribute::DontEnum)); + }; + + putUnlessLocked(vm.propertyNames->line, jsNumber(m_lineColumn.line)); + putUnlessLocked(vm.propertyNames->column, jsNumber(m_lineColumn.column)); if (!m_sourceURL.isEmpty()) - putDirect(vm, vm.propertyNames->sourceURL, jsString(vm, WTF::move(m_sourceURL)), attributes); + putUnlessLocked(vm.propertyNames->sourceURL, jsString(vm, WTF::move(m_sourceURL))); if (!m_stackPropertyAlreadyMaterialized) - putDirect(vm, vm.propertyNames->stack, stack, attributes); + putUnlessLocked(vm.propertyNames->stack, stack); return true; } From 6024d7f1511ec40f4eb344fe00cc5af6d6008f70 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 13 Sep 2026 09:42:42 +0000 Subject: [PATCH 2/2] ErrorInstance: a writable non-configurable property still takes the value A property that the callback made non-configurable keeps its attributes. If it is a plain writable data property, as "stack" is after Object.seal(), it still takes the value, because an assignment could store it. Only a read-only property and an accessor are skipped. A sealed error then has the result of Error.prepareStackTrace as its "stack", as in Node. --- Source/JavaScriptCore/runtime/ErrorInstance.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Source/JavaScriptCore/runtime/ErrorInstance.cpp b/Source/JavaScriptCore/runtime/ErrorInstance.cpp index 4e2054075af33..916d5ba0a86f5 100644 --- a/Source/JavaScriptCore/runtime/ErrorInstance.cpp +++ b/Source/JavaScriptCore/runtime/ErrorInstance.cpp @@ -457,17 +457,22 @@ bool ErrorInstance::materializeErrorInfoIfNeeded(VM& vm) } // fn can run user code (Error.prepareStackTrace), which can freeze this error or make one - // of these properties non-configurable. putDirect() checks nothing, so skip each write - // that an ordinary [[DefineOwnProperty]] would reject at this point. + // of these properties non-configurable. putDirect() checks nothing. So a property that is + // absent is not added to an error that became non-extensible, and a non-configurable + // property keeps its attributes. It takes the value only if an assignment could store it. bool becameNonExtensible = wasExtensible && !isStructureExtensible(); auto putUnlessLocked = [&](PropertyName propertyName, JSValue value) { + unsigned attributes = static_cast(PropertyAttribute::DontEnum); unsigned currentAttributes; if (isValidOffset(structure()->get(vm, propertyName, currentAttributes))) { - if (currentAttributes & PropertyAttribute::DontDelete) - return; + if (currentAttributes & PropertyAttribute::DontDelete) { + if (currentAttributes & PropertyAttribute::ReadOnlyOrAccessorOrCustomAccessorOrValue) + return; + attributes = currentAttributes; + } } else if (becameNonExtensible) return; - putDirect(vm, propertyName, value, static_cast(PropertyAttribute::DontEnum)); + putDirect(vm, propertyName, value, attributes); }; putUnlessLocked(vm.propertyNames->line, jsNumber(m_lineColumn.line));