No Code Attached Yet bug
avatar dishu305
dishu305
28 Oct 2026

What happened?

Steps to Reproduce

  1. Enable File Cache in Joomla Global Configuration.
  2. Trigger a cache miss for a specific resource, causing FileStorage::store() to write a new cache file.
  3. Simulate high concurrency where a second request calls FileStorage::get() for the same cache ID while the first request is still executing fwrite().
  4. Observe that get() may read the file while it is truncated (due to fopen with wb mode), resulting in incomplete serialized data, JSON errors, or corrupted HTML.

Expected Result

The get() method should either wait for the write to complete, or read the previous valid cache file. A cache write should be atomic to prevent serving corrupted data to end users.

Actual Result

Because fopen($path, 'wb') immediately truncates the file, concurrent reads can capture the file in a partially written state. The internal $_locked_files array only prevents the same PHP process from reading its own incomplete write, but does not protect against other concurrent PHP-FPM workers.

Proposed Solution

Implement an atomic write pattern in libraries/src/Cache/Storage/FileStorage.php within the store() method:

  1. Write the cache data to a temporary file in the same directory (e.g., using file_put_contents with LOCK_EX).
  2. Once the write is complete and flushed, atomically rename() the temporary file to the final cache path.
  3. This ensures that get() will either see the old valid file or the new complete file, never a truncated one.

Note: Care must be taken with rename() behavior on Windows environments, potentially requiring a prior unlink() of the destination file if it exists.

System Information

  • Joomla Version: 6.2-dev
  • PHP Version: 8.2+
  • Environment: Concurrent request simulation (e.g., Apache Benchmark or custom script)

Version

6.2

Expected result

No response

Actual result

No response

System Information

No response

Additional Comments

No response

avatar dishu305 dishu305 - open - 28 Oct 2026
avatar joomla-cms-bot joomla-cms-bot - change - 28 Oct 2026
Labels Added: No Code Attached Yet bug
avatar joomla-cms-bot joomla-cms-bot - labeled - 28 Oct 2026
avatar brianteeman
brianteeman - comment - 28 Oct 2026

this reads like an AI and not a real world scenario

avatar brianteeman
brianteeman - comment - 28 Oct 2026

I’d like to see a reproducible test case that demonstrates this actually happening before we start changing the core cache implementation to fix a theoretical problem. Can you provide a script that reliably reproduces the issue?

avatar dishu305
dishu305 - comment - 28 Oct 2026

Hi @brianteeman, thank you for the direct feedback. You are right that the initial description was theoretical.
To demonstrate that this is a real-world scenario, I wrote a standalone PHP script that reproduces the exact filesystem race condition caused by fopen($path, 'wb').
You can run this script directly in any PHP CLI environment:

<?php
$file = ['/tmp/joomla_race_test.txt';]( #[url]([url]([url]( @url))))
$expectedData = str_repeat('CACHE_DATA_PAYLOAD_', 500);

// 1. Pre-populate the file to simulate an existing valid cache file
file_put_contents($file, $expectedData);
echo "1. Initial valid cache file size: " . filesize($file) . " bytes\n";

// 2. Create a temporary "Reader" script (simulating a concurrent web request)
$readerCode = "<?php\nusleep(500000);\n\$content = @file_get_contents('/tmp/joomla_race_test.txt');\n\$len = strlen(\$content);\nif (\$len === 0) {\n    echo 'RACE CONDITION CONFIRMED: Concurrent reader saw an EMPTY (0 bytes) file!' . PHP_EOL;\n} else {\n    echo 'Reader saw ' . \$len . ' bytes.' . PHP_EOL;\n}\n";
file_put_contents('/tmp/simulated_reader.php', $readerCode);

echo "2. Starting concurrent reader process...\n";

// 3. Start the reader in the background
$process = proc_open('php /tmp/simulated_reader.php', [
    1 => ['pipe', 'w'], // stdout
    2 => ['pipe', 'w']  // stderr
], $pipes);

if (is_resource($process)) {
    // 4. Simulate the OLD FileStorage::store() logic
    echo "3. Writer opening file with 'wb' mode (THIS TRUNCATES THE FILE)...\n";
    $fp = fopen($file, 'wb'); 
    
    // Simulate slow disk I/O or a large payload taking time to write
    usleep(1000000); // Sleep for 1 second while file is truncated!
    
    fwrite($fp, $expectedData);
    fclose($fp);
    echo "4. Writer finished.\n";

    // 5. Capture reader output
    echo "\n--- CONCURRENT READER OUTPUT ---\n";
    echo stream_get_contents($pipes[1]);
    echo "--------------------------------\n";
    
    fclose($pipes[1]);
    proc_close($process);
}

// Cleanup
@unlink($file);
@unlink('/tmp/simulated_reader.php');

When run, this script consistently outputs: RACE CONDITION CONFIRMED: Concurrent reader saw an EMPTY (0 bytes) file!
This happens because the fopen call with wb truncates the file to 0 bytes immediately. The background process (simulating a concurrent web request) reads the file while the writer is "sleeping" (simulating slow disk I/O or a large payload), resulting in an empty cache file being served.
I have also linked a Pull Request (#48725) that implements the atomic write fix (temp file + rename) and includes a Unit Test to prevent regression. I hope this provides the concrete evidence needed.

Image

Add a Comment

Login with GitHub to post a comment