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
124 changes: 123 additions & 1 deletion ext/standard/JitStatKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
use PHPLLVM\Value;

/**
* Thin libc stat(2)/lstat(2)/access(2) for path predicates (#19215).
* Thin libc stat(2)/lstat(2)/access(2) for path predicates + long fields (#19215, #27013).
*
* Standalone mode helpers avoid LLVM miscompile when nested into helper TUs (#8555).
* Keep glibc layout here — not in {@see JitStat} (#9112 shrink).
* Long fields: NestedJIT VmStatCache arrays mis-read under thin AOT (#27013); peer
* {@see \PHPCompiler\JIT\Builtin\FtokRuntime} emits the platform leaf in LLVM.
* php-src: ext/standard/filestat.c
*/
final class JitStatKernel
Expand All @@ -23,6 +25,20 @@ final class JitStatKernel
/** offsetof(struct stat, st_mode) on Linux x86_64 glibc */
private const STAT_MODE_OFFSET = 24;

/** @var array<int, array{offset: int, width: int}> fieldId → {byte offset, load width 4|8} */
private const LONG_FIELD_LAYOUT = [
// StatFieldsJitHelper::FIELD_* — glibc x86_64 struct stat
StatFieldsJitHelper::FIELD_SIZE => ['offset' => 48, 'width' => 8], // st_size
StatFieldsJitHelper::FIELD_MTIME => ['offset' => 88, 'width' => 8], // st_mtim.tv_sec
StatFieldsJitHelper::FIELD_ATIME => ['offset' => 72, 'width' => 8], // st_atim.tv_sec
StatFieldsJitHelper::FIELD_CTIME => ['offset' => 104, 'width' => 8], // st_ctim.tv_sec
StatFieldsJitHelper::FIELD_INO => ['offset' => 8, 'width' => 8], // st_ino
StatFieldsJitHelper::FIELD_UID => ['offset' => 28, 'width' => 4], // st_uid
StatFieldsJitHelper::FIELD_GID => ['offset' => 32, 'width' => 4], // st_gid
StatFieldsJitHelper::FIELD_DEV => ['offset' => 0, 'width' => 8], // st_dev
StatFieldsJitHelper::FIELD_MODE => ['offset' => 24, 'width' => 4], // st_mode
];

/** @return Value i32 — st_mode, or -1 on failure */
public static function mode(Context $context, Value $pathStr, bool $useLstat): Value
{
Expand All @@ -32,6 +48,19 @@ public static function mode(Context $context, Value $pathStr, bool $useLstat): V
return $context->builder->call($fn, $pathStr);
}

/**
* Read a php-stat long field via libc (i64, or -1 on failure).
*
* @param Value $useLstat i64 — 0 = stat(2), nonzero = lstat(2)
* @param Value $fieldId i64 — {@see StatFieldsJitHelper} FIELD_* constant
*/
public static function longField(Context $context, Value $pathStr, Value $useLstat, Value $fieldId): Value
{
$fn = self::ensureLongFieldStandalone($context);

return $context->builder->call($fn, $pathStr, $useLstat, $fieldId);
}

/** @return Value i1 — access(2) succeeds */
public static function accessOk(Context $context, Value $pathStr, int $mode): Value
{
Expand Down Expand Up @@ -95,4 +124,97 @@ private static function ensureModeStandalone(Context $context, string $statFn):

return $fn;
}

private static function ensureLongFieldStandalone(Context $context): Value
{
$name = '__phpc_jit_stat_long_field_kernel';
$existing = $context->module->getNamedFunction($name);
if (null !== $existing && $existing->countBasicBlocks() > 0) {
$context->registerFunction($name, $existing);

return $existing;
}

$strPtr = $context->getTypeFromString('__string__*');
$i64 = $context->getTypeFromString('int64');
$fn = $context->module->addFunction(
$name,
$context->context->functionType($i64, false, $strPtr, $i64, $i64)
);
$saved = $context->builder;
$context->builder = $context->context->builderCreate();

$entry = $fn->appendBasicBlock('entry');
$fail = $fn->appendBasicBlock('fail');
$statOk = $fn->appendBasicBlock('stat_ok');
$useLstatBlock = $fn->appendBasicBlock('use_lstat');
$useStatBlock = $fn->appendBasicBlock('use_stat');
$context->builder->positionAtEnd($entry);

$path = $fn->getParam(0);
$useLstat = $fn->getParam(1);
$fieldId = $fn->getParam(2);
$i32 = $context->getTypeFromString('int32');
$i8 = $context->getTypeFromString('int8');
$i8p = $context->getTypeFromString('int8*');
$map = $context->structFieldMap['__string__'];
$pathPtr = $context->builder->structGep($path, $map['value']);
$bufType = $i8->arrayType(self::STAT_BUF_SIZE);
$buf = $context->builder->alloca($bufType, 1, 'phpc_stat_long_buf');
$bufPtr = $context->builder->pointerCast($buf, $i8p);
$wantLstat = $context->builder->icmp(Builder::INT_NE, $useLstat, $i64->constInt(0, false));
$context->builder->branchIf($wantLstat, $useLstatBlock, $useStatBlock);

$context->builder->positionAtEnd($useStatBlock);
$statRet = $context->builder->call($context->lookupFunction('stat'), $pathPtr, $bufPtr);
$statEnd = $context->builder->getInsertBlock();
$context->builder->branch($statOk);

$context->builder->positionAtEnd($useLstatBlock);
$lstatRet = $context->builder->call($context->lookupFunction('lstat'), $pathPtr, $bufPtr);
$lstatEnd = $context->builder->getInsertBlock();
$context->builder->branch($statOk);

$context->builder->positionAtEnd($statOk);
$rc = $context->builder->phi($i32);
$rc->addIncoming($statRet, $statEnd);
$rc->addIncoming($lstatRet, $lstatEnd);
$failed = $context->builder->icmp(Builder::INT_NE, $rc, $i32->constInt(0, false));
$dispatch = $fn->appendBasicBlock('dispatch');
$context->builder->branchIf($failed, $fail, $dispatch);

$context->builder->positionAtEnd($dispatch);
// Chain of field selects — fieldId is almost always a compile-time constant at call sites.
$next = $dispatch;
foreach (self::LONG_FIELD_LAYOUT as $id => $layout) {
$matchBlock = $fn->appendBasicBlock('field_'.$id);
$contBlock = $fn->appendBasicBlock('field_cont_'.$id);
$context->builder->positionAtEnd($next);
$isMatch = $context->builder->icmp(Builder::INT_EQ, $fieldId, $i64->constInt($id, false));
$context->builder->branchIf($isMatch, $matchBlock, $contBlock);

$context->builder->positionAtEnd($matchBlock);
$bytePtr = $context->builder->gep($bufPtr, $i64->constInt($layout['offset'], false));
if (8 === $layout['width']) {
$valPtr = $context->builder->pointerCast($bytePtr, $i64->pointerType(0));
$loaded = $context->builder->load($valPtr);
} else {
$valPtr = $context->builder->pointerCast($bytePtr, $i32->pointerType(0));
$loaded = $context->builder->zext($context->builder->load($valPtr), $i64);
}
$context->builder->returnValue($loaded);
$next = $contBlock;
}
$context->builder->positionAtEnd($next);
$context->builder->returnValue($i64->constInt(-1, true));

$context->builder->positionAtEnd($fail);
$context->builder->returnValue($i64->constInt(-1, true));

$context->builder->clearInsertionPosition();
$context->builder = $saved;
$context->registerFunction($name, $fn);

return $fn;
}
}
33 changes: 21 additions & 12 deletions ext/standard/JitStatPathKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PHPCompiler\JIT\Builtin\StatCacheRuntime;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\JitNestedHelperCoerce;
use PHPCompiler\JIT\JitVmHelperLink;
use PHPLLVM\Builder;
use PHPLLVM\Value\Function_ as LlvmFunction;
Expand All @@ -19,7 +20,7 @@
* {@see phpc_stat_mode_kernel} / {@see phpc_access_kernel}.
*
* Helper compile: bundled {@see JitVmHelperLink::ensureCompiledBundle} (peer Pack #22842 /
* SessionStorage #23284).
* SessionStorage #23284). Long-field ABI uses libc via {@see JitStatKernel::longField} (#27013).
*
* SSOT: {@see StatPathJitHelper}, {@see StatFieldsJitHelper}
* php-src: ext/standard/filestat.c
Expand Down Expand Up @@ -76,8 +77,6 @@ final class JitStatPathKernel

private const IS_EXECUTABLE_HELPER = 'PHPCompiler\\ext\\standard\\StatPathJitHelper::isExecutable';

private const LONG_FIELD_HELPER = 'PHPCompiler\\ext\\standard\\StatFieldsJitHelper::longField';

private const FILETYPE_LABEL_HELPER = 'PHPCompiler\\ext\\standard\\StatFieldsJitHelper::filetypeLabel';

private const DISK_FREE_HELPER = 'PHPCompiler\\ext\\standard\\StatFieldsJitHelper::diskFreeBytes';
Expand All @@ -93,7 +92,7 @@ final class JitStatPathKernel
self::IS_READABLE_HELPER,
self::IS_WRITABLE_HELPER,
self::IS_EXECUTABLE_HELPER,
self::LONG_FIELD_HELPER,
// longField: libc via JitStatKernel (#27013) — not NestedJIT VmStatCache arrays
self::FILETYPE_LABEL_HELPER,
self::DISK_FREE_HELPER,
self::DISK_TOTAL_HELPER,
Expand Down Expand Up @@ -225,13 +224,16 @@ private static function implementLongFieldBridge(Context $context): void
$context->builder->branchIf($nullPath, $fail, $run);

$context->builder->positionAtEnd($run);
$value = $context->builder->call(
self::helperFunction($context, self::LONG_FIELD_HELPER),
$path,
$fn->getParam(1),
$fn->getParam(2)
// Thin AOT NestedJIT of VmStatCache arrays returns type-tag/pointer garbage for
// filesize/filemtime/… (#27013). Peer path predicates + FtokRuntime: libc leaf.
$context->builder->returnValue(
JitStatKernel::longField(
$context,
$path,
$fn->getParam(1),
$fn->getParam(2)
)
);
$context->builder->returnValue($value);

$context->builder->positionAtEnd($fail);
$context->builder->returnValue($i64->constInt(-1, true));
Expand Down Expand Up @@ -306,8 +308,15 @@ private static function implementDiskBridge(Context $context, string $abiName, s
$context->builder->branchIf($nullPath, $fail, $run);

$context->builder->positionAtEnd($run);
$bytes = $context->builder->call(self::helperFunction($context, $helper), $path);
$context->builder->returnValue($bytes);
// Same boxed-int extract as longField (#27013 / #20266).
$raw = JitNestedHelperCoerce::callHelper(
$context,
self::helperFunction($context, $helper),
[$path]
);
$context->builder->returnValue(
JitNestedHelperCoerce::coerceBridgeResult($context, $raw, $i64)
);

$context->builder->positionAtEnd($fail);
$context->builder->returnValue($i64->constInt(-1, true));
Expand Down
10 changes: 9 additions & 1 deletion ext/standard/StatFieldsJitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
/**
* Stat field reads for compiled JIT/AOT modules (#9112, php-in-PHP).
*
* Long fields (filesize/filemtime/…): AOT bridge uses libc via {@see JitStatKernel::longField}
* (#27013) — NestedJIT of this VmStatCache path mis-reads array values under thin standalone.
* This helper remains the FIELD_* id table + filetype/disk NestedJIT surface.
*
* VM SSOT: {@see VmStatCache}, {@see VmFs}, {@see VmFsDiskNative}
* php-src: ext/standard/filestat.c
*/
Expand All @@ -30,7 +34,11 @@ final class StatFieldsJitHelper

public const FIELD_MODE = 8;

/** @return int field value, or -1 on failure (LLVM i64 ABI) */
/**
* VM / host fallback for long fields (AOT uses {@see JitStatKernel::longField}).
*
* @return int field value, or -1 on failure (LLVM i64 ABI)
*/
public static function longField(string $path, int $useLstat, int $fieldId): int
{
if ('' === $path) {
Expand Down
10 changes: 10 additions & 0 deletions test/repro/issue_27013_aot_filesize.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
/**
* #27013 — AOT filesize() must match Zend st_size (not NestedJIT array garbage).
*
* Run: php bin/vm.php test/repro/issue_27013_aot_filesize.php
* AOT: php bin/compile.php -o /tmp/aot_fs test/repro/issue_27013_aot_filesize.php && /tmp/aot_fs
*/
$path = '/tmp/phpc_27013_filesize_probe.txt';
file_put_contents($path, 'hi');
echo filesize($path), "\n";
5 changes: 5 additions & 0 deletions test/unit/StatPathRuntimeShrinkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public function testKernelUsesJitVmHelperLinkBundle(): void
$this->assertStringContainsString('StatCacheRuntime::ensureLinked', $source);
$this->assertStringContainsString('JitVmHelperLink::ensureCompiledBundle', $source);
$this->assertStringContainsString('JitVmHelperLink::lookupCompiled', $source);
$this->assertStringContainsString('JitStatKernel::longField', $source);
$this->assertStringContainsString('#27013', $source);
$this->assertStringNotContainsString('NestedJitCompileScope::run', $source);
$this->assertStringNotContainsString('parseAndCompile', $source);
$this->assertStringNotContainsString('new JIT(', $source);
Expand Down Expand Up @@ -99,6 +101,9 @@ public function testStatPathJitHelperUsesStatModeKernelNotExternalVmStatPath():
$this->assertStringContainsString("lookupFunction(\$statFn)", $kernel);
$this->assertStringContainsString("lookupFunction('access')", $kernel);
$this->assertStringContainsString('STAT_MODE_OFFSET', $kernel);
$this->assertStringContainsString('LONG_FIELD_LAYOUT', $kernel);
$this->assertStringContainsString('ensureLongFieldStandalone', $kernel);
$this->assertStringContainsString('#27013', $kernel);
}

public function testStatFieldsJitHelperDelegatesToVmStatCache(): void
Expand Down
Loading