Skip to content

Defer object destructors to the next VM safepoint (staging CI, stacked on #22515)#157

Open
iliaal wants to merge 2 commits into
masterfrom
feat/deferred-dtors
Open

Defer object destructors to the next VM safepoint (staging CI, stacked on #22515)#157
iliaal wants to merge 2 commits into
masterfrom
feat/deferred-dtors

Conversation

@iliaal

@iliaal iliaal commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Staging CI validation for object destructor deferral, stacked on the error-handler deferral in php#22515 (commit c00badd). This PR carries both commits so the full CI matrix runs against the combined change; it is not for upstream merge as-is. If CI is clean the destructor commit becomes a follow-up to php#22515.

The destructor commit runs __destruct at the next VM safepoint instead of inline when a refcount reaches zero, reusing the deferred-effects infrastructure from php#22515. Logical death stays synchronous (a WeakReference reads null and a WeakMap entry is gone at the drop); only the physical destructor and free defer. Catchability, exactly-once, and exception composition match inline destruction across the interpreter, tracing JIT, and function JIT. Code with no try is unchanged; a try pays a small fixed constant.

iliaal added 2 commits June 29, 2026 16:56
A user error handler raised mid-opcode can reenter the VM and free or
mutate state the opcode still holds a raw pointer into, a long-standing
source of use-after-free and memory-corruption bugs. Defer the handler
to the next vm_interrupt safepoint, after the opcode completes. The
interpreter and both JITs reach that safepoint through the existing
vm_interrupt mechanism. A diagnostic silenced by @ keeps its
error_reporting across the deferral; diagnostics raised from internal
functions still run synchronously, since the function may gate on
EG(exception) or throw right after emitting.

Function JIT flushes at the interpreter's safepoints: post-call with the
callee frame still on top, each jump-target block, and function entry,
spilling the live SSA values before it resumes. Exception handling
flushes any buffered diagnostic with the pending exception saved and
restored around the handler, so nothing is dropped when a later opcode
throws. Under the CALL VM the flush is deferred past a call being
assembled, so a throwing handler cannot unwind into a half-built frame
and fault in cleanup_unfinished_calls.

Fixes phpGH-20018
Fixes phpGH-16792
Fixes phpGH-17416
Fixes phpGH-18274
Fixes phpGH-20042
Fixes phpGH-21245
Fixes phpGH-22419
Run an object's __destruct at the next VM safepoint instead of inline at the
point its refcount reaches zero, so a destructor can no longer reenter the VM
in the middle of an opcode's operand cleanup. Logical death stays synchronous:
a WeakReference reads null and a WeakMap entry is gone at the drop, and only
the physical destructor call and the free are deferred. Safepoints are entry
into a user function, method, closure, magic method or hook, a statement-level
internal call, every try-region boundary (entry, normal exit,
return/break/continue/goto/throw out of a try, and the end of a finally body),
a loop back-edge, and script or fiber end.

Because a flushed destructor exception is thrown from an opline still inside
the try region that owns it, the existing exception table delivers it to the
same catch that would have caught an inline throw; region scoping is positional
and needs no per-region runtime state. Several destructor exceptions at one
safepoint compose into a single getPrevious chain, an exit or unwind exception
wins over a destructor exception, and every object is destroyed exactly once.
Behavior is identical across the interpreter, tracing JIT, and function JIT.
Generators and fibers carry their own deferred-destructor queue, so a
destructor deferred inside one runs there rather than in the consumer.
@iliaal iliaal force-pushed the feat/deferred-dtors branch from 961b3e3 to 211a832 Compare July 12, 2026 23:54
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.

1 participant