From 6748ad7c419cbf71cb02337cb9310d9a007e465f Mon Sep 17 00:00:00 2001 From: Chen Bin Date: Sun, 12 Apr 2026 17:04:20 +0800 Subject: [PATCH 1/4] Fix use-after-free in StopWorld joint destruction Replace per-body `DestroyBody` loop in `StopWorld()` with 3-phase bulk cleanup to eliminate use-after-free when `DestroyBody` calls `DestroyJoint` on joints whose memory has already been freed by a previous `DestroyBody`. The crash is deterministic with gear joints (`m_bodyA == m_bodyB` causes both joint edges to register in the same body's list, so `DestroyJoint` corrupts the list being iterated) and probabilistic with normal joints (depends on GC timing and `b2BlockAllocator` memory reuse zeroing the vtable). --- librtt/Rtt_PhysicsWorld.cpp | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/librtt/Rtt_PhysicsWorld.cpp b/librtt/Rtt_PhysicsWorld.cpp index c597ac3e2..463942381 100644 --- a/librtt/Rtt_PhysicsWorld.cpp +++ b/librtt/Rtt_PhysicsWorld.cpp @@ -181,19 +181,19 @@ PhysicsWorld::StopWorld() SetProperty( kIsWorldRunning, false ); fWorld->SetContactListener( NULL ); + fWorld->SetDestructionListener( NULL ); - // The b2World is about to destroy the block allocator that owns - // the memory for b2Body objects, so we have to pre-emptively - // iterate over bodies and detach from display object const void *groundBodyUserdata = LuaLibPhysics::GetGroundBodyUserdata(); - for ( b2Body *body = fWorld->GetBodyList(), *nextBody = NULL; + // Phase 1: Detach display objects from physics bodies. + // We do NOT call DestroyBody here — doing so during bulk shutdown + // causes use-after-free when DestroyBody(bodyA) calls DestroyJoint + // and modifies bodyB which may have already been freed, or when + // the block allocator reuses freed joint memory (zeroing the vtable). + for ( b2Body *body = fWorld->GetBodyList(); NULL != body; - body = nextBody ) + body = body->GetNext() ) { - // Prefetch next body before we delete body - nextBody = body->GetNext(); - if ( body->GetUserData() ) { if ( body->GetUserData() != groundBodyUserdata ) @@ -202,10 +202,26 @@ PhysicsWorld::StopWorld() o->RemoveExtensions(); } } + } - fWorld->DestroyBody( body ); + // Phase 2: Invalidate any remaining joint wrappers so that + // Lua GC finalizers won't access freed b2Joint memory later. + void *finalizedUserdata = UserdataWrapper::GetFinalizedValue(); + for ( b2Joint *joint = fWorld->GetJointList(); + NULL != joint; + joint = joint->GetNext() ) + { + UserdataWrapper *wrapper = (UserdataWrapper *)joint->GetUserData(); + if ( wrapper && finalizedUserdata != wrapper ) + { + wrapper->Invalidate(); + } } + // Phase 3: Delete the world. ~b2World cleans up fixture shape + // allocations, and ~b2BlockAllocator bulk-frees all body/joint/ + // fixture memory. This avoids the complex per-body DestroyBody → + // DestroyJoint chain that caused joint double-free crashes. Rtt_DELETE( fWorld ); fWorld = NULL; From 6bd57bfb93335ca8094f01eb88082ca2039cea1f Mon Sep 17 00:00:00 2001 From: Chen Bin Date: Sun, 12 Apr 2026 17:01:50 +0800 Subject: [PATCH 2/4] Add ownership check in PhysicsJoint::Finalizer Add bidirectional ownership check before writing to `b2Joint` userdata in the Lua GC finalizer. Only clear the joint's userdata if it still points back to this wrapper (`joint->GetUserData() == wrapper`). Without the check, the finalizer could write to memory that `b2BlockAllocator` has already freed and reused. --- librtt/Rtt_PhysicsJoint.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/librtt/Rtt_PhysicsJoint.cpp b/librtt/Rtt_PhysicsJoint.cpp index 87bf5ac65..1effe8008 100644 --- a/librtt/Rtt_PhysicsJoint.cpp +++ b/librtt/Rtt_PhysicsJoint.cpp @@ -1428,8 +1428,15 @@ PhysicsJoint::Finalizer( lua_State *L ) b2Joint *joint = (b2Joint*)wrapper->Dereference(); if ( joint ) { - // Make sure joint no longer points to this wrapper that we're about to destroy - joint->SetUserData( NULL ); + // Only clear userdata if the joint still points back to this wrapper. + // If it doesn't, the joint may have already been destroyed by + // DestroyBody/DestroyJoint (SayGoodbye already invalidated the wrapper), + // and the pointer could be dangling or reused memory. + void *userData = joint->GetUserData(); + if ( userData == wrapper ) + { + joint->SetUserData( NULL ); + } } Rtt_DELETE( wrapper ); From 6c82d43e94ac6e9c8b35597d406eed4296b9a67d Mon Sep 17 00:00:00 2001 From: Labo Lado Date: Wed, 8 Apr 2026 15:27:42 +0800 Subject: [PATCH 3/4] Fix physics use-after-free in StepWorld for offscreen display objects ~DisplayObjectExtensions previously checked GetParent() before clearing b2Body userData. GetParent() returns NULL for objects with IsRenderedOffScreen flag (snapshot.group, canvas texture cache groups), causing SetUserData(NULL) to be skipped. This leaves dangling pointers in the Box2D world body list, which StepWorld dereferences on the next frame, causing SIGSEGV. The fix removes the parent dependency and unconditionally clears userData. This is safe because SetUserData(NULL) has no side effects and the body is lazily destroyed by StepWorld when it finds NULL userData. --- librtt/Rtt_DisplayObjectExtensions.cpp | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/librtt/Rtt_DisplayObjectExtensions.cpp b/librtt/Rtt_DisplayObjectExtensions.cpp index aa23e3f2e..8f07794f3 100644 --- a/librtt/Rtt_DisplayObjectExtensions.cpp +++ b/librtt/Rtt_DisplayObjectExtensions.cpp @@ -42,23 +42,15 @@ DisplayObjectExtensions::~DisplayObjectExtensions() #ifdef Rtt_PHYSICS if ( fBody ) { - GroupObject *parent = fOwner.GetParent(); - if ( Rtt_VERIFY( parent ) ) - { - fBody->SetUserData( NULL ); - - // Do NOT DestroyBody here. Instead, at end of StepWorld(), we lazily - // detect if the body's userdata is NULL. If it is, we know to destroy - // the body. - /* - Runtime *runtime = static_cast< Runtime* >( Rtt_AllocatorGetUserdata( parent->Allocator() ) ); - b2World *world = runtime->GetWorld(); - if ( Rtt_VERIFY( world ) ) - { - world->DestroyBody( fBody ); - } - */ - } + // Always clear UserData to prevent StepWorld from dereferencing + // a freed DisplayObject. GetParent() returns NULL for objects + // with IsRenderedOffScreen (snapshot.group, canvas cache), which + // previously caused SetUserData(NULL) to be skipped. + fBody->SetUserData( NULL ); + + // Do NOT DestroyBody here. Instead, at end of StepWorld(), we lazily + // detect if the body's userdata is NULL. If it is, we know to destroy + // the body. } #endif // Rtt_PHYSICS } From eca6a93ad8304132e7d7e7d86f951e6fea286c94 Mon Sep 17 00:00:00 2001 From: Chen Bin Date: Sun, 12 Apr 2026 16:58:53 +0800 Subject: [PATCH 4/4] Pre-invalidate joint wrappers on body removal When a display object with a physics body is destroyed (via `removeSelf` or group removal), invalidate all attached joint `UserdataWrapper`s immediately in the destructor, before setting `body->SetUserData(NULL)`. This closes the timing window between `removeSelf` and `StepWorld`'s deferred `DestroyBody`: without pre-invalidation, a GC cycle in that window could run `PhysicsJoint::Finalizer` on a still-valid wrapper pointing to a joint about to be destroyed. --- librtt/Rtt_DisplayObjectExtensions.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/librtt/Rtt_DisplayObjectExtensions.cpp b/librtt/Rtt_DisplayObjectExtensions.cpp index 8f07794f3..757ac2ff7 100644 --- a/librtt/Rtt_DisplayObjectExtensions.cpp +++ b/librtt/Rtt_DisplayObjectExtensions.cpp @@ -17,6 +17,7 @@ #include "Rtt_Lua.h" #include "Rtt_LuaContext.h" #include "Rtt_LuaLibPhysics.h" +#include "Rtt_LuaAux.h" #include "Rtt_PhysicsWorld.h" #include "Rtt_Runtime.h" @@ -42,6 +43,25 @@ DisplayObjectExtensions::~DisplayObjectExtensions() #ifdef Rtt_PHYSICS if ( fBody ) { + // Invalidate UserdataWrappers for all joints attached to this body + // BEFORE clearing the body's UserData. When StepWorld later calls + // DestroyBody, Box2D will destroy these joints internally. Without + // pre-invalidation, the Lua GC could trigger PhysicsJoint::Finalizer + // on a dangling b2Joint pointer, causing use-after-free (SIGSEGV in + // b2Fixture::Destroy). + b2JointEdge *je = fBody->GetJointList(); + while ( je ) + { + b2Joint *joint = je->joint; + UserdataWrapper *wrapper = (UserdataWrapper *)joint->GetUserData(); + if ( wrapper && UserdataWrapper::GetFinalizedValue() != wrapper ) + { + wrapper->Invalidate(); + } + joint->SetUserData( NULL ); + je = je->next; + } + // Always clear UserData to prevent StepWorld from dereferencing // a freed DisplayObject. GetParent() returns NULL for objects // with IsRenderedOffScreen (snapshot.group, canvas cache), which