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();