From 478c3a323ce39e7efe50bbca73ea3784779c2a22 Mon Sep 17 00:00:00 2001 From: Xinhao Yuan Date: Tue, 4 Aug 2026 16:44:40 -0700 Subject: [PATCH] No public description PiperOrigin-RevId: 959298250 --- .github/workflows/bazel_test_centipede.yml | 47 +++- BUILD | 8 + MODULE.bazel | 4 + centipede/BUILD | 17 +- centipede/command.cc | 250 +++++++++++++++++- centipede/command.h | 12 + centipede/command_test.cc | 152 ++++++++--- centipede/command_test_helper.cc | 70 ++++- centipede/puzzles/BUILD | 1 - centipede/puzzles/autodictionary_stress.cc | 7 +- centipede/puzzles/byte_cmp_4.cc | 2 +- centipede/puzzles/callstack.cc | 8 +- centipede/puzzles/deep_recursion.cc | 10 +- centipede/puzzles/independent_compares.cc | 2 +- centipede/puzzles/memcmp_3.cc | 7 +- centipede/puzzles/memcmp_4.cc | 4 +- centipede/puzzles/memcmp_4_may_inline.cc | 3 +- centipede/puzzles/oom.cc | 4 +- centipede/puzzles/paths.cc | 6 +- centipede/puzzles/per_batch_timeout.cc | 8 +- centipede/puzzles/per_input_timeout.cc | 5 +- .../puzzles/pthread_exit_uint32_cmp_1.cc | 9 +- centipede/puzzles/puzzle.bzl | 18 +- centipede/puzzles/run_puzzle.cc | 179 +++++++++++++ centipede/puzzles/run_puzzle.sh | 122 --------- centipede/puzzles/strcasecmp.cc | 7 +- centipede/puzzles/strcmp.cc | 7 +- centipede/puzzles/strncmp.cc | 7 +- centipede/puzzles/switch.cc | 4 +- centipede/puzzles/thread_uint32_cmp_1.cc | 8 +- centipede/puzzles/uint32_cmp_1.cc | 8 +- centipede/symbol_table.cc | 10 +- centipede/util.cc | 90 ++++++- centipede/util_test.cc | 20 +- common/test_util.cc | 31 ++- common/test_util.h | 2 +- 36 files changed, 886 insertions(+), 263 deletions(-) create mode 100644 BUILD create mode 100644 centipede/puzzles/run_puzzle.cc delete mode 100755 centipede/puzzles/run_puzzle.sh diff --git a/.github/workflows/bazel_test_centipede.yml b/.github/workflows/bazel_test_centipede.yml index 1bbcba0d4..ce547afdf 100644 --- a/.github/workflows/bazel_test_centipede.yml +++ b/.github/workflows/bazel_test_centipede.yml @@ -30,6 +30,8 @@ jobs: # TODO(xinhaoyuan): Bump to 24.04 after https://github.com/llvm/llvm-project/issues/102443 # is fixed. runs-on: ubuntu-22.04 + permissions: + contents: read timeout-minutes: 60 strategy: matrix: @@ -39,13 +41,13 @@ jobs: run: | sudo sysctl -w kernel.core_pattern="" - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install dependencies run: | sudo apt-get update && sudo apt-get install -yq \ clang llvm libssl-dev - name: Restore latest cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 with: path: "~/.cache/bazel" key: bazel-centipede-cache-${{ matrix.config }} @@ -84,19 +86,21 @@ jobs: bazel test --no//fuzztest:use_riegeli --test_output=errors --linkopt=-fsanitize=address --copt=-fsanitize=address --test_env=ASAN_OPTIONS=detect_leaks=0 --platform_suffix=asan --test_timeout=600 centipede/puzzles:all - name: Save new cache based on main if: github.ref == 'refs/heads/main' - uses: actions/cache/save@v4 + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 with: path: "~/.cache/bazel" key: bazel-centipede-cache-${{ matrix.config }}-${{ github.run_id }} run_tests_mac: name: Run Centipede tests (MacOS) runs-on: macos-15 + permissions: + contents: read timeout-minutes: 60 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Restore latest cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 with: path: "~/.cache/bazel" key: bazel-centipede-cache-mac @@ -127,7 +131,38 @@ jobs: bazel --output_user_root="${HOME}/.cache/bazel" test --test_output=errors --no//fuzztest:use_riegeli --linkopt=-fsanitize=address --copt=-fsanitize=address --test_env=ASAN_OPTIONS=detect_leaks=0 --platform_suffix=asan --test_timeout=600 centipede/puzzles:all - name: Save new cache based on main if: github.ref == 'refs/heads/main' - uses: actions/cache/save@v4 + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 with: path: "~/.cache/bazel" key: bazel-centipede-cache-mac-${{ github.run_id }} + run_tests_win: + name: Run Centipede tests (Windows) + runs-on: windows-latest + permissions: + contents: read + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Restore latest cache + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + with: + path: "~/.cache/bazel" + key: bazel-centipede-cache-win-${{ matrix.config }} + restore-keys: bazel-centipede-cache-win-${{ matrix.config }}- + - name: Set environment variable + run: echo "USE_BAZEL_VERSION=8.7.0" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Run unit tests + if: ${{ !cancelled() }} + run: | + <# Only supported libraries are tested here. #> ` + bazelisk test --disk_cache=~/.cache/bazel --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli ` + --extra_toolchains=@local_config_cc//:cc-toolchain-x64_windows-clang-cl ` + --extra_execution_platforms=//:x64_windows-clang-cl --enable_runfiles ` + -- centipede:util_test centipede:command_test + - name: Save new cache based on main + # if: github.ref == 'refs/heads/main' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + with: + path: "~/.cache/bazel" + key: bazel-centipede-cache-win-${{ github.run_id }} diff --git a/BUILD b/BUILD new file mode 100644 index 000000000..4f91cb387 --- /dev/null +++ b/BUILD @@ -0,0 +1,8 @@ +platform( + name = "x64_windows-clang-cl", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:windows", + "@bazel_tools//tools/cpp:clang-cl", + ], +) diff --git a/MODULE.bazel b/MODULE.bazel index 4610482c3..741fdab3b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -23,6 +23,10 @@ bazel_dep( name = "rules_cc", version = "0.2.17", ) + +cc_configure = use_extension("@rules_cc//cc:extensions.bzl", "cc_configure_extension") +use_repo(cc_configure, "local_config_cc") + bazel_dep( name = "rules_shell", version = "0.6.1", diff --git a/centipede/BUILD b/centipede/BUILD index 3ad6faeea..78a2ea2a3 100644 --- a/centipede/BUILD +++ b/centipede/BUILD @@ -598,6 +598,8 @@ cc_library( ":stop", ":util", "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/hash", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", @@ -605,6 +607,7 @@ cc_library( "@abseil-cpp//absl/synchronization", "@abseil-cpp//absl/time", "@com_google_fuzztest//common:logging", + "@com_google_fuzztest//fuzztest/internal:escaping", ], ) @@ -1413,7 +1416,12 @@ cc_test( cc_test( name = "util_test", srcs = ["util_test.cc"], - copts = ["-fno-signed-char"], + copts = select({ + "@platforms//os:windows": [ + "/J", + ], + "//conditions:default": ["-fno-signed-char"], + }), deps = [ ":feature", ":thread_pool", @@ -1740,10 +1748,13 @@ cc_binary( name = "command_test_helper", srcs = ["command_test_helper.cc"], deps = [ - ":runner_fork_server", "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/strings", "@abseil-cpp//absl/time", - ], + ] + select({ + "@platforms//os:windows": [], + "//conditions:default": [":runner_fork_server"], + }), ) cc_test( diff --git a/centipede/command.cc b/centipede/command.cc index 972a05627..a5c7b1696 100644 --- a/centipede/command.cc +++ b/centipede/command.cc @@ -14,6 +14,7 @@ #include "./centipede/command.h" +#if !defined(_WIN32) #include #include #include @@ -22,6 +23,7 @@ #include #include #include +#endif #ifdef __APPLE__ #include @@ -43,8 +45,11 @@ #include #include "absl/base/const_init.h" +#include "absl/container/flat_hash_map.h" +#include "absl/hash/hash.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/strings/ascii.h" // NOLINT #include "absl/strings/match.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" @@ -58,6 +63,7 @@ #include "./centipede/stop.h" #include "./centipede/util.h" #include "./common/logging.h" +#include "./fuzztest/internal/escaping.h" #if !defined(_MSC_VER) // Needed to pass the current environment to posix_spawn, which needs an @@ -72,8 +78,10 @@ namespace { constexpr std::string_view kCommandLineSeparator(" \\\n"); constexpr std::string_view kNoForkServerRequestPrefix("%f"); +#if defined(_WIN32) +#else absl::StatusOr GetProcessCreationStamp(pid_t pid) { -#ifdef __APPLE__ +#if defined(__APPLE__) struct proc_bsdinfo info = {}; if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, PROC_PIDTBSDINFO_SIZE) != PROC_PIDTBSDINFO_SIZE) { @@ -112,6 +120,7 @@ absl::StatusOr GetProcessCreationStamp(pid_t pid) { return std::string(fields[kFieldIndexOfStartTimeAfterComm]); #endif } +#endif std::string GetUniqueSuffix() { static std::atomic suffix_counter = {0}; @@ -122,6 +131,9 @@ std::string GetUniqueSuffix() { // TODO(ussuri): Encapsulate as much of the fork server functionality from // this source as possible in this struct, and make it a class. +#if defined(_WIN32) +struct Command::ForkServerProps {}; +#else struct Command::ForkServerProps { // The file paths of the comms pipes. std::string fifo_path_[2]; @@ -176,6 +188,7 @@ struct Command::ForkServerProps { } } }; +#endif // NOTE: Because std::unique_ptr requires T to be a complete type wherever // the deleter is instantiated, the special member functions must be defined @@ -185,9 +198,13 @@ Command::~Command() { if (is_executing()) { FUZZTEST_LOG(WARNING) << "Destructing Command object for " << path() << " with " +#if defined(_WIN32) + << GetProcessId(win_process_handle_) +#else << (fork_server_ ? absl::StrCat("fork server PID ", fork_server_->pid_) : absl::StrCat("PID ", pid_)) +#endif << " still running. Requesting it to force-stop " "without waiting for it..."; RequestStop(/*force=*/true); @@ -201,6 +218,41 @@ Command::Command(std::string_view path, Options options) Command::Command(std::string_view path) : Command{path, {}} {} std::string Command::ToString() const { +#if defined(_WIN32) + std::string path = path_; + if (absl::StartsWith(path, kNoForkServerRequestPrefix)) { + path = path.substr(kNoForkServerRequestPrefix.size()); + } + constexpr std::string_view kTempFileWildCard = "@@"; + if (absl::StrContains(path, kTempFileWildCard)) { + FUZZTEST_CHECK(!options_.temp_file_path.empty()); + std::string temp_file = options_.temp_file_path; + path = absl::StrReplaceAll(path, {{kTempFileWildCard, temp_file}}); + } + std::string binary_cmd = path; + auto Escape = [](std::string s) { + std::string r = "\""; + size_t num_bs = 0; + for (size_t i = 0; i < s.size(); ++i) { + if (s[i] == '"') { + r.append(std::string(num_bs + 1, '\\')); + num_bs = 0; + } else if (s[i] == '\\') { + ++num_bs; + } else { + num_bs = 0; + } + r += s[i]; + } + r.append(num_bs, '\\'); + r += '"'; + return r; + }; + for (const auto& arg : options_.args) { + absl::StrAppend(&binary_cmd, " ", Escape(arg)); + } + return binary_cmd; +#else std::vector ss; ss.reserve(/*env*/ 1 + options_.env_diff.size() + /*path*/ 1 + /*args*/ options_.args.size() + /*out/err*/ 2); @@ -217,7 +269,7 @@ std::string Command::ToString() const { } } for (auto& var : env_to_set) { - ss.push_back(std::move(var)); + ss.push_back(ShellEscape(var)); } // path. std::string path = path_; @@ -235,25 +287,32 @@ std::string Command::ToString() const { ss.push_back(std::move(path)); // args. for (const auto& arg : options_.args) { - ss.push_back(arg); + ss.push_back(ShellEscape(arg)); + } + // in/out/err. + if (!options_.stdin_file_path.empty()) { + ss.push_back(absl::StrCat("< ", ShellEscape(options_.stdin_file_path))); } - // out/err. if (!stdout_file_.empty()) { - ss.push_back(absl::StrCat("> ", stdout_file_)); + ss.push_back(absl::StrCat("> ", ShellEscape(stdout_file_))); } if (!stderr_file_.empty()) { if (stdout_file_ != stderr_file_) { - ss.push_back(absl::StrCat("2> ", stderr_file_)); + ss.push_back(absl::StrCat("2> ", ShellEscape(stderr_file_))); } else { ss.push_back("2>&1"); } } // Trim trailing space and return. return absl::StrJoin(ss, kCommandLineSeparator); +#endif } bool Command::StartForkServer(std::string_view temp_dir_path, std::string_view prefix) { +#if defined(_WIN32) + return false; +#else if (absl::StartsWith(path_, kNoForkServerRequestPrefix)) { FUZZTEST_VLOG(2) << "Fork server disabled for " << path(); return false; @@ -340,6 +399,7 @@ bool Command::StartForkServer(std::string_view temp_dir_path, } fork_server_->creation_stamp = *std::move(creation_stamp); return true; +#endif // _WIN32 } void Command::ResetRedirectionFiles(std::string_view new_suffix) { @@ -364,6 +424,9 @@ void Command::ResetRedirectionFiles(std::string_view new_suffix) { } absl::Status Command::VerifyForkServerIsHealthy() { +#if defined(_WIN32) + return absl::UnimplementedError("Fork server not supported on Windows"); +#else // Preconditions: the callers (`Execute()`) should call us only when the fork // server is presumed to be running (`fork_server_pid_` >= 0). If it is, the // comms pipes are guaranteed to be opened by `StartForkServer()`. @@ -389,11 +452,150 @@ absl::Status Command::VerifyForkServerIsHealthy() { fork_server_->creation_stamp, ", but got ", *creation_stamp)); } return absl::OkStatus(); +#endif } bool Command::ExecuteAsync() { FUZZTEST_CHECK(!is_executing()); +#if defined(_WIN32) + FUZZTEST_CHECK_EQ(win_process_handle_, INVALID_HANDLE_VALUE); + ResetRedirectionFiles(GetUniqueSuffix()); + command_line_ = ToString(); + + struct CaseInsensitiveHash { + size_t operator()(std::string_view s) const { + return absl::Hash{}(absl::AsciiStrToLower(s)); + } + }; + + struct CaseInsensitiveEqual { + bool operator()(std::string_view a, std::string_view b) const { + return absl::EqualsIgnoreCase(a, b); + } + }; + + absl::flat_hash_map + env_map; + + LPCH env_strings = GetEnvironmentStringsA(); + if (env_strings != nullptr) { + const char* ptr = env_strings; + while (*ptr != '\0') { + std::string_view entry(ptr); + ptr += entry.size() + 1; + size_t eq_pos = entry.find('=', 1); + if (eq_pos != std::string_view::npos) { + env_map[std::string(entry.substr(0, eq_pos))] = + std::string(entry.substr(eq_pos + 1)); + } else { + env_map[std::string(entry)] = ""; + } + } + FreeEnvironmentStringsA(env_strings); + } + + for (std::string_view env_var : options_.env_diff) { + if (absl::StartsWith(env_var, "-")) { + std::string_view key = env_var.substr(1); + if (absl::EndsWith(key, "=")) { + key = key.substr(0, key.size() - 1); + } + env_map.erase(std::string(key)); + } else { + auto pos = env_var.find('='); + if (pos != std::string_view::npos) { + std::string key(env_var.substr(0, pos)); + std::string val(env_var.substr(pos + 1)); + env_map[key] = val; + } + } + } + + std::vector env_block; + for (const auto& [key, val] : env_map) { + std::string entry = absl::StrCat(key, "=", val); + env_block.insert(env_block.end(), entry.begin(), entry.end()); + env_block.push_back('\0'); + } + env_block.push_back('\0'); + + STARTUPINFOA si = {sizeof(si)}; + PROCESS_INFORMATION pi = {}; + si.dwFlags = STARTF_USESTDHANDLES; + + SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE}; + HANDLE hIn = INVALID_HANDLE_VALUE; + HANDLE hOut = INVALID_HANDLE_VALUE; + HANDLE hErr = INVALID_HANDLE_VALUE; + + if (!options_.stdin_file_path.empty()) { + hIn = CreateFileA(options_.stdin_file_path.c_str(), FILE_READ_DATA, + /*dwShareMode=*/0, &sa, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (hIn == INVALID_HANDLE_VALUE) { + FUZZTEST_LOG(ERROR) << "Failed to open stdin file: " + << options_.stdin_file_path; + return false; + } + si.hStdInput = hIn; + } else { + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + } + + if (!stdout_file_.empty()) { + hOut = CreateFileA(stdout_file_.c_str(), FILE_WRITE_DATA, FILE_SHARE_READ, + &sa, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hOut == INVALID_HANDLE_VALUE) { + FUZZTEST_LOG(ERROR) << "Failed to open stdout file: " << stdout_file_; + return false; + } + si.hStdOutput = hOut; + } else { + si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + } + + if (!stderr_file_.empty()) { + if (stderr_file_ == stdout_file_) { + si.hStdError = si.hStdOutput; + } else { + hErr = CreateFileA(stderr_file_.c_str(), FILE_WRITE_DATA, FILE_SHARE_READ, + &sa, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hErr == INVALID_HANDLE_VALUE) { + FUZZTEST_LOG(ERROR) << "Failed to open stderr file: " << stderr_file_; + return false; + } + si.hStdError = hErr; + } + } else { + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); + } + + std::string cmd = command_line_; + const BOOL cp_res = CreateProcessA( + NULL, cmd.data(), NULL, NULL, TRUE, 0, + env_block.empty() ? NULL : env_block.data(), NULL, &si, &pi); + + if (!cp_res) { + DWORD err = GetLastError(); + FUZZTEST_LOG(ERROR) << "CreateProcessA failed for '" << cmd + << "': error=" << err; + if (hIn != INVALID_HANDLE_VALUE) CloseHandle(hIn); + if (hOut != INVALID_HANDLE_VALUE) CloseHandle(hOut); + if (hErr != INVALID_HANDLE_VALUE && hErr != hOut) CloseHandle(hErr); + return false; + } + + if (hIn != INVALID_HANDLE_VALUE) CloseHandle(hIn); + if (hOut != INVALID_HANDLE_VALUE) CloseHandle(hOut); + if (hErr != INVALID_HANDLE_VALUE && hErr != hOut) CloseHandle(hErr); + + win_process_handle_ = pi.hProcess; + CloseHandle(pi.hThread); + is_executing_ = true; + return true; +#else if (fork_server_ != nullptr) { FUZZTEST_VLOG(1) << "Sending execution request to fork server"; @@ -430,11 +632,41 @@ bool Command::ExecuteAsync() { is_executing_ = true; return true; +#endif } std::optional Command::Wait(absl::Time deadline, StopCondition* stop_condition) { FUZZTEST_CHECK(is_executing()); +#if defined(_WIN32) + FUZZTEST_CHECK_NE(win_process_handle_, INVALID_HANDLE_VALUE); + DWORD timeout_ms = INFINITE; + if (deadline != absl::InfiniteFuture()) { + auto dur = deadline - absl::Now(); + if (dur <= absl::ZeroDuration()) { + timeout_ms = 0; + } else { + timeout_ms = static_cast(absl::ToInt64Milliseconds(dur)); + } + } + DWORD res = WaitForSingleObject(win_process_handle_, timeout_ms); + if (res == WAIT_TIMEOUT) { + VlogProblemInfo( + absl::StrCat("Timeout while waiting for command process: deadline is ", + deadline), + /*vlog_level=*/1); + return std::nullopt; + } + DWORD exit_code = 0; + GetExitCodeProcess(win_process_handle_, &exit_code); + CloseHandle(win_process_handle_); + win_process_handle_ = INVALID_HANDLE_VALUE; + is_executing_ = false; + if (exit_code == STATUS_CONTROL_C_EXIT && stop_condition != nullptr) { + stop_condition->RequestEarlyStop(EXIT_FAILURE); + } + return static_cast(exit_code); +#else int exit_code = EXIT_SUCCESS; if (fork_server_ != nullptr) { @@ -527,10 +759,15 @@ std::optional Command::Wait(absl::Time deadline, } return exit_code; +#endif } void Command::RequestStop(bool force) { FUZZTEST_CHECK(is_executing()); +#if defined(_WIN32) + FUZZTEST_CHECK_NE(win_process_handle_, INVALID_HANDLE_VALUE); + TerminateProcess(win_process_handle_, 1); +#else if (fork_server_) { FUZZTEST_CHECK_NE(fork_server_->pid_, -1); // Cannot send SIGKILL to the fork server as it kills only the parent @@ -541,6 +778,7 @@ void Command::RequestStop(bool force) { } FUZZTEST_CHECK_NE(pid_, -1); kill(pid_, force ? SIGKILL : SIGTERM); +#endif } std::string Command::ReadRedirectedStdout() const { diff --git a/centipede/command.h b/centipede/command.h index b574a84ee..5cef8f52c 100644 --- a/centipede/command.h +++ b/centipede/command.h @@ -15,6 +15,12 @@ #ifndef THIRD_PARTY_CENTIPEDE_COMMAND_H_ #define THIRD_PARTY_CENTIPEDE_COMMAND_H_ +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOGDI +#include +#endif + #include #include #include @@ -52,6 +58,8 @@ class Command final { // `Command` automatically unlinks any previous redirected files on // execution and destruction. std::string stderr_file_prefix; + // Redirect stdin from this file path if non-empty. + std::string stdin_file_path; // "@@" in the command will be replaced with `temp_file_path`. std::string temp_file_path; }; @@ -127,7 +135,11 @@ class Command final { private: struct ForkServerProps; +#if defined(_WIN32) + HANDLE win_process_handle_ = INVALID_HANDLE_VALUE; +#else int pid_ = -1; +#endif bool is_executing_ = false; // Derived from Options::{stdout,stderr}_file_prefix, with the realized suffix diff --git a/centipede/command_test.cc b/centipede/command_test.cc index 48f4d0730..0640a0654 100644 --- a/centipede/command_test.cc +++ b/centipede/command_test.cc @@ -15,7 +15,9 @@ #include "./centipede/command.h" #include +#if !defined(_WIN32) #include // NOLINT(for WTERMSIG) +#endif #include #include // NOLINT @@ -26,6 +28,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/strings/str_cat.h" #include "absl/strings/substitute.h" #include "absl/time/clock.h" #include "absl/time/time.h" @@ -33,58 +36,120 @@ #include "./centipede/util.h" #include "./common/test_util.h" +#if defined(_WIN32) +#define setenv(n, v, _r) _putenv_s(n, v) +#endif + namespace fuzztest::internal { namespace { +using ::testing::AllOf; +using ::testing::HasSubstr; using ::testing::Optional; -TEST(CommandTest, ToString) { - EXPECT_EQ(Command{"x"}.ToString(), "exec env \\\nx"); - { - Command::Options cmd_options; - cmd_options.args = {"arg1", "arg2"}; - EXPECT_EQ((Command{"path", std::move(cmd_options)}.ToString()), - "exec env \\\npath \\\narg1 \\\narg2"); - } - { - Command::Options cmd_options; - cmd_options.env_diff = {"K1=V1", "K2=V2", "-K3"}; - EXPECT_EQ((Command{"x", std::move(cmd_options)}.ToString()), - "exec env \\\n-u K3 \\\nK1=V1 \\\nK2=V2 \\\nx"); - } -} - TEST(CommandTest, Execute) { + const std::string helper = + GetDataDependencyFilepath("centipede/command_test_helper").string(); StopCondition stop_condition; // Check for default exit code. - Command echo{"echo"}; - EXPECT_EQ(echo.Execute(&stop_condition), 0); + Command::Options options_success; + options_success.args = {"success"}; + Command success_cmd{helper, std::move(options_success)}; + EXPECT_EQ(success_cmd.Execute(&stop_condition), 0); EXPECT_FALSE(stop_condition.ShouldStop()); // Check for exit code 7. - Command exit7{"bash -c 'exit 7'"}; + Command::Options options_ret7; + options_ret7.args = {"ret7"}; + Command exit7{helper, std::move(options_ret7)}; EXPECT_EQ(exit7.Execute(&stop_condition), 7); EXPECT_FALSE(stop_condition.ShouldStop()); } TEST(CommandTest, HandlesInterruptedCommand) { + const std::string helper = + GetDataDependencyFilepath("centipede/command_test_helper").string(); StopCondition stop_condition; - Command self_sigint{"bash -c 'kill -SIGINT $$'"}; - self_sigint.ExecuteAsync(); - self_sigint.Wait(absl::InfiniteFuture(), &stop_condition); + Command::Options options_ctrlc; + options_ctrlc.args = {"ctrlc"}; + Command self_ctrlc{helper, std::move(options_ctrlc)}; + // Cannot set to SIG_IGN as the command would inherit that. + signal(SIGINT, [](int) {}); + self_ctrlc.ExecuteAsync(); + signal(SIGINT, SIG_DFL); + self_ctrlc.Wait(absl::InfiniteFuture(), &stop_condition); EXPECT_TRUE(stop_condition.ShouldStop()); } +TEST(CommandTest, ExecuteWithOptions) { + const std::filesystem::path test_tmpdir = GetTestTempDir(test_info_->name()); + const std::string helper = + GetDataDependencyFilepath("centipede/command_test_helper").string(); + + { + const std::string log_prefix = (test_tmpdir / "args").string(); + Command::Options cmd_options; + cmd_options.args = {"echo_args", "arg1", "arg2"}; + cmd_options.stdout_file_prefix = log_prefix; + Command cmd{helper, std::move(cmd_options)}; + EXPECT_EQ(cmd.Execute(), 0); + std::string log_contents; + ReadFromLocalFile(cmd.stdout_file(), log_contents); + EXPECT_THAT(log_contents, + AllOf(HasSubstr("arg[0]=arg1\n"), HasSubstr("arg[1]=arg2\n"))); + } + + { + setenv("K3", "V3", 1); + const std::string log_prefix = (test_tmpdir / "env").string(); + Command::Options cmd_options; + cmd_options.args = {"echo_env", "K1", "K2", "K3"}; + cmd_options.env_diff = {"K1=V1", "K2=V2", "-K3"}; + cmd_options.stdout_file_prefix = log_prefix; + Command cmd{helper, std::move(cmd_options)}; + EXPECT_EQ(cmd.Execute(), 0); + std::string log_contents; + ReadFromLocalFile(cmd.stdout_file(), log_contents); + EXPECT_THAT(log_contents, AllOf(HasSubstr("K1=V1\n"), HasSubstr("K2=V2\n"), + HasSubstr("K3=\n"))); + } + + { + const std::string stdin_file = (test_tmpdir / "input.txt").string(); + WriteToLocalFile(stdin_file, "hello stdin"); + const std::string log_prefix = (test_tmpdir / "stdin").string(); + Command::Options cmd_options; + cmd_options.args = {"echo_stdin"}; + cmd_options.stdin_file_path = stdin_file; + cmd_options.stdout_file_prefix = log_prefix; + Command cmd{helper, std::move(cmd_options)}; + EXPECT_EQ(cmd.Execute(), 0); + std::string log_contents; + ReadFromLocalFile(cmd.stdout_file(), log_contents); + EXPECT_THAT(log_contents, HasSubstr("hello stdin")); + } +} + TEST(CommandTest, InputFileWildCard) { + const std::filesystem::path test_tmpdir = GetTestTempDir(test_info_->name()); + const std::string helper = + GetDataDependencyFilepath("centipede/command_test_helper").string(); + const std::string log_prefix = (test_tmpdir / "wildcard").string(); + Command::Options cmd_options; cmd_options.temp_file_path = "TEMP_FILE"; - Command cmd{"foo bar @@ baz", std::move(cmd_options)}; - EXPECT_EQ(cmd.ToString(), "exec env \\\nfoo bar TEMP_FILE baz"); + cmd_options.stdout_file_prefix = log_prefix; + Command cmd{absl::StrCat(helper, " @@"), std::move(cmd_options)}; + EXPECT_EQ(cmd.Execute(), 17); + std::string log_contents; + ReadFromLocalFile(cmd.stdout_file(), log_contents); + EXPECT_EQ(log_contents, "Got input: TEMP_FILE\n"); } +#if !defined(_WIN32) TEST(CommandTest, ForkServer) { - const std::string test_tmpdir = GetTestTempDir(test_info_->name()); + const std::filesystem::path test_tmpdir = GetTestTempDir(test_info_->name()); const std::string helper = GetDataDependencyFilepath("centipede/command_test_helper"); @@ -92,93 +157,93 @@ TEST(CommandTest, ForkServer) { { const std::string input = "success"; - const std::string log_prefix = std::filesystem::path{test_tmpdir} / input; + const std::string log_prefix = test_tmpdir / input; Command::Options cmd_options; cmd_options.args = {input}; cmd_options.stdout_file_prefix = log_prefix; cmd_options.stderr_file_prefix = log_prefix; Command cmd{helper, std::move(cmd_options)}; - EXPECT_TRUE(cmd.StartForkServer(test_tmpdir, "ForkServer")); + EXPECT_TRUE(cmd.StartForkServer(test_tmpdir.string(), "ForkServer")); EXPECT_EQ(cmd.Execute(), EXIT_SUCCESS); std::string log_contents; ReadFromLocalFile(cmd.stdout_file(), log_contents); - EXPECT_EQ(log_contents, absl::Substitute("Got input: $0", input)); + EXPECT_EQ(log_contents, absl::Substitute("Got input: $0\n", input)); } { const std::string input = "fail"; - const std::string log_prefix = std::filesystem::path{test_tmpdir} / input; + const std::string log_prefix = test_tmpdir / input; Command::Options cmd_options; cmd_options.args = {input}; cmd_options.stdout_file_prefix = log_prefix; cmd_options.stderr_file_prefix = log_prefix; Command cmd{helper, std::move(cmd_options)}; - EXPECT_TRUE(cmd.StartForkServer(test_tmpdir, "ForkServer")); + EXPECT_TRUE(cmd.StartForkServer(test_tmpdir.string(), "ForkServer")); EXPECT_EQ(cmd.Execute(), EXIT_FAILURE); std::string log_contents; ReadFromLocalFile(cmd.stdout_file(), log_contents); - EXPECT_EQ(log_contents, absl::Substitute("Got input: $0", input)); + EXPECT_EQ(log_contents, absl::Substitute("Got input: $0\n", input)); } { const std::string input = "ret42"; - const std::string log_prefix = std::filesystem::path{test_tmpdir} / input; + const std::string log_prefix = test_tmpdir / input; Command::Options cmd_options; cmd_options.args = {input}; cmd_options.stdout_file_prefix = log_prefix; cmd_options.stderr_file_prefix = log_prefix; Command cmd{helper, std::move(cmd_options)}; - EXPECT_TRUE(cmd.StartForkServer(test_tmpdir, "ForkServer")); + EXPECT_TRUE(cmd.StartForkServer(test_tmpdir.string(), "ForkServer")); EXPECT_EQ(cmd.Execute(), 42); std::string log_contents; ReadFromLocalFile(cmd.stdout_file(), log_contents); - EXPECT_EQ(log_contents, absl::Substitute("Got input: $0", input)); + EXPECT_EQ(log_contents, absl::Substitute("Got input: $0\n", input)); } { const std::string input = "abort"; - const std::string log_prefix = std::filesystem::path{test_tmpdir} / input; + const std::string log_prefix = test_tmpdir / input; Command::Options cmd_options; cmd_options.args = {input}; cmd_options.stdout_file_prefix = log_prefix; cmd_options.stderr_file_prefix = log_prefix; Command cmd{helper, std::move(cmd_options)}; - EXPECT_TRUE(cmd.StartForkServer(test_tmpdir, "ForkServer")); + EXPECT_TRUE(cmd.StartForkServer(test_tmpdir.string(), "ForkServer")); // WTERMSIG() needs an lvalue on some platforms. const int ret = cmd.Execute(); EXPECT_EQ(WTERMSIG(ret), SIGABRT); std::string log_contents; ReadFromLocalFile(cmd.stdout_file(), log_contents); - EXPECT_EQ(log_contents, absl::Substitute("Got input: $0", input)); + EXPECT_EQ(log_contents, absl::Substitute("Got input: $0\n", input)); } { const std::string input = "sleep"; - const std::string log_prefix = std::filesystem::path{test_tmpdir} / input; + const std::string log_prefix = test_tmpdir / input; Command::Options cmd_options; cmd_options.args = {input}; cmd_options.stdout_file_prefix = log_prefix; cmd_options.stderr_file_prefix = log_prefix; Command cmd{helper, std::move(cmd_options)}; - ASSERT_TRUE(cmd.StartForkServer(test_tmpdir, "ForkServer")); + ASSERT_TRUE(cmd.StartForkServer(test_tmpdir.string(), "ForkServer")); ASSERT_TRUE(cmd.ExecuteAsync()); EXPECT_EQ(cmd.Wait(absl::Now() + absl::Seconds(2)), std::nullopt); cmd.RequestStop(/*force=*/false); EXPECT_THAT(cmd.Wait(absl::Now() + absl::Seconds(2)), Optional(SIGTERM)); std::string log_contents; ReadFromLocalFile(cmd.stdout_file(), log_contents); - EXPECT_EQ(log_contents, absl::Substitute("Got input: $0", input)); + EXPECT_EQ(log_contents, absl::Substitute("Got input: $0\n", input)); } { const std::string input = "hang"; - const std::string log_prefix = std::filesystem::path{test_tmpdir} / input; + const std::string log_prefix = test_tmpdir / input; Command::Options cmd_options; cmd_options.args = {input}; cmd_options.stdout_file_prefix = log_prefix; cmd_options.stderr_file_prefix = log_prefix; Command cmd{helper, std::move(cmd_options)}; - ASSERT_TRUE(cmd.StartForkServer(test_tmpdir, "ForkServer")); + ASSERT_TRUE(cmd.StartForkServer(test_tmpdir.string(), "ForkServer")); ASSERT_TRUE(cmd.ExecuteAsync()); EXPECT_EQ(cmd.Wait(absl::Now() + absl::Seconds(2)), std::nullopt); cmd.RequestStop(/*force=*/false); @@ -187,11 +252,12 @@ TEST(CommandTest, ForkServer) { EXPECT_THAT(cmd.Wait(absl::Now() + absl::Seconds(2)), Optional(SIGKILL)); std::string log_contents; ReadFromLocalFile(cmd.stdout_file(), log_contents); - EXPECT_EQ(log_contents, absl::Substitute("Got input: $0", input)); + EXPECT_EQ(log_contents, absl::Substitute("Got input: $0\n", input)); } // TODO(kcc): [impl] test what happens if the child is interrupted. } +#endif } // namespace } // namespace fuzztest::internal diff --git a/centipede/command_test_helper.cc b/centipede/command_test_helper.cc index d3153a853..b7ad1be20 100644 --- a/centipede/command_test_helper.cc +++ b/centipede/command_test_helper.cc @@ -12,33 +12,93 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include - #include #include #include #include #include +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOGDI +#include +#include +#include +#endif + #include "absl/base/nullability.h" +#include "absl/strings/match.h" +#include "absl/strings/numbers.h" +#include "absl/strings/string_view.h" #include "absl/time/clock.h" #include "absl/time/time.h" // A binary linked with the fork server that exits/crashes in different ways. int main(int argc, char** absl_nonnull argv) { - assert(argc == 2); - printf("Got input: %s", argv[1]); +#if defined(_WIN32) + // Disable the automatic \n -> \r\n conversion. + _setmode(1, _O_BINARY); +#endif + + assert(argc >= 2); + printf("Got input: %s\n", argv[1]); + + if (!strcmp(argv[1], "echo_args")) { + for (int i = 2; i < argc; ++i) { + printf("arg[%d]=%s\n", i - 2, argv[i]); + } + fflush(stdout); + return EXIT_SUCCESS; + } + + if (!strcmp(argv[1], "echo_env")) { + for (int i = 2; i < argc; ++i) { + const char* val = getenv(argv[i]); + printf("%s=%s\n", argv[i], val ? val : ""); + } + fflush(stdout); + return EXIT_SUCCESS; + } + + if (!strcmp(argv[1], "echo_stdin")) { + char buf[1024]; + while (fgets(buf, sizeof(buf), stdin)) { + fputs(buf, stdout); + } + fflush(stdout); + return EXIT_SUCCESS; + } + fflush(stdout); + if (!strcmp(argv[1], "success")) return EXIT_SUCCESS; if (!strcmp(argv[1], "fail")) return EXIT_FAILURE; - if (!strcmp(argv[1], "ret42")) return 42; + + int ret_code = 0; + if (absl::StartsWith(argv[1], "ret") && + absl::SimpleAtoi(argv[1] + 3, &ret_code)) { + return ret_code; + } + if (!strcmp(argv[1], "abort")) abort(); + if (!strcmp(argv[1], "ctrlc")) { +#if defined(_WIN32) + SetConsoleCtrlHandler(NULL, FALSE); + GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); + Sleep(INFINITE); +#else + raise(SIGINT); +#endif + return EXIT_SUCCESS; + } // Sleep longer than kTimeout in CommandDeathTest_ForkServerHangingBinary. if (!strcmp(argv[1], "sleep")) absl::SleepFor(absl::Seconds(5)); if (!strcmp(argv[1], "hang")) { +#if !defined(_WIN32) struct sigaction act{}; act.sa_handler = [](int) {}; sigaction(SIGTERM, &act, nullptr); +#endif absl::SleepFor(absl::Seconds(10)); } diff --git a/centipede/puzzles/BUILD b/centipede/puzzles/BUILD index b87aad063..3d83aefb9 100644 --- a/centipede/puzzles/BUILD +++ b/centipede/puzzles/BUILD @@ -26,7 +26,6 @@ licenses(["notice"]) package(default_visibility = ["@com_google_fuzztest//centipede/puzzles:__subpackages__"]) -# The puzzles use a simple configuration language, see run_puzzle.sh. [puzzle(name = n) for n in [ "byte_cmp_4", "callstack", diff --git a/centipede/puzzles/autodictionary_stress.cc b/centipede/puzzles/autodictionary_stress.cc index 297d5ee20..40a415db1 100644 --- a/centipede/puzzles/autodictionary_stress.cc +++ b/centipede/puzzles/autodictionary_stress.cc @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Centipede puzzle: stress test for --use_auto_dictionary=1. -// RUN: Run --use_auto_dictionary=1 --use_cmp_features=0 -j 5 -// RUN: ExpectInLog "Input bytes.*: abcdxyzVeryLongStringKeyword" +// CASE main: ARG: --use_auto_dictionary=1 +// CASE main: ARG: --use_cmp_features=0 +// CASE main: ARG: --j=5 +// CASE main: MATCH: Input bytes.*: abcdxyzVeryLongStringKeyword // TODO(kcc): we currently use --use_cmp_features=0 because otherwise // the corpus gets too large and the puzzle does not get solved quickly. diff --git a/centipede/puzzles/byte_cmp_4.cc b/centipede/puzzles/byte_cmp_4.cc index 8fb9c4cc6..9bdcf9656 100644 --- a/centipede/puzzles/byte_cmp_4.cc +++ b/centipede/puzzles/byte_cmp_4.cc @@ -13,7 +13,7 @@ // limitations under the License. // Centipede puzzle: sequence of 4 1-byte comparisons. -// RUN: Run && SolutionIs fuzZ +// CASE main: MATCH: Input bytes *: fuzZ #include #include #include diff --git a/centipede/puzzles/callstack.cc b/centipede/puzzles/callstack.cc index 4fc9a8b36..7fd160185 100644 --- a/centipede/puzzles/callstack.cc +++ b/centipede/puzzles/callstack.cc @@ -18,10 +18,10 @@ // All functions but F0 also modify `g_result`. // The puzzle can be solved if the call sequence is FA->FB->FC->FD->FE. -// clang-format off -// RUN: Run --callstack_level=10 --use_cmp_features=0 --use_dataflow_features=0 // NOLINT -// RUN: SolutionIs ABCDE -// clang-format on +// CASE main: ARG: --callstack_level=10 +// CASE main: ARG: --use_cmp_features=0 +// CASE main: ARG: --use_dataflow_features=0 +// CASE main: MATCH: Input bytes *: ABCDE #include #include #include diff --git a/centipede/puzzles/deep_recursion.cc b/centipede/puzzles/deep_recursion.cc index 6e7572d6a..9440cbda6 100644 --- a/centipede/puzzles/deep_recursion.cc +++ b/centipede/puzzles/deep_recursion.cc @@ -13,11 +13,11 @@ // limitations under the License. // Centipede puzzle: use callstack features to reach deep recursion -// clang-format off -// RUN: Run --callstack_level=10 --use_cmp_features=0 --max_len=10 --num_runs=10000000 # NOLINT -// RUN: SolutionIs ABCDEF -// clang-format on -#include +// CASE main: ARG: --callstack_level=10 +// CASE main: ARG: --use_cmp_features=0 +// CASE main: ARG: --max_len=10 +// CASE main: ARG: --num_runs=10000000 +// CASE main: MATCH: Input bytes *: ABCDEF #include #include diff --git a/centipede/puzzles/independent_compares.cc b/centipede/puzzles/independent_compares.cc index 07294f906..0679482dc 100644 --- a/centipede/puzzles/independent_compares.cc +++ b/centipede/puzzles/independent_compares.cc @@ -13,7 +13,7 @@ // limitations under the License. // Centipede puzzle: 4 independent compares setting a mask. -// RUN: Run && SolutionIs FUZZ +// CASE main: MATCH: Input bytes *: FUZZ #include #include #include diff --git a/centipede/puzzles/memcmp_3.cc b/centipede/puzzles/memcmp_3.cc index 36244e31a..c12ffe08c 100644 --- a/centipede/puzzles/memcmp_3.cc +++ b/centipede/puzzles/memcmp_3.cc @@ -14,9 +14,10 @@ // Centipede puzzle: one 3-byte memcmp. Check the output in the log. // Disable use_auto_dictionary so that we test other functionality. -// RUN: Run --use_auto_dictionary=false && SolutionIs fUz -// RUN: ExpectInLog "TEXT IN STDOUT" -// RUN: ExpectInLog "TEXT IN STDERR" +// CASE main: ARG: --use_auto_dictionary=false +// CASE main: MATCH: Input bytes *: fUz +// CASE main: MATCH: TEXT IN STDOUT +// CASE main: MATCH: TEXT IN STDERR #include #include diff --git a/centipede/puzzles/memcmp_4.cc b/centipede/puzzles/memcmp_4.cc index 97bbbd530..deefe4a71 100644 --- a/centipede/puzzles/memcmp_4.cc +++ b/centipede/puzzles/memcmp_4.cc @@ -14,7 +14,9 @@ // Centipede puzzle: one 4-byte memcmp. // Disable use_auto_dictionary so that we test other functionality. -// RUN: Run --use_auto_dictionary=false --max_len=10 && SolutionIs fuZz +// CASE main: ARG: --use_auto_dictionary=false +// CASE main: ARG: --max_len=10 +// CASE main: MATCH: Input bytes *: fuZz #include #include diff --git a/centipede/puzzles/memcmp_4_may_inline.cc b/centipede/puzzles/memcmp_4_may_inline.cc index bc2f062f0..746796db2 100644 --- a/centipede/puzzles/memcmp_4_may_inline.cc +++ b/centipede/puzzles/memcmp_4_may_inline.cc @@ -14,7 +14,8 @@ // Centipede puzzle: one 4-byte memcmp, which may get inlined. // Disable use_auto_dictionary so that we test other functionality. -// RUN: Run --use_auto_dictionary=false && SolutionIs FUZz +// CASE main: ARG: --use_auto_dictionary=false +// CASE main: MATCH: Input bytes *: FUZz #include #include diff --git a/centipede/puzzles/oom.cc b/centipede/puzzles/oom.cc index bb20e5dc4..07270d231 100644 --- a/centipede/puzzles/oom.cc +++ b/centipede/puzzles/oom.cc @@ -13,7 +13,9 @@ // limitations under the License. // Centipede puzzle: easy-to-reach OOM. -// RUN: Run --rss_limit_mb=1000 && SolutionIs OOM && ExpectOOM +// CASE main: ARG: --rss_limit_mb=1000 +// CASE main: MATCH: Input bytes *: OOM +// CASE main: MATCH: Failure.*: rss-limit-exceeded #include #include #include diff --git a/centipede/puzzles/paths.cc b/centipede/puzzles/paths.cc index ab8de1afd..654300b64 100644 --- a/centipede/puzzles/paths.cc +++ b/centipede/puzzles/paths.cc @@ -16,8 +16,10 @@ // We disable use_dataflow_features because on this puzzle // it is also effective. -// RUN: Run --use_dataflow_features=0 --use_cmp_features=0 --path_level=10 -// RUN: ExpectInLog "Input bytes.*: .x1.x2.x3" +// CASE main: ARG: --use_dataflow_features=0 +// CASE main: ARG: --use_cmp_features=0 +// CASE main: ARG: --path_level=10 +// CASE main: MATCH: Input bytes.*: .x1.x2.x3 // This puzzle aborts on input "\x1\x2\x3" // The code here has very little control flow, but an exponential number of diff --git a/centipede/puzzles/per_batch_timeout.cc b/centipede/puzzles/per_batch_timeout.cc index f64f00909..9b2e50670 100644 --- a/centipede/puzzles/per_batch_timeout.cc +++ b/centipede/puzzles/per_batch_timeout.cc @@ -13,10 +13,10 @@ // limitations under the License. // Centipede puzzle: easy-to-reach per-batch timeout. -// clang-format off -// NOLINTNEXTLINE -// RUN: Run --batch_size=10 --timeout_per_input=2 --timeout_per_batch=7 && ExpectPerBatchTimeout -// clang-format on +// CASE main: ARG: --batch_size=10 +// CASE main: ARG: --timeout_per_input=2 +// CASE main: ARG: --timeout_per_batch=7 +// CASE main: MATCH: Failure.*: per-batch-timeout-exceeded #include diff --git a/centipede/puzzles/per_input_timeout.cc b/centipede/puzzles/per_input_timeout.cc index d2865430d..3f2e4e365 100644 --- a/centipede/puzzles/per_input_timeout.cc +++ b/centipede/puzzles/per_input_timeout.cc @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Centipede puzzle: easy-to-reach per-input timeout. -// RUN: Run --timeout_per_input=2 && SolutionIs SLO && ExpectPerInputTimeout +// CASE main: ARG: --timeout_per_input=2 +// CASE main: MATCH: Input bytes *: SLO +// CASE main: MATCH: Failure.*: per-input-timeout-exceeded #include diff --git a/centipede/puzzles/pthread_exit_uint32_cmp_1.cc b/centipede/puzzles/pthread_exit_uint32_cmp_1.cc index 1e5bade52..f132c089b 100644 --- a/centipede/puzzles/pthread_exit_uint32_cmp_1.cc +++ b/centipede/puzzles/pthread_exit_uint32_cmp_1.cc @@ -15,10 +15,11 @@ // Centipede puzzle: one 4-byte cmp, in a separate thread using pthread // interface. We should be able to solve it w/o cmp features *or* w/o auto // dictionary. -// -// RUN: Run && SolutionIs Fuzz -// RUN: Run --use_auto_dictionary=0 && SolutionIs Fuzz -// RUN: Run --use_cmp_features=0 && SolutionIs Fuzz +// CASE main: MATCH: Input bytes *: Fuzz +// CASE no_cmp: ARG: --use_cmp_features=0 +// CASE no_cmp: MATCH: Input bytes *: Fuzz +// CASE no_autodict: ARG: --use_auto_dictionary=0 +// CASE no_autodict: MATCH: Input bytes *: Fuzz #include diff --git a/centipede/puzzles/puzzle.bzl b/centipede/puzzles/puzzle.bzl index a446b967a..91189ff35 100644 --- a/centipede/puzzles/puzzle.bzl +++ b/centipede/puzzles/puzzle.bzl @@ -14,7 +14,7 @@ """BUILD rule for Centipede puzzles""" -load("@rules_shell//shell:sh_test.bzl", "sh_test") +load("@rules_cc//cc:cc_test.bzl", "cc_test") load("@com_google_fuzztest//centipede/testing:build_defs.bzl", "centipede_fuzz_target") def puzzle(name): @@ -36,13 +36,23 @@ def puzzle(name): # repeatability. Each sh_test performs a single run with a single seed, so # that the log is minimal. for seed in ["1", "2"]: - sh_test( + cc_test( name = "run_" + seed + "_" + name, - srcs = ["run_puzzle.sh"], + srcs = ["run_puzzle.cc"], + args = ["--seed=" + seed, "--puzzle=" + name], data = [ ":" + name, name + ".cc", "@com_google_fuzztest//centipede:centipede_uninstrumented", - "@com_google_fuzztest//centipede:test_util_sh", + ], + deps = [ + "@googletest//:gtest", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/flags:parse", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/time", + "@com_google_fuzztest//centipede:command", + "@com_google_fuzztest//common:logging", + "@com_google_fuzztest//common:test_util", ], ) diff --git a/centipede/puzzles/run_puzzle.cc b/centipede/puzzles/run_puzzle.cc new file mode 100644 index 000000000..939675ae8 --- /dev/null +++ b/centipede/puzzles/run_puzzle.cc @@ -0,0 +1,179 @@ +// Copyright 2026 The FuzzTest Authors. +// +// 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 +// +// https://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 // NOLINT +#include +#include +#include +#include +#include +#include // NOLINT +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "absl/time/time.h" +#include "./centipede/command.h" +#include "./common/logging.h" +#include "./common/test_util.h" + +ABSL_FLAG(int, seed, 0, "The random seed to use"); +ABSL_FLAG(std::string, puzzle, "", "The name of the puzzle to run"); + +namespace fuzztest::internal { +namespace { + +using testing::ContainsRegex; +using testing::Value; + +std::string ReadFile(std::filesystem::path path) { + std::ifstream file(path); + std::stringstream ss; + ss << file.rdbuf(); + return ss.str(); +} + +constexpr std::string_view kCasePrefix = "// CASE "; +constexpr std::string_view kArgPrefix = "ARG:"; +constexpr std::string_view kMatchPrefix = "MATCH:"; + +std::vector GetCasesInPuzzle(std::string_view puzzle) { + const std::string puzzle_source_path = + GetDataDependencyFilepath( + absl::StrCat("centipede/puzzles/", puzzle, ".cc")) + .string(); + std::error_code ec; + FUZZTEST_CHECK(std::filesystem::exists(puzzle_source_path, ec)) + << "Puzzle source " << puzzle_source_path << " does not exist"; + const std::string puzzle_source = ReadFile(puzzle_source_path); + std::set found_cases; + for (std::string_view line : absl::StrSplit(puzzle_source, '\n')) { + const auto case_start_pos = line.find(kCasePrefix); + if (case_start_pos == line.npos) continue; + const auto case_end_pos = + line.find(":", case_start_pos + kCasePrefix.size()); + if (case_end_pos == line.npos) continue; + if (line.find(kMatchPrefix, case_end_pos) == line.npos) continue; + found_cases.insert(std::string{ + line.substr(case_start_pos + kCasePrefix.size(), + case_end_pos - case_start_pos - kCasePrefix.size())}); + } + return {found_cases.begin(), found_cases.end()}; +} + +class PuzzleTest : public testing::Test { + public: + PuzzleTest(std::string_view puzzle, std::string_view case_name) + : puzzle_{puzzle}, case_name_{case_name} {} + + void TestBody() { + const std::string centipede_path = + GetDataDependencyFilepath("centipede/centipede_uninstrumented") + .string(); + const std::string puzzle_binary_path = + GetDataDependencyFilepath(absl::StrCat("centipede/puzzles/", puzzle_)) + .string(); + const std::string puzzle_source_path = + GetDataDependencyFilepath( + absl::StrCat("centipede/puzzles/", puzzle_, ".cc")) + .string(); + std::error_code ec; + FUZZTEST_CHECK(std::filesystem::exists(puzzle_binary_path, ec)) + << "Puzzle binary " << puzzle_binary_path << " does not exist"; + FUZZTEST_CHECK(std::filesystem::exists(puzzle_source_path, ec)) + << "Puzzle source " << puzzle_source_path << " does not exist"; + TempDir tmp_dir{absl::StrCat(puzzle_, "_", case_name_)}; + + const std::string case_prefix = absl::StrCat(kCasePrefix, case_name_, ":"); + std::vector extra_args; + std::vector output_matches; + const std::string puzzle_source = ReadFile(puzzle_source_path); + for (std::string_view line : absl::StrSplit(puzzle_source, '\n')) { + const auto case_pos = line.find(case_prefix); + if (case_pos == line.npos) continue; + if (const auto arg_pos = + line.find(kArgPrefix, case_pos + case_prefix.size()); + arg_pos != line.npos) { + auto arg = std::string{absl::StripAsciiWhitespace( + line.substr(arg_pos + kArgPrefix.size()))}; + FUZZTEST_LOG(INFO) << "Using arg: " << arg; + extra_args.push_back(std::move(arg)); + continue; + } + if (const auto output_match_pos = + line.find(kMatchPrefix, case_pos + case_prefix.size()); + output_match_pos != line.npos) { + auto output_match = std::string{absl::StripAsciiWhitespace( + line.substr(output_match_pos + kMatchPrefix.size()))}; + FUZZTEST_LOG(INFO) << "Using match: " << output_match; + output_matches.push_back(std::move(output_match)); + continue; + } + } + FUZZTEST_CHECK(!output_matches.empty()) + << "Need at least one output match for case " << case_name_; + + Command::Options cmd_options; + cmd_options.args = { + absl::StrCat("--workdir=", (tmp_dir.path() / "workdir").string()), + absl::StrCat("--binary=", puzzle_binary_path), + "--populate_binary_info=0", + absl::StrCat("--seed=", absl::GetFlag(FLAGS_seed)), + "--num_runs=2000000", + "--shmem_size_mb=100", + "--exit_on_crash", + }; + cmd_options.args.insert(cmd_options.args.end(), extra_args.begin(), + extra_args.end()); + + const std::string output_prefix = (tmp_dir.path() / "out_").string(); + cmd_options.stdout_file_prefix = output_prefix; + cmd_options.stderr_file_prefix = output_prefix; + Command cmd(centipede_path, cmd_options); + FUZZTEST_CHECK(cmd.ExecuteAsync()); + (void)cmd.Wait(absl::Now() + absl::Seconds(30)); + const std::string output = ReadFile(cmd.stdout_file()); + for (const auto& output_match : output_matches) { + EXPECT_TRUE(Value(output, ContainsRegex(output_match))) << output; + } + } + + private: + std::string puzzle_; + std::string case_name_; +}; + +} // namespace +} // namespace fuzztest::internal + +int main(int argc, char** argv) { + absl::ParseCommandLine(argc, argv); + testing::InitGoogleTest(&argc, argv); + const std::string puzzle = absl::GetFlag(FLAGS_puzzle); + FUZZTEST_CHECK(!puzzle.empty()); + const auto cases = fuzztest::internal::GetCasesInPuzzle(puzzle); + for (const auto& case_name : cases) { + testing::RegisterTest( + "Puzzle", case_name.c_str(), nullptr, nullptr, __FILE__, __LINE__, + [puzzle, case_name]() -> testing::Test* { + return new fuzztest::internal::PuzzleTest{puzzle, case_name}; + }); + } + return RUN_ALL_TESTS(); +} diff --git a/centipede/puzzles/run_puzzle.sh b/centipede/puzzles/run_puzzle.sh deleted file mode 100755 index d6d12c853..000000000 --- a/centipede/puzzles/run_puzzle.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/bin/bash - -# Copyright 2022 The Centipede Authors. -# -# 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 -# -# https://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. - -# Run a short fuzzing session for one puzzle and check the outcome. -# This script is executed under the name run_S_PUZZLE_NAME, where S is a single -# digit representing the seed, so we get the seed and puzzle name from $0. -# Every puzzle must have one or more lines containing "RUN:" -# This script will execute directly in the current context. -# can use the functions defined in this file, see USER_FUNCTIONS. - -set -eu -o pipefail - -ls -la "$(dirname "$0")" - -source "$(dirname "$0")/../test_util.sh" - -readonly centipede_dir="$(fuzztest::internal::get_centipede_test_srcdir)" -fuzztest::internal::maybe_set_var_to_executable_path centipede "${centipede_dir}/centipede_uninstrumented" -readonly centipede -fuzztest::internal::maybe_set_var_to_executable_path llvm_symbolizer "$(fuzztest::internal::get_llvm_symbolizer_path)" -readonly llvm_symbolizer -fuzztest::internal::maybe_set_var_to_executable_path objdump "$(fuzztest::internal::get_objdump_path)" -readonly objdump - -readonly target_name="$(basename "$0")" -readonly seed_and_puzzle_name="${target_name#run_}" -readonly seed="${seed_and_puzzle_name:0:1}" -readonly puzzle_name="${seed_and_puzzle_name:2}" -readonly puzzle_source_name="${puzzle_name}.cc" -readonly puzzle_path="${centipede_dir}/puzzles/${puzzle_name}" -readonly puzzle_source_path="${centipede_dir}/puzzles/${puzzle_source_name}" - -readonly workdir="${TEST_TMPDIR}/workdir" -readonly log="${TEST_TMPDIR}/log" -readonly script="${TEST_TMPDIR}/script" - -# Read the configuration from the puzzle source. -grep 'RUN:' "${puzzle_source_path}" | sed 's/^.*RUN://' > "${script}" -echo "======== SCRIPT" -cat "${script}" -echo "======== END SCRIPT" - -##################################### USER_FUNCTIONS - -# Runs Centipede with additional parameters in $@, saves the result in log, cats -# the log. Expects Centipede to exit with failure. -function Run() { - echo "======== Run $*" - rm -rf "${workdir}" - mkdir "${workdir}" - if "${centipede}" \ - --workdir "${workdir}" \ - --binary "${puzzle_path}" \ - --symbolizer_path="${llvm_symbolizer}" \ - --objdump_path="${objdump}$" \ - --seed="${seed}" \ - --num_runs=2000000 \ - --timeout_per_input=10 \ - --exit_on_crash \ - "$@" \ - 2>&1 | tee "${log}" - then - # Centipede must exit with failure. - return 1 - fi -} - -# Checks that $1 is the solution for the puzzle. -function SolutionIs() { - echo "====== ${FUNCNAME[0]}: $1" - fuzztest::internal::assert_regex_in_file "Input bytes.*: $1" "${log}" -} - -# Expects that Centipede found a per-input timeout. -function ExpectPerInputTimeout() { - echo "======= ${FUNCNAME[0]}" - fuzztest::internal::assert_regex_in_file "Per-input timeout exceeded" "${log}" - fuzztest::internal::assert_regex_in_file "Failure.*: per-input-timeout-exceeded" "${log}" -} - -# Expects that Centipede found a per-batch timeout. -function ExpectPerBatchTimeout() { - echo "======= ${FUNCNAME[0]}" - fuzztest::internal::assert_regex_in_file "Failure.*: per-batch-timeout-exceeded" "${log}" - fuzztest::internal::assert_regex_in_file \ - "Failure applies to entire batch: not executing inputs one-by-one" "${log}" - fuzztest::internal::assert_regex_not_in_file \ - "Executing inputs one-by-one, trying to find the reproducer" "${log}" -} - -# Expects that Centipede found a OOM. -function ExpectOOM() { - echo "======= ${FUNCNAME[0]}" - fuzztest::internal::assert_regex_in_file "RSS limit exceeded" "${log}" - fuzztest::internal::assert_regex_in_file "Failure.*: rss-limit-exceeded" "${log}" -} - -# Expects that $1 is found in the log. -function ExpectInLog() { - echo "======= ${FUNCNAME[0]}: $1" - fuzztest::internal::assert_regex_in_file "$1" "${log}" -} - -##################################### end USER_FUNCTIONS - -# shellcheck disable=SC1090 -source "${script}" - -echo PASS diff --git a/centipede/puzzles/strcasecmp.cc b/centipede/puzzles/strcasecmp.cc index c9aaadd72..670a9f538 100644 --- a/centipede/puzzles/strcasecmp.cc +++ b/centipede/puzzles/strcasecmp.cc @@ -15,9 +15,10 @@ // Centipede puzzle: one 3-byte strncasecmp and one 3-byte strcasecmp. Check // the output in the log. Disable use_auto_dictionary so that we test other // functionality. -// RUN: Run --use_auto_dictionary=false && SolutionIs 123456 -// RUN: ExpectInLog "TEXT IN STDOUT" -// RUN: ExpectInLog "TEXT IN STDERR" +// CASE main: ARG: --use_auto_dictionary=false +// CASE main: MATCH: Input bytes *: 123456 +// CASE main: MATCH: TEXT IN STDOUT +// CASE main: MATCH: TEXT IN STDERR #include diff --git a/centipede/puzzles/strcmp.cc b/centipede/puzzles/strcmp.cc index c61bb1706..338d338fd 100644 --- a/centipede/puzzles/strcmp.cc +++ b/centipede/puzzles/strcmp.cc @@ -14,9 +14,10 @@ // Centipede puzzle: one 4-byte strcmp. Check the output in the log. // Disable use_auto_dictionary so that we test other functionality. -// RUN: Run --use_auto_dictionary=false && SolutionIs fUzZ -// RUN: ExpectInLog "TEXT IN STDOUT" -// RUN: ExpectInLog "TEXT IN STDERR" +// CASE main: ARG: --use_auto_dictionary=false +// CASE main: MATCH: Input bytes *: fUzZ +// CASE main: MATCH: TEXT IN STDOUT +// CASE main: MATCH: TEXT IN STDERR #include #include diff --git a/centipede/puzzles/strncmp.cc b/centipede/puzzles/strncmp.cc index 4668e114c..74f622d0d 100644 --- a/centipede/puzzles/strncmp.cc +++ b/centipede/puzzles/strncmp.cc @@ -14,9 +14,10 @@ // Centipede puzzle: one 4-byte strncmp. Check the output in the log. // Disable use_auto_dictionary so that we test other functionality. -// RUN: Run --use_auto_dictionary=false && SolutionIs fUzZ -// RUN: ExpectInLog "TEXT IN STDOUT" -// RUN: ExpectInLog "TEXT IN STDERR" +// CASE main: ARG: --use_auto_dictionary=false +// CASE main: MATCH: Input bytes *: fUzZ +// CASE main: MATCH: TEXT IN STDOUT +// CASE main: MATCH: TEXT IN STDERR #include #include diff --git a/centipede/puzzles/switch.cc b/centipede/puzzles/switch.cc index 40a61a22d..8e7f09e23 100644 --- a/centipede/puzzles/switch.cc +++ b/centipede/puzzles/switch.cc @@ -13,7 +13,9 @@ // limitations under the License. // Centipede puzzle: one 4-byte switch. -// RUN: Run --max_len=10 --use_cmp_features=0 && ExpectInLog "deadbeef found!" +// CASE main: ARG: --max_len=10 +// CASE main: ARG: --use_cmp_features=0 +// CASE main: MATCH: deadbeef found! #include #include diff --git a/centipede/puzzles/thread_uint32_cmp_1.cc b/centipede/puzzles/thread_uint32_cmp_1.cc index 066581515..962fe3e27 100644 --- a/centipede/puzzles/thread_uint32_cmp_1.cc +++ b/centipede/puzzles/thread_uint32_cmp_1.cc @@ -14,9 +14,11 @@ // Centipede puzzle: one 4-byte cmp, in a separate thread. // We should be able to solve it w/o cmp features *or* w/o auto dictionary. -// RUN: Run && SolutionIs Fuzz -// RUN: Run --use_auto_dictionary=0 && SolutionIs Fuzz -// RUN: Run --use_cmp_features=0 && SolutionIs Fuzz +// CASE main: MATCH: Input bytes *: Fuzz +// CASE no_cmp: ARG: --use_cmp_features=0 +// CASE no_cmp: MATCH: Input bytes *: Fuzz +// CASE no_autodict: ARG: --use_auto_dictionary=0 +// CASE no_autodict: MATCH: Input bytes *: Fuzz #include #include diff --git a/centipede/puzzles/uint32_cmp_1.cc b/centipede/puzzles/uint32_cmp_1.cc index f41879b15..3b4df0135 100644 --- a/centipede/puzzles/uint32_cmp_1.cc +++ b/centipede/puzzles/uint32_cmp_1.cc @@ -14,9 +14,11 @@ // Centipede puzzle: one 4-byte cmp. // We should be able to solve it w/o cmp features *or* w/o auto dictionary. -// RUN: Run && SolutionIs Fuzz -// RUN: Run --use_cmp_features=0 && SolutionIs Fuzz -// RUN: Run --use_auto_dictionary=0 && SolutionIs Fuzz +// CASE main: MATCH: Input bytes *: Fuzz +// CASE no_cmp: ARG: --use_cmp_features=0 +// CASE no_cmp: MATCH: Input bytes *: Fuzz +// CASE no_autodict: ARG: --use_auto_dictionary=0 +// CASE no_autodict: MATCH: Input bytes *: Fuzz #include #include diff --git a/centipede/symbol_table.cc b/centipede/symbol_table.cc index e693b4817..be0d8e8d4 100644 --- a/centipede/symbol_table.cc +++ b/centipede/symbol_table.cc @@ -99,7 +99,8 @@ void SymbolTable::GetSymbolsFromOneDso(absl::Span pc_infos, std::string_view tmp_dir_path) { static std::atomic_size_t unique_id = 0; size_t unique_id_value = unique_id.fetch_add(1); - const std::string dso_basename = std::filesystem::path{dso_path}.filename(); + const std::string dso_basename = + std::filesystem::path{dso_path}.filename().string(); const ScopedFile pcs_file{ tmp_dir_path, absl::StrCat(dso_basename, ".pcs.", unique_id_value)}; const auto symbols_file_prefix = std::filesystem::path{tmp_dir_path} / @@ -120,10 +121,9 @@ void SymbolTable::GetSymbolsFromOneDso(absl::Span pc_infos, "--no-inlines", "-e", std::string(dso_path), - "<", - std::string(pcs_file.path()), }; - cmd_options.stdout_file_prefix = symbols_file_prefix; + cmd_options.stdin_file_path = std::string(pcs_file.path()); + cmd_options.stdout_file_prefix = symbols_file_prefix.string(); Command cmd{symbolizer_path, std::move(cmd_options)}; int exit_code = cmd.Execute(); if (exit_code != EXIT_SUCCESS) { @@ -172,7 +172,7 @@ void SymbolTable::GetSymbolsFromBinary(const PCTable &pc_table, { // Symbolization is quite IO-bound so we arbitrarily run 30 at once // even if we have few CPUs. - const size_t num_threads = std::min(dso_table.size(), 30UL); + const size_t num_threads = std::min(dso_table.size(), size_t{30}); fuzztest::internal::ThreadPool thread_pool(num_threads); for (size_t dso_id = 0; dso_id < dso_table.size(); ++dso_id) { const auto &dso_info = dso_table[dso_id]; diff --git a/centipede/util.cc b/centipede/util.cc index ab2eb19ef..748b76c97 100644 --- a/centipede/util.cc +++ b/centipede/util.cc @@ -17,8 +17,16 @@ #include "./centipede/util.h" +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOGDI +#include +#include +#include +#else #include #include +#endif #include #include @@ -47,6 +55,7 @@ #include "absl/base/const_init.h" #include "absl/base/nullability.h" #include "absl/base/thread_annotations.h" +#include "absl/strings/match.h" #include "absl/strings/str_format.h" #include "absl/strings/str_replace.h" #include "absl/strings/str_split.h" @@ -63,8 +72,13 @@ namespace fuzztest::internal { size_t GetRandomSeed(size_t seed) { if (seed != 0) return seed; +#if defined(_WIN32) + return time(nullptr) + GetCurrentProcessId() + + std::hash{}(std::this_thread::get_id()); +#else return time(nullptr) + getpid() + std::hash{}(std::this_thread::get_id()); +#endif } std::string AsPrintableString(ByteSpan data, size_t max_len) { @@ -83,7 +97,7 @@ std::string AsPrintableString(ByteSpan data, size_t max_len) { template void ReadFromLocalFile(std::string_view file_path, Container &data) { - std::ifstream f(std::string{file_path}); + std::ifstream f(std::string{file_path}, std::ios::in | std::ios::binary); if (!f) return; f.seekg(0, std::ios_base::end); auto size = f.tellg(); @@ -112,12 +126,13 @@ void ReadFromLocalFile(std::string_view file_path, } void ClearLocalFileContents(std::string_view file_path) { - std::ofstream f(std::string{file_path}, std::ios::out | std::ios::trunc); + std::ofstream f(std::string{file_path}, + std::ios::out | std::ios::trunc | std::ios::binary); FUZZTEST_CHECK(f) << "Failed to clear the file: " << file_path; } void WriteToLocalFile(std::string_view file_path, ByteSpan data) { - std::ofstream f(std::string{file_path}); + std::ofstream f(std::string{file_path}, std::ios::out | std::ios::binary); FUZZTEST_CHECK(f) << "Failed to open local file: " << file_path; f.write(reinterpret_cast(data.data()), static_cast(data.size())); @@ -136,13 +151,15 @@ void WriteToLocalFile(std::string_view file_path, const FeatureVec &data) { void WriteToLocalHashedFileInDir(std::string_view dir_path, ByteSpan data) { if (dir_path.empty()) return; - std::string file_path = std::filesystem::path(dir_path).append(Hash(data)); + std::string file_path = + std::filesystem::path(dir_path).append(Hash(data)).string(); WriteToLocalFile(file_path, data); } void WriteToRemoteHashedFileInDir(std::string_view dir_path, ByteSpan data) { if (dir_path.empty()) return; - std::string file_path = std::filesystem::path(dir_path).append(Hash(data)); + std::string file_path = + std::filesystem::path(dir_path).append(Hash(data)).string(); FUZZTEST_CHECK_OK( RemoteFileSetContents(file_path, std::string(data.begin(), data.end()))); } @@ -155,17 +172,24 @@ std::string HashOfFileContents(std::string_view file_path) { } std::string ProcessAndThreadUniqueID(std::string_view prefix) { - // operator << is the only way to serialize std::this_thread::get_id(). std::ostringstream oss; +#if defined(_WIN32) + oss << prefix << GetCurrentProcessId() << "-" << GetCurrentThreadId(); +#else + // operator << is the only way to serialize std::this_thread::get_id(). oss << prefix << getpid() << "-" << std::this_thread::get_id(); +#endif return oss.str(); } std::string TemporaryLocalDirPath() { const char *TMPDIR = getenv("TMPDIR"); + if (!TMPDIR) TMPDIR = getenv("TEMP"); + if (!TMPDIR) TMPDIR = getenv("TMP"); std::string tmp = TMPDIR ? TMPDIR : "/tmp"; - return std::filesystem::path(tmp).append( - ProcessAndThreadUniqueID("centipede-")); + return std::filesystem::path(tmp) + .append(ProcessAndThreadUniqueID("centipede-")) + .string(); } // We need to maintain a global set of dirs that CreateLocalDirRemovedAtExit() @@ -189,13 +213,15 @@ static void RemoveDirsAtExit() { void CreateLocalDirRemovedAtExit(std::string_view path) { // Safeguard against removing dirs not created by TemporaryLocalDirPath(). - FUZZTEST_CHECK_NE(path.find("/centipede-"), std::string::npos); + FUZZTEST_CHECK(absl::StrContains(path, "/centipede-") || + absl::StrContains(path, "\\centipede-")); // Create the dir. std::error_code error; - std::filesystem::remove_all(path, error); - FUZZTEST_LOG_IF(ERROR, error) - << "Unable to clean up existing dir " << path << ": " << error.message(); - std::filesystem::create_directories(path); + std::filesystem::path p(path); + if (std::filesystem::exists(p, error)) { + std::filesystem::remove_all(p, error); + } + std::filesystem::create_directories(p, error); // Add to dirs_to_delete_at_exit. absl::MutexLock lock(dirs_to_delete_at_exit_mutex); if (!dirs_to_delete_at_exit) { @@ -206,7 +232,7 @@ void CreateLocalDirRemovedAtExit(std::string_view path) { } ScopedFile::ScopedFile(std::string_view dir_path, std::string_view name) - : my_path_(std::filesystem::path(dir_path) / name) {} + : my_path_((std::filesystem::path(dir_path) / name).string()) {} ScopedFile::~ScopedFile() { std::error_code error; @@ -359,16 +385,52 @@ std::vector RandomWeightedSubset(absl::Span set, return res; } +#if defined(_WIN32) +static LONG CALLBACK +AutoCommitPageFaultHandler(PEXCEPTION_POINTERS ExceptionInfo) { + if (ExceptionInfo->ExceptionRecord->ExceptionCode == + EXCEPTION_ACCESS_VIOLATION) { + ULONG_PTR fault_addr = + ExceptionInfo->ExceptionRecord->ExceptionInformation[1]; + if (VirtualAlloc(reinterpret_cast(fault_addr), 1, MEM_COMMIT, + PAGE_READWRITE) != nullptr) { + return EXCEPTION_CONTINUE_EXECUTION; + } + } + return EXCEPTION_CONTINUE_SEARCH; +} +#endif + uint8_t *MmapNoReserve(size_t size) { +#if defined(_WIN32) + // Set up page fault handler to commit page on demand. + static bool installed_veh = []() { + AddVectoredExceptionHandler(1, AutoCommitPageFaultHandler); + return true; + }(); + (void)installed_veh; + // MEM_RESERVE has different semantics and does not contradict with + // MAP_NORESERVE for mmap. + auto result = VirtualAlloc(nullptr, size, MEM_RESERVE, PAGE_READWRITE); + FUZZTEST_CHECK(result != nullptr) + << "VirtualAlloc failed for size " << size << " err=" << GetLastError(); + return reinterpret_cast(result); +#else auto result = mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); FUZZTEST_CHECK(result != MAP_FAILED); return reinterpret_cast(result); +#endif } void Munmap(uint8_t *ptr, size_t size) { +#if defined(_WIN32) + BOOL result = VirtualFree(ptr, 0, MEM_RELEASE); + FUZZTEST_CHECK(result != 0); +#else auto result = munmap(ptr, size); FUZZTEST_CHECK_EQ(result, 0); +#endif } int PollTimeoutMs(absl::Duration timeout) { diff --git a/centipede/util_test.cc b/centipede/util_test.cc index 1629f9212..c5a6aaea7 100644 --- a/centipede/util_test.cc +++ b/centipede/util_test.cc @@ -32,6 +32,10 @@ #include "./common/hash.h" #include "./common/logging.h" +#if defined(_WIN32) +#define setenv(n, v, _r) _putenv_s(n, v) +#endif + namespace fuzztest::internal { TEST(UtilTest, AsString) { @@ -116,7 +120,8 @@ TEST(UtilTest, TemporaryLocalDirPath) { auto temp_dir = TemporaryLocalDirPath(); // Create dir, create a file there, write to file, read from it, remove dir. std::filesystem::create_directories(temp_dir); - std::string temp_file_path = std::filesystem::path(temp_dir).append("blah"); + std::string temp_file_path = + std::filesystem::path(temp_dir).append("blah").string(); ByteArray written_data{1, 2, 3}; WriteToLocalFile(temp_file_path, written_data); ByteArray read_data; @@ -162,8 +167,8 @@ TEST(UtilTest, CreateLocalDirRemovedAtExit) { EXPECT_TRUE(std::filesystem::exists(tmpdir)); setenv("CENTIPEDE_UTIL_TEST_TEMP_DIR", tmpdir.c_str(), 1); // Create two subdirs via CreateLocalDirRemovedAtExit. - std::string subdir1 = std::filesystem::path(tmpdir).append("1"); - std::string subdir2 = std::filesystem::path(tmpdir).append("2"); + std::string subdir1 = std::filesystem::path(tmpdir).append("1").string(); + std::string subdir2 = std::filesystem::path(tmpdir).append("2").string(); CreateLocalDirRemovedAtExit(subdir1); CreateLocalDirRemovedAtExit(subdir2); EXPECT_TRUE(std::filesystem::exists(subdir1)); @@ -293,6 +298,15 @@ TEST(UtilTest, RemoveSubset) { testing::ElementsAre(std::vector{1}, std::vector{3})); } +TEST(UtilTest, MmapTest) { + static constexpr size_t kBufSize = 1 << 30; // 1 GiB + auto* buf = MmapNoReserve(kBufSize); + ASSERT_NE(buf, nullptr); + EXPECT_EQ(buf[1234], 0); + EXPECT_EQ(buf[567890], 0); + Munmap(buf, kBufSize); +} + TEST(UtilTest, PollTimeoutMsWorks) { EXPECT_GT(PollTimeoutMs(absl::ZeroDuration()), 0); EXPECT_GT(PollTimeoutMs(-absl::InfiniteDuration()), 0); diff --git a/common/test_util.cc b/common/test_util.cc index 3232d551f..4e04ecf43 100644 --- a/common/test_util.cc +++ b/common/test_util.cc @@ -18,10 +18,23 @@ #include #include // NOLINT +#if !defined(_WIN32) +#include +#else +#define WIN32_LEAN_AND_MEAN +#define NOGDI +#include +#include +#endif + #include "gtest/gtest.h" #include "absl/strings/str_cat.h" #include "./common/logging.h" +#if defined(_WIN32) +#define setenv(n, v, _r) _putenv_s(n, v) +#endif + namespace fuzztest::internal { std::filesystem::path GetTestTempDir(std::string_view subdir) { @@ -35,11 +48,18 @@ std::filesystem::path GetTestTempDir(std::string_view subdir) { FUZZTEST_CHECK(!error) << "Failed to create dir: " VV(dir) << error.message(); } - return std::filesystem::canonical(dir); + return std::filesystem::absolute(dir); } std::string GetTempFilePath(std::string_view subdir, size_t i) { - return GetTestTempDir(subdir) / absl::StrCat("tmp.", getpid(), ".", i); + return (GetTestTempDir(subdir) / absl::StrCat("tmp.", +#if defined(_WIN32) + GetCurrentProcessId(), +#else + getpid(), +#endif + ".", i)) + .string(); } std::filesystem::path GetTestRunfilesDir() { @@ -59,6 +79,13 @@ std::filesystem::path GetDataDependencyFilepath(std::string_view rel_path) { const auto runfiles_dir = GetTestRunfilesDir(); auto path = runfiles_dir; path.append(rel_path); + std::error_code ec; + if (std::filesystem::exists(path, ec)) return path; +#if defined(_WIN32) + auto win_path = path; + win_path += ".exe"; + if (std::filesystem::exists(win_path, ec)) return win_path; +#endif FUZZTEST_CHECK(std::filesystem::exists(path)) // << "No such path: " << VV(path) << VV(runfiles_dir) << VV(rel_path); return path; diff --git a/common/test_util.h b/common/test_util.h index c859bff94..e363f1868 100644 --- a/common/test_util.h +++ b/common/test_util.h @@ -79,7 +79,7 @@ class TempDir { const std::filesystem::path& path() const { return path_; } std::string GetFilePath(std::string_view file_name) const { - return path_ / file_name; + return (path_ / file_name).string(); } std::string CreateSubdir(std::string_view name) const {