Skip to content

Commit 430f1be

Browse files
committed
Fix GH-8739: OPcache restart corrupts SHM in threaded SAPIs (ZTS)
Before executing a scheduled restart (OOM, hash overflow or opcache_reset()), accel_is_inactive() checks that no request is still reading shared memory. On POSIX this check is implemented with fcntl() record locks: readers take an F_RDLCK in accel_activate_add() and the restarting thread probes for it with F_GETLK. POSIX record locks are per-process: a process never conflicts with its own locks. In threaded SAPIs (FrankenPHP, mod_php with a threaded MPM), all in-flight requests are threads of one process, so F_GETLK never reports a conflict and accel_is_inactive() always returns true. The restart then runs zend_accel_hash_clean() and accel_interned_strings_restore_state() while other threads execute op_arrays and hold interned string pointers in the wiped memory, corrupting the heap ("zend_mm_heap corrupted", SIGSEGV/SIGABRT). Fix it the way ZEND_WIN32 already solved the same problem for threaded SAPIs on Windows: track in-flight requests with an atomic counter in SHM. Requests register in RINIT (only while the accelerator is enabled, so registrations stop as soon as a restart is pending and the counter drains within the duration of the longest in-flight request) and deregister in accel_post_deactivate(). accel_is_inactive() refuses to restart while the counter is non-zero. The counter is only compiled in for ZTS non-Windows builds; NTS process-based SAPIs (php-fpm, mod_php prefork) keep using fcntl() locks, whose kernel-side cleanup on process death they rely on. Verified on a production Symfony workload under FrankenPHP (256 threads, ~120 concurrent requests): repeated opcache_reset() calls and wasted-memory-exhaustion restarts, both previously crashing within seconds, now complete safely after draining readers.
1 parent 61a1b9a commit 430f1be

3 files changed

Lines changed: 68 additions & 0 deletions

File tree

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ PHP NEWS
6262
- Opcache:
6363
. Fixed tracing JIT crash when a VM interrupt is handled during an observed
6464
user function call. (Levi Morrison)
65+
. Fixed bug GH-8739 (OPcache restart corrupts shared memory under threaded
66+
SAPIs: fcntl()-based reader tracking cannot see threads of the same
67+
process). (Yoan Arnaudov)
6568
. Fixed bug GH-22004 (Assertion failure at ext/opcache/jit/zend_jit_trace.c).
6669
(Arnaud)
6770

ext/opcache/ZendAccelerator.c

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,38 @@ static inline void accel_unlock_all(void)
402402
#endif
403403
}
404404

405+
#if defined(ZTS) && !defined(ZEND_WIN32)
406+
/* In threaded SAPIs (FrankenPHP, mod_php with a threaded MPM, ...) requests
407+
* run as threads of one process. The fcntl() record locks used by
408+
* accel_activate_add()/accel_is_inactive() are per-process: a process never
409+
* conflicts with its own locks, so in-flight readers running as threads are
410+
* invisible to accel_is_inactive(). It then wrongly reports the cache as
411+
* inactive and a scheduled restart wipes SHM (hash table + interned strings)
412+
* under running requests, corrupting the heap (GH-8739, GH-14471, GH-18517).
413+
*
414+
* Mirror the approach ZEND_WIN32 already uses for the same reason (threaded
415+
* SAPIs): track in-flight requests with an atomic counter in SHM and have
416+
* accel_is_inactive() refuse to restart while it is non-zero. Registration
417+
* is gated on ZCG(accelerator_enabled), so once a restart is pending (the
418+
* accelerator is disabled) new requests no longer register and the counter
419+
* drains within the duration of the longest in-flight request. */
420+
static zend_always_inline void accel_ts_request_enter(void)
421+
{
422+
if (!ZCG(ts_reader_registered)) {
423+
__atomic_add_fetch(&ZCSG(ts_active_requests), 1, __ATOMIC_SEQ_CST);
424+
ZCG(ts_reader_registered) = true;
425+
}
426+
}
427+
428+
static zend_always_inline void accel_ts_request_leave(void)
429+
{
430+
if (ZCG(ts_reader_registered)) {
431+
__atomic_sub_fetch(&ZCSG(ts_active_requests), 1, __ATOMIC_SEQ_CST);
432+
ZCG(ts_reader_registered) = false;
433+
}
434+
}
435+
#endif
436+
405437
/* Interned strings support */
406438

407439
/* O+ disables creation of interned strings by regular PHP compiler, instead,
@@ -894,6 +926,13 @@ static inline void kill_all_lockers(struct flock *mem_usage_check)
894926

895927
static inline bool accel_is_inactive(void)
896928
{
929+
#if defined(ZTS) && !defined(ZEND_WIN32)
930+
/* Threads of this process may still be inside requests registered as
931+
* SHM readers; the fcntl() probe below cannot detect them. */
932+
if (__atomic_load_n(&ZCSG(ts_active_requests), __ATOMIC_SEQ_CST) != 0) {
933+
return false;
934+
}
935+
#endif
897936
#ifdef ZEND_WIN32
898937
/* on Windows, we don't need kill_all_lockers() because SAPIs
899938
that work on Windows don't manage child processes (and we
@@ -2742,6 +2781,17 @@ ZEND_RINIT_FUNCTION(zend_accelerator)
27422781

27432782
ZCG(accelerator_enabled) = ZCSG(accelerator_enabled);
27442783

2784+
#if defined(ZTS) && !defined(ZEND_WIN32)
2785+
if (ZCG(accelerator_enabled)) {
2786+
accel_ts_request_enter();
2787+
if (UNEXPECTED(ZCSG(restart_in_progress))) {
2788+
/* lost the race against a restart that passed accel_is_inactive()
2789+
* before our registration became visible; run uncached */
2790+
ZCG(accelerator_enabled) = false;
2791+
}
2792+
}
2793+
#endif
2794+
27452795
SHM_PROTECT();
27462796
HANDLE_UNBLOCK_INTERRUPTIONS();
27472797

@@ -2793,6 +2843,14 @@ zend_result accel_post_deactivate(void)
27932843
accel_unlock_all();
27942844
ZCG(counted) = false;
27952845

2846+
#if defined(ZTS) && !defined(ZEND_WIN32)
2847+
if (!file_cache_only && accel_shared_globals) {
2848+
SHM_UNPROTECT();
2849+
accel_ts_request_leave();
2850+
SHM_PROTECT();
2851+
}
2852+
#endif
2853+
27962854
return SUCCESS;
27972855
}
27982856

ext/opcache/ZendAccelerator.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ typedef struct _zend_accel_directives {
197197

198198
typedef struct _zend_accel_globals {
199199
bool counted; /* the process uses shared memory */
200+
bool ts_reader_registered; /* this request is counted in ZCSG(ts_active_requests) (ZTS only) */
200201
bool enabled;
201202
bool locked; /* thread obtained exclusive lock */
202203
bool accelerator_enabled; /* accelerator enabled for current request */
@@ -267,6 +268,12 @@ typedef struct _zend_accel_shared_globals {
267268
#ifdef ZEND_WIN32
268269
LONGLONG mem_usage;
269270
LONGLONG restart_in;
271+
#elif defined(ZTS)
272+
/* In-flight requests registered as SHM readers. POSIX fcntl() record
273+
* locks cannot track readers that are threads of one process (a process
274+
* never conflicts with its own locks), so threaded SAPIs need an
275+
* explicit counter — mirroring what ZEND_WIN32 does with mem_usage. */
276+
uint32_t ts_active_requests;
270277
#endif
271278
bool restart_in_progress;
272279
bool jit_counters_stopped;

0 commit comments

Comments
 (0)