Skip to content

Throw UnwindExit on timeout - #25187

Open
arnaud-lb wants to merge 6 commits into
php:masterfrom
arnaud-lb:timeout-unwind
Open

arnaud-lb wants to merge 6 commits into
php:masterfrom
arnaud-lb:timeout-unwind

Conversation

@arnaud-lb

Copy link
Copy Markdown
Member

max_execution_time is handled like this:

  • A signal handler sets EG(vm_interrupt) and EG(timed_out)
  • The VM calls zend_timeout() from a safe point when these flags are set
  • zend_timeout() triggers a fatal error -> zend_bailout()

Change the last step so that zend_timeout() throws an UnwindExit exception instead.

@arnaud-lb
arnaud-lb marked this pull request as ready for review November 6, 2026 12:50
@arnaud-lb
arnaud-lb requested a review from dstogov as a code owner November 6, 2026 12:50
Comment thread Zend/zend_execute_API.c Resolved
Comment thread Zend/zend_execute_API.c Resolved

@iliaal iliaal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phpdbg: in the ZEND_WIN32 block of phpdbg_prompt.c, exception is read before zend_timeout(), which now returns with an UnwindExit set, so that iteration misses it. Checking the timeout before reading EG(exception) avoids that.

zend_timeout() losing ZEND_NORETURN probably wants an UPGRADING.INTERNALS line, since extensions calling it now get control back with an exception set.

Comment thread Zend/zend_vm_def.h
#endif
if (atomic_load(&EG(timed_out))) {
zend_timeout();
HANDLE_EXCEPTION();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EG(opline_before_exception) here is the opline the interrupt fired before, not one that ran, so ZEND_HANDLE_EXCEPTION dtors its stale result. With while (true) { $s = $a . 'y'; } it frees the string $s still owns, and a shutdown function reading $s hits a UAF under valgrind. zend_handle_delayed_oom() NULLs opline_before_exception for this reason; doing the same after zend_timeout() here clears it. The JIT stub is unaffected thanks to zend_jit_exception_in_interrupt_handler_helper().

Comment thread Zend/zend_execute_API.c
&& execute_data->opline->opcode != ZEND_HANDLE_EXCEPTION) {
EG(opline_before_exception) = EG(current_execute_data)->opline;
EG(current_execute_data)->opline = EG(exception_op);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates zend_throw_unwind_exit() plus a guard; could the guard move into that helper instead?

@@ -0,0 +1,16 @@
--TEST--

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs --EXTENSIONS-- zend_test.

@@ -0,0 +1,22 @@
--TEST--

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs --EXTENSIONS-- zend_test.

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.

2 participants