Defer user error handlers raised from user-code opcodes#22515
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
|
/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. |
|
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. |
|
@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:
|
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.