Defer object destructors to the next VM safepoint (staging CI, stacked on #22515)#157
Open
iliaal wants to merge 2 commits into
Open
Defer object destructors to the next VM safepoint (staging CI, stacked on #22515)#157iliaal wants to merge 2 commits into
iliaal wants to merge 2 commits into
Conversation
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.
961b3e3 to
211a832
Compare
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.
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.