diff --git a/ext/standard/Module.php b/ext/standard/Module.php index 70a442856d04..766ee2a0cdcf 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -16,6 +16,7 @@ use PHPCompiler\JIT\Builtin\StringCaseCompare; use PHPCompiler\JIT\Builtin\StringStrcoll; use PHPCompiler\JIT\Builtin\StringStrpbrk; +use PHPCompiler\JIT\Builtin\StringStrspn; use PHPCompiler\ModuleAbstract; use PHPCompiler\Runtime; use PHPCompiler\VM; @@ -1019,17 +1020,9 @@ public function jitInit(JIT\Context $context): void $fn = $context->module->addFunction('substr_compare', $ft); $context->registerFunction('substr_compare', $fn); } - foreach (['strspn', 'strcspn'] as $name) { - try { - $context->lookupFunction($name); - } catch (\Throwable $e) { - $i8p = $context->getTypeFromString('int8*'); - $sizeT = $context->getTypeFromString('size_t'); - $ft = $context->context->functionType($sizeT, false, $i8p, $i8p); - $fn = $context->module->addFunction($name, $ft); - $context->registerFunction($name, $fn); - } - } + // libc strspn/strcspn — never emit PHP bridges under these names (#26861). + \PHPCompiler\JIT\LibcExtern::register($context); + StringStrspn::ensureLinked($context); StringStrpbrk::ensureLinked($context); try { $context->lookupFunction('strrchr'); diff --git a/ext/standard/PasswordJitHelper.php b/ext/standard/PasswordJitHelper.php index e43bf487605d..fcf2b6ab2e4c 100644 --- a/ext/standard/PasswordJitHelper.php +++ b/ext/standard/PasswordJitHelper.php @@ -157,30 +157,50 @@ private static function verifyArgvThin(string $password, string $hash): int private static function bcryptEncodeSalt22(string $rnd16): string { + // NestedJIT: never gate the loop on strlen($out) after `$out .=` — strlen stays 0 + // and the loop never ends (#26861). Track emitted length in `$n` instead. // Local literal — NestedJIT class-const string can be null (#26773). $itoa = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $out = ''; - $len = \strlen($rnd16); $i = 0; - while (\strlen($out) < 22) { - $c1 = \ord($rnd16[$i++]); - $c2 = $i < $len ? \ord($rnd16[$i++]) : 0; + $n = 0; + while ($n < 22) { + $c1 = \ord($rnd16[$i]); + $i = $i + 1; + $c2 = 0; + if ($i < 16) { + $c2 = \ord($rnd16[$i]); + $i = $i + 1; + } $out .= $itoa[$c1 >> 2]; - if (\strlen($out) >= 22) { + $n = $n + 1; + if ($n >= 22) { break; } $out .= $itoa[(($c1 & 0x03) << 4) | ($c2 >> 4)]; - if (\strlen($out) >= 22) { + $n = $n + 1; + if ($n >= 22) { break; } - $c3 = $i < $len ? \ord($rnd16[$i++]) : 0; + $c3 = 0; + if ($i < 16) { + $c3 = \ord($rnd16[$i]); + $i = $i + 1; + } $out .= $itoa[(($c2 & 0x0f) << 2) | ($c3 >> 6)]; - if (\strlen($out) >= 22) { + $n = $n + 1; + if ($n >= 22) { break; } $out .= $itoa[$c3 & 0x3f]; + $n = $n + 1; + } + // Fixed-22 copy — avoid substr() NestedJIT edge cases (#26861 / soundex peer). + $result = ''; + for ($j = 0; $j < 22; ++$j) { + $result .= $out[$j]; } - return \substr($out, 0, 22); + return $result; } } diff --git a/ext/standard/strcasecmp.php b/ext/standard/strcasecmp.php index 0cb0d4f7d216..31e7938deb7c 100644 --- a/ext/standard/strcasecmp.php +++ b/ext/standard/strcasecmp.php @@ -48,7 +48,7 @@ public function call(Context $context, JITVariable ...$args): Value StringStrcasecmp::ensureLinked($context); $p0 = $this->stringDataPtr($context, self::jitStringArg($context, $args[0], 0, 'string1')); $p1 = $this->stringDataPtr($context, self::jitStringArg($context, $args[1], 1, 'string2')); - $fn = $context->lookupFunction('strcasecmp'); + $fn = $context->lookupFunction(\PHPCompiler\JIT\Builtin\StringCaseCompare::ABI_STRCASECMP); $raw = $context->builder->call($fn, $p0, $p1); $i64 = $context->getTypeFromString('int64'); diff --git a/ext/standard/strcoll.php b/ext/standard/strcoll.php index 6e50ef69fc8e..32b10663776a 100644 --- a/ext/standard/strcoll.php +++ b/ext/standard/strcoll.php @@ -48,7 +48,7 @@ public function call(Context $context, JITVariable ...$args): Value StringStrcoll::ensureLinked($context); $p0 = $this->stringDataPtr($context, self::jitStringArg($context, $args[0], 0, 'string1')); $p1 = $this->stringDataPtr($context, self::jitStringArg($context, $args[1], 1, 'string2')); - $fn = $context->lookupFunction('strcoll'); + $fn = $context->lookupFunction(StringStrcoll::ABI_STRCOLL); $raw = $context->builder->call($fn, $p0, $p1); $i64 = $context->getTypeFromString('int64'); diff --git a/ext/standard/strncasecmp.php b/ext/standard/strncasecmp.php index 8d763cad216b..905b934f8021 100644 --- a/ext/standard/strncasecmp.php +++ b/ext/standard/strncasecmp.php @@ -56,7 +56,12 @@ public function call(Context $context, JITVariable ...$args): Value ), $context->getTypeFromString('size_t') ); - $raw = $context->builder->call($context->lookupFunction('strncasecmp'), $p0, $p1, $length); + $raw = $context->builder->call( + $context->lookupFunction(\PHPCompiler\JIT\Builtin\StringCaseCompare::ABI_STRNCASECMP), + $p0, + $p1, + $length + ); $i64 = $context->getTypeFromString('int64'); return $context->builder->sExt($raw, $i64); diff --git a/lib/AOT/Linker.php b/lib/AOT/Linker.php index c1698f774c82..52ed382ba2fc 100644 --- a/lib/AOT/Linker.php +++ b/lib/AOT/Linker.php @@ -110,6 +110,7 @@ public static function link(string $objectFile, string $executable): void escapeshellarg($ld), AotDebugSymbols::linkFlag(), self::helperMuldefsFlag('-z muldefs'), + self::libcNameHideFlag(false), '-dynamic-linker /lib64/ld-linux-x86-64.so.2', escapeshellarg('/usr/lib/x86_64-linux-gnu/crt1.o'), escapeshellarg($crtbegin), @@ -145,7 +146,7 @@ public static function link(string $objectFile, string $executable): void // When linking with the bundled clang, ensure we can still resolve host libraries // (libpcre2-8, libcrypt, ...). Some bootstrap envs only ship the runtime .so/.a under // /usr/lib/x86_64-linux-gnu without a full sysroot lib tree. - $cmd = escapeshellarg($clang).' '.AotDebugSymbols::linkFlag().self::helperMuldefsFlag(' -Wl,-z,muldefs').$objects.' '.self::HOST_LIB_SEARCH.' -lm '.self::runtimeLinkLibs().' -o '.escapeshellarg($executable); + $cmd = escapeshellarg($clang).' '.AotDebugSymbols::linkFlag().self::helperMuldefsFlag(' -Wl,-z,muldefs').self::libcNameHideFlag(true).$objects.' '.self::HOST_LIB_SEARCH.' -lm '.self::runtimeLinkLibs().' -o '.escapeshellarg($executable); self::run($cmd, $env); self::unlinkIfTemp($runtimeObjects); @@ -161,6 +162,22 @@ private static function helperMuldefsFlag(string $flag): string return [] !== HelperRuntimeCache::linkObjects() ? ' '.$flag.' ' : ''; } + /** + * Version script that keeps libc-colliding PHP bridge leftovers local (#26861). + * + * @param bool $asWlPrefix true when the driver is clang/gcc (needs -Wl,) + */ + private static function libcNameHideFlag(bool $asWlPrefix): string + { + $ver = __DIR__.'/libc-name-hide.ver'; + if (!\is_file($ver)) { + return ''; + } + $arg = '--version-script='.$ver; + + return $asWlPrefix ? ' -Wl,'.\escapeshellarg($arg).' ' : ' '.\escapeshellarg($arg).' '; + } + /** * @return list */ @@ -500,7 +517,7 @@ private static function linkWithSystemCompiler( continue; } $cmd = escapeshellarg($path) . ' ' - . AotDebugSymbols::linkFlag() . self::helperMuldefsFlag(' -Wl,-z,muldefs') . $objects . ' '.self::HOST_LIB_SEARCH.' -lm '.self::RUNTIME_LINK_LIBS.' -o ' . escapeshellarg($executable); + . AotDebugSymbols::linkFlag() . self::helperMuldefsFlag(' -Wl,-z,muldefs') . self::libcNameHideFlag(true) . $objects . ' '.self::HOST_LIB_SEARCH.' -lm '.self::RUNTIME_LINK_LIBS.' -o ' . escapeshellarg($executable); $captured = self::runCaptured($cmd, null); if (0 === $captured['code']) { self::unlinkIfTemp($runtimeObjects); diff --git a/lib/AOT/libc-name-hide.ver b/lib/AOT/libc-name-hide.ver new file mode 100644 index 000000000000..7e797f369403 --- /dev/null +++ b/lib/AOT/libc-name-hide.ver @@ -0,0 +1,15 @@ +# Hide PHP string-bridge leftovers that collide with libc (#26861). +# Stale helper-runtime TUs may still define T strspn/strcspn/strcasecmp/…; +# if those stay dynamically exported, libxcrypt crypt(3) interposes them and +# returns *0 (password_hash/crypt AOT break). Main module now emits +# __compiler_* names; this script keeps any residual libc-named copies local. +{ + global: + *; + local: + strspn; + strcspn; + strcasecmp; + strncasecmp; + strcoll; +}; diff --git a/lib/JIT/Builtin/StringCaseCompare.php b/lib/JIT/Builtin/StringCaseCompare.php index c4e2ded0f0b7..71d1833fee21 100644 --- a/lib/JIT/Builtin/StringCaseCompare.php +++ b/lib/JIT/Builtin/StringCaseCompare.php @@ -15,7 +15,8 @@ * JIT/AOT link for strcasecmp/strncasecmp via CaseCompareJitHelper PHP (#15225, #23862). * * Helper compile: {@see JitVmHelperLink::ensureCompiled} (peer StringStrtotime #23832). - * Replaces libc `strcasecmp`/`strncasecmp` LLVM lookups in ext/standard. Keeps i8* ABI. + * PHP bridges use `__compiler_strcasecmp` / `__compiler_strncasecmp` so AOT does not + * export libc-named symbols that interpose into libxcrypt (#26861). Keeps i8* ABI. * SSOT: {@see \PHPCompiler\ext\standard\VmString} */ final class StringCaseCompare @@ -26,6 +27,10 @@ final class StringCaseCompare private const STRNCASECMP_HELPER = 'PHPCompiler\\ext\\standard\\CaseCompareJitHelper::strncasecmpArgv'; + public const ABI_STRCASECMP = '__compiler_strcasecmp'; + + public const ABI_STRNCASECMP = '__compiler_strncasecmp'; + /** @var list */ private const COMPILED_HELPERS = [ self::STRCASECMP_HELPER, @@ -34,12 +39,12 @@ final class StringCaseCompare public static function ensureStrcasecmpLinked(Context $context): void { - self::implementBinaryNamed($context, 'strcasecmp', self::STRCASECMP_HELPER); + self::implementBinaryNamed($context, self::ABI_STRCASECMP, self::STRCASECMP_HELPER); } public static function ensureStrncasecmpLinked(Context $context): void { - self::implementTernaryNamed($context, 'strncasecmp', self::STRNCASECMP_HELPER); + self::implementTernaryNamed($context, self::ABI_STRNCASECMP, self::STRNCASECMP_HELPER); } public static function ensureStandaloneBodies(Context $context): void diff --git a/lib/JIT/Builtin/StringStrcoll.php b/lib/JIT/Builtin/StringStrcoll.php index 3a16acfcb232..c9446213e957 100644 --- a/lib/JIT/Builtin/StringStrcoll.php +++ b/lib/JIT/Builtin/StringStrcoll.php @@ -15,7 +15,7 @@ * JIT/AOT link for strcoll via StrcollJitHelper PHP (#13566 phase 2, #22256). * * Helper compile: {@see JitVmHelperLink::ensureCompiled} (peer CopyRuntime #22231). - * Replaces libc `strcoll` LLVM lookups in ext/standard and ksort SORT_LOCALE_STRING. + * PHP bridge uses `__compiler_strcoll` so AOT does not export libc `strcoll` (#26861). * SSOT: {@see \PHPCompiler\ext\standard\VmLocaleCollate} */ final class StringStrcoll @@ -24,6 +24,8 @@ final class StringStrcoll private const STRCOLL_HELPER = 'PHPCompiler\\ext\\standard\\StrcollJitHelper::strcollArgv'; + public const ABI_STRCOLL = '__compiler_strcoll'; + /** @var list */ private const COMPILED_HELPERS = [ self::STRCOLL_HELPER, @@ -31,7 +33,7 @@ final class StringStrcoll public static function ensureLinked(Context $context): void { - self::implementNamed($context, 'strcoll', self::STRCOLL_HELPER); + self::implementNamed($context, self::ABI_STRCOLL, self::STRCOLL_HELPER); } public static function ensureStandaloneBodies(Context $context): void diff --git a/lib/JIT/Builtin/StringStrspn.php b/lib/JIT/Builtin/StringStrspn.php index 8c624ac11565..c0c43a292cfc 100644 --- a/lib/JIT/Builtin/StringStrspn.php +++ b/lib/JIT/Builtin/StringStrspn.php @@ -36,11 +36,16 @@ final class StringStrspn self::STRCSPN_TWO_ARG, ]; - /** @var list */ + /** + * LLVM ABI names must not collide with libc — AOT exports of `strspn`/`strcspn` + * interpose into libxcrypt and make crypt(3) return `*0` (#26861). + * + * @var list + */ private const ABI_FUNCTIONS = [ 'phpc_strspn_extended', - 'strspn', - 'strcspn', + '__compiler_strspn', + '__compiler_strcspn', ]; public static function ensureLinked(Context $context): void @@ -70,8 +75,8 @@ public static function implement(Context $context): void self::ensureJitHelperCompiled($context); self::implementExtendedBridge($context); - self::implementTwoArgBridge($context, 'strspn', self::STRSPN_TWO_ARG); - self::implementTwoArgBridge($context, 'strcspn', self::STRCSPN_TWO_ARG); + self::implementTwoArgBridge($context, '__compiler_strspn', self::STRSPN_TWO_ARG); + self::implementTwoArgBridge($context, '__compiler_strcspn', self::STRCSPN_TWO_ARG); self::registerLinkedRuntime($context); if (null !== $savedBlock) { diff --git a/lib/JIT/Builtin/Type/HashTable.php b/lib/JIT/Builtin/Type/HashTable.php index 4d8386df96af..56759c2dcdcc 100644 --- a/lib/JIT/Builtin/Type/HashTable.php +++ b/lib/JIT/Builtin/Type/HashTable.php @@ -231,7 +231,7 @@ private function ensureLibcStringCompare(): void $this->context->registerFunction('strnatcasecmp', $fn); } try { - $this->context->lookupFunction('strcoll'); + $this->context->lookupFunction(StringStrcoll::ABI_STRCOLL); } catch (\Throwable $e) { StringStrcoll::ensureLinked($this->context); } @@ -2268,7 +2268,7 @@ private function implementSortStringKeyValuesLocale(): void $valNext ); $cmp = $this->context->builder->call( - $this->context->lookupFunction('strcoll'), + $this->context->lookupFunction(StringStrcoll::ABI_STRCOLL), $this->stringDataPtr($strCur), $this->stringDataPtr($strNext) ); diff --git a/lib/JIT/LibcExtern.php b/lib/JIT/LibcExtern.php index 16d2d7bacd00..41f5e1f6061e 100644 --- a/lib/JIT/LibcExtern.php +++ b/lib/JIT/LibcExtern.php @@ -40,6 +40,9 @@ public static function register(Context $context): void 'strncmp' => [$i32, false, [$i8p, $i8p, $sizeT]], 'strcasecmp' => [$i32, false, [$i8p, $i8p]], 'strncasecmp' => [$i32, false, [$i8p, $i8p, $sizeT]], + 'strcoll' => [$i32, false, [$i8p, $i8p]], + 'strspn' => [$sizeT, false, [$i8p, $i8p]], + 'strcspn' => [$sizeT, false, [$i8p, $i8p]], 'strchr' => [$i8p, false, [$i8p, $i32]], 'strstr' => [$i8p, false, [$i8p, $i8p]], 'strrchr' => [$i8p, false, [$i8p, $i32]], diff --git a/prelinked/helper-runtime/x86_64-linux/units/ext_standard_PasswordJitHelper_php/manifest.json b/prelinked/helper-runtime/x86_64-linux/units/ext_standard_PasswordJitHelper_php/manifest.json index 531f95cc7e00..a5d86b9c43a3 100644 --- a/prelinked/helper-runtime/x86_64-linux/units/ext_standard_PasswordJitHelper_php/manifest.json +++ b/prelinked/helper-runtime/x86_64-linux/units/ext_standard_PasswordJitHelper_php/manifest.json @@ -1 +1 @@ -{"fingerprint":"3d2e9dcef9141e3b49b1","fingerprint_version":2,"unit":"/ext/standard/PasswordJitHelper.php","deps":["/ext/standard/PasswordJitHelper.php","/ext/standard/VmPassword.php"],"helpers":{"phpcompiler\\ext\\standard\\passwordjithelper::hashargv":"phpcompiler_ext_standard_passwordjithelper__hashargv","phpcompiler\\ext\\standard\\passwordjithelper::verifyargv":"phpcompiler_ext_standard_passwordjithelper__verifyargv","phpcompiler\\ext\\standard\\passwordjithelper::cryptargv":"phpcompiler_ext_standard_passwordjithelper__cryptargv","phpcompiler\\ext\\standard\\passwordjithelper::getinfohashtable":"phpcompiler_ext_standard_passwordjithelper__getinfohashtable","phpcompiler\\ext\\standard\\passwordjithelper::needsrehashargv":"phpcompiler_ext_standard_passwordjithelper__needsrehashargv","phpcompiler\\ext\\standard\\passwordjithelper::algoshashtable":"phpcompiler_ext_standard_passwordjithelper__algoshashtable"},"init_symbol":"__init__unit_ext_standard_PasswordJitHelper_php","shutdown_symbol":"__shutdown__unit_ext_standard_PasswordJitHelper_php","init_via_global_ctor":true,"runtime_safe":true} +{"fingerprint":"bf48d7b353f8e2594ea3","fingerprint_version":2,"unit":"/ext/standard/PasswordJitHelper.php","deps":["/ext/standard/PasswordJitHelper.php","/ext/standard/VmPassword.php"],"helpers":{"phpcompiler\\ext\\standard\\passwordjithelper::hashargv":"phpcompiler_ext_standard_passwordjithelper__hashargv","phpcompiler\\ext\\standard\\passwordjithelper::verifyargv":"phpcompiler_ext_standard_passwordjithelper__verifyargv","phpcompiler\\ext\\standard\\passwordjithelper::cryptargv":"phpcompiler_ext_standard_passwordjithelper__cryptargv","phpcompiler\\ext\\standard\\passwordjithelper::getinfohashtable":"phpcompiler_ext_standard_passwordjithelper__getinfohashtable","phpcompiler\\ext\\standard\\passwordjithelper::needsrehashargv":"phpcompiler_ext_standard_passwordjithelper__needsrehashargv","phpcompiler\\ext\\standard\\passwordjithelper::algoshashtable":"phpcompiler_ext_standard_passwordjithelper__algoshashtable"},"init_symbol":"__init__unit_ext_standard_PasswordJitHelper_php","shutdown_symbol":"__shutdown__unit_ext_standard_PasswordJitHelper_php","init_via_global_ctor":true,"runtime_safe":true} diff --git a/prelinked/helper-runtime/x86_64-linux/units/ext_standard_PasswordJitHelper_php/unit.o b/prelinked/helper-runtime/x86_64-linux/units/ext_standard_PasswordJitHelper_php/unit.o index f6282bcf67df..d2db46cce32f 100644 Binary files a/prelinked/helper-runtime/x86_64-linux/units/ext_standard_PasswordJitHelper_php/unit.o and b/prelinked/helper-runtime/x86_64-linux/units/ext_standard_PasswordJitHelper_php/unit.o differ diff --git a/test/repro/maintainer_gap_aot_password_hash_bcrypt.php b/test/repro/maintainer_gap_aot_password_hash_bcrypt.php new file mode 100644 index 000000000000..a49a99257e2a --- /dev/null +++ b/test/repro/maintainer_gap_aot_password_hash_bcrypt.php @@ -0,0 +1,16 @@ + 4]); +if (!\is_string($h)) { + echo "type=", \gettype($h), "\n"; + exit(2); +} +echo \password_verify('secret', $h) ? 'ok' : 'bad', "\n"; +echo \password_verify('wrong', $h) ? 'bad' : 'ok', "\n"; diff --git a/test/unit/LibcNameCollisionRuntimeShrinkTest.php b/test/unit/LibcNameCollisionRuntimeShrinkTest.php new file mode 100644 index 000000000000..ad5dd3ba15ab --- /dev/null +++ b/test/unit/LibcNameCollisionRuntimeShrinkTest.php @@ -0,0 +1,49 @@ +assertStringContainsString('__compiler_strspn', $source); + $this->assertStringContainsString('__compiler_strcspn', $source); + $this->assertStringNotContainsString("'strspn'", $source); + $this->assertStringNotContainsString("'strcspn'", $source); + } + + public function testStringCaseCompareUsesCompilerPrefixedAbi(): void + { + $this->assertSame('__compiler_strcasecmp', StringCaseCompare::ABI_STRCASECMP); + $this->assertSame('__compiler_strncasecmp', StringCaseCompare::ABI_STRNCASECMP); + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/StringCaseCompare.php'); + $this->assertStringContainsString('__compiler_strcasecmp', $source); + $this->assertStringContainsString('__compiler_strncasecmp', $source); + } + + public function testStringStrcollUsesCompilerPrefixedAbi(): void + { + $this->assertSame('__compiler_strcoll', StringStrcoll::ABI_STRCOLL); + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/StringStrcoll.php'); + $this->assertStringContainsString('__compiler_strcoll', $source); + } + + public function testLibcExternDeclaresRealStrspnFamily(): void + { + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/LibcExtern.php'); + $this->assertStringContainsString("'strspn'", $source); + $this->assertStringContainsString("'strcspn'", $source); + $this->assertStringContainsString("'strcoll'", $source); + } +} diff --git a/test/unit/PasswordCryptoRuntimeShrinkTest.php b/test/unit/PasswordCryptoRuntimeShrinkTest.php index 0fad834ca6d3..d3b8d00eb9ec 100644 --- a/test/unit/PasswordCryptoRuntimeShrinkTest.php +++ b/test/unit/PasswordCryptoRuntimeShrinkTest.php @@ -56,6 +56,10 @@ public function testPasswordJitHelperDelegatesToVmPassword(): void $this->assertStringContainsString('phpc_argon2_hash', $source); $this->assertStringContainsString('phpc_libcrypt_kernel', $source); $this->assertStringContainsString('NestedJitCompileScope::isActive', $source); + // NestedJIT: strlen($out) after .= never grows — infinite loop / SEGV (#26861). + $this->assertStringContainsString('while ($n < 22)', $source); + $this->assertMatchesRegularExpression('/while\s*\(\s*\$n\s*<\s*22\s*\)/', $source); + $this->assertDoesNotMatchRegularExpression('/while\s*\(\s*\\\\?strlen\s*\(\s*\$out\s*\)/', $source); } public function testPasswordJitHelperHashMatchesVmPassword(): void