[php-src] Issue #22567: Windows ZTS CLI SAPI should refresh its TSRMLS cache during request activation
[email protected] (matyhtf) Fri, 3 Jul 2026 03:47:56 +0000
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/22567
Author: matyhtf
### 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:
```c
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.
## 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:
```c
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`:
```c
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
```plain
PHP 8.4.12
```
### Operating System
Windows 10 x86-64