Commit 7249e7e424e for php.net
commit 7249e7e424ea73375e98975b41dbb2ae9594e2e3
Author: Florian Engelhardt <florian.engelhardt@datadoghq.com>
Date: Fri Aug 14 10:21:47 2026 +0200
Fix OPcache memory protection race under ZTS (#23081)
diff --git a/ext/opcache/ZendAccelerator.h b/ext/opcache/ZendAccelerator.h
index 486074ef001..991df4efd3c 100644
--- a/ext/opcache/ZendAccelerator.h
+++ b/ext/opcache/ZendAccelerator.h
@@ -199,6 +199,9 @@ typedef struct _zend_accel_globals {
bool counted; /* the process uses shared memory */
bool enabled;
bool locked; /* thread obtained exclusive lock */
+#ifdef ZTS
+ uint32_t unprotect_depth;
+#endif
bool accelerator_enabled; /* accelerator enabled for current request */
bool pcre_reseted;
zend_accel_directives accel_directives;
diff --git a/ext/opcache/zend_shared_alloc.c b/ext/opcache/zend_shared_alloc.c
index 8516493dd87..7589cccbe55 100644
--- a/ext/opcache/zend_shared_alloc.c
+++ b/ext/opcache/zend_shared_alloc.c
@@ -55,6 +55,11 @@ static const char *g_shared_model;
/* pointer to globals allocated in SHM and shared across processes */
ZEND_EXT_API zend_smm_shared_globals *smm_shared_globals;
+#ifdef ZTS
+static MUTEX_T zts_protect_lock;
+static uint32_t zts_unprotected_threads;
+#endif
+
#ifndef ZEND_WIN32
#ifdef ZTS
static MUTEX_T zts_lock;
@@ -184,6 +189,11 @@ int zend_shared_alloc_startup(size_t requested_size, size_t reserved_size)
int res = ALLOC_FAILURE;
int i;
+#ifdef ZTS
+ zts_protect_lock = tsrm_mutex_alloc();
+ zts_unprotected_threads = 0;
+#endif
+
/* shared_free must be valid before we call zend_shared_alloc()
* - make it temporarily point to a local variable
*/
@@ -338,6 +348,9 @@ void zend_shared_alloc_shutdown(void)
tsrm_mutex_free(zts_lock);
# endif
#endif
+#ifdef ZTS
+ tsrm_mutex_free(zts_protect_lock);
+#endif
}
static size_t zend_shared_alloc_get_largest_free_block(void)
@@ -625,25 +638,37 @@ const char *zend_accel_get_shared_model(void)
void zend_accel_shared_protect(bool protected)
{
-#ifdef HAVE_MPROTECT
+#if defined(HAVE_MPROTECT) || defined(ZEND_WIN32)
int i;
if (!smm_shared_globals) {
return;
}
+# ifdef ZTS
+ /* Memory protection is process-wide, so overlapping writers must be tracked across threads. */
+ tsrm_mutex_lock(zts_protect_lock);
+ if (protected) {
+ if (ZCG(unprotect_depth) && --ZCG(unprotect_depth) == 0) {
+ ZEND_ASSERT(zts_unprotected_threads > 0);
+ zts_unprotected_threads--;
+ }
+ if (zts_unprotected_threads) {
+ tsrm_mutex_unlock(zts_protect_lock);
+ return;
+ }
+ } else if (ZCG(unprotect_depth)++ == 0) {
+ zts_unprotected_threads++;
+ }
+# endif
+
+# ifdef HAVE_MPROTECT
const int mode = protected ? PROT_READ : PROT_READ|PROT_WRITE;
for (i = 0; i < ZSMMG(shared_segments_count); i++) {
mprotect(ZSMMG(shared_segments)[i]->p, ZSMMG(shared_segments)[i]->end, mode);
}
-#elif defined(ZEND_WIN32)
- int i;
-
- if (!smm_shared_globals) {
- return;
- }
-
+# elif defined(ZEND_WIN32)
const int mode = protected ? PAGE_READONLY : PAGE_READWRITE;
for (i = 0; i < ZSMMG(shared_segments_count); i++) {
@@ -652,6 +677,11 @@ void zend_accel_shared_protect(bool protected)
zend_accel_error_noreturn(ACCEL_LOG_ERROR, "Failed to protect memory");
}
}
+# endif
+
+# ifdef ZTS
+ tsrm_mutex_unlock(zts_protect_lock);
+# endif
#endif
}