Commit a6f85c3bfd0 for php.net
commit a6f85c3bfd000f8eea2c9b0d03d0af081580a800
Author: Florian Engelhardt <florian.engelhardt@datadoghq.com>
Date: Wed Jul 15 14:28:09 2026 +0200
Re-enable JIT for ZTS builds on Apple Silicon
For __APPLE__ && __aarch64__ && ZTS, this moves the JIT buffer out of the OPcache shared memory and creates a dedicated mapping.
Background:
OPcache JIT is currently disabled for ZTS builds on Apple Silicon. The JIT buffer normally lives at the end of OPcache's MAP_SHARED | MAP_ANON allocation. In ZTS, one thread may generate code while another executes existing code, so the buffer must remain executable while it is writable.
Apple Silicon rejects RWX anonymous mappings with EPERM. Apple's supported solution is MAP_JIT with pthread_jit_write_protect_np(), which provides per-thread write protection, but macOS rejects MAP_JIT | MAP_SHARED with EINVAL. The existing combined OPcache/JIT mapping won't work on Apple Silicon.
Fixes GH-13400
Closes GH-22712
diff --git a/NEWS b/NEWS
index 6b1e857a870..5bb78b98c4e 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,9 @@ PHP NEWS
. Fixed bug GH-22668 (Heap buffer over-read when a column value exceeds the
driver-reported display size). (iliaal)
+- Opcache:
+ . Re-enable JIT for ZTS builds on Apple Silicon. (realFlowControl)
+
- PDO_ODBC:
. Fixed bug GH-22667 (Heap buffer over-read when a column value exceeds the
driver-reported display size). (iliaal)
diff --git a/UPGRADING b/UPGRADING
index 57a1f6a0eba..26e50b215f7 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -486,6 +486,9 @@ PHP 8.6 UPGRADE NOTES
- mysqli
. Added new constant MYSQLI_OPT_COMPRESS.
+- Opcache
+ . JIT is now supported for ZTS builds on Apple Silicon.
+
========================================
10. New Global Constants
========================================
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index cedc21c376a..ac6d6787a6d 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -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)
@@ -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;
@@ -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;
}
}
diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4
index 3798499a451..2a1ac4e332f 100644
--- a/ext/opcache/config.m4
+++ b/ext/opcache/config.m4
@@ -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], [
diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c
index fbbfab6b243..6f550f0cf36 100644
--- a/ext/opcache/jit/zend_jit.c
+++ b/ext/opcache/jit/zend_jit.c
@@ -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
@@ -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;
@@ -3517,17 +3516,14 @@ 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));
}
@@ -3535,11 +3531,11 @@ void zend_jit_unprotect(void)
#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);
@@ -3552,13 +3548,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));
}
@@ -3780,20 +3773,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);
+ 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));
@@ -3874,6 +3883,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 */
diff --git a/ext/opcache/jit/zend_jit.h b/ext/opcache/jit/zend_jit.h
index 2671ddd23e2..c4080d86bc6 100644
--- a/ext/opcache/jit/zend_jit.h
+++ b/ext/opcache/jit/zend_jit.h
@@ -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
+
#if defined(__x86_64__) || defined(i386) || defined(ZEND_WIN32)
# define ZEND_JIT_TARGET_X86 1
# define ZEND_JIT_TARGET_ARM64 0
diff --git a/ext/opcache/tests/jit/gh13400.phpt b/ext/opcache/tests/jit/gh13400.phpt
new file mode 100644
index 00000000000..db3d7b75576
--- /dev/null
+++ b/ext/opcache/tests/jit/gh13400.phpt
@@ -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)