Skip to content

Defer user error handlers raised from user-code opcodes#22515

Open
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:gh20018-upstream
Open

Defer user error handlers raised from user-code opcodes#22515
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:gh20018-upstream

Conversation

@iliaal

@iliaal iliaal commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Defers a user error handler raised from a user-code opcode to the next VM safepoint, after the opcode completes, so the handler cannot reenter and free or mutate state the opcode still holds a raw pointer into. 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.

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. Diagnostics raised from internal functions still run synchronously, since the function may gate on EG(exception) or throw right after emitting; php_sort() and the array pointer functions pin their by-ref argument across the deprecation they raise.

Under the CALL VM (Windows and macOS), a deferred error could otherwise flush while a call frame is being assembled: the throwing handler would unwind into the half-built frame and cleanup_unfinished_calls would destroy an unsent argument slot, which is NULL there (the hybrid VM keeps a live value, so glibc/x86_64 never faults). The flush is deferred past a call being assembled in both the interpreter interrupt helper and the JIT interrupt stub, so this cannot happen.

This is a narrow, error-handlers-only take on GH-20018. arnaud-lb#31 is the broader reference: it also delays destructors and adds a set_error_handler($delay) opt-out with PromotedErrorException.

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
@iliaal

iliaal commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

/cc @arnaud-lb. A narrower take on GH-20018 than your #31: error handlers only, no destructors, no $delay/PromotedErrorException. It defers the flush past a half-built call rather than repairing the throwing opline like your zend_interrupt_consume, which keeps it small enough to land the aggregated crash fixes now (incl. the CALL-VM cleanup_unfinished_calls fault on Windows/macOS from gh18262). If you'd rather land the full delayed-effects design in one go, I can close this or fold the crash fixes into #31.

@iluuu1994

Copy link
Copy Markdown
Member

Destructors cause effectively all the same issues (#20001), so will this not be an incomplete solution?

@iliaal

iliaal commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Destructors cause effectively all the same issues (#20001), so will this not be an incomplete solution?

It is a first big part that solves a big chunk, I will work to extend it to other bits, but this is the 1st part that is independent and solves a whole class of issues already.

@arnaud-lb

Copy link
Copy Markdown
Member

@iliaal I'm working on something else right now, but I will take the time to review your approach when I come back to this topic.

arnaud-lb#31 is larger for a few reasons:

  • It attempts to demonstrate that the approach works for related problems (user error handlers and destructors), so that we can commit to it.
  • It attempts to reduce disruption, or to provide workarounds ($delay=false/PromotedErrorException is a workaround for the use-case of promoting warnings to exceptions)

@iliaal

iliaal commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

I do want to tackle other bits, but it is pretty heady area, the from the bug reports (#16792, #17416, #18274, #20042, #21245, #22419) I saw/reviewed the error handler seems like most common issue. So I took a much less ambitious goal at a starting point.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants