Skip to content
Merged
3 changes: 3 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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
========================================
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
73 changes: 44 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 @@ -3517,29 +3516,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 +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));
}
Expand Down Expand Up @@ -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);
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 +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 */
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
Comment thread
realFlowControl marked this conversation as resolved.

#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)