No public description

PiperOrigin-RevId: 953602830
diff --git a/.bazelrc b/.bazelrc
index 19ff5f5..5d9c8c3 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -44,3 +44,9 @@
 #  bazel run //bazel:setup_configs > fuzztest.bazelrc
 #
 try-import %workspace%/fuzztest.bazelrc
+
+# Configure rules_rust to use the nightly channel toolchain by default.
+build --@rules_rust//rust/toolchain/channel:channel=nightly
+
+# Link standard C++ library for Rust binaries on Linux
+build:linux --linkopt=-lstdc++
diff --git a/BUILD.bazel b/BUILD.bazel
new file mode 100644
index 0000000..7627798
--- /dev/null
+++ b/BUILD.bazel
@@ -0,0 +1 @@
+exports_files(["MODULE.bazel"])
diff --git a/MODULE.bazel b/MODULE.bazel
index 4610482..08577ec 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -28,6 +28,48 @@
     version = "0.6.1",
 )
 bazel_dep(
+    name = "rules_rust",
+    version = "0.71.3",
+)
+
+rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
+rust.toolchain(
+    edition = "2024",
+    versions = ["nightly/2025-01-15"],
+)
+use_repo(rust, "rust_toolchains")
+register_toolchains("@rust_toolchains//:all")
+
+crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate")
+crate.spec(package = "anyhow", version = "1.0.100")
+crate.spec(package = "clap", version = "4.6.1", features = ["derive", "env"])
+crate.spec(package = "convert_case", version = "0.6.0")
+crate.spec(package = "googletest", version = "0.14.3")
+crate.spec(package = "humantime", version = "2.4.0")
+crate.spec(package = "inventory", version = "0.3.20")
+crate.spec(package = "num-traits", version = "0.2.19")
+crate.spec(package = "postcard", version = "1.0.0", features = ["use-std"])
+crate.spec(package = "proc-macro2", version = "1.0.106")
+crate.spec(package = "proc-macro-crate", version = "3.4.0")
+crate.spec(package = "quote", version = "1.0.45")
+crate.spec(package = "rand", version = "0.10.1")
+crate.spec(package = "serde", version = "1.0.219", features = ["derive"])
+crate.spec(package = "spin", version = "0.10.0")
+crate.spec(
+    package = "syn",
+    version = "2.0.117",
+    features = [
+        "extra-traits",
+        "full",
+        "parsing",
+        "printing",
+        "visit-mut",
+    ],
+)
+crate.spec(package = "tempfile", version = "3.27.0")
+crate.from_specs(name = "crate_index")
+use_repo(crate, "crate_index")
+bazel_dep(
     name = "abseil-cpp",
     version = "20260526.0",
 )
diff --git a/centipede/BUILD b/centipede/BUILD
index 518d7a6..057fb5e 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -980,8 +980,6 @@
     name = "engine_worker",
     srcs = [
         "engine_worker.cc",
-        "runner_utils.cc",
-        "runner_utils.h",
     ],
     hdrs = ["engine_worker_abi.h"],
     deps = [
@@ -990,6 +988,7 @@
         ":feature",
         ":runner_request",
         ":runner_result",
+        ":runner_utils",
         ":shared_memory_blob_sequence",
         "@abseil-cpp//absl/base:nullability",
         "@com_google_fuzztest//common:defs",
@@ -1216,8 +1215,6 @@
         "reverse_pc_table.h",
         "runner_dl_info.cc",
         "runner_dl_info.h",
-        "runner_utils.cc",
-        "runner_utils.h",
         "sancov_callbacks.cc",
         "sancov_interceptors.cc",
         "sancov_object_array.cc",
@@ -1239,6 +1236,7 @@
         ":foreach_nonzero",
         ":int_utils",
         ":runner_cmp_trace",
+        ":runner_utils",
         "@abseil-cpp//absl/base:core_headers",
         "@abseil-cpp//absl/base:nullability",
         "@abseil-cpp//absl/numeric:bits",
@@ -2002,3 +2000,23 @@
         ":test_util_sh",
     ],
 )
+
+cc_library(
+    name = "runner_utils",
+    srcs = ["runner_utils.cc"],
+    hdrs = ["runner_utils.h"],
+    copts = DISABLE_SANCOV_COPTS,
+    deps = [
+        "@abseil-cpp//absl/base:nullability",
+    ],
+)
+
+cc_static_library(
+    name = "centipede_engine_static",
+    deps = [
+        ":engine_controller_with_subprocess",
+        ":engine_worker",
+        ":sancov_runtime",
+        ":weak_sancov_stubs",
+    ],
+)
diff --git a/rust/.rustfmt.toml b/rust/.rustfmt.toml
new file mode 100644
index 0000000..33b356d
--- /dev/null
+++ b/rust/.rustfmt.toml
@@ -0,0 +1,3 @@
+use_field_init_shorthand = true
+use_try_shorthand = true
+edition = "2021"
diff --git a/rust/BUILD b/rust/BUILD
new file mode 100644
index 0000000..e41586b
--- /dev/null
+++ b/rust/BUILD
@@ -0,0 +1,45 @@
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+
+licenses(["notice"])
+
+exports_files(["BUILD"])
+
+rust_library(
+    name = "fuzztest",
+    srcs = glob([
+        "src/**/*.rs",
+    ]),
+    edition = "2024",
+    proc_macro_deps = [
+        "@com_google_fuzztest//rust/fuzztest_macro:fuzztest_macro",
+    ],
+    rustc_flags = ["-Zallow-features=cfg_sanitize"],
+    visibility = ["__subpackages__"],
+    deps = [
+        "@crate_index//:anyhow",  # v1
+        "@crate_index//:clap",  # v4
+        "@crate_index//:humantime",  # v2
+        "@crate_index//:inventory",  # v0_3
+        "@crate_index//:num-traits",
+        "@crate_index//:postcard",  # v1
+        "@crate_index//:rand",  # v0_10
+        "@crate_index//:serde",  # v1
+        "@crate_index//:spin",  # v0_10
+        "@crate_index//:tempfile",  # v3
+        "@com_google_fuzztest//rust/coverage",
+        "@com_google_fuzztest//rust/engine",
+        "@com_google_fuzztest//rust/options:fuzztest_options",
+    ],
+)
+
+rust_test(
+    name = "fuzztest_test",
+    # Avoid interference when setting/resetting environment variables in tests.
+    args = ["--test-threads=1"],
+    crate = ":fuzztest",
+    edition = "2024",
+    rustc_flags = ["-Zallow-features=cfg_sanitize"],
+    deps = [
+        "@crate_index//:googletest",
+    ],
+)
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
new file mode 100644
index 0000000..0c9bf49
--- /dev/null
+++ b/rust/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "fuzztest"
+version = "0.1.0"
+edition = "2024"
+
+[[test]]
+name = "trybuild"
+path = "tests/trybuild.rs"
+
+[dependencies]
+anyhow = "1.0.100"
+fuzztest-macro = { path = "fuzztest_macro" }
+fuzztest-options = { path = "options" }
+inventory = "0.3.20"
+num-traits = "0.2"
+postcard = { version = "1.0.0", features = ["use-std"] }
+rand = { version = "0.10.1", features = ["std_rng"] }
+serde = { version = "1.0", features = ["derive"] }
+coverage = { path = "coverage" }
+engine = { path = "engine", package = "engine-ffi" }
+spin = "0.10.0"
+tempfile = "3.27.0"
+clap = { version = "4.6.1", features = ["derive", "env"] }
+humantime = "2.4.0"
+
+[dev-dependencies]
+googletest = "0.14.3"
+
+[dev-dependencies.trybuild]
+version = "1.0.103"
+features = ["diff"]
+
+[profile.fuzztest]
+inherits = 'release'
+panic = 'abort'
+opt-level = "s"
+debug = true
+split-debuginfo = "packed"
diff --git a/rust/README.md b/rust/README.md
new file mode 100644
index 0000000..482c203
--- /dev/null
+++ b/rust/README.md
@@ -0,0 +1,63 @@
+# FuzzTest Rust
+
+A framework for fuzzing Rust projects using Google FuzzTest.
+
+## Prerequisites
+
+1.  Clone the repository.
+2.  Build the C++ Centipede static library engine using Bazel (from the
+    repository root):
+
+    ```bash
+    cd /path/to/fuzztest
+    bazel build //centipede:centipede_engine_static
+    ```
+
+3.  Copy the libcentipede_engine_static.a library to another location:
+
+    ```bash
+    mkdir -p $HOME/.local/lib
+    cp /path/to/fuzztest/bazel-bin/centipede/libcentipede_engine_static.a \
+        $HOME/.local/lib/
+    ```
+
+## Setup a Project
+
+1.  Create a new Rust project:
+
+    ```bash
+    cargo new my_fuzz_project --bin
+    ```
+
+2.  Add `fuzztest` as a dependency in your `Cargo.toml`:
+
+    ```toml
+    [dependencies]
+    fuzztest = { path = "/path/to/fuzztest/rust" }
+    googletest = "0.14.3"
+    ```
+
+## Write a Fuzz Test
+
+In `src/main.rs`:
+
+```rust
+#[cfg(test)]
+mod tests {
+    use fuzztest::domains::arbitrary::Arbitrary;
+    use fuzztest::fuzztest;
+
+    #[fuzztest(a = Arbitrary::<i32>::default(), b = Arbitrary::<i32>::default())]
+    fn test_addition(a: i32, b: i32) {
+        let _ = a.wrapping_add(b);
+    }
+}
+```
+
+## Build and Run Fuzz Tests
+
+To run the fuzz tests, you must specify:
+
+```bash
+RUSTFLAGS="-L $HOME/.local/lib" cargo test __fuzztest_mod__::test_addition
+```
diff --git a/rust/cargo_fuzztest/BUILD b/rust/cargo_fuzztest/BUILD
new file mode 100644
index 0000000..dc8cfef
--- /dev/null
+++ b/rust/cargo_fuzztest/BUILD
@@ -0,0 +1,34 @@
+load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
+
+licenses(["notice"])
+
+rust_library(
+    name = "cargo_fuzztest",
+    srcs = ["src/lib.rs"],
+    edition = "2024",
+    visibility = ["@com_google_fuzztest//rust:__subpackages__"],
+    deps = [
+        "@crate_index//:anyhow",  # v1
+        "@crate_index//:clap",  # v4
+        "@crate_index//:serde",  # v1
+        "@crate_index//:serde_json",  # v1
+    ],
+)
+
+rust_binary(
+    name = "cargo_fuzztest_bin",
+    srcs = ["src/main.rs"],
+    edition = "2024",
+    visibility = ["@com_google_fuzztest//rust:__subpackages__"],
+    deps = [
+        ":cargo_fuzztest",
+        "@crate_index//:anyhow",  # v1
+        "@crate_index//:clap",  # v4
+    ],
+)
+
+rust_test(
+    name = "cargo_fuzztest_lib_test",
+    crate = ":cargo_fuzztest",
+    edition = "2024",
+)
diff --git a/rust/cargo_fuzztest/Cargo.toml b/rust/cargo_fuzztest/Cargo.toml
new file mode 100644
index 0000000..7d6efe8
--- /dev/null
+++ b/rust/cargo_fuzztest/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "cargo-fuzztest"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+clap = { version = "4.5", features = ["derive"] }
+anyhow = "1.0.100"
+serde = "1.0.228"
+serde_json = "1.0.150"
+fuzztest-options = { path = "../options" }
diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs
new file mode 100644
index 0000000..9fe0067
--- /dev/null
+++ b/rust/cargo_fuzztest/src/lib.rs
@@ -0,0 +1,242 @@
+use anyhow::{Context, Result};
+use clap::Parser;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+#[derive(Parser, Debug, Clone, Default)]
+pub struct CargoFuzzTestOptions {
+    /// List all fuzz tests in the crate without running them.
+    #[arg(long)]
+    pub list: bool,
+}
+
+impl CargoFuzzTestOptions {
+    /// Returns the active domain `ExecutionMode` derived from CLI options.
+    pub fn execution_mode(&self) -> ExecutionMode {
+        if self.list {
+            return ExecutionMode::ListFuzzTests;
+        }
+        ExecutionMode::SmokeTest
+    }
+}
+
+/// Domain execution mode for `cargo-fuzztest`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ExecutionMode {
+    /// List all discovered fuzz tests without running them.
+    ListFuzzTests,
+
+    SmokeTest,
+}
+
+/// Queries Cargo via `cargo -vV` to discover the default host target triple (e.g. `x86_64-unknown-linux-gnu`).
+pub fn get_host_target_triple() -> anyhow::Result<String> {
+    let cargo_bin = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
+    let output = std::process::Command::new(&cargo_bin)
+        .arg("-vV")
+        .output()
+        .context("while attempting to run `cargo -vV` to determine host target triple")?;
+
+    if !output.status.success() {
+        anyhow::bail!("cargo -vV exited with status: {:?}", output.status);
+    }
+
+    let stdout_str = String::from_utf8(output.stdout)
+        .context("while attempting to parse stdout of cargo -vV as UTF-8")?;
+    parse_host_triple(&stdout_str)
+}
+
+/// Parses the output of `cargo -vV` to extract the host target triple.
+///
+/// Cargo's verbose version output contains lines of key-value metadata, including
+/// a `host: <triple>` line representing the host build architecture.
+fn parse_host_triple(stdout: &str) -> anyhow::Result<String> {
+    for line in stdout.lines() {
+        if let Some(triple) = line.strip_prefix("host: ") {
+            return Ok(triple.trim().to_string());
+        }
+    }
+    anyhow::bail!("Failed to find 'host: ' line in cargo -vV output")
+}
+
+/// Parses CLI options for the `cargo-fuzztest` binary.
+///
+/// When invoked via `cargo fuzztest ...`, Cargo passes `"fuzztest"` as `argv[1]`.
+/// We strip `"fuzztest"` if present so `clap` can parse options cleanly.
+pub fn parse_cli_options() -> CargoFuzzTestOptions {
+    let mut args: Vec<std::ffi::OsString> = std::env::args_os().collect();
+    if args.len() > 1 && args[1] == "fuzztest" {
+        args.remove(1);
+    }
+    CargoFuzzTestOptions::parse_from(args)
+}
+
+pub struct FuzztestRunner {
+    pub host_triple: String,
+    pub options: CargoFuzzTestOptions,
+}
+
+impl FuzztestRunner {
+    pub fn new(host_triple: String, options: CargoFuzzTestOptions) -> Self {
+        Self { host_triple, options }
+    }
+
+    /// Constructs `cargo test --no-run --message-format=json --target <host_triple> --lib`.
+    pub fn build_compile_command(&self) -> Command {
+        let mut cmd = Command::new("cargo");
+        cmd.args([
+            "test",
+            "--no-run",
+            "--message-format=json",
+            "--target",
+            &self.host_triple,
+            "--lib",
+        ]);
+
+        // We set the target-specific rustflags (`CARGO_TARGET_<TRIPLE>_RUSTFLAGS`) instead of a global
+        // `RUSTFLAGS` environment variable.
+        //
+        // If a global `RUSTFLAGS` is set, Cargo applies sanitizer coverage flags to both target code
+        // and host tools (such as build scripts and proc-macros). This causes host tool compilation
+        // to fail during linking because host tools are not linked against the fuzzer runtime
+        // (which implements the sanitizer coverage callbacks).
+        //
+        // Using a target-specific key combined with Cargo's explicit `--target` flag ensures
+        // that host tool helper crates are compiled normally, while only our target fuzz tests
+        // receive fuzzer instrumentation.
+        let target_env_key =
+            format!("CARGO_TARGET_{}_RUSTFLAGS", self.host_triple.replace('-', "_").to_uppercase());
+        let rustflags_value = "-Cpasses=sancov-module \
+                                       -Cllvm-args=-sanitizer-coverage-level=4 \
+                                       -Cllvm-args=-sanitizer-coverage-trace-pc-guard \
+                                       -Cllvm-args=-sanitizer-coverage-pc-table \
+                                       -Cllvm-args=-sanitizer-coverage-trace-compares";
+        cmd.env(target_env_key, rustflags_value);
+
+        cmd
+    }
+
+    /// Parses the JSON stream emitted by `cargo test --message-format=json` to extract the compiled test executable.
+    pub fn parse_compiler_messages(json_output: &str) -> Result<PathBuf> {
+        let mut executables = Vec::new();
+
+        for line in json_output.lines() {
+            let trimmed = line.trim();
+            if trimmed.is_empty() {
+                continue;
+            }
+            if let Ok(val) = serde_json::from_str::<serde_json::Value>(trimmed) {
+                let is_compiler_artifact = val["reason"] == "compiler-artifact";
+                let is_test_profile = val["profile"]["test"].as_bool().unwrap_or(false);
+
+                if is_compiler_artifact && is_test_profile {
+                    if let Some(exe) = val["executable"].as_str() {
+                        executables.push(PathBuf::from(exe));
+                    }
+                }
+            }
+        }
+
+        match executables.len() {
+                1 => Ok(executables.remove(0)),
+                0 => anyhow::bail!("no test executable found in Cargo compiler JSON output"),
+                // TODO(the-shank): support multiple test executables, for example from --lib and
+                // --bins targets.
+                _ => anyhow::bail!(
+                    "multiple test executables found in Cargo compiler output: {:?}. Expected exactly one.",
+                    executables
+                ),
+            }
+    }
+
+    /// Construct the direct binary invocation command.
+    pub fn build_run_command(&self, test_binary: &Path) -> Command {
+        let mut cmd = Command::new(test_binary);
+        cmd.arg("__fuzztest_mod__");
+
+        let mode = self.options.execution_mode();
+        match mode {
+            ExecutionMode::ListFuzzTests => {
+                cmd.arg("--list");
+            }
+            ExecutionMode::SmokeTest => {
+                // nothing to be done
+            }
+        }
+
+        cmd
+    }
+
+    /// Runs the tool in two steps:
+    /// 1. build the test binary
+    /// 2. execute the target mode command on the compiled test binary
+    pub fn run(&self) -> Result<i32> {
+        // 1. build the test binary
+        let compile_output = self
+            .build_compile_command()
+            .output()
+            .context("while attempting to execute `cargo test --no-run` compilation command")?;
+
+        if !compile_output.status.success() {
+            // print compiler's stderr to help the user diagnose compilation errors.
+            eprintln!("{}", String::from_utf8_lossy(&compile_output.stderr));
+            anyhow::bail!(
+                "cargo compilation failed with exit code: {:?}",
+                compile_output.status.code()
+            );
+        }
+
+        let json_stdout = String::from_utf8(compile_output.stdout)
+            .context("while attempting to parse compilation JSON output as UTF-8")?;
+        let test_binary = Self::parse_compiler_messages(&json_stdout)?;
+
+        // 2. execute command according to the selected execution mode
+        let mut run_cmd = self.build_run_command(&test_binary);
+        let status =
+            run_cmd.status().context("while attempting to execute compiled test binary")?;
+
+        Ok(status.code().unwrap_or(1))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_host_triple_valid() {
+        let stdout = "rustc 1.75.0 (82e5872f3 2023-12-21)\n\
+                      binary: rustc\n\
+                      commit-hash: 82e5872f3d693c9d7d4c2c56a8fb7f59d47c4058\n\
+                      commit-date: 2023-12-21\n\
+                      host: x86_64-unknown-linux-gnu\n\
+                      release: 1.75.0\n\
+                      LLVM version: 17.0.6\n";
+        let result = parse_host_triple(stdout).expect("valid stdout parsing should succeed");
+        assert_eq!(result, "x86_64-unknown-linux-gnu");
+    }
+
+    #[test]
+    fn test_parse_host_triple_missing_host() {
+        let stdout = "rustc 1.75.0 (82e5872f3 2023-12-21)\n\
+                      binary: rustc\n";
+        let result = parse_host_triple(stdout);
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("Failed to find 'host: '"));
+    }
+
+    #[test]
+    fn test_build_run_command() {
+        let options = CargoFuzzTestOptions { list: true };
+        let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
+        let cmd = runner.build_run_command(Path::new("/tmp/test_bin"));
+        let args: Vec<String> = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect();
+        assert_eq!(args, vec!["__fuzztest_mod__", "--list"]);
+    }
+
+    #[test]
+    fn test_execution_mode_list_fuzz_tests() {
+        let options = CargoFuzzTestOptions { list: true };
+        assert_eq!(options.execution_mode(), ExecutionMode::ListFuzzTests);
+    }
+}
diff --git a/rust/cargo_fuzztest/src/main.rs b/rust/cargo_fuzztest/src/main.rs
new file mode 100644
index 0000000..f3d9767
--- /dev/null
+++ b/rust/cargo_fuzztest/src/main.rs
@@ -0,0 +1,11 @@
+use cargo_fuzztest::FuzztestRunner;
+
+fn main() -> anyhow::Result<()> {
+    let host_triple = cargo_fuzztest::get_host_target_triple()?;
+    let options = cargo_fuzztest::parse_cli_options();
+
+    let runner = FuzztestRunner::new(host_triple, options);
+    let exit_code = runner.run()?;
+
+    std::process::exit(exit_code);
+}
diff --git a/rust/coverage/BUILD b/rust/coverage/BUILD
new file mode 100644
index 0000000..4cf89ce
--- /dev/null
+++ b/rust/coverage/BUILD
@@ -0,0 +1,36 @@
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+
+licenses(["notice"])
+
+rust_library(
+    name = "coverage",
+    srcs = [
+        "src/coverage.rs",
+        "src/lib.rs",
+    ],
+    deps = [
+        "@com_google_fuzztest//centipede:sancov_runtime",
+    ],
+    edition = "2024",
+    visibility = ["@com_google_fuzztest//rust:__subpackages__"],
+)
+
+rust_test(
+    name = "coverage_test",
+    # Avoid interference on coverage collection between threads.
+    args = ["--test-threads=1"],
+    crate = ":coverage",
+    edition = "2024",
+    env = {
+        "CENTIPEDE_RUNNER_FLAGS": ":use_cmp_features:",
+    },
+    # TODO: b/437896409 - Replace with global config once it's available.
+    rustc_flags = [
+        "-Cpasses=sancov-module",
+        "-Cllvm-args=-sanitizer-coverage-level=1",
+        "-Cllvm-args=-sanitizer-coverage-trace-compares",
+    ],
+    deps = [
+        "@crate_index//:googletest",
+    ],
+)
diff --git a/rust/coverage/Cargo.toml b/rust/coverage/Cargo.toml
new file mode 100644
index 0000000..3072a02
--- /dev/null
+++ b/rust/coverage/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "coverage"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
diff --git a/rust/coverage/src/coverage.rs b/rust/coverage/src/coverage.rs
new file mode 100644
index 0000000..258987e
--- /dev/null
+++ b/rust/coverage/src/coverage.rs
@@ -0,0 +1,78 @@
+use std::slice;
+
+#[repr(C)]
+pub struct SanCovRuntimeRawFeatureParts {
+    // Safety invariant: The only way to receive this type is through `SanCovRuntimeGetCoverage()`
+    // which guarantees that `ptr` always points to a valid slice of `size` elements.
+    ptr: *const u64,
+    size: usize,
+}
+
+impl SanCovRuntimeRawFeatureParts {
+    /// # Safety
+    ///
+    /// Must not be held on when its data has to be mutated. For example, do not hold on to it
+    /// when calling `SanCovRuntimeGetCoverage()`.
+    pub unsafe fn as_slice(&self) -> &[u64] {
+        // Safety: self ensures that `ptr` always points to a valid slice of `size` elements.
+        unsafe { slice::from_raw_parts(self.ptr, self.size) }
+    }
+}
+
+unsafe extern "C" {
+    pub safe fn SanCovRuntimeClearCoverage(full_clear: bool);
+
+    pub safe fn SanCovRuntimeGetCoverage(reject_input: bool) -> SanCovRuntimeRawFeatureParts;
+
+    pub safe fn SanCovRuntimePostProcessCoverage(reject_input: bool);
+
+    // Exposed only for testing purposes.
+    pub safe fn __sanitizer_cov_trace_const_cmp1(Arg1: u8, Arg2: u8);
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use googletest::prelude::*;
+
+    #[gtest]
+    fn should_collect_features_from_sancov_callback() {
+        SanCovRuntimeClearCoverage(true);
+
+        __sanitizer_cov_trace_const_cmp1(1, 1);
+
+        let features = SanCovRuntimeGetCoverage(false);
+        expect_false!(features.ptr.is_null());
+        expect_gt!(features.size, 0);
+    }
+
+    #[gtest]
+    fn should_not_collect_features_from_empty_user_code() {
+        SanCovRuntimeClearCoverage(true);
+
+        // An execution where sancov hooks such as
+        // `__sanitizer_cov_trace_const_cmp1` are not called.
+
+        let features = SanCovRuntimeGetCoverage(false);
+        expect_false!(features.ptr.is_null());
+        expect_eq!(features.size, 0);
+    }
+
+    #[gtest]
+    fn should_collect_features_from_user_code() {
+        let x = std::env::args().count();
+
+        SanCovRuntimeClearCoverage(true);
+
+        {
+            // An LLVM trace cmp callback is expected to be instrumented here.
+            if x == 2 {
+                std::hint::black_box(2); // To prevent dead code elimination.
+            }
+        }
+
+        let features = SanCovRuntimeGetCoverage(false);
+        expect_false!(features.ptr.is_null());
+        expect_gt!(features.size, 0);
+    }
+}
diff --git a/rust/coverage/src/lib.rs b/rust/coverage/src/lib.rs
new file mode 100644
index 0000000..16eca13
--- /dev/null
+++ b/rust/coverage/src/lib.rs
@@ -0,0 +1,15 @@
+mod coverage;
+
+pub use coverage::SanCovRuntimeRawFeatureParts;
+
+pub fn prepare_coverage(full_clear: bool) {
+    coverage::SanCovRuntimeClearCoverage(full_clear)
+}
+
+pub fn get_coverage(reject_input: bool) -> SanCovRuntimeRawFeatureParts {
+    coverage::SanCovRuntimeGetCoverage(reject_input)
+}
+
+pub fn post_process_coverage(reject_input: bool) {
+    coverage::SanCovRuntimePostProcessCoverage(reject_input)
+}
diff --git a/rust/e2e_tests/BUILD b/rust/e2e_tests/BUILD
new file mode 100644
index 0000000..9ebb21b
--- /dev/null
+++ b/rust/e2e_tests/BUILD
@@ -0,0 +1,98 @@
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+
+licenses(["notice"])
+
+rust_test(
+    name = "worker_test",
+    srcs = ["worker_test.rs"],
+    data = [
+        "@com_google_fuzztest//rust/e2e_tests/testdata:fuzztest_main",
+    ],
+    edition = "2024",
+    deps = [
+        ":test_utils",
+        "@crate_index//:googletest",
+    ],
+)
+
+rust_test(
+    name = "standalone_mode_test",
+    srcs = ["standalone_mode_test.rs"],
+    data = [
+        "@com_google_fuzztest//centipede:centipede_uninstrumented",
+        "@com_google_fuzztest//rust/e2e_tests/testdata:fuzztest_main",
+        "@com_google_fuzztest//rust/e2e_tests/testdata:standalone_fuzz_tests_bin",
+    ],
+    edition = "2024",
+    deps = [
+        ":test_utils",
+        "@crate_index//:googletest",
+    ],
+)
+
+rust_test(
+    name = "worker_with_centipede_test",
+    srcs = ["worker_with_centipede_test.rs"],
+    data = [
+        "@com_google_fuzztest//centipede:centipede_uninstrumented",
+        "@com_google_fuzztest//rust/e2e_tests/testdata:fuzztest_main",
+    ],
+    edition = "2024",
+    deps = [
+        ":test_utils",
+        "@crate_index//:googletest",
+        "@crate_index//:rand",  # v0_10
+    ],
+)
+
+rust_test(
+    name = "worker_with_centipede_sanitizer_test",
+    srcs = ["worker_with_centipede_sanitizer_test.rs"],
+    data = [
+        "@com_google_fuzztest//centipede:centipede_uninstrumented",
+        "@com_google_fuzztest//rust/e2e_tests/testdata:fuzztest_main",
+    ],
+    edition = "2024",
+    rustc_flags = ["-Zallow-features=cfg_sanitize"],
+    deps = [
+        ":test_utils",
+        "@crate_index//:googletest",
+    ],
+)
+
+rust_library(
+    name = "test_utils",
+    testonly = 1,
+    srcs = ["test_utils.rs"],
+    edition = "2024",
+    deps = [
+        "@crate_index//:googletest",
+    ],
+)
+
+rust_test(
+    name = "gtest_registration_test",
+    srcs = ["gtest_registration_test.rs"],
+    data = [
+        "@com_google_fuzztest//rust/e2e_tests/testdata:fuzz_tests_as_gtests",
+    ],
+    edition = "2024",
+    deps = [
+        ":test_utils",
+        "@crate_index//:googletest",
+    ],
+)
+
+rust_test(
+    name = "replay_test",
+    srcs = ["replay_test.rs"],
+    data = [
+        "@com_google_fuzztest//centipede:centipede_uninstrumented",
+        "@com_google_fuzztest//rust/e2e_tests/testdata:replay_fuzz_tests_bin",
+    ],
+    edition = "2024",
+    deps = [
+        ":test_utils",
+        "@crate_index//:googletest",
+    ],
+)
diff --git a/rust/e2e_tests/gtest_registration_test.rs b/rust/e2e_tests/gtest_registration_test.rs
new file mode 100644
index 0000000..59e0b61
--- /dev/null
+++ b/rust/e2e_tests/gtest_registration_test.rs
@@ -0,0 +1,46 @@
+use googletest::matchers;
+use googletest::prelude::*;
+use std::env;
+use std::path::PathBuf;
+use std::process::Command;
+
+#[gtest]
+fn test_fuzztests_are_registered_as_gtests() {
+    let src_dir_path = PathBuf::from(env::var("TEST_SRCDIR").unwrap());
+    let target_binary_path = src_dir_path
+    .join("_main/rust/e2e_tests/testdata/fuzz_tests_as_gtests");
+
+    // Run the test binary with `--list` to list all registered tests.
+    let output = Command::new(&target_binary_path)
+        .arg("--list")
+        .output()
+        .expect("Failed to execute test binary");
+
+    assert!(output.status.success(), "Test binary failed to execute with --list");
+
+    let stdout = String::from_utf8_lossy(&output.stdout);
+
+    // Verify that the expected fuzz tests are listed as regular tests.
+    expect_that!(stdout, matchers::contains_substring("bool_fuzz_test: test"));
+    expect_that!(stdout, matchers::contains_substring("multi_arg_fuzz_test: test"));
+}
+
+#[gtest]
+fn test_fuzztest_property_function_executes_in_gtest() {
+    let src_dir_path = PathBuf::from(env::var("TEST_SRCDIR").unwrap());
+    let target_binary_path = src_dir_path
+    .join("_main/rust/e2e_tests/testdata/fuzz_tests_as_gtests");
+
+    // Run a fuzz test
+    let output = Command::new(&target_binary_path)
+        .arg("bool_fuzz_test")
+        .arg("--nocapture")
+        .output()
+        .expect("Failed to execute test binary");
+
+    assert!(output.status.success(), "Test binary failed to execute `bool_fuzz_test`");
+
+    let stdout = String::from_utf8_lossy(&output.stdout);
+
+    expect_that!(stdout, matchers::contains_substring("bool_fuzz_test property function ran..."));
+}
diff --git a/rust/e2e_tests/replay_test.rs b/rust/e2e_tests/replay_test.rs
new file mode 100644
index 0000000..2b46a5a
--- /dev/null
+++ b/rust/e2e_tests/replay_test.rs
@@ -0,0 +1,103 @@
+use googletest::matchers;
+use googletest::prelude::*;
+use std::process::Command;
+use test_utils::EnvVars;
+
+#[gtest]
+fn replay_by_id_reproduces_panic(fixture: &EnvVars) {
+    let test_name = "__fuzztest_mod__find_bug_fuzz_test::find_bug_fuzz_test";
+    let normalized_test_name = test_name.replace("::", ".");
+    let crash_id = "my_custom_crash_id";
+
+    let target_binary_path = fixture
+        .target_binary_path
+        .parent()
+        .expect("target_binary_path must have a parent")
+        .join("replay_fuzz_tests_bin");
+
+    // use a relative path for the target-binary
+    let relative_target_binary =
+        target_binary_path.strip_prefix("/").unwrap_or(&target_binary_path);
+
+    let db_dir = fixture.tmp_dir_path.join("corpus_db");
+    let crash_file_dir =
+        db_dir.join(relative_target_binary).join(&normalized_test_name).join("crashing");
+
+    // Write the crashing input to the database
+    std::fs::create_dir_all(&crash_file_dir).expect("Failed to create corpus_db directory");
+    std::fs::write(crash_file_dir.join(crash_id), &[1, 123])
+        .expect("Failed to write crash file to db");
+
+    let process = Command::new(&target_binary_path)
+        .arg(test_name)
+        .arg("--nocapture")
+        .env("FUZZTEST_REPLAY_ID", crash_id)
+        .env("FUZZTEST_CORPUS_DB", &db_dir)
+        .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) // Needed for FFI export
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped())
+        .spawn()
+        .expect("Failed to spawn binary");
+
+    let output = process.wait_with_output().expect("Should terminate");
+    let stderr = String::from_utf8_lossy(&output.stderr);
+
+    expect_that!(output.status.success(), eq(false));
+    expect_that!(stderr, matchers::contains_substring("panicked at"));
+    expect_that!(stderr, matchers::contains_substring("Bug found!"));
+    expect_that!(stderr, matchers::contains_substring("FuzzTest controller reported failure"));
+}
+
+#[gtest]
+fn replay_all_reproduces_all_failures(fixture: &EnvVars) {
+    let test_name = "__fuzztest_mod__find_bug_fuzz_test::find_bug_fuzz_test";
+    let normalized_test_name = test_name.replace("::", ".");
+
+    let target_binary_path = fixture
+        .target_binary_path
+        .parent()
+        .expect("target_binary_path must have a parent")
+        .join("replay_fuzz_tests_bin");
+
+    // use a relative path for the target-binary
+    let relative_target_binary =
+        target_binary_path.strip_prefix("/").unwrap_or(&target_binary_path);
+
+    let db_dir = fixture.tmp_dir_path.join("corpus_db_all");
+    let crash_file_dir =
+        db_dir.join(relative_target_binary).join(&normalized_test_name).join("crashing");
+
+    // Write multiple crashing inputs to the database
+    std::fs::create_dir_all(&crash_file_dir).expect("Failed to create corpus_db directory");
+
+    // Length 1, [123]
+    std::fs::write(crash_file_dir.join("crash_1"), &[1, 123])
+        .expect("Failed to write crash_1 file to db");
+
+    // Length 2, [123, 44]
+    std::fs::write(crash_file_dir.join("crash_2"), &[2, 123, 44])
+        .expect("Failed to write crash_2 file to db");
+
+    // Length 3, [123, 55, 66]
+    std::fs::write(crash_file_dir.join("crash_3"), &[3, 123, 55, 66])
+        .expect("Failed to write crash_3 file to db");
+
+    let process = std::process::Command::new(&target_binary_path)
+        .arg(test_name)
+        .env("FUZZTEST_REPLAY_FINDINGS", "true")
+        .env("FUZZTEST_CORPUS_DB", &db_dir)
+        .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) // Needed for FFI export
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped())
+        .spawn()
+        .expect("Failed to spawn binary");
+
+    let output = process.wait_with_output().expect("Should terminate");
+    let stderr = String::from_utf8_lossy(&output.stderr);
+
+    expect_that!(output.status.success(), eq(true));
+
+    // Check that "Bug found!" appears exactly 3 times
+    let bug_count = stderr.matches("Bug found!").count();
+    expect_that!(bug_count, eq(3));
+}
diff --git a/rust/e2e_tests/run_with_centipede.sh b/rust/e2e_tests/run_with_centipede.sh
new file mode 100755
index 0000000..b52785b
--- /dev/null
+++ b/rust/e2e_tests/run_with_centipede.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+#
+# Use this script for when one wants to manually fuzz a test target with
+# Centipede: internal only. Workdir for Centipede is the dir from where this
+# script is executed. User is in charge of removing the corpus to start anew.
+#
+# This script will build Centipede and the fuzz target every time by default.
+# If you wish to skip building Centipede (speeds up execution), set
+# --build_centipede=false in the command line for subsequent runs.
+#
+# First argument should be the fuzz target.
+# Example usage:
+#
+# ./third_party/googlefuzztest/rust/e2e_tests/run_with_centipede.sh \
+#   //third_party/googlefuzztest/rust/e2e_tests/testdata:fuzztest_main \
+#   --fuzz=find_rarer_bug_fuzz_test --fuzz_for=10s
+
+source gbash.sh || exit
+
+DEFINE_string artifact_dir "/tmp" "Build artifacts will be stored here."
+DEFINE_string fuzz_for "" "How much time to run the fuzz test for"
+DEFINE_string fuzz --required "" "The name of the fuzz test to run"
+DEFINE_bool build_centipede true "If false, will not build Centipede."
+
+gbash::init_google "$@"
+set -- "${GBASH_ARGV[@]}"
+
+readonly FUZZ_TARGET="${1}"
+readonly CENTIPEDE_TARGET="//third_party/googlefuzztest/centipede/google:centipede_uninstrumented"
+
+readonly SYMLINK="${FLAGS_artifact_dir%/}/blaze-"
+readonly CENTIPEDE_SYMLINK="${SYMLINK}centipede-"
+readonly TARGET_SYMLINK="${SYMLINK}target-"
+readonly CENTIPEDE_PATH="${CENTIPEDE_SYMLINK}bin/third_party/googlefuzztest/centipede/google/centipede_uninstrumented"
+
+get_fuzz_target_binary_path() {
+  # Remove leading double slashes.
+  local FUZZ_TARGET_PATH="${FUZZ_TARGET##//}"
+  # Replace last colon with a slash.
+  echo "${TARGET_SYMLINK}bin/${FUZZ_TARGET_PATH%:*}/${FUZZ_TARGET_PATH##*:}"
+}
+
+build_all() {
+
+  if (( FLAGS_build_centipede )); then
+    # BUILD Centipede.
+    bazel build -c opt --symlink_prefix="${CENTIPEDE_SYMLINK}" \
+      "${CENTIPEDE_TARGET}"
+  fi
+
+  # TODO: b/437896409 - Use a dedicated config for Rust FuzzTest.
+  # BUILD the fuzz target.
+  bazel build --symlink_prefix="${TARGET_SYMLINK}" "${FUZZ_TARGET}" \
+    --config=rust-cov --config=asan
+
+}
+
+main() {
+
+  build_all
+
+  declare -a centipede_flags=(
+    --binary="$(get_fuzz_target_binary_path)" \
+    --persistent_mode=0 --fork_server=0 --populate_binary_info=0 \
+    --test_name="${FLAGS_fuzz}" \
+  )
+
+  if [[ -n "${FLAGS_fuzz_for}" ]]; then
+    centipede_flags+=(--stop_after="${FLAGS_fuzz_for}")
+  fi
+
+ "${CENTIPEDE_PATH}" "${centipede_flags[@]}"
+}
+
+main
diff --git a/rust/e2e_tests/standalone_mode_test.rs b/rust/e2e_tests/standalone_mode_test.rs
new file mode 100644
index 0000000..adb5253
--- /dev/null
+++ b/rust/e2e_tests/standalone_mode_test.rs
@@ -0,0 +1,297 @@
+use googletest::matchers;
+use googletest::prelude::*;
+use std::process::Command;
+use test_utils::EnvVars;
+
+#[gtest]
+fn standalone_mode_invokes_centipede(fixture: &EnvVars) {
+    let target_binary_path =
+        fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin");
+    let test_name = "__fuzztest_mod__standalone_validation_test::standalone_validation_test";
+
+    let process = Command::new(&target_binary_path)
+        .arg(test_name)
+        .env("FUZZTEST_FUZZ_FOR", "3s")
+        .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true")
+        .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path)
+        .env("RUST_TEST_NOCAPTURE", "1")
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped())
+        .spawn()
+        .expect("Failed to spawn binary");
+
+    let output = process.wait_with_output().expect("Should terminate");
+    let stderr = String::from_utf8_lossy(&output.stderr);
+
+    // Assert that Centipede actually ran the test binary, and forwarded the worker log.
+    // We check for "LOG: STANDALONE_VALIDATION_WORKER_EXECUTED". The prefix "LOG: " indicates that
+    // this output is from centipede forwarding the output of the worker (due to --print_runner_log),
+    // whereas the rest is from the fuzztest itself.
+    expect_that!(
+        stderr,
+        matchers::contains_substring("LOG: STANDALONE_VALIDATION_WORKER_EXECUTED")
+    );
+}
+
+#[gtest]
+fn standalone_mode_invokes_the_correct_fuzztest(fixture: &EnvVars) {
+    let target_binary_path =
+        fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin");
+    let test_name = "__fuzztest_mod__standalone_validation_test::standalone_validation_test";
+
+    let process = Command::new(&target_binary_path)
+        .arg(test_name)
+        .env("FUZZTEST_FUZZ_FOR", "3s")
+        .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true")
+        .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path)
+        .env("RUST_TEST_NOCAPTURE", "1")
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped())
+        .spawn()
+        .expect("Failed to spawn binary");
+
+    let output = process.wait_with_output().expect("Should terminate");
+    let stderr = String::from_utf8_lossy(&output.stderr);
+
+    // Check that the first test is executed
+    expect_that!(
+        stderr,
+        matchers::contains_substring("LOG: STANDALONE_VALIDATION_WORKER_EXECUTED")
+    );
+
+    // Check that the second test does not get executed
+    expect_that!(stderr, not(matchers::contains_substring("SECOND_VALIDATION_TEST_EXECUTED")));
+}
+
+#[gtest]
+fn standalone_mode_handles_worker_crash(fixture: &EnvVars) {
+    let test_name = "__fuzztest_mod__find_bug_fuzz_test::find_bug_fuzz_test";
+
+    let process = Command::new(&fixture.target_binary_path)
+        .arg(test_name)
+        .env("FUZZTEST_FUZZ_FOR", "5s")
+        .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true")
+        .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path)
+        .env("RUST_TEST_NOCAPTURE", "1")
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped())
+        .spawn()
+        .expect("Failed to spawn binary");
+
+    let output = process.wait_with_output().expect("Should terminate");
+    let stderr = String::from_utf8_lossy(&output.stderr);
+
+    expect_that!(stderr, matchers::contains_substring("Property function ran but crashed."));
+    expect_that!(stderr, matchers::contains_regex("Signature[ \t]*: Unwinding panic"));
+    // Centipede prefixes logs from the crashing worker with "CRASH LOG: ".
+    expect_that!(stderr, matchers::contains_substring("CRASH LOG: Bug found!"));
+}
+
+#[gtest]
+fn standalone_mode_spawns_parallel_jobs(fixture: &EnvVars) {
+    let target_binary_path =
+        fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin");
+    let test_name = "__fuzztest_mod__jobs_test::jobs_test";
+
+    let process = Command::new(&target_binary_path)
+        .arg(test_name)
+        .env("FUZZTEST_FUZZ_FOR", "5s")
+        .env("FUZZTEST_JOBS", "4")
+        .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true")
+        .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path)
+        .env("RUST_TEST_NOCAPTURE", "1")
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped())
+        .spawn()
+        .expect("Failed to spawn binary");
+
+    let output = process.wait_with_output().expect("Should terminate");
+    let stderr = String::from_utf8_lossy(&output.stderr);
+
+    // Extract all PIDs from logs.
+    // Expected format: "LOG: JOBS_TEST_PID: <pid>"
+    use std::collections::HashSet;
+    let mut pids = HashSet::new();
+    for line in stderr.lines() {
+        if let Some(pos) = line.find("LOG: JOBS_TEST_PID: ") {
+            let pid_str = &line[pos + "LOG: JOBS_TEST_PID: ".len()..];
+            if let Ok(pid) = pid_str.parse::<u32>() {
+                pids.insert(pid);
+            }
+        }
+    }
+
+    // We expect 4 different PIDs
+    expect_that!(pids.len(), eq(4));
+}
+
+#[gtest]
+fn standalone_mode_replay_corpus_per_test_budget(fixture: &EnvVars) {
+    let target_binary_path =
+        fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin");
+
+    let corpus_db = fixture.tmp_dir_path.join("corpus_db_per_test");
+    let work_dir_root = fixture.tmp_dir_path.join("WD_root_per_test");
+
+    // Centipede appends the binary identifier (relative path) to the corpus database path.
+    let identifier = target_binary_path.to_str().unwrap();
+    let identifier_relative = identifier.strip_prefix('/').unwrap_or(identifier);
+
+    // Create corpus dirs for tests
+    let test1_dir = corpus_db
+        .join(identifier_relative)
+        .join("__fuzztest_mod__standalone_validation_test.standalone_validation_test")
+        .join("coverage");
+    let test2_dir = corpus_db
+        .join(identifier_relative)
+        .join("__fuzztest_mod__second_validation_test.second_validation_test")
+        .join("coverage");
+
+    std::fs::create_dir_all(&test1_dir).unwrap();
+    std::fs::create_dir_all(&test2_dir).unwrap();
+
+    // Write some corpus inputs.
+    // Assuming Arbitrary<i32> consumes 4 bytes.
+    use std::fs::File;
+    use std::io::Write;
+
+    let mut file1 = File::create(test1_dir.join("input1")).unwrap();
+    file1.write_all(&[1, 0, 0, 0]).unwrap();
+
+    let mut file2 = File::create(test1_dir.join("input2")).unwrap();
+    file2.write_all(&[2, 0, 0, 0]).unwrap();
+
+    let mut file3 = File::create(test2_dir.join("input3")).unwrap();
+    file3.write_all(&[3, 0, 0, 0]).unwrap();
+
+    let process_per_test = Command::new(&target_binary_path)
+        .arg("__fuzztest_mod__")
+        .env("FUZZTEST_REPLAY_CORPUS_FOR", "3s")
+        .env("FUZZTEST_TIME_BUDGET_TYPE", "per-test")
+        .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true")
+        .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path)
+        .env("FUZZTEST_CORPUS_DB", corpus_db.to_str().unwrap())
+        .env("FUZZTEST_WORKDIR_ROOT", work_dir_root.to_str().unwrap())
+        .env("RUST_TEST_NOCAPTURE", "1")
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped())
+        .spawn()
+        .expect("Failed to spawn binary");
+
+    let output_per_test = process_per_test.wait_with_output().expect("Should terminate");
+    let stderr_per_test = String::from_utf8_lossy(&output_per_test.stderr);
+
+    // Verify logs for per-test
+    expect_that!(
+        stderr_per_test,
+        matchers::contains_substring("Replaying __fuzztest_mod__standalone_validation_test.standalone_validation_test for 3s")
+    );
+    expect_that!(
+        stderr_per_test,
+        matchers::contains_substring(
+            "Replaying __fuzztest_mod__second_validation_test.second_validation_test for 3s"
+        )
+    );
+
+    // Count executions for per-test
+    let mut val1_count = 0;
+    let mut val2_count = 0;
+    for line in stderr_per_test.lines() {
+        if line.contains("STANDALONE_VALIDATION_INPUT:") {
+            val1_count += 1;
+        }
+        if line.contains("SECOND_VALIDATION_INPUT:") {
+            val2_count += 1;
+        }
+    }
+    // we had 2 inputs for first test
+    expect_that!(val1_count, eq(2));
+    // and 1 input for second test
+    expect_that!(val2_count, eq(1));
+}
+
+#[gtest]
+fn standalone_mode_replay_corpus_total_budget(fixture: &EnvVars) {
+    let target_binary_path =
+        fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin");
+
+    let corpus_db = fixture.tmp_dir_path.join("corpus_db_total");
+    let work_dir_root = fixture.tmp_dir_path.join("WD_root_total");
+
+    // Centipede appends the binary identifier (relative path) to the corpus database path.
+    let identifier = target_binary_path.to_str().unwrap();
+    let identifier_relative = identifier.strip_prefix('/').unwrap_or(identifier);
+
+    // Create corpus dirs for tests
+    let test1_dir = corpus_db
+        .join(identifier_relative)
+        .join("__fuzztest_mod__standalone_validation_test.standalone_validation_test")
+        .join("coverage");
+    let test2_dir = corpus_db
+        .join(identifier_relative)
+        .join("__fuzztest_mod__second_validation_test.second_validation_test")
+        .join("coverage");
+
+    std::fs::create_dir_all(&test1_dir).unwrap();
+    std::fs::create_dir_all(&test2_dir).unwrap();
+
+    // Write some corpus inputs.
+    // Assuming Arbitrary<i32> consumes 4 bytes.
+    use std::fs::File;
+    use std::io::Write;
+
+    let mut file1 = File::create(test1_dir.join("input1")).unwrap();
+    file1.write_all(&[1, 0, 0, 0]).unwrap();
+
+    let mut file2 = File::create(test1_dir.join("input2")).unwrap();
+    file2.write_all(&[2, 0, 0, 0]).unwrap();
+
+    let mut file3 = File::create(test2_dir.join("input3")).unwrap();
+    file3.write_all(&[3, 0, 0, 0]).unwrap();
+
+    // We have 3 fuzz tests in the binary. Total 3s / 3 = 1s per test.
+    let process_total = Command::new(&target_binary_path)
+        .arg("__fuzztest_mod__")
+        .env("FUZZTEST_REPLAY_CORPUS_FOR", "3s")
+        .env("FUZZTEST_TIME_BUDGET_TYPE", "total")
+        .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true")
+        .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path)
+        .env("FUZZTEST_CORPUS_DB", corpus_db.to_str().unwrap())
+        .env("FUZZTEST_WORKDIR_ROOT", work_dir_root.to_str().unwrap())
+        .env("RUST_TEST_NOCAPTURE", "1")
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::piped())
+        .spawn()
+        .expect("Failed to spawn binary");
+
+    let output_total = process_total.wait_with_output().expect("Should terminate");
+    let stderr_total = String::from_utf8_lossy(&output_total.stderr);
+
+    // Verify logs for total -> should be 1s
+    expect_that!(
+        stderr_total,
+        matchers::contains_substring("Replaying __fuzztest_mod__standalone_validation_test.standalone_validation_test for 1s")
+    );
+    expect_that!(
+        stderr_total,
+        matchers::contains_substring(
+            "Replaying __fuzztest_mod__second_validation_test.second_validation_test for 1s"
+        )
+    );
+
+    // Counts should still match because it finishes early anyway
+    let mut val1_count_total = 0;
+    let mut val2_count_total = 0;
+    for line in stderr_total.lines() {
+        if line.contains("STANDALONE_VALIDATION_INPUT:") {
+            val1_count_total += 1;
+        }
+        if line.contains("SECOND_VALIDATION_INPUT:") {
+            val2_count_total += 1;
+        }
+    }
+    // we had 2 inputs for first test
+    expect_that!(val1_count_total, eq(2));
+    // and 1 input for second test
+    expect_that!(val2_count_total, eq(1));
+}
diff --git a/rust/e2e_tests/test_utils.rs b/rust/e2e_tests/test_utils.rs
new file mode 100644
index 0000000..60044e7
--- /dev/null
+++ b/rust/e2e_tests/test_utils.rs
@@ -0,0 +1,66 @@
+use googletest::fixtures::Fixture;
+use std::env;
+use std::path::PathBuf;
+use std::process::Command;
+
+pub struct EnvVars {
+    pub tmp_dir_path: PathBuf,
+    pub target_binary_path: PathBuf,
+    pub centipede_path: PathBuf,
+}
+
+impl Fixture for EnvVars {
+    fn set_up() -> googletest::Result<Self> {
+        const TARGET_BINARY: &str =
+        "_main/rust/e2e_tests/testdata/fuzztest_main";
+
+        const CENTIPEDE: &str =
+        "_main/centipede/centipede_uninstrumented";
+        let src_dir_path = PathBuf::from(env::var("TEST_SRCDIR").unwrap());
+        let tmp_dir_path = PathBuf::from(env::var("TEST_TMPDIR").unwrap());
+        let target_binary_path = src_dir_path.join(PathBuf::from(TARGET_BINARY));
+        let centipede_path = src_dir_path.join(PathBuf::from(CENTIPEDE));
+
+        Ok(EnvVars { tmp_dir_path, target_binary_path, centipede_path })
+    }
+    fn tear_down(self) -> googletest::Result<()> {
+        Ok(())
+    }
+}
+
+/// Returns stderr of a Centipede process with `args` that is expected to terminate through some
+/// internal flag. e.g. `--exit_on_crash` or `--stop_after`.
+///
+/// Each arg of `args` should be a string that Centipede recognizes containing a flag with a
+/// possible value, e.g. `--test_name=my_test_name` or `--exit_on_crash`. In addition to `args`,
+/// the function will also pass `--populate_binary_info=0`, `--fork_server=0`,
+/// `--persistent_mode=0`, and `--env_diff_for_binaries`.
+pub fn run_centipede_with_args_expect_termination(fixture: &EnvVars, args: &[&str]) -> String {
+    // Disable interference from Bazel environment variables.
+    let env_diff = [
+        "-TEST_DIAGNOSTICS_OUTPUT_DIR",
+        "-TEST_INFRASTRUCTURE_FAILURE_FILE",
+        "-TEST_LOGSPLITTER_OUTPUT_FILE",
+        "-TEST_PREMATURE_EXIT_FILE",
+        "-TEST_RANDOM_SEED",
+        "-TEST_RUN_NUMBER",
+        "-TEST_SHARD_INDEX",
+        "-TEST_SHARD_STATUS_FILE",
+        "-TEST_TOTAL_SHARDS",
+        "-TEST_UNDECLARED_OUTPUTS_ANNOTATIONS_DIR",
+        "-TEST_UNDECLARED_OUTPUTS_DIR",
+        "-TEST_WARNINGS_OUTPUT_FILE",
+        "-GTEST_OUTPUT",
+        "-XML_OUTPUT_FILE",
+    ];
+    let process = Command::new(&fixture.centipede_path)
+        .arg("--populate_binary_info=0")
+        .arg("--fork_server=0")
+        .arg("--persistent_mode=0")
+        .arg(format!("--env_diff_for_binaries={}", env_diff.join(",")))
+        .args(args)
+        .output()
+        .expect("Centipede should have executed");
+
+    String::from_utf8_lossy(&process.stderr).to_string()
+}
diff --git a/rust/e2e_tests/testdata/BUILD b/rust/e2e_tests/testdata/BUILD
new file mode 100644
index 0000000..65111b2
--- /dev/null
+++ b/rust/e2e_tests/testdata/BUILD
@@ -0,0 +1,106 @@
+load("@rules_rust//rust:defs.bzl", "rust_test")
+
+package(default_visibility = ["@com_google_fuzztest//rust/e2e_tests:__subpackages__"])
+
+licenses(["notice"])
+
+rust_test(
+    name = "fuzztest_main",
+    testonly = 1,
+    srcs = [
+        "fuzz_tests.rs",
+    ],
+    edition = "2024",
+    # TODO: b/437896409 - Replace with global config once it's available.
+    rustc_flags = [
+        "-Ccodegen-units=1",
+        "-Clink-dead-code",
+        "-Cpasses=sancov-module",
+        "-Cllvm-args=-sanitizer-coverage-level=1",
+        "-Cllvm-args=-sanitizer-coverage-trace-compares",
+    ],
+    tags = [
+        "manual",
+        "notap",
+    ],
+    deps = [
+        "@crate_index//:googletest",
+        "@crate_index//:anyhow",  # v1
+        "@crate_index//:inventory",  # v0_3
+        "@crate_index//:rand",  # v0_10
+        "@com_google_fuzztest//rust:fuzztest",
+    ],
+)
+
+rust_test(
+    name = "standalone_fuzz_tests_bin",
+    testonly = 1,
+    srcs = [
+        "standalone_fuzz_tests.rs",
+    ],
+    edition = "2024",
+    rustc_flags = [
+        "-Ccodegen-units=1",
+        "-Clink-dead-code",
+        "-Cpasses=sancov-module",
+        "-Cllvm-args=-sanitizer-coverage-level=1",
+        "-Cllvm-args=-sanitizer-coverage-trace-compares",
+    ],
+    tags = [
+        "manual",
+        "notap",
+    ],
+    deps = [
+        "@crate_index//:googletest",
+        "@crate_index//:anyhow",  # v1
+        "@crate_index//:inventory",  # v0_3
+        "@crate_index//:rand",  # v0_10
+        "@com_google_fuzztest//rust:fuzztest",
+    ],
+)
+
+rust_test(
+    name = "replay_fuzz_tests_bin",
+    testonly = 1,
+    srcs = [
+        "replay_fuzz_tests.rs",
+    ],
+    edition = "2024",
+    rustc_flags = [
+        "-Ccodegen-units=1",
+        "-Clink-dead-code",
+        "-Cpasses=sancov-module",
+        "-Cllvm-args=-sanitizer-coverage-level=1",
+        "-Cllvm-args=-sanitizer-coverage-trace-compares",
+    ],
+    tags = [
+        "manual",
+        "notap",
+    ],
+    deps = [
+        "@crate_index//:googletest",
+        "@crate_index//:anyhow",  # v1
+        "@crate_index//:inventory",  # v0_3
+        "@crate_index//:rand",  # v0_10
+        "@com_google_fuzztest//rust:fuzztest",
+    ],
+)
+
+rust_test(
+    name = "fuzz_tests_as_gtests",
+    srcs = ["fuzz_tests_as_gtests.rs"],
+    edition = "2024",
+    rustc_flags = ["-Zallow-features=cfg_sanitize"],
+    tags = [
+        "manual",
+        "notap",
+    ],
+    deps = [
+        "@crate_index//:googletest",
+        "@crate_index//:anyhow",  # v1
+        "@crate_index//:inventory",  # v0_3
+        "@crate_index//:postcard",  # v1
+        "@crate_index//:rand",  # v0_10
+        "@com_google_fuzztest//rust:fuzztest",
+    ],
+)
diff --git a/rust/e2e_tests/testdata/fuzz_tests.rs b/rust/e2e_tests/testdata/fuzz_tests.rs
new file mode 100644
index 0000000..1a5e874
--- /dev/null
+++ b/rust/e2e_tests/testdata/fuzz_tests.rs
@@ -0,0 +1,171 @@
+use fuzztest::domains::arbitrary::Arbitrary;
+use fuzztest::domains::range::InRange;
+use fuzztest::domains::Domain;
+use fuzztest::fuzztest;
+use rand::RngExt;
+use std::mem::MaybeUninit;
+
+struct ByteVectorDomain {}
+
+impl ByteVectorDomain {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+// Test-only domain.
+impl Domain for ByteVectorDomain {
+    type UserValue<'user> = Vec<u8>;
+    type CorpusValue = Vec<u8>;
+
+    fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
+        let mut val = vec![0u8; rng.random_range(0..100)];
+        rng.fill(&mut val[..]);
+        Ok(val)
+    }
+
+    fn mutate(
+        &self,
+        val: &mut Self::CorpusValue,
+        rng: &mut dyn rand::Rng,
+        only_shrink: bool,
+    ) -> anyhow::Result<()> {
+        if only_shrink {
+            if rng.random::<f32>() < 0.05 && !val.is_empty() {
+                val.remove(rng.random_range(..val.len()));
+            }
+
+            for elem in val {
+                if *elem != 0 {
+                    *elem -= 1;
+                }
+            }
+
+            return Ok(());
+        }
+        if rng.random::<f32>() < 0.05 {
+            val.insert(rng.random_range(0..=val.len()), rng.random());
+        }
+
+        if rng.random::<f32>() < 0.05 && !val.is_empty() {
+            val.remove(rng.random_range(..val.len()));
+        }
+
+        let start = rng.random_range(0..=val.len());
+        let end = rng.random_range(start..=val.len());
+        rng.fill(&mut val[start..end]);
+
+        Ok(())
+    }
+
+    fn get_user_value<'a>(
+        &self,
+        val: &'a Self::CorpusValue,
+    ) -> anyhow::Result<Self::UserValue<'a>> {
+        Ok(val.clone())
+    }
+}
+
+#[fuzztest(_a = Arbitrary::<bool>::default())]
+fn bool_fuzz_test(_a: bool) {}
+
+#[fuzztest(_a = Arbitrary::<i32>::default())]
+fn i32_fuzz_test(_a: i32) {}
+
+#[fuzztest(
+    _a = Arbitrary::<i32>::default(),
+    _b = Arbitrary::<f32>::default(),
+    _c = Arbitrary::<bool>::default(),
+    _d = ByteVectorDomain::new()
+)]
+fn multi_arg_fuzz_test(_a: i32, _b: f32, _c: bool, _d: Vec<u8>) {}
+
+// Designed to fail in a "rare" instance.
+#[fuzztest(a = ByteVectorDomain::new())]
+fn find_bug_fuzz_test(a: Vec<u8>) {
+    if !a.is_empty() && a[0] == 123 {
+        panic!("Bug found!");
+    }
+}
+
+// Designed to fail in an even rarer instance to prove that coverage is working.
+#[fuzztest(a = ByteVectorDomain::new())]
+fn find_rarer_bug_fuzz_test(a: Vec<u8>) {
+    if a.len() == 5 && a[0] == b'P' && a[1] == b'a' && a[2] == b'N' && a[3] == b'i' && a[4] == b'C'
+    {
+        panic!("Bug found!");
+    }
+}
+
+#[fuzztest(a = ByteVectorDomain::new(), b = ByteVectorDomain::new())]
+fn find_rarer_bug_2_args_fuzz_test(a: Vec<u8>, b: Vec<u8>) {
+    if !a.is_empty() && a[0] >= 128 && !b.is_empty() && b[0] == 42 {
+        panic!("Bug found!");
+    }
+}
+
+#[fuzztest(a = ByteVectorDomain::new(), b = Arbitrary::<f32>::default(), c = InRange::<i32>::new(0, 100))]
+fn find_rarer_bug_3_args_fuzz_test(a: Vec<u8>, b: f32, c: i32) {
+    if !a.is_empty() && a[0] >= 128 && b < 0.0 && c >= 95 {
+        panic!("Bug found!");
+    }
+}
+#[fuzztest(a = ByteVectorDomain::new())]
+fn use_after_free_asan_death_test(mut a: Vec<u8>) {
+    if !a.is_empty() && a[0] == 123 {
+        let ptr = a.as_mut_ptr();
+        drop(a);
+        // Intentionally trigger a use-after-free bug for ASAN to detect.
+        unsafe {
+            *(std::hint::black_box(ptr)) = 1;
+        }
+    }
+}
+
+#[fuzztest(a = ByteVectorDomain::new())]
+fn msan_death_test(a: Vec<u8>) {
+    if !a.is_empty() && a[0] == 123 {
+        // Intentionally trigger an uninitialized memory bug for MSAN to detect.
+        unsafe {
+            let x: MaybeUninit<i32> = MaybeUninit::uninit();
+            let y = std::hint::black_box(x).assume_init();
+            println!("y: {}", y);
+        }
+    }
+}
+
+struct FallibleDomain {}
+
+impl FallibleDomain {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl Domain for FallibleDomain {
+    type UserValue<'user> = u32;
+    type CorpusValue = u32;
+
+    fn init(&self, _rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
+        Ok(0)
+    }
+
+    fn mutate(
+        &self,
+        _val: &mut Self::CorpusValue,
+        _rng: &mut dyn rand::Rng,
+        _only_shrink: bool,
+    ) -> anyhow::Result<()> {
+        anyhow::bail!("Intentional mutate failure")
+    }
+
+    fn get_user_value<'a>(
+        &self,
+        val: &'a Self::CorpusValue,
+    ) -> anyhow::Result<Self::UserValue<'a>> {
+        Ok(*val)
+    }
+}
+
+#[fuzztest(a = FallibleDomain::new())]
+fn fallible_fuzz_test(_a: u32) {}
diff --git a/rust/e2e_tests/testdata/fuzz_tests_as_gtests.rs b/rust/e2e_tests/testdata/fuzz_tests_as_gtests.rs
new file mode 100644
index 0000000..4e2ed7b
--- /dev/null
+++ b/rust/e2e_tests/testdata/fuzz_tests_as_gtests.rs
@@ -0,0 +1,18 @@
+use fuzztest::domains::arbitrary::Arbitrary;
+use fuzztest::fuzztest;
+use std::sync::Once;
+
+#[fuzztest(_a = Arbitrary::<bool>::default())]
+fn bool_fuzz_test(_a: bool) {
+    static ONCE: Once = Once::new();
+    ONCE.call_once(|| {
+        println!("bool_fuzz_test property function ran...");
+    });
+}
+
+#[fuzztest(
+    _a = Arbitrary::<i32>::default(),
+    _b = Arbitrary::<f32>::default(),
+    _c = Arbitrary::<bool>::default(),
+)]
+fn multi_arg_fuzz_test(_a: i32, _b: f32, _c: bool) {}
diff --git a/rust/e2e_tests/testdata/replay_fuzz_tests.rs b/rust/e2e_tests/testdata/replay_fuzz_tests.rs
new file mode 100644
index 0000000..efa610d
--- /dev/null
+++ b/rust/e2e_tests/testdata/replay_fuzz_tests.rs
@@ -0,0 +1,83 @@
+use fuzztest::domains::arbitrary::Arbitrary;
+use fuzztest::domains::Domain;
+use fuzztest::fuzztest;
+use rand::RngExt;
+
+struct ByteVectorDomain {}
+
+impl ByteVectorDomain {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+// Test-only domain.
+impl Domain for ByteVectorDomain {
+    type UserValue<'user> = Vec<u8>;
+    type CorpusValue = Vec<u8>;
+
+    fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
+        let mut val = vec![0u8; rng.random_range(0..100)];
+        rng.fill(&mut val[..]);
+        Ok(val)
+    }
+
+    fn mutate(
+        &self,
+        val: &mut Self::CorpusValue,
+        rng: &mut dyn rand::Rng,
+        only_shrink: bool,
+    ) -> anyhow::Result<()> {
+        if only_shrink {
+            if rng.random::<f32>() < 0.05 && !val.is_empty() {
+                val.remove(rng.random_range(..val.len()));
+            }
+
+            for elem in val {
+                if *elem != 0 {
+                    *elem -= 1;
+                }
+            }
+
+            return Ok(());
+        }
+        if rng.random::<f32>() < 0.05 {
+            val.insert(rng.random_range(0..=val.len()), rng.random());
+        }
+
+        if rng.random::<f32>() < 0.05 && !val.is_empty() {
+            val.remove(rng.random_range(..val.len()));
+        }
+
+        let start = rng.random_range(0..=val.len());
+        let end = rng.random_range(start..=val.len());
+        rng.fill(&mut val[start..end]);
+
+        Ok(())
+    }
+
+    fn get_user_value<'a>(
+        &self,
+        val: &'a Self::CorpusValue,
+    ) -> anyhow::Result<Self::UserValue<'a>> {
+        Ok(val.clone())
+    }
+}
+
+#[fuzztest(a = ByteVectorDomain::new())]
+fn find_bug_fuzz_test(a: Vec<u8>) {
+    println!("PROPERTY_FUNCTION_EXECUTED");
+    if !a.is_empty() && a[0] == 123 {
+        panic!("Bug found!");
+    }
+}
+
+#[fuzztest(_a = Arbitrary::<i32>::default())]
+fn untargeted_test_1(_a: i32) {
+    println!("UNTARGETED_TEST_1_EXECUTED");
+}
+
+#[fuzztest(_a = Arbitrary::<i32>::default())]
+fn untargeted_test_2(_a: i32) {
+    println!("UNTARGETED_TEST_2_EXECUTED");
+}
diff --git a/rust/e2e_tests/testdata/standalone_fuzz_tests.rs b/rust/e2e_tests/testdata/standalone_fuzz_tests.rs
new file mode 100644
index 0000000..e8d7d53
--- /dev/null
+++ b/rust/e2e_tests/testdata/standalone_fuzz_tests.rs
@@ -0,0 +1,21 @@
+use fuzztest::domains::arbitrary::Arbitrary;
+use fuzztest::fuzztest;
+
+#[fuzztest(_a = Arbitrary::<i32>::default())]
+fn standalone_validation_test(_a: i32) {
+    // Minimal test function to validate standalone mode.
+    println!("STANDALONE_VALIDATION_WORKER_EXECUTED");
+    println!("STANDALONE_VALIDATION_INPUT: {}", _a);
+}
+
+#[fuzztest(_a = Arbitrary::<i32>::default())]
+fn second_validation_test(_a: i32) {
+    println!("SECOND_VALIDATION_TEST_EXECUTED");
+    println!("SECOND_VALIDATION_INPUT: {}", _a);
+}
+
+#[fuzztest(_a = Arbitrary::<i32>::default())]
+fn jobs_test(_a: i32) {
+    println!("JOBS_TEST_PID: {}", std::process::id());
+    std::thread::sleep(std::time::Duration::from_millis(250));
+}
diff --git a/rust/e2e_tests/worker_test.rs b/rust/e2e_tests/worker_test.rs
new file mode 100644
index 0000000..7676af5
--- /dev/null
+++ b/rust/e2e_tests/worker_test.rs
@@ -0,0 +1,26 @@
+use googletest::matchers;
+use googletest::prelude::*;
+use std::fs;
+use std::process::Command;
+use test_utils::EnvVars;
+
+#[gtest]
+fn get_binary_id_test(fixture: &EnvVars) {
+    let binary_id_file = fixture.tmp_dir_path.join("binary_id.txt");
+
+    let mut process = Command::new(&fixture.target_binary_path)
+        .env(
+            "CENTIPEDE_RUNNER_FLAGS",
+            format!(":dump_binary_id:binary_id_output={}:", binary_id_file.display()),
+        )
+        .spawn()
+        .expect("Failed to spawn binary");
+
+    process.wait().expect("Should terminate");
+    expect_that!(
+        fs::read_to_string(binary_id_file).unwrap(),
+        matchers::contains_substring(
+            (fixture.target_binary_path.file_name()).unwrap().to_string_lossy()
+        )
+    );
+}
diff --git a/rust/e2e_tests/worker_with_centipede_sanitizer_test.rs b/rust/e2e_tests/worker_with_centipede_sanitizer_test.rs
new file mode 100644
index 0000000..f4c1221
--- /dev/null
+++ b/rust/e2e_tests/worker_with_centipede_sanitizer_test.rs
@@ -0,0 +1,49 @@
+#![feature(cfg_sanitize)]
+
+#[cfg(any(sanitize = "address", sanitize = "memory"))]
+use googletest::matchers;
+#[cfg(any(sanitize = "address", sanitize = "memory"))]
+use googletest::prelude::*;
+#[cfg(any(sanitize = "address", sanitize = "memory"))]
+use std::fs;
+#[cfg(any(sanitize = "address", sanitize = "memory"))]
+use test_utils::EnvVars;
+
+#[gtest]
+#[cfg(sanitize = "address")]
+fn ensure_use_after_free_signature_with_asan(fixture: &EnvVars) {
+    let work_dir = fixture.tmp_dir_path.join("WD"); // For permissions
+    fs::create_dir_all(&work_dir).expect("Failed to create working directory");
+
+    let args = [
+        &format!("--binary={}", fixture.target_binary_path.display()),
+        &format!("--workdir={}", work_dir.display()),
+        "--exit_on_crash",
+        "--test_name=__fuzztest_mod__use_after_free_asan_death_test.use_after_free_asan_death_test",
+    ];
+
+    let stderr = test_utils::run_centipede_with_args_expect_termination(fixture, &args);
+
+    expect_that!(stderr, matchers::contains_regex("Signature[ \t]*: heap-use-after-free"));
+}
+
+// TODO(yamilmorales): Enable this test on presubmit with --config=msan.
+#[gtest]
+#[cfg(sanitize = "memory")]
+fn ensure_sanitizer_crash_signature_with_msan(fixture: &EnvVars) {
+    let work_dir = fixture.tmp_dir_path.join("WD"); // For permissions
+    fs::create_dir_all(&work_dir).expect("Failed to create working directory");
+
+    let args = [
+        &format!("--binary={}", fixture.target_binary_path.display()),
+        &format!("--workdir={}", work_dir.display()),
+        "--exit_on_crash",
+        "--test_name=__fuzztest_mod__msan_death_test.msan_death_test",
+        "--use_cmp_features=0", // Prevent msan from detecting nested bugs and aborting without
+                                // triggering the death callback.
+    ];
+
+    let stderr = test_utils::run_centipede_with_args_expect_termination(fixture, &args);
+
+    expect_that!(stderr, matchers::contains_regex("Signature[ \t]*: Sanitizer crash"));
+}
diff --git a/rust/e2e_tests/worker_with_centipede_test.rs b/rust/e2e_tests/worker_with_centipede_test.rs
new file mode 100644
index 0000000..4a16a7f
--- /dev/null
+++ b/rust/e2e_tests/worker_with_centipede_test.rs
@@ -0,0 +1,121 @@
+use googletest::matchers;
+use googletest::prelude::*;
+use rand::RngExt;
+use std::fs;
+use test_utils::EnvVars;
+
+fn run_centipede_test<'a>(
+    fixture: &EnvVars,
+    test_name: &str,
+    extra_args: impl IntoIterator<Item = &'a str>,
+) -> String {
+    let random_suffix: String =
+        rand::rng().sample_iter(&rand::distr::Alphanumeric).take(10).map(char::from).collect();
+    let work_dir_name = format!("WD_{}_{}", test_name, random_suffix);
+    let work_dir = fixture.tmp_dir_path.join(work_dir_name);
+    fs::create_dir_all(&work_dir).expect("Failed to create working directory");
+
+    let rust_test_name = test_name.replace(".", "::");
+    let mut args: Vec<String> = vec![
+        format!(
+            "--binary={} --nocapture {} --exact",
+            fixture.target_binary_path.display(),
+            rust_test_name
+        ),
+        format!("--workdir={}", work_dir.display()),
+        format!("--test_name={}", test_name),
+    ];
+    args.extend(extra_args.into_iter().map(|s| s.to_string()));
+
+    let args: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
+    test_utils::run_centipede_with_args_expect_termination(fixture, &args)
+}
+
+#[gtest]
+fn find_bug_with_centipede_test(fixture: &EnvVars) {
+    let stderr = run_centipede_test(
+        fixture,
+        "__fuzztest_mod__find_bug_fuzz_test.find_bug_fuzz_test",
+        ["--exit_on_crash"],
+    );
+
+    expect_that!(stderr, matchers::contains_substring("Property function ran but crashed."));
+    expect_that!(stderr, matchers::contains_regex("Signature[ \t]*: Unwinding panic"));
+    expect_that!(stderr, matchers::contains_substring("CRASH LOG: Bug found!"));
+}
+
+#[gtest]
+fn ensure_custom_mutator_with_centipede_test(fixture: &EnvVars) {
+    let stderr = run_centipede_test(
+        fixture,
+        "__fuzztest_mod__find_bug_fuzz_test.find_bug_fuzz_test",
+        ["--stop_after=10s"],
+    );
+
+    // In the 10 seconds that Centipede ran, the fuzzing loop should have requested to mutate.
+    expect_that!(stderr, matchers::contains_substring("Custom mutator detected"));
+}
+
+#[gtest]
+fn ensure_seed_emition_with_centipede_test(fixture: &EnvVars) {
+    let stderr = run_centipede_test(
+        fixture,
+        "__fuzztest_mod__find_bug_fuzz_test.find_bug_fuzz_test",
+        ["--exit_on_crash", "--require_seeds"],
+    );
+
+    // If not present, then exited early because no seeds were emitted.
+    expect_that!(stderr, matchers::contains_substring("Number of input seeds available"));
+}
+
+#[gtest]
+fn ensure_features_work_finding_rarer_bug(fixture: &EnvVars) {
+    let stderr = run_centipede_test(
+        fixture,
+        "__fuzztest_mod__find_rarer_bug_fuzz_test.find_rarer_bug_fuzz_test",
+        ["--exit_on_crash"],
+    );
+
+    // Centipede's passes a length-prefixed screen, so the initial \\\\x5 stands for
+    // that length, not any kind of hex-escaping.
+    expect_that!(stderr, matchers::contains_regex("Input bytes[ \t]*: \\\\x5PaNiC"));
+    expect_that!(stderr, matchers::contains_substring("CRASH LOG: Bug found!"));
+}
+
+#[gtest]
+fn ensure_features_work_finding_rarer_bug_2_args(fixture: &EnvVars) {
+    let stderr = run_centipede_test(
+        fixture,
+        "__fuzztest_mod__find_rarer_bug_2_args_fuzz_test.find_rarer_bug_2_args_fuzz_test",
+        ["--exit_on_crash"],
+    );
+
+    expect_that!(stderr, matchers::contains_substring("CRASH LOG: Bug found!"));
+}
+
+#[gtest]
+fn ensure_features_work_finding_rarer_bug_3_args(fixture: &EnvVars) {
+    let stderr = run_centipede_test(
+        fixture,
+        "__fuzztest_mod__find_rarer_bug_3_args_fuzz_test.find_rarer_bug_3_args_fuzz_test",
+        ["--exit_on_crash"],
+    );
+
+    expect_that!(stderr, matchers::contains_substring("CRASH LOG: Bug found!"));
+}
+
+#[gtest]
+fn ensure_emit_error_with_centipede_test(fixture: &EnvVars) {
+    let stderr = run_centipede_test(
+        fixture,
+        "__fuzztest_mod__fallible_fuzz_test.fallible_fuzz_test",
+        ["--stop_after=5s"],
+    );
+
+    expect_that!(
+        stderr,
+        matchers::contains_substring(
+            "Emitted failure output: Failed to mutate: Intentional mutate failure"
+        )
+    );
+}
diff --git a/rust/engine/BUILD b/rust/engine/BUILD
new file mode 100644
index 0000000..8004b3b
--- /dev/null
+++ b/rust/engine/BUILD
@@ -0,0 +1,18 @@
+load("@rules_rust//rust:defs.bzl", "rust_library")
+
+licenses(["notice"])
+
+rust_library(
+    name = "engine",
+    srcs = [
+        "src/engine_ffi.rs",
+        "src/lib.rs",
+    ],
+    deps = [
+        "@com_google_fuzztest//centipede:engine_abi",
+        "@com_google_fuzztest//centipede:engine_controller_with_subprocess",
+        "@com_google_fuzztest//centipede:engine_worker",
+    ],
+    edition = "2024",
+    visibility = ["@com_google_fuzztest//rust:__subpackages__"],
+)
diff --git a/rust/engine/Cargo.toml b/rust/engine/Cargo.toml
new file mode 100644
index 0000000..e19dc15
--- /dev/null
+++ b/rust/engine/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "engine-ffi"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+
+
diff --git a/rust/engine/build.rs b/rust/engine/build.rs
new file mode 100644
index 0000000..05b5942
--- /dev/null
+++ b/rust/engine/build.rs
@@ -0,0 +1,29 @@
+//! Build script for the `engine-ffi` crate.
+//!
+//! ## Prerequisites
+//!
+//! Before compiling or testing this crate using Cargo (`cargo build` or `cargo test`),
+//! you must first compile the C++ Centipede dependencies using Bazel (or Blaze):
+//!
+//! ```bash
+//! bazel build //centipede:centipede_engine_static
+//! ```
+//!
+//! This command generates the required static library archive `libcentipede_engine_static.a`
+//! under `bazel-bin/centipede` (or `blaze-bin/centipede`).
+//!
+//! You must provide the path to this library via `RUSTFLAGS` when running Cargo:
+//!
+//! ```bash
+//! RUSTFLAGS="-L bazel-bin/centipede" cargo build
+//! ```
+
+fn main() {
+    println!("cargo:rustc-link-lib=static=centipede_engine_static");
+
+    // Link required system libraries
+    println!("cargo:rustc-link-lib=dylib=stdc++");
+    println!("cargo:rustc-link-lib=dylib=rt");
+    println!("cargo:rustc-link-lib=dylib=dl");
+    println!("cargo:rustc-link-lib=dylib=pthread");
+}
diff --git a/rust/engine/src/engine_ffi.rs b/rust/engine/src/engine_ffi.rs
new file mode 100644
index 0000000..69e2c2c
--- /dev/null
+++ b/rust/engine/src/engine_ffi.rs
@@ -0,0 +1,441 @@
+// The source of truth for the FFI definitions in this file are these C++ header files:
+// - engine_abi.h
+// - engine_worker_abi.h
+// - engine_controller_abi.h
+// TODO(b/532255598): use crubit
+
+use core::ffi::c_int;
+
+use core::marker::PhantomData;
+use core::marker::PhantomPinned;
+use core::ptr;
+
+// Opaque handles for in-memory input objects used by the test, which
+// may be serialized/deserialized by the engine.
+#[repr(transparent)]
+pub struct FuzzTestInputHandle(pub usize);
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct FuzzTestBytesView {
+    pub data: *const u8,
+    pub size: usize,
+}
+
+impl FuzzTestBytesView {
+    pub fn from_bytes(bytes: &[u8]) -> Self {
+        Self { data: bytes.as_ptr(), size: bytes.len() }
+    }
+
+    pub fn to_bytes(&self) -> *const [u8] {
+        if self.data.is_null() {
+            &[]
+        } else {
+            ptr::slice_from_raw_parts(self.data, self.size)
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct FuzzTestBytesViews {
+    pub views: *const FuzzTestBytesView,
+    pub count: usize,
+}
+
+impl FuzzTestBytesViews {
+    pub fn from_slice(views: &[FuzzTestBytesView]) -> Self {
+        Self { views: views.as_ptr(), count: views.len() }
+    }
+
+    pub fn as_slice(&self) -> *const [FuzzTestBytesView] {
+        if self.views.is_null() {
+            &[]
+        } else {
+            ptr::slice_from_raw_parts(self.views, self.count)
+        }
+    }
+}
+
+#[repr(C)]
+pub struct FuzzTestDiagnosticSinkCtx {
+    _opaque: (),
+    _not_send_nor_sync: PhantomData<*mut ()>,
+    _not_unpin: PhantomPinned,
+}
+
+/// Sink for diagnostics during the setup and execution of the test
+/// methods. Methods in this sink are async-safe and thread-safe.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct FuzzTestDiagnosticSink {
+    pub ctx: *mut FuzzTestDiagnosticSinkCtx,
+
+    /// Emits an unrecoverable error with a human-readable `message`. Engine would
+    /// propagate the error to the controller command when in the worker mode.
+    pub emit_error: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestDiagnosticSinkCtx,
+            message: *const FuzzTestBytesView,
+        ),
+    >,
+
+    /// Emits a warning with a human-readable `message`. Engine would log
+    /// the warning and continue gracefully when in the worker mode.
+    pub emit_warning: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestDiagnosticSinkCtx,
+            message: *const FuzzTestBytesView,
+        ),
+    >,
+
+    /// Emits a finding for running a test input within `Execute()` with a
+    /// human-readable `description`, and `signature` for deduplication.
+    ///
+    /// Must not be called if the engine is not calling `Execute()`. If called
+    /// multiple times within the same `Execute()` window, a random one would take
+    /// effect.
+    pub emit_finding: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestDiagnosticSinkCtx,
+            description: *const FuzzTestBytesView,
+            signature: *const FuzzTestBytesView,
+        ),
+    >,
+}
+
+// SAFETY: The engine guarantees that the diagnostic sink and its context are
+// thread-safe and async-safe.
+unsafe impl Send for FuzzTestDiagnosticSink {}
+unsafe impl Sync for FuzzTestDiagnosticSink {}
+
+#[repr(C)]
+pub struct FuzzTestBytesSinkCtx {
+    _opaque: (),
+    _not_send_nor_sync: PhantomData<*mut ()>,
+    _not_unpin: PhantomPinned,
+}
+
+/// Sink for bytes data.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct FuzzTestBytesSink {
+    pub ctx: *mut FuzzTestBytesSinkCtx,
+
+    /// Emits a byte buffer. Multiple emissions are concatenated.
+    pub emit: Option<
+        unsafe extern "C" fn(ctx: *mut FuzzTestBytesSinkCtx, view: *const FuzzTestBytesView),
+    >,
+}
+
+#[repr(C)]
+pub struct FuzzTestInputSinkCtx {
+    _opaque: (),
+    _not_send_nor_sync: PhantomData<*mut ()>,
+    _not_unpin: PhantomPinned,
+}
+
+/// Sink for seed inputs.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct FuzzTestInputSink {
+    pub ctx: *mut FuzzTestInputSinkCtx,
+
+    /// Emits a test `input` to the engine. Engine would call
+    /// `FuzzTestAdapter::FreeInput` on the emitted input after the engine is
+    /// done with it.
+    pub emit:
+        Option<unsafe extern "C" fn(ctx: *mut FuzzTestInputSinkCtx, input: FuzzTestInputHandle)>,
+}
+
+#[repr(C)]
+pub struct FuzzTestUint64sView {
+    pub data: *const u64,
+    pub size: usize,
+}
+
+impl FuzzTestUint64sView {
+    pub fn from_slice(slice: &[u64]) -> Self {
+        Self { data: slice.as_ptr(), size: slice.len() }
+    }
+
+    pub fn as_slice(&self) -> *const [u64] {
+        if self.data.is_null() || self.size == 0 {
+            &[]
+        } else {
+            ptr::slice_from_raw_parts(self.data, self.size)
+        }
+    }
+
+    pub fn to_bytes(&self) -> *const [u8] {
+        if self.data.is_null() {
+            &[]
+        } else {
+            ptr::slice_from_raw_parts(
+                self.data as *const u8,
+                self.size * core::mem::size_of::<u64>(),
+            )
+        }
+    }
+}
+/// Constants for the layout of the coverage feature as a 64-bit unsigned
+/// integer:
+///
+///   - Bits 63..59: 5-bit domain ID of the feature. Each domain is a
+///     logically independent feature namespace registered in
+///     `FuzzTestAdapter::SetUpCoverageDomains`.
+///   - Bits 58..32: 27-bit feature ID within the domain.
+///   - Bits 31..0:  32-bit counter value of the feature.
+///
+pub struct FuzzTestCoverageFeatureLayout;
+
+impl FuzzTestCoverageFeatureLayout {
+    pub const COUNTER_START_BIT: u32 = 0;
+    pub const COUNTER_BIT_SIZE: u32 = 32;
+    pub const FEATURE_ID_START_BIT: u32 = 32;
+    pub const FEATURE_ID_BIT_SIZE: u32 = 27;
+    pub const DOMAIN_ID_START_BIT: u32 = 59;
+    pub const DOMAIN_ID_BIT_SIZE: u32 = 5;
+}
+
+#[repr(C)]
+pub struct FuzzTestFeedbackSinkCtx {
+    _opaque: (),
+    _not_send_nor_sync: PhantomData<*mut ()>,
+    _not_unpin: PhantomPinned,
+}
+
+/// Sink for execution feedback.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct FuzzTestFeedbackSink {
+    pub ctx: *mut FuzzTestFeedbackSinkCtx,
+
+    /// Emits an array of coverage features captured from the execution
+    /// inside `Execute` call. See `FuzzTestCoverageFeatureLayout` for the feature
+    /// layout.
+    ///
+    /// Multiple emissions are concatenated.
+    pub emit_coverage_features: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestFeedbackSinkCtx,
+            features: *const FuzzTestUint64sView,
+        ),
+    >,
+}
+
+/// Information of a coverage domain.
+#[repr(C)]
+pub struct FuzzTestCoverageDomain {
+    /// 5-bit domain ID.
+    pub domain_id: u8,
+    /// Human-readable name of the domain for logging.
+    pub name: FuzzTestBytesView,
+    /// Number of bits used for the feature IDs in this domain, must be <= 27.
+    pub feature_id_bit_size: u8,
+    /// Number of bits used for the counter values in this domain, must be <= 32.
+    pub counter_bit_size: u8,
+}
+
+#[repr(C)]
+pub struct FuzzTestDomainRegistryCtx {
+    _opaque: (),
+    _not_send_nor_sync: PhantomData<*mut ()>,
+    _not_unpin: PhantomPinned,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct FuzzTestCoverageDomainRegistry {
+    pub ctx: *mut FuzzTestDomainRegistryCtx,
+
+    /// Registers a new coverage `domain`.
+    pub register: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestDomainRegistryCtx,
+            domain: *const FuzzTestCoverageDomain,
+        ),
+    >,
+}
+
+#[repr(C)]
+pub struct FuzzTestAdapterCtx {
+    _opaque: (),
+    _not_send_nor_sync: PhantomData<*mut ()>,
+    _not_unpin: PhantomPinned,
+}
+
+#[repr(C)]
+pub struct FuzzTestAdapter {
+    pub ctx: *mut FuzzTestAdapterCtx,
+
+    /// Sets up coverage domains using the domain `registry`.
+    /// The domain registrations must be the same for the all the test adapters of
+    /// the same test (identified by the test name and the binary).
+    pub set_up_coverage_domains: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestAdapterCtx,
+            registry: *const FuzzTestCoverageDomainRegistry,
+        ),
+    >,
+
+    /// [Optional] Emits any preset seed inputs of the test using `sink`.
+    /// The output must be the same for the all the test adapters of the same test
+    /// (identified by the test name and the binary).
+    pub get_preset_seed_inputs:
+        Option<unsafe extern "C" fn(ctx: *mut FuzzTestAdapterCtx, sink: *const FuzzTestInputSink)>,
+
+    /// Emits a randomly generated seed input using `sink`.
+    pub get_random_seed_input:
+        Option<unsafe extern "C" fn(ctx: *mut FuzzTestAdapterCtx, sink: *const FuzzTestInputSink)>,
+
+    /// Mutates from `origin`, and emits the mutant using `sink`. `shrink` != 0
+    /// means to generate smaller mutant.
+    ///
+    /// It should not change the content/metadata of `origin`.
+    pub mutate: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestAdapterCtx,
+            origin: FuzzTestInputHandle,
+            shrink: c_int,
+            sink: *const FuzzTestInputSink,
+        ),
+    >,
+
+    /// [Optional] Performs cross-over mutation using `origin` and `other`, and
+    /// emits the mutant using `sink`.
+    ///
+    /// It should not change the content/metadata of `origin` or `other`.
+    pub cross_over: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestAdapterCtx,
+            origin: FuzzTestInputHandle,
+            other: FuzzTestInputHandle,
+            sink: *const FuzzTestInputSink,
+        ),
+    >,
+
+    /// Executes `input` for testing and emits any feedback to `sink`.
+    ///
+    /// The `input` metadata may be updated by the adapter for further
+    /// mutations. The `input` content, which affects the test behavior of
+    /// `Execute()`, should not be changed.
+    pub execute: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestAdapterCtx,
+            input: FuzzTestInputHandle,
+            sink: *const FuzzTestFeedbackSink,
+        ),
+    >,
+
+    /// Serializes the test `input` content into bytes using `sink`.
+    pub serialize_input_content: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestAdapterCtx,
+            input: FuzzTestInputHandle,
+            sink: *const FuzzTestBytesSink,
+        ),
+    >,
+
+    /// Deserializes the test `input` content from serialized `content` into
+    /// a `FuzzTestInputHandle` using `sink`.
+    pub deserialize_input_content: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestAdapterCtx,
+            content: *const FuzzTestBytesView,
+            sink: *const FuzzTestInputSink,
+        ),
+    >,
+
+    /// [Optional] Serializes the test `input` metadata into bytes using `sink`.
+    pub serialize_input_metadata: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestAdapterCtx,
+            input: FuzzTestInputHandle,
+            sink: *const FuzzTestBytesSink,
+        ),
+    >,
+
+    /// [Optional] Updates the test `input` metadata from serialized `metadata`.
+    pub update_input_metadata: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestAdapterCtx,
+            metadata: *const FuzzTestBytesView,
+            input: FuzzTestInputHandle,
+        ),
+    >,
+
+    /// Callback to run when the engine is done with `input`.
+    pub free_input:
+        Option<unsafe extern "C" fn(ctx: *mut FuzzTestAdapterCtx, input: FuzzTestInputHandle)>,
+
+    /// Callback to run when the engine is done with `ctx` (and the adapter).
+    pub free_ctx: Option<unsafe extern "C" fn(ctx: *mut FuzzTestAdapterCtx)>,
+}
+
+#[repr(C)]
+pub struct FuzzTestAdapterManagerCtx {
+    _opaque: (),
+    _not_send_nor_sync: PhantomData<*mut ()>,
+    _not_unpin: PhantomPinned,
+}
+
+#[repr(C)]
+pub struct FuzzTestAdapterManager {
+    pub ctx: *mut FuzzTestAdapterManagerCtx,
+
+    /// [Optional] Emits the ID for the current binary.
+    pub get_binary_id: Option<
+        unsafe extern "C" fn(ctx: *mut FuzzTestAdapterManagerCtx, sink: *const FuzzTestBytesSink),
+    >,
+
+    /// Emits the test name.
+    pub get_test_name: Option<
+        unsafe extern "C" fn(ctx: *mut FuzzTestAdapterManagerCtx, sink: *const FuzzTestBytesSink),
+    >,
+
+    /// Constructs an adapter of the test into `adapter_out`. Any diagnostics
+    /// happening during the construction or running the adapter should be emitted
+    /// to `diagnostic_sink`. `diagnostic_sink` is guaranteed to live until
+    /// `FreeCtx` is called on the adapter.
+    pub construct_adapter: Option<
+        unsafe extern "C" fn(
+            ctx: *mut FuzzTestAdapterManagerCtx,
+            diagnostic_sink: *const FuzzTestDiagnosticSink,
+            adapter_out: *mut FuzzTestAdapter,
+        ),
+    >,
+}
+
+#[repr(i32)]
+pub enum FuzzTestWorkerStatus {
+    /// Test should finish with a success.
+    Success = 0,
+    /// Test should finish with a failure.
+    Failure = 1,
+    /// Test should continue with controller commands.
+    NotRequired = 2,
+}
+
+#[derive(PartialEq, Eq)]
+#[repr(i32)]
+pub enum FuzzTestControllerStatus {
+    Success = 0,
+    Failure = 1,
+}
+
+#[allow(improper_ctypes)]
+unsafe extern "C" {
+    /// Try to run as a FuzzTest worker with `manager` if needed.
+    pub fn FuzzTestWorkerMaybeRun(manager: *const FuzzTestAdapterManager) -> FuzzTestWorkerStatus;
+
+    /// Returns whether the current process is running as a FuzzTest worker -
+    /// non-zero means yes and zero means no. It can be called outside of tests.
+    pub fn FuzzTestWorkerIsRequired() -> c_int;
+
+    /// Run the FuzzTest controller with `flags` and `manager`.
+    pub fn FuzzTestControllerRun(
+        manager: *const FuzzTestAdapterManager,
+        flags: *const FuzzTestBytesViews,
+    ) -> FuzzTestControllerStatus;
+}
diff --git a/rust/engine/src/lib.rs b/rust/engine/src/lib.rs
new file mode 100644
index 0000000..7594c50
--- /dev/null
+++ b/rust/engine/src/lib.rs
@@ -0,0 +1,268 @@
+pub mod engine_ffi;
+
+use std::marker::PhantomData;
+
+#[allow(improper_ctypes)]
+unsafe extern "C" {
+    fn SanCovRuntimeSetUpCoverageDomains(
+        registry: *const engine_ffi::FuzzTestCoverageDomainRegistry,
+    ) -> usize;
+
+    fn SanCovRuntimeEmitFeatures(sink: *const engine_ffi::FuzzTestFeedbackSink);
+}
+
+/// A zero-sized witness token proving that a test is currently executing.
+///
+/// Some of the methods on FuzzTestDiagnosticSink require that they are called only if the engine is
+/// calling `Execute` callback of FuzzTestAdapter, which includes:
+///
+/// * `emit_finding`
+///
+/// The methods receiving this type can safely assume that the `Execute` callback is active because
+/// the caller ensures this by creating this type.
+pub struct ExecuteContext {
+    _private: PhantomData<()>,
+}
+
+impl ExecuteContext {
+    /// Creates a new execution context.
+    ///
+    /// # Safety
+    ///
+    /// * The caller must ensure that it is in the `Execute` callback of FuzzTestAdapter.
+    pub unsafe fn new() -> Self {
+        Self { _private: PhantomData }
+    }
+}
+
+#[derive(Clone)]
+pub struct DiagnosticSink {
+    raw: engine_ffi::FuzzTestDiagnosticSink,
+}
+
+impl DiagnosticSink {
+    /// Creates a safe wrapper from a pointer to `FuzzTestDiagnosticSink`.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that:
+    /// * `raw` is a non-null, properly aligned pointer to a valid `FuzzTestDiagnosticSink`.
+    /// * `(*raw).ctx` is a valid context pointer.
+    /// * The sink and its context remain valid for the duration of `DiagnosticSink`'s usage
+    ///   (guaranteed by the engine until `FreeCtx` is called on the adapter).
+    pub unsafe fn from_raw(raw: *const engine_ffi::FuzzTestDiagnosticSink) -> Self {
+        // SAFETY: Caller guarantees `raw` is a valid pointer to `FuzzTestDiagnosticSink`.
+        let raw_sink = unsafe { *raw };
+        Self { raw: raw_sink }
+    }
+
+    /// Emits an unrecoverable error message to the underlying diagnostic sink.
+    pub fn emit_error(&self, message: &str) {
+        let emit = self.raw.emit_error.expect("EmitError is required by ABI");
+        let view = engine_ffi::FuzzTestBytesView::from_bytes(message.as_bytes());
+        // SAFETY: The presence of `self` guarantees that the context of the underlying
+        // `FuzzTestDiagnosticSink` remains live for the duration of this sink.
+        unsafe {
+            emit(self.raw.ctx, &view);
+        }
+    }
+
+    /// Emits a warning message to the underlying diagnostic sink.
+    pub fn emit_warning(&self, message: &str) {
+        let emit = self.raw.emit_warning.expect("EmitWarning is required by ABI");
+        let view = engine_ffi::FuzzTestBytesView::from_bytes(message.as_bytes());
+        // SAFETY: The presence of `self` guarantees that the context of the underlying
+        // `FuzzTestDiagnosticSink` remains live for the duration of this sink.
+        unsafe {
+            emit(self.raw.ctx, &view);
+        }
+    }
+
+    /// Emits a test finding (e.g., a crash or bug) to the underlying diagnostic sink.
+    ///
+    /// This must only be called during test execution, which is guaranteed by the
+    /// [`ExecuteContext`] token.
+    pub fn emit_finding(&self, _token: &ExecuteContext, description: &str, signature: &str) {
+        let emit = self.raw.emit_finding.expect("EmitFinding is required by ABI");
+        let desc_view = engine_ffi::FuzzTestBytesView::from_bytes(description.as_bytes());
+        let sig_view = engine_ffi::FuzzTestBytesView::from_bytes(signature.as_bytes());
+        // SAFETY: The presence of `self` guarantees that the context of the underlying
+        // `FuzzTestDiagnosticSink` remains live for the duration of this sink.
+        // The witness `_token` further proves execution is currently within the `Execute` callback.
+        unsafe {
+            emit(self.raw.ctx, &desc_view, &sig_view);
+        }
+    }
+}
+
+pub struct BytesSink {
+    raw: engine_ffi::FuzzTestBytesSink,
+}
+
+impl BytesSink {
+    /// Creates a safe wrapper from a pointer to `FuzzTestBytesSink`.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that:
+    /// * `raw` is a non-null, properly aligned pointer to a valid `FuzzTestBytesSink`.
+    /// * `(*raw).ctx` is a valid context pointer.
+    /// * The sink and its context remain valid for the duration of `BytesSink`'s usage
+    ///   (guaranteed by the engine until `FreeCtx` is called on the adapter).
+    pub unsafe fn from_raw(raw: *const engine_ffi::FuzzTestBytesSink) -> Self {
+        // SAFETY: Caller guarantees `raw` is a valid pointer to `FuzzTestBytesSink`.
+        let raw_sink = unsafe { *raw };
+        Self { raw: raw_sink }
+    }
+
+    /// Emits a byte slice to the underlying bytes sink.
+    ///
+    /// Multiple calls to this function will concatenate the emitted bytes.
+    pub fn emit(&mut self, bytes: &[u8]) {
+        let emit = self.raw.emit.expect("Emit is required by ABI");
+        let view = engine_ffi::FuzzTestBytesView::from_bytes(bytes);
+        // SAFETY: The presence of `self` guarantees `from_raw` was called with a valid context
+        // pointer (`self.raw.ctx`) that remains live for the duration of this call.
+        unsafe {
+            emit(self.raw.ctx, &view);
+        }
+    }
+}
+
+pub struct InputSink {
+    raw: engine_ffi::FuzzTestInputSink,
+}
+
+impl InputSink {
+    /// Creates a safe wrapper from a pointer to `FuzzTestInputSink`.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that:
+    /// * `raw` is a non-null, properly aligned pointer to a valid `FuzzTestInputSink`.
+    /// * `(*raw).ctx` is a valid context pointer.
+    /// * The sink and its context remain valid for the duration of `InputSink`'s usage
+    ///   (guaranteed by the engine until `FreeCtx` is called on the adapter).
+    pub unsafe fn from_raw(raw: *const engine_ffi::FuzzTestInputSink) -> Self {
+        // SAFETY: Caller guarantees `raw` is a valid pointer to `FuzzTestInputSink`.
+        let raw_sink = unsafe { *raw };
+        Self { raw: raw_sink }
+    }
+
+    /// Emits a test input handle to the underlying input sink.
+    ///
+    /// This function transfers ownership of the input represented by the handle
+    /// to the engine. The engine is responsible for freeing it.
+    pub fn emit(&mut self, input: engine_ffi::FuzzTestInputHandle) {
+        let emit = self.raw.emit.expect("Emit is required by ABI");
+        // SAFETY: The presence of `self` guarantees `from_raw` was called with a valid context
+        // pointer (`self.raw.ctx`) that remains live for the duration of this call.
+        unsafe {
+            emit(self.raw.ctx, input);
+        }
+    }
+}
+
+pub struct FeedbackSink {
+    raw: engine_ffi::FuzzTestFeedbackSink,
+}
+
+impl FeedbackSink {
+    /// Creates a safe wrapper from a pointer to `FuzzTestFeedbackSink`.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that:
+    /// * `raw` is a non-null, properly aligned pointer to a valid `FuzzTestFeedbackSink`.
+    /// * `(*raw).ctx` is a valid context pointer.
+    /// * The sink and its context remain valid for the duration of `FeedbackSink`'s usage
+    ///   (guaranteed by the engine until `FreeCtx` is called on the adapter).
+    pub unsafe fn from_raw(raw: *const engine_ffi::FuzzTestFeedbackSink) -> Self {
+        // SAFETY: Caller guarantees `raw` is a valid pointer to `FuzzTestFeedbackSink`.
+        let raw_sink = unsafe { *raw };
+        Self { raw: raw_sink }
+    }
+
+    /// Emits execution coverage features to the underlying feedback sink.
+    pub fn emit_coverage_features(&mut self, features: &[u64]) {
+        let emit =
+            self.raw.emit_coverage_features.expect("EmitCoverageFeatures is required by ABI");
+        let view = engine_ffi::FuzzTestUint64sView::from_slice(features);
+        // SAFETY: The presence of `self` guarantees `from_raw` was called with a valid context
+        // pointer (`self.raw.ctx`) that remains live for the duration of this call.
+        unsafe {
+            emit(self.raw.ctx, &view);
+        }
+    }
+
+    /// Emits SanitizerCoverage features.
+    pub fn emit_sancov_features(&mut self) {
+        // SAFETY: the sink pointer is guaranteed to be valid by the framework.
+        unsafe { SanCovRuntimeEmitFeatures(&self.raw) };
+    }
+}
+
+/// Information describing a coverage feature domain.
+///
+/// This is a high-level  wrapper for [`engine_ffi::FuzzTestCoverageDomain`],
+/// which is the Rust representation of the C struct `FuzzTestCoverageDomain`
+/// defined in `engine_abi.h` (the source of truth for the ABI).
+///
+/// Unlike the FFI representation which uses [`engine_ffi::FuzzTestBytesView`],
+/// this type uses Rust slices (`&[u8]`) for safety and convenience. It can be
+/// converted to the FFI representation using `From`/`Into`.
+///
+/// A coverage domain represents a logically independent feature namespace registered in
+/// `CoverageDomainRegistry`. FuzzTest packs 64-bit coverage features into three parts:
+/// * Bits 63..59 (5 bits): 5-bit domain ID (`domain_id`, up to 32 domains).
+/// * Bits 58..32 (27 bits): Feature ID within the domain (`feature_id_bit_size` <= 27).
+/// * Bits 31..0 (32 bits): Feature counter value (`counter_bit_size` <= 32).
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct CoverageDomain<'a> {
+    /// 5-bit unique domain identifier (must be < 32).
+    pub domain_id: u8,
+    /// Human-readable name of the domain for logging and debugging.
+    pub name: &'a str,
+    /// Number of bits used for feature IDs in this domain (must be <= 27).
+    pub feature_id_bit_size: u8,
+    /// Number of bits used for feature counter values in this domain (must be <= 32).
+    pub counter_bit_size: u8,
+}
+
+impl<'a> From<CoverageDomain<'a>> for engine_ffi::FuzzTestCoverageDomain {
+    fn from(domain: CoverageDomain<'a>) -> Self {
+        Self {
+            domain_id: domain.domain_id,
+            name: engine_ffi::FuzzTestBytesView::from_bytes(domain.name.as_bytes()),
+            feature_id_bit_size: domain.feature_id_bit_size,
+            counter_bit_size: domain.counter_bit_size,
+        }
+    }
+}
+
+pub struct CoverageDomainRegistry {
+    raw: engine_ffi::FuzzTestCoverageDomainRegistry,
+}
+
+impl CoverageDomainRegistry {
+    /// Creates a safe wrapper from a pointer to `FuzzTestCoverageDomainRegistry`.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure that:
+    /// * `raw` is a non-null, properly aligned pointer to a valid `FuzzTestCoverageDomainRegistry`.
+    /// * `(*raw).ctx` is a valid context pointer.
+    /// * The registry and its context remain valid for the duration of `CoverageDomainRegistry`'s usage
+    ///   (guaranteed by the engine until `FreeCtx` is called on the adapter).
+    pub unsafe fn from_raw(raw: *const engine_ffi::FuzzTestCoverageDomainRegistry) -> Self {
+        // SAFETY: Caller guarantees `raw` is a valid pointer to `FuzzTestCoverageDomainRegistry`.
+        let raw_registry = unsafe { *raw };
+        Self { raw: raw_registry }
+    }
+
+    /// Sets up SanitizerCoverage domains.
+    pub fn set_up_sancov_domains(&self) {
+        // SAFETY: The registry pointer is guaranteed to be valid by the framework.
+        unsafe { SanCovRuntimeSetUpCoverageDomains(&self.raw) };
+    }
+}
diff --git a/rust/fuzztest_macro/.rustfmt.toml b/rust/fuzztest_macro/.rustfmt.toml
new file mode 100644
index 0000000..33b356d
--- /dev/null
+++ b/rust/fuzztest_macro/.rustfmt.toml
@@ -0,0 +1,3 @@
+use_field_init_shorthand = true
+use_try_shorthand = true
+edition = "2021"
diff --git a/rust/fuzztest_macro/BUILD b/rust/fuzztest_macro/BUILD
new file mode 100644
index 0000000..13b6775
--- /dev/null
+++ b/rust/fuzztest_macro/BUILD
@@ -0,0 +1,41 @@
+load("@rules_rust//rust:defs.bzl", "rust_proc_macro", "rust_test")
+
+licenses(["notice"])
+
+rust_proc_macro(
+    name = "fuzztest_macro",
+    srcs = glob(
+        [
+            "src/**/*.rs",
+        ],
+        exclude = ["src/test_main.rs"],
+    ),
+    edition = "2024",
+    visibility = ["@com_google_fuzztest//rust:__subpackages__"],
+    deps = [
+        "@crate_index//:convert_case",  # v0_4
+        "@crate_index//:proc-macro2",
+        "@crate_index//:proc-macro-crate",
+        "@crate_index//:quote",  # v1
+        "@crate_index//:syn",  # v2
+    ],
+)
+
+rust_test(
+    name = "fuzztest_macro_test",
+    srcs = glob(
+        ["src/**/*.rs"],
+        exclude = ["src/lib.rs"],
+    ),
+    crate_root = "src/test_main.rs",
+    edition = "2024",
+    proc_macro_deps = [":fuzztest_macro"],
+    deps = [
+        "@crate_index//:googletest",
+        "@crate_index//:convert_case",  # v0_4
+        "@crate_index//:proc-macro2",
+        "@crate_index//:proc-macro-crate",
+        "@crate_index//:quote",  # v1
+        "@crate_index//:syn",  # v2
+    ],
+)
diff --git a/rust/fuzztest_macro/Cargo.toml b/rust/fuzztest_macro/Cargo.toml
new file mode 100644
index 0000000..2b616da
--- /dev/null
+++ b/rust/fuzztest_macro/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "fuzztest-macro"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+convert_case = "0.4.0"
+proc-macro2 = "1.0.106"
+proc-macro-crate = "3.4.0"
+quote = "1.0.45"
+syn = { version = "2.0.117", features = ["extra-traits", "full", "parsing", "printing", "visit-mut"] }
+
+
+[dev-dependencies]
+googletest = "0.14.3"
diff --git a/rust/fuzztest_macro/src/helpers.rs b/rust/fuzztest_macro/src/helpers.rs
new file mode 100644
index 0000000..14578c9
--- /dev/null
+++ b/rust/fuzztest_macro/src/helpers.rs
@@ -0,0 +1,26 @@
+use proc_macro2::Span;
+use proc_macro2::TokenStream;
+use proc_macro_crate::FoundCrate;
+use quote::quote;
+use syn::Ident;
+
+mod fn_item_utils;
+mod fuzztest_domain;
+pub mod test_registration;
+
+#[derive(Debug, Clone)]
+struct TestFnArgument<'a> {
+    name: &'a syn::Ident,
+    ty: syn::Type,
+}
+
+fn import_fuzztest_crate() -> TokenStream {
+    match proc_macro_crate::crate_name("fuzztest") {
+        Ok(FoundCrate::Itself) => quote! { crate },
+        Ok(FoundCrate::Name(name)) => {
+            let ident = Ident::new(&name, Span::call_site());
+            quote! { #ident }
+        }
+        Err(_) => quote! { ::fuzztest },
+    }
+}
diff --git a/rust/fuzztest_macro/src/helpers/fn_item_utils.rs b/rust/fuzztest_macro/src/helpers/fn_item_utils.rs
new file mode 100644
index 0000000..0fd0f2c
--- /dev/null
+++ b/rust/fuzztest_macro/src/helpers/fn_item_utils.rs
@@ -0,0 +1,233 @@
+use syn::parse_quote;
+use syn::visit_mut::{self, VisitMut};
+use syn::BareFnArg;
+use syn::Error;
+use syn::FnArg;
+use syn::Pat;
+use syn::Signature;
+use syn::TypeBareFn;
+
+use super::TestFnArgument;
+
+struct LifetimeReplacer<'a> {
+    user_value_lifetime_generic: &'a syn::Lifetime,
+}
+
+impl<'a> VisitMut for LifetimeReplacer<'a> {
+    fn visit_lifetime_mut(&mut self, lifetime: &mut syn::Lifetime) {
+        // If the lifetime is `'static`, we don't want to replace it.
+        if lifetime.ident != "static" {
+            *lifetime = self.user_value_lifetime_generic.clone();
+        }
+        visit_mut::visit_lifetime_mut(self, lifetime);
+    }
+}
+
+/// Extracts the type of the arguments of a given function item (ie: a `syn::ItemFn`).
+pub fn extract_test_function_arguments<'a>(
+    fn_signature: &'a Signature,
+    user_value_lifetime_generic: &syn::Lifetime,
+) -> syn::Result<Vec<TestFnArgument<'a>>> {
+    fn_signature
+        .inputs
+        .iter()
+        .map(|arg| match arg {
+            FnArg::Receiver(_) => {
+                Err(Error::new_spanned(arg, "Receiver is not supported in property functions."))
+            }
+            FnArg::Typed(pat_type) => match pat_type.pat.as_ref() {
+                Pat::Ident(pat_ident) => {
+                    let mut ty = pat_type.ty.as_ref().clone();
+                    let mut lifetime_replacer = LifetimeReplacer { user_value_lifetime_generic };
+                    lifetime_replacer.visit_type_mut(&mut ty);
+                    Ok(TestFnArgument { name: &pat_ident.ident, ty })
+                }
+                pat => Err(Error::new_spanned(pat, "Expected an identifier")),
+            },
+        })
+        .collect::<syn::Result<Vec<_>>>()
+}
+
+/// Extract a bare function pointer type (`syn::TypeBareFn``) from a given
+/// signature (`syn::Signature`)
+///
+/// For example, passing in a `syn::Signature` representing the function
+/// `fn test_function(mut a: i32, b: std::string::String)` would yield the
+/// following bare function pointer type `fn(i32, std::string::String)`.
+///
+/// Example usage:
+/// ```markdown
+/// ```
+/// # use syn::parse_quote;
+///
+/// let signature = parse_quote! {
+///   fn test_function(mut a: i32, b: std::string::String)
+/// };
+///
+/// assert_eq!(fn_sig_to_bare_type(&signature), parse_quote!(fn(i32, std::string::String)));
+/// ```
+/// ```
+///
+pub fn fn_sig_to_bare_type(fn_sig: &Signature) -> syn::Result<TypeBareFn> {
+    let generics = &fn_sig.generics;
+    if generics.type_params().count() > 0 || generics.const_params().count() > 0 {
+        return Err(Error::new_spanned(
+            generics,
+            "Property function should not have any Type or Const generics",
+        ));
+    }
+
+    let bare_input_args = fn_sig
+        .inputs
+        .iter()
+        .map(|arg| match arg {
+            FnArg::Receiver(_) => {
+                Err(Error::new_spanned(arg, "Receiver is not supported in property functions."))
+            }
+            FnArg::Typed(pat_type) => Ok(BareFnArg {
+                attrs: pat_type.attrs.clone(),
+                name: None,
+                ty: pat_type.ty.as_ref().clone(),
+            }),
+        })
+        .collect::<syn::Result<Vec<_>>>()?;
+    let fn_lifetime_generics = generics.lifetimes().collect::<Vec<_>>();
+    let unsafety = &fn_sig.unsafety;
+    let abi = &fn_sig.abi;
+    let variadic = &fn_sig.variadic;
+    let output = &fn_sig.output;
+    if fn_lifetime_generics.is_empty() {
+        Ok(parse_quote! {
+          #unsafety #abi fn (#(#bare_input_args),* #variadic) #output
+        })
+    } else {
+        Ok(parse_quote! {
+          for< #(#fn_lifetime_generics),* > #unsafety #abi fn (#(#bare_input_args),* #variadic) #output
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use googletest::prelude::*;
+
+    #[googletest::test]
+    fn test_extract_test_function_arguments_succeeds() {
+        let fn_item: Signature = syn::parse_quote! {
+            fn test_function(mut a: i32, b: std::string::String)
+        };
+        let user_value_lifetime_generic = parse_quote!('user);
+
+        let test_fn_arguments =
+            extract_test_function_arguments(&fn_item, &user_value_lifetime_generic);
+
+        expect_that!(
+            test_fn_arguments,
+            ok(elements_are![
+                matches_pattern!(TestFnArgument {
+                    name: eq(&&quote::format_ident!("a")),
+                    ty: eq(&syn::parse_quote! { i32 }),
+                }),
+                matches_pattern!(TestFnArgument {
+                    name: eq(&&quote::format_ident!("b")),
+                    ty: eq(&syn::parse_quote! { std::string::String }),
+                })
+            ])
+        );
+    }
+
+    #[googletest::test]
+    fn test_extract_test_function_arguments_with_lifetimes_succeeds() {
+        let fn_item: Signature = syn::parse_quote! {
+            fn test_function<'a, 'b>(mut a: i32, b: &'a str, c: &'b Vec<&'static u8>)
+        };
+        let user_value_lifetime_generic = parse_quote!('user);
+
+        let test_fn_arguments =
+            extract_test_function_arguments(&fn_item, &user_value_lifetime_generic);
+
+        expect_that!(
+            test_fn_arguments,
+            ok(elements_are![
+                matches_pattern!(TestFnArgument {
+                    name: eq(&&quote::format_ident!("a")),
+                    ty: eq(&syn::parse_quote! { i32 }),
+                }),
+                matches_pattern!(TestFnArgument {
+                    name: eq(&&quote::format_ident!("b")),
+                    ty: eq(&syn::parse_quote! { &'user str }),
+                }),
+                matches_pattern!(TestFnArgument {
+                    name: eq(&&quote::format_ident!("c")),
+                    ty: eq(&syn::parse_quote! { &'user Vec<&'static u8> }),
+                })
+            ])
+        );
+    }
+
+    #[googletest::test]
+    fn test_extract_test_function_arguments_fails_with_non_ident_arg() {
+        let fn_item: Signature = syn::parse_quote! {
+            fn test_function(
+              MyStruct { a }: MyStruct, b: std::string::String)
+        };
+        let user_value_lifetime_generic = parse_quote!('user);
+
+        let test_fn_arguments =
+            extract_test_function_arguments(&fn_item, &user_value_lifetime_generic);
+
+        expect_that!(
+            test_fn_arguments,
+            err(displays_as(contains_substring("Expected an identifier")))
+        );
+    }
+
+    #[googletest::test]
+    fn test_extract_test_function_arguments_fails_with_receiver_arg() {
+        let fn_item: Signature = syn::parse_quote! {
+            fn test_function(&self, b: std::string::String)
+        };
+        let user_value_lifetime_generic = parse_quote!('user);
+
+        let test_fn_arguments =
+            extract_test_function_arguments(&fn_item, &user_value_lifetime_generic);
+
+        expect_that!(
+            test_fn_arguments,
+            err(displays_as(contains_substring(
+                "Receiver is not supported in property functions."
+            )))
+        );
+    }
+
+    #[googletest::test]
+    fn test_convert_fn_sig_to_bare_type_fn_succeeds() {
+        expect_that!(
+            fn_sig_to_bare_type(&parse_quote! {
+              fn test_function(a: i32, b: std::string::String) -> bool
+            }),
+            ok(eq(&parse_quote! {fn (i32, std::string::String) -> bool }))
+        )
+    }
+
+    #[googletest::test]
+    fn test_convert_fn_sig_with_no_return_type_to_bare_type_fn_succeeds() {
+        expect_that!(
+            fn_sig_to_bare_type(&parse_quote! {
+              fn test_function(a: i32, b: std::string::String)
+            }),
+            ok(eq(&parse_quote! {fn (i32, std::string::String) }))
+        )
+    }
+
+    #[googletest::test]
+    fn test_convert_fn_sig_with_lifetimes_to_bare_type_fn_succeeds() {
+        expect_that!(
+            fn_sig_to_bare_type(&parse_quote! {
+              fn test_function<'a, 'b>(a: std::vec::Vec<&'a i32>, b: Test<'b>)
+            }),
+            ok(eq(&parse_quote! {for<'a, 'b> fn (std::vec::Vec<&'a i32>, Test<'b>) }))
+        )
+    }
+}
diff --git a/rust/fuzztest_macro/src/helpers/fuzztest_domain.rs b/rust/fuzztest_macro/src/helpers/fuzztest_domain.rs
new file mode 100644
index 0000000..31e334d
--- /dev/null
+++ b/rust/fuzztest_macro/src/helpers/fuzztest_domain.rs
@@ -0,0 +1,162 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::parse_quote;
+use syn::Generics;
+use syn::Ident;
+
+use super::import_fuzztest_crate;
+use super::TestFnArgument;
+
+/// Generates a wrapper domain that will contain all domains of the test
+/// function arguments.
+pub fn generate_fuzztest_domain<'a, 'b: 'a>(
+    domain_struct_name: &Ident,
+    test_fn_args: impl IntoIterator<Item = &'a TestFnArgument<'b>>,
+    user_value_lifetime_generic: &syn::Lifetime,
+) -> (TokenStream, Vec<Ident>) {
+    let crate_name = import_fuzztest_crate();
+    let test_fn_args = test_fn_args.into_iter().collect::<Vec<_>>();
+
+    let domain_generics =
+        (0..test_fn_args.len()).map(|idx| quote::format_ident!("T{idx}")).collect::<Vec<_>>();
+
+    let corpus_domain_generics = domain_generics
+        .iter()
+        .map(|generic| parse_quote!(#generic::CorpusValue))
+        .collect::<Vec<syn::Type>>();
+    let user_value_domain_generics = domain_generics
+        .iter()
+        .map(|generic| parse_quote!(#generic::UserValue<#user_value_lifetime_generic>))
+        .collect::<Vec<syn::Type>>();
+    let mut generics: Generics = parse_quote! { < #(#domain_generics),* > };
+
+    let test_fn_args_with_generics =
+        test_fn_args.into_iter().zip(generics.type_params().cloned()).collect::<Vec<_>>();
+
+    let mut field_names = vec![];
+    let mut domain_field_types = vec![];
+
+    let where_clauses = generics.make_where_clause();
+    for (TestFnArgument { ty, name }, syn::TypeParam { ident: domain_gen, .. }) in
+        &test_fn_args_with_generics
+    {
+        where_clauses.predicates.push(
+          parse_quote! {
+            for <#user_value_lifetime_generic> #domain_gen: #crate_name::domains::Domain<UserValue<#user_value_lifetime_generic> = #ty >
+          });
+        where_clauses.predicates.push(parse_quote! { #domain_gen::CorpusValue: #crate_name::serde::Serialize + #crate_name::serde::de::DeserializeOwned + ::std::clone::Clone });
+        field_names.push((*name).clone());
+        domain_field_types.push(domain_gen);
+    }
+
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    let serde_crate_path = format!("{}", quote!(#crate_name::serde));
+
+    let domain_definition_tokens = quote! {
+      #[derive(#crate_name::serde::Serialize, #crate_name::serde::Deserialize, ::std::fmt::Debug, ::std::clone::Clone)]
+      #[serde(crate = #serde_crate_path)]
+      struct #domain_struct_name #ty_generics {
+        #(#field_names : #domain_field_types),*
+      }
+
+      impl #impl_generics #crate_name::domains::Domain for #domain_struct_name #ty_generics #where_clause {
+        type UserValue<#user_value_lifetime_generic> = #domain_struct_name <#(#user_value_domain_generics),*>;
+        type CorpusValue = #domain_struct_name <#(#corpus_domain_generics),*>;
+
+        fn init(&self, rng: &mut dyn ::fuzztest::reexports::rand::Rng) -> ::fuzztest::reexports::anyhow::Result<Self::CorpusValue> {
+          Ok(#domain_struct_name {
+            #(#field_names: self.#field_names.init(rng)?),*
+          })
+        }
+
+        fn mutate(
+            &self,
+            val: &mut Self::CorpusValue,
+            rng: &mut dyn ::fuzztest::reexports::rand::Rng,
+            only_shrink: bool,
+        ) -> ::fuzztest::reexports::anyhow::Result<()> {
+          #( self.#field_names.mutate(&mut val.#field_names, rng, only_shrink)?; )*
+          Ok(())
+        }
+
+        fn get_user_value<'a>(&self, corpus_value: &'a Self::CorpusValue) -> ::fuzztest::reexports::anyhow::Result<Self::UserValue<'a>> {
+          Ok(#domain_struct_name {
+            #(#field_names: self.#field_names.get_user_value(&corpus_value.#field_names)?),*
+          })
+        }
+      }
+    };
+    (domain_definition_tokens, field_names)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use googletest::prelude::*;
+
+    #[googletest::test]
+    fn test_generate_fuzztest_domain() {
+        let (domain_definition_tokens, field_names) = generate_fuzztest_domain(
+            &quote::format_ident!("__FuzzTestTestFuzzStateWrapper"),
+            &[
+                TestFnArgument {
+                    name: &quote::format_ident!("a"),
+                    ty: parse_quote!(&'user Vec<&'user i32>),
+                },
+                TestFnArgument {
+                    name: &quote::format_ident!("b"),
+                    ty: parse_quote!(std::string::String),
+                },
+            ],
+            &parse_quote!('user),
+        );
+        expect_that!(
+            field_names,
+            elements_are![eq(&quote::format_ident!("a")), eq(&quote::format_ident!("b"))]
+        );
+        expect_that!(domain_definition_tokens.to_string(), eq( &quote! {
+            #[derive(::fuzztest::serde::Serialize, ::fuzztest::serde::Deserialize, ::std::fmt::Debug, ::std::clone::Clone)]
+            #[serde(crate = ":: fuzztest :: serde")]
+            struct __FuzzTestTestFuzzStateWrapper<T0, T1> {
+              a: T0,
+              b: T1
+            }
+
+            impl<T0, T1> ::fuzztest::domains::Domain for __FuzzTestTestFuzzStateWrapper<T0, T1>
+            where for < 'user > T0: ::fuzztest::domains::Domain<UserValue< 'user > = &'user Vec<&'user i32> >,
+                  T0::CorpusValue: ::fuzztest::serde::Serialize + ::fuzztest::serde::de::DeserializeOwned + ::std::clone::Clone,
+                  for < 'user > T1: ::fuzztest::domains::Domain<UserValue< 'user > = std::string::String>,
+                  T1::CorpusValue: ::fuzztest::serde::Serialize + ::fuzztest::serde::de::DeserializeOwned + ::std::clone::Clone {
+              type UserValue<'user> = __FuzzTestTestFuzzStateWrapper<T0::UserValue<'user>, T1::UserValue<'user> >;
+              type CorpusValue = __FuzzTestTestFuzzStateWrapper<T0::CorpusValue, T1::CorpusValue>;
+
+              fn init(&self, rng: &mut dyn ::fuzztest::reexports::rand::Rng) -> ::fuzztest::reexports::anyhow::Result<Self::CorpusValue> {
+                Ok(__FuzzTestTestFuzzStateWrapper {
+                  a: self.a.init(rng)?,
+                  b: self.b.init(rng)?
+                })
+              }
+
+              fn mutate(
+                  &self,
+                  val: &mut Self::CorpusValue,
+                  rng: &mut dyn ::fuzztest::reexports::rand::Rng,
+                  only_shrink: bool,
+              ) -> ::fuzztest::reexports::anyhow::Result<()> {
+                self.a.mutate(&mut val.a, rng, only_shrink)?;
+                self.b.mutate(&mut val.b, rng, only_shrink)?;
+                Ok(())
+              }
+
+              fn get_user_value<'a>(&self, corpus_value: &'a Self::CorpusValue) -> ::fuzztest::reexports::anyhow::Result<Self::UserValue<'a>> {
+                Ok(__FuzzTestTestFuzzStateWrapper {
+                  a: self.a.get_user_value(&corpus_value.a)?,
+                  b: self.b.get_user_value(&corpus_value.b)?
+                })
+              }
+            }
+          }
+          .to_string())
+      );
+    }
+}
diff --git a/rust/fuzztest_macro/src/helpers/test_registration.rs b/rust/fuzztest_macro/src/helpers/test_registration.rs
new file mode 100644
index 0000000..d44614c
--- /dev/null
+++ b/rust/fuzztest_macro/src/helpers/test_registration.rs
@@ -0,0 +1,378 @@
+use convert_case::Case;
+use convert_case::Casing;
+use proc_macro2::Span;
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::parse_quote;
+use syn::Expr;
+use syn::Generics;
+use syn::Ident;
+use syn::Lifetime;
+use syn::Signature;
+
+use super::fn_item_utils::extract_test_function_arguments;
+use super::fn_item_utils::fn_sig_to_bare_type;
+use super::fuzztest_domain::generate_fuzztest_domain;
+use super::import_fuzztest_crate;
+use super::TestFnArgument;
+
+const USER_VALUE_LIFETIME_GENERIC_NAME: &str = "'user";
+
+#[derive(Clone, Debug)]
+pub struct FuzzTestObjectDefinitionAndFactory {
+    pub fuzztest_object_factory_name: Ident,
+    pub fuzztest_object_tokenstream: TokenStream,
+}
+
+#[derive(Clone, Debug)]
+pub struct GTestDefinition {
+    pub gtest_tokens: TokenStream,
+}
+
+/// A context struct holding the pre-computed information and token streams required
+/// to generate the fuzz test registration, struct definitions, and integration with the test framework.
+///
+/// This context is created once per property function.
+pub struct FuzzTestRegistrationCtx<'a> {
+    test_fn_signature: &'a Signature,
+    test_fn_ident: &'a Ident,
+    prop_fn_ident: Ident,
+    test_fn_args: Vec<TestFnArgument<'a>>,
+    crate_name: TokenStream,
+    fuzz_test_domain_definition: TokenStream,
+    fuzz_test_domain_field_names: Vec<Ident>,
+    fuzz_test_struct_instance_tokens: TokenStream,
+    fuzz_test_struct_factory_fn_name: Ident,
+}
+
+impl<'a> FuzzTestRegistrationCtx<'a> {
+    /// Creates a new registration context from a property function signature and its domain constructors.
+    ///
+    /// This method analyzes the inputs, derives necessary identifiers and lifetime generics, and
+    /// pre-computes the tokenstream for the fuzz test struct instance.
+    pub fn new(
+        test_fn_signature: &'a Signature,
+        domain_ctors: impl IntoIterator<Item = &'a Expr>,
+    ) -> syn::Result<Self> {
+        let user_value_lifetime_generic =
+            Lifetime::new(USER_VALUE_LIFETIME_GENERIC_NAME, Span::call_site());
+
+        let test_fn_args =
+            extract_test_function_arguments(test_fn_signature, &user_value_lifetime_generic)?;
+        if test_fn_args.is_empty() {
+            return Err(syn::Error::new_spanned(
+                test_fn_signature,
+                "fuzztest requires at least one argument for the property function",
+            ));
+        }
+
+        let test_fn_generics = &test_fn_signature.generics;
+        if test_fn_generics.type_params().count() > 0 || test_fn_generics.const_params().count() > 0
+        {
+            return Err(syn::Error::new_spanned(
+                test_fn_generics,
+                "Property function should not have any Type or Const generics",
+            ));
+        }
+
+        let test_fn_ident = &test_fn_signature.ident;
+        let prop_fn_ident = quote::format_ident!("__property_fn__{}", test_fn_ident);
+        let domain_ctors = domain_ctors.into_iter().collect::<Vec<_>>();
+        let fuzz_test_struct_name =
+            quote::format_ident!("__FuzzTest{}", test_fn_ident.to_string().to_case(Case::Pascal));
+        let crate_name = import_fuzztest_crate();
+
+        let domain_struct_name = quote::format_ident!(
+            "__FuzzTest{}StateWrapper",
+            test_fn_ident.to_string().to_case(Case::Pascal)
+        );
+
+        let (fuzz_test_domain_definition, fuzz_test_domain_field_names) = generate_fuzztest_domain(
+            &domain_struct_name,
+            &test_fn_args,
+            &user_value_lifetime_generic,
+        );
+
+        let fuzz_test_struct_instance_tokens = quote!(
+          #fuzz_test_struct_name {
+              domain: #domain_struct_name {
+                  #(#fuzz_test_domain_field_names: #domain_ctors),*
+              },
+              test_fn: #prop_fn_ident
+          }
+        );
+
+        let fuzz_test_struct_factory_fn_name =
+            quote::format_ident!("{}_factory", fuzz_test_struct_name);
+
+        Ok(Self {
+            test_fn_signature,
+            test_fn_ident,
+            prop_fn_ident,
+            test_fn_args,
+            crate_name,
+            fuzz_test_domain_definition,
+            fuzz_test_domain_field_names,
+            fuzz_test_struct_instance_tokens,
+            fuzz_test_struct_factory_fn_name,
+        })
+    }
+
+    pub fn prop_fn_ident(&self) -> &Ident {
+        &self.prop_fn_ident
+    }
+
+    /// Generates a GoogleTest-compatible test function that wraps the fuzz test.
+    ///
+    /// This allows the fuzz test to be executed as a regular tests.
+    pub fn generate_gtest(&self) -> syn::Result<GTestDefinition> {
+        let gtest_name = &self.test_fn_ident;
+        let crate_name = &self.crate_name;
+        let test_fn_name = self.test_fn_ident.to_string();
+        let fuzz_test_factory = &self.fuzz_test_struct_factory_fn_name;
+
+        let tokens = quote!(
+            #[::googletest::prelude::gtest]
+            fn #gtest_name() {
+                // Note: In this setup, `module_path!()` returns a path that starts with the crate name
+                // (e.g., `crate_name::module::path`). We want to strip this prefix to make the
+                // test name similar to standard `libtest` paths.
+                let full_test_name = concat!(module_path!(), "::", #test_fn_name);
+                let test_name = full_test_name
+                    .split_once("::")
+                    .expect("Crate name is always the first path segment")
+                    .1;
+                let manager = #crate_name::worker::RustFuzzTestAdapterManager {
+                    test_name,
+                    fuzz_test_factory: #fuzz_test_factory,
+                };
+                #crate_name::worker::process(manager);
+            }
+        );
+
+        Ok(GTestDefinition { gtest_tokens: tokens })
+    }
+
+    /// Generates the test `struct` that will contain the state of the test:
+    ///  - the domain of the property function inputs
+    ///  - the property function itself
+    ///
+    /// The generated test `struct` object will implement the FuzzTest trait.
+    pub fn generate_test_registration(&self) -> syn::Result<FuzzTestObjectDefinitionAndFactory> {
+        let crate_name = &self.crate_name;
+        let user_value_lifetime_generic =
+            Lifetime::new(USER_VALUE_LIFETIME_GENERIC_NAME, Span::call_site());
+        let test_fn_args = &self.test_fn_args;
+        let test_fn_args_len = test_fn_args.len();
+
+        let test_fn_type = fn_sig_to_bare_type(self.test_fn_signature)?;
+
+        let test_fn_ident = self.test_fn_ident;
+        let fuzz_test_struct_name =
+            quote::format_ident!("__FuzzTest{}", test_fn_ident.to_string().to_case(Case::Pascal));
+        let domain_struct_name = quote::format_ident!(
+            "__FuzzTest{}StateWrapper",
+            test_fn_ident.to_string().to_case(Case::Pascal)
+        );
+        let test_fn_name = test_fn_ident.to_string();
+        let fuzz_test_struct_factory_fn_name = &self.fuzz_test_struct_factory_fn_name;
+
+        let domain_generics =
+            (0..test_fn_args_len).map(|idx| quote::format_ident!("T{idx}")).collect::<Vec<_>>();
+        let corpus_generics = domain_generics
+            .iter()
+            .map(|generic| parse_quote!(#generic::CorpusValue))
+            .collect::<Vec<syn::Type>>();
+
+        let mut generics: Generics = parse_quote! { < #(#domain_generics),* > };
+
+        let test_fn_args_with_generics = test_fn_args
+            .iter()
+            .zip(generics.type_params().cloned())
+            .zip(corpus_generics.iter())
+            .collect::<Vec<_>>();
+
+        let where_clauses = generics.make_where_clause();
+        for ((TestFnArgument { ty, .. }, syn::TypeParam { ident: domain_gen, .. }), corpus_gen) in
+            &test_fn_args_with_generics
+        {
+            where_clauses.predicates.push(
+          parse_quote! {
+            for <#user_value_lifetime_generic> #domain_gen: #crate_name::domains::Domain<UserValue<#user_value_lifetime_generic> = #ty >
+        });
+            where_clauses.predicates.push(parse_quote! { #corpus_gen: 'static });
+        }
+
+        let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+
+        let fuzztest_instance = &self.fuzz_test_struct_instance_tokens;
+        let fuzz_test_domain_field_names = &self.fuzz_test_domain_field_names;
+        let fuzz_test_domain_definition = &self.fuzz_test_domain_definition;
+
+        let fuzztest_struct_definition_and_factory = quote! {
+          #fuzz_test_domain_definition
+
+          struct #fuzz_test_struct_name #generics {
+            domain: #domain_struct_name #generics,
+            test_fn: #test_fn_type
+          }
+
+          impl #impl_generics #crate_name::internal::FuzzTest for #fuzz_test_struct_name #ty_generics #where_clause {
+              fn name(&self) -> &'static str {
+                  #test_fn_name
+              }
+              fn activate(&mut self) {
+                  todo!("Not implemented!")
+              }
+              fn mutate(&mut self) {
+                  todo!("Not implemented!")
+              }
+              fn execute<'a>(&self, args: &'a #crate_name::domains::GenericCorpusValue) -> bool {
+                  use #crate_name::domains::Domain;
+
+                  let wrapper = args
+                                .downcast_ref::<#domain_struct_name<#(#corpus_generics),*>>()
+                                .expect("Attempt to recover user value before testing failed.");
+
+                  let user_value = self.domain.get_user_value(wrapper).expect("Failed to get user value from corpus value");
+
+                  let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (self.test_fn)(#(user_value.#fuzz_test_domain_field_names),* ) ));
+
+                  result.is_ok()
+              }
+              fn print_finding_report(&self) {
+                  todo!("Not implemented!")
+              }
+              fn domains(&self) -> &dyn #crate_name::domains::GenericDomain {
+                &self.domain
+              }
+          }
+
+          fn #fuzz_test_struct_factory_fn_name() -> #crate_name::internal::BoxedFuzzTest {
+            ::std::boxed::Box::new(#fuzztest_instance)
+          }
+        };
+
+        Ok(FuzzTestObjectDefinitionAndFactory {
+            fuzztest_object_factory_name: fuzz_test_struct_factory_fn_name.clone(),
+            fuzztest_object_tokenstream: fuzztest_struct_definition_and_factory,
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use googletest::matchers;
+    use googletest::prelude::*;
+
+    #[googletest::test]
+    fn test_generate_test_registration() {
+        let sig: Signature = parse_quote! { fn test_fuzz(a: i32, b: std::string::String) };
+        let expr1: Expr =
+            parse_quote! { ::fuzztest::domains::arbitrary::Arbitrary::<i32>::default() };
+        let expr2: Expr =
+            parse_quote! { ::fuzztest::domains::arbitrary::Arbitrary::<String>::default() };
+        let context = FuzzTestRegistrationCtx::new(&sig, [&expr1, &expr2]).unwrap();
+        let FuzzTestObjectDefinitionAndFactory {
+            fuzztest_object_factory_name,
+            fuzztest_object_tokenstream,
+        } = context.generate_test_registration().unwrap();
+        expect_that!(
+            fuzztest_object_factory_name,
+            eq(&quote::format_ident!("__FuzzTestTestFuzz_factory"))
+        );
+        expect_that!(
+          fuzztest_object_tokenstream.to_string(), ends_with( quote! {
+              struct __FuzzTestTestFuzz<T0, T1> {
+                domain: __FuzzTestTestFuzzStateWrapper<T0, T1>,
+                test_fn: fn(i32, std::string::String)
+              }
+
+              impl<T0, T1> ::fuzztest::internal::FuzzTest for __FuzzTestTestFuzz<T0, T1>
+              where for <'user> T0: ::fuzztest::domains::Domain<UserValue<'user> = i32>,
+                    T0::CorpusValue: 'static,
+                    for <'user> T1: ::fuzztest::domains::Domain<UserValue<'user> = std::string::String>,
+                    T1::CorpusValue: 'static {
+                  fn name(&self) -> &'static str {
+                    "test_fuzz"
+                  }
+                  fn activate(&mut self) {
+                    todo!("Not implemented!")
+                  }
+                  fn mutate(&mut self) {
+                    todo!("Not implemented!")
+                  }
+                  fn execute<'a>(&self, args: &'a ::fuzztest::domains::GenericCorpusValue) -> bool {
+                    use ::fuzztest::domains::Domain;
+
+                    let wrapper = args
+                            .downcast_ref::<__FuzzTestTestFuzzStateWrapper<T0::CorpusValue, T1::CorpusValue>>()
+                            .expect("Attempt to recover user value before testing failed.");
+
+                    let user_value = self.domain.get_user_value(wrapper).expect("Failed to get user value from corpus value");
+                    // Safety: Data is not reused after the test.
+                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (self.test_fn)(user_value.a, user_value.b) ));
+
+                    result.is_ok()
+                  }
+                  fn print_finding_report(&self) {
+                    todo!("Not implemented!")
+                  }
+                  fn domains(&self) -> &dyn ::fuzztest::domains::GenericDomain {
+                    &self.domain
+                  }
+              }
+
+              fn __FuzzTestTestFuzz_factory() -> ::fuzztest::internal::BoxedFuzzTest {
+                ::std::boxed::Box::new(__FuzzTestTestFuzz {
+                  domain: __FuzzTestTestFuzzStateWrapper {
+                    a: ::fuzztest::domains::arbitrary::Arbitrary::<i32>::default(),
+                    b: ::fuzztest::domains::arbitrary::Arbitrary::<String>::default()
+                  },
+                  test_fn: __property_fn__test_fuzz
+                })
+              }
+            }
+            .to_string())
+        );
+    }
+
+    #[googletest::test]
+    fn test_generate_gtest() {
+        let sig: Signature = parse_quote! { fn test_fuzz(a: i32, b: std::string::String) };
+        let expr1: Expr =
+            parse_quote! { ::fuzztest::domains::arbitrary::Arbitrary::<i32>::default() };
+        let expr2: Expr =
+            parse_quote! { ::fuzztest::domains::arbitrary::Arbitrary::<String>::default() };
+        let context = FuzzTestRegistrationCtx::new(&sig, [&expr1, &expr2]).unwrap();
+        let GTestDefinition { gtest_tokens } = context.generate_gtest().unwrap();
+
+        // test that the gtest was generated
+        expect_that!(
+            gtest_tokens.to_string(),
+            matchers::contains_substring(
+                quote! {
+                    #[::googletest::prelude::gtest]
+                    fn test_fuzz()
+                }
+                .to_string()
+            )
+        );
+
+        // test that the test_name is the test path
+        expect_that!(
+            gtest_tokens.to_string(),
+            matchers::contains_substring(
+                quote! {
+                    let full_test_name = concat!(module_path!(), "::", "test_fuzz");
+                    let test_name = full_test_name
+                        .split_once("::")
+                        .expect("Crate name is always the first path segment")
+                        .1;
+                }
+                .to_string()
+            )
+        )
+    }
+}
diff --git a/rust/fuzztest_macro/src/lib.rs b/rust/fuzztest_macro/src/lib.rs
new file mode 100644
index 0000000..19450ea
--- /dev/null
+++ b/rust/fuzztest_macro/src/lib.rs
@@ -0,0 +1,126 @@
+/// Modules here cannot be public (ie: `pub`) because a proc-macro crate cannot export anything else
+/// other than a proc-macro. That is to say, a function tagged with `#[proc_macro]`,
+/// `#[proc_macro_attribute]`, `#[proc_macro_derive]`
+mod helpers;
+
+use proc_macro::TokenStream;
+
+use quote::quote;
+use syn::parse::{Parse, ParseStream, Parser};
+use syn::{parse_macro_input, Item};
+
+use helpers::test_registration::FuzzTestObjectDefinitionAndFactory;
+use helpers::test_registration::FuzzTestRegistrationCtx;
+use helpers::test_registration::GTestDefinition;
+
+#[derive(Debug)]
+struct FuzzTestArg {
+    _ident: syn::Ident,
+    _eq_token: syn::Token![=],
+    domain_ty: syn::Expr,
+}
+
+impl Parse for FuzzTestArg {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        Ok(Self { _ident: input.parse()?, _eq_token: input.parse()?, domain_ty: input.parse()? })
+    }
+}
+
+/// The `fuzztest` macro is used to generate and register a fuzz test object to be used by a fuzz
+/// testing engine.
+///
+/// The macro takes a list of arguments, where each argument is a domain object used to generate
+/// values for the property function inputs.
+///
+/// Example usage:
+/// ```markdown
+/// ```
+/// # use fuzztest::fuzztest;
+/// # use fuzztest::domains::arbitrary::Arbitrary;
+/// # use fuzztest::domains::range::InRange;
+///
+/// #[fuzztest(a = Arbitrary::<i32>::default(), b = InRange::new(0, 10))]
+/// fn fuzz_test_function(a: i32, b: i32) {
+///     assert_eq!(a, b);
+/// }
+/// ```
+/// ```
+///
+#[proc_macro_attribute]
+pub fn fuzztest(args: TokenStream, input: TokenStream) -> TokenStream {
+    let item = parse_macro_input!(input as Item);
+
+    let syn::Item::Fn(mut test_fn) = item else {
+        return syn::Error::new_spanned(item, "not a function").into_compile_error().into();
+    };
+
+    let original_ident = test_fn.sig.ident.clone();
+    let fuzztest_name = original_ident.to_string();
+
+    // TODO(mathuxny-73): Support the case where there are no arguments. Probably we should return an error
+    //  in this case as it may not make sense to have a FuzzTest without input.
+    let args =
+        syn::punctuated::Punctuated::<FuzzTestArg, syn::Token![,]>::parse_terminated.parse(args);
+
+    let args = match args {
+        Ok(args) => args,
+        Err(e) => return e.into_compile_error().into(),
+    };
+
+    let domain_ctors = args.iter().map(|arg| &arg.domain_ty);
+
+    let fuzztest_registration_context =
+        match FuzzTestRegistrationCtx::new(&test_fn.sig, domain_ctors) {
+            Ok(fuzztest_registration_context) => fuzztest_registration_context,
+            Err(e) => return e.into_compile_error().into(),
+        };
+
+    let FuzzTestObjectDefinitionAndFactory {
+        fuzztest_object_factory_name,
+        fuzztest_object_tokenstream,
+    } = match fuzztest_registration_context.generate_test_registration() {
+        Ok(fuzztest_object) => fuzztest_object,
+        Err(e) => return e.into_compile_error().into(),
+    };
+
+    let GTestDefinition { gtest_tokens } = match fuzztest_registration_context.generate_gtest() {
+        Ok(gtest_tokens) => gtest_tokens,
+        Err(e) => return e.into_compile_error().into(),
+    };
+
+    test_fn.sig.ident = fuzztest_registration_context.prop_fn_ident().clone();
+
+    let fuzztest_mod_name = quote::format_ident!("__fuzztest_mod__{}", original_ident);
+
+    // TODO(mathuxny-73): Extract the current module path from the invocation of the macro
+    //   (ie: from `item.attrs`)
+    let tokens = quote! {
+        #[allow(non_snake_case)]
+        mod #fuzztest_mod_name {
+            use super::*;
+
+            #test_fn
+
+            #fuzztest_object_tokenstream
+
+            const _: () = {
+                static FUZZTEST_INFO: ::fuzztest::internal::FuzzTestInfo =
+                ::fuzztest::internal::FuzzTestInfo {
+                    name: #fuzztest_name,
+                    file: file!(),
+                    line: line!(),
+                };
+
+                ::fuzztest::reexports::inventory::submit! {
+                ::fuzztest::internal::FuzzTestRegistration {
+                    info: &FUZZTEST_INFO,
+                    factory: #fuzztest_object_factory_name
+                }
+                }
+            };
+
+            #gtest_tokens
+        }
+    };
+    tokens.into()
+}
diff --git a/rust/fuzztest_macro/src/test_main.rs b/rust/fuzztest_macro/src/test_main.rs
new file mode 100644
index 0000000..f01ede3
--- /dev/null
+++ b/rust/fuzztest_macro/src/test_main.rs
@@ -0,0 +1,4 @@
+// This file exists only as a workaround. It serves as the crate root for the `fuzztest_macro_test`
+// BUILD target.
+// This file should not be included in the `fuzztest_macro` crate.
+mod helpers;
diff --git a/rust/options/BUILD b/rust/options/BUILD
new file mode 100644
index 0000000..b85746a
--- /dev/null
+++ b/rust/options/BUILD
@@ -0,0 +1,30 @@
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+
+licenses(["notice"])
+
+exports_files(["BUILD"])
+
+rust_library(
+    name = "fuzztest_options",
+    srcs = glob([
+        "src/**/*.rs",
+    ]),
+    edition = "2024",
+    visibility = ["@com_google_fuzztest//rust:__subpackages__"],
+    deps = [
+        "@crate_index//:anyhow",  # v1
+        "@crate_index//:clap",  # v4
+        "@crate_index//:humantime",  # v2
+    ],
+)
+
+rust_test(
+    name = "fuzztest_options_test",
+    # Avoid interference when setting/resetting environment variables in tests.
+    args = ["--test-threads=1"],
+    crate = ":fuzztest_options",
+    edition = "2024",
+    deps = [
+        "@crate_index//:googletest",
+    ],
+)
diff --git a/rust/options/Cargo.toml b/rust/options/Cargo.toml
new file mode 100644
index 0000000..f1adde6
--- /dev/null
+++ b/rust/options/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "fuzztest-options"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+clap = { version = "4.6.1", features = ["derive", "env"] }
+humantime = "2.4.0"
+
+[dev-dependencies]
+googletest = "0.14.3"
diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs
new file mode 100644
index 0000000..05b9a32
--- /dev/null
+++ b/rust/options/src/lib.rs
@@ -0,0 +1,224 @@
+// This module provides the core command-line flag and environment variable options
+// structure (`FuzzTestOptions`) and domain execution modes (`ExecutionMode`).
+// Both the test harness runtime and host tools (e.g. `cargo-fuzztest`) can share option parsing
+// logic.
+
+use clap::{Parser, ValueEnum};
+use humantime::Duration;
+
+/// Time budget calculation type for replay corpus mode.
+#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum TimeBudgetType {
+    #[default]
+    PerTest,
+    Total,
+}
+
+/// Parses a fuzzing duration string from `FUZZTEST_FUZZ_FOR`.
+///
+/// Matches `"inf"` or `"infinite"` to [`FuzzFor::Indefinitely`]. All other values
+/// are parsed as standard human-readable durations (for example, `"5s"` or `"10m"`).
+fn parse_fuzz_for(s: &str) -> anyhow::Result<FuzzFor> {
+    let s_lower = s.trim().to_lowercase();
+    if s_lower == "inf" || s_lower == "infinite" {
+        Ok(FuzzFor::Indefinitely)
+    } else {
+        let duration = s.parse()?;
+        Ok(FuzzFor::Duration(duration))
+    }
+}
+
+/// Command-line and environment variable options parsed for the FuzzTest harness.
+#[derive(Parser, Debug, Clone, Default)]
+pub struct FuzzTestOptions {
+    /// The working root directory.
+    #[arg(env = "FUZZTEST_WORKDIR_ROOT", long)]
+    pub workdir_root: Option<String>,
+
+    /// The duration for which each test should be fuzzed.
+    #[arg(env = "FUZZTEST_FUZZ_FOR", long, value_parser = parse_fuzz_for)]
+    pub fuzz_for: Option<FuzzFor>,
+
+    /// If true, subprocess logs are printed after every batch. Note that crash logs are always printed
+    /// regardless of this flag's value.
+    #[arg(env = "FUZZTEST_PRINT_SUBPROCESS_LOG", long)]
+    pub print_subprocess_log: bool,
+
+    /// Number of parallel jobs to run.
+    #[arg(env = "FUZZTEST_JOBS", long)]
+    pub jobs: Option<usize>,
+
+    /// The crash ID to be replayed from the corpus database.
+    ///
+    /// If set, `corpus_db` must also be specified. This mode retrieves the crashing input
+    /// associated with the given ID from the database and executes the property function.
+    #[arg(env = "FUZZTEST_REPLAY_ID", long, requires = "corpus_db")]
+    pub replay_id: Option<String>,
+
+    /// Replay all crashing inputs from the corpus database.
+    #[arg(env = "FUZZTEST_REPLAY_FINDINGS", long)]
+    pub replay_findings: bool,
+
+    /// Replay the corpus for a specified duration.
+    #[arg(env = "FUZZTEST_REPLAY_CORPUS_FOR", long)]
+    pub replay_corpus_for: Option<Duration>,
+
+    /// Time budget calculation type for replay corpus mode.
+    #[arg(env = "FUZZTEST_TIME_BUDGET_TYPE", long, value_enum, default_value_t = TimeBudgetType::PerTest)]
+    pub time_budget_type: TimeBudgetType,
+
+    /// The path to the corpus database.
+    ///
+    /// If set to non-empty, updates/queries the corpus database that contains coverage,
+    /// regression, and crashing inputs for each test binary and fuzz test.
+    #[arg(env = "FUZZTEST_CORPUS_DB", long)]
+    pub corpus_db: Option<String>,
+}
+
+impl FuzzTestOptions {
+    /// Evaluates the raw options and maps them to a strongly-typed domain `ExecutionMode`.
+    ///
+    /// This decouples raw environment/CLI option parsing from execution mode validation.
+    pub fn execution_mode(&self) -> ExecutionMode {
+        if let Some(replay_corpus_for) = self.replay_corpus_for {
+            return ExecutionMode::ReplayCorpus(ReplayCorpusOptions {
+                replay_corpus_for,
+                time_budget_type: self.time_budget_type,
+            });
+        }
+
+        if self.replay_findings {
+            return ExecutionMode::ReplayAllCrashes;
+        }
+
+        if let Some(replay_id) = &self.replay_id {
+            return ExecutionMode::ReplayCrash(ReplayCrashOptions { replay_id: replay_id.clone() });
+        }
+
+        if let Some(fuzz_for) = self.fuzz_for {
+            return ExecutionMode::Fuzz(FuzzOptions { fuzz_for, jobs: self.jobs.clone() });
+        }
+
+        ExecutionMode::SmokeTest
+    }
+}
+
+/// Strongly-typed domain execution mode for test runs.
+///
+/// This enum represents the parsed high-level user intent derived from `FuzzTestOptions`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ExecutionMode {
+    /// Smoke test execution (regular unit test fallback).
+    SmokeTest,
+
+    /// Fuzzing mode.
+    Fuzz(FuzzOptions),
+
+    /// Replay a specific crash ID from the corpus database.
+    ReplayCrash(ReplayCrashOptions),
+
+    /// Replay all crashing inputs stored in the corpus database.
+    ReplayAllCrashes,
+
+    /// Replay corpus inputs for a specified duration.
+    ReplayCorpus(ReplayCorpusOptions),
+}
+
+/// Mode-specific options for continuous fuzzing.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct FuzzOptions {
+    pub fuzz_for: FuzzFor,
+
+    /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it will
+    /// use its own default value.
+    pub jobs: Option<usize>,
+}
+
+/// The duration or limit for fuzzing.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FuzzFor {
+    /// Fuzz indefinitely until it is manually stopped or a crash is found.
+    Indefinitely,
+
+    /// Fuzz for a specific duration.
+    Duration(Duration),
+}
+
+/// Mode-specific options for replaying a specific database crash.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ReplayCrashOptions {
+    pub replay_id: String,
+}
+
+/// Mode-specific options for replaying corpus for a duration.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ReplayCorpusOptions {
+    pub replay_corpus_for: Duration,
+    pub time_budget_type: TimeBudgetType,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use googletest::prelude::*;
+    use std::ffi::OsString;
+
+    #[gtest]
+    fn test_fuzz_options_parsing_env() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_FUZZ_FOR", "5s");
+        }
+
+        let options = FuzzTestOptions::parse_from(std::iter::empty::<OsString>());
+
+        expect_true!(options.fuzz_for.is_some());
+        expect_that!(options.execution_mode(), matches_pattern!(ExecutionMode::Fuzz(_)));
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_FUZZ_FOR");
+        }
+    }
+
+    #[gtest]
+    fn test_replay_id_requires_corpus_db() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_ID", "my_crash_123");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_REPLAY_ID");
+        }
+
+        let err = result.expect_err("parsing should fail when corpus_db is missing");
+        expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument));
+    }
+
+    #[gtest]
+    fn test_replay_id_with_corpus_db_succeeds() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_ID", "my_crash_123");
+            std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
+        }
+
+        let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_REPLAY_ID");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        let options =
+            result.expect("parsing should succeed when both replay_id and corpus_db are present");
+        expect_that!(options.replay_id.as_deref(), eq(Some("my_crash_123")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+    }
+}
diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml
new file mode 100644
index 0000000..0c3034f
--- /dev/null
+++ b/rust/rust-toolchain.toml
@@ -0,0 +1,9 @@
+# This file is used to configure the Rust toolchain for FuzzTest.
+#
+# The channel is set to nightly as the fuzztest crate makes use of the compiler
+# sanitizers, which are not yet part of the stable Rust toolchain.
+# See https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html
+#
+# See https://rust-lang.github.io/rustup/concepts/toolchains.html
+[toolchain]
+channel = "nightly"
diff --git a/rust/src/crash_handler.rs b/rust/src/crash_handler.rs
new file mode 100644
index 0000000..5dddf70
--- /dev/null
+++ b/rust/src/crash_handler.rs
@@ -0,0 +1,38 @@
+#[cfg(any(sanitize = "address", sanitize = "memory"))]
+mod callbacks {
+    use std::ffi::{c_char, CStr};
+
+    unsafe extern "C" {
+        pub safe fn __sanitizer_set_death_callback(callback: Option<extern "C" fn()>);
+
+        pub safe fn __asan_get_report_description() -> *const c_char;
+    }
+
+    pub extern "C" fn sanitizer_death_callback() {
+        use crate::worker;
+
+        let (description, signature) = if cfg!(sanitize = "address") {
+            let char_ptr = __asan_get_report_description();
+            // Safety: `ptr` points to a valid null terminated string.
+            let signature_cstr = unsafe { CStr::from_ptr(char_ptr) };
+            (
+                "Property function ran but address sanitizer caught a bug",
+                signature_cstr.to_str().unwrap_or("ASan crash"),
+            )
+        } else {
+            ("Property function ran but a sanitizer caught a bug", "Sanitizer crash")
+        };
+        worker::try_emit_finding(description, signature);
+    }
+}
+
+/// Be able to emit failures before exiting fully from the process for non-unwinding panics and/or
+/// unrecoverable crashes.
+pub fn register_crash_handler() {
+    // TODO(yamilmorales): Consider allowing more sanitizers here, and find some other way to
+    // recognize sanitizers if this feature is not stabilized by the time we need to support Cargo.
+    #[cfg(any(sanitize = "address", sanitize = "memory"))]
+    {
+        callbacks::__sanitizer_set_death_callback(Some(callbacks::sanitizer_death_callback));
+    }
+}
diff --git a/rust/src/domains.rs b/rust/src/domains.rs
new file mode 100644
index 0000000..a35fe6b
--- /dev/null
+++ b/rust/src/domains.rs
@@ -0,0 +1,230 @@
+pub mod arbitrary;
+pub mod range;
+pub mod tuple_of;
+pub mod utility;
+
+use anyhow;
+use anyhow::Context;
+
+use std::any::Any;
+
+pub trait CloneAny: Any {
+    fn clone_box(&self) -> Box<dyn CloneAny>;
+    fn as_any(&self) -> &dyn Any;
+    fn as_mut_any(&mut self) -> &mut dyn Any;
+}
+
+impl dyn CloneAny {
+    /// Return `true` if the underlying type is of type `T`
+    pub fn is<T: Any>(&self) -> bool {
+        self.as_any().is::<T>()
+    }
+
+    /// Returns a reference to the concrete value if it is of type `T`, or `None` otherwise.
+    pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
+        self.as_any().downcast_ref::<T>()
+    }
+
+    /// Returns a mutable reference to the concrete value if it is of type `T`, or `None` otherwise.
+    pub fn downcast_mut<T: Any>(&mut self) -> Option<&mut T> {
+        self.as_mut_any().downcast_mut::<T>()
+    }
+}
+
+impl<T: Any + Clone> CloneAny for T {
+    fn clone_box(&self) -> Box<dyn CloneAny> {
+        // prevent infinite recursion if T is a nested Box type.
+        if let Some(boxed) = self.as_any().downcast_ref::<Box<dyn CloneAny>>() {
+            return (**boxed).clone_box();
+        }
+        Box::new(self.clone())
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn as_mut_any(&mut self) -> &mut dyn Any {
+        self
+    }
+}
+
+impl Clone for Box<dyn CloneAny> {
+    fn clone(&self) -> Self {
+        self.clone_box()
+    }
+}
+
+/// Type alias for a type-erased corpus value.
+pub type GenericCorpusValue = Box<dyn CloneAny>;
+
+/// Type alias for a type-erased user value.
+pub type GenericUserValue = Box<dyn CloneAny>;
+
+/// A trait for types that represent a set of values that an input can take.
+///
+/// Domain types are used to represent the domain of values that a fuzz test input can take
+/// throughout the fuzzing process.
+/// Domain types are used by the fuzzing engine through the APIs defined in this trait.
+///
+/// ## Serialization/Deserialization of CorpusValue and UserValue relationship.
+///
+/// The values drawn from a domain are always of type `CorpusValue`. These values are the
+/// internal representation of the value and they are not directly usable by the fuzz property
+/// function.
+///
+/// The `get_user_value` method is used to retrieve the user value from the corpus value. For
+/// example, if the domain outputs an `&str` then the CorpusValue could be a `String` and the
+/// `get_user_value` method would be used to retrieve the `&str` from the `String`.
+///
+/// The `parse_corpus` (resp. `serialize_corpus`) method is used deserialize (resp. serialize)
+/// the corpus value from (to) a slice of bytes (resp. a vector of bytes).
+///
+/// ### Relationship diagram
+///
+/// The methods below are responsible for transforming between `UserValue`, `CorpusValue`, and the
+/// serialized representation of `CorpusValue`. Here's a quick overview:
+///
+/// ```text
+///        +-- get_user_value() <---+     +-- parse_corpus() <---+
+///        |                        |     |                      |
+///        v                        |     v                      |
+///   UserValue<'a>               CorpusValue                  &[u8]
+///                                       |                      ^
+///                                       |                      |
+///                                       +-> serialize_corpus() +
+/// ```
+pub trait Domain {
+    /// The type of the values that the domain outputs. This should of the same type as the
+    /// parameter of the fuzz property function.
+    type UserValue<'user>;
+    /// The type of the corpus from which the values of type UserValue are drawn.
+    /// For example, if the domain should output an `&str` then the UserValue should be `&str` and
+    /// the CorpusValue could the owned data structured that the `&str` points to (eg: String).
+    /// The CorpusValue type should implement `serde::Serialize`, `serde::de::DeserializeOwned` and
+    /// `Clone`.
+    type CorpusValue: ::serde::Serialize + ::serde::de::DeserializeOwned + Clone;
+
+    /// Initializes a new value drawn from the domain.
+    fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue>;
+
+    /// Mutates the value in `val` to a new value drawn from the domain.
+    ///
+    /// If `only_shrink` is `true`, then the mutation must not increase the size of the corpus
+    /// value. Otherwise, the mutation can both shrink and grow the corpus value.
+    fn mutate(
+        &self,
+        val: &mut Self::CorpusValue,
+        rng: &mut dyn rand::Rng,
+        only_shrink: bool,
+    ) -> anyhow::Result<()>;
+
+    /// Retrieves a UserValue from a given CorpusValue.
+    ///
+    /// This is used to convert the corpus value into the user value that can then be passed to the
+    /// fuzz property function.
+    fn get_user_value<'a>(
+        &self,
+        corpus_value: &'a Self::CorpusValue,
+    ) -> anyhow::Result<Self::UserValue<'a>>;
+
+    /// Turns a slice of bytes into `CorpusValue`.
+    ///
+    /// By default, it uses Postcard to deserialize the data.
+    fn parse_corpus(&self, data: &[u8]) -> anyhow::Result<Self::CorpusValue> {
+        postcard::from_bytes(data).context("Failed to deserialize corpus value from bytes")
+    }
+
+    /// Serializes `corpus_value` to a Vec of bytes (ie, Vec<u8>).
+    ///
+    /// By default, it uses Postcard to serialize the data.
+    fn serialize_corpus(&self, corpus_value: &Self::CorpusValue) -> anyhow::Result<Vec<u8>> {
+        postcard::to_stdvec(corpus_value).context("Failed to serialize corpus value to bytes")
+    }
+}
+
+/// A type-erased interface for Domain types.
+///
+/// This trait is used to expose a common interface for Domain types to the fuzzing engine through
+/// the `FuzzTest` trait.
+/// The methods of this trait are similar to that of the `Domain` trait, but they operate on
+/// a type-erased `GenericCorpusValue` instead of an explicit type.
+pub trait GenericDomain {
+    /// Initializes a new value drawn from the domain.
+    ///
+    /// See `Domain::init` for more details.
+    fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<GenericCorpusValue>;
+
+    /// Mutates the value in `val` to a new value drawn from the domain.
+    ///
+    /// See `Domain::mutate` for more details.
+    fn mutate(
+        &self,
+        val: &mut GenericCorpusValue,
+        rng: &mut dyn rand::Rng,
+        only_shrink: bool,
+    ) -> anyhow::Result<()>;
+
+    /// Parses a slice of bytes into a `GenericCorpusValue`.
+    ///
+    /// See `Domain::parse_corpus` for more details.
+    fn parse_corpus(&self, data: &[u8]) -> anyhow::Result<GenericCorpusValue>;
+
+    /// Serializes a `GenericCorpusValue` to a Vec of bytes (ie, Vec<u8>).
+    ///
+    /// See `Domain::serialize_corpus` for more details.
+    fn serialize_corpus(&self, val: &GenericCorpusValue) -> anyhow::Result<Vec<u8>>;
+}
+
+/// Blanket implementation of the `GenericDomain` trait for types implementing the `Domain` trait.
+///
+/// This implementation is a simple pass-through implementation that uses the methods of the
+/// `Domain` trait to implement the `GenericDomain` trait.
+impl<D> GenericDomain for D
+where
+    D: Domain,
+    D::CorpusValue: 'static,
+{
+    fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<GenericCorpusValue> {
+        Ok(Box::new(self.init(rng)?))
+    }
+
+    /// Retrieves the underlying `Domain`'s `CorpusValue` from the `GenericCorpusValue` and calls
+    /// the underlying `Domain::mutate` method.
+    ///
+    /// If the `GenericCorpusValue` cannot be downcasted to the underlying `Domain`'s `CorpusValue`
+    /// then an Error is returned.
+    ///
+    /// See `GenericDomain::mutate` for more details.
+    fn mutate(
+        &self,
+        val: &mut GenericCorpusValue,
+        rng: &mut dyn rand::Rng,
+        only_shrink: bool,
+    ) -> anyhow::Result<()> {
+        self.mutate(
+            val.downcast_mut().context("Failed to retrieve the Corpus Value")?,
+            rng,
+            only_shrink,
+        )
+    }
+
+    /// Calls the underlying `Domain::parse_corpus` method with the `data` and wraps the resulting
+    /// `CorpusValue` in a `GenericCorpusValue`.
+    ///
+    /// See `GenericDomain::parse_corpus` for more details.
+    fn parse_corpus(&self, data: &[u8]) -> anyhow::Result<GenericCorpusValue> {
+        Ok(Box::new(self.parse_corpus(data)?))
+    }
+
+    /// Retrieves the underlying `Domain`'s `CorpusValue` from the `GenericCorpusValue` and calls
+    /// the underlying `Domain::serialize_corpus` method.
+    ///
+    /// If the `GenericCorpusValue` cannot be downcasted to the underlying `Domain`'s `CorpusValue`
+    /// then an Error is returned.
+    ///
+    /// See `GenericDomain::serialize_corpus` for more details.
+    fn serialize_corpus(&self, val: &GenericCorpusValue) -> anyhow::Result<Vec<u8>> {
+        self.serialize_corpus(val.downcast_ref().context("Failed to retrieve the Corpus Value")?)
+    }
+}
diff --git a/rust/src/domains/arbitrary.rs b/rust/src/domains/arbitrary.rs
new file mode 100644
index 0000000..07d7db5
--- /dev/null
+++ b/rust/src/domains/arbitrary.rs
@@ -0,0 +1,763 @@
+use super::utility::choose_value;
+use super::utility::mutate_integer;
+use super::utility::shrink_towards;
+use super::Domain;
+
+use anyhow;
+use rand::RngExt;
+
+/// Generates arbitrary values of type `T`.
+///
+/// This domain is used to generate values of a specific type without any constraints
+/// beyond the type's intrinsic range. For example, `Arbitrary<i32>` generates `i32`
+/// values spanning the whole `i32` domain `[i32::MIN, i32::MAX]`.
+///
+/// Example usage:
+/// ```
+/// # use fuzztest::domains::Domain;
+/// # use fuzztest::domains::arbitrary::Arbitrary;
+/// # use rand::rngs::SmallRng;
+/// # use rand::SeedableRng;
+///
+/// let arbitrary_i32 = Arbitrary::<i32>::default();
+/// let mut rng = SmallRng::seed_from_u64(73);
+///
+/// let sample = arbitrary_i32.init(&mut rng);
+/// assert!(sample.is_ok());
+/// ```
+
+pub struct Arbitrary<T> {
+    _phantom: std::marker::PhantomData<T>,
+}
+
+// We cannot just use `#[derive(Default)]` because `T` might not be `Default`.
+impl<T> Default for Arbitrary<T> {
+    fn default() -> Self {
+        Self { _phantom: std::marker::PhantomData }
+    }
+}
+
+impl<T> Arbitrary<T> {
+    /// Creates a new `Arbitrary` domain for the given type `T`.
+    pub fn new() -> Self {
+        Self { _phantom: std::marker::PhantomData }
+    }
+}
+
+impl Domain for Arbitrary<bool> {
+    type UserValue<'user> = bool;
+    type CorpusValue = bool;
+
+    fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
+        Ok(rng.random())
+    }
+
+    fn mutate(
+        &self,
+        val: &mut Self::CorpusValue,
+        rng: &mut dyn rand::Rng,
+        only_shrink: bool,
+    ) -> anyhow::Result<()> {
+        if only_shrink {
+            // Convention taken from the C++ fuzztest library.
+            *val = false;
+        } else {
+            *val = rng.random();
+        }
+        Ok(())
+    }
+
+    fn get_user_value<'a>(
+        &self,
+        corpus_value: &'a Self::CorpusValue,
+    ) -> anyhow::Result<Self::UserValue<'a>> {
+        Ok(*corpus_value)
+    }
+}
+
+macro_rules! impl_domain_for_integer {
+    // We need an additional "integer type" for this, because
+    // rand::distr::Distribution<> is not implemented for StandardUniform for isize and usize.
+    // so we perform the generation using integer types and cast to the size types if needed.
+    ($ty:ty, $int_ty:ty) => {
+        impl Domain for Arbitrary<$ty> {
+            type UserValue<'user> = $ty;
+            type CorpusValue = $ty;
+
+            fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
+                // We generate a the equivalent integer type so this works for size types.
+                let val: $int_ty = choose_value(rng);
+                Ok(val as $ty)
+            }
+
+            fn mutate(
+                &self,
+                val: &mut Self::CorpusValue,
+                rng: &mut dyn rand::Rng,
+                only_shrink: bool,
+            ) -> anyhow::Result<()> {
+                if only_shrink {
+                    *val = shrink_towards(rng, *val as $int_ty, 0) as $ty;
+                } else {
+                    let original_val = *val;
+                    loop {
+                        *val = mutate_integer(rng, *val as $int_ty, 5, None, None) as $ty;
+                        if *val != original_val {
+                            break;
+                        }
+                    }
+                }
+                Ok(())
+            }
+
+            fn get_user_value<'a>(
+                &self,
+                corpus_value: &'a Self::CorpusValue,
+            ) -> anyhow::Result<Self::UserValue<'a>> {
+                Ok(*corpus_value)
+            }
+        }
+    };
+}
+
+impl_domain_for_integer!(i8, i8);
+impl_domain_for_integer!(u8, u8);
+impl_domain_for_integer!(i16, i16);
+impl_domain_for_integer!(u16, u16);
+impl_domain_for_integer!(i32, i32);
+impl_domain_for_integer!(u32, u32);
+impl_domain_for_integer!(i64, i64);
+impl_domain_for_integer!(u64, u64);
+impl_domain_for_integer!(i128, i128);
+impl_domain_for_integer!(u128, u128);
+impl_domain_for_integer!(isize, i64);
+impl_domain_for_integer!(usize, u64);
+
+macro_rules! impl_domain_for_float {
+    ($ty:ty) => {
+        impl Domain for Arbitrary<$ty> {
+            type UserValue<'user> = $ty;
+            type CorpusValue = $ty;
+
+            fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
+                Ok(choose_value(rng))
+            }
+
+            fn mutate(
+                &self,
+                val: &mut Self::CorpusValue,
+                rng: &mut dyn rand::Rng,
+                only_shrink: bool,
+            ) -> anyhow::Result<()> {
+                if only_shrink {
+                    // Shrink only finite, non-terminal values.
+                    if val.is_finite() && *val != 0.0 {
+                        *val = shrink_towards(rng, *val, 0.0);
+                    }
+                    return Ok(());
+                }
+                // Non-shrinking mutation.
+                let original_val = *val;
+                loop {
+                    // Loop to ensure mutation
+                    if !val.is_finite() {
+                        // NaN or Infinity
+                        *val = self.init(rng)?;
+                    } else {
+                        match rng.random_range(0..4) {
+                            0 => *val /= 2.0,
+                            1 => *val = -*val,
+                            2 => *val += 1.0,
+                            3 => *val *= 3.0,
+                            _ => unreachable!(),
+                        }
+                    }
+
+                    if (val.is_nan() && original_val.is_nan()) || *val == original_val {
+                        continue;
+                    }
+                    break;
+                }
+                Ok(())
+            }
+
+            fn get_user_value<'a>(
+                &self,
+                corpus_value: &'a Self::CorpusValue,
+            ) -> anyhow::Result<Self::UserValue<'a>> {
+                Ok(*corpus_value)
+            }
+        }
+    };
+}
+
+impl_domain_for_float!(f32);
+impl_domain_for_float!(f64);
+
+const SURROGATE_START: u32 = 0xD800;
+const SURROGATE_END: u32 = 0xDFFF;
+const MAX_VALID_CODEPOINT: u32 = 0x10FFFF;
+const NUM_VALID_CODEPOINTS: u32 = MAX_VALID_CODEPOINT - (SURROGATE_END - SURROGATE_START);
+
+/// Maps a `char` to a `u32` in a contiguous range.
+///
+/// Rust's `char` type represents a Unicode Scalar Value, which means it cannot be a
+/// surrogate code point (U+D800 to U+DFFF). This function maps `char` values to a
+/// contiguous integer range by shifting down the code points that are above the
+/// surrogate range. This is useful for mutation strategies like random walks over
+/// a dense integer space.
+fn map_char_to_int(c: char) -> u32 {
+    let val = c as u32;
+    if val >= SURROGATE_START {
+        val - (SURROGATE_END - SURROGATE_START + 1)
+    } else {
+        val
+    }
+}
+
+/// Maps a `u32` from the contiguous range back to a `char`.
+///
+/// This is the inverse of `map_char_to_int`. It converts an integer from the
+/// dense range back to a `char`. Integers that fall within the range previously
+/// occupied by surrogates are shifted up to their correct code points.
+fn map_int_to_char(u: u32) -> char {
+    assert!(
+        u < NUM_VALID_CODEPOINTS,
+        "input to map_int_to_char out of range: got {}, expected value < {}",
+        u,
+        NUM_VALID_CODEPOINTS
+    );
+    let val = if u >= SURROGATE_START { u + (SURROGATE_END - SURROGATE_START + 1) } else { u };
+    std::char::from_u32(val).unwrap()
+}
+
+impl Domain for Arbitrary<char> {
+    type UserValue<'user> = char;
+    type CorpusValue = char;
+
+    fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
+        Ok(choose_value(rng))
+    }
+
+    fn mutate(
+        &self,
+        val: &mut Self::CorpusValue,
+        rng: &mut dyn rand::Rng,
+        only_shrink: bool,
+    ) -> anyhow::Result<()> {
+        if only_shrink {
+            // Shrink towards the null character (0 as char).
+            *val = shrink_towards(rng, *val, 0 as char);
+        } else {
+            let original_val = *val;
+            loop {
+                // Map the current char to a u32 in a contiguous range.
+                let mapped_val = map_char_to_int(*val);
+                // Mutate the u32 value within the valid range of mapped code points.
+                let mutated_mapped_val =
+                    mutate_integer(rng, mapped_val, 5, Some(0), Some(NUM_VALID_CODEPOINTS - 1));
+                // Map the mutated u32 back to a char.
+                *val = map_int_to_char(mutated_mapped_val);
+                if *val != original_val {
+                    break;
+                }
+            }
+        }
+        Ok(())
+    }
+
+    fn get_user_value<'a>(
+        &self,
+        corpus_value: &'a Self::CorpusValue,
+    ) -> anyhow::Result<Self::UserValue<'a>> {
+        Ok(*corpus_value)
+    }
+}
+
+impl Domain for Arbitrary<()> {
+    type UserValue<'user> = ();
+    type CorpusValue = ();
+
+    fn init(&self, _rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
+        Ok(())
+    }
+
+    fn mutate(
+        &self,
+        _val: &mut Self::CorpusValue,
+        _rng: &mut dyn rand::Rng,
+        _only_shrink: bool,
+    ) -> anyhow::Result<()> {
+        // No possible mutation for the unit type
+        Ok(())
+    }
+
+    fn get_user_value<'a>(
+        &self,
+        _corpus_value: &'a Self::CorpusValue,
+    ) -> anyhow::Result<Self::UserValue<'a>> {
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::collections::HashSet;
+
+    use crate::domains::utility::SpecialValues;
+    use num_traits::{Float, One, Zero};
+    use rand::distr::uniform::SampleUniform;
+    use rand::distr::{Distribution, StandardUniform};
+    use rand::rngs::{SmallRng, SysRng};
+    use rand::SeedableRng;
+
+    use crate::domains::Domain;
+
+    use super::*;
+
+    type CorpusValueForArbitrary<T> = <Arbitrary<T> as Domain>::CorpusValue;
+
+    fn get_rng() -> SmallRng {
+        SmallRng::try_from_rng(&mut SysRng).unwrap()
+    }
+
+    /// Trait to be able to treat numerical types uniformly w.r.t. finiteness and shrinking target.
+    trait NumTraitsExtended {
+        fn is_finite_value(&self) -> bool;
+        fn is_at_shrink_target(&self) -> bool;
+    }
+
+    macro_rules! impl_num_traits_extended_for_floats {
+        ($($ty:ty),*) => {
+            $(
+                impl NumTraitsExtended for $ty {
+                    fn is_finite_value(&self) -> bool {
+                        self.is_finite()
+                    }
+                    fn is_at_shrink_target(&self) -> bool {
+                        // We arbitrarily use EPSILON^2 as the accuracy margin for approximate equality.
+                        self.abs() < <$ty>::EPSILON * <$ty>::EPSILON
+                    }
+                }
+            )*
+        };
+    }
+
+    impl_num_traits_extended_for_floats!(f32, f64);
+
+    macro_rules! impl_num_traits_extended_for_non_floats {
+        ($($ty:ty),*) => {
+            $(
+                impl NumTraitsExtended for $ty {
+                    fn is_finite_value(&self) -> bool {
+                        true
+                    }
+                    fn is_at_shrink_target(&self) -> bool {
+                        *self == <$ty>::default()
+                    }
+                }
+            )*
+        };
+    }
+
+    impl_num_traits_extended_for_non_floats!(
+        i8,
+        i16,
+        i32,
+        i64,
+        i128,
+        isize,
+        u8,
+        u16,
+        u32,
+        u64,
+        u128,
+        usize,
+        bool,
+        char,
+        ()
+    );
+
+    fn test_arbitrary_mutations_change_value<T>()
+    where
+        for<'user> Arbitrary<T>: Domain<UserValue<'user> = T>,
+        CorpusValueForArbitrary<T>:
+            std::fmt::Debug + Default + Clone + Copy + PartialOrd + PartialEq + 'static,
+    {
+        let domain = Arbitrary::<T>::default();
+        let mut rng = get_rng();
+        let mut value = domain.init(&mut rng).unwrap();
+
+        // Mutation is guaranteed to change the value.
+        for _ in 0..100 {
+            let original_value = value;
+            domain.mutate(&mut value, &mut rng, false).unwrap();
+            assert_ne!(
+                value,
+                original_value,
+                "Mutation did not change the value for type {}",
+                std::any::type_name::<T>()
+            );
+        }
+    }
+
+    fn test_arbitrary_mutations_generates_diverse_values<T>()
+    where
+        for<'user> Arbitrary<T>: Domain<UserValue<'user> = T>,
+        CorpusValueForArbitrary<T>: std::fmt::Debug
+            + Default
+            + Clone
+            + Copy
+            + PartialOrd
+            + PartialEq
+            + Eq
+            + std::hash::Hash
+            + 'static,
+    {
+        let domain = Arbitrary::<T>::default();
+        let mut rng = get_rng();
+        for _ in 0..100 {
+            let mut value = domain.init(&mut rng).unwrap();
+            let mut values = HashSet::new();
+            values.insert(value);
+
+            for _ in 0..100 {
+                domain.mutate(&mut value, &mut rng, false).unwrap();
+                values.insert(value);
+            }
+            assert!(
+                values.len() >= 50,
+                "Mutation did not generate diverse values for type {}: found {} distinct values, expected >= 50",
+                std::any::type_name::<T>(),
+                values.len()
+            );
+        }
+    }
+
+    fn test_arbitrary_shrinking_reduces_and_terminates<T>()
+    where
+        for<'user> Arbitrary<T>: Domain<UserValue<'user> = T>,
+        CorpusValueForArbitrary<T>: std::fmt::Debug
+            + Default
+            + Clone
+            + Copy
+            + PartialOrd
+            + PartialEq
+            + NumTraitsExtended
+            + 'static,
+    {
+        let domain = Arbitrary::<T>::default();
+        let mut rng = get_rng();
+
+        // Get a value that is not the shrink target
+        let shrink_target = CorpusValueForArbitrary::<T>::default();
+        let mut value;
+        loop {
+            value = domain.init(&mut rng).unwrap();
+            if !value.is_at_shrink_target() && value.is_finite_value() {
+                break;
+            }
+        }
+
+        for _ in 0..1000 {
+            let prev_value = value;
+            domain.mutate(&mut value, &mut rng, true).unwrap();
+
+            if value.is_at_shrink_target() {
+                // Ensure that once the shrink target is reached, further shrinking doesn't change it.
+                domain.mutate(&mut value, &mut rng, true).unwrap();
+                assert!(
+                    value.is_at_shrink_target(),
+                    "Shrinking past the target changed the value for type {}",
+                    std::any::type_name::<T>()
+                );
+                return;
+            }
+
+            // Check progress
+            if prev_value > shrink_target {
+                assert!(
+                    value < prev_value,
+                    "Value should decrease when shrinking towards target from above. Type: {}, Prev: {:?}, Current: {:?}",
+                    std::any::type_name::<T>(),
+                    prev_value,
+                    value
+                );
+                assert!(
+                    value >= shrink_target,
+                    "Value should not shrink past the target from above. Type: {}, Prev: {:?}, Current: {:?}, Target: {:?}",
+                    std::any::type_name::<T>(),
+                    prev_value,
+                    value,
+                    shrink_target
+                );
+            } else {
+                // prev_value < shrink_target
+                assert!(
+                    value > prev_value,
+                    "Value should increase when shrinking towards target from below. Type: {}, Prev: {:?}, Current: {:?}",
+                    std::any::type_name::<T>(),
+                    prev_value,
+                    value
+                );
+                assert!(
+                    value <= shrink_target,
+                    "Value should not shrink past the target from below. Type: {}, Prev: {:?}, Current: {:?}, Target: {:?}",
+                    std::any::type_name::<T>(),
+                    prev_value,
+                    value,
+                    shrink_target
+                );
+            }
+        }
+
+        panic!(
+            "Shrinking did not terminate in 1000 iterations for type {}",
+            std::any::type_name::<T>()
+        );
+    }
+
+    fn test_arbitrary<T>()
+    where
+        for<'user> Arbitrary<T>: Domain<UserValue<'user> = T>,
+        CorpusValueForArbitrary<T>: std::fmt::Debug
+            + Default
+            + Clone
+            + Copy
+            + PartialOrd
+            + PartialEq
+            + Eq
+            + std::hash::Hash
+            + NumTraitsExtended
+            + 'static,
+    {
+        test_arbitrary_mutations_change_value::<T>();
+        test_arbitrary_mutations_generates_diverse_values::<T>();
+        test_arbitrary_shrinking_reduces_and_terminates::<T>();
+    }
+
+    #[test]
+    fn test_i8() {
+        test_arbitrary::<i8>();
+    }
+    #[test]
+    fn test_u8() {
+        test_arbitrary::<u8>();
+    }
+    #[test]
+    fn test_i16() {
+        test_arbitrary::<i16>();
+    }
+    #[test]
+    fn test_u16() {
+        test_arbitrary::<u16>();
+    }
+    #[test]
+    fn test_i32() {
+        test_arbitrary::<i32>();
+    }
+    #[test]
+    fn test_u32() {
+        test_arbitrary::<u32>();
+    }
+    #[test]
+    fn test_i64() {
+        test_arbitrary::<i64>();
+    }
+    #[test]
+    fn test_u64() {
+        test_arbitrary::<u64>();
+    }
+    #[test]
+    fn test_i128() {
+        test_arbitrary::<i128>();
+    }
+    #[test]
+    fn test_u128() {
+        test_arbitrary::<u128>();
+    }
+    #[test]
+    fn test_isize() {
+        test_arbitrary::<isize>();
+    }
+    #[test]
+    fn test_usize() {
+        test_arbitrary::<usize>();
+    }
+
+    #[test]
+    fn test_char() {
+        test_arbitrary::<char>();
+    }
+
+    fn test_bool_shrink() {
+        let domain = Arbitrary::<bool>::default();
+        let mut rng = get_rng();
+        let mut value = true;
+        domain.mutate(&mut value, &mut rng, true).unwrap();
+        assert_eq!(value, false);
+        domain.mutate(&mut value, &mut rng, true).unwrap();
+        assert_eq!(value, false);
+    }
+
+    #[test]
+    fn test_bool() {
+        test_arbitrary_shrinking_reduces_and_terminates::<bool>();
+        test_bool_shrink();
+    }
+    #[test]
+    fn test_unit() {
+        let mut rng = get_rng();
+        let domain = Arbitrary::<()>::default();
+
+        // init() always returns ()
+        assert_eq!(domain.init(&mut rng).unwrap(), ());
+
+        // mutate (non-shrinking) always returns ()
+        let mut value = ();
+        domain.mutate(&mut value, &mut rng, false).unwrap();
+        assert_eq!(value, ());
+
+        // mutate (shrinking) always returns ()
+        let mut value = ();
+        domain.mutate(&mut value, &mut rng, true).unwrap();
+        assert_eq!(value, ());
+    }
+
+    fn test_float_special_value_shrink<T>()
+    where
+        for<'user> Arbitrary<T>: Domain<UserValue<'user> = T>,
+        CorpusValueForArbitrary<T>:
+            Float + SampleUniform + std::fmt::Display + std::fmt::Debug + SpecialValues + 'static,
+        StandardUniform: Distribution<T>,
+    {
+        let domain = Arbitrary::<T>::default();
+        let mut rng = get_rng();
+
+        // Positive.
+        let mut v = CorpusValueForArbitrary::<T>::one();
+        let original_v = v;
+        domain.mutate(&mut v, &mut rng, true).unwrap();
+        assert!(
+            v >= CorpusValueForArbitrary::<T>::zero() && v < original_v,
+            "Shrinking {} to {}",
+            original_v,
+            v
+        );
+
+        // Negative.
+        let mut v = -CorpusValueForArbitrary::<T>::one();
+        let original_v = v;
+        domain.mutate(&mut v, &mut rng, true).unwrap();
+        assert!(
+            v > original_v && v <= CorpusValueForArbitrary::<T>::zero(),
+            "Shrinking {} to {}",
+            original_v,
+            v
+        );
+
+        // Zero.
+        let mut v = CorpusValueForArbitrary::<T>::zero();
+        domain.mutate(&mut v, &mut rng, true).unwrap();
+        assert_eq!(v, CorpusValueForArbitrary::<T>::zero());
+
+        let mut v = CorpusValueForArbitrary::<T>::neg_zero();
+        domain.mutate(&mut v, &mut rng, true).unwrap();
+        assert_eq!(v, CorpusValueForArbitrary::<T>::neg_zero());
+
+        // NaN / Inf are not changed by this shrink implementation.
+        let mut v = CorpusValueForArbitrary::<T>::nan();
+        domain.mutate(&mut v, &mut rng, true).unwrap();
+        assert!(v.is_nan(), "NaN should remain NaN");
+
+        let mut v = CorpusValueForArbitrary::<T>::infinity();
+        domain.mutate(&mut v, &mut rng, true).unwrap();
+        assert!(v.is_infinite() && v.is_sign_positive(), "Infinity should remain Infinity");
+
+        let mut v = CorpusValueForArbitrary::<T>::neg_infinity();
+        domain.mutate(&mut v, &mut rng, true).unwrap();
+        assert!(v.is_infinite() && v.is_sign_negative(), "Neg Infinity should remain Neg Infinity");
+    }
+
+    #[test]
+    fn test_f32() {
+        test_arbitrary_mutations_change_value::<f32>();
+        test_arbitrary_shrinking_reduces_and_terminates::<f32>();
+        test_float_special_value_shrink::<f32>();
+    }
+
+    #[test]
+    fn test_f64() {
+        test_arbitrary_mutations_change_value::<f64>();
+        test_arbitrary_shrinking_reduces_and_terminates::<f64>();
+        test_float_special_value_shrink::<f64>();
+    }
+
+    #[test]
+    fn test_char_mutate_boundaries() {
+        let domain = Arbitrary::<char>::default();
+        let mut rng = get_rng();
+        let mut val = '\u{0000}';
+        domain.mutate(&mut val, &mut rng, false).unwrap();
+        assert!(val >= '\u{0000}');
+
+        val = '\u{10FFFF}';
+        domain.mutate(&mut val, &mut rng, false).unwrap();
+        assert!(val <= '\u{10FFFF}');
+
+        val = '\u{D7FF}'; // Before surrogate
+        domain.mutate(&mut val, &mut rng, false).unwrap();
+
+        val = '\u{E000}'; // After surrogate
+        domain.mutate(&mut val, &mut rng, false).unwrap();
+    }
+
+    #[test]
+    fn test_char_mapping() {
+        // Test boundary conditions and values around the surrogate range
+        assert_eq!(map_char_to_int('\u{0000}'), 0);
+        assert_eq!(map_int_to_char(0), '\u{0000}');
+
+        let before_surrogate = SURROGATE_START - 1;
+        assert_eq!(
+            map_char_to_int(std::char::from_u32(before_surrogate).unwrap()),
+            before_surrogate
+        );
+        assert_eq!(
+            map_int_to_char(before_surrogate),
+            std::char::from_u32(before_surrogate).unwrap()
+        );
+
+        let after_surrogate = SURROGATE_END + 1;
+        let mapped_after_surrogate = map_char_to_int(std::char::from_u32(after_surrogate).unwrap());
+        assert_eq!(mapped_after_surrogate, SURROGATE_START);
+        assert_eq!(map_int_to_char(SURROGATE_START), std::char::from_u32(after_surrogate).unwrap());
+
+        assert_eq!(map_char_to_int('\u{10FFFF}'), NUM_VALID_CODEPOINTS - 1);
+        assert_eq!(map_int_to_char(NUM_VALID_CODEPOINTS - 1), '\u{10FFFF}');
+    }
+
+    #[test]
+    fn test_char_shrink_to_null() {
+        for _ in 0..10 {
+            let domain = Arbitrary::<char>::default();
+            let mut rng = get_rng();
+            let mut value = domain.init(&mut rng).unwrap();
+
+            let mut iterations = 0;
+            const MAX_ITERATIONS: u32 = 100_000;
+
+            while value != '\0' && iterations < MAX_ITERATIONS {
+                domain.mutate(&mut value, &mut rng, true).unwrap();
+                // Ensure that the value is always a valid char after mutation.
+                assert!(std::char::from_u32(value as u32).is_some());
+                iterations += 1;
+            }
+            assert_eq!(
+                value, '\0',
+                "Char did not shrink to null within {} iterations. Final value: {}",
+                MAX_ITERATIONS, value
+            );
+        }
+    }
+}
diff --git a/rust/src/domains/range.rs b/rust/src/domains/range.rs
new file mode 100644
index 0000000..fd44e20
--- /dev/null
+++ b/rust/src/domains/range.rs
@@ -0,0 +1,85 @@
+use super::Domain;
+
+use anyhow;
+use rand::distr::uniform::SampleUniform;
+use rand::distr::uniform::UniformSampler;
+
+/// Generates values of type `T` in a given range.
+///
+/// For example, `InRange::new(0, 100)` generates integer
+/// values from the inclusive range `[0, 100]`.
+///
+/// Example usage:
+/// ```
+/// # use fuzztest::domains::Domain;
+/// # use fuzztest::domains::range::InRange;
+/// # use rand::prelude::*;
+///
+/// let range_i32 = InRange::new(21i32, 73);
+/// let sample = range_i32.init(&mut rand::rng());
+///
+/// assert!(sample.is_ok());
+/// let sample = sample.unwrap();
+/// assert!(sample >= 21);
+/// assert!(sample <= 73);
+/// ```
+pub struct InRange<T> {
+    lower: T,
+    upper: T,
+}
+
+impl InRange<i32> {
+    pub fn new(lower: i32, upper: i32) -> Self {
+        Self { lower, upper }
+    }
+
+    pub fn get_in_range(&self, rng: &mut dyn rand::Rng) -> i32 {
+        <i32 as SampleUniform>::Sampler::sample_single(self.lower, self.upper, rng)
+            .expect("Failed to sample from Uniform distribution")
+    }
+}
+
+impl Domain for InRange<i32> {
+    type UserValue<'user> = i32;
+    type CorpusValue = i32;
+
+    fn init(&self, rng: &mut dyn rand::Rng) -> anyhow::Result<Self::CorpusValue> {
+        Ok(self.get_in_range(rng))
+    }
+
+    fn mutate(
+        &self,
+        val: &mut Self::CorpusValue,
+        rng: &mut dyn rand::Rng,
+        only_shrink: bool,
+    ) -> anyhow::Result<()> {
+        if only_shrink {
+            *val -= 1;
+        } else {
+            *val = self.get_in_range(rng);
+        }
+        Ok(())
+    }
+
+    fn get_user_value<'a>(
+        &self,
+        corpus_value: &'a Self::CorpusValue,
+    ) -> anyhow::Result<Self::UserValue<'a>> {
+        Ok(*corpus_value)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use rand::SeedableRng;
+
+    use super::*;
+
+    #[test]
+    fn test_in_range() {
+        let in_range = InRange::<i32>::new(0, 100);
+        let mut rng = rand::rngs::StdRng::from_seed([73; 32]);
+        let in_range_val = in_range.get_in_range(&mut rng);
+        assert!(in_range_val >= 0 && in_range_val <= 100);
+    }
+}
diff --git a/rust/src/domains/tuple_of.rs b/rust/src/domains/tuple_of.rs
new file mode 100644
index 0000000..ac63964
--- /dev/null
+++ b/rust/src/domains/tuple_of.rs
@@ -0,0 +1,6 @@
+// TODO(yamilmorales, tinmar, mathuxny-73): Replace generated wrapper domain (to deduplicate
+// implementations for tests with the same number of parameters) with these, meaning we should
+// `impl Domain` on these tuples.
+
+pub type TupleOf1<T0> = (T0,);
+pub type TupleOf2<T0, T1> = (T0, T1);
diff --git a/rust/src/domains/utility.rs b/rust/src/domains/utility.rs
new file mode 100644
index 0000000..2d63441
--- /dev/null
+++ b/rust/src/domains/utility.rs
@@ -0,0 +1,404 @@
+use std::cmp::Ordering;
+
+use num_traits::PrimInt;
+use rand::distr::uniform::SampleUniform;
+use rand::distr::{Distribution, StandardUniform};
+use rand::RngExt;
+
+/// Shrinks a `val` towards a `target` value.
+///
+/// The new value is chosen uniformly from the range between `val` and `target`.
+/// - If `val > target`, the range is `[target, val)`.
+/// - If `val < target`, the range is `(val, target]`.
+/// - If `val == target`, `val` is returned.
+///
+/// # Arguments
+/// * `rng`: Random number generator.
+/// * `val`: The current value to shrink.
+/// * `target`: The value to shrink towards.
+pub fn shrink_towards<T, R: rand::Rng + ?Sized>(rng: &mut R, val: T, target: T) -> T
+where
+    T: SampleUniform + PartialOrd + Copy + std::fmt::Display,
+{
+    match val.partial_cmp(&target) {
+        Some(Ordering::Equal) => val,
+        Some(Ordering::Less) => {
+            // val < target
+            // Sample from (val, target].
+            // To do that, we sample from [val, target) and return target if we draw val.
+            let sample = rng.random_range(val..target);
+            if sample == val {
+                target
+            } else {
+                sample
+            }
+        }
+        Some(Ordering::Greater) => {
+            // val > target
+            // Sample from [target, val).
+            rng.random_range(target..val)
+        }
+        None => val, // Cannot compare, return original
+    }
+}
+
+/// Mutates an integer value using one of several strategies.
+///
+/// The mutation strategies are:
+/// 1.  Random Walk: Add/subtract a small value within the given `range`.
+/// 2.  Bit Flip: Flip a single random bit.
+/// 3.  Random Choice: Replace with a new completely random value.
+///
+/// # Arguments
+/// * `rng`: Random number generator.
+/// * `val`: The integer value to mutate.
+/// * `range`: The maximum +/- delta for the random walk.
+/// * `min_value`: Optional minimum boundary for mutation. If `None`, `T::min_value()` is used.
+/// * `max_value`: Optional maximum boundary for mutation. If `None`, `T::max_value()` is used.
+// REQUIRES: range > 0
+// REQUIRES: min_value <= max_value (after unwrapping)
+// TODO(b/405382579): Add dictionary-based mutation strategies similar to C++ implementation
+// (See `fuzztest/internal/domains/value_mutation_helpers.h`)
+pub fn mutate_integer<T, R: rand::Rng + ?Sized>(
+    rng: &mut R,
+    val: T,
+    range: T,
+    min_value: Option<T>,
+    max_value: Option<T>,
+) -> T
+where
+    T: PrimInt + SampleUniform + std::fmt::Display,
+{
+    assert!(range > T::zero(), "mutate_integer: range value cannot be <= 0: {range}");
+
+    let min_val = min_value.unwrap_or(T::min_value());
+    let max_val = max_value.unwrap_or(T::max_value());
+    assert!(
+        min_val < max_val,
+        "mutate_integer: min_val must be strictly smaller than max_val: min={min_val}, max={max_val}"
+    );
+
+    match rng.random_range(0..3) {
+        0 => {
+            // 1/3 chance: Random Walk
+            // Establish a window around the current value. If val > max_val, we should walk
+            // around max_val instead to bring the value back into a valid range.
+            let center = val.clamp(min_val, max_val);
+            let lo = center.saturating_sub(range).max(min_val);
+            let hi = center.saturating_add(range).min(max_val);
+
+            // Sample uniformly within the clamped window [lo, hi].
+            rng.random_range(lo..=hi)
+        }
+        1 => {
+            // 1/3 chance: Flip a random bit
+            let num_bits = std::mem::size_of::<T>() * 8;
+            let bit_index = rng.random_range(0..num_bits);
+            let mask = T::one() << bit_index;
+            let result = val ^ mask;
+            if result < min_val || result > max_val {
+                val
+            } else {
+                result
+            }
+        }
+        _ => {
+            // 1/3 chance: Replace with a new random value
+            rng.random_range(min_val..=max_val)
+        }
+    }
+}
+
+pub trait SpecialValues: Sized + Copy {
+    fn get() -> &'static [Self];
+}
+
+macro_rules! impl_special_values_signed {
+    ($($ty:ty),*) => {
+        $(
+            impl SpecialValues for $ty {
+                fn get() -> &'static [Self] {
+                    &[0, 1, -1, <$ty>::MIN, <$ty>::MAX]
+                }
+            }
+        )*
+    };
+}
+
+macro_rules! impl_special_values_unsigned {
+    ($($ty:ty),*) => {
+        $(
+            impl SpecialValues for $ty {
+                fn get() -> &'static [Self] {
+                    &[0, 1, <$ty>::MAX, <$ty>::MAX >> 1]
+                }
+            }
+        )*
+    };
+}
+
+impl_special_values_signed!(i8, i16, i32, i64, i128);
+impl_special_values_unsigned!(u8, u16, u32, u64, u128);
+
+macro_rules! impl_special_values_float {
+    ($($ty:ty),*) => {
+        $(
+            impl SpecialValues for $ty {
+                fn get() -> &'static [Self] {
+                    &[0.0, -0.0, 1.0, -1.0, <$ty>::MIN, <$ty>::MAX, <$ty>::INFINITY, <$ty>::NEG_INFINITY, <$ty>::NAN]
+                }
+            }
+        )*
+    };
+}
+impl_special_values_float!(f32, f64);
+
+impl SpecialValues for char {
+    fn get() -> &'static [Self] {
+        &[
+            '\0',         // Null
+            'a',          // Common ASCII
+            '~',          // Last printable ASCII
+            '\n',         // Newline
+            ' ',          // Space
+            '\u{0080}',   // Start of multi-byte UTF-8
+            '\u{D7FF}',   // Before surrogate range
+            '\u{E000}',   // After surrogate range
+            '\u{10FFFF}', // Max valid Unicode scalar value
+        ]
+    }
+}
+
+/// Chooses a value of type `T`, potentially from a set of "special values".
+///
+/// This function has a higher chance of returning one of the predefined "special values"
+/// for the type `T` (as defined by the `SpecialValues` trait). Otherwise, it generates
+/// a completely random value of type `T`.
+///
+/// # Arguments
+/// * `rng`: Random number generator.
+///
+/// # Type Parameters
+/// * `T`: The type of the value to choose. Must implement `SpecialValues` and
+///        `StandardUniform` must be able to generate values of type `T`.
+pub fn choose_value<T, R: rand::Rng + ?Sized>(rng: &mut R) -> T
+where
+    T: SpecialValues + 'static,
+    StandardUniform: Distribution<T>,
+{
+    let special_values = T::get();
+    let index = rng.random_range(0..=special_values.len());
+    if index < special_values.len() {
+        special_values[index]
+    } else {
+        rng.random()
+    }
+}
+
+/// Mutates a float value.
+pub fn mutate_float<T, R: rand::Rng + ?Sized>(
+    rng: &mut R,
+    val: &mut T,
+    only_shrink: bool,
+    _range: Option<(T, T)>, // TODO: Implement range support for floats.
+) -> anyhow::Result<()>
+where
+    T: num_traits::Float + SampleUniform + std::fmt::Display + Copy + SpecialValues + 'static,
+    StandardUniform: Distribution<T>,
+{
+    if only_shrink {
+        // Shrink only finite, non-terminal values.
+        if val.is_finite() && *val != T::zero() {
+            *val = shrink_towards(rng, *val, T::zero());
+        }
+        return Ok(());
+    }
+    // Non-shrinking mutation.
+    let original_val = *val;
+    loop {
+        // Loop to ensure mutation
+        if !val.is_finite() {
+            // NaN or Infinity
+            *val = choose_value(rng);
+        } else {
+            match rng.random_range(0..4) {
+                0 => *val = *val / (T::one() + T::one()),
+                1 => *val = -*val,
+                2 => *val = *val + T::one(),
+                3 => *val = *val * (T::one() + T::one() + T::one()),
+                _ => unreachable!(),
+            }
+        }
+
+        if (val.is_nan() && original_val.is_nan()) || *val == original_val {
+            continue;
+        }
+        break;
+    }
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use rand::{
+        rngs::{SmallRng, SysRng},
+        SeedableRng,
+    };
+
+    fn get_rng() -> SmallRng {
+        SmallRng::try_from_rng(&mut SysRng).unwrap()
+    }
+
+    fn check_shrink_towards<T>(smaller: T, larger: T)
+    where
+        T: SampleUniform + PartialOrd + Copy + std::fmt::Display + std::fmt::Debug + PartialEq,
+    {
+        let mut rng = get_rng();
+
+        // val > target (e.g. larger -> smaller)
+        // Range should be [smaller, larger)
+        let mut results = Vec::new();
+        for _ in 0..100 {
+            let sample = shrink_towards(&mut rng, larger, smaller);
+            assert!(sample >= smaller && sample < larger);
+            if !results.contains(&sample) {
+                results.push(sample);
+            }
+        }
+        assert!(results.len() > 1);
+
+        // val < target (e.g. smaller -> larger)
+        // Range should be (smaller, larger]
+        let mut results = Vec::new();
+        for _ in 0..100 {
+            let sample = shrink_towards(&mut rng, smaller, larger);
+            assert!(sample > smaller && sample <= larger);
+            if !results.contains(&sample) {
+                results.push(sample);
+            }
+        }
+        assert!(results.len() > 1);
+
+        // identical target
+        let sample = shrink_towards(&mut rng, smaller, smaller);
+        assert_eq!(sample, smaller);
+    }
+
+    #[test]
+    fn test_shrink_towards_all_types() {
+        check_shrink_towards::<i8>(5, 10);
+        check_shrink_towards::<u8>(5, 10);
+        check_shrink_towards::<i16>(5, 10);
+        check_shrink_towards::<u16>(5, 10);
+        check_shrink_towards::<i32>(5, 10);
+        check_shrink_towards::<u32>(5, 10);
+        check_shrink_towards::<i64>(5, 10);
+        check_shrink_towards::<u64>(5, 10);
+        check_shrink_towards::<i128>(5, 10);
+        check_shrink_towards::<u128>(5, 10);
+        check_shrink_towards::<f32>(5.0, 10.0);
+        check_shrink_towards::<f64>(5.0, 10.0);
+    }
+
+    fn check_mutate_integer<T>()
+    where
+        T: PrimInt + SampleUniform + std::fmt::Display + std::fmt::Debug + SpecialValues + 'static,
+        StandardUniform: Distribution<T>,
+    {
+        let mut rng = get_rng();
+
+        let min_val = T::zero();
+        let max_val = T::one() + T::one() + T::one() + T::one() + T::one();
+        let range = T::one() + T::one();
+
+        // Bounded mutation stays in bounds
+        for _ in 0..100 {
+            let val: T = choose_value(&mut rng).clamp(min_val, max_val);
+            let next = mutate_integer(&mut rng, val, range, Some(min_val), Some(max_val));
+            assert!(
+                next >= min_val && next <= max_val,
+                "mutate_integer bounded range check failed for value {} not in [{}, {}]",
+                next,
+                min_val,
+                max_val
+            );
+        }
+
+        // Unbounded mutation changes value
+        let mut val = T::zero();
+        let mut changed = false;
+        for _ in 0..100 {
+            let next = mutate_integer(&mut rng, val, range, None, None);
+            if next != val {
+                changed = true;
+                break;
+            }
+            val = next;
+        }
+        assert!(changed, "mutate_integer did not change value");
+    }
+
+    #[test]
+    fn test_mutate_integer_all_types() {
+        check_mutate_integer::<i8>();
+        check_mutate_integer::<u8>();
+        check_mutate_integer::<i16>();
+        check_mutate_integer::<u16>();
+        check_mutate_integer::<i32>();
+        check_mutate_integer::<u32>();
+        check_mutate_integer::<i64>();
+        check_mutate_integer::<u64>();
+        check_mutate_integer::<i128>();
+        check_mutate_integer::<u128>();
+    }
+
+    fn check_mutate_float<T>()
+    where
+        T: num_traits::Float
+            + SampleUniform
+            + std::fmt::Display
+            + std::fmt::Debug
+            + Copy
+            + SpecialValues
+            + 'static,
+        StandardUniform: Distribution<T>,
+    {
+        let mut rng = get_rng();
+
+        // Mutation changes value
+        let mut val = T::one();
+        let mut changed = false;
+        for _ in 0..100 {
+            let mut next = val;
+            mutate_float(&mut rng, &mut next, false, None).unwrap();
+            // Floats might mutate to NaN
+            if next != val || (next.is_nan() && !val.is_nan()) {
+                changed = true;
+                if next.is_nan() {
+                    val = T::one();
+                } else {
+                    val = next;
+                }
+            }
+        }
+        assert!(changed, "mutate_float did not change value");
+
+        // Shrinking
+        let mut val = T::one() + T::one() + T::one();
+        let original_abs = val.abs();
+        for _ in 0..10 {
+            let mut next = val;
+            mutate_float(&mut rng, &mut next, true, None).unwrap();
+            assert!(next.abs() <= val.abs(), "mutate_float shrink increased absolute value");
+            val = next;
+        }
+        assert!(val.abs() < original_abs, "mutate_float did not shrink value");
+    }
+
+    #[test]
+    fn test_mutate_float_all_types() {
+        check_mutate_float::<f32>();
+        check_mutate_float::<f64>();
+    }
+}
diff --git a/rust/src/internal.rs b/rust/src/internal.rs
new file mode 100644
index 0000000..d440b68
--- /dev/null
+++ b/rust/src/internal.rs
@@ -0,0 +1,57 @@
+use super::domains::GenericCorpusValue;
+use super::domains::GenericDomain;
+use std::collections::HashMap;
+use std::sync::LazyLock;
+
+/// A trait implemented by types used to Fuzz a given property function.
+///
+/// When a test is annotated with the `fuzztest` macro attribute, that test will be wrapped in a
+/// FuzzTestObject that implements this trait. The FuzzTestObject is created by the macro and holds,
+/// among others, the property function and the domains of its arguments.
+/// The FuzzTestObject is then manipulated by Fuzzing Test Engine through the APIs of this trait.
+pub trait FuzzTest {
+    // TODO(tinmar): refine arguments as we get a clearer picture of
+    // what the dispatcher interface looks like
+    fn name(&self) -> &'static str;
+    fn activate(&mut self);
+    fn mutate(&mut self);
+    /// Executes the property function with the given arguments
+    /// (will attempt to downcast to actual user values).
+    ///
+    /// Returns `true` if the property function holds, `false` if it crashes.
+    fn execute<'a>(&self, args: &'a GenericCorpusValue) -> bool;
+    fn print_finding_report(&self);
+    fn domains(&self) -> &dyn GenericDomain;
+}
+
+/// Identifies the property function of a fuzz test.
+///
+/// `FuzzTestInfo` is instantiated by the `fuzztest` macro which populates the fields with the
+/// appropriate values.
+pub struct FuzzTestInfo {
+    pub name: &'static str,
+    pub file: &'static str,
+    pub line: u32,
+}
+
+pub type BoxedFuzzTest = Box<dyn FuzzTest + Send + Sync>;
+
+pub struct FuzzTestRegistration {
+    pub info: &'static FuzzTestInfo,
+    pub factory: fn() -> BoxedFuzzTest,
+}
+
+inventory::collect!(FuzzTestRegistration);
+
+pub static FUZZ_TEST_NAME_TO_FACTORY: LazyLock<HashMap<&str, fn() -> BoxedFuzzTest>> =
+    LazyLock::new(|| {
+        inventory::iter
+            .into_iter()
+            .map(|&FuzzTestRegistration { info, factory }| (info.name, factory))
+            .collect()
+    });
+
+pub struct InputStateAndDomain<I, D> {
+    pub input_state: Option<I>,
+    pub domain: D,
+}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
new file mode 100644
index 0000000..b447424
--- /dev/null
+++ b/rust/src/lib.rs
@@ -0,0 +1,20 @@
+#![feature(cfg_sanitize)]
+
+mod crash_handler;
+pub mod domains;
+pub mod internal;
+pub mod worker;
+
+pub use fuzztest_macro::fuzztest;
+pub use serde;
+
+pub mod options;
+pub use options::get_fuzztest_options;
+pub use options::FuzzTestOptions;
+
+// Re-export helper crates used in macro expansion to decouple downstream Cargo.toml files.
+pub mod reexports {
+    pub use anyhow;
+    pub use inventory;
+    pub use rand;
+}
diff --git a/rust/src/options.rs b/rust/src/options.rs
new file mode 100644
index 0000000..e3e1b4c
--- /dev/null
+++ b/rust/src/options.rs
@@ -0,0 +1,701 @@
+use crate::internal::FuzzTestRegistration;
+use ::engine::engine_ffi;
+use anyhow::Context;
+use clap::Parser;
+use std::ffi::CString;
+use std::ffi::OsString;
+use std::path::Path;
+use std::sync::OnceLock;
+use tempfile::{NamedTempFile, TempDir};
+
+pub use fuzztest_options::{
+    ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions,
+    TimeBudgetType,
+};
+
+/// Returns a lazily-initialized static reference to the global `FuzzTestOptions`.
+pub fn get_fuzztest_options() -> &'static FuzzTestOptions {
+    static OPTIONS: OnceLock<FuzzTestOptions> = OnceLock::new();
+    // We parse FuzzTestOptions from an empty iterator so that clap would parse the options only
+    // from environment variables (like `FUZZTEST_FUZZ_FOR` etc.). We (currently) do not envisage
+    // support for passing flags on cli as the Rust's libtest harness does not support custom
+    // flags.
+    OPTIONS.get_or_init(|| FuzzTestOptions::parse_from(std::iter::empty::<OsString>()))
+}
+
+pub trait ExecutionModeExt {
+    /// Prepares the concrete `ExecutionAction` payload for execution by building any required
+    /// `CentipedeArgs` or temporary runtime assets.
+    fn prepare_action(
+        &self,
+        options: &FuzzTestOptions,
+        current_test_name: Option<&str>,
+    ) -> anyhow::Result<ExecutionAction>;
+}
+
+impl ExecutionModeExt for ExecutionMode {
+    fn prepare_action(
+        &self,
+        options: &FuzzTestOptions,
+        current_test_name: Option<&str>,
+    ) -> anyhow::Result<ExecutionAction> {
+        match self {
+            ExecutionMode::SmokeTest => Ok(ExecutionAction::SmokeTest),
+            ExecutionMode::Fuzz(_)
+            | ExecutionMode::ReplayCrash(_)
+            | ExecutionMode::ReplayCorpus(_) => {
+                let centipede_args =
+                    CentipedeArgs::from_execution_mode(self, options, current_test_name, None)?
+                        .context(
+                            "while attempting to build CentipedeArgs for standalone execution mode",
+                        )?;
+                Ok(ExecutionAction::Standalone(centipede_args))
+            }
+            ExecutionMode::ReplayAllCrashes => {
+                let list_file = NamedTempFile::new()
+                    .context("while attempting to create temp file for ReplayAllCrashes")?;
+                let centipede_args = CentipedeArgs::from_execution_mode(
+                    self,
+                    options,
+                    current_test_name,
+                    Some(list_file.path()),
+                )?
+                .context("while attempting to build CentipedeArgs for replay all crashes mode")?;
+                Ok(ExecutionAction::ReplayAllCrashes { args: centipede_args, list_file })
+            }
+        }
+    }
+}
+
+/// The prepared, ready-to-run execution payload passed to the test worker.
+///
+/// This enum bridges parsed domain intent (`ExecutionMode`) with the concrete runtime
+/// assets (`CentipedeArgs`, temporary files, workdir paths) needed to execute the test run.
+#[derive(Debug)]
+pub enum ExecutionAction {
+    /// Run as a smoke test (regular unit test) with random inputs for a short duration.
+    SmokeTest,
+
+    /// Run in standalone mode, invoking the Centipede fuzzing controller.
+    Standalone(CentipedeArgs),
+
+    /// Replay all crash IDs from the database (carries `list_file` for worker post-processing).
+    ReplayAllCrashes { args: CentipedeArgs, list_file: NamedTempFile },
+}
+
+/// Encapsulates command-line arguments and lifetime holders for Centipede execution.
+#[derive(Debug)]
+pub struct CentipedeArgs {
+    // Holds the owned C-compatible strings. We must keep these alive because the `views`
+    // field contains pointers into the memory owned by these `CString`s.
+    _c_strings: Vec<CString>,
+
+    // The (optional) TempDir instance which is to be used as a workdir by centipede.
+    // We keep the ownership here so that it is not cleaned while the centipede execution is still
+    // in progress.
+    _temp_workdir: Option<TempDir>,
+
+    // FFI-compatible views into the arguments in `_c_strings`. These are passed across
+    // the FFI boundary to Centipede.
+    views: Vec<engine_ffi::FuzzTestBytesView>,
+}
+
+impl CentipedeArgs {
+    fn new(args: Vec<CString>, temp_workdir: Option<TempDir>) -> Self {
+        let views = args
+            .iter()
+            .map(|arg| engine_ffi::FuzzTestBytesView::from_bytes(arg.as_bytes()))
+            .collect();
+
+        Self { _c_strings: args, _temp_workdir: temp_workdir, views }
+    }
+
+    /// Decides if Centipede needs to be invoked based on the execution mode,
+    /// and constructs the appropriate arguments for it if so.
+    ///
+    /// Returns `Ok(Some(args))` if Centipede should be invoked, `Ok(None)` for `SmokeTest`,
+    /// or `Err` if argument generation fails.
+    pub fn from_execution_mode(
+        mode: &ExecutionMode,
+        options: &FuzzTestOptions,
+        current_test_name: Option<&str>,
+        list_file_path: Option<&Path>,
+    ) -> anyhow::Result<Option<Self>> {
+        let mode_opts = match mode {
+            ExecutionMode::SmokeTest => return Ok(None),
+            other => other,
+        };
+
+        let mut args = Vec::new();
+        let mut add_arg = |arg: String| -> anyhow::Result<()> {
+            args.push(CString::new(arg).context("while attempting to create CString arg")?);
+            Ok(())
+        };
+
+        // ==============================================================================
+        // 1. Common Base Arguments (Required across all Centipede executions)
+        // ==============================================================================
+        let argv0 = std::env::args().next().context("while attempting to get argv[0]")?;
+
+        if let Some(test_name) = current_test_name {
+            add_arg(format!("--binary={argv0} {test_name} --exact --nocapture"))?;
+            let normalized_test_name = test_name.replace("::", ".");
+            add_arg(format!("--test_name={normalized_test_name}"))?;
+        } else {
+            // We append the filter for fuzztests mod so that only fuzztests are executed and not unit tests.
+            add_arg(format!("--binary={argv0} __fuzztest_mod__ --nocapture"))?;
+        }
+
+        // Binary identifier is the same as the binary path (stripped of leading slash)
+        let binary_id = argv0.strip_prefix('/').unwrap_or(&argv0);
+        add_arg(format!("--fuzztest_binary_identifier={binary_id}"))?;
+
+        add_arg("--populate_binary_info=false".to_string())?;
+        add_arg("--fork_server=false".to_string())?;
+
+        add_arg(format!("--print_runner_log={}", options.print_subprocess_log))?;
+
+        // Add a dummy hash so as to avoid Centipede computing the hash of the binary.
+        add_arg("--binary_hash=DUMMY_HASH".to_string())?;
+
+        // ==============================================================================
+        // 2. Common Working Directory & Corpus Database Resolution
+        // ==============================================================================
+        let (opt_corpusdb, opt_workdir_root, opt_workdir) =
+            get_corpusdb_and_workdir_root_and_workdir(options)
+                .context("while attempting to resolve corpus and workdir options")?;
+
+        if let Some(corpus_db) = opt_corpusdb {
+            add_arg(format!("--fuzztest_corpus_database={corpus_db}"))?;
+        }
+
+        if let Some(workdir_root) = opt_workdir_root {
+            add_arg(format!("--fuzztest_workdir_root={workdir_root}"))?;
+        }
+
+        if let Some(temp_workdir) = &opt_workdir {
+            let workdir_path = temp_workdir
+                .path()
+                .to_str()
+                .context("while attempting to convert temp dir path to str")?
+                .to_string();
+            add_arg(format!("--workdir={workdir_path}"))?;
+        }
+
+        // ==============================================================================
+        // 3. Common Environment Variables Differential
+        // ==============================================================================
+        let env_diff = Self::default_env_diff();
+        add_arg(format!("--env_diff_for_binaries={}", env_diff.join(",")))?;
+
+        // ==============================================================================
+        // 4. Mode-Specific Arguments
+        // ==============================================================================
+        match mode_opts {
+            ExecutionMode::Fuzz(fuzz_opts) => {
+                match &fuzz_opts.fuzz_for {
+                    FuzzFor::Indefinitely => {
+                        // not specifying `--stop_after` means to run indefinitely.
+                    }
+                    FuzzFor::Duration(duration) => {
+                        add_arg(format!("--stop_after={duration}"))?;
+                    }
+                }
+                if let Some(jobs) = &fuzz_opts.jobs {
+                    add_arg(format!("--j={jobs}"))?;
+                }
+            }
+            ExecutionMode::ReplayCrash(replay_opts) => {
+                add_arg("--replay_crash".to_string())?;
+                add_arg(format!("--crash_id={}", replay_opts.replay_id))?;
+                add_arg("--exit_on_crash".to_string())?;
+            }
+            ExecutionMode::ReplayAllCrashes => {
+                let path = list_file_path
+                    .context("while attempting to get list_file path for ReplayAllCrashes")?;
+                add_arg("--list_crash_ids=true".to_string())?;
+                add_arg(format!("--list_crash_ids_file={}", path.display()))?;
+            }
+            ExecutionMode::ReplayCorpus(replay_corpus_opts) => {
+                let time_limit = match replay_corpus_opts.time_budget_type {
+                    TimeBudgetType::PerTest => replay_corpus_opts.replay_corpus_for,
+                    TimeBudgetType::Total => {
+                        let num_tests = inventory::iter::<FuzzTestRegistration>().count();
+                        if num_tests == 0 {
+                            replay_corpus_opts.replay_corpus_for
+                        } else {
+                            (*replay_corpus_opts.replay_corpus_for.as_ref() / (num_tests as u32))
+                                .into()
+                        }
+                    }
+                };
+                add_arg("--fuzztest_only_replay=true".to_string())?;
+                add_arg("--fuzztest_replay_coverage_inputs=true".to_string())?;
+                add_arg("--load_shards_only=true".to_string())?;
+                add_arg(format!("--fuzztest_time_limit_per_test={time_limit}"))?;
+            }
+            ExecutionMode::SmokeTest => unreachable!(),
+        }
+
+        Ok(Some(Self::new(args, opt_workdir)))
+    }
+
+    /// Generates the default environment variable diff list.
+    ///
+    /// This replicates the logic in `GetEnvDiffForBinaries` from
+    /// `fuzztest/internal/centipede_adaptor.cc`.
+    ///
+    /// When Centipede spawns the test binary as a subprocess, we do not want it to inherit
+    /// environment variables set by the test harness (like Blaze/Bazel). Inheriting these
+    /// could cause the subprocess to think it's running in a sharded environment, try to
+    /// write to restricted output files, or trigger other harness-specific behavior that
+    /// interferes with fuzzing.
+    fn default_env_diff() -> Vec<String> {
+        let mut env_diff = vec![
+            "-TEST_DIAGNOSTICS_OUTPUT_DIR".to_string(),
+            "-TEST_INFRASTRUCTURE_FAILURE_FILE".to_string(),
+            "-TEST_LOGSPLITTER_OUTPUT_FILE".to_string(),
+            "-TEST_PREMATURE_EXIT_FILE".to_string(),
+            "-TEST_RANDOM_SEED".to_string(),
+            "-TEST_RUN_NUMBER".to_string(),
+            "-TEST_SHARD_INDEX".to_string(),
+            "-TEST_SHARD_STATUS_FILE".to_string(),
+            "-TEST_TOTAL_SHARDS".to_string(),
+            "-TEST_UNDECLARED_OUTPUTS_ANNOTATIONS_DIR".to_string(),
+            "-TEST_UNDECLARED_OUTPUTS_DIR".to_string(),
+            "-TEST_WARNINGS_OUTPUT_FILE".to_string(),
+            "-GTEST_OUTPUT".to_string(),
+            "-XML_OUTPUT_FILE".to_string(),
+        ];
+
+        env_diff
+    }
+
+    /// Exposes the FFI-compatible argument views vector for C++ Centipede binding.
+    pub fn as_bytes_views(&self) -> engine_ffi::FuzzTestBytesViews {
+        engine_ffi::FuzzTestBytesViews { views: self.views.as_ptr(), count: self.views.len() }
+    }
+}
+
+/// Determines the `ExecutionAction` for the given test name by inspecting global options.
+pub fn determine_execution_action(current_test_name: Option<&str>) -> ExecutionAction {
+    determine_execution_action_internal(get_fuzztest_options(), current_test_name)
+}
+
+fn determine_execution_action_internal(
+    options: &FuzzTestOptions,
+    current_test_name: Option<&str>,
+) -> ExecutionAction {
+    options
+        .execution_mode()
+        .prepare_action(options, current_test_name)
+        .expect("failed to prepare execution action from fuzztest options")
+}
+
+fn get_corpusdb_and_workdir_root_and_workdir(
+    options: &FuzzTestOptions,
+) -> anyhow::Result<(Option<&str>, Option<&str>, Option<TempDir>)> {
+    let corpus_db = options.corpus_db.as_deref();
+    let workdir_root = options.workdir_root.as_deref();
+
+    if let Some(corpus_db) = corpus_db {
+        if let Some(workdir_root) = workdir_root {
+            Ok((Some(corpus_db), Some(workdir_root), None))
+        } else {
+            let temp_dir =
+                TempDir::new().context("while attempting to create temporary working directory")?;
+            Ok((Some(corpus_db), None, Some(temp_dir)))
+        }
+    } else {
+        if workdir_root.is_some() {
+            anyhow::bail!("NOT ALLOWED: workdir_root provided without corpus_db")
+        } else {
+            let temp_dir =
+                TempDir::new().context("while attempting to create temporary working directory")?;
+            Ok((None, None, Some(temp_dir)))
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use googletest::prelude::*;
+    use std::ffi::OsString;
+
+    #[gtest]
+    fn test_fuzz_options_parsing_env() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_FUZZ_FOR", "5s");
+        }
+
+        let options = FuzzTestOptions::parse_from(std::iter::empty::<OsString>());
+
+        expect_true!(options.fuzz_for.is_some());
+        expect_that!(options.execution_mode(), matches_pattern!(ExecutionMode::Fuzz(_)));
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_FUZZ_FOR");
+        }
+    }
+
+    #[gtest]
+    fn test_fuzz_options_parsing_indefinite_env() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_FUZZ_FOR", "inf");
+        }
+
+        let options = FuzzTestOptions::parse_from(std::iter::empty::<OsString>());
+
+        expect_that!(options.fuzz_for, eq(Some(FuzzFor::Indefinitely)));
+        let mode = options.execution_mode();
+        let ExecutionMode::Fuzz(fuzz_opts) = mode else {
+            panic!("Expected ExecutionMode::Fuzz");
+        };
+        expect_that!(fuzz_opts.fuzz_for, eq(FuzzFor::Indefinitely));
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_FUZZ_FOR");
+        }
+    }
+
+    #[gtest]
+    fn test_determine_execution_action_standalone_indefinite() {
+        let options =
+            FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() };
+        let action = determine_execution_action_internal(&options, Some("my_mod::my_test"));
+
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        // When running indefinitely, --stop_after must be omitted so Centipede runs until interrupted.
+        expect_false!(args_str.iter().any(|s| s.starts_with("--stop_after=")));
+        expect_true!(args_str.contains(&"--test_name=my_mod.my_test"));
+    }
+
+    #[gtest]
+    fn test_determine_execution_action_smoke_test() {
+        let options = FuzzTestOptions::default();
+        let action = determine_execution_action_internal(&options, Some("my_mod::my_test"));
+        assert!(matches!(action, ExecutionAction::SmokeTest));
+    }
+
+    #[gtest]
+    fn test_determine_execution_action_standalone() {
+        let expected_duration = "10s".parse().unwrap();
+        let options = FuzzTestOptions {
+            fuzz_for: Some(FuzzFor::Duration(expected_duration)),
+            ..Default::default()
+        };
+        let action = determine_execution_action_internal(&options, Some("my_mod::my_test"));
+
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        expect_that!(args._c_strings.len(), eq(args.views.len()));
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        expect_true!(args_str
+            .iter()
+            .any(|s| s.starts_with("--binary=") && s.contains("my_mod::my_test --exact")));
+
+        expect_true!(args_str.contains(&"--stop_after=10s"));
+        expect_true!(args_str.contains(&"--test_name=my_mod.my_test"));
+        expect_true!(args_str.iter().any(|s| s.starts_with("--env_diff_for_binaries=")));
+
+        // Verify that a temporary workdir was created and passed as an argument.
+        expect_true!(args._temp_workdir.is_some());
+        let temp_workdir_path = args._temp_workdir.as_ref().unwrap().path().to_str().unwrap();
+        let expected_workdir_arg = format!("--workdir={}", temp_workdir_path);
+        expect_true!(args_str.contains(&expected_workdir_arg.as_str()));
+    }
+
+    #[gtest]
+    fn test_centipede_args_binary_identifier() {
+        let expected_duration = "1s".parse().unwrap();
+        let options = FuzzTestOptions {
+            fuzz_for: Some(FuzzFor::Duration(expected_duration)),
+            ..Default::default()
+        };
+        let action = determine_execution_action_internal(&options, Some("my_mod::my_test"));
+
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        let argv0 = std::env::args().next().expect("get argv[0]");
+        let binary_id = argv0.strip_prefix('/').unwrap_or(&argv0);
+        let expected_binary_id_arg = format!("--fuzztest_binary_identifier={binary_id}");
+
+        expect_true!(args_str.contains(&expected_binary_id_arg.as_str()));
+    }
+
+    #[gtest]
+    fn test_centipede_args_jobs() {
+        let expected_duration = "1s".parse().expect("failed to parse duration");
+        let options_no_jobs = FuzzTestOptions {
+            fuzz_for: Some(FuzzFor::Duration(expected_duration)),
+            ..Default::default()
+        };
+        let action_no_jobs =
+            determine_execution_action_internal(&options_no_jobs, Some("my_mod::my_test"));
+        let ExecutionAction::Standalone(args_no_jobs) = action_no_jobs else {
+            panic!("Expected Standalone action");
+        };
+        let args_str_no_jobs: Vec<&str> =
+            args_no_jobs._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+        assert!(!args_str_no_jobs.iter().any(|s| s.starts_with("--j=")));
+
+        let options_with_jobs = FuzzTestOptions {
+            fuzz_for: Some(FuzzFor::Duration(expected_duration)),
+            jobs: Some(4),
+            ..Default::default()
+        };
+        let action_with_jobs =
+            determine_execution_action_internal(&options_with_jobs, Some("my_mod::my_test"));
+        let ExecutionAction::Standalone(args_with_jobs) = action_with_jobs else {
+            panic!("Expected Standalone action");
+        };
+        let args_str_with_jobs: Vec<&str> =
+            args_with_jobs._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+        assert!(args_str_with_jobs.contains(&"--j=4"));
+    }
+
+    #[gtest]
+    fn test_default_env_diff_set() {
+        let duration = "1s".parse().unwrap();
+        let options =
+            FuzzTestOptions { fuzz_for: Some(FuzzFor::Duration(duration)), ..Default::default() };
+        let action = determine_execution_action_internal(&options, Some("my_mod::my_test"));
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        let env_diff_arg = args_str
+            .iter()
+            .find(|s| s.starts_with("--env_diff_for_binaries="))
+            .expect("missing --env_diff_for_binaries arg");
+
+        expect_true!(env_diff_arg.contains("-TEST_SHARD_INDEX"));
+        expect_true!(env_diff_arg.contains("-XML_OUTPUT_FILE"));
+        expect_true!(env_diff_arg.contains("-GTEST_OUTPUT"));
+    }
+
+    #[gtest]
+    fn test_determine_execution_action_replay_crash_id() {
+        let options = FuzzTestOptions {
+            replay_id: Some("my_crash_123".to_string()),
+            corpus_db: Some("/tmp/corpus_db".to_string()),
+            ..Default::default()
+        };
+        let action = determine_execution_action_internal(&options, Some("my_mod::my_test"));
+
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        expect_true!(args_str.contains(&"--replay_crash"));
+        expect_true!(args_str.contains(&"--crash_id=my_crash_123"));
+        expect_true!(args_str.contains(&"--fuzztest_corpus_database=/tmp/corpus_db"));
+        expect_true!(args_str.contains(&"--test_name=my_mod.my_test"));
+        expect_true!(args_str.iter().any(|s| s.starts_with("--workdir=")));
+    }
+
+    #[gtest]
+    fn test_determine_execution_action_replay_findings() {
+        let options = FuzzTestOptions {
+            replay_findings: true,
+            corpus_db: Some("/tmp/corpus_db".to_string()),
+            ..Default::default()
+        };
+        let action = determine_execution_action_internal(&options, Some("my_mod::my_test"));
+
+        let ExecutionAction::ReplayAllCrashes { args, list_file } = action else {
+            panic!("Expected ReplayAllCrashes action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        expect_true!(args_str.contains(&"--list_crash_ids=true"));
+        expect_true!(args_str
+            .contains(&format!("--list_crash_ids_file={}", list_file.path().display()).as_str()));
+        expect_true!(args_str.contains(&"--fuzztest_corpus_database=/tmp/corpus_db"));
+        expect_true!(args_str.iter().any(|s| s.starts_with("--workdir=")));
+        expect_true!(args_str.contains(&"--test_name=my_mod.my_test"));
+    }
+
+    #[gtest]
+    fn test_determine_execution_action_replay_corpus() {
+        let expected_duration = "10s".parse().expect("failed to parse duration");
+        let options = FuzzTestOptions {
+            replay_corpus_for: Some(expected_duration),
+            time_budget_type: TimeBudgetType::PerTest,
+            ..Default::default()
+        };
+        let action = determine_execution_action_internal(&options, Some("my_mod::my_test"));
+
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        assert!(args_str
+            .iter()
+            .any(|s| s.starts_with("--binary=") && s.contains("my_mod::my_test --exact")));
+        assert!(args_str.contains(&"--fuzztest_only_replay=true"));
+        assert!(args_str.contains(&"--fuzztest_time_limit_per_test=10s"));
+    }
+
+    #[gtest]
+    fn test_determine_execution_action_replay_corpus_total_budget() {
+        let expected_duration = "10s".parse().expect("failed to parse duration");
+        let options = FuzzTestOptions {
+            replay_corpus_for: Some(expected_duration),
+            time_budget_type: TimeBudgetType::Total,
+            ..Default::default()
+        };
+        let action = determine_execution_action_internal(&options, Some("my_mod::my_test"));
+
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        let num_tests = inventory::iter::<FuzzTestRegistration>().count();
+        let expected_limit = if num_tests == 0 {
+            expected_duration
+        } else {
+            (*expected_duration.as_ref() / (num_tests as u32)).into()
+        };
+        let expected_limit_str = format!("--fuzztest_time_limit_per_test={}", expected_limit);
+
+        assert!(args_str.contains(&expected_limit_str.as_str()));
+    }
+
+    #[gtest]
+    fn test_determine_execution_action_replay_corpus_no_test_name() {
+        let expected_duration = "10s".parse().expect("failed to parse duration");
+        let options = FuzzTestOptions {
+            replay_corpus_for: Some(expected_duration),
+            time_budget_type: TimeBudgetType::PerTest,
+            ..Default::default()
+        };
+        let action = determine_execution_action_internal(&options, None);
+
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        assert!(args_str.iter().any(|s| s.contains("__fuzztest_mod__")));
+        assert!(args_str.contains(&"--fuzztest_only_replay=true"));
+        assert!(args_str.contains(&"--fuzztest_time_limit_per_test=10s"));
+        assert!(!args_str.iter().any(|s| s.starts_with("--test_name=")));
+    }
+
+    #[gtest]
+    fn test_get_corpusdb_and_workdir_default_creates_temp_workdir() -> Result<()> {
+        let options = FuzzTestOptions::default();
+        let (corpus_db, workdir_root, workdir) =
+            get_corpusdb_and_workdir_root_and_workdir(&options).or_fail()?;
+        expect_true!(corpus_db.is_none());
+        expect_true!(workdir_root.is_none());
+        expect_true!(workdir.is_some());
+        Ok(())
+    }
+
+    #[gtest]
+    fn test_get_corpusdb_and_workdir_succeeds_with_corpusdb_and_workdir_root() -> Result<()> {
+        let options = FuzzTestOptions {
+            corpus_db: Some("/tmp/db".to_string()),
+            workdir_root: Some("/tmp/root".to_string()),
+            ..Default::default()
+        };
+        let (corpus_db, workdir_root, workdir) =
+            get_corpusdb_and_workdir_root_and_workdir(&options).or_fail()?;
+        expect_that!(corpus_db, eq(Some("/tmp/db")));
+        expect_that!(workdir_root, eq(Some("/tmp/root")));
+        expect_true!(workdir.is_none());
+        Ok(())
+    }
+
+    #[gtest]
+    fn test_get_corpusdb_and_workdir_succeeds_with_corpusdb_only() -> Result<()> {
+        let options =
+            FuzzTestOptions { corpus_db: Some("/tmp/db".to_string()), ..Default::default() };
+        let (corpus_db, workdir_root, workdir) =
+            get_corpusdb_and_workdir_root_and_workdir(&options).or_fail()?;
+        expect_that!(corpus_db, eq(Some("/tmp/db")));
+        expect_true!(workdir_root.is_none());
+        expect_true!(workdir.is_some());
+        Ok(())
+    }
+
+    #[gtest]
+    fn test_replay_id_requires_corpus_db() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_ID", "my_crash_123");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_REPLAY_ID");
+        }
+
+        let err = result.expect_err("parsing should fail when corpus_db is missing");
+        expect_that!(err.kind(), eq(clap::error::ErrorKind::MissingRequiredArgument));
+    }
+
+    #[gtest]
+    fn test_replay_id_with_corpus_db_succeeds() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_ID", "my_crash_123");
+            std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
+        }
+
+        let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_REPLAY_ID");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        let options =
+            result.expect("parsing should succeed when both replay_id and corpus_db are present");
+        expect_that!(options.replay_id.as_deref(), eq(Some("my_crash_123")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+    }
+}
diff --git a/rust/src/worker.rs b/rust/src/worker.rs
new file mode 100644
index 0000000..79bf31c
--- /dev/null
+++ b/rust/src/worker.rs
@@ -0,0 +1,769 @@
+use super::domains::GenericCorpusValue;
+use super::internal::{BoxedFuzzTest, FuzzTest};
+use super::options::{self, CentipedeArgs, ExecutionAction};
+use ::engine::engine_ffi;
+use ::engine::{
+    BytesSink, CoverageDomainRegistry, DiagnosticSink, ExecuteContext, FeedbackSink, InputSink,
+};
+use spin::Mutex;
+use std::ffi::CString;
+use std::path::Path;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::LazyLock;
+
+/// The DiagnosticSink provided by the engine while creating the adapter.
+///
+/// Storing the `DiagnosticSink` in this global is necessary because C death callbacks (e.g.,
+/// sanitizer traps in `crash_handler.rs`) do not have access to the adapter instance and must rely
+/// on a global lookup to report crashes.
+static DIAGNOSTIC_SINK: Mutex<Option<DiagnosticSink>> = Mutex::new(None);
+
+/// A global execution context token.
+///
+/// This is set to `Some(ExecuteContext)` when the fuzz test is currently executing
+/// (inside the `execute` method of `RustFuzzTestAdapter`) and `None` otherwise.
+///
+/// The inner ExecuteContext is used as a witness token to prove to the fuzzing engine that findings
+/// are being emitted during the execution of a test case.
+/// A global is necessary because C death callbacks (e.g. sanitizer traps in `crash_handler.rs`) do
+/// not have access to the adapter instance.
+static EXECUTE_CONTEXT: Mutex<Option<ExecuteContext>> = Mutex::new(None);
+
+/// Sets the global DiagnosticSink.
+pub(crate) fn set_diagnostic_sink(sink: DiagnosticSink) {
+    let mut guard = DIAGNOSTIC_SINK.lock();
+    *guard = Some(sink);
+}
+
+/// Clears the global DiagnosticSink, setting it to None.
+pub(crate) fn clear_diagnostic_sink() {
+    let mut guard = DIAGNOSTIC_SINK.lock();
+    *guard = None;
+}
+
+/// Emits an error into the DiagnosticSink if the DiagnosticSink is set.
+/// This function blocks till it can get the lock on the global DiagnosticSink and is not
+/// signal-safe.
+pub(crate) fn emit_error(message: &str) {
+    DIAGNOSTIC_SINK.lock().as_ref().map(|sink| sink.emit_error(message));
+}
+
+/// Emits a finding into the DiagnosticSink if the DiagnosticSink is set.
+/// This function blocks till it can get the lock on the global DiagnosticSink and is not
+/// signal-safe. Call `try_emit_finding` from a signal handler instead.
+pub(crate) fn emit_finding(description: &str, signature: &str) {
+    let guard = EXECUTE_CONTEXT.lock();
+    let token =
+        guard.as_ref().expect("emit_finding should only be called when a test is executing");
+    DIAGNOSTIC_SINK
+        .lock()
+        .as_ref()
+        .expect("diagnostic sink should be present when a test is executing")
+        .emit_finding(token, description, signature);
+}
+
+/// Emits a finding into the DiagnosticSink if the DiagnosticSink is set, and we are in the Execute
+/// callback context. It only tries once to get the lock. The return value indicates if the finding
+/// was successfully emitted.
+///
+/// This function return false if:
+/// - it could not get the lock on DiagnosticSink.
+/// - it got the lock but the global DiagnosticSink was not set.
+/// - diagnostic sink was set but the test was not executing.
+#[allow(dead_code)]
+pub(crate) fn try_emit_finding(description: &str, signature: &str) -> bool {
+    let Some(diagnostic_guard) = DIAGNOSTIC_SINK.try_lock() else {
+        return false;
+    };
+    let Some(sink) = diagnostic_guard.as_ref() else {
+        return false;
+    };
+    let Some(execution_guard) = EXECUTE_CONTEXT.try_lock() else {
+        return false;
+    };
+    let Some(token) = execution_guard.as_ref() else {
+        return false;
+    };
+    sink.emit_finding(token, description, signature);
+    return true;
+}
+
+// We double-box the input because `GenericCorpusValue` is a fat pointer (`Box<dyn Any>`),
+// which cannot be directly represented as a single `usize` in `FuzzTestInputHandle` for FFI.
+// By boxing it again, we get a thin pointer (`Box<Box<dyn Any>>`) that can be safely cast
+// to `usize`.
+// The memory is reclaimed when the engine calls `free_input`, which reconstructs the outer
+// box using `Box::from_raw` and drops it, thereby dropping the inner box and the value.
+fn pack_input(input: GenericCorpusValue) -> engine_ffi::FuzzTestInputHandle {
+    let ptr = Box::into_raw(Box::new(input));
+    engine_ffi::FuzzTestInputHandle(ptr as usize)
+}
+
+/// A guard that manages the global `EXECUTE_CONTEXT` state, setting it to `Some` on creation,
+/// and resetting to `None` on drop.
+struct ExecutionGuard;
+
+impl ExecutionGuard {
+    /// Enters the execution context by setting `EXECUTE_CONTEXT`.
+    ///
+    /// # Safety
+    /// The caller must ensure we are in a valid state to create an `ExecuteContext`.
+    unsafe fn enter() -> Self {
+        *EXECUTE_CONTEXT.lock() = Some(unsafe { ExecuteContext::new() });
+        Self
+    }
+}
+
+impl Drop for ExecutionGuard {
+    fn drop(&mut self) {
+        *EXECUTE_CONTEXT.lock() = None;
+    }
+}
+
+pub struct RustFuzzTestAdapter {
+    fuzz_test: BoxedFuzzTest,
+}
+
+impl RustFuzzTestAdapter {
+    pub fn set_up_coverage_domains(&self, registry: &mut CoverageDomainRegistry) {
+        registry.set_up_sancov_domains();
+    }
+
+    pub fn get_preset_seed_inputs(&self, _sink: &mut InputSink) {
+        // TODO(the-shank): implement this once the macro has support for specifying preset seed
+        // inputs
+    }
+
+    pub fn get_random_seed_input(&self, sink: &mut InputSink) {
+        match self.fuzz_test.domains().init(&mut rand::rng()) {
+            Ok(val) => {
+                sink.emit(pack_input(val));
+            }
+            Err(e) => {
+                emit_error(&format!("Failed to initialize random seed: {:?}", e));
+            }
+        }
+    }
+
+    pub fn mutate(&self, origin: &GenericCorpusValue, shrink: bool, sink: &mut InputSink) {
+        let mut mutant = origin.clone();
+
+        if let Err(e) = self.fuzz_test.domains().mutate(&mut mutant, &mut rand::rng(), shrink) {
+            emit_error(&format!("Failed to mutate: {:?}", e));
+            return;
+        }
+
+        sink.emit(pack_input(mutant));
+    }
+
+    pub fn cross_over(
+        &self,
+        origin: &GenericCorpusValue,
+        _other: &GenericCorpusValue,
+        sink: &mut InputSink,
+    ) {
+        // TODO(the-shank): implement cross_over. Currently we are just calling mutate.
+        self.mutate(origin, false, sink);
+    }
+
+    pub fn execute(&self, input: &GenericCorpusValue, sink: &mut FeedbackSink) {
+        // TODO(the-shank): modify this in order to support persistent mode and batch execution.
+        static CLEAR_STARTUP_COVERAGE: AtomicBool = AtomicBool::new(true);
+        coverage::prepare_coverage(CLEAR_STARTUP_COVERAGE.swap(false, Ordering::AcqRel));
+
+        // SAFETY: We are currently in the `execute` callback of `RustFuzzTestAdapter`.
+        let _guard = unsafe { ExecutionGuard::enter() };
+
+        let result = self.fuzz_test.execute(input);
+
+        coverage::post_process_coverage(false);
+
+        sink.emit_sancov_features();
+
+        if !result {
+            emit_finding("Property function ran but crashed.", "Unwinding panic");
+        }
+    }
+
+    pub fn serialize_input_content(&self, input: &GenericCorpusValue, sink: &mut BytesSink) {
+        match self.fuzz_test.domains().serialize_corpus(input) {
+            Ok(serialized) => {
+                sink.emit(&serialized);
+            }
+            Err(e) => {
+                emit_error(&format!("Failed to serialize input: {:?}", e));
+            }
+        }
+    }
+
+    pub fn deserialize_input_content(&self, content: &[u8], sink: &mut InputSink) {
+        match self.fuzz_test.domains().parse_corpus(content) {
+            Ok(val) => {
+                sink.emit(pack_input(val));
+            }
+            Err(e) => {
+                emit_error(&format!("Failed to deserialize input: {:?}", e));
+            }
+        }
+    }
+
+    pub fn serialize_input_metadata(&self, _input: &GenericCorpusValue, _sink: &mut BytesSink) {
+        // TODO(the-shank): to be implemented
+    }
+
+    pub fn update_input_metadata(&self, _metadata: &[u8], _input: &mut GenericCorpusValue) {
+        // TODO(the-shank): to be implemented
+    }
+
+    pub fn free_input(&self, input: engine_ffi::FuzzTestInputHandle) {
+        if input.0 != 0 {
+            // SAFETY: The engine guarantees `input` was created by `deserialize_input_content_callback`
+            // (or `emit` in `InputSink`) and has not been freed yet.
+            unsafe {
+                let _ = Box::from_raw(input.0 as *mut GenericCorpusValue);
+            }
+        }
+    }
+}
+
+pub struct RustFuzzTestAdapterManager {
+    pub test_name: &'static str,
+    pub fuzz_test_factory: fn() -> BoxedFuzzTest,
+}
+
+impl RustFuzzTestAdapterManager {
+    pub fn get_binary_id(&self, sink: &mut BytesSink) {
+        static ARGV0: LazyLock<CString> = LazyLock::new(|| {
+            CString::new(
+                Path::new(&std::env::args().nth(0).unwrap())
+                    .file_name()
+                    .and_then(|f| f.to_str())
+                    .unwrap_or(""),
+            )
+            .expect("argv[0] should not contain null bytes")
+        });
+        sink.emit(ARGV0.as_bytes());
+    }
+
+    // Replaces "::" with "." to avoid parsing issues in Centipede. Centipede uses ":" as a
+    // separator in CENTIPEDE_RUNNER_FLAGS and its parser replaces all ":" with "\0", which would
+    // truncate Rust paths.
+    pub fn get_test_name(&self, sink: &mut BytesSink) {
+        let name = self.test_name.replace("::", ".");
+        sink.emit(name.as_bytes());
+    }
+
+    pub fn construct_adapter(&self) -> RustFuzzTestAdapter {
+        let fuzz_test = (self.fuzz_test_factory)();
+        RustFuzzTestAdapter { fuzz_test }
+    }
+
+    pub fn construct_fuzz_test(&self) -> BoxedFuzzTest {
+        (self.fuzz_test_factory)()
+    }
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapterManager` passed during initialization.
+/// * `sink` is a valid pointer to a `FuzzTestBytesSink` whose lifetime extends for the duration
+///   of this call.
+pub unsafe extern "C" fn get_binary_id_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterManagerCtx,
+    sink: *const engine_ffi::FuzzTestBytesSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapterManager`
+    // passed during initialization.
+    let manager = unsafe { &*(ctx as *const RustFuzzTestAdapterManager) };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to a `FuzzTestBytesSink`
+    // whose lifetime extends for the duration of this call.
+    let mut safe_sink = unsafe { BytesSink::from_raw(sink) };
+    manager.get_binary_id(&mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapterManager` passed during initialization.
+/// * `sink` is a valid pointer to a `FuzzTestBytesSink` whose lifetime extends for the duration
+///   of this call.
+pub unsafe extern "C" fn get_test_name_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterManagerCtx,
+    sink: *const engine_ffi::FuzzTestBytesSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapterManager`
+    // passed during initialization.
+    let manager = unsafe { &*(ctx as *const RustFuzzTestAdapterManager) };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to a `FuzzTestBytesSink`
+    // whose lifetime extends for the duration of this call.
+    let mut safe_sink = unsafe { BytesSink::from_raw(sink) };
+    manager.get_test_name(&mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapterManager` passed during initialization.
+/// * `diagnostic_sink` is a valid pointer to a `FuzzTestDiagnosticSink` whose lifetime extends
+///   until `FreeCtx` is called on the adapter.
+/// * `adapter_out` is a valid pointer to write the output `FuzzTestAdapter`.
+pub unsafe extern "C" fn construct_adapter_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterManagerCtx,
+    diagnostic_sink: *const engine_ffi::FuzzTestDiagnosticSink,
+    adapter_out: *mut engine_ffi::FuzzTestAdapter,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapterManager`
+    // passed during initialization.
+    let manager = unsafe { &*(ctx as *const RustFuzzTestAdapterManager) };
+
+    // SAFETY: The engine guarantees `diagnostic_sink` is a valid pointer to a `FuzzTestDiagnosticSink`
+    // whose lifetime extends until `FreeCtx` is called on the adapter.
+    let safe_sink = unsafe { DiagnosticSink::from_raw(diagnostic_sink) };
+
+    set_diagnostic_sink(safe_sink);
+
+    // NOTE: `safe_sink` is not passed to `construct_adapter` or stored in `RustFuzzTestAdapter`.
+    // Instead, it is maintained in the global `DIAGNOSTIC_SINK` mutex via `set_diagnostic_sink`.
+    // This is necessary because C death callbacks (e.g., sanitizer traps in `crash_handler.rs`) do not
+    // have access to the adapter `self` pointer and must rely on a global lookup to report crashes.
+    let adapter = manager.construct_adapter();
+    let boxed_adapter = Box::new(adapter);
+    // SAFETY: The engine guarantees `adapter_out` is a valid pointer to write the output adapter.
+    // The returned raw pointer `ctx` will be managed and eventually freed by `free_ctx_callback`.
+    unsafe {
+        *adapter_out = engine_ffi::FuzzTestAdapter {
+            ctx: Box::into_raw(boxed_adapter) as *mut engine_ffi::FuzzTestAdapterCtx,
+            set_up_coverage_domains: Some(set_up_coverage_domains_callback),
+            get_preset_seed_inputs: Some(get_preset_seed_inputs_callback),
+            get_random_seed_input: Some(get_random_seed_input_callback),
+            mutate: Some(mutate_callback),
+            cross_over: Some(cross_over_callback),
+            execute: Some(execute_callback),
+            serialize_input_content: Some(serialize_input_content_callback),
+            deserialize_input_content: Some(deserialize_input_content_callback),
+            serialize_input_metadata: Some(serialize_input_metadata_callback),
+            update_input_metadata: Some(update_input_metadata_callback),
+            free_input: Some(free_input_callback),
+            free_ctx: Some(free_ctx_callback),
+        };
+    }
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `registry` is a valid pointer to `FuzzTestCoverageDomainRegistry`.
+pub unsafe extern "C" fn set_up_coverage_domains_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    registry: *const engine_ffi::FuzzTestCoverageDomainRegistry,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `registry` is a valid pointer to `FuzzTestCoverageDomainRegistry`.
+    let mut registry = unsafe { CoverageDomainRegistry::from_raw(registry) };
+    adapter.set_up_coverage_domains(&mut registry);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `sink` is a valid pointer to `FuzzTestInputSink`.
+pub unsafe extern "C" fn get_preset_seed_inputs_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    sink: *const engine_ffi::FuzzTestInputSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to `FuzzTestInputSink`.
+    let mut safe_sink = unsafe { InputSink::from_raw(sink) };
+    adapter.get_preset_seed_inputs(&mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `sink` is a valid pointer to `FuzzTestInputSink`.
+pub unsafe extern "C" fn get_random_seed_input_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    sink: *const engine_ffi::FuzzTestInputSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to `FuzzTestInputSink`.
+    let mut safe_sink = unsafe { InputSink::from_raw(sink) };
+    adapter.get_random_seed_input(&mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `origin` is a valid `FuzzTestInputHandle` pointing to a heap-allocated `GenericCorpusValue`
+///   managed by the framework.
+/// * `sink` is a valid pointer to `FuzzTestInputSink`.
+pub unsafe extern "C" fn mutate_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    origin: engine_ffi::FuzzTestInputHandle,
+    shrink: std::ffi::c_int,
+    sink: *const engine_ffi::FuzzTestInputSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `origin` is a valid `FuzzTestInputHandle` pointing to
+    // a heap-allocated `GenericCorpusValue` managed by the framework.
+    let origin_ref = unsafe { &*(origin.0 as *const GenericCorpusValue) };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to `FuzzTestInputSink`.
+    let mut safe_sink = unsafe { InputSink::from_raw(sink) };
+    adapter.mutate(origin_ref, shrink != 0, &mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `origin` is a valid `FuzzTestInputHandle` pointing to a heap-allocated `GenericCorpusValue`
+///   managed by the framework.
+/// * `other` is a valid `FuzzTestInputHandle` pointing to a heap-allocated `GenericCorpusValue`
+///   managed by the framework.
+/// * `sink` is a valid pointer to `FuzzTestInputSink`.
+pub unsafe extern "C" fn cross_over_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    origin: engine_ffi::FuzzTestInputHandle,
+    other: engine_ffi::FuzzTestInputHandle,
+    sink: *const engine_ffi::FuzzTestInputSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `origin` is a valid `FuzzTestInputHandle` pointing to
+    // a heap-allocated `GenericCorpusValue` managed by the framework.
+    let origin_ref = unsafe { &*(origin.0 as *const GenericCorpusValue) };
+    // SAFETY: The engine guarantees `other` is a valid `FuzzTestInputHandle` pointing to
+    // a heap-allocated `GenericCorpusValue` managed by the framework.
+    let other_ref = unsafe { &*(other.0 as *const GenericCorpusValue) };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to `FuzzTestInputSink`.
+    let mut safe_sink = unsafe { InputSink::from_raw(sink) };
+    adapter.cross_over(origin_ref, other_ref, &mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `input` is a valid `FuzzTestInputHandle` pointing to a heap-allocated `GenericCorpusValue`
+///   managed by the framework.
+/// * `sink` is a valid pointer to `FuzzTestFeedbackSink`.
+pub unsafe extern "C" fn execute_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    input: engine_ffi::FuzzTestInputHandle,
+    sink: *const engine_ffi::FuzzTestFeedbackSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `input` is a valid `FuzzTestInputHandle` pointing to
+    // a heap-allocated `GenericCorpusValue` managed by the framework.
+    let input_ref = unsafe { &*(input.0 as *const GenericCorpusValue) };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to `FuzzTestFeedbackSink`.
+    let mut safe_sink = unsafe { FeedbackSink::from_raw(sink) };
+
+    adapter.execute(input_ref, &mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `input` is a valid `FuzzTestInputHandle` pointing to a heap-allocated `GenericCorpusValue`
+///   managed by the framework.
+/// * `sink` is a valid pointer to `FuzzTestBytesSink`.
+pub unsafe extern "C" fn serialize_input_content_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    input: engine_ffi::FuzzTestInputHandle,
+    sink: *const engine_ffi::FuzzTestBytesSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `input` is a valid `FuzzTestInputHandle` pointing to
+    // a heap-allocated `GenericCorpusValue` managed by the framework.
+    let input_ref = unsafe { &*(input.0 as *const GenericCorpusValue) };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to `FuzzTestBytesSink`.
+    let mut safe_sink = unsafe { BytesSink::from_raw(sink) };
+    adapter.serialize_input_content(input_ref, &mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `content` is a valid pointer to `FuzzTestBytesView` containing serialized input content.
+/// * `sink` is a valid pointer to `FuzzTestInputSink`.
+pub unsafe extern "C" fn deserialize_input_content_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    content: *const engine_ffi::FuzzTestBytesView,
+    sink: *const engine_ffi::FuzzTestInputSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `content` is a valid pointer to `FuzzTestBytesView`
+    // containing serialized input content.
+    let bytes = unsafe { &*(*content).to_bytes() };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to `FuzzTestInputSink`.
+    let mut safe_sink = unsafe { InputSink::from_raw(sink) };
+    adapter.deserialize_input_content(bytes, &mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `input` is a valid `FuzzTestInputHandle` pointing to a heap-allocated `GenericCorpusValue`
+///   managed by the framework.
+/// * `sink` is a valid pointer to `FuzzTestBytesSink`.
+pub unsafe extern "C" fn serialize_input_metadata_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    input: engine_ffi::FuzzTestInputHandle,
+    sink: *const engine_ffi::FuzzTestBytesSink,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `input` is a valid `FuzzTestInputHandle` pointing to
+    // a heap-allocated `GenericCorpusValue` managed by the framework.
+    let input_ref = unsafe { &*(input.0 as *const GenericCorpusValue) };
+    // SAFETY: The engine guarantees `sink` is a valid pointer to `FuzzTestBytesSink`.
+    let mut safe_sink = unsafe { BytesSink::from_raw(sink) };
+    adapter.serialize_input_metadata(input_ref, &mut safe_sink);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `metadata` is a valid pointer to `FuzzTestBytesView` containing serialized input metadata.
+/// * `input` is a valid `FuzzTestInputHandle` pointing to a heap-allocated `GenericCorpusValue`
+///   managed by the framework, and the engine guarantees exclusive access to it for the call duration.
+pub unsafe extern "C" fn update_input_metadata_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    metadata: *const engine_ffi::FuzzTestBytesView,
+    input: engine_ffi::FuzzTestInputHandle,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    // SAFETY: The engine guarantees `metadata` is a valid pointer to `FuzzTestBytesView`
+    // containing serialized input metadata.
+    let bytes = unsafe { &*(*metadata).to_bytes() };
+    // SAFETY: The engine guarantees `input` is a valid `FuzzTestInputHandle` pointing to
+    // a heap-allocated `GenericCorpusValue` managed by the framework, and provides exclusive
+    // access to it.
+    let input_ref = unsafe { &mut *(input.0 as *mut GenericCorpusValue) };
+    adapter.update_input_metadata(bytes, input_ref);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+/// * `input` is a valid `FuzzTestInputHandle` pointing to a heap-allocated `GenericCorpusValue`
+///   managed by the framework that has not been freed yet.
+pub unsafe extern "C" fn free_input_callback(
+    ctx: *mut engine_ffi::FuzzTestAdapterCtx,
+    input: engine_ffi::FuzzTestInputHandle,
+) {
+    // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+    // created by `construct_adapter_callback`.
+    let adapter = unsafe { &*(ctx as *const RustFuzzTestAdapter) };
+    adapter.free_input(input);
+}
+
+/// # Safety
+///
+/// The caller must ensure that:
+/// * `ctx` is a valid pointer to the `RustFuzzTestAdapter` created by `construct_adapter_callback`.
+pub unsafe extern "C" fn free_ctx_callback(ctx: *mut engine_ffi::FuzzTestAdapterCtx) {
+    if !ctx.is_null() {
+        // SAFETY: The engine guarantees `ctx` is a valid pointer to the `RustFuzzTestAdapter`
+        // created by `construct_adapter_callback`.
+        let ptr = ctx as *mut RustFuzzTestAdapter;
+        drop(unsafe { Box::from_raw(ptr) });
+
+        clear_diagnostic_sink();
+    }
+}
+
+pub fn run_smoke_test(fuzztest: &dyn FuzzTest) {
+    let start_time = std::time::Instant::now();
+
+    // TODO(the-shank): these should be configurable externally.
+    let smoke_test_duration = std::time::Duration::from_secs(1);
+    let only_shrink = false;
+
+    // TODO(the-shank): the rng seed should be configurable
+    let mut rng = rand::rng();
+
+    let mut generic_corpus_value = fuzztest
+        .domains()
+        .init(&mut rng)
+        .expect("domain initialization should succeed to provide an initial corpus value");
+
+    let result = fuzztest.execute(&generic_corpus_value);
+    // DISCUSS: probably need a more "googletest" approach to handling the result here?
+    assert!(result);
+
+    while start_time.elapsed() < smoke_test_duration {
+        fuzztest
+            .domains()
+            .mutate(&mut generic_corpus_value, &mut rng, only_shrink)
+            .expect("domain mutation should succeed");
+        let result = fuzztest.execute(&generic_corpus_value);
+        // DISCUSS: probably need a more "googletest" approach to handling the result here?
+        assert!(result);
+    }
+}
+
+pub enum WorkerStatus {
+    Success,
+    Failure,
+}
+
+/// Processes and executes a fuzz test using the provided [`RustFuzzTestAdapterManager`].
+///
+/// This function serves as the primary entry point for running a fuzz test. It manages test
+/// execution across two operational modes:
+///
+/// 1. Worker Mode: Registers signal/sanitizer crash handlers and attempts to hand off execution to
+///    the external fuzzing engine (e.g., Centipede) via [`maybe_run_as_worker`].
+///    - If the binary was spawned as a worker process by the fuzzing engine, control remains in the
+///      engine loop until complete.
+///    - Returns cleanly on [`WorkerStatus::Success`], or panics on [`WorkerStatus::Failure`] to
+///      signal test failure to the harness.
+/// 2. Smoke Test Mode: If worker mode is not active (e.g., during standard `blaze test` or
+///    `cargo test` unit test runs), falls back to executing a short local smoke test using sample
+///    inputs and mutation iterations to verify property function sanity.
+pub fn process(manager: RustFuzzTestAdapterManager) {
+    super::crash_handler::register_crash_handler();
+
+    // box it to get a stable heap address.
+    let manager = Box::new(manager);
+
+    if let Some(status) = maybe_run_as_worker(&manager) {
+        match status {
+            WorkerStatus::Success => {
+                // do nothing, the harness would continue to the next test
+                return;
+            }
+            WorkerStatus::Failure => {
+                std::process::exit(1);
+            }
+        }
+    }
+
+    // worker mode was not run -- determine what to do next.
+    match options::determine_execution_action(Some(manager.test_name)) {
+        ExecutionAction::Standalone(centipede_args) => {
+            let status = run_standalone_mode(&manager, centipede_args);
+            if status == engine_ffi::FuzzTestControllerStatus::Failure {
+                panic!("FuzzTest controller reported failure");
+            }
+        }
+        ExecutionAction::ReplayAllCrashes { args, list_file } => {
+            let status = run_standalone_mode(&manager, args);
+            if status == engine_ffi::FuzzTestControllerStatus::Failure {
+                panic!("FuzzTest controller reported failure");
+            }
+
+            // Now read the file and replay each crash
+            if let Ok(contents) = std::fs::read_to_string(list_file.path()) {
+                let options = options::get_fuzztest_options();
+                for crash_id in contents.lines() {
+                    let crash_id = crash_id.trim();
+                    if crash_id.is_empty() {
+                        continue;
+                    }
+                    let replay_mode =
+                        options::ExecutionMode::ReplayCrash(options::ReplayCrashOptions {
+                            replay_id: crash_id.to_string(),
+                        });
+                    if let Ok(Some(replay_args)) = CentipedeArgs::from_execution_mode(
+                        &replay_mode,
+                        options,
+                        Some(manager.test_name),
+                        None,
+                    ) {
+                        // We ignore the result here because replaying a crash is expected to fail
+                        // (report failure). We want to continue replaying other crashes.
+                        let _ = run_standalone_mode(&manager, replay_args);
+                    }
+                }
+            }
+        }
+        ExecutionAction::SmokeTest => {
+            let fuzz_test = manager.construct_fuzz_test();
+            run_smoke_test(fuzz_test.as_ref());
+        }
+    }
+}
+
+fn make_ffi_manager(manager: &RustFuzzTestAdapterManager) -> engine_ffi::FuzzTestAdapterManager {
+    let ctx =
+        manager as *const RustFuzzTestAdapterManager as *mut engine_ffi::FuzzTestAdapterManagerCtx;
+
+    engine_ffi::FuzzTestAdapterManager {
+        ctx,
+        get_binary_id: Some(get_binary_id_callback),
+        get_test_name: Some(get_test_name_callback),
+        construct_adapter: Some(construct_adapter_callback),
+    }
+}
+
+/// Attempts to run the fuzz test in Centipede worker mode using the FFI engine interface.
+///
+/// This function constructs a C-compatible [`engine_ffi::FuzzTestAdapterManager`] wrapping the given `manager`
+/// with required callback function pointers (`get_binary_id_callback`, `get_test_name_callback`, `construct_adapter_callback`),
+/// and invokes [`engine_ffi::FuzzTestWorkerMaybeRun`].
+/// This function constructs a C-compatible [`engine_ffi::FuzzTestAdapterManager`] wrapping
+/// the given `manager` with required callback function pointers (`get_binary_id_callback`,
+/// `get_test_name_callback`, `construct_adapter_callback`), and invokes
+/// [`engine_ffi::FuzzTestWorkerMaybeRun`].
+///
+/// # Returns
+/// - `Some(WorkerStatus::Success)`: The process ran in worker mode and completed successfully.
+/// - `Some(WorkerStatus::Failure)`: The process ran in worker mode and encountered a failure.
+/// - `None`: Worker mode was not requested (`kFuzzTestWorkerNotRequired`). The caller should fall
+///   back to standalone smoke-test execution.
+fn maybe_run_as_worker(manager: &RustFuzzTestAdapterManager) -> Option<WorkerStatus> {
+    let ffi_manager = make_ffi_manager(manager);
+
+    // SAFETY: `manager` is a valid heap-allocated `RustFuzzTestAdapterManager`,
+    // and `ffi_manager` contains valid C-compatible function pointer callbacks.
+    let status = unsafe { engine_ffi::FuzzTestWorkerMaybeRun(&ffi_manager) };
+
+    match status {
+        engine_ffi::FuzzTestWorkerStatus::Success => Some(WorkerStatus::Success),
+        engine_ffi::FuzzTestWorkerStatus::Failure => Some(WorkerStatus::Failure),
+        engine_ffi::FuzzTestWorkerStatus::NotRequired => None,
+    }
+}
+
+fn run_standalone_mode(
+    manager: &RustFuzzTestAdapterManager,
+    centipede_args: CentipedeArgs,
+) -> engine_ffi::FuzzTestControllerStatus {
+    let ffi_manager = make_ffi_manager(manager);
+
+    // SAFETY: `ffi_manager` wraps `manager` which is guaranteed to be alive. `centipede_args` is
+    // alive until the end of this function, since we have the ownership, ensuring that the
+    // pointers passed to `FuzzTestControllerRun` remain valid.
+    unsafe { engine_ffi::FuzzTestControllerRun(&ffi_manager, &centipede_args.as_bytes_views()) }
+}
diff --git a/rust/tests/macro_compiles.rs b/rust/tests/macro_compiles.rs
new file mode 100644
index 0000000..a294458
--- /dev/null
+++ b/rust/tests/macro_compiles.rs
@@ -0,0 +1,10 @@
+use fuzztest::domains::arbitrary::Arbitrary;
+use fuzztest::fuzztest;
+
+#[fuzztest(_a = Arbitrary::<i32>::default())]
+fn fuzztest_macro_compiles(_a: i32) {}
+
+#[fuzztest(_a = Arbitrary::<i32>::default(), _b = Arbitrary::<i32>::default())]
+fn fuzztest_macro_compiles_with_two_args(_a: i32, _b: i32) {}
+
+fn main() {}
diff --git a/rust/tests/trybuild.rs b/rust/tests/trybuild.rs
new file mode 100644
index 0000000..c2d9ebd
--- /dev/null
+++ b/rust/tests/trybuild.rs
@@ -0,0 +1,5 @@
+#[test]
+fn fuzztest() {
+    let t = trybuild::TestCases::new();
+    t.pass("tests/macro_compiles.rs");
+}