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

// A wrapper which knows to execute a given fuzzer within a fuzztest
// executable that contains multiple fuzzers.
// The fuzzer binary is assumed to be in the same directory as this binary.

#include <cerrno>
#include <iostream>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_ostream_operators.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "testing/libfuzzer/buildflags.h"

#if BUILDFLAG(IS_POSIX)
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "base/posix/eintr_wrapper.h"
#include "base/posix/safe_strerror.h"
#endif

extern const char* kFuzzerBinary;
extern const char* kFuzzerArgs;

namespace {

constexpr int kErrorExitCode = -1;  // equal to 255

#if BUILDFLAG(USE_CENTIPEDE)

void HandleReplayMode(auto& args) {
  // We're handling a centipede based fuzzer. If the last argument is a
  // filepath, we're trying to replay a testcase, since it doesn't make sense
  // to get a filepath when running with the centipede binary.
  if (args.size() <= 1) {
    return;
  }
  base::FilePath test_case(args.back());
  if (!base::PathExists(test_case)) {
    return;
  }

  auto env = base::Environment::Create();
#if BUILDFLAG(IS_WIN)
  auto env_value = base::WideToUTF8(args.back());
#else
  auto env_value = args.back();
#endif
  env->SetVar("FUZZTEST_REPLAY", env_value);
  env->UnSetVar("CENTIPEDE_RUNNER_FLAGS");
  std::cerr << "FuzzTest wrapper setting env var: FUZZTEST_REPLAY="
            << args.back() << '\n';

  // We must not add the testcase to the command line, as this will not be
  // parsed correctly by centipede.
  args.pop_back();
}

#endif  // BUILDFLAG(USE_CENTIPEDE)

base::ProcessId g_child_pid = 0;

// Setup signal handlers so we can propagate signals, i.e. SIGTERM and SIGINT,
// to the underlying fuzzer.
void SetupSignalHandlers() {
#if BUILDFLAG(IS_POSIX)
  auto* signal_handler = +[](int signum) {
    if (g_child_pid > 0) {
      kill(g_child_pid, signum);
      int status;
      HANDLE_EINTR(waitpid(g_child_pid, &status, /*options=*/0));
    }

    struct sigaction action;
    action.sa_handler = SIG_DFL;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;

    sigaction(signum, &action, /*oldact=*/nullptr);
    kill(getpid(), signum);
  };

  struct sigaction action;
  action.sa_handler = signal_handler;
  sigemptyset(&action.sa_mask);
  action.sa_flags = 0;

  sigaction(SIGTERM, &action, /*oldact=*/nullptr);
  sigaction(SIGINT, &action, /*oldact=*/nullptr);
#endif
}

}  // namespace

int main(int argc, const char* const* argv) {
  SetupSignalHandlers();
  base::CommandLine::Init(argc, argv);
  base::FilePath fuzzer_path;
  if (!base::PathService::Get(base::DIR_EXE, &fuzzer_path)) {
    std::cerr << "[FuzzTest Wrapper] ERROR: Failed to obtain base::DIR_EXE via "
                 "PathService.\n";
    return kErrorExitCode;
  }
  fuzzer_path = fuzzer_path.AppendASCII(kFuzzerBinary);
  base::LaunchOptions launch_options;
  base::CommandLine cmdline(fuzzer_path);
  std::vector<std::string_view> additional_args = base::SplitStringPiece(
      kFuzzerArgs, " ", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  for (auto arg : additional_args) {
    cmdline.AppendArg(arg);
  }
  auto args = base::CommandLine::ForCurrentProcess()->argv();
#if BUILDFLAG(USE_CENTIPEDE)
  HandleReplayMode(args);
#endif  // BUILDFLAG(USE_CENTIPEDE)

  bool skipped_first = false;
  for (auto arg : args) {
    if (!skipped_first) {
      skipped_first = true;
      continue;
    }
    // We avoid AppendArguments because it parses switches then reorders things.
    cmdline.AppendArgNative(arg);
  }
  // Pre-flight diagnostic checks before launching the underlying test binary.
  if (!base::PathExists(fuzzer_path)) {
    std::cerr
        << "[FuzzTest Wrapper] ERROR: Target fuzzer binary not found at path: "
        << fuzzer_path.value() << "\n"
        << "[FuzzTest Wrapper] Please ensure that '" << kFuzzerBinary
        << "' is listed in this wrapper's .runtime_deps and was unpacked "
           "correctly by the bot.\n";
    return kErrorExitCode;
  }

#if BUILDFLAG(IS_POSIX)
  if (access(fuzzer_path.value().c_str(), X_OK) != 0) {
    int access_err = errno;
    std::cerr << "[FuzzTest Wrapper] ERROR: Target fuzzer binary exists at "
              << fuzzer_path.value() << " but is not executable (errno "
              << access_err << ": " << base::safe_strerror(access_err)
              << ").\n";
    return kErrorExitCode;
  }
#endif

  std::cerr << "FuzzTest wrapper launching:" << cmdline.GetCommandLineString()
            << "\n";
  base::Process p = base::LaunchProcess(cmdline, launch_options);
  if (!p.IsValid()) {
    std::cerr
        << "[FuzzTest Wrapper] ERROR: base::LaunchProcess failed to launch: "
        << fuzzer_path.value() << "\n"
        << "[FuzzTest Wrapper] Check system logs or execvp/CreateProcess "
           "errors above.\n";
    return kErrorExitCode;
  }
  g_child_pid = p.Pid();
  int exit_code;
  if (!p.WaitForExit(&exit_code)) {
    std::cerr
        << "[FuzzTest Wrapper] ERROR: WaitForExit failed on child process.\n";
    return kErrorExitCode;
  }
  if (exit_code != 0) {
    std::cerr << "[FuzzTest Wrapper] NOTICE: Underlying fuzzer binary exited "
                 "with non-zero code "
              << exit_code << "\n";
  }
  return exit_code;
}

#if defined(WIN32)
#define ALWAYS_EXPORT __declspec(dllexport)
#else
#define ALWAYS_EXPORT __attribute__((visibility("default")))
#endif

ALWAYS_EXPORT extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data,
                                                    size_t size) {
  // No-op. This symbol exists to ensure that this binary is detected as
  // a fuzzer by ClusterFuzz's heuristics. It never actually gets called.
  return -1;
}
