From 603c4615217068fbe702b23e227b7e3c13211125 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 14 Jul 2026 13:23:33 +0700 Subject: [PATCH 01/38] perf: embed executor and compiler globals in the __thread TLS cache struct --- TSRM/TSRM.c | 5 +++- TSRM/TSRM.h | 33 ++++++++++++++++------- Zend/zend.c | 28 ++++++++++++++------ Zend/zend_globals.h | 11 ++++---- Zend/zend_globals_macros.h | 36 ++++++++++++++++++++++++-- ext/opcache/jit/tls/zend_jit_tls_win.c | 21 +++++---------- ext/opcache/jit/zend_jit_ir.c | 12 ++++----- main/main.c | 7 +---- sapi/apache2handler/config.m4 | 4 +++ win32/dllmain.c | 12 +++++++++ 10 files changed, 117 insertions(+), 52 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index a9f6ed5c59bb..476e8d0a18e8 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -855,7 +855,10 @@ TSRM_API void *tsrm_get_ls_cache(void) /* Returns offset of tsrm_ls_cache slot from Thread Control Block address */ TSRM_API size_t tsrm_get_ls_cache_tcb_offset(void) {/*{{{*/ -#if defined(__APPLE__) && defined(__x86_64__) +#if defined(TSRM_TLS_MODEL_GLOBAL_DYNAMIC) + /* No constant TCB offset under global-dynamic, can't use fast path */ + return 0; +#elif defined(__APPLE__) && defined(__x86_64__) // TODO: Implement support for fast JIT ZTS code ??? return 0; #elif defined(__x86_64__) && defined(__GNUC__) && !defined(__FreeBSD__) && \ diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index 6a72dd3c02f3..85d860cb00aa 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -167,8 +167,13 @@ TSRM_API bool tsrm_is_managed_thread(void); # define TSRM_TLS_MODEL_ATTR # define TSRM_TLS_MODEL_DEFAULT #elif defined(__PIC__) && !defined(__PIE__) -# define TSRM_TLS_MODEL_ATTR __attribute__((tls_model("initial-exec"))) -# define TSRM_TLS_MODEL_INITIAL_EXEC +# if defined(TSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC) +# define TSRM_TLS_MODEL_ATTR __attribute__((tls_model("global-dynamic"))) +# define TSRM_TLS_MODEL_GLOBAL_DYNAMIC +# else +# define TSRM_TLS_MODEL_ATTR __attribute__((tls_model("initial-exec"))) +# define TSRM_TLS_MODEL_INITIAL_EXEC +# endif #else # define TSRM_TLS_MODEL_ATTR __attribute__((tls_model("local-exec"))) # define TSRM_TLS_MODEL_LOCAL_EXEC @@ -186,17 +191,27 @@ TSRM_API bool tsrm_is_managed_thread(void); #define TSRMG_BULK_STATIC(id, type) ((type) (*((void ***) TSRMLS_CACHE))[TSRM_UNSHUFFLE_RSRC_ID(id)]) #define TSRMG_FAST_STATIC(offset, type, element) (TSRMG_FAST_BULK_STATIC(offset, type)->element) #define TSRMG_FAST_BULK_STATIC(offset, type) ((type) (((char*) TSRMLS_CACHE)+(offset))) +struct _zend_tsrm_ls_cache; +#if defined(ZEND_WIN32) && !defined(LIBZEND_EXPORTS) +/* Windows can't dllexport the TLS struct, so outside Zend each module + * keeps a per-module `void *` pointer and reaches EG/CG via the resource-id indirection. */ +# define ZEND_TSRMLS_CACHE_T void * +# define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *_tsrm_ls_cache TSRM_TLS_MODEL_ATTR = NULL; +# define TSRMLS_CACHE_DEFINE() TSRM_TLS void *_tsrm_ls_cache = NULL; +#else +# define ZEND_TSRMLS_CACHE_T struct _zend_tsrm_ls_cache +# define TSRMLS_MAIN_CACHE_DEFINE() +# define TSRMLS_CACHE_DEFINE() +#endif #ifdef __cplusplus -#define TSRMLS_MAIN_CACHE_EXTERN() extern "C" { extern TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR; } -#define TSRMLS_CACHE_EXTERN() extern "C" { extern TSRM_TLS void *TSRMLS_CACHE; } +#define TSRMLS_MAIN_CACHE_EXTERN() extern "C" { extern TSRM_TLS ZEND_TSRMLS_CACHE_T _tsrm_ls_cache TSRM_TLS_MODEL_ATTR; } +#define TSRMLS_CACHE_EXTERN() extern "C" { extern TSRM_TLS ZEND_TSRMLS_CACHE_T _tsrm_ls_cache; } #else -#define TSRMLS_MAIN_CACHE_EXTERN() extern TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR; -#define TSRMLS_CACHE_EXTERN() extern TSRM_TLS void *TSRMLS_CACHE; +#define TSRMLS_MAIN_CACHE_EXTERN() extern TSRM_TLS ZEND_TSRMLS_CACHE_T _tsrm_ls_cache TSRM_TLS_MODEL_ATTR; +#define TSRMLS_CACHE_EXTERN() extern TSRM_TLS ZEND_TSRMLS_CACHE_T _tsrm_ls_cache; #endif -#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL; -#define TSRMLS_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE = NULL; #define TSRMLS_CACHE_UPDATE() TSRMLS_CACHE = tsrm_get_ls_cache() -#define TSRMLS_CACHE _tsrm_ls_cache +#define TSRMLS_CACHE (*(void **) &_tsrm_ls_cache) #ifdef __cplusplus } diff --git a/Zend/zend.c b/Zend/zend.c index 3b16dc9c315b..51da3e1b4399 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -52,17 +52,30 @@ static bool startup_done = false; #ifdef ZTS ZEND_API int compiler_globals_id; ZEND_API int executor_globals_id; -ZEND_API size_t compiler_globals_offset; -ZEND_API size_t executor_globals_offset; +ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache = {0}; +#if defined(_WIN64) && defined(ZEND_TLS_DIRECT) +/* Holds &_tsrm_ls_cache in a TEB TLS slot (filled by DllMain) so EG()/CG() reach it + * with a single gs:[] load rather than the 3-load __declspec(thread) lookup. */ +ZEND_TLS_API unsigned long zend_win_tsrm_cache_slot = 0; +ZEND_API void zend_win_tsrm_cache_init(bool alloc) +{ + if (alloc) { + zend_win_tsrm_cache_slot = TlsAlloc(); + ZEND_ASSERT(zend_win_tsrm_cache_slot < 64); /* must be a direct TEB TlsSlot */ + } + TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); +} +#endif /* ts_allocate_tls_id takes a callback so each thread resolves its own block. - * A plain &language_scanner_globals would capture only the registering thread's address. */ + * A plain &..._tls would capture only the registering thread's address. */ +static void *executor_globals_tls_addr(void) { return &_tsrm_ls_cache.eg; } +static void *compiler_globals_tls_addr(void) { return &_tsrm_ls_cache.cg; } static void *language_scanner_globals_tls_addr(void) { return &language_scanner_globals; } static HashTable *global_function_table = NULL; static HashTable *global_class_table = NULL; static HashTable *global_constants_table = NULL; static HashTable *global_auto_globals_table = NULL; static HashTable *global_persistent_list = NULL; -TSRMLS_MAIN_CACHE_DEFINE() # define GLOBAL_FUNCTION_TABLE global_function_table # define GLOBAL_CLASS_TABLE global_class_table # define GLOBAL_CONSTANTS_TABLE global_constants_table @@ -807,6 +820,7 @@ static void compiler_globals_dtor(zend_compiler_globals *compiler_globals) /* {{ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{{ */ { + _tsrm_ls_cache.self = &_tsrm_ls_cache; zend_startup_constants(); zend_copy_constants(executor_globals->zend_constants, GLOBAL_CONSTANTS_TABLE); zend_init_rsrc_plist(); @@ -1025,11 +1039,9 @@ void zend_startup(zend_utility_functions *utility_functions) /* {{{ */ zend_init_rsrc_list_dtors(); #ifdef ZTS - ts_allocate_fast_id_at(&compiler_globals_id, &compiler_globals_offset, ZEND_CG_OFFSET, sizeof(zend_compiler_globals), (ts_allocate_ctor) compiler_globals_ctor, (ts_allocate_dtor) compiler_globals_dtor); - ts_allocate_fast_id_at(&executor_globals_id, &executor_globals_offset, ZEND_EG_OFFSET, sizeof(zend_executor_globals), (ts_allocate_ctor) executor_globals_ctor, (ts_allocate_dtor) executor_globals_dtor); + ts_allocate_tls_id(&compiler_globals_id, compiler_globals_tls_addr, sizeof(zend_compiler_globals), (ts_allocate_ctor) compiler_globals_ctor, (ts_allocate_dtor) compiler_globals_dtor); + ts_allocate_tls_id(&executor_globals_id, executor_globals_tls_addr, sizeof(zend_executor_globals), (ts_allocate_ctor) executor_globals_ctor, (ts_allocate_dtor) executor_globals_dtor); ts_allocate_tls_id(&language_scanner_globals_id, language_scanner_globals_tls_addr, sizeof(zend_php_scanner_globals), (ts_allocate_ctor) php_scanner_globals_ctor, NULL); - ZEND_ASSERT(compiler_globals_offset == ZEND_CG_OFFSET); - ZEND_ASSERT(executor_globals_offset == ZEND_EG_OFFSET); ts_allocate_fast_id(&ini_scanner_globals_id, &ini_scanner_globals_offset, sizeof(zend_ini_scanner_globals), (ts_allocate_ctor) ini_scanner_globals_ctor, NULL); compiler_globals = ts_resource(compiler_globals_id); executor_globals = ts_resource(executor_globals_id); diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index b7835ed63226..801fae65992d 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -51,8 +51,6 @@ BEGIN_EXTERN_C() ZEND_API extern int compiler_globals_id; ZEND_API extern int executor_globals_id; -ZEND_API extern size_t compiler_globals_offset; -ZEND_API extern size_t executor_globals_offset; END_EXTERN_C() #endif @@ -338,9 +336,12 @@ struct _zend_executor_globals { }; #ifdef ZTS -/* Compile-time offsets of the hot globals, in a reserved region just before *_tsrm_ls_cache. */ -# define ZEND_CG_OFFSET (-(ptrdiff_t) TSRM_ALIGNED_SIZE(sizeof(zend_compiler_globals))) -# define ZEND_EG_OFFSET (ZEND_CG_OFFSET - (ptrdiff_t) TSRM_ALIGNED_SIZE(sizeof(zend_executor_globals))) +struct _zend_tsrm_ls_cache { + void *cache; + void *self; + zend_executor_globals eg; + zend_compiler_globals cg; +}; #endif #define EG_FLAGS_INITIAL (0) diff --git a/Zend/zend_globals_macros.h b/Zend/zend_globals_macros.h index adb3913ece71..7233e753ee1a 100644 --- a/Zend/zend_globals_macros.h +++ b/Zend/zend_globals_macros.h @@ -26,11 +26,39 @@ typedef struct _zend_executor_globals zend_executor_globals; typedef struct _zend_php_scanner_globals zend_php_scanner_globals; typedef struct _zend_ini_scanner_globals zend_ini_scanner_globals; +#ifdef ZEND_WIN32 +# define ZEND_TLS_API +# ifdef LIBZEND_EXPORTS +# define ZEND_TLS_DIRECT 1 +# endif +#else +# define ZEND_TLS_API ZEND_API +# define ZEND_TLS_DIRECT 1 +#endif + BEGIN_EXTERN_C() +#ifdef ZTS +typedef struct _zend_tsrm_ls_cache zend_tsrm_ls_cache; +# ifdef ZEND_TLS_DIRECT +extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache; +/* See zenc.c: zend_win_tsrm_cache_init */ +# if defined(_WIN64) +extern ZEND_TLS_API unsigned long zend_win_tsrm_cache_slot; +# define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(0x1480 + zend_win_tsrm_cache_slot * 8)) +# else +# define ZEND_TSRM_CACHE_PTR (&_tsrm_ls_cache) +# endif +# endif +#endif + /* Compiler */ #ifdef ZTS -# define CG(v) ZEND_TSRMG_FAST(ZEND_CG_OFFSET, zend_compiler_globals *, v) +# ifdef ZEND_TLS_DIRECT +# define CG(v) (ZEND_TSRM_CACHE_PTR->cg.v) +# else +# define CG(v) ZEND_TSRMG(compiler_globals_id, zend_compiler_globals *, v) +# endif #else # define CG(v) (compiler_globals.v) extern ZEND_API struct _zend_compiler_globals compiler_globals; @@ -40,7 +68,11 @@ ZEND_API int zendparse(void); /* Executor */ #ifdef ZTS -# define EG(v) ZEND_TSRMG_FAST(ZEND_EG_OFFSET, zend_executor_globals *, v) +# ifdef ZEND_TLS_DIRECT +# define EG(v) (ZEND_TSRM_CACHE_PTR->eg.v) +# else +# define EG(v) ZEND_TSRMG(executor_globals_id, zend_executor_globals *, v) +# endif #else # define EG(v) (executor_globals.v) extern ZEND_API zend_executor_globals executor_globals; diff --git a/ext/opcache/jit/tls/zend_jit_tls_win.c b/ext/opcache/jit/tls/zend_jit_tls_win.c index 5646f3dcba0b..58fab12bf386 100644 --- a/ext/opcache/jit/tls/zend_jit_tls_win.c +++ b/ext/opcache/jit/tls/zend_jit_tls_win.c @@ -33,30 +33,21 @@ zend_result zend_jit_resolve_tsrm_ls_cache_offsets( size_t *module_index, size_t *module_offset ) { - /* To find offset of "_tsrm_ls_cache" in TLS segment we perform a linear scan of local TLS memory */ - /* Probably, it might be better solution */ + /* Offset of _tsrm_ls_cache within this module's TLS block. */ #ifdef _WIN64 void ***tls_mem = ((void****)__readgsqword(0x58))[_tls_index]; #else void ***tls_mem = ((void****)__readfsdword(0x2c))[_tls_index]; #endif - void *val = _tsrm_ls_cache; - size_t offset = 0; size_t size = (char*)&_tls_end - (char*)&_tls_start; - - while (offset < size) { - if (*tls_mem == val) { - *module_index = _tls_index * sizeof(void*); - *module_offset = offset; - return SUCCESS; - } - tls_mem++; - offset += sizeof(void*); - } + size_t offset = (size_t)((char*)&_tsrm_ls_cache - (char*)tls_mem); if (offset >= size) { zend_accel_error_noreturn(ACCEL_LOG_FATAL, "Could not enable JIT: offset >= size"); + return FAILURE; } - return FAILURE; + *module_index = _tls_index * sizeof(void*); + *module_offset = offset; + return SUCCESS; } diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 627b1f9437a2..e0085af99bfc 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -206,10 +206,10 @@ static size_t tsrm_tls_index = -1; static size_t tsrm_tls_offset = -1; # define EG_TLS_OFFSET(field) \ - (executor_globals_offset + offsetof(zend_executor_globals, field)) + (tsrm_ls_cache_tcb_offset + offsetof(zend_tsrm_ls_cache, eg) + offsetof(zend_executor_globals, field)) # define CG_TLS_OFFSET(field) \ - (compiler_globals_offset + offsetof(zend_compiler_globals, field)) + (tsrm_ls_cache_tcb_offset + offsetof(zend_tsrm_ls_cache, cg) + offsetof(zend_compiler_globals, field)) # define jit_EG(_field) \ ir_ADD_OFFSET(jit_TLS(jit), EG_TLS_OFFSET(_field)) @@ -496,7 +496,7 @@ static const char* zend_reg_name(int8_t reg) #ifdef ZTS static void * ZEND_FASTCALL zend_jit_get_tsrm_ls_cache(void) { - return _tsrm_ls_cache; + return &_tsrm_ls_cache; } static ir_ref jit_TLS(zend_jit_ctx *jit) @@ -521,10 +521,10 @@ static ir_ref jit_TLS(zend_jit_ctx *jit) if (tsrm_ls_cache_tcb_offset == 0 && tsrm_tls_index == -1) { jit->tls = ir_CALL(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_get_tsrm_ls_cache)); + } else if (tsrm_ls_cache_tcb_offset) { + jit->tls = ir_TLS(0, IR_NULL); } else { - jit->tls = ir_TLS( - tsrm_ls_cache_tcb_offset ? tsrm_ls_cache_tcb_offset : tsrm_tls_index, - tsrm_ls_cache_tcb_offset ? IR_NULL : tsrm_tls_offset); + jit->tls = ir_TLS(tsrm_tls_index, tsrm_tls_offset + offsetof(zend_tsrm_ls_cache, self)); } return jit->tls; diff --git a/main/main.c b/main/main.c index 7a8d440c0d75..1f3249e7d173 100644 --- a/main/main.c +++ b/main/main.c @@ -2750,9 +2750,8 @@ PHPAPI zend_result php_lint_script(zend_file_handle *file) /* {{{ php_reserve_tsrm_memory */ PHPAPI void php_reserve_tsrm_memory(void) { + /* CG/EG live in native __thread storage and need no reserved TSRM space. */ tsrm_reserve( - TSRM_ALIGNED_SIZE(sizeof(zend_compiler_globals)) + - TSRM_ALIGNED_SIZE(sizeof(zend_executor_globals)) + TSRM_ALIGNED_SIZE(sizeof(zend_ini_scanner_globals)) + TSRM_ALIGNED_SIZE(sizeof(virtual_cwd_globals)) + #ifdef ZEND_SIGNALS @@ -2774,10 +2773,6 @@ PHPAPI bool php_tsrm_startup_ex(int expected_threads) { bool ret = tsrm_startup(expected_threads, 1, 0, NULL); php_reserve_tsrm_memory(); - /* Must cover the total size of every ZEND_*_OFFSET global, or the furthest underflows the block. */ - tsrm_reserve_fast_front( - TSRM_ALIGNED_SIZE(sizeof(zend_compiler_globals)) + - TSRM_ALIGNED_SIZE(sizeof(zend_executor_globals))); (void)ts_resource(0); return ret; } diff --git a/sapi/apache2handler/config.m4 b/sapi/apache2handler/config.m4 index 3001a4d61d9a..12a0b80972c9 100644 --- a/sapi/apache2handler/config.m4 +++ b/sapi/apache2handler/config.m4 @@ -94,6 +94,10 @@ if test "$PHP_APXS2" != "no"; then LIBPHP_CFLAGS="-shared" PHP_SUBST([LIBPHP_CFLAGS]) + dnl httpd dlopen's libphp.so without linking against it, so _tsrm_ls_cache can't + dnl use initial-exec (overflows the static TLS surplus) + AS_VAR_APPEND([CFLAGS], [" -DTSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC"]) + php_sapi_apache2handler_type=shared AS_CASE([$host_alias], [*aix*], [ diff --git a/win32/dllmain.c b/win32/dllmain.c index 168bef2baa22..bb9386b007ac 100644 --- a/win32/dllmain.c +++ b/win32/dllmain.c @@ -27,10 +27,22 @@ eq. initializing something before the DLL even is available to be called. */ +#if defined(_WIN64) && defined(ZTS) +ZEND_API void zend_win_tsrm_cache_init(bool alloc); +#endif + BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID dummy) { BOOL ret = TRUE; +#if defined(_WIN64) && defined(ZTS) + if (reason == DLL_PROCESS_ATTACH) { + zend_win_tsrm_cache_init(true); + } else if (reason == DLL_THREAD_ATTACH) { + zend_win_tsrm_cache_init(false); + } +#endif + #ifdef HAVE_LIBXML /* This imply that only LIBXML_STATIC_FOR_DLL is supported ATM. If that changes, this place will need some rework. From 49fd6e3bebc0e51198922593fa7dc25c6b8cd6e7 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 23 Jul 2026 13:49:46 +0700 Subject: [PATCH 02/38] fix typo --- Zend/zend_globals_macros.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/zend_globals_macros.h b/Zend/zend_globals_macros.h index 7233e753ee1a..2ddb8132c407 100644 --- a/Zend/zend_globals_macros.h +++ b/Zend/zend_globals_macros.h @@ -42,7 +42,7 @@ BEGIN_EXTERN_C() typedef struct _zend_tsrm_ls_cache zend_tsrm_ls_cache; # ifdef ZEND_TLS_DIRECT extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache; -/* See zenc.c: zend_win_tsrm_cache_init */ +/* See zend.c: zend_win_tsrm_cache_init */ # if defined(_WIN64) extern ZEND_TLS_API unsigned long zend_win_tsrm_cache_slot; # define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(0x1480 + zend_win_tsrm_cache_slot * 8)) From 8afed0bfc1eae64037f18ea7efba42f6a34a20df Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 11 Aug 2026 17:36:46 +0200 Subject: [PATCH 03/38] leave the option of initial-exec tls model even for apache2handler if packagers want to set the glibc tls surplus in their httpd package --- UPGRADING.INTERNALS | 4 ++++ configure.ac | 35 +++++++++++++++++++++++++++++++++++ sapi/apache2handler/config.m4 | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index e542dcfec571..93b40f437000 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -33,6 +33,10 @@ PHP 8.7 INTERNALS UPGRADE NOTES - Unix build system changes: . Autoconf minimum required version upgraded to 2.71. + . Added --with-tsrm-tls-model (initial-exec, global-dynamic, auto) to pick the + TLS model for the ZTS globals cache; auto uses global-dynamic for dlopen()ed + SAPIs. Forcing initial-exec there needs a larger static TLS surplus: set + GLIBC_TUNABLES=glibc.rtld.optional_static_tls=8192 for the host process. ======================== 3. Module changes diff --git a/configure.ac b/configure.ac index 50df06a6b479..acee081dd633 100644 --- a/configure.ac +++ b/configure.ac @@ -276,6 +276,9 @@ SAPI_SHARED=libs/$SAPI_LIBNAME_SHARED SAPI_STATIC=libs/$SAPI_LIBNAME_STATIC SAPI_LIBTOOL=libphp.la +dnl Set by SAPIs whose host program dlopen()s libphp without linking against it. +php_tsrm_dlopened_sapi=no + PHP_CONFIGURE_PART([Configuring SAPI modules]) esyscmd(./build/config-stubs sapi) @@ -877,6 +880,38 @@ AS_VAR_IF([PHP_THREAD_SAFETY], [yes], [ AC_MSG_RESULT([yes]) ]) +PHP_ARG_WITH([tsrm-tls-model],, + [AS_HELP_STRING([[--with-tsrm-tls-model=MODEL]], + [TLS model for the ZTS globals cache: initial-exec, global-dynamic, or + auto for global-dynamic on dlopen()ed SAPIs and initial-exec elsewhere + [auto]])], + [auto], + [no]) + +AS_VAR_IF([PHP_THREAD_SAFETY], [yes], [ + AS_CASE([$PHP_TSRM_TLS_MODEL], + [auto], [ + AS_VAR_IF([php_tsrm_dlopened_sapi], [yes], + [php_tsrm_tls_model=global-dynamic], + [php_tsrm_tls_model=initial-exec]) + ], + [initial-exec|global-dynamic], [php_tsrm_tls_model=$PHP_TSRM_TLS_MODEL], + [AC_MSG_ERROR(m4_text_wrap([ + Unknown TLS model '$PHP_TSRM_TLS_MODEL' given to --with-tsrm-tls-model. + Valid values are: auto, initial-exec, global-dynamic. + ]))]) + + AC_MSG_CHECKING([which TLS model to use for the ZTS globals cache]) + AC_MSG_RESULT([$php_tsrm_tls_model]) + + dnl In php_config.h, not CFLAGS, so phpize builds use the same model. + AS_VAR_IF([php_tsrm_tls_model], [global-dynamic], [ + AC_DEFINE([TSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC], [1], + [Define to 1 to use the global-dynamic TLS model for the ZTS globals + cache.]) + ]) +]) + PHP_ARG_ENABLE([rtld-now], [whether to dlopen extensions with RTLD_NOW instead of RTLD_LAZY], [AS_HELP_STRING([--enable-rtld-now], diff --git a/sapi/apache2handler/config.m4 b/sapi/apache2handler/config.m4 index 12a0b80972c9..a1cf6ae4e7ec 100644 --- a/sapi/apache2handler/config.m4 +++ b/sapi/apache2handler/config.m4 @@ -96,7 +96,7 @@ if test "$PHP_APXS2" != "no"; then dnl httpd dlopen's libphp.so without linking against it, so _tsrm_ls_cache can't dnl use initial-exec (overflows the static TLS surplus) - AS_VAR_APPEND([CFLAGS], [" -DTSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC"]) + php_tsrm_dlopened_sapi=yes php_sapi_apache2handler_type=shared AS_CASE([$host_alias], From 8cbf6f5bd60b932368ac59938528a80f1b984c27 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 11 Aug 2026 18:01:29 +0200 Subject: [PATCH 04/38] remove ts_allocate_fast_id_at again (unused, now that the four globals are in TLS storage) --- TSRM/TSRM.c | 46 ++++++++++------------------------------------ TSRM/TSRM.h | 5 ----- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index 476e8d0a18e8..9e1ae852dec8 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -59,7 +59,6 @@ static int resource_types_table_size; /* Reserved space for fast globals access */ static size_t tsrm_reserved_pos = 0; static size_t tsrm_reserved_size = 0; -static size_t tsrm_reserved_front = 0; static MUTEX_T tsmm_mutex; /* thread-safe memory manager mutex */ static MUTEX_T tsrm_env_mutex; /* tsrm environ mutex */ @@ -157,7 +156,6 @@ TSRM_API bool tsrm_startup(int expected_threads, int expected_resources, int deb tsrm_reserved_pos = 0; tsrm_reserved_size = 0; - tsrm_reserved_front = 0; tsrm_env_mutex = tsrm_mutex_alloc(); @@ -216,7 +214,7 @@ TSRM_API void tsrm_shutdown(void) } else { free(p->storage); } - free((char *) p - tsrm_reserved_front); + free(p); p = next_p; } } @@ -243,7 +241,6 @@ TSRM_API void tsrm_shutdown(void) tsrm_reserved_pos = 0; tsrm_reserved_size = 0; - tsrm_reserved_front = 0; }/*}}}*/ /* {{{ */ @@ -336,20 +333,13 @@ TSRM_API void tsrm_reserve(size_t size) }/*}}}*/ -/* Carve a fixed-offset front region out of the reserved space. It is placed - * before the TLS entry, so the hot globals get compile-time-constant negative - * offsets from the cache pointer. */ -TSRM_API void tsrm_reserve_fast_front(size_t size) -{ - tsrm_reserved_front = TSRM_ALIGNED_SIZE(size); - tsrm_reserved_size -= tsrm_reserved_front; -} - - /* allocates a new fast thread-safe-resource id */ TSRM_API ts_rsrc_id ts_allocate_fast_id(ts_rsrc_id *rsrc_id, size_t *offset, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor) {/*{{{*/ + TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtaining a new fast resource id, %d bytes", size)); + tsrm_mutex_lock(tsmm_mutex); + size = TSRM_ALIGNED_SIZE(size); if (tsrm_reserved_size - tsrm_reserved_pos < size) { TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate space for fast resource")); @@ -358,27 +348,13 @@ TSRM_API ts_rsrc_id ts_allocate_fast_id(ts_rsrc_id *rsrc_id, size_t *offset, siz tsrm_mutex_unlock(tsmm_mutex); return 0; } - ptrdiff_t fixed_offset = TSRM_ALIGNED_SIZE(sizeof(tsrm_tls_entry)) + tsrm_reserved_pos; + *offset = TSRM_ALIGNED_SIZE(sizeof(tsrm_tls_entry)) + tsrm_reserved_pos; tsrm_reserved_pos += size; - tsrm_mutex_unlock(tsmm_mutex); - - return ts_allocate_fast_id_at(rsrc_id, offset, fixed_offset, size, ctor, dtor); -}/*}}}*/ - - -TSRM_API ts_rsrc_id ts_allocate_fast_id_at(ts_rsrc_id *rsrc_id, size_t *offset, ptrdiff_t fixed_offset, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor) -{ - TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtaining a new fast resource id, %d bytes", size)); - - tsrm_mutex_lock(tsmm_mutex); /* obtain a resource id */ *rsrc_id = TSRM_SHUFFLE_RSRC_ID(id_count++); TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtained resource id %d", *rsrc_id)); - size = TSRM_ALIGNED_SIZE(size); - *offset = (size_t) fixed_offset; - /* store the new resource type in the resource sizes table */ if (resource_types_table_size < id_count) { tsrm_resource_type *_tmp; @@ -405,7 +381,7 @@ TSRM_API ts_rsrc_id ts_allocate_fast_id_at(ts_rsrc_id *rsrc_id, size_t *offset, TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Successfully allocated new resource id %d", *rsrc_id)); return *rsrc_id; -} +}/*}}}*/ /* allocates a resource id whose per-thread storage is a native __thread block */ TSRM_API ts_rsrc_id ts_allocate_tls_id(ts_rsrc_id *rsrc_id, void *(*tls_addr)(void), size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor) @@ -453,10 +429,8 @@ static void set_thread_local_storage_resource_to(tsrm_tls_entry *thread_resource static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id) {/*{{{*/ TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Creating data structures for thread %x", thread_id)); - /* The entry follows the fixed-offset front region. - * hot globals live at negative offsets from the TLS cache pointer. */ - char *block = (char *) malloc(tsrm_reserved_front + TSRM_ALIGNED_SIZE(sizeof(tsrm_tls_entry)) + tsrm_reserved_size); - (*thread_resources_ptr) = (tsrm_tls_entry *) (block + tsrm_reserved_front); + /* Fast resources live in the reserved space right behind the entry. */ + (*thread_resources_ptr) = (tsrm_tls_entry *) malloc(TSRM_ALIGNED_SIZE(sizeof(tsrm_tls_entry)) + tsrm_reserved_size); (*thread_resources_ptr)->storage = NULL; if (id_count > 0) { (*thread_resources_ptr)->storage = (void **) malloc(sizeof(void *)*id_count); @@ -569,7 +543,7 @@ TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id) * thread's blocks were never constructed, so keep tls dtors from running. */ thread_resources->thread_id = 0; ts_free_resources(thread_resources); - free((char *) thread_resources - tsrm_reserved_front); + free(thread_resources); /* Allocate a new resource at the same point in the linked list, and relink the next pointer */ allocate_new_resource(last_thread_resources, thread_id); thread_resources = *last_thread_resources; @@ -611,7 +585,7 @@ void ts_free_thread(void) tsrm_tls_table[hash_value] = thread_resources->next; } tsrm_tls_set(0); - free((char *) thread_resources - tsrm_reserved_front); + free(thread_resources); break; } if (thread_resources->next) { diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index 85d860cb00aa..f95ed8fe1351 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -95,11 +95,6 @@ TSRM_API ts_rsrc_id ts_allocate_id(ts_rsrc_id *rsrc_id, size_t size, ts_allocate TSRM_API void tsrm_reserve(size_t size); TSRM_API ts_rsrc_id ts_allocate_fast_id(ts_rsrc_id *rsrc_id, size_t *offset, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor); -/* Fast resources at caller-chosen, compile-time-constant offsets. The fixed - * front region must be reserved after tsrm_reserve() and before any fast id. */ -TSRM_API void tsrm_reserve_fast_front(size_t size); -TSRM_API ts_rsrc_id ts_allocate_fast_id_at(ts_rsrc_id *rsrc_id, size_t *offset, ptrdiff_t fixed_offset, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor); - /* Resource whose per-thread storage is a native __thread block. * Must be called at startup before any other thread exists. */ TSRM_API ts_rsrc_id ts_allocate_tls_id(ts_rsrc_id *rsrc_id, void *(*tls_addr)(void), size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor); From 549ecdeb978335898b8dbfecd59b4502fa387f26 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 12:55:40 +0200 Subject: [PATCH 05/38] guard ts_apply_for_id and ts_free_id for own threads globals too --- TSRM/TSRM.c | 12 ++++++++++-- UPGRADING.INTERNALS | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index 9e1ae852dec8..e34189b7b020 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -600,6 +600,7 @@ void ts_free_thread(void) void ts_free_id(ts_rsrc_id id) {/*{{{*/ int rsrc_id = TSRM_UNSHUFFLE_RSRC_ID(id); + THREAD_T this_thread = tsrm_thread_id(); tsrm_mutex_lock(tsmm_mutex); @@ -611,7 +612,9 @@ void ts_free_id(ts_rsrc_id id) while (p) { if (p->count > rsrc_id && p->storage[rsrc_id]) { - if (resource_types_table) { + /* A __thread block of a foreign thread is inaccessible. */ + if (resource_types_table + && (!resource_types_table[rsrc_id].tls_addr || p->thread_id == this_thread)) { if (resource_types_table[rsrc_id].dtor) { resource_types_table[rsrc_id].dtor(p->storage[rsrc_id]); } @@ -635,15 +638,20 @@ void ts_free_id(ts_rsrc_id id) TSRM_API void ts_apply_for_id(ts_rsrc_id id, void (*cb)(void *)) { int rsrc_id = TSRM_UNSHUFFLE_RSRC_ID(id); + THREAD_T this_thread = tsrm_thread_id(); tsrm_mutex_lock(tsmm_mutex); if (tsrm_tls_table && resource_types_table) { + bool tls_backed = resource_types_table[rsrc_id].tls_addr != NULL; + for (int i = 0; i < tsrm_tls_table_size; i++) { tsrm_tls_entry *p = tsrm_tls_table[i]; while (p) { - if (p->count > rsrc_id && p->storage[rsrc_id]) { + /* A __thread block of a foreign thread is inaccessible. */ + if (p->count > rsrc_id && p->storage[rsrc_id] + && (!tls_backed || p->thread_id == this_thread)) { cb(p->storage[rsrc_id]); } p = p->next; diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 93b40f437000..7eccce2f751b 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -21,6 +21,8 @@ PHP 8.7 INTERNALS UPGRADE NOTES - The sapi_force_http_10() function was removed. - The sapi_get_fd() function was removed. - The sapi_get_target_uid() and sapi_get_target_gid() functions were removed. +- The ZTS symbols compiler_globals_offset and executor_globals_offset have been + removed: CG()/EG() now reach the globals through a __thread struct. ======================== 2. Build system changes From d83721cdb26a11a1d18ba86550ed46d044c8fccf Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 13:28:30 +0200 Subject: [PATCH 06/38] reword comment --- sapi/apache2handler/config.m4 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sapi/apache2handler/config.m4 b/sapi/apache2handler/config.m4 index a1cf6ae4e7ec..a8d3491df8b5 100644 --- a/sapi/apache2handler/config.m4 +++ b/sapi/apache2handler/config.m4 @@ -94,8 +94,10 @@ if test "$PHP_APXS2" != "no"; then LIBPHP_CFLAGS="-shared" PHP_SUBST([LIBPHP_CFLAGS]) - dnl httpd dlopen's libphp.so without linking against it, so _tsrm_ls_cache can't - dnl use initial-exec (overflows the static TLS surplus) + dnl httpd dlopen's libphp.so without linking against it, so we must + dnl use global-dynamic by default (_tsrm_ls_cache overflows the default + dnf static TLS surplus). Force --with-tsrm-tls-model=initil-exec to + dnf overwrite this behaviour, see UPGRADING.INTERNALS php_tsrm_dlopened_sapi=yes php_sapi_apache2handler_type=shared From bd07fbb334452b29a75ed36a01cccf20d6ef6637 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 14:21:42 +0200 Subject: [PATCH 07/38] stupid typo --- sapi/apache2handler/config.m4 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sapi/apache2handler/config.m4 b/sapi/apache2handler/config.m4 index a8d3491df8b5..c4498f1dae30 100644 --- a/sapi/apache2handler/config.m4 +++ b/sapi/apache2handler/config.m4 @@ -96,8 +96,8 @@ if test "$PHP_APXS2" != "no"; then dnl httpd dlopen's libphp.so without linking against it, so we must dnl use global-dynamic by default (_tsrm_ls_cache overflows the default - dnf static TLS surplus). Force --with-tsrm-tls-model=initil-exec to - dnf overwrite this behaviour, see UPGRADING.INTERNALS + dnl static TLS surplus). Force --with-tsrm-tls-model=initil-exec to + dnl overwrite this behaviour, see UPGRADING.INTERNALS php_tsrm_dlopened_sapi=yes php_sapi_apache2handler_type=shared From 573d9800b1bb5d1f49634ff772025714397b6ac5 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 17:35:22 +0200 Subject: [PATCH 08/38] clarification --- TSRM/TSRM.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index f95ed8fe1351..dae2df38e86b 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -188,7 +188,7 @@ TSRM_API bool tsrm_is_managed_thread(void); #define TSRMG_FAST_BULK_STATIC(offset, type) ((type) (((char*) TSRMLS_CACHE)+(offset))) struct _zend_tsrm_ls_cache; #if defined(ZEND_WIN32) && !defined(LIBZEND_EXPORTS) -/* Windows can't dllexport the TLS struct, so outside Zend each module +/* Windows can't dllexport __declspec(thread) symbols, so outside Zend each module * keeps a per-module `void *` pointer and reaches EG/CG via the resource-id indirection. */ # define ZEND_TSRMLS_CACHE_T void * # define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *_tsrm_ls_cache TSRM_TLS_MODEL_ATTR = NULL; From c67e552a5fadead0131366574b9a94d25677306f Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 17:43:41 +0200 Subject: [PATCH 09/38] abort, rather than assert --- Zend/zend.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Zend/zend.c b/Zend/zend.c index 51da3e1b4399..17688c8b7a55 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -61,7 +61,20 @@ ZEND_API void zend_win_tsrm_cache_init(bool alloc) { if (alloc) { zend_win_tsrm_cache_slot = TlsAlloc(); - ZEND_ASSERT(zend_win_tsrm_cache_slot < 64); /* must be a direct TEB TlsSlot */ + if (zend_win_tsrm_cache_slot == TLS_OUT_OF_INDEXES) { + fprintf(stderr, "PHP Startup: TlsAlloc() failed, no TLS slot available " + "for the ZTS globals cache\n"); + abort(); + } + if (zend_win_tsrm_cache_slot >= TLS_MINIMUM_AVAILABLE) { + /* Beyond the first TLS_MINIMUM_AVAILABLE slots the loader uses + * TEB->TlsExpansionSlots, which the inline gs:[] read in + * ZEND_TSRM_CACHE_PTR cannot reach. */ + fprintf(stderr, "PHP Startup: TlsAlloc() returned slot %lu, but only the " + "first %d direct TEB slots are usable for the ZTS globals cache\n", + zend_win_tsrm_cache_slot, TLS_MINIMUM_AVAILABLE); + abort(); + } } TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); } From f37864b33adf611c7c8814e7c4b4f1c24dc43926 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 13 Aug 2026 19:59:25 +0200 Subject: [PATCH 10/38] largely revert jit, just offsetof self* in the tsrm struct --- ext/opcache/jit/zend_jit_ir.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index e0085af99bfc..2ff581f404f2 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -206,10 +206,10 @@ static size_t tsrm_tls_index = -1; static size_t tsrm_tls_offset = -1; # define EG_TLS_OFFSET(field) \ - (tsrm_ls_cache_tcb_offset + offsetof(zend_tsrm_ls_cache, eg) + offsetof(zend_executor_globals, field)) + (offsetof(zend_tsrm_ls_cache, eg) + offsetof(zend_executor_globals, field)) # define CG_TLS_OFFSET(field) \ - (tsrm_ls_cache_tcb_offset + offsetof(zend_tsrm_ls_cache, cg) + offsetof(zend_compiler_globals, field)) + (offsetof(zend_tsrm_ls_cache, cg) + offsetof(zend_compiler_globals, field)) # define jit_EG(_field) \ ir_ADD_OFFSET(jit_TLS(jit), EG_TLS_OFFSET(_field)) @@ -521,10 +521,17 @@ static ir_ref jit_TLS(zend_jit_ctx *jit) if (tsrm_ls_cache_tcb_offset == 0 && tsrm_tls_index == -1) { jit->tls = ir_CALL(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_get_tsrm_ls_cache)); - } else if (tsrm_ls_cache_tcb_offset) { - jit->tls = ir_TLS(0, IR_NULL); } else { - jit->tls = ir_TLS(tsrm_tls_index, tsrm_tls_offset + offsetof(zend_tsrm_ls_cache, self)); + /* ir_TLS() loads the word stored at _tsrm_ls_cache, so read the "self" + * back-pointer to end up with the address of the cache struct itself. + * The globals live inside it, not behind its first (`cache`) field. */ + jit->tls = ir_TLS( + tsrm_ls_cache_tcb_offset + ? tsrm_ls_cache_tcb_offset + offsetof(zend_tsrm_ls_cache, self) + : tsrm_tls_index, + tsrm_ls_cache_tcb_offset + ? IR_NULL + : tsrm_tls_offset + offsetof(zend_tsrm_ls_cache, self)); } return jit->tls; From 4d670df1f9fd21cd21e29f261daa2bdeebd43349 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 13 Aug 2026 20:58:31 +0200 Subject: [PATCH 11/38] LE guard for external embedders --- TSRM/TSRM.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index dae2df38e86b..4fc064dfae92 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -161,7 +161,7 @@ TSRM_API bool tsrm_is_managed_thread(void); || defined(__MUSL__) || defined(__HAIKU__) || defined(_AIX) # define TSRM_TLS_MODEL_ATTR # define TSRM_TLS_MODEL_DEFAULT -#elif defined(__PIC__) && !defined(__PIE__) +#elif (defined(__PIC__) && !defined(__PIE__)) || !defined(ZEND_ENABLE_STATIC_TSRMLS_CACHE) # if defined(TSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC) # define TSRM_TLS_MODEL_ATTR __attribute__((tls_model("global-dynamic"))) # define TSRM_TLS_MODEL_GLOBAL_DYNAMIC From 25559c2b1e5bcd66b313884c5cef76e3cf874787 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 20 Aug 2026 17:33:49 +0200 Subject: [PATCH 12/38] drop unused TSRMLS_MAIN_CACHE_DEFINE() --- TSRM/TSRM.c | 2 -- TSRM/TSRM.h | 3 --- 2 files changed, 5 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index e34189b7b020..00a400b72237 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -337,9 +337,7 @@ TSRM_API void tsrm_reserve(size_t size) TSRM_API ts_rsrc_id ts_allocate_fast_id(ts_rsrc_id *rsrc_id, size_t *offset, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor) {/*{{{*/ TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtaining a new fast resource id, %d bytes", size)); - tsrm_mutex_lock(tsmm_mutex); - size = TSRM_ALIGNED_SIZE(size); if (tsrm_reserved_size - tsrm_reserved_pos < size) { TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate space for fast resource")); diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index 4fc064dfae92..2017f7e93b25 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -191,11 +191,9 @@ struct _zend_tsrm_ls_cache; /* Windows can't dllexport __declspec(thread) symbols, so outside Zend each module * keeps a per-module `void *` pointer and reaches EG/CG via the resource-id indirection. */ # define ZEND_TSRMLS_CACHE_T void * -# define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *_tsrm_ls_cache TSRM_TLS_MODEL_ATTR = NULL; # define TSRMLS_CACHE_DEFINE() TSRM_TLS void *_tsrm_ls_cache = NULL; #else # define ZEND_TSRMLS_CACHE_T struct _zend_tsrm_ls_cache -# define TSRMLS_MAIN_CACHE_DEFINE() # define TSRMLS_CACHE_DEFINE() #endif #ifdef __cplusplus @@ -219,7 +217,6 @@ struct _zend_tsrm_ls_cache; #define TSRMG_STATIC(id, type, element) #define TSRMLS_MAIN_CACHE_EXTERN() -#define TSRMLS_MAIN_CACHE_DEFINE() #define TSRMLS_CACHE_EXTERN() #define TSRMLS_CACHE_DEFINE() #define TSRMLS_CACHE_UPDATE() From 07862bca6c9d24ef3e5086498bd842905b9abcdf Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 20 Aug 2026 17:33:55 +0200 Subject: [PATCH 13/38] spelling mistakes --- sapi/apache2handler/config.m4 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sapi/apache2handler/config.m4 b/sapi/apache2handler/config.m4 index c4498f1dae30..e6c4490076e6 100644 --- a/sapi/apache2handler/config.m4 +++ b/sapi/apache2handler/config.m4 @@ -96,8 +96,8 @@ if test "$PHP_APXS2" != "no"; then dnl httpd dlopen's libphp.so without linking against it, so we must dnl use global-dynamic by default (_tsrm_ls_cache overflows the default - dnl static TLS surplus). Force --with-tsrm-tls-model=initil-exec to - dnl overwrite this behaviour, see UPGRADING.INTERNALS + dnl static TLS surplus). Force --with-tsrm-tls-model=initial-exec to + dnl override this behaviour, see UPGRADING.INTERNALS php_tsrm_dlopened_sapi=yes php_sapi_apache2handler_type=shared From 18c0f6532c1b843e209207514d7f7271ad0bd081 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 20 Aug 2026 17:41:34 +0200 Subject: [PATCH 14/38] remove useless line --- ext/opcache/jit/tls/zend_jit_tls_win.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/opcache/jit/tls/zend_jit_tls_win.c b/ext/opcache/jit/tls/zend_jit_tls_win.c index 58fab12bf386..9bd79571eea3 100644 --- a/ext/opcache/jit/tls/zend_jit_tls_win.c +++ b/ext/opcache/jit/tls/zend_jit_tls_win.c @@ -44,7 +44,6 @@ zend_result zend_jit_resolve_tsrm_ls_cache_offsets( if (offset >= size) { zend_accel_error_noreturn(ACCEL_LOG_FATAL, "Could not enable JIT: offset >= size"); - return FAILURE; } *module_index = _tls_index * sizeof(void*); From bcf1a46bec655f0a2e604583d75d2c6567a754b9 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 20 Aug 2026 18:53:21 +0200 Subject: [PATCH 15/38] move GC globals into __thread storage too --- Zend/zend_gc.c | 15 ++++----------- Zend/zend_gc.h | 4 ---- main/main.c | 4 ++-- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index 5de2b69bf568..e1883aa09cd0 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -306,12 +306,12 @@ typedef struct _zend_gc_globals { #ifdef ZTS static int gc_globals_id; -static size_t gc_globals_offset; -#define GC_G(v) ZEND_TSRMG_FAST(gc_globals_offset, zend_gc_globals *, v) +static TSRM_TLS TSRM_TLS_MODEL_ATTR zend_gc_globals gc_globals; +static void *gc_globals_tls_addr(void) { return &gc_globals; } #else -#define GC_G(v) (gc_globals.v) static zend_gc_globals gc_globals; #endif +#define GC_G(v) (gc_globals.v) #if GC_BENCH # define GC_BENCH_INC(counter) GC_G(counter)++ @@ -549,7 +549,7 @@ static void gc_globals_ctor_ex(zend_gc_globals *gc_globals) void gc_globals_ctor(void) { #ifdef ZTS - ts_allocate_fast_id(&gc_globals_id, &gc_globals_offset, sizeof(zend_gc_globals), (ts_allocate_ctor) gc_globals_ctor_ex, (ts_allocate_dtor) root_buffer_dtor); + ts_allocate_tls_id(&gc_globals_id, gc_globals_tls_addr, sizeof(zend_gc_globals), (ts_allocate_ctor) gc_globals_ctor_ex, (ts_allocate_dtor) root_buffer_dtor); #else gc_globals_ctor_ex(&gc_globals); #endif @@ -2310,13 +2310,6 @@ void gc_bench_print(void) } #endif -#ifdef ZTS -size_t zend_gc_globals_size(void) -{ - return sizeof(zend_gc_globals); -} -#endif - static ZEND_FUNCTION(gc_destructor_fiber) { uint32_t idx, end; diff --git a/Zend/zend_gc.h b/Zend/zend_gc.h index b72b90ccd5ef..55b12070b99d 100644 --- a/Zend/zend_gc.h +++ b/Zend/zend_gc.h @@ -69,10 +69,6 @@ void gc_globals_ctor(void); void gc_globals_dtor(void); void gc_reset(void); -#ifdef ZTS -size_t zend_gc_globals_size(void); -#endif - #define GC_REMOVE_FROM_BUFFER(p) do { \ zend_refcounted *_p = (zend_refcounted*)(p); \ if (GC_TYPE_INFO(_p) & GC_INFO_MASK) { \ diff --git a/main/main.c b/main/main.c index 1f3249e7d173..8c561d27bd3b 100644 --- a/main/main.c +++ b/main/main.c @@ -2750,14 +2750,14 @@ PHPAPI zend_result php_lint_script(zend_file_handle *file) /* {{{ php_reserve_tsrm_memory */ PHPAPI void php_reserve_tsrm_memory(void) { - /* CG/EG live in native __thread storage and need no reserved TSRM space. */ + /* CG/EG/GC globals live in native __thread storage and need no + * reserved TSRM space. */ tsrm_reserve( TSRM_ALIGNED_SIZE(sizeof(zend_ini_scanner_globals)) + TSRM_ALIGNED_SIZE(sizeof(virtual_cwd_globals)) + #ifdef ZEND_SIGNALS TSRM_ALIGNED_SIZE(sizeof(zend_signal_globals_t)) + #endif - TSRM_ALIGNED_SIZE(zend_gc_globals_size()) + TSRM_ALIGNED_SIZE(sizeof(php_core_globals)) + TSRM_ALIGNED_SIZE(sizeof(sapi_globals_struct)) + TSRM_ALIGNED_SIZE(sizeof(zend_accel_globals)) + From 016e1abbfa4456d4b67b359a13ae96dbd74826cc Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 27 Aug 2026 15:58:10 +0200 Subject: [PATCH 16/38] feedback: teb slot needs _m_x64 --- Zend/zend.c | 2 +- Zend/zend_globals_macros.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Zend/zend.c b/Zend/zend.c index 17688c8b7a55..f2dacfe0d167 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -53,7 +53,7 @@ static bool startup_done = false; ZEND_API int compiler_globals_id; ZEND_API int executor_globals_id; ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache = {0}; -#if defined(_WIN64) && defined(ZEND_TLS_DIRECT) +#if defined(_WIN64) && defined(_M_X64) && defined(ZEND_TLS_DIRECT) /* Holds &_tsrm_ls_cache in a TEB TLS slot (filled by DllMain) so EG()/CG() reach it * with a single gs:[] load rather than the 3-load __declspec(thread) lookup. */ ZEND_TLS_API unsigned long zend_win_tsrm_cache_slot = 0; diff --git a/Zend/zend_globals_macros.h b/Zend/zend_globals_macros.h index 2ddb8132c407..6dba8d2fb03b 100644 --- a/Zend/zend_globals_macros.h +++ b/Zend/zend_globals_macros.h @@ -43,7 +43,7 @@ typedef struct _zend_tsrm_ls_cache zend_tsrm_ls_cache; # ifdef ZEND_TLS_DIRECT extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache; /* See zend.c: zend_win_tsrm_cache_init */ -# if defined(_WIN64) +# if defined(_WIN64) && defined(_M_X64) extern ZEND_TLS_API unsigned long zend_win_tsrm_cache_slot; # define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(0x1480 + zend_win_tsrm_cache_slot * 8)) # else From 555646df0491528ce7900387407d0d0052f83a5a Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 27 Aug 2026 16:04:34 +0200 Subject: [PATCH 17/38] feedback: precompute teb slot offset --- Zend/zend.c | 14 +++++++++++++- Zend/zend_globals_macros.h | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Zend/zend.c b/Zend/zend.c index f2dacfe0d167..8b70ed6e25bd 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -56,7 +56,11 @@ ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache = {0 #if defined(_WIN64) && defined(_M_X64) && defined(ZEND_TLS_DIRECT) /* Holds &_tsrm_ls_cache in a TEB TLS slot (filled by DllMain) so EG()/CG() reach it * with a single gs:[] load rather than the 3-load __declspec(thread) lookup. */ -ZEND_TLS_API unsigned long zend_win_tsrm_cache_slot = 0; +#define ZEND_WIN_TEB_TLS_SLOTS 0x1480 + +static DWORD zend_win_tsrm_cache_slot = 0; +ZEND_TLS_API unsigned long zend_win_tsrm_cache_offset = 0; + ZEND_API void zend_win_tsrm_cache_init(bool alloc) { if (alloc) { @@ -75,8 +79,16 @@ ZEND_API void zend_win_tsrm_cache_init(bool alloc) zend_win_tsrm_cache_slot, TLS_MINIMUM_AVAILABLE); abort(); } + zend_win_tsrm_cache_offset = ZEND_WIN_TEB_TLS_SLOTS + + zend_win_tsrm_cache_slot * (unsigned long) sizeof(void*); } TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); + /* Verify our layout assumptions work */ + if (ZEND_TSRM_CACHE_PTR != &_tsrm_ls_cache) { + fprintf(stderr, "PHP Startup: the ZTS globals cache is not reachable through " + "TEB offset %lu\n", zend_win_tsrm_cache_offset); + abort(); + } } #endif /* ts_allocate_tls_id takes a callback so each thread resolves its own block. diff --git a/Zend/zend_globals_macros.h b/Zend/zend_globals_macros.h index 6dba8d2fb03b..bb1420ccdc84 100644 --- a/Zend/zend_globals_macros.h +++ b/Zend/zend_globals_macros.h @@ -44,8 +44,8 @@ typedef struct _zend_tsrm_ls_cache zend_tsrm_ls_cache; extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache; /* See zend.c: zend_win_tsrm_cache_init */ # if defined(_WIN64) && defined(_M_X64) -extern ZEND_TLS_API unsigned long zend_win_tsrm_cache_slot; -# define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(0x1480 + zend_win_tsrm_cache_slot * 8)) +extern ZEND_TLS_API unsigned long zend_win_tsrm_cache_offset; +# define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(zend_win_tsrm_cache_offset)) # else # define ZEND_TSRM_CACHE_PTR (&_tsrm_ls_cache) # endif From 096ee6b4e5d0253c8ddf094fa10bb01e9ad7c69d Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 27 Aug 2026 16:22:41 +0200 Subject: [PATCH 18/38] feedback: move macros into tsrm.h --- TSRM/TSRM.h | 31 ++++++++++++++++++++++++++----- Zend/zend.c | 2 +- Zend/zend_globals_macros.h | 32 ++++---------------------------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index 2017f7e93b25..972d357852ce 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -186,15 +186,36 @@ TSRM_API bool tsrm_is_managed_thread(void); #define TSRMG_BULK_STATIC(id, type) ((type) (*((void ***) TSRMLS_CACHE))[TSRM_UNSHUFFLE_RSRC_ID(id)]) #define TSRMG_FAST_STATIC(offset, type, element) (TSRMG_FAST_BULK_STATIC(offset, type)->element) #define TSRMG_FAST_BULK_STATIC(offset, type) ((type) (((char*) TSRMLS_CACHE)+(offset))) -struct _zend_tsrm_ls_cache; -#if defined(ZEND_WIN32) && !defined(LIBZEND_EXPORTS) + /* Windows can't dllexport __declspec(thread) symbols, so outside Zend each module * keeps a per-module `void *` pointer and reaches EG/CG via the resource-id indirection. */ -# define ZEND_TSRMLS_CACHE_T void * -# define TSRMLS_CACHE_DEFINE() TSRM_TLS void *_tsrm_ls_cache = NULL; +#ifdef ZEND_WIN32 +# define ZEND_TLS_API +# ifdef LIBZEND_EXPORTS +# define ZEND_TLS_DIRECT 1 +# endif #else -# define ZEND_TSRMLS_CACHE_T struct _zend_tsrm_ls_cache +# define ZEND_TLS_API ZEND_API +# define ZEND_TLS_DIRECT 1 +#endif + +struct _zend_tsrm_ls_cache; +typedef struct _zend_tsrm_ls_cache zend_tsrm_ls_cache; + +#ifdef ZEND_TLS_DIRECT +# define ZEND_TSRMLS_CACHE_T zend_tsrm_ls_cache # define TSRMLS_CACHE_DEFINE() +extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache; +# if defined(_WIN64) && defined(_M_X64) +/* See zend.c: zend_win_tsrm_cache_init */ +extern unsigned long zend_win_tsrm_cache_offset; +# define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(zend_win_tsrm_cache_offset)) +# else +# define ZEND_TSRM_CACHE_PTR (&_tsrm_ls_cache) +# endif +#else +# define ZEND_TSRMLS_CACHE_T void * +# define TSRMLS_CACHE_DEFINE() TSRM_TLS void *_tsrm_ls_cache = NULL; #endif #ifdef __cplusplus #define TSRMLS_MAIN_CACHE_EXTERN() extern "C" { extern TSRM_TLS ZEND_TSRMLS_CACHE_T _tsrm_ls_cache TSRM_TLS_MODEL_ATTR; } diff --git a/Zend/zend.c b/Zend/zend.c index 8b70ed6e25bd..571e28fbc570 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -59,7 +59,7 @@ ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache = {0 #define ZEND_WIN_TEB_TLS_SLOTS 0x1480 static DWORD zend_win_tsrm_cache_slot = 0; -ZEND_TLS_API unsigned long zend_win_tsrm_cache_offset = 0; +unsigned long zend_win_tsrm_cache_offset = 0; ZEND_API void zend_win_tsrm_cache_init(bool alloc) { diff --git a/Zend/zend_globals_macros.h b/Zend/zend_globals_macros.h index bb1420ccdc84..89265ef4e7ef 100644 --- a/Zend/zend_globals_macros.h +++ b/Zend/zend_globals_macros.h @@ -26,32 +26,8 @@ typedef struct _zend_executor_globals zend_executor_globals; typedef struct _zend_php_scanner_globals zend_php_scanner_globals; typedef struct _zend_ini_scanner_globals zend_ini_scanner_globals; -#ifdef ZEND_WIN32 -# define ZEND_TLS_API -# ifdef LIBZEND_EXPORTS -# define ZEND_TLS_DIRECT 1 -# endif -#else -# define ZEND_TLS_API ZEND_API -# define ZEND_TLS_DIRECT 1 -#endif - BEGIN_EXTERN_C() -#ifdef ZTS -typedef struct _zend_tsrm_ls_cache zend_tsrm_ls_cache; -# ifdef ZEND_TLS_DIRECT -extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache; -/* See zend.c: zend_win_tsrm_cache_init */ -# if defined(_WIN64) && defined(_M_X64) -extern ZEND_TLS_API unsigned long zend_win_tsrm_cache_offset; -# define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(zend_win_tsrm_cache_offset)) -# else -# define ZEND_TSRM_CACHE_PTR (&_tsrm_ls_cache) -# endif -# endif -#endif - /* Compiler */ #ifdef ZTS # ifdef ZEND_TLS_DIRECT @@ -81,15 +57,15 @@ extern ZEND_API zend_executor_globals executor_globals; /* Language Scanner */ #ifdef ZTS extern ZEND_API ts_rsrc_id language_scanner_globals_id; -# if defined(ZEND_WIN32) && !defined(LIBZEND_EXPORTS) -# define LANG_SCNG(v) TSRMG(language_scanner_globals_id, zend_php_scanner_globals *, v) -# else +# ifdef ZEND_TLS_DIRECT # ifdef ZEND_WIN32 extern TSRM_TLS zend_php_scanner_globals language_scanner_globals; # else -extern ZEND_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_php_scanner_globals language_scanner_globals; +extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_php_scanner_globals language_scanner_globals; # endif # define LANG_SCNG(v) (language_scanner_globals.v) +# else +# define LANG_SCNG(v) TSRMG(language_scanner_globals_id, zend_php_scanner_globals *, v) # endif #else extern ZEND_API zend_php_scanner_globals language_scanner_globals; From b34b6c6fcf079962ded14d7dfcacbc49b5859ef1 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 27 Aug 2026 16:27:39 +0200 Subject: [PATCH 19/38] feedback: add zend_tsrmg_direct macro --- TSRM/TSRM.h | 2 ++ Zend/zend_globals_macros.h | 12 ++---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index 972d357852ce..e30faa06e920 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -213,9 +213,11 @@ extern unsigned long zend_win_tsrm_cache_offset; # else # define ZEND_TSRM_CACHE_PTR (&_tsrm_ls_cache) # endif +# define ZEND_TSRMG_DIRECT(id, type, member, element) (ZEND_TSRM_CACHE_PTR->member.element) #else # define ZEND_TSRMLS_CACHE_T void * # define TSRMLS_CACHE_DEFINE() TSRM_TLS void *_tsrm_ls_cache = NULL; +# define ZEND_TSRMG_DIRECT(id, type, member, element) ZEND_TSRMG(id, type, element) #endif #ifdef __cplusplus #define TSRMLS_MAIN_CACHE_EXTERN() extern "C" { extern TSRM_TLS ZEND_TSRMLS_CACHE_T _tsrm_ls_cache TSRM_TLS_MODEL_ATTR; } diff --git a/Zend/zend_globals_macros.h b/Zend/zend_globals_macros.h index 89265ef4e7ef..684ed696adf1 100644 --- a/Zend/zend_globals_macros.h +++ b/Zend/zend_globals_macros.h @@ -30,11 +30,7 @@ BEGIN_EXTERN_C() /* Compiler */ #ifdef ZTS -# ifdef ZEND_TLS_DIRECT -# define CG(v) (ZEND_TSRM_CACHE_PTR->cg.v) -# else -# define CG(v) ZEND_TSRMG(compiler_globals_id, zend_compiler_globals *, v) -# endif +# define CG(v) ZEND_TSRMG_DIRECT(compiler_globals_id, zend_compiler_globals *, cg, v) #else # define CG(v) (compiler_globals.v) extern ZEND_API struct _zend_compiler_globals compiler_globals; @@ -44,11 +40,7 @@ ZEND_API int zendparse(void); /* Executor */ #ifdef ZTS -# ifdef ZEND_TLS_DIRECT -# define EG(v) (ZEND_TSRM_CACHE_PTR->eg.v) -# else -# define EG(v) ZEND_TSRMG(executor_globals_id, zend_executor_globals *, v) -# endif +# define EG(v) ZEND_TSRMG_DIRECT(executor_globals_id, zend_executor_globals *, eg, v) #else # define EG(v) (executor_globals.v) extern ZEND_API zend_executor_globals executor_globals; From bed8bda74e09405c423e39e6eeb10eeafd5666da Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 27 Aug 2026 16:32:01 +0200 Subject: [PATCH 20/38] feedback: move zend_win_tsrm_cache_init to tsrm.c --- TSRM/TSRM.c | 36 ++++++++++++++++++++++++++++++++++++ TSRM/TSRM.h | 4 +++- Zend/zend.c | 38 -------------------------------------- win32/dllmain.c | 6 +----- 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index 00a400b72237..dc8ff4b8b883 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -29,6 +29,42 @@ typedef struct _tsrm_tls_entry tsrm_tls_entry; /* TSRMLS_CACHE_DEFINE; is already done in Zend, this is being always compiled statically. */ TSRMLS_CACHE_EXTERN(); +#ifdef ZEND_WIN_TSRM_TEB_SLOT +/* Holds &_tsrm_ls_cache in a TEB TLS slot so EG()/CG() reach it + * with a single gs:[] load rather than the 3-load __declspec(thread) lookup. */ +#define ZEND_WIN_TEB_TLS_SLOTS 0x1480 + +static DWORD zend_win_tsrm_cache_slot = 0; +unsigned long zend_win_tsrm_cache_offset = 0; + +ZEND_API void zend_win_tsrm_cache_init(bool alloc) +{ + if (alloc) { + zend_win_tsrm_cache_slot = TlsAlloc(); + if (zend_win_tsrm_cache_slot == TLS_OUT_OF_INDEXES) { + fprintf(stderr, "PHP Startup: TlsAlloc() failed, no TLS slot available " + "for the ZTS globals cache\n"); + abort(); + } + if (zend_win_tsrm_cache_slot >= TLS_MINIMUM_AVAILABLE) { + fprintf(stderr, "PHP Startup: TlsAlloc() returned slot %lu, but only the " + "first %d direct TEB slots are usable for the ZTS globals cache\n", + zend_win_tsrm_cache_slot, TLS_MINIMUM_AVAILABLE); + abort(); + } + zend_win_tsrm_cache_offset = ZEND_WIN_TEB_TLS_SLOTS + + zend_win_tsrm_cache_slot * (unsigned long) sizeof(void*); + } + TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); + /* Verify our layout assumptions work */ + if (ZEND_TSRM_CACHE_PTR != &_tsrm_ls_cache) { + fprintf(stderr, "PHP Startup: the ZTS globals cache is not reachable through " + "TEB offset %lu\n", zend_win_tsrm_cache_offset); + abort(); + } +} +#endif + struct _tsrm_tls_entry { void **storage; int count; diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index e30faa06e920..080373e9045e 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -207,8 +207,10 @@ typedef struct _zend_tsrm_ls_cache zend_tsrm_ls_cache; # define TSRMLS_CACHE_DEFINE() extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache; # if defined(_WIN64) && defined(_M_X64) -/* See zend.c: zend_win_tsrm_cache_init */ +/* See TSRM.c: zend_win_tsrm_cache_init */ +# define ZEND_WIN_TSRM_TEB_SLOT 1 extern unsigned long zend_win_tsrm_cache_offset; +ZEND_API void zend_win_tsrm_cache_init(bool alloc); # define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(zend_win_tsrm_cache_offset)) # else # define ZEND_TSRM_CACHE_PTR (&_tsrm_ls_cache) diff --git a/Zend/zend.c b/Zend/zend.c index 571e28fbc570..ecb3bd6736da 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -53,44 +53,6 @@ static bool startup_done = false; ZEND_API int compiler_globals_id; ZEND_API int executor_globals_id; ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cache = {0}; -#if defined(_WIN64) && defined(_M_X64) && defined(ZEND_TLS_DIRECT) -/* Holds &_tsrm_ls_cache in a TEB TLS slot (filled by DllMain) so EG()/CG() reach it - * with a single gs:[] load rather than the 3-load __declspec(thread) lookup. */ -#define ZEND_WIN_TEB_TLS_SLOTS 0x1480 - -static DWORD zend_win_tsrm_cache_slot = 0; -unsigned long zend_win_tsrm_cache_offset = 0; - -ZEND_API void zend_win_tsrm_cache_init(bool alloc) -{ - if (alloc) { - zend_win_tsrm_cache_slot = TlsAlloc(); - if (zend_win_tsrm_cache_slot == TLS_OUT_OF_INDEXES) { - fprintf(stderr, "PHP Startup: TlsAlloc() failed, no TLS slot available " - "for the ZTS globals cache\n"); - abort(); - } - if (zend_win_tsrm_cache_slot >= TLS_MINIMUM_AVAILABLE) { - /* Beyond the first TLS_MINIMUM_AVAILABLE slots the loader uses - * TEB->TlsExpansionSlots, which the inline gs:[] read in - * ZEND_TSRM_CACHE_PTR cannot reach. */ - fprintf(stderr, "PHP Startup: TlsAlloc() returned slot %lu, but only the " - "first %d direct TEB slots are usable for the ZTS globals cache\n", - zend_win_tsrm_cache_slot, TLS_MINIMUM_AVAILABLE); - abort(); - } - zend_win_tsrm_cache_offset = ZEND_WIN_TEB_TLS_SLOTS - + zend_win_tsrm_cache_slot * (unsigned long) sizeof(void*); - } - TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); - /* Verify our layout assumptions work */ - if (ZEND_TSRM_CACHE_PTR != &_tsrm_ls_cache) { - fprintf(stderr, "PHP Startup: the ZTS globals cache is not reachable through " - "TEB offset %lu\n", zend_win_tsrm_cache_offset); - abort(); - } -} -#endif /* ts_allocate_tls_id takes a callback so each thread resolves its own block. * A plain &..._tls would capture only the registering thread's address. */ static void *executor_globals_tls_addr(void) { return &_tsrm_ls_cache.eg; } diff --git a/win32/dllmain.c b/win32/dllmain.c index bb9386b007ac..5a4a57ed3f7d 100644 --- a/win32/dllmain.c +++ b/win32/dllmain.c @@ -27,15 +27,11 @@ eq. initializing something before the DLL even is available to be called. */ -#if defined(_WIN64) && defined(ZTS) -ZEND_API void zend_win_tsrm_cache_init(bool alloc); -#endif - BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID dummy) { BOOL ret = TRUE; -#if defined(_WIN64) && defined(ZTS) +#ifdef ZEND_WIN_TSRM_TEB_SLOT if (reason == DLL_PROCESS_ATTACH) { zend_win_tsrm_cache_init(true); } else if (reason == DLL_THREAD_ATTACH) { From 261db20bb1786be80e2786a5012541f6f1d9e1ac Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 27 Aug 2026 16:36:36 +0200 Subject: [PATCH 21/38] feedback: tsrm initializes tls slot --- TSRM/TSRM.c | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index dc8ff4b8b883..e2c1c9d0fefd 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -34,9 +34,22 @@ TSRMLS_CACHE_EXTERN(); * with a single gs:[] load rather than the 3-load __declspec(thread) lookup. */ #define ZEND_WIN_TEB_TLS_SLOTS 0x1480 -static DWORD zend_win_tsrm_cache_slot = 0; +static DWORD zend_win_tsrm_cache_slot = TLS_OUT_OF_INDEXES; unsigned long zend_win_tsrm_cache_offset = 0; +static void zend_win_tsrm_cache_publish(void) +{ + if (zend_win_tsrm_cache_slot != TLS_OUT_OF_INDEXES) { + TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); + /* Verify our layout assumptions work */ + if (ZEND_TSRM_CACHE_PTR != &_tsrm_ls_cache) { + fprintf(stderr, "PHP Startup: the ZTS globals cache is not reachable through " + "TEB offset %lu\n", zend_win_tsrm_cache_offset); + abort(); + } + } +} + ZEND_API void zend_win_tsrm_cache_init(bool alloc) { if (alloc) { @@ -55,14 +68,10 @@ ZEND_API void zend_win_tsrm_cache_init(bool alloc) zend_win_tsrm_cache_offset = ZEND_WIN_TEB_TLS_SLOTS + zend_win_tsrm_cache_slot * (unsigned long) sizeof(void*); } - TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); - /* Verify our layout assumptions work */ - if (ZEND_TSRM_CACHE_PTR != &_tsrm_ls_cache) { - fprintf(stderr, "PHP Startup: the ZTS globals cache is not reachable through " - "TEB offset %lu\n", zend_win_tsrm_cache_offset); - abort(); - } + zend_win_tsrm_cache_publish(); } +#else +# define zend_win_tsrm_cache_publish() do {} while (0) #endif struct _tsrm_tls_entry { @@ -457,6 +466,7 @@ static void set_thread_local_storage_resource_to(tsrm_tls_entry *thread_resource { tsrm_tls_set(thread_resource); TSRMLS_CACHE = thread_resource; + zend_win_tsrm_cache_publish(); } /* Must be called with tsmm_mutex held */ From 11afd15a3112aa0bc1910a2d0a1cf629fd679d6d Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 27 Aug 2026 17:36:51 +0200 Subject: [PATCH 22/38] perf: non-volatile gs read on clang --- TSRM/TSRM.c | 2 +- TSRM/TSRM.h | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index e2c1c9d0fefd..7f468ac34602 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -42,7 +42,7 @@ static void zend_win_tsrm_cache_publish(void) if (zend_win_tsrm_cache_slot != TLS_OUT_OF_INDEXES) { TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); /* Verify our layout assumptions work */ - if (ZEND_TSRM_CACHE_PTR != &_tsrm_ls_cache) { + if ((zend_tsrm_ls_cache *) __readgsqword(zend_win_tsrm_cache_offset) != &_tsrm_ls_cache) { fprintf(stderr, "PHP Startup: the ZTS globals cache is not reachable through " "TEB offset %lu\n", zend_win_tsrm_cache_offset); abort(); diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index 080373e9045e..ea82123c8920 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -211,7 +211,19 @@ extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cac # define ZEND_WIN_TSRM_TEB_SLOT 1 extern unsigned long zend_win_tsrm_cache_offset; ZEND_API void zend_win_tsrm_cache_init(bool alloc); -# define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(zend_win_tsrm_cache_offset)) +# ifdef __clang__ +static __inline__ __attribute__((const, always_inline)) zend_tsrm_ls_cache *zend_win_tsrm_cache_ptr(void) +{ + zend_tsrm_ls_cache *ptr; + __asm__ ("movq %%gs:(%1), %0" + : "=r" (ptr) + : "r" ((uintptr_t) zend_win_tsrm_cache_offset)); + return ptr; +} +# define ZEND_TSRM_CACHE_PTR zend_win_tsrm_cache_ptr() +# else +# define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(zend_win_tsrm_cache_offset)) +# endif # else # define ZEND_TSRM_CACHE_PTR (&_tsrm_ls_cache) # endif From f5b3499e7e1d6f3520129e38ec833af9bc7e4975 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 28 Aug 2026 16:11:20 +0200 Subject: [PATCH 23/38] tsrm: identify thread entries by pointer, not by recycled thread id ts_free_resources(), ts_free_thread(), ts_free_id() and ts_apply_for_id() matched entries with p->thread_id == tsrm_thread_id(). That is ambiguous in exactly the case the surrounding code exists to handle: a stale entry of a dead thread can carry the live thread's recycled id. Compare against the entry that tsrm_tls_get() hands out instead. The recycle path in ts_resource_ex() no longer runs the stale entry's destructors. It used to point the TLS cache at an entry whose native TLS block had died with its thread, so any destructor reaching for EG or CG read freed memory. Leaking the dead thread's module globals is the better trade; a child process that recycles thread ids gets respawned by the SAPI anyway. Hold the shutdown marker across the window so that signal handlers stay away from both the stale entry and its replacement. Also reset tls_key, the TSRM tables and TSRMLS_CACHE on shutdown. --- TSRM/TSRM.c | 91 ++++++++++++++++++++++++--------------------- TSRM/TSRM.h | 3 +- UPGRADING.INTERNALS | 5 +++ 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index 7f468ac34602..1552355e84e6 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -149,7 +149,7 @@ static FILE *tsrm_error_file; #endif #ifdef TSRM_WIN32 -static DWORD tls_key; +static DWORD tls_key = TLS_OUT_OF_INDEXES; # define tsrm_tls_set(what) TlsSetValue(tls_key, (void*)(what)) # define tsrm_tls_get() TlsGetValue(tls_key) #else @@ -207,18 +207,17 @@ TSRM_API bool tsrm_startup(int expected_threads, int expected_resources, int deb return 1; }/*}}}*/ -static void ts_free_resources(tsrm_tls_entry *thread_resources) +static void ts_free_resources( + tsrm_tls_entry *thread_resources, bool destroy_tls_resources, bool run_dtors) { - bool own_thread = thread_resources->thread_id == tsrm_thread_id(); - /* Need to destroy in reverse order to respect dependencies. */ for (int i = thread_resources->count - 1; i >= 0; i--) { if (!resource_types_table[i].done) { - /* A __thread block of a foreign thread is inaccessible. */ - if (resource_types_table[i].tls_addr && !own_thread) { + /* Native TLS may only be accessed by its owning thread. */ + if (resource_types_table[i].tls_addr && !destroy_tls_resources) { continue; } - if (resource_types_table[i].dtor) { + if (run_dtors && resource_types_table[i].dtor) { resource_types_table[i].dtor(thread_resources->storage[i]); } @@ -232,14 +231,16 @@ static void ts_free_resources(tsrm_tls_entry *thread_resources) } /* Shutdown TSRM (call once for the entire process). Tears down every thread left - * in the table. For resources allocated with ts_allocate_tls_id(), only the dtor - * of the calling thread is invoked. */ + * in the table. Native TLS dtors only run for the calling thread. */ TSRM_API void tsrm_shutdown(void) {/*{{{*/ + tsrm_tls_entry *current_thread_resources; + if (is_thread_shutdown) { /* shutdown must only occur once */ return; } + current_thread_resources = tsrm_tls_get(); is_thread_shutdown = true; @@ -255,7 +256,7 @@ TSRM_API void tsrm_shutdown(void) next_p = p->next; if (resource_types_table) { /* This call will already free p->storage for us */ - ts_free_resources(p); + ts_free_resources(p, p == current_thread_resources, true); } else { free(p->storage); } @@ -264,7 +265,9 @@ TSRM_API void tsrm_shutdown(void) } } free(tsrm_tls_table); + tsrm_tls_table = NULL; free(resource_types_table); + resource_types_table = NULL; tsrm_mutex_free(tsmm_mutex); tsrm_mutex_free(tsrm_env_mutex); TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Shutdown TSRM")); @@ -273,10 +276,12 @@ TSRM_API void tsrm_shutdown(void) } #ifdef TSRM_WIN32 TlsFree(tls_key); + tls_key = TLS_OUT_OF_INDEXES; #else pthread_setspecific(tls_key, 0); pthread_key_delete(tls_key); #endif + TSRMLS_CACHE = NULL; if (tsrm_shutdown_handler) { tsrm_shutdown_handler(); } @@ -569,29 +574,26 @@ TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id) * goes away, but its resources are never cleaned up, and then that thread ID is reused. * Since we don't always have a way to know when a thread goes away, we can't clean up * the thread's resources before the new thread spawns. - * To solve this issue, we'll free up the old thread resources gracefully (gracefully - * because there might still be resources open like database connection which need to - * be shut down cleanly). After freeing up, we'll create the new resources for this thread - * as if the stale resources never existed in the first place. From that point forward, - * it is as if that situation never occurred. + * The old native TLS is no longer accessible, so its destructors cannot run safely. + * Release the directly owned allocations and create fresh resources for the new thread. * The fact that this situation happens isn't that bad because a child process containing * threads will eventually be respawned anyway by the SAPI, so the stale threads won't last * forever. */ TSRM_ASSERT(thread_resources->thread_id == thread_id); if (thread_id == tsrm_thread_id() && !tsrm_tls_get()) { tsrm_tls_entry *next = thread_resources->next; - /* In case that extensions don't use the pointer passed from the dtor, but incorrectly - * use the global pointer, we need to setup the global pointer temporarily here. */ - set_thread_local_storage_resource_to(thread_resources); - /* Dead thread with a recycled id: its __thread blocks are gone, and this - * thread's blocks were never constructed, so keep tls dtors from running. */ - thread_resources->thread_id = 0; - ts_free_resources(thread_resources); + /* Keep signal handlers away from both the stale entry and the replacement + * until all of the replacement's resources have been constructed. */ + is_thread_shutdown = true; + /* The dead thread's native TLS is gone, and its remaining resource dtors + * may depend on those globals. Only release directly owned allocations. */ + ts_free_resources(thread_resources, false, false); free(thread_resources); /* Allocate a new resource at the same point in the linked list, and relink the next pointer */ allocate_new_resource(last_thread_resources, thread_id); thread_resources = *last_thread_resources; thread_resources->next = next; + is_thread_shutdown = false; /* We don't have to tail-call ts_resource_ex, we can take the fast path to the return * because we already have the correct pointer. */ } @@ -609,34 +611,39 @@ TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id) /* frees all resources allocated for the current thread */ void ts_free_thread(void) {/*{{{*/ - tsrm_tls_entry *thread_resources; - THREAD_T thread_id = tsrm_thread_id(); + tsrm_tls_entry *thread_resources = tsrm_tls_get(); + tsrm_tls_entry *p; int hash_value; tsrm_tls_entry *last=NULL; TSRM_ASSERT(!in_main_thread); + if (!thread_resources) { + return; + } tsrm_mutex_lock(tsmm_mutex); - hash_value = THREAD_HASH_OF(thread_id, tsrm_tls_table_size); - thread_resources = tsrm_tls_table[hash_value]; + hash_value = THREAD_HASH_OF(thread_resources->thread_id, tsrm_tls_table_size); + p = tsrm_tls_table[hash_value]; - while (thread_resources) { - if (thread_resources->thread_id == thread_id) { - ts_free_resources(thread_resources); + while (p) { + if (p == thread_resources) { + ts_free_resources(thread_resources, true, true); if (last) { - last->next = thread_resources->next; + last->next = p->next; } else { - tsrm_tls_table[hash_value] = thread_resources->next; + tsrm_tls_table[hash_value] = p->next; } tsrm_tls_set(0); + TSRMLS_CACHE = NULL; free(thread_resources); - break; - } - if (thread_resources->next) { - last = thread_resources; + tsrm_mutex_unlock(tsmm_mutex); + return; } - thread_resources = thread_resources->next; + last = p; + p = p->next; } + tsrm_tls_set(0); + TSRMLS_CACHE = NULL; tsrm_mutex_unlock(tsmm_mutex); }/*}}}*/ @@ -644,7 +651,7 @@ void ts_free_thread(void) void ts_free_id(ts_rsrc_id id) {/*{{{*/ int rsrc_id = TSRM_UNSHUFFLE_RSRC_ID(id); - THREAD_T this_thread = tsrm_thread_id(); + tsrm_tls_entry *current_thread_resources = tsrm_tls_get(); tsrm_mutex_lock(tsmm_mutex); @@ -656,9 +663,9 @@ void ts_free_id(ts_rsrc_id id) while (p) { if (p->count > rsrc_id && p->storage[rsrc_id]) { - /* A __thread block of a foreign thread is inaccessible. */ + /* Native TLS may only be accessed by its owning thread. */ if (resource_types_table - && (!resource_types_table[rsrc_id].tls_addr || p->thread_id == this_thread)) { + && (!resource_types_table[rsrc_id].tls_addr || p == current_thread_resources)) { if (resource_types_table[rsrc_id].dtor) { resource_types_table[rsrc_id].dtor(p->storage[rsrc_id]); } @@ -682,7 +689,7 @@ void ts_free_id(ts_rsrc_id id) TSRM_API void ts_apply_for_id(ts_rsrc_id id, void (*cb)(void *)) { int rsrc_id = TSRM_UNSHUFFLE_RSRC_ID(id); - THREAD_T this_thread = tsrm_thread_id(); + tsrm_tls_entry *current_thread_resources = tsrm_tls_get(); tsrm_mutex_lock(tsmm_mutex); @@ -693,9 +700,9 @@ TSRM_API void ts_apply_for_id(ts_rsrc_id id, void (*cb)(void *)) tsrm_tls_entry *p = tsrm_tls_table[i]; while (p) { - /* A __thread block of a foreign thread is inaccessible. */ + /* Native TLS may only be accessed by its owning thread. */ if (p->count > rsrc_id && p->storage[rsrc_id] - && (!tls_backed || p->thread_id == this_thread)) { + && (!tls_backed || p == current_thread_resources)) { cb(p->storage[rsrc_id]); } p = p->next; diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index ea82123c8920..6bdd7f994ebd 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -96,7 +96,8 @@ TSRM_API void tsrm_reserve(size_t size); TSRM_API ts_rsrc_id ts_allocate_fast_id(ts_rsrc_id *rsrc_id, size_t *offset, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor); /* Resource whose per-thread storage is a native __thread block. - * Must be called at startup before any other thread exists. */ + * Must be called at startup before any other thread exists. Its destructor may + * only run on the owning thread. */ TSRM_API ts_rsrc_id ts_allocate_tls_id(ts_rsrc_id *rsrc_id, void *(*tls_addr)(void), size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor); /* fetches the requested resource for the current thread */ diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 7eccce2f751b..225352e1efda 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -61,3 +61,8 @@ PHP 8.7 INTERNALS UPGRADE NOTES . The force_http_10 method has been removed. . The get_fd method has been removed. . The get_target_uid and get_target_gid methods have been removed. + +- SAPIs should explicitly release a thread's resources by calling + ts_free_thread() before terminating it. Native TLS resource destructors only + run on their owning thread; tsrm_shutdown() cannot release them afterwards. + All PHP-managed worker threads must be quiescent before tsrm_shutdown(). From 6f60a87c7fe169e34b025cad1c09f97a020c3749 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 28 Aug 2026 16:40:34 +0200 Subject: [PATCH 24/38] fix tests --- ext/opcache/jit/tls/testing/test.sh | 4 +++- ext/opcache/jit/tls/testing/user.c | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/opcache/jit/tls/testing/test.sh b/ext/opcache/jit/tls/testing/test.sh index c6b003caafdd..f184eee57fa5 100755 --- a/ext/opcache/jit/tls/testing/test.sh +++ b/ext/opcache/jit/tls/testing/test.sh @@ -208,7 +208,9 @@ for CC in clang gcc; do esac for LD in $LDs; do for opt in -O0 -O3; do - CFLAGS="$MACHINE $MUSL $opt -Werror -I$root/ext/opcache -I$root/Zend -I$root" + # def.c stubs tsrm_get_ls_cache_tcb_offset() to 0, which only happens under + # global-dynamic, so reference the cache with that model too. + CFLAGS="$MACHINE $MUSL $opt -Werror -DTSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC -I$root/ext/opcache -I$root/Zend -I$root" LDFLAGS="$MACHINE -fuse-ld=$LD $RPATH" for pic in "-fPIC" "-fno-PIC $STATIC"; do diff --git a/ext/opcache/jit/tls/testing/user.c b/ext/opcache/jit/tls/testing/user.c index c27e608f3f4b..446b876033d2 100644 --- a/ext/opcache/jit/tls/testing/user.c +++ b/ext/opcache/jit/tls/testing/user.c @@ -3,8 +3,6 @@ #include "../zend_jit_tls.h" -extern __thread void* _tsrm_ls_cache; - int test(void) { size_t tcb_offset = 0; @@ -12,7 +10,7 @@ int test(void) size_t module_offset = -1; /* Ensure the slot is allocated */ - _tsrm_ls_cache = NULL; + void *cache = &_tsrm_ls_cache; zend_result result = zend_jit_resolve_tsrm_ls_cache_offsets( &tcb_offset, &module_index, &module_offset); @@ -24,5 +22,5 @@ int test(void) return 0; } - return zend_jit_tsrm_ls_cache_address(tcb_offset, module_index, module_offset) == &_tsrm_ls_cache; + return zend_jit_tsrm_ls_cache_address(tcb_offset, module_index, module_offset) == cache; } From c2d4528b418451fa13120d73d057aa3c552a8841 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 28 Aug 2026 17:41:02 +0200 Subject: [PATCH 25/38] fix gdb script --- .gdbinit | 20 ++++++++++++-------- main/debug_gdb_scripts.c | 20 ++++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.gdbinit b/.gdbinit index 4477828265e0..df4cbffb5fa0 100644 --- a/.gdbinit +++ b/.gdbinit @@ -3,18 +3,22 @@ define set_ts end document set_ts - set the ts resource, it is impossible for gdb to - call ts_resource_ex while no process is running, - but we could get the resource from the argument - of frame info. + set a legacy TSRM resource cache pointer when it is available + from a frame argument. Pass 0 to use the current thread's + direct executor and compiler globals. end define ____executor_globals if basic_functions_module.zts - set $tsrm_ls = _tsrm_ls_cache - set $eg = ((zend_executor_globals*) (*((void ***) $tsrm_ls))[executor_globals_id-1]) - set $cg = ((zend_compiler_globals*) (*((void ***) $tsrm_ls))[compiler_globals_id-1]) - set $eg_ptr = $eg + if !$tsrm_ls + set $eg = _tsrm_ls_cache.eg + set $cg = _tsrm_ls_cache.cg + set $eg_ptr = (zend_executor_globals*) &_tsrm_ls_cache.eg + else + set $eg = ((zend_executor_globals*) (*((void ***) $tsrm_ls))[executor_globals_id-1]) + set $cg = ((zend_compiler_globals*) (*((void ***) $tsrm_ls))[compiler_globals_id-1]) + set $eg_ptr = $eg + end else set $eg = executor_globals set $cg = compiler_globals diff --git a/main/debug_gdb_scripts.c b/main/debug_gdb_scripts.c index 0fb9450388dc..57d204b8d92a 100644 --- a/main/debug_gdb_scripts.c +++ b/main/debug_gdb_scripts.c @@ -16,18 +16,22 @@ __asm__( ".ascii \"end\\n\"\n" ".ascii \"\\n\"\n" ".ascii \"document set_ts\\n\"\n" - ".ascii \" set the ts resource, it is impossible for gdb to\\n\"\n" - ".ascii \" call ts_resource_ex while no process is running,\\n\"\n" - ".ascii \" but we could get the resource from the argument\\n\"\n" - ".ascii \" of frame info.\\n\"\n" + ".ascii \" set a legacy TSRM resource cache pointer when it is available\\n\"\n" + ".ascii \" from a frame argument. Pass 0 to use the current thread\\\\'s\\n\"\n" + ".ascii \" direct executor and compiler globals.\\n\"\n" ".ascii \"end\\n\"\n" ".ascii \"\\n\"\n" ".ascii \"define ____executor_globals\\n\"\n" ".ascii \" if basic_functions_module.zts\\n\"\n" - ".ascii \" set $tsrm_ls = _tsrm_ls_cache\\n\"\n" - ".ascii \" set $eg = ((zend_executor_globals*) (*((void ***) $tsrm_ls))[executor_globals_id-1])\\n\"\n" - ".ascii \" set $cg = ((zend_compiler_globals*) (*((void ***) $tsrm_ls))[compiler_globals_id-1])\\n\"\n" - ".ascii \" set $eg_ptr = $eg\\n\"\n" + ".ascii \" if !$tsrm_ls\\n\"\n" + ".ascii \" set $eg = _tsrm_ls_cache.eg\\n\"\n" + ".ascii \" set $cg = _tsrm_ls_cache.cg\\n\"\n" + ".ascii \" set $eg_ptr = (zend_executor_globals*) &_tsrm_ls_cache.eg\\n\"\n" + ".ascii \" else\\n\"\n" + ".ascii \" set $eg = ((zend_executor_globals*) (*((void ***) $tsrm_ls))[executor_globals_id-1])\\n\"\n" + ".ascii \" set $cg = ((zend_compiler_globals*) (*((void ***) $tsrm_ls))[compiler_globals_id-1])\\n\"\n" + ".ascii \" set $eg_ptr = $eg\\n\"\n" + ".ascii \" end\\n\"\n" ".ascii \" else\\n\"\n" ".ascii \" set $eg = executor_globals\\n\"\n" ".ascii \" set $cg = compiler_globals\\n\"\n" From a3c09d3174868f1a611cd7516645fc43452fc269 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 28 Aug 2026 17:41:30 +0200 Subject: [PATCH 26/38] fall back to &_tsrm_ls_cache in case something blew the early TEB slot budget --- TSRM/TSRM.c | 43 ++++++++++++++++++++++++------------------- TSRM/TSRM.h | 18 +++++++++++++++--- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index 1552355e84e6..d39b26951e64 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -37,36 +37,41 @@ TSRMLS_CACHE_EXTERN(); static DWORD zend_win_tsrm_cache_slot = TLS_OUT_OF_INDEXES; unsigned long zend_win_tsrm_cache_offset = 0; +ZEND_API zend_tsrm_ls_cache *zend_win_tsrm_cache_fallback(void) +{ + return &_tsrm_ls_cache; +} + static void zend_win_tsrm_cache_publish(void) { - if (zend_win_tsrm_cache_slot != TLS_OUT_OF_INDEXES) { - TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); - /* Verify our layout assumptions work */ - if ((zend_tsrm_ls_cache *) __readgsqword(zend_win_tsrm_cache_offset) != &_tsrm_ls_cache) { - fprintf(stderr, "PHP Startup: the ZTS globals cache is not reachable through " - "TEB offset %lu\n", zend_win_tsrm_cache_offset); - abort(); - } + if (zend_win_tsrm_cache_slot == TLS_OUT_OF_INDEXES) { + return; + } + TlsSetValue(zend_win_tsrm_cache_slot, &_tsrm_ls_cache); + /* Verify our layout assumptions work */ + if ((zend_tsrm_ls_cache *) __readgsqword(zend_win_tsrm_cache_offset) != &_tsrm_ls_cache) { + fprintf(stderr, "PHP Startup: the ZTS globals cache is not reachable through " + "TEB offset %lu, falling back to __declspec(thread)\n", + zend_win_tsrm_cache_offset); + zend_win_tsrm_cache_offset = 0; + zend_win_tsrm_cache_slot = TLS_OUT_OF_INDEXES; } } ZEND_API void zend_win_tsrm_cache_init(bool alloc) { if (alloc) { - zend_win_tsrm_cache_slot = TlsAlloc(); - if (zend_win_tsrm_cache_slot == TLS_OUT_OF_INDEXES) { - fprintf(stderr, "PHP Startup: TlsAlloc() failed, no TLS slot available " - "for the ZTS globals cache\n"); - abort(); + DWORD slot = TlsAlloc(); + if (slot == TLS_OUT_OF_INDEXES) { + return; } - if (zend_win_tsrm_cache_slot >= TLS_MINIMUM_AVAILABLE) { - fprintf(stderr, "PHP Startup: TlsAlloc() returned slot %lu, but only the " - "first %d direct TEB slots are usable for the ZTS globals cache\n", - zend_win_tsrm_cache_slot, TLS_MINIMUM_AVAILABLE); - abort(); + if (slot >= TLS_MINIMUM_AVAILABLE) { + TlsFree(slot); + return; } + zend_win_tsrm_cache_slot = slot; zend_win_tsrm_cache_offset = ZEND_WIN_TEB_TLS_SLOTS - + zend_win_tsrm_cache_slot * (unsigned long) sizeof(void*); + + slot * (unsigned long) sizeof(void*); } zend_win_tsrm_cache_publish(); } diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index 6bdd7f994ebd..b85daf4c3d1d 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -212,19 +212,31 @@ extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cac # define ZEND_WIN_TSRM_TEB_SLOT 1 extern unsigned long zend_win_tsrm_cache_offset; ZEND_API void zend_win_tsrm_cache_init(bool alloc); +ZEND_API zend_tsrm_ls_cache *zend_win_tsrm_cache_fallback(void); # ifdef __clang__ static __inline__ __attribute__((const, always_inline)) zend_tsrm_ls_cache *zend_win_tsrm_cache_ptr(void) { + uintptr_t offset = zend_win_tsrm_cache_offset; zend_tsrm_ls_cache *ptr; + if (__builtin_expect(offset == 0, 0)) { + return zend_win_tsrm_cache_fallback(); + } __asm__ ("movq %%gs:(%1), %0" : "=r" (ptr) - : "r" ((uintptr_t) zend_win_tsrm_cache_offset)); + : "r" (offset)); return ptr; } -# define ZEND_TSRM_CACHE_PTR zend_win_tsrm_cache_ptr() # else -# define ZEND_TSRM_CACHE_PTR ((zend_tsrm_ls_cache*)__readgsqword(zend_win_tsrm_cache_offset)) +static __forceinline zend_tsrm_ls_cache *zend_win_tsrm_cache_ptr(void) +{ + unsigned long offset = zend_win_tsrm_cache_offset; + if (offset == 0) { + return zend_win_tsrm_cache_fallback(); + } + return (zend_tsrm_ls_cache *) __readgsqword(offset); +} # endif +# define ZEND_TSRM_CACHE_PTR zend_win_tsrm_cache_ptr() # else # define ZEND_TSRM_CACHE_PTR (&_tsrm_ls_cache) # endif From 02ae5f7653196236e0077ab9e492e9d8e65dc5d8 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 28 Aug 2026 23:12:33 +0200 Subject: [PATCH 27/38] suggestion: TlsFree in case of dll unload or late-TEB fallback --- TSRM/TSRM.c | 12 ++++++++++-- TSRM/TSRM.h | 1 + win32/dllmain.c | 10 ++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index d39b26951e64..a81fce931c30 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -42,6 +42,15 @@ ZEND_API zend_tsrm_ls_cache *zend_win_tsrm_cache_fallback(void) return &_tsrm_ls_cache; } +ZEND_API void zend_win_tsrm_cache_shutdown(void) +{ + zend_win_tsrm_cache_offset = 0; + if (zend_win_tsrm_cache_slot != TLS_OUT_OF_INDEXES) { + TlsFree(zend_win_tsrm_cache_slot); + zend_win_tsrm_cache_slot = TLS_OUT_OF_INDEXES; + } +} + static void zend_win_tsrm_cache_publish(void) { if (zend_win_tsrm_cache_slot == TLS_OUT_OF_INDEXES) { @@ -53,8 +62,7 @@ static void zend_win_tsrm_cache_publish(void) fprintf(stderr, "PHP Startup: the ZTS globals cache is not reachable through " "TEB offset %lu, falling back to __declspec(thread)\n", zend_win_tsrm_cache_offset); - zend_win_tsrm_cache_offset = 0; - zend_win_tsrm_cache_slot = TLS_OUT_OF_INDEXES; + zend_win_tsrm_cache_shutdown(); } } diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index b85daf4c3d1d..f7492fe87609 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -212,6 +212,7 @@ extern ZEND_TLS_API TSRM_TLS TSRM_TLS_MODEL_ATTR zend_tsrm_ls_cache _tsrm_ls_cac # define ZEND_WIN_TSRM_TEB_SLOT 1 extern unsigned long zend_win_tsrm_cache_offset; ZEND_API void zend_win_tsrm_cache_init(bool alloc); +ZEND_API void zend_win_tsrm_cache_shutdown(void); ZEND_API zend_tsrm_ls_cache *zend_win_tsrm_cache_fallback(void); # ifdef __clang__ static __inline__ __attribute__((const, always_inline)) zend_tsrm_ls_cache *zend_win_tsrm_cache_ptr(void) diff --git a/win32/dllmain.c b/win32/dllmain.c index 5a4a57ed3f7d..b810b774d4d7 100644 --- a/win32/dllmain.c +++ b/win32/dllmain.c @@ -27,7 +27,7 @@ eq. initializing something before the DLL even is available to be called. */ -BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID dummy) +BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved) { BOOL ret = TRUE; @@ -36,6 +36,12 @@ BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID dummy) zend_win_tsrm_cache_init(true); } else if (reason == DLL_THREAD_ATTACH) { zend_win_tsrm_cache_init(false); + } else if (reason == DLL_PROCESS_DETACH && reserved == NULL) { + /* https://learn.microsoft.com/en-us/windows/win32/dlls/dllmain + * When handling DLL_PROCESS_DETACH, a DLL should free resources + * such as heap memory only if the DLL is being unloaded + * dynamically (the lpvReserved parameter is NULL). */ + zend_win_tsrm_cache_shutdown(); } #endif @@ -45,7 +51,7 @@ BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID dummy) TODO Also this should be revisited as no initialization might be needed for TS build (libxml build with TLS support. */ - ret = ret && xmlDllMain(inst, reason, dummy); + ret = ret && xmlDllMain(inst, reason, reserved); #endif return ret; From 6b94d631561c7e0159a4452f2ceb917509fc4f79 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 4 Sep 2026 23:16:55 +0200 Subject: [PATCH 28/38] let ts_free_thread() clear EG(persistent_list) to prevent leaks --- TSRM/TSRM.c | 13 +++++++++++++ TSRM/TSRM.h | 2 ++ Zend/zend.c | 23 +++++++++++++---------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index a81fce931c30..6393adc8c411 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -125,6 +125,7 @@ static MUTEX_T tsrm_env_mutex; /* tsrm environ mutex */ static tsrm_thread_begin_func_t tsrm_new_thread_begin_handler = NULL; static tsrm_thread_end_func_t tsrm_new_thread_end_handler = NULL; static tsrm_shutdown_func_t tsrm_shutdown_handler = NULL; +static tsrm_shutdown_func_t tsrm_thread_free_handler = NULL; /* Debug support */ int tsrm_error(int level, const char *format, ...); @@ -301,6 +302,7 @@ TSRM_API void tsrm_shutdown(void) tsrm_new_thread_begin_handler = NULL; tsrm_new_thread_end_handler = NULL; tsrm_shutdown_handler = NULL; + tsrm_thread_free_handler = NULL; tsrm_reserved_pos = 0; tsrm_reserved_size = 0; @@ -634,6 +636,11 @@ void ts_free_thread(void) return; } + /* Release resources that depend on TSRM before taking its lock. */ + if (tsrm_thread_free_handler) { + tsrm_thread_free_handler(); + } + tsrm_mutex_lock(tsmm_mutex); hash_value = THREAD_HASH_OF(thread_resources->thread_id, tsrm_tls_table_size); p = tsrm_tls_table[hash_value]; @@ -848,6 +855,12 @@ TSRM_API void *tsrm_set_shutdown_handler(tsrm_shutdown_func_t shutdown_handler) }/*}}}*/ +void tsrm_set_thread_free_handler(tsrm_shutdown_func_t thread_free_handler) +{ + tsrm_thread_free_handler = thread_free_handler; +} + + /* * Debug support */ diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index f7492fe87609..cd2ffff80194 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -140,6 +140,8 @@ TSRM_API int tsrm_sigmask(int how, const sigset_t *set, sigset_t *oldset); TSRM_API void *tsrm_set_new_thread_begin_handler(tsrm_thread_begin_func_t new_thread_begin_handler); TSRM_API void *tsrm_set_new_thread_end_handler(tsrm_thread_end_func_t new_thread_end_handler); TSRM_API void *tsrm_set_shutdown_handler(tsrm_shutdown_func_t shutdown_handler); +/* Internal Zend hook, run before any of the thread's resources are destroyed. */ +void tsrm_set_thread_free_handler(tsrm_shutdown_func_t thread_free_handler); TSRM_API void *tsrm_get_ls_cache(void); TSRM_API size_t tsrm_get_ls_cache_tcb_offset(void); diff --git a/Zend/zend.c b/Zend/zend.c index ecb3bd6736da..eff4e2c6a508 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -62,7 +62,6 @@ static HashTable *global_function_table = NULL; static HashTable *global_class_table = NULL; static HashTable *global_constants_table = NULL; static HashTable *global_auto_globals_table = NULL; -static HashTable *global_persistent_list = NULL; # define GLOBAL_FUNCTION_TABLE global_function_table # define GLOBAL_CLASS_TABLE global_class_table # define GLOBAL_CONSTANTS_TABLE global_constants_table @@ -858,13 +857,17 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{ } /* }}} */ -static void executor_globals_persistent_list_dtor(void *storage) +static void zend_thread_free_handler(void) { - zend_executor_globals *executor_globals = storage; + volatile bool completed = false; - if (&executor_globals->persistent_list != global_persistent_list) { - zend_destroy_rsrc_list(&executor_globals->persistent_list); - } + do { + zend_try { + zend_destroy_rsrc_list(&EG(persistent_list)); + completed = true; + } zend_end_try(); + } while (!completed); + zend_init_rsrc_plist(); } static void executor_globals_dtor(zend_executor_globals *executor_globals) /* {{{ */ @@ -1084,6 +1087,7 @@ void zend_startup(zend_utility_functions *utility_functions) /* {{{ */ #ifdef ZTS tsrm_set_new_thread_end_handler(zend_new_thread_end_handler); + tsrm_set_thread_free_handler(zend_thread_free_handler); tsrm_set_shutdown_handler(zend_interned_strings_dtor); #endif @@ -1154,7 +1158,6 @@ zend_result zend_post_startup(void) /* {{{ */ EG(zend_constants) = NULL; executor_globals_ctor(executor_globals); - global_persistent_list = &EG(persistent_list); zend_copy_ini_directives(); #else global_map_ptr_last = CG(map_ptr_last); @@ -1171,12 +1174,12 @@ zend_result zend_post_startup(void) /* {{{ */ void zend_shutdown(void) /* {{{ */ { +#ifdef ZTS + tsrm_set_thread_free_handler(NULL); +#endif zend_vm_dtor(); zend_destroy_rsrc_list(&EG(persistent_list)); -#ifdef ZTS - ts_apply_for_id(executor_globals_id, executor_globals_persistent_list_dtor); -#endif zend_destroy_modules(); virtual_cwd_deactivate(); From 10f8c5570cbeaa5db7a7799e90b5d50e989e8c44 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 4 Sep 2026 23:20:46 +0200 Subject: [PATCH 29/38] attach automatic pthread_key/fls callbacks to automatically call `ts_free_thread` at thread exit SAPI's should already been doing this on their own before, but the apache2handler doesn't get control over threads, so it didn't call ts_free_thread and therefore started leaking resources with the move of per-thread globals from heap storage to native __thread storage --- TSRM/TSRM.c | 134 +++++++++++++++++++++++++++++++++++++++++++- TSRM/TSRM.h | 3 + UPGRADING.INTERNALS | 14 +++-- win32/dllmain.c | 5 ++ 4 files changed, 151 insertions(+), 5 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index 6393adc8c411..2b3c8c7b1dc5 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -92,6 +92,9 @@ struct _tsrm_tls_entry { int count; THREAD_T thread_id; tsrm_tls_entry *next; +#ifdef TSRM_WIN32 + void *thread_exit_data; +#endif }; typedef struct { @@ -162,19 +165,128 @@ static FILE *tsrm_error_file; } #endif +static bool tsrm_thread_exit_armed = false; + #ifdef TSRM_WIN32 static DWORD tls_key = TLS_OUT_OF_INDEXES; +static DWORD tsrm_exit_key = FLS_OUT_OF_INDEXES; +static BOOLEAN (NTAPI *tsrm_dll_shutdown_in_progress)(void); + +typedef struct { + tsrm_tls_entry *thread_resources; +} tsrm_thread_exit_data; + +# define TSRM_THREAD_EXIT_CC WINAPI # define tsrm_tls_set(what) TlsSetValue(tls_key, (void*)(what)) # define tsrm_tls_get() TlsGetValue(tls_key) + +static void tsrm_exit_key_set(tsrm_tls_entry *thread_resources) +{ + tsrm_thread_exit_data *data; + + if (!tsrm_thread_exit_armed) { + return; + } + + data = FlsGetValue(tsrm_exit_key); + if (!thread_resources) { + if (data && FlsSetValue(tsrm_exit_key, NULL)) { + free(data); + } + return; + } + + if (!data) { + data = malloc(sizeof(tsrm_thread_exit_data)); + if (!data) { + return; + } + data->thread_resources = thread_resources; + if (!FlsSetValue(tsrm_exit_key, data)) { + free(data); + return; + } + thread_resources->thread_exit_data = data; + return; + } + data->thread_resources = thread_resources; + thread_resources->thread_exit_data = data; +} #else static pthread_key_t tls_key; +static pthread_key_t tsrm_exit_key; +# define TSRM_THREAD_EXIT_CC # define tsrm_tls_set(what) pthread_setspecific(tls_key, (void*)(what)) # define tsrm_tls_get() pthread_getspecific(tls_key) +# define tsrm_exit_key_set(what) do { if (tsrm_thread_exit_armed) { pthread_setspecific(tsrm_exit_key, (void*)(what)); } } while (0) #endif TSRM_TLS bool in_main_thread = false; TSRM_TLS bool is_thread_shutdown = false; +static void TSRM_THREAD_EXIT_CC tsrm_thread_exit_handler(void *arg) +{ +#ifdef TSRM_WIN32 + tsrm_thread_exit_data *data = arg; + tsrm_tls_entry *thread_resources; + + if (tsrm_dll_shutdown_in_progress()) { + return; + } + /* FLS callbacks also run when another fiber is deleted. TSRM storage is + * thread-wide, so only tear it down for the exiting execution context. */ + if (!tsrm_thread_exit_armed) { + free(data); + return; + } + if (FlsGetValue(tsrm_exit_key) != data) { + free(data); + return; + } + FlsSetValue(tsrm_exit_key, NULL); + thread_resources = data->thread_resources; + if (thread_resources != TSRMLS_CACHE || thread_resources->thread_exit_data != data + || in_main_thread || !tsrm_tls_table) { + free(data); + return; + } +#else + tsrm_tls_entry *thread_resources = (tsrm_tls_entry *) arg; + + if (!tsrm_thread_exit_armed || arg != TSRMLS_CACHE || in_main_thread || !tsrm_tls_table) { + return; + } +#endif + + tsrm_tls_set(thread_resources); + ts_free_thread(); +#ifdef TSRM_WIN32 + free(data); +#endif +} + +TSRM_API void tsrm_thread_exit_disarm(void) +{ + if (!tsrm_thread_exit_armed) { + return; + } + tsrm_thread_exit_armed = false; +#ifdef TSRM_WIN32 + DWORD key = tsrm_exit_key; + tsrm_exit_key = FLS_OUT_OF_INDEXES; + FlsFree(key); +#else + pthread_key_delete(tsrm_exit_key); +#endif +} + +#if !defined(TSRM_WIN32) && defined(__GNUC__) +static void __attribute__((destructor)) tsrm_thread_exit_unload(void) +{ + tsrm_thread_exit_disarm(); +} +#endif + /* Startup TSRM (call once for the entire process) */ TSRM_API bool tsrm_startup(int expected_threads, int expected_resources, int debug_level, const char *debug_filename) {/*{{{*/ @@ -218,6 +330,14 @@ TSRM_API bool tsrm_startup(int expected_threads, int expected_resources, int deb tsrm_env_mutex = tsrm_mutex_alloc(); +#ifdef TSRM_WIN32 + tsrm_dll_shutdown_in_progress = (BOOLEAN (NTAPI *)(void)) (void *) GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlDllShutdownInProgress"); + tsrm_exit_key = tsrm_dll_shutdown_in_progress ? FlsAlloc(tsrm_thread_exit_handler) : FLS_OUT_OF_INDEXES; + tsrm_thread_exit_armed = tsrm_exit_key != FLS_OUT_OF_INDEXES; +#else + tsrm_thread_exit_armed = pthread_key_create(&tsrm_exit_key, tsrm_thread_exit_handler) == 0; +#endif + return 1; }/*}}}*/ @@ -263,6 +383,8 @@ TSRM_API void tsrm_shutdown(void) return; } + tsrm_thread_exit_disarm(); + for (int i=0; icount = id_count; (*thread_resources_ptr)->thread_id = thread_id; (*thread_resources_ptr)->next = NULL; +#ifdef TSRM_WIN32 + (*thread_resources_ptr)->thread_exit_data = NULL; +#endif /* Set thread local storage to this new thread resources structure */ set_thread_local_storage_resource_to(*thread_resources_ptr); @@ -529,6 +654,11 @@ static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_ if (tsrm_new_thread_end_handler) { tsrm_new_thread_end_handler(thread_id); } + + /* A thread exiting from a resource constructor cannot safely clean up an + * entry that is still being built while tsmm_mutex is held. */ + tsrm_exit_key_set(*thread_resources_ptr); + is_thread_shutdown = false; }/*}}}*/ /* fetches the requested resource for the current thread */ @@ -608,7 +738,6 @@ TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id) allocate_new_resource(last_thread_resources, thread_id); thread_resources = *last_thread_resources; thread_resources->next = next; - is_thread_shutdown = false; /* We don't have to tail-call ts_resource_ex, we can take the fast path to the return * because we already have the correct pointer. */ } @@ -636,6 +765,7 @@ void ts_free_thread(void) return; } + is_thread_shutdown = true; /* Release resources that depend on TSRM before taking its lock. */ if (tsrm_thread_free_handler) { tsrm_thread_free_handler(); @@ -654,6 +784,7 @@ void ts_free_thread(void) tsrm_tls_table[hash_value] = p->next; } tsrm_tls_set(0); + tsrm_exit_key_set(0); TSRMLS_CACHE = NULL; free(thread_resources); tsrm_mutex_unlock(tsmm_mutex); @@ -663,6 +794,7 @@ void ts_free_thread(void) p = p->next; } tsrm_tls_set(0); + tsrm_exit_key_set(0); TSRMLS_CACHE = NULL; tsrm_mutex_unlock(tsmm_mutex); }/*}}}*/ diff --git a/TSRM/TSRM.h b/TSRM/TSRM.h index cd2ffff80194..8c479f6d13ac 100644 --- a/TSRM/TSRM.h +++ b/TSRM/TSRM.h @@ -107,6 +107,9 @@ TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id); /* frees all resources allocated for the current thread */ TSRM_API void ts_free_thread(void); +/* Disable process-wide automatic thread-exit cleanup. */ +TSRM_API void tsrm_thread_exit_disarm(void); + /* deallocates all occurrences of a given id */ TSRM_API void ts_free_id(ts_rsrc_id id); diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 225352e1efda..2a6f3c492a4b 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -62,7 +62,13 @@ PHP 8.7 INTERNALS UPGRADE NOTES . The get_fd method has been removed. . The get_target_uid and get_target_gid methods have been removed. -- SAPIs should explicitly release a thread's resources by calling - ts_free_thread() before terminating it. Native TLS resource destructors only - run on their owning thread; tsrm_shutdown() cannot release them afterwards. - All PHP-managed worker threads must be quiescent before tsrm_shutdown(). +- Per-thread TSRM resources are now released best-effort at normal thread exit, + because native TLS resource destructors can only run on their owning thread and + tsrm_shutdown() cannot release them afterwards. Prefer explicitly calling ts_free_thread(). + +- tsrm_shutdown() and the bundled library-unload hooks automatically disarm + thread-exit cleanup. Embedders must stop and join all PHP worker threads + before calling tsrm_shutdown() or unloading PHP. + +- EG, CG, AG and SCNG are now allocated with ts_allocate_tls_id() and live in native + __thread storage in ZTS builds. diff --git a/win32/dllmain.c b/win32/dllmain.c index b810b774d4d7..cbc8fce8ff45 100644 --- a/win32/dllmain.c +++ b/win32/dllmain.c @@ -31,6 +31,11 @@ BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved) { BOOL ret = TRUE; +#ifdef ZTS + if (reason == DLL_PROCESS_DETACH && reserved == NULL) { + tsrm_thread_exit_disarm(); + } +#endif #ifdef ZEND_WIN_TSRM_TEB_SLOT if (reason == DLL_PROCESS_ATTACH) { zend_win_tsrm_cache_init(true); From 28ea67abaf1f4bc6ee56e68af0e20f16e19eeb63 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sun, 20 Sep 2026 22:04:41 +0200 Subject: [PATCH 30/38] set up _tlv_atexit() for macOS plist cleanup --- TSRM/TSRM.c | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index 2b3c8c7b1dc5..598ff659db89 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -17,6 +17,10 @@ #include #include +#ifdef __APPLE__ +# include +#endif + #if ZEND_DEBUG # include # define TSRM_ASSERT(c) assert(c) @@ -218,7 +222,28 @@ static pthread_key_t tsrm_exit_key; # define TSRM_THREAD_EXIT_CC # define tsrm_tls_set(what) pthread_setspecific(tls_key, (void*)(what)) # define tsrm_tls_get() pthread_getspecific(tls_key) -# define tsrm_exit_key_set(what) do { if (tsrm_thread_exit_armed) { pthread_setspecific(tsrm_exit_key, (void*)(what)); } } while (0) + +# ifdef __APPLE__ +static TSRM_TLS bool tsrm_thread_exit_registered = false; +static void tsrm_thread_exit_handler(void *arg); + +static void tsrm_exit_key_set(tsrm_tls_entry *thread_resources) +{ + if (!tsrm_thread_exit_armed) { + return; + } + + if (pthread_setspecific(tsrm_exit_key, thread_resources) != 0) { + return; + } + if (thread_resources && !tsrm_thread_exit_registered) { + tsrm_thread_exit_registered = true; + _tlv_atexit(tsrm_thread_exit_handler, NULL); + } +} +# else +# define tsrm_exit_key_set(what) do { if (tsrm_thread_exit_armed) { pthread_setspecific(tsrm_exit_key, (void*)(what)); } } while (0) +# endif #endif TSRM_TLS bool in_main_thread = false; @@ -251,9 +276,16 @@ static void TSRM_THREAD_EXIT_CC tsrm_thread_exit_handler(void *arg) return; } #else +# ifdef __APPLE__ + tsrm_thread_exit_registered = false; + if (!tsrm_thread_exit_armed) { + return; + } + arg = pthread_getspecific(tsrm_exit_key); +# endif tsrm_tls_entry *thread_resources = (tsrm_tls_entry *) arg; - if (!tsrm_thread_exit_armed || arg != TSRMLS_CACHE || in_main_thread || !tsrm_tls_table) { + if (!tsrm_thread_exit_armed || !arg || arg != TSRMLS_CACHE || in_main_thread || !tsrm_tls_table) { return; } #endif @@ -334,6 +366,8 @@ TSRM_API bool tsrm_startup(int expected_threads, int expected_resources, int deb tsrm_dll_shutdown_in_progress = (BOOLEAN (NTAPI *)(void)) (void *) GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlDllShutdownInProgress"); tsrm_exit_key = tsrm_dll_shutdown_in_progress ? FlsAlloc(tsrm_thread_exit_handler) : FLS_OUT_OF_INDEXES; tsrm_thread_exit_armed = tsrm_exit_key != FLS_OUT_OF_INDEXES; +#elif defined(__APPLE__) + tsrm_thread_exit_armed = pthread_key_create(&tsrm_exit_key, NULL) == 0; #else tsrm_thread_exit_armed = pthread_key_create(&tsrm_exit_key, tsrm_thread_exit_handler) == 0; #endif From d34ae6b35c8a7e7916fee7158323627bd8ed9be8 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 21 Sep 2026 16:19:39 +0200 Subject: [PATCH 31/38] use ir TLS_ADDR --- Zend/zend.c | 1 - Zend/zend_globals.h | 1 - ext/opcache/jit/zend_jit_ir.c | 36 +++++++++++++++-------------------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/Zend/zend.c b/Zend/zend.c index eff4e2c6a508..b2f9e8db3d86 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -806,7 +806,6 @@ static void compiler_globals_dtor(zend_compiler_globals *compiler_globals) /* {{ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{{ */ { - _tsrm_ls_cache.self = &_tsrm_ls_cache; zend_startup_constants(); zend_copy_constants(executor_globals->zend_constants, GLOBAL_CONSTANTS_TABLE); zend_init_rsrc_plist(); diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 801fae65992d..d12aabd76af2 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -338,7 +338,6 @@ struct _zend_executor_globals { #ifdef ZTS struct _zend_tsrm_ls_cache { void *cache; - void *self; zend_executor_globals eg; zend_compiler_globals cg; }; diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 2ff581f404f2..28f0bfaf2747 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -212,10 +212,10 @@ static size_t tsrm_tls_offset = -1; (offsetof(zend_tsrm_ls_cache, cg) + offsetof(zend_compiler_globals, field)) # define jit_EG(_field) \ - ir_ADD_OFFSET(jit_TLS(jit), EG_TLS_OFFSET(_field)) + jit_TLS_ADDR(jit, EG_TLS_OFFSET(_field)) # define jit_CG(_field) \ - ir_ADD_OFFSET(jit_TLS(jit), CG_TLS_OFFSET(_field)) + jit_TLS_ADDR(jit, CG_TLS_OFFSET(_field)) #else @@ -359,6 +359,7 @@ static int zend_jit_assign_to_variable(zend_jit_ctx *jit, bool check_exception); static ir_ref jit_CONST_FUNC(zend_jit_ctx *jit, uintptr_t addr, uint16_t flags); +static ir_ref jit_ADD_OFFSET(zend_jit_ctx *jit, ir_ref addr, uintptr_t offset); static void zend_jit_preserve_parent_regs(zend_jit_ctx *jit, zend_ssa *ssa, @@ -499,17 +500,23 @@ static void * ZEND_FASTCALL zend_jit_get_tsrm_ls_cache(void) return &_tsrm_ls_cache; } -static ir_ref jit_TLS(zend_jit_ctx *jit) +static ir_ref jit_TLS_ADDR(zend_jit_ctx *jit, size_t offset) { ZEND_ASSERT(jit->ctx.control); + if (tsrm_ls_cache_tcb_offset) { + return ir_TLS_ADDR(-1, tsrm_ls_cache_tcb_offset + offset); + } else if (tsrm_tls_index != -1) { + return ir_TLS_ADDR(tsrm_tls_index, tsrm_tls_offset + offset); + } + if (jit->tls) { - /* Emit "TLS" once for basic block */ + /* Reuse the fallback call within a basic block. */ ir_insn *insn; ir_ref ref = jit->ctx.control; while (1) { if (ref == jit->tls) { - return jit->tls; + return ir_ADD_OFFSET(jit->tls, offset); } insn = &jit->ctx.ir_base[ref]; if (insn->op >= IR_START || insn->op == IR_CALL) { @@ -519,22 +526,9 @@ static ir_ref jit_TLS(zend_jit_ctx *jit) } } - if (tsrm_ls_cache_tcb_offset == 0 && tsrm_tls_index == -1) { - jit->tls = ir_CALL(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_get_tsrm_ls_cache)); - } else { - /* ir_TLS() loads the word stored at _tsrm_ls_cache, so read the "self" - * back-pointer to end up with the address of the cache struct itself. - * The globals live inside it, not behind its first (`cache`) field. */ - jit->tls = ir_TLS( - tsrm_ls_cache_tcb_offset - ? tsrm_ls_cache_tcb_offset + offsetof(zend_tsrm_ls_cache, self) - : tsrm_tls_index, - tsrm_ls_cache_tcb_offset - ? IR_NULL - : tsrm_tls_offset + offsetof(zend_tsrm_ls_cache, self)); - } - - return jit->tls; + jit->tls = ir_CALL(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_get_tsrm_ls_cache)); + + return ir_ADD_OFFSET(jit->tls, offset); } #endif From 961a094d8c3e45359ac82820b5a9bd6fcd6c6314 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 13 Oct 2026 20:12:23 +0200 Subject: [PATCH 32/38] macro -> function --- TSRM/TSRM.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index 598ff659db89..dedf01c5a516 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -242,7 +242,14 @@ static void tsrm_exit_key_set(tsrm_tls_entry *thread_resources) } } # else -# define tsrm_exit_key_set(what) do { if (tsrm_thread_exit_armed) { pthread_setspecific(tsrm_exit_key, (void*)(what)); } } while (0) +static void tsrm_exit_key_set(tsrm_tls_entry *thread_resources) +{ + if (!tsrm_thread_exit_armed) { + return; + } + + pthread_setspecific(tsrm_exit_key, thread_resources); +} # endif #endif From e28efae5c0a57b9064526267c6fe432819c64a36 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 13 Oct 2026 20:15:47 +0200 Subject: [PATCH 33/38] add comment explaining why we use fls instead of just tlsalloc --- TSRM/TSRM.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TSRM/TSRM.c b/TSRM/TSRM.c index dedf01c5a516..6e2fb409a37e 100644 --- a/TSRM/TSRM.c +++ b/TSRM/TSRM.c @@ -184,6 +184,10 @@ typedef struct { # define tsrm_tls_set(what) TlsSetValue(tls_key, (void*)(what)) # define tsrm_tls_get() TlsGetValue(tls_key) +/* TlsAlloc() does not provide thread-exit callbacks with parameters, which we rely on here.. + * This calls ts_free_thread() if the SAPI has not done so explicitly, + * cleaning up EG(persistent_list) and other TSRM resources on the owning + * thread before its native TLS is released. */ static void tsrm_exit_key_set(tsrm_tls_entry *thread_resources) { tsrm_thread_exit_data *data; From 23f2ab67351189cbf4b85ac5588c326e2c2e8c8e Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 13 Oct 2026 20:44:47 +0200 Subject: [PATCH 34/38] cache index based tls jit too --- ext/opcache/jit/zend_jit_ir.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index bf850e37a6ea..8782071030ef 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -502,12 +502,10 @@ static ir_ref jit_TLS_ADDR(zend_jit_ctx *jit, size_t offset) ZEND_ASSERT(jit->ctx.control); if (tsrm_ls_cache_tcb_offset) { return ir_TLS_ADDR(-1, tsrm_ls_cache_tcb_offset + offset); - } else if (tsrm_tls_index != -1) { - return ir_TLS_ADDR(tsrm_tls_index, tsrm_tls_offset + offset); } if (jit->tls) { - /* Reuse the fallback call within a basic block. */ + /* Reuse the TLS lookup within a basic block. */ ir_insn *insn; ir_ref ref = jit->ctx.control; @@ -523,7 +521,11 @@ static ir_ref jit_TLS_ADDR(zend_jit_ctx *jit, size_t offset) } } - jit->tls = ir_CALL(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_get_tsrm_ls_cache)); + if (tsrm_tls_index != -1) { + jit->tls = ir_TLS_ADDR(tsrm_tls_index, tsrm_tls_offset); + } else { + jit->tls = ir_CALL(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_get_tsrm_ls_cache)); + } return ir_ADD_OFFSET(jit->tls, offset); } From 11b9414755e1aa5a8d9f03a9a2c79750d615a34d Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 15 Oct 2026 15:36:47 +0200 Subject: [PATCH 35/38] figure out linker relaxation for initial-exec --- ext/opcache/jit/tls/testing/test.sh | 4 +--- ext/opcache/jit/tls/zend_jit_tls_x86_64.c | 29 +++++++++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/ext/opcache/jit/tls/testing/test.sh b/ext/opcache/jit/tls/testing/test.sh index f184eee57fa5..c6b003caafdd 100755 --- a/ext/opcache/jit/tls/testing/test.sh +++ b/ext/opcache/jit/tls/testing/test.sh @@ -208,9 +208,7 @@ for CC in clang gcc; do esac for LD in $LDs; do for opt in -O0 -O3; do - # def.c stubs tsrm_get_ls_cache_tcb_offset() to 0, which only happens under - # global-dynamic, so reference the cache with that model too. - CFLAGS="$MACHINE $MUSL $opt -Werror -DTSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC -I$root/ext/opcache -I$root/Zend -I$root" + CFLAGS="$MACHINE $MUSL $opt -Werror -I$root/ext/opcache -I$root/Zend -I$root" LDFLAGS="$MACHINE -fuse-ld=$LD $RPATH" for pic in "-fPIC" "-fno-PIC $STATIC"; do diff --git a/ext/opcache/jit/tls/zend_jit_tls_x86_64.c b/ext/opcache/jit/tls/zend_jit_tls_x86_64.c index f3495505c524..d1ffbd9aeb69 100644 --- a/ext/opcache/jit/tls/zend_jit_tls_x86_64.c +++ b/ext/opcache/jit/tls/zend_jit_tls_x86_64.c @@ -166,7 +166,9 @@ zend_result zend_jit_resolve_tsrm_ls_cache_offsets( /* * 64 48 8b 04 25 00 00 00 00 movq %fs:0x0,%rax - * 48 8d 80 f8 ff ff ff leaq -0x8(%rax),%rax + * 48 8d 80 f8 ff ff ff leaq -0x8(%rax),%rax (Local Exec) + * or + * 48 03 05 xx xx xx xx addq any(%rip),%rax (Initial Exec) */ // movq %fs:0x0,%rax @@ -177,21 +179,24 @@ zend_result zend_jit_resolve_tsrm_ls_cache_offsets( return FAILURE; } - // leaq any(%rax),$rax - if (memcmp(&code[9], "\x48\x8d\x80", 3) != 0) { + int32_t displacement; + memcpy(&displacement, &code[12], sizeof(displacement)); + uintptr_t offset; + + if (memcmp(&code[9], "\x48\x8d\x80", 3) == 0) { + // local-exec, leaq any(%rax),$rax + offset = (uintptr_t)displacement; + } else if (memcmp(&code[9], "\x48\x03\x05", 3) == 0) { + // initial-exec, addq any(%rip),%rax + uintptr_t got_addr = (uintptr_t)code + 16 + displacement; + memcpy(&offset, (void*)got_addr, sizeof(offset)); + } else { uint64_t bytes; - memcpy(&bytes, &code[10], 8); - zend_accel_error(ACCEL_LOG_DEBUG, "leaq insn does not match: 0x%016" PRIx64 "\n", bytes); + memcpy(&bytes, &code[9], 8); + zend_accel_error(ACCEL_LOG_DEBUG, "leaq/addq insn does not match: 0x%016" PRIx64 "\n", bytes); return FAILURE; } - /* Extract immediate values */ - - uintptr_t offset = (uintptr_t)(int32_t)(((uint32_t)code[15] << 24) - | ((uint32_t)code[14] << 16) - | ((uint32_t)code[13] << 8) - | ((uint32_t)code[12])); - if ((uintptr_t)&_tsrm_ls_cache - (uintptr_t)thread_pointer == offset) { *tcb_offset = offset; zend_accel_error(ACCEL_LOG_DEBUG, "static tls at offset %" PRIxPTR " from thread pointer (inferred from code)\n", offset); From 12139879eb8af8dd1076ff88dbe596f57e952edc Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 15 Oct 2026 15:37:27 +0200 Subject: [PATCH 36/38] fix old typo --- ext/opcache/jit/tls/zend_jit_tls_x86_64.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opcache/jit/tls/zend_jit_tls_x86_64.c b/ext/opcache/jit/tls/zend_jit_tls_x86_64.c index d1ffbd9aeb69..688319ede30e 100644 --- a/ext/opcache/jit/tls/zend_jit_tls_x86_64.c +++ b/ext/opcache/jit/tls/zend_jit_tls_x86_64.c @@ -184,7 +184,7 @@ zend_result zend_jit_resolve_tsrm_ls_cache_offsets( uintptr_t offset; if (memcmp(&code[9], "\x48\x8d\x80", 3) == 0) { - // local-exec, leaq any(%rax),$rax + // local-exec, leaq any(%rax),%rax offset = (uintptr_t)displacement; } else if (memcmp(&code[9], "\x48\x03\x05", 3) == 0) { // initial-exec, addq any(%rip),%rax From c83767de48d04fd59bf13463ff1fc0fa75627f16 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 15 Oct 2026 16:08:07 +0200 Subject: [PATCH 37/38] aarch64 too --- ext/opcache/jit/tls/zend_jit_tls_aarch64.c | 55 +++++++++++++++------- ext/opcache/jit/tls/zend_jit_tls_x86_64.c | 6 ++- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/ext/opcache/jit/tls/zend_jit_tls_aarch64.c b/ext/opcache/jit/tls/zend_jit_tls_aarch64.c index 80d19090312f..0a2fbdac11d2 100644 --- a/ext/opcache/jit/tls/zend_jit_tls_aarch64.c +++ b/ext/opcache/jit/tls/zend_jit_tls_aarch64.c @@ -192,18 +192,6 @@ zend_result zend_jit_resolve_tsrm_ls_cache_offsets( /* Code was changed by the linker. Check if we recognize the updated code */ - // movz x0, #0, lsl #16 - if ((insn[0] & ~AARCH64_MOVZ_IMM_MASK) != 0xd2a00000) { - zend_accel_error(ACCEL_LOG_DEBUG, "movz insn does not match: 0x%08" PRIx32 "\n", insn[0]); - return FAILURE; - } - - // movk x0, #0x10 - if ((insn[1] & ~AARCH64_MOVK_IMM_MASK) != 0xf2800000) { - zend_accel_error(ACCEL_LOG_DEBUG, "movk insn does not match: 0x%08" PRIx32 "\n", insn[1]); - return FAILURE; - } - // nop for (int i = 0; i < 2; i++) { if (insn[2+i] != AARCH64_NOP) { @@ -212,12 +200,45 @@ zend_result zend_jit_resolve_tsrm_ls_cache_offsets( } } - /* Extract immediate values */ + uint64_t offset; + if ((insn[0] & ~AARCH64_ADRP_IMM_MASK) == 0x90000000) { + /* intial-exec */ + // ldr x0, [x0, #any] + if ((insn[1] & ~AARCH64_LDR_UNSIGNED_IMM_MASK) != 0xf9400000) { + zend_accel_error(ACCEL_LOG_DEBUG, "ldr insn does not match: 0x%08" PRIx32 "\n", insn[1]); + return FAILURE; + } - uint64_t movz_imm = (insn[0] & AARCH64_MOVZ_IMM_MASK) >> 5; - uint64_t movz_shift = (((insn[0] & AARCH64_MOVZ_HW_MASK) >> 21) << 4); - uint64_t movk_imm = (insn[1] & AARCH64_MOVK_IMM_MASK) >> 5; - uint64_t offset = (movz_imm << movz_shift) | movk_imm; + uint64_t adrp_immhi = (insn[0] & AARCH64_ADRP_IMMHI_MASK) >> AARCH64_ADRP_IMMHI_START; + uint64_t adrp_immlo = (insn[0] & AARCH64_ADRP_IMMLO_MASK) >> AARCH64_ADRP_IMMLO_START; + int64_t adrp_imm = (adrp_immhi << AARCH64_ADRP_IMMLO_WIDTH) | adrp_immlo; + if (adrp_imm & (1 << 20)) { + adrp_imm -= 1 << 21; + } + uint64_t ldr_imm = (insn[1] & AARCH64_LDR_UNSIGNED_IMM_MASK) >> 10; + uintptr_t got_addr = ((uintptr_t)insn & ~(uintptr_t)(4096-1)) + adrp_imm * 4096 + ldr_imm * 8; + memcpy(&offset, (void*)got_addr, sizeof(offset)); + } else { + /* local-exec */ + // movz x0, #0, lsl #16 + if ((insn[0] & ~AARCH64_MOVZ_IMM_MASK) != 0xd2a00000) { + zend_accel_error(ACCEL_LOG_DEBUG, "movz insn does not match: 0x%08" PRIx32 "\n", insn[0]); + return FAILURE; + } + + // movk x0, #0x10 + if ((insn[1] & ~AARCH64_MOVK_IMM_MASK) != 0xf2800000) { + zend_accel_error(ACCEL_LOG_DEBUG, "movk insn does not match: 0x%08" PRIx32 "\n", insn[1]); + return FAILURE; + } + + /* Extract immediate values */ + + uint64_t movz_imm = (insn[0] & AARCH64_MOVZ_IMM_MASK) >> 5; + uint64_t movz_shift = (((insn[0] & AARCH64_MOVZ_HW_MASK) >> 21) << 4); + uint64_t movk_imm = (insn[1] & AARCH64_MOVK_IMM_MASK) >> 5; + offset = (movz_imm << movz_shift) | movk_imm; + } if ((uintptr_t)&_tsrm_ls_cache - (uintptr_t)thread_pointer == offset) { zend_accel_error(ACCEL_LOG_DEBUG, "static tls at offset %" PRIxPTR " from thread pointer (inferred from code)\n", offset); diff --git a/ext/opcache/jit/tls/zend_jit_tls_x86_64.c b/ext/opcache/jit/tls/zend_jit_tls_x86_64.c index 688319ede30e..e003f3fbd74a 100644 --- a/ext/opcache/jit/tls/zend_jit_tls_x86_64.c +++ b/ext/opcache/jit/tls/zend_jit_tls_x86_64.c @@ -184,10 +184,12 @@ zend_result zend_jit_resolve_tsrm_ls_cache_offsets( uintptr_t offset; if (memcmp(&code[9], "\x48\x8d\x80", 3) == 0) { - // local-exec, leaq any(%rax),%rax + /* local-exec */ + // leaq any(%rax),%rax offset = (uintptr_t)displacement; } else if (memcmp(&code[9], "\x48\x03\x05", 3) == 0) { - // initial-exec, addq any(%rip),%rax + /* initial-exec */ + // addq any(%rip),%rax uintptr_t got_addr = (uintptr_t)code + 16 + displacement; memcpy(&offset, (void*)got_addr, sizeof(offset)); } else { From 865e14db1152ba6045039b1902eefbad04592363 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 15 Oct 2026 16:33:33 +0200 Subject: [PATCH 38/38] no surplus case gets global dynamic model --- ext/opcache/jit/tls/testing/test.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/opcache/jit/tls/testing/test.sh b/ext/opcache/jit/tls/testing/test.sh index c6b003caafdd..5cd9e8246e82 100755 --- a/ext/opcache/jit/tls/testing/test.sh +++ b/ext/opcache/jit/tls/testing/test.sh @@ -136,8 +136,8 @@ dl_def_static_user_no_surplus() { rm -f main - $CC $CFLAGS -ggdb3 -fPIC -o tls.o -c $TLSC - $CC $CFLAGS -ggdb3 -fPIC -o user.o -c user.c + $CC $CFLAGS -DTSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC -ggdb3 -fPIC -o tls.o -c $TLSC + $CC $CFLAGS -DTSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC -ggdb3 -fPIC -o user.o -c user.c $CC $CFLAGS -DNO_SURPLUS -ggdb3 -fPIC -o def.o -c def.c $CC $CFLAGS $LDFLAGS -shared -o libdef.so def.o user.o tls.o @@ -151,8 +151,8 @@ dl_def_shared_user_no_surplus() { rm -f main - $CC $CFLAGS -ggdb3 -fPIC -o tls.o -c $TLSC - $CC $CFLAGS -ggdb3 -fPIC -o user.o -c user.c + $CC $CFLAGS -DTSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC -ggdb3 -fPIC -o tls.o -c $TLSC + $CC $CFLAGS -DTSRM_TLS_MODEL_USE_GLOBAL_DYNAMIC -ggdb3 -fPIC -o user.o -c user.c $CC $CFLAGS $LDFLAGS -shared -o libuser.so user.o tls.o $CC $CFLAGS -DNO_SURPLUS -ggdb3 -fPIC -o def.o -c def.c