From bad942c899771a421d12a53b14862d594d99ac82 Mon Sep 17 00:00:00 2001 From: iamluc Date: Wed, 11 Dec 2024 10:10:06 +0100 Subject: [PATCH] Fix user-after-free with debugger --- ext/exception_serialize.c | 16 +- ...ion-replay_non_regression_2989_mysqli.phpt | 155 ++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt diff --git a/ext/exception_serialize.c b/ext/exception_serialize.c index 909f506fd47..4f93af47d57 100644 --- a/ext/exception_serialize.c +++ b/ext/exception_serialize.c @@ -230,6 +230,10 @@ void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, con zend_get_properties_for(zv, ZEND_PROP_PURPOSE_DEBUG) #endif : Z_OBJPROP_P(zv); + + if (!ht) { + break; + } ZEND_HASH_REVERSE_FOREACH_STR_KEY_VAL(ht, key, val) { if (!key) { continue; @@ -258,6 +262,16 @@ void ddtrace_create_capture_value(zval *zv, struct ddog_CaptureValue *value, con ddtrace_snapshot_redacted_name(&value_capture, fieldname); ZVAL_DEINDIRECT(val); ddtrace_create_capture_value(val, &value_capture, config, remaining_nesting - 1); + + // zend_get_properties_for can create ephemeral values that are released just after this loop + // We must persist them in the arena. +#if PHP_VERSION_ID >= 70400 + if (ce->type == ZEND_INTERNAL_CLASS) { + char *persisted_val = zend_arena_alloc(&DDTRACE_G(debugger_capture_arena), value_capture.value.len); + memcpy(persisted_val, value_capture.value.ptr, value_capture.value.len); + value_capture.value.ptr = persisted_val; + } +#endif ddog_capture_value_add_field(value, fieldname, value_capture); } ZEND_HASH_FOREACH_END(); if (ce->type == ZEND_INTERNAL_CLASS) { @@ -392,7 +406,7 @@ static void ddtrace_collect_exception_debug_data(zend_object *exception, zend_st LOG(TRACE, "Skipping exception replay capture due to hash %.*s already recently hit", hash_len, exception_hash); return; } - + char *exception_id = zend_arena_alloc(&DDTRACE_G(debugger_capture_arena), uuid_len); ddog_snapshot_format_new_uuid((uint8_t(*)[uuid_len])exception_id); diff --git a/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt b/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt new file mode 100644 index 00000000000..acead776754 --- /dev/null +++ b/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt @@ -0,0 +1,155 @@ +--TEST-- +Non regression test for use-after-free segfault in exception replay +--SKIPIF-- + + + +--ENV-- +DD_AGENT_HOST=request-replayer +DD_TRACE_AGENT_PORT=80 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_EXCEPTION_REPLAY_ENABLED=1 +DD_EXCEPTION_REPLAY_CAPTURE_INTERVAL_SECONDS=1 +--INI-- +datadog.trace.agent_test_session_token=live-debugger/non_regression_2989_mysqli +--FILE-- +query('SELECT 1'); + $mysqli->close(); +} catch (Exception $e) { + $span = \DDTrace\start_span(); + $span->exception = $e; + \DDTrace\close_span(); +} + +$dlr = new DebuggerLogReplayer; +$log = $dlr->waitForDebuggerDataAndReplay(); +$log = json_decode($log["body"], true); + +function recursive_ksort(&$arr) { + if (is_array($arr)) { + ksort($arr); + array_walk($arr, 'recursive_ksort'); + } +} + +recursive_ksort($log[0]["debugger"]["snapshot"]["captures"]); +var_dump($log[0]); + +?> +--CLEAN-- + +--EXPECTF-- +Warning: mysqli::__construct(): php_network_getaddresses: getaddrinfo for @@_INVALID_@@ failed: Name or service not known in %s +array(5) { + ["service"]=> + string(47) "exception-replay_non_regression_2989_mysqli.php" + ["ddsource"]=> + string(11) "dd_debugger" + ["timestamp"]=> + int(%d) + ["debugger"]=> + array(1) { + ["snapshot"]=> + array(8) { + ["language"]=> + string(3) "php" + ["id"]=> + string(36) "%s" + ["timestamp"]=> + int(%d) + ["exceptionCaptureId"]=> + string(36) "%s" + ["exceptionHash"]=> + string(16) "%s" + ["frameIndex"]=> + int(0) + ["captures"]=> + array(1) { + ["return"]=> + array(1) { + ["arguments"]=> + array(4) { + ["hostname"]=> + array(2) { + ["type"]=> + string(6) "string" + ["value"]=> + string(13) "@@_INVALID_@@" + } + ["password"]=> + array(1) { + ["type"]=> + string(23) "SensitiveParameterValue" + } + ["this"]=> + array(2) { + ["fields"]=> + array(4) { + ["client_info"]=> + array(2) { + ["type"]=> + string(6) "string" + ["value"]=> + string(14) "%s" + } + ["client_version"]=> + array(2) { + ["type"]=> + string(3) "int" + ["value"]=> + string(5) "%d" + } + ["connect_errno"]=> + array(2) { + ["type"]=> + string(3) "int" + ["value"]=> + string(4) "2002" + } + ["connect_error"]=> + array(2) { + ["type"]=> + string(6) "string" + ["value"]=> + string(89) "php_network_getaddresses: getaddrinfo for @@_INVALID_@@ failed: Name or service not known" + } + } + ["type"]=> + string(6) "mysqli" + } + ["username"]=> + array(2) { + ["type"]=> + string(6) "string" + ["value"]=> + string(3) "foo" + } + } + } + } + ["probe"]=> + array(2) { + ["id"]=> + string(0) "" + ["location"]=> + array(2) { + ["method"]=> + string(11) "__construct" + ["type"]=> + string(6) "mysqli" + } + } + } + } + ["message"]=> + NULL +}