Skip to content

Commit

Permalink
Dependency composer auto loader guard to force use of EDOT delivered …
Browse files Browse the repository at this point in the history
…code (#64) (#165)
  • Loading branch information
intuibase authored Feb 12, 2025
1 parent ebbf97a commit d092881
Show file tree
Hide file tree
Showing 22 changed files with 656 additions and 8 deletions.
2 changes: 1 addition & 1 deletion elastic-otel-php.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version=0.3.0
supported_php_versions=(81 82 83 84)
php_headers_version=2.0
logger_features_enum_values=ALL=0,MODULE=1,REQUEST=2,TRANSPORT=3,BOOTSTRAP=4,HOOKS=5,INSTRUMENTATION=6,OTEL=7
logger_features_enum_values=ALL=0,MODULE=1,REQUEST=2,TRANSPORT=3,BOOTSTRAP=4,HOOKS=5,INSTRUMENTATION=6,OTEL=7,DEPGUARD=8
26 changes: 25 additions & 1 deletion prod/native/extension/code/Hooking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,38 @@ static void elastic_execute_internal(INTERNAL_FUNCTION_PARAMETERS) {
ELASTICAPM_G(globals)->inferredSpans_->attachBacktraceIfInterrupted();
}

void Hooking::replaceHooks(bool enableInferredSpansHooks) {
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_try {
if (Hooking::getInstance().getOriginalZendCompileFile()) {
return Hooking::getInstance().getOriginalZendCompileFile()(file_handle, type);
}
}
zend_catch {
ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "%s: original call error", __FUNCTION__);
}
zend_end_try();
return nullptr;
}

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
10 changes: 9 additions & 1 deletion prod/native/extension/code/Hooking.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Hooking {
public:
using zend_execute_internal_t = void (*)(zend_execute_data *execute_data, zval *return_value);
using zend_interrupt_function_t = void (*)(zend_execute_data *execute_data);
using zend_compile_file_t = zend_op_array *(*)(zend_file_handle *file_handle, int type);

static Hooking &getInstance() {
static Hooking instance;
Expand All @@ -37,11 +38,13 @@ class Hooking {
void fetchOriginalHooks() {
original_execute_internal_ = zend_execute_internal;
original_zend_interrupt_function_ = zend_interrupt_function;
original_zend_compile_file_ = zend_compile_file;
}

void restoreOriginalHooks() {
zend_execute_internal = original_execute_internal_;
zend_interrupt_function = original_zend_interrupt_function_;
zend_compile_file = original_zend_compile_file_;
}

zend_execute_internal_t getOriginalExecuteInternal() {
Expand All @@ -52,7 +55,11 @@ class Hooking {
return original_zend_interrupt_function_;
}

void replaceHooks(bool enableInferredSpansHooks);
zend_compile_file_t getOriginalZendCompileFile() {
return original_zend_compile_file_;
}

void replaceHooks(bool enableInferredSpansHooks, bool enableDepenecyAutoloaderGuard);

private:
Hooking(Hooking const &) = delete;
Expand All @@ -61,6 +68,7 @@ class Hooking {

zend_execute_internal_t original_execute_internal_ = nullptr;
zend_interrupt_function_t original_zend_interrupt_function_ = nullptr;
zend_compile_file_t original_zend_compile_file_ = nullptr;
};

} // namespace elasticapm::php
2 changes: 1 addition & 1 deletion prod/native/extension/code/ModuleInit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ void elasticApmModuleInit(int moduleType, int moduleNumber) {

ELOGF_DEBUG(globals->logger_, MODULE, "MINIT Replacing hooks");
elasticapm::php::Hooking::getInstance().fetchOriginalHooks();
elasticapm::php::Hooking::getInstance().replaceHooks(globals->config_->get().inferred_spans_enabled);
elasticapm::php::Hooking::getInstance().replaceHooks(globals->config_->get().inferred_spans_enabled, globals->config_->get().dependency_autoloader_guard_enabled);

zend_observer_activate();
zend_observer_fcall_register(elasticapm::php::elasticRegisterObserver);
Expand Down
10 changes: 10 additions & 0 deletions prod/native/extension/phpt/tests/includes/bootstrap_mock.inc
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ final class PhpPartFacade

public static function handleError(): void {
}

public static function inferredSpans(int $durationMs, bool $internalFunction): bool {
return true;
}

public static function debugPreHook(mixed $object, array $params, ?string $class, string $function, ?string $filename, ?int $lineno): void {
}

public static function debugPostHook(mixed $object, array $params, mixed $retval, ?Throwable $exception): void {
}
}
4 changes: 3 additions & 1 deletion prod/native/libcommon/code/AgentGlobals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "InstrumentedFunctionHooksStorage.h"
#include "CommonUtils.h"
#include "transport/HttpTransportAsync.h"
#include "DependencyAutoLoaderGuard.h"

#include "LogFeature.h"
#include <signal.h>
Expand All @@ -49,13 +50,14 @@ AgentGlobals::AgentGlobals(std::shared_ptr<LoggerInterface> logger,
config_(std::make_shared<elasticapm::php::ConfigurationStorage>(std::move(updateConfigurationSnapshot))),
logger_(std::move(logger)),
bridge_(std::move(bridge)),
dependencyAutoLoaderGuard_(std::make_shared<DependencyAutoLoaderGuard>(bridge_, logger_)),
hooksStorage_(std::move(hooksStorage)),
sapi_(std::make_shared<elasticapm::php::PhpSapi>(bridge_->getPhpSapiName())),
inferredSpans_(std::move(inferredSpans)),
periodicTaskExecutor_(),
httpTransportAsync_(std::make_unique<elasticapm::php::transport::HttpTransportAsync<>>(logger_, config_)),
sharedMemory_(std::make_shared<elasticapm::php::SharedMemoryState>()),
requestScope_(std::make_shared<elasticapm::php::RequestScope>(logger_, bridge_, sapi_, sharedMemory_, inferredSpans_, config_, [hs = hooksStorage_]() { hs->clear(); }, [this]() { return getPeriodicTaskExecutor();})),
requestScope_(std::make_shared<elasticapm::php::RequestScope>(logger_, bridge_, sapi_, sharedMemory_, dependencyAutoLoaderGuard_, inferredSpans_, config_, [hs = hooksStorage_]() { hs->clear(); }, [this]() { return getPeriodicTaskExecutor();})),
logSinkStdErr_(std::move(logSinkStdErr)),
logSinkSysLog_(std::move(logSinkSysLog)),
logSinkFile_(std::move(logSinkFile))
Expand Down
4 changes: 3 additions & 1 deletion prod/native/libcommon/code/AgentGlobals.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ class ConfigurationSnapshot;
class LoggerSinkInterface;
class LogSinkFile;
class InstrumentedFunctionHooksStorageInterface;
class DependencyAutoLoaderGuard;
namespace transport {
class CurlSender;
class HttpEndpoints;
template <typename Sender, typename Endpoints>
class HttpTransportAsync;
}
} // namespace transport

// clang-format off

Expand All @@ -64,6 +65,7 @@ class AgentGlobals {
std::shared_ptr<ConfigurationStorage> config_;
std::shared_ptr<LoggerInterface> logger_;
std::shared_ptr<PhpBridgeInterface> bridge_;
std::shared_ptr<DependencyAutoLoaderGuard> dependencyAutoLoaderGuard_;
std::shared_ptr<InstrumentedFunctionHooksStorageInterface> hooksStorage_;
std::shared_ptr<PhpSapi> sapi_;
std::shared_ptr<InferredSpans> inferredSpans_;
Expand Down
4 changes: 3 additions & 1 deletion prod/native/libcommon/code/ConfigurationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ class ConfigurationManager {
BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_REDUCTION_ENABLED, OptionMetadata::type::boolean, false),
BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_STACKTRACE_ENABLED, OptionMetadata::type::boolean, false),
BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_SAMPLING_INTERVAL, OptionMetadata::type::duration, false),
BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_MIN_DURATION, OptionMetadata::type::duration, false)};
BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_MIN_DURATION, OptionMetadata::type::duration, false),
BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_DEPENDENCY_AUTOLOADER_GUARD_ENABLED, OptionMetadata::type::boolean, false)
};

// clang-format on
};
Expand Down
4 changes: 4 additions & 0 deletions prod/native/libcommon/code/ConfigurationSnapshot.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
#define ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_SAMPLING_INTERVAL inferred_spans_sampling_interval
#define ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_MIN_DURATION inferred_spans_min_duration

#define ELASTIC_OTEL_CFG_OPT_NAME_DEPENDENCY_AUTOLOADER_GUARD_ENABLED dependency_autoloader_guard_enabled

namespace elasticapm::php {

using namespace std::string_literals;
Expand Down Expand Up @@ -74,6 +76,8 @@ struct ConfigurationSnapshot {
std::chrono::milliseconds ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_SAMPLING_INTERVAL = std::chrono::milliseconds(50);
std::chrono::milliseconds ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_MIN_DURATION = std::chrono::milliseconds(0);

bool ELASTIC_OTEL_CFG_OPT_NAME_DEPENDENCY_AUTOLOADER_GUARD_ENABLED = true;

uint64_t revision = 0;
};

Expand Down
102 changes: 102 additions & 0 deletions prod/native/libcommon/code/DependencyAutoLoaderGuard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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 "DependencyAutoLoaderGuard.h"
#include "LoggerInterface.h"
#include "PhpBridgeInterface.h"

#include <functional>
#include <filesystem>
#include <format>
#include <set>

namespace elasticapm::php {
using namespace std::string_view_literals;

void DependencyAutoLoaderGuard::setBootstrapPath(std::string_view bootstrapFilePath) {
auto [major, minor] = bridge_->getPhpVersionMajorMinor();
auto path = std::filesystem::path(bootstrapFilePath).parent_path();
path /= std::format("vendor_{}{}", major, minor);
vendorPath_ = path.c_str();
ELOGF_DEBUG(logger_, DEPGUARD, "vendor path set to: " PRsv, PRsvArg(vendorPath_));
}

bool DependencyAutoLoaderGuard::shouldDiscardFileCompilation(std::string_view fileName) {
try {
std::string compiledFilePath = std::filesystem::exists(fileName) ? std::filesystem::canonical(fileName) : fileName;

if (compiledFilePath.starts_with(vendorPath_)) {
return false;
}

auto [lastClass, lastFunction] = bridge_->getNewlyCompiledFiles(
[this](std::string_view name) {
// storing only dependencies delivered by EDOT
if (name.starts_with(vendorPath_)) {
if (name.substr(vendorPath_.length()).starts_with("/composer/")) { // skip compsoer files - they must be compiled
ELOGF_TRACE(logger_, DEPGUARD, "Skipping storage of composer files: " PRsv, PRsvArg(name));
return;
}
compiledFiles_.insert(name);
ELOGF_TRACE(logger_, DEPGUARD, "Storing file: " PRsv, PRsvArg(name));
}
},
lastClass_, lastFunction_);

lastClass_ = lastClass;
lastFunction_ = lastFunction;

if (wasDeliveredByEDOT(compiledFilePath)) {
ELOGF_DEBUG(logger_, DEPGUARD, "Compilation of file '" PRsv "' will be discarded", PRsvArg(compiledFilePath));
return true;
}

} catch (std::exception const &e) {
ELOGF_WARNING(logger_, DEPGUARD, "shouldDiscardFileCompilation of file '" PRsv "' throwed: %s", PRsvArg(fileName), e.what());
return false;
}

return false;
}

bool DependencyAutoLoaderGuard::wasDeliveredByEDOT(std::string_view fileName) const {
constexpr std::string_view vendor = "/vendor/"sv;

auto vendorPos = fileName.find(vendor);
if (vendorPos == std::string_view::npos) {
return false;
}

auto afterVendor = fileName.substr(vendorPos + vendor.size());

auto found = std::find_if(std::begin(compiledFiles_), std::end(compiledFiles_), [afterVendor, bootstrapLen = vendorPath_.length()](std::string_view storedFile) -> bool {
std::string_view fileView = storedFile.substr(bootstrapLen + 1); // add 1 for slash
if (fileView == afterVendor) {
return true;
}
return false;
});

if (found != std::end(compiledFiles_)) {
return true;
}
return false;
}

} // namespace elasticapm::php
67 changes: 67 additions & 0 deletions prod/native/libcommon/code/DependencyAutoLoaderGuard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.
*/

#pragma once

#include <functional>
#include <memory>
#include <set>

namespace elasticapm::php {

class LoggerInterface;
class PhpBridgeInterface;

class DependencyAutoLoaderGuard {
public:
DependencyAutoLoaderGuard(std::shared_ptr<PhpBridgeInterface> bridge, std::shared_ptr<LoggerInterface> logger) : bridge_(std::move(bridge)), logger_(std::move(logger)) {
}

void setBootstrapPath(std::string_view bootstrapFilePath);

void onRequestInit() {
clear();
}

void onRequestShutdown() {
clear();
}

bool shouldDiscardFileCompilation(std::string_view fileName);

private:
bool wasDeliveredByEDOT(std::string_view fileName) const;

void clear() {
lastClass_ = 0;
lastFunction_ = 0;
compiledFiles_.clear();
}

private:
std::shared_ptr<PhpBridgeInterface> bridge_;
std::shared_ptr<LoggerInterface> logger_;
std::set<std::string_view> compiledFiles_; // string_view is safe because we're removing data on request end, they're request scope safe

std::size_t lastClass_ = 0;
std::size_t lastFunction_ = 0;

std::string vendorPath_;
};
} // namespace elasticapm::php
6 changes: 6 additions & 0 deletions prod/native/libcommon/code/PhpBridgeInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

#include "LogLevel.h"
#include <chrono>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
Expand Down Expand Up @@ -59,6 +60,11 @@ class PhpBridgeInterface {
virtual bool isScriptRestricedByOpcacheAPI() const = 0;
virtual bool detectOpcacheRestartPending() const = 0;
virtual bool isOpcacheEnabled() const = 0;

virtual void getCompiledFiles(std::function<void(std::string_view)> recordFile) const = 0;
virtual std::pair<std::size_t, std::size_t> getNewlyCompiledFiles(std::function<void(std::string_view)> recordFile, std::size_t lastClassIndex, std::size_t lastFunctionIndex) const = 0;

virtual std::pair<int, int> getPhpVersionMajorMinor() const = 0;
};

}
Loading

0 comments on commit d092881

Please sign in to comment.