PHP-FPM process management woes

From: Date: Mon, 20 Dec 2021 18:41:15 +0000
Subject: PHP-FPM process management woes
Groups: php.internals 
Request: Send a blank email to internals+get-116684@lists.php.net to get a copy of this message
Hi! In the last few days I have been investigating Xdebug bug #2051: "Segfault on fatal error when setting xdebug.mode via php-fpm pool config" (https://bugs.xdebug.org/view.php?id=2051). I have now tracked this down to an unexpectedness in how PHP-FPM handles extension/module initialisation. Literature since at least 2006 (Sara's Extending and Embedding PHP book), as well as numerous presentations that I have seen and given, and including the online PHP Internals Book (https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html), always expected that the MINIT (and MSHUTDOWN) functions are called once per worker process. However, PHP-FPM does not do that. It only calls extension's MINITs once in the main control process, but it *does* call an MSHUTDOWN for each worker process. In the past, this has already caused an issue where I couldn't create a monitoring thread in MINIT, and then wait for it to end in MSHUTDOWN, as MSHUTDOWN would wait on the same thread in each worker process, although only one was created in MINIT. The way how PHP-FPM handles this breaks the generally assumed "one MINIT, one MSHUTDOWN call" per process approach. In particular, in the case of Xdebug bug #2051 it creates a problem with the following set-up: 1. php.ini has a xdebug.mode=off 2. the pool configuration has a php_admin_value[xdebug.mode] = debug directive When PHP-FPM starts up, it calls Xdebug's MINIT, which checks the value of the xdebug.mode INI setting. If it set to off it does **nothing**, including setting up handles such as the zend_error_cb handlers in xdebug_base_minit:: PHP_MINIT_FUNCTION(xdebug) { … if (XDEBUG_MODE_IS_OFF()) { return SUCCESS; } … xdebug_base_minit(INIT_FUNC_ARGS_PASSTHRU); } MINIT sets the xdebug_old_error_cb and xdebug_new_error_cb handlers to something else than NULL:: void xdebug_base_minit(INIT_FUNC_ARGS) { /* Record Zend and Xdebug error callbacks, the actual setting is done in * base on RINIT */ xdebug_old_error_cb = zend_error_cb; xdebug_new_error_cb = xdebug_error_cb; In each RINIT, which is called once per request, it also checks the INI setting, and if set, uses some of the handlers that were set-up in MINIT. Please note that PHP-FPM has changed the value of xdebug.mode in this worker process to debug (ie, not off), (simplified):: PHP_RINIT_FUNCTION(xdebug) { … if (XDEBUG_MODE_IS_OFF()) { return SUCCESS; } … xdebug_base_rinit(); } void xdebug_base_rinit() { xdebug_base_use_xdebug_error_cb(); } void xdebug_base_use_xdebug_error_cb(void) { zend_error_cb = xdebug_new_error_cb; } When now an error occurs, PHP calls zend_error_cb, but this is now unset (NULL) as when MINIT was called, the xdebug.mode setting was still set to off, but during RINIT it was changed by PHP-FPM to debug. Xdebug does not expect this setting to change, especially because it is marked as PHP_INI_SYSTEM. In my opinion this is a bug (or two) in PHP-FPM, as: - it does not follow the expected one MINIT/MSHUTDOWN cycle, that for example Apache (1 and 2) use, and which is documented in books and online material (and my memory) - it disrespects PHP_INI_SYSTEM My suggestion for a fix would be to emulate what Apache always did: One MINIT/MSHUTDOWN in the main control process (I think it needed that to be able to implement php_admin_value and php_value), and then additionally also in each worker process. I did have a brief look at implementing this, but haven't managed to get it to work yet — mainly because I am unfamiliar with the PHP-FPM code at the moment. cheers, Derick -- PHP 7.4 Release Manager Host of PHP Internals News: https://phpinternals.news Like Xdebug? Consider supporting me: https://xdebug.org/support https://derickrethans.nl | https://xdebug.org | https://dram.io twitter: @derickr and @xdebug

« previous php.internals (#116684) next »