diff --git a/ext/opcache/opcache.stub.php b/ext/opcache/opcache.stub.php index 526da238219a4..32673bb1dcee8 100644 --- a/ext/opcache/opcache.stub.php +++ b/ext/opcache/opcache.stub.php @@ -23,3 +23,5 @@ function opcache_jit_blacklist(Closure $closure): void {} function opcache_get_configuration(): array|false {} function opcache_is_script_cached(string $filename): bool {} + +function opcache_is_script_cached_in_file_cache(string $filename): bool {} diff --git a/ext/opcache/opcache_arginfo.h b/ext/opcache/opcache_arginfo.h index b4dc1f33a5fd8..7fff6b1eb0da9 100644 --- a/ext/opcache/opcache_arginfo.h +++ b/ext/opcache/opcache_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: c416c231c5d1270b7e5961f84cc3ca3e29db4959 */ + * Stub hash: a8de025fa96a78db3a26d53a18bb2b365d094eca */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_opcache_reset, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() @@ -26,6 +26,8 @@ ZEND_END_ARG_INFO() #define arginfo_opcache_is_script_cached arginfo_opcache_compile_file +#define arginfo_opcache_is_script_cached_in_file_cache arginfo_opcache_compile_file + ZEND_FUNCTION(opcache_reset); ZEND_FUNCTION(opcache_get_status); ZEND_FUNCTION(opcache_compile_file); @@ -33,6 +35,7 @@ ZEND_FUNCTION(opcache_invalidate); ZEND_FUNCTION(opcache_jit_blacklist); ZEND_FUNCTION(opcache_get_configuration); ZEND_FUNCTION(opcache_is_script_cached); +ZEND_FUNCTION(opcache_is_script_cached_in_file_cache); static const zend_function_entry ext_functions[] = { ZEND_FE(opcache_reset, arginfo_opcache_reset) @@ -42,5 +45,6 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(opcache_jit_blacklist, arginfo_opcache_jit_blacklist) ZEND_FE(opcache_get_configuration, arginfo_opcache_get_configuration) ZEND_FE(opcache_is_script_cached, arginfo_opcache_is_script_cached) + ZEND_FE(opcache_is_script_cached_in_file_cache, arginfo_opcache_is_script_cached_in_file_cache) ZEND_FE_END }; diff --git a/ext/opcache/tests/gh16551_998.inc b/ext/opcache/tests/gh16551_998.inc new file mode 100644 index 0000000000000..a769c4f5b323d --- /dev/null +++ b/ext/opcache/tests/gh16551_998.inc @@ -0,0 +1,5 @@ + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +opcache.jit_buffer_size=0 +opcache.file_cache="{PWD}/gh16551_fileonly_cache" +opcache.file_cache_only=1 +opcache.validate_timestamps=1 +--EXTENSIONS-- +opcache +--FILE-- + 0); // Expect true after compile + +echo "\nAttempting require:\n"; +require $file; // Outputs 9, should execute from file cache + +echo "\nState after require:\n"; +// State remains unchanged +var_dump(opcache_is_script_cached($file)); +var_dump(opcache_is_script_cached_in_file_cache($file)); + +echo "\nChecking uncached file initial state:\n"; +// SHM false, File Cache might be true in Pass 2 for this file too +var_dump(opcache_is_script_cached($uncached_file)); +var_dump(opcache_is_script_cached_in_file_cache($uncached_file)); + +echo "\nRequiring uncached file:\n"; +require $uncached_file; // Outputs 8, should compile to file cache now + +echo "\nState after requiring uncached file:\n"; +// SHM remains false, File cache becomes true for this file +var_dump(opcache_is_script_cached($uncached_file)); +var_dump(opcache_is_script_cached_in_file_cache($uncached_file)); + +?> +--CLEAN-- +isDir()) { + @rmdir($fileinfo->getRealPath()); + } else { + @unlink($fileinfo->getRealPath()); + } + } + @rmdir($dir); + } catch (UnexpectedValueException $e) { @rmdir($dir); } catch (Exception $e) { @rmdir($dir); } +} + +removeDirRecursive($baseCacheDir); +?> +--EXPECTF-- +Initial state (file_cache_only mode): +bool(false) +bool(%s) + +Attempting opcache_compile_file(): + +State after compile attempt: +bool(false) +bool(true) + +Checking file system for compiled file: +bool(true) + +Attempting require: +9 + +State after require: +bool(false) +bool(true) + +Checking uncached file initial state: +bool(false) +bool(%s) + +Requiring uncached file: +8 + +State after requiring uncached file: +bool(false) +bool(true) diff --git a/ext/opcache/tests/gh16551_invalidate_file_cache_only.phpt b/ext/opcache/tests/gh16551_invalidate_file_cache_only.phpt new file mode 100644 index 0000000000000..493173735ec86 --- /dev/null +++ b/ext/opcache/tests/gh16551_invalidate_file_cache_only.phpt @@ -0,0 +1,124 @@ +--TEST-- +GH-16551: Verify opcache_invalidate fails or is ineffective when opcache.file_cache_only=1 +--SKIPIF-- + +--INI-- +; Main test runs with file_cache_only=1 +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +opcache.jit_buffer_size=0 +opcache.file_cache="{PWD}/gh16551_invalidate_fco_cache" +opcache.file_cache_only=1 +opcache.validate_timestamps=0 +--EXTENSIONS-- +opcache +--FILE-- + +--CLEAN-- +isDir()) { + @rmdir($fileinfo->getRealPath()); + } else { + @unlink($fileinfo->getRealPath()); + } + } + @rmdir($dir); + } catch (UnexpectedValueException $e) { @rmdir($dir); } catch (Exception $e) { @rmdir($dir); } +} + +removeDirRecursive($baseCacheDir); +?> +--EXPECTF-- +Running helper script to populate cache... +Helper script successful. + +Verifying initial state (file_cache_only=1, cache populated): +bool(false) +bool(true) + +Attempting opcache_invalidate() with file_cache_only=1: +bool(false) + +Verifying state after invalidate attempt: +bool(false) +bool(true) \ No newline at end of file diff --git a/ext/opcache/tests/gh16551_jit_read_behavior.phpt b/ext/opcache/tests/gh16551_jit_read_behavior.phpt new file mode 100644 index 0000000000000..0dfe22388b166 --- /dev/null +++ b/ext/opcache/tests/gh16551_jit_read_behavior.phpt @@ -0,0 +1,155 @@ +--TEST-- +GH-16551: Verify reads work with JIT enabled from pre-populated cache +--SKIPIF-- + +--INI-- +; Main test process runs WITH JIT ENABLED +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=16M ; Ensure JIT has buffer +opcache.file_cache="{PWD}/gh16551_jit_read_cache" +opcache.file_cache_read_only=1 ; (default, writes disabled by JIT anyway) +opcache.validate_timestamps=1 +--EXTENSIONS-- +opcache +--FILE-- + +--CLEAN-- +isDir()) { + @rmdir($fileinfo->getRealPath()); + } else { + @unlink($fileinfo->getRealPath()); + } + } + @rmdir($dir); + } catch (UnexpectedValueException $e) { @rmdir($dir); } catch (Exception $e) { @rmdir($dir); } +} + +removeDirRecursive($baseCacheDir); +?> +--EXPECTF-- +Running helper script to populate cache (JIT disabled)... +Helper script successful. + +Main process JIT status: +bool(true) + +Initial state before read attempt (JIT enabled, cache populated by helper): +bool(false) +bool(true) + +Attempting require of pre-cached file with JIT enabled: +9 + +State after require (JIT enabled): +bool(%s) +bool(true) + +Attempting to compile another file with JIT enabled: +bool(true) +bool(false) diff --git a/ext/opcache/tests/gh16551_populate_and_check.phpt b/ext/opcache/tests/gh16551_populate_and_check.phpt new file mode 100644 index 0000000000000..ab37ec01a8aca --- /dev/null +++ b/ext/opcache/tests/gh16551_populate_and_check.phpt @@ -0,0 +1,94 @@ +--TEST-- +GH-16551: Populate memory and file cache, check status +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +opcache.jit_buffer_size=0 +opcache.file_cache="{PWD}/gh16551_populate_cache" +opcache.file_cache_only=0 +opcache.validate_timestamps=1 +--EXTENSIONS-- +opcache +--FILE-- + +--CLEAN-- +isDir()) { + @rmdir($fileinfo->getRealPath()); + } else { + @unlink($fileinfo->getRealPath()); + } + } + @rmdir($dir); + } catch (UnexpectedValueException $e) { @rmdir($dir); } catch (Exception $e) { @rmdir($dir); } +} + +removeDirRecursive($baseCacheDir); +?> +--EXPECTF-- +Initial state: +bool(%s) +bool(%s) + +Populating cache... + +State after opcache_compile_file(): +bool(true) +bool(true) + +Running file via require: +9 + +State after require: +bool(true) +bool(true) diff --git a/ext/opcache/tests/gh16551_populate_cache_helper.inc b/ext/opcache/tests/gh16551_populate_cache_helper.inc new file mode 100644 index 0000000000000..1c69a4b6dd134 --- /dev/null +++ b/ext/opcache/tests/gh16551_populate_cache_helper.inc @@ -0,0 +1,74 @@ + \n"; + exit(1); +} + +$fileToCache = $argv[1]; +$cacheDirPath = $argv[2]; // Path for verification and output file +$outputFile = $argv[3]; // File to signal completion/success + +// Basic validation +if (!file_exists($fileToCache)) { + @file_put_contents($outputFile, "ERROR: File to cache '$fileToCache' not found."); + exit(1); +} +// Ensure base directory exists, primarily for the output file placement +if (!is_dir($cacheDirPath) && !@mkdir($cacheDirPath, 0777, true)) { + @file_put_contents($outputFile, "ERROR: Could not create cache directory '$cacheDirPath' for output."); + // Don't necessarily exit, opcache might still create it, but log potential issue. + // Proceed with caution. +} + +// Check if OPcache extension is loaded (essential) +if (!extension_loaded('Zend OPcache')) { + @file_put_contents($outputFile, "ERROR: Zend OPcache extension not loaded in helper script."); + exit(1); +} + +// Compile the file to populate the cache (relies on -d flags for correct behavior) +$compileSuccess = opcache_compile_file($fileToCache); + +if (!$compileSuccess) { + @file_put_contents($outputFile, "ERROR: opcache_compile_file failed for '$fileToCache'. Check -d INI settings."); + exit(1); +} + +// Verification step: Check if the file cache was actually created +// This confirms the INI settings allowed writing to the expected location. +if (substr(PHP_OS, 0, 3) === 'WIN') { + // Use dirname() on the *target* file path, sanitize drive letter + $sanitizedDirName = str_replace(':', '', dirname($fileToCache)); + $pattern = $cacheDirPath . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . $sanitizedDirName . DIRECTORY_SEPARATOR . basename($fileToCache) . '.bin'; +} else { + // Use dirname() on the *target* file path + $pattern = $cacheDirPath . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . dirname($fileToCache) . DIRECTORY_SEPARATOR . basename($fileToCache) . '.bin'; +} + +// Use glob with a small delay/retry for filesystem eventual consistency +$tries = 3; +$found = []; +while ($tries > 0) { + clearstatcache(); // Clear filesystem stat cache + $found = glob($pattern); + if (!empty($found)) { + break; + } + $tries--; + if ($tries > 0) { + usleep(100000); // Wait 100ms before retrying glob + } +} + +if (!empty($found)) { + @file_put_contents($outputFile, "SUCCESS"); + exit(0); +} else { + @file_put_contents($outputFile, "ERROR: opcache_compile_file succeeded but cache file pattern not found after retries: $pattern"); + exit(1); +} diff --git a/ext/opcache/tests/gh16551_readonly_read_behavior.phpt b/ext/opcache/tests/gh16551_readonly_read_behavior.phpt new file mode 100644 index 0000000000000..c857bfdbfcfd8 --- /dev/null +++ b/ext/opcache/tests/gh16551_readonly_read_behavior.phpt @@ -0,0 +1,127 @@ +--TEST-- +GH-16551: Verify reads work with opcache.file_cache_read_only=1 from pre-populated cache +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +opcache.jit_buffer_size=0 +opcache.file_cache="{PWD}/gh16551_readonly_read_cache" +opcache.file_cache_read_only=1 +opcache.validate_timestamps=1 +--EXTENSIONS-- +opcache +--FILE-- + +--CLEAN-- +isDir()) { + @rmdir($fileinfo->getRealPath()); + } else { + @unlink($fileinfo->getRealPath()); + } + } + @rmdir($dir); + } catch (UnexpectedValueException $e) { @rmdir($dir); } catch (Exception $e) { @rmdir($dir); } +} + +removeDirRecursive($baseCacheDir); +?> +--EXPECTF-- +Running helper script to populate cache... +Helper script successful. + +Initial state before read attempt (read-only mode, cache populated by helper): +bool(false) +bool(true) + +Attempting require of pre-cached file in read-only mode: +9 + +State after require (read-only mode): +bool(%s) +bool(true) diff --git a/ext/opcache/tests/gh16551_readonly_write_behavior.phpt b/ext/opcache/tests/gh16551_readonly_write_behavior.phpt new file mode 100644 index 0000000000000..f1babd23171fd --- /dev/null +++ b/ext/opcache/tests/gh16551_readonly_write_behavior.phpt @@ -0,0 +1,116 @@ +--TEST-- +GH-16551: Verify writes are blocked by opcache.file_cache_read_only=1 +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +opcache.jit_buffer_size=0 +opcache.file_cache="{PWD}/gh16551_readonly_write_cache" +opcache.file_cache_read_only=1 +opcache.validate_timestamps=1 +opcache.file_cache_consistency_checks=1 +--EXTENSIONS-- +opcache +--FILE-- + +--CLEAN-- +isDir()) { + @rmdir($fileinfo->getRealPath()); + } else { + @unlink($fileinfo->getRealPath()); + } + } + @rmdir($dir); + } catch (UnexpectedValueException $e) { @rmdir($dir); } catch (Exception $e) { @rmdir($dir); } +} + +removeDirRecursive($baseCacheDir); +?> +--EXPECT-- +Initial state (read-only mode): +bool(false) +bool(false) +bool(false) +bool(false) + +Attempting opcache_compile_file() in read-only mode: + +State after compile attempt: +bool(true) +bool(false) + +Attempting require in read-only mode: +8 + +State after require attempt: +bool(true) +bool(false) + +Attempting opcache_invalidate() on SHM-cached script in read-only mode: +bool(true) + +State after invalidate attempt: +bool(false) +bool(false) diff --git a/ext/opcache/tests/gh16979_check_file_cache_function.phpt b/ext/opcache/tests/gh16979_check_file_cache_function.phpt new file mode 100644 index 0000000000000..828a8415fb455 --- /dev/null +++ b/ext/opcache/tests/gh16979_check_file_cache_function.phpt @@ -0,0 +1,136 @@ +--TEST-- +GH-16979: Test opcache_is_script_cached_in_file_cache function +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +opcache.jit_buffer_size=0 +opcache.file_cache="{PWD}/gh16979_cache" +opcache.file_cache_only=0 +opcache.validate_timestamps=0 +--EXTENSIONS-- +opcache +--FILE-- + +--CLEAN-- +isDir()) { + @rmdir($fileinfo->getRealPath()); + } else { + @unlink($fileinfo->getRealPath()); + } + } + @rmdir($dir); + } catch (UnexpectedValueException $e) { @rmdir($dir); } catch (Exception $e) { @rmdir($dir); } +} + +removeDirRecursive($baseCacheDir); +?> +--EXPECTF-- +Initial state: +bool(%s) +bool(%s) + +Populating cache via opcache_compile_file(): + +State after compile: +bool(true) +bool(true) + +Invalidating SHM only (force=false): + +State after SHM invalidate: +bool(false) +bool(false) + +Re-populated state: +bool(true) +bool(true) + +Forcing file cache invalidation (force=true): + +State after file cache invalidate: +bool(false) +bool(false) + +Checking file system: +bool(true) diff --git a/ext/opcache/zend_accelerator_module.c b/ext/opcache/zend_accelerator_module.c index 0c52b98dc453c..f7b4d4852a329 100644 --- a/ext/opcache/zend_accelerator_module.c +++ b/ext/opcache/zend_accelerator_module.c @@ -22,11 +22,13 @@ #include #include "php.h" +#include "zend.h" #include "ZendAccelerator.h" #include "zend_API.h" #include "zend_closures.h" #include "zend_shared_alloc.h" #include "zend_accelerator_blacklist.h" +#include "zend_file_cache.h" #include "php_ini.h" #include "SAPI.h" #include "zend_virtual_cwd.h" @@ -363,6 +365,29 @@ static int filename_is_in_cache(zend_string *filename) return 0; } +static int filename_is_in_file_cache(zend_string *filename) +{ + zend_string *realpath; + + realpath = zend_resolve_path(filename); + + if (!realpath) { + return 0; + } + + zend_file_handle handle; + + zend_stream_init_filename_ex(&handle, filename); + handle.opened_path = realpath; + + zend_persistent_script *result = zend_file_cache_script_load_ex(&handle, true); + + zend_destroy_file_handle(&handle); + + return result != NULL; +} + + static int accel_file_in_cache(INTERNAL_FUNCTION_PARAMETERS) { if (ZEND_NUM_ARGS() == 1) { @@ -998,3 +1023,28 @@ ZEND_FUNCTION(opcache_is_script_cached) RETURN_BOOL(filename_is_in_cache(script_name)); } + +/* {{{ Return true if the script is cached in OPCache file cache, false if it is not cached or if OPCache is not running. */ +ZEND_FUNCTION(opcache_is_script_cached_in_file_cache) +{ + zend_string *script_name; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(script_name) + ZEND_PARSE_PARAMETERS_END(); + + if (!validate_api_restriction()) { + RETURN_FALSE; + } + + // account for accelerator_enabled = false when file_cache_only = true + if (!(ZCG(accelerator_enabled) || ZCG(accel_directives).file_cache_only)) { + RETURN_FALSE; + } + + if (!ZCG(accel_directives).file_cache) { + RETURN_FALSE; + } + + RETURN_BOOL(filename_is_in_file_cache(script_name)); +} diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index d2b714d937dac..05baac2a8781b 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -1844,7 +1844,14 @@ static void zend_file_cache_unserialize(zend_persistent_script *script, zend_file_cache_unserialize_early_bindings(script, buf); } +static zend_persistent_script file_cache_validate_success_script; + zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handle) +{ + return zend_file_cache_script_load_ex(file_handle, false); +} + +zend_persistent_script *zend_file_cache_script_load_ex(zend_file_handle *file_handle, bool validate_only) { zend_string *full_path = file_handle->opened_path; int fd; @@ -1921,6 +1928,16 @@ zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handl return NULL; } + /* return here if validating */ + if (validate_only) { + if (zend_file_cache_flock(fd, LOCK_UN) != 0) { + zend_accel_error(ACCEL_LOG_WARNING, "opcache cannot unlock file '%s'\n", filename); + } + close(fd); + efree(filename); + return &file_cache_validate_success_script; + } + checkpoint = zend_arena_checkpoint(CG(arena)); #if defined(__AVX__) || defined(__SSE2__) /* Align to 64-byte boundary */ diff --git a/ext/opcache/zend_file_cache.h b/ext/opcache/zend_file_cache.h index 8f067f5f37abb..452f6b2c4c243 100644 --- a/ext/opcache/zend_file_cache.h +++ b/ext/opcache/zend_file_cache.h @@ -21,6 +21,7 @@ int zend_file_cache_script_store(zend_persistent_script *script, bool in_shm); zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handle); +zend_persistent_script *zend_file_cache_script_load_ex(zend_file_handle *file_handle, bool validate_only); void zend_file_cache_invalidate(zend_string *full_path); #endif /* ZEND_FILE_CACHE_H */