From 5ea45ac16d1e7ec9caba2387b196bdd5614ef1cc Mon Sep 17 00:00:00 2001 From: PurHur Date: Tue, 21 Jul 2026 15:12:20 +0000 Subject: [PATCH] Fix session_start AOT abort and false bool return (#21892). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NestedJIT mis-lowers VmSession::createId; generate ids in SessionCreateIdJitHelper. Value-box bool copy used __value__readLong (no NATIVE_BOOL arm → always 0). Co-authored-by: Cursor --- ext/standard/JitSessionLifecycleKernel.php | 3 +- ext/standard/JitSessionStorageKernel.php | 7 +- ext/standard/SessionCreateIdJitHelper.php | 78 ++++++++++++++++--- lib/JIT/Builtin/SessionStart.php | 19 ++--- lib/JIT/JitValueBox.php | 26 ++++++- .../unit/SessionCreateIdRuntimeShrinkTest.php | 7 +- 6 files changed, 105 insertions(+), 35 deletions(-) diff --git a/ext/standard/JitSessionLifecycleKernel.php b/ext/standard/JitSessionLifecycleKernel.php index 111172d6772..683e498b611 100644 --- a/ext/standard/JitSessionLifecycleKernel.php +++ b/ext/standard/JitSessionLifecycleKernel.php @@ -111,7 +111,8 @@ private static function implementStandaloneRuntime(Context $context): void $context->builder->branchIf($isActive, $bbInactive, $bbCheckHeaders); $context->builder->positionAtEnd($bbInactive); - SessionStart::emitWriteBool($context, $outPtr, false); + // php-src: session already active → true (+ E_NOTICE); not false. + SessionStart::emitWriteBool($context, $outPtr, true); $context->builder->branch($bbDone); $context->builder->positionAtEnd($bbCheckHeaders); diff --git a/ext/standard/JitSessionStorageKernel.php b/ext/standard/JitSessionStorageKernel.php index fb2f605c70b..66d9ade375e 100644 --- a/ext/standard/JitSessionStorageKernel.php +++ b/ext/standard/JitSessionStorageKernel.php @@ -303,7 +303,6 @@ private static function implementEmitSetcookieBridge(Context $context, LlvmFunct $i64 = $context->getTypeFromString('int64'); $i32 = $context->getTypeFromString('int32'); - $strPtr = $context->getTypeFromString('__string__*'); $idLen = $context->builder->load(SessionStorageGlobals::$idLenGlobal); $hasId = $context->builder->icmp(Builder::INT_SGT, $idLen, $i64->constInt(0, false)); $bbDone = BasicBlockHelper::append($context, 'ss_setcookie_bridge_done'); @@ -318,16 +317,18 @@ private static function implementEmitSetcookieBridge(Context $context, LlvmFunct ); $valueStr = self::bufferToString($context, SessionStorageGlobals::$idBufGlobal, $idLen); $pathStr = self::literalString($context, '/'); + // NestedJIT PendingHeadersJitHelper::addSetcookie expects string, not null (#21892). + $emptyStr = self::literalString($context, ''); $context->builder->call( $context->lookupFunction('__phpc_setcookie_add'), $nameStr, $valueStr, $i64->constInt(0, false), $pathStr, - $strPtr->constNull(), + $emptyStr, $i32->constInt(0, false), $i32->constInt(0, false), - $strPtr->constNull(), + $emptyStr, $i32->constInt(0, false) ); $context->builder->branch($bbDone); diff --git a/ext/standard/SessionCreateIdJitHelper.php b/ext/standard/SessionCreateIdJitHelper.php index bde6059e276..6ced2c71ff3 100644 --- a/ext/standard/SessionCreateIdJitHelper.php +++ b/ext/standard/SessionCreateIdJitHelper.php @@ -4,32 +4,86 @@ namespace PHPCompiler\ext\standard; +use PHPCompiler\ext\session\SessionFileStorage; + /** * session_create_id() semantics for compiled JIT/AOT modules (#9500, php-in-PHP). * - * SSOT: {@see VmSession::createId} - * php-src: ext/session/session.c — php_session_create_id + * Nested-JIT must not call {@see VmSession::createId()} — static method return is + * mis-lowered to the VmSession class object (AOT abort in randomIdString; #21892). + * Keep generate/bin_to_readable logic here using {@see random_bytes()} (#1974). + * + * php-src: ext/session/session.c — php_session_create_id / bin_to_readable */ final class SessionCreateIdJitHelper { + /** Default php.ini session.sid_length / session.sid_bits_per_character (#10864). */ + private const SID_LENGTH = 26; + + private const SID_BITS_PER_CHAR = 5; + + /** php-src bin_to_readable alphabet (64 glyphs). */ + private const BIN_MAP = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,-'; + public static function randomIdString(): string { - $result = VmSession::createId(null); - if (!\is_string($result)) { - throw new \LogicException('session random id must be string (#9500)'); - } - - return $result; + return self::generateId(); } /** @return string|null null when php-src session_create_id() would return false */ public static function createIdNullable(?string $prefix): ?string { - $result = VmSession::createId($prefix); - if (false === $result) { - return null; + if (null !== $prefix && '' !== $prefix) { + if (\strlen($prefix) > VmSession::MAX_ID_LEN) { + throw new \ValueError( + 'session_create_id(): Argument #1 ($prefix) cannot be longer than ' + .VmSession::MAX_ID_LEN.' characters' + ); + } + if ($prefix !== SessionFileStorage::sanitizeId($prefix)) { + return null; + } + } + $generated = self::generateId(); + if (null === $prefix || '' === $prefix) { + return $generated; + } + + return $prefix.$generated; + } + + private static function generateId(): string + { + return self::binToReadable( + \random_bytes(self::SID_LENGTH), + self::SID_LENGTH, + self::SID_BITS_PER_CHAR + ); + } + + /** php-src ext/session/session.c bin_to_readable(). */ + private static function binToReadable(string $bytes, int $outLength, int $bitsPerChar): string + { + $map = self::BIN_MAP; + $out = ''; + $byteLen = \strlen($bytes); + $p = 0; + $w = 0; + $have = 0; + $mask = (1 << $bitsPerChar) - 1; + for ($i = 0; $i < $outLength; ++$i) { + while ($have < $bitsPerChar) { + if ($p >= $byteLen) { + break; + } + $w |= (\ord($bytes[$p++]) << $have); + $have += 8; + } + $out .= $map[$w & $mask]; + $w >>= $bitsPerChar; + $have -= $bitsPerChar; } - return $result; + return $out; } } diff --git a/lib/JIT/Builtin/SessionStart.php b/lib/JIT/Builtin/SessionStart.php index 4f1eaeb8683..3dea3ac4da9 100644 --- a/lib/JIT/Builtin/SessionStart.php +++ b/lib/JIT/Builtin/SessionStart.php @@ -26,21 +26,12 @@ public static function implement(Context $context): void public static function emitWriteBool(Context $context, Value $outPtr, bool $value): void { - $valMap = $context->structFieldMap['__value__']; - $i8 = $context->getTypeFromString('int8'); - $i32 = $context->getTypeFromString('int32'); - $i64 = $context->getTypeFromString('int64'); - $context->builder->store( - $i8->constInt(Variable::TYPE_NATIVE_BOOL, false), - $context->builder->structGep($outPtr, $valMap['type']) - ); - $valueField = $context->builder->structGep($outPtr, $valMap['value']); - $firstByte = $context->builder->inBoundsGEP( - $valueField, - $i32->constInt(0, false), - $i64->constInt(0, false) + // Use the canonical value-box ABI (same as JitValueBox::writeBool) — #21892. + $context->builder->call( + $context->lookupFunction('__value__writeBool'), + $outPtr, + $context->getTypeFromString('int32')->constInt($value ? 1 : 0, false) ); - $context->builder->store($i8->constInt($value ? 1 : 0, false), $firstByte); } /** diff --git a/lib/JIT/JitValueBox.php b/lib/JIT/JitValueBox.php index 0b9bd5b16cf..07446ef461f 100644 --- a/lib/JIT/JitValueBox.php +++ b/lib/JIT/JitValueBox.php @@ -541,13 +541,18 @@ private static function copyBetweenPointers(Context $context, Value $destPtr, Va $context->builder->branchIf($isBool, $boolBlock, $afterBool); $context->builder->positionAtEnd($boolBlock); - $boolLong = $context->builder->call($context->lookupFunction('__value__readLong'), $srcPtr); + // __value__readLong has no TYPE_NATIVE_BOOL arm (returns 0) — #21892 / JitZendScalarCast. + $boolByte = self::readBoolByte($context, $srcPtr); $i32 = $context->getTypeFromString('int32'); $context->builder->call( $context->lookupFunction('__value__writeBool'), $destPtr, $context->builder->zExt( - $context->builder->truncOrBitCast($boolLong, $context->getTypeFromString('int1')), + $context->builder->icmp( + Builder::INT_NE, + $boolByte, + $context->getTypeFromString('int8')->constInt(0, false) + ), $i32 ) ); @@ -576,6 +581,23 @@ private static function copyBetweenPointers(Context $context, Value $destPtr, Va BasicBlockHelper::branchToFreshContinue($context, 'after_value_copy_'.$tag); } + /** + * Read boxed bool payload (writeBool stores int8 at value[0]). + * Do not use {@see __value__readLong} — no NATIVE_BOOL arm (#21892). + */ + public static function readBoolByte(Context $context, Value $valuePtr): Value + { + $valuePtr = self::normalizeValuePtr($context, $valuePtr); + $map = $context->structFieldMap['__value__']; + $i8 = $context->getTypeFromString('int8'); + $bytePtr = $context->builder->pointerCast( + $context->builder->structGep($valuePtr, $map['value']), + $i8->pointerType(0) + ); + + return $context->builder->load($bytePtr); + } + public static function writeBool(Context $context, Value $slot, Value $bool): void { $map = $context->structFieldMap['__value__']; diff --git a/test/unit/SessionCreateIdRuntimeShrinkTest.php b/test/unit/SessionCreateIdRuntimeShrinkTest.php index 0724f2c5fb8..c3a996631af 100644 --- a/test/unit/SessionCreateIdRuntimeShrinkTest.php +++ b/test/unit/SessionCreateIdRuntimeShrinkTest.php @@ -24,8 +24,9 @@ public function testSessionCreateIdRuntimeUsesJitHelperNotLlvmEntropy(): void public function testSessionCreateIdJitHelperMatchesVmSession(): void { $id = SessionCreateIdJitHelper::randomIdString(); - $this->assertSame(32, \strlen($id)); - $this->assertMatchesRegularExpression('/^[0-9a-f]{32}$/', $id); + // php-src defaults: session.sid_length=26, sid_bits_per_character=5 (#10864). + $this->assertSame(26, \strlen($id)); + $this->assertMatchesRegularExpression('/^[0-9a-zA-Z,-]{26}$/', $id); $prefixed = SessionCreateIdJitHelper::createIdNullable('app-'); $this->assertIsString($prefixed); @@ -33,7 +34,7 @@ public function testSessionCreateIdJitHelperMatchesVmSession(): void $vm = VmSession::createId('app-'); $this->assertIsString($vm); - $this->assertSame(36, \strlen($vm)); + $this->assertSame(30, \strlen($vm)); } public function testSessionCreateIdJitHelperReturnsNullOnInvalidPrefix(): void