[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: 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.

« previous php.bugs (#252995) next »