From f590d0a91ab3615655a3350269419bf19fd43a9f Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 26 Jan 2026 21:29:52 -0700 Subject: [PATCH 1/7] feat: zend_extension can have a zend_module_entry --- Zend/zend_extensions.c | 33 +++++++++++++++++++++++++++++++++ Zend/zend_extensions.h | 3 ++- ext/standard/dl.c | 23 ++++++++++++++++++++--- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/Zend/zend_extensions.c b/Zend/zend_extensions.c index a4e5a38f90d89..074425e16c77d 100644 --- a/Zend/zend_extensions.c +++ b/Zend/zend_extensions.c @@ -19,6 +19,8 @@ #include "zend_extensions.h" #include "zend_system_id.h" +#include "zend_API.h" +#include "zend_modules.h" ZEND_API zend_llist zend_extensions; ZEND_API uint32_t zend_extension_flags = 0; @@ -151,6 +153,7 @@ void zend_register_extension(zend_extension *new_extension, DL_HANDLE handle) { #if ZEND_EXTENSIONS_SUPPORT zend_extension extension; + zend_module_entry *module; extension = *new_extension; extension.handle = handle; @@ -159,6 +162,36 @@ void zend_register_extension(zend_extension *new_extension, DL_HANDLE handle) zend_llist_add_element(&zend_extensions, &extension); + module = extension.module_entry; + if (module) { + if (module->zend_api != ZEND_MODULE_API_NO) { + zend_error(E_CORE_WARNING, + "Hybrid module \"%s\" from Zend extension \"%s\" cannot be initialized: " + "Module API=%d, PHP API=%d", + module->name ? module->name : "", + extension.name ? extension.name : "", + module->zend_api, ZEND_MODULE_API_NO); + return; + } + + if (!module->build_id || strcmp(module->build_id, ZEND_MODULE_BUILD_ID)) { + zend_error(E_CORE_WARNING, + "Hybrid module \"%s\" from Zend extension \"%s\" cannot be initialized: " + "Module build ID=%s, PHP build ID=%s", + module->name ? module->name : "", + extension.name ? extension.name : "", + module->build_id ? module->build_id : "", + ZEND_MODULE_BUILD_ID); + return; + } + + module->handle = NULL; + if (zend_register_module_ex(module, MODULE_PERSISTENT) == NULL) { + /* Errors already reported by zend_register_module_ex(). */ + return; + } + } + if (extension.op_array_ctor) { zend_extension_flags |= ZEND_EXTENSIONS_HAVE_OP_ARRAY_CTOR; } diff --git a/Zend/zend_extensions.h b/Zend/zend_extensions.h index 4de8ad5414c2a..9bd5aadbe2746 100644 --- a/Zend/zend_extensions.h +++ b/Zend/zend_extensions.h @@ -54,6 +54,7 @@ typedef struct _zend_extension_version_info { #define ZEND_EXTENSION_BUILD_ID "API" ZEND_TOSTR(ZEND_EXTENSION_API_NO) ZEND_BUILD_TS ZEND_BUILD_DEBUG ZEND_BUILD_SYSTEM ZEND_BUILD_EXTRA typedef struct _zend_extension zend_extension; +typedef struct _zend_module_entry zend_module_entry; /* Typedef's for zend_extension function pointers */ typedef int (*startup_func_t)(zend_extension *extension); @@ -101,7 +102,7 @@ struct _zend_extension { int (*build_id_check)(const char* build_id); op_array_persist_calc_func_t op_array_persist_calc; op_array_persist_func_t op_array_persist; - void *reserved5; + zend_module_entry *module_entry; void *reserved6; void *reserved7; void *reserved8; diff --git a/ext/standard/dl.c b/ext/standard/dl.c index 31adbceac8c29..74f3db34e90e4 100644 --- a/ext/standard/dl.c +++ b/ext/standard/dl.c @@ -21,6 +21,8 @@ #include "php_globals.h" #include "php_ini.h" #include "ext/standard/info.h" +#include "Zend/zend_API.h" +#include "Zend/zend_extensions.h" #include "SAPI.h" @@ -173,13 +175,12 @@ PHPAPI int php_load_extension(const char *filename, int type, int start_now) efree(orig_libpath); efree(err1); } - efree(libpath); - #ifdef PHP_WIN32 if (!php_win32_image_compatible(handle, &err1)) { php_error_docref(NULL, error_type, "%s", err1); efree(err1); DL_UNLOAD(handle); + efree(libpath); return FAILURE; } #endif @@ -195,12 +196,23 @@ PHPAPI int php_load_extension(const char *filename, int type, int start_now) } if (!get_module) { - if (DL_FETCH_SYMBOL(handle, "zend_extension_entry") || DL_FETCH_SYMBOL(handle, "_zend_extension_entry")) { + zend_extension *zend_extension_entry = (zend_extension *) DL_FETCH_SYMBOL(handle, "zend_extension_entry"); + if (!zend_extension_entry) { + zend_extension_entry = (zend_extension *) DL_FETCH_SYMBOL(handle, "_zend_extension_entry"); + } + if (zend_extension_entry && zend_extension_entry->module_entry) { + zend_result result = zend_load_extension_handle(handle, libpath); + efree(libpath); + return result; + } + if (zend_extension_entry) { DL_UNLOAD(handle); + efree(libpath); php_error_docref(NULL, error_type, "Invalid library (appears to be a Zend Extension, try loading using zend_extension=%s from php.ini)", filename); return FAILURE; } DL_UNLOAD(handle); + efree(libpath); php_error_docref(NULL, error_type, "Invalid library (maybe not a PHP library) '%s'", filename); return FAILURE; } @@ -208,6 +220,7 @@ PHPAPI int php_load_extension(const char *filename, int type, int start_now) if (zend_hash_str_exists(&module_registry, module_entry->name, strlen(module_entry->name))) { zend_error(E_CORE_WARNING, "Module \"%s\" is already loaded", module_entry->name); DL_UNLOAD(handle); + efree(libpath); return FAILURE; } if (module_entry->zend_api != ZEND_MODULE_API_NO) { @@ -218,6 +231,7 @@ PHPAPI int php_load_extension(const char *filename, int type, int start_now) "These options need to match\n", module_entry->name, module_entry->zend_api, ZEND_MODULE_API_NO); DL_UNLOAD(handle); + efree(libpath); return FAILURE; } if(strcmp(module_entry->build_id, ZEND_MODULE_BUILD_ID)) { @@ -228,15 +242,18 @@ PHPAPI int php_load_extension(const char *filename, int type, int start_now) "These options need to match\n", module_entry->name, module_entry->build_id, ZEND_MODULE_BUILD_ID); DL_UNLOAD(handle); + efree(libpath); return FAILURE; } if ((module_entry = zend_register_module_ex(module_entry, type)) == NULL) { DL_UNLOAD(handle); + efree(libpath); return FAILURE; } module_entry->handle = handle; + efree(libpath); if ((type == MODULE_TEMPORARY || start_now) && zend_startup_module_ex(module_entry) == FAILURE) { DL_UNLOAD(handle); From ab22c05f579f3e7476eaa9816cdb6203f00c673a Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 27 Jan 2026 14:41:54 -0700 Subject: [PATCH 2/7] refactor: extract php_try_load_hybrid_zend_extension --- ext/standard/dl.c | 50 +++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/ext/standard/dl.c b/ext/standard/dl.c index 74f3db34e90e4..cf6726ddf2b91 100644 --- a/ext/standard/dl.c +++ b/ext/standard/dl.c @@ -108,6 +108,28 @@ PHPAPI void *php_load_shlib(const char *path, char **errp) } /* }}} */ +/* This helper handles the hybrid zend_{extension,module_entry} fallback path. + * It unloads the handle on failure but it does not free libpath. */ +static zend_result php_try_load_hybrid_zend_extension( + DL_HANDLE handle, const char *filename, char *libpath, int error_type) +{ + zend_extension *zend_extension_entry = (zend_extension *) DL_FETCH_SYMBOL(handle, "zend_extension_entry"); + if (!zend_extension_entry) { + zend_extension_entry = (zend_extension *) DL_FETCH_SYMBOL(handle, "_zend_extension_entry"); + } + if (zend_extension_entry && zend_extension_entry->module_entry) { + return zend_load_extension_handle(handle, libpath); + } + if (zend_extension_entry) { + php_error_docref(NULL, error_type, "Invalid library (appears to be a Zend Extension, try loading using zend_extension=%s from php.ini)", filename); + DL_UNLOAD(handle); + return FAILURE; + } + php_error_docref(NULL, error_type, "Invalid library (maybe not a PHP library) '%s'", filename); + DL_UNLOAD(handle); + return FAILURE; +} + /* {{{ php_load_extension */ PHPAPI int php_load_extension(const char *filename, int type, int start_now) { @@ -196,31 +218,17 @@ PHPAPI int php_load_extension(const char *filename, int type, int start_now) } if (!get_module) { - zend_extension *zend_extension_entry = (zend_extension *) DL_FETCH_SYMBOL(handle, "zend_extension_entry"); - if (!zend_extension_entry) { - zend_extension_entry = (zend_extension *) DL_FETCH_SYMBOL(handle, "_zend_extension_entry"); - } - if (zend_extension_entry && zend_extension_entry->module_entry) { - zend_result result = zend_load_extension_handle(handle, libpath); - efree(libpath); - return result; - } - if (zend_extension_entry) { - DL_UNLOAD(handle); - efree(libpath); - php_error_docref(NULL, error_type, "Invalid library (appears to be a Zend Extension, try loading using zend_extension=%s from php.ini)", filename); - return FAILURE; - } - DL_UNLOAD(handle); + // If get_module still isn't found, maybe it's a zend_extension with a module entry. + zend_result result = php_try_load_hybrid_zend_extension(handle, filename, libpath, error_type); efree(libpath); - php_error_docref(NULL, error_type, "Invalid library (maybe not a PHP library) '%s'", filename); - return FAILURE; + return result; } + efree(libpath); + module_entry = get_module(); if (zend_hash_str_exists(&module_registry, module_entry->name, strlen(module_entry->name))) { zend_error(E_CORE_WARNING, "Module \"%s\" is already loaded", module_entry->name); DL_UNLOAD(handle); - efree(libpath); return FAILURE; } if (module_entry->zend_api != ZEND_MODULE_API_NO) { @@ -231,7 +239,6 @@ PHPAPI int php_load_extension(const char *filename, int type, int start_now) "These options need to match\n", module_entry->name, module_entry->zend_api, ZEND_MODULE_API_NO); DL_UNLOAD(handle); - efree(libpath); return FAILURE; } if(strcmp(module_entry->build_id, ZEND_MODULE_BUILD_ID)) { @@ -242,18 +249,15 @@ PHPAPI int php_load_extension(const char *filename, int type, int start_now) "These options need to match\n", module_entry->name, module_entry->build_id, ZEND_MODULE_BUILD_ID); DL_UNLOAD(handle); - efree(libpath); return FAILURE; } if ((module_entry = zend_register_module_ex(module_entry, type)) == NULL) { DL_UNLOAD(handle); - efree(libpath); return FAILURE; } module_entry->handle = handle; - efree(libpath); if ((type == MODULE_TEMPORARY || start_now) && zend_startup_module_ex(module_entry) == FAILURE) { DL_UNLOAD(handle); From ed9a9bbb9b09d87de9e53c8685a618728f5748de Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 27 Jan 2026 15:06:33 -0700 Subject: [PATCH 3/7] refactor: extract zend_try_register_hybrid_module --- Zend/zend_extensions.c | 77 +++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/Zend/zend_extensions.c b/Zend/zend_extensions.c index 074425e16c77d..5251d2e85c1fb 100644 --- a/Zend/zend_extensions.c +++ b/Zend/zend_extensions.c @@ -28,6 +28,44 @@ ZEND_API int zend_op_array_extension_handles = 0; ZEND_API int zend_internal_function_extension_handles = 0; static int last_resource_number; +// TODO: revisit this name if we want to make it ZEND_API. +static zend_result zend_try_register_hybrid_module(zend_extension *extension) +{ + zend_module_entry *module = extension->module_entry; + + if (!module) { + return SUCCESS; + } + + if (module->zend_api != ZEND_MODULE_API_NO) { + zend_error(E_CORE_WARNING, + "Hybrid module \"%s\" from Zend extension \"%s\" cannot be initialized: " + "Module API=%d, PHP API=%d", + module->name ? module->name : "", + extension->name ? extension->name : "", + module->zend_api, ZEND_MODULE_API_NO); + return FAILURE; + } + + if (!module->build_id || strcmp(module->build_id, ZEND_MODULE_BUILD_ID)) { + zend_error(E_CORE_WARNING, + "Hybrid module \"%s\" from Zend extension \"%s\" cannot be initialized: " + "Module build ID=%s, PHP build ID=%s", + module->name ? module->name : "", + extension->name ? extension->name : "", + module->build_id ? module->build_id : "", + ZEND_MODULE_BUILD_ID); + return FAILURE; + } + + module->handle = NULL; + if (zend_register_module_ex(module, MODULE_PERSISTENT) == NULL) { + return FAILURE; + } + + return SUCCESS; +} + zend_result zend_load_extension(const char *path) { #if ZEND_EXTENSIONS_SUPPORT @@ -136,6 +174,14 @@ zend_result zend_load_extension_handle(DL_HANDLE handle, const char *path) return FAILURE; } + // The module_entry is loaded registered before the extension is registered + // because zend_register_extension() returns void and is ZEND_API, so + // operations which can fail need to be peformed before it. + if (zend_try_register_hybrid_module(new_extension) != SUCCESS) { + DL_UNLOAD(handle); + return FAILURE; + } + zend_register_extension(new_extension, handle); return SUCCESS; #else @@ -153,7 +199,6 @@ void zend_register_extension(zend_extension *new_extension, DL_HANDLE handle) { #if ZEND_EXTENSIONS_SUPPORT zend_extension extension; - zend_module_entry *module; extension = *new_extension; extension.handle = handle; @@ -162,36 +207,6 @@ void zend_register_extension(zend_extension *new_extension, DL_HANDLE handle) zend_llist_add_element(&zend_extensions, &extension); - module = extension.module_entry; - if (module) { - if (module->zend_api != ZEND_MODULE_API_NO) { - zend_error(E_CORE_WARNING, - "Hybrid module \"%s\" from Zend extension \"%s\" cannot be initialized: " - "Module API=%d, PHP API=%d", - module->name ? module->name : "", - extension.name ? extension.name : "", - module->zend_api, ZEND_MODULE_API_NO); - return; - } - - if (!module->build_id || strcmp(module->build_id, ZEND_MODULE_BUILD_ID)) { - zend_error(E_CORE_WARNING, - "Hybrid module \"%s\" from Zend extension \"%s\" cannot be initialized: " - "Module build ID=%s, PHP build ID=%s", - module->name ? module->name : "", - extension.name ? extension.name : "", - module->build_id ? module->build_id : "", - ZEND_MODULE_BUILD_ID); - return; - } - - module->handle = NULL; - if (zend_register_module_ex(module, MODULE_PERSISTENT) == NULL) { - /* Errors already reported by zend_register_module_ex(). */ - return; - } - } - if (extension.op_array_ctor) { zend_extension_flags |= ZEND_EXTENSIONS_HAVE_OP_ARRAY_CTOR; } From 0bc5587336584894eb57e70696eb91ad4a38e883 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 27 Jan 2026 15:11:34 -0700 Subject: [PATCH 4/7] docs: DL_HANDLE ownership for hybrid extensions --- Zend/zend_extensions.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Zend/zend_extensions.c b/Zend/zend_extensions.c index 5251d2e85c1fb..4fba8374b9088 100644 --- a/Zend/zend_extensions.c +++ b/Zend/zend_extensions.c @@ -58,12 +58,9 @@ static zend_result zend_try_register_hybrid_module(zend_extension *extension) return FAILURE; } + // The handle is owned by the zend_extension for hybrid extensions. module->handle = NULL; - if (zend_register_module_ex(module, MODULE_PERSISTENT) == NULL) { - return FAILURE; - } - - return SUCCESS; + return zend_register_module_ex(module, MODULE_PERSISTENT) ? SUCCESS : FAILURE; } zend_result zend_load_extension(const char *path) From f5d45ce8d3c3b88707419d655c95528378f58f2e Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 27 Jan 2026 15:17:32 -0700 Subject: [PATCH 5/7] docs: fix grammar --- Zend/zend_extensions.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Zend/zend_extensions.c b/Zend/zend_extensions.c index 4fba8374b9088..84c1f81903041 100644 --- a/Zend/zend_extensions.c +++ b/Zend/zend_extensions.c @@ -171,9 +171,9 @@ zend_result zend_load_extension_handle(DL_HANDLE handle, const char *path) return FAILURE; } - // The module_entry is loaded registered before the extension is registered + // The module_entry is registered before the extension is registered // because zend_register_extension() returns void and is ZEND_API, so - // operations which can fail need to be peformed before it. + // operations which can fail need to be performed before it. if (zend_try_register_hybrid_module(new_extension) != SUCCESS) { DL_UNLOAD(handle); return FAILURE; From e835bed53e968dd93a9161e9dafca06167b6cb9c Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 27 Jan 2026 16:14:06 -0700 Subject: [PATCH 6/7] feat: add ZEND_MODULE_ENTRY --- Zend/zend_API.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Zend/zend_API.h b/Zend/zend_API.h index c1ccbf13666a5..028e99f43c4dc 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -238,9 +238,11 @@ typedef struct _zend_fcall_info_cache { #define ZEND_MODULE_GLOBALS_CTOR_D(module) void ZEND_MODULE_GLOBALS_CTOR_N(module)(zend_##module##_globals *module##_globals) #define ZEND_MODULE_GLOBALS_DTOR_D(module) void ZEND_MODULE_GLOBALS_DTOR_N(module)(zend_##module##_globals *module##_globals) +#define ZEND_MODULE_ENTRY(name) (&name##_module_entry) + #define ZEND_GET_MODULE(name) \ BEGIN_EXTERN_C()\ - ZEND_DLEXPORT zend_module_entry *get_module(void) { return &name##_module_entry; }\ + ZEND_DLEXPORT zend_module_entry *get_module(void) { return ZEND_MODULE_ENTRY(name); }\ END_EXTERN_C() #define ZEND_BEGIN_MODULE_GLOBALS(module_name) \ From 5d983e7634a0914ed82f4f80dd2a4a06e4fba143 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 27 Jan 2026 16:16:02 -0700 Subject: [PATCH 7/7] docs: zend_extension::module_entry --- Zend/zend_extensions.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Zend/zend_extensions.h b/Zend/zend_extensions.h index 9bd5aadbe2746..fad81050b603a 100644 --- a/Zend/zend_extensions.h +++ b/Zend/zend_extensions.h @@ -102,6 +102,18 @@ struct _zend_extension { int (*build_id_check)(const char* build_id); op_array_persist_calc_func_t op_array_persist_calc; op_array_persist_func_t op_array_persist; + + /* Setting a module_entry indicates a hybrid extension, meaning an + * extension which is also a module. Such extensions can be loaded with + * either "zend_extension=" or "extension=" by INI. + * + * The symbol "get_module" must _not_ be exported, i.e. don't call + * ZEND_GET_MODULE(), and instead use ZEND_MODULE_ENTRY() to assign a value + * to `.module_entry`. + * + * The DL_HANDLE is owned by the zend_extension for hybrid extensions, so + * the handle should be null for the module entry. + */ zend_module_entry *module_entry; void *reserved6; void *reserved7;