mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Implement WebKit GC integration for Yoga Node/Config classes
- Replace DECLARE_VISIT_CHILDREN with visitAdditionalChildren pattern for proper GC integration - Implement visitOutputConstraints for objects with volatile marking behavior (following WebKit guide) - Add opaque root management for YogaNodeImpl* pointers to ensure GC reachability - Create separate JSYogaConfigOwner to fix WeakHandleOwner type confusion bug - Fix ownership tracking with m_ownsYogaNode flag to prevent double-freeing during cloning - Add safe YGNodeFree tracking to prevent heap-use-after-free in complex scenarios - Implement hierarchy-aware node freeing (only free root nodes, let Yoga handle children) - Individual Yoga tests pass; multi-test scenarios have remaining ASAN issues under investigation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -23,13 +23,9 @@ void JSYogaConfigOwner::finalize(JSC::Handle<JSC::Unknown> handle, void* context
|
||||
bool JSYogaConfigOwner::isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown> handle, void* context, JSC::AbstractSlotVisitor& visitor, ASCIILiteral* reason)
|
||||
{
|
||||
UNUSED_PARAM(handle);
|
||||
|
||||
auto* impl = static_cast<YogaConfigImpl*>(context);
|
||||
|
||||
UNUSED_PARAM(context);
|
||||
// YogaConfig doesn't currently use opaque roots, so always return false
|
||||
// This allows normal GC collection based on JS reference reachability
|
||||
fprintf(stderr, "[DEBUG] JSYogaConfigOwner::isReachableFromOpaqueRoots called for YogaConfigImpl %p, reachable: false\n", impl);
|
||||
|
||||
if (reason)
|
||||
*reason = "YogaConfig not using opaque roots"_s;
|
||||
|
||||
|
||||
@@ -117,8 +117,6 @@ JSC::GCClient::IsoSubspace* JSYogaNode::subspaceFor(JSC::VM& vm)
|
||||
template<typename Visitor>
|
||||
void JSYogaNode::visitAdditionalChildren(Visitor& visitor)
|
||||
{
|
||||
fprintf(stderr, "[DEBUG] JSYogaNode::visitAdditionalChildren called for %p\n", this);
|
||||
|
||||
visitor.append(m_measureFunc);
|
||||
visitor.append(m_dirtiedFunc);
|
||||
visitor.append(m_baselineFunc);
|
||||
@@ -126,7 +124,6 @@ void JSYogaNode::visitAdditionalChildren(Visitor& visitor)
|
||||
|
||||
// Use the YogaNodeImpl pointer as opaque root instead of YGNodeRef
|
||||
// This avoids use-after-free when YGNode memory is freed but YogaNodeImpl still exists
|
||||
fprintf(stderr, "[DEBUG] JSYogaNode::visitAdditionalChildren adding YogaNodeImpl %p as opaque root for JSYogaNode %p\n", &m_impl.get(), this);
|
||||
visitor.addOpaqueRoot(&m_impl.get());
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,6 @@ void JSYogaNodeOwner::finalize(JSC::Handle<JSC::Unknown> handle, void* context)
|
||||
// The context contains our YogaNodeImpl
|
||||
auto* impl = static_cast<YogaNodeImpl*>(context);
|
||||
|
||||
fprintf(stderr, "[DEBUG] JSYogaNodeOwner::finalize called for YogaNodeImpl %p\n", impl);
|
||||
|
||||
// Deref the YogaNodeImpl - this will decrease its reference count
|
||||
// and potentially destroy it if no other references exist
|
||||
impl->deref();
|
||||
@@ -49,9 +47,6 @@ bool JSYogaNodeOwner::isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown> handl
|
||||
|
||||
// Check if the YogaNodeImpl itself is reachable as opaque root
|
||||
bool reachable = visitor.containsOpaqueRoot(impl);
|
||||
fprintf(stderr, "[DEBUG] JSYogaNodeOwner::isReachableFromOpaqueRoots called for YogaNodeImpl %p, reachable: %s\n",
|
||||
impl, reachable ? "true" : "false");
|
||||
|
||||
if (reason)
|
||||
*reason = "YogaNode reachable from root"_s;
|
||||
|
||||
|
||||
@@ -32,10 +32,7 @@ void YogaConfigImpl::setJSWrapper(JSYogaConfig* wrapper)
|
||||
// This prevents ref count leaks if setJSWrapper is called multiple times
|
||||
if (!m_wrapper) {
|
||||
// Increment ref count for the weak handle context
|
||||
fprintf(stderr, "[DEBUG] YogaConfigImpl::setJSWrapper %p calling ref() for JS wrapper %p\n", this, wrapper);
|
||||
this->ref();
|
||||
} else {
|
||||
fprintf(stderr, "[DEBUG] YogaConfigImpl::setJSWrapper %p already has wrapper, replacing with %p\n", this, wrapper);
|
||||
}
|
||||
|
||||
// Create weak reference with our JS owner
|
||||
|
||||
@@ -3,9 +3,27 @@
|
||||
#include "JSYogaConfig.h"
|
||||
#include "JSYogaNodeOwner.h"
|
||||
#include <yoga/Yoga.h>
|
||||
#include <wtf/HashSet.h>
|
||||
#include <wtf/Lock.h>
|
||||
|
||||
namespace Bun {
|
||||
|
||||
// Global set to track freed YGNodes to prevent double-freeing
|
||||
static Lock s_freedNodesLock;
|
||||
static HashSet<void*> s_freedNodes;
|
||||
|
||||
static void safeYGNodeFree(YGNodeRef node) {
|
||||
if (!node) return;
|
||||
|
||||
Locker locker { s_freedNodesLock };
|
||||
if (s_freedNodes.contains(node)) {
|
||||
return; // Already freed
|
||||
}
|
||||
|
||||
s_freedNodes.add(node);
|
||||
YGNodeFree(node);
|
||||
}
|
||||
|
||||
Ref<YogaNodeImpl> YogaNodeImpl::create(YGConfigRef config, JSYogaConfig* jsConfig)
|
||||
{
|
||||
return adoptRef(*new YogaNodeImpl(config, jsConfig));
|
||||
@@ -13,6 +31,7 @@ Ref<YogaNodeImpl> YogaNodeImpl::create(YGConfigRef config, JSYogaConfig* jsConfi
|
||||
|
||||
YogaNodeImpl::YogaNodeImpl(YGConfigRef config, JSYogaConfig* jsConfig)
|
||||
: m_jsConfig(jsConfig)
|
||||
, m_ownsYogaNode(true)
|
||||
{
|
||||
if (config) {
|
||||
m_yogaNode = YGNodeNewWithConfig(config);
|
||||
@@ -30,13 +49,14 @@ YogaNodeImpl::~YogaNodeImpl()
|
||||
// Clear the context pointer to avoid callbacks during cleanup
|
||||
YGNodeSetContext(m_yogaNode, nullptr);
|
||||
|
||||
// Remove from parent to avoid use-after-free when parent tries to clear owner
|
||||
YGNodeRef parent = YGNodeGetParent(m_yogaNode);
|
||||
if (parent) {
|
||||
YGNodeRemoveChild(parent, m_yogaNode);
|
||||
// Only free the node if we own it and it has no parent
|
||||
// Nodes with parents should be freed when the parent is freed
|
||||
if (m_ownsYogaNode) {
|
||||
YGNodeRef parent = YGNodeGetParent(m_yogaNode);
|
||||
if (!parent) {
|
||||
safeYGNodeFree(m_yogaNode);
|
||||
}
|
||||
}
|
||||
|
||||
YGNodeFree(m_yogaNode);
|
||||
m_yogaNode = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -47,10 +67,7 @@ void YogaNodeImpl::setJSWrapper(JSYogaNode* wrapper)
|
||||
// This prevents ref count leaks if setJSWrapper is called multiple times
|
||||
if (!m_wrapper) {
|
||||
// Increment ref count for the weak handle context
|
||||
fprintf(stderr, "[DEBUG] YogaNodeImpl::setJSWrapper %p calling ref() for JS wrapper %p\n", this, wrapper);
|
||||
this->ref();
|
||||
} else {
|
||||
fprintf(stderr, "[DEBUG] YogaNodeImpl::setJSWrapper %p already has wrapper, replacing with %p\n", this, wrapper);
|
||||
}
|
||||
|
||||
// Create weak reference with our JS owner
|
||||
@@ -84,11 +101,16 @@ void YogaNodeImpl::replaceYogaNode(YGNodeRef newNode)
|
||||
{
|
||||
if (m_yogaNode) {
|
||||
YGNodeSetContext(m_yogaNode, nullptr);
|
||||
YGNodeFree(m_yogaNode);
|
||||
// Only free the old node if we owned it
|
||||
if (m_ownsYogaNode) {
|
||||
safeYGNodeFree(m_yogaNode);
|
||||
}
|
||||
}
|
||||
m_yogaNode = newNode;
|
||||
if (newNode) {
|
||||
YGNodeSetContext(newNode, this);
|
||||
// Cloned nodes are owned by us - YGNodeClone creates a new node we must free
|
||||
m_ownsYogaNode = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ private:
|
||||
YGNodeRef m_yogaNode;
|
||||
JSC::Weak<JSYogaNode> m_wrapper;
|
||||
JSYogaConfig* m_jsConfig;
|
||||
bool m_ownsYogaNode; // Track if we should free the YGNode in destructor
|
||||
};
|
||||
|
||||
} // namespace Bun
|
||||
|
||||
Reference in New Issue
Block a user