Description
Summary
On Windows ZTS builds, php.exe, php8ts.dll, and extension DLLs each have their own image-local static TSRMLS cache when ZEND_ENABLE_STATIC_TSRMLS_CACHE=1 is enabled.
The CLI SAPI defines its own cache in sapi/cli/php_cli.c through ZEND_TSRMLS_CACHE_DEFINE(), but it does not currently provide an activate callback. When PHP request startup is executed from a non-main native thread, php8ts.dll and the extension DLL can update their own cache for that thread, while the CLI executable image cache may remain unset or stale.
This can crash in CLI SAPI code that uses SG() from the worker thread. One observed crash point is:
static int sapi_cli_deactivate(void)
{
fflush(stdout);
if (SG(request_info).argv0) {
free(SG(request_info).argv0);
SG(request_info).argv0 = NULL;
}
return SUCCESS;
}
The crash happens while evaluating SG(request_info).argv0, before free(), because the CLI image-local TSRMLS cache was not refreshed for the current thread.
https://github.com/swoole/swoole-src/blob/master/ext-src/swoole_thread.cc#L560
Environment
- Platform: Windows x64
- Build: ZTS, Debug
- PHP: 8.4.x
- Compiler: Visual C++ 2022
- Static TSRMLS cache: enabled through
ZEND_ENABLE_STATIC_TSRMLS_CACHE=1
- Reproducer context: a PHP extension creates a native thread and runs a PHP request in that thread
Why this is Windows-specific
On Windows PE/COFF, each executable or DLL image can own a separate copy of static data. With static TSRMLS cache enabled:
php8ts.dll has its own cache pointer.
- An extension DLL has its own cache pointer.
php.exe also has its own cache pointer.
An extension can call ZEND_TSRMLS_CACHE_UPDATE() for its own DLL after creating a thread. Core code in php8ts.dll also works with its own cache. However, CLI SAPI code compiled into php.exe has a separate static cache and needs to refresh it before using SG(), EG(), or PG() in that thread.
Proposed fix
Add a CLI SAPI activate callback and refresh the CLI executable image cache there on Windows ZTS builds:
static int sapi_cli_activate(void)
{
#if defined(PHP_WIN32) && defined(ZTS)
ZEND_TSRMLS_CACHE_UPDATE();
#endif
return SUCCESS;
}
Then register it in cli_sapi_module:
php_cli_startup, /* startup */
php_module_shutdown_wrapper, /* shutdown */
sapi_cli_activate, /* activate */
sapi_cli_deactivate, /* deactivate */
sapi_activate() already calls sapi_module.activate during request startup, which makes this the natural location to refresh SAPI-local thread cache state before later CLI SAPI callbacks run in the same thread.
Expected result
CLI SAPI code can safely access SG(), EG(), and PG() from a request running in a non-main native thread on Windows ZTS builds, provided that request startup has completed.
Notes
This issue is not about sharing request globals between modules. The actual TSRM resource for the thread is shared. The problem is that Windows image-local static cache pointers must be updated separately for each PE image that uses the static TSRMLS cache fast path.
PHP Version
Operating System
Windows 10 x86-64
Description
Summary
On Windows ZTS builds,
php.exe,php8ts.dll, and extension DLLs each have their own image-local static TSRMLS cache whenZEND_ENABLE_STATIC_TSRMLS_CACHE=1is enabled.The CLI SAPI defines its own cache in
sapi/cli/php_cli.cthroughZEND_TSRMLS_CACHE_DEFINE(), but it does not currently provide anactivatecallback. When PHP request startup is executed from a non-main native thread,php8ts.dlland the extension DLL can update their own cache for that thread, while the CLI executable image cache may remain unset or stale.This can crash in CLI SAPI code that uses
SG()from the worker thread. One observed crash point is:The crash happens while evaluating
SG(request_info).argv0, beforefree(), because the CLI image-local TSRMLS cache was not refreshed for the current thread.https://github.com/swoole/swoole-src/blob/master/ext-src/swoole_thread.cc#L560
Environment
ZEND_ENABLE_STATIC_TSRMLS_CACHE=1Why this is Windows-specific
On Windows PE/COFF, each executable or DLL image can own a separate copy of static data. With static TSRMLS cache enabled:
php8ts.dllhas its own cache pointer.php.exealso has its own cache pointer.An extension can call
ZEND_TSRMLS_CACHE_UPDATE()for its own DLL after creating a thread. Core code inphp8ts.dllalso works with its own cache. However, CLI SAPI code compiled intophp.exehas a separate static cache and needs to refresh it before usingSG(),EG(), orPG()in that thread.Proposed fix
Add a CLI SAPI
activatecallback and refresh the CLI executable image cache there on Windows ZTS builds:Then register it in
cli_sapi_module:sapi_activate()already callssapi_module.activateduring request startup, which makes this the natural location to refresh SAPI-local thread cache state before later CLI SAPI callbacks run in the same thread.Expected result
CLI SAPI code can safely access
SG(),EG(), andPG()from a request running in a non-main native thread on Windows ZTS builds, provided that request startup has completed.Notes
This issue is not about sharing request globals between modules. The actual TSRM resource for the thread is shared. The problem is that Windows image-local static cache pointers must be updated separately for each PE image that uses the static TSRMLS cache fast path.
PHP Version
Operating System
Windows 10 x86-64