Skip to content

Commit deaa02b

Browse files
Scusemuafacebook-github-bot
authored andcommitted
Return an empty stack, not a blank frame, when no trace is available (meta-pytorch#3855)
Summary: `captureNativeErrorStack()` promises "an empty vector when it is unavailable", but does not deliver it. `folly::split('\n', "", v)` yields a vector holding one empty string, so an unavailable trace is reported to Scuba as a single blank frame rather than as no frames. Two paths reach that empty string, and only one was considered. The header guarded on `__has_include(<dwarf.h>)`, treating a missing header as the only way capture can be unavailable. But `Symbolizer.h:292-293` documents that the **real** implementation also returns `""` when a trace is not available, so the bug is reachable in a build with a fully working symbolizer. Gating on a header was never the right test. This replaces the header probe with a test of the actual result. `Symbolizer.h` is now included unconditionally -- it already supplies an inline stub returning `""` in builds without a symbolizer, so duplicating folly's capability macros here served no purpose -- and the empty case is handled where it is observable. `folly::split` also now ignores empty tokens, so a trailing newline no longer contributes a blank trailing frame. The split/demangle/`skipInternalFrames` sequence is unchanged and moves intact into `detail::normalizeStackTrace`, which is exposed so the behaviour can be tested directly. Without that seam the empty case could only be reached by building against a folly without a symbolizer, which is not something a unit test can arrange. No behavioural change for a healthy capture: the same frames, in the same order, with the same plumbing frames stripped. Differential Revision: D117752032
1 parent 08d0316 commit deaa02b

3 files changed

Lines changed: 97 additions & 19 deletions

File tree

comms/utils/logger/ErrorStackUtil.cc

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@
33
#include "comms/utils/logger/ErrorStackUtil.h"
44

55
#include <array>
6-
#include <sstream>
76
#include <string_view>
87

9-
#if __has_include(<dwarf.h>)
10-
#include <folly/debugging/symbolizer/Symbolizer.h>
11-
#endif
128
#include <folly/String.h>
9+
#include <folly/debugging/symbolizer/Symbolizer.h>
1310

1411
namespace {
1512
// Leading native-stack frames that belong to the logging / Scuba plumbing
@@ -55,25 +52,33 @@ void skipInternalFrames(std::vector<std::string>& frames) {
5552

5653
namespace meta::comms::logger {
5754

58-
std::vector<std::string> captureNativeErrorStack() {
59-
std::vector<std::string> stackTraceDemangled;
55+
namespace detail {
56+
57+
std::vector<std::string> normalizeStackTrace(const std::string& trace) {
58+
// folly returns "" both from the no-symbolizer stub and, per Symbolizer.h,
59+
// from the real implementation when no trace is available. Splitting "" would
60+
// yield one empty element -- a bogus frame where there should be none.
61+
if (trace.empty()) {
62+
return {};
63+
}
6064

61-
// Get stack trace (requires elfutils/libdwarf for folly Symbolizer)
62-
#if __has_include(<dwarf.h>)
63-
std::stringstream ss;
64-
ss << folly::symbolizer::getStackTraceStr();
65+
std::vector<std::string> frames;
6566
// @lint-ignore CLANGTIDY
66-
folly::split('\n', ss.str(), stackTraceDemangled);
67-
for (auto& line : stackTraceDemangled) {
67+
folly::split('\n', trace, frames, /* ignoreEmpty */ true);
68+
for (auto& line : frames) {
6869
auto demangledLine = folly::demangle(line.c_str()).toStdString();
6970
line.swap(demangledLine);
7071
}
7172
// Drop the leading logging / Scuba plumbing frames so the recorded stack
7273
// starts near the real error site.
73-
skipInternalFrames(stackTraceDemangled);
74-
#endif
74+
skipInternalFrames(frames);
75+
return frames;
76+
}
7577

76-
return stackTraceDemangled;
78+
} // namespace detail
79+
80+
std::vector<std::string> captureNativeErrorStack() {
81+
return detail::normalizeStackTrace(folly::symbolizer::getStackTraceStr());
7782
}
7883

7984
} // namespace meta::comms::logger

comms/utils/logger/ErrorStackUtil.h

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,19 @@
88
namespace meta::comms::logger {
99

1010
// Capture the native symbolized error stack once, dropping the leading logging
11-
// / Scuba plumbing frames. Returns the symbolized stack when symbolizer support
12-
// is available, and an empty vector when it is unavailable (no dwarf.h). The
13-
// result can be shared across all error reporters so the expensive capture runs
14-
// only once per error.
11+
// / Scuba plumbing frames. Returns an empty vector when the build has no
12+
// symbolizer support and when no trace could be obtained, so callers must treat
13+
// an empty result as "no stack available" rather than as an error. The result
14+
// can be shared across all error reporters so the expensive capture runs only
15+
// once per error.
1516
std::vector<std::string> captureNativeErrorStack();
1617

18+
namespace detail {
19+
// Turns a raw symbolizer trace into frames: one per line, demangled, with the
20+
// leading logging / Scuba plumbing removed. An empty trace yields an empty
21+
// vector. Exposed so that case can be tested without building against a folly
22+
// that has no symbolizer.
23+
std::vector<std::string> normalizeStackTrace(const std::string& trace);
24+
} // namespace detail
25+
1726
} // namespace meta::comms::logger
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
#include "comms/utils/logger/ErrorStackUtil.h"
4+
5+
#include <folly/debugging/symbolizer/Symbolizer.h>
6+
#include <folly/portability/GTest.h>
7+
8+
using meta::comms::logger::captureNativeErrorStack;
9+
using meta::comms::logger::detail::normalizeStackTrace;
10+
11+
// This target exercises the real symbolizer path. If folly is ever configured
12+
// without one here, the capture test below would silently pass against the
13+
// inline stub instead, so assert the capability rather than infer it.
14+
static_assert(
15+
FOLLY_HAVE_ELF && FOLLY_HAVE_DWARF,
16+
"buck2 is expected to build folly with a real symbolizer; without it the "
17+
"captureNativeErrorStack test would exercise the empty-string stub");
18+
19+
TEST(ErrorStackUtilTest, EmptyTraceYieldsEmptyVector) {
20+
// Both the no-symbolizer stub and the real implementation return "" when no
21+
// trace is available; splitting it would yield one bogus frame.
22+
EXPECT_TRUE(normalizeStackTrace("").empty());
23+
}
24+
25+
TEST(ErrorStackUtilTest, SplitsFramesOnNewlines) {
26+
const std::vector<std::string> expected = {"frame_one", "frame_two"};
27+
EXPECT_EQ(normalizeStackTrace("frame_one\nframe_two"), expected);
28+
}
29+
30+
TEST(ErrorStackUtilTest, IgnoresBlankLines) {
31+
// A trailing newline must not produce an empty trailing frame.
32+
const std::vector<std::string> expected = {"frame_one", "frame_two"};
33+
EXPECT_EQ(normalizeStackTrace("frame_one\n\nframe_two\n"), expected);
34+
}
35+
36+
TEST(ErrorStackUtilTest, DropsLeadingPlumbingFrames) {
37+
// Leading logging / Scuba frames are stripped so the stack starts at the
38+
// real error site.
39+
const std::vector<std::string> expected = {"real_error_site", "caller"};
40+
EXPECT_EQ(
41+
normalizeStackTrace(
42+
"folly::symbolizer::getStackTraceStr()\n"
43+
"NcclScubaSample::setError\n"
44+
"real_error_site\n"
45+
"caller"),
46+
expected);
47+
}
48+
49+
TEST(ErrorStackUtilTest, KeepsInternalMarkerAppearingAfterRealFrame) {
50+
// Only *leading* plumbing is dropped -- a marker deeper in the stack is a
51+
// genuine frame.
52+
const std::vector<std::string> expected = {
53+
"real_error_site", "logErrorToScuba"};
54+
EXPECT_EQ(normalizeStackTrace("real_error_site\nlogErrorToScuba"), expected);
55+
}
56+
57+
TEST(ErrorStackUtilTest, CaptureProducesNoEmptyFrames) {
58+
// Symbolizer.h documents that even the real implementation may return "",
59+
// so the result is allowed to be empty; what must never happen is a frame
60+
// that is itself empty.
61+
for (const auto& frame : captureNativeErrorStack()) {
62+
EXPECT_FALSE(frame.empty());
63+
}
64+
}

0 commit comments

Comments
 (0)