Add lifetime tracking debug utility to QUICHE.
PiperOrigin-RevId: 615244192
diff --git a/build/source_list.bzl b/build/source_list.bzl
index cce928f..9d9cf94 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -13,6 +13,7 @@
"common/capsule.h",
"common/http/http_header_block.h",
"common/http/http_header_storage.h",
+ "common/lifetime_tracking.h",
"common/masque/connect_ip_datagram_payload.h",
"common/masque/connect_udp_datagram_payload.h",
"common/platform/api/quiche_bug_tracker.h",
@@ -1057,6 +1058,7 @@
"common/capsule_test.cc",
"common/http/http_header_block_test.cc",
"common/http/http_header_storage_test.cc",
+ "common/lifetime_tracking_test.cc",
"common/masque/connect_ip_datagram_payload_test.cc",
"common/masque/connect_udp_datagram_payload_test.cc",
"common/platform/api/quiche_file_utils_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index 36d7c2d..589f737 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -13,6 +13,7 @@
"src/quiche/common/capsule.h",
"src/quiche/common/http/http_header_block.h",
"src/quiche/common/http/http_header_storage.h",
+ "src/quiche/common/lifetime_tracking.h",
"src/quiche/common/masque/connect_ip_datagram_payload.h",
"src/quiche/common/masque/connect_udp_datagram_payload.h",
"src/quiche/common/platform/api/quiche_bug_tracker.h",
@@ -1058,6 +1059,7 @@
"src/quiche/common/capsule_test.cc",
"src/quiche/common/http/http_header_block_test.cc",
"src/quiche/common/http/http_header_storage_test.cc",
+ "src/quiche/common/lifetime_tracking_test.cc",
"src/quiche/common/masque/connect_ip_datagram_payload_test.cc",
"src/quiche/common/masque/connect_udp_datagram_payload_test.cc",
"src/quiche/common/platform/api/quiche_file_utils_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index 5c154ef..274f204 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -12,6 +12,7 @@
"quiche/common/capsule.h",
"quiche/common/http/http_header_block.h",
"quiche/common/http/http_header_storage.h",
+ "quiche/common/lifetime_tracking.h",
"quiche/common/masque/connect_ip_datagram_payload.h",
"quiche/common/masque/connect_udp_datagram_payload.h",
"quiche/common/platform/api/quiche_bug_tracker.h",
@@ -1057,6 +1058,7 @@
"quiche/common/capsule_test.cc",
"quiche/common/http/http_header_block_test.cc",
"quiche/common/http/http_header_storage_test.cc",
+ "quiche/common/lifetime_tracking_test.cc",
"quiche/common/masque/connect_ip_datagram_payload_test.cc",
"quiche/common/masque/connect_udp_datagram_payload_test.cc",
"quiche/common/platform/api/quiche_file_utils_test.cc",
diff --git a/quiche/common/lifetime_tracking.h b/quiche/common/lifetime_tracking.h
new file mode 100644
index 0000000..34f8c6b
--- /dev/null
+++ b/quiche/common/lifetime_tracking.h
@@ -0,0 +1,142 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This file implements helper classes to track c++ object lifetimes. They are
+// useful to debug use-after-free issues in environments where the cost of ASAN
+// is too high.
+//
+// Suppose you have an object of type "MyClass" and a raw pointer "ptr" pointing
+// to it, and you suspect a dereference of "ptr" is unsafe because the object it
+// points to is dead. You can do
+//
+// (1) Add a LifetimeTrackable member to "MyClass". Alternatively, you can also
+// change MyClass to inherit from LifetimeTrackable.
+//
+// struct MyClass {
+// ...... existing members ......
+// LifetimeTrackable trackable;
+// }
+//
+// (2) Add a LifetimeTracker alongside the "ptr".
+//
+// ptr = new MyClass().
+// tracker = ptr->trackable.NewTracker();
+//
+// (3) Before the potentially dangerous dereference, check whether *ptr is dead:
+//
+// if (tracker.IsTrackedObjectDead()) {
+// // ptr->trackable has been destructed. Log its destruction stack below.
+// QUICHE_LOG(ERROR) << "*ptr has bee destructed: " << tracker;
+// }
+// ptr->MethodCall(); // Possibly a use-after-free
+//
+// All classes defined in this file are not thread safe.
+
+#ifndef QUICHE_COMMON_LIFETIME_TRACKING_H_
+#define QUICHE_COMMON_LIFETIME_TRACKING_H_
+
+#include <array>
+#include <cstddef>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/log/die_if_null.h"
+#include "absl/strings/str_format.h"
+#include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/common/platform/api/quiche_stack_trace.h"
+
+namespace quiche {
+namespace test {
+class LifetimeTrackingTest;
+} // namespace test
+
+// LifetimeInfo holds information about a LifetimeTrackable object.
+struct QUICHE_EXPORT LifetimeInfo {
+ bool IsDead() const { return destructor_stack.has_value(); }
+
+ // If IsDead(), the stack when the LifetimeTrackable object is destructed.
+ std::optional<std::vector<void*>> destructor_stack;
+};
+
+// LifetimeTracker tracks the lifetime of a LifetimeTrackable object, by holding
+// a reference to its LifetimeInfo.
+class QUICHE_EXPORT LifetimeTracker {
+ public:
+ // Copy constructor and assignment operator allow this tracker to track the
+ // same object as |other|.
+ LifetimeTracker(const LifetimeTracker& other) { CopyFrom(other); }
+ LifetimeTracker& operator=(const LifetimeTracker& other) {
+ CopyFrom(other);
+ return *this;
+ }
+
+ // Move constructor and assignment are implemented as copies, to prevent the
+ // moved-from object from tracking "nothing".
+ LifetimeTracker(LifetimeTracker&& other) { CopyFrom(other); }
+ LifetimeTracker& operator=(LifetimeTracker&& other) {
+ CopyFrom(other);
+ return *this;
+ }
+
+ // Whether the tracked object is dead.
+ bool IsTrackedObjectDead() const { return info_->IsDead(); }
+
+ template <typename Sink>
+ friend void AbslStringify(Sink& sink, const LifetimeTracker& tracker) {
+ if (tracker.info_->IsDead()) {
+ absl::Format(&sink, "Tracked object has died with %v",
+ SymbolizeStackTrace(*tracker.info_->destructor_stack));
+ } else {
+ absl::Format(&sink, "Tracked object is alive.");
+ }
+ }
+
+ private:
+ friend class LifetimeTrackable;
+ explicit LifetimeTracker(std::shared_ptr<const LifetimeInfo> info)
+ : info_(ABSL_DIE_IF_NULL(info)) {}
+ void CopyFrom(const LifetimeTracker& other) { info_ = other.info_; }
+
+ std::shared_ptr<const LifetimeInfo> info_;
+};
+
+// LifetimeTrackable allows its lifetime to be tracked by any number of
+// LifetimeTracker(s).
+class QUICHE_EXPORT LifetimeTrackable {
+ public:
+ LifetimeTrackable() = default;
+ virtual ~LifetimeTrackable() {
+ if (info_ != nullptr) {
+ info_->destructor_stack = CurrentStackTrace();
+ }
+ }
+
+ // LifetimeTrackable only tracks the memory occupied by itself. All copy/move
+ // constructors and assignments are no-op.
+ LifetimeTrackable(const LifetimeTrackable&) : LifetimeTrackable() {}
+ LifetimeTrackable& operator=(const LifetimeTrackable&) { return *this; }
+ LifetimeTrackable(LifetimeTrackable&&) : LifetimeTrackable() {}
+ LifetimeTrackable& operator=(LifetimeTrackable&&) { return *this; }
+
+ LifetimeTracker NewTracker() {
+ if (info_ == nullptr) {
+ info_ = std::make_shared<LifetimeInfo>();
+ }
+ return LifetimeTracker(info_);
+ }
+
+ private:
+ friend class test::LifetimeTrackingTest;
+ // nullptr if this object is not tracked by any LifetimeTracker.
+ std::shared_ptr<LifetimeInfo> info_;
+};
+
+} // namespace quiche
+
+#endif // QUICHE_COMMON_LIFETIME_TRACKING_H_
diff --git a/quiche/common/lifetime_tracking_test.cc b/quiche/common/lifetime_tracking_test.cc
new file mode 100644
index 0000000..94b0ba1
--- /dev/null
+++ b/quiche/common/lifetime_tracking_test.cc
@@ -0,0 +1,181 @@
+#include "quiche/common/lifetime_tracking.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/strings/str_cat.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/platform/api/quiche_test.h"
+
+namespace quiche {
+namespace test {
+
+struct ComposedTrackable {
+ LifetimeTrackable trackable;
+};
+
+struct InheritedTrackable : LifetimeTrackable {};
+
+enum class TrackableType {
+ kComposed,
+ kInherited,
+};
+
+// Used by ::testing::PrintToStringParamName().
+std::string PrintToString(const TrackableType& type) {
+ switch (type) {
+ case TrackableType::kComposed:
+ return "Composed";
+ case TrackableType::kInherited:
+ return "Inherited";
+ default:
+ QUICHE_LOG(FATAL) << "Unknown TrackableType: " << static_cast<int>(type);
+ }
+}
+
+class LifetimeTrackingTest : public QuicheTestWithParam<TrackableType> {
+ protected:
+ LifetimeTrackingTest() {
+ if (GetParam() == TrackableType::kComposed) {
+ composed_trackable_ = std::make_unique<ComposedTrackable>();
+ } else {
+ inherited_trackable_ = std::make_unique<InheritedTrackable>();
+ }
+ }
+
+ // Returns the trackable object. Must be called before FreeTrackable.
+ LifetimeTrackable& GetTrackable() {
+ if (composed_trackable_ != nullptr) {
+ return composed_trackable_->trackable;
+ } else {
+ return *inherited_trackable_;
+ }
+ }
+
+ // Returns a trackable.info_.
+ const std::shared_ptr<LifetimeInfo>& GetLifetimeInfoFromTrackable(
+ LifetimeTrackable& trackable) {
+ return trackable.info_;
+ }
+
+ const std::shared_ptr<LifetimeInfo>& GetLifetimeInfoFromTrackable() {
+ return GetLifetimeInfoFromTrackable(GetTrackable());
+ }
+
+ void FreeTrackable() {
+ composed_trackable_ = nullptr;
+ inherited_trackable_ = nullptr;
+ }
+
+ std::unique_ptr<ComposedTrackable> composed_trackable_;
+ std::unique_ptr<InheritedTrackable> inherited_trackable_;
+};
+
+TEST_P(LifetimeTrackingTest, TrackableButNeverTracked) {
+ EXPECT_EQ(GetLifetimeInfoFromTrackable(), nullptr);
+}
+
+TEST_P(LifetimeTrackingTest, SingleTrackerQueryLiveness) {
+ LifetimeTracker tracker = GetTrackable().NewTracker();
+ EXPECT_FALSE(tracker.IsTrackedObjectDead());
+ EXPECT_THAT(absl::StrCat(tracker),
+ testing::HasSubstr("Tracked object is alive"));
+ FreeTrackable();
+ EXPECT_TRUE(tracker.IsTrackedObjectDead());
+ EXPECT_THAT(absl::StrCat(tracker),
+ testing::HasSubstr("Tracked object has died"));
+}
+
+TEST_P(LifetimeTrackingTest, MultiTrackersQueryLiveness) {
+ LifetimeTracker tracker1 = GetTrackable().NewTracker();
+ LifetimeTracker tracker2 = GetTrackable().NewTracker();
+ LifetimeTracker tracker3 = tracker2;
+ LifetimeTracker tracker4 = std::move(tracker3);
+ LifetimeTracker tracker5(std::move(tracker4));
+ LifetimeTrackable another_trackable;
+ LifetimeTracker tracker6 = another_trackable.NewTracker();
+ LifetimeTracker tracker7 = another_trackable.NewTracker();
+ tracker6 = tracker2;
+ tracker7 = std::move(tracker2);
+ EXPECT_FALSE(tracker1.IsTrackedObjectDead());
+ EXPECT_FALSE(
+ tracker2.IsTrackedObjectDead()); // NOLINT(bugprone-use-after-move)
+ EXPECT_FALSE(
+ tracker3.IsTrackedObjectDead()); // NOLINT(bugprone-use-after-move)
+ EXPECT_FALSE(
+ tracker4.IsTrackedObjectDead()); // NOLINT(bugprone-use-after-move)
+ EXPECT_FALSE(tracker5.IsTrackedObjectDead());
+ EXPECT_FALSE(tracker6.IsTrackedObjectDead());
+ EXPECT_FALSE(tracker7.IsTrackedObjectDead());
+ FreeTrackable();
+ EXPECT_TRUE(tracker1.IsTrackedObjectDead());
+ EXPECT_TRUE(
+ tracker2.IsTrackedObjectDead()); // NOLINT(bugprone-use-after-move)
+ EXPECT_TRUE(
+ tracker3.IsTrackedObjectDead()); // NOLINT(bugprone-use-after-move)
+ EXPECT_TRUE(
+ tracker4.IsTrackedObjectDead()); // NOLINT(bugprone-use-after-move)
+ EXPECT_TRUE(tracker5.IsTrackedObjectDead());
+ EXPECT_TRUE(tracker6.IsTrackedObjectDead());
+ EXPECT_TRUE(tracker7.IsTrackedObjectDead());
+}
+
+TEST_P(LifetimeTrackingTest, CopyTrackableIsNoop) {
+ LifetimeTracker tracker = GetTrackable().NewTracker();
+ const LifetimeInfo* info = GetLifetimeInfoFromTrackable().get();
+ EXPECT_NE(info, nullptr);
+ LifetimeTrackable trackable2(GetTrackable());
+ EXPECT_EQ(GetLifetimeInfoFromTrackable(trackable2), nullptr);
+
+ LifetimeTrackable trackable3;
+ trackable3 = GetTrackable();
+ EXPECT_EQ(GetLifetimeInfoFromTrackable(trackable3), nullptr);
+
+ EXPECT_EQ(GetLifetimeInfoFromTrackable().get(), info);
+}
+
+TEST_P(LifetimeTrackingTest, MoveTrackableIsNoop) {
+ LifetimeTracker tracker = GetTrackable().NewTracker();
+ const LifetimeInfo* info = GetLifetimeInfoFromTrackable().get();
+ EXPECT_NE(info, nullptr);
+ LifetimeTrackable trackable2(std::move(GetTrackable()));
+ EXPECT_EQ(GetLifetimeInfoFromTrackable(trackable2), nullptr);
+
+ LifetimeTrackable trackable3;
+ trackable3 = std::move(GetTrackable());
+ EXPECT_EQ(GetLifetimeInfoFromTrackable(trackable3), nullptr);
+
+ EXPECT_EQ(GetLifetimeInfoFromTrackable().get(), info);
+}
+
+TEST_P(LifetimeTrackingTest, ObjectDiedDueToVectorRealloc) {
+ if (GetParam() == TrackableType::kComposed) {
+ return;
+ }
+
+ std::vector<InheritedTrackable> trackables;
+
+ // Append 1 element to the vector and keep track of its life.
+ InheritedTrackable& trackable = trackables.emplace_back();
+ LifetimeTracker tracker = trackable.NewTracker();
+ EXPECT_FALSE(tracker.IsTrackedObjectDead());
+
+ // Append 1000 more elements to the vector, |trackable| should be destroyed by
+ // vector realloc.
+ for (int i = 0; i < 1000; ++i) {
+ trackables.emplace_back();
+ }
+
+ // Accessing |trackable| is a use-after-free.
+ EXPECT_TRUE(tracker.IsTrackedObjectDead());
+}
+
+INSTANTIATE_TEST_SUITE_P(Tests, LifetimeTrackingTest,
+ testing::Values(TrackableType::kComposed,
+ TrackableType::kInherited),
+ testing::PrintToStringParamName());
+
+} // namespace test
+} // namespace quiche
diff --git a/quiche/common/platform/api/quiche_stack_trace.h b/quiche/common/platform/api/quiche_stack_trace.h
index 4c07577..57fd654 100644
--- a/quiche/common/platform/api/quiche_stack_trace.h
+++ b/quiche/common/platform/api/quiche_stack_trace.h
@@ -6,11 +6,22 @@
#define QUICHE_COMMON_PLATFORM_API_QUICHE_STACK_TRACE_H_
#include <string>
+#include <vector>
+
+#include "absl/types/span.h"
#include "quiche_platform_impl/quiche_stack_trace_impl.h"
namespace quiche {
+inline std::vector<void*> CurrentStackTrace() {
+ return CurrentStackTraceImpl();
+}
+
+inline std::string SymbolizeStackTrace(absl::Span<void* const> stacktrace) {
+ return SymbolizeStackTraceImpl(stacktrace);
+}
+
// Returns a human-readable stack trace. Mostly used in error logging and
// related features.
inline std::string QuicheStackTrace() { return QuicheStackTraceImpl(); }
diff --git a/quiche/common/platform/api/quiche_stack_trace_test.cc b/quiche/common/platform/api/quiche_stack_trace_test.cc
index 74a4ca9..f99b709 100644
--- a/quiche/common/platform/api/quiche_stack_trace_test.cc
+++ b/quiche/common/platform/api/quiche_stack_trace_test.cc
@@ -28,6 +28,13 @@
return result;
}
+ABSL_ATTRIBUTE_NOINLINE std::string
+QuicheDesignatedTwoStepStackTraceTestFunction() {
+ std::string result = SymbolizeStackTrace(CurrentStackTrace());
+ ABSL_BLOCK_TAIL_CALL_OPTIMIZATION();
+ return result;
+}
+
TEST(QuicheStackTraceTest, GetStackTrace) {
if (!ShouldRunTest()) {
return;
@@ -38,6 +45,16 @@
testing::HasSubstr("QuicheDesignatedStackTraceTestFunction"));
}
+TEST(QuicheStackTraceTest, GetStackTraceInTwoSteps) {
+ if (!ShouldRunTest()) {
+ return;
+ }
+
+ std::string stacktrace = QuicheDesignatedTwoStepStackTraceTestFunction();
+ EXPECT_THAT(stacktrace, testing::HasSubstr(
+ "QuicheDesignatedTwoStepStackTraceTestFunction"));
+}
+
} // namespace
} // namespace test
} // namespace quiche
diff --git a/quiche/common/platform/default/quiche_platform_impl/quiche_stack_trace_impl.cc b/quiche/common/platform/default/quiche_platform_impl/quiche_stack_trace_impl.cc
index 9b0c969..b94da0c 100644
--- a/quiche/common/platform/default/quiche_platform_impl/quiche_stack_trace_impl.cc
+++ b/quiche/common/platform/default/quiche_platform_impl/quiche_stack_trace_impl.cc
@@ -4,6 +4,7 @@
#include "quiche_platform_impl/quiche_stack_trace_impl.h"
+#include <string>
#include <vector>
#include "absl/base/macros.h"
@@ -11,6 +12,7 @@
#include "absl/debugging/symbolize.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
+#include "absl/types/span.h"
namespace quiche {
@@ -20,15 +22,18 @@
constexpr absl::string_view kUnknownSymbol = "(unknown)";
} // namespace
-std::string QuicheStackTraceImpl() {
+std::vector<void*> CurrentStackTraceImpl() {
std::vector<void*> stacktrace(kMaxStackSize, nullptr);
int num_frames = absl::GetStackTrace(stacktrace.data(), stacktrace.size(),
/*skip_count=*/0);
if (num_frames <= 0) {
- return "";
+ return {};
}
stacktrace.resize(num_frames);
+ return stacktrace;
+}
+std::string SymbolizeStackTraceImpl(absl::Span<void* const> stacktrace) {
std::string formatted_trace = "Stack trace:\n";
for (void* function : stacktrace) {
char symbol_name[kMaxSymbolSize];
@@ -40,6 +45,10 @@
return formatted_trace;
}
+std::string QuicheStackTraceImpl() {
+ return SymbolizeStackTraceImpl(CurrentStackTraceImpl());
+}
+
bool QuicheShouldRunStackTraceTestImpl() {
void* unused[4]; // An arbitrary small number of stack frames to trace.
int stack_traces_found =
diff --git a/quiche/common/platform/default/quiche_platform_impl/quiche_stack_trace_impl.h b/quiche/common/platform/default/quiche_platform_impl/quiche_stack_trace_impl.h
index 5228e41..6fd2a38 100644
--- a/quiche/common/platform/default/quiche_platform_impl/quiche_stack_trace_impl.h
+++ b/quiche/common/platform/default/quiche_platform_impl/quiche_stack_trace_impl.h
@@ -6,9 +6,14 @@
#define QUICHE_COMMON_PLATFORM_DEFAULT_QUICHE_PLATFORM_IMPL_QUICHE_STACK_TRACE_IMPL_H_
#include <string>
+#include <vector>
+
+#include "absl/types/span.h"
namespace quiche {
+std::vector<void*> CurrentStackTraceImpl();
+std::string SymbolizeStackTraceImpl(absl::Span<void* const> stacktrace);
std::string QuicheStackTraceImpl();
bool QuicheShouldRunStackTraceTestImpl();