From ab9d121f483ceb3c465f233f872d4d54840caaf7 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 6 Oct 2025 18:25:40 +0200 Subject: [PATCH 1/2] Fix access to uninitialized variables in preload_load() preload_load() reads EG(class_table) and EG(function_table), but these may not be initialized. Move these accesses out of preload_load(). Closes GH-20081 --- NEWS | 4 ++++ ext/opcache/ZendAccelerator.c | 15 ++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index b4b0521051f1..a897bf0b733c 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,10 @@ PHP NEWS . Fixed bug GH-19974 (fpm_status_export_to_zval segfault for parallel execution). (Jakub Zelenka, txuna) +- Opcache: + . Fixed bug GH-20081 (access to uninitialized vars in preload_load()). + (Arnaud) + - Random: . Fix Randomizer::__serialize() w.r.t. INDIRECTs. (nielsdos) diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 1b0101dbfd6c..f8564999eef6 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -4345,15 +4345,6 @@ static void preload_load(void) } } - if (EG(zend_constants)) { - EG(persistent_constants_count) = EG(zend_constants)->nNumUsed; - } - if (EG(function_table)) { - EG(persistent_functions_count) = EG(function_table)->nNumUsed; - } - if (EG(class_table)) { - EG(persistent_classes_count) = EG(class_table)->nNumUsed; - } if (CG(map_ptr_last) != ZCSG(map_ptr_last)) { size_t old_map_ptr_last = CG(map_ptr_last); CG(map_ptr_last) = ZCSG(map_ptr_last); @@ -4589,6 +4580,12 @@ static zend_result accel_preload(const char *config, bool in_child) preload_load(); + /* Update persistent counts, as shutdown will discard anything past + * that, and these tables are aliases to global ones at this point. */ + EG(persistent_functions_count) = EG(function_table)->nNumUsed; + EG(persistent_classes_count) = EG(class_table)->nNumUsed; + EG(persistent_constants_count) = EG(zend_constants)->nNumUsed; + /* Store individual scripts with unlinked classes */ HANDLE_BLOCK_INTERRUPTIONS(); SHM_UNPROTECT(); From 8f488f9556dd869e4e55bb876bf75af5e1493e20 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 6 Oct 2025 17:51:39 +0200 Subject: [PATCH 2/2] Mask USR1/HUP while waiting for preloading Preloading may fork and wait for the child to exit. In case waitpid() is interrupted, the parent exits with a fatal error. This is fine when the syscall is interrupted by a signal whose disposition is set to terminate the process, but not otherwise. In the apache2handler SAPI, the parent is the control process. Restarting apache2 is done by sending SIGUSR1 or SIGHUP to the control process. Doing that during the waitpid() syscall would cause the control process to exit instead. Block the USR1 and HUP signals from being delivered during the syscall when running the apache2handler SAPI, as these are not supposed to terminate the process. FPM is fine as it masks relevant signals during php startup. Fixes GH-20051 Closes GH-20079 Co-authored-by: mycozyhom --- ext/opcache/ZendAccelerator.c | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 37ebc6228765..e6a2b90e8fff 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -5090,13 +5090,45 @@ static zend_result accel_finish_startup(void) exit(ret == SUCCESS ? 0 : 1); } else { /* parent */ - int status; +# ifdef HAVE_SIGPROCMASK + /* Interrupting the waitpid() call below with a signal would cause the + * process to exit. This is fine when the signal disposition is set to + * terminate the process, but not otherwise. + * When running the apache2handler, preloading is performed in the + * control process. SIGUSR1 and SIGHUP are used to tell the control + * process to restart children. Exiting when these signals are received + * would unexpectedly shutdown the server instead of restarting it. + * Block the USR1 and HUP signals from being delivered during the + * syscall when running the apache2handler SAPI, as these are not + * supposed to terminate the process. See GH-20051. */ + bool is_apache2handler = strcmp(sapi_module.name, "apache2handler") == 0; + sigset_t set, oldset; + if (is_apache2handler) { + if (sigemptyset(&set) + || sigaddset(&set, SIGUSR1) + || sigaddset(&set, SIGHUP)) { + ZEND_UNREACHABLE(); + } + if (sigprocmask(SIG_BLOCK, &set, &oldset)) { + ZEND_UNREACHABLE(); + } + } +# endif + int status; if (waitpid(pid, &status, 0) < 0) { zend_shared_alloc_unlock(); zend_accel_error_noreturn(ACCEL_LOG_FATAL, "Preloading failed to waitpid(%d)", pid); } +# ifdef HAVE_SIGPROCMASK + if (is_apache2handler) { + if (sigprocmask(SIG_SETMASK, &oldset, NULL)) { + ZEND_UNREACHABLE(); + } + } +# endif + if (ZCSG(preload_script)) { preload_load(zend_map_ptr_static_last); }