Skip to content

Fix GH-24577: CE-cache flag on interned class name visible before map_ptr offset - #24585

Open
prateekbhujel wants to merge 2 commits into
php:PHP-8.4from
prateekbhujel:prateek/fix-gh24577-ce-cache-race
Open

prateekbhujel wants to merge 2 commits into
php:PHP-8.4from
prateekbhujel:prateek/fix-gh24577-ce-cache-race

Conversation

@prateekbhujel

@prateekbhujel prateekbhujel commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes GH-24577.

Problem

When OPcache assigns a class cache slot (map_ptr offset) to an interned class name string in shared memory, it stores the offset in type_name->gc.refcount and marks the string with the IS_STR_CLASS_NAME_MAP_PTR flag.

Without memory barriers, the compiler or CPU can reorder these operations (or make the flag visible to concurrent worker processes before the offset is written). In Zend/zend.c, the flag was also being set before the refcount in source order.

If another worker process checks ZSTR_HAS_CE_CACHE() and sees IS_STR_CLASS_NAME_MAP_PTR while gc.refcount is still 2 (the default refcount for SHM interned strings), it treats 2 as a valid cache offset and dereferences unaligned memory at map_ptr_base + 2, causing a SIGSEGV.

Solution

Use release and acquire memory barriers to ensure the offset is always written and visible before the flag:

  1. Writers:
    • In zend_persist.c (zend_accel_pending_ce_cache_commit), ZendAccelerator.c (accel_new_interned_string), and zend.c (zend_alloc_ce_cache), write GC_SET_REFCOUNT first, issue ZEND_ATOMIC_FENCE_RELEASE(), and then set IS_STR_CLASS_NAME_MAP_PTR.
    • In ZendAccelerator.c (accel_interned_strings_restore_state), clear the flag first, issue ZEND_ATOMIC_FENCE_RELEASE(), and then reset gc.refcount to 2.
  2. Readers:
    • In ZSTR_HAS_CE_CACHE(), issue ZEND_ATOMIC_FENCE_ACQUIRE() when IS_STR_CLASS_NAME_MAP_PTR is detected, ensuring the reader always sees the published map_ptr offset.
  3. Regression Test:
    • Added ext/opcache/tests/gh24577.phpt, which pre-populates shared interned strings with refcount 2 and verifies concurrent readers and writers under load.

When OPcache persists a script into shared memory, zend_accel_pending_ce_cache_commit()
(and zend_alloc_ce_cache() in Zend/zend.c) writes the allocated map_ptr offset
into type_name->gc.refcount and sets IS_STR_CLASS_NAME_MAP_PTR on the shared
interned string. Because these are plain non-atomic stores to adjacent struct
fields, the compiler or weakly ordered CPU can make IS_STR_CLASS_NAME_MAP_PTR
visible to concurrent worker processes before the map_ptr offset overwrites the
initial SHM interned string refcount of 2.

Previously, ZSTR_VALID_CE_CACHE() only checked
(GC_REFCOUNT(s) - 1) / sizeof(void *) < CG(map_ptr_last), which accepted
refcount 2 ((2 - 1) / 8 == 0 < CG(map_ptr_last)) and dereferenced an unaligned
address at map_ptr_base + 2.

Require GC_REFCOUNT(s) > 2 in ZSTR_VALID_CE_CACHE() and ZSTR_SET_CE_CACHE_EX(),
and order GC_SET_REFCOUNT before GC_ADD_FLAGS in zend_alloc_ce_cache().
@prateekbhujel
prateekbhujel force-pushed the prateek/fix-gh24577-ce-cache-race branch from 5c21092 to 4f811bd Compare October 24, 2026 03:47
@prateekbhujel
prateekbhujel changed the base branch from master to PHP-8.4 October 24, 2026 03:47

@ndossche ndossche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if a solution using memory barriers is feasible, we should do that instead of the hardening. Nothing prevents the compiler or CPU from reordering the assignments again.
This will require adding barriers in a few places as there are multiple places where GC_ADD_FLAGS(type_name, IS_STR_CLASS_NAME_MAP_PTR); is done

Replace hardening check with explicit release and acquire memory barriers
to prevent compiler and CPU reordering between GC_SET_REFCOUNT and
GC_ADD_FLAGS(..., IS_STR_CLASS_NAME_MAP_PTR) across all interned class cache
writers and readers.
@prateekbhujel

Copy link
Copy Markdown
Contributor Author

Agreed, memory barriers are definitely the right approach here instead of the refcount guard.

I've reverted the ZSTR_VALID_CE_CACHE workaround and switched to release/acquire fences with ZEND_ATOMIC_FENCE_*:

  • ZSTR_HAS_CE_CACHE() now issues an acquire fence when IS_STR_CLASS_NAME_MAP_PTR is set.
  • Added release fences between GC_SET_REFCOUNT and IS_STR_CLASS_NAME_MAP_PTR across all writer sites (zend_persist.c, ZendAccelerator.c, and zend_alloc_ce_cache in zend.c, where the write order was also flipped so the refcount store happens first).

Verified the emitted store ordering in disassembly and ext/opcache/tests/gh24577.phpt is passing.

@ndossche

Copy link
Copy Markdown
Member

Can you update the PR description to accurately reflect the current version of the PR's code please?

@prateekbhujel

Copy link
Copy Markdown
Contributor Author

Done, updated the description to reflect the memory barrier changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OPcache: CE-cache flag on a shared interned class name becomes visible before its map_ptr offset; FPM child SIGSEGV on a cold cache

2 participants