No public description

PiperOrigin-RevId: 953590931
diff --git a/.github/workflows/bazel_test_centipede.yml b/.github/workflows/bazel_test_centipede.yml
index 1bbcba0..d8ac95e 100644
--- a/.github/workflows/bazel_test_centipede.yml
+++ b/.github/workflows/bazel_test_centipede.yml
@@ -25,109 +25,149 @@
   workflow_dispatch:
 
 jobs:
-  run_tests:
-    name: Run Centipede tests
-    # TODO(xinhaoyuan): Bump to 24.04 after https://github.com/llvm/llvm-project/issues/102443
-    # is fixed.
-    runs-on: ubuntu-22.04
-    timeout-minutes: 60
-    strategy:
-      matrix:
-        config: ['default', 'noriegeli', 'asan']
-    steps:
-      - name: Disable core dumping and piping due to slowness
-        run: |
-          sudo sysctl -w kernel.core_pattern=""
-      - name: Checkout repository
-        uses: actions/checkout@v4
-      - 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
-        with:
-          path: "~/.cache/bazel"
-          key: bazel-centipede-cache-${{ matrix.config }}
-          restore-keys: bazel-centipede-cache-${{ matrix.config }}-
-      - name: Run unit tests
-        if: matrix.config == 'default'
-        run: |
-          bazel test --local_test_jobs=1 --test_output=streamed centipede:all
-      - name: Run e2e tests
-        if: matrix.config == 'default'
-        run: |
-          bazel test --test_output=errors centipede/testing:instrumentation_test centipede/testing:runner_test
-      - name: Run puzzles
-        if: matrix.config == 'default'
-        run: |
-          bazel test --test_output=errors --test_timeout=600 centipede/puzzles:all
-      - name: Run puzzles with ASAN
-        if: matrix.config == 'asan'
-        run: |
-          bazel test --test_output=errors --linkopt=-fsanitize=address --copt=-fsanitize=address --test_env=ASAN_OPTIONS=detect_leaks=0 --test_timeout=600 centipede/puzzles:all
-      - name: Run unit tests without Riegeli
-        if: matrix.config == 'noriegeli'
-        run: |
-          bazel test --no//fuzztest:use_riegeli --local_test_jobs=1 --test_output=streamed centipede:all
-      - name: Run e2e tests without Riegeli
-        if: matrix.config == 'noriegeli'
-        run: |
-          bazel test --no//fuzztest:use_riegeli --test_output=errors centipede/testing:instrumentation_test centipede/testing:runner_test
-      - name: Run puzzles without Riegeli
-        if: matrix.config == 'noriegeli'
-        run: |
-          bazel test --no//fuzztest:use_riegeli --test_output=errors --test_timeout=600 centipede/puzzles:all
-      - name: Run puzzles without Riegeli with ASAN
-        if: matrix.config == 'noriegeli'
-        run: |
-          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
-        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
+  # run_tests:
+  #   name: Run Centipede tests
+  #   # TODO(xinhaoyuan): Bump to 24.04 after https://github.com/llvm/llvm-project/issues/102443
+  #   # is fixed.
+  #   runs-on: ubuntu-22.04
+  #   timeout-minutes: 60
+  #   strategy:
+  #     matrix:
+  #       config: ['default', 'noriegeli', 'asan']
+  #   steps:
+  #     - name: Disable core dumping and piping due to slowness
+  #       run: |
+  #         sudo sysctl -w kernel.core_pattern=""
+  #     - name: Checkout repository
+  #       uses: actions/checkout@v4
+  #     - 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
+  #       with:
+  #         path: "~/.cache/bazel"
+  #         key: bazel-centipede-cache-${{ matrix.config }}
+  #         restore-keys: bazel-centipede-cache-${{ matrix.config }}-
+  #     - name: Run unit tests
+  #       if: matrix.config == 'default'
+  #       run: |
+  #         bazel test --local_test_jobs=1 --test_output=streamed centipede:all
+  #     - name: Run e2e tests
+  #       if: matrix.config == 'default'
+  #       run: |
+  #         bazel test --test_output=errors centipede/testing:instrumentation_test centipede/testing:runner_test
+  #     - name: Run puzzles
+  #       if: matrix.config == 'default'
+  #       run: |
+  #         bazel test --test_output=errors --test_timeout=600 centipede/puzzles:all
+  #     - name: Run puzzles with ASAN
+  #       if: matrix.config == 'asan'
+  #       run: |
+  #         bazel test --test_output=errors --linkopt=-fsanitize=address --copt=-fsanitize=address --test_env=ASAN_OPTIONS=detect_leaks=0 --test_timeout=600 centipede/puzzles:all
+  #     - name: Run unit tests without Riegeli
+  #       if: matrix.config == 'noriegeli'
+  #       run: |
+  #         bazel test --no//fuzztest:use_riegeli --local_test_jobs=1 --test_output=streamed centipede:all
+  #     - name: Run e2e tests without Riegeli
+  #       if: matrix.config == 'noriegeli'
+  #       run: |
+  #         bazel test --no//fuzztest:use_riegeli --test_output=errors centipede/testing:instrumentation_test centipede/testing:runner_test
+  #     - name: Run puzzles without Riegeli
+  #       if: matrix.config == 'noriegeli'
+  #       run: |
+  #         bazel test --no//fuzztest:use_riegeli --test_output=errors --test_timeout=600 centipede/puzzles:all
+  #     - name: Run puzzles without Riegeli with ASAN
+  #       if: matrix.config == 'noriegeli'
+  #       run: |
+  #         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
+  #       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
+  #   timeout-minutes: 60
+  #   steps:
+  #     - name: Checkout repository
+  #       uses: actions/checkout@v4
+  #     - name: Restore latest cache
+  #       uses: actions/cache/restore@v4
+  #       with:
+  #         path: "~/.cache/bazel"
+  #         key: bazel-centipede-cache-mac
+  #         restore-keys: bazel-centipede-cache-mac-
+  #     - name: Add LLVM symbolizer to path
+  #       run: |
+  #         ln -s $(brew --prefix llvm@18)/bin/llvm-symbolizer /usr/local/bin
+  #     - name: Run unit tests
+  #       if: ${{ !cancelled() }}
+  #       run: |
+  #         # Timed wait and rusage is inaccurate for unknown reasons in MacOS for a few testing
+  #         # environments, thus disabling a few related non-critical tests.
+  #         bazel --output_user_root="${HOME}/.cache/bazel" test --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli -- centipede:all \
+  #               -centipede:periodic_action_test -centipede:rusage_profiler_test -centipede:rusage_stats_test && \
+  #         bazel --output_user_root="${HOME}/.cache/bazel" test --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli --test_filter='-*ValidateTimelapseSnapshots' -- centipede:rusage_profiler_test && \
+  #         bazel --output_user_root="${HOME}/.cache/bazel" test --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli --test_filter='-RUsageMemoryTest.Accuracy' -- centipede:rusage_stats_test
+  #     - name: Run e2e tests without Riegeli
+  #       if: ${{ !cancelled() }}
+  #       run: |
+  #         bazel --output_user_root="${HOME}/.cache/bazel" test --test_output=errors --no//fuzztest:use_riegeli centipede/testing:instrumentation_test centipede/testing:runner_test centipede/testing:multi_dso_test
+  #     - name: Run puzzles without Riegeli
+  #       if: ${{ !cancelled() }}
+  #       run: |
+  #         bazel --output_user_root="${HOME}/.cache/bazel" test --test_output=errors --no//fuzztest:use_riegeli --test_timeout=600 centipede/puzzles:all
+  #     - name: Run puzzles without Riegeli with ASAN
+  #       if: ${{ !cancelled() }}
+  #       run: |
+  #         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
+  #       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@v4
-      - name: Restore latest cache
-        uses: actions/cache/restore@v4
-        with:
-          path: "~/.cache/bazel"
-          key: bazel-centipede-cache-mac
-          restore-keys: bazel-centipede-cache-mac-
-      - name: Add LLVM symbolizer to path
-        run: |
-          ln -s $(brew --prefix llvm@18)/bin/llvm-symbolizer /usr/local/bin
-      - name: Run unit tests
-        if: ${{ !cancelled() }}
-        run: |
-          # Timed wait and rusage is inaccurate for unknown reasons in MacOS for a few testing
-          # environments, thus disabling a few related non-critical tests.
-          bazel --output_user_root="${HOME}/.cache/bazel" test --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli -- centipede:all \
-                -centipede:periodic_action_test -centipede:rusage_profiler_test -centipede:rusage_stats_test && \
-          bazel --output_user_root="${HOME}/.cache/bazel" test --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli --test_filter='-*ValidateTimelapseSnapshots' -- centipede:rusage_profiler_test && \
-          bazel --output_user_root="${HOME}/.cache/bazel" test --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli --test_filter='-RUsageMemoryTest.Accuracy' -- centipede:rusage_stats_test
-      - name: Run e2e tests without Riegeli
-        if: ${{ !cancelled() }}
-        run: |
-          bazel --output_user_root="${HOME}/.cache/bazel" test --test_output=errors --no//fuzztest:use_riegeli centipede/testing:instrumentation_test centipede/testing:runner_test centipede/testing:multi_dso_test
-      - name: Run puzzles without Riegeli
-        if: ${{ !cancelled() }}
-        run: |
-          bazel --output_user_root="${HOME}/.cache/bazel" test --test_output=errors --no//fuzztest:use_riegeli --test_timeout=600 centipede/puzzles:all
+        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 puzzles without Riegeli with ASAN
         if: ${{ !cancelled() }}
         run: |
-          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
+          bazelisk test --disk_cache=~/.cache/bazel --platform_suffix=asan `
+            --extra_toolchains=@local_config_cc//:cc-toolchain-x64_windows-clang-cl `
+            --extra_execution_platforms=//:x64_windows-clang-cl `
+            --//fuzztest:use_riegeli=false `
+            --copt=-fsanitize=address `
+            --copt=-fno-sanitize-blacklist <# https://github.com/llvm/llvm-project/issues/34691 #> `
+            --linkopt=-wholearchive:clang_rt.asan_dynamic-x86_64.lib `
+            --linkopt=-Wholearchive:clang_rt.asan_dynamic_runtime_thunk-x86_64.lib `
+            --linkopt=/opt:nolldtailmerge <# https://github.com/llvm/llvm-project/issues/62078 #> `
+            --test_output=errors --enable_runfiles --local_test_jobs=1 `
+            "--test_env=PATH=C:\Program Files\LLVM\lib\clang\20\lib\windows;%PATH%" `
+            --test_env=ASAN_OPTIONS=quarantine_size_mb=16 `
+            --test_tag_filters=-no-windows --build_tests_only=true `
+            centipede/puzzles:all
       - name: Save new cache based on main
-        if: github.ref == 'refs/heads/main'
-        uses: actions/cache/save@v4
+        # if: github.ref == 'refs/heads/main'
+        uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25  # v5.1.0
         with:
           path: "~/.cache/bazel"
-          key: bazel-centipede-cache-mac-${{ github.run_id }}
+          key: bazel-centipede-cache-win-${{ github.run_id }}
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..4f91cb3
--- /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 4610482..741fdab 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -23,6 +23,10 @@
     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 3ad6fae..b3716bf 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -215,6 +215,10 @@
     name = "util",
     srcs = ["util.cc"],
     hdrs = ["util.h"],
+    linkopts = select({
+        "@platforms//os:windows": ["-DEFAULTLIB:ws2_32.lib"],
+        "//conditions:default": [],
+    }),
     deps = [
         ":feature",
         "@abseil-cpp//absl/base:core_headers",
@@ -246,10 +250,12 @@
     srcs = ["config_init.cc"],
     hdrs = ["config_init.h"],
     deps = [
+        ":centipede_flags",
         ":config_util",
         "@abseil-cpp//absl/base:core_headers",
         "@abseil-cpp//absl/base:log_severity",
         "@abseil-cpp//absl/flags:config",
+        "@abseil-cpp//absl/flags:flag",
         "@abseil-cpp//absl/flags:parse",
         "@abseil-cpp//absl/log:globals",
         "@abseil-cpp//absl/log:initialize",
@@ -340,7 +346,11 @@
         "@abseil-cpp//absl/functional:any_invocable",
         "@abseil-cpp//absl/synchronization",
         "@abseil-cpp//absl/time",
-    ],
+    ] + select({
+        "@platforms//os:windows": [],
+        "//conditions:default": [
+        ],
+    }),
 )
 
 cc_library(
@@ -349,6 +359,7 @@
     hdrs = ["stats.h"],
     linkopts = select({
         "@platforms//os:macos": [],
+        "@platforms//os:windows": [],
         "//conditions:default": [
             "-latomic",  # for std::atomic::load()/store().
         ],
@@ -417,6 +428,7 @@
     copts = DISABLE_SANCOV_COPTS,
     linkopts = select({
         "@platforms//os:macos": [],
+        "@platforms//os:windows": [],
         "//conditions:default": [
             "-lrt",  # for shm_open
         ],
@@ -605,6 +617,7 @@
         "@abseil-cpp//absl/synchronization",
         "@abseil-cpp//absl/time",
         "@com_google_fuzztest//common:logging",
+        "@com_google_fuzztest//fuzztest/internal:escaping",
     ],
 )
 
@@ -629,6 +642,10 @@
     name = "centipede_callbacks",
     srcs = ["centipede_callbacks.cc"],
     hdrs = ["centipede_callbacks.h"],
+    linkopts = select({
+        "@platforms//os:windows": ["-DEFAULTLIB:ws2_32.lib"],
+        "//conditions:default": [],
+    }),
     deps = [
         ":binary_info",
         ":byte_array_mutator",
@@ -683,6 +700,7 @@
     hdrs = ["stop.h"],
     linkopts = select({
         "@platforms//os:macos": [],
+        "@platforms//os:windows": [],
         "//conditions:default": [
             "-latomic",  # for std::atomic::load()/store().
         ],
@@ -1007,6 +1025,10 @@
         "runner_utils.h",
     ],
     hdrs = ["engine_worker_abi.h"],
+    linkopts = select({
+        "@platforms//os:windows": ["-DEFAULTLIB:ws2_32.lib"],
+        "//conditions:default": [],
+    }),
     deps = [
         ":engine_abi",
         ":execution_metadata",
@@ -1014,6 +1036,7 @@
         ":runner_request",
         ":runner_result",
         ":shared_memory_blob_sequence",
+        ":util",
         "@abseil-cpp//absl/base:nullability",
         "@com_google_fuzztest//common:defs",
     ],
@@ -1024,6 +1047,7 @@
     srcs = ["engine_controller_with_subprocess.cc"],
     hdrs = ["engine_controller_abi.h"],
     deps = [
+        ":command",
         "@com_google_fuzztest//centipede:engine_abi",
         "@com_google_fuzztest//fuzztest/internal:escaping",
     ],
@@ -1065,22 +1089,29 @@
 # Need to allow MSAN to avoid false positives when interacting with dependencies.
 RUNNER_COPTS = [
     "-fsanitize-coverage=0",
-    "-fno-sanitize=address,hwaddress,thread,undefined",
-]
+] + select({
+    "@platforms//os:windows": [],
+    "//conditions:default": ["-fno-sanitize=address,hwaddress,thread,undefined"],
+})
 
 RUNNER_SANITIZED_COPTS = [
     "-fsanitize-coverage=0",
 ]
 
-RUNNER_LINKOPTS = [
-    "-ldl",  # for dlsym
-    "-lpthread",  # for pthread_once
-] + select({
+RUNNER_LINKOPTS = select({
+    "@platforms//os:windows": [
+        "-DEFAULTLIB:psapi.lib",
+        "-DEFAULTLIB:ws2_32.lib",
+    ],
     "@platforms//os:macos": [
+        "-ldl",  # for dlsym
+        "-lpthread",  # for pthread_once
         # For compatible behavior on weak symbols.
         "-Wl,-undefined,dynamic_lookup",
     ],
     "//conditions:default": [
+        "-ldl",  # for dlsym
+        "-lpthread",  # for pthread_once
         "-lrt",  # for shm_open
     ],
 })
@@ -1997,6 +2028,7 @@
         ":centipede_callbacks",
         ":centipede_default_callbacks",
         ":centipede_interface",
+        ":command",
         ":environment",
         ":feature",
         ":mutation_data",
diff --git a/centipede/binary_info.cc b/centipede/binary_info.cc
index 0d02c53..246613b 100644
--- a/centipede/binary_info.cc
+++ b/centipede/binary_info.cc
@@ -64,7 +64,7 @@
       ":arg2=", cf_table_path.path(), ":arg3=", dso_table_path.path(), ":"));
   Command::Options cmd_options;
   cmd_options.env_diff = std::move(env_diff);
-  cmd_options.stdout_file_prefix = log_prefix;
+  cmd_options.stdout_file_prefix = log_prefix.string();
   Command cmd{binary_path_with_args, std::move(cmd_options)};
   int exit_code = cmd.Execute();
   if (exit_code != EXIT_SUCCESS) {
@@ -129,20 +129,20 @@
   std::string symbol_table_contents;
   // TODO(b/295978603): move calculation of paths into WorkDir class.
   FUZZTEST_CHECK_OK(RemoteFileGetContents(
-      (std::filesystem::path(dir) / kSymbolTableFileName).c_str(),
+      (std::filesystem::path(dir) / kSymbolTableFileName).string(),
       symbol_table_contents));
   std::istringstream symbol_table_stream(symbol_table_contents);
   symbols.ReadFromLLVMSymbolizer(symbol_table_stream);
 
   std::string pc_table_contents;
   FUZZTEST_CHECK_OK(RemoteFileGetContents(
-      (std::filesystem::path(dir) / kPCTableFileName).c_str(),
+      (std::filesystem::path(dir) / kPCTableFileName).string(),
       pc_table_contents));
   std::istringstream pc_table_stream(pc_table_contents);
   pc_table = ReadPcTable(pc_table_stream);
 
   cf_table =
-      ReadCfTable((std::filesystem::path(dir) / kCfTableFileName).c_str());
+      ReadCfTable((std::filesystem::path(dir) / kCfTableFileName).string());
 }
 
 void BinaryInfo::Write(std::string_view dir) {
@@ -150,19 +150,19 @@
   symbols.WriteToLLVMSymbolizer(symbol_table_stream);
   // TODO(b/295978603): move calculation of paths into WorkDir class.
   FUZZTEST_CHECK_OK(RemoteFileSetContents(
-      (std::filesystem::path(dir) / kSymbolTableFileName).c_str(),
+      (std::filesystem::path(dir) / kSymbolTableFileName).string(),
       symbol_table_stream.str()));
 
   std::ostringstream pc_table_stream;
   WritePcTable(pc_table, pc_table_stream);
   FUZZTEST_CHECK_OK(RemoteFileSetContents(
-      (std::filesystem::path(dir) / kPCTableFileName).c_str(),
+      (std::filesystem::path(dir) / kPCTableFileName).string(),
       pc_table_stream.str()));
 
   std::ostringstream cf_table_stream;
   WriteCfTable(cf_table, cf_table_stream);
   FUZZTEST_CHECK_OK(RemoteFileSetContents(
-      (std::filesystem::path(dir) / kCfTableFileName).c_str(),
+      (std::filesystem::path(dir) / kCfTableFileName).string(),
       cf_table_stream.str()));
 }
 
diff --git a/centipede/byte_array_mutator.cc b/centipede/byte_array_mutator.cc
index fc2597d..bd64a84 100644
--- a/centipede/byte_array_mutator.cc
+++ b/centipede/byte_array_mutator.cc
@@ -292,7 +292,7 @@
                                           const ByteArray &other) {
   // Overwrite data[pos:pos+size] with other[first:first+size].
   // Overwrite no more than half of data.
-  size_t max_size = std::max(1UL, data.size() / 2);
+  size_t max_size = std::max(size_t{1}, data.size() / 2);
   size_t first = rng_() % other.size();
   max_size = std::min(max_size, other.size() - first);
   size_t size = 1 + rng_() % max_size;
diff --git a/centipede/centipede.cc b/centipede/centipede.cc
index e2a0ee3..6328d87 100644
--- a/centipede/centipede.cc
+++ b/centipede/centipede.cc
@@ -137,14 +137,18 @@
       function_filter_(env_.function_filter, symbols_),
       coverage_logger_(coverage_logger),
       stats_(stats),
-      input_filter_path_(std::filesystem::path(TemporaryLocalDirPath())
-                             .append("filter-input")),
+      input_filter_path_{[&] {
+        auto dir = TemporaryLocalDirPath();
+        CreateLocalDirRemovedAtExit(dir);
+        return (std::filesystem::path(dir) / "filter-input").string();
+      }()},
       input_filter_cmd_{[&] {
         Command::Options cmd_options;
         cmd_options.args = {input_filter_path_};
         cmd_options.stdout_file_prefix =
             std::filesystem::path(TemporaryLocalDirPath())
-                .append("filter-input.log");
+                .append("filter-input.log")
+                .string();
         cmd_options.stderr_file_prefix = cmd_options.stdout_file_prefix;
         return Command{env_.input_filter, std::move(cmd_options)};
       }()},
@@ -243,7 +247,8 @@
         std::vector<std::string> reproducer_paths,
         RemoteListFiles(reproducer_dir, /*recursively=*/false));
     for (const auto &reproducer_path : reproducer_paths) {
-      std::string id = std::filesystem::path{reproducer_path}.filename();
+      std::string id =
+          std::filesystem::path{reproducer_path}.filename().string();
       if (auto [_it, inserted] = crash_ids.insert(id); !inserted) {
         continue;
       }
@@ -1086,11 +1091,12 @@
       auto hash = Hash(one_input);
       auto crash_dir = wd_.CrashReproducerDirPaths().MyShard();
       FUZZTEST_CHECK_OK(RemoteMkdir(crash_dir));
-      std::string input_file_path = std::filesystem::path(crash_dir) / hash;
+      std::string input_file_path =
+          (std::filesystem::path(crash_dir) / hash).string();
       auto crash_metadata_dir = wd_.CrashMetadataDirPaths().MyShard();
       FUZZTEST_CHECK_OK(RemoteMkdir(crash_metadata_dir));
       std::string crash_metadata_path_prefix =
-          std::filesystem::path(crash_metadata_dir) / hash;
+          (std::filesystem::path(crash_metadata_dir) / hash).string();
       FUZZTEST_LOG(INFO)
           << log_prefix << "Detected crash-reproducing input:"
           << "\nInput index    : " << input_idx << "\nInput bytes    : "
@@ -1139,21 +1145,25 @@
   FUZZTEST_CHECK_OK(RemoteMkdir(crash_dir));
   std::string crashing_batch_name =
       absl::StrCat("crashing_batch-", suspect_hash);
-  std::string save_dir = std::filesystem::path(crash_dir) / crashing_batch_name;
+  std::string save_dir =
+      (std::filesystem::path(crash_dir) / crashing_batch_name).string();
   FUZZTEST_CHECK_OK(RemoteMkdir(save_dir));
   FUZZTEST_LOG(INFO) << log_prefix
                      << "Saving used inputs from batch to: " << save_dir;
   for (int i = 0; i <= suspect_input_idx; ++i) {
     const auto &one_input = input_vec[i];
     auto hash = Hash(one_input);
-    std::string file_path = std::filesystem::path(save_dir).append(
-        absl::StrFormat("input-%010d-%s", i, hash));
+    std::string file_path =
+        std::filesystem::path(save_dir)
+            .append(absl::StrFormat("input-%010d-%s", i, hash))
+            .string();
     FUZZTEST_CHECK_OK(RemoteFileSetContents(file_path, one_input));
   }
   auto crash_metadata_dir = wd_.CrashMetadataDirPaths().MyShard();
   FUZZTEST_CHECK_OK(RemoteMkdir(crash_metadata_dir));
   std::string crash_metadata_file_path =
-      std::filesystem::path(crash_metadata_dir) / crashing_batch_name;
+      (std::filesystem::path(crash_metadata_dir) / crashing_batch_name)
+          .string();
   FUZZTEST_LOG(INFO) << log_prefix << "Saving crash metadata to: "
                      << crash_metadata_file_path;
   FUZZTEST_CHECK_OK(RemoteFileSetContents(crash_metadata_file_path,
diff --git a/centipede/centipede_callbacks.cc b/centipede/centipede_callbacks.cc
index c615992..440203f 100644
--- a/centipede/centipede_callbacks.cc
+++ b/centipede/centipede_callbacks.cc
@@ -14,12 +14,26 @@
 
 #include "./centipede/centipede_callbacks.h"
 
+#if !defined(_WIN32)
 #include <fcntl.h>
 #include <sys/poll.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <sys/un.h>
+#include <unistd.h>
+#else
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+// clang-format off
+// Must be before <afunix.h>
+#include <ws2tcpip.h>
+// clang-format on
+#include <afunix.h>
+#include <io.h>
+#include <windows.h>
+#include <winsock2.h>
+#endif
 
 #include <algorithm>
 #include <cerrno>
@@ -41,6 +55,7 @@
 #include "absl/strings/ascii.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_join.h"
+#include "absl/strings/str_replace.h"
 #include "absl/strings/str_split.h"
 #include "absl/synchronization/mutex.h"
 #include "absl/time/clock.h"
@@ -52,6 +67,7 @@
 #include "./centipede/mutation_data.h"
 #include "./centipede/runner_request.h"
 #include "./centipede/runner_result.h"
+#include "./centipede/shared_memory_blob_sequence.h"
 #include "./centipede/stop.h"
 #include "./centipede/util.h"
 #include "./centipede/workdir.h"
@@ -62,14 +78,36 @@
 
 namespace fuzztest::internal {
 
+namespace {
+
+#if defined(_WIN32)
+using socket_t = SOCKET;
+constexpr socket_t kInvalidSocket = INVALID_SOCKET;
+#else
+using socket_t = int;
+constexpr socket_t kInvalidSocket = -1;
+#endif
+
+std::string RunnerFlagEscape(std::string_view value) {
+  return absl::StrReplaceAll(value, {
+                                        {":", "\\:"},
+                                        {"\\", "\\\\"},
+                                    });
+}
+
+}  // namespace
+
 class CentipedeCallbacks::PersistentModeServer {
  public:
   explicit PersistentModeServer(std::string server_path)
       : server_path_(std::move(server_path)) {
     FUZZTEST_CHECK(!server_path_.empty());
+#if defined(_WIN32)
+    EnsureWinsockInitialized();
+#endif
 
     server_socket_ = socket(AF_UNIX, SOCK_STREAM, 0);
-    FUZZTEST_PCHECK(server_socket_ != -1);
+    FUZZTEST_PCHECK(server_socket_ != kInvalidSocket);
 
     SetCloseOnExec(server_socket_);
     SetNonBlocking(server_socket_);
@@ -84,13 +122,23 @@
           << " is too long. Truncating it to " << new_server_path;
       server_path_ = std::move(new_server_path);
     }
-    server_path_.copy(server_addr.sun_path, sizeof(server_addr.sun_path));
+    std::string win_sock_path = server_path_;
+    std::memset(server_addr.sun_path, 0, sizeof(server_addr.sun_path));
+    std::strncpy(server_addr.sun_path, win_sock_path.c_str(),
+                 sizeof(server_addr.sun_path) - 1);
 
+#if !defined(_WIN32)
     static constexpr int kEnable = 1;
     FUZZTEST_PCHECK(setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR,
-                               &kEnable, sizeof(kEnable)) != -1);
-    FUZZTEST_PCHECK(bind(server_socket_, (struct sockaddr*)&server_addr,
-                         sizeof(server_addr)) != -1);
+                               reinterpret_cast<const char*>(&kEnable),
+                               sizeof(kEnable)) != -1);
+#endif
+    std::error_code ec;
+    std::filesystem::remove(server_path_, ec);
+    std::filesystem::remove(win_sock_path, ec);
+    int bind_ret = bind(server_socket_, (struct sockaddr*)&server_addr,
+                        sizeof(server_addr));
+    FUZZTEST_PCHECK(bind_ret != -1);
 
     // Expect one child process to connect to the server.
     static constexpr int kBacklog = 1;
@@ -105,7 +153,7 @@
     if (!EnsureConnection(deadline)) {
       return false;
     }
-    FUZZTEST_CHECK_NE(conn_socket_, -1);
+    FUZZTEST_CHECK(conn_socket_ != kInvalidSocket);
     FUZZTEST_CHECK(!in_batch_);
     if (!WriteFd(conn_socket_, deadline, PersistentModeRequest::kRunBatch)) {
       FUZZTEST_LOG(ERROR)
@@ -123,7 +171,7 @@
   // code if running without persistent mode, hence the name). Returns false
   // otherwise.
   bool WaitBatch(absl::Time deadline, int& exit_code) {
-    FUZZTEST_CHECK_NE(conn_socket_, -1);
+    FUZZTEST_CHECK(conn_socket_ != kInvalidSocket);
     FUZZTEST_CHECK(in_batch_);
     if (!ReadFd(conn_socket_, deadline, exit_code)) {
       FUZZTEST_LOG(ERROR)
@@ -139,7 +187,7 @@
 
   void RequestExit(absl::Time deadline) {
     if (!EnsureConnection(deadline)) return;
-    FUZZTEST_CHECK_NE(conn_socket_, -1);
+    FUZZTEST_CHECK(conn_socket_ != kInvalidSocket);
     if (!WriteFd(conn_socket_, deadline, PersistentModeRequest::kExit)) {
       FUZZTEST_LOG(ERROR)
           << "Failed to request the persistent mode client to exit - "
@@ -149,13 +197,17 @@
   }
 
   ~PersistentModeServer() {
-    if (conn_socket_ != -1) {
+    if (conn_socket_ != kInvalidSocket) {
       Disconnect();
     }
 
-    FUZZTEST_CHECK_NE(server_socket_, -1);
+    FUZZTEST_CHECK(server_socket_ != kInvalidSocket);
+#if defined(_WIN32)
+    closesocket(server_socket_);
+#else
     FUZZTEST_PCHECK(close(server_socket_) != -1);
-    server_socket_ = -1;
+#endif
+    server_socket_ = kInvalidSocket;
 
     std::error_code ec;
     FUZZTEST_CHECK(!server_path_.empty());
@@ -167,27 +219,51 @@
   }
 
  private:
-  static void SetCloseOnExec(int fd) {
+  static void SetCloseOnExec(socket_t fd) {
+#if defined(_WIN32)
+    SetHandleInformation(reinterpret_cast<HANDLE>(fd), HANDLE_FLAG_INHERIT, 0);
+#else
     int flags = fcntl(fd, F_GETFD);
     FUZZTEST_PCHECK(flags != -1);
     flags |= FD_CLOEXEC;
     FUZZTEST_PCHECK(fcntl(fd, F_SETFD, flags) != -1);
+#endif
   }
 
-  static void SetNonBlocking(int fd) {
+  static void SetNonBlocking(socket_t fd) {
+#if defined(_WIN32)
+    u_long mode = 1;
+    ioctlsocket(fd, FIONBIO, &mode);
+#else
     int flags = fcntl(fd, F_GETFL);
     FUZZTEST_PCHECK(flags != -1);
     flags |= O_NONBLOCK;
     FUZZTEST_PCHECK(fcntl(fd, F_SETFL, flags) != -1);
+#endif
   }
 
-  std::string_view FdName(int fd) {
+  std::string_view FdName(socket_t fd) {
     return fd == server_socket_ ? "server" : "connection";
   }
 
-  bool PollFd(int fd, int event, absl::Time deadline) {
+  bool PollFd(socket_t fd, int event, absl::Time deadline) {
     FUZZTEST_CHECK(event == POLLIN || event == POLLOUT)
         << "`event` must be POLLIN or POLLOUT";
+#if defined(_WIN32)
+    fd_set fds;
+    FD_ZERO(&fds);
+    FD_SET(fd, &fds);
+    timeval tv{};
+    int timeout_ms = PollTimeoutMs(deadline - absl::Now());
+    if (timeout_ms >= 0) {
+      tv.tv_sec = timeout_ms / 1000;
+      tv.tv_usec = (timeout_ms % 1000) * 1000;
+    }
+    int select_ret =
+        select(0, event == POLLIN ? &fds : NULL, event == POLLOUT ? &fds : NULL,
+               NULL, timeout_ms >= 0 ? &tv : NULL);
+    return select_ret > 0;
+#else
     struct pollfd poll_fd{};
     int poll_ret = -1;
     do {
@@ -196,18 +272,13 @@
     } while (poll_ret < 0 && errno == EINTR);
     if (poll_ret == 1 && (poll_fd.revents & (event | POLLHUP)) == event) {
       return true;
-    } else if (poll_ret < 0) {
-      FUZZTEST_PLOG(ERROR) << "Persistent mode: poll() failed on "
-                           << FdName(fd);
-    } else if (poll_ret == 0) {
-      FUZZTEST_LOG(ERROR) << "Persistent mode: poll() timed out on "
-                          << FdName(fd);
     }
+#endif
     return false;
   }
 
   template <typename T>
-  bool ReadFd(int fd, absl::Time deadline, T& data) {
+  bool ReadFd(socket_t fd, absl::Time deadline, T& data) {
     static_assert(
         std::is_trivial_v<T> && std::is_standard_layout_v<T>,
         "ReadFd() must be used on a trivial type with standard layout");
@@ -217,7 +288,11 @@
       if (!PollFd(fd, POLLIN, deadline)) {
         return false;
       }
+#if defined(_WIN32)
+      auto r = recv(fd, cursor, static_cast<int>(end - cursor), 0);
+#else
       ssize_t r = read(fd, cursor, end - cursor);
+#endif
       if (r > 0) {
         cursor += r;
         FUZZTEST_CHECK(cursor <= end)
@@ -228,7 +303,12 @@
         FUZZTEST_LOG(ERROR) << "read() returned 0 early on " << FdName(fd);
         return false;
       }
+#if defined(_WIN32)
+      if (WSAGetLastError() == WSAEWOULDBLOCK || WSAGetLastError() == WSAEINTR)
+        continue;
+#else
       if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue;
+#endif
       FUZZTEST_PLOG(ERROR) << "read() fails on " << FdName(fd);
       return false;
     }
@@ -236,7 +316,7 @@
   }
 
   template <typename T>
-  bool WriteFd(int fd, absl::Time deadline, const T& data) {
+  bool WriteFd(socket_t fd, absl::Time deadline, const T& data) {
     static_assert(
         std::is_trivial_v<T> && std::is_standard_layout_v<T>,
         "WriteFd() must be used on a trivial type with standard layout");
@@ -246,7 +326,11 @@
       if (!PollFd(fd, POLLOUT, deadline)) {
         return false;
       }
+#if defined(_WIN32)
+      auto r = send(fd, cursor, static_cast<int>(end - cursor), 0);
+#else
       ssize_t r = write(fd, cursor, end - cursor);
+#endif
       if (r > 0) {
         cursor += r;
         FUZZTEST_CHECK(cursor <= end)
@@ -257,7 +341,12 @@
         FUZZTEST_LOG(ERROR) << "write() returned 0 early on " << FdName(fd);
         return false;
       }
+#if defined(_WIN32)
+      if (WSAGetLastError() == WSAEWOULDBLOCK || WSAGetLastError() == WSAEINTR)
+        continue;
+#else
       if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue;
+#endif
       FUZZTEST_PLOG(ERROR) << "write() fails on " << FdName(fd);
       return false;
     }
@@ -265,22 +354,27 @@
   }
 
   bool EnsureConnection(absl::Time deadline) {
-    if (conn_socket_ != -1) return true;
+    if (conn_socket_ != kInvalidSocket) return true;
     // Since the runner always tries to connect to the persistent mode
     // socket at the beginning of the execution, waiting for the connection
     // should be fast if the the runner is able to connect at all. But we
     // need to give enough time for the binary to load and reach the runner
     // logic (120s should be enough).
     deadline = std::min(deadline, absl::Now() + absl::Seconds(120));
-    FUZZTEST_CHECK_NE(server_socket_, -1);
+    FUZZTEST_CHECK(server_socket_ != kInvalidSocket);
     do {
       if (!PollFd(server_socket_, POLLIN, deadline)) {
         return false;
       }
       conn_socket_ = accept(server_socket_, nullptr, 0);
-    } while (conn_socket_ == -1 &&
-             (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK));
-    FUZZTEST_PCHECK(conn_socket_ != -1);
+    } while (
+        conn_socket_ == kInvalidSocket &&
+#if defined(_WIN32)
+        (WSAGetLastError() == WSAEINTR || WSAGetLastError() == WSAEWOULDBLOCK));
+#else
+        (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK));
+#endif
+    FUZZTEST_CHECK(conn_socket_ != kInvalidSocket);
 
     SetCloseOnExec(conn_socket_);
     SetNonBlocking(conn_socket_);
@@ -288,14 +382,18 @@
   }
 
   void Disconnect() {
-    FUZZTEST_CHECK_NE(conn_socket_, -1);
+    FUZZTEST_CHECK(conn_socket_ != kInvalidSocket);
+#if defined(_WIN32)
+    closesocket(conn_socket_);
+#else
     FUZZTEST_PCHECK(close(conn_socket_) != -1);
-    conn_socket_ = -1;
+#endif
+    conn_socket_ = kInvalidSocket;
   }
 
   std::string server_path_;
-  int server_socket_ = -1;
-  int conn_socket_ = -1;
+  socket_t server_socket_ = kInvalidSocket;
+  socket_t conn_socket_ = kInvalidSocket;
   bool in_batch_ = false;
 };
 
@@ -334,6 +432,26 @@
   }
 }
 
+CentipedeCallbacks::CentipedeCallbacks(const Environment& env,
+                                       StopCondition& stop_condition)
+    : env_(env),
+      stop_condition_(stop_condition),
+      byte_array_mutator_(env.knobs, GetRandomSeed(env.seed)),
+      fuzztest_mutator_(env.knobs, GetRandomSeed(env.seed)) {
+  inputs_blobseq_ = std::make_unique<SharedMemoryBlobSequence>(
+      shmem_name1_.c_str(), env.shmem_size_mb << 20, env.use_posix_shmem);
+  outputs_blobseq_ = std::make_unique<SharedMemoryBlobSequence>(
+      shmem_name2_.c_str(), env.shmem_size_mb << 20, env.use_posix_shmem);
+  CreateLocalDirRemovedAtExit(temp_dir_);
+  if (env.use_legacy_default_mutator) {
+    bool r = byte_array_mutator_.set_max_len(env.max_len);
+    FUZZTEST_CHECK(r);
+  } else {
+    bool r = fuzztest_mutator_.set_max_len(env.max_len);
+    FUZZTEST_CHECK(r);
+  }
+}
+
 std::string CentipedeCallbacks::ConstructRunnerFlags(
     std::string_view extra_flags, bool disable_coverage) {
   std::vector<std::string> flags = {
@@ -358,11 +476,12 @@
     if (env_.use_dataflow_features) flags.emplace_back("use_dataflow_features");
   }
   if (!env_.runner_dl_path_suffix.empty()) {
-    flags.emplace_back(
-        absl::StrCat("dl_path_suffix=", env_.runner_dl_path_suffix));
+    flags.emplace_back(absl::StrCat(
+        "dl_path_suffix=", RunnerFlagEscape(env_.runner_dl_path_suffix)));
   }
   if (!env_.pcs_file_path.empty())
-    flags.emplace_back(absl::StrCat("pcs_file_path=", env_.pcs_file_path));
+    flags.emplace_back(
+        absl::StrCat("pcs_file_path=", RunnerFlagEscape(env_.pcs_file_path)));
   if (!extra_flags.empty()) flags.emplace_back(extra_flags);
   flags.emplace_back("");
   return absl::StrJoin(flags, ":");
@@ -388,24 +507,34 @@
     // The current construction seems to be fine (usually below 100 bytes) for
     // the Linux limit (108 bytes), but we put the descriptive part to the end
     // make it still likely meaningful when truncated.
+    std::string tmp_prefix = getenv("TMPDIR") ? getenv("TMPDIR")
+                             : getenv("TEMP") ? getenv("TEMP")
+                             : getenv("TMP")  ? getenv("TMP")
+                                              : "/tmp";
     std::string server_path =
-        absl::StrCat(ProcessAndThreadUniqueID("/tmp/centipede-"), "-",
-                     Hash(binary), "-persistent-mode");
+        (std::filesystem::path(tmp_prefix) /
+         absl::StrCat(ProcessAndThreadUniqueID("centipede-"), "-", Hash(binary),
+                      "-pm.sock"))
+            .string();
     persistent_mode_server =
         std::make_unique<CentipedeCallbacks::PersistentModeServer>(
             std::move(server_path));
   }
   std::vector<std::string> env_diff = env_.env_diff_for_binaries;
   env_diff.push_back(ConstructRunnerFlags(
-      absl::StrCat(":shmem:test=", env_.test_name, ":arg1=",
-                   inputs_blobseq_.path(), ":arg2=", outputs_blobseq_.path(),
-                   ":failure_description_path=", failure_description_path_,
-                   ":failure_signature_path=", failure_signature_path_,
-                   persistent_mode_server == nullptr
-                       ? ""
-                       : absl::StrCat(":persistent_mode_socket=",
-                                      persistent_mode_server->server_path()),
-                   ":"),
+      absl::StrCat(
+          ":shmem:test=", RunnerFlagEscape(env_.test_name),
+          ":arg1=", RunnerFlagEscape(inputs_blobseq_->path()),
+          ":arg2=", RunnerFlagEscape(outputs_blobseq_->path()),
+          ":failure_description_path=",
+          RunnerFlagEscape(failure_description_path_),
+          ":failure_signature_path=", RunnerFlagEscape(failure_signature_path_),
+          persistent_mode_server == nullptr
+              ? ""
+              : absl::StrCat(
+                    ":persistent_mode_socket=",
+                    RunnerFlagEscape(persistent_mode_server->server_path())),
+          ":"),
       disable_coverage));
 
   if (env_.clang_coverage_binary == binary) {
@@ -539,8 +668,8 @@
   batch_result.ClearAndResize(inputs.size());
 
   // Reset the blobseqs.
-  inputs_blobseq_.Reset();
-  outputs_blobseq_.Reset();
+  inputs_blobseq_->Reset();
+  outputs_blobseq_->Reset();
 
   size_t num_inputs_written = 0;
 
@@ -550,7 +679,7 @@
     num_inputs_written = 1;
   } else {
     // Feed the inputs to inputs_blobseq_.
-    num_inputs_written = RequestExecution(inputs, inputs_blobseq_);
+    num_inputs_written = RequestExecution(inputs, *inputs_blobseq_);
   }
 
   if (num_inputs_written != inputs.size()) {
@@ -562,16 +691,16 @@
   // Run.
   const auto batch_start_time = absl::Now();
   const int exit_code = RunBatchForBinary(binary);
-  inputs_blobseq_.ReleaseSharedMemory();  // Inputs are already consumed.
+  inputs_blobseq_->ReleaseSharedMemory();  // Inputs are already consumed.
   const bool batch_timed_out =
       env_.timeout_per_batch > 0 &&
       absl::Now() - batch_start_time > absl::Seconds(env_.timeout_per_batch);
 
   // Get results.
   batch_result.exit_code() = exit_code;
-  const bool read_success = batch_result.Read(outputs_blobseq_);
+  const bool read_success = batch_result.Read(*outputs_blobseq_);
   FUZZTEST_LOG_IF(ERROR, !read_success) << "Failed to read batch result!";
-  outputs_blobseq_.ReleaseSharedMemory();  // Outputs are already consumed.
+  outputs_blobseq_->ReleaseSharedMemory();  // Outputs are already consumed.
 
   // We may have fewer feature blobs than inputs if
   // * some inputs were not written (i.e. num_inputs_written < inputs.size).
@@ -602,7 +731,8 @@
 
   // TODO: b/467103298 - Handle failures when the exit code is zero, e.g., when
   // the target exits via `std::_Exit(0)`.
-  if (exit_code != EXIT_SUCCESS) {
+  if (exit_code != EXIT_SUCCESS ||
+      std::filesystem::exists(failure_description_path_)) {
     ReadFromLocalFile(last_execute_log_path_, batch_result.log());
 
     if (std::filesystem::exists(failure_description_path_)) {
@@ -649,7 +779,7 @@
 
   std::string centipede_runner_flags = absl::StrCat(
       "CENTIPEDE_RUNNER_FLAGS=:dump_seed_inputs:test=", env_.test_name,
-      ":arg1=", output_dir.string(), ":");
+      ":arg1=", RunnerFlagEscape(output_dir.string()), ":");
   if (!env_.runner_dl_path_suffix.empty()) {
     absl::StrAppend(&centipede_runner_flags,
                     "dl_path_suffix=", env_.runner_dl_path_suffix, ":");
@@ -687,7 +817,7 @@
 
   std::vector<std::string> seed_input_filenames;
   for (const auto& dir_ent : std::filesystem::directory_iterator(output_dir)) {
-    seed_input_filenames.push_back(dir_ent.path().filename());
+    seed_input_filenames.push_back(dir_ent.path().filename().string());
   }
   std::sort(seed_input_filenames.begin(), seed_input_filenames.end());
   num_avail_seeds = seed_input_filenames.size();
@@ -711,14 +841,13 @@
 // See also: `DumpSerializedTargetConfigToFile()`.
 bool CentipedeCallbacks::GetSerializedTargetConfigViaExternalBinary(
     std::string_view binary, std::string& serialized_config) {
-  const auto config_file_path =
-      std::filesystem::path{temp_dir_} / "configuration";
+  const std::string config_file_path = temp_dir_ + "/configuration";
   std::string centipede_runner_flags =
       absl::StrCat("CENTIPEDE_RUNNER_FLAGS=:dump_configuration:arg1=",
-                   config_file_path.string(), ":");
+                   RunnerFlagEscape(config_file_path), ":");
   if (!env_.runner_dl_path_suffix.empty()) {
-    absl::StrAppend(&centipede_runner_flags,
-                    "dl_path_suffix=", env_.runner_dl_path_suffix, ":");
+    absl::StrAppend(&centipede_runner_flags, "dl_path_suffix=",
+                    RunnerFlagEscape(env_.runner_dl_path_suffix), ":");
   }
   Command::Options cmd_options;
   cmd_options.env_diff = env_.env_diff_for_binaries;
@@ -732,7 +861,7 @@
 
   if (is_success) {
     if (std::filesystem::exists(config_file_path)) {
-      ReadFromLocalFile(config_file_path.string(), serialized_config);
+      ReadFromLocalFile(config_file_path, serialized_config);
     } else {
       serialized_config = "";
     }
@@ -740,9 +869,11 @@
   if (env_.print_runner_log || !is_success) {
     PrintExecutionLog();
   }
-  std::error_code error;
-  std::filesystem::remove(config_file_path, error);
-  FUZZTEST_CHECK(!error);
+  if (std::filesystem::exists(config_file_path)) {
+    std::error_code error;
+    std::filesystem::remove(config_file_path, error);
+    FUZZTEST_CHECK(!error);
+  }
 
   return is_success;
 }
@@ -755,17 +886,17 @@
       << "Standalone binary does not support custom mutator";
 
   auto start_time = absl::Now();
-  inputs_blobseq_.Reset();
-  outputs_blobseq_.Reset();
+  inputs_blobseq_->Reset();
+  outputs_blobseq_->Reset();
 
   size_t num_inputs_written =
-      RequestMutation(num_mutants, inputs, inputs_blobseq_);
+      RequestMutation(num_mutants, inputs, *inputs_blobseq_);
   FUZZTEST_LOG_IF(INFO, num_inputs_written != inputs.size())
       << VV(num_inputs_written) << VV(inputs.size());
 
   // Execute.
   const int exit_code = RunBatchForBinary(binary);
-  inputs_blobseq_.ReleaseSharedMemory();  // Inputs are already consumed.
+  inputs_blobseq_->ReleaseSharedMemory();  // Inputs are already consumed.
 
   if (exit_code != EXIT_SUCCESS) {
     FUZZTEST_LOG(WARNING) << "Custom mutator failed with exit code: "
@@ -777,8 +908,8 @@
 
   MutationResult result;
   result.exit_code() = exit_code;
-  result.Read(num_mutants, outputs_blobseq_);
-  outputs_blobseq_.ReleaseSharedMemory();  // Outputs are already consumed.
+  result.Read(num_mutants, *outputs_blobseq_);
+  outputs_blobseq_->ReleaseSharedMemory();  // Outputs are already consumed.
 
   FUZZTEST_VLOG(1) << __FUNCTION__ << " took " << (absl::Now() - start_time);
   return result;
@@ -834,7 +965,7 @@
   absl::MutexLock lock(GetExecutionLoggingMutex());
   for (const auto& log_line :
        absl::StrSplit(absl::StripAsciiWhitespace(log_text), '\n')) {
-    FUZZTEST_LOG(INFO).NoPrefix() << "LOG: " << log_line;
+    FUZZTEST_LOG(INFO).NoPrefix() << "LOG: " << log_line << "\n";
   }
 }
 
diff --git a/centipede/centipede_callbacks.h b/centipede/centipede_callbacks.h
index 38d6bc3..17e16ed 100644
--- a/centipede/centipede_callbacks.h
+++ b/centipede/centipede_callbacks.h
@@ -48,20 +48,7 @@
 // Note: the interface is not yet stable and may change w/o a notice.
 class CentipedeCallbacks {
  public:
-  CentipedeCallbacks(const Environment& env, StopCondition& stop_condition)
-      : env_(env),
-        stop_condition_(stop_condition),
-        byte_array_mutator_(env.knobs, GetRandomSeed(env.seed)),
-        fuzztest_mutator_(env.knobs, GetRandomSeed(env.seed)),
-        inputs_blobseq_(shmem_name1_.c_str(), env.shmem_size_mb << 20,
-                        env.use_posix_shmem),
-        outputs_blobseq_(shmem_name2_.c_str(), env.shmem_size_mb << 20,
-                         env.use_posix_shmem) {
-    if (env.use_legacy_default_mutator)
-      FUZZTEST_CHECK(byte_array_mutator_.set_max_len(env.max_len));
-    else
-      FUZZTEST_CHECK(fuzztest_mutator_.set_max_len(env.max_len));
-  }
+  CentipedeCallbacks(const Environment& env, StopCondition& stop_condition);
   virtual ~CentipedeCallbacks() {}
 
   // Feeds `inputs` into the `binary`, for every input populates `batch_result`.
@@ -188,23 +175,23 @@
   // They are computed in CTOR, to avoid extra computation in the hot loop.
   std::string temp_dir_ = TemporaryLocalDirPath();
   std::string temp_input_file_path_ =
-      std::filesystem::path(temp_dir_).append("temp_input_file");
+      std::filesystem::path(temp_dir_).append("temp_input_file").string();
   const std::string execute_log_prefix_ =
-      std::filesystem::path(temp_dir_).append("log");
+      std::filesystem::path(temp_dir_).append("log").string();
   // An owned file path to save the last execution log before it goes out of
   // scope.
   const std::string saved_execute_log_path_ =
-      std::filesystem::path(temp_dir_).append("saved_log");
+      std::filesystem::path(temp_dir_).append("saved_log").string();
   std::string last_execute_log_path_;
   std::string failure_description_path_ =
-      std::filesystem::path(temp_dir_).append("failure_description");
+      std::filesystem::path(temp_dir_).append("failure_description").string();
   std::string failure_signature_path_ =
-      std::filesystem::path(temp_dir_).append("failure_signature");
+      std::filesystem::path(temp_dir_).append("failure_signature").string();
   const std::string shmem_name1_ = ProcessAndThreadUniqueID("/ctpd-shm1-");
   const std::string shmem_name2_ = ProcessAndThreadUniqueID("/ctpd-shm2-");
 
-  SharedMemoryBlobSequence inputs_blobseq_;
-  SharedMemoryBlobSequence outputs_blobseq_;
+  std::unique_ptr<SharedMemoryBlobSequence> inputs_blobseq_;
+  std::unique_ptr<SharedMemoryBlobSequence> outputs_blobseq_;
 
   // Need unique_ptr indirection because CommandContext is not movable/copyable
   // due to Command.
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc
index 489bd97..30681e7 100644
--- a/centipede/centipede_interface.cc
+++ b/centipede/centipede_interface.cc
@@ -14,7 +14,14 @@
 
 #include "./centipede/centipede_interface.h"
 
+#if !defined(_WIN32)
 #include <unistd.h>
+#else
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <process.h>
+#include <windows.h>
+#endif
 
 #include <algorithm>
 #include <atomic>
@@ -81,7 +88,7 @@
 void ForEachBlob(const Environment& env, StopCondition& stop_condition) {
   auto tmpdir = TemporaryLocalDirPath();
   CreateLocalDirRemovedAtExit(tmpdir);
-  std::string tmpfile = std::filesystem::path(tmpdir).append("t");
+  std::string tmpfile = (std::filesystem::path(tmpdir) / "t").string();
 
   for (const auto& arg : env.args) {
     FUZZTEST_LOG(INFO) << "Running '" << env.for_each_blob << "' on " << arg;
@@ -153,7 +160,8 @@
     binary_info.Write(binary_info_dir);
   }
   if (binary_info.uses_legacy_trace_pc_instrumentation) {
-    pcs_file_path = std::filesystem::path(TemporaryLocalDirPath()) / "pcs";
+    pcs_file_path =
+        (std::filesystem::path(TemporaryLocalDirPath()) / "pcs").string();
     SavePCTableToFile(binary_info.pc_table, pcs_file_path);
   }
   if (env.use_pcpair_features) {
@@ -194,7 +202,7 @@
       PeriodicAction::Options{
           /*sleep_before_each=*/
           [](size_t iteration) {
-            return absl::Minutes(std::clamp(iteration, 0UL, 10UL));
+            return absl::Minutes(std::clamp(iteration, size_t{0}, size_t{10}));
           },
       });
   if (!envs.front().experiment.empty() || FUZZTEST_VLOG_IS_ON(1)) {
@@ -205,7 +213,8 @@
         PeriodicAction::Options{
             /*sleep_before_each=*/
             [](size_t iteration) {
-              return absl::Seconds(std::clamp(iteration, 5UL, 600UL));
+              return absl::Seconds(
+                  std::clamp(iteration, size_t{5}, size_t{600}));
             },
         });
   }
@@ -290,7 +299,8 @@
     coverage.shard_rel_glob =
         std::filesystem::path{
             workdir.DistilledCorpusFilePaths().AllShardsGlob()}
-            .filename();
+            .filename()
+            .string();
     coverage.individual_input_rel_glob = "*";
     coverage.sampled_fraction_or_count = 1.0f;
     sources.push_back(std::move(coverage));
@@ -300,7 +310,8 @@
   // We're seeding the current corpus files.
   destination.shard_rel_glob =
       std::filesystem::path{workdir.CorpusFilePaths().AllShardsGlob()}
-          .filename();
+          .filename()
+          .string();
   destination.shard_index_digits = WorkDir::kDigitsInShardIndex;
   destination.num_shards = static_cast<uint32_t>(env.num_threads);
   return {
@@ -391,7 +402,7 @@
   stop_condition.SetStopTime(absl::InfiniteFuture());
 
   if (!is_workdir_specified) {
-    env.workdir = base_workdir_path / env.test_name;
+    env.workdir = (base_workdir_path / env.test_name).string();
   }
   const auto execution_id_path =
       (base_workdir_path / absl::StrCat(env.test_name, ".execution_id"))
@@ -465,8 +476,8 @@
   if (!is_resuming) {
     FUZZTEST_CHECK_OK(GenerateSeedCorpusFromConfig(
         GetSeedCorpusConfig(
-            env, regression_dir.c_str(),
-            env.fuzztest_replay_coverage_inputs ? coverage_dir.c_str() : ""),
+            env, regression_dir.string(),
+            env.fuzztest_replay_coverage_inputs ? coverage_dir.string() : ""),
         env.binary_name, env.binary_hash))
         << "while generating the seed corpus";
   }
@@ -474,7 +485,7 @@
   absl::Duration time_limit = env.fuzztest_time_limit_per_test;
   absl::Duration time_spent = absl::ZeroDuration();
   const std::string fuzzing_time_file =
-      std::filesystem::path(env.workdir) / "fuzzing_time";
+      (std::filesystem::path(env.workdir) / "fuzzing_time").string();
   if (is_resuming && RemotePathExists(fuzzing_time_file)) {
     time_spent = ReadFuzzingTime(fuzzing_time_file);
     time_limit = std::max(time_limit - time_spent, absl::ZeroDuration());
@@ -507,10 +518,11 @@
 
   if (!stats_root_path.empty()) {
     const auto stats_dir = stats_root_path / env.test_name;
-    FUZZTEST_CHECK_OK(RemoteMkdir(stats_dir.c_str()));
+    FUZZTEST_CHECK_OK(RemoteMkdir(stats_dir.string()));
     FUZZTEST_CHECK_OK(RemoteFileRename(
         workdir.FuzzingStatsPath(),
-        (stats_dir / absl::StrCat("fuzzing_stats_", execution_stamp)).c_str()));
+        (stats_dir / absl::StrCat("fuzzing_stats_", execution_stamp))
+            .string()));
   }
 
   if (stop_condition.EarlyStopRequested()) {
@@ -559,21 +571,22 @@
 
   // Distill and store the coverage corpus.
   Distill(env);
-  if (RemotePathExists(coverage_dir.c_str())) {
+  if (RemotePathExists(coverage_dir.string())) {
     // In the future, we will store k latest coverage corpora for some k, but
     // for now we only keep the latest one.
     FUZZTEST_CHECK_OK(
-        RemotePathDelete(coverage_dir.c_str(), /*recursively=*/true));
+        RemotePathDelete(coverage_dir.string(), /*recursively=*/true));
   }
-  FUZZTEST_CHECK_OK(RemoteMkdir(coverage_dir.c_str()));
+  FUZZTEST_CHECK_OK(RemoteMkdir(coverage_dir.string()));
   std::vector<std::string> distilled_corpus_files;
   FUZZTEST_CHECK_OK(
       RemoteGlobMatch(workdir.DistilledCorpusFilePaths().AllShardsGlob(),
                       distilled_corpus_files));
   for (const std::string& corpus_file : distilled_corpus_files) {
-    const std::string file_name = std::filesystem::path(corpus_file).filename();
+    const std::string file_name =
+        std::filesystem::path(corpus_file).filename().string();
     FUZZTEST_CHECK_OK(
-        RemoteFileRename(corpus_file, (coverage_dir / file_name).c_str()));
+        RemoteFileRename(corpus_file, (coverage_dir / file_name).string()));
   }
 }
 
@@ -596,7 +609,8 @@
   std::vector<std::string> results;
   results.reserve(crash_paths.size());
   for (const auto& crash_path : crash_paths) {
-    std::string crash_id = std::filesystem::path{crash_path}.filename();
+    std::string crash_id =
+        std::filesystem::path{crash_path}.filename().string();
     results.push_back(std::move(crash_id));
   }
   FUZZTEST_CHECK_OK(RemoteFileSetContents(env.list_crash_ids_file,
@@ -617,7 +631,7 @@
                          "crashing";
   const WorkDir workdir{env};
   SeedCorpusSource crash_corpus_source;
-  crash_corpus_source.dir_glob = crash_dir;
+  crash_corpus_source.dir_glob = crash_dir.string();
   crash_corpus_source.num_recent_dirs = 1;
   crash_corpus_source.individual_input_rel_glob = env.crash_id;
   crash_corpus_source.sampled_fraction_or_count = 1.0f;
@@ -627,7 +641,8 @@
           /*dir_path=*/env.workdir,
           /*shard_rel_glob=*/
           std::filesystem::path{workdir.CorpusFilePaths().AllShardsGlob()}
-              .filename(),
+              .filename()
+              .string(),
           /*shard_index_digits=*/WorkDir::kDigitsInShardIndex,
           /*num_shards=*/1}};
   FUZZTEST_CHECK_OK(GenerateSeedCorpusFromConfig(
@@ -661,11 +676,12 @@
                          env.fuzztest_binary_identifier / env.test_name /
                          "crashing";
   std::string crash_contents;
-  const auto read_status =
-      RemoteFileGetContents((crash_dir / env.crash_id).c_str(), crash_contents);
+  const auto read_status = RemoteFileGetContents(
+      (crash_dir / env.crash_id).string(), crash_contents);
   if (!read_status.ok()) {
     FUZZTEST_LOG(ERROR) << "Failed reading the crash " << env.crash_id
-                        << " from " << crash_dir.c_str() << ": " << read_status;
+                        << " from " << crash_dir.string() << ": "
+                        << read_status;
     return EXIT_FAILURE;
   }
   const auto write_status =
diff --git a/centipede/centipede_main.cc b/centipede/centipede_main.cc
index e38dcaf..d8aa3ba 100644
--- a/centipede/centipede_main.cc
+++ b/centipede/centipede_main.cc
@@ -12,7 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#if !defined(_WIN32)
 #include <unistd.h>
+#else
+#include <io.h>
+#endif
 
 #include <csignal>
 #include <cstdlib>
@@ -29,6 +33,11 @@
 fuzztest::internal::StopCondition global_stop_condition;
 
 void SetSignalHandlers() {
+#if defined(_WIN32)
+  std::signal(SIGINT, [](int received_signum) {
+    global_stop_condition.RequestEarlyStop(EXIT_FAILURE);
+  });
+#else
   struct sigaction sigact = {};
   sigact.sa_flags = SA_ONSTACK;
   sigact.sa_handler = [](int received_signum) {
@@ -39,6 +48,7 @@
     global_stop_condition.RequestEarlyStop(EXIT_FAILURE);
   };
   sigaction(SIGINT, &sigact, nullptr);
+#endif
 }
 }  // namespace
 
diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc
index 58539ff..e2e0b14 100644
--- a/centipede/centipede_test.cc
+++ b/centipede/centipede_test.cc
@@ -37,6 +37,7 @@
 #include "./centipede/centipede_callbacks.h"
 #include "./centipede/centipede_default_callbacks.h"
 #include "./centipede/centipede_interface.h"
+#include "./centipede/command.h"
 #include "./centipede/environment.h"
 #include "./centipede/feature.h"
 #include "./centipede/mutation_data.h"
@@ -64,6 +65,10 @@
 using ::testing::Not;
 using ::testing::SizeIs;
 
+#if defined(_WIN32) && !defined(WEXITSTATUS)
+#define WEXITSTATUS(s) (s)
+#endif
+
 inline std::vector<ByteSpan> AsByteSpans(const std::vector<ByteArray>& inputs) {
   std::vector<ByteSpan> results;
   results.reserve(inputs.size());
@@ -152,7 +157,7 @@
   TempCorpusDir tmp_dir{test_info_->name()};
   Environment env;
   env.log_level = 0;  // Disable most of the logging in the test.
-  env.workdir = tmp_dir.path();
+  env.workdir = tmp_dir.path().string();
   env.num_runs = 100000;  // Enough to run through all 1- and 2-byte inputs.
   env.batch_size = 7;     // Just some small number.
   env.require_pc_table = false;  // No PC table here.
@@ -180,11 +185,11 @@
   TempDir corpus_dir{test_info_->name(), "corpus"};
   Environment env;
   env.log_level = 0;  // Disable most of the logging in the test.
-  env.workdir = workdir_1.path();
+  env.workdir = workdir_1.path().string();
   env.num_runs = 100000;  // Enough to run through all 1- and 2-byte inputs.
   env.batch_size = 7;     // Just some small number.
   env.require_pc_table = false;  // No PC table here.
-  env.corpus_dir.push_back(corpus_dir.path());
+  env.corpus_dir.push_back(corpus_dir.path().string());
 
   // Need to wrap each CentipedeMain in a scope to make sure the shmem is
   // released before the next call. Otherwise it may fail in MacOS.
@@ -201,7 +206,7 @@
 
   {
     // Second, run without fuzzing using the same corpus_dir.
-    env.workdir = workdir_2.path();
+    env.workdir = workdir_2.path().string();
     env.num_runs = 0;
     CentipedeMock mock_2(env);
     NonOwningCallbacksFactory factory_2(mock_2);
@@ -217,11 +222,11 @@
   TempDir corpus_dir{test_info_->name(), "corpus"};
   Environment env;
   env.log_level = 0;  // Disable most of the logging in the test.
-  env.workdir = workdir_1.path();
+  env.workdir = workdir_1.path().string();
   env.num_runs = 100000;  // Enough to run through all 1- and 2-byte inputs.
   env.batch_size = 7;     // Just some small number.
   env.require_pc_table = false;  // No PC table here.
-  env.corpus_dir.push_back(corpus_dir.path());
+  env.corpus_dir.push_back(corpus_dir.path().string());
 
   {
     // First, generate corpus files in corpus_dir.
@@ -237,7 +242,7 @@
   {
     // Second, run without fuzzing using the same corpus_dir, but as
     // output-only.
-    env.workdir = workdir_2.path();
+    env.workdir = workdir_2.path().string();
     env.num_runs = 0;
     env.first_corpus_dir_output_only = true;
     CentipedeMock mock_2(env);
@@ -252,7 +257,7 @@
   TempCorpusDir tmp_dir{test_info_->name()};
   Environment env;
   env.log_level = 0;  // Disable most of the logging in the test.
-  env.workdir = tmp_dir.path();
+  env.workdir = tmp_dir.path().string();
   env.num_runs = 100000;  // Enough to run through all 1- and 2-byte inputs.
   env.batch_size = 7;     // Just some small number.
   env.require_pc_table = false;  // No PC table here.
@@ -273,7 +278,7 @@
 TEST(Centipede, ShardsAndDistillTest) {
   TempCorpusDir tmp_dir{test_info_->name()};
   Environment env;
-  env.workdir = tmp_dir.path();
+  env.workdir = tmp_dir.path().string();
   env.log_level = 0;  // Disable most of the logging in the test.
   size_t combined_num_runs = 100000;  // Enough to run through all inputs.
   env.total_shards = 20;
@@ -336,13 +341,14 @@
 TEST(Centipede, InputFilter) {
   TempCorpusDir tmp_dir{test_info_->name()};
   Environment env;
-  env.workdir = tmp_dir.path();
+  env.workdir = tmp_dir.path().string();
   env.num_runs = 256;            // Enough to run through all 1- byte inputs.
   env.log_level = 0;             // Disable most of the logging in the test.
   env.require_pc_table = false;  // No PC table here.
   // Add %f so that test_input_filter doesn't need to be linked with forkserver.
-  env.input_filter = "%f" + std::string{GetDataDependencyFilepath(
-                                "centipede/testing/test_input_filter")};
+  env.input_filter =
+      "%f" +
+      GetDataDependencyFilepath("centipede/testing/test_input_filter").string();
   CentipedeMock mock(env);
   NonOwningCallbacksFactory factory(mock);
   CentipedeMain(env, factory);  // Run fuzzing.
@@ -404,10 +410,10 @@
 TEST_F(CentipedeWithTemporaryLocalDir, MutateViaExternalBinary) {
   // This binary contains a test-friendly custom mutator.
   const std::string binary_with_custom_mutator =
-      GetDataDependencyFilepath("centipede/testing/test_fuzz_target");
+      GetDataDependencyFilepath("centipede/testing/test_fuzz_target").string();
   // This binary does not contain a custom mutator.
   const std::string binary_without_custom_mutator =
-      GetDataDependencyFilepath("centipede/testing/abort_fuzz_target");
+      GetDataDependencyFilepath("centipede/testing/abort_fuzz_target").string();
   // Mutate a couple of different inputs.
   std::vector<ByteArray> inputs = {{0, 1, 2}, {3, 4}};
   // The custom mutator in the test binary will revert the order of bytes
@@ -547,7 +553,7 @@
   // each.
   TempCorpusDir work_tmp_dir{test_info_->name(), "workdir"};
   Environment env;
-  env.workdir = work_tmp_dir.path();
+  env.workdir = work_tmp_dir.path().string();
   env.num_runs = 3;              // Just a few runs.
   env.require_pc_table = false;  // No PC table here.
   MergeMock mock(env);
@@ -563,7 +569,7 @@
   // the seed {0} each.
   TempCorpusDir merge_tmp_dir(test_info_->name(), "merge_from");
   Environment merge_env;
-  merge_env.workdir = merge_tmp_dir.path();
+  merge_env.workdir = merge_tmp_dir.path().string();
   merge_env.num_runs = 4;
   merge_env.require_pc_table = false;  // No PC table here.
   mock.Reset();
@@ -578,7 +584,7 @@
   // Merge shards of `merge_env` into shards of `env`.
   // Shard 0 will receive one extra input: {4}
   // Shard 1 will receive two extra inputs: {7}, {8}
-  env.merge_from = merge_tmp_dir.path();
+  env.merge_from = merge_tmp_dir.path().string();
   env.num_runs = 0;
   for (env.my_shard_index = 0; env.my_shard_index < 2; ++env.my_shard_index) {
     CentipedeMain(env, factory);
@@ -648,12 +654,13 @@
 static std::vector<ByteArray> RunWithFunctionFilter(
     std::string_view function_filter, const TempDir& tmp_dir) {
   Environment env;
-  env.workdir = tmp_dir.path();
+  env.workdir = tmp_dir.path().string();
   env.seed = 1;  // make the runs predictable.
   env.exec_time_weight_scaling = false;
   env.num_runs = 100;
   env.batch_size = 10;
-  env.binary = GetDataDependencyFilepath("centipede/testing/test_fuzz_target");
+  env.binary =
+      GetDataDependencyFilepath("centipede/testing/test_fuzz_target").string();
   env.coverage_binary = env.binary;
   // Must symbolize in order for the filter to work.
   env.symbolizer_path = GetLLVMSymbolizerPath();
@@ -768,8 +775,9 @@
   const std::string& dir_path = arg;
   std::vector<FileAndContents> files_and_contents;
   for (const auto& dir_ent : std::filesystem::directory_iterator(dir_path)) {
-    auto file_and_contents = FileAndContents{dir_ent.path().filename()};
-    ReadFromLocalFile(dir_ent.path().c_str(), file_and_contents.contents);
+    auto file_and_contents =
+        FileAndContents{dir_ent.path().filename().string()};
+    ReadFromLocalFile(dir_ent.path().string(), file_and_contents.contents);
     files_and_contents.push_back(std::move(file_and_contents));
   }
   return ExplainMatchResult(expected_files_and_contents, files_and_contents,
@@ -782,7 +790,7 @@
 TEST(Centipede, ExtraBinaries) {
   TempDir tmp_dir{test_info_->name()};
   Environment env;
-  env.workdir = tmp_dir.path();
+  env.workdir = tmp_dir.path().string();
   env.num_runs = 100;
   env.batch_size = 10;
   env.log_level = 1;
@@ -910,7 +918,7 @@
 
   TempDir temp_dir{test_info_->name()};
   Environment env;
-  env.workdir = temp_dir.path();
+  env.workdir = temp_dir.path().string();
   env.num_runs = kBatchSize * kNumBatches;
   env.batch_size = kBatchSize;
   // No real binary: prevent attempts by Centipede to read a PCtable from it.
@@ -935,7 +943,7 @@
     std::vector<std::string> found_crash_file_names;
     for (auto const& dir_ent :
          std::filesystem::directory_iterator(crashes_dir_path)) {
-      found_crash_file_names.push_back(dir_ent.path().filename());
+      found_crash_file_names.push_back(dir_ent.path().filename().string());
     }
     // TODO(ussuri): Verify exact names/contents of the files, not just count.
     EXPECT_EQ(found_crash_file_names.size(), kCrashingInputIdxInBatch + 1);
@@ -947,7 +955,7 @@
   // Verify that when `env.batch_triage_suspect_only` is set, only triage the
   // suspect.
   TempDir suspect_only_temp_dir{test_info_->name()};
-  env.workdir = suspect_only_temp_dir.path();
+  env.workdir = suspect_only_temp_dir.path().string();
   env.batch_triage_suspect_only = true;
   UndetectedCrashingInputMock suspect_only_mock(env, kCrashingInputIdx);
   NonOwningCallbacksFactory suspect_only_factory(suspect_only_mock);
@@ -958,8 +966,8 @@
 
 TEST_F(CentipedeWithTemporaryLocalDir, GetsSeedInputs) {
   Environment env;
-  env.binary =
-      GetDataDependencyFilepath("centipede/testing/seeded_fuzz_target");
+  env.binary = GetDataDependencyFilepath("centipede/testing/seeded_fuzz_target")
+                   .string();
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
   std::vector<ByteArray> seeds;
@@ -971,7 +979,8 @@
 TEST_F(CentipedeWithTemporaryLocalDir, GetsSerializedTargetConfig) {
   Environment env;
   env.binary =
-      GetDataDependencyFilepath("centipede/testing/fuzz_target_with_config");
+      GetDataDependencyFilepath("centipede/testing/fuzz_target_with_config")
+          .string();
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
   const auto serialized_config = callbacks.GetSerializedTargetConfig();
@@ -984,7 +993,7 @@
   Environment env;
   env.binary = absl::StrCat(
       GetDataDependencyFilepath("centipede/testing/fuzz_target_with_config")
-          .c_str(),
+          .string(),
       " --simulate_failure");
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
@@ -995,7 +1004,8 @@
 TEST_F(CentipedeWithTemporaryLocalDir, CleansUpMetadataAfterStartup) {
   Environment env;
   env.binary = GetDataDependencyFilepath(
-      "centipede/testing/expensive_startup_fuzz_target");
+                   "centipede/testing/expensive_startup_fuzz_target")
+                   .string();
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
   BatchResult batch_result;
@@ -1042,7 +1052,7 @@
 TEST(Centipede, RunsExecuteCallbackInTheCurrentThreadWhenFuzzingWithOneThread) {
   TempDir temp_dir{test_info_->name()};
   Environment env;
-  env.workdir = temp_dir.path();
+  env.workdir = temp_dir.path().string();
   env.require_pc_table = false;
   ASSERT_EQ(env.num_threads, 1);
   FakeCentipedeCallbacksForThreadChecking callbacks(env,
@@ -1057,7 +1067,8 @@
 
 TEST_F(CentipedeWithTemporaryLocalDir, DetectsStackOverflow) {
   Environment env;
-  env.binary = GetDataDependencyFilepath("centipede/testing/test_fuzz_target");
+  env.binary =
+      GetDataDependencyFilepath("centipede/testing/test_fuzz_target").string();
   env.stack_limit_kb = 64;
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
@@ -1100,7 +1111,7 @@
   TempDir temp_dir{test_info_->name()};
   Environment env;
   env.log_level = 0;  // Disable most of the logging in the test.
-  env.workdir = temp_dir.path();
+  env.workdir = temp_dir.path().string();
   env.batch_size = 7;            // Just some small number.
   env.require_pc_table = false;  // No PC table here.
   SetupFailureCallbacks mock(env);
@@ -1140,7 +1151,7 @@
   TempDir temp_dir{test_info_->name()};
   Environment env;
   env.log_level = 0;  // Disable most of the logging in the test.
-  env.workdir = temp_dir.path();
+  env.workdir = temp_dir.path().string();
   env.batch_size = 7;            // Just some small number.
   env.require_pc_table = false;  // No PC table here.
   SkippedTestCallbacks mock(env);
@@ -1180,7 +1191,7 @@
   TempDir temp_dir{test_info_->name()};
   Environment env;
   env.log_level = 0;  // Disable most of the logging in the test.
-  env.workdir = temp_dir.path();
+  env.workdir = temp_dir.path().string();
   env.batch_size = 7;  // Just some small number.
   env.num_runs = 100;
   env.require_pc_table = false;  // No PC table here.
@@ -1226,7 +1237,7 @@
 TEST(Centipede, DoesNotReduceInputWhenTheOptionIsUnset) {
   TempCorpusDir tmp_dir{test_info_->name()};
   Environment env;
-  env.workdir = tmp_dir.path();
+  env.workdir = tmp_dir.path().string();
   env.num_runs = 1000000;  // Should be enough
   env.batch_size = 7;      // Just some small number.
   env.require_pc_table = false;
@@ -1247,7 +1258,7 @@
 TEST(Centipede, ReducesInputWhenTheOptionIsSet) {
   TempCorpusDir tmp_dir{test_info_->name()};
   Environment env;
-  env.workdir = tmp_dir.path();
+  env.workdir = tmp_dir.path().string();
   env.num_runs = 1000000;  // Should be enough
   env.batch_size = 7;      // Just some small number.
   env.require_pc_table = false;
@@ -1268,7 +1279,8 @@
 TEST_F(CentipedeWithTemporaryLocalDir, UsesProvidedCustomMutator) {
   Environment env;
   env.binary = GetDataDependencyFilepath(
-      "centipede/testing/fuzz_target_with_custom_mutator");
+                   "centipede/testing/fuzz_target_with_custom_mutator")
+                   .string();
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
   const std::vector<ByteArray> inputs = {{99}};
@@ -1285,7 +1297,7 @@
   env.binary =
       absl::StrCat(GetDataDependencyFilepath(
                        "centipede/testing/fuzz_target_with_custom_mutator")
-                       .c_str(),
+                       .string(),
                    " --simulate_failure");
   StopCondition stop_condition;
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
@@ -1301,7 +1313,8 @@
 TEST_F(CentipedeWithTemporaryLocalDir,
        FallsBackToBuiltInMutatorWhenCustomMutatorNotProvided) {
   Environment env;
-  env.binary = GetDataDependencyFilepath("centipede/testing/abort_fuzz_target");
+  env.binary =
+      GetDataDependencyFilepath("centipede/testing/abort_fuzz_target").string();
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
   const std::vector<ByteArray> inputs = {{1}, {2}, {3}, {4}, {5}, {6}};
@@ -1330,7 +1343,8 @@
 TEST_F(CentipedeWithTemporaryLocalDir, HangingFuzzTargetExitsAfterTimeout) {
   Environment env;
   env.binary =
-      GetDataDependencyFilepath("centipede/testing/hanging_fuzz_target");
+      GetDataDependencyFilepath("centipede/testing/hanging_fuzz_target")
+          .string();
   BatchResult batch_result;
   const std::vector<ByteArray> inputs = {{0}};
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
@@ -1345,7 +1359,8 @@
 
 TEST_F(CentipedeWithTemporaryLocalDir, ExecuteEndsAfterCustomFailure) {
   Environment env;
-  env.binary = GetDataDependencyFilepath("centipede/testing/test_fuzz_target");
+  env.binary =
+      GetDataDependencyFilepath("centipede/testing/test_fuzz_target").string();
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
   BatchResult result;
   std::vector<ByteArray> inputs = {
@@ -1365,7 +1380,8 @@
 TEST_F(CentipedeWithTemporaryLocalDir, ToleratesAsyncFailureInMutation) {
   Environment env;
   env.binary =
-      GetDataDependencyFilepath("centipede/testing/async_failing_target");
+      GetDataDependencyFilepath("centipede/testing/async_failing_target")
+          .string();
   StopCondition stop_condition;
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
   BatchResult result;
@@ -1389,14 +1405,16 @@
 TEST_F(CentipedeWithTemporaryLocalDir, EngineWorksInWorkerMode) {
   TempCorpusDir tmp_dir{test_info_->name()};
   Environment env;
-  env.workdir = tmp_dir.path();
+  env.workdir = tmp_dir.path().string();
   env.binary = GetDataDependencyFilepath(
-      "centipede/testing/test_binary_for_engine_testing");
+                   "centipede/testing/test_binary_for_engine_testing")
+                   .string();
   env.test_name = "some_test";
   env.populate_binary_info = false;
   env.fork_server = false;
   env.persistent_mode = true;
   env.exit_on_crash = true;
+  env.print_runner_log = true;
   env.stop_at = absl::Now() + absl::Seconds(10);
   fuzztest::internal::DefaultCallbacksFactory<
       fuzztest::internal::CentipedeDefaultCallbacks>
@@ -1406,21 +1424,30 @@
 }
 
 TEST_F(CentipedeWithTemporaryLocalDir, EngineWorksInStandaloneMode) {
-  const std::string centipede_path =
-      GetDataDependencyFilepath("centipede/centipede");
-  const std::string test_binary_path = GetDataDependencyFilepath(
-      "centipede/testing/test_binary_for_engine_testing");
-  // Create a temporary dir and enter it for running the test binary, because
-  // the test binary uses CWD as the engine workdir.
-  TempCorpusDir tmp_dir{test_info_->name()};
-  const auto test_command =
-      absl::StrCat("cd ", tmp_dir.path().string(),
-                   " && env FUZZTEST_CENTIPEDE_BINARY_PATH=", centipede_path,
-                   " ", test_binary_path);
   EXPECT_DEATH(
       [&] {
-        const int status = std::system(test_command.c_str());
-        std::exit(WEXITSTATUS(status));
+        const std::string centipede_path =
+            GetDataDependencyFilepath("centipede/centipede").string();
+        std::string test_binary_path =
+            GetDataDependencyFilepath(
+                "centipede/testing/test_binary_for_engine_testing")
+                .string();
+        // Create a temporary dir and enter it for running the test binary,
+        // because the test binary uses CWD as the engine workdir.
+        TempDir tmp_dir{test_info_->name()};
+#if defined(_WIN32)
+        _chdir(tmp_dir.path().string().c_str());
+#else
+        chdir(tmp_dir.path().string().c_str());
+#endif
+
+        Command::Options cmd_options;
+        cmd_options.env_diff.push_back(
+            absl::StrCat("FUZZTEST_CENTIPEDE_BINARY_PATH=", centipede_path));
+        Command cmd(test_binary_path, cmd_options);
+        cmd.ExecuteAsync();
+        auto result = cmd.Wait(absl::Now() + absl::Seconds(10));
+        std::exit(result.value_or(EXIT_FAILURE));
       }(),
       ContainsRegex("Failure *: some_failure_description"));
 }
diff --git a/centipede/command.cc b/centipede/command.cc
index 972a056..4c70c32 100644
--- a/centipede/command.cc
+++ b/centipede/command.cc
@@ -14,6 +14,14 @@
 
 #include "./centipede/command.h"
 
+#if defined(_WIN32)
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <io.h>
+#include <process.h>
+#include <windows.h>
+using pid_t = int;
+#else
 #include <errno.h>
 #include <fcntl.h>
 #include <spawn.h>
@@ -22,6 +30,7 @@
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <unistd.h>
+#endif
 
 #ifdef __APPLE__
 #include <inttypes.h>
@@ -35,6 +44,7 @@
 #include <cstdlib>
 #include <filesystem>  // NOLINT
 #include <fstream>
+#include <map>  // NOLINT
 #include <optional>
 #include <string>
 #include <string_view>
@@ -45,6 +55,7 @@
 #include "absl/base/const_init.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 +69,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
@@ -73,7 +85,9 @@
 constexpr std::string_view kNoForkServerRequestPrefix("%f");
 
 absl::StatusOr<std::string> GetProcessCreationStamp(pid_t pid) {
-#ifdef __APPLE__
+#if defined(_WIN32)
+  return "0";
+#elif defined(__APPLE__)
   struct proc_bsdinfo info = {};
   if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, PROC_PIDTBSDINFO_SIZE) !=
       PROC_PIDTBSDINFO_SIZE) {
@@ -122,6 +136,11 @@
 
 // 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 {
+  pid_t pid_ = -1;
+};
+#else
 struct Command::ForkServerProps {
   // The file paths of the comms pipes.
   std::string fifo_path_[2];
@@ -176,6 +195,7 @@
     }
   }
 };
+#endif
 
 // NOTE: Because std::unique_ptr<T> requires T to be a complete type wherever
 // the deleter is instantiated, the special member functions must be defined
@@ -201,6 +221,41 @@
 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<std::string> ss;
   ss.reserve(/*env*/ 1 + options_.env_diff.size() + /*path*/ 1 +
              /*args*/ options_.args.size() + /*out/err*/ 2);
@@ -235,7 +290,7 @@
   ss.push_back(std::move(path));
   // args.
   for (const auto& arg : options_.args) {
-    ss.push_back(arg);
+    ss.push_back(ShellEscape(arg));
   }
   // out/err.
   if (!stdout_file_.empty()) {
@@ -250,10 +305,14 @@
   }
   // 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 @@
   }
   fork_server_->creation_stamp = *std::move(creation_stamp);
   return true;
+#endif  // _WIN32
 }
 
 void Command::ResetRedirectionFiles(std::string_view new_suffix) {
@@ -364,6 +424,9 @@
 }
 
 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,123 @@
         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(pid_, -1);
+  ResetRedirectionFiles(GetUniqueSuffix());
+  command_line_ = ToString();
+
+  struct CaseInsensitiveLess {
+    bool operator()(std::string_view a, std::string_view b) const {
+      if (absl::EqualsIgnoreCase(a, b)) return false;
+      return absl::AsciiStrToLower(a) < absl::AsciiStrToLower(b);
+    }
+  };
+
+  std::map<std::string, std::string, CaseInsensitiveLess> 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<char> 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;
+  si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
+
+  SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE};
+  HANDLE hOut = INVALID_HANDLE_VALUE;
+  HANDLE hErr = INVALID_HANDLE_VALUE;
+
+  if (!stdout_file_.empty()) {
+    hOut = CreateFileA(stdout_file_.c_str(), FILE_WRITE_DATA, FILE_SHARE_READ,
+                       &sa, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+    si.hStdOutput =
+        (hOut != INVALID_HANDLE_VALUE) ? hOut : GetStdHandle(STD_OUTPUT_HANDLE);
+  } 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);
+      si.hStdError = (hErr != INVALID_HANDLE_VALUE)
+                         ? hErr
+                         : GetStdHandle(STD_ERROR_HANDLE);
+    }
+  } 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 (hOut != INVALID_HANDLE_VALUE) CloseHandle(hOut);
+    if (hErr != INVALID_HANDLE_VALUE && hErr != hOut) CloseHandle(hErr);
+    return false;
+  }
+
+  if (hOut != INVALID_HANDLE_VALUE) CloseHandle(hOut);
+  if (hErr != INVALID_HANDLE_VALUE && hErr != hOut) CloseHandle(hErr);
+
+  win_process_handle_ = pi.hProcess;
+  CloseHandle(pi.hThread);
+  pid_ = static_cast<int>(pi.dwProcessId);
+  is_executing_ = true;
+  return true;
+#else
   if (fork_server_ != nullptr) {
     FUZZTEST_VLOG(1) << "Sending execution request to fork server";
 
@@ -430,11 +605,39 @@
 
   is_executing_ = true;
   return true;
+#endif
 }
 
 std::optional<int> Command::Wait(absl::Time deadline,
                                  StopCondition* stop_condition) {
   FUZZTEST_CHECK(is_executing());
+#if defined(_WIN32)
+  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<DWORD>(absl::ToInt64Milliseconds(dur));
+    }
+  }
+  DWORD res =
+      WaitForSingleObject(static_cast<HANDLE>(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(static_cast<HANDLE>(win_process_handle_), &exit_code);
+  CloseHandle(static_cast<HANDLE>(win_process_handle_));
+  win_process_handle_ = nullptr;
+  pid_ = -1;
+  is_executing_ = false;
+  return static_cast<int>(exit_code);
+#else
   int exit_code = EXIT_SUCCESS;
 
   if (fork_server_ != nullptr) {
@@ -527,10 +730,16 @@
   }
 
   return exit_code;
+#endif
 }
 
 void Command::RequestStop(bool force) {
   FUZZTEST_CHECK(is_executing());
+#if defined(_WIN32)
+  if (win_process_handle_ != nullptr) {
+    TerminateProcess(static_cast<HANDLE>(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 +750,7 @@
   }
   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 b574a84..fd84b8a 100644
--- a/centipede/command.h
+++ b/centipede/command.h
@@ -128,6 +128,9 @@
   struct ForkServerProps;
 
   int pid_ = -1;
+#if defined(_WIN32)
+  void* win_process_handle_ = nullptr;
+#endif
   bool is_executing_ = false;
 
   // Derived from Options::{stdout,stderr}_file_prefix, with the realized suffix
diff --git a/centipede/config_file.cc b/centipede/config_file.cc
index 89c0613..e0d59e0 100644
--- a/centipede/config_file.cc
+++ b/centipede/config_file.cc
@@ -162,7 +162,7 @@
   if (!path.empty() && !std::filesystem::exists(path)) {  // assume remote
     // Read the remote file.
     std::string contents;
-    FUZZTEST_CHECK_OK(RemoteFileGetContents(path.c_str(), contents));
+    FUZZTEST_CHECK_OK(RemoteFileGetContents(path.string(), contents));
 
     // Save a temporary local copy.
     const std::filesystem::path tmp_dir = TemporaryLocalDirPath();
@@ -170,11 +170,11 @@
     FUZZTEST_LOG(INFO) << "Localizing remote config: " << VV(path)
                        << VV(local_path);
     // NOTE: Ignore "Remote" in the API names here: the paths are always local.
-    FUZZTEST_CHECK_OK(RemoteMkdir(tmp_dir.c_str()));
-    FUZZTEST_CHECK_OK(RemoteFileSetContents(local_path.c_str(), contents));
+    FUZZTEST_CHECK_OK(RemoteMkdir(tmp_dir.string()));
+    FUZZTEST_CHECK_OK(RemoteFileSetContents(local_path.string(), contents));
 
     // Augment the argv to point at the local copy and ensure it is cleaned up.
-    replacements.emplace_back(path.c_str(), local_path.c_str());
+    replacements.emplace_back(path.string(), local_path.string());
     cleanup = [local_path]() { std::filesystem::remove(local_path); };
   }
 
@@ -245,7 +245,7 @@
     } else {
       file_contents = flags_str;
     }
-    FUZZTEST_CHECK_OK(RemoteFileSetContents(path.c_str(), file_contents));
+    FUZZTEST_CHECK_OK(RemoteFileSetContents(path.string(), file_contents));
   }
 
   return path;
diff --git a/centipede/corpus.cc b/centipede/corpus.cc
index 5c194b5..b13b15b 100644
--- a/centipede/corpus.cc
+++ b/centipede/corpus.cc
@@ -204,7 +204,7 @@
   // The corpus must not be empty, hence target_size is at least 1.
   // It should also be <= max_corpus_size.
   size_t target_size = std::min(
-      max_corpus_size, std::max(1UL, records_.size() - num_zero_weights));
+      max_corpus_size, std::max(size_t{1}, records_.size() - num_zero_weights));
   auto subset_to_remove =
       weighted_distribution_.RemoveRandomWeightedSubset(target_size, rng);
   if (subset_to_remove.size() == records_.size()) {
diff --git a/centipede/crash_deduplication.cc b/centipede/crash_deduplication.cc
index b5ae094..37592c2 100644
--- a/centipede/crash_deduplication.cc
+++ b/centipede/crash_deduplication.cc
@@ -94,7 +94,7 @@
   std::vector<ExistingCrash> existing_crashes;
   ASSIGN_OR_RETURN_IF_NOT_OK(
       const std::vector<std::string> input_files,
-      RemoteListFiles(crashing_dir.c_str(), /*recursively=*/false));
+      RemoteListFiles(crashing_dir.string(), /*recursively=*/false));
 
   existing_crashes.reserve(input_files.size());
   for (const std::string& input_file : input_files) {
@@ -121,14 +121,14 @@
   std::vector<IncubatingCrash> incubating_crashes;
   ASSIGN_OR_RETURN_IF_NOT_OK(
       const std::vector<std::string> input_files,
-      RemoteListFiles(incubating_dir.c_str(), /*recursively=*/false));
+      RemoteListFiles(incubating_dir.string(), /*recursively=*/false));
 
   incubating_crashes.reserve(input_files.size());
   for (const std::string& input_file : input_files) {
     IncubatingCrash incubating;
     incubating.details.input_path = input_file;
     incubating.details.input_signature =
-        std::filesystem::path(input_file).filename().c_str();
+        std::filesystem::path(input_file).filename().string();
     incubating_crashes.push_back(std::move(incubating));
   }
   return incubating_crashes;
@@ -274,9 +274,9 @@
       GetInputFileName(bug_id, crash_signature, details.input_signature);
   std::filesystem::path new_input_path = crashing_dir / new_input_file_name;
 
-  if (details.input_path != new_input_path.c_str()) {
+  if (details.input_path != new_input_path.string()) {
     RETURN_IF_NOT_OK(
-        RemoteFileCopy(details.input_path, new_input_path.c_str()));
+        RemoteFileCopy(details.input_path, new_input_path.string()));
   }
 
   crash_summary.AddCrash({/*id=*/new_input_file_name,
@@ -321,7 +321,7 @@
   crash_summary.AddCrash({
       /*id=*/std::filesystem::path(existing.crash_report.details.input_path)
           .filename()
-          .c_str(),
+          .string(),
       /*category=*/description,
       existing.crash_report.signature,
       description,
@@ -375,7 +375,7 @@
         std::filesystem::path dest_path =
             incubating_dir / existing.crash_report.details.input_signature;
         RETURN_IF_NOT_OK(RemoteFileRename(
-            existing.crash_report.details.input_path, dest_path.c_str()));
+            existing.crash_report.details.input_path, dest_path.string()));
         break;
       }
 
@@ -424,12 +424,12 @@
     const std::filesystem::path& source_dir,
     const std::filesystem::path& regression_dir, absl::Duration ttl,
     absl::Clock& clock) {
-  if (!RemotePathExists(source_dir.c_str())) {
+  if (!RemotePathExists(source_dir.string())) {
     return absl::OkStatus();
   }
   ASSIGN_OR_RETURN_IF_NOT_OK(
       const std::vector<std::string> active_crash_files,
-      RemoteListFiles(source_dir.c_str(), /*recursively=*/false));
+      RemoteListFiles(source_dir.string(), /*recursively=*/false));
 
   absl::Time now = clock.TimeNow();
 
@@ -442,11 +442,11 @@
       if (input_file_components.ok()) {
         dest_filename = input_file_components->input_signature;
       } else {
-        dest_filename = std::filesystem::path(file_path).filename().c_str();
+        dest_filename = std::filesystem::path(file_path).filename().string();
       }
 
       std::filesystem::path dest_path = regression_dir / dest_filename;
-      RETURN_IF_NOT_OK(RemoteFileRename(file_path, dest_path.c_str()));
+      RETURN_IF_NOT_OK(RemoteFileRename(file_path, dest_path.string()));
     }
   }
   return absl::OkStatus();
@@ -472,9 +472,10 @@
 
     for (std::string& crashing_input_path : crashing_input_paths) {
       std::string crashing_input_file_name =
-          std::filesystem::path(crashing_input_path).filename();
+          std::filesystem::path(crashing_input_path).filename().string();
       const std::string crash_signature_path =
-          crash_metadata_dir / absl::StrCat(crashing_input_file_name, ".sig");
+          (crash_metadata_dir / absl::StrCat(crashing_input_file_name, ".sig"))
+              .string();
       std::string crash_signature;
       const absl::Status status =
           RemoteFileGetContents(crash_signature_path, crash_signature);
@@ -496,7 +497,8 @@
       if (crashes.contains(crash_signature)) continue;
 
       const std::string crash_description_path =
-          crash_metadata_dir / absl::StrCat(crashing_input_file_name, ".desc");
+          (crash_metadata_dir / absl::StrCat(crashing_input_file_name, ".desc"))
+              .string();
       std::string crash_description;
       const absl::Status description_status =
           RemoteFileGetContents(crash_description_path, crash_description);
@@ -537,9 +539,9 @@
         new_crashes_by_signature,
     CrashSummary& crash_summary, StopCondition& stop_condition,
     absl::Duration regression_ttl, absl::Clock& clock) {
-  RETURN_IF_NOT_OK(RemoteMkdir(crashing_dir.c_str()));
-  RETURN_IF_NOT_OK(RemoteMkdir(regression_dir.c_str()));
-  RETURN_IF_NOT_OK(RemoteMkdir(incubating_dir.c_str()));
+  RETURN_IF_NOT_OK(RemoteMkdir(crashing_dir.string()));
+  RETURN_IF_NOT_OK(RemoteMkdir(regression_dir.string()));
+  RETURN_IF_NOT_OK(RemoteMkdir(incubating_dir.string()));
 
   ASSIGN_OR_RETURN_IF_NOT_OK(auto existing_crashes,
                              ReadExistingCrashes(crashing_dir));
diff --git a/centipede/dispatcher_flag_helper.h b/centipede/dispatcher_flag_helper.h
index d143831..7f553e0 100644
--- a/centipede/dispatcher_flag_helper.h
+++ b/centipede/dispatcher_flag_helper.h
@@ -20,6 +20,18 @@
 #include <cstdint>
 #include <cstring>
 
+#if defined(_WIN32)
+inline static char* strndup(const char* s, size_t n) {
+  size_t len = strnlen(s, n);
+  char* ret = static_cast<char*>(malloc(len + 1));
+  if (ret) {
+    memcpy(ret, s, len);
+    ret[len] = '\0';
+  }
+  return ret;
+}
+#endif
+
 #include "absl/base/nullability.h"
 
 namespace fuzztest::internal {
@@ -62,12 +74,39 @@
   const char *absl_nullable GetStringFlag(const char *absl_nonnull flag) const {
     if (!flags) return nullptr;
     // Extract "value" from ":flag=value:" inside centipede_runner_flags.
-    const char *beg = strstr(flags, flag);
+    const size_t flag_len = strlen(flag);
+    const char* beg = flags;
+    while (true) {
+      beg = strstr(beg, flag);
+      if (beg == nullptr) return nullptr;
+      // Find the closest non-backslash before the candiate position.
+      const char* cur = beg - 1;
+      while (cur >= flags && *cur == '\\') --cur;
+      // There are even number of backslashes before the flag, meaning it's not
+      // escaped.
+      if (((beg - cur) & 1) == 1) break;
+      beg = beg + flag_len;
+    }
     if (!beg) return nullptr;
-    const char *value_beg = beg + strlen(flag);
-    const char *end = strstr(value_beg, ":");
-    if (!end) return nullptr;
-    return strndup(value_beg, end - value_beg);
+    const char* value_beg = beg + flag_len;
+    const char* end = value_beg;
+    while (*end) {
+      if (*end == '\\' && *(end + 1) != 0) {
+        end += 2;
+        continue;
+      }
+      if (*end == ':') break;
+      ++end;
+    }
+    if (!end || !*end) return nullptr;
+    char* r = strndup(value_beg, end - value_beg);
+    size_t d = 0;
+    for (const char* c = r; *c != 0; ++c) {
+      if (*c == '\\' && *(c + 1) != 0) ++c;
+      r[d++] = *c;
+    }
+    r[d] = 0;
+    return r;
   }
 };
 
diff --git a/centipede/engine_controller_with_subprocess.cc b/centipede/engine_controller_with_subprocess.cc
index 2ffc89d..ad9c795 100644
--- a/centipede/engine_controller_with_subprocess.cc
+++ b/centipede/engine_controller_with_subprocess.cc
@@ -12,17 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#if !defined(_WIN32)
 #include <sys/wait.h>
+#endif
 
 #include <cstdlib>
 #include <cstring>
 #include <string>
 
+#include "./centipede/command.h"
 #include "./centipede/engine_abi.h"
 #include "./centipede/engine_controller_abi.h"
-#include "./fuzztest/internal/escaping.h"
 
-using fuzztest::internal::ShellEscape;
+using fuzztest::internal::Command;
 
 FuzzTestControllerStatus FuzzTestControllerRun(
     const FuzzTestAdapterManager* manager, const FuzzTestBytesViews* flags) {
@@ -35,17 +37,19 @@
   if (centipede_binary_path == nullptr) {
     return kFuzzTestControllerFailure;
   }
-  std::string command;
-  command.append(ShellEscape(centipede_binary_path));
+  Command::Options cmd_options;
   for (size_t flag_index = 0; flag_index < flags->count; ++flag_index) {
     const FuzzTestBytesView flag = flags->views[flag_index];
-    command.append(" ");
-    command.append(
-        ShellEscape({reinterpret_cast<const char*>(flag.data), flag.size}));
+    cmd_options.args.push_back(
+        {reinterpret_cast<const char*>(flag.data), flag.size});
   }
-  int ret = system(command.c_str());
-  if (ret == -1) return kFuzzTestControllerFailure;
+  Command cmd(centipede_binary_path, cmd_options);
+  int ret = cmd.Execute();
+#if defined(_WIN32)
+  return ret == 0 ? kFuzzTestControllerSuccess : kFuzzTestControllerFailure;
+#else
   return WIFEXITED(ret) && WEXITSTATUS(ret) == EXIT_SUCCESS
              ? kFuzzTestControllerSuccess
              : kFuzzTestControllerFailure;
+#endif
 }
diff --git a/centipede/engine_worker.cc b/centipede/engine_worker.cc
index a1febf0..92c3eff 100644
--- a/centipede/engine_worker.cc
+++ b/centipede/engine_worker.cc
@@ -12,11 +12,37 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 #include <fcntl.h>
+
+#if !defined(_WIN32)
 #include <sys/resource.h>
 #include <sys/socket.h>
 #include <sys/time.h>
 #include <sys/un.h>
 #include <unistd.h>
+#else
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+// clang-format off
+// Must be before <afunix.h>
+#include <ws2tcpip.h>
+// clang-format on
+#include <afunix.h>
+#include <io.h>
+#include <process.h>
+#include <psapi.h>
+#include <windows.h>
+#include <winsock2.h>
+#endif
+
+#if defined(_WIN32)
+#ifndef PATH_MAX
+#define PATH_MAX 4096
+#endif
+inline static int rand_r(unsigned int* seed) {
+  *seed = *seed * 1103515245 + 12345;
+  return static_cast<int>((*seed / 65536) % 32768);
+}
+#endif
 
 #include <algorithm>
 #include <array>
@@ -63,6 +89,12 @@
     char err_buf[80];
     const auto err_str = [&]() -> std::string_view {
       static constexpr std::string_view kFallbackMsg = "[strerror_r failed]";
+#if defined(_WIN32)
+      if (strerror_s(err_buf, sizeof(err_buf), first.saved_errno) != 0) {
+        return kFallbackMsg;
+      }
+      return err_buf;
+#else
       auto result = strerror_r(first.saved_errno, err_buf, sizeof(err_buf));
       constexpr bool xsi_strerror_r = std::is_same_v<decltype(result), int>;
       constexpr bool gnu_strerror_r = std::is_same_v<decltype(result), char*>;
@@ -75,15 +107,25 @@
         if (result == nullptr) return kFallbackMsg;
         return result;
       }
+#endif
     }();
     WorkerLog(err_str);
   } else if constexpr (std::is_same_v<LogLnSync, T>) {
+#if defined(_WIN32)
+    _write(2, "\n", 1);
+    _commit(2);
+#else
     write(STDERR_FILENO, "\n", 1);
     fsync(STDERR_FILENO);
+#endif
   } else {
     std::string_view sv = first;
     while (!sv.empty()) {
+#if defined(_WIN32)
+      const int r = _write(2, sv.data(), static_cast<unsigned int>(sv.size()));
+#else
       const int r = write(STDERR_FILENO, sv.data(), sv.size());
+#endif
       if (r <= 0) break;
       sv = sv.substr(r);
     }
@@ -111,7 +153,7 @@
 // allocates memory (enforced by `WorkerInitEarly`). After that it would be
 // signal-safe.
 //
-// The worker flags format is `:(NAME=VALUE|SWITCH:)+`. `GetWorkerFlags`
+// The worker flags format is `:(NAME=VALUE:|SWITCH:)+`. `GetWorkerFlags`
 // replaces `:` with '\0' so that we can get null-terminated strings of VALUE
 // without copying them, which is important for signal-safety.
 const WorkerFlags& GetWorkerFlags() {
@@ -127,15 +169,23 @@
       WorkerLog("Cannot allocate the worker flags", LogLnSync{});
       std::_Exit(1);
     }
-    memcpy(str, env_flags, len);
-    str[len] = 0;
-    WorkerLog("Got worker flags ", std::string_view{str, len}, LogLnSync{});
-    // Post-processing to make '\0' as the separator, making each item as a
-    // null-terminating string to be used without copying it.
-    for (size_t i = 0; i < len; ++i) {
-      if (str[i] == ':') str[i] = 0;
+    size_t src = 0;
+    size_t dst = 0;
+    while (src < len) {
+      if (env_flags[src] == ':') {
+        str[dst] = '\0';
+      } else {
+        if (env_flags[src] == '\\' && src + 1 < len) {
+          ++src;
+        }
+        str[dst] = env_flags[src];
+      }
+      ++src;
+      ++dst;
     }
-    return WorkerFlags{true, len, str};
+    str[dst] = 0;
+    WorkerLog("Got worker flags ", std::string_view{str, dst}, LogLnSync{});
+    return WorkerFlags{true, dst, str};
   }();
   return worker_flags;
 }
@@ -181,8 +231,14 @@
 void TrySetFileContents(const char* absl_nonnull path, bool append,
                         C... contents) {
   // Needs to be signal-safe.
+#if defined(_WIN32)
+  int f = _open(
+      path, _O_CREAT | _O_WRONLY | _O_BINARY | (append ? _O_APPEND : _O_TRUNC),
+      _S_IREAD | _S_IWRITE);
+#else
   int f = open(path, O_CREAT | O_WRONLY | (append ? O_APPEND : O_TRUNC),
                /*mode=*/0660);
+#endif
   if (f == -1) {
     WorkerLog("cannot open path ", path, ": ", LogErrNo{}, LogLnSync{});
     return;
@@ -190,7 +246,11 @@
   ([&] {
     std::string_view sv = contents;
     while (!sv.empty()) {
+#if defined(_WIN32)
+      const int r = _write(f, sv.data(), static_cast<unsigned int>(sv.size()));
+#else
       const int r = write(f, sv.data(), sv.size());
+#endif
       if (r < 0) {
         WorkerLog("write() failed on ", path, ": ", LogErrNo{}, LogLnSync{});
         return false;
@@ -205,12 +265,21 @@
     return true;
   }() &&
    ...);  // NOLINT - stop fighting with auto-fomatting.
+#if defined(_WIN32)
+  if (_commit(f) != 0) {
+    WorkerLog("_commit() failed on ", path, ": ", LogErrNo{}, LogLnSync{});
+  }
+  if (_close(f) != 0) {
+    WorkerLog("_close() failed on ", path, ": ", LogErrNo{}, LogLnSync{});
+  }
+#else
   if (fsync(f) != 0) {
     WorkerLog("fsync() failed on ", path, ": ", LogErrNo{}, LogLnSync{});
   }
   if (close(f) != 0) {
     WorkerLog("close() failed on ", path, ": ", LogErrNo{}, LogLnSync{});
   }
+#endif
 }
 
 enum class WorkerAction {
@@ -346,15 +415,45 @@
   return {reinterpret_cast<const char*>(bytes.data()), bytes.size()};
 }
 
-// Zero initialized.
-static int persistent_mode_socket;
+#if defined(_WIN32)
+using worker_socket_t = SOCKET;
+constexpr worker_socket_t kInvalidWorkerSocket = INVALID_SOCKET;
+inline static void CloseWorkerSocket(worker_socket_t s) { closesocket(s); }
+#else
+using worker_socket_t = int;
+constexpr worker_socket_t kInvalidWorkerSocket = -1;
+inline static void CloseWorkerSocket(worker_socket_t s) { close(s); }
+#endif
 
+static worker_socket_t persistent_mode_socket = kInvalidWorkerSocket;
+
+#if defined(_WIN32)
+void WorkerInitEarly();
+static int init_worker_early_fn() {
+  WorkerInitEarly();
+  return 0;
+}
+#pragma section(".CRT$XCU", read)
+extern "C" __declspec(allocate(".CRT$XCU")) int (*p_init_worker_early)() =
+    init_worker_early_fn;
+#pragma comment(linker, "/include:p_init_worker_early")
+void WorkerInitEarly() {
+#else
 __attribute__((constructor(200))) void WorkerInitEarly() {
+#endif
   const char* persistent_mode_socket_path =
       GetWorkerFlag(kWorkerPersistentModeSocketPathFlagHeader);
   if (persistent_mode_socket_path == nullptr) return;
+
+#if defined(_WIN32)
+  [[maybe_unused]] static bool init_wsa = []() {
+    WSADATA wsa_data;
+    return WSAStartup(MAKEWORD(2, 2), &wsa_data) == 0;
+  }();
+#endif
+
   persistent_mode_socket = socket(AF_UNIX, SOCK_STREAM, 0);
-  if (persistent_mode_socket < 0) {
+  if (persistent_mode_socket == kInvalidWorkerSocket) {
     WorkerLog(
         "Failed to create persistent mode socket - not running persistent "
         "mode.",
@@ -370,27 +469,42 @@
       "persistent mode socket path string must be fit in sockaddr_un.sun_path");
   std::memcpy(addr.sun_path, persistent_mode_socket_path, socket_path_len);
 
-  int connect_ret = 0;
-  do {
+  int connect_ret = -1;
+  for (int retry = 0; retry < 200; ++retry) {
     connect_ret =
         connect(persistent_mode_socket, (struct sockaddr*)&addr, sizeof(addr));
-  } while (connect_ret == -1 && errno == EINTR);
+    if (connect_ret == 0) break;
+#if defined(_WIN32)
+    int err = WSAGetLastError();
+    if (err == WSAEINTR || err == WSAEWOULDBLOCK || err == WSAECONNREFUSED) {
+      Sleep(10);
+      continue;
+    }
+#else
+    if (errno == EINTR) continue;
+#endif
+    break;
+  }
   if (connect_ret == -1) {
     WorkerLog("Failed to connect the persistent mode socket to ",
               persistent_mode_socket_path, LogLnSync{});
-    (void)close(persistent_mode_socket);
-    persistent_mode_socket = -1;
+    CloseWorkerSocket(persistent_mode_socket);
+    persistent_mode_socket = kInvalidWorkerSocket;
     return;
   }
 
+#if defined(_WIN32)
+  SetHandleInformation(reinterpret_cast<HANDLE>(persistent_mode_socket),
+                       HANDLE_FLAG_INHERIT, 0);
+#else
   int flags = fcntl(persistent_mode_socket, F_GETFD);
   if (flags == -1) {
     WorkerLog(
         "fcntl(F_GETFD) failed on the persistent mode socket - exiting "
         "persistent mode",
         LogLnSync{});
-    (void)close(persistent_mode_socket);
-    persistent_mode_socket = -1;
+    CloseWorkerSocket(persistent_mode_socket);
+    persistent_mode_socket = kInvalidWorkerSocket;
     return;
   }
   flags |= FD_CLOEXEC;
@@ -399,10 +513,11 @@
         "fcntl(F_SETFD) failed on the persistent mode socket - exiting "
         "persistent mode",
         LogLnSync{});
-    (void)close(persistent_mode_socket);
-    persistent_mode_socket = -1;
+    CloseWorkerSocket(persistent_mode_socket);
+    persistent_mode_socket = kInvalidWorkerSocket;
     return;
   }
+#endif
   WorkerLog("Persistent mode: connected to ", persistent_mode_socket_path,
             LogLnSync{});
 }
@@ -542,7 +657,7 @@
   for (size_t i = 0; i < seed_handles.size(); ++i) {
     char seed_path_buf[PATH_MAX];
     const size_t num_path_chars =
-        snprintf(seed_path_buf, PATH_MAX, "%s/%09lu", output_dir, i);
+        snprintf(seed_path_buf, PATH_MAX, "%s/%09zu", output_dir, i);
     WorkerCheck(num_path_chars < PATH_MAX, "seed path reaches PATH_MAX");
     std::vector<uint8_t> serialized_input;
     const auto sink = GetBytesSinkTo(serialized_input);
@@ -560,10 +675,19 @@
 
 // Returns the current time in microseconds.
 uint64_t TimeInUsec() {
+  [[maybe_unused]] constexpr size_t kUsecInSec = 1000000;
+#if defined(_WIN32)
+  FILETIME ft;
+  GetSystemTimeAsFileTime(&ft);
+  ULARGE_INTEGER uli;
+  uli.LowPart = ft.dwLowDateTime;
+  uli.HighPart = ft.dwHighDateTime;
+  return static_cast<uint64_t>(uli.QuadPart / 10);
+#else
   struct timeval tv = {};
-  constexpr size_t kUsecInSec = 1000000;
   gettimeofday(&tv, nullptr);
   return tv.tv_sec * kUsecInSec + tv.tv_usec;
+#endif
 }
 
 void WorkerDoMutate(const FuzzTestAdapter& adapter) {
@@ -723,6 +847,14 @@
   } timer;
 
   auto GetPeakRSSMb = []() -> size_t {
+#if defined(_WIN32)
+    PROCESS_MEMORY_COUNTERS pmc = {};
+    pmc.cb = sizeof(pmc);
+    if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
+      return pmc.PeakWorkingSetSize >> 20;
+    }
+    return 0;
+#else
     struct rusage usage = {};
     if (getrusage(RUSAGE_SELF, &usage) != 0) return 0;
 #ifdef __APPLE__
@@ -733,6 +865,7 @@
     // On Linux, ru_maxrss is in KiB
     return usage.ru_maxrss >> 10;
 #endif  // __APPLE__
+#endif
   };
 
   // In-loop variables declared outside to save allocations.
@@ -869,10 +1002,13 @@
     } else {
       // Reset stdout/stderr.
       for (int fd = 1; fd <= 2; fd++) {
+#if defined(_WIN32)
+        _lseek(fd, 0, SEEK_SET);
+        (void)_chsize(fd, 0);
+#else
         lseek(fd, 0, SEEK_SET);
-        // NOTE: Allow ftruncate() to fail by ignoring its return; that's okay
-        // to happen when the stdout/stderr are not redirected to a file.
         (void)ftruncate(fd, 0);
+#endif
       }
       WorkerLog(
           "FuzzTest engine worker (",
@@ -993,7 +1129,7 @@
   WorkerCheck(adapter.FreeInput != nullptr, "FreeInput must be defined");
   WorkerCheck(adapter.FreeCtx != nullptr, "FreeCtx must be defined");
 
-  if (persistent_mode_socket > 0) {
+  if (persistent_mode_socket != kInvalidWorkerSocket) {
     HandlePersistentMode(adapter);
   } else if (action == WorkerAction::kTestGetSeeds) {
     WorkerDoGetSeeds(adapter);
diff --git a/centipede/fuzztest_mutator.cc b/centipede/fuzztest_mutator.cc
index 1af4e17..df039ce 100644
--- a/centipede/fuzztest_mutator.cc
+++ b/centipede/fuzztest_mutator.cc
@@ -115,11 +115,11 @@
   ~MutatorDomain() {}
 };
 
-FuzzTestMutator::FuzzTestMutator(const Knobs &knobs, uint64_t seed)
+FuzzTestMutator::FuzzTestMutator(const Knobs& knobs, uint64_t seed)
     : knobs_(knobs),
       prng_(seed),
-      mutation_metadata_(std::make_unique<MutationMetadata>()),
-      domain_(std::make_unique<MutatorDomain>()) {
+      mutation_metadata_(std::make_unique<MutationMetadata>()) {
+  domain_ = std::make_unique<MutatorDomain>();
   domain_->WithMinSize(1).WithMaxSize(max_len_);
 }
 
@@ -139,7 +139,7 @@
                                          const ByteArray &other) {
   // Overwrite data[pos:pos+size] with other[first:first+size].
   // Overwrite no more than half of data.
-  size_t max_size = std::max(1UL, data.size() / 2);
+  size_t max_size = std::max(size_t{1}, data.size() / 2);
   const auto first = absl::Uniform<size_t>(prng_, 0, other.size());
   max_size = std::min(max_size, other.size() - first);
   const auto size = absl::Uniform<size_t>(prng_, 1, max_size + 1);
diff --git a/centipede/minimize_crash.cc b/centipede/minimize_crash.cc
index 3a75160..4a812d7 100644
--- a/centipede/minimize_crash.cc
+++ b/centipede/minimize_crash.cc
@@ -68,7 +68,7 @@
     // Write the crasher to disk.
     auto hash = Hash(crasher);
     auto dir = crash_dir_path_;
-    std::string file_path = dir.append(hash);
+    std::string file_path = dir.append(hash).string();
     WriteToLocalFile(file_path, crasher);
   }
 
diff --git a/centipede/periodic_action.cc b/centipede/periodic_action.cc
index d1f42fe..c0e84db 100644
--- a/centipede/periodic_action.cc
+++ b/centipede/periodic_action.cc
@@ -23,6 +23,10 @@
 #include "absl/functional/any_invocable.h"
 #include "absl/synchronization/mutex.h"
 #include "absl/time/time.h"
+#if defined(_WIN32)
+#include <thread>  // NOLINT
+#else
+#endif
 
 namespace fuzztest::internal {
 
diff --git a/centipede/puzzles/BUILD b/centipede/puzzles/BUILD
index b87aad0..3f62e6b 100644
--- a/centipede/puzzles/BUILD
+++ b/centipede/puzzles/BUILD
@@ -26,7 +26,6 @@
 
 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",
@@ -35,7 +34,6 @@
     "memcmp_4_may_inline",
     "memcmp_3",
     "strcmp",
-    "strcasecmp",
     "strncmp",
     "switch",
     "uint32_cmp_1",
@@ -46,5 +44,12 @@
     "autodictionary_stress",
     "paths",
     "thread_uint32_cmp_1",
+]]
+
+[puzzle(
+    name = n,
+    tags = ["no-windows"],
+) for n in [
+    "strcasecmp",
     "pthread_exit_uint32_cmp_1",
 ]]
diff --git a/centipede/puzzles/autodictionary_stress.cc b/centipede/puzzles/autodictionary_stress.cc
index 297d5ee..40a415d 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 8fb9c4c..9bdcf96 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 <cstddef>
 #include <cstdint>
 #include <cstdlib>
diff --git a/centipede/puzzles/callstack.cc b/centipede/puzzles/callstack.cc
index 4fc9a8b..7fd1601 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 <cstddef>
 #include <cstdint>
 #include <cstdlib>
diff --git a/centipede/puzzles/deep_recursion.cc b/centipede/puzzles/deep_recursion.cc
index 6e7572d..9440cbd 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 <sys/resource.h>
+// 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 <cstddef>
 #include <cstdint>
diff --git a/centipede/puzzles/independent_compares.cc b/centipede/puzzles/independent_compares.cc
index 07294f9..0679482 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 <cstddef>
 #include <cstdint>
 #include <cstdlib>
diff --git a/centipede/puzzles/memcmp_3.cc b/centipede/puzzles/memcmp_3.cc
index 36244e3..c12ffe0 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 <cstdint>
 #include <cstdio>
diff --git a/centipede/puzzles/memcmp_4.cc b/centipede/puzzles/memcmp_4.cc
index 97bbbd5..deefe4a 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 <cstdint>
 #include <cstdlib>
diff --git a/centipede/puzzles/memcmp_4_may_inline.cc b/centipede/puzzles/memcmp_4_may_inline.cc
index bc2f062..746796d 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 <cstdint>
 #include <cstdlib>
diff --git a/centipede/puzzles/oom.cc b/centipede/puzzles/oom.cc
index bb20e5d..9515a3c 100644
--- a/centipede/puzzles/oom.cc
+++ b/centipede/puzzles/oom.cc
@@ -12,8 +12,9 @@
 // See the License for the specific language governing permissions and
 // 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 <cstddef>
 #include <cstdint>
 #include <cstdio>
diff --git a/centipede/puzzles/paths.cc b/centipede/puzzles/paths.cc
index ab8de1a..654300b 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 f64f009..f278597 100644
--- a/centipede/puzzles/per_batch_timeout.cc
+++ b/centipede/puzzles/per_batch_timeout.cc
@@ -13,12 +13,16 @@
 // 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
 
+#if defined(_WIN32)
+#include <windows.h>
+#else
 #include <unistd.h>
+#endif
 
 #include <cstddef>
 #include <cstdint>
@@ -27,6 +31,10 @@
   // Within the --timeout_per_input, but we have no solution, so the runner
   // should keep running us until it exceeds --timeout_per_batch, then report a
   // failure back to the engine.
-  sleep(1);
+#if defined(_WIN32)
+  Sleep(1000);
+#else
+  sleep(1);  // NOLINT
+#endif
   return 0;
 }
diff --git a/centipede/puzzles/per_input_timeout.cc b/centipede/puzzles/per_input_timeout.cc
index d286543..26a1f82 100644
--- a/centipede/puzzles/per_input_timeout.cc
+++ b/centipede/puzzles/per_input_timeout.cc
@@ -12,17 +12,27 @@
 // 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
 
+#if defined(_WIN32)
+#include <windows.h>
+#else
 #include <unistd.h>
+#endif
 
 #include <cstddef>
 #include <cstdint>
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
   if (size == 3 && data[0] == 'S' && data[1] == 'L' && data[2] == 'O') {
-    sleep(1000);  // Dies with timeout.
+    // Dies with timeout.
+#if defined(_WIN32)
+    Sleep(1000000);
+#else
+    sleep(1000);  // NOLINT
+#endif
   }
   return 0;
 }
diff --git a/centipede/puzzles/pthread_exit_uint32_cmp_1.cc b/centipede/puzzles/pthread_exit_uint32_cmp_1.cc
index 1e5bade..f132c08 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 <pthread.h>
 
diff --git a/centipede/puzzles/puzzle.bzl b/centipede/puzzles/puzzle.bzl
index a446b96..1e87757 100644
--- a/centipede/puzzles/puzzle.bzl
+++ b/centipede/puzzles/puzzle.bzl
@@ -14,14 +14,15 @@
 
 """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):
+def puzzle(name, tags = []):
     """Generates a cc_fuzz_target target instrumented with sancov and a sh script to run it.
 
     Args:
       name: A unique name for this target
+      tags: Tags for this target
     """
 
     centipede_fuzz_target(
@@ -36,13 +37,24 @@
     # 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",
+            ],
+            tags = tags,
         )
diff --git a/centipede/puzzles/run_puzzle.cc b/centipede/puzzles/run_puzzle.cc
new file mode 100644
index 0000000..939675a
--- /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 <filesystem>  // NOLINT
+#include <fstream>
+#include <set>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <system_error>  // NOLINT
+#include <utility>
+#include <vector>
+
+#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<std::string> 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<std::string> 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<std::string> extra_args;
+    std::vector<std::string> 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 d6d12c8..0000000
--- 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:<program-text>"
-# This script will execute <program-text> directly in the current context.
-# <program-text> 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 c9aaadd..670a9f5 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 <strings.h>
 
diff --git a/centipede/puzzles/strcmp.cc b/centipede/puzzles/strcmp.cc
index c61bb17..338d338 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 <cstdint>
 #include <cstdio>
diff --git a/centipede/puzzles/strncmp.cc b/centipede/puzzles/strncmp.cc
index 4668e11..74f622d 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 <cstdint>
 #include <cstdio>
diff --git a/centipede/puzzles/switch.cc b/centipede/puzzles/switch.cc
index 40a61a2..8e7f09e 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 <cstdint>
 #include <cstdio>
diff --git a/centipede/puzzles/thread_uint32_cmp_1.cc b/centipede/puzzles/thread_uint32_cmp_1.cc
index 0665815..962fe3e 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 <cstdint>
 #include <cstdlib>
diff --git a/centipede/puzzles/uint32_cmp_1.cc b/centipede/puzzles/uint32_cmp_1.cc
index f41879b..3b4df01 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 <cstdint>
 #include <cstdlib>
diff --git a/centipede/resource_pool.h b/centipede/resource_pool.h
index c33a8ff..2c1ea91 100644
--- a/centipede/resource_pool.h
+++ b/centipede/resource_pool.h
@@ -15,9 +15,15 @@
 #ifndef FUZZTEST_CENTIPEDE_RESOURCE_RESOURCE_POOL_H_
 #define FUZZTEST_CENTIPEDE_RESOURCE_RESOURCE_POOL_H_
 
+#if defined(_WIN32)
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <windows.h>
+#else
 #include <sys/syscall.h>
 #include <sys/types.h>
 #include <unistd.h>
+#endif
 
 #include <ostream>
 #include <string>
diff --git a/centipede/runner.cc b/centipede/runner.cc
index e734e6d..00b58fd 100644
--- a/centipede/runner.cc
+++ b/centipede/runner.cc
@@ -23,12 +23,39 @@
 #include "./centipede/runner.h"
 
 #include <fcntl.h>
+#if !defined(_WIN32)
 #include <pthread.h>  // NOLINT: use pthread to avoid extra dependencies.
 #include <sys/resource.h>
 #include <sys/socket.h>
 #include <sys/time.h>
 #include <sys/un.h>
 #include <unistd.h>
+#else
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+// clang-format off
+// Must be before <afunix.h>
+#include <ws2tcpip.h>
+// clang-format on
+#include <afunix.h>
+#include <io.h>
+#include <process.h>
+#include <psapi.h>
+#include <windows.h>
+#include <winsock2.h>
+
+#include <thread>  // NOLINT
+inline static void EnsureWinsockInitialized() {
+  static WSADATA wsa_data;
+  [[maybe_unused]] static int init = WSAStartup(MAKEWORD(2, 2), &wsa_data);
+}
+inline static int rand_r(unsigned int* seed) {
+  *seed = *seed * 1103515245 + 12345;
+  return static_cast<int>((*seed / 65536) % 32768);
+}
+inline static void sleep(unsigned int seconds) { Sleep(seconds * 1000); }
+inline static int ftruncate(int fd, size_t size) { return _chsize(fd, size); }
+#endif
 
 #include <algorithm>
 #include <atomic>
@@ -81,6 +108,14 @@
 }  // namespace
 
 static size_t GetPeakRSSMb() {
+#if defined(_WIN32)
+  PROCESS_MEMORY_COUNTERS pmc = {};
+  pmc.cb = sizeof(pmc);
+  if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
+    return pmc.PeakWorkingSetSize >> 20;
+  }
+  return 0;
+#else
   struct rusage usage = {};
   if (getrusage(RUSAGE_SELF, &usage) != 0) return 0;
 #ifdef __APPLE__
@@ -91,14 +126,24 @@
   // On Linux, ru_maxrss is in KiB
   return usage.ru_maxrss >> 10;
 #endif  // __APPLE__
+#endif
 }
 
 // Returns the current time in microseconds.
 static uint64_t TimeInUsec() {
+#if defined(_WIN32)
+  FILETIME ft;
+  GetSystemTimeAsFileTime(&ft);
+  ULARGE_INTEGER uli;
+  uli.LowPart = ft.dwLowDateTime;
+  uli.HighPart = ft.dwHighDateTime;
+  return static_cast<uint64_t>(uli.QuadPart / 10);
+#else
   struct timeval tv = {};
   constexpr size_t kUsecInSec = 1000000;
   gettimeofday(&tv, nullptr);
   return tv.tv_sec * kUsecInSec + tv.tv_usec;
+#endif
 }
 
 // Atomic flags to make sure that (a) watchdog failure is reported only for
@@ -195,7 +240,11 @@
   tls.ignore = true;
   state->watchdog_thread_started = true;
   while (true) {
+#if defined(_WIN32)
+    Sleep(1000);
+#else
     sleep(1);
+#endif
 
     // No calls to ResetInputTimer() yet: input execution hasn't started.
     if (state->input_start_time == 0) continue;
@@ -235,12 +284,20 @@
           run_time_flags.timeout_per_input.load(),
           run_time_flags.rss_limit_mb.load(),
           state->run_time_flags.stack_limit_kb.load());
+#if defined(_WIN32)
+  std::thread(WatchdogThread, nullptr).detach();
+#else
   pthread_t watchdog_thread;
   pthread_create(&watchdog_thread, nullptr, WatchdogThread, nullptr);
   pthread_detach(watchdog_thread);
+#endif
   // Wait until the watchdog actually starts and initializes itself.
   while (!state->watchdog_thread_started) {
+#if defined(_WIN32)
+    Sleep(0);
+#else
     sleep(0);
+#endif
   }
 }
 
@@ -559,7 +616,7 @@
     if (seed_index >= 1000000000) return;
     char seed_path_buf[PATH_MAX];
     const size_t num_path_chars =
-        snprintf(seed_path_buf, PATH_MAX, "%s/%09lu", output_dir, seed_index);
+        snprintf(seed_path_buf, PATH_MAX, "%s/%09zu", output_dir, seed_index);
     PrintErrorAndExitIf(num_path_chars >= PATH_MAX,
                         "seed path reaches PATH_MAX");
     FILE* output_file = fopen(seed_path_buf, "w");
@@ -787,6 +844,9 @@
 
 // Returns the current process VmSize, in bytes.
 static size_t GetVmSizeInBytes() {
+#if defined(_WIN32)
+  return 0;
+#else
   FILE* f = fopen("/proc/self/statm", "r");  // man proc
   if (!f) return 0;
   size_t vm_size = 0;
@@ -794,10 +854,12 @@
   (void)fscanf(f, "%zd", &vm_size);
   fclose(f);
   return vm_size * getpagesize();  // proc gives VmSize in pages.
+#endif
 }
 
 // Sets RLIMIT_CORE, RLIMIT_AS
 static void SetLimits() {
+#if !defined(_WIN32)
   // Disable core dumping.
   struct rlimit core_limits;
   getrlimit(RLIMIT_CORE, &core_limits);
@@ -826,6 +888,7 @@
             "VmSize is %zdGb, suspecting ASAN/MSAN/TSAN\n",
             vm_size_in_bytes >> 30);
   }
+#endif
 }
 
 // Create a fake reference to ForkServerCallMeVeryEarly() here so that the
@@ -845,9 +908,13 @@
   if (state->persistent_mode_socket_path == nullptr) {
     return;
   }
+#if defined(_WIN32)
+  EnsureWinsockInitialized();
+#endif
   state->persistent_mode_socket = socket(AF_UNIX, SOCK_STREAM, 0);
   if (state->persistent_mode_socket < 0) {
     fprintf(stderr, "Failed to create persistent mode socket\n");
+    return;
   }
 
   struct sockaddr_un addr{};
@@ -863,26 +930,44 @@
   do {
     connect_ret = connect(state->persistent_mode_socket,
                           (struct sockaddr*)&addr, sizeof(addr));
-  } while (connect_ret == -1 && errno == EINTR);
+  } while (connect_ret == -1 &&
+#if defined(_WIN32)
+           WSAGetLastError() == WSAEINTR
+#else
+           errno == EINTR
+#endif
+  );
   if (connect_ret == -1) {
     fprintf(stderr, "Failed to connect the persistent mode socket to %s\n",
             state->persistent_mode_socket_path);
+#if defined(_WIN32)
+    (void)closesocket(state->persistent_mode_socket);
+#else
     (void)close(state->persistent_mode_socket);
+#endif
     state->persistent_mode_socket = -1;
+    return;
   }
 
+#if defined(_WIN32)
+  SetHandleInformation(reinterpret_cast<HANDLE>(state->persistent_mode_socket),
+                       HANDLE_FLAG_INHERIT, 0);
+#else
   int flags = fcntl(state->persistent_mode_socket, F_GETFD);
   if (flags == -1) {
     fprintf(stderr, "fcntl(F_GETFD) failed\n");
     (void)close(state->persistent_mode_socket);
     state->persistent_mode_socket = -1;
+    return;
   }
   flags |= FD_CLOEXEC;
   if (fcntl(state->persistent_mode_socket, F_SETFD, flags) == -1) {
     fprintf(stderr, "fcntl(F_SETFD) failed\n");
     (void)close(state->persistent_mode_socket);
     state->persistent_mode_socket = -1;
+    return;
   }
+#endif
 }
 
 GlobalRunnerState::GlobalRunnerState() {
diff --git a/centipede/runner.h b/centipede/runner.h
index 5ddc3b3..85894b0 100644
--- a/centipede/runner.h
+++ b/centipede/runner.h
@@ -15,7 +15,6 @@
 #ifndef THIRD_PARTY_CENTIPEDE_RUNNER_H_
 #define THIRD_PARTY_CENTIPEDE_RUNNER_H_
 
-#include <pthread.h>  // NOLINT: use pthread to avoid extra dependencies.
 #include <time.h>
 
 #include <atomic>
@@ -27,6 +26,7 @@
 #include "./centipede/runner_interface.h"
 #include "./centipede/runner_result.h"
 #include "./centipede/runner_utils.h"
+#include "./centipede/sancov_state.h"
 
 namespace fuzztest::internal {
 
@@ -84,7 +84,7 @@
       flag_helper.GetStringFlag(":persistent_mode_socket=");
   int persistent_mode_socket = 0;
 
-  pthread_mutex_t execution_result_override_mu = PTHREAD_MUTEX_INITIALIZER;
+  MutexType execution_result_override_mu = MUTEX_INITIALIZER;
   // If not nullptr, it points to a batch result with either zero or one
   // execution. When an execution result present, it will be passed as the
   // execution result of the current test input. The object is owned and cleaned
diff --git a/centipede/runner_cmp_trace.h b/centipede/runner_cmp_trace.h
index 53d15c6..fcd7405 100644
--- a/centipede/runner_cmp_trace.h
+++ b/centipede/runner_cmp_trace.h
@@ -18,7 +18,13 @@
 // Capturing arguments of CMP instructions, memcmp, and similar.
 // WARNING: this code needs to have minimal dependencies.
 
+#if !defined(_WIN32)
 #include <sys/time.h>
+#else
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <windows.h>
+#endif
 
 #include <cstddef>
 #include <cstdint>
@@ -63,12 +69,21 @@
       to_clear = false;
       if (rand_seed_ == 0) {
         // Initialize the random seed (likely) once.
+#if defined(_WIN32)
+        FILETIME ft;
+        GetSystemTimeAsFileTime(&ft);
+        ULARGE_INTEGER uli;
+        uli.LowPart = ft.dwLowDateTime;
+        uli.HighPart = ft.dwHighDateTime;
+        rand_seed_ = static_cast<uint64_t>(uli.QuadPart / 10);
+#else
         struct timeval tv = {};
         constexpr size_t kUsecInSec = 1000000;
         // There is a chance that `gettimeofday()` triggers `Capture()`
         // recursively, but this should be fine as we unset `to_clear` before.
         gettimeofday(&tv, nullptr);
         rand_seed_ = tv.tv_sec * kUsecInSec + tv.tv_usec;
+#endif
       }
     }
     if (size > kNumBytesPerValue) size = kNumBytesPerValue;
diff --git a/centipede/runner_dl_info.cc b/centipede/runner_dl_info.cc
index 95e2286..ba8c3e3 100644
--- a/centipede/runner_dl_info.cc
+++ b/centipede/runner_dl_info.cc
@@ -17,11 +17,19 @@
 #ifdef __APPLE__
 #include <dlfcn.h>
 #include <mach-o/dyld.h>
+#elif defined(_WIN32)
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <windows.h>
+// clang-format off
+// Must be after <windows.h>
+#include <psapi.h>
+// clang-format on
 #else  // __APPLE__
 #include <elf.h>
 #include <link.h>  // dl_iterate_phdr
-#endif             // __APPLE__
 #include <unistd.h>
+#endif  // __APPLE__
 
 #include <cinttypes>
 #include <cstddef>
@@ -184,6 +192,46 @@
   });
 }
 
+#elif defined(_WIN32)
+
+namespace {
+
+DlInfo GetDlInfoFromModule(HMODULE hModule) {
+  DlInfo result;
+  result.Clear();
+  if (hModule == nullptr) return result;
+
+  MODULEINFO modInfo = {};
+  if (GetModuleInformation(GetCurrentProcess(), hModule, &modInfo,
+                           sizeof(modInfo))) {
+    result.start_address = reinterpret_cast<uintptr_t>(modInfo.lpBaseOfDll);
+    result.size = modInfo.SizeOfImage;
+    result.link_offset = result.start_address;
+    GetModuleFileNameA(hModule, result.path, sizeof(result.path));
+  }
+  return result;
+}
+
+}  // namespace
+
+DlInfo GetDlInfo(const char* absl_nullable dl_path_suffix) {
+  HMODULE hModule = nullptr;
+  if (dl_path_suffix == nullptr) {
+    hModule = GetModuleHandleA(nullptr);
+  } else {
+    hModule = GetModuleHandleA(dl_path_suffix);
+  }
+  return GetDlInfoFromModule(hModule);
+}
+
+DlInfo GetDlInfo(uintptr_t pc) {
+  HMODULE hModule = nullptr;
+  GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
+                         GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+                     reinterpret_cast<LPCSTR>(pc), &hModule);
+  return GetDlInfoFromModule(hModule);
+}
+
 #else  // __APPLE__
 
 namespace {
diff --git a/centipede/runner_fork_server.cc b/centipede/runner_fork_server.cc
index 237f95f..ae45812 100644
--- a/centipede/runner_fork_server.cc
+++ b/centipede/runner_fork_server.cc
@@ -51,6 +51,12 @@
 // We try to avoid any high-level code here, even most of libc because this code
 // works too early in the process. E.g. getenv() will not work yet.
 
+#if defined(_WIN32)
+namespace fuzztest::internal {
+void ForkServerCallMeVeryEarly() {}
+}  // namespace fuzztest::internal
+#else
+
 #include <fcntl.h>
 #ifdef __APPLE__
 #include <sys/sysctl.h>
@@ -422,3 +428,5 @@
 #endif  // __APPLE__
 
 }  // namespace fuzztest::internal
+
+#endif  // !defined(_WIN32)
diff --git a/centipede/runner_interface.h b/centipede/runner_interface.h
index 9b560bd..cdb0d1a 100644
--- a/centipede/runner_interface.h
+++ b/centipede/runner_interface.h
@@ -45,16 +45,23 @@
     const uint8_t* data1, size_t size1, const uint8_t* data2, size_t size2,
     uint8_t* out, size_t max_out_size, unsigned int seed);
 
+#if !defined(_WIN32)
+#define CENTIPEDE_WEAK_ATTRIBUTE __attribute__((weak))
+#else
+#define CENTIPEDE_WEAK_ATTRIBUTE __attribute__((weak_import))
+#endif
+
 // This is the header-less interface of libFuzzer, see
 // https://llvm.org/docs/LibFuzzer.html.
 extern "C" {
 int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size);
-__attribute__((weak)) int LLVMFuzzerInitialize(int* absl_nonnull argc,
-                                               char*** absl_nonnull argv);
-__attribute__((weak)) size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size,
-                                                     size_t max_size,
-                                                     unsigned int seed);
-__attribute__((weak)) size_t LLVMFuzzerCustomCrossOver(
+CENTIPEDE_WEAK_ATTRIBUTE int LLVMFuzzerInitialize(int* absl_nonnull argc,
+                                                  char*** absl_nonnull argv);
+CENTIPEDE_WEAK_ATTRIBUTE size_t LLVMFuzzerCustomMutator(uint8_t* data,
+                                                        size_t size,
+                                                        size_t max_size,
+                                                        unsigned int seed);
+CENTIPEDE_WEAK_ATTRIBUTE size_t LLVMFuzzerCustomCrossOver(
     const uint8_t* data1, size_t size1, const uint8_t* data2, size_t size2,
     uint8_t* out, size_t max_out_size, unsigned int seed);
 }  // extern "C"
diff --git a/centipede/runner_utils.cc b/centipede/runner_utils.cc
index 8010574..807cf0d 100644
--- a/centipede/runner_utils.cc
+++ b/centipede/runner_utils.cc
@@ -14,8 +14,17 @@
 
 #include "./centipede/runner_utils.h"
 
+#if !defined(_WIN32)
 #include <pthread.h>
 #include <unistd.h>
+#else
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <io.h>
+#include <process.h>
+#include <windows.h>
+#include <winsock2.h>
+#endif
 
 #include <cerrno>
 #include <cstdint>
@@ -39,7 +48,13 @@
 }
 
 uintptr_t GetCurrentThreadStackRegionLow() {
-#ifdef __APPLE__
+#if defined(_WIN32)
+  MEMORY_BASIC_INFORMATION mbi = {};
+  if (VirtualQuery(&mbi, &mbi, sizeof(mbi)) != 0) {
+    return reinterpret_cast<uintptr_t>(mbi.AllocationBase);
+  }
+  return 0;
+#elif defined(__APPLE__)
   pthread_t self = pthread_self();
   const auto stack_addr =
       reinterpret_cast<uintptr_t>(pthread_get_stackaddr_np(self));
@@ -63,34 +78,54 @@
   RunnerCheck(stack_region_low != 0,
               "the current thread stack region starts from 0 - unexpected!");
   return stack_region_low;
-#endif  // __APPLE__
+#endif
 }
 
-bool ReadAll(int fd, char* data, size_t size) {
+bool ReadAll(intptr_t fd, char* data, size_t size) {
   while (size > 0) {
+#if defined(_WIN32)
+    int r = recv(static_cast<SOCKET>(fd), data, static_cast<int>(size), 0);
+    if (r <= 0 && WSAGetLastError() != 0) {
+      r = _read(static_cast<int>(fd), data, static_cast<unsigned int>(size));
+    }
+#else
     ssize_t r = read(fd, data, size);
+#endif
     if (r > 0) {
-      // read() guarantees r <= size
       data += r;
       size -= r;
       continue;
     }
+#if defined(_WIN32)
+    if (r == -1 && (WSAGetLastError() == WSAEINTR || errno == EINTR)) continue;
+#else
     if (r == -1 && errno == EINTR) continue;
+#endif
     return false;
   }
   return true;
 }
 
-bool WriteAll(int fd, const char* data, size_t size) {
+bool WriteAll(intptr_t fd, const char* data, size_t size) {
   while (size > 0) {
+#if defined(_WIN32)
+    int r = send(static_cast<SOCKET>(fd), data, static_cast<int>(size), 0);
+    if (r <= 0 && WSAGetLastError() != 0) {
+      r = _write(static_cast<int>(fd), data, static_cast<unsigned int>(size));
+    }
+#else
     ssize_t r = write(fd, data, size);
+#endif
     if (r > 0) {
-      // write() guarantees r <= size
       data += r;
       size -= r;
       continue;
     }
+#if defined(_WIN32)
+    if (r == -1 && (WSAGetLastError() == WSAEINTR || errno == EINTR)) continue;
+#else
     if (r == -1 && errno == EINTR) continue;
+#endif
     return false;
   }
   return true;
diff --git a/centipede/runner_utils.h b/centipede/runner_utils.h
index 1722bfa..5327667 100644
--- a/centipede/runner_utils.h
+++ b/centipede/runner_utils.h
@@ -64,12 +64,12 @@
 // Reads `size` bytes to `data` from `fd` with retires (assuming `fd` is
 // blocking so there is no busy-spinning). Returns true if all bytes are
 // written, false otherwise due to errors.
-bool ReadAll(int fd, char* data, size_t size);
+bool ReadAll(intptr_t fd, char* data, size_t size);
 
 // Writes `size` bytes from `data` to `fd` with retires (assuming `fd` is
 // blocking so there is no busy-spinning). Returns true if all bytes are
 // written, false otherwise due to errors.
-bool WriteAll(int fd, const char* data, size_t size);
+bool WriteAll(intptr_t fd, const char* data, size_t size);
 
 // Leak sanitizer interface functions.
 extern "C" void __lsan_register_root_region(const void* p, size_t size);
diff --git a/centipede/rusage_stats.cc b/centipede/rusage_stats.cc
index c5bf850..af8582f 100644
--- a/centipede/rusage_stats.cc
+++ b/centipede/rusage_stats.cc
@@ -14,14 +14,20 @@
 
 #include "./centipede/rusage_stats.h"
 
-#ifdef __APPLE__
+#if defined(_WIN32)
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <process.h>
+#include <windows.h>
+#elif defined(__APPLE__)
 #include <libproc.h>
 #include <sys/proc.h>
 #include <sys/sysctl.h>
-#endif  // __APPLE__
+#else
 #include <sys/syscall.h>
 #include <sys/types.h>
 #include <unistd.h>
+#endif
 
 #include <array>
 #include <cinttypes>
@@ -52,28 +58,52 @@
 //------------------------------------------------------------------------------
 
 ProcessTimer::ProcessTimer() : start_time_{absl::Now()}, start_rusage_{} {
+#if !defined(_WIN32)
   getrusage(RUSAGE_SELF, &start_rusage_);
+#endif
 }
 
 void ProcessTimer::Get(double& user, double& sys, double& wall) const {
+#if !defined(_WIN32)
   struct rusage curr_rusage = {};
   getrusage(RUSAGE_SELF, &curr_rusage);
-  // clang-format off
   user = absl::ToDoubleSeconds(
       absl::DurationFromTimeval(curr_rusage.ru_utime) -
       absl::DurationFromTimeval(start_rusage_.ru_utime));
   sys = absl::ToDoubleSeconds(
       absl::DurationFromTimeval(curr_rusage.ru_stime) -
       absl::DurationFromTimeval(start_rusage_.ru_stime));
+#else
+  user = 0;
+  sys = 0;
+#endif
   wall = absl::ToDoubleSeconds(absl::Now() - start_time_);
-  // clang-format on
 }
 
 //------------------------------------------------------------------------------
 //                               RUsageScope
 //------------------------------------------------------------------------------
 
-#ifdef __APPLE__
+#if defined(_WIN32)
+class RUsageScope::PlatformInfo {
+ public:
+  enum ProcFile : size_t {
+    kSched = 0,
+    kStatm = 1,
+    kStatus = 2,
+    kNumDoNotUseDirectly = 3
+  };
+  PlatformInfo(pid_t pid) : pid_(pid) {}
+  pid_t pid() const { return pid_; }
+  const std::string& GetProcFilePath(ProcFile file) const {
+    static const std::string empty;
+    return empty;
+  }
+
+ private:
+  pid_t pid_;
+};
+#elif defined(__APPLE__)
 class RUsageScope::PlatformInfo {
  public:
   PlatformInfo(pid_t pid) : pid_(pid) {}
@@ -112,7 +142,11 @@
 #endif
 
 RUsageScope RUsageScope::ThisProcess() {  //
+#if defined(_WIN32)
+  return RUsageScope{static_cast<int>(GetCurrentProcessId())};
+#else
   return RUsageScope{getpid()};
+#endif
 }
 
 RUsageScope RUsageScope::Process(pid_t pid) {  //
@@ -517,7 +551,9 @@
 RUsageMemory RUsageMemory::Snapshot(const RUsageScope& scope) {
   [[maybe_unused]] MemSize vsize = 0, rss = 0, shared = 0, code = 0, unused = 0,
                            data = 0, vpeak = 0;
-#ifdef __APPLE__
+#if defined(_WIN32)
+  // Win32 stub
+#elif defined(__APPLE__)
   if (scope.info().pid() != getpid()) return {};
   struct proc_taskinfo pti = {};
   FUZZTEST_CHECK(proc_pidinfo(scope.info().pid(), PROC_PIDTASKINFO, 0, &pti,
diff --git a/centipede/rusage_stats.h b/centipede/rusage_stats.h
index d0d1cde..125033e 100644
--- a/centipede/rusage_stats.h
+++ b/centipede/rusage_stats.h
@@ -21,7 +21,23 @@
 #ifndef THIRD_PARTY_CENTIPEDE_RUSAGE_STATS_H_
 #define THIRD_PARTY_CENTIPEDE_RUSAGE_STATS_H_
 
+#if defined(_WIN32)
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <windows.h>
+#include <winsock2.h>
+using pid_t = int;
+struct rusage {
+  struct timeval ru_utime;
+  struct timeval ru_stime;
+  size_t ru_maxrss;
+};
+#ifndef RUSAGE_SELF
+#define RUSAGE_SELF 0
+#endif
+#else
 #include <sys/resource.h>
+#endif
 
 #include <array>
 #include <cstddef>
diff --git a/centipede/sancov_callbacks.cc b/centipede/sancov_callbacks.cc
index 3e5d0c5..5e6019c 100644
--- a/centipede/sancov_callbacks.cc
+++ b/centipede/sancov_callbacks.cc
@@ -15,7 +15,13 @@
 // Instrumentation callbacks for SanitizerCoverage (sancov).
 // https://clang.llvm.org/docs/SanitizerCoverage.html
 
+#if !defined(_WIN32)
 #include <pthread.h>
+#else
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <windows.h>
+#endif
 
 #include <cstddef>
 #include <cstdint>
@@ -278,9 +284,11 @@
                          sp >= tls.stack_region_low &&
                          tls.stack_region_low > 0)) {
     tls.lowest_sp = sp;
+#if !defined(_WIN32)
     if (fuzztest::internal::CheckStackLimit == nullptr) {
       return;
     }
+#endif
     fuzztest::internal::CheckStackLimit(tls.top_frame_sp - sp,
                                         /*is_current_stack=*/true);
   }
@@ -353,7 +361,6 @@
 // __sanitizer_cov_trace_pc_guard() because
 // a) there is not use case for that currently and
 // b) it will slowdown the hot function.
-static pthread_once_t main_object_lazy_init_once = PTHREAD_ONCE_INIT;
 static void MainObjectLazyInitOnceCallback() {
   sancov_state->main_object = fuzztest::internal::GetDlInfo(
       sancov_state->flag_helper.GetStringFlag(":dl_path_suffix="));
@@ -362,9 +369,22 @@
   UpdatePcCounterSetSizeAligned(sancov_state->reverse_pc_table.NumPcs());
 }
 
+#if defined(_WIN32)
+static INIT_ONCE main_object_lazy_init_once = INIT_ONCE_STATIC_INIT;
+static BOOL CALLBACK MainObjectLazyInitOnceWin32(PINIT_ONCE, PVOID, PVOID*) {
+  MainObjectLazyInitOnceCallback();
+  return TRUE;
+}
+__attribute__((noinline)) static void MainObjectLazyInit() {
+  InitOnceExecuteOnce(&main_object_lazy_init_once, MainObjectLazyInitOnceWin32,
+                      nullptr, nullptr);
+}
+#else
+static pthread_once_t main_object_lazy_init_once = PTHREAD_ONCE_INIT;
 __attribute__((noinline)) static void MainObjectLazyInit() {
   pthread_once(&main_object_lazy_init_once, MainObjectLazyInitOnceCallback);
 }
+#endif
 
 // TODO(kcc): [impl] add proper testing for this callback.
 // TODO(kcc): make sure the pc_table in the engine understands the raw PCs.
diff --git a/centipede/sancov_interceptors.cc b/centipede/sancov_interceptors.cc
index cf1e9ac..14ce957 100644
--- a/centipede/sancov_interceptors.cc
+++ b/centipede/sancov_interceptors.cc
@@ -14,6 +14,93 @@
 
 // Function interceptors for Centipede.
 
+#if defined(_WIN32)
+#include "./centipede/runner_utils.h"
+#include "./centipede/sancov_state.h"
+
+namespace fuzztest::internal {
+void SancovInterceptor() {}
+}  // namespace fuzztest::internal
+
+using fuzztest::internal::tls;
+
+extern "C" {
+FUZZTEST_NO_SANITIZE
+void __sanitizer_weak_hook_strcasecmp(void*, const char* s1, const char* s2,
+                                      int result) {
+  if (ABSL_PREDICT_FALSE(!tls.traced)) return;
+  if (s1 == nullptr || s2 == nullptr) return;
+  size_t len = 0;
+  while (s1[len] && s2[len]) ++len;
+  tls.TraceMemCmp(reinterpret_cast<uintptr_t>(__builtin_return_address(0)),
+                  reinterpret_cast<const uint8_t*>(s1),
+                  reinterpret_cast<const uint8_t*>(s2), len, result == 0);
+}
+
+FUZZTEST_NO_SANITIZE
+void __sanitizer_weak_hook_memcmp(void*, const void* s1, const void* s2,
+                                  size_t n, int result) {
+  if (ABSL_PREDICT_FALSE(!tls.traced)) return;
+  if (s1 == nullptr || s2 == nullptr) return;
+  tls.TraceMemCmp(reinterpret_cast<uintptr_t>(__builtin_return_address(0)),
+                  reinterpret_cast<const uint8_t*>(s1),
+                  reinterpret_cast<const uint8_t*>(s2), n, result == 0);
+}
+
+FUZZTEST_NO_SANITIZE
+void __sanitizer_weak_hook_strncmp(void*, const char* s1, const char* s2,
+                                   size_t n, int result) {
+  if (ABSL_PREDICT_FALSE(!tls.traced)) return;
+  if (s1 == nullptr || s2 == nullptr) return;
+  size_t len = 0;
+  while (len < n && s1[len] && s2[len]) ++len;
+  tls.TraceMemCmp(reinterpret_cast<uintptr_t>(__builtin_return_address(0)),
+                  reinterpret_cast<const uint8_t*>(s1),
+                  reinterpret_cast<const uint8_t*>(s2), len, result == 0);
+}
+
+FUZZTEST_NO_SANITIZE
+void __sanitizer_weak_hook_strcmp(void*, const char* s1, const char* s2,
+                                  int result) {
+  if (ABSL_PREDICT_FALSE(!tls.traced)) return;
+  if (s1 == nullptr || s2 == nullptr) return;
+  size_t len = 0;
+  while (s1[len] && s2[len]) ++len;
+  tls.TraceMemCmp(reinterpret_cast<uintptr_t>(__builtin_return_address(0)),
+                  reinterpret_cast<const uint8_t*>(s1),
+                  reinterpret_cast<const uint8_t*>(s2), len, result == 0);
+}
+
+FUZZTEST_NO_SANITIZE
+void __sanitizer_weak_hook_strncasecmp(void* caller_pc, const char* s1,
+                                       const char* s2, size_t n, int result) {
+  if (ABSL_PREDICT_FALSE(!tls.traced)) return;
+  if (s1 == nullptr || s2 == nullptr) return;
+  size_t len = 0;
+  while (len < n && s1[len] && s2[len]) ++len;
+  tls.TraceMemCmp(reinterpret_cast<uintptr_t>(__builtin_return_address(0)),
+                  reinterpret_cast<const uint8_t*>(s1),
+                  reinterpret_cast<const uint8_t*>(s2), len, result == 0);
+}
+}
+
+namespace {
+
+void NTAPI tls_callback(PVOID, DWORD, PVOID) {
+  // May be calling multiple times, but it's fine as `OnThreadStart` is
+  // idempotent.
+  tls.OnThreadStart();
+}
+
+}  // namespace
+
+#pragma data_seg(".CRT$XLB")
+// Must be globally visible.
+PIMAGE_TLS_CALLBACK centipede_sancov_thread_callback = tls_callback;
+#pragma data_seg()
+
+#else
+
 #include <dlfcn.h>  // for dlsym()
 #include <pthread.h>
 
@@ -290,3 +377,5 @@
   // Run the actual pthread_create.
   return REAL(pthread_create)(thread, attr, MyThreadStart, wrapped_args);
 }
+
+#endif  // !defined(_WIN32)
diff --git a/centipede/sancov_state.cc b/centipede/sancov_state.cc
index c5bbaf3..50cb4f1 100644
--- a/centipede/sancov_state.cc
+++ b/centipede/sancov_state.cc
@@ -14,7 +14,9 @@
 
 #include "./centipede/sancov_state.h"
 
+#if !defined(_WIN32)
 #include <unistd.h>
+#endif
 
 #include <cstdint>
 #include <cstdio>
@@ -122,6 +124,7 @@
 }
 
 void ThreadLocalSancovState::OnThreadStart() {
+  if (tls.started) return;
   termination_detector.EnsureAlive();
   tls.started = true;
   // Always trace threads by default. Internal threads that do not want tracing
@@ -146,6 +149,7 @@
 }
 
 void ThreadLocalSancovState::OnThreadStop() {
+  if (!tls.started) return;
   tls.traced = false;
   LockGuard lock(sancov_state->tls_list_mu);
   const size_t sancov_lowest_sp = *tls.sancov_lowest_sp;
@@ -469,9 +473,13 @@
       }
     }
     const size_t sp_diff = tls.top_frame_sp - lowest_sp;
+#if !defined(_WIN32)
     if (CheckStackLimit != nullptr) {
       CheckStackLimit(sp_diff, /*is_current_stack=*/false);
     }
+#else
+    CheckStackLimit(sp_diff, /*is_current_stack=*/false);
+#endif
     if (sancov_state->flags.callstack_level != 0) {
       feature_handler(feature_domains::kCallStack.ConvertToMe(sp_diff));
     }
diff --git a/centipede/sancov_state.h b/centipede/sancov_state.h
index bde30e1..baf34b8 100644
--- a/centipede/sancov_state.h
+++ b/centipede/sancov_state.h
@@ -15,7 +15,13 @@
 #ifndef FUZZTEST_CENTIPEDE_SANCOV_STATE_H_
 #define FUZZTEST_CENTIPEDE_SANCOV_STATE_H_
 
+#if !defined(_WIN32)
 #include <pthread.h>
+#else
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <windows.h>
+#endif
 
 #include <algorithm>
 #include <cstddef>
@@ -49,14 +55,26 @@
 // An arbitrarily large size.
 constexpr size_t kCmpFeatureSetSize = 1 << 18;
 
-// Like std::lock_guard, but for pthread_mutex_t.
+#if defined(_WIN32)
+using MutexType = SRWLOCK;
+#define MUTEX_INITIALIZER SRWLOCK_INIT
+inline void MutexLock(MutexType* mu) { AcquireSRWLockExclusive(mu); }
+inline void MutexUnlock(MutexType* mu) { ReleaseSRWLockExclusive(mu); }
+#else
+using MutexType = pthread_mutex_t;
+#define MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER
+inline void MutexLock(MutexType* mu) { pthread_mutex_lock(mu); }
+inline void MutexUnlock(MutexType* mu) { pthread_mutex_unlock(mu); }
+#endif
+
+// Like std::lock_guard, but for MutexType.
 class LockGuard {
  public:
-  explicit LockGuard(pthread_mutex_t &mu) : mu_(mu) { pthread_mutex_lock(&mu); }
-  ~LockGuard() { pthread_mutex_unlock(&mu_); }
+  explicit LockGuard(MutexType& mu) : mu_(mu) { MutexLock(&mu_); }
+  ~LockGuard() { MutexUnlock(&mu_); }
 
  private:
-  pthread_mutex_t &mu_;
+  MutexType& mu_;
 };
 
 // Flags derived from CENTIPEDE_RUNNER_FLAGS.
@@ -192,7 +210,7 @@
   // Doubly linked list of detached TLSs.
   ThreadLocalSancovState *detached_tls_list;
   // Guards `tls_list` and `detached_tls_list`.
-  pthread_mutex_t tls_list_mu = PTHREAD_MUTEX_INITIALIZER;
+  MutexType tls_list_mu = MUTEX_INITIALIZER;
   // Iterates all TLS objects under tls_list_mu, except those with `ignore` set.
   // Calls `callback()` on every TLS.
   template <typename Callback>
@@ -319,8 +337,12 @@
 
 // Check for stack limit for `stack_usage`, with `is_current_stack` set if it
 // is for the current calling stack.
+#if !defined(_WIN32)
 __attribute__((weak)) void CheckStackLimit(size_t stack_usage,
                                            bool is_current_stack);
+#else
+void CheckStackLimit(size_t stack_usage, bool is_current_stack);
+#endif
 
 extern ExplicitLifetime<SancovState> sancov_state;
 extern __thread ThreadLocalSancovState tls;
diff --git a/centipede/seed_corpus_maker_lib.cc b/centipede/seed_corpus_maker_lib.cc
index d9171c7..cd47839 100644
--- a/centipede/seed_corpus_maker_lib.cc
+++ b/centipede/seed_corpus_maker_lib.cc
@@ -127,7 +127,7 @@
     absl::flat_hash_set<std::string> current_corpus_shard_fnames;
     if (!source.shard_rel_glob.empty()) {
       std::vector<std::string> matched_fnames;
-      const std::string glob = fs::path{dir} / source.shard_rel_glob;
+      const std::string glob = (fs::path{dir} / source.shard_rel_glob).string();
       const auto match_status = RemoteGlobMatch(glob, matched_fnames);
       if (!match_status.ok() && !absl::IsNotFound(match_status)) {
         FUZZTEST_LOG(ERROR) << "Got error when glob-matching in " << dir << ": "
@@ -144,7 +144,8 @@
     }
     if (!source.individual_input_rel_glob.empty()) {
       std::vector<std::string> matched_fnames;
-      const std::string glob = fs::path{dir} / source.individual_input_rel_glob;
+      const std::string glob =
+          (fs::path{dir} / source.individual_input_rel_glob).string();
       const auto match_status = RemoteGlobMatch(glob, matched_fnames);
       if (!match_status.ok() && !absl::IsNotFound(match_status)) {
         FUZZTEST_LOG(ERROR) << "Got error when glob-matching in " << dir << ": "
@@ -426,7 +427,7 @@
         const std::string corpus_rel_fname =
             absl::StrReplaceAll(destination.shard_rel_glob, {{"*", shard_idx}});
         const std::string corpus_fname =
-            fs::path{destination.dir_path} / corpus_rel_fname;
+            (fs::path{destination.dir_path} / corpus_rel_fname).string();
 
         const auto work_dir = WorkDir::FromCorpusShardPath(  //
             corpus_fname, coverage_binary_name, coverage_binary_hash);
diff --git a/centipede/shared_memory_blob_sequence.cc b/centipede/shared_memory_blob_sequence.cc
index 3a24126..7026004 100644
--- a/centipede/shared_memory_blob_sequence.cc
+++ b/centipede/shared_memory_blob_sequence.cc
@@ -14,15 +14,23 @@
 
 #include "./centipede/shared_memory_blob_sequence.h"
 
+#if defined(_WIN32)
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <windows.h>
+#else
 #include <fcntl.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/mman.h>
 #include <sys/stat.h>
 #include <unistd.h>
+#endif
 
 #include <cstdint>
 #include <cstdio>
+#include <cstdlib>
+#include <cstring>
 
 #include "absl/base/nullability.h"
 
@@ -104,6 +112,59 @@
   had_writes_after_reset_ = false;
 }
 
+#if defined(_WIN32)
+
+SharedMemoryBlobSequence::SharedMemoryBlobSequence(const char* name,
+                                                   size_t size,
+                                                   bool use_posix_shmem) {
+  ErrorOnFailure(size < sizeof(Blob::size), "Size too small");
+  size_ = size;
+  strncpy(path_, name, PATH_MAX - 1);
+  path_[PATH_MAX - 1] = '\0';
+  path_is_owned_ = true;
+  HANDLE hMap =
+      CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
+                         static_cast<DWORD>(size_ >> 32),
+                         static_cast<DWORD>(size_ & 0xFFFFFFFF), path_);
+  ErrorOnFailure(hMap == NULL, "CreateFileMappingA() failed");
+  mapping_handle_ = hMap;
+  data_ = static_cast<uint8_t*>(
+      MapViewOfFile(hMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, size_));
+  ErrorOnFailure(data_ == NULL, "MapViewOfFile() failed");
+}
+
+SharedMemoryBlobSequence::SharedMemoryBlobSequence(const char* path) {
+  strncpy(path_, path, PATH_MAX - 1);
+  path_[PATH_MAX - 1] = '\0';
+  HANDLE hMap = OpenFileMappingA(FILE_MAP_READ | FILE_MAP_WRITE, FALSE, path_);
+  ErrorOnFailure(hMap == NULL, "OpenFileMappingA() failed");
+  mapping_handle_ = hMap;
+  data_ = static_cast<uint8_t*>(
+      MapViewOfFile(hMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0));
+  ErrorOnFailure(data_ == NULL, "MapViewOfFile() failed");
+  MEMORY_BASIC_INFORMATION mbi = {};
+  if (VirtualQuery(data_, &mbi, sizeof(mbi)) != 0) {
+    size_ = mbi.RegionSize;
+  }
+}
+
+void SharedMemoryBlobSequence::MmapData() {}
+
+SharedMemoryBlobSequence::~SharedMemoryBlobSequence() {
+  if (data_ != nullptr) {
+    UnmapViewOfFile(data_);
+  }
+  if (mapping_handle_ != nullptr) {
+    CloseHandle(static_cast<HANDLE>(mapping_handle_));
+  }
+}
+
+void SharedMemoryBlobSequence::ReleaseSharedMemory() {}
+
+size_t SharedMemoryBlobSequence::NumBytesUsed() const { return size_; }
+
+#else  // !_WIN32
+
 SharedMemoryBlobSequence::SharedMemoryBlobSequence(const char *name,
                                                    size_t size,
                                                    bool use_posix_shmem) {
@@ -185,4 +246,6 @@
   return statbuf.st_blocks * S_BLKSIZE;
 }
 
+#endif  // _WIN32
+
 }  // namespace fuzztest::internal
diff --git a/centipede/shared_memory_blob_sequence.h b/centipede/shared_memory_blob_sequence.h
index fc69c10..8d91119 100644
--- a/centipede/shared_memory_blob_sequence.h
+++ b/centipede/shared_memory_blob_sequence.h
@@ -175,6 +175,10 @@
   const char *absl_nonnull path() const { return path_; }
 
  private:
+#ifndef PATH_MAX
+#define PATH_MAX 4096
+#endif
+
   // mmaps `size_` bytes from `fd_`, assigns to `data_`. Crashes if mmap failed.
   void MmapData();
 
@@ -182,6 +186,9 @@
   // passed in.
   char path_[PATH_MAX] = {0};
   int fd_ = -1;  // file descriptor used to mmap the shared memory region.
+#if defined(_WIN32)
+  void* mapping_handle_ = nullptr;
+#endif
   // Whether the file pointed to by path_ is owned by this and needs to be
   // deallocated on destruction.
   bool path_is_owned_ = false;
diff --git a/centipede/shared_memory_blob_sequence_test.cc b/centipede/shared_memory_blob_sequence_test.cc
index 2b9f557..4e8210f 100644
--- a/centipede/shared_memory_blob_sequence_test.cc
+++ b/centipede/shared_memory_blob_sequence_test.cc
@@ -14,7 +14,14 @@
 
 #include "./centipede/shared_memory_blob_sequence.h"
 
+#if !defined(_WIN32)
 #include <unistd.h>
+#else
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <process.h>
+#include <windows.h>
+#endif
 
 #include <cstdint>
 #include <cstdlib>
@@ -30,7 +37,12 @@
 
 std::string ShmemName() {
   std::ostringstream oss;
+#if defined(_WIN32)
+  oss << "/shm_test-" << GetCurrentProcessId() << "-"
+      << std::this_thread::get_id();
+#else
   oss << "/shm_test-" << getpid() << "-" << std::this_thread::get_id();
+#endif
   return oss.str();
 }
 
@@ -237,8 +249,8 @@
   EXPECT_FALSE(blob2.IsValid());
 }
 
-// MacOS does not support releasing the shm memory.
-#ifndef __APPLE__
+// MacOS and Windows do not support releasing the shm memory.
+#if !defined(__APPLE__) && !defined(_WIN32)
 // Test ReleaseSharedMemory and NumBytesUsed.
 TEST_P(SharedMemoryBlobSequenceTest, ReleaseSharedMemory) {
   // Allocate a blob sequence with 1M bytes of storage.
diff --git a/centipede/symbol_table.cc b/centipede/symbol_table.cc
index e693b48..f347775 100644
--- a/centipede/symbol_table.cc
+++ b/centipede/symbol_table.cc
@@ -99,7 +99,8 @@
                                        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} /
@@ -123,7 +124,7 @@
       "<",
       std::string(pcs_file.path()),
   };
-  cmd_options.stdout_file_prefix = symbols_file_prefix;
+  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 +173,7 @@
   {
     // 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/testing/test_fuzz_target.cc b/centipede/testing/test_fuzz_target.cc
index 37ad23d..6b5edaf 100644
--- a/centipede/testing/test_fuzz_target.cc
+++ b/centipede/testing/test_fuzz_target.cc
@@ -20,7 +20,12 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#if !defined(_WIN32)
 #include <unistd.h>
+#else
+#include <windows.h>
+inline void sleep(unsigned int seconds) { Sleep(seconds * 1000); }
+#endif
 
 #include <utility>
 
@@ -112,6 +117,7 @@
     CallThisRecursively(100);
   }
 
+#if !defined(_WIN32)
   // Stack overflow that triggers an instrumented SIGABRT handler in the current
   // stack.
   if (size == 4 && data[0] == 's' && data[1] == 't' && data[2] == 'k' &&
@@ -169,6 +175,7 @@
     }
     free(sigstk.ss_sp);
   }
+#endif
 
   // Call SingleEdgeFunc() if input is "func1".
   if (size == 5 && data[0] == 'f' && data[1] == 'u' && data[2] == 'n' &&
diff --git a/centipede/util.cc b/centipede/util.cc
index ab2eb19..dd0153f 100644
--- a/centipede/util.cc
+++ b/centipede/util.cc
@@ -17,8 +17,43 @@
 
 #include "./centipede/util.h"
 
+#if defined(_WIN32)
+#define WIN32_LEAN_AND_MEAN
+#define NOGDI
+#include <process.h>
+#include <windows.h>
+#include <winsock2.h>
+extern "C" {
+void win_atomic_store(size_t size, void* ptr, void* val,
+                      int model) __asm__("__atomic_store");
+void win_atomic_store(size_t size, void* ptr, void* val, int model) {
+  static CRITICAL_SECTION cs;
+  [[maybe_unused]] static bool init = []() {
+    InitializeCriticalSection(&cs);
+    return true;
+  }();
+  EnterCriticalSection(&cs);
+  std::memcpy(ptr, val, size);
+  LeaveCriticalSection(&cs);
+}
+
+void win_atomic_load(size_t size, const void* ptr, void* val,
+                     int model) __asm__("__atomic_load");
+void win_atomic_load(size_t size, const void* ptr, void* val, int model) {
+  static CRITICAL_SECTION cs;
+  [[maybe_unused]] static bool init = []() {
+    InitializeCriticalSection(&cs);
+    return true;
+  }();
+  EnterCriticalSection(&cs);
+  std::memcpy(val, ptr, size);
+  LeaveCriticalSection(&cs);
+}
+}
+#else
 #include <sys/mman.h>
 #include <unistd.h>
+#endif
 
 #include <algorithm>
 #include <cctype>
@@ -47,6 +82,7 @@
 #include "absl/base/const_init.h"
 #include "absl/base/nullability.h"
 #include "absl/base/thread_annotations.h"
+#include "absl/strings/str_cat.h"
 #include "absl/strings/str_format.h"
 #include "absl/strings/str_replace.h"
 #include "absl/strings/str_split.h"
@@ -61,10 +97,24 @@
 
 namespace fuzztest::internal {
 
+void EnsureWinsockInitialized() {
+#if defined(_WIN32)
+  [[maybe_unused]] static bool init = []() {
+    WSADATA wsa_data;
+    return WSAStartup(MAKEWORD(2, 2), &wsa_data) == 0;
+  }();
+#endif
+}
+
 size_t GetRandomSeed(size_t seed) {
   if (seed != 0) return seed;
+#if defined(_WIN32)
+  return time(nullptr) + GetCurrentProcessId() +
+         std::hash<std::thread::id>{}(std::this_thread::get_id());
+#else
   return time(nullptr) + getpid() +
          std::hash<std::thread::id>{}(std::this_thread::get_id());
+#endif
 }
 
 std::string AsPrintableString(ByteSpan data, size_t max_len) {
@@ -83,7 +133,7 @@
 
 template <typename Container>
 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 +162,13 @@
 }
 
 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<const char *>(data.data()),
           static_cast<int64_t>(data.size()));
@@ -136,13 +187,15 @@
 
 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 +208,25 @@
 }
 
 std::string ProcessAndThreadUniqueID(std::string_view prefix) {
-  // operator << is the only way to serialize std::this_thread::get_id().
-  std::ostringstream oss;
-  oss << prefix << getpid() << "-" << std::this_thread::get_id();
-  return oss.str();
+  static std::atomic<size_t> counter{0};
+  std::string res;
+#if defined(_WIN32)
+  res = absl::StrCat(prefix, GetCurrentProcessId(), "-", GetCurrentThreadId(),
+                     "-", counter.fetch_add(1));
+#else
+  res = absl::StrCat(prefix, getpid(), "-", counter.fetch_add(1));
+#endif
+  return res;
 }
 
 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 +250,15 @@
 
 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(path.find("/centipede-") != std::string::npos ||
+                 path.find("\\centipede-") != std::string::npos);
   // 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 +269,7 @@
 }
 
 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 +422,53 @@
   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<void*>(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)
+  static bool installed_veh = []() {
+    AddVectoredExceptionHandler(1, AutoCommitPageFaultHandler);
+    return true;
+  }();
+  (void)installed_veh;
+  auto result =
+      VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
+  if (result == nullptr) {
+    result = VirtualAlloc(nullptr, size, MEM_RESERVE, PAGE_READWRITE);
+  }
+  FUZZTEST_CHECK(result != nullptr)
+      << "VirtualAlloc failed for size " << size << " err=" << GetLastError();
+  return reinterpret_cast<uint8_t*>(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<uint8_t *>(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.h b/centipede/util.h
index 4905c68..0cf8e12 100644
--- a/centipede/util.h
+++ b/centipede/util.h
@@ -31,6 +31,9 @@
 
 namespace fuzztest::internal {
 
+// Ensures WSAStartup is called on Windows before any socket operations.
+void EnsureWinsockInitialized();
+
 // Returns the hash of the contents of the file `file_path`. Supports the file
 // being remote. Returns an empty string if the `file_path` is empty.
 std::string HashOfFileContents(std::string_view file_path);
diff --git a/centipede/workdir.cc b/centipede/workdir.cc
index c0f4090..676f8b6 100644
--- a/centipede/workdir.cc
+++ b/centipede/workdir.cc
@@ -58,7 +58,7 @@
 WorkDir::PathShards::PathShards(std::string_view base_dir,
                                 std::string_view rel_prefix,
                                 size_t my_shard_index)
-    : prefix_{std::filesystem::path(base_dir) / rel_prefix},
+    : prefix_{(std::filesystem::path(base_dir) / rel_prefix).string()},
       my_shard_index_{my_shard_index} {}
 
 std::string WorkDir::PathShards::Shard(size_t shard_index) const {
@@ -97,12 +97,12 @@
     std::string_view binary_name,        //
     std::string_view binary_hash) {
   const std::filesystem::path path{corpus_shard_path};
-  const std::string dir = path.parent_path();
-  const std::string stem = path.stem();
+  const std::string dir = path.parent_path().string();
+  const std::string stem = path.stem().string();
   FUZZTEST_CHECK(stem == kCorpusShardStem ||
                  absl::StartsWith(stem, kDistilledCorpusShardStemPrefix))
       << VV(corpus_shard_path);
-  const std::string dot_ext = path.extension();
+  const std::string dot_ext = path.extension().string();
   FUZZTEST_CHECK(!dot_ext.empty() && dot_ext[0] == '.')
       << VV(corpus_shard_path);
   const std::string ext = dot_ext.substr(1);
@@ -142,8 +142,9 @@
 }
 
 std::string WorkDir::CoverageDirPath() const {
-  return std::filesystem::path(workdir_) /
-         absl::StrCat(binary_name_, "-", binary_hash_);
+  return (std::filesystem::path(workdir_) /
+          absl::StrCat(binary_name_, "-", binary_hash_))
+      .string();
 }
 
 WorkDir::PathShards WorkDir::CrashReproducerDirPaths() const {
@@ -155,11 +156,11 @@
 }
 
 std::string WorkDir::BinaryInfoDirPath() const {
-  return std::filesystem::path(CoverageDirPath()) / "binary-info";
+  return (std::filesystem::path(CoverageDirPath()) / "binary-info").string();
 }
 
 std::string WorkDir::DebugInfoDirPath() const {
-  return std::filesystem::path(workdir_) / "debug";
+  return (std::filesystem::path(workdir_) / "debug").string();
 }
 
 WorkDir::PathShards WorkDir::DistilledCorpusFilePaths() const {
@@ -179,53 +180,60 @@
 }
 
 std::string WorkDir::CoverageReportPath(std::string_view annotation) const {
-  return std::filesystem::path(workdir_) /
-         absl::StrFormat("coverage-report-%s.%0*d%s.txt", binary_name_,
-                         kDigitsInShardIndex, my_shard_index_,
-                         NormalizeAnnotation(annotation));
+  return (std::filesystem::path(workdir_) /
+          absl::StrFormat("coverage-report-%s.%0*d%s.txt", binary_name_,
+                          kDigitsInShardIndex, my_shard_index_,
+                          NormalizeAnnotation(annotation)))
+      .string();
 }
 
 std::string WorkDir::CorpusStatsPath(std::string_view annotation) const {
-  return std::filesystem::path(workdir_) /
-         absl::StrFormat("corpus-stats-%s.%0*d%s.json", binary_name_,
-                         kDigitsInShardIndex, my_shard_index_,
-                         NormalizeAnnotation(annotation));
+  return (std::filesystem::path(workdir_) /
+          absl::StrFormat("corpus-stats-%s.%0*d%s.json", binary_name_,
+                          kDigitsInShardIndex, my_shard_index_,
+                          NormalizeAnnotation(annotation)))
+      .string();
 }
 
 std::string WorkDir::FuzzingStatsPath(std::string_view annotation) const {
-  return std::filesystem::path(workdir_) /
-         absl::StrFormat("fuzzing-stats-%s.%0*d%s.csv", binary_name_,
-                         kDigitsInShardIndex, my_shard_index_,
-                         NormalizeAnnotation(annotation));
+  return (std::filesystem::path(workdir_) /
+          absl::StrFormat("fuzzing-stats-%s.%0*d%s.csv", binary_name_,
+                          kDigitsInShardIndex, my_shard_index_,
+                          NormalizeAnnotation(annotation)))
+      .string();
 }
 
 std::string WorkDir::SourceBasedCoverageRawProfilePath() const {
   // Pass %m to enable online merge mode: updates file in place instead of
   // replacing it %m is replaced by lprofGetLoadModuleSignature(void) which
   // should be consistent for a fixed binary
-  return std::filesystem::path(CoverageDirPath()) /
-         absl::StrFormat("clang_coverage.%0*d.%s.profraw", kDigitsInShardIndex,
-                         my_shard_index_, "%m");
+  return (std::filesystem::path(CoverageDirPath()) /
+          absl::StrFormat("clang_coverage.%0*d.%s.profraw", kDigitsInShardIndex,
+                          my_shard_index_, "%m"))
+      .string();
 }
 
 std::string WorkDir::SourceBasedCoverageIndexedProfilePath() const {
-  return std::filesystem::path(CoverageDirPath()) /
-         absl::StrFormat("clang_coverage.profdata");
+  return (std::filesystem::path(CoverageDirPath()) /
+          absl::StrFormat("clang_coverage.profdata"))
+      .string();
 }
 
 std::string WorkDir::SourceBasedCoverageReportPath(
     std::string_view annotation) const {
-  return std::filesystem::path(workdir_) /
-         absl::StrFormat("source-coverage-report-%s.%0*d%s", binary_name_,
-                         kDigitsInShardIndex, my_shard_index_,
-                         NormalizeAnnotation(annotation));
+  return (std::filesystem::path(workdir_) /
+          absl::StrFormat("source-coverage-report-%s.%0*d%s", binary_name_,
+                          kDigitsInShardIndex, my_shard_index_,
+                          NormalizeAnnotation(annotation)))
+      .string();
 }
 
 std::string WorkDir::RUsageReportPath(std::string_view annotation) const {
-  return std::filesystem::path(workdir_) /
-         (absl::StrFormat("rusage-report-%s.%0*d%s.txt", binary_name_,
-                          kDigitsInShardIndex, my_shard_index_,
-                          NormalizeAnnotation(annotation)));
+  return (std::filesystem::path(workdir_) /
+          (absl::StrFormat("rusage-report-%s.%0*d%s.txt", binary_name_,
+                           kDigitsInShardIndex, my_shard_index_,
+                           NormalizeAnnotation(annotation))))
+      .string();
 }
 
 std::vector<std::string> WorkDir::EnumerateRawCoverageProfiles() const {
@@ -247,7 +255,7 @@
   std::vector<std::string> raw_profiles;
   for (const auto &entry : dir_iter) {
     if (entry.is_regular_file() && entry.path().extension() == ".profraw")
-      raw_profiles.push_back(entry.path());
+      raw_profiles.push_back(entry.path().string());
   }
   return raw_profiles;
 }
diff --git a/common/remote_file_oss.cc b/common/remote_file_oss.cc
index d5ccbff..dc3e3dd 100644
--- a/common/remote_file_oss.cc
+++ b/common/remote_file_oss.cc
@@ -62,7 +62,13 @@
  public:
   static absl::StatusOr<LocalRemoteFile *> Create(std::string path,
                                                   std::string_view mode) {
-    FILE *file = std::fopen(path.c_str(), mode.data());
+    std::string mode_str(mode);
+#if defined(_WIN32)
+    if (mode_str.find('b') == std::string::npos) {
+      mode_str += "b";
+    }
+#endif
+    FILE* file = std::fopen(path.c_str(), mode_str.c_str());
     if (file == nullptr) {
       return absl::UnknownError(absl::StrCat(
           "fopen() failed, path: ", path, ", errno: ", std::strerror(errno)));
@@ -344,7 +350,7 @@
 }
 
 absl::StatusOr<int64_t> RemoteFileGetSize(std::string_view path) {
-  FILE *f = std::fopen(path.data(), "r");
+  FILE* f = std::fopen(path.data(), "rb");
   if (f == nullptr) {
     return absl::UnknownError(
         absl::StrCat("fopen() failed, path: ", std::string(path),
diff --git a/common/test_util.cc b/common/test_util.cc
index 3232d55..fac7238 100644
--- a/common/test_util.cc
+++ b/common/test_util.cc
@@ -18,6 +18,17 @@
 #include <string_view>
 #include <system_error>  // NOLINT
 
+#if !defined(_WIN32)
+#include <unistd.h>
+#else
+#include <process.h>
+#include <windows.h>
+inline static int getpid() { return static_cast<int>(GetCurrentProcessId()); }
+inline static void setenv(const char* name, const char* value, int) {
+  _putenv_s(name, value);
+}
+#endif
+
 #include "gtest/gtest.h"
 #include "absl/strings/str_cat.h"
 #include "./common/logging.h"
@@ -35,11 +46,12 @@
     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.", getpid(), ".", i))
+      .string();
 }
 
 std::filesystem::path GetTestRunfilesDir() {
diff --git a/common/test_util.h b/common/test_util.h
index c859bff..e363f18 100644
--- a/common/test_util.h
+++ b/common/test_util.h
@@ -79,7 +79,7 @@
   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 {