diff --git a/agent/config.m4 b/agent/config.m4 index 9547c8044..1de342c03 100644 --- a/agent/config.m4 +++ b/agent/config.m4 @@ -190,7 +190,7 @@ if test "$PHP_NEWRELIC" = "yes"; then php_explain_pdo_mysql.c php_extension.c php_file_get_contents.c \ php_globals.c php_hash.c php_header.c php_httprequest_send.c \ php_internal_instrument.c php_minit.c php_mshutdown.c php_mysql.c \ - php_mysqli.c php_newrelic.c php_nrini.c php_output.c php_pdo.c \ + php_mysqli.c php_newrelic.c php_nrini.c php_observer.c php_output.c php_pdo.c \ php_pdo_mysql.c php_pdo_pgsql.c php_pgsql.c php_psr7.c php_redis.c \ php_rinit.c php_rshutdown.c php_samplers.c php_stack.c \ php_stacked_segment.c php_txn.c php_user_instrument.c \ diff --git a/agent/php_error.c b/agent/php_error.c index 918518c63..0de933b3b 100644 --- a/agent/php_error.c +++ b/agent/php_error.c @@ -449,14 +449,20 @@ static int nr_php_should_record_error(int type, const char* format TSRMLS_DC) { } #if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO +/* Prior to PHP8 these error_filename and error_lineno were only used to pass + * on to the error handler that the agent overwrote. With PHP8+, these values + * are currently unused since the agent is already recording the stack trace. + * HOWEVER, when code level metrics(CLM) are incorporated, these values can be + * used to add lineno and filename to error traces. + */ void nr_php_error_cb(int type, - zend_string* error_filename, - uint error_lineno, + zend_string* error_filename NRUNUSED, + uint error_lineno NRUNUSED, zend_string* message) { #elif ZEND_MODULE_API_NO == ZEND_8_0_X_API_NO void nr_php_error_cb(int type, - const char* error_filename, - uint error_lineno, + const char* error_filename NRUNUSED, + uint error_lineno NRUNUSED, zend_string* message) { #else void nr_php_error_cb(int type, @@ -502,17 +508,16 @@ void nr_php_error_cb(int type, } /* - * Call through to the actual error handler. + * Call through to the actual error handler for PHP 7.4 and below. + * For PHP 8+ we have registered our error handler with the Observer + * API so there is no need to callback to the original. */ - if (0 != NR_PHP_PROCESS_GLOBALS(orig_error_cb)) { #if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO + if (0 != NR_PHP_PROCESS_GLOBALS(orig_error_cb)) { NR_PHP_PROCESS_GLOBALS(orig_error_cb) (type, error_filename, error_lineno, format, args); -#else - NR_PHP_PROCESS_GLOBALS(orig_error_cb) - (type, error_filename, error_lineno, message); -#endif /* PHP < 8.0 */ } +#endif /* PHP < 8.0 */ } nr_status_t nr_php_error_record_exception(nrtxn_t* txn, diff --git a/agent/php_execute.c b/agent/php_execute.c index 35a4c78c5..f3933dd4f 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -29,9 +29,9 @@ #include "util_url.h" #include "util_metrics.h" #include "util_number_converter.h" -#include "php_execute.h" #include "fw_support.h" #include "fw_hooks.h" +#include "php_observer.h" /* * This wall of text is important. Read it. Understand it. Really. @@ -1538,3 +1538,43 @@ void nr_php_user_instrumentation_from_opcache(TSRMLS_D) { end: nr_php_zval_free(&status); } + +/* + * nr_php_observer_fcall_begin and nr_php_observer_fcall_end + * are Observer API function handlers that are the entry point to instrumenting + * userland code and should replicate the functionality of + * nr_php_execute_enabled, nr_php_execute, and nr_php_execute_show that are used + * when hooking in via zend_execute_ex. + * + * Observer API functionality was added with PHP 8.0. + * See nr_php_observer.h/c for more information. + */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ +void nr_php_observer_fcall_begin(zend_execute_data* execute_data) { + /* + * Instrument the function. + * This and any other needed helper functions will replace: + * nr_php_execute_enabled + * nr_php_execute + * nr_php_execute_show + */ + + if (NULL == execute_data) { + return; + } +} + +void nr_php_observer_fcall_end(zend_execute_data* execute_data, + zval* return_value) { + /* + * Instrument the function. + * This and any other needed helper functions will replace: + * nr_php_execute_enabled + * nr_php_execute + * nr_php_execute_show + */ + if ((NULL == execute_data) || (NULL == return_value)) { + return; + } +} +#endif diff --git a/agent/php_execute.h b/agent/php_execute.h index dfefdc8c8..a594a28a2 100644 --- a/agent/php_execute.h +++ b/agent/php_execute.h @@ -64,6 +64,8 @@ extern void nr_framework_create_metric(TSRMLS_D); * This is necessary to correctly instrument frameworks and libraries * that are preloaded. */ + extern void nr_php_user_instrumentation_from_opcache(TSRMLS_D); +#include "php_observer.h" #endif /* PHP_EXECUTE_HDR */ diff --git a/agent/php_hooks.h b/agent/php_hooks.h index 5f21f6f98..6f1e1bd47 100644 --- a/agent/php_hooks.h +++ b/agent/php_hooks.h @@ -38,13 +38,9 @@ extern void nr_php_execute(NR_EXECUTE_PROTO TSRMLS_DC); * are strongly encouraged to use the new error notification API instead. * Error notification callbacks are guaranteed to be called regardless of the * users error_reporting setting or userland error handler return values. - * Register notification callbacks during MINIT of an extension: -void my_error_notify_cb(int type, - const char *error_filename, - uint32_t error_lineno, - zend_string *message) { - } - zend_register_error_notify_callback(my_error_notify_cb); + * + * The register notification callbacks during MINIT of an extension are done in + * `php_observer.c/h`. */ #if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO extern void nr_php_error_cb(int type, diff --git a/agent/php_includes.h b/agent/php_includes.h index b53b26abc..764a846fd 100644 --- a/agent/php_includes.h +++ b/agent/php_includes.h @@ -53,6 +53,10 @@ #define ZEND_8_0_X_API_NO 20200930 #define ZEND_8_1_X_API_NO 20210902 +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ +#include "Zend/zend_observer.h" +#endif + #if ZEND_MODULE_API_NO >= ZEND_5_6_X_API_NO #include "Zend/zend_virtual_cwd.h" #else /* PHP < 5.6 */ diff --git a/agent/php_minit.c b/agent/php_minit.c index 66dc70e1e..7081c927c 100644 --- a/agent/php_minit.c +++ b/agent/php_minit.c @@ -38,6 +38,8 @@ #include "fw_laravel.h" #include "lib_guzzle4.h" +#include "php_observer.h" + static void php_newrelic_init_globals(zend_newrelic_globals* nrg) { if (nrunlikely(NULL == nrg)) { return; @@ -408,6 +410,7 @@ PHP_MINIT_FUNCTION(newrelic) { zend_extension dummy; #else char dummy[] = "newrelic"; + nr_php_observer_minit(); #endif (void)type; @@ -723,6 +726,7 @@ void nr_php_late_initialization(void) { * forward the errors, so if a user has Xdebug loaded, we do not install * our own error callback handler. Otherwise, we do. */ +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO /* < PHP8 */ if (0 == zend_get_extension("Xdebug")) { NR_PHP_PROCESS_GLOBALS(orig_error_cb) = zend_error_cb; zend_error_cb = nr_php_error_cb; @@ -731,6 +735,7 @@ void nr_php_late_initialization(void) { "the Xdebug extension prevents the New Relic agent from " "gathering errors. No errors will be recorded."); } +#endif /* end of < PHP8 */ /* * Install our signal handler, unless the user has set a special flag diff --git a/agent/php_observer.c b/agent/php_observer.c new file mode 100644 index 000000000..70fee1793 --- /dev/null +++ b/agent/php_observer.c @@ -0,0 +1,97 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * This file handles the initialization that happens once per module load. + */ +#include "php_agent.h" + +#include +#include + +#include "php_api_distributed_trace.h" +#include "php_environment.h" +#include "php_error.h" +#include "php_extension.h" +#include "php_globals.h" +#include "php_header.h" +#include "php_hooks.h" +#include "php_internal_instrument.h" +#include "php_samplers.h" +#include "php_user_instrument.h" +#include "php_vm.h" +#include "php_wrapper.h" +#include "fw_laravel.h" +#include "lib_guzzle4.h" +#include "lib_guzzle6.h" +#include "nr_agent.h" +#include "nr_app.h" +#include "nr_banner.h" +#include "nr_daemon_spawn.h" +#include "util_logging.h" +#include "util_memory.h" +#include "util_signals.h" +#include "util_strings.h" +#include "util_syscalls.h" +#include "util_threads.h" + +#include "php_observer.h" +#include "php_execute.h" + +/* + * Observer API functionality was added with PHP 8.0. + * + * The Observer API provide function handlers that trigger on every userland + * function begin and end. The handlers provide all zend_execute_data and the + * end handler provides the return value pointer. The previous way to hook into + * PHP was via zend_execute_ex which will hook all userland function calls with + * significant overhead for doing the call. However, depending on user stack + * size settings, it could potentially generate an extremely deep call stack in + * PHP because zend_execute_ex limits stack size to whatever user settings + * are. Observer API bypasses the stack overflow issue that an agent could run + * into when intercepting userland calls. Additionally, with PHP 8.0, JIT + * optimizations could optimize out a call to zend_execute_ex and the agent + * would not be able to overwite that call properly as the agent wouldn't have + * access to the JITed information. This could lead to segfaults and caused PHP + * to decide to disable JIT when detecting extensions that overwrote + * zend_execute_ex. + * + * It only provides ZEND_USER_FUNCTIONS yet as it was assumed mechanisms already + * exist to monitor internal functions by overwriting internal function + * handlers. This will be included in PHP 8.2: Registered + * zend_observer_fcall_init handlers are now also called for internal functions. + * + * Without overwriting the execute function and therefore being responsible for + * continuing the execution of ALL functions that we intercepted, the agent is + * provided zend_execute_data on each function start/end and is then able to use + * it with our currently existing logic and instrumentation. + */ + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ +/* + * Register the begin and end function handlers with the Observer API. + */ +static zend_observer_fcall_handlers nr_php_fcall_register_handlers( + zend_execute_data* execute_data) { + zend_observer_fcall_handlers handlers = {NULL, NULL}; + if (NULL == execute_data) { + return handlers; + } + if ((NULL == execute_data->func) + || (ZEND_INTERNAL_FUNCTION == execute_data->func->type)) { + return handlers; + } + handlers.begin = nr_php_observer_fcall_begin; + handlers.end = nr_php_observer_fcall_end; + return handlers; +} + +void nr_php_observer_minit() { + /* + * Register the Observer API handlers. + */ + zend_observer_fcall_register(nr_php_fcall_register_handlers); + zend_observer_error_register(nr_php_error_cb); +} + +#endif diff --git a/agent/php_observer.h b/agent/php_observer.h new file mode 100644 index 000000000..d9e693140 --- /dev/null +++ b/agent/php_observer.h @@ -0,0 +1,68 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * This file is the wrapper for PHP 8+ Observer API (OAPI) functionality. + * + * The registered function handlers are the entry points of instrumentation and + * are implemented in php_execute.c which contains the brains/helper functions + * required to monitor PHP. + */ + +#ifndef NEWRELIC_PHP_AGENT_PHP_OBSERVER_H +#define NEWRELIC_PHP_AGENT_PHP_OBSERVER_H + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ + +#include "Zend/zend_observer.h" + +/* + * Purpose : Register the OAPI function handlers and any other minit actions. + * + * Params : None + * + * Returns : Void. + */ +void nr_php_observer_minit(); + +/* + * Purpose : Call the necessary functions needed to instrument a function by + * updating a transaction or segment for a function that has just + * started. This function is registered via the Observer API and will be called + * by the zend engine every time a function begins. The zend engine directly + * provides the zend_execute_data which has all details we need to know about + * the function. This and nr_php_execute_observer_fcall_end sum to provide all + * the functionality of nr_php_execute and nr_php _execute_enabled and as such + * will use all the helper functions they also used. + * + * + * Params : 1. zend_execute_data: everything we need to know about the + * function. + * + * Returns : Void. + */ +void nr_php_observer_fcall_begin(zend_execute_data* execute_data); +/* + * Purpose : Call the necessary functions needed to instrument a function when + * updating a transaction or segment for a function that has just + * ended. This function is registered via the Observer API and will be called by + * the zend engine every time a function ends. The zend engine directly + * provides the zend_execute_data and the return_value pointer, both of which + * have all details that the agent needs to know about the function. This and + * nr_php_execute_observer_fcall_start sum to provide all the functionality of + * nr_php_execute and nr_php_execute_enabled and as such will use all the helper + * functions they also used. + * + * + * Params : 1. zend_execute_data: everything to know about the function. + * 2. return_value: function return value information + * + * Returns : Void. + */ +void nr_php_observer_fcall_end(zend_execute_data* execute_data, + zval* return_value); +#endif /* PHP8+ */ + +#endif // NEWRELIC_PHP_AGENT_PHP_OBSERVER_H