// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/debug/asan_service.h"

#include "base/compiler_specific.h"

#if defined(ADDRESS_SANITIZER)
#include <sanitizer/asan_interface.h>

#include "base/compiler_specific.h"
#include "base/containers/flat_map.h"
#include "base/debug/task_trace.h"
#include "base/environment.h"
#include "base/no_destructor.h"
#include "base/process/process.h"
#include "base/process/process_handle.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringize_macros.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"

#if BUILDFLAG(IS_WIN)
#include "base/logging.h"
#include "base/win/windows_types.h"
#endif  // BUILDFLAG(IS_WIN)

#if defined(COMPONENT_BUILD) && BUILDFLAG(IS_WIN)
// In component builds on Windows, weak function exported by ASan have the
// `__dll` suffix. ASan itself uses the `alternatename` directive to account for
// that.
#pragma comment(linker,                                            \
                "/alternatename:__sanitizer_report_error_summary=" \
                "__sanitizer_report_error_summary__dll")
#pragma comment(linker,                                     \
                "/alternatename:__sanitizer_set_report_fd=" \
                "__sanitizer_set_report_fd__dll")
#endif  // defined(COMPONENT_BUILD) && BUILDFLAG(IS_WIN)

namespace base {
namespace debug {

namespace {
NO_SANITIZE("address")
void TaskTraceErrorCallback(const char* reason,
                            bool* should_exit_cleanly,
                            bool* should_abort) {
  // Use the sanitizer api to symbolize the task trace, which otherwise might
  // not symbolize properly. This also lets us format the task trace in the
  // same way as the address sanitizer backtraces, which also means that we can
  // get the stack trace symbolized with asan_symbolize.py in the cases where
  // symbolization at runtime fails.
  std::array<const void*, 4> addresses;
  size_t address_count = TaskTrace().GetAddresses(addresses);

  AsanService::GetInstance()->Log("Task trace:");
  size_t frame_index = 0;
  for (size_t i = 0; i < std::min(address_count, addresses.size()); ++i) {
    char buffer[4096] = {};
    void* address = const_cast<void*>(addresses[i]);
    __sanitizer_symbolize_pc(address, "%p %F %L", buffer, sizeof(buffer));
    for (const char* ptr = buffer; *ptr != 0;
         UNSAFE_TODO(ptr += strlen(ptr) + 1)) {
      AsanService::GetInstance()->Log("    #%i %s", frame_index++, ptr);
    }
  }
  AsanService::GetInstance()->Log("");
}

constexpr std::string_view kKeyValuePairDelimiters = " ,:\n\t\r";

NO_SANITIZE("address")
void ParseAsanFlagFromString(std::string_view input,
                             flat_map<std::string, std::string>& flags) {
  for (std::string_view token : base::SplitStringPiece(
           input, kKeyValuePairDelimiters, base::TRIM_WHITESPACE,
           base::SPLIT_WANT_NONEMPTY)) {
    if (auto pair = base::SplitStringOnce(token, '=')) {
      flags[std::string(pair->first)] = std::string(pair->second);
    }
  }
}

// Asan doesn't provide its current flags. We will emulate almost the same way
// as Asan initializes its flags.
NO_SANITIZE("address")
void ParseAsanFlags(flat_map<std::string, std::string>& flags) {
  // Asan default value.

  // Asan compile definition: ASAN_DEFAULT_OPTIONS.
#if defined(ASAN_DEFAULT_OPTIONS)
  ParseAsanFlagFromString(STRINGIZE(ASAN_DEFAULT_OPTIONS), flags);
#endif
  // Override from user-specified string.
  ParseAsanFlagFromString(__asan_default_options(), flags);

  // Override from command line: ASAN_OPTIONS.
  std::unique_ptr<Environment> env(Environment::Create());
  std::optional<std::string> asan_options = env->GetVar("ASAN_OPTIONS");
  if (asan_options.has_value()) {
    ParseAsanFlagFromString(asan_options.value(), flags);
  }
}

int GetAsanIntFlag(const flat_map<std::string, std::string>& flags,
                   std::string_view flag_name,
                   int default_value) {
  const auto& it = flags.find(flag_name);
  if (it == flags.cend()) {
    return default_value;
  }
  int value;
  if (StringToInt(it->second, &value)) {
    return value;
  }
  return default_value;
}

bool GetAsanBoolFlag(const flat_map<std::string, std::string>& flags,
                     std::string_view flag_name,
                     bool default_value) {
  const auto& it = flags.find(flag_name);
  if (it == flags.cend()) {
    return default_value;
  }
  if (it->second == "true" || it->second == "1") {
    return true;
  }
  if (it->second == "false" || it->second == "0") {
    return false;
  }
  return default_value;
}

}  // namespace

// static
NO_SANITIZE("address")
AsanService* AsanService::GetInstance() {
  static NoDestructor<AsanService> instance;
  return instance.get();
}

void AsanService::Initialize() {
  AutoLock lock(lock_);
  if (!is_initialized_) {
#if BUILDFLAG(IS_WIN)
    if (logging::IsLoggingToFileEnabled()) {
      // Sandboxed processes cannot open files but are provided a HANDLE.
      HANDLE log_handle = logging::DuplicateLogFileHandle();
      if (log_handle) {
        // Sanitizer APIs need a HANDLE cast to void*.
        __sanitizer_set_report_fd(reinterpret_cast<void*>(log_handle));
      }
    }
#endif  // BUILDFLAG(IS_WIN)
    __asan_set_error_report_callback(ErrorReportCallback);
    error_callbacks_.push_back(TaskTraceErrorCallback);

    // Default values as per
    // https://github.com/google/sanitizers/wiki/SanitizerCommonFlags
    flat_map<std::string, std::string> flags;
    ParseAsanFlags(flags);
    halt_on_error_ =
        GetAsanBoolFlag(flags, "halt_on_error", /*default value=*/true);
    detect_leak_ = GetAsanBoolFlag(flags, "detect_leaks",
#if BUILDFLAG(IS_APPLE)
                                   /*default value=*/false
#else
                                   /*default value=*/true
#endif
    );
    exitcode_ = GetAsanIntFlag(flags, "exitcode", /*default value=*/1);
    is_initialized_ = true;
  }
}

NO_SANITIZE("address")
void AsanService::Log(const char* format, ...) {
  va_list ap;
  va_start(ap, format);
  auto formatted_message = UNSAFE_TODO(StringPrintV(format, ap));
  va_end(ap);

  // Despite its name, the function just prints the input to the destination
  // configured by ASan.
  __sanitizer_report_error_summary(formatted_message.c_str());
}

void AsanService::Abort() {
  Process::TerminateCurrentProcessImmediately(exitcode_);
}

void AsanService::AddErrorCallback(ErrorCallback error_callback) {
  AutoLock lock(lock_);
  CHECK(is_initialized_);
  error_callbacks_.push_back(error_callback);
}

void AsanService::ResetErrorCallbacksForTesting() {
  AutoLock lock(lock_);
  error_callbacks_.clear();
}

NO_SANITIZE("address")
void AsanService::RunErrorCallbacks(const char* reason) {
  ProcessId process_id = GetCurrentProcId();
  bool should_exit_cleanly = false;
  bool should_abort = true;

  // We can hold `lock_` throughout the error callbacks, since ASan doesn't
  // re-enter when handling nested errors on the same thread.
  AutoLock lock(lock_);

  Log("\n==%i==ADDITIONAL INFO", (int)process_id);
  Log("\n==%i==Note: Please include this section with the ASan report.",
      (int)process_id);
  for (const auto& error_callback : error_callbacks_) {
    error_callback(reason, &should_exit_cleanly, &should_abort);
  }
  Log("\n==%i==END OF ADDITIONAL INFO", (int)process_id);

  if (should_exit_cleanly) {
    Log("\n==%i==EXITING", (int)process_id);
    Process::TerminateCurrentProcessImmediately(0);
  } else if (should_abort) {
    Log("\n==%i==ABORTING", (int)process_id);
    Abort();
  } else if (halt_on_error()) {
    Log("AsanService ErrorCallback has cleared should_abort, but ASAN_OPTIONS "
        "does not contain halt_on_error=0, so AddressSanitizer will abort!");
  }
}

// static
NO_SANITIZE("address")
void AsanService::ErrorReportCallback(const char* reason) {
  AsanService::GetInstance()->RunErrorCallbacks(reason);
}

AsanService::AsanService() = default;

}  // namespace debug
}  // namespace base

#endif  // defined(ADDRESS_SANITIZER)
