prod/native/extension/code/Hooking.cpp (108 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "Hooking.h"
#include "ModuleGlobals.h"
#include "PhpBridge.h"
#include "PhpErrorData.h"
#include <memory>
#include <string_view>
#include "InternalFunctionInstrumentation.h"
#include "PeriodicTaskExecutor.h"
#include "RequestScope.h"
#include "InferredSpans.h"
#include "os/OsUtils.h"
#include <main/php_version.h>
#include <Zend/zend_API.h>
#include <Zend/zend_execute.h>
#include <Zend/zend_observer.h>
namespace elasticapm::php {
#if PHP_VERSION_ID < 80100
void elastic_observer_error_cb(int type, const char *error_filename, uint32_t error_lineno, zend_string *message) {
std::string_view fileName = error_filename ? std::string_view{error_filename} : std::string_view{};
#else
void elastic_observer_error_cb(int type, zend_string *error_filename, uint32_t error_lineno, zend_string *message) {
std::string_view fileName = error_filename ? std::string_view{ZSTR_VAL(error_filename), ZSTR_LEN(error_filename)} : std::string_view{};
#endif
std::string_view msg = message && ZSTR_VAL(message) ? std::string_view{ZSTR_VAL(message), ZSTR_LEN(message)} : std::string_view{};
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "elastic_observer_error_cb type: %d, fn: " PRsv ":%d, msg: " PRsv " ED: %p", type, PRsvArg(fileName), error_lineno, PRsvArg(msg), EG(current_execute_data));
static bool errorHandling = false;
if (errorHandling) {
ELOGF_WARNING(ELASTICAPM_G(globals)->logger_, HOOKS, "elastic_observer_error_cb detected error handler loop, skipping error handler");
return;
}
// we're looking if function (inside which error was thrown) is instrumented - if yes, w're skipping default error instrumentation and letting post hook to handler error.
if (EG(current_execute_data)) {
auto hash = getClassAndFunctionHashFromExecuteData(EG(current_execute_data));
if (hash) {
if (ELASTICAPM_G(globals)->logger_ && ELASTICAPM_G(globals)->logger_->doesMeetsLevelCondition(LogLevel::logLevel_debug)) {
auto [cls, fun] = getClassAndFunctionName(EG(current_execute_data));
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "elastic_observer_error_cb currentED: %p currentEXception: %p hash: 0x%X " PRsv "::" PRsv, EG(current_execute_data), EG(exception), hash, PRsvArg(cls), PRsvArg(fun));
}
auto callbacks = reinterpret_cast<InstrumentedFunctionHooksStorage_t *>(EAPM_GL(hooksStorage_).get())->find(hash);
if (callbacks) {
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "elastic_observer_error_cb type: %d, fn: " PRsv ":%d, msg: " PRsv ". Skipping default error instrumentation because function is instrumented and error will be passed to posthook", type, PRsvArg(fileName), error_lineno, PRsvArg(msg));
return;
}
} else {
ELOGF_WARNING(ELASTICAPM_G(globals)->logger_, HOOKS, "elastic_observer_error_cb currentED: %p currentEXception: %p func null, msg: " PRsv, EG(current_execute_data), EG(exception), PRsvArg(msg));
}
}
errorHandling = true;
ELASTICAPM_G(globals)->requestScope_->handleError(type, fileName, error_lineno, msg, static_cast<bool>(EG(current_execute_data)));
errorHandling = false;
}
static void elastic_interrupt_function(zend_execute_data *execute_data) {
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "%s: interrupt", __FUNCTION__);
ELASTICAPM_G(globals)->inferredSpans_->attachBacktraceIfInterrupted();
zend_try {
if (Hooking::getInstance().getOriginalZendInterruptFunction()) {
Hooking::getInstance().getOriginalZendInterruptFunction()(execute_data);
}
}
zend_catch {
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "%s: original call error", __FUNCTION__);
}
zend_end_try();
}
static void elastic_execute_internal(INTERNAL_FUNCTION_PARAMETERS) {
zend_try {
if (Hooking::getInstance().getOriginalExecuteInternal()) {
Hooking::getInstance().getOriginalExecuteInternal()(INTERNAL_FUNCTION_PARAM_PASSTHRU);
} else {
execute_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
}
zend_catch {
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "%s: original call error", __FUNCTION__);
}
zend_end_try();
ELASTICAPM_G(globals)->inferredSpans_->attachBacktraceIfInterrupted();
}
static zend_op_array *elastic_compile_file(zend_file_handle *file_handle, int type) {
std::string_view file = file_handle->opened_path ? std::string_view(ZSTR_VAL(file_handle->opened_path), ZSTR_LEN(file_handle->opened_path)) : std::string_view(ZSTR_VAL(file_handle->filename), ZSTR_LEN(file_handle->filename));
if (ELASTICAPM_G(globals)->dependencyAutoLoaderGuard_->shouldDiscardFileCompilation(file)) {
return nullptr;
}
zend_op_array *ret = nullptr;
zend_try {
if (Hooking::getInstance().getOriginalZendCompileFile()) {
ret = Hooking::getInstance().getOriginalZendCompileFile()(file_handle, type);
}
}
zend_catch {
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "%s: original call error", __FUNCTION__);
ret = nullptr;
}
zend_end_try();
return ret;
}
void Hooking::replaceHooks(bool enableInferredSpansHooks, bool enableDepenecyAutoloaderGuard) {
zend_observer_error_register(elastic_observer_error_cb);
if (enableInferredSpansHooks) {
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "Hooked into zend_execute_internal and zend_interrupt_function");
zend_execute_internal = elastic_execute_internal;
zend_interrupt_function = elastic_interrupt_function;
}
if (enableDepenecyAutoloaderGuard) {
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "Hooked into zend_compile_file, original ptr: %p, new ptr: %p", zend_compile_file, elastic_compile_file);
zend_compile_file = elastic_compile_file;
}
}
} // namespace elasticapm::php