[php-src] Issue #24319: opcache: `zend_accel_hash_update()` links new entries without a release barrier; lock-free lookups can see a half-initialised
entry on aarch64
| From: | iloveleon19 | Date: | Wed, 14 Oct 2026 10:22:50 +0000 |
| Subject: | [php-src] Issue #24319: opcache: `zend_accel_hash_update()` links new entries without a release barrier; lock-free lookups can see a half-initialised entry on aarch64 |
||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-252995@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24319
Author: iloveleon19
Prepared with an AI assistant; I reviewed the analysis and the numbers. The text below is
AI-generated (quoted, as asked in CONTRIBUTING.md).
>
zend_accel_hash_update() fills a new
> zend_accel_hash_entry (data,
> indirect, hash_value,
> key, next) and then links it with
> accel_hash->hash_table[index] = entry, all with plain stores
> while holding the SHM lock. zend_accel_hash_find_ex() runs without
> that lock (from persistent_compile_file()), so nothing orders the
> field stores before the link for a reader on another core.
>
> **Setup**: PHP 8.2.32 ZTS, opcache on (no JIT, no preload, no file cache), FrankenPHP 1.12.4
> worker mode (one process, many threads), aarch64 (AWS Graviton), a large Laravel Octane app. Crashes
> happen within seconds of a worker reload, when every thread compiles a burst of new files. The
> GH-23637 fix (873a997d) is backported in this build.
>
> **What we observed**
> - A reader-side probe (writer code untouched) logged, twice, a bucket entry whose
> hash_value matched while
> entry->key was NULL; an immediate re-read returned the correct
> interned key. The entry was the most recently allocated slot and no opcache restart had happened.
> The unprobed build crashes at the same instruction
> (ZSTR_LEN(entry->key), fault address 0x10) in
> zend_accel_hash_find() / gQÄ÷ õ
> !KåÕªTzend_accel_hash_find_entry().
> - A core from another crash shows the reader returning ¦Öã—7~aÅ
> ¬ñüGE-entry->data of an alias entry as if it were direct: memory has
> indirect == 1, the reader took the indirect ==
> 0 branch, and persistent_script->script.filename
> was really the hash_value of the bucket it points to. The crash
> itself was later, in zend_hash_add().
>
> **Proposed fix** (PR to follow, same fence macros as GH-23637): a release fence before the link
> and before updating data/indirect of
> an existing entry; an acquire fence after loading a non-NULL bucket head in
> zend_accel_hash_find_ex() and in
> accelerator_get_scripts().
>
> **Results** (same deploy-and-reload load, no probes): without the fix, 9 crashes of this kind
> in 2607 reloads (8 with key == NULL, 1 with the stale
> indirect); with it, 0 crashes of any kind in 4368 reloads (2730 in
> the same two runs, plus a separate 1638-reload run on a newer build of the same base image, without
> a control). The mechanism is our best explanation, not something we observed directly: we saw where
> readers fail and what they read, not the writer's stores becoming visible out of order. The
> counts are small. On x86_64 (unpatched PHP 8.2.33) we ran about 1867 deploy cycles with no crash at
> reload; there was one crash during the composer step, without symbols, whose type we could not
> determine.
>
> **Still open**
> - Converting an existing direct entry to indirect changes two fields
> (data, indirect); a lock-free reader
> can see them in an inconsistent combination, and a fence cannot make that atomic. We have not seen
> this case in practice.
> - These remain plain accesses in C11 terms; pointer release/acquire would be the cleaner
> long-term form.