folly/folly/debugging/symbolizer/test/StackTraceTest.cpp

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <cstring>

#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Task.h>
#include <folly/experimental/symbolizer/StackTrace.h>
#include <folly/experimental/symbolizer/Symbolizer.h>
#include <folly/lang/Hint.h>
#include <folly/test/TestUtils.h>
#include <folly/testing/TestUtil.h>

#include <boost/regex.hpp>

#include <glog/logging.h>

#include <folly/portability/GTest.h>

#if FOLLY_HAVE_ELF && FOLLY_HAVE_DWARF

using namespace folly;
using namespace folly::symbolizer;

FOLLY_NOINLINE void foo1();
FOLLY_NOINLINE void foo2();

void verifyStackTraces() {
  constexpr size_t kMaxAddresses = 100;
  FrameArray<kMaxAddresses> fa;
  CHECK(getStackTrace(fa));

  FrameArray<kMaxAddresses> faSafe;
  CHECK(getStackTraceSafe(faSafe));

  FrameArray<kMaxAddresses> faHeap;
  CHECK(getStackTraceHeap(faHeap));

  CHECK_EQ(fa.frameCount, faSafe.frameCount);
  CHECK_EQ(fa.frameCount, faHeap.frameCount);

  if (VLOG_IS_ON(1)) {
    Symbolizer symbolizer;
    OStreamSymbolizePrinter printer(std::cerr, SymbolizePrinter::COLOR_IF_TTY);

    symbolizer.symbolize(fa);
    VLOG(1) << "getStackTrace\n";
    printer.println(fa);

    symbolizer.symbolize(faSafe);
    VLOG(1) << "getStackTraceSafe\n";
    printer.println(faSafe);

    symbolizer.symbolize(faHeap);
    VLOG(1) << "getStackTraceHeap\n";
    printer.println(faHeap);
  }

  // Other than the top 2 frames (this one and getStackTrace /
  // getStackTraceSafe), the stack traces should be identical
  for (size_t i = 2; i < fa.frameCount; ++i) {
    LOG(INFO) << "i=" << i << " " << std::hex << "0x" << fa.addresses[i]
              << " 0x" << faSafe.addresses[i] << " 0x" << faHeap.addresses[i];
    EXPECT_EQ(fa.addresses[i], faSafe.addresses[i]);
    EXPECT_EQ(fa.addresses[i], faHeap.addresses[i]);
  }
}

void foo1() {
  foo2();
}

void foo2() {
  verifyStackTraces();
}

volatile bool handled = false;
void handler(int /* num */, siginfo_t* /* info */, void* /* ctx */) {
  // Yes, getStackTrace and VLOG aren't async-signal-safe, but signals
  // raised with raise() aren't "async" signals.
  foo1();
  handled = true;
}

TEST(StackTraceTest, Simple) {
  foo1();
}

TEST(StackTraceTest, Signal) {
  if (folly::kIsSanitizeThread) {
    // TSAN doesn't like signal-unsafe functions in a signal handler regardless
    // of how the signal is raised. So skip the test in that case.
    SKIP() << "Not supported for TSAN";
  }
  struct sigaction sa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_sigaction = handler;
  sa.sa_flags = SA_RESETHAND | SA_SIGINFO;
  CHECK_ERR(sigaction(SIGUSR1, &sa, nullptr));
  raise(SIGUSR1);
  EXPECT_TRUE(handled);
}

ssize_t read_all(int fd, uint8_t* buffer, size_t size) {
  uint8_t* pos = buffer;
  ssize_t bytes_read;
  do {
    bytes_read = read(fd, pos, size);
    if (bytes_read < 0) {
      if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
        continue;
      }
      return bytes_read;
    }

    pos += bytes_read;
    size -= bytes_read;
  } while (bytes_read > 0 && size > 0);

  return pos - buffer;
}

// Returns the position in the file after done reading.
off_t get_stack_trace(int fd, size_t file_pos, uint8_t* buffer, size_t count) {
  off_t rv = lseek(fd, file_pos, SEEK_SET);
  CHECK_EQ(rv, (off_t)file_pos);

  // Subtract 1 from size of buffer to hold nullptr.
  ssize_t bytes_read = read_all(fd, buffer, count - 1);
  CHECK_GT(bytes_read, 0);
  buffer[bytes_read] = '\0';
  return lseek(fd, 0, SEEK_CUR);
}

template <class StackTracePrinter>
void testStackTracePrinter(StackTracePrinter& printer, int fd) {
  ASSERT_GT(fd, 0);

  printer.printStackTrace(true);
  printer.flush();

  std::array<uint8_t, 4000> first;
  off_t pos = get_stack_trace(fd, 0, first.data(), first.size());
  ASSERT_GT(pos, 0);

  printer.printStackTrace(true);
  printer.flush();

  std::array<uint8_t, 4000> second;
  get_stack_trace(fd, pos, second.data(), second.size());

  // The first two lines refer to this stack frame, which is different in the
  // two cases, so strip those off.  The rest should be equal.
  ASSERT_STREQ(
      strchr(strchr((const char*)first.data(), '\n') + 1, '\n') + 1,
      strchr(strchr((const char*)second.data(), '\n') + 1, '\n') + 1);
}

TEST(StackTraceTest, SafeStackTracePrinter) {
  test::TemporaryFile file;

  SafeStackTracePrinter printer{file.fd()};

  testStackTracePrinter<SafeStackTracePrinter>(printer, file.fd());
}

TEST(StackTraceTest, FastStackTracePrinter) {
  test::TemporaryFile file;

  FastStackTracePrinter printer{
      std::make_unique<FDSymbolizePrinter>(file.fd())};

  testStackTracePrinter<FastStackTracePrinter>(printer, file.fd());
}

TEST(StackTraceTest, TerseStackTracePrinter) {
  test::TemporaryFile file;

  FastStackTracePrinter printer{
      std::make_unique<FDSymbolizePrinter>(file.fd(), SymbolizePrinter::TERSE)};

  testStackTracePrinter<FastStackTracePrinter>(printer, file.fd());
}

TEST(StackTraceTest, TerseFileAndLineStackTracePrinter) {
  test::TemporaryFile file;

  FastStackTracePrinter printer{std::make_unique<FDSymbolizePrinter>(
      file.fd(), SymbolizePrinter::TERSE_FILE_AND_LINE)};

  testStackTracePrinter<FastStackTracePrinter>(printer, file.fd());
}

namespace {
constexpr int frames = 5;
FOLLY_NOINLINE void foo(FrameArray<frames>& addresses) {
  getStackTraceSafe(addresses);
}

FOLLY_NOINLINE void bar(FrameArray<frames>& addresses) {
  foo(addresses);
}

FOLLY_NOINLINE void baz(FrameArray<frames>& addresses) {
  bar(addresses);
}
} // namespace

TEST(StackTraceTest, TerseFileAndLineStackTracePrinterOutput) {
  SKIP_IF(!Symbolizer::isAvailable());

  Symbolizer symbolizer(LocationInfoMode::FULL);
  FrameArray<frames> addresses;
  StringSymbolizePrinter printer(SymbolizePrinter::TERSE_FILE_AND_LINE);
  baz(addresses);
  symbolizer.symbolize(addresses);
  printer.println(addresses, 0);

  // Match a sequence of file+line results that should appear as:
  // ./folly/debugging/symbolizer/test/StackTraceTest.cpp:202
  // or:
  // (unknown)
  boost::regex regex("((([^:]*:[0-9]*)|(\\(unknown\\)))\n)+");
  auto match = boost::regex_match(
      printer.str(), regex, boost::regex_constants::match_not_dot_newline);

  ASSERT_TRUE(match);
}

namespace {
FOLLY_ALWAYS_INLINE void verifyAsyncStackTraces() {
  constexpr size_t kMaxAddresses = 100;
  FrameArray<kMaxAddresses> fa;
  CHECK(getAsyncStackTraceSafe(fa));

  CHECK_GT(fa.frameCount, 0);

  Symbolizer symbolizer;
  symbolizer.symbolize(fa);
  symbolizer::StringSymbolizePrinter printer;
  printer.println(fa);
  auto stackTraceStr = printer.str();

  if (VLOG_IS_ON(1)) {
    VLOG(1) << "getAsyncStackTrace\n" << stackTraceStr;
  }

  // These functions should appear in this relative order
  std::vector<std::string> funcNames{
      "funcF",
      "funcE",
      "co_funcD",
      "co_funcC",
      "funcB2_blocking",
      "funcB1",
      "co_funcB0",
      "co_funcA2",
      "co_funcA1",
      "co_funcA0",
  };
  std::vector<size_t> positions;
  for (const auto& funcName : funcNames) {
    SCOPED_TRACE(funcName);
    auto pos = stackTraceStr.find(funcName);
    ASSERT_NE(pos, std::string::npos);
    positions.push_back(pos);
  }
  for (size_t i = 0; i < positions.size() - 1; ++i) {
    ASSERT_LT(positions[i], positions[i + 1]);
  }
}

FOLLY_NOINLINE void funcF() {
  verifyAsyncStackTraces();
}

FOLLY_NOINLINE void funcE() {
  funcF();
  compiler_must_not_elide(0); // prevent tail-call above
}

FOLLY_NOINLINE folly::coro::Task<void> co_funcD() {
  funcE();
  co_return;
}

FOLLY_NOINLINE folly::coro::Task<void> co_funcC() {
  co_await co_funcD();
}

FOLLY_NOINLINE void funcB2_blocking() {
  // This should trigger a new AsyncStackRoot
  folly::coro::blockingWait(co_funcC());
}

FOLLY_NOINLINE void funcB1() {
  funcB2_blocking();
  compiler_must_not_elide(0); // prevent tail-call above
}

FOLLY_NOINLINE folly::coro::Task<void> co_funcB0() {
  funcB1();
  co_return;
}

FOLLY_NOINLINE folly::coro::Task<void> co_funcA2() {
  co_await co_funcB0();
}

FOLLY_NOINLINE folly::coro::Task<void> co_funcA1() {
  co_await co_funcA2();
}

FOLLY_NOINLINE folly::coro::Task<void> co_funcA0() {
  co_await co_funcA1();
}
} // namespace

TEST(StackTraceTest, AsyncStackTraceSimple) {
  folly::coro::blockingWait(co_funcA0());
}

#endif // FOLLY_HAVE_ELF && FOLLY_HAVE_DWARF