Skip to content

Fix GH-22206: avoid musttail in zend_runtime_jit() in the tail-call VM - #23054

Open
lazerg wants to merge 1 commit into
php:PHP-8.5from
lazerg:fix/gh-22206-tailcall-setjmp
Open

Fix GH-22206: avoid musttail in zend_runtime_jit() in the tail-call VM#23054
lazerg wants to merge 1 commit into
php:PHP-8.5from
lazerg:fix/gh-22206-tailcall-setjmp

Conversation

@lazerg

@lazerg lazerg commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

With --disable-gcc-global-regs and a compiler that supports preserve_none, opcache is built with ZEND_VM_KIND_TAILCALL, where ZEND_OPCODE_RETURN() expands to a guaranteed tail call. zend_runtime_jit() does its work inside zend_try(), which uses setjmp(), and GCC 16 rejects that combination with "cannot tail-call: caller uses setjmp".

GH-22320 fixed the --enable-gcc-global-regs side of the same report. Here the tail call isn't needed: zend_runtime_jit() is a cold one-shot JIT trigger rather than a hot VM handler, so a plain call works and the returned opline still goes back to the VM loop as before.

Checked with GCC 16.1.0: a --disable-gcc-global-regs build fails on ext/opcache/jit/zend_jit.lo before the change and compiles after it. ext/opcache and Zend tests pass on a tail-call VM build.

Fixes GH-22206

@arnaud-lb arnaud-lb 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.

This is not really a supported configuration, and even with this fix the build fails on my machine with errors related to musttail:

Zend/zend_vm_execute.h: In function 'ZEND_RETURN_BY_REF_SPEC_OBSERVER_TAILCALL_HANDLER':
Zend/zend_vm_execute.h:53688:66: error: address of automatic variable 'observer_retval' can escape to 'musttail' call [-Werror=maybe-musttail-local-addr]
53688 | # define ZEND_VM_CONTINUE()                    ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))
      |                                                                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Zend/zend_vm_execute.h:53687:69: note: in definition of macro 'ZEND_VM_TAIL_CALL'
53687 | # define ZEND_VM_TAIL_CALL(call)               ZEND_MUSTTAIL return call
      |                                                                     ^~~~
Zend/zend_vm_execute.h:53695:91: note: in expansion of macro 'ZEND_VM_CONTINUE'
53695 | # define ZEND_VM_DISPATCH_TO_LEAVE_HELPER(helper) opline = &call_leave_op; SAVE_OPLINE(); ZEND_VM_CONTINUE()
      |                                                                                           ^~~~~~~~~~~~~~~~
Zend/zend_vm_execute.h:57867:9: note: in expansion of macro 'ZEND_VM_DISPATCH_TO_LEAVE_HELPER'
57867 |         ZEND_VM_DISPATCH_TO_LEAVE_HELPER(zend_leave_helper_SPEC_TAILCALL);
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

GCC's musttail is stricter than Clang, so we can not support the TAILCALL VM on GCC yet.

Normally the TAILCALL VM is not selected on GCC due to global regs supports, but --disable-gcc-global-regs breaks this. Maybe we should explicitly enable the TAILCALL VM only on Clang.

@lazerg

lazerg commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

I reproduced this. On macOS/aarch64 with GCC 16.1 and --disable-gcc-global-regs, compiling Zend/zend_execute.c produces 124 -Wmaybe-musttail-local-addr warnings, spread over 66 generated *_TAILCALL_HANDLER functions and 13 distinct locals:

hval (34), obj (21), _cache_slot (18), retval (12), error (9), key_tmp (8), new_expr (7), c (5), count (3), tmp (3), observer_retval (2), len (1), ctx (1).

Your build stopped on observer_retval because you compile with --enable-werror. Without -Werror the file still compiles and prints all 124.

So observer_retval is not a special case. The handler passes return_value, which can be &observer_retval, to zend_observer_fcall_end(). That is an opaque call, so GCC cannot prove an observer does not keep the pointer. Then ZEND_VM_DISPATCH_TO_LEAVE_HELPER musttails away and the frame is gone. Every other warning has the same shape: the address of a local reaches a non-inlined callee, and the handler ends in ZEND_VM_CONTINUE().

Two more things I checked:

  • All 124 are the "maybe" variant. Zero definite -Wmusttail-local-addr.
  • GCC does not drop the tail call when it warns. Minimal case:
void sink(int *p);
int next(void);
int f(void) {
    int local = 42;
    sink(&local);
    [[gnu::musttail]] return next();
}

compiles to bl _sink followed by b _next on aarch64. The frame is released while the pointer may still be live, so -Wno-maybe-musttail-local-addr would only hide the hazard.

I agree with your conclusion. Fixing observer_retval would just move the first failure to hval, then to the next one. The selection lives in the generated Zend/zend_vm_opcodes.h:

#elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(__aarch64__))
# define ZEND_VM_KIND		ZEND_VM_KIND_TAILCALL

emitted by Zend/zend_vm_gen.php:2526, so adding defined(__clang__) is a small change: the generator line plus the regenerated header.

I did not do it here for two reasons. It is a support decision that is yours to make. And it makes this PR unnecessary: the setjmp conflict in zend_runtime_jit() only happens on GCC, so gating the TAILCALL VM to Clang removes that failure too. I can prepare the gate, either as another commit on this PR or as a separate one, and close this. Tell me which you prefer.

One side note. GCC 16.1 on darwin/aarch64 also ICEs on the TAILCALL build, a segfault in the vartrack RTL pass, in both Zend/zend_execute.c and ext/opcache/jit/zend_jit_vm_helpers.c. I had to compile with -g0 to get past it and collect the warnings above. That looks like a separate GCC bug, but it is one more sign that GCC and the TAILCALL VM are not close to working together.

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