litespeedtech / litespeedtech/lscache_wp

Race condition in Optimizer::serve() causes PHP warnings when concurrent requests generate the same CSS file

Open
#967 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
257
Forks
123
PR merge metrics
No merged PRs in 30d

Description

## Description

`Optimizer::serve()` in `src/optimizer.cls.php` contains a TOCTOU (time-of-check/time-of-use) race condition that produces PHP warnings under concurrent load when two requests attempt to generate the same combined/minified CSS file simultaneously.

## Affected version

7.8.1

## User-facing impact

When the race fires, `md5_file()` returns `false`, causing `serve()` to return `['.css', 'css']` (a filename with no hash). This has two consequences:

1. **Unstyled page for the affected visitor** — the browser receives `` which 404s, causing all combined styles to fail to load.
2. **Broken URL persists in the database** — `Data::save_url()` is called with `$filecon_md5 = false` and stores the broken filename. Subsequent visitors hitting the same URL may also receive the broken stylesheet reference until the LiteSpeed cache is manually purged.

## Error messages observed in WP_DEBUG_LOG

```
PHP Warning: md5_file(/path/to/wp-content/litespeed/css/abc123.css.tmp): Failed to open stream: No such file or directory in .../litespeed-cache/src/optimizer.cls.php on line 148
PHP Warning: rename(/path/to/wp-content/litespeed/css/abc123.css.tmp, /path/to/wp-content/litespeed/css/.css): No such file or directory in .../litespeed-cache/src/optimizer.cls.php on line 153
```

## Root cause

The guard at lines 109–114 is intended to prevent two concurrent requests from generating the same file:

```php
$tmp_static_file = $static_file . '.tmp';
if (file_exists($tmp_static_file) && time() - filemtime($tmp_static_file) <= 600) {
// some other request is generating
return false;
}
File::save($tmp_static_file, '', true);
```

However, `file_exists()` and `File::save()` are two separate non-atomic operations. Two concurrent requests for the same URL can both pass the `file_exists()` check before either has created the file:

1. **Request A**: `file_exists(tmp)` → `false` → passes guard
2. **Request B**: `file_exists(tmp)` → `false` → passes guard (both slip through simultaneously)
3. **Request A**: `File::save(tmp, '')` → creates tmp file
4. **Request B**: `File::save(tmp, '')` → overwrites with empty content
5. **Request A**: writes CSS content, minifies, calls `md5_file(tmp)` → gets hash, calls `rename(tmp, finalfile)` → **tmp file is now gone**
6. **Request B**: `md5_file(tmp)` → ⚠️ `false` — No such file or directory (line 148)
7. **Request B**: `rename(tmp, '.css')` → ⚠️ fails — No such file or directory (line 153)

Note: `File::save()` uses `file_put_contents(..., LOCK_EX)` internally, but `LOCK_EX` is an advisory flock — it does not block `rename()`, which operates on directory entries and ignores advisory locks entirely. The lock does not protect against this race.

## Steps to reproduce

The race window is narrow (between `rename()` in one worker and `md5_file()` in another), but the following PHP script confirms the bug using `pcntl_fork()` to synchronise two processes to the exact vulnerable moment:

```php

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/optimizer.cls.php at the temporary-file guard around lines 109–114 and the md5_file()/rename() calls around lines 148–153. Use the provided pcntl_fork() reproducer to confirm concurrent requests cannot remove each other’s temporary file. Done means concurrent generation avoids PHP warnings, broken .css URLs, and invalid Data::save_url() entries.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.