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
20 changes: 16 additions & 4 deletions docs/bootstrap-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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):
Expand All @@ -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`

Expand Down Expand Up @@ -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):
Expand Down
116 changes: 116 additions & 0 deletions ext/standard/VmFsTouchLibcThinAbi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

/**
* Thin libc utime(2) ABI for touch() when host \\touch() is unavailable or would
* re-enter __compiler_touch under NestedJIT AOT (#28995, #12145).
*
* Justified thin ABI: setting file times requires a platform utime/utimes call;
* Pure PHP cannot invent mtime/atime. Quarantined here — not in {@see VmFsTouchPure}.
*
* php-src: ext/standard/filestat.c — php_touch / VCWD_UTIME
*/
final class VmFsTouchLibcThinAbi
{
private static ?\FFI $ffi = null;

private static bool $unavailable = false;

public static function available(): bool
{
return null !== self::ffi();
}

/**
* utime(2) with explicit actime/modtime (seconds since epoch).
*
* @return bool true on success
*/
public static function utime(string $path, int $atime, int $mtime): bool
{
if ('' === $path || str_contains($path, "\0")) {
return false;
}
$ffi = self::ffi();
if (null === $ffi) {
return false;
}

try {
// Linux utimbuf is { time_t actime; time_t modtime; } — two adjacent longs.
$times = $ffi->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;
}
}
148 changes: 138 additions & 10 deletions ext/standard/VmFsTouchPure.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,28 @@
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.
* 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
* 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).
*
* 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
Expand All @@ -24,7 +35,12 @@ public static function touch(string $path, ?int $mtime = null, ?int $atime = nul
return false;
}

if (!VmStatPath::exists($path)) {
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');
if (false === $handle) {
return false;
Expand All @@ -35,21 +51,133 @@ public static function touch(string $path, ?int $mtime = null, ?int $atime = nul
}

if (null === $mtime && null === $atime) {
if (\function_exists('touch')) {
return @\touch($path);
}
// 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);
} finally {
self::$reentrant = false;
}
if ($ok) {
self::clearHostStatCache($path);
}

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;
}
}

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;
}
}

if (null === $mtime && null === $atime) {
$handle = VmFsOpenNative::open($path, 'a');
if (false !== $handle) {
return VmFs::fclose($handle);
}

return VmFs::fclose($handle);
return VmFsTouchLibcThinAbi::utimeNow($path);
}

if (!\function_exists('touch')) {
$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;
}

// 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 @\touch($path, $mtime, $atime);
return false;
}

private static function now(): int
{
if (\function_exists('time')) {
return (int) \time();
}

return 0;
}

/** 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);
}
}
}
Loading
Loading