Skip to content

Delayed effects#30

Closed
arnaud-lb wants to merge 22 commits into
masterfrom
delayed-effects-2
Closed

Delayed effects#30
arnaud-lb wants to merge 22 commits into
masterfrom
delayed-effects-2

Conversation

@arnaud-lb
Copy link
Copy Markdown
Owner

@arnaud-lb arnaud-lb commented May 21, 2026

Delay user error handlers and destructors until the next vm_interrupt safe point.

Error handlers

Identified use-cases of user error handlers:

  1. Custom logging
  2. Promoting to exceptions
  3. Catching warnings in native function wrappers, e.g.
function better_fwrite() {
    set_error_handler(function ($errno, $errstr) use (&$error) {
        $error = $errstr;
    });
    fwrite(...);
    if ($error) {
        throw SomeException($error);
    }
    restor_error_handler();
}

How these use-cases are affected:

  • Error handling is unaffected when no user error handler is installed (display_errors and log_errors will display/log without delay).
  • Use-case 1 is mostly unaffected as the $errline and $errfile callback parameters remain correct. debug_backtrace() may reflect delaying, however.
  • Use-case 3 is unaffected, as vm interrupts are checked after returning from internal functions
  • Use-case 2 is broken as the exception may be thrown too late.

To mitigate use-case 2, we introduce a new set_error_handler() argument: $delay=true. When false, errors specified in the $error_levels parameter are promoted to exceptions (PromotedErrorException), and the error handler is executed when the exception is handled (in ZEND_HANDLE_EXCEPTION). Promoting to exception allows internal code to safely unwind (exceptions are already properly handled in code paths that may trigger errors), and to call the user error handler as soon as the current opcode terminates. The user error handler can replace the promoted exception by throwing an other exception.

It would be possible to provide backtraces reflecting the origin of the error accurately. Currently backtraces reflect the point at which the error handler is called, which may be a few lines after in use-cases 1 and 3, or in a nested function call if no safe point is reached in the originating function.

Destructors

Identified use-cases of destructors:

  • Cleanup when leaving scope (function)

Currently, interrupt safe points are:

  • When entering a user function
  • Just after leaving an internal function
  • Backwards jumps (loops) and some forward jumps

This would prevent destructors from being executed right when leaving a function. To mitigate this, we move the first safe point to "When leaving a user function", and we introduce a safe point when increasing the VM call stack size, to preserve timeout handling in case of deep recursion:

  • When leaving a user function (after leaving the frame)
  • Just after leaving an internal function
  • Backwards jumps (loops)
  • When growing the VM call stack

Backtraces in destructors will reflect the point at which they are called. In case a dtor is called in the "when leaving a user function" point, the backtrace will not reflect the function that was just left. This is already the case for destructors of objects referenced by local variables (which are destroyed after leaving the frame), but this is new for other objects, if no safe point was reached before leaving the function.

Corner cases

  • An exception is thrown, but errors were pending. Errors are handled just before handling the exception. If an error handle throws, we have to chose which exception takes precedence and which one is liked as previous exception.
  • An error is promoted to exception, and another exception is throw before any exception is handled. This happens for example in call_user_func("static::f"): May emit a deprecation, followed by a TypeError. Currently we ignore the second exception, otherwise it prevents calling user error handlers.
  • Multiple errors would be promoted to exceptions. In this case we promote only the first one and ignore subsequent ones. This is already what happens currently when a user error handler has thrown and new errors are emitted before the exception is handled.

Performance

There is a 0.5% regression on the symfony benchmark, but I attribute this to binary layout.

Summary of interrupt changes

  • The ZEND_VM_ENTER_EX() interrupt check has been removed
  • A new interrupt check has been added in the zend_vm_stack_push_call_frame() slow path (before extending the stack), for the purpose of timeout handling during recursion
  • A new interrupt check has been added in zend_leave_helper() so that pending handlers/dtors are executed when leaving a function

TODO

  • Implement delayed destructors
  • Free live vars after throwing from interrupt handler
  • Remove/replace the ZEND_VM_ENTER_EX() interrupt check

nikic and others added 15 commits May 19, 2026 18:25
This is a prototype for fixing a long-standing source of interrupt
vulnerabilities: A notice is emitted during execution of an opcode,
resulting in an error handling being run. The error handler modifies
some data structure the opcode is working on, resulting in UAF or
other memory corruption.

The idea here is to instead collect notices and only process them
after the opcode. This is implemented similarly to exception
handling, by switching to a ZEND_HANDLE_DELAYED_ERROR opcode,
which will then switch back to the normal opcode stream.

Unfortunately, what this prototype implements is not sufficient.
Opcodes that acquire direct (INDIRECT) references to zvals require
that no interrupts occur between the producing and the consuming
opcode. Chains of W/RW opcodes should be executed without interrupt.
Currently, the notice is only delayed until after the first opcode,
which still results in an illegal interrupt (bug78598.phpt shows
a UAF with this change).

I'm not sure how to best handle that issue.
There are 3 categories of failing tests here:

 - Many are throwing in a user error handlers, therefore relied on non-delayed
   behavior. Fixed these by installing the error handler with
   promote_to_exception: true.
 - Some are just printing the error message. Their output changes due to delay.
   These are fixed by updating the EXPECT section.
 - And finally, many are testing the adverse effects of mutations in error
   handlers. These are now irrelevant, but these are also fixed by updating the
   EXPECT section.
Comment thread Zend/zend.c Outdated
Comment thread Zend/zend_vm_def.h
@arnaud-lb arnaud-lb closed this May 28, 2026
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.

3 participants