Fix GH-22206: avoid musttail in zend_runtime_jit() in the tail-call VM - #23054
Fix GH-22206: avoid musttail in zend_runtime_jit() in the tail-call VM#23054lazerg wants to merge 3 commits into
Conversation
arnaud-lb
left a comment
There was a problem hiding this comment.
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.
|
I reproduced this. On macOS/aarch64 with GCC 16.1 and
Your build stopped on So Two more things I checked:
void sink(int *p);
int next(void);
int f(void) {
int local = 42;
sink(&local);
[[gnu::musttail]] return next();
}compiles to I agree with your conclusion. Fixing #elif defined(HAVE_MUSTTAIL) && defined(HAVE_PRESERVE_NONE) && (defined(__x86_64__) || defined(__aarch64__))
# define ZEND_VM_KIND ZEND_VM_KIND_TAILCALLemitted by 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 One side note. GCC 16.1 on darwin/aarch64 also ICEs on the TAILCALL build, a segfault in the vartrack RTL pass, in both |
|
I didn't realize that this was a warning and not a hard error, thank you for pointing this. Maybe this was not the case before? I'm not sure. So we we could add But given your last paragraph I would be in favor of adding |
|
Sure, that's an easy change on my end, one line in the generator plus regenerating |
|
The TAILCALL VM is only really supported on Clang, and as we have found it doesn't build on GCC due to multiple factors, so disabling on non-Clang compilers is fine. |
|
Done, pushed the guard. GCC now falls back to the call VM, Clang still gets tail-call. Let me know if you want it as its own commit or squashed into this one. |
| # if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL | ||
| /* zend_try() uses setjmp(), which GCC 16 refuses to combine with the guaranteed | ||
| * tail call of ZEND_OPCODE_RETURN(); a plain call is fine for this cold path. */ | ||
| return ((zend_vm_opcode_handler_t)opline->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); | ||
| # else | ||
| ZEND_OPCODE_RETURN(); | ||
| # endif |
There was a problem hiding this comment.
Let's also revert this change. Or if you prefer, we could move the zend_try {} part of this function to a separate one, to avoid the conflict while also avoiding the risk non-tail recursion.
There was a problem hiding this comment.
Reverted. Compiled the original zend_runtime_jit() with Clang and checked the generated assembly: it emits a real tail branch (br x0 on arm64, no return address pushed) at the return site, even with the earlier sigsetjmp() call in the function. So Clang honors the tail call here without any workaround, no need for the split-function approach either.
With
--disable-gcc-global-regsand a compiler that supportspreserve_none, opcache is built withZEND_VM_KIND_TAILCALL, whereZEND_OPCODE_RETURN()expands to a guaranteed tail call.zend_runtime_jit()does its work insidezend_try(), which usessetjmp(), and GCC 16 rejects that combination with "cannot tail-call: caller uses setjmp".GH-22320 fixed the
--enable-gcc-global-regsside 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-regsbuild fails onext/opcache/jit/zend_jit.lobefore the change and compiles after it. ext/opcache and Zend tests pass on a tail-call VM build.Fixes GH-22206