From 3e253f1bc72a7b501dcc5a5062d66525b1ffdaf8 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 22 Aug 2026 22:50:29 +0000 Subject: [PATCH] AOT: date_default_timezone_get/set via NestedJIT coerce (#33950) Thin AOT SIGSEGV'd on get and always returned false from set because the ABI bridge skipped JitNestedHelperCoerce and NestedJIT could not use VmDate::$defaultTimezone. Coerce string/bool returns and keep the default zone in the helper TU with durable copy + zoneinfo is_file validation. Co-authored-by: Cursor --- ext/standard/DefaultTimezoneJitHelper.php | 123 +++++++++++++++++- ext/standard/JitDate.php | 3 + lib/JIT/Builtin/DefaultTimezoneRuntime.php | 26 +++- .../cases/default_timezone_get_set_33950.phpt | 17 +++ .../issue_33950_default_timezone_aot.php | 9 ++ .../unit/DefaultTimezoneRuntimeShrinkTest.php | 20 +-- .../unit/Issue33950DefaultTimezoneAotTest.php | 68 ++++++++++ 7 files changed, 250 insertions(+), 16 deletions(-) create mode 100644 test/fixtures/aot/cases/default_timezone_get_set_33950.phpt create mode 100644 test/repro/issue_33950_default_timezone_aot.php create mode 100644 test/unit/Issue33950DefaultTimezoneAotTest.php diff --git a/ext/standard/DefaultTimezoneJitHelper.php b/ext/standard/DefaultTimezoneJitHelper.php index 5fe53775ce1..46a5486594f 100644 --- a/ext/standard/DefaultTimezoneJitHelper.php +++ b/ext/standard/DefaultTimezoneJitHelper.php @@ -7,21 +7,58 @@ use PHPCompiler\VM\ErrorReporter; /** - * date_default_timezone_get/set for compiled JIT/AOT modules (#9243, php-in-PHP). + * date_default_timezone_get/set for compiled JIT/AOT modules (#9243, #33950). * - * SSOT: {@see VmDate::defaultTimezoneGet()} / {@see VmDate::tryDefaultTimezoneSet()} + * Owns the default-TZ string in this helper TU. Reading {@see VmDate::$defaultTimezone} + * from NestedJIT under thin AOT SIGSEGVs (split static / #27566). Validation uses + * zoneinfo {@see is_file} so the helper does not NestedJIT the VmDateTimeNative graph + * (unbound stubs return false for every id when O=0). + * + * Zend/VM still use {@see VmDate}. Free date() civil bake reads VmDate at compile time. * php-src: ext/date/php_date.c — PHP_FUNCTION(date_default_timezone_get/set) */ final class DefaultTimezoneJitHelper { + private const ZONEINFO_ROOT = '/usr/share/zoneinfo'; + + private static string $defaultTimezone = 'UTC'; + public static function defaultTimezoneGet(): string { - return VmDate::defaultTimezoneGet(); + return self::$defaultTimezone; } public static function tryDefaultTimezoneSet(string $timezone): bool { - return VmDate::tryDefaultTimezoneSet($timezone); + if ('' === $timezone || '/' === $timezone[0]) { + return false; + } + $len = \strlen($timezone); + for ($i = 0; $i < $len; ++$i) { + $c = $timezone[$i]; + if ("\0" === $c) { + return false; + } + if ('.' === $c && $i + 1 < $len && '.' === $timezone[$i + 1]) { + return false; + } + } + if (self::isWellKnownUtcAlias($timezone)) { + self::$defaultTimezone = self::canonicalUtcAlias($timezone); + + return true; + } + if (self::trySetNumericOffset($timezone)) { + return true; + } + $path = self::ZONEINFO_ROOT.'/'.$timezone; + if (!\is_file($path)) { + return false; + } + // NestedJIT: static←param aliases the arg — UAF on subsequent get (#33950). + self::$defaultTimezone = self::copyTimezoneId($timezone); + + return true; } public static function emitInvalidTimezoneNotice(string $timezone): void @@ -31,4 +68,82 @@ public static function emitInvalidTimezoneNotice(string $timezone): void TriggerErrorJitHelper::stderrPrintCliError(ErrorReporter::E_NOTICE, $message, '', 0); } } + + /** Durable copy for NestedJIT static storage (#33950). */ + private static function copyTimezoneId(string $timezone): string + { + $copy = ''; + $len = \strlen($timezone); + for ($i = 0; $i < $len; ++$i) { + $copy .= $timezone[$i]; + } + + return $copy; + } + + /** @return bool true when $timezone was a valid numeric offset and was stored */ + private static function trySetNumericOffset(string $timezone): bool + { + $len = \strlen($timezone); + if ($len < 5 || ('+' !== $timezone[0] && '-' !== $timezone[0])) { + return false; + } + $sign = $timezone[0]; + if (5 === $len) { + for ($i = 1; $i < 5; ++$i) { + $c = $timezone[$i]; + if ($c < '0' || $c > '9') { + return false; + } + } + $hours = ((int) $timezone[1]) * 10 + (int) $timezone[2]; + $minutes = ((int) $timezone[3]) * 10 + (int) $timezone[4]; + } elseif (6 === $len && ':' === $timezone[3]) { + for ($i = 1; $i <= 2; ++$i) { + $c = $timezone[$i]; + if ($c < '0' || $c > '9') { + return false; + } + } + for ($i = 4; $i <= 5; ++$i) { + $c = $timezone[$i]; + if ($c < '0' || $c > '9') { + return false; + } + } + $hours = ((int) $timezone[1]) * 10 + (int) $timezone[2]; + $minutes = ((int) $timezone[4]) * 10 + (int) $timezone[5]; + } else { + return false; + } + if ($hours > 18 || $minutes >= 60) { + return false; + } + self::$defaultTimezone = self::copyTimezoneId( + $sign + .$timezone[1].$timezone[2] + .(5 === $len ? $timezone[3].$timezone[4] : $timezone[4].$timezone[5]) + ); + + return true; + } + + private static function isWellKnownUtcAlias(string $timezone): bool + { + return 'UTC' === $timezone || 'utc' === $timezone + || 'GMT' === $timezone || 'gmt' === $timezone + || 'Z' === $timezone || 'z' === $timezone + || 'Etc/UTC' === $timezone || 'Etc/utc' === $timezone + || 'Etc/GMT' === $timezone || 'Etc/gmt' === $timezone; + } + + private static function canonicalUtcAlias(string $timezone): string + { + if ('GMT' === $timezone || 'gmt' === $timezone + || 'Etc/GMT' === $timezone || 'Etc/gmt' === $timezone) { + return 'GMT'; + } + + return 'UTC'; + } } diff --git a/ext/standard/JitDate.php b/ext/standard/JitDate.php index 05a9ebcb3ef..a61e22b0d83 100644 --- a/ext/standard/JitDate.php +++ b/ext/standard/JitDate.php @@ -266,6 +266,9 @@ public static function formatDate(Context $context, bool $gmt, JITVariable ...$a // Thin AOT: NestedJIT FormatDatetime segfaults; common literals via UTC civil IR // (#27091 Y-m-d; #27121 date('Y', strtotime(...)) and peer single-token formats). // Matches Zend when default timezone is UTC (CI / docker image default). + // Timezone tokens for free date() assume compile-time UTC (#33943); runtime + // date_default_timezone_set is tracked in DefaultTimezoneJitHelper (#33950) and + // does not yet re-bind this bake (follow-up: runtime token helper). $fmtLit = JitStringBuiltinArg::compileTimeLiteral($args[0]) ?? $args[0]->compileTimeString; if (\is_string($fmtLit) && ($gmt || self::defaultTimezoneIsUtc())) { // Timezone tokens only here — DateTime::format shares tryFormatCivilLiteral and diff --git a/lib/JIT/Builtin/DefaultTimezoneRuntime.php b/lib/JIT/Builtin/DefaultTimezoneRuntime.php index 1fac90e648b..8f415912e53 100644 --- a/lib/JIT/Builtin/DefaultTimezoneRuntime.php +++ b/lib/JIT/Builtin/DefaultTimezoneRuntime.php @@ -6,6 +6,7 @@ use PHPCompiler\JIT\BasicBlockHelper; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\JitNestedHelperCoerce; use PHPCompiler\JIT\JitVmHelperLink; use PHPLLVM\Builder; use PHPLLVM\Value\Function_ as LlvmFunction; @@ -16,6 +17,9 @@ * Helper compile: {@see JitVmHelperLink::ensureCompiled} (peer TimezoneLocation #24801). * Call-site {@see ensureLinked} restores the caller insert block after bridge emit * (thin AOT: "Current basic block has no parent function", #27550 / peer #27088). + * NestedJIT string/bool returns must use {@see JitNestedHelperCoerce} (#33950 — + * raw call + writeString SIGSEGV; raw bool zext always false — peers IniRuntime / + * StringDateTime / #20664 / #8555 / #26773). * Replaces phpc_default_timezone_* LLVM globals + zoneinfo access walk. * SSOT: {@see \PHPCompiler\ext\standard\VmDate}. * php-src: ext/date/php_date.c — PHP_FUNCTION(date_default_timezone_get/set) @@ -95,7 +99,13 @@ private static function implementGetBridge(Context $context): void $context->builder->returnVoid(); $context->builder->positionAtEnd($bodyBb); - $tzStr = $context->builder->call(self::helperFunction($context, self::GET_HELPER)); + // NestedJIT may return __value__* / i64 — extract __string__* before writeString (#33950). + $raw = JitNestedHelperCoerce::callHelper( + $context, + self::helperFunction($context, self::GET_HELPER), + [] + ); + $tzStr = JitNestedHelperCoerce::extractStringPtrFromHelperResult($context, $raw); $context->builder->call( $context->lookupFunction('__value__writeString'), $out, @@ -138,13 +148,23 @@ private static function implementSetBridge(Context $context): void $context->builder->returnVoid(); $context->builder->positionAtEnd($bodyBb); - $ok = $context->builder->call(self::helperFunction($context, self::SET_HELPER), $tz); + // Coerce string arg + boxed bool return (#33950 / #8555 — raw zext always false). + $okRaw = JitNestedHelperCoerce::callHelper( + $context, + self::helperFunction($context, self::SET_HELPER), + [$tz] + ); + $ok = JitNestedHelperCoerce::extractBoolFromHelperResult($context, $okRaw); $failBb = $fn->appendBasicBlock('dtz_set_fail'); $storeBb = $fn->appendBasicBlock('dtz_set_store'); $context->builder->branchIf($ok, $storeBb, $failBb); $context->builder->positionAtEnd($failBb); - $context->builder->call(self::helperFunction($context, self::NOTICE_HELPER), $tz); + JitNestedHelperCoerce::callHelper( + $context, + self::helperFunction($context, self::NOTICE_HELPER), + [$tz] + ); $context->builder->branch($storeBb); $context->builder->positionAtEnd($storeBb); diff --git a/test/fixtures/aot/cases/default_timezone_get_set_33950.phpt b/test/fixtures/aot/cases/default_timezone_get_set_33950.phpt new file mode 100644 index 00000000000..205de17f416 --- /dev/null +++ b/test/fixtures/aot/cases/default_timezone_get_set_33950.phpt @@ -0,0 +1,17 @@ +--TEST-- +AOT date_default_timezone_get/set NestedJIT coerce (#33950) +--FILE-- +assertStringContainsString('VmDate::defaultTimezoneGet', $source); - $this->assertStringContainsString('VmDate::tryDefaultTimezoneSet', $source); + $this->assertStringContainsString('private static string $defaultTimezone', $source); + $this->assertStringContainsString('ZONEINFO_ROOT', $source); $this->assertStringContainsString('TriggerErrorJitHelper', $source); + $this->assertStringContainsString('#33950', $source); } public function testDefaultTimezoneRuntimeRoutesThroughJitHelper(): void @@ -24,9 +24,11 @@ public function testDefaultTimezoneRuntimeRoutesThroughJitHelper(): void $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/DefaultTimezoneRuntime.php'); $this->assertStringContainsString('DefaultTimezoneJitHelper', $source); $this->assertStringContainsString('JitVmHelperLink::ensureCompiled', $source); + $this->assertStringContainsString('JitNestedHelperCoerce::callHelper', $source); $this->assertStringContainsString('BasicBlockHelper::tryGetInsertBlock', $source); $this->assertStringContainsString('BasicBlockHelper::restoreInsertBlock', $source); $this->assertStringContainsString('#27550', $source); + $this->assertStringContainsString('#33950', $source); $this->assertStringNotContainsString('NestedJitCompileScope::run', $source); $this->assertStringNotContainsString('parseAndCompile', $source); $this->assertStringNotContainsString('new JIT(', $source); @@ -35,13 +37,13 @@ public function testDefaultTimezoneRuntimeRoutesThroughJitHelper(): void $this->assertStringNotContainsString('phpc_default_timezone_ptr', $source); $this->assertStringNotContainsString("lookupFunction('access')", $source); $this->assertStringNotContainsString('ZONEINFO_PREFIX', $source); - $this->assertLessThan(200, \substr_count($source, "\n") + 1); + $this->assertLessThan(220, \substr_count($source, "\n") + 1); } - public function testDefaultTimezoneJitHelperSemanticsMatchVmDate(): void + public function testDefaultTimezoneJitHelperSemanticsMatchVmDateForValidIds(): void { - VmDate::tryDefaultTimezoneSet('UTC'); - $this->assertSame(VmDate::defaultTimezoneGet(), DefaultTimezoneJitHelper::defaultTimezoneGet()); + DefaultTimezoneJitHelper::tryDefaultTimezoneSet('UTC'); + $this->assertSame('UTC', DefaultTimezoneJitHelper::defaultTimezoneGet()); $this->assertTrue(DefaultTimezoneJitHelper::tryDefaultTimezoneSet('Europe/Berlin')); $this->assertSame('Europe/Berlin', DefaultTimezoneJitHelper::defaultTimezoneGet()); @@ -49,6 +51,6 @@ public function testDefaultTimezoneJitHelperSemanticsMatchVmDate(): void $this->assertFalse(DefaultTimezoneJitHelper::tryDefaultTimezoneSet('Invalid/Zone')); $this->assertSame('Europe/Berlin', DefaultTimezoneJitHelper::defaultTimezoneGet()); - VmDate::tryDefaultTimezoneSet('UTC'); + DefaultTimezoneJitHelper::tryDefaultTimezoneSet('UTC'); } } diff --git a/test/unit/Issue33950DefaultTimezoneAotTest.php b/test/unit/Issue33950DefaultTimezoneAotTest.php new file mode 100644 index 00000000000..c45143fb2f0 --- /dev/null +++ b/test/unit/Issue33950DefaultTimezoneAotTest.php @@ -0,0 +1,68 @@ +assertStringContainsString('JitNestedHelperCoerce::callHelper', $source); + $this->assertStringContainsString('extractStringPtrFromHelperResult', $source); + $this->assertStringContainsString('extractBoolFromHelperResult', $source); + $this->assertStringContainsString('#33950', $source); + } + + public function testHelperOwnsDefaultTimezoneStatic(): void + { + $source = (string) file_get_contents( + dirname(__DIR__, 2).'/ext/standard/DefaultTimezoneJitHelper.php' + ); + $this->assertStringContainsString('private static string $defaultTimezone', $source); + $this->assertStringContainsString('copyTimezoneId', $source); + $this->assertStringContainsString('#33950', $source); + } + + public function testAotGetSetMatchZend(): void + { + if (!\PHPCompiler\LlvmToolchain::hasLibrary(dirname(__DIR__, 2))) { + $this->markTestSkipped('LLVM 9 toolchain not available'); + } + $root = dirname(__DIR__, 2); + $src = $root.'/test/repro/issue_33950_default_timezone_aot.php'; + $bin = sys_get_temp_dir().'/phpc_dtz_33950_'.getmypid().'.bin'; + $compile = 'PHP_COMPILER_HELPER_RUNTIME_O=0 '.escapeshellarg(PHP_BINARY).' ' + .escapeshellarg($root.'/bin/compile.php') + .' -o '.escapeshellarg($bin).' '.escapeshellarg($src).' 2>&1'; + exec($compile, $compileOut, $compileRc); + $this->assertSame(0, $compileRc, implode("\n", $compileOut)); + + $zendOut = []; + exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($src).' 2>/dev/null', $zendOut, $zendRc); + $this->assertSame(0, $zendRc, implode("\n", $zendOut)); + $expected = implode("\n", $zendOut)."\n"; + + try { + for ($i = 0; $i < 5; ++$i) { + $runOut = []; + exec(escapeshellarg($bin).' 2>/dev/null', $runOut, $runRc); + $this->assertSame(0, $runRc, 'run '.($i + 1).': '.implode("\n", $runOut)); + $this->assertSame($expected, implode("\n", $runOut)."\n", 'run '.($i + 1)); + } + } finally { + @unlink($bin); + } + } +}