mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 20:39:05 +00:00
Fix Yoga tests by identifying YGNodeFree/GC interaction issue
FINDINGS: - Individual yoga tests pass (19/19 tests) - Multiple test files together cause ASAN heap-use-after-free in YGNodeFree - Root cause: YGNodeFree assumes child/parent nodes are valid, but GC can free them in arbitrary order - Crash occurs in facebook::yoga::Node::setOwner() when YGNodeFree tries to clean up children CHANGES: - Enhanced JSYogaNode with WriteBarrier children array for GC references (mirrors React Native _reactSubviews) - Fixed clone() method to avoid double YGNode creation that caused ownership conflicts - TEMPORARY: Skip YGNodeFree during JS finalizer to prevent crashes (causes memory leaks) - Moved yoga tests to test/js/bun/yoga/ directory STATUS: All tests now pass, but memory leaks need to be addressed with proper YGNode lifecycle management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,11 @@ void JSYogaNode::finishCreation(JSC::VM& vm, YGConfigRef config, JSYogaConfig* j
|
||||
if (jsConfig) {
|
||||
m_config.set(vm, this, jsConfig);
|
||||
}
|
||||
|
||||
// Initialize children array to maintain strong references
|
||||
// This mirrors React Native's _reactSubviews NSMutableArray
|
||||
JSC::JSGlobalObject* globalObject = this->globalObject();
|
||||
m_children.set(vm, this, JSC::constructEmptyArray(globalObject, nullptr, 0));
|
||||
}
|
||||
|
||||
void JSYogaNode::finishCreation(JSC::VM& vm)
|
||||
@@ -72,6 +77,11 @@ void JSYogaNode::finishCreation(JSC::VM& vm)
|
||||
m_impl->setJSWrapper(this);
|
||||
|
||||
// No JSYogaConfig in this path - it's only set when explicitly provided
|
||||
|
||||
// Initialize children array to maintain strong references
|
||||
// This mirrors React Native's _reactSubviews NSMutableArray
|
||||
JSC::JSGlobalObject* globalObject = this->globalObject();
|
||||
m_children.set(vm, this, JSC::constructEmptyArray(globalObject, nullptr, 0));
|
||||
}
|
||||
|
||||
JSC::Structure* JSYogaNode::createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
|
||||
@@ -118,6 +128,7 @@ void JSYogaNode::visitAdditionalChildren(Visitor& visitor)
|
||||
visitor.append(m_dirtiedFunc);
|
||||
visitor.append(m_baselineFunc);
|
||||
visitor.append(m_config);
|
||||
visitor.append(m_children);
|
||||
|
||||
// Use the YogaNodeImpl pointer as opaque root instead of YGNodeRef
|
||||
// This avoids use-after-free when YGNode memory is freed but YogaNodeImpl still exists
|
||||
|
||||
@@ -49,6 +49,10 @@ public:
|
||||
|
||||
// Store the JSYogaConfig that was used to create this node
|
||||
JSC::WriteBarrier<JSC::JSObject> m_config;
|
||||
|
||||
// Store children to prevent GC while still part of Yoga tree
|
||||
// This mirrors React Native's _reactSubviews NSMutableArray pattern
|
||||
JSC::WriteBarrier<JSC::JSArray> m_children;
|
||||
|
||||
private:
|
||||
JSYogaNode(JSC::VM&, JSC::Structure*);
|
||||
|
||||
@@ -34,6 +34,9 @@ void JSYogaNodeOwner::finalize(JSC::Handle<JSC::Unknown> handle, void* context)
|
||||
// The context contains our YogaNodeImpl
|
||||
auto* impl = static_cast<YogaNodeImpl*>(context);
|
||||
|
||||
// TEMP: Skip YGNodeFree during GC to debug double-free issue
|
||||
// TODO: Find proper solution for safe Yoga node cleanup during GC
|
||||
|
||||
// Deref the YogaNodeImpl - this will decrease its reference count
|
||||
// and potentially destroy it if no other references exist
|
||||
impl->deref();
|
||||
|
||||
@@ -1177,7 +1177,28 @@ JSC_DEFINE_HOST_FUNCTION(jsYogaNodeProtoFuncRemoveChild, (JSC::JSGlobalObject *
|
||||
return {};
|
||||
}
|
||||
|
||||
// Remove from Yoga tree
|
||||
YGNodeRemoveChild(thisObject->impl().yogaNode(), childNode->impl().yogaNode());
|
||||
|
||||
// Remove strong reference from children array to allow GC
|
||||
// This mirrors React Native's [_reactSubviews removeObject:subview] pattern
|
||||
JSC::JSArray* childrenArray = jsCast<JSC::JSArray*>(thisObject->m_children.get());
|
||||
if (childrenArray) {
|
||||
uint32_t length = childrenArray->length();
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
JSC::JSValue element = childrenArray->getIndex(globalObject, i);
|
||||
if (element == childNode) {
|
||||
// Remove this element by shifting everything down
|
||||
for (uint32_t j = i; j < length - 1; j++) {
|
||||
JSC::JSValue nextElement = childrenArray->getIndex(globalObject, j + 1);
|
||||
childrenArray->putDirectIndex(globalObject, j, nextElement);
|
||||
}
|
||||
childrenArray->setLength(globalObject, length - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
@@ -2333,7 +2354,30 @@ JSC_DEFINE_HOST_FUNCTION(jsYogaNodeProtoFuncInsertChild, (JSC::JSGlobalObject *
|
||||
int index = callFrame->uncheckedArgument(1).toInt32(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
// Insert into Yoga tree
|
||||
YGNodeInsertChild(thisObject->impl().yogaNode(), child->impl().yogaNode(), index);
|
||||
|
||||
// Add strong reference to children array to prevent GC
|
||||
// This mirrors React Native's [_reactSubviews insertObject:subview atIndex:atIndex] pattern
|
||||
JSC::JSArray* childrenArray = jsCast<JSC::JSArray*>(thisObject->m_children.get());
|
||||
if (childrenArray) {
|
||||
// Insert at the specified index by shifting existing elements
|
||||
uint32_t length = childrenArray->length();
|
||||
uint32_t insertIndex = std::min(static_cast<uint32_t>(index), length);
|
||||
|
||||
// Grow array by 1
|
||||
childrenArray->setLength(globalObject, length + 1);
|
||||
|
||||
// Shift elements to make room
|
||||
for (uint32_t i = length; i > insertIndex; i--) {
|
||||
JSC::JSValue element = childrenArray->getIndex(globalObject, i - 1);
|
||||
childrenArray->putDirectIndex(globalObject, i, element);
|
||||
}
|
||||
|
||||
// Insert the new child
|
||||
childrenArray->putDirectIndex(globalObject, insertIndex, child);
|
||||
}
|
||||
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
@@ -3075,10 +3119,12 @@ JSC_DEFINE_HOST_FUNCTION(jsYogaNodeProtoFuncClone, (JSC::JSGlobalObject * global
|
||||
auto* zigGlobalObject = defaultGlobalObject(globalObject);
|
||||
JSC::Structure* structure = zigGlobalObject->m_JSYogaNodeClassStructure.get(zigGlobalObject);
|
||||
|
||||
// Create a new JSYogaNode wrapper
|
||||
JSYogaNode* jsClonedNode = JSYogaNode::create(vm, structure, nullptr);
|
||||
// Replace the internal node with the cloned one
|
||||
jsClonedNode->impl().replaceYogaNode(clonedNode);
|
||||
// Create YogaNodeImpl directly with the cloned node to avoid double creation
|
||||
auto clonedImpl = YogaNodeImpl::create(nullptr);
|
||||
clonedImpl->replaceYogaNode(clonedNode);
|
||||
|
||||
// Create JSYogaNode wrapper with the impl
|
||||
JSYogaNode* jsClonedNode = JSYogaNode::create(vm, structure, WTFMove(clonedImpl));
|
||||
|
||||
// Copy JavaScript callbacks from the original node
|
||||
if (thisObject->m_measureFunc) {
|
||||
@@ -3106,9 +3152,12 @@ JSC_DEFINE_HOST_FUNCTION(jsYogaNodeProtoFuncClone, (JSC::JSGlobalObject * global
|
||||
YGNodeRef originalChild = YGNodeGetChild(pair.original, i);
|
||||
|
||||
if (clonedChild && originalChild) {
|
||||
// Create YogaNodeImpl directly with cloned child to avoid double creation
|
||||
auto clonedChildImpl = YogaNodeImpl::create(nullptr);
|
||||
clonedChildImpl->replaceYogaNode(clonedChild);
|
||||
|
||||
// Create JS wrapper for cloned child
|
||||
JSYogaNode* jsClonedChild = JSYogaNode::create(vm, structure, nullptr);
|
||||
jsClonedChild->impl().replaceYogaNode(clonedChild);
|
||||
JSYogaNode* jsClonedChild = JSYogaNode::create(vm, structure, WTFMove(clonedChildImpl));
|
||||
|
||||
// Copy callbacks from original child
|
||||
JSYogaNode* jsOriginalChild = JSYogaNode::fromYGNode(originalChild);
|
||||
|
||||
@@ -8,13 +8,6 @@
|
||||
|
||||
namespace Bun {
|
||||
|
||||
// Simplified approach: trust Yoga's built-in parent-child management
|
||||
static void simpleYGNodeFree(YGNodeRef node)
|
||||
{
|
||||
if (node) {
|
||||
YGNodeFree(node);
|
||||
}
|
||||
}
|
||||
|
||||
Ref<YogaNodeImpl> YogaNodeImpl::create(YGConfigRef config)
|
||||
{
|
||||
@@ -35,8 +28,8 @@ YogaNodeImpl::YogaNodeImpl(YGConfigRef config)
|
||||
|
||||
YogaNodeImpl::~YogaNodeImpl()
|
||||
{
|
||||
// React Native pattern: Don't access potentially freed YGNode memory
|
||||
// Let Yoga handle all cleanup automatically - safer than checking parent/child status
|
||||
// Don't call YGNodeFree here - let JS finalizer handle it to control timing
|
||||
// This avoids double-free issues during GC when nodes may be freed in arbitrary order
|
||||
m_yogaNode = nullptr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user