Commit 5e7bad47483 for php
commit 5e7bad4748317a9bf06a2ece93ac2755d81c4579
Author: Pavel Gushcha <pavimus@gmail.com>
Date: Fri Oct 9 17:03:58 2026 +0000
Fix preloaded functions JIT-compiled on load being overwritten after restart
zend_jit_startup() records the JIT buffer position that
zend_jit_restart() rewinds to (dasm_ptr[1]) before preloading runs.
With a JIT trigger that compiles on script load (e.g. opcache.jit=function)
preloaded functions are compiled during preloading, i.e. after that
point. An OPcache restart (opcache_reset(), OOM, hash overflow) then
rewinds the buffer below their code, while their handlers still point
into it: the next JIT compilation overwrites the code and calling a
preloaded function returns garbage or crashes. This happens in NTS with
a single process as well.
zend_jit_restart_preloaded_op_array() only resets the hot counters of
preloaded functions; functions compiled on load cannot be restored.
Record the restart point again after preloading, so code generated by
preloading survives restarts. Traces and functions compiled at runtime
are discarded as before.
Closes GH-24274.
diff --git a/NEWS b/NEWS
index de5109dd05c..edd2429a340 100644
--- a/NEWS
+++ b/NEWS
@@ -183,6 +183,8 @@ PHP NEWS
. Fix block pass dropping ZEND_FAST_CALL OP2 definition. (Mrmaxmeier)
. Fixed bug GH-23626 (orphaned temporaries cause leaks and assertions).
(ndossche, khaledalam)
+ . Fix preloaded functions JIT-compiled on load being overwritten
+ after restart. (Pavel Gushcha)
- OpenSSL:
. Fixed bug GH-24784 (openssl_pkcs7_read() and openssl_cms_read() overwrite
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index 0ea820fb292..4df75049e17 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -4790,6 +4790,9 @@ static zend_result accel_preload(const char *config, bool in_child)
zend_shared_alloc_save_state();
accel_interned_strings_save_state();
+#ifdef HAVE_JIT
+ zend_jit_save_restart_point();
+#endif
SHM_PROTECT();
HANDLE_UNBLOCK_INTERRUPTIONS();
diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c
index 679b7db269f..30169921173 100644
--- a/ext/opcache/jit/zend_jit.c
+++ b/ext/opcache/jit/zend_jit.c
@@ -3733,10 +3733,7 @@ void zend_jit_startup(void *buf, size_t size, bool reattached)
zend_jit_trace_startup(reattached);
- zend_jit_unprotect();
- /* save JIT buffer pos */
- dasm_ptr[1] = dasm_ptr[0];
- zend_jit_protect();
+ zend_jit_save_restart_point();
}
void zend_jit_shutdown(void)
@@ -3918,4 +3915,15 @@ void zend_jit_restart(void)
}
}
+/* Saves the current JIT buffer position as the one zend_jit_restart() rewinds to,
+ * so that the code generated so far is kept across restarts */
+void zend_jit_save_restart_point(void)
+{
+ if (dasm_ptr) {
+ zend_jit_unprotect();
+ dasm_ptr[1] = dasm_ptr[0];
+ zend_jit_protect();
+ }
+}
+
#endif /* HAVE_JIT */
diff --git a/ext/opcache/jit/zend_jit.h b/ext/opcache/jit/zend_jit.h
index 9178d340a0e..f2d2c8a7d18 100644
--- a/ext/opcache/jit/zend_jit.h
+++ b/ext/opcache/jit/zend_jit.h
@@ -167,6 +167,7 @@ void zend_jit_deactivate(void);
ZEND_EXT_API void zend_jit_status(zval *ret);
ZEND_EXT_API void zend_jit_blacklist_function(zend_op_array *op_array);
void zend_jit_restart(void);
+void zend_jit_save_restart_point(void);
#define ZREG_LOAD (1<<0)
#define ZREG_STORE (1<<1)
diff --git a/ext/opcache/tests/jit/preload_restart_function_jit.phpt b/ext/opcache/tests/jit/preload_restart_function_jit.phpt
new file mode 100644
index 00000000000..5571b52dfc2
--- /dev/null
+++ b/ext/opcache/tests/jit/preload_restart_function_jit.phpt
@@ -0,0 +1,82 @@
+--TEST--
+Preloaded functions JIT-compiled on script load must survive an OPcache restart
+--EXTENSIONS--
+opcache
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows');
+if (ini_get('opcache.jit') === false) die('skip PHP is compiled without JIT');
+?>
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=disable
+opcache.jit_buffer_size=16M
+--CONFLICTS--
+server
+--FILE--
+<?php
+$dir = __DIR__ . '/preload_restart_function_jit';
+@mkdir($dir);
+
+file_put_contents($dir . '/preload.php', <<<'PHP'
+<?php
+function preloaded_sum(int $n): int {
+ $s = 0;
+ for ($i = 0; $i < $n; $i++) {
+ $s += ($i * 3) ^ ($i >> 2);
+ }
+ return $s;
+}
+function preloaded_str(string $x): string {
+ return strrev($x) . strlen($x);
+}
+PHP);
+
+file_put_contents($dir . '/call.php', <<<'PHP'
+<?php
+echo preloaded_sum(1000), " ", preloaded_str("hello"), "\n";
+PHP);
+
+file_put_contents($dir . '/reset.php', <<<'PHP'
+<?php
+var_dump(opcache_reset());
+PHP);
+
+/* Compiling a new script after the restart reuses the rewound JIT buffer */
+$code = "<?php\n";
+for ($i = 0; $i < 200; $i++) {
+ $code .= "function f$i(int \$n): int { \$s = $i; for (\$k = 0; \$k < \$n; \$k++) { \$s = (\$s * 31 + \$k) % 1000003; } return \$s; }\n";
+}
+$code .= "echo f0(10) + f199(10), \"\\n\";\n";
+file_put_contents($dir . '/new.php', $code);
+
+include __DIR__ . '/../php_cli_server.inc';
+
+$ini = '-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.jit=function -d opcache.jit_buffer_size=16M'
+ . ' -d opcache.validate_timestamps=0 -d opcache.file_update_protection=0 -d opcache.preload_user=root'
+ . ' -d opcache.preload=' . $dir . '/preload.php';
+if (file_exists(ini_get('extension_dir') . '/opcache.' . PHP_SHLIB_SUFFIX)) {
+ $ini .= ' -d zend_extension=opcache.' . PHP_SHLIB_SUFFIX;
+}
+php_cli_server_start($ini);
+
+$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/jit/preload_restart_function_jit/';
+foreach (['call', 'reset', 'new', 'call', 'call'] as $script) {
+ echo @file_get_contents($base . $script . '.php') ?: "no response\n";
+}
+?>
+--CLEAN--
+<?php
+$dir = __DIR__ . '/preload_restart_function_jit';
+foreach (['preload', 'call', 'reset', 'new'] as $script) {
+ @unlink("$dir/$script.php");
+}
+@rmdir($dir);
+?>
+--EXPECT--
+1502868 olleh5
+bool(true)
+1289143
+1502868 olleh5
+1502868 olleh5