[fix](be) Guard empty stack trace formatting (#65731)

An empty or too-short libunwind stack trace underflows the
remaining-frame count in `StackTrace::toString()` after it skips its
internal frames. Formatting an error `Status` then reads beyond the
fixed frame array and can crash the BE.

Treat non-positive stack-capture results as empty, render a trace with
fewer frames than the requested skip as the existing `<Empty trace>`,
and preserve signal-context offsets after slicing. A debug-point BEUT
injects an empty trace into the real `Status::NotFound` capture/logging
path.
This commit is contained in:
zclllyybb authored and GitHub committed 2026-07-17 10:31:20 +08:00
1 parent 4df43e95c3
commit 2ec5821692
2 files changed
+47 -5

No files matched your search

+18 -5
View File
@@ -44,6 +44,7 @@
#include "common/memory_sanitizer.h"
#include "common/symbol_index.h"
#include "exec/common/hex.h"
#include "util/debug_points.h"
#include "util/string_util.h"
namespace {
@@ -304,11 +305,17 @@ StackTrace::StackTrace(const ucontext_t& signal_context) {
}
void StackTrace::tryCapture() {
int frame_count = 0;
#if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__)
size = unw_backtrace(frame_pointers.data(), capacity);
frame_count = unw_backtrace(frame_pointers.data(), capacity);
#else
size = backtrace(frame_pointers.data(), capacity);
frame_count = backtrace(frame_pointers.data(), capacity);
#endif
size = frame_count > 0 ? static_cast<size_t>(frame_count) : 0;
if (doris::config::enable_debug_points &&
doris::DebugPoints::instance()->is_enable("StackTrace::tryCapture.empty_trace")) {
size = 0;
}
__msan_unpoison(frame_pointers.data(), size * sizeof(frame_pointers[0]));
}
@@ -480,11 +487,17 @@ std::string StackTrace::toString(int start_pointers_index,
const std::string& dwarf_location_info_mode) const {
// Default delete the first three frame pointers, which are inside the stack_trace.cpp.
start_pointers_index += 3;
if (start_pointers_index < 0 || static_cast<size_t>(start_pointers_index) >= size) {
// Unwind can legitimately return fewer frames than the internal frames we normally hide.
return toStringCached({}, 0, 0, dwarf_location_info_mode);
}
const auto first_frame = static_cast<size_t>(start_pointers_index);
StackTrace::FramePointers frame_pointers_raw {};
std::copy(frame_pointers.begin() + start_pointers_index, frame_pointers.end(),
std::copy(frame_pointers.begin() + first_frame, frame_pointers.end(),
frame_pointers_raw.begin());
return toStringCached(frame_pointers_raw, offset, size - start_pointers_index,
dwarf_location_info_mode);
return toStringCached(frame_pointers_raw, offset > first_frame ? offset - first_frame : 0,
size - first_frame, dwarf_location_info_mode);
}
std::string StackTrace::toString(void** frame_pointers_raw, size_t offset, size_t size,
+29
View File
@@ -21,11 +21,33 @@
#include <gtest/gtest-test-part.h>
#include "gtest/gtest_pred_impl.h"
#include "util/debug_points.h"
namespace doris {
class StatusTest : public testing::Test {};
class ScopedEmptyStackTraceFault {
public:
ScopedEmptyStackTraceFault()
: _enable_debug_points(config::enable_debug_points),
_enable_stacktrace(config::enable_stacktrace) {
config::enable_debug_points = true;
config::enable_stacktrace = true;
DebugPoints::instance()->add("StackTrace::tryCapture.empty_trace");
}
~ScopedEmptyStackTraceFault() {
DebugPoints::instance()->remove("StackTrace::tryCapture.empty_trace");
config::enable_stacktrace = _enable_stacktrace;
config::enable_debug_points = _enable_debug_points;
}
private:
const bool _enable_debug_points;
const bool _enable_stacktrace;
};
TEST_F(StatusTest, OK) {
// default
Status st;
@@ -117,6 +139,13 @@ TEST_F(StatusTest, Error) {
}
}
TEST_F(StatusTest, EmptyCapturedStackTrace) {
ScopedEmptyStackTraceFault fault;
Status status = Status::NotFound("fault-injected empty stack trace");
EXPECT_NE(status.to_string().find("<Empty trace>"), std::string::npos);
}
TEST_F(StatusTest /*unused*/, Format /*unused*/) {
// status == ok
Status st_ok = Status::OK();