910e62b5创建于 1月15日历史提交
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/test/fuzzing/in_process_fuzzer.h"

#include <vector>

#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/test_timeouts.h"
#include "chrome/renderer/chrome_content_renderer_client.h"
#include "chrome/test/base/chrome_test_launcher.h"
#include "chrome/test/fuzzing/in_process_fuzzer_buildflags.h"
#include "content/public/app/content_main.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h"
#include "content/public/test/test_launcher.h"
#include "third_party/blink/public/web/web_testing_support.h"

#if BUILDFLAG(IS_WIN)
#include "base/strings/sys_string_conversions.h"
#endif  // BUILDFLAG(IS_WIN)

// This is provided within libfuzzer, and documented, but is not its headers.
extern "C" int LLVMFuzzerRunDriver(int* argc,
                                   char*** argv,
                                   int (*UserCb)(const uint8_t* Data,
                                                 size_t Size));

namespace {

std::string_view RunLoopTimeoutBehaviorToString(
    RunLoopTimeoutBehavior behavior) {
  switch (behavior) {
    case RunLoopTimeoutBehavior::kDefault:
      return "kDefault";
    case RunLoopTimeoutBehavior::kContinue:
      return "kContinue";
    case RunLoopTimeoutBehavior::kDeclareInfiniteLoop:
      return "kDeclareInfiniteLoop";
  }
  return "kUnknown";
}

void LogRunLoopTimeoutCallback(RunLoopTimeoutBehavior behavior) {
  LOG(INFO) << "Custom RunLoop timeout callback triggered ("
            << RunLoopTimeoutBehaviorToString(behavior) << ").";
}

}  // namespace

InProcessFuzzerFactoryBase* g_in_process_fuzzer_factory;

InProcessFuzzer::InProcessFuzzer(InProcessFuzzerOptions options)
    : options_(options) {}

InProcessFuzzer::~InProcessFuzzer() = default;

bool InProcessFuzzer::UseSingleProcessMode() {
  return true;
}

base::CommandLine::StringVector
InProcessFuzzer::GetChromiumCommandLineArguments() {
  base::CommandLine::StringVector empty;
  return empty;
}

void InProcessFuzzer::SetUp() {
  // Overrides the default 60s run loop timeout set by `BrowserTestBase`. See
  // https://source.chromium.org/chromium/chromium/src/+/main:content/public/test/browser_test_base.cc?q=ScopedRunLoopTimeout.
  // All of the fuzzing engines that we use are having timeouts features, and
  // this timeout can vary depending on the number of tested testcases. We must
  // let the engines handle timeouts, and set the maximum here.
  base::test::ScopedRunLoopTimeout scoped_timeout(FROM_HERE,
                                                  base::TimeDelta::Max());

  switch (options_.run_loop_timeout_behavior) {
    case RunLoopTimeoutBehavior::kContinue:
      KeepRunningOnTimeout();
      break;
    case RunLoopTimeoutBehavior::kDeclareInfiniteLoop:
      DeclareInfiniteLoopOnTimeout();
      break;
    case RunLoopTimeoutBehavior::kDefault:
      break;
  }

  // Note that browser tests are being launched by the `SetUp` method.
  InProcessBrowserTest::SetUp();
}

void InProcessFuzzer::KeepRunningOnTimeout() {
  base::test::ScopedRunLoopTimeout::SetTimeoutCallbackForTesting(
      std::make_unique<base::test::ScopedRunLoopTimeout::TimeoutCallback>(
          base::IgnoreArgs<const base::Location&,
                           base::RepeatingCallback<std::string()>,
                           const base::Location&>(base::BindRepeating(
              &LogRunLoopTimeoutCallback, RunLoopTimeoutBehavior::kContinue))));
}

void InProcessFuzzer::DeclareInfiniteLoopOnTimeout() {
  base::test::ScopedRunLoopTimeout::SetTimeoutCallbackForTesting(
      std::make_unique<base::test::ScopedRunLoopTimeout::TimeoutCallback>(
          base::IgnoreArgs<const base::Location&,
                           base::RepeatingCallback<std::string()>,
                           const base::Location&>(
              base::BindRepeating(&InProcessFuzzer::DeclareInfiniteLoop,
                                  base::Unretained(this)))
              .Then(base::BindRepeating(
                  &LogRunLoopTimeoutCallback,
                  RunLoopTimeoutBehavior::kDeclareInfiniteLoop))));
}

void InProcessFuzzer::Run(
    const std::vector<std::string>& libfuzzer_command_line) {
  libfuzzer_command_line_ = libfuzzer_command_line;
  SetUp();
  TearDown();
}

void InProcessFuzzer::SetUpOnMainThread() {
  InProcessBrowserTest::SetUpOnMainThread();

  // All of the engines that are being used to run those fuzzers are handling
  // process interruption. In case we let Chrome handle those signals itself,
  // we end up exiting the fuzzing process, and the engine records the last
  // run as a crash since it cannot not determine the reason why the process
  // terminated.
#if BUILDFLAG(IS_POSIX)
  signal(SIGTERM, SIG_DFL);
  signal(SIGINT, SIG_DFL);
#if defined(MEMORY_TOOL_REPLACES_ALLOCATOR)
  // In case we're being built with a memory tool (asan, msan...), we should
  // let it handle this signal so that we get better reporting.
  // As of now, since both in-process stack traces and the crashpad handler are
  // being disabled, this is the only signal that we need to reset since it's
  // being set in
  // https://source.chromium.org/chromium/chromium/src/+/main:content/public/test/browser_test_base.cc?q=SignalHandler
  signal(SIGSEGV, SIG_DFL);
#endif  // BUILDFLAG(MEMORY_TOOL_REPLACES_ALLOCATOR)
#endif  // BUILDFLAG(IS_POSIX)
}

bool InProcessFuzzer::InMergeMode() const {
  for (const auto& arg : libfuzzer_command_line_) {
    if (arg.starts_with("-merge")) {
      return true;
    }
  }
  return false;
}

InProcessFuzzer* g_test;

// The following three classes are only meant to inject `internals` into JS.
// This object is needed by our IPC based fuzzers, and could also be needed by
// other in the future.
class InternalsObjectFrameInjector : public content::RenderFrameObserver {
 public:
  explicit InternalsObjectFrameInjector(content::RenderFrame* render_frame)
      : content::RenderFrameObserver(render_frame) {}
  void DidClearWindowObject() override {
    blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
    blink::WebTestingSupport::InjectInternalsObject(frame);
  }
  void OnDestruct() override { delete this; }
};

class InternalsObjectRendererInjector : public ChromeContentRendererClient {
 public:
  void RenderFrameCreated(content::RenderFrame* render_frame) override {
    new InternalsObjectFrameInjector(render_frame);
  }
};

class FuzzerChromeMainDelegate : public ChromeTestChromeMainDelegate {
 public:
  FuzzerChromeMainDelegate() = default;
  content::ContentRendererClient* CreateContentRendererClient() override {
    return new InternalsObjectRendererInjector();
  }
};

class FuzzerTestLauncherDelegate : public content::TestLauncherDelegate {
 public:
  FuzzerTestLauncherDelegate(std::unique_ptr<InProcessFuzzer>&& fuzzer,
                             std::vector<std::string>&& libfuzzer_arguments)
      : fuzzer_(std::move(fuzzer)),
        libfuzzer_arguments_(std::move(libfuzzer_arguments)) {
  }

  int RunTestSuite(int argc, char** argv) override {
    fuzzer_->Run(libfuzzer_arguments_);
    return 0;
  }
#if !BUILDFLAG(IS_ANDROID)
  // Android browser tests set the ContentMainDelegate itself for the test
  // harness to use, and do not go through ContentMain() in TestLauncher.
  content::ContentMainDelegate* CreateContentMainDelegate() override {
    return new FuzzerChromeMainDelegate();
  }
#endif

 private:
  std::unique_ptr<InProcessFuzzer> fuzzer_;
  std::vector<std::string> libfuzzer_arguments_;
};

int fuzz_callback(const uint8_t* data, size_t size) {
  return g_test->DoFuzz(data, size);
}

void InProcessFuzzer::RunTestOnMainThread() {
  std::vector<char*> argv;
  for (const auto& arg : libfuzzer_command_line_) {
    argv.push_back((char*)arg.data());
  }
  argv.push_back(nullptr);
  int argc = argv.size() - 1;
  char** argv2 = argv.data();
  g_test = this;
  auto res = LLVMFuzzerRunDriver(&argc, &argv2, fuzz_callback);
  if (exit_after_fuzz_case_) {
    LOG(INFO) << "Early exit requested - exiting after LLVMFuzzerRunDriver.";
    exit(0);
  }
  // Performing exit here allows us to skip the browser shutdown phase thus
  // avoid potential hangs. This is the behaviour already observed in libfuzzer
  // which performs exits within LLVMFuzzerRunDriver. It is important to use the
  // LLVMFuzzerRunDriver return value for the exit value since this helps
  // centipede determine which input crashed when rerunning inputs one-by-one.
  exit(res);
}

int InProcessFuzzer::DoFuzz(const uint8_t* data, size_t size) {
  // We actually exit before running the *next* fuzz case to give an opportunity
  // to return the return value to the fuzzing engine.
  if (exit_after_fuzz_case_) {
    LOG(INFO) << "Early exit requested - exiting after fuzz case.";
    exit(0);
  }
  std::optional<base::test::ScopedRunLoopTimeout> scoped_timeout;
  if (options_.run_loop_timeout) {
    scoped_timeout.emplace(FROM_HERE, *options_.run_loop_timeout);
  }
  return Fuzz(data, size);
}

/// Used only for child processes (e.g. renderers etc.)
class ChildProcessTestLauncherDelegate : public content::TestLauncherDelegate {
 public:
  ChildProcessTestLauncherDelegate() = default;
  int RunTestSuite(int argc, char** argv) override {
    LOG(FATAL) << "Trying to run tests in child";
  }
#if !BUILDFLAG(IS_ANDROID)
  // Android browser tests set the ContentMainDelegate itself for the test
  // harness to use, and do not go through ContentMain() in TestLauncher.
  content::ContentMainDelegate* CreateContentMainDelegate() override {
    return new FuzzerChromeMainDelegate();
  }
#endif
};

// Main function for running in process fuzz tests.
// This aims to replicate //chrome browser tests as much as possible; we want
// the whole browser environment to be available for this sort of test in as
// realistic a fashion as possible.
int main(int argc, char** argv) {
  base::AtExitManager atexit_manager;
  base::CommandLine::Init(argc, argv);

  // Oh dear, you've got to the part of the code relating to command lines.
  // I'm sorry.
  // Here are our constraints:
  // * Both libfuzzer/centipede and Chromium expect a full command line
  // * We set the format of neither command line
  // * Chromium will launch other Chromium processes, giving them a command
  // line.
  // * The centipede runner will launch our fuzzer, giving it a command line.
  // So, at this point, we have to figure out heuristics for what's up.
  // Are we the original fuzzer process, in which case we pass the CLI to
  // libfuzzer/centipede, and ask for a suitable Chromium command line from
  // our fuzz test? Or, are we a child Chromium process which has been
  // launched from a previous Chromium process? Well, dear reader, there are
  // no telltail arguments guaranteed to be on either, so we're going to
  // use a heuristic. If the first argument starts with --, we're assuming
  // we're a Chromium child.

  if (base::CommandLine::ForCurrentProcess()->argv().size() > 1) {
    if (base::StartsWith(base::CommandLine::ForCurrentProcess()->argv()[1],
                         FILE_PATH_LITERAL("--"))) {
      // If we're a Chromium child, we don't alter the command-line,
      // and in fact the libfuzzer code will never run, so we don't need to
      // pass any arguments through to libfuzzer.
      // Ensure we don't create the InProcessFuzzer in this branch as it
      // initializes unwelcome things in its base class constructors.
      ChildProcessTestLauncherDelegate delegate;
      return LaunchChromeTests(1, &delegate, argc, argv);
    }
  }

  // We are the outermost process, let's run the fuzzer.
  std::unique_ptr<InProcessFuzzer> fuzzer =
      g_in_process_fuzzer_factory->CreateInProcessFuzzer();
#if BUILDFLAG(IS_WIN)
  // Convert std::wstring (Windows command lines) to std::string
  // (as needed by libfuzzer).
  std::vector<std::string> libfuzzer_arguments;
  auto wide_argv = base::CommandLine::ForCurrentProcess()->argv();
  for (auto arg : wide_argv) {
    libfuzzer_arguments.push_back(base::SysWideToUTF8(arg));
  }
#else
  std::vector<std::string> libfuzzer_arguments =
      base::CommandLine::ForCurrentProcess()->argv();
#endif  // BUILDFLAG(IS_WIN)
  base::CommandLine::StringType executable_name =
      base::CommandLine::ForCurrentProcess()->argv().at(0);
  base::CommandLine::StringVector chromium_arguments =
      fuzzer->GetChromiumCommandLineArguments();
  chromium_arguments.insert(chromium_arguments.begin(), executable_name);
  chromium_arguments.push_back(FILE_PATH_LITERAL("--single-process-tests"));
  if (fuzzer->UseSingleProcessMode()) {
    chromium_arguments.push_back(FILE_PATH_LITERAL("--single-process"));
    chromium_arguments.push_back(
        FILE_PATH_LITERAL("--disable-crashpad-for-testing"));
  } else {
#if BUILDFLAG(IS_CENTIPEDE)
    // Static destructors in centipede runner code will open
    // shared memory handles if they find the runner flags lurking in this
    // environment variable. If child processes do this, they'll
    // unfortunately clobber valid information from the main process
    // that it's trying to pass back to the external centipede executor.
    // Fortunately, this variable is only read during centipede's
    // static initializers, so we can now safely clear it in our parent
    // (browser) process in order to ensure it's unset for all children.
    // TODO(crbug.com/383356867) - stop depending on centipede internals
    // like this
    unsetenv("CENTIPEDE_RUNNER_FLAGS");
#endif
  }
  chromium_arguments.push_back(FILE_PATH_LITERAL("--no-zygote"));
  chromium_arguments.push_back(FILE_PATH_LITERAL("--no-sandbox"));
  chromium_arguments.push_back(FILE_PATH_LITERAL("--disable-gpu"));
  chromium_arguments.push_back(
      FILE_PATH_LITERAL("--enable-unsafe-swiftshader"));
#if defined(MEMORY_TOOL_REPLACES_ALLOCATOR)
  // We disable in-process stack trace handling in case we're using memory
  // tools so that we get better reporting on what happened in case of
  // SIGSEGV.
  chromium_arguments.push_back(
      FILE_PATH_LITERAL("--disable-in-process-stack-traces"));
#endif
  base::CommandLine::ForCurrentProcess()->InitFromArgv(chromium_arguments);

  // Various bits of setup are done by base::TestSuite::Initialize.
  // As we're not a functional test suite, most of those things are not
  // necessary, but at least this is:
  TestTimeouts::Initialize();

  FuzzerTestLauncherDelegate fuzzer_launcher_delegate(
      std::move(fuzzer), std::move(libfuzzer_arguments));
  return LaunchChromeTests(1, &fuzzer_launcher_delegate, argc, argv);
}