Skip to content

Windows ZTS CLI SAPI should refresh its TSRMLS cache during request activation #22567

Description

@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:

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

PHP 8.4.12

Operating System

Windows 10 x86-64

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions