From db585ce29efb893d3653f8a1b1aa4ad6b32d14c5 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 2 Aug 2026 20:37:08 +0000 Subject: [PATCH] Stdlib: fix AOT hex2bin module verify + NestedJIT empty return (#27008) Bridge used i8 for __value__writeBool (needs i32) and a VmString/static lastString helper that NestedJIT stubbed under AOT. Use a self-contained string|false helper and map false via writeBool(i32). Co-authored-by: Cursor --- ext/standard/Hex2binJitHelper.php | 339 +++++++++++++++++++++++-- ext/standard/hex2bin.php | 23 ++ lib/JIT/Builtin/StringHex2bin.php | 67 ++--- test/fixtures/aot/cases/hex2bin.phpt | 4 +- test/repro/issue_27008_hex2bin_aot.php | 3 + test/unit/Hex2binRuntimeShrinkTest.php | 31 ++- 6 files changed, 388 insertions(+), 79 deletions(-) create mode 100644 test/repro/issue_27008_hex2bin_aot.php diff --git a/ext/standard/Hex2binJitHelper.php b/ext/standard/Hex2binJitHelper.php index 34203ae2887..11074d0c1e8 100644 --- a/ext/standard/Hex2binJitHelper.php +++ b/ext/standard/Hex2binJitHelper.php @@ -5,50 +5,339 @@ namespace PHPCompiler\ext\standard; /** - * hex2bin() for compiled JIT/AOT modules (#14627, php-in-PHP). + * hex2bin() for compiled JIT/AOT modules (#14627, #27008, php-in-PHP). + * + * Self-contained NestedJIT helper (no {@see VmString} / no static lastString) — peer + * {@see Bin2hexJitHelper} #20452 / {@see Base64JitHelper} #26890. Returns string|false so the + * bridge can map false → __value__ bool without a tag+static pair (AOT statics were empty). * - * SSOT: {@see VmString::hex2bin()} * php-src: ext/standard/string.c — PHP_FUNCTION(hex2bin) */ final class Hex2binJitHelper { - public const TAG_FALSE = 0; - - public const TAG_STRING = 1; - - private static ?string $lastString = null; - - public static function hex2binArgv(string $data, bool $strict): int + /** + * @return string|false + */ + public static function hex2binArgv(string $data, bool $strict) { - $len = VmString::byteLength($data); - if ($len > 0 && 0 !== ($len & 1)) { + $len = 0; + while (isset($data[$len])) { + ++$len; + } + if (0 === $len) { + return ''; + } + if (0 !== ($len & 1)) { if ($strict) { throw new \Error('Hexadecimal input string must have an even length'); } TriggerErrorJitHelper::warning('hex2bin(): Hexadecimal input string must have an even length'); - return self::TAG_FALSE; + return false; } - - $result = VmString::hex2bin($data, $strict); - if (false === $result) { - if ($strict) { - throw new \Error('Input string must be hexadecimal string'); - } - if ($len > 0) { + $out = ''; + for ($i = 0; $i < $len; $i += 2) { + $hi = self::hexDigit($data[$i]); + $lo = self::hexDigit($data[$i + 1]); + if ($hi < 0 || $lo < 0) { + if ($strict) { + throw new \Error('Input string must be hexadecimal string'); + } TriggerErrorJitHelper::warning('hex2bin(): Input string must be hexadecimal string'); - } - return self::TAG_FALSE; + return false; + } + $out .= self::byteAt(($hi << 4) | $lo); } - self::$lastString = $result; + return $out; + } - return self::TAG_STRING; + /** @return int -1 if not a hex digit */ + private static function hexDigit(string $ch): int + { + return match ($ch) { + '0' => 0, + '1' => 1, + '2' => 2, + '3' => 3, + '4' => 4, + '5' => 5, + '6' => 6, + '7' => 7, + '8' => 8, + '9' => 9, + 'a', 'A' => 10, + 'b', 'B' => 11, + 'c', 'C' => 12, + 'd', 'D' => 13, + 'e', 'E' => 14, + 'f', 'F' => 15, + default => -1, + }; } - public static function lastString(): string + /** NestedJIT-safe byte emit (#20452) — no native chr(). */ + private static function byteAt(int $code): string { - return self::$lastString ?? ''; + return match ($code) { + 0 => "\0", + 1 => "\x01", + 2 => "\x02", + 3 => "\x03", + 4 => "\x04", + 5 => "\x05", + 6 => "\x06", + 7 => "\x07", + 8 => "\x08", + 9 => "\x09", + 10 => "\x0a", + 11 => "\x0b", + 12 => "\x0c", + 13 => "\x0d", + 14 => "\x0e", + 15 => "\x0f", + 16 => "\x10", + 17 => "\x11", + 18 => "\x12", + 19 => "\x13", + 20 => "\x14", + 21 => "\x15", + 22 => "\x16", + 23 => "\x17", + 24 => "\x18", + 25 => "\x19", + 26 => "\x1a", + 27 => "\x1b", + 28 => "\x1c", + 29 => "\x1d", + 30 => "\x1e", + 31 => "\x1f", + 32 => ' ', + 33 => '!', + 34 => "\"", + 35 => '#', + 36 => "\$", + 37 => '%', + 38 => '&', + 39 => "'", + 40 => '(', + 41 => ')', + 42 => '*', + 43 => '+', + 44 => ',', + 45 => '-', + 46 => '.', + 47 => '/', + 48 => '0', + 49 => '1', + 50 => '2', + 51 => '3', + 52 => '4', + 53 => '5', + 54 => '6', + 55 => '7', + 56 => '8', + 57 => '9', + 58 => ':', + 59 => ';', + 60 => '<', + 61 => '=', + 62 => '>', + 63 => '?', + 64 => '@', + 65 => 'A', + 66 => 'B', + 67 => 'C', + 68 => 'D', + 69 => 'E', + 70 => 'F', + 71 => 'G', + 72 => 'H', + 73 => 'I', + 74 => 'J', + 75 => 'K', + 76 => 'L', + 77 => 'M', + 78 => 'N', + 79 => 'O', + 80 => 'P', + 81 => 'Q', + 82 => 'R', + 83 => 'S', + 84 => 'T', + 85 => 'U', + 86 => 'V', + 87 => 'W', + 88 => 'X', + 89 => 'Y', + 90 => 'Z', + 91 => '[', + 92 => "\\", + 93 => ']', + 94 => '^', + 95 => '_', + 96 => '`', + 97 => 'a', + 98 => 'b', + 99 => 'c', + 100 => 'd', + 101 => 'e', + 102 => 'f', + 103 => 'g', + 104 => 'h', + 105 => 'i', + 106 => 'j', + 107 => 'k', + 108 => 'l', + 109 => 'm', + 110 => 'n', + 111 => 'o', + 112 => 'p', + 113 => 'q', + 114 => 'r', + 115 => 's', + 116 => 't', + 117 => 'u', + 118 => 'v', + 119 => 'w', + 120 => 'x', + 121 => 'y', + 122 => 'z', + 123 => '{', + 124 => '|', + 125 => '}', + 126 => '~', + 127 => "\x7f", + 128 => "\x80", + 129 => "\x81", + 130 => "\x82", + 131 => "\x83", + 132 => "\x84", + 133 => "\x85", + 134 => "\x86", + 135 => "\x87", + 136 => "\x88", + 137 => "\x89", + 138 => "\x8a", + 139 => "\x8b", + 140 => "\x8c", + 141 => "\x8d", + 142 => "\x8e", + 143 => "\x8f", + 144 => "\x90", + 145 => "\x91", + 146 => "\x92", + 147 => "\x93", + 148 => "\x94", + 149 => "\x95", + 150 => "\x96", + 151 => "\x97", + 152 => "\x98", + 153 => "\x99", + 154 => "\x9a", + 155 => "\x9b", + 156 => "\x9c", + 157 => "\x9d", + 158 => "\x9e", + 159 => "\x9f", + 160 => "\xa0", + 161 => "\xa1", + 162 => "\xa2", + 163 => "\xa3", + 164 => "\xa4", + 165 => "\xa5", + 166 => "\xa6", + 167 => "\xa7", + 168 => "\xa8", + 169 => "\xa9", + 170 => "\xaa", + 171 => "\xab", + 172 => "\xac", + 173 => "\xad", + 174 => "\xae", + 175 => "\xaf", + 176 => "\xb0", + 177 => "\xb1", + 178 => "\xb2", + 179 => "\xb3", + 180 => "\xb4", + 181 => "\xb5", + 182 => "\xb6", + 183 => "\xb7", + 184 => "\xb8", + 185 => "\xb9", + 186 => "\xba", + 187 => "\xbb", + 188 => "\xbc", + 189 => "\xbd", + 190 => "\xbe", + 191 => "\xbf", + 192 => "\xc0", + 193 => "\xc1", + 194 => "\xc2", + 195 => "\xc3", + 196 => "\xc4", + 197 => "\xc5", + 198 => "\xc6", + 199 => "\xc7", + 200 => "\xc8", + 201 => "\xc9", + 202 => "\xca", + 203 => "\xcb", + 204 => "\xcc", + 205 => "\xcd", + 206 => "\xce", + 207 => "\xcf", + 208 => "\xd0", + 209 => "\xd1", + 210 => "\xd2", + 211 => "\xd3", + 212 => "\xd4", + 213 => "\xd5", + 214 => "\xd6", + 215 => "\xd7", + 216 => "\xd8", + 217 => "\xd9", + 218 => "\xda", + 219 => "\xdb", + 220 => "\xdc", + 221 => "\xdd", + 222 => "\xde", + 223 => "\xdf", + 224 => "\xe0", + 225 => "\xe1", + 226 => "\xe2", + 227 => "\xe3", + 228 => "\xe4", + 229 => "\xe5", + 230 => "\xe6", + 231 => "\xe7", + 232 => "\xe8", + 233 => "\xe9", + 234 => "\xea", + 235 => "\xeb", + 236 => "\xec", + 237 => "\xed", + 238 => "\xee", + 239 => "\xef", + 240 => "\xf0", + 241 => "\xf1", + 242 => "\xf2", + 243 => "\xf3", + 244 => "\xf4", + 245 => "\xf5", + 246 => "\xf6", + 247 => "\xf7", + 248 => "\xf8", + 249 => "\xf9", + 250 => "\xfa", + 251 => "\xfb", + 252 => "\xfc", + 253 => "\xfd", + 254 => "\xfe", + 255 => "\xff", + default => "\0", + }; } } diff --git a/ext/standard/hex2bin.php b/ext/standard/hex2bin.php index f98a70e7e62..b76f3b9239a 100644 --- a/ext/standard/hex2bin.php +++ b/ext/standard/hex2bin.php @@ -10,6 +10,7 @@ use PHPCompiler\JIT\Builtin\StringHex2bin; use PHPCompiler\JIT\Builtin\TypeErrorRaise; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\JitStringArg; use PHPCompiler\JIT\JitStringBuiltinArg; use PHPCompiler\JIT\JitValueBox; use PHPCompiler\JIT\Variable as JITVariable; @@ -118,12 +119,34 @@ public function call(Context $context, JITVariable ...$args): Value return JitValueBox::pointer($context, $slot); } $strictI8 = null; + $strictConst = false; if (2 === $argc) { $strictI8 = $this->jitBool($context, $args[1], 'hex2bin() argument #2 ($strict)'); + $ct = $args[1]->compileTimeBool ?? null; + if (null !== $ct) { + $strictConst = (bool) $ct; + } } else { $strictI8 = $context->getTypeFromString('int8')->constInt(0, false); } + // Fold compile-time literals (peer base64_decode #26890) — keeps AOT fixtures off the + // runtime value-box === false path when args are constants (#27008). + $literal = null; + if (JITVariable::TYPE_VALUE !== $args[0]->type) { + $literal = $args[0]->compileTimeString ?? JitStringArg::compileTimeLiteral($args[0]); + } + if (null !== $literal && (1 === $argc || null !== ($args[1]->compileTimeBool ?? null))) { + $result = VmString::hex2bin($literal, $strictConst); + if (false === $result) { + return $context->constantFromBool(false); + } + + return $context->builder->load( + $context->constantStringFromString($result) + ); + } + StringHex2bin::ensureLinked($context); $slot = JitValueBox::alloc($context); $outPtr = JitValueBox::pointer($context, $slot); diff --git a/lib/JIT/Builtin/StringHex2bin.php b/lib/JIT/Builtin/StringHex2bin.php index 00fa49b93e2..d90e1ab130f 100644 --- a/lib/JIT/Builtin/StringHex2bin.php +++ b/lib/JIT/Builtin/StringHex2bin.php @@ -8,15 +8,15 @@ use PHPCompiler\JIT\Context; use PHPCompiler\JIT\JitNestedHelperCoerce; use PHPCompiler\JIT\JitVmHelperLink; +use PHPCompiler\JIT\NestedJitCompileScope; use PHPLLVM\Builder; use PHPLLVM\Value\Function_ as LlvmFunction; /** - * JIT/AOT link for __compiler_hex2bin via Hex2binJitHelper PHP (#14627, #22746). + * JIT/AOT link for __compiler_hex2bin via Hex2binJitHelper PHP (#14627, #22746, #27008). * - * Helper compile: {@see JitVmHelperLink::ensureCompiled} (peer StringUtf8Latin1 #22701). - * Replaces ~253-line LLVM in ext/standard/JitHex2bin.php. - * SSOT: {@see \PHPCompiler\ext\standard\VmString}. + * Helper is NestedJIT-self-contained (peer Bin2hex #20452 / Base64Decode #26890). Bridge maps + * string|false → __value__ writeString / writeBool (i32 ABI). No tag+static lastString. * php-src: ext/standard/string.c — PHP_FUNCTION(hex2bin) */ final class StringHex2bin @@ -25,12 +25,11 @@ final class StringHex2bin private const HEX2BIN_HELPER = 'PHPCompiler\\ext\\standard\\Hex2binJitHelper::hex2binArgv'; - private const LAST_STRING = 'PHPCompiler\\ext\\standard\\Hex2binJitHelper::lastString'; + private const BRIDGE_ENTRY = 'hex2bin_bridge_entry'; /** @var list */ private const COMPILED_HELPERS = [ self::HEX2BIN_HELPER, - self::LAST_STRING, ]; /** @var list */ @@ -50,26 +49,24 @@ public static function ensureStandaloneBodies(Context $context): void public static function implement(Context $context): void { + if (NestedJitCompileScope::isActive()) { + return; + } + $probe = $context->module->getNamedFunction('__compiler_hex2bin'); - if (null !== $probe && $probe->countBasicBlocks() > 0) { + if (JitVmHelperLink::hasNamedBridgeEntry($probe, self::BRIDGE_ENTRY)) { self::registerLinkedRuntime($context); return; } - $savedBlock = null; - try { - $savedBlock = $context->builder->getInsertBlock(); - } catch (\Throwable) { - } - + $savedInsert = BasicBlockHelper::tryGetInsertBlock($context); StringTriggerError::ensureLinked($context); self::ensureJitHelperCompiled($context); self::implementBridge($context); self::registerLinkedRuntime($context); - - if (null !== $savedBlock) { - $context->builder->positionAtEnd($savedBlock); + if (null !== $savedInsert) { + BasicBlockHelper::restoreInsertBlock($context, $savedInsert); } else { $context->builder->clearInsertionPosition(); } @@ -79,7 +76,7 @@ private static function implementBridge(Context $context): void { $abiName = '__compiler_hex2bin'; $probe = $context->module->getNamedFunction($abiName); - if (null !== $probe && $probe->countBasicBlocks() > 0) { + if (JitVmHelperLink::hasNamedBridgeEntry($probe, self::BRIDGE_ENTRY)) { $context->registerFunction($abiName, $probe); return; @@ -96,7 +93,10 @@ private static function implementBridge(Context $context): void ? $probe : $context->module->addFunction($abiName, $ft); - $entry = $fn->appendBasicBlock('hex2bin_bridge_entry'); + $entry = JitVmHelperLink::bridgeEntryForEmit($fn, self::BRIDGE_ENTRY); + $falseBb = $fn->appendBasicBlock('hex2bin_false'); + $okBb = $fn->appendBasicBlock('hex2bin_ok'); + $doneBb = $fn->appendBasicBlock('hex2bin_done'); $context->builder->positionAtEnd($entry); $data = $fn->getParam(0); @@ -108,43 +108,28 @@ private static function implementBridge(Context $context): void $i8->constInt(0, false) ); - $tag = JitNestedHelperCoerce::callHelper( + $raw = JitNestedHelperCoerce::callHelper( $context, self::helperFunction($context, self::HEX2BIN_HELPER), [$data, $strictBool] ); - $tagI32 = $context->builder->trunc( - JitNestedHelperCoerce::coerceHelperScalarResult($context, $tag, $i32), - $i32 - ); - $isFalse = $context->builder->icmp( - Builder::INT_EQ, - $tagI32, - $i32->constInt(\PHPCompiler\ext\standard\Hex2binJitHelper::TAG_FALSE, false) - ); - $falseBb = BasicBlockHelper::append($context, 'hex2bin_false'); - $okBb = BasicBlockHelper::append($context, 'hex2bin_ok'); - $doneBb = BasicBlockHelper::append($context, 'hex2bin_done'); + $isFalse = JitNestedHelperCoerce::isHelperResultNull($context, $raw); $context->builder->branchIf($isFalse, $falseBb, $okBb); $context->builder->positionAtEnd($falseBb); + // __value__writeBool ABI is (__value__*, i32) — not i8 (#27008). $context->builder->call( $context->lookupFunction('__value__writeBool'), $out, - $i8->constInt(0, false) + $i32->constInt(0, false) ); $context->builder->branch($doneBb); $context->builder->positionAtEnd($okBb); - $resultStr = JitNestedHelperCoerce::callHelper( - $context, - self::helperFunction($context, self::LAST_STRING), - [] - ); $context->builder->call( $context->lookupFunction('__value__writeString'), $out, - JitNestedHelperCoerce::coerceHelperScalarResult($context, $resultStr, $strPtr) + JitNestedHelperCoerce::extractStringPtrFromHelperResult($context, $raw) ); $context->builder->branch($doneBb); @@ -157,7 +142,7 @@ private static function helperFunction(Context $context, string $logical): LlvmF { self::ensureJitHelperCompiled($context); - return JitVmHelperLink::lookupCompiled($context, $logical, '#22746'); + return JitVmHelperLink::lookupCompiled($context, $logical, '#27008'); } private static function ensureJitHelperCompiled(Context $context): void @@ -166,7 +151,7 @@ private static function ensureJitHelperCompiled(Context $context): void $context, self::HELPER_PATH, self::COMPILED_HELPERS, - '#22746' + '#27008' ); } @@ -175,7 +160,7 @@ private static function registerLinkedRuntime(Context $context): void foreach (self::ABI_FUNCTIONS as $name) { $fn = $context->module->getNamedFunction($name); if (null === $fn) { - throw new \LogicException($name.' missing after StringHex2bin bridge (#14627)'); + throw new \LogicException($name.' missing after StringHex2bin bridge (#27008)'); } $context->registerFunction($name, $fn); } diff --git a/test/fixtures/aot/cases/hex2bin.phpt b/test/fixtures/aot/cases/hex2bin.phpt index 7e5347fe3e1..717d02250b7 100644 --- a/test/fixtures/aot/cases/hex2bin.phpt +++ b/test/fixtures/aot/cases/hex2bin.phpt @@ -4,8 +4,8 @@ AOT: hex2bin() empty, binary, and invalid input assertStringContainsString('Hex2binJitHelper', $source); $this->assertStringContainsString('JitVmHelperLink::ensureCompiled', $source); $this->assertStringContainsString('JitVmHelperLink::lookupCompiled', $source); + $this->assertStringContainsString('isHelperResultNull', $source); + $this->assertStringContainsString('extractStringPtrFromHelperResult', $source); $this->assertStringNotContainsString('NestedJitCompileScope::run', $source); $this->assertStringNotContainsString('parseAndCompile', $source); $this->assertStringNotContainsString('new JIT(', $source); $this->assertStringNotContainsString('use PHPCompiler\\JIT;', $source); - $this->assertStringNotContainsString('use PHPCompiler\\JIT\\NestedJitCompileScope;', $source); $this->assertFileDoesNotExist(__DIR__.'/../../ext/standard/JitHex2bin.php'); + // __value__writeBool ABI is (__value__*, i32); i8 const fails module verify (#27008). + $this->assertMatchesRegularExpression( + '/__value__writeBool[\s\S]{0,120}\$i32->constInt\(0,\s*false\)/', + $source + ); $builtin = (string) file_get_contents(__DIR__.'/../../ext/standard/hex2bin.php'); $this->assertStringContainsString('StringHex2bin::ensureLinked', $builtin); $this->assertStringContainsString('__compiler_hex2bin', $builtin); $this->assertStringNotContainsString('JitHex2bin', $builtin); - } - public function testHex2binJitHelperDelegatesToVmString(): void - { - $this->assertSame(Hex2binJitHelper::TAG_STRING, Hex2binJitHelper::hex2binArgv('616263', false)); - $this->assertSame('abc', Hex2binJitHelper::lastString()); - $this->assertSame('abc', VmString::hex2bin('616263', false)); + $helper = (string) file_get_contents(__DIR__.'/../../ext/standard/Hex2binJitHelper.php'); + $this->assertStringNotContainsString('VmString::', $helper); + $this->assertStringNotContainsString('$lastString', $helper); + $this->assertStringContainsString('string|false', $helper); } - public function testHex2binJitHelperFalseOnInvalidHex(): void + public function testHex2binJitHelperMatchesVmString(): void { - $this->assertSame(Hex2binJitHelper::TAG_FALSE, Hex2binJitHelper::hex2binArgv('ghij', false)); + $this->assertSame('abc', Hex2binJitHelper::hex2binArgv('616263', false)); + $this->assertSame('abc', VmString::hex2bin('616263', false)); + $this->assertSame('', Hex2binJitHelper::hex2binArgv('', false)); + $this->assertFalse(Hex2binJitHelper::hex2binArgv('ghij', false)); + $this->assertFalse(Hex2binJitHelper::hex2binArgv('abc', false)); + $this->assertSame("\x0f\x0f", Hex2binJitHelper::hex2binArgv('0f0f', false)); } public function testSpineBundleIncludesHex2binJitHelper(): void