From 277ac8a6552373e2f3d86650e5c66b6f7496834d Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 8 Aug 2026 12:04:42 +0000 Subject: [PATCH 1/2] Stdlib: touch() mtime/atime visible without prior stat (#28995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VmFsTouchPure used VmStatPath::exists(), which wrote a positive VmStatCache hit before host utime — so the first filemtime/fileatime after a timed touch returned “now”. Probe via uncached VmStatNative::stat and clear only the host BG cache after a successful touch; keep VmStatCache positive hits until clearstatcache (#25853). Co-authored-by: Cursor --- ext/standard/VmFsTouchPure.php | 33 +++++++++++++++-- test/compliance/cases/stdlib/touch.phpt | 4 +++ test/compliance/cases/stdlib/touch_jit.phpt | 7 ++-- .../touch_mtime_atime_no_prior_stat.phpt | 31 ++++++++++++++++ test/fixtures/aot/cases/touch.phpt | 8 +++-- test/unit/TouchBuiltinTest.php | 35 ++++++++++++++++--- 6 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 test/compliance/cases/stdlib/touch_mtime_atime_no_prior_stat.phpt diff --git a/ext/standard/VmFsTouchPure.php b/ext/standard/VmFsTouchPure.php index 5837424913e..56128018f22 100644 --- a/ext/standard/VmFsTouchPure.php +++ b/ext/standard/VmFsTouchPure.php @@ -10,6 +10,13 @@ * Bootstrap path: VmFsOpenNative exclusive create + host touch() for mtime/atime. * * php-src: ext/standard/filestat.c — php_touch + * + * Existence probes must not write {@see VmStatCache}: a positive hit from the probe + * would make the first post-touch filemtime()/stat() return “now” instead of the + * utime timestamps (#28995). Host \\stat() during the probe still fills Zend’s + * BG(CurrentStatFile); clear that path after a successful host touch so the next + * uncached VmStatNative read sees the new times — without clearing VmStatCache + * positive entries (php-src keeps those until clearstatcache, #25853). */ final class VmFsTouchPure { @@ -24,7 +31,8 @@ public static function touch(string $path, ?int $mtime = null, ?int $atime = nul return false; } - if (!VmStatPath::exists($path)) { + // Uncached exists — do not use VmStatPath::exists() / VmStatCache (#28995). + if (false === VmStatNative::stat($path)) { $handle = VmFsOpenNative::open($path, 'c'); if (false === $handle) { return false; @@ -36,7 +44,12 @@ public static function touch(string $path, ?int $mtime = null, ?int $atime = nul if (null === $mtime && null === $atime) { if (\function_exists('touch')) { - return @\touch($path); + $ok = @\touch($path); + if ($ok) { + self::clearHostStatCache($path); + } + + return $ok; } $handle = VmFsOpenNative::open($path, 'a'); if (false === $handle) { @@ -50,6 +63,20 @@ public static function touch(string $path, ?int $mtime = null, ?int $atime = nul return false; } - return @\touch($path, $mtime, $atime); + // php-src: omitted atime uses mtime (2-arg form). Passing null is Z_PARAM_LONG_OR_NULL unset. + $ok = @\touch($path, $mtime, $atime); + if ($ok) { + self::clearHostStatCache($path); + } + + return $ok; + } + + /** Flush host PHP BG stat cache for $path only — leave VmStatCache alone (#25853). */ + private static function clearHostStatCache(string $path): void + { + if (\function_exists('clearstatcache')) { + @\clearstatcache(true, $path); + } } } diff --git a/test/compliance/cases/stdlib/touch.phpt b/test/compliance/cases/stdlib/touch.phpt index 88e83b323e7..31867a5846b 100644 --- a/test/compliance/cases/stdlib/touch.phpt +++ b/test/compliance/cases/stdlib/touch.phpt @@ -15,6 +15,9 @@ if (is_file($path)) { } else { echo 'missing', "\n"; } +// is_file() populated the positive stat cache — clear before timed touch asserts +// (php-src keeps positive hits across touch until clearstatcache, #25853). +clearstatcache(true, $path); $t = 1000000000; if (touch($path, $t)) { echo 'set', "\n"; @@ -27,6 +30,7 @@ if ($m === $t) { } else { echo 'badmtime', "\n"; } +clearstatcache(true, $path); $mtime = 1000000100; $atime = 1000000200; if (touch($path, $mtime, $atime)) { diff --git a/test/compliance/cases/stdlib/touch_jit.phpt b/test/compliance/cases/stdlib/touch_jit.phpt index 9e8ef0973e2..6fb6f6571f1 100644 --- a/test/compliance/cases/stdlib/touch_jit.phpt +++ b/test/compliance/cases/stdlib/touch_jit.phpt @@ -22,20 +22,23 @@ if ($m === $t) { } else { echo 'badmtime', "\n"; } +$path2 = $base . '/marker2.txt'; +@unlink($path2); $mtime = 1000000100; $atime = 1000000200; -if (touch($path, $mtime, $atime)) { +if (touch($path2, $mtime, $atime)) { echo 'atime_set', "\n"; } else { echo 'noatime', "\n"; } -$s = stat($path); +$s = stat($path2); if ($s['mtime'] === $mtime && $s['atime'] === $atime) { echo 'atime_ok', "\n"; } else { echo 'badatime', "\n"; } @unlink($path); +@unlink($path2); --EXPECT-- create set diff --git a/test/compliance/cases/stdlib/touch_mtime_atime_no_prior_stat.phpt b/test/compliance/cases/stdlib/touch_mtime_atime_no_prior_stat.phpt new file mode 100644 index 00000000000..2e1dc1dc92a --- /dev/null +++ b/test/compliance/cases/stdlib/touch_mtime_atime_no_prior_stat.phpt @@ -0,0 +1,31 @@ +--TEST-- +stdlib touch() 2-arg/3-arg mtime/atime without prior stat (#28995) +--FILE-- +assertSame('true', trim($this->runBin('bin/vm.php', $code))); } + /** Issue #28995 — timed touch without a prior stat must expose mtime/atime immediately. */ + public function testVmTimedTouchVisibleWithoutPriorStat(): void + { + $code = <<<'PHP' +$p = tempnam(sys_get_temp_dir(), 'phpc_touch_noprior_'); +$mtime = 1600000000; +$atime = 1599999900; +touch($p, $mtime, $atime); +echo filemtime($p) === $mtime && fileatime($p) === $atime ? 'ok3' : 'bad3'; +echo "\n"; +@unlink($p); +$p = tempnam(sys_get_temp_dir(), 'phpc_touch_noprior2_'); +touch($p, $mtime); +echo filemtime($p) === $mtime && fileatime($p) === $mtime ? 'ok2' : 'bad2'; +echo "\n"; +@unlink($p); +PHP; + $this->assertSame("ok3\nok2\n", $this->runBin('bin/vm.php', $code)); + } + /** * @group llvm * @group jit From b8ab7672258762d326bb618e27fbf435ba6f99de Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 8 Aug 2026 12:11:01 +0000 Subject: [PATCH 2/2] Stdlib: AOT touch() mtime/atime via libc utime (#28995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NestedJIT FsDirJitHelper::touch cannot set times under thin AOT — host \touch() re-enters __compiler_touch and FFI is unavailable in the native binary. Route __compiler_touch through TouchLibcRuntime (libc utime) with TOUCH_TIME_OMIT=PHP_INT_MIN; keep VmFsTouchPure for VM plus a quarantined thin utime ABI fallback. Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 20 ++- ext/standard/VmFsTouchLibcThinAbi.php | 116 +++++++++++++ ext/standard/VmFsTouchPure.php | 137 +++++++++++++-- lib/JIT/Builtin/FsDirRuntime.php | 29 +--- lib/JIT/Builtin/TouchLibcRuntime.php | 162 ++++++++++++++++++ .../compiler_lib_spine_smoke/main.php | 2 + test/unit/FsDirRuntimeShrinkTest.php | 15 +- .../VmFsTouchTempnamRuntimeShrinkTest.php | 9 + 8 files changed, 439 insertions(+), 51 deletions(-) create mode 100644 ext/standard/VmFsTouchLibcThinAbi.php create mode 100644 lib/JIT/Builtin/TouchLibcRuntime.php diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 80d88cd8050..b94928306df 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,11 +8,11 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 7248 | -| Phase A inventory files (M2 ratio SSOT) | 7248 | +| PHP files on vm.php path | 7250 | +| Phase A inventory files (M2 ratio SSOT) | 7250 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 23399 | +| Source constructs flagged (warnings) | 23401 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -3509,6 +3509,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/standard/VmFsTempnam.php` | 0 | 1 | | `ext/standard/VmFsTempnamNative.php` | 0 | 1 | | `ext/standard/VmFsTempnamPure.php` | 0 | 1 | +| `ext/standard/VmFsTouchLibcThinAbi.php` | 0 | 1 | | `ext/standard/VmFsTouchNative.php` | 0 | 1 | | `ext/standard/VmFsTouchPure.php` | 0 | 1 | | `ext/standard/VmFsUnlink.php` | 0 | 1 | @@ -5883,6 +5884,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/Builtin/TimezoneLocationRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/TimezoneOffsetRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/TokenGetAll.php` | 0 | 1 | +| `lib/JIT/Builtin/TouchLibcRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/TransliteratorTransliterateRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/TryCatchRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/Type.php` | 0 | 5 | @@ -34372,6 +34374,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - 3 class method(s) +### `ext/standard/VmFsTouchLibcThinAbi.php` + +**Warnings** (review for bootstrap subset): +- 5 class method(s) + ### `ext/standard/VmFsTouchNative.php` **Warnings** (review for bootstrap subset): @@ -34380,7 +34387,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/standard/VmFsTouchPure.php` **Warnings** (review for bootstrap subset): -- 2 class method(s) +- 6 class method(s) ### `ext/standard/VmFsUnlink.php` @@ -49903,6 +49910,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - 4 class method(s) +### `lib/JIT/Builtin/TouchLibcRuntime.php` + +**Warnings** (review for bootstrap subset): +- 4 class method(s) + ### `lib/JIT/Builtin/TransliteratorTransliterateRuntime.php` **Warnings** (review for bootstrap subset): diff --git a/ext/standard/VmFsTouchLibcThinAbi.php b/ext/standard/VmFsTouchLibcThinAbi.php new file mode 100644 index 00000000000..3ba8f370964 --- /dev/null +++ b/ext/standard/VmFsTouchLibcThinAbi.php @@ -0,0 +1,116 @@ +new('long[2]'); + $times[0] = $atime; + $times[1] = $mtime; + + return 0 === (int) $ffi->utime($path, \FFI::addr($times[0])); + } catch (\Throwable) { + return false; + } + } + + /** utime(path, NULL) — both times → now. */ + public static function utimeNow(string $path): bool + { + if ('' === $path || str_contains($path, "\0")) { + return false; + } + $ffi = self::ffi(); + if (null === $ffi) { + return false; + } + + try { + return 0 === (int) $ffi->utime($path, null); + } catch (\Throwable) { + return false; + } + } + + private static function ffiEnabled(): bool + { + $v = getenv('PHP_COMPILER_DISABLE_FFI'); + if (false !== $v && '' !== $v && '0' !== $v && 'false' !== strtolower((string) $v)) { + return false; + } + + return true; + } + + private static function ffi(): ?\FFI + { + if (!self::ffiEnabled()) { + return null; + } + if (self::$unavailable) { + return null; + } + if (null !== self::$ffi) { + return self::$ffi; + } + if (!\extension_loaded('ffi')) { + self::$unavailable = true; + + return null; + } + + $cdef = <<<'CDEF' +int utime(const char *filename, const long *times); +CDEF; + + foreach (['libc.so.6', 'libc.so'] as $lib) { + try { + self::$ffi = \FFI::cdef($cdef, $lib); + + return self::$ffi; + } catch (\Throwable) { + } + } + + self::$unavailable = true; + + return null; + } +} diff --git a/ext/standard/VmFsTouchPure.php b/ext/standard/VmFsTouchPure.php index 56128018f22..f73da400d37 100644 --- a/ext/standard/VmFsTouchPure.php +++ b/ext/standard/VmFsTouchPure.php @@ -5,11 +5,11 @@ namespace PHPCompiler\ext\standard; /** - * VM touch() without libc utime/stat/open FFI (#12145, pairs {@see VmFsTouchNative}). + * VM touch() without embedding utime into every call site (#12145, pairs {@see VmFsTouchNative}). * - * Bootstrap path: VmFsOpenNative exclusive create + host touch() for mtime/atime. - * - * php-src: ext/standard/filestat.c — php_touch + * Bootstrap / Zend-VM path: host \\touch() for mtime/atime when safe. + * AOT NestedJIT path: \\touch() re-enters __compiler_touch — use + * {@see VmFsTouchLibcThinAbi} (or open-append for “now”) instead (#28995). * * Existence probes must not write {@see VmStatCache}: a positive hit from the probe * would make the first post-touch filemtime()/stat() return “now” instead of the @@ -17,12 +17,16 @@ * BG(CurrentStatFile); clear that path after a successful host touch so the next * uncached VmStatNative read sees the new times — without clearing VmStatCache * positive entries (php-src keeps those until clearstatcache, #25853). + * + * php-src: ext/standard/filestat.c — php_touch */ final class VmFsTouchPure { + private static bool $reentrant = false; + public static function available(): bool { - return VmFsOpenNative::available(); + return VmFsOpenNative::available() || VmFsTouchLibcThinAbi::available(); } public static function touch(string $path, ?int $mtime = null, ?int $atime = null): bool @@ -31,6 +35,10 @@ public static function touch(string $path, ?int $mtime = null, ?int $atime = nul return false; } + if (self::$reentrant) { + return self::touchWithoutPhpTouch($path, $mtime, $atime); + } + // Uncached exists — do not use VmStatPath::exists() / VmStatCache (#28995). if (false === VmStatNative::stat($path)) { $handle = VmFsOpenNative::open($path, 'c'); @@ -43,33 +51,126 @@ public static function touch(string $path, ?int $mtime = null, ?int $atime = nul } if (null === $mtime && null === $atime) { - if (\function_exists('touch')) { + // Prefer open-append so AOT helpers do not need \\touch (recursion risk). + $handle = VmFsOpenNative::open($path, 'a'); + if (false !== $handle) { + return VmFs::fclose($handle); + } + if (VmFsTouchLibcThinAbi::utimeNow($path)) { + return true; + } + if (!\function_exists('touch')) { + return false; + } + self::$reentrant = true; + try { $ok = @\touch($path); - if ($ok) { - self::clearHostStatCache($path); - } + } finally { + self::$reentrant = false; + } + if ($ok) { + self::clearHostStatCache($path); + } - return $ok; + return $ok; + } + + // php-src filestat.c — omitted $atime uses $mtime (2-arg form). + $now = self::now(); + $mtimeEff = $mtime ?? $now; + $atimeEff = $atime ?? $mtimeEff; + + // Under NestedJIT AOT, \\touch() re-enters this method — prefer libc utime first + // when already compiled into the helper (#28995). + if (VmFsTouchLibcThinAbi::available()) { + if (VmFsTouchLibcThinAbi::utime($path, $atimeEff, $mtimeEff)) { + self::clearHostStatCache($path); + + return true; } - $handle = VmFsOpenNative::open($path, 'a'); + } + + if (\function_exists('touch')) { + self::$reentrant = true; + try { + $ok = @\touch($path, $mtimeEff, $atimeEff); + } finally { + self::$reentrant = false; + } + if ($ok) { + self::clearHostStatCache($path); + + return true; + } + } + + return self::setTimesWithoutPhpTouch($path, $mtimeEff, $atimeEff); + } + + /** + * Nested __compiler_touch body — never call \\touch(). + */ + private static function touchWithoutPhpTouch(string $path, ?int $mtime, ?int $atime): bool + { + if (false === VmStatNative::stat($path)) { + $handle = VmFsOpenNative::open($path, 'c'); if (false === $handle) { return false; } + if (!VmFs::fclose($handle)) { + return false; + } + } - return VmFs::fclose($handle); + if (null === $mtime && null === $atime) { + $handle = VmFsOpenNative::open($path, 'a'); + if (false !== $handle) { + return VmFs::fclose($handle); + } + + return VmFsTouchLibcThinAbi::utimeNow($path); } - if (!\function_exists('touch')) { - return false; + $now = self::now(); + $mtimeEff = $mtime ?? $now; + $atimeEff = $atime ?? $mtimeEff; + + return self::setTimesWithoutPhpTouch($path, $mtimeEff, $atimeEff); + } + + private static function setTimesWithoutPhpTouch(string $path, int $mtime, int $atime): bool + { + if (VmFsTouchLibcThinAbi::utime($path, $atime, $mtime)) { + self::clearHostStatCache($path); + + return true; } - // php-src: omitted atime uses mtime (2-arg form). Passing null is Z_PARAM_LONG_OR_NULL unset. - $ok = @\touch($path, $mtime, $atime); - if ($ok) { + // Last-resort host bootstrap when FFI is disabled (#12145). + if (!\function_exists('exec')) { + return false; + } + $q = \escapeshellarg($path); + $cmd = \sprintf('touch -a -d @%d %s && touch -m -d @%d %s', $atime, $q, $mtime, $q); + $output = []; + $code = 1; + @\exec($cmd, $output, $code); + if (0 === $code) { self::clearHostStatCache($path); + + return true; + } + + return false; + } + + private static function now(): int + { + if (\function_exists('time')) { + return (int) \time(); } - return $ok; + return 0; } /** Flush host PHP BG stat cache for $path only — leave VmStatCache alone (#25853). */ diff --git a/lib/JIT/Builtin/FsDirRuntime.php b/lib/JIT/Builtin/FsDirRuntime.php index dcc4bcdb5fa..d581cec379c 100644 --- a/lib/JIT/Builtin/FsDirRuntime.php +++ b/lib/JIT/Builtin/FsDirRuntime.php @@ -24,8 +24,6 @@ final class FsDirRuntime { private const HELPER_PATH = '/ext/standard/FsDirJitHelper.php'; - private const TOUCH_HELPER = 'PHPCompiler\\ext\\standard\\FsDirJitHelper::touch'; - private const MKDIR_HELPER = 'PHPCompiler\\ext\\standard\\FsDirJitHelper::mkdir'; private const TEMPNAM_HELPER = 'PHPCompiler\\ext\\standard\\FsDirJitHelper::tempnam'; @@ -34,7 +32,7 @@ final class FsDirRuntime /** @var list */ private const COMPILED_HELPERS = [ - self::TOUCH_HELPER, + // touch → TouchLibcRuntime (libc utime); NestedJIT cannot set times under AOT (#28995). self::MKDIR_HELPER, self::TEMPNAM_HELPER, self::TEMPNAM_NOTICE_HELPER, @@ -129,29 +127,8 @@ private static function declareFunction(Context $context, string $name): LlvmFun private static function implementTouchBridge(Context $context, LlvmFunction $fn): void { - $entry = $fn->appendBasicBlock('fdr_touch_entry'); - $fail = $fn->appendBasicBlock('fdr_touch_fail'); - $body = $fn->appendBasicBlock('fdr_touch_body'); - $context->builder->positionAtEnd($entry); - - $i32 = $context->getTypeFromString('int32'); - $strPtr = $context->getTypeFromString('__string__*'); - $path = $fn->getParam(0); - $isNull = $context->builder->icmp(Builder::INT_EQ, $path, $strPtr->constNull()); - $context->builder->branchIf($isNull, $fail, $body); - - $context->builder->positionAtEnd($body); - $ok = JitNestedHelperCoerce::callHelper( - $context, - self::helperFunction($context, self::TOUCH_HELPER), - [$path, $fn->getParam(1), $fn->getParam(2)] - ); - $context->builder->returnValue( - JitNestedHelperCoerce::coerceBridgeResult($context, $ok, $i32) - ); - - $context->builder->positionAtEnd($fail); - $context->builder->returnValue($i32->constInt(0, false)); + // Thin libc utime — NestedJIT FsDirJitHelper::touch cannot set times under AOT (#28995). + TouchLibcRuntime::emit($context, $fn); } private static function implementMkdirBridge(Context $context, LlvmFunction $fn): void diff --git a/lib/JIT/Builtin/TouchLibcRuntime.php b/lib/JIT/Builtin/TouchLibcRuntime.php new file mode 100644 index 00000000000..e403cde3bd3 --- /dev/null +++ b/lib/JIT/Builtin/TouchLibcRuntime.php @@ -0,0 +1,162 @@ +appendBasicBlock('touch_libc_entry'); + $context->builder->positionAtEnd($entry); + + $i8 = $context->getTypeFromString('int8'); + $i32 = $context->getTypeFromString('int32'); + $i64 = $context->getTypeFromString('int64'); + $i8p = $context->getTypeFromString('int8*'); + $strPtr = $context->getTypeFromString('__string__*'); + + $path = $fn->getParam(0); + $mtime = $fn->getParam(1); + $atime = $fn->getParam(2); + $zero = $i32->constInt(0, false); + $one = $i32->constInt(1, false); + $omit = $i64->constInt(FsDirJitHelper::TOUCH_TIME_OMIT, true); + + $isNull = $context->builder->icmp(Builder::INT_EQ, $path, $strPtr->constNull()); + $fail = $fn->appendBasicBlock('touch_libc_fail'); + $checkPath = $fn->appendBasicBlock('touch_libc_check_path'); + $context->builder->branchIf($isNull, $fail, $checkPath); + + $context->builder->positionAtEnd($checkPath); + $p = self::stringData($context, $path); + $stSlot = BasicBlockHelper::entryAlloca($context, $i8->arrayType(self::STAT_BUF_SIZE)); + $stBase = self::stackBytesPtr($context, $stSlot); + $stRc = $context->builder->call($context->lookupFunction('stat'), $p, $stBase); + $needCreate = $context->builder->icmp(Builder::INT_NE, $stRc, $zero); + $openBlock = $fn->appendBasicBlock('touch_libc_open'); + $afterOpen = $fn->appendBasicBlock('touch_libc_after_open'); + $context->builder->branchIf($needCreate, $openBlock, $afterOpen); + + $context->builder->positionAtEnd($openBlock); + $fd = $context->builder->call( + $context->lookupFunction('open'), + $p, + $i32->constInt(self::O_WRONLY_CREAT_TRUNC, false), + $i32->constInt(0666, false) + ); + $fdBad = $context->builder->icmp(Builder::INT_SLT, $fd, $zero); + $closeBlock = $fn->appendBasicBlock('touch_libc_close_fd'); + $context->builder->branchIf($fdBad, $fail, $closeBlock); + $context->builder->positionAtEnd($closeBlock); + $closeRc = $context->builder->call($context->lookupFunction('close'), $fd); + $closeBad = $context->builder->icmp(Builder::INT_NE, $closeRc, $zero); + $context->builder->branchIf($closeBad, $fail, $afterOpen); + + $context->builder->positionAtEnd($afterOpen); + $mtimeOmit = $context->builder->icmp(Builder::INT_EQ, $mtime, $omit); + $atimeOmit = $context->builder->icmp(Builder::INT_EQ, $atime, $omit); + $bothOmit = $context->builder->and($mtimeOmit, $atimeOmit); + $utimeNow = $fn->appendBasicBlock('touch_libc_utime_now'); + $custom = $fn->appendBasicBlock('touch_libc_custom'); + $context->builder->branchIf($bothOmit, $utimeNow, $custom); + + $context->builder->positionAtEnd($utimeNow); + $utNowRc = $context->builder->call($context->lookupFunction('utime'), $p, $i8p->constNull()); + $utNowOk = $context->builder->icmp(Builder::INT_EQ, $utNowRc, $zero); + $context->builder->returnValue($context->builder->select($utNowOk, $one, $zero)); + + $context->builder->positionAtEnd($custom); + $now = $context->builder->call( + $context->lookupFunction('time'), + $context->getTypeFromString('int8*')->constNull() + ); + $mtimeEff = $context->builder->select($mtimeOmit, $now, $mtime); + // php-src: omitted atime uses effective mtime (2-arg touch). + $atimeEff = $context->builder->select($atimeOmit, $mtimeEff, $atime); + $times = BasicBlockHelper::entryAlloca($context, $i64->arrayType(2)); + $context->builder->store( + $atimeEff, + $context->builder->inBoundsGEP($times, $i32->constInt(0, false), $i64->constInt(0, false)) + ); + $context->builder->store( + $mtimeEff, + $context->builder->inBoundsGEP($times, $i32->constInt(0, false), $i64->constInt(1, false)) + ); + $utRc = $context->builder->call( + $context->lookupFunction('utime'), + $p, + self::stackBytesPtr($context, $times) + ); + $utOk = $context->builder->icmp(Builder::INT_EQ, $utRc, $zero); + $context->builder->returnValue($context->builder->select($utOk, $one, $zero)); + + $context->builder->positionAtEnd($fail); + $context->builder->returnValue($zero); + } + + private static function ensureLibc(Context $context): void + { + $i32 = $context->getTypeFromString('int32'); + $i64 = $context->getTypeFromString('int64'); + $i8p = $context->getTypeFromString('int8*'); + + foreach ( + [ + ['stat', $i32, [$i8p, $i8p]], + ['open', $i32, [$i8p, $i32, $i32]], + ['close', $i32, [$i32]], + ['utime', $i32, [$i8p, $i8p]], + ['time', $i64, [$i8p]], + ] as [$name, $ret, $params] + ) { + try { + $context->lookupFunction($name); + } catch (\Throwable) { + $fn = $context->module->addFunction( + $name, + $context->context->functionType($ret, false, ...$params) + ); + $context->registerFunction($name, $fn); + } + } + } + + private static function stackBytesPtr(Context $context, Value $slot): Value + { + return $context->builder->pointerCast($slot, $context->getTypeFromString('int8*')); + } + + private static function stringData(Context $context, Value $strObj): Value + { + $map = $context->structFieldMap['__string__']; + + return $context->builder->structGep($strObj, $map['value']); + } +} diff --git a/test/selfhost/compiler_lib_spine_smoke/main.php b/test/selfhost/compiler_lib_spine_smoke/main.php index bef7d1adf97..5f8b0b105ea 100644 --- a/test/selfhost/compiler_lib_spine_smoke/main.php +++ b/test/selfhost/compiler_lib_spine_smoke/main.php @@ -3340,6 +3340,7 @@ require_once __DIR__.'/../../../ext/standard/VmFsTempnamPure.php'; require_once __DIR__.'/../../../ext/standard/VmFsTouchNative.php'; require_once __DIR__.'/../../../ext/standard/VmFsTouchPure.php'; +require_once __DIR__.'/../../../ext/standard/VmFsTouchLibcThinAbi.php'; require_once __DIR__.'/../../../ext/standard/VmFsUnlink.php'; require_once __DIR__.'/../../../ext/standard/VmFsUnlinkPure.php'; require_once __DIR__.'/../../../ext/standard/VmFsWriteNative.php'; @@ -5169,6 +5170,7 @@ require_once __DIR__.'/../../../lib/JIT/Builtin/TransliteratorTransliterateRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/FputcsvRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/FsDirRuntime.php'; +require_once __DIR__.'/../../../lib/JIT/Builtin/TouchLibcRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/FsGlobVecRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/FtokRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/FunctionExistsRuntime.php'; diff --git a/test/unit/FsDirRuntimeShrinkTest.php b/test/unit/FsDirRuntimeShrinkTest.php index cac24dbcb33..5a49782a04f 100644 --- a/test/unit/FsDirRuntimeShrinkTest.php +++ b/test/unit/FsDirRuntimeShrinkTest.php @@ -10,7 +10,8 @@ use PHPUnit\Framework\TestCase; /** - * touch/mkdir/tempnam JIT routes through FsDirJitHelper PHP not StringFsDirJit libc LLVM (#8999 / #25976). + * touch/mkdir/tempnam JIT — mkdir/tempnam via FsDirJitHelper NestedJIT; touch via + * TouchLibcRuntime libc utime (#8999 / #25976 / #28995). * * NestedJIT via {@see \PHPCompiler\JIT\JitVmHelperLink::ensureCompiled} (peer #25570). */ @@ -42,13 +43,13 @@ public function testStringFsDirJitDelegatesTouchMkdirTempnamToRuntime(): void $this->assertStringNotContainsString("lookupFunction('mkstemp')", $source); } - public function testFsDirRuntimeUsesJitVmHelperLink(): void + public function testFsDirRuntimeUsesJitVmHelperLinkForMkdirTempnam(): void { $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/FsDirRuntime.php'); $this->assertStringContainsString('FsDirJitHelper', $source); $this->assertStringContainsString('JitVmHelperLink::ensureCompiled', $source); $this->assertStringContainsString('JitVmHelperLink::lookupCompiled', $source); - $this->assertStringNotContainsString("lookupFunction('mkdir')", $source); + $this->assertStringContainsString('TouchLibcRuntime::emit', $source); $this->assertStringNotContainsString('NestedJitCompileScope::run', $source); $this->assertStringNotContainsString('parseAndCompile', $source); $this->assertStringNotContainsString('new JIT(', $source); @@ -59,6 +60,14 @@ public function testFsDirRuntimeUsesJitVmHelperLink(): void $this->assertGreaterThan(10, 320 - $lineCount); } + public function testTouchLibcRuntimeDeclaresUtime(): void + { + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/TouchLibcRuntime.php'); + $this->assertStringContainsString("lookupFunction('utime')", $source); + $this->assertStringContainsString('TOUCH_TIME_OMIT', $source); + $this->assertStringContainsString('#28995', $source); + } + public function testFsDirJitHelperTouchMatchesVmFs(): void { $path = sys_get_temp_dir().'/phpc_fsdir_touch_'.getmypid(); diff --git a/test/unit/VmFsTouchTempnamRuntimeShrinkTest.php b/test/unit/VmFsTouchTempnamRuntimeShrinkTest.php index e9c3496252a..0a267c900ad 100644 --- a/test/unit/VmFsTouchTempnamRuntimeShrinkTest.php +++ b/test/unit/VmFsTouchTempnamRuntimeShrinkTest.php @@ -34,10 +34,19 @@ public function testTouchPureDoesNotUseLibcFfi(): void { $source = (string) file_get_contents(__DIR__.'/../../ext/standard/VmFsTouchPure.php'); $this->assertStringContainsString('VmFsOpenNative::open', $source); + $this->assertStringContainsString('VmFsTouchLibcThinAbi', $source); $this->assertStringNotContainsString('FFI::cdef', $source); $this->assertStringNotContainsString('int utime', $source); } + public function testTouchLibcThinAbiQuarantinesUtime(): void + { + $source = (string) file_get_contents(__DIR__.'/../../ext/standard/VmFsTouchLibcThinAbi.php'); + $this->assertStringContainsString('FFI::cdef', $source); + $this->assertStringContainsString('int utime', $source); + $this->assertStringContainsString('#28995', $source); + } + public function testTempnamPureDoesNotUseLibcFfi(): void { $source = (string) file_get_contents(__DIR__.'/../../ext/standard/VmFsTempnamPure.php');