Skip to content

Commit 4ebe8ad

Browse files
committed
fix: add rwlock for implicit opcache restarts (OOM/hash overflow)
PR php#2073 drains all threads for explicit opcache_reset() calls, but WordPress triggers implicit restarts via opcache_invalidate() filling opcache memory → zend_accel_schedule_restart() → restart_pending → the actual reset runs during the next php_request_startup(). That path bypasses the opcache_reset() override and races on shared memory. Add a pthread_rwlock around the request lifecycle: - Normal requests: read lock (concurrent, single atomic CAS) - When zend_accel_schedule_restart_hook fires: set an atomic flag - Next php_request_startup(): sees flag → write lock (exclusive), blocks until all current requests complete, resets safely - After startup: unlock, all threads resume Combined with PR php#2073, this covers both explicit opcache_reset() (thread drain) and implicit OOM/hash-overflow restarts (rwlock). Testing: without this patch, crashes every ~10 min on a WordPress multisite. With PR php#2073 alone, same crash frequency. With the rwlock from our PR php#2349 alone, crashes reduced to every 6-12 hours (worker threads not covered). Combined fix expected to eliminate both paths.
1 parent d5ed5da commit 4ebe8ad

1 file changed

Lines changed: 50 additions & 0 deletions

File tree

frankenphp.c

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "frankenphp.h"
22
#include <SAPI.h>
3+
#include <Zend/zend.h>
34
#include <Zend/zend_alloc.h>
45
#include <Zend/zend_exceptions.h>
56
#include <Zend/zend_interfaces.h>
@@ -83,6 +84,33 @@ bool original_user_abort_setting = 0;
8384
frankenphp_interned_strings_t frankenphp_strings = {0};
8485
HashTable *main_thread_env = NULL;
8586

87+
/* Implicit opcache restart safety — prevents zend_mm_heap corrupted
88+
* crashes from the implicit restart path (OOM, hash overflow).
89+
*
90+
* PR #2073 handles explicit opcache_reset() calls by draining all
91+
* threads through the Go state machine. However, WordPress triggers
92+
* implicit restarts via opcache_invalidate() filling opcache memory,
93+
* which fires zend_accel_schedule_restart() → restart_pending → the
94+
* actual reset during the next php_request_startup(). That path is
95+
* not covered by the opcache_reset() override.
96+
*
97+
* Fix: pthread_rwlock around the request lifecycle. Normal requests
98+
* take a read lock (concurrent). When the restart hook fires, it sets
99+
* a flag; the next php_request_startup() acquires a write lock,
100+
* blocking until all other threads' requests complete, then performs
101+
* the reset exclusively. */
102+
static pthread_rwlock_t frankenphp_opcache_rwlock =
103+
PTHREAD_RWLOCK_INITIALIZER;
104+
static volatile int frankenphp_opcache_restart_pending = 0;
105+
106+
#if defined(ZTS) && PHP_VERSION_ID >= 80400
107+
static void frankenphp_opcache_restart_hook(int reason) {
108+
(void)reason;
109+
__atomic_store_n(&frankenphp_opcache_restart_pending, 1,
110+
__ATOMIC_RELEASE);
111+
}
112+
#endif
113+
86114
__thread uintptr_t thread_index;
87115
__thread bool is_worker_thread = false;
88116
__thread HashTable *sandboxed_env = NULL;
@@ -1111,7 +1139,20 @@ static void *php_thread(void *arg) {
11111139

11121140
frankenphp_update_request_context();
11131141

1142+
/* Implicit opcache restart: if scheduled, take exclusive access
1143+
* so the reset in php_request_startup() runs while no other
1144+
* thread touches shared memory. Otherwise read lock. */
1145+
if (__atomic_load_n(&frankenphp_opcache_restart_pending,
1146+
__ATOMIC_ACQUIRE)) {
1147+
pthread_rwlock_wrlock(&frankenphp_opcache_rwlock);
1148+
__atomic_store_n(&frankenphp_opcache_restart_pending, 0,
1149+
__ATOMIC_RELEASE);
1150+
} else {
1151+
pthread_rwlock_rdlock(&frankenphp_opcache_rwlock);
1152+
}
1153+
11141154
if (UNEXPECTED(php_request_startup() == FAILURE)) {
1155+
pthread_rwlock_unlock(&frankenphp_opcache_rwlock);
11151156
/* Request startup failed, bail out to zend_catch */
11161157
frankenphp_log_message("Request startup failed, thread is unhealthy",
11171158
LOG_ERR);
@@ -1143,6 +1184,7 @@ static void *php_thread(void *arg) {
11431184

11441185
/* shutdown the request, potential bailout to zend_catch */
11451186
php_request_shutdown((void *)0);
1187+
pthread_rwlock_unlock(&frankenphp_opcache_rwlock);
11461188
frankenphp_free_request_context();
11471189
go_frankenphp_after_script_execution(thread_index, EG(exit_status));
11481190
}
@@ -1158,6 +1200,7 @@ static void *php_thread(void *arg) {
11581200
zend_catch {}
11591201
zend_end_try();
11601202
}
1203+
pthread_rwlock_unlock(&frankenphp_opcache_rwlock);
11611204

11621205
/* Log the last error message, it must be cleared to prevent a crash when
11631206
* freeing execution globals */
@@ -1277,6 +1320,13 @@ static void *php_main(void *arg) {
12771320

12781321
frankenphp_sapi_module.startup(&frankenphp_sapi_module);
12791322

1323+
#if defined(ZTS) && PHP_VERSION_ID >= 80400
1324+
/* Hook implicit opcache restarts (OOM, hash overflow). The hook
1325+
* sets a flag; the next php_request_startup() acquires exclusive
1326+
* access via the rwlock so the reset runs safely. */
1327+
zend_accel_schedule_restart_hook = frankenphp_opcache_restart_hook;
1328+
#endif
1329+
12801330
/* check if a default filter is set in php.ini and only filter if
12811331
* it is, this is deprecated and will be removed in PHP 9 */
12821332
char *default_filter;

0 commit comments

Comments
 (0)