[php-src] Issue #24361: opcache_compile_file() on a file with a fatal compile error disables the cycle collector for the rest of the request
| From: | likemusic | Date: | Thu, 15 Oct 2026 19:18:13 +0000 |
| Subject: | [php-src] Issue #24361: opcache_compile_file() on a file with a fatal compile error disables the cycle collector for the rest of the request | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253005@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24361
Author: likemusic
### Description
When
opcache_compile_file() is given a file that fails with a fatal compile error, the
function catches the bailout, emits a warning, returns false and lets the script
continue. But zend_bailout() has already called gc_protect(1), and nothing
resets it until the next request. From that point on gc_possible_root() drops every new
root candidate, so cycles created later in the request are never collected, not even by an explicit
gc_collect_cycles().
The following code:
```php
<?php
$bad = sys_get_temp_dir() . "/bad.php";
file_put_contents($bad, "<?php\nbreak 2;\n");
class Node { public $self; }
var_dump(gc_status()["protected"]);
var_dump(@opcache_compile_file($bad));
var_dump(gc_status()["protected"]);
for ($i = 0; $i < 100000; $i++) { $o = new Node; $o->self = $o; }
var_dump(gc_collect_cycles());
var_dump(gc_status()["collected"]);
```
Run with php -n -d opcache.enable_cli=1 (master, debug build).
Resulted in this output:
```
bool(false)
Fatal error: 'break' not in the 'loop' or 'switch' context in
/tmp/bad.php on line 2
Stack trace:
#0 /path/to/test.php(8): opcache_compile_file('/tmp/bad.php')
#1 {main}
bool(false)
bool(true)
int(0)
int(0)
```
But I expected this output instead (the second protected stays false and
the cycles get collected; the last two numbers are what the same script prints with the
opcache_compile_file() line removed):
```
bool(false)
Fatal error: 'break' not in the 'loop' or 'switch' context in
/tmp/bad.php on line 2
Stack trace:
#0 /path/to/test.php(8): opcache_compile_file('/tmp/bad.php')
#1 {main}
bool(false)
bool(false)
int(9999)
int(99999)
```
Measured with a slightly longer script (prints gc_status() and
memory_get_usage() after creating 100 000 self-referencing objects):
| build | opcache_compile_file() called | protected after | GC runs |
collected | memory at end |
|---|---|---|---|---|---|
| master 0b4a5a84492, debug, NTS | no | false | 10 | 100 000 | 583 136 |
| master 0b4a5a84492, debug, NTS | yes | **true** | 0 | **0** | 11 141 648 |
| master, debug, opcache.enable_cli=0 | yes (returns false without compiling) | false |
10 | 100 000 | 635 656 |
| PHP-8.4 81fd6105bc6, release + ASAN, NTS | no | false | 10 | 100 000 | 414 792 |
| PHP-8.4 81fd6105bc6, release + ASAN, NTS | yes | **true** | 0 | **0** | 6 942 856 |
| PHP-8.3 9c6ad6caeeb, release, NTS | no | false | 10 | 100 000 | 471 080 |
| PHP-8.3 9c6ad6caeeb, release, NTS | yes | **true** | 0 | **0** | 7 023 624 |
Where it happens: ext/opcache/zend_accelerator_module.c,
ZEND_FUNCTION(opcache_compile_file): the zend_try { persistent_compile_file(...)
} zend_catch { ... zend_error(E_WARNING, "... could not compile file %s") } branch
(non-preload path). The catch restores EG(current_execute_data) but not the GC
protection that zend_bailout() set (Zend/zend.c,
_zend_bailout() → gc_protect(1)). The catch dates back to the
introduction of the function (69454d9f0f7, 2013), the gc_protect(1) in
_zend_bailout() to f844d40fdc3 (2018, PHP 7.3.0), so presumably every version since 7.3
is affected.
The same class of problem is reported for ext/soap in #10026 (SoapClient converts fatal errors into
SoapFault after a bailout). I found this second instance while auditing the
zend_catch sites that continue execution after a bailout.
Possible directions (not a proposal of one in particular):
- report compile failures from opcache_compile_file() without a bailout, if the
compiler path allows it;
- or document that after a fatal compile error the rest of the request runs without the cycle
collector.
### PHP Version
```
PHP 8.7.0-dev (cli) (NTS DEBUG), master 0b4a5a84492
PHP 8.4.28-dev (cli) (NTS), PHP-8.4 81fd6105bc6
PHP 8.3.36-dev (cli) (NTS), PHP-8.3 9c6ad6caeeb
```
### Operating System
Ubuntu 24.04, x86_64