Bali, Indonesia
Insights

Why Deleted Webshells Still Run Until OPcache Is Cleared

October 19, 2026
0:00
You spot a malicious backdoor script sitting in an upload directory, verify its contents, and remove it with rm. A few minutes later, your web server access logs show remote attackers continuing to hit the exact same URL, receiving clean HTTP 200 responses and executing arbitrary commands. The file is completely absent from the file system, yet the backdoor behaves as if nothing changed. This behavior routinely causes confusion during server cleanup. It is not caused by ghost processes or rootkits hiding directory entries; it is standard PHP OPcache behavior. When OPcache is active, PHP does not run files directly from disk on every HTTP hit. It compiles PHP code into low-level instructions called bytecode and stores that bytecode in shared memory. Here are the most common misconceptions about how OPcache handles removed files and how to clear them reliably.
It feels logical to assume that once an inode or file pointer disappears from a storage volume, a web server cannot possibly execute it. In a vanilla environment without caching, this holds true: PHP asks the kernel to open the file descriptor, receives an error, and returns a 404 response. OPcache alters this sequence entirely. The primary purpose of OPcache is eliminating the disk reads and compilation steps that slow down PHP applications. When a request targets a script, PHP first checks whether compiled bytecode for that specific real path already exists in shared memory. If an entry exists and the cache settings do not mandate an immediate disk check, PHP executes the bytecode directly from RAM. As far as the PHP-FPM worker is concerned, the source code on disk is completely irrelevant once the compiled opcodes reside in memory. Until the cache entry is invalidated or evicted, requests pointing to the deleted script URL continue to run with the exact privileges of your web server process.
Many administrators know that OPcache checks timestamps, so they expect the engine to instantly realize the file is gone. OPcache does have an automated revalidation mechanism controlled by the directive opcache.validate_timestamps, but it does not run a synchronous disk check on every HTTP request. Instead, OPcache relies on opcache.revalidate_freq, which specifies how many seconds must elapse before PHP checks whether a file has changed. If this value is set to a higher interval or disabled altogether for performance in production environments, PHP skips disk queries entirely. Furthermore, when opcache.validate_timestamps is set to zero (a common configuration on high-traffic or immutable server deployments), PHP will never check the disk for updates or deletions after the initial compile. The bytecode persists in memory indefinitely until the PHP master process restarts or receives a cache reset signal. Even when revalidation is enabled with a short interval, an attacker running continuous loops against a webshell can keep interacting with the script until the check window lapses. In some edge cases involving path resolution caching, deleted files may linger in worker memory longer than expected.
A common troubleshooting step is opening an SSH terminal and issuing a command like php -r 'opcache_reset();'. The terminal prints no errors, giving the impression that the cache has been flushed. Yet testing the deleted webshell URL still yields an active HTTP 200 response. This happens because the command-line interface (CLI) and the web process (PHP-FPM, Apache mod_php, or LiteSpeed) operate in entirely different memory segments. Shared memory segments are tied to the parent process pool. When you invoke php on the command line, your operating system spawns a transient, short-lived PHP process with its own empty OPcache allocation. Running opcache_reset() inside that session flushes the CLI memory segment, which was already created milliseconds prior, and leaves the persistent memory segment of your web server untouched. To invalidate OPcache inside the web environment, the reset command must be evaluated by the actual web worker, or the master PHP service must be reloaded.
When administrators realize that CLI commands failed and deleted scripts are still answering HTTP requests, some resort to rebooting the entire operating system. While a reboot clears RAM and eliminates cached bytecode, it causes unnecessary downtime for every service running on the host. To evict the webshell cleanly, you only need to tell the PHP master process to discard its current worker pool and shared memory table. This can be done by issuing a graceful service reload or by sending a reload signal to the PHP-FPM master process, which replaces worker processes without dropping in-flight HTTP connections.
BASH
# Check the active PHP-FPM service name
systemctl list-units --type=service | grep fpm

# Gracefully reload the service without dropping active connections
systemctl reload php8.2-fpm
Alternatively, if you run on an environment where service restarts require change tickets or cause multi-tenant disruption, you can drop a temporary PHP file into the web root accessible via HTTP containing opcache_invalidate('/path/to/deleted/webshell.php', true); and call it once through curl. That tells the web worker directly to purge the bytecode matching that path from shared memory, after which you immediately remove the helper script. Cleaning infected files from the file system is only the first step in remediation. Until you account for the persistence of shared memory in production web stacks, your application may remain actively compromised long after your scanner reports a clean disk.