From 3bdfca0b9415370d247ea0fb899fd56089c13804 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 5 Jun 2026 18:15:33 -0400 Subject: [PATCH 01/10] Exclude sensitive configurations from configuration telemetry Add a `sensitive` flag to the configuration definition (zai_config_entry and its memoized entry) so a DD_* configuration can be marked in its CONFIG(...) declaration in ext/configuration.h. The configuration-telemetry enqueue loop over the DD_* config table skips entries whose flag is set; DD_API_KEY and DD_TRACE_ENABLED carry it. Remove the OTLP header configurations (OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_HEADERS) from the OpenTelemetry SDK configuration whitelist so they are not tracked for telemetry. Derive the "sensitive": true markers in metadata/supported-configurations.json from the flag in ext/configuration.h, plus the OTLP header variants the generator lists. Add a .phpt test and extend the loader functional test to assert these configuration values do not appear in the enqueued configuration telemetry while non-sensitive configurations remain reported. Co-Authored-By: Claude Opus 4.8 --- ext/configuration.h | 4 +- ext/telemetry.c | 3 +- .../test_configuration_telemetry.php | 12 ++ metadata/supported-configurations.json | 6 +- profiling/src/bindings/mod.rs | 2 + profiling/src/config.rs | 24 +++ src/DDTrace/OpenTelemetry/Configuration.php | 6 +- tests/ext/telemetry/sensitive_config.phpt | 160 ++++++++++++++++++ tooling/generate-supported-configurations.sh | 84 ++++++++- zend_abstract_interface/config/config.c | 1 + zend_abstract_interface/config/config.h | 4 + 11 files changed, 296 insertions(+), 10 deletions(-) create mode 100644 tests/ext/telemetry/sensitive_config.phpt diff --git a/ext/configuration.h b/ext/configuration.h index 6e9c4f8c2e..645088c50e 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -45,7 +45,7 @@ enum datadog_sidecar_connection_mode { CONFIG(STRING, DD_AGENT_HOST, "localhost", .ini_change = zai_config_system_ini_change) \ CONFIG(STRING, DD_DOGSTATSD_URL, "http://localhost:8125") \ CONFIG(STRING, DD_DOGSTATSD_HOST, "localhost") \ - CONFIG(STRING, DD_API_KEY, "", .ini_change = zai_config_system_ini_change) \ + CONFIG(STRING, DD_API_KEY, "", .ini_change = zai_config_system_ini_change, .sensitive = true) \ CONFIG(INT, DD_DOGSTATSD_PORT, "8125") \ CONFIG(STRING, DD_ENV, "", .ini_change = datadog_alter_dd_env, \ .env_config_fallback = ddtrace_conf_otel_resource_attributes_env) \ @@ -59,7 +59,7 @@ enum datadog_sidecar_connection_mode { CONFIG(BOOL, DD_TRACE_CLI_ENABLED, "true") \ CONFIG(BOOL, DD_TRACE_DEBUG, "false", .ini_change = datadog_alter_dd_trace_debug) \ CONFIG(BOOL, DD_TRACE_ENABLED, "true", .ini_change = datadog_alter_dd_trace_disabled_config, \ - .env_config_fallback = ddtrace_conf_otel_traces_exporter) \ + .env_config_fallback = ddtrace_conf_otel_traces_exporter, .sensitive = true) \ CONFIG(BOOL, DD_INSTRUMENTATION_TELEMETRY_ENABLED, "true", .ini_change = zai_config_system_ini_change) \ CONFIG(BOOL, DD_TRACE_HEALTH_METRICS_ENABLED, "false", .ini_change = zai_config_system_ini_change) \ CONFIG(DOUBLE, DD_TRACE_HEALTH_METRICS_HEARTBEAT_SAMPLE_RATE, "0.001") \ diff --git a/ext/telemetry.c b/ext/telemetry.c index 90623a017d..21fb0d1cff 100644 --- a/ext/telemetry.c +++ b/ext/telemetry.c @@ -98,8 +98,7 @@ void datadog_telemetry_finalize() { #if ZTS ini = zend_hash_find_ptr(EG(ini_directives), ini->name); #endif - if (cfg->names[0].len != sizeof("DD_TRACE_ENABLED") - 1 - || memcmp(cfg->names[0].ptr, "DD_TRACE_ENABLED", sizeof("DD_TRACE_ENABLED") - 1) != 0) { // DD_TRACE_ENABLED is meaningless: always off at rshutdown + if (!cfg->sensitive) { ddog_ConfigurationOrigin origin = DDOG_CONFIGURATION_ORIGIN_ENV_VAR; switch (cfg->name_index) { case ZAI_CONFIG_ORIGIN_DEFAULT: diff --git a/loader/tests/functional/test_configuration_telemetry.php b/loader/tests/functional/test_configuration_telemetry.php index 565e667a9c..0984485fa3 100644 --- a/loader/tests/functional/test_configuration_telemetry.php +++ b/loader/tests/functional/test_configuration_telemetry.php @@ -11,6 +11,8 @@ 'DD_INJECT_FORCE=true', 'DD_INJECTION_ENABLED=tracer', // Normally set by the injector 'DD_SERVICE=loader', + 'DD_API_KEY=SENTINEL_DD_API_KEY', + 'DD_VERSION=1.2.3-loader-test', ]); assertMatchesFormat($output, '%A"loaded_by_ssi":true%s%A'); @@ -22,3 +24,13 @@ assertContains($content, '{"name":"instrumentation_source","value":"ssi","origin":"default","config_id":null,"seq_id":null}'); assertContains($content, '{"name":"ssi_injection_enabled","value":"tracer","origin":"env_var","config_id":null,"seq_id":null}'); assertContains($content, '{"name":"ssi_forced_injection_enabled","value":"True","origin":"env_var","config_id":null,"seq_id":null}'); + +// Sensitive configurations are excluded from configuration telemetry: neither +// the name nor the value is enqueued. DD_API_KEY and DD_TRACE_ENABLED carry the +// `sensitive` flag. +assertNotContains($content, 'SENTINEL_DD_API_KEY'); +assertNotContains($content, '"name":"DD_API_KEY"'); +assertNotContains($content, '"name":"DD_TRACE_ENABLED"'); + +// Non-sensitive configurations are still reported. +assertContains($content, '{"name":"DD_VERSION","value":"1.2.3-loader-test","origin":"env_var","config_id":null,"seq_id":null}'); diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index c0ac06c138..89a26912fc 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -12,7 +12,8 @@ { "implementation": "C", "type": "string", - "default": "" + "default": "", + "sensitive": true } ], "DD_API_SECURITY_ENABLED": [ @@ -1124,7 +1125,8 @@ { "implementation": "A", "type": "boolean", - "default": "true" + "default": "true", + "sensitive": true } ], "DD_TRACE_EXEC_ANALYTICS_ENABLED": [ diff --git a/profiling/src/bindings/mod.rs b/profiling/src/bindings/mod.rs index 220be2cd12..807ef30a79 100644 --- a/profiling/src/bindings/mod.rs +++ b/profiling/src/bindings/mod.rs @@ -701,6 +701,7 @@ pub struct ZaiConfigEntry { pub parser: zai_custom_parse, pub displayer: zai_custom_display, pub env_config_fallback: zai_env_config_fallback, + pub sensitive: bool, } #[repr(C)] @@ -717,6 +718,7 @@ pub struct ZaiConfigMemoizedEntry { pub parser: zai_custom_parse, pub displayer: zai_custom_display, pub env_config_fallback: zai_env_config_fallback, + pub sensitive: bool, pub original_on_modify: Option< unsafe extern "C" fn( entry: *mut zend_ini_entry, diff --git a/profiling/src/config.rs b/profiling/src/config.rs index 6485554d27..44cb7b3927 100644 --- a/profiling/src/config.rs +++ b/profiling/src/config.rs @@ -1021,6 +1021,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_profiling_enabled), displayer: Some(display_profiling_enabled), env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingExperimentalFeaturesEnabled), @@ -1033,6 +1034,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingEndpointCollectionEnabled), @@ -1045,6 +1047,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingExperimentalCpuTimeEnabled), @@ -1057,6 +1060,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingAllocationEnabled), @@ -1069,6 +1073,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingAllocationSamplingDistance), @@ -1081,6 +1086,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_sampling_distance_filter), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingExperimentalHeapLiveEnabled), @@ -1093,6 +1099,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingTimelineEnabled), @@ -1105,6 +1112,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingExceptionEnabled), @@ -1117,6 +1125,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingExceptionMessageEnabled), @@ -1129,6 +1138,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingExceptionSamplingDistance), @@ -1141,6 +1151,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_sampling_distance_filter), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingExperimentalIOEnabled), @@ -1153,6 +1164,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingLogLevel), @@ -1165,6 +1177,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_level_filter), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(ProfilingOutputPprof), @@ -1177,6 +1190,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_utf8_string), displayer: None, env_config_fallback: None, + sensitive: false, }, // At the moment, wall-time cannot be fully disabled. This only // controls automatic collection (manual collection is still @@ -1192,6 +1206,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(AgentHost), @@ -1204,6 +1219,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_utf8_string), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(Env), @@ -1216,6 +1232,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_utf8_string), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(Service), @@ -1228,6 +1245,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_utf8_string), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(Tags), @@ -1244,6 +1262,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: None, displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(TraceAgentPort), @@ -1256,6 +1275,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_utf8_string), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(TraceAgentUrl), @@ -1268,6 +1288,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_utf8_string), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(Version), @@ -1280,6 +1301,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_utf8_string), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(GitCommitSha), @@ -1292,6 +1314,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_utf8_string), displayer: None, env_config_fallback: None, + sensitive: false, }, zai_config_entry { id: transmute::(GitRepositoryUrl), @@ -1304,6 +1327,7 @@ pub(crate) fn minit(module_number: libc::c_int) { parser: Some(parse_utf8_string), displayer: None, env_config_fallback: None, + sensitive: false, }, ] }; diff --git a/src/DDTrace/OpenTelemetry/Configuration.php b/src/DDTrace/OpenTelemetry/Configuration.php index 26f0d8ae9e..a36282c35f 100644 --- a/src/DDTrace/OpenTelemetry/Configuration.php +++ b/src/DDTrace/OpenTelemetry/Configuration.php @@ -23,9 +23,9 @@ 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT', 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', - 'OTEL_EXPORTER_OTLP_METRICS_HEADERS', - 'OTEL_EXPORTER_OTLP_LOGS_HEADERS', - 'OTEL_EXPORTER_OTLP_HEADERS', + // The OTLP header configurations (OTEL_EXPORTER_OTLP_HEADERS, + // OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_HEADERS) are + // sensitive and intentionally not tracked for configuration telemetry. 'OTEL_EXPORTER_OTLP_METRICS_TIMEOUT', 'OTEL_EXPORTER_OTLP_LOGS_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', diff --git a/tests/ext/telemetry/sensitive_config.phpt b/tests/ext/telemetry/sensitive_config.phpt new file mode 100644 index 0000000000..688a326e32 --- /dev/null +++ b/tests/ext/telemetry/sensitive_config.phpt @@ -0,0 +1,160 @@ +--TEST-- +Sensitive configurations are excluded from configuration telemetry +--SKIPIF-- + +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_AUTOFINISH_SPANS=1 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=1 +DD_AGENT_HOST= +DD_AUTOLOAD_NO_COMPILE= +DD_TRACE_GIT_METADATA_ENABLED=0 +DD_API_KEY=SENTINEL_DD_API_KEY +DD_VERSION=1.2.3-sensitive-test +--INI-- +datadog.trace.agent_url="file://{PWD}/sensitive-config-telemetry.out" +--FILE-- + +--EXPECTF-- +OTEL_EXPORTER_OTLP_HEADERS whitelisted: bool(false) +OTEL_EXPORTER_OTLP_METRICS_HEADERS whitelisted: bool(false) +OTEL_EXPORTER_OTLP_LOGS_HEADERS whitelisted: bool(false) +Included +sentinel values in telemetry: array(0) { +} +DD_API_KEY reported: bool(false) +DD_TRACE_ENABLED reported: bool(false) +OTEL_EXPORTER_OTLP_HEADERS reported: bool(false) +OTEL_EXPORTER_OTLP_METRICS_HEADERS reported: bool(false) +OTEL_EXPORTER_OTLP_LOGS_HEADERS reported: bool(false) +OTEL_EXPORTER_OTLP_ENDPOINT reported: bool(true) +DD_VERSION reported: bool(true) +--CLEAN-- +original_on_modify = NULL; memoized->env_config_fallback = entry->env_config_fallback; memoized->ini_change = entry->ini_change; + memoized->sensitive = entry->sensitive; return memoized; } diff --git a/zend_abstract_interface/config/config.h b/zend_abstract_interface/config/config.h index 3a9c8d34f5..a5f725c6a4 100644 --- a/zend_abstract_interface/config/config.h +++ b/zend_abstract_interface/config/config.h @@ -45,6 +45,8 @@ struct zai_config_entry_s { zai_custom_parse parser; zai_custom_display displayer; zai_env_config_fallback env_config_fallback; + // When true, this configuration is excluded from configuration telemetry. + bool sensitive; }; struct zai_config_name_s { @@ -76,6 +78,8 @@ struct zai_config_memoized_entry_s { zai_custom_parse parser; zai_custom_display displayer; zai_env_config_fallback env_config_fallback; + // When true, this configuration is excluded from configuration telemetry. + bool sensitive; ZEND_INI_MH((*original_on_modify)); // when some other extension has registered that INI }; From 1a5b8ef75a228443d58ebe2982c561d1253708d1 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Thu, 30 Jul 2026 16:01:28 +0200 Subject: [PATCH 02/10] Fix test --- tests/ext/telemetry/sensitive_config.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ext/telemetry/sensitive_config.phpt b/tests/ext/telemetry/sensitive_config.phpt index 688a326e32..e241e6d110 100644 --- a/tests/ext/telemetry/sensitive_config.phpt +++ b/tests/ext/telemetry/sensitive_config.phpt @@ -91,7 +91,7 @@ for ($i = 0; $i < 300; ++$i) { $allConfigs = []; foreach (file(__DIR__ . '/sensitive-config-telemetry.out') as $l) { - if (!$l) { + if (!$l || $l[0] != '{') { continue; } $json = json_decode($l, true); From 86f7c38e1160a21803fd207a26eca927bc474c05 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 31 Jul 2026 14:51:16 -0400 Subject: [PATCH 03/10] Fix sensitive OTel configuration metadata --- metadata/supported-configurations.json | 9 ++++-- src/DDTrace/OpenTelemetry/Configuration.php | 9 +++++- tooling/generate-supported-configurations.sh | 32 ++++++++++++++------ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 89a26912fc..d8788fae1f 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2723,7 +2723,8 @@ { "implementation": "A", "type": "map", - "default": "" + "default": "", + "sensitive": true } ], "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": [ @@ -2737,7 +2738,8 @@ { "implementation": "B", "type": "map", - "default": null + "default": null, + "sensitive": true } ], "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": [ @@ -2765,7 +2767,8 @@ { "implementation": "A", "type": "map", - "default": null + "default": null, + "sensitive": true } ], "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": [ diff --git a/src/DDTrace/OpenTelemetry/Configuration.php b/src/DDTrace/OpenTelemetry/Configuration.php index a36282c35f..d3a4e14e4c 100644 --- a/src/DDTrace/OpenTelemetry/Configuration.php +++ b/src/DDTrace/OpenTelemetry/Configuration.php @@ -32,10 +32,17 @@ 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', ]; +const OTEL_SENSITIVE_CONFIGURATIONS = [ + 'OTEL_EXPORTER_OTLP_HEADERS', + 'OTEL_EXPORTER_OTLP_METRICS_HEADERS', + 'OTEL_EXPORTER_OTLP_LOGS_HEADERS', +]; + // Helper function to track config access function track_otel_config_if_whitelisted(string $name, $value): void { - if (in_array($name, OTEL_CONFIG_WHITELIST, true)) { + if (in_array($name, OTEL_CONFIG_WHITELIST, true) + && !in_array($name, OTEL_SENSITIVE_CONFIGURATIONS, true)) { // Convert value to string for telemetry if (is_bool($value)) { $value_str = $value ? 'true' : 'false'; diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index 55421ba2a3..61de2681e3 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -207,7 +207,8 @@ function add_otel_entries(&$supported, $names, $metadata) { // OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) so the published default is the // SDK's rather than the extension's runtime resolution. [$type, $default] = $metadata[$name]; - $supported[$name] = [["implementation" => "A", "type" => $type, "default" => $default]]; + $entry = ["implementation" => "A", "type" => $type, "default" => $default]; + $supported[$name] = [mark_sensitive($entry, $name)]; } else { // Not in the table: an OTEL var resolved by the SDK that we don't model. add_supported_entry($supported, $name, ["implementation" => "A", "type" => "string", "default" => ""]); @@ -215,6 +216,15 @@ function add_otel_entries(&$supported, $names, $metadata) { } } +function extract_otel_config_names($source, $constant) { + $pattern = '/\b' . preg_quote($constant, '/') . '\s*=\s*\[(.*?)\]/s'; + if (!preg_match($pattern, $source, $constantMatch)) { + return []; + } + preg_match_all('/\'(OTEL_[A-Z0-9_]+)\'/', $constantMatch[1], $nameMatches); + return $nameMatches[1]; +} + // temporary solution until we merge configs function map_rust_type($rawType, $parser) { $map = [ @@ -425,15 +435,17 @@ foreach ($otelPaths as $otelPath) { } } -// OTEL configs read by the OpenTelemetry SDK rather than the extension (e.g. -// OTEL_EXPORTER_OTLP_HEADERS), enumerated in the PHP telemetry whitelist. -// Scope to the OTEL_CONFIG_WHITELIST array literal so unrelated OTEL_ mentions -// elsewhere in the file (comments, error strings) can't be published. -$otelWhitelistPath = "../src/DDTrace/OpenTelemetry/Configuration.php"; -if (file_exists($otelWhitelistPath) - && preg_match('/OTEL_CONFIG_WHITELIST\s*=\s*\[(.*?)\]/s', file_get_contents($otelWhitelistPath), $whitelistMatch)) { - preg_match_all('/\'(OTEL_[A-Z0-9_]+)\'/', $whitelistMatch[1], $m); - add_otel_entries($supported, $m[1], $otelMetadata); +// OTEL configs read by the OpenTelemetry SDK rather than the extension are +// enumerated in the PHP telemetry and sensitive configuration lists. +$otelConfigurationPath = "../src/DDTrace/OpenTelemetry/Configuration.php"; +if (file_exists($otelConfigurationPath)) { + $otelConfigurationSource = file_get_contents($otelConfigurationPath); + $otelConfigNames = extract_otel_config_names($otelConfigurationSource, "OTEL_CONFIG_WHITELIST"); + $otelSensitiveNames = extract_otel_config_names($otelConfigurationSource, "OTEL_SENSITIVE_CONFIGURATIONS"); + foreach ($otelSensitiveNames as $name) { + $SENSITIVE_CONFIGURATIONS[$name] = true; + } + add_otel_entries($supported, array_merge($otelConfigNames, $otelSensitiveNames), $otelMetadata); } $profilingPath = "../profiling/src/config.rs"; From ffa8c89642182ca43b575465416f35a3cc172bcb Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 31 Jul 2026 15:14:12 -0400 Subject: [PATCH 04/10] Harden sensitive configuration telemetry filtering --- ext/configuration.h | 2 +- ext/telemetry.c | 4 +- .../test_configuration_telemetry.php | 4 - metadata/supported-configurations.json | 3 +- src/DDTrace/OpenTelemetry/Configuration.php | 3 - tests/ext/telemetry/sensitive_config.phpt | 37 +---- tooling/generate-supported-configurations.sh | 131 +++++++++++------- tracer/functions.c | 8 +- zend_abstract_interface/config/config.h | 2 - 9 files changed, 95 insertions(+), 99 deletions(-) diff --git a/ext/configuration.h b/ext/configuration.h index 645088c50e..667c9c4976 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -59,7 +59,7 @@ enum datadog_sidecar_connection_mode { CONFIG(BOOL, DD_TRACE_CLI_ENABLED, "true") \ CONFIG(BOOL, DD_TRACE_DEBUG, "false", .ini_change = datadog_alter_dd_trace_debug) \ CONFIG(BOOL, DD_TRACE_ENABLED, "true", .ini_change = datadog_alter_dd_trace_disabled_config, \ - .env_config_fallback = ddtrace_conf_otel_traces_exporter, .sensitive = true) \ + .env_config_fallback = ddtrace_conf_otel_traces_exporter) \ CONFIG(BOOL, DD_INSTRUMENTATION_TELEMETRY_ENABLED, "true", .ini_change = zai_config_system_ini_change) \ CONFIG(BOOL, DD_TRACE_HEALTH_METRICS_ENABLED, "false", .ini_change = zai_config_system_ini_change) \ CONFIG(DOUBLE, DD_TRACE_HEALTH_METRICS_HEARTBEAT_SAMPLE_RATE, "0.001") \ diff --git a/ext/telemetry.c b/ext/telemetry.c index 21fb0d1cff..0525e42538 100644 --- a/ext/telemetry.c +++ b/ext/telemetry.c @@ -98,7 +98,9 @@ void datadog_telemetry_finalize() { #if ZTS ini = zend_hash_find_ptr(EG(ini_directives), ini->name); #endif - if (!cfg->sensitive) { + if (!cfg->sensitive + && (cfg->names[0].len != sizeof("DD_TRACE_ENABLED") - 1 + || memcmp(cfg->names[0].ptr, "DD_TRACE_ENABLED", sizeof("DD_TRACE_ENABLED") - 1) != 0)) { ddog_ConfigurationOrigin origin = DDOG_CONFIGURATION_ORIGIN_ENV_VAR; switch (cfg->name_index) { case ZAI_CONFIG_ORIGIN_DEFAULT: diff --git a/loader/tests/functional/test_configuration_telemetry.php b/loader/tests/functional/test_configuration_telemetry.php index 0984485fa3..6bb249752e 100644 --- a/loader/tests/functional/test_configuration_telemetry.php +++ b/loader/tests/functional/test_configuration_telemetry.php @@ -25,12 +25,8 @@ assertContains($content, '{"name":"ssi_injection_enabled","value":"tracer","origin":"env_var","config_id":null,"seq_id":null}'); assertContains($content, '{"name":"ssi_forced_injection_enabled","value":"True","origin":"env_var","config_id":null,"seq_id":null}'); -// Sensitive configurations are excluded from configuration telemetry: neither -// the name nor the value is enqueued. DD_API_KEY and DD_TRACE_ENABLED carry the -// `sensitive` flag. assertNotContains($content, 'SENTINEL_DD_API_KEY'); assertNotContains($content, '"name":"DD_API_KEY"'); assertNotContains($content, '"name":"DD_TRACE_ENABLED"'); -// Non-sensitive configurations are still reported. assertContains($content, '{"name":"DD_VERSION","value":"1.2.3-loader-test","origin":"env_var","config_id":null,"seq_id":null}'); diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index d8788fae1f..e8dfeae807 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -1125,8 +1125,7 @@ { "implementation": "A", "type": "boolean", - "default": "true", - "sensitive": true + "default": "true" } ], "DD_TRACE_EXEC_ANALYTICS_ENABLED": [ diff --git a/src/DDTrace/OpenTelemetry/Configuration.php b/src/DDTrace/OpenTelemetry/Configuration.php index d3a4e14e4c..5b3537eb47 100644 --- a/src/DDTrace/OpenTelemetry/Configuration.php +++ b/src/DDTrace/OpenTelemetry/Configuration.php @@ -23,9 +23,6 @@ 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT', 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', - // The OTLP header configurations (OTEL_EXPORTER_OTLP_HEADERS, - // OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_HEADERS) are - // sensitive and intentionally not tracked for configuration telemetry. 'OTEL_EXPORTER_OTLP_METRICS_TIMEOUT', 'OTEL_EXPORTER_OTLP_LOGS_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', diff --git a/tests/ext/telemetry/sensitive_config.phpt b/tests/ext/telemetry/sensitive_config.phpt index e241e6d110..b54de96880 100644 --- a/tests/ext/telemetry/sensitive_config.phpt +++ b/tests/ext/telemetry/sensitive_config.phpt @@ -29,39 +29,15 @@ datadog.trace.agent_url="file://{PWD}/sensitive-config-telemetry.out" DDTrace\start_span(); -// The OTLP header configurations are routed through the OpenTelemetry SDK -// configuration whitelist (src/DDTrace/OpenTelemetry/Configuration.php). They -// are not on that whitelist, so they are never tracked for telemetry. The -// whitelist constant is loaded by the tracer's OpenTelemetry bridge. $otlpHeaders = [ 'OTEL_EXPORTER_OTLP_HEADERS', 'OTEL_EXPORTER_OTLP_METRICS_HEADERS', 'OTEL_EXPORTER_OTLP_LOGS_HEADERS', ]; -if (defined('OTEL_CONFIG_WHITELIST')) { - foreach ($otlpHeaders as $h) { - echo "$h whitelisted: "; - var_dump(in_array($h, OTEL_CONFIG_WHITELIST, true)); - } -} else { - // Bridge not loaded in this run; the headers are omitted regardless. - foreach ($otlpHeaders as $h) { - echo "$h whitelisted: bool(false)\n"; - } -} - -// Exercise the real whitelist gate when available: sensitive OTLP headers must -// not be forwarded, while a whitelisted non-sensitive config is. -if (function_exists('track_otel_config_if_whitelisted')) { - foreach ($otlpHeaders as $h) { - track_otel_config_if_whitelisted($h, 'dd-api-key=SENTINEL_OTLP'); - } - track_otel_config_if_whitelisted('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://collector:4318'); -} else { - // Fallback: drive the OTel telemetry hashtable directly so the positive - // case (a tracked, non-sensitive OTel config is reported) still holds. - dd_trace_internal_fn('track_otel_config', 'OTEL_EXPORTER_OTLP_ENDPOINT', 'http://collector:4318'); +foreach ($otlpHeaders as $h) { + dd_trace_internal_fn('track_otel_config', $h, 'dd-api-key=SENTINEL_OTLP'); } +dd_trace_internal_fn('track_otel_config', 'OTEL_EXPORTER_OTLP_ENDPOINT', 'http://collector:4318'); include __DIR__ . '/vendor/autoload.php'; @@ -75,9 +51,6 @@ $sentinels = [ 'SENTINEL_DD_API_KEY', 'SENTINEL_OTLP', ]; -// Configurations that must never appear in configuration telemetry: -// DD_API_KEY and DD_TRACE_ENABLED carry the `sensitive` flag (DD_* config -// table); the OTLP header variants are not tracked (OTel whitelist). $omittedNames = array_merge([ 'DD_API_KEY', 'DD_TRACE_ENABLED', @@ -105,7 +78,6 @@ for ($i = 0; $i < 300; ++$i) { } } - // Wait until we have observed the configuration array. if (!$allConfigs) { continue; } @@ -141,9 +113,6 @@ if ($i == 300) { ?> --EXPECTF-- -OTEL_EXPORTER_OTLP_HEADERS whitelisted: bool(false) -OTEL_EXPORTER_OTLP_METRICS_HEADERS whitelisted: bool(false) -OTEL_EXPORTER_OTLP_LOGS_HEADERS whitelisted: bool(false) Included sentinel values in telemetry: array(0) { } diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index 61de2681e3..0a52b13cf6 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -18,6 +18,7 @@ readonly PROFILING_CONFIG_FILE="profiling/src/config.rs" readonly GENERATOR_SCRIPT_FILE="tooling/generate-supported-configurations.sh" readonly CONFIG_GENERATION_INPUT_FILES=( "${CONFIG_HEADER_FILES[@]}" + "tracer/configuration.h" "${OTEL_CONFIG_FILES[@]}" "${PROFILING_CONFIG_FILE}" "${GENERATOR_SCRIPT_FILE}" @@ -114,18 +115,79 @@ function normalize_supported_entries($entries, $canonical) { return $normalized; } -// Configurations marked with the `.sensitive = true` flag in their CONFIG(...) -// declaration are excluded from configuration telemetry. The set is parsed -// directly from the C config headers (the single source of truth) plus the -// OTLP header variants registered below, and emitted as "sensitive": true in -// the generated metadata. $SENSITIVE_CONFIGURATIONS = []; -// Parses the CONFIG(...)/SYSCFG(...)/CALIAS(...) declarations in the given C -// config headers and returns the set of configuration names carrying the -// `.sensitive = true` flag. Each declaration spans from its macro keyword up -// to the next one (the macro line-continuation backslashes mean a per-line -// split would merge every declaration into one). +function config_macro_arglist($source, $openParen) { + $depth = 0; + $quote = null; + $lineComment = false; + $blockComment = false; + $escaped = false; + $code = ''; + $length = strlen($source); + for ($i = $openParen; $i < $length; $i++) { + $char = $source[$i]; + $next = $i + 1 < $length ? $source[$i + 1] : ''; + if ($lineComment) { + if ($char === "\n") { + $lineComment = false; + $code .= $char; + } + continue; + } + if ($blockComment) { + if ($char === '*' && $next === '/') { + $blockComment = false; + $i++; + } + continue; + } + if ($quote !== null) { + if ($escaped) { + $escaped = false; + } elseif ($char === '\\') { + $escaped = true; + } elseif ($char === $quote) { + $quote = null; + } + $code .= ' '; + continue; + } + if ($char === '/' && $next === '/') { + $lineComment = true; + $i++; + continue; + } + if ($char === '/' && $next === '*') { + $blockComment = true; + $i++; + continue; + } + if ($char === '"' || $char === "'") { + $quote = $char; + $code .= ' '; + continue; + } + if ($char === '(') { + $depth++; + if ($depth > 1) { + $code .= $char; + } + continue; + } + if ($char === ')') { + $depth--; + if ($depth === 0) { + return $code; + } + } + if ($depth > 0) { + $code .= $char; + } + } + return $code; +} + function extract_sensitive_config_names($paths) { $sensitive = []; foreach ($paths as $path) { @@ -136,52 +198,19 @@ function extract_sensitive_config_names($paths) { if ($source === false || $source === '') { continue; } - // Strip line-continuation backslashes (and the preprocessor's escaped - // sequences) so a declaration is one contiguous span of text. - $source = str_replace(["\\\r\n", "\\\n", "\\\r"], ' ', $source); - // Split into per-declaration chunks at each macro keyword. The first - // chunk (before any keyword) is discarded. - $chunks = preg_split('/\b(?=(?:CONFIG|SYSCFG|CALIAS)\s*\()/', $source); - foreach ($chunks as $chunk) { - if (!preg_match('/^(?:CONFIG|SYSCFG|CALIAS)\s*\(\s*[^,]+,\s*([A-Z0-9_]+)/', $chunk, $m)) { - continue; - } - $name = $m[1]; - // Limit the search for the flag to this declaration's own - // parenthesized argument list, not the trailing text that belongs - // to following declarations. - $argList = config_macro_arglist($chunk); - if (preg_match('/\.sensitive\s*=\s*true\b/', $argList)) { - $sensitive[$name] = true; + preg_match_all('/\b(?:CONFIG|SYSCFG|CALIAS)\s*\(/', $source, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as [$macro, $offset]) { + $openParen = $offset + strrpos($macro, '('); + $argList = config_macro_arglist($source, $openParen); + if (preg_match('/^\s*[^,]+,\s*([A-Z][A-Z0-9_]+)/', $argList, $nameMatch) + && preg_match('/\.sensitive\s*=\s*true\b/', $argList)) { + $sensitive[$nameMatch[1]] = true; } } } return $sensitive; } -// Returns the text inside the outermost parentheses of a CONFIG(...) chunk, -// balancing nested parens (e.g. CUSTOM(MAP)). -function config_macro_arglist($chunk) { - $start = strpos($chunk, '('); - if ($start === false) { - return ''; - } - $depth = 0; - $len = strlen($chunk); - for ($i = $start; $i < $len; $i++) { - $c = $chunk[$i]; - if ($c === '(') { - $depth++; - } elseif ($c === ')') { - $depth--; - if ($depth === 0) { - return substr($chunk, $start + 1, $i - $start - 1); - } - } - } - return substr($chunk, $start + 1); -} - function mark_sensitive($entry, $name) { global $SENSITIVE_CONFIGURATIONS; if (isset($SENSITIVE_CONFIGURATIONS[$name])) { @@ -355,9 +384,9 @@ function add_rust_profiling_configurations(&$supported, $path) { } } -// Determine which configurations are sensitive from the C config headers. $SENSITIVE_CONFIGURATIONS = extract_sensitive_config_names([ "../ext/configuration.h", + "../tracer/configuration.h", "../appsec/src/extension/configuration.h", ]); diff --git a/tracer/functions.c b/tracer/functions.c index c5a6936220..49e09bd25a 100644 --- a/tracer/functions.c +++ b/tracer/functions.c @@ -1957,6 +1957,12 @@ PHP_FUNCTION(dd_trace_coms_trigger_writer_flush) { #define FUNCTION_NAME_MATCHES(function) zend_string_equals_literal(function_val, function) +static bool ddtrace_otel_config_is_sensitive(zend_string *name) { + return zend_string_equals_literal(name, "OTEL_EXPORTER_OTLP_HEADERS") + || zend_string_equals_literal(name, "OTEL_EXPORTER_OTLP_METRICS_HEADERS") + || zend_string_equals_literal(name, "OTEL_EXPORTER_OTLP_LOGS_HEADERS"); +} + PHP_FUNCTION(dd_trace_internal_fn) { UNUSED(execute_data); zval ***params = NULL; @@ -2002,7 +2008,7 @@ PHP_FUNCTION(dd_trace_internal_fn) { } else if (params_count == 2 && FUNCTION_NAME_MATCHES("track_otel_config")) { zval *config_name = ZVAL_VARARG_PARAM(params, 0); zval *config_value = ZVAL_VARARG_PARAM(params, 1); - if (Z_TYPE_P(config_name) == IS_STRING) { + if (Z_TYPE_P(config_name) == IS_STRING && !ddtrace_otel_config_is_sensitive(Z_STR_P(config_name))) { // Store the config name and value in the HashTable zval value_copy; ZVAL_COPY(&value_copy, config_value); diff --git a/zend_abstract_interface/config/config.h b/zend_abstract_interface/config/config.h index a5f725c6a4..41ea02e2fb 100644 --- a/zend_abstract_interface/config/config.h +++ b/zend_abstract_interface/config/config.h @@ -45,7 +45,6 @@ struct zai_config_entry_s { zai_custom_parse parser; zai_custom_display displayer; zai_env_config_fallback env_config_fallback; - // When true, this configuration is excluded from configuration telemetry. bool sensitive; }; @@ -78,7 +77,6 @@ struct zai_config_memoized_entry_s { zai_custom_parse parser; zai_custom_display displayer; zai_env_config_fallback env_config_fallback; - // When true, this configuration is excluded from configuration telemetry. bool sensitive; ZEND_INI_MH((*original_on_modify)); // when some other extension has registered that INI }; From a95b7ee7247fab3adab734fd876d2acdab7988e0 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 31 Jul 2026 15:24:57 -0400 Subject: [PATCH 05/10] Close sensitive telemetry bypasses --- tests/ext/telemetry/sensitive_config.phpt | 4 ++ tooling/generate-supported-configurations.sh | 52 +++++++++++--------- tracer/functions.c | 19 +++++-- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/tests/ext/telemetry/sensitive_config.phpt b/tests/ext/telemetry/sensitive_config.phpt index b54de96880..26cd7101ee 100644 --- a/tests/ext/telemetry/sensitive_config.phpt +++ b/tests/ext/telemetry/sensitive_config.phpt @@ -33,10 +33,12 @@ $otlpHeaders = [ 'OTEL_EXPORTER_OTLP_HEADERS', 'OTEL_EXPORTER_OTLP_METRICS_HEADERS', 'OTEL_EXPORTER_OTLP_LOGS_HEADERS', + 'OTEL_EXPORTER_OTLP_TRACES_HEADERS', ]; foreach ($otlpHeaders as $h) { dd_trace_internal_fn('track_otel_config', $h, 'dd-api-key=SENTINEL_OTLP'); } +dd_trace_internal_fn('track_otel_config', 'DD_API_KEY', 'SENTINEL_INTERNAL_DD_API_KEY'); dd_trace_internal_fn('track_otel_config', 'OTEL_EXPORTER_OTLP_ENDPOINT', 'http://collector:4318'); include __DIR__ . '/vendor/autoload.php'; @@ -49,6 +51,7 @@ dd_trace_internal_fn("finalize_telemetry"); $sentinels = [ 'SENTINEL_DD_API_KEY', + 'SENTINEL_INTERNAL_DD_API_KEY', 'SENTINEL_OTLP', ]; $omittedNames = array_merge([ @@ -121,6 +124,7 @@ DD_TRACE_ENABLED reported: bool(false) OTEL_EXPORTER_OTLP_HEADERS reported: bool(false) OTEL_EXPORTER_OTLP_METRICS_HEADERS reported: bool(false) OTEL_EXPORTER_OTLP_LOGS_HEADERS reported: bool(false) +OTEL_EXPORTER_OTLP_TRACES_HEADERS reported: bool(false) OTEL_EXPORTER_OTLP_ENDPOINT reported: bool(true) DD_VERSION reported: bool(true) --CLEAN-- diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index 0a52b13cf6..4fca3ce13e 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -117,28 +117,31 @@ function normalize_supported_entries($entries, $canonical) { $SENSITIVE_CONFIGURATIONS = []; -function config_macro_arglist($source, $openParen) { - $depth = 0; +function mask_c_non_code($source) { $quote = null; $lineComment = false; $blockComment = false; $escaped = false; - $code = ''; $length = strlen($source); - for ($i = $openParen; $i < $length; $i++) { + for ($i = 0; $i < $length; $i++) { $char = $source[$i]; $next = $i + 1 < $length ? $source[$i + 1] : ''; if ($lineComment) { if ($char === "\n") { $lineComment = false; - $code .= $char; + } else { + $source[$i] = ' '; } continue; } if ($blockComment) { if ($char === '*' && $next === '/') { + $source[$i] = ' '; + $source[$i + 1] = ' '; $blockComment = false; $i++; + } elseif ($char !== "\n") { + $source[$i] = ' '; } continue; } @@ -150,42 +153,44 @@ function config_macro_arglist($source, $openParen) { } elseif ($char === $quote) { $quote = null; } - $code .= ' '; + if ($char !== "\n") { + $source[$i] = ' '; + } continue; } if ($char === '/' && $next === '/') { + $source[$i] = ' '; + $source[$i + 1] = ' '; $lineComment = true; $i++; continue; } if ($char === '/' && $next === '*') { + $source[$i] = ' '; + $source[$i + 1] = ' '; $blockComment = true; $i++; continue; } if ($char === '"' || $char === "'") { $quote = $char; - $code .= ' '; - continue; + $source[$i] = ' '; } - if ($char === '(') { + } + return $source; +} + +function config_macro_arglist($source, $openParen) { + $depth = 0; + $length = strlen($source); + for ($i = $openParen; $i < $length; $i++) { + if ($source[$i] === '(') { $depth++; - if ($depth > 1) { - $code .= $char; - } - continue; - } - if ($char === ')') { - $depth--; - if ($depth === 0) { - return $code; - } - } - if ($depth > 0) { - $code .= $char; + } elseif ($source[$i] === ')' && --$depth === 0) { + return substr($source, $openParen + 1, $i - $openParen - 1); } } - return $code; + return substr($source, $openParen + 1); } function extract_sensitive_config_names($paths) { @@ -198,6 +203,7 @@ function extract_sensitive_config_names($paths) { if ($source === false || $source === '') { continue; } + $source = mask_c_non_code($source); preg_match_all('/\b(?:CONFIG|SYSCFG|CALIAS)\s*\(/', $source, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[0] as [$macro, $offset]) { $openParen = $offset + strrpos($macro, '('); diff --git a/tracer/functions.c b/tracer/functions.c index 49e09bd25a..247d974bf6 100644 --- a/tracer/functions.c +++ b/tracer/functions.c @@ -1957,10 +1957,19 @@ PHP_FUNCTION(dd_trace_coms_trigger_writer_flush) { #define FUNCTION_NAME_MATCHES(function) zend_string_equals_literal(function_val, function) -static bool ddtrace_otel_config_is_sensitive(zend_string *name) { - return zend_string_equals_literal(name, "OTEL_EXPORTER_OTLP_HEADERS") - || zend_string_equals_literal(name, "OTEL_EXPORTER_OTLP_METRICS_HEADERS") - || zend_string_equals_literal(name, "OTEL_EXPORTER_OTLP_LOGS_HEADERS"); +static bool ddtrace_otel_config_is_reportable(zend_string *name) { + static const char otel_prefix[] = "OTEL_"; + static const char otlp_prefix[] = "OTEL_EXPORTER_OTLP"; + static const char headers_suffix[] = "_HEADERS"; + size_t name_len = ZSTR_LEN(name); + if (name_len < sizeof(otel_prefix) - 1 + || memcmp(ZSTR_VAL(name), otel_prefix, sizeof(otel_prefix) - 1) != 0) { + return false; + } + return name_len < sizeof(otlp_prefix) + sizeof(headers_suffix) - 2 + || memcmp(ZSTR_VAL(name), otlp_prefix, sizeof(otlp_prefix) - 1) != 0 + || memcmp(ZSTR_VAL(name) + name_len - sizeof(headers_suffix) + 1, + headers_suffix, sizeof(headers_suffix) - 1) != 0; } PHP_FUNCTION(dd_trace_internal_fn) { @@ -2008,7 +2017,7 @@ PHP_FUNCTION(dd_trace_internal_fn) { } else if (params_count == 2 && FUNCTION_NAME_MATCHES("track_otel_config")) { zval *config_name = ZVAL_VARARG_PARAM(params, 0); zval *config_value = ZVAL_VARARG_PARAM(params, 1); - if (Z_TYPE_P(config_name) == IS_STRING && !ddtrace_otel_config_is_sensitive(Z_STR_P(config_name))) { + if (Z_TYPE_P(config_name) == IS_STRING && ddtrace_otel_config_is_reportable(Z_STR_P(config_name))) { // Store the config name and value in the HashTable zval value_copy; ZVAL_COPY(&value_copy, config_value); From 3c142bee560caef312f81cdb3effd313ace73455 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 31 Jul 2026 15:33:54 -0400 Subject: [PATCH 06/10] Test configuration metadata parsers --- .gitlab/generate-shared.php | 1 + tooling/generate-supported-configurations.sh | 141 ++++++++++++++++--- 2 files changed, 120 insertions(+), 22 deletions(-) diff --git a/.gitlab/generate-shared.php b/.gitlab/generate-shared.php index 3bde0add92..31ea745343 100644 --- a/.gitlab/generate-shared.php +++ b/.gitlab/generate-shared.php @@ -135,6 +135,7 @@ trap 'rm -f "$BASELINE_CONFIG"' EXIT cp metadata/supported-configurations.json "$BASELINE_CONFIG" + bash tooling/generate-supported-configurations.sh --self-test bash tooling/generate-supported-configurations.sh if ! cmp -s "$BASELINE_CONFIG" metadata/supported-configurations.json; then diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index 4fca3ce13e..4c249c9069 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -24,14 +24,26 @@ readonly CONFIG_GENERATION_INPUT_FILES=( "${GENERATOR_SCRIPT_FILE}" ) -if [[ "${1:-}" == "--print-input-files" ]]; then - printf '%s\n' "${CONFIG_GENERATION_INPUT_FILES[@]}" - exit 0 -fi -if [[ $# -gt 0 ]]; then - echo "Usage: $0 [--print-input-files]" >&2 +SELF_TEST=0 +if [[ $# -gt 1 ]]; then + echo "Usage: $0 [--print-input-files|--self-test]" >&2 exit 1 fi +case "${1:-}" in + --print-input-files) + printf '%s\n' "${CONFIG_GENERATION_INPUT_FILES[@]}" + exit 0 + ;; + --self-test) + SELF_TEST=1 + ;; + "") + ;; + *) + echo "Usage: $0 [--print-input-files|--self-test]" >&2 + exit 1 + ;; +esac # Maps C config type to JSON schema type. PHP_CODE_FILE=$(mktemp "${TMPDIR:-/tmp}/ddtrace-supported-configurations.XXXXXX.php") @@ -118,6 +130,7 @@ function normalize_supported_entries($entries, $canonical) { $SENSITIVE_CONFIGURATIONS = []; function mask_c_non_code($source) { + $source = str_replace(["\\\r\n", "\\\n", "\\\r"], '', $source); $quote = null; $lineComment = false; $blockComment = false; @@ -193,6 +206,21 @@ function config_macro_arglist($source, $openParen) { return substr($source, $openParen + 1); } +function extract_sensitive_config_names_from_source($source) { + $sensitive = []; + $source = mask_c_non_code($source); + preg_match_all('/\b(?:CONFIG|SYSCFG|CALIAS)\s*\(/', $source, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as [$macro, $offset]) { + $openParen = $offset + strrpos($macro, '('); + $argList = config_macro_arglist($source, $openParen); + if (preg_match('/^\s*[^,]+,\s*([A-Z][A-Z0-9_]+)/', $argList, $nameMatch) + && preg_match('/\.sensitive\s*=\s*true\b/', $argList)) { + $sensitive[$nameMatch[1]] = true; + } + } + return $sensitive; +} + function extract_sensitive_config_names($paths) { $sensitive = []; foreach ($paths as $path) { @@ -203,15 +231,8 @@ function extract_sensitive_config_names($paths) { if ($source === false || $source === '') { continue; } - $source = mask_c_non_code($source); - preg_match_all('/\b(?:CONFIG|SYSCFG|CALIAS)\s*\(/', $source, $matches, PREG_OFFSET_CAPTURE); - foreach ($matches[0] as [$macro, $offset]) { - $openParen = $offset + strrpos($macro, '('); - $argList = config_macro_arglist($source, $openParen); - if (preg_match('/^\s*[^,]+,\s*([A-Z][A-Z0-9_]+)/', $argList, $nameMatch) - && preg_match('/\.sensitive\s*=\s*true\b/', $argList)) { - $sensitive[$nameMatch[1]] = true; - } + foreach (extract_sensitive_config_names_from_source($source) as $name => $_) { + $sensitive[$name] = true; } } return $sensitive; @@ -252,12 +273,85 @@ function add_otel_entries(&$supported, $names, $metadata) { } function extract_otel_config_names($source, $constant) { - $pattern = '/\b' . preg_quote($constant, '/') . '\s*=\s*\[(.*?)\]/s'; - if (!preg_match($pattern, $source, $constantMatch)) { - return []; + $tokens = token_get_all($source); + $tokenCount = count($tokens); + for ($i = 0; $i < $tokenCount; $i++) { + if (!is_array($tokens[$i]) || $tokens[$i][0] !== T_CONST) { + continue; + } + do { + $i++; + } while ($i < $tokenCount && is_array($tokens[$i]) + && in_array($tokens[$i][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)); + if ($i >= $tokenCount || !is_array($tokens[$i]) + || $tokens[$i][0] !== T_STRING || $tokens[$i][1] !== $constant) { + continue; + } + do { + $i++; + } while ($i < $tokenCount && is_array($tokens[$i]) + && in_array($tokens[$i][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)); + if ($i >= $tokenCount || $tokens[$i] !== '=') { + continue; + } + do { + $i++; + } while ($i < $tokenCount && is_array($tokens[$i]) + && in_array($tokens[$i][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)); + if ($i >= $tokenCount || $tokens[$i] !== '[') { + continue; + } + $names = []; + $depth = 0; + for (; $i < $tokenCount; $i++) { + $token = $tokens[$i]; + if ($token === '[') { + $depth++; + } elseif ($token === ']' && --$depth === 0) { + return $names; + } elseif ($depth > 0 && is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING + && preg_match('/^[\'\"](OTEL_[A-Z0-9_]+)[\'\"]$/D', $token[1], $nameMatch)) { + $names[] = $nameMatch[1]; + } + } + return $names; + } + return []; +} + +if (getenv('DDTRACE_CONFIG_GENERATOR_SELF_TEST')) { + $cSource = <<<'C' +// CONFIG(STRING, DD_FALSE_LINE, "", .sensitive = true) \ +CONFIG(STRING, DD_FALSE_SPLICED_LINE, "", .sensitive = true) +/* CONFIG(STRING, DD_FALSE_BLOCK, "", .sensitive = true) */ +const char *false_string = "CONFIG(STRING, DD_FALSE_STRING, \"\", .sensitive = true)"; +CONFIG(CUSTOM(MAP), DD_REAL_NESTED, "escaped quote: \" and )", .sensitive = true) +CONFIG(STRING, DD_REAL_EVEN_ESCAPE, "escaped slash: \\", .sensitive = true) +C; + $sensitive = extract_sensitive_config_names_from_source($cSource); + $expectedSensitive = ['DD_REAL_EVEN_ESCAPE', 'DD_REAL_NESTED']; + $actualSensitive = array_keys($sensitive); + sort($actualSensitive); + if ($actualSensitive !== $expectedSensitive) { + throw new RuntimeException('C sensitive configuration parser self-test failed: ' . json_encode($actualSensitive)); + } + + $phpSource = <<<'PHP' +../ext/version.h #ifndef PHP_DDTRACE_VERSION #define PHP_DDTRACE_VERSION "$(cat "../VERSION")" From 9a92bd6f509f4fbd9b5954b1ef36bfcc42fde7cf Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 31 Jul 2026 15:37:39 -0400 Subject: [PATCH 07/10] Handle nested OTel configuration literals --- tooling/generate-supported-configurations.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index 4c249c9069..63d3c544b5 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -309,9 +309,16 @@ function extract_otel_config_names($source, $constant) { $depth++; } elseif ($token === ']' && --$depth === 0) { return $names; - } elseif ($depth > 0 && is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING - && preg_match('/^[\'\"](OTEL_[A-Z0-9_]+)[\'\"]$/D', $token[1], $nameMatch)) { - $names[] = $nameMatch[1]; + } elseif ($depth === 1 && is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING) { + $literal = $token[1]; + $quote = $literal[0]; + $value = substr($literal, 1, -1); + $value = $quote === "'" + ? strtr($value, ["\\\\" => "\\", "\\'" => "'"]) + : stripcslashes($value); + if (preg_match('/^OTEL_[A-Z0-9_]+$/D', $value)) { + $names[] = $value; + } } } return $names; @@ -344,11 +351,13 @@ const OTEL_CONFIG_WHITELIST = [ 'OTEL_REAL_ONE', // 'OTEL_FALSE_ENTRY', "OTEL_REAL_TWO", + ['OTEL_FALSE_NESTED'], + "OTEL_REAL_\x54HREE", "ignored ] and escaped quote: \" OTEL_FALSE_STRING", ]; PHP; $otelNames = extract_otel_config_names($phpSource, 'OTEL_CONFIG_WHITELIST'); - if ($otelNames !== ['OTEL_REAL_ONE', 'OTEL_REAL_TWO']) { + if ($otelNames !== ['OTEL_REAL_ONE', 'OTEL_REAL_TWO', 'OTEL_REAL_THREE']) { throw new RuntimeException('OTel configuration parser self-test failed: ' . json_encode($otelNames)); } exit(0); From c2d77ac731a26fbb563e102e6d23ab86111df614 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 31 Jul 2026 15:41:26 -0400 Subject: [PATCH 08/10] Match PHP configuration literal semantics --- tooling/generate-supported-configurations.sh | 61 ++++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index 63d3c544b5..25bb607c26 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -272,6 +272,20 @@ function add_otel_entries(&$supported, $names, $metadata) { } } +function php_token_is_ignored($token) { + return is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true); +} + +function php_next_significant_token($tokens, $index, $direction) { + $tokenCount = count($tokens); + for ($i = $index + $direction; $i >= 0 && $i < $tokenCount; $i += $direction) { + if (!php_token_is_ignored($tokens[$i])) { + return $i; + } + } + return null; +} + function extract_otel_config_names($source, $constant) { $tokens = token_get_all($source); $tokenCount = count($tokens); @@ -279,43 +293,36 @@ function extract_otel_config_names($source, $constant) { if (!is_array($tokens[$i]) || $tokens[$i][0] !== T_CONST) { continue; } - do { - $i++; - } while ($i < $tokenCount && is_array($tokens[$i]) - && in_array($tokens[$i][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)); - if ($i >= $tokenCount || !is_array($tokens[$i]) - || $tokens[$i][0] !== T_STRING || $tokens[$i][1] !== $constant) { + $nameIndex = php_next_significant_token($tokens, $i, 1); + if ($nameIndex === null || !is_array($tokens[$nameIndex]) + || $tokens[$nameIndex][0] !== T_STRING || $tokens[$nameIndex][1] !== $constant) { continue; } - do { - $i++; - } while ($i < $tokenCount && is_array($tokens[$i]) - && in_array($tokens[$i][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)); - if ($i >= $tokenCount || $tokens[$i] !== '=') { + $equalsIndex = php_next_significant_token($tokens, $nameIndex, 1); + if ($equalsIndex === null || $tokens[$equalsIndex] !== '=') { continue; } - do { - $i++; - } while ($i < $tokenCount && is_array($tokens[$i]) - && in_array($tokens[$i][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)); - if ($i >= $tokenCount || $tokens[$i] !== '[') { + $arrayIndex = php_next_significant_token($tokens, $equalsIndex, 1); + if ($arrayIndex === null || $tokens[$arrayIndex] !== '[') { continue; } $names = []; $depth = 0; - for (; $i < $tokenCount; $i++) { - $token = $tokens[$i]; + for ($j = $arrayIndex; $j < $tokenCount; $j++) { + $token = $tokens[$j]; if ($token === '[') { $depth++; } elseif ($token === ']' && --$depth === 0) { return $names; } elseif ($depth === 1 && is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING) { - $literal = $token[1]; - $quote = $literal[0]; - $value = substr($literal, 1, -1); - $value = $quote === "'" - ? strtr($value, ["\\\\" => "\\", "\\'" => "'"]) - : stripcslashes($value); + $previousIndex = php_next_significant_token($tokens, $j, -1); + $nextIndex = php_next_significant_token($tokens, $j, 1); + if ($previousIndex === null || $nextIndex === null + || ($tokens[$previousIndex] !== '[' && $tokens[$previousIndex] !== ',') + || ($tokens[$nextIndex] !== ',' && $tokens[$nextIndex] !== ']')) { + continue; + } + $value = eval('return ' . $token[1] . ';'); if (preg_match('/^OTEL_[A-Z0-9_]+$/D', $value)) { $names[] = $value; } @@ -353,11 +360,15 @@ const OTEL_CONFIG_WHITELIST = [ "OTEL_REAL_TWO", ['OTEL_FALSE_NESTED'], "OTEL_REAL_\x54HREE", + "OTEL_REAL_\u{46}OUR", + "OTEL_FALSE\Q", + 'OTEL_FALSE_KEY' => false, + 'OTEL_FALSE_CONCAT' . '_SUFFIX', "ignored ] and escaped quote: \" OTEL_FALSE_STRING", ]; PHP; $otelNames = extract_otel_config_names($phpSource, 'OTEL_CONFIG_WHITELIST'); - if ($otelNames !== ['OTEL_REAL_ONE', 'OTEL_REAL_TWO', 'OTEL_REAL_THREE']) { + if ($otelNames !== ['OTEL_REAL_ONE', 'OTEL_REAL_TWO', 'OTEL_REAL_THREE', 'OTEL_REAL_FOUR']) { throw new RuntimeException('OTel configuration parser self-test failed: ' . json_encode($otelNames)); } exit(0); From 6948604c528d42dbc5fdbdb2367315bf19cf62b1 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 31 Jul 2026 15:43:40 -0400 Subject: [PATCH 09/10] Support keyed OTel configuration values --- tooling/generate-supported-configurations.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index 25bb607c26..f11f95ea18 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -317,8 +317,10 @@ function extract_otel_config_names($source, $constant) { } elseif ($depth === 1 && is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING) { $previousIndex = php_next_significant_token($tokens, $j, -1); $nextIndex = php_next_significant_token($tokens, $j, 1); + $previousToken = $previousIndex === null ? null : $tokens[$previousIndex]; if ($previousIndex === null || $nextIndex === null - || ($tokens[$previousIndex] !== '[' && $tokens[$previousIndex] !== ',') + || ($previousToken !== '[' && $previousToken !== ',' + && (!is_array($previousToken) || $previousToken[0] !== T_DOUBLE_ARROW)) || ($tokens[$nextIndex] !== ',' && $tokens[$nextIndex] !== ']')) { continue; } @@ -363,12 +365,13 @@ const OTEL_CONFIG_WHITELIST = [ "OTEL_REAL_\u{46}OUR", "OTEL_FALSE\Q", 'OTEL_FALSE_KEY' => false, + 7 => 'OTEL_REAL_FIVE', 'OTEL_FALSE_CONCAT' . '_SUFFIX', "ignored ] and escaped quote: \" OTEL_FALSE_STRING", ]; PHP; $otelNames = extract_otel_config_names($phpSource, 'OTEL_CONFIG_WHITELIST'); - if ($otelNames !== ['OTEL_REAL_ONE', 'OTEL_REAL_TWO', 'OTEL_REAL_THREE', 'OTEL_REAL_FOUR']) { + if ($otelNames !== ['OTEL_REAL_ONE', 'OTEL_REAL_TWO', 'OTEL_REAL_THREE', 'OTEL_REAL_FOUR', 'OTEL_REAL_FIVE']) { throw new RuntimeException('OTel configuration parser self-test failed: ' . json_encode($otelNames)); } exit(0); From ea536a18b64e919ac7c577176325724ed75dd62e Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 31 Jul 2026 15:47:05 -0400 Subject: [PATCH 10/10] Reject ambiguous OTel configuration entries --- tooling/generate-supported-configurations.sh | 54 ++++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index f11f95ea18..2df6ba3e98 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -307,30 +307,27 @@ function extract_otel_config_names($source, $constant) { continue; } $names = []; - $depth = 0; - for ($j = $arrayIndex; $j < $tokenCount; $j++) { + for ($j = $arrayIndex + 1; $j < $tokenCount; $j++) { $token = $tokens[$j]; - if ($token === '[') { - $depth++; - } elseif ($token === ']' && --$depth === 0) { + if (php_token_is_ignored($token) || $token === ',') { + continue; + } + if ($token === ']') { return $names; - } elseif ($depth === 1 && is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING) { - $previousIndex = php_next_significant_token($tokens, $j, -1); + } + if (is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING) { $nextIndex = php_next_significant_token($tokens, $j, 1); - $previousToken = $previousIndex === null ? null : $tokens[$previousIndex]; - if ($previousIndex === null || $nextIndex === null - || ($previousToken !== '[' && $previousToken !== ',' - && (!is_array($previousToken) || $previousToken[0] !== T_DOUBLE_ARROW)) - || ($tokens[$nextIndex] !== ',' && $tokens[$nextIndex] !== ']')) { + if ($nextIndex !== null && ($tokens[$nextIndex] === ',' || $tokens[$nextIndex] === ']')) { + $value = eval('return ' . $token[1] . ';'); + if (preg_match('/^OTEL_[A-Z0-9_]+$/D', $value)) { + $names[] = $value; + } continue; } - $value = eval('return ' . $token[1] . ';'); - if (preg_match('/^OTEL_[A-Z0-9_]+$/D', $value)) { - $names[] = $value; - } } + throw new RuntimeException("Unsupported entry in $constant; use direct string literals"); } - return $names; + throw new RuntimeException("Unterminated array for $constant"); } return []; } @@ -360,20 +357,33 @@ const OTEL_CONFIG_WHITELIST = [ 'OTEL_REAL_ONE', // 'OTEL_FALSE_ENTRY', "OTEL_REAL_TWO", - ['OTEL_FALSE_NESTED'], "OTEL_REAL_\x54HREE", "OTEL_REAL_\u{46}OUR", "OTEL_FALSE\Q", - 'OTEL_FALSE_KEY' => false, - 7 => 'OTEL_REAL_FIVE', - 'OTEL_FALSE_CONCAT' . '_SUFFIX', "ignored ] and escaped quote: \" OTEL_FALSE_STRING", ]; PHP; $otelNames = extract_otel_config_names($phpSource, 'OTEL_CONFIG_WHITELIST'); - if ($otelNames !== ['OTEL_REAL_ONE', 'OTEL_REAL_TWO', 'OTEL_REAL_THREE', 'OTEL_REAL_FOUR', 'OTEL_REAL_FIVE']) { + if ($otelNames !== ['OTEL_REAL_ONE', 'OTEL_REAL_TWO', 'OTEL_REAL_THREE', 'OTEL_REAL_FOUR']) { throw new RuntimeException('OTel configuration parser self-test failed: ' . json_encode($otelNames)); } + $unsupportedEntries = [ + "['OTEL_NESTED']", + "'OTEL_STRING_KEY' => false", + "7 => 'OTEL_KEYED_VALUE'", + "'OTEL_CONCAT' . '_VALUE'", + "('OTEL_PAREN')", + "OTEL_CONSTANT_REFERENCE", + ]; + foreach ($unsupportedEntries as $entry) { + $unsupportedSource = "