Fix box2d physics use-after-free crashes#905
Open
zero-meta wants to merge 4 commits into
Open
Conversation
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).
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.
~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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix physics use-after-free crashes
Summary
Fix multiple use-after-free vulnerabilities in the Box2D physics binding that cause
SIGSEGVcrashes in production (Our Google Play App).DestroyBodyloop with bulk cleanup to eliminate double-free when gear joints havem_bodyA == m_bodyB(both edges in same body's joint list)GetParent()guard in~DisplayObjectExtensionsso offscreen display objects (snapshot.group, canvas texture cache) unconditionally clearbody->SetUserData(NULL), preventing dangling pointer dereference on next frame. The same as this PR: (removed) #894joint->GetUserData() == wrapperguard before writing tob2Jointuserdata in Lua GC finalizerUserdataWrappers immediately in~DisplayObjectExtensions, closing the GC timing window betweenremoveSelfandStepWorld's deferredDestroyBodyCrash signatures (from production)
Prior fix context
This PR #858 (commit
c8c3bd1e) ("Core: fix box2d joint use after free when world deleted") addedDestroyBody(body)to theStopWorldloop. This fixed the original bug wheredelete fWorldbulk-freed all joint memory without callingSayGoodbye, leaving joint wrappers dangling for the GC Finalizer to crash on. WithDestroyBody, each joint getsSayGoodbye→wrapper->Invalidate()before being freed, making the Finalizer safe for all normal joints.However,
DestroyBodyintroduced a new deterministic crash for gear joints:b2GearJointhasm_bodyA == m_bodyB == ground, so both joint edges are in the same body's list, andDestroyBody(ground)double-frees the gear joint.This PR's 3-phase
StopWorldsolves both problems simultaneously: noDestroyBody(avoids gear joint double-free), but manually iterates the joint list towrapper->Invalidate()(replacesSayGoodbye's role).Gear joint deterministic crash — test code
The gear joint crash is 100% reproducible.
b2GearJointoverridesm_bodyA/m_bodyBto the sub-joints' other bodies, causing both joint edges to register in the same body's list (ground). WhenDestroyBody(ground)iterates this list, it frees the gear joint viaedgeB, then follows the prefetchednextpointer toedgeA— which is now freed memory.Paste into
main.luaand run — crashes immediately onphysics.stop():