Skip to content

TOCTOU race in BlazeManager::isFoldedExpired() → ErrorException when compiled view is removed between isExpired() and file_get_contents() #175

@brandonsparkles

Description

@brandonsparkles

Summary

BlazeManager::isFoldedExpired() (src/BlazeManager.php:291-299) performs a "check-then-read" on the compiled view file without guarding against the file disappearing between the two calls. If the compiled file is deleted between isExpired($path) and file_get_contents($compiled), PHP raises ErrorException("file_get_contents(...): Failed to open stream: No such file or directory"), producing a 500.

Version: livewire/blaze v1.0.11 on Laravel 12, PHP 8.4.

Offending code

https://github.com/livewire/blaze/blob/main/src/BlazeManager.php#L291-L299

$expired = $compiler->isExpired($path);        // stat says "fresh"
$isExpired = false;
if (! $expired) {
    $contents = file_get_contents($compiled);  // file now gone → ErrorException
    $isExpired = (new FrontMatter)->sourceContainsExpiredFoldedDependencies($contents);
}

Reproduction

  1. Long-running PHP-FPM workers render a Blade view through Blaze (populates PHP's stat cache).
  2. Something clears storage/framework/views/ — e.g. php artisan view:clear or optimize:clear during a deploy.
  3. A concurrent request hits the race:
    • isExpired() returns false from the stat cache,
    • the compiled file is already gone,
    • file_get_contents() throws.
  4. The failure is memoized per-worker in \$this->expiredMemo[\$path], so the same worker keeps 500'ing for every subsequent request to that view until the worker cycles or the stat cache expires.

Why Blade doesn't hit this

`Illuminate\View\Engines\CompilerEngine` handles the missing-file case by (re)compiling when the compiled file is absent. Blaze's extra `file_get_contents` lookup does not.

Suggested fix

Either:

```php
// Option A: null-guard the read and treat missing as "compile again"
$contents = @file_get_contents($compiled);
if ($contents === false) {
$compiler->compile($path);
$contents = (string) file_get_contents($compiled);
}
```

```php
// Option B: drop the stat cache before the read
clearstatcache(true, $compiled);
if (! is_file($compiled)) {
$compiler->compile($path);
}
$contents = (string) file_get_contents($compiled);
```

Option A is closest to Blade's own pattern and requires no extra syscalls in the happy path.

Impact

Any deploy or ops action that clears the view cache under live traffic can produce sticky 500s on random Blade-rendered routes until PHP-FPM is reloaded.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions