fix: resolve GC segfault in JSYogaNode lifecycle management

- Add missing cellLock() in JSYogaNode::visitOutputConstraints to
  prevent concurrent GC reads of WriteBarriers during mutation
- Clear all WriteBarriers (m_children, m_measureFunc, m_dirtiedFunc,
  m_baselineFunc, m_config) in free() and freeRecursive()
- Empty m_children JSArray in removeAllChildren() to prevent stale
  child references
This commit is contained in:
Ciro Spaciari MacBook
2026-02-03 17:33:03 -08:00
parent 077e636f8f
commit 2c5ee4862d
2 changed files with 32 additions and 2 deletions

View File

@@ -141,6 +141,14 @@ template<typename Visitor>
void JSYogaNode::visitOutputConstraints(JSC::JSCell* cell, Visitor& visitor)
{
auto* thisObject = jsCast<JSYogaNode*>(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);

View File

@@ -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<JSC::JSArray*>(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();
}
}