From 64963a9bcad9415132fe84134724328b864dfee0 Mon Sep 17 00:00:00 2001 From: wadakatu Date: Wed, 19 Aug 2026 18:29:12 +0900 Subject: [PATCH] report the real reason when FFI startup fails libraryLoad() caught every FFI\Exception and routed it to Utils::debugLog(), which does nothing unless the user has installed a PSR-3 logger. The caller then threw "Make sure that you've installed libvips", whatever the real cause was. That is what issue #286 hit: FFI had refused to run, but the message pointed at libvips. Keep the last failure message and include it in the exception, so the engine's own explanation reaches the user. That also removes the need to guess at ffi.enable. Under ffi.enable=preload the engine allows an FFI call iff the SAPI is exactly "cli", or the immediate calling function is ZEND_ACC_PRELOADED, or preload compilation is running (php-src ext/ffi/ffi.c:2996-3011), so the same value means usable in one process and dead in another, and ZEND_ACC_PRELOADED is not exposed to PHP. PHP already answers this per call, and now says why when the answer is no. Two consequences: - php-vips works under ffi.enable=preload when php-vips' src is in opcache.preload -- no header file and no FFI::scope needed. FFI::preload() does that from the app's preload script, so the file list is ours rather than something every user has to get right. - "preload" is PHP's compiled-in default (ffi.c:5332) and php.ini-production ships the setting commented out, so php-vips no longer refuses to start on a stock PHP install. --- CHANGELOG.md | 2 ++ README.md | 37 +++++++++++++++++++++++---- src/FFI.php | 58 +++++++++++++++++++++++++++++++++++++------ tests/PreloadTest.php | 40 +++++++++++++++++++++++++++++ tests/preload.php | 9 +++++++ 5 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 tests/PreloadTest.php create mode 100644 tests/preload.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 81d970bf..944066b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to `php-vips` will be documented in this file. ## master +- report the real reason when FFI startup fails, and support + `ffi.enable=preload` with a new `FFI::preload()` [wadakatu] - better ffi startup diagnostics [ping-localhost] - add setBlock() and setBlockUntrusted() to control operation blocking [jcupitt] diff --git a/README.md b/README.md index e435a8ed..a7d3016a 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,39 @@ to your `composer.json`: } ``` -php-vips does not yet support preloading, so you need to enable FFI globally. -This has some security implications, since anyone who can run php on your -server can use it to call any native library they have access to. +The simplest thing is `ffi.enable=true`, but that lets any PHP code on your +server call any native library it has access to. You can avoid that with +`ffi.enable=preload`, which is PHP's default: FFI is then only usable from code +that PHP loaded at startup via `opcache.preload`, so php-vips can use it but +the rest of your application cannot. -Of course if attackers are running their own PHP code on your webserver you -are probably already toast, unfortunately. +To run php-vips that way, write a preload script: + +```php + $libraryName]); + self::$libraryLoadError = ""; foreach (self::$libraryPaths as $path) { Utils::debugLog("trying path", ["path" => $path]); try { @@ -261,6 +305,7 @@ private static function libraryLoad( Utils::debugLog("success", []); return $library; } catch (\FFI\Exception $e) { + self::$libraryLoadError = $e->getMessage(); Utils::debugLog("init", [ "msg" => "library load failed", "exception" => $e->getMessage() @@ -281,11 +326,6 @@ private static function init(): void if (!extension_loaded("ffi")) { throw new Exception("FFI extension not loaded"); } - $enable = ini_get("ffi.enable"); - if ($enable != "true" && - $enable != "1") { - throw new Exception("ffi.enable set to '$enable', not 'true'"); - } $vips_libname = self::libraryName("libvips", 42); $glib_libname = self::libraryName("libglib-2.0", 0); @@ -315,8 +355,12 @@ private static function init(): void if (!empty(self::$libraryPaths)) { $msg .= " in any of ['" . implode("', '", self::$libraryPaths) . "']"; } - $msg .= ". Make sure that you've installed libvips and that '$vips_libname'"; - $msg .= " is on your system's library search path."; + if (self::$libraryLoadError !== "") { + $msg .= ". The error was: " . self::$libraryLoadError; + } + $msg .= ". Check that libvips is installed, that '$vips_libname' is on"; + $msg .= " your system's library search path, and that FFI is usable"; + $msg .= " (see the ffi.enable notes in the php-vips README)."; throw new Exception($msg); } diff --git a/tests/PreloadTest.php b/tests/PreloadTest.php new file mode 100644 index 00000000..194cb809 --- /dev/null +++ b/tests/PreloadTest.php @@ -0,0 +1,40 @@ +markTestSkipped('no opcache'); + } + + /* opcache.preload can only be set at startup, so we have to use a + * subprocess. + */ + $command = escapeshellarg(PHP_BINARY) . ' -d opcache.enable_cli=1'; + + /* Before php 8.3, preloading as root fails unless you name the user + * to drop to. php warns if you set it when you are not root. + */ + if (function_exists('posix_geteuid') && posix_geteuid() === 0) { + $command .= ' -d opcache.preload_user=root'; + } + + $output = shell_exec( + $command . + ' -d opcache.preload=' . escapeshellarg(__DIR__ . '/preload.php') . + ' -r ' . escapeshellarg( + '$status = opcache_get_status();' . + 'echo in_array("Jcupitt\\Vips\\Image", ' . + '$status["preload_statistics"]["classes"]) ? "preloaded" : "missing";' + ) . ' 2>&1' + ); + + $this->assertStringNotContainsString("Can't preload", $output); + $this->assertStringContainsString('preloaded', $output); + } +} diff --git a/tests/preload.php b/tests/preload.php new file mode 100644 index 00000000..8aea0831 --- /dev/null +++ b/tests/preload.php @@ -0,0 +1,9 @@ +