From a6f85c3bfd000f8eea2c9b0d03d0af081580a800 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Wed, 15 Jul 2026 14:28:09 +0200 Subject: [PATCH 1/2] 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 --- NEWS | 3 ++ UPGRADING | 3 ++ ext/opcache/ZendAccelerator.c | 25 ++++++---- ext/opcache/config.m4 | 7 ++- ext/opcache/jit/zend_jit.c | 73 ++++++++++++++++++------------ ext/opcache/jit/zend_jit.h | 7 +++ ext/opcache/tests/jit/gh13400.phpt | 55 ++++++++++++++++++++++ 7 files changed, 131 insertions(+), 42 deletions(-) create mode 100644 ext/opcache/tests/jit/gh13400.phpt diff --git a/NEWS b/NEWS index 6b1e857a8708..5bb78b98c4e7 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 57a1f6a0ebad..26e50b215f77 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 cedc21c376a4..ac6d6787a6d6 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 3798499a4511..2a1ac4e332fc 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 fbbfab6b243c..6f550f0cf36c 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 #include +#include #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 2671ddd23e2d..c4080d86bc6d 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 000000000000..db3d7b755761 --- /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-- + +--FILE-- + +--EXPECT-- +bool(true) +int(42) From e2ea940fc95393452b2d8d52ea268368bfb43afe Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Wed, 15 Jul 2026 20:40:19 +0800 Subject: [PATCH 2/2] ext/gmp: Fix GMP operator RHS overflow for GMP values (#22656) ```php string(1) "1" } object(GMP)#2 (1) { ["num"]=> string(1) "2" } ``` This PR fix the overflow by rejecting large gmp objects in these calculation (throws ValueError) --- NEWS | 4 ++ ext/gmp/gmp.c | 46 ++++++++++++------- .../tests/gmp_operator_rhs_gmp_overflow.phpt | 33 +++++++++++++ 3 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 ext/gmp/tests/gmp_operator_rhs_gmp_overflow.phpt diff --git a/NEWS b/NEWS index 5bb78b98c4e7..cb10b3be0b6d 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.6.0alpha3 +- GMP: + . Fixed GMP power and shift operators to reject GMP right operands outside + the unsigned long range instead of silently truncating them. (Weilin Du) + - ODBC: . Fixed bug GH-22668 (Heap buffer over-read when a column value exceeds the driver-reported display size). (iliaal) diff --git a/ext/gmp/gmp.c b/ext/gmp/gmp.c index 59209c4e1be3..b05706e48297 100644 --- a/ext/gmp/gmp.c +++ b/ext/gmp/gmp.c @@ -327,8 +327,17 @@ static zend_result binop_operator_helper(gmp_binary_op_t gmp_op, zval *return_va typedef void (*gmp_binary_ui_op_t)(mpz_ptr, mpz_srcptr, gmp_ulong); +static void gmp_shift_operator_range_error(uint8_t opcode) { + zend_throw_error( + zend_ce_value_error, "%s must be between 0 and %lu", + opcode == ZEND_POW ? "Exponent" : "Shift", ULONG_MAX + ); +} + static zend_result shift_operator_helper(gmp_binary_ui_op_t op, zval *return_value, zval *op1, zval *op2, uint8_t opcode) { zend_long shift = 0; + gmp_ulong shift_ui = 0; + bool have_shift_ui = false; if (UNEXPECTED(Z_TYPE_P(op2) != IS_LONG)) { if (UNEXPECTED(!IS_GMP(op2))) { @@ -349,31 +358,36 @@ static zend_result shift_operator_helper(gmp_binary_ui_op_t op, zval *return_val goto typeof_op_failure; } } else { - // TODO We shouldn't cast the GMP object to int here - shift = zval_get_long(op2); + mpz_ptr gmpnum_shift = GET_GMP_FROM_ZVAL(op2); + if (!mpz_fits_ulong_p(gmpnum_shift)) { + gmp_shift_operator_range_error(opcode); + return FAILURE; + } + shift_ui = (gmp_ulong) mpz_get_ui(gmpnum_shift); + have_shift_ui = true; } } else { shift = Z_LVAL_P(op2); } - if (shift < 0 || shift > ULONG_MAX) { - zend_throw_error( - zend_ce_value_error, "%s must be between 0 and %lu", - opcode == ZEND_POW ? "Exponent" : "Shift", ULONG_MAX - ); - return FAILURE; - } else { - mpz_ptr gmpnum_op, gmpnum_result; - - if (!gmp_zend_parse_arg_into_mpz_ex(op1, &gmpnum_op, 1, true)) { - goto typeof_op_failure; + if (!have_shift_ui) { + if (shift < 0 || shift > ULONG_MAX) { + gmp_shift_operator_range_error(opcode); + return FAILURE; } + shift_ui = (gmp_ulong) shift; + } - INIT_GMP_RETVAL(gmpnum_result); - op(gmpnum_result, gmpnum_op, (gmp_ulong) shift); - return SUCCESS; + mpz_ptr gmpnum_op, gmpnum_result; + + if (!gmp_zend_parse_arg_into_mpz_ex(op1, &gmpnum_op, 1, true)) { + goto typeof_op_failure; } + INIT_GMP_RETVAL(gmpnum_result); + op(gmpnum_result, gmpnum_op, shift_ui); + return SUCCESS; + typeof_op_failure: ; /* Returning FAILURE without throwing an exception would emit the * Unsupported operand types: GMP OP TypeOfOp2 diff --git a/ext/gmp/tests/gmp_operator_rhs_gmp_overflow.phpt b/ext/gmp/tests/gmp_operator_rhs_gmp_overflow.phpt new file mode 100644 index 000000000000..e8b6139ace0f --- /dev/null +++ b/ext/gmp/tests/gmp_operator_rhs_gmp_overflow.phpt @@ -0,0 +1,33 @@ +--TEST-- +GMP operator right operand rejects values outside the unsigned long range +--EXTENSIONS-- +gmp +--FILE-- +getMessage(), PHP_EOL; +} + +try { + var_dump(gmp_init(2) << $too_large); +} catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + var_dump(gmp_init(2) >> $too_large); +} catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; +} + +echo "Done\n"; +?> +--EXPECTF-- +Exponent must be between 0 and %d +Shift must be between 0 and %d +Shift must be between 0 and %d +Done