Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions ext/standard/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down
38 changes: 29 additions & 9 deletions ext/standard/PasswordJitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion ext/standard/strcasecmp.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
2 changes: 1 addition & 1 deletion ext/standard/strcoll.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
7 changes: 6 additions & 1 deletion ext/standard/strncasecmp.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
21 changes: 19 additions & 2 deletions lib/AOT/Linker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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);

Expand All @@ -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<string>
*/
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions lib/AOT/libc-name-hide.ver
Original file line number Diff line number Diff line change
@@ -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;
};
11 changes: 8 additions & 3 deletions lib/JIT/Builtin/StringCaseCompare.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string> */
private const COMPILED_HELPERS = [
self::STRCASECMP_HELPER,
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions lib/JIT/Builtin/StringStrcoll.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,14 +24,16 @@ final class StringStrcoll

private const STRCOLL_HELPER = 'PHPCompiler\\ext\\standard\\StrcollJitHelper::strcollArgv';

public const ABI_STRCOLL = '__compiler_strcoll';

/** @var list<string> */
private const COMPILED_HELPERS = [
self::STRCOLL_HELPER,
];

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
Expand Down
15 changes: 10 additions & 5 deletions lib/JIT/Builtin/StringStrspn.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ final class StringStrspn
self::STRCSPN_TWO_ARG,
];

/** @var list<string> */
/**
* 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<string>
*/
private const ABI_FUNCTIONS = [
'phpc_strspn_extended',
'strspn',
'strcspn',
'__compiler_strspn',
'__compiler_strcspn',
];

public static function ensureLinked(Context $context): void
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions lib/JIT/Builtin/Type/HashTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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)
);
Expand Down
3 changes: 3 additions & 0 deletions lib/JIT/LibcExtern.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
Expand Down
Original file line number Diff line number Diff line change
@@ -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}
Binary file not shown.
16 changes: 16 additions & 0 deletions test/repro/maintainer_gap_aot_password_hash_bcrypt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/**
* #26861 — AOT password_hash(PASSWORD_BCRYPT) must verify like Zend (no SEGV / false hash).
*
* Root causes fixed: (1) AOT exported strspn/strcspn under libc names → libxcrypt
* crypt(3) returned *0; (2) NestedJIT bcryptEncodeSalt22 strlen($out) after .= never
* grew → infinite loop / SEGV.
*/
$h = password_hash('secret', PASSWORD_BCRYPT, ['cost' => 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";
49 changes: 49 additions & 0 deletions test/unit/LibcNameCollisionRuntimeShrinkTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\Test\Unit;

use PHPCompiler\JIT\Builtin\StringCaseCompare;
use PHPCompiler\JIT\Builtin\StringStrcoll;
use PHPUnit\Framework\TestCase;

/**
* PHP string bridges must not export libc symbol names — AOT interposition breaks
* libxcrypt crypt(3) (password_hash/crypt return *0 / SEGV) (#26861).
*/
final class LibcNameCollisionRuntimeShrinkTest extends TestCase
{
public function testStringStrspnUsesCompilerPrefixedAbi(): void
{
$source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/StringStrspn.php');
$this->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);
}
}
4 changes: 4 additions & 0 deletions test/unit/PasswordCryptoRuntimeShrinkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading