Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,6 @@ jobs:
if: ${{ inputs.all_variations }}
uses: ./.github/actions/test-macos
- name: Test Tracing JIT
if: ${{ matrix.arch == 'X64' || !matrix.zts }}
uses: ./.github/actions/test-macos
with:
enableOpcache: true
Expand All @@ -356,7 +355,7 @@ jobs:
with:
enableOpcache: true
- name: Test Function JIT
if: ${{ inputs.all_variations && (matrix.arch == 'X64' || !matrix.zts) }}
if: ${{ inputs.all_variations }}
uses: ./.github/actions/test-macos
with:
enableOpcache: true
Expand Down
25 changes: 16 additions & 9 deletions ext/opcache/ZendAccelerator.c
Original file line number Diff line number Diff line change
Expand Up @@ -3259,8 +3259,9 @@ static zend_result accel_post_startup(void)
file_cache_only = ZCG(accel_directives).file_cache_only;
if (!file_cache_only) {
size_t shm_size = ZCG(accel_directives).memory_consumption;
#ifdef HAVE_JIT
size_t jit_size = 0;
#ifdef HAVE_JIT
size_t jit_buffer_size = 0;
bool reattached = false;

if (JIT_G(enabled) && JIT_G(buffer_size)
Expand All @@ -3272,15 +3273,16 @@ static zend_result accel_post_startup(void)
zend_accel_error_noreturn(ACCEL_LOG_FATAL, "Failure to initialize shared memory structures - can't get page size.");
abort();
}
jit_size = JIT_G(buffer_size);
jit_size = ZEND_MM_ALIGNED_SIZE_EX(jit_size, page_size);
jit_buffer_size = JIT_G(buffer_size);
jit_buffer_size = ZEND_MM_ALIGNED_SIZE_EX(jit_buffer_size, page_size);
# ifndef ZEND_JIT_USE_APPLE_MAP_JIT
jit_size = jit_buffer_size;
shm_size += jit_size;
# endif
}
#endif

switch (zend_shared_alloc_startup(shm_size, jit_size)) {
#else
switch (zend_shared_alloc_startup(shm_size, 0)) {
#endif
case ALLOC_SUCCESS:
if (zend_accel_init_shm() == FAILURE) {
accel_startup_ok = false;
Expand Down Expand Up @@ -3334,10 +3336,15 @@ static zend_result accel_post_startup(void)
if (JIT_G(buffer_size) == 0) {
JIT_G(enabled) = false;
JIT_G(on) = false;
} else if (!ZSMMG(reserved)) {
zend_accel_error_noreturn(ACCEL_LOG_FATAL, "Could not enable JIT: could not use reserved buffer!");
} else {
zend_jit_startup(ZSMMG(reserved), jit_size, reattached);
# ifdef ZEND_JIT_USE_APPLE_MAP_JIT
zend_jit_startup(NULL, jit_buffer_size, reattached);
# else
if (!ZSMMG(reserved)) {
zend_accel_error_noreturn(ACCEL_LOG_FATAL, "Could not enable JIT: could not use reserved buffer!");
}
zend_jit_startup(ZSMMG(reserved), jit_buffer_size, reattached);
# endif
zend_jit_startup_ok = true;
}
}
Expand Down
7 changes: 3 additions & 4 deletions ext/opcache/config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ AS_VAR_IF([PHP_OPCACHE_JIT], [yes], [
PHP_OPCACHE_JIT=no
])

if test "$host_vendor" = "apple" && test "$host_cpu" = "aarch64" && test "$PHP_THREAD_SAFETY" = "yes"; then
AC_MSG_WARN([JIT not supported on Apple Silicon with ZTS])
PHP_OPCACHE_JIT=no
fi
AS_IF([test "$host_vendor" = "apple" && test "$host_cpu" = "aarch64" && test "$PHP_THREAD_SAFETY" = "yes"],
[AC_CHECK_FUNC([pthread_jit_write_protect_np], [],
[AC_MSG_ERROR([OPcache JIT on Apple Silicon with ZTS requires pthread_jit_write_protect_np()])])])
])

AS_VAR_IF([PHP_OPCACHE_JIT], [yes], [
Expand Down
77 changes: 48 additions & 29 deletions ext/opcache/jit/zend_jit.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@

#include "jit/zend_jit_internal.h"

#ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
#ifdef ZEND_JIT_USE_APPLE_MAP_JIT
#include <mach/vm_inherit.h>
#include <pthread.h>
#include <sys/mman.h>
#endif

#ifdef ZTS
Expand Down Expand Up @@ -77,9 +79,6 @@ int16_t zend_jit_hot_counters[ZEND_HOT_COUNTERS_COUNT];

const zend_op *zend_jit_halt_op = NULL;
const zend_op *zend_jit_interrupt_op = NULL;
#ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
static int zend_write_protect = 1;
#endif

static void *dasm_buf = NULL;
static void *dasm_end = NULL;
Expand Down Expand Up @@ -831,7 +830,11 @@ ZEND_EXT_API void zend_jit_status(zval *ret)
add_assoc_long(&stats, "opt_level", JIT_G(opt_level));
add_assoc_long(&stats, "opt_flags", JIT_G(opt_flags));
if (dasm_buf) {
#ifdef ZEND_JIT_USE_APPLE_MAP_JIT
add_assoc_long(&stats, "buffer_size", dasm_size);
#else
Comment on lines +833 to +835

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.

I don't understand this change, could you comment on why this is necessary?

add_assoc_long(&stats, "buffer_size", (char*)dasm_end - (char*)dasm_buf);
#endif
add_assoc_long(&stats, "buffer_free", (char*)dasm_end - (char*)*dasm_ptr);
} else {
add_assoc_long(&stats, "buffer_size", 0);
Expand Down Expand Up @@ -3517,29 +3520,26 @@ int zend_jit_script(zend_script *script)

void zend_jit_unprotect(void)
{
#ifdef HAVE_MPROTECT
#ifdef ZEND_JIT_USE_APPLE_MAP_JIT
pthread_jit_write_protect_np(0);
#elif defined(HAVE_MPROTECT)
if (!(JIT_G(debug) & (ZEND_JIT_DEBUG_GDB|ZEND_JIT_DEBUG_PERF_DUMP))) {
int opts = PROT_READ | PROT_WRITE;
#ifdef ZTS
#ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
if (zend_write_protect) {
pthread_jit_write_protect_np(0);
}
#endif
# ifdef ZTS
opts |= PROT_EXEC;
#endif
# endif
if (mprotect(dasm_buf, dasm_size, opts) != 0) {
fprintf(stderr, "mprotect() failed [%d] %s\n", errno, strerror(errno));
}
}
#elif defined(_WIN32)
if (!(JIT_G(debug) & (ZEND_JIT_DEBUG_GDB|ZEND_JIT_DEBUG_PERF_DUMP))) {
DWORD old, new;
#ifdef ZTS
# ifdef ZTS
new = PAGE_EXECUTE_READWRITE;
#else
# else
new = PAGE_READWRITE;
#endif
# endif
if (!VirtualProtect(dasm_buf, dasm_size, new, &old)) {
DWORD err = GetLastError();
char *msg = php_win32_error_to_msg(err);
Expand All @@ -3552,13 +3552,10 @@ void zend_jit_unprotect(void)

void zend_jit_protect(void)
{
#ifdef HAVE_MPROTECT
#ifdef ZEND_JIT_USE_APPLE_MAP_JIT
pthread_jit_write_protect_np(1);
#elif defined(HAVE_MPROTECT)
if (!(JIT_G(debug) & (ZEND_JIT_DEBUG_GDB|ZEND_JIT_DEBUG_PERF_DUMP))) {
#ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
if (zend_write_protect) {
pthread_jit_write_protect_np(1);
}
#endif
if (mprotect(dasm_buf, dasm_size, PROT_READ | PROT_EXEC) != 0) {
fprintf(stderr, "mprotect() failed [%d] %s\n", errno, strerror(errno));
}
Expand Down Expand Up @@ -3780,20 +3777,36 @@ void zend_jit_startup(void *buf, size_t size, bool reattached)
zend_jit_interrupt_op = zend_get_interrupt_op();
zend_jit_profile_counter_rid = zend_get_op_array_extension_handle(ACCELERATOR_PRODUCT_NAME);

#ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
zend_write_protect = pthread_jit_write_protect_supported_np();
#ifdef ZEND_JIT_USE_APPLE_MAP_JIT
buf = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON | MAP_JIT, -1, 0);
Comment thread
realFlowControl marked this conversation as resolved.
if (buf == MAP_FAILED) {
int error = errno;
zend_accel_error_noreturn(ACCEL_LOG_FATAL,
"Unable to allocate %zu bytes for JIT buffer using MAP_JIT: %s (%d)",
size, strerror(error), error);
}
if (minherit(buf, size, VM_INHERIT_SHARE) != 0) {
int error = errno;
munmap(buf, size);
zend_accel_error_noreturn(ACCEL_LOG_FATAL,
"Unable to share JIT buffer across fork using minherit(): %s (%d)",
strerror(error), error);
}
if (!pthread_jit_write_protect_supported_np()) {
munmap(buf, size);
zend_accel_error_noreturn(ACCEL_LOG_FATAL,
"Apple Silicon ZTS JIT requires pthread_jit_write_protect_np() support");
}
#endif

dasm_buf = buf;
dasm_size = size;
dasm_ptr = dasm_end = (void*)(((char*)dasm_buf) + size - sizeof(*dasm_ptr) * 2);

#ifdef HAVE_MPROTECT
#ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
if (zend_write_protect) {
pthread_jit_write_protect_np(1);
}
#endif
#ifdef ZEND_JIT_USE_APPLE_MAP_JIT
pthread_jit_write_protect_np(1);
#elif defined(HAVE_MPROTECT)
if (JIT_G(debug) & (ZEND_JIT_DEBUG_GDB|ZEND_JIT_DEBUG_PERF_DUMP)) {
if (mprotect(dasm_buf, dasm_size, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
fprintf(stderr, "mprotect() failed [%d] %s\n", errno, strerror(errno));
Expand Down Expand Up @@ -3874,6 +3887,12 @@ void zend_jit_shutdown(void)
zend_jit_trace_free_caches(&jit_globals);
#endif

#ifdef ZEND_JIT_USE_APPLE_MAP_JIT
if (dasm_buf != NULL) {
munmap(dasm_buf, dasm_size);
}
#endif

/* Reset global pointers to prevent use-after-free in `zend_jit_status()`
* after gracefully restarting Apache with mod_php, see:
* https://github.com/php/php-src/pull/19212 */
Expand Down
7 changes: 7 additions & 0 deletions ext/opcache/jit/zend_jit.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
#ifndef HAVE_JIT_H
#define HAVE_JIT_H

#if defined(__APPLE__) && defined(__aarch64__) && defined(ZTS)
# ifndef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
# error "Apple Silicon ZTS JIT requires pthread_jit_write_protect_np()"
# endif
# define ZEND_JIT_USE_APPLE_MAP_JIT 1
#endif

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.

Would it be feasible to unify all __APPLE__ code variations?

@realFlowControl realFlowControl Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Technically the split could be done for all macOS builds, but the protection path cannot be unified. pthread_jit_write_protect_supported_np() is supported on Apple Silicon but returns false on Intel, where the existing shared RWX transition works.

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 makes sense. Can we merge the ZEND_JIT_USE_APPLE_MAP_JIT and HAVE_PTHREAD_JIT_WRITE_PROTECT_NP variations?

@realFlowControl realFlowControl Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice finding, I cleaned it up in 8f51da2 . I also change the config.m4 to fail on invalid combinations. I think it makes more sense to explicitly fail compared to implicitly just disable JIT.

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.

Thank you for the changes. There are also cases where we call pthread_jit_write_protect_np() outside of ZEND_JIT_USE_APPLE_MAP_JIT, but that's probably not reachable?

#ifdef ZEND_JIT_USE_APPLE_MAP_JIT
pthread_jit_write_protect_np(0);
#elif defined(HAVE_MPROTECT)
if (!(JIT_G(debug) & (ZEND_JIT_DEBUG_GDB|ZEND_JIT_DEBUG_PERF_DUMP))) {
int opts = PROT_READ | PROT_WRITE;
# ifdef ZTS
# ifdef HAVE_PTHREAD_JIT_WRITE_PROTECT_NP
if (zend_write_protect) {
pthread_jit_write_protect_np(0);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did a bit of research and found that those where added in #8382 and adjusted in #9371. They are effectively obsolete: Apple Silicon ZTS now takes the dedicated MAP_JIT path, Intel reports pthread JIT write protection as unsupported, and Apple Silicon NTS uses a non-MAP_JIT buffer where the pthread call was unnecessary. I removed the old generic calls and scoped the pthread API entirely to ZEND_JIT_USE_APPLE_MAP_JIT.


#if defined(__x86_64__) || defined(i386) || defined(ZEND_WIN32)
# define ZEND_JIT_TARGET_X86 1
# define ZEND_JIT_TARGET_ARM64 0
Expand Down
55 changes: 55 additions & 0 deletions ext/opcache/tests/jit/gh13400.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
GH-13400: JIT code generated after fork is visible to the parent
--DESCRIPTION--
OPcache metadata and generated JIT code are shared by forked workers, so the
JIT mapping and its allocation pointer must remain shared after fork. This test
records the available JIT space before forking and makes only the child call a
function often enough to generate code. The parent then verifies that it sees
the reduced free space and can execute the generated function.

Without shared inheritance, a private JIT mapping becomes copy-on-write in the
child. The parent's free-space check would print bool(false), while shared
OPcache metadata could point the parent at code bytes that exist only in the
child and cause a crash instead of printing int(42). A startup-only test would
not detect this failure in fork-based SAPIs such as FPM or Apache.
--EXTENSIONS--
opcache
pcntl
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.file_update_protection=0
opcache.jit=tracing
opcache.jit_buffer_size=16M
opcache.jit_hot_func=1
--SKIPIF--
<?php
if (!function_exists('pcntl_fork')) die('skip pcntl_fork() not available');
if (!(opcache_get_status()['jit']['on'] ?? false)) die('skip JIT is not available');
?>
--FILE--
<?php
function value(): int {
return 42;
}

$bufferFree = opcache_get_status()['jit']['buffer_free'];
$pid = pcntl_fork();
if ($pid === 0) {
for ($i = 0; $i < 100; $i++) {
value();
}
exit(0);
}
if ($pid === -1) {
echo "pcntl_fork() failed\n";
exit(1);
}

pcntl_waitpid($pid, $status);
var_dump(opcache_get_status()['jit']['buffer_free'] < $bufferFree);
var_dump(value());
?>
--EXPECT--
bool(true)
int(42)
Loading