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
123 changes: 119 additions & 4 deletions ext/standard/DefaultTimezoneJitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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';
}
}
3 changes: 3 additions & 0 deletions ext/standard/JitDate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 23 additions & 3 deletions lib/JIT/Builtin/DefaultTimezoneRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions test/fixtures/aot/cases/default_timezone_get_set_33950.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
AOT date_default_timezone_get/set NestedJIT coerce (#33950)
--FILE--
<?php
echo date_default_timezone_get(), "\n";
var_export(date_default_timezone_set('Europe/Berlin'));
echo "\n";
echo date_default_timezone_get(), "\n";
var_export(date_default_timezone_set('Not/AZone'));
echo "\n";
echo date_default_timezone_get(), "\n";
--EXPECT--
UTC
true
Europe/Berlin
false
Europe/Berlin
9 changes: 9 additions & 0 deletions test/repro/issue_33950_default_timezone_aot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
// AOT: date_default_timezone_get/set NestedJIT coerce (#33950)
echo date_default_timezone_get(), "\n";
var_export(date_default_timezone_set('Europe/Berlin'));
echo "\n";
echo date_default_timezone_get(), "\n";
var_export(date_default_timezone_set('Not/AZone'));
echo "\n";
echo date_default_timezone_get(), "\n";
20 changes: 11 additions & 9 deletions test/unit/DefaultTimezoneRuntimeShrinkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,30 @@
namespace PHPCompiler\Test\Unit;

use PHPCompiler\ext\standard\DefaultTimezoneJitHelper;
use PHPCompiler\ext\standard\VmDate;
use PHPUnit\Framework\TestCase;

/** DefaultTimezoneRuntime routes through DefaultTimezoneJitHelper PHP not LLVM globals (#9243, #24962). */
final class DefaultTimezoneRuntimeShrinkTest extends TestCase
{
public function testDefaultTimezoneJitHelperDelegatesToVmDate(): void
public function testDefaultTimezoneJitHelperOwnsProcessDefault(): void
{
$source = (string) file_get_contents(__DIR__.'/../../ext/standard/DefaultTimezoneJitHelper.php');
$this->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
{
$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);
Expand All @@ -35,20 +37,20 @@ 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());

$this->assertFalse(DefaultTimezoneJitHelper::tryDefaultTimezoneSet('Invalid/Zone'));
$this->assertSame('Europe/Berlin', DefaultTimezoneJitHelper::defaultTimezoneGet());

VmDate::tryDefaultTimezoneSet('UTC');
DefaultTimezoneJitHelper::tryDefaultTimezoneSet('UTC');
}
}
68 changes: 68 additions & 0 deletions test/unit/Issue33950DefaultTimezoneAotTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\Test\Unit;

use PHPUnit\Framework\TestCase;

/**
* AOT date_default_timezone_get/set NestedJIT coerce (#33950).
*
* @group llvm
* @group aot
*/
final class Issue33950DefaultTimezoneAotTest extends TestCase
{
public function testRuntimeUsesNestedHelperCoerce(): void
{
$source = (string) file_get_contents(
dirname(__DIR__, 2).'/lib/JIT/Builtin/DefaultTimezoneRuntime.php'
);
$this->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);
}
}
}
Loading