Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loader: disable the tracer in case of a potential incompatibility (extension/jit) #2853

Merged
merged 8 commits into from
Sep 27, 2024
25 changes: 23 additions & 2 deletions .circleci/continue_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3332,6 +3332,9 @@ jobs:
switch_php_version:
type: string
default: none
use_valgrind:
type: boolean
default: false
resource_class:
type: string
default: medium
Expand All @@ -3354,6 +3357,18 @@ jobs:
command: |
set -xeuo pipefail

if [[ "<< parameters.php_major_minor >>" == "8.3" ]]; then
export XDEBUG_SO_NAME=xdebug-3.3.0.so
elif [[ "<< parameters.php_major_minor >>" == "8.2" ]]; then
export XDEBUG_SO_NAME=xdebug-3.2.2.so
elif [[ "<< parameters.php_major_minor >>" == "8.1" ]]; then
export XDEBUG_SO_NAME=xdebug-3.1.0.so
elif [[ "<< parameters.php_major_minor >>" == "8.0" ]]; then
export XDEBUG_SO_NAME=xdebug-3.0.0.so
elif [[ "<< parameters.php_major_minor >>" == "7.4" ]]; then
export XDEBUG_SO_NAME=xdebug-2.9.5.so
fi

rm -rf dd-library-php-ssi
tar -xzf dd-library-php-ssi-*-linux.tar.gz
export DD_LOADER_PACKAGE_PATH=${PWD}/dd-library-php-ssi
Expand All @@ -3362,6 +3377,10 @@ jobs:
mkdir -p modules
cp ../dd-library-php-ssi/linux-gnu/loader/dd_library_loader.so modules/
./bin/test.sh
if [[ "<< parameters.php_major_minor >>" == "8.3" ]]; then
true
<<# parameters.use_valgrind >>echo "Run with Valgrind" ; TEST_USE_VALGRIND=1 ./bin/test.sh<</ parameters.use_valgrind >>
fi
./bin/check_glibc_version.sh

"test_loader_alpine":
Expand Down Expand Up @@ -3394,6 +3413,7 @@ jobs:
set -xeuo pipefail

apk add --no-cache curl-dev << parameters.alpine_packages >>
export XDEBUG_SO_NAME=xdebug.so

rm -rf dd-library-php-ssi
tar -xzf dd-library-php-ssi-*-linux.tar.gz
Expand Down Expand Up @@ -4921,6 +4941,7 @@ workflows:
name: "[Linux/x86_64] Loader PHP << matrix.php_major_minor >>-<< matrix.switch_php_version >>"
docker_image: "datadog/dd-trace-ci:php-<< matrix.php_major_minor >>_buster"
resource_class: "medium"
use_valgrind: true
matrix:
parameters:
php_major_minor:
Expand Down Expand Up @@ -4976,7 +4997,7 @@ workflows:
name: "[Alpine/x86_64] Loader PHP Alpine << matrix.alpine_version >>"
docker_image: "alpine:<< matrix.alpine_version >>"
resource_class: "medium"
alpine_packages: "php83 php83-dev"
alpine_packages: "php83 php83-dev php83-pecl-xdebug"
matrix:
parameters:
alpine_version:
Expand All @@ -4990,7 +5011,7 @@ workflows:
name: "[Alpine/aarch64] Loader PHP Alpine << matrix.alpine_version >>"
docker_image: "alpine:<< matrix.alpine_version >>"
resource_class: "arm.medium"
alpine_packages: "php83 php83-dev"
alpine_packages: "php83 php83-dev php83-pecl-xdebug"
matrix:
parameters:
alpine_version:
Expand Down
11 changes: 11 additions & 0 deletions loader/compat_php.c
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,14 @@ zval *ddloader_zend_hash_update(HashTable *ht, zend_string *key, zval *pData) {

return NULL;
}

// From PHP 8.0
bool ddloader_zend_ini_parse_bool(zend_string *str) {
if ((ZSTR_LEN(str) == 4 && strcasecmp(ZSTR_VAL(str), "true") == 0)
|| (ZSTR_LEN(str) == 3 && strcasecmp(ZSTR_VAL(str), "yes") == 0)
|| (ZSTR_LEN(str) == 2 && strcasecmp(ZSTR_VAL(str), "on") == 0)) {
return true;
} else {
return atoi(ZSTR_VAL(str)) != 0;
}
}
1 change: 1 addition & 0 deletions loader/compat_php.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ zval *ddloader_zend_hash_set_bucket_key(int php_api_no, HashTable *ht, Bucket *b
void ddloader_replace_zend_error_cb(int php_api_no);
void ddloader_restore_zend_error_cb();
zval *ddloader_zend_hash_update(HashTable *ht, zend_string *key, zval *pData);
bool ddloader_zend_ini_parse_bool(zend_string *str);

#endif /* DD_LIBRARY_LOADER_COMPAT_PHP_H */
120 changes: 102 additions & 18 deletions loader/dd_library_loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <php.h>
#include <php_ini.h>
#include <stdbool.h>
#include <main/SAPI.h>
#include <ext/standard/basic_functions.h>

#include "compat_php.h"
Expand All @@ -28,6 +29,8 @@ static bool injection_forced = false;
# define OS_PATH "linux-gnu/"
#endif

static void ddloader_telemetryf(telemetry_reason reason, const char *format, ...);

static char *ddtrace_pre_load_hook(void) {
char *libddtrace_php;
int res = asprintf(&libddtrace_php, "%s/%sloader/libddtrace_php.so", package_path, OS_PATH);
Expand Down Expand Up @@ -60,6 +63,74 @@ static char *ddtrace_pre_load_hook(void) {
return NULL;
}

static bool ddloader_is_ext_loaded(const char *name) {
return zend_hash_str_find_ptr(&module_registry, name, strlen(name))
|| zend_get_extension(name)
;
}

static zval *ddloader_ini_get_configuration(const char *name, size_t name_len) {
HashTable *configuration_hash = php_ini_get_configuration_hash();
if (!configuration_hash) {
return NULL;
}
zend_string *ini = ddloader_zend_string_init(php_api_no, name, name_len, 1);
zval *val = zend_hash_find(configuration_hash, ini);
ddloader_zend_string_release(php_api_no, ini);

return val;
}

static void ddloader_ini_set_configuration(const char *name, size_t name_len, const char *value, size_t value_len) {
HashTable *configuration_hash = php_ini_get_configuration_hash();
if (!configuration_hash) {
return;
}

zend_string *zstr_name = ddloader_zend_string_init(php_api_no, name, name_len, 1);
zend_string *zstr_value = ddloader_zend_string_init(php_api_no, value, value_len, 1);

zval tmp;
ZVAL_STR(&tmp, zstr_value);
ddloader_zend_hash_update(configuration_hash, zstr_name, &tmp);
ddloader_zend_string_release(php_api_no, zstr_name);
}

static bool ddloader_is_opcache_jit_enabled() {
// JIT is only PHP 8.0+
if (php_api_no < 20200930 || !ddloader_is_ext_loaded("Zend OPcache")) {
return false;
}

// opcache.enable = false (default: true)
zval *opcache_enable = ddloader_ini_get_configuration(ZEND_STRL("opcache.enable"));
if (opcache_enable && Z_TYPE_P(opcache_enable) == IS_STRING && !ddloader_zend_ini_parse_bool(Z_STR_P(opcache_enable))) {
return false;
}
if (strcmp("cli", sapi_module.name) == 0) {
// opcache.enable_cli = false (default: false)
zval *opcache_enable_cli = ddloader_ini_get_configuration(ZEND_STRL("opcache.enable_cli"));
if (!opcache_enable_cli || Z_TYPE_P(opcache_enable_cli) != IS_STRING || !ddloader_zend_ini_parse_bool(Z_STR_P(opcache_enable_cli))) {
return false;
}
}
if (php_api_no > 20230831) { // PHP > 8.3 (https://php.watch/versions/8.4/opcache-jit-ini-default-changes)
// opcache.jit == disable (default: disable)
zval *opcache_jit = ddloader_ini_get_configuration(ZEND_STRL("opcache.jit"));
if (!opcache_jit || Z_TYPE_P(opcache_jit) != IS_STRING || strcmp(Z_STRVAL_P(opcache_jit), "disable") == 0 || strcmp(Z_STRVAL_P(opcache_jit), "off") == 0) {
return false;
}
} else {
// opcache.jit_buffer_size = 0 (default: 0)
zval *opcache_jit_buffer_size = ddloader_ini_get_configuration(ZEND_STRL("opcache.jit_buffer_size"));
if (!opcache_jit_buffer_size || Z_TYPE_P(opcache_jit_buffer_size) != IS_STRING || strcmp(Z_STRVAL_P(opcache_jit_buffer_size), "0") == 0) {
return false;
}
}

return true;
}

static void ddtrace_pre_minit_hook(void) {
HashTable *configuration_hash = php_ini_get_configuration_hash();
if (configuration_hash) {
Expand All @@ -69,13 +140,36 @@ static void ddtrace_pre_minit_hook(void) {
}

// Set 'datadog.trace.sources_path' setting
zend_string *name = ddloader_zend_string_init(php_api_no, ZEND_STRL("datadog.trace.sources_path"), 1);
zend_string *value = ddloader_zend_string_init(php_api_no, sources_path, strlen(sources_path), 1);
ddloader_ini_set_configuration(ZEND_STRL("datadog.trace.sources_path"), sources_path, strlen(sources_path));
free(sources_path);
}

zval tmp;
ZVAL_STR(&tmp, value);
ddloader_zend_hash_update(configuration_hash, name, &tmp);
// Load, but disable the tracer if runtime configuration is not safe for auto-injection
bool disable_tracer = false;

char *incompatible_exts[] = {"Xdebug", "the ionCube PHP Loader", "newrelic", "blackfire", "pcov"};
for (size_t i = 0; i < sizeof(incompatible_exts) / sizeof(incompatible_exts[0]); ++i) {
if (ddloader_is_ext_loaded(incompatible_exts[i])) {
if (force_load) {
LOG(WARN, "Potentially incompatible extension detected: %s. Ignoring as DD_INJECT_FORCE is enabled", incompatible_exts[i]);
} else {
LOG(WARN, "Potentially incompatible extension detected: %s. ddtrace will be disabled unless the environment DD_INJECT_FORCE is set to '1', 'true', 'yes' or 'on'", incompatible_exts[i]);
disable_tracer = true;
}
}
}

if (ddloader_is_opcache_jit_enabled()) {
if (force_load) {
LOG(WARN, "OPcache JIT is enabled and may cause instability. Ignoring as DD_INJECT_FORCE is enabled");
} else {
LOG(WARN, "OPcache JIT is enabled and may cause instability. ddtrace will be disabled unless the environment DD_INJECT_FORCE is set to '1', 'true', 'yes' or 'on'");
disable_tracer = true;
}
}

if (disable_tracer) {
ddloader_ini_set_configuration(ZEND_STRL("ddtrace.disable"), ZEND_STRL("1"));
}
}

Expand Down Expand Up @@ -460,26 +554,19 @@ static int ddloader_load_extension(unsigned int php_api_no, char *module_build_i
config->module_number = module_entry->module_number;
config->version = (char *)module_entry->version;

return SUCCESS;
goto ok;
iamluc marked this conversation as resolved.
Show resolved Hide resolved

abort_and_unload:
LOG(INFO, "Unloading the library");
DL_UNLOAD(handle);
abort:
LOG(INFO, "Abort the loader");
ok:
free(ext_path);

return SUCCESS;
}

static void ddloader_strtolower(char *dest, char *src) {
while (*src) {
*dest = (char)tolower((int)*src);
++dest;
++src;
}
}

static bool ddloader_is_truthy(char *str) {
if (!str) {
return false;
Expand All @@ -490,10 +577,7 @@ static bool ddloader_is_truthy(char *str) {
return false;
}

char lower[5] = {0};
ddloader_strtolower(lower, str);

return (strcmp(lower, "1") == 0 || strcmp(lower, "true") == 0 || strcmp(lower, "yes") == 0 || strcmp(lower, "on") == 0);
return (strcasecmp(str, "1") == 0 || strcasecmp(str, "true") == 0 || strcasecmp(str, "yes") == 0 || strcasecmp(str, "on") == 0);
}

static inline void ddloader_configure() {
Expand Down
10 changes: 6 additions & 4 deletions loader/php_dd_library_loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ typedef enum {

#define TELEMETRY(reason, format, ...) ddloader_telemetryf(reason, format, ##__VA_ARGS__);

#define DECLARE_INJECTED_EXT(name, dir, _pre_load_hook, _pre_minit_hook, deps) \
{ \
.ext_name = name, .ext_dir = dir, .tmp_name = name "_injected", .tmp_deps = deps, .pre_load_hook = _pre_load_hook, .pre_minit_hook = _pre_minit_hook, .orig_module_startup_func = NULL, \
.orig_module_deps = NULL, .orig_module_functions = NULL, .module_number = -1, .version = NULL \
#define DECLARE_INJECTED_EXT(name, dir, _pre_load_hook, _pre_minit_hook, deps) \
{ \
.ext_name = name, .ext_dir = dir, .tmp_name = name "_injected", .tmp_deps = deps, \
.pre_load_hook = _pre_load_hook, .pre_minit_hook = _pre_minit_hook, \
.orig_module_startup_func = NULL, .orig_module_deps = NULL, .orig_module_functions = NULL, \
.module_number = -1, .version = NULL \
}

typedef struct _injected_ext {
Expand Down
23 changes: 23 additions & 0 deletions loader/tests/functional/fixtures/opcache_jit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

function boolstr($bool) {
return ($bool ? 'YES' : 'NO');
}

echo "ddtrace.disable: ".boolstr((bool) ini_get('ddtrace.disable'))."\n";

if (!extension_loaded('Zend OPcache')) {
echo "OPcache is not loaded\n";
return;
}

$status = opcache_get_status();
if ($status === false) {
echo "OPcache is disabled\n";
return;
}

echo "opcache_enabled: ".boolstr($status['opcache_enabled'])."\n";
echo "jit.enabled: ".boolstr($status['jit']['enabled'])."\n";
echo "jit.on: ".boolstr($status['jit']['on'])."\n";
echo "jit.buffer_size: ".$status['jit']['buffer_size']."\n";
37 changes: 36 additions & 1 deletion loader/tests/functional/includes/autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

require_once __DIR__.'/assert.php';

set_exception_handler(function (\Exception $ex) {
set_exception_handler(function (\Throwable $ex) {
$trace = $ex->getTrace();
$file = $trace[0]['file'] ?: '';
$line = $trace[0]['line'] ?: '';
Expand All @@ -22,8 +22,20 @@ function runCLI($args, $useLoader = true, $env = [], $noIni = true) {
if (!isset($_SERVER['DD_LOADER_PACKAGE_PATH'])) {
$env[] = "DD_LOADER_PACKAGE_PATH=/home/circleci/app/dd-library-php";
}

$valgrind = use_valgrind();
if ($valgrind) {
$env[] = 'USE_ZEND_ALLOC=0';
$env[] = 'ZEND_DONT_UNLOAD_MODULES=1';
}

$cmd = implode(' ', $env).' ';

$valgrindLogFile = tempnam(sys_get_temp_dir(), 'valgrind_loader_test_');;
if ($valgrind) {
$cmd .= "valgrind -q --log-file=$valgrindLogFile --suppressions=./valgrind.supp --gen-suppressions=all --tool=memcheck --trace-children=no --undef-value-errors=no --vex-iropt-register-updates=allregs-at-mem-access --leak-check=full ";
}

$cmd .= 'php';
if ($noIni) {
$cmd .= ' -n';
Expand All @@ -40,10 +52,22 @@ function runCLI($args, $useLoader = true, $env = [], $noIni = true) {
}

$res = exec($cmd, $output, $result_code);

if ($valgrind) {
$valgrindOutput = file_get_contents($valgrindLogFile);
@unlink($valgrindLogFile);
}

if (!is_string($res) || $result_code !== 0) {
throw new \Exception(sprintf('Error while executing "%s" (exit code: %d): \n\n', $cmd, $result_code, $output));
}

if ($valgrind) {
if (strlen($valgrindOutput)) {
throw new \Exception("Memory leak detected:\n".$valgrindOutput);
}
}

return implode("\n", $output);
}

Expand All @@ -55,6 +79,10 @@ function debug() {
return (bool) (isset($_SERVER['DEBUG']) ? $_SERVER['DEBUG'] : false);
}

function use_valgrind() {
return (bool) (isset($_SERVER['TEST_USE_VALGRIND']) ? $_SERVER['TEST_USE_VALGRIND'] : false);
}

function skip_if_php5() {
if (PHP_MAJOR_VERSION <= 5) {
echo "Skip: test is not compatible with PHP 5\n";
Expand All @@ -69,6 +97,13 @@ function skip_if_not_php5() {
}
}

function skip_if_not_php8() {
if (PHP_MAJOR_VERSION < 8) {
echo "Skip: test requires PHP 8\n";
exit(0);
}
}

function skip_if_opcache_missing() {
$output = runCLI('-dzend_extension=opcache -m');
if (strpos($output, 'Zend OPcache') === false) {
Expand Down
Loading
Loading