#include "frankenphp.h" #include #include #include #include #include #include #include #ifdef HAVE_PHP_SESSION #include #endif #include #include #ifdef PHP_WIN32 #include #else #include #endif #include #include #include #include #include #include #include #include #include #include #include #ifndef ZEND_WIN32 #include #endif #if defined(__linux__) #include #elif defined(__FreeBSD__) || defined(__OpenBSD__) #include #endif #if PHP_VERSION_ID >= 80600 #include #else #include "emulate_php_cli.h" #endif #include "_cgo_export.h" #include "frankenphp_arginfo.h" #if defined(PHP_WIN32) && defined(ZTS) ZEND_TSRMLS_CACHE_DEFINE() #endif /** * The list of modules to reload on each request. If an external module * requires to be reloaded between requests, it is possible to hook on * `sapi_module.activate` and `sapi_module.deactivate`. * * @see https://github.com/DataDog/dd-trace-php/pull/3169 for an example */ static const char *MODULES_TO_RELOAD[] = {"filter", NULL}; frankenphp_version frankenphp_get_version() { return (frankenphp_version){ PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION, PHP_EXTRA_VERSION, PHP_VERSION, PHP_VERSION_ID, }; } frankenphp_config frankenphp_get_config() { return (frankenphp_config){ #ifdef ZTS true, #else false, #endif #ifdef ZEND_SIGNALS true, #else false, #endif #ifdef ZEND_MAX_EXECUTION_TIMERS true, #else false, #endif }; } bool should_filter_var = 0; bool original_user_abort_setting = 0; frankenphp_interned_strings_t frankenphp_strings = {0}; HashTable *main_thread_env = NULL; __thread uintptr_t thread_index; __thread bool is_worker_thread = false; __thread HashTable *sandboxed_env = NULL; void frankenphp_update_local_thread_context(bool is_worker) { is_worker_thread = is_worker; /* workers should keep running if the user aborts the connection */ PG(ignore_user_abort) = is_worker ? 1 : original_user_abort_setting; } static void frankenphp_update_request_context() { /* the server context is stored on the go side, still SG(server_context) needs * to not be NULL */ SG(server_context) = (void *)1; /* status It is not reset by zend engine, set it to 200. */ SG(sapi_headers).http_response_code = 200; char *authorization_header = go_update_request_info(thread_index, &SG(request_info)); /* let PHP handle basic auth */ php_handle_auth_data(authorization_header); } static void frankenphp_free_request_context() { if (SG(request_info).cookie_data != NULL) { free(SG(request_info).cookie_data); SG(request_info).cookie_data = NULL; } /* freed via thread.Unpin() */ SG(request_info).request_method = NULL; SG(request_info).query_string = NULL; SG(request_info).content_type = NULL; SG(request_info).path_translated = NULL; SG(request_info).request_uri = NULL; } /* reset all 'auto globals' in worker mode except of $_ENV * see: php_hash_environment() */ static void frankenphp_reset_super_globals() { zend_try { /* only $_FILES needs to be flushed explicitly * $_GET, $_POST, $_COOKIE and $_SERVER are flushed on reimport * $_ENV is not flushed * for more info see: php_startup_auto_globals() */ zval *files = &PG(http_globals)[TRACK_VARS_FILES]; zval_ptr_dtor_nogc(files); memset(files, 0, sizeof(*files)); /* $_SESSION must be explicitly deleted from the symbol table. * Unlike other superglobals, $_SESSION is stored in EG(symbol_table) * with a reference to PS(http_session_vars). The session RSHUTDOWN * only decrements the refcount but doesn't remove it from the symbol * table, causing data to leak between requests. */ zend_hash_str_del(&EG(symbol_table), "_SESSION", sizeof("_SESSION") - 1); } zend_end_try(); zend_auto_global *auto_global; zend_string *_env = ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_ENV); zend_string *_server = ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_SERVER); ZEND_HASH_MAP_FOREACH_PTR(CG(auto_globals), auto_global) { if (auto_global->name == _env) { /* skip $_ENV */ } else if (auto_global->name == _server) { /* always reimport $_SERVER */ auto_global->armed = auto_global->auto_global_callback(auto_global->name); } else if (auto_global->jit) { /* JIT globals ($_REQUEST, $GLOBALS) need special handling: * - $GLOBALS will always be handled by the application, we skip it * For $_REQUEST: * - If in symbol_table: re-initialize with current request data * - If not: do nothing, it may be armed by jit later */ if (auto_global->name == ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_REQUEST) && zend_hash_exists(&EG(symbol_table), auto_global->name)) { auto_global->armed = auto_global->auto_global_callback(auto_global->name); } } else if (auto_global->auto_global_callback) { /* $_GET, $_POST, $_COOKIE, $_FILES are reimported here */ auto_global->armed = auto_global->auto_global_callback(auto_global->name); } else { /* $_SESSION will land here (not an http_global) */ auto_global->armed = 0; } } ZEND_HASH_FOREACH_END(); } /* * free php_stream resources that are temporary (php_stream_temp_ops) * streams are globally registered in EG(regular_list), see zend_list.c * this fixes a leak when reading the body of a request */ static void frankenphp_release_temporary_streams() { zend_resource *val; int stream_type = php_file_le_stream(); ZEND_HASH_FOREACH_PTR(&EG(regular_list), val) { /* verify the resource is a stream */ if (val->type == stream_type) { php_stream *stream = (php_stream *)val->ptr; if (stream != NULL && stream->ops == &php_stream_temp_ops && stream->__exposed == 0 && GC_REFCOUNT(val) == 1) { ZEND_ASSERT(!stream->is_persistent); zend_list_delete(val); } } } ZEND_HASH_FOREACH_END(); } #ifdef HAVE_PHP_SESSION /* Reset session state between worker requests, preserving user handlers. * Based on php_rshutdown_session_globals() + php_rinit_session_globals(). */ static void frankenphp_reset_session_state(void) { if (PS(session_status) == php_session_active) { php_session_flush(1); } if (!Z_ISUNDEF(PS(http_session_vars))) { zval_ptr_dtor(&PS(http_session_vars)); ZVAL_UNDEF(&PS(http_session_vars)); } if (PS(mod_data) || PS(mod_user_implemented)) { zend_try { PS(mod)->s_close(&PS(mod_data)); } zend_end_try(); } if (PS(id)) { zend_string_release_ex(PS(id), 0); PS(id) = NULL; } if (PS(session_vars)) { zend_string_release_ex(PS(session_vars), 0); PS(session_vars) = NULL; } /* PS(mod_user_class_name) and PS(mod_user_names) are preserved */ #if PHP_VERSION_ID >= 80300 if (PS(session_started_filename)) { zend_string_release(PS(session_started_filename)); PS(session_started_filename) = NULL; PS(session_started_lineno) = 0; } #endif PS(session_status) = php_session_none; PS(in_save_handler) = 0; PS(set_handler) = 0; PS(mod_data) = NULL; PS(mod_user_is_open) = 0; PS(define_sid) = 1; } #endif /* Adapted from php_request_shutdown */ static void frankenphp_worker_request_shutdown() { /* Flush all output buffers */ zend_try { php_output_end_all(); } zend_end_try(); const char **module_name; zend_module_entry *module; for (module_name = MODULES_TO_RELOAD; *module_name; module_name++) { if ((module = zend_hash_str_find_ptr(&module_registry, *module_name, strlen(*module_name)))) { module->request_shutdown_func(module->type, module->module_number); } } #ifdef HAVE_PHP_SESSION frankenphp_reset_session_state(); #endif /* Shutdown output layer (send the set HTTP headers, cleanup output handlers, * etc.) */ zend_try { php_output_deactivate(); } zend_end_try(); /* SAPI related shutdown (free stuff) */ zend_try { sapi_deactivate(); } zend_end_try(); frankenphp_free_request_context(); zend_set_memory_limit(PG(memory_limit)); } // shutdown the dummy request that starts the worker script bool frankenphp_shutdown_dummy_request(void) { if (SG(server_context) == NULL) { return false; } frankenphp_worker_request_shutdown(); return true; } void get_full_env(zval *track_vars_array) { zend_hash_copy(Z_ARR_P(track_vars_array), main_thread_env, NULL); } /* Adapted from php_request_startup() */ static int frankenphp_worker_request_startup() { int retval = SUCCESS; frankenphp_update_request_context(); zend_try { frankenphp_release_temporary_streams(); php_output_activate(); /* initialize global variables */ PG(header_is_being_sent) = 0; PG(connection_status) = PHP_CONNECTION_NORMAL; /* Keep the current execution context */ sapi_activate(); #ifdef ZEND_MAX_EXECUTION_TIMERS if (PG(max_input_time) == -1) { zend_set_timeout(EG(timeout_seconds), 1); } else { zend_set_timeout(PG(max_input_time), 1); } #endif if (PG(expose_php)) { sapi_add_header(SAPI_PHP_VERSION_HEADER, sizeof(SAPI_PHP_VERSION_HEADER) - 1, 1); } if (PG(output_handler) && PG(output_handler)[0]) { zval oh; ZVAL_STRING(&oh, PG(output_handler)); php_output_start_user(&oh, 0, PHP_OUTPUT_HANDLER_STDFLAGS); zval_ptr_dtor(&oh); } else if (PG(output_buffering)) { php_output_start_user(NULL, PG(output_buffering) > 1 ? PG(output_buffering) : 0, PHP_OUTPUT_HANDLER_STDFLAGS); } else if (PG(implicit_flush)) { php_output_set_implicit_flush(1); } frankenphp_reset_super_globals(); const char **module_name; zend_module_entry *module; for (module_name = MODULES_TO_RELOAD; *module_name; module_name++) { if ((module = zend_hash_str_find_ptr(&module_registry, *module_name, strlen(*module_name))) && module->request_startup_func) { module->request_startup_func(module->type, module->module_number); } } } zend_catch { retval = FAILURE; } zend_end_try(); SG(sapi_started) = 1; return retval; } PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */ ZEND_PARSE_PARAMETERS_NONE(); if (go_is_context_done(thread_index)) { RETURN_FALSE; } php_output_end_all(); php_header(); go_frankenphp_finish_php_request(thread_index); RETURN_TRUE; } /* }}} */ /* {{{ Call go's putenv to prevent race conditions */ PHP_FUNCTION(frankenphp_putenv) { char *setting; size_t setting_len; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(setting, setting_len) ZEND_PARSE_PARAMETERS_END(); // Cast str_len to int (ensure it fits in an int) if (setting_len > INT_MAX) { php_error(E_WARNING, "String length exceeds maximum integer value"); RETURN_FALSE; } if (setting_len == 0 || setting[0] == '=') { zend_argument_value_error(1, "must have a valid syntax"); RETURN_THROWS(); } if (sandboxed_env == NULL) { sandboxed_env = zend_array_dup(main_thread_env); } /* cut at null byte to stay consistent with regular putenv */ char *null_pos = memchr(setting, '\0', setting_len); if (null_pos != NULL) { setting_len = null_pos - setting; } /* cut the string at the first '=' */ char *eq_pos = memchr(setting, '=', setting_len); bool success = true; /* no '=' found, delete the variable */ if (eq_pos == NULL) { success = go_putenv(setting, (int)setting_len, NULL, 0); if (success) { zend_hash_str_del(sandboxed_env, setting, setting_len); } RETURN_BOOL(success); } size_t name_len = eq_pos - setting; size_t value_len = (setting_len > name_len + 1) ? (setting_len - name_len - 1) : 0; success = go_putenv(setting, (int)name_len, eq_pos + 1, (int)value_len); if (success) { zval val = {0}; ZVAL_STRINGL(&val, eq_pos + 1, value_len); zend_hash_str_update(sandboxed_env, setting, name_len, &val); } RETURN_BOOL(success); } /* }}} */ /* {{{ Get the env from the sandboxed environment */ PHP_FUNCTION(frankenphp_getenv) { zend_string *name = NULL; bool local_only = 0; ZEND_PARSE_PARAMETERS_START(0, 2) Z_PARAM_OPTIONAL Z_PARAM_STR_OR_NULL(name) Z_PARAM_BOOL(local_only) ZEND_PARSE_PARAMETERS_END(); HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env; if (!name) { RETURN_ARR(zend_array_dup(ht)); return; } zval *env_val = zend_hash_find(ht, name); if (env_val && Z_TYPE_P(env_val) == IS_STRING) { zend_string *str = Z_STR_P(env_val); zend_string_addref(str); RETVAL_STR(str); } else { RETVAL_FALSE; } } /* }}} */ /* {{{ Fetch all HTTP request headers */ PHP_FUNCTION(frankenphp_request_headers) { ZEND_PARSE_PARAMETERS_NONE(); struct go_apache_request_headers_return headers = go_apache_request_headers(thread_index); array_init_size(return_value, headers.r1); for (size_t i = 0; i < headers.r1; i++) { go_string key = headers.r0[i * 2]; go_string val = headers.r0[i * 2 + 1]; add_assoc_stringl_ex(return_value, key.data, key.len, val.data, val.len); } } /* }}} */ /* add_response_header and apache_response_headers are copied from * https://github.com/php/php-src/blob/master/sapi/cli/php_cli_server.c * Copyright (c) The PHP Group * Licensed under The PHP License * Original authors: Moriyoshi Koizumi and Xinchen Hui * */ static void add_response_header(sapi_header_struct *h, zval *return_value) /* {{{ */ { if (h->header_len > 0) { char *s; size_t len = 0; ALLOCA_FLAG(use_heap) char *p = strchr(h->header, ':'); if (NULL != p) { len = p - h->header; } if (len > 0) { while (len != 0 && (h->header[len - 1] == ' ' || h->header[len - 1] == '\t')) { len--; } if (len) { s = do_alloca(len + 1, use_heap); memcpy(s, h->header, len); s[len] = 0; do { p++; } while (*p == ' ' || *p == '\t'); add_assoc_stringl_ex(return_value, s, len, p, h->header_len - (p - h->header)); free_alloca(s, use_heap); } } } } /* }}} */ PHP_FUNCTION(frankenphp_response_headers) /* {{{ */ { ZEND_PARSE_PARAMETERS_NONE(); array_init(return_value); zend_llist_apply_with_argument( &SG(sapi_headers).headers, (llist_apply_with_arg_func_t)add_response_header, return_value); } /* }}} */ PHP_FUNCTION(frankenphp_handle_request) { zend_fcall_info fci; zend_fcall_info_cache fcc; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_FUNC(fci, fcc) ZEND_PARSE_PARAMETERS_END(); if (!is_worker_thread) { /* not a worker, throw an error */ zend_throw_exception( spl_ce_RuntimeException, "frankenphp_handle_request() called while not in worker mode", 0); RETURN_THROWS(); } #ifdef ZEND_MAX_EXECUTION_TIMERS /* Disable timeouts while waiting for a request to handle */ zend_unset_timeout(); #endif struct go_frankenphp_worker_handle_request_start_return result = go_frankenphp_worker_handle_request_start(thread_index); if (frankenphp_worker_request_startup() == FAILURE /* Shutting down */ || !result.r0) { RETURN_FALSE; } #ifdef ZEND_MAX_EXECUTION_TIMERS /* * Reset default timeout */ if (PG(max_input_time) != -1) { zend_set_timeout(INI_INT("max_execution_time"), 0); } #endif /* Call the PHP func passed to frankenphp_handle_request() */ zval retval = {0}; zval *callback_ret = NULL; fci.size = sizeof fci; fci.retval = &retval; fci.params = result.r1; fci.param_count = result.r1 == NULL ? 0 : 1; if (zend_call_function(&fci, &fcc) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) { callback_ret = &retval; /* pass NULL instead of the NULL zval as return value */ if (Z_TYPE(retval) == IS_NULL) { callback_ret = NULL; } } /* * If an exception occurred, print the message to the client before * closing the connection. */ if (EG(exception)) { if (!zend_is_unwind_exit(EG(exception)) && !zend_is_graceful_exit(EG(exception))) { zend_exception_error(EG(exception), E_ERROR); } else { /* exit() will jump directly to after php_execute_script */ zend_bailout(); } } frankenphp_worker_request_shutdown(); go_frankenphp_finish_worker_request(thread_index, callback_ret); if (result.r1 != NULL) { zval_ptr_dtor(result.r1); } if (callback_ret != NULL) { zval_ptr_dtor(&retval); } RETURN_TRUE; } PHP_FUNCTION(headers_send) { zend_long response_code = 200; ZEND_PARSE_PARAMETERS_START(0, 1) Z_PARAM_OPTIONAL Z_PARAM_LONG(response_code) ZEND_PARSE_PARAMETERS_END(); int previous_status_code = SG(sapi_headers).http_response_code; SG(sapi_headers).http_response_code = response_code; if (response_code >= 100 && response_code < 200) { int ret = sapi_module.send_headers(&SG(sapi_headers)); SG(sapi_headers).http_response_code = previous_status_code; RETURN_LONG(ret); } RETURN_LONG(sapi_send_headers()); } PHP_FUNCTION(mercure_publish) { zval *topics; zend_string *data = NULL, *id = NULL, *type = NULL; zend_bool private = 0; zend_long retry = 0; bool retry_is_null = 1; ZEND_PARSE_PARAMETERS_START(1, 6) Z_PARAM_ZVAL(topics) Z_PARAM_OPTIONAL Z_PARAM_STR_OR_NULL(data) Z_PARAM_BOOL(private) Z_PARAM_STR_OR_NULL(id) Z_PARAM_STR_OR_NULL(type) Z_PARAM_LONG_OR_NULL(retry, retry_is_null) ZEND_PARSE_PARAMETERS_END(); if (Z_TYPE_P(topics) != IS_ARRAY && Z_TYPE_P(topics) != IS_STRING) { zend_argument_type_error(1, "must be of type array|string"); RETURN_THROWS(); } struct go_mercure_publish_return result = go_mercure_publish(thread_index, topics, data, private, id, type, retry); switch (result.r1) { case 0: RETURN_STR(result.r0); case 1: zend_throw_exception(spl_ce_RuntimeException, "No Mercure hub configured", 0); RETURN_THROWS(); case 2: zend_throw_exception(spl_ce_RuntimeException, "Publish failed", 0); RETURN_THROWS(); } zend_throw_exception(spl_ce_RuntimeException, "FrankenPHP not built with Mercure support", 0); RETURN_THROWS(); } PHP_FUNCTION(frankenphp_log) { zend_string *message = NULL; zend_long level = 0; zval *context = NULL; ZEND_PARSE_PARAMETERS_START(1, 3) Z_PARAM_STR(message) Z_PARAM_OPTIONAL Z_PARAM_LONG(level) Z_PARAM_ARRAY(context) ZEND_PARSE_PARAMETERS_END(); char *ret = NULL; ret = go_log_attrs(thread_index, message, level, context); if (ret != NULL) { zend_throw_exception(spl_ce_RuntimeException, ret, 0); free(ret); RETURN_THROWS(); } } PHP_MINIT_FUNCTION(frankenphp) { register_frankenphp_symbols(module_number); zend_function *func; // Override putenv func = zend_hash_str_find_ptr(CG(function_table), "putenv", sizeof("putenv") - 1); if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) { ((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_putenv); } else { php_error(E_WARNING, "Failed to find built-in putenv function"); } // Override getenv func = zend_hash_str_find_ptr(CG(function_table), "getenv", sizeof("getenv") - 1); if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) { ((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_getenv); } else { php_error(E_WARNING, "Failed to find built-in getenv function"); } return SUCCESS; } static zend_module_entry frankenphp_module = { STANDARD_MODULE_HEADER, "frankenphp", ext_functions, /* function table */ PHP_MINIT(frankenphp), /* initialization */ NULL, /* shutdown */ NULL, /* request initialization */ NULL, /* request shutdown */ NULL, /* information */ TOSTRING(FRANKENPHP_VERSION), STANDARD_MODULE_PROPERTIES}; static int frankenphp_startup(sapi_module_struct *sapi_module) { php_import_environment_variables = get_full_env; return php_module_startup(sapi_module, &frankenphp_module); } static int frankenphp_deactivate(void) { return SUCCESS; } static size_t frankenphp_ub_write(const char *str, size_t str_length) { struct go_ub_write_return result = go_ub_write(thread_index, (char *)str, str_length); if (result.r1) { php_handle_aborted_connection(); } return result.r0; } static int frankenphp_send_headers(sapi_headers_struct *sapi_headers) { if (SG(request_info).no_headers == 1) { return SAPI_HEADER_SENT_SUCCESSFULLY; } int status; if (SG(sapi_headers).http_status_line) { status = atoi((SG(sapi_headers).http_status_line) + 9); } else { status = SG(sapi_headers).http_response_code; if (!status) { status = 200; } } bool success = go_write_headers(thread_index, status, &sapi_headers->headers); if (success) { return SAPI_HEADER_SENT_SUCCESSFULLY; } return SAPI_HEADER_SEND_FAILED; } static void frankenphp_sapi_flush(void *server_context) { sapi_send_headers(); if (go_sapi_flush(thread_index)) { php_handle_aborted_connection(); } } static size_t frankenphp_read_post(char *buffer, size_t count_bytes) { return go_read_post(thread_index, buffer, count_bytes); } static char *frankenphp_read_cookies(void) { return go_read_cookies(thread_index); } /* all variables with well defined keys can safely be registered like this */ static inline void frankenphp_register_trusted_var(zend_string *z_key, char *value, size_t val_len, HashTable *ht) { if (value == NULL) { zval empty; ZVAL_EMPTY_STRING(&empty); zend_hash_update_ind(ht, z_key, &empty); return; } size_t new_val_len = val_len; if (!should_filter_var || sapi_module.input_filter(PARSE_SERVER, ZSTR_VAL(z_key), &value, new_val_len, &new_val_len)) { zval z_value; ZVAL_STRINGL_FAST(&z_value, value, new_val_len); zend_hash_update_ind(ht, z_key, &z_value); } } /* Register known $_SERVER variables in bulk to avoid cgo overhead */ void frankenphp_register_server_vars(zval *track_vars_array, frankenphp_server_vars vars) { HashTable *ht = Z_ARRVAL_P(track_vars_array); zend_hash_extend(ht, vars.total_num_vars, 0); // update values with variable strings #define FRANKENPHP_REGISTER_VAR(name) \ frankenphp_register_trusted_var(frankenphp_strings.name, vars.name, \ vars.name##_len, ht) FRANKENPHP_REGISTER_VAR(remote_addr); FRANKENPHP_REGISTER_VAR(remote_host); FRANKENPHP_REGISTER_VAR(remote_port); FRANKENPHP_REGISTER_VAR(document_root); FRANKENPHP_REGISTER_VAR(path_info); FRANKENPHP_REGISTER_VAR(php_self); FRANKENPHP_REGISTER_VAR(document_uri); FRANKENPHP_REGISTER_VAR(script_filename); FRANKENPHP_REGISTER_VAR(script_name); FRANKENPHP_REGISTER_VAR(ssl_cipher); FRANKENPHP_REGISTER_VAR(server_name); FRANKENPHP_REGISTER_VAR(server_port); FRANKENPHP_REGISTER_VAR(content_length); FRANKENPHP_REGISTER_VAR(server_protocol); FRANKENPHP_REGISTER_VAR(http_host); FRANKENPHP_REGISTER_VAR(request_uri); #undef FRANKENPHP_REGISTER_VAR /* update values with hard-coded zend_strings */ zval zv; ZVAL_STR(&zv, frankenphp_strings.cgi11); zend_hash_update_ind(ht, frankenphp_strings.gateway_interface, &zv); ZVAL_STR(&zv, frankenphp_strings.frankenphp); zend_hash_update_ind(ht, frankenphp_strings.server_software, &zv); ZVAL_STR(&zv, vars.request_scheme); zend_hash_update_ind(ht, frankenphp_strings.request_scheme, &zv); ZVAL_STR(&zv, vars.ssl_protocol); zend_hash_update_ind(ht, frankenphp_strings.ssl_protocol, &zv); ZVAL_STR(&zv, vars.https); zend_hash_update_ind(ht, frankenphp_strings.https, &zv); /* update values with always empty strings */ ZVAL_EMPTY_STRING(&zv); zend_hash_update_ind(ht, frankenphp_strings.auth_type, &zv); zend_hash_update_ind(ht, frankenphp_strings.remote_ident, &zv); } /** Create an immutable zend_string that lasts for the whole process **/ zend_string *frankenphp_init_persistent_string(const char *string, size_t len) { /* persistent strings will be ignored by the GC at the end of a request */ zend_string *z_string = zend_string_init(string, len, 1); zend_string_hash_val(z_string); /* interned strings will not be ref counted by the GC */ GC_ADD_FLAGS(z_string, IS_STR_INTERNED); return z_string; } /* initialize all hard-coded zend_strings once per process */ static void frankenphp_init_interned_strings(void) { if (frankenphp_strings.remote_addr != NULL) { return; /* already initialized */ } #define F_INITIALIZE_FIELD(name, str) \ frankenphp_strings.name = \ frankenphp_init_persistent_string(str, sizeof(str) - 1); FRANKENPHP_INTERNED_STRINGS_LIST(F_INITIALIZE_FIELD) #undef F_INITIALIZE_FIELD } /* Register variables from SG(request_info) into $_SERVER */ static inline void frankenphp_register_variable_from_request_info(zend_string *zKey, char *value, bool must_be_present, zval *track_vars_array) { if (value != NULL) { frankenphp_register_trusted_var(zKey, value, strlen(value), Z_ARRVAL_P(track_vars_array)); } else if (must_be_present) { frankenphp_register_trusted_var(zKey, NULL, 0, Z_ARRVAL_P(track_vars_array)); } } static void frankenphp_register_variables_from_request_info(zval *track_vars_array) { frankenphp_register_variable_from_request_info( frankenphp_strings.content_type, (char *)SG(request_info).content_type, true, track_vars_array); frankenphp_register_variable_from_request_info( frankenphp_strings.path_translated, (char *)SG(request_info).path_translated, false, track_vars_array); frankenphp_register_variable_from_request_info( frankenphp_strings.query_string, SG(request_info).query_string, true, track_vars_array); frankenphp_register_variable_from_request_info( frankenphp_strings.remote_user, (char *)SG(request_info).auth_user, false, track_vars_array); frankenphp_register_variable_from_request_info( frankenphp_strings.request_method, (char *)SG(request_info).request_method, false, track_vars_array); } /* Only hard-coded keys may be registered this way */ void frankenphp_register_known_variable(zend_string *z_key, char *value, size_t val_len, zval *track_vars_array) { frankenphp_register_trusted_var(z_key, value, val_len, Z_ARRVAL_P(track_vars_array)); } /* variables with user-defined keys must be registered safely * see: php_variables.c -> php_register_variable_ex (#1106) */ void frankenphp_register_variable_safe(char *key, char *val, size_t val_len, zval *track_vars_array) { if (key == NULL) { return; } if (val == NULL) { val = ""; } size_t new_val_len = val_len; if (!should_filter_var || sapi_module.input_filter(PARSE_SERVER, key, &val, new_val_len, &new_val_len)) { php_register_variable_safe(key, val, new_val_len, track_vars_array); } } void register_server_variable_filtered(const char *key, char **val, size_t *val_len, zval *track_vars_array) { if (sapi_module.input_filter(PARSE_SERVER, key, val, *val_len, val_len)) { php_register_variable_safe(key, *val, *val_len, track_vars_array); } } static void frankenphp_register_variables(zval *track_vars_array) { /* https://www.php.net/manual/en/reserved.variables.server.php */ /* In CGI mode, the environment is part of the $_SERVER variables. * $_SERVER and $_ENV should only contain values from the original * environment, not values added though putenv */ zend_hash_copy(Z_ARR_P(track_vars_array), main_thread_env, NULL); /* import CGI variables from the request context in go */ go_register_server_variables(thread_index, track_vars_array); /* Some variables are already present in SG(request_info) */ frankenphp_register_variables_from_request_info(track_vars_array); } static void frankenphp_log_message(const char *message, int syslog_type_int) { go_log(thread_index, (char *)message, syslog_type_int); } static char *frankenphp_getenv(const char *name, size_t name_len) { HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env; zval *env_val = zend_hash_str_find(ht, name, name_len); if (env_val && Z_TYPE_P(env_val) == IS_STRING) { zend_string *str = Z_STR_P(env_val); return ZSTR_VAL(str); } return NULL; } sapi_module_struct frankenphp_sapi_module = { "frankenphp", /* name */ "FrankenPHP", /* pretty name */ frankenphp_startup, /* startup */ php_module_shutdown_wrapper, /* shutdown */ NULL, /* activate */ frankenphp_deactivate, /* deactivate */ frankenphp_ub_write, /* unbuffered write */ frankenphp_sapi_flush, /* flush */ NULL, /* get uid */ frankenphp_getenv, /* getenv */ php_error, /* error handler */ NULL, /* header handler */ frankenphp_send_headers, /* send headers handler */ NULL, /* send header handler */ frankenphp_read_post, /* read POST data */ frankenphp_read_cookies, /* read Cookies */ frankenphp_register_variables, /* register server variables */ frankenphp_log_message, /* Log message */ NULL, /* Get request time */ NULL, /* Child terminate */ STANDARD_SAPI_MODULE_PROPERTIES}; /* Sets thread name for profiling and debugging. * * Adapted from https://github.com/Pithikos/C-Thread-Pool * Copyright: Johan Hanssen Seferidis * License: MIT */ static void set_thread_name(char *thread_name) { #if defined(__linux__) /* Use prctl instead to prevent using _GNU_SOURCE flag and implicit * declaration */ prctl(PR_SET_NAME, thread_name); #elif defined(__APPLE__) && defined(__MACH__) pthread_setname_np(thread_name); #elif defined(__FreeBSD__) || defined(__OpenBSD__) pthread_set_name_np(pthread_self(), thread_name); #endif } static void *php_thread(void *arg) { thread_index = (uintptr_t)arg; char thread_name[16] = {0}; snprintf(thread_name, 16, "php-%" PRIxPTR, thread_index); set_thread_name(thread_name); #ifdef ZTS /* initial resource fetch */ (void)ts_resource(0); #ifdef PHP_WIN32 ZEND_TSRMLS_CACHE_UPDATE(); #endif #endif // loop until Go signals to stop char *scriptName = NULL; while ((scriptName = go_frankenphp_before_script_execution(thread_index))) { go_frankenphp_after_script_execution(thread_index, frankenphp_execute_script(scriptName)); } #ifdef ZTS ts_free_thread(); #endif go_frankenphp_on_thread_shutdown(thread_index); return NULL; } static void *php_main(void *arg) { #ifndef ZEND_WIN32 /* * SIGPIPE must be masked in non-Go threads: * https://pkg.go.dev/os/signal#hdr-Go_programs_that_use_cgo_or_SWIG */ sigset_t set; sigemptyset(&set); sigaddset(&set, SIGPIPE); if (pthread_sigmask(SIG_BLOCK, &set, NULL) != 0) { perror("failed to block SIGPIPE"); exit(EXIT_FAILURE); } #endif set_thread_name("php-main"); #ifdef ZTS #if (PHP_VERSION_ID >= 80300) php_tsrm_startup_ex((intptr_t)arg); #else php_tsrm_startup(); #endif /*tsrm_error_set(TSRM_ERROR_LEVEL_INFO, NULL);*/ #ifdef PHP_WIN32 ZEND_TSRMLS_CACHE_UPDATE(); #endif #endif sapi_startup(&frankenphp_sapi_module); /* TODO: adapted from https://github.com/php/php-src/pull/16958, remove when * merged. */ #ifdef PHP_WIN32 { const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT; HMODULE module; /* Use a larger buffer to support long module paths on Windows. */ wchar_t filename[32768]; if (GetModuleHandleExW(flags, (LPCWSTR)&frankenphp_sapi_module, &module)) { const DWORD filename_capacity = (DWORD)_countof(filename); DWORD len = GetModuleFileNameW(module, filename, filename_capacity); if (len > 0 && len < filename_capacity) { wchar_t *slash = wcsrchr(filename, L'\\'); if (slash) { *slash = L'\0'; if (!SetDllDirectoryW(filename)) { fprintf(stderr, "Warning: SetDllDirectoryW failed (error %lu)\n", GetLastError()); } } } } } #endif #ifdef ZEND_MAX_EXECUTION_TIMERS /* overwrite php.ini with custom user settings */ char *php_ini_overrides = go_get_custom_php_ini(false); #else /* overwrite php.ini with custom user settings and disable * max_execution_timers */ char *php_ini_overrides = go_get_custom_php_ini(true); #endif if (php_ini_overrides != NULL) { frankenphp_sapi_module.ini_entries = php_ini_overrides; } frankenphp_init_interned_strings(); frankenphp_sapi_module.startup(&frankenphp_sapi_module); /* check if a default filter is set in php.ini and only filter if * it is, this is deprecated and will be removed in PHP 9 */ char *default_filter; cfg_get_string("filter.default", &default_filter); should_filter_var = default_filter != NULL; original_user_abort_setting = PG(ignore_user_abort); /* take a snapshot of the environment for sandboxing */ if (main_thread_env == NULL) { main_thread_env = pemalloc(sizeof(HashTable), 1); zend_hash_init(main_thread_env, 8, NULL, NULL, 1); go_init_os_env(main_thread_env); } go_frankenphp_main_thread_is_ready(); /* channel closed, shutdown gracefully */ frankenphp_sapi_module.shutdown(&frankenphp_sapi_module); sapi_shutdown(); #ifdef ZTS tsrm_shutdown(); #endif if (frankenphp_sapi_module.ini_entries) { free((char *)frankenphp_sapi_module.ini_entries); frankenphp_sapi_module.ini_entries = NULL; } go_frankenphp_shutdown_main_thread(); return NULL; } int frankenphp_new_main_thread(int num_threads) { pthread_t thread; if (pthread_create(&thread, NULL, &php_main, (void *)(intptr_t)num_threads) != 0) { return -1; } return pthread_detach(thread); } bool frankenphp_new_php_thread(uintptr_t thread_index) { pthread_t thread; if (pthread_create(&thread, NULL, &php_thread, (void *)thread_index) != 0) { return false; } pthread_detach(thread); return true; } static int frankenphp_request_startup() { frankenphp_update_request_context(); if (php_request_startup() == SUCCESS) { return SUCCESS; } php_request_shutdown((void *)0); frankenphp_free_request_context(); return FAILURE; } int frankenphp_execute_script(char *file_name) { if (frankenphp_request_startup() == FAILURE) { return FAILURE; } int status = SUCCESS; zend_file_handle file_handle; zend_stream_init_filename(&file_handle, file_name); file_handle.primary_script = 1; zend_first_try { EG(exit_status) = 0; php_execute_script(&file_handle); status = EG(exit_status); } zend_catch { status = EG(exit_status); } zend_end_try(); zend_destroy_file_handle(&file_handle); /* Reset the sandboxed environment */ if (sandboxed_env != NULL) { zend_hash_release(sandboxed_env); sandboxed_env = NULL; } php_request_shutdown((void *)0); frankenphp_free_request_context(); return status; } typedef struct { char *script; int argc; char **argv; bool eval; } cli_exec_args_t; static void *execute_script_cli(void *arg) { cli_exec_args_t *args = (cli_exec_args_t *)arg; volatile int v = PHP_VERSION_ID; (void)v; #if PHP_VERSION_ID >= 80600 return (void *)(intptr_t)do_php_cli(args->argc, args->argv); #else return (void *)(intptr_t)emulate_script_cli(args); #endif } int frankenphp_execute_script_cli(char *script, int argc, char **argv, bool eval) { pthread_t thread; int err; void *exit_status; cli_exec_args_t args = { .script = script, .argc = argc, .argv = argv, .eval = eval}; /* * Start the script in a dedicated thread to prevent conflicts between Go and * PHP signal handlers */ err = pthread_create(&thread, NULL, execute_script_cli, &args); if (err != 0) { return err; } err = pthread_join(thread, &exit_status); if (err != 0) { return err; } return (intptr_t)exit_status; } int frankenphp_reset_opcache(void) { zend_function *opcache_reset = zend_hash_str_find_ptr(CG(function_table), ZEND_STRL("opcache_reset")); if (opcache_reset) { zend_call_known_function(opcache_reset, NULL, NULL, NULL, 0, NULL, NULL); } return 0; } int frankenphp_get_current_memory_limit() { return PG(memory_limit); } static zend_module_entry **modules = NULL; static int modules_len = 0; static int (*original_php_register_internal_extensions_func)(void) = NULL; int register_internal_extensions(void) { if (original_php_register_internal_extensions_func != NULL && original_php_register_internal_extensions_func() != SUCCESS) { return FAILURE; } for (int i = 0; i < modules_len; i++) { if (zend_register_internal_module(modules[i]) == NULL) { return FAILURE; } } modules = NULL; modules_len = 0; return SUCCESS; } void register_extensions(zend_module_entry **m, int len) { modules = m; modules_len = len; original_php_register_internal_extensions_func = php_register_internal_extensions_func; php_register_internal_extensions_func = register_internal_extensions; }