diff --git a/NEWS b/NEWS index faee75f69429..087e989ecafb 100644 --- a/NEWS +++ b/NEWS @@ -66,6 +66,8 @@ PHP NEWS - Opcache: . Fixed range inference of assignments through typed references. (Ilia Alshanetsky) + . Fixed bug GH-17770 (JIT-enabled scripts were not stored in the file cache). + (gregharmon, splitice) . Fixed bug GH-23693 (Tracing JIT produces wrong results for a guard on a loop-invariant addition). (Ilia Alshanetsky) . Fixed OSS-Fuzz #545352966 (default value AST of an SHM-persisted partial). diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index 2b71ce7a1fba..deb28dd87b1d 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3323,6 +3323,47 @@ static int zend_jit_setup_hot_counters(zend_op_array *op_array) return SUCCESS; } +void zend_jit_restore_original_handler(const zend_op_array *op_array, uint32_t index, zend_op *opline) +{ + zend_func_info *func_info = ZEND_FUNC_INFO(op_array); + + if (func_info) { + if (func_info->flags & ZEND_FUNC_JIT_ON_HOT_TRACE) { + zend_jit_op_array_trace_extension *ext = (zend_jit_op_array_trace_extension*)func_info; + opline->handler = ext->trace_info[index].orig_handler; + return; + } + if (func_info->flags & ZEND_FUNC_JIT_ON_HOT_COUNTERS) { + zend_jit_op_array_hot_extension *ext = (zend_jit_op_array_hot_extension*)func_info; + opline->handler = ext->orig_handlers[index]; + return; + } + if (func_info->flags & (ZEND_FUNC_JIT_ON_FIRST_EXEC | ZEND_FUNC_JIT_ON_PROF_REQUEST)) { + zend_jit_op_array_extension *ext = (zend_jit_op_array_extension*)func_info; + uint32_t entry = 0; + + if (!(op_array->fn_flags & ZEND_ACC_HAS_TYPE_HINTS)) { + while (entry < op_array->last + && (op_array->opcodes[entry].opcode == ZEND_RECV + || op_array->opcodes[entry].opcode == ZEND_RECV_INIT)) { + entry++; + } + } + if (index == entry) { + opline->handler = ext->orig_handler; + return; + } + } + } + + /* Script-load JIT does not retain a function extension. Only its generated + * handlers need to be rebuilt; other VM handlers may be specialized. */ + if (dasm_buf && (uintptr_t)op_array->opcodes[index].handler >= (uintptr_t)dasm_buf + && (uintptr_t)op_array->opcodes[index].handler < (uintptr_t)dasm_end) { + ZEND_VM_SET_OPCODE_HANDLER(opline); + } +} + #include "jit/zend_jit_trace.c" int zend_jit_op_array(zend_op_array *op_array, zend_script *script) diff --git a/ext/opcache/jit/zend_jit.h b/ext/opcache/jit/zend_jit.h index c4080d86bc6d..7d8c0c473679 100644 --- a/ext/opcache/jit/zend_jit.h +++ b/ext/opcache/jit/zend_jit.h @@ -161,6 +161,7 @@ extern zend_jit_globals jit_globals; int zend_jit_op_array(zend_op_array *op_array, zend_script *script); int zend_jit_script(zend_script *script); +void zend_jit_restore_original_handler(const zend_op_array *op_array, uint32_t index, zend_op *opline); void zend_jit_unprotect(void); void zend_jit_protect(void); void zend_jit_init(void); diff --git a/ext/opcache/tests/jit/gh17770_file_cache.phpt b/ext/opcache/tests/jit/gh17770_file_cache.phpt new file mode 100644 index 000000000000..09d9b96b5da1 --- /dev/null +++ b/ext/opcache/tests/jit/gh17770_file_cache.phpt @@ -0,0 +1,130 @@ +--TEST-- +GH-17770: JIT scripts survive a file cache round trip +--EXTENSIONS-- +opcache +--INI-- +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=16M +--SKIPIF-- + ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); +$output = stream_get_contents($pipes[1]); +fclose($pipes[1]); +fclose($pipes[2]); +if (proc_close($process) !== 0 || $output !== '1') die('skip JIT is unavailable'); +?> +--FILE-- +value(); +} +echo $box->value(), "\n"; +echo gh17770_array([]), "\n"; +PHP; + +function runScript(string $script, string $cache, string $jit, bool $cacheOnly): void { + $extension = ini_get('extension_dir') . DIRECTORY_SEPARATOR + . (PHP_OS_FAMILY === 'Windows' ? 'php_opcache.dll' : 'opcache.so'); + $cmd = [PHP_BINARY, '-n']; + if (file_exists($extension)) { + array_push($cmd, '-d', 'zend_extension="' . $extension . '"'); + } + array_push($cmd, + '-d', 'opcache.enable_cli=1', + '-d', 'opcache.jit_buffer_size=16M', + '-d', 'opcache.jit=' . $jit, + '-d', 'opcache.jit_hot_func=1', + '-d', 'opcache.file_cache="' . $cache . '"', + '-d', 'opcache.file_cache_only=' . (int) $cacheOnly, + '-d', 'opcache.validate_timestamps=0', + '-d', 'opcache.file_update_protection=0', + '-r', '$before = opcache_get_status(false)["jit"]["buffer_free"] ?? 0; ' + . 'require $argv[1]; ' + . '$after = opcache_get_status(false)["jit"]["buffer_free"] ?? 0; ' + . 'echo (int) ($before > $after), "\\n";', + $script, + ); + $process = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); + $output = stream_get_contents($pipes[1]); + $errors = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $status = proc_close($process); + if ($status !== 0 || $output !== ($cacheOnly ? "42\n42\n0\n" : "42\n42\n1\n") || $errors !== '') { + throw new RuntimeException("JIT $jit, file_cache_only=" . (int) $cacheOnly + . ": exit=$status, output=" . var_export($output, true) . ", errors=$errors"); + } +} + +foreach (['1205', '1215', '1235', '1255'] as $jit) { + $cache = $dir . '/' . $jit; + @mkdir($cache); + file_put_contents($script, $source); + runScript($script, $cache, $jit, false); + + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($cache)); + $found = false; + foreach ($files as $file) { + if ($file->isFile() && $file->getExtension() === 'bin') { + $found = true; + break; + } + } + if (!$found) { + throw new RuntimeException("JIT $jit did not write a file cache entry"); + } + + file_put_contents($script, ' +--CLEAN-- +isDir() ? rmdir($entry->getPathname()) : unlink($entry->getPathname()); + } + rmdir($dir); +} +?> +--EXPECT-- +1205 OK +1215 OK +1235 OK +1255 OK diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index 0b6cabe42015..fffa25dcff1c 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -37,6 +37,7 @@ #include "zend_accelerator_hash.h" #ifdef HAVE_JIT +#include "Optimizer/zend_func_info.h" #include "jit/zend_jit.h" #endif @@ -500,6 +501,10 @@ static void zend_file_cache_serialize_op_array(zend_op_array *op_arra zend_file_cache_metainfo *info, void *buf) { +#ifdef HAVE_JIT + /* Function info and JIT extensions refer to shared memory and generated code. */ + ZEND_SET_FUNC_INFO(op_array, NULL); +#endif ZEND_MAP_PTR_INIT(op_array->static_variables_ptr, NULL); ZEND_MAP_PTR_INIT(op_array->run_time_cache, NULL); @@ -555,6 +560,11 @@ static void zend_file_cache_serialize_op_array(zend_op_array *op_arra { zend_op *opline, *end; +#ifdef HAVE_JIT + zend_op *start_opline; + const zend_op_array *original = + (const zend_op_array*)((char*)script->mem + ((char*)op_array - (char*)buf)); +#endif #if !ZEND_USE_ABS_CONST_ADDR zval *literals = op_array->literals; @@ -565,7 +575,13 @@ static void zend_file_cache_serialize_op_array(zend_op_array *op_arra opline = op_array->opcodes; UNSERIALIZE_PTR(opline); end = opline + op_array->last; +#ifdef HAVE_JIT + start_opline = opline; +#endif while (opline < end) { +#ifdef HAVE_JIT + zend_jit_restore_original_handler(original, (uint32_t)(opline - start_opline), opline); +#endif if (opline->opcode == ZEND_OP_DATA && (opline-1)->opcode == ZEND_DECLARE_ATTRIBUTED_CONST ) { @@ -1163,13 +1179,6 @@ int zend_file_cache_script_store(zend_persistent_script *script, bool in_shm) zend_file_cache_metainfo info; void *mem, *buf; -#ifdef HAVE_JIT - /* FIXME: dump jited codes out to file cache? */ - if (JIT_G(on)) { - return FAILURE; - } -#endif - if (ZCG(accel_directives).file_cache_read_only) { return FAILURE; } @@ -2123,6 +2132,16 @@ zend_persistent_script *zend_file_cache_script_load_ex(zend_file_handle *file_ha script->corrupted = false; if (cache_it) { +#ifdef HAVE_JIT + if (JIT_G(on)) { + /* The file cache contains bytecode only. Rebuild JIT in shared memory. */ + zend_shared_alloc_init_xlat_table(); + zend_jit_unprotect(); + zend_jit_script(&script->script); + zend_jit_protect(); + zend_shared_alloc_destroy_xlat_table(); + } +#endif ZCSG(map_ptr_last) = CG(map_ptr_last); script->dynamic_members.last_used = ZCG(request_time);