diff --git a/src/Turbo/TurboExtensionSelector.php b/src/Turbo/TurboExtensionSelector.php index 485f2ac3792..1a802535dbb 100644 --- a/src/Turbo/TurboExtensionSelector.php +++ b/src/Turbo/TurboExtensionSelector.php @@ -3,13 +3,13 @@ namespace PHPStan\Turbo; use Phar; -use function count; use function dirname; +use function file_get_contents; use function getenv; -use function glob; use function is_file; use function php_uname; use function sprintf; +use function str_contains; use const PHP_DEBUG; use const PHP_MAJOR_VERSION; use const PHP_MINOR_VERSION; @@ -115,18 +115,25 @@ public static function resolvePlatformDirectory(string $osFamily, string $machin } /** - * libc has no PHP constant; this is the same filesystem heuristic - * datadog-setup.php uses. + * libc has no PHP constant; this checks what loader is actually + * mapped into the running process rather than whether a musl loader + * merely exists somewhere on disk — a glibc host with musl-tools + * installed has one too, and would otherwise be misdetected as musl. */ public static function isMusl(): bool { - if (is_file('/etc/alpine-release')) { - return true; - } + return self::resolveIsMusl(@file_get_contents('/proc/self/maps'), is_file('/etc/alpine-release')); + } - $muslLoaders = glob('/lib/ld-musl-*'); + public static function resolveIsMusl(string|false $selfMaps, bool $hasAlpineRelease): bool + { + if ($selfMaps !== false) { + return str_contains($selfMaps, '/ld-musl-'); + } - return $muslLoaders !== false && count($muslLoaders) > 0; + // no procfs to inspect (non-Linux, or a sandboxed container without + // /proc) — Alpine's own musl PHP packages are still worth detecting + return $hasAlpineRelease; } } diff --git a/tests/PHPStan/Turbo/TurboExtensionSelectorTest.php b/tests/PHPStan/Turbo/TurboExtensionSelectorTest.php index 3db926401af..63a05152959 100644 --- a/tests/PHPStan/Turbo/TurboExtensionSelectorTest.php +++ b/tests/PHPStan/Turbo/TurboExtensionSelectorTest.php @@ -35,4 +35,44 @@ public function testResolvePlatformDirectory(string $osFamily, string $machine, $this->assertSame($expected, TurboExtensionSelector::resolvePlatformDirectory($osFamily, $machine, $isMusl)); } + /** + * @return iterable + */ + public static function dataResolveIsMusl(): iterable + { + // captured from `php -r 'echo file_get_contents("/proc/self/maps");'` + // running inside the official php:8.4-cli (glibc/Debian) image + $glibcMaps = <<<'MAPS' + 58d76a000000-58d76a159000 r--p 00000000 00:67 14567852 /usr/local/bin/php + 58d76a200000-58d76a714000 r-xp 00200000 00:67 14567852 /usr/local/bin/php + 7da75ca7a000-7da75ca85000 r--p 00029000 00:67 14556924 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 + 7da75ca85000-7da75ca87000 r--p 00034000 00:67 14556924 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 + 7ffe37e20000-7ffe37e41000 rw-p 00000000 00:00 0 [stack] + MAPS; + + // same command, inside php:8.4-cli-alpine3.23 — /lib/ld-musl-x86_64.so.1 + // is the musl dynamic loader mapped into the process + $muslMaps = <<<'MAPS' + 58adcee00000-58adcef58000 r--p 00000000 00:67 14555462 /usr/local/bin/php + 58adcf000000-58adcf525000 r-xp 00200000 00:67 14555462 /usr/local/bin/php + 7934c7f6a000-7934c7f7e000 r--p 00000000 00:67 14554411 /lib/ld-musl-x86_64.so.1 + 7934c7f7e000-7934c7fd6000 r-xp 00014000 00:67 14554411 /lib/ld-musl-x86_64.so.1 + 7934c800c000-7934c8010000 rw-p 00000000 00:00 0 + MAPS; + + // glibc host with a musl loader present on disk (e.g. musl-tools + // installed) but not mapped into this process — the false positive + // the old glob-based heuristic produced + yield [$glibcMaps, false, false]; + yield [$muslMaps, false, true]; + yield [false, false, false]; + yield [false, true, true]; + } + + #[DataProvider('dataResolveIsMusl')] + public function testResolveIsMusl(string|false $selfMaps, bool $hasAlpineRelease, bool $expected): void + { + $this->assertSame($expected, TurboExtensionSelector::resolveIsMusl($selfMaps, $hasAlpineRelease)); + } + }