diff --git a/src/bun.js/bindings/JSYogaNode.cpp b/src/bun.js/bindings/JSYogaNode.cpp index a3102abd2c..29ee05b674 100644 --- a/src/bun.js/bindings/JSYogaNode.cpp +++ b/src/bun.js/bindings/JSYogaNode.cpp @@ -141,6 +141,14 @@ template void JSYogaNode::visitOutputConstraints(JSC::JSCell* cell, Visitor& visitor) { auto* thisObject = jsCast(cell); + + // Lock for concurrent GC thread safety - the mutator thread may be modifying + // WriteBarriers (m_children, m_measureFunc, etc.) concurrently via insertChild, + // removeChild, setMeasureFunc, free(), etc. Without this lock, the GC thread + // can read a torn/partially-written pointer from a WriteBarrier, leading to + // a segfault in validateCell when it tries to decode a corrupted StructureID. + WTF::Locker locker { thisObject->cellLock() }; + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitOutputConstraints(thisObject, visitor); diff --git a/src/bun.js/bindings/JSYogaPrototype.cpp b/src/bun.js/bindings/JSYogaPrototype.cpp index db6db250ab..44986e5d4f 100644 --- a/src/bun.js/bindings/JSYogaPrototype.cpp +++ b/src/bun.js/bindings/JSYogaPrototype.cpp @@ -864,6 +864,15 @@ JSC_DEFINE_HOST_FUNCTION(jsYogaNodeProtoFuncFree, (JSC::JSGlobalObject * globalO // Lifecycle managed by RefCounted } + // Clear all WriteBarriers so the GC doesn't try to visit stale pointers. + // Without this, a freed node's visitAdditionalChildren could attempt to + // visit objects that are no longer valid. + thisObject->m_children.clear(); + thisObject->m_measureFunc.clear(); + thisObject->m_dirtiedFunc.clear(); + thisObject->m_baselineFunc.clear(); + thisObject->m_config.clear(); + return JSC::JSValue::encode(JSC::jsUndefined()); } @@ -3100,6 +3109,15 @@ JSC_DEFINE_HOST_FUNCTION(jsYogaNodeProtoFuncRemoveAllChildren, (JSC::JSGlobalObj CHECK_YOGA_NODE_FREED(thisObject); YGNodeRemoveAllChildren(thisObject->impl().yogaNode()); + + // Clear the children array to release strong references, matching the Yoga tree state. + // Without this, the m_children JSArray retains stale references to removed children. + if (thisObject->m_children) { + JSC::JSArray* childrenArray = jsCast(thisObject->m_children.get()); + if (childrenArray) + childrenArray->setLength(globalObject, 0); + } + return JSC::JSValue::encode(JSC::jsUndefined()); } @@ -3160,10 +3178,14 @@ JSC_DEFINE_HOST_FUNCTION(jsYogaNodeProtoFuncFreeRecursive, (JSC::JSGlobalObject } } - // Clear the JS wrapper for this node + // Clear the JS wrapper's WriteBarriers so the GC doesn't visit stale pointers JSYogaNode* jsNode = JSYogaNode::fromYGNode(currentNode); if (jsNode) { - // Lifecycle managed by RefCounted + jsNode->m_children.clear(); + jsNode->m_measureFunc.clear(); + jsNode->m_dirtiedFunc.clear(); + jsNode->m_baselineFunc.clear(); + jsNode->m_config.clear(); } }