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

No one assigned

    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