Skip to content

Fix physics use-after-free crash for offscreen display objects#894

Open
labolado wants to merge 1 commit intocoronalabs:masterfrom
labolado:fix/physics-uaf-upstream
Open

Fix physics use-after-free crash for offscreen display objects#894
labolado wants to merge 1 commit intocoronalabs:masterfrom
labolado:fix/physics-uaf-upstream

Conversation

@labolado
Copy link
Copy Markdown

@labolado labolado commented Apr 8, 2026

Summary

Fix SIGSEGV crash in PhysicsWorld::StepWorld() when physics bodies are used with offscreen display objects (snapshot groups, canvas texture caches).

Problem

~DisplayObjectExtensions checks GetParent() before clearing b2Body::SetUserData(NULL). For objects with IsRenderedOffScreen=true, GetParent() always returns NULL, so SetUserData(NULL) is skipped. This leaves dangling pointers in the Box2D body list.

Fix

Unconditionally call fBody->SetUserData(NULL). Safe because it has no side effects — the body is lazily destroyed by StepWorld when it finds NULL userData.

Reproduction

macOS (deterministic, instant crash)

Run with MallocScribble=1:

local physics = require("physics")
physics.start()
local snap = display.newSnapshot(200, 200)
physics.addBody(snap.group, "dynamic", {shape={-50,-50,50,-50,50,50,-50,50}})
physics.removeBody(snap.group) -- SetUserData(NULL) SKIPPED
display.remove(snap)
collectgarbage("collect")
-- Next frame: SIGSEGV in StepWorld

iOS (deterministic, crashes within seconds)

On iOS, freed memory must be overwritten by new allocations. The test creates dangling pointers via the same snapshot.group pattern, plus light memory churn (small canvas textures + display objects) to force page reuse:

local physics = require("physics")
physics.start()
physics.setGravity(0, 9.8)

local W, H = display.contentWidth, display.contentHeight
local ground = display.newRect(W/2, H-10, W, 20)
physics.addBody(ground, "static")

local canvasPool = {}

Runtime:addEventListener("enterFrame", function()
    -- Create dangling bodies via snapshot.group
    for i = 1, 8 do
        local snap = display.newSnapshot(64, 64)
        if not snap then return end
        pcall(function()
            physics.addBody(snap.group, "dynamic", {
                shape={-20,-20, 20,-20, 20,20, -20,20}, density=1
            })
        end)
        pcall(function() physics.removeBody(snap.group) end)
        display.remove(snap)
    end
    collectgarbage("collect")

    -- Light memory churn to reuse freed pages
    for i = 1, 3 do
        local c = graphics.newTexture({type="canvas", width=256, height=256})
        if c then
            local r = display.newRect(0, 0, 256, 256)
            r:setFillColor(math.random(), math.random(), math.random())
            c:draw(r); c:invalidate(); display.remove(r)
            canvasPool[#canvasPool+1] = c
        end
    end
    while #canvasPool > 8 do
        table.remove(canvasPool, 1):releaseSelf()
    end
    collectgarbage("collect")
end)

Verified Results

Before fix After fix
macOS + MallocScribble signal 11 (SIGSEGV), instant 30s+ stable
iPhone 17 Pro Max signal 11 (SIGSEGV), seconds 65s+ stable, no OOM

~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.
@clang-clang-clang
Copy link
Copy Markdown
Contributor

Hi, just curious, is it 6 FPS during testing or just a typo? Thanks.

    ...
    if frame >= 360 then
        print("PASS: 60 seconds without crash")
        os.exit(0)
    end
    ...

@labolado
Copy link
Copy Markdown
Author

labolado commented Apr 8, 2026

Good catch — that was leftover test infrastructure. Updated the PR with a clean minimal reproduction (7 lines for macOS + MallocScribble, full test for iOS).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants