opcache_compile_file() on a file with a fatal compile error disables the cycle collector for the rest of the request
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
Start with the zend_catch branch of ZEND_FUNCTION(opcache_compile_file) in ext/opcache/zend_accelerator_module.c, then look at _zend_bailout() in Zend/zend.c, which calls gc_protect(1). Reproduce with the script in the issue and check that gc_status()['protected'] returns to false after the failed compile. Done means the cycle collector runs again after the catch, or the behaviour is documented and the gc_protect state is restored.
索引モデルが issue の本文から書いたものです。
説明
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
$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
- 主要言語
- C
- スター
- 40.7k
- フォーク
- 8.3k
- 平均マージ
- 2日 17時間
- マージ済み PR(30日)
- 159
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
php/php-src のほかの issue
-
Bug Status: Needs Triage
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
php/php-src#24769 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
zend_set_local_var() overwrites existing compiled variable (CV) zval without calling zval_ptr_dtor()対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンBug Status: Needs Triage
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
master: part of the mbstring bailout fix got lost in a merge対応中かも @lazerg が 5 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
Bug Status: Needs Triage
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
メンテナーはふだん 1 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
JustAResearcher/gpu-unlock-ada#98 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
メンテナーはふだん 2 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
-
bug intrinsic medium priority rt_lib
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
メンテナーはふだん 1 日以内に返信
-
[CLI] Clarify distinction between 'capture camera' and 'capture screen' in help text and outputオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
espressif/esp-vision#18 ·