// 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();
//
// (2a) Optionally, annotate the LifetimeTrackable with information that could
//      be useful in the event of an unexpected dead object. NOTE: each
//      annotation results in a std::string allocation.
//
//     ptr->trackable.Annotate(absl::StrCat(connection_id, " socket closed"));
//
// (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 <memory>
#include <optional>
#include <utility>
#include <vector>

#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "quiche/common/platform/api/quiche_export.h"
#include "quiche/common/platform/api/quiche_logging.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;

  // Annotations to the LifetimeTrackable during its lifetime.
  std::vector<std::string> annotations;
};

// 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%s%s",
                   SymbolizeStackTrace(*tracker.info_->destructor_stack),
                   tracker.info_->annotations.empty() ? "" : "\nannotations:\n",
                   tracker.info_->annotations.empty()
                       ? ""
                       : absl::StrJoin(tracker.info_->annotations, "\n"));
    } else {
      absl::Format(&sink, "Tracked object is alive.");
    }
  }

 private:
  friend class LifetimeTrackable;
  explicit LifetimeTracker(std::shared_ptr<const LifetimeInfo> info)
      : info_(std::move(info)) {
    QUICHE_CHECK(info_ != nullptr)
        << "Passed a null info pointer into the lifetime tracker";
  }
  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_);
  }

  void Annotate(std::string annotation) {
    if (info_ != nullptr) {
      info_->annotations.push_back(std::move(annotation));
    }
  }

  bool IsTracked() const { return info_ != nullptr; }

 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_
