Track mutant origins.

This is to enable input reduction (replacing corpus input with smaller mutants if the coverage matches) and corpus mutation stats for new scheduling methods.

PiperOrigin-RevId: 877456645
diff --git a/centipede/byte_array_mutator.cc b/centipede/byte_array_mutator.cc
index f117eea..fc2597d 100644
--- a/centipede/byte_array_mutator.cc
+++ b/centipede/byte_array_mutator.cc
@@ -333,8 +333,8 @@
   std::vector<Mutant> mutants;
   mutants.reserve(num_mutants);
   for (size_t i = 0; i < num_mutants; ++i) {
-    Mutant mutant;
-    mutant.data = inputs[rng_() % num_inputs].data;
+    const size_t origin = rng_() % num_inputs;
+    auto mutant = Mutant{inputs[origin].data, origin};
     if (mutant.data.size() <= max_len_ &&
         knobs_.GenerateBool(knob_mutate_or_crossover, rng_())) {
       // Do crossover only if the mutant is not over the max_len_.
diff --git a/centipede/centipede.cc b/centipede/centipede.cc
index 345af69..f204cce 100644
--- a/centipede/centipede.cc
+++ b/centipede/centipede.cc
@@ -100,6 +100,19 @@
 
 namespace fuzztest::internal {
 
+namespace {
+
+std::vector<MutantRef> InputsToMutantRefs(const std::vector<ByteSpan>& inputs) {
+  std::vector<MutantRef> mutants;
+  mutants.reserve(inputs.size());
+  for (const auto input : inputs) {
+    mutants.push_back(MutantRef{input, Mutant::kOriginNone});
+  }
+  return mutants;
+}
+
+}  // namespace
+
 Centipede::Centipede(const Environment& env, CentipedeCallbacks& user_callbacks,
                      const BinaryInfo& binary_info,
                      CoverageLogger& coverage_logger, std::atomic<Stats>& stats)
@@ -428,20 +441,24 @@
 }
 
 bool Centipede::RunBatch(
-    absl::Span<const ByteSpan> input_vec,
+    absl::Span<const MutantRef> mutants,
     BlobFileWriter* absl_nullable corpus_file,
     BlobFileWriter* absl_nullable features_file,
     BlobFileWriter* absl_nullable unconditional_features_file) {
   BatchResult batch_result;
-  bool success = ExecuteAndReportCrash(env_.binary, input_vec, batch_result);
-  FUZZTEST_CHECK_EQ(input_vec.size(), batch_result.results().size());
+  std::vector<ByteSpan> inputs;
+  inputs.reserve(mutants.size());
+  for (auto mutant : mutants) {
+    inputs.push_back(mutant.data);
+  }
+  bool success = ExecuteAndReportCrash(env_.binary, inputs, batch_result);
+  FUZZTEST_CHECK_EQ(mutants.size(), batch_result.results().size());
 
   for (const auto &extra_binary : env_.extra_binaries) {
     if (ShouldStop()) break;
     BatchResult extra_batch_result;
-    success =
-        ExecuteAndReportCrash(extra_binary, input_vec, extra_batch_result) &&
-        success;
+    success = ExecuteAndReportCrash(extra_binary, inputs, extra_batch_result) &&
+              success;
   }
   if (EarlyStopRequested()) return false;
   if (!success && env_.exit_on_crash) {
@@ -449,9 +466,8 @@
     RequestEarlyStop(EXIT_FAILURE);
     return false;
   }
-  FUZZTEST_CHECK_EQ(batch_result.results().size(), input_vec.size());
   bool batch_gained_new_coverage = false;
-  for (size_t i = 0; i < input_vec.size(); i++) {
+  for (size_t i = 0; i < mutants.size(); i++) {
     if (ShouldStop()) break;
     FeatureVec &fv = batch_result.results()[i].mutable_features();
     bool function_filter_passed = function_filter_.filter(fv);
@@ -460,28 +476,28 @@
       input_gained_new_coverage = true;
     if (unconditional_features_file != nullptr) {
       FUZZTEST_CHECK_OK(unconditional_features_file->Write(
-          PackFeaturesAndHash(input_vec[i], fv)));
+          PackFeaturesAndHash(inputs[i], fv)));
     }
     if (input_gained_new_coverage) {
       // TODO(kcc): [impl] add stats for filtered-out inputs.
-      if (!InputPassesFilter(input_vec[i])) continue;
+      if (!InputPassesFilter(inputs[i])) continue;
       fs_.MergeFeatures(fv);
       LogFeaturesAsSymbols(fv);
       batch_gained_new_coverage = true;
       FUZZTEST_CHECK_GT(fv.size(), 0UL);
       if (function_filter_passed) {
-        corpus_.Add(input_vec[i], fv, batch_result.results()[i].metadata(),
+        corpus_.Add(inputs[i], fv, batch_result.results()[i].metadata(),
                     batch_result.results()[i].stats(), fs_, coverage_frontier_);
       }
       if (corpus_file != nullptr) {
-        FUZZTEST_CHECK_OK(corpus_file->Write(input_vec[i]));
+        FUZZTEST_CHECK_OK(corpus_file->Write(inputs[i]));
       }
       if (!env_.corpus_dir.empty() && !env_.corpus_dir[0].empty()) {
-        WriteToLocalHashedFileInDir(env_.corpus_dir[0], input_vec[i]);
+        WriteToLocalHashedFileInDir(env_.corpus_dir[0], inputs[i]);
       }
       if (features_file != nullptr) {
         FUZZTEST_CHECK_OK(
-            features_file->Write(PackFeaturesAndHash(input_vec[i], fv)));
+            features_file->Write(PackFeaturesAndHash(inputs[i], fv)));
       }
     }
   }
@@ -583,8 +599,9 @@
   while (!to_rerun.empty()) {
     if (ShouldStop()) break;
     size_t batch_size = std::min(to_rerun.size(), env_.batch_size);
-    std::vector<ByteSpan> batch(to_rerun.end() - batch_size, to_rerun.end());
-    if (RunBatch(batch, nullptr, nullptr, features_file.get())) {
+    if (RunBatch(
+            InputsToMutantRefs({to_rerun.end() - batch_size, to_rerun.end()}),
+            nullptr, nullptr, features_file.get())) {
       UpdateAndMaybeLogStats("rerun-old", 1);
     }
     to_rerun.resize(to_rerun.size() - batch_size);
@@ -779,7 +796,7 @@
     seed_inputs.push_back({0});
   }
 
-  RunBatch(std::vector<ByteSpan>{seed_inputs.begin(), seed_inputs.end()},
+  RunBatch(InputsToMutantRefs({seed_inputs.begin(), seed_inputs.end()}),
            corpus_file, features_file,
            /*unconditional_features_file=*/nullptr);
   FUZZTEST_LOG(INFO) << "Number of input seeds available: "
@@ -861,11 +878,15 @@
     auto remaining_runs = env_.num_runs - new_runs;
     auto batch_size = std::min(env_.batch_size, remaining_runs);
     std::vector<MutationInputRef> mutation_inputs;
+    std::vector<size_t> mutate_input_to_corpus_idx;
     mutation_inputs.reserve(env_.mutate_batch_size);
+    mutate_input_to_corpus_idx.reserve(env_.mutate_batch_size);
     for (size_t i = 0; i < env_.mutate_batch_size; i++) {
-      const auto& corpus_record = env_.use_corpus_weights
-                                      ? corpus_.WeightedRandom(rng_)
-                                      : corpus_.UniformRandom(rng_);
+      const size_t origin = env_.use_corpus_weights
+                                ? corpus_.WeightedRandom(rng_)
+                                : corpus_.UniformRandom(rng_);
+      mutate_input_to_corpus_idx.push_back(origin);
+      const auto& corpus_record = corpus_.Records()[origin];
       mutation_inputs.push_back(
           MutationInputRef{corpus_record.data, &corpus_record.metadata});
     }
@@ -875,13 +896,19 @@
     if (ShouldStop()) break;
     new_runs += mutants.size();
 
-    std::vector<ByteSpan> inputs;
-    inputs.reserve(mutants.size());
+    std::vector<MutantRef> mutant_refs;
+    mutant_refs.reserve(mutants.size());
     for (auto& mutant : mutants) {
-      inputs.push_back(mutant.data);
+      if (mutant.origin == Mutant::kOriginNone) {
+        mutant_refs.push_back(MutantRef{mutant.data, Mutant::kOriginNone});
+      } else {
+        FUZZTEST_CHECK_LT(mutant.origin, mutate_input_to_corpus_idx.size());
+        mutant_refs.push_back(
+            MutantRef{mutant.data, mutate_input_to_corpus_idx[mutant.origin]});
+      }
     }
     bool gained_new_coverage =
-        RunBatch(inputs, corpus_file.get(), features_file.get(), nullptr);
+        RunBatch(mutant_refs, corpus_file.get(), features_file.get(), nullptr);
 
     if (gained_new_coverage) {
       UpdateAndMaybeLogStats("new-feature", 1);
diff --git a/centipede/centipede.h b/centipede/centipede.h
index b090b1f..18fa83d 100644
--- a/centipede/centipede.h
+++ b/centipede/centipede.h
@@ -76,7 +76,7 @@
                                      std::string_view dir);
 
  private:
-  // Executes inputs from `input_vec`.
+  // Executes inputs from `mutants` and update the corpus.
   // For every input, its pruned features are written to
   // `unconditional_features_file`, (if that's non-null).
   // For every input that caused new features to be observed:
@@ -84,8 +84,8 @@
   //   * the input is written to `corpus_file` (if that's non-null).
   //   * its features are written to `features_file` (if that's non-null).
   // Returns true if new features were observed.
-  // Post-condition: `batch_result.results.size()` == `input_vec.size()`.
-  bool RunBatch(absl::Span<const ByteSpan> input_vec,
+  // Post-condition: `batch_result.results.size()` == `mutants.size()`.
+  bool RunBatch(absl::Span<const MutantRef> mutants,
                 BlobFileWriter* absl_nullable corpus_file,
                 BlobFileWriter* absl_nullable features_file,
                 BlobFileWriter* absl_nullable unconditional_features_file);
diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc
index a034cc1..d5497f3 100644
--- a/centipede/centipede_test.cc
+++ b/centipede/centipede_test.cc
@@ -121,12 +121,13 @@
     for (size_t i = 0; i < num_mutants; ++i) {
       num_mutations_++;
       if (num_mutations_ < 256) {
-        mutants.push_back({/*data=*/{static_cast<uint8_t>(num_mutations_)}});
+        mutants.push_back({/*data=*/{static_cast<uint8_t>(num_mutations_)},
+                           Mutant::kOriginNone});
         continue;
       }
       uint8_t byte0 = (num_mutations_ - 256) / 256;
       uint8_t byte1 = (num_mutations_ - 256) % 256;
-      mutants.push_back({/*data=*/{byte0, byte1}});
+      mutants.push_back({/*data=*/{byte0, byte1}, Mutant::kOriginNone});
     }
     return mutants;
   }
@@ -514,6 +515,7 @@
     for (auto &mutant : mutants) {
       mutant.data.resize(1);
       mutant.data[0] = ++number_of_mutations_;
+      mutant.origin = Mutant::kOriginNone;
     }
     return mutants;
   }
@@ -603,7 +605,8 @@
     std::vector<Mutant> mutants;
     mutants.reserve(num_mutants);
     for (size_t i = 0; i < num_mutants; ++i) {
-      mutants.push_back({/*data=*/GetMutant(++number_of_mutations_)});
+      mutants.push_back(
+          {/*data=*/GetMutant(++number_of_mutations_), Mutant::kOriginNone});
     }
     return mutants;
   }
@@ -720,6 +723,7 @@
     for (auto &mutant : mutants) {
       mutant.data.resize(1);
       mutant.data[0] = ++number_of_mutations_;
+      mutant.origin = Mutant::kOriginNone;
     }
     return mutants;
   }
@@ -853,7 +857,8 @@
     mutants.reserve(num_mutants);
     for (size_t i = 0; i < num_mutants; ++i) {
       // The contents of each mutant is simply its sequential number.
-      mutants.push_back({/*data=*/{static_cast<uint8_t>(curr_input_idx_++)}});
+      mutants.push_back({/*data=*/{static_cast<uint8_t>(curr_input_idx_++)},
+                         Mutant::kOriginNone});
     }
     return mutants;
   }
@@ -1009,7 +1014,7 @@
 
   std::vector<Mutant> Mutate(absl::Span<const MutationInputRef> inputs,
                              size_t num_mutants) override {
-    return {num_mutants, {/*data=*/{0}}};
+    return {num_mutants, {/*data=*/{0}, Mutant::kOriginNone}};
   }
 
   bool thread_check_passed() { return thread_check_passed_; }
@@ -1065,7 +1070,7 @@
 
   std::vector<Mutant> Mutate(absl::Span<const MutationInputRef> inputs,
                              size_t num_mutants) override {
-    return {num_mutants, {/*data=*/{0}}};
+    return {num_mutants, {/*data=*/{0}, Mutant::kOriginNone}};
   }
 
   int execute_count() const { return execute_count_; }
@@ -1103,7 +1108,7 @@
 
   std::vector<Mutant> Mutate(absl::Span<const MutationInputRef> inputs,
                              size_t num_mutants) override {
-    return {num_mutants, {/*data=*/{0}}};
+    return {num_mutants, {/*data=*/{0}, Mutant::kOriginNone}};
   }
 
   int execute_count() const { return execute_count_; }
@@ -1141,7 +1146,7 @@
 
   std::vector<Mutant> Mutate(absl::Span<const MutationInputRef> inputs,
                              size_t num_mutants) override {
-    return {num_mutants, {/*data=*/{0}}};
+    return {num_mutants, {/*data=*/{0}, Mutant::kOriginNone}};
   }
 
   int execute_count() const { return execute_count_; }
diff --git a/centipede/corpus.cc b/centipede/corpus.cc
index def1496..ba1df9b 100644
--- a/centipede/corpus.cc
+++ b/centipede/corpus.cc
@@ -242,12 +242,12 @@
   weighted_distribution_.AddWeight(0);
 }
 
-const CorpusRecord& Corpus::WeightedRandom(absl::BitGenRef rng) const {
-  return records_[weighted_distribution_.RandomIndex(rng)];
+size_t Corpus::WeightedRandom(absl::BitGenRef rng) const {
+  return weighted_distribution_.RandomIndex(rng);
 }
 
-const CorpusRecord& Corpus::UniformRandom(absl::BitGenRef rng) const {
-  return records_[absl::Uniform<size_t>(rng, 0, records_.size())];
+size_t Corpus::UniformRandom(absl::BitGenRef rng) const {
+  return absl::Uniform<size_t>(rng, 0, records_.size());
 }
 
 void Corpus::DumpStatsToFile(const FeatureSet &fs, std::string_view filepath,
diff --git a/centipede/corpus.h b/centipede/corpus.h
index 72ef3ce..4c16ad3 100644
--- a/centipede/corpus.h
+++ b/centipede/corpus.h
@@ -149,11 +149,11 @@
   size_t NumActive() const { return records_.size(); }
   // Returns the max and avg sizes of the inputs.
   std::pair<size_t, size_t> MaxAndAvgSize() const;
-  // Returns a random active corpus record using weighted distribution.
+  // Returns a random active corpus record index using weighted distribution.
   // See WeightedDistribution.
-  const CorpusRecord& WeightedRandom(absl::BitGenRef rng) const;
-  // Returns a random active corpus record using uniform distribution.
-  const CorpusRecord& UniformRandom(absl::BitGenRef rng) const;
+  size_t WeightedRandom(absl::BitGenRef rng) const;
+  // Returns a random active corpus record index using uniform distribution.
+  size_t UniformRandom(absl::BitGenRef rng) const;
   // Returns the element with index 'idx', where `idx` < NumActive().
   const ByteArray &Get(size_t idx) const { return records_[idx].data; }
   // Returns the execution metadata for the element `idx`, `idx` < NumActive().
diff --git a/centipede/corpus_test.cc b/centipede/corpus_test.cc
index 4b47d5c..46e4d4e 100644
--- a/centipede/corpus_test.cc
+++ b/centipede/corpus_test.cc
@@ -173,8 +173,7 @@
     freq.clear();
     freq.resize(corpus.NumActive());
     for (int i = 0; i < kNumIter; i++) {
-      const auto& record = corpus.WeightedRandom(rng);
-      const auto id = record.data[0];
+      const auto id = corpus.Records()[corpus.WeightedRandom(rng)].data[0];
       ASSERT_LT(id, freq.size());
       freq[id]++;
     }
@@ -215,8 +214,7 @@
     freq.clear();
     freq.resize(corpus.NumActive());
     for (int i = 0; i < kNumIter; i++) {
-      const auto& record = corpus.WeightedRandom(rng);
-      const auto id = record.data[0];
+      const auto id = corpus.Records()[corpus.WeightedRandom(rng)].data[0];
       ASSERT_LT(id, freq.size());
       freq[id]++;
     }
@@ -256,8 +254,7 @@
     freq.clear();
     freq.resize(corpus.NumActive());
     for (int i = 0; i < kNumIter; i++) {
-      const auto& record = corpus.WeightedRandom(rng);
-      const auto id = record.data[0];
+      const auto id = corpus.Records()[corpus.WeightedRandom(rng)].data[0];
       ASSERT_LT(id, freq.size());
       freq[id]++;
     }
@@ -300,10 +297,8 @@
     freq.clear();
     freq.resize(corpus.NumActive());
     for (int i = 0; i < kNumIter; i++) {
-      const auto& record = corpus.WeightedRandom(rng);
-      const auto id = record.data[0];
-      ASSERT_LT(id, freq.size());
-      freq[id]++;
+      const size_t idx = corpus.WeightedRandom(rng);
+      freq[idx]++;
     }
   };
 
diff --git a/centipede/dispatcher.cc b/centipede/dispatcher.cc
index 6980b35..3ecb1f0 100644
--- a/centipede/dispatcher.cc
+++ b/centipede/dispatcher.cc
@@ -537,10 +537,13 @@
                   "mutant must be non-empty with a valid pointer");
   auto* output = GetOutputsBlobSequence();
   DispatcherCheck(output != nullptr, "outputs blob sequence must exist");
-  DispatcherCheck(
-      MutationResult::WriteMutant(
-          MutantRef{{static_cast<const uint8_t*>(data), size}}, *output),
-      "failed to write mutant");
+  DispatcherCheck(MutationResult::WriteMutant(
+                      MutantRef{{static_cast<const uint8_t*>(data), size},
+                                // TODO(xinhaoyuan): change the dispatcher
+                                // interface to include the origin.
+                                fuzztest::internal::Mutant::kOriginNone},
+                      *output),
+                  "failed to write mutant");
 }
 
 void FuzzTestDispatcherEmitFeedbackAs32BitFeatures(const uint32_t* features,
diff --git a/centipede/fuzztest_mutator.cc b/centipede/fuzztest_mutator.cc
index f46041f..2fcf8d2 100644
--- a/centipede/fuzztest_mutator.cc
+++ b/centipede/fuzztest_mutator.cc
@@ -146,13 +146,13 @@
   cmp_tables.resize(inputs.size());
   std::vector<Mutant> mutants;
   mutants.reserve(num_mutants);
-  for (int i = 0; i < num_mutants; ++i) {
-    auto index = absl::Uniform<size_t>(prng_, 0, inputs.size());
-    if (!cmp_tables[index].has_value() && inputs[index].metadata != nullptr) {
-      cmp_tables[index].emplace(/*compact=*/true);
-      PopulateCmpEntries(*inputs[index].metadata, *cmp_tables[index]);
+  for (size_t i = 0; i < num_mutants; ++i) {
+    const size_t origin = absl::Uniform<size_t>(prng_, 0, inputs.size());
+    if (!cmp_tables[origin].has_value() && inputs[origin].metadata != nullptr) {
+      cmp_tables[origin].emplace(/*compact=*/true);
+      PopulateCmpEntries(*inputs[origin].metadata, *cmp_tables[origin]);
     }
-    Mutant mutant = {/*data=*/inputs[index].data};
+    auto mutant = Mutant{inputs[origin].data, origin};
     if (mutant.data.size() > max_len_) mutant.data.resize(max_len_);
     if (knobs_.GenerateBool(knob_mutate_or_crossover, prng_())) {
       // Perform crossover with some other input. It may be the same input.
@@ -162,8 +162,8 @@
     } else {
       domain_->Mutate(
           mutant.data, prng_,
-          {/*cmp_tables=*/cmp_tables[index].has_value() ? &*cmp_tables[index]
-                                                        : nullptr},
+          {/*cmp_tables=*/cmp_tables[origin].has_value() ? &*cmp_tables[origin]
+                                                         : nullptr},
           /*only_shrink=*/false);
     }
     mutants.push_back(std::move(mutant));
diff --git a/centipede/mutation_data.h b/centipede/mutation_data.h
index a4ef7f1..4dc0ee5 100644
--- a/centipede/mutation_data.h
+++ b/centipede/mutation_data.h
@@ -19,6 +19,7 @@
 #ifndef THIRD_PARTY_CENTIPEDE_MUTATION_DATA_H_
 #define THIRD_PARTY_CENTIPEDE_MUTATION_DATA_H_
 
+#include <cstddef>
 #include <vector>
 
 #include "./centipede/execution_metadata.h"
@@ -52,10 +53,16 @@
 struct Mutant {
   // The mutant `data`.
   ByteArray data;
+  // The index of the input used to mutate into `data`. The base array may be
+  // different depending on the context: As mutation output it refers to the
+  // mutation input batch; As execution input it refers to the in-memory corpus.
+  size_t origin = kOriginNone;
+  // A special `origin` value to indicate that the mutant has no origin.
+  static constexpr size_t kOriginNone = static_cast<size_t>(-1);
 };
 
 inline bool operator==(const Mutant& mutant, const Mutant& other) {
-  return mutant.data == other.data;
+  return mutant.data == other.data && mutant.origin == other.origin;
 }
 
 // A reference counterpart of `Mutant`. Needed because it can be constructed
@@ -66,9 +73,11 @@
 
   explicit MutantRef(const Mutant& mutant) : data(mutant.data) {}
 
-  explicit MutantRef(ByteSpan data) : data(data) {}
+  explicit MutantRef(ByteSpan data, size_t origin)
+      : data(data), origin(origin) {}
 
   ByteSpan data;
+  size_t origin = Mutant::kOriginNone;
 };
 
 inline std::vector<ByteArray> GetDataFromMutants(
diff --git a/centipede/runner.cc b/centipede/runner.cc
index 1ccc2f8..3cf4888 100644
--- a/centipede/runner.cc
+++ b/centipede/runner.cc
@@ -646,7 +646,9 @@
        attempt < num_mutants * kAverageMutationAttempts &&
        num_outputs < num_mutants;
        ++attempt) {
-    const auto& input_data = inputs[rand_r(&seed) % num_inputs].data;
+    mutant.origin = rand_r(&seed) % num_inputs;
+    const auto& input_data = inputs[mutant.origin].data;
+
     size_t size = std::min(input_data.size(), max_mutant_size);
     mutant.data.resize(max_mutant_size);
     std::copy(input_data.cbegin(), input_data.cbegin() + size,
diff --git a/centipede/runner_result.cc b/centipede/runner_result.cc
index 7760fa5..07b7810 100644
--- a/centipede/runner_result.cc
+++ b/centipede/runner_result.cc
@@ -45,6 +45,7 @@
 
   // Mutation result tags.
   kTagHasCustomMutator,
+  kTagMutantOrigin,
   kTagMutant,
 };
 
@@ -187,6 +188,10 @@
 }
 
 bool MutationResult::WriteMutant(MutantRef mutant, BlobSequence& blobseq) {
+  if (!blobseq.Write({kTagMutantOrigin, sizeof(mutant.origin),
+                      reinterpret_cast<const uint8_t*>(&mutant.origin)})) {
+    return false;
+  }
   return blobseq.Write({kTagMutant, mutant.data.size(), mutant.data.data()});
 }
 
@@ -200,10 +205,17 @@
   mutants_.clear();
   mutants_.reserve(num_mutants);
   for (size_t i = 0; i < num_mutants; ++i) {
+    size_t origin = Mutant::kOriginNone;
+    {
+      const Blob blob = blobseq.Read();
+      if (blob.tag != kTagMutantOrigin) return false;
+      if (blob.size != sizeof(origin)) return false;
+      std::memcpy(&origin, blob.data, sizeof(origin));
+    }
     const Blob blob = blobseq.Read();
     if (blob.tag != kTagMutant) return false;
     if (blob.size == 0) break;
-    mutants_.push_back({ByteArray{blob.data, blob.data + blob.size}});
+    mutants_.push_back({ByteArray{blob.data, blob.data + blob.size}, origin});
   }
   return true;
 }
diff --git a/centipede/runner_result_test.cc b/centipede/runner_result_test.cc
index 5ac46f3..20ce433 100644
--- a/centipede/runner_result_test.cc
+++ b/centipede/runner_result_test.cc
@@ -214,18 +214,18 @@
 
   // Write a mutation result.
   ASSERT_TRUE(MutationResult::WriteHasCustomMutator(true, blobseq));
-  ASSERT_TRUE(MutationResult::WriteMutant(MutantRef{{1, 2, 3}}, blobseq));
-  ASSERT_TRUE(MutationResult::WriteMutant(MutantRef{{4, 5, 6}}, blobseq));
-  ASSERT_TRUE(MutationResult::WriteMutant(MutantRef{{7, 8, 9}}, blobseq));
+  ASSERT_TRUE(MutationResult::WriteMutant(MutantRef{{1, 2, 3}, 3}, blobseq));
+  ASSERT_TRUE(MutationResult::WriteMutant(MutantRef{{4, 5, 6}, 2}, blobseq));
+  ASSERT_TRUE(MutationResult::WriteMutant(MutantRef{{7, 8, 9}, 1}, blobseq));
   blobseq.Reset();
 
   MutationResult mutation_result;
   ASSERT_TRUE(mutation_result.Read(3, blobseq));
 
   EXPECT_TRUE(mutation_result.has_custom_mutator());
-  EXPECT_THAT(
-      mutation_result.mutants(),
-      ElementsAre(Mutant{{1, 2, 3}}, Mutant{{4, 5, 6}}, Mutant{{7, 8, 9}}));
+  EXPECT_THAT(mutation_result.mutants(),
+              ElementsAre(Mutant{{1, 2, 3}, 3}, Mutant{{4, 5, 6}, 2},
+                          Mutant{{7, 8, 9}, 1}));
 }
 
 TEST(ExecutionResult, ReadResultSucceedsOnlyWithInputBegin) {
diff --git a/centipede/testing/fuzz_target_with_custom_mutator.cc b/centipede/testing/fuzz_target_with_custom_mutator.cc
index ec686ed..a760a19 100644
--- a/centipede/testing/fuzz_target_with_custom_mutator.cc
+++ b/centipede/testing/fuzz_target_with_custom_mutator.cc
@@ -43,7 +43,7 @@
               std::function<void(MutantRef)> new_mutant_callback) override {
     for (size_t i = 0; i < inputs.size() && i < num_mutants; ++i) {
       // Just return the original input as a mutant.
-      new_mutant_callback(MutantRef{inputs[i].data});
+      new_mutant_callback(MutantRef{inputs[i].data, i});
     }
     return true;
   }
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc
index 33b2628..ef874eb 100644
--- a/fuzztest/internal/centipede_adaptor.cc
+++ b/fuzztest/internal/centipede_adaptor.cc
@@ -523,6 +523,7 @@
     absl::Cleanup cmp_tables_cleaner = [this]() { cmp_tables.clear(); };
     for (size_t i = 0; i < num_mutants; ++i) {
       const auto choice = absl::Uniform<double>(prng_, 0, 1);
+      size_t origin_index = Mutant::kOriginNone;
       std::string mutant_data;
       constexpr double kDomainInitRatio = 0.0001;
       if (choice < kDomainInitRatio) {
@@ -530,8 +531,7 @@
             SerializeIRObject(fuzzer_impl_.params_domain_.SerializeCorpus(
                 fuzzer_impl_.params_domain_.Init(prng_)));
       } else {
-        const auto origin_index =
-            absl::Uniform<size_t>(prng_, 0, inputs.size());
+        origin_index = absl::Uniform<size_t>(prng_, 0, inputs.size());
         const auto& origin = inputs[origin_index].data;
         auto parsed_origin =
             fuzzer_impl_.TryParse({(const char*)origin.data(), origin.size()});
@@ -554,7 +554,8 @@
             fuzzer_impl_.params_domain_.SerializeCorpus(mutant.args));
       }
       new_mutant_callback(
-          MutantRef{{(unsigned char*)mutant_data.data(), mutant_data.size()}});
+          MutantRef{{(unsigned char*)mutant_data.data(), mutant_data.size()},
+                    origin_index});
     }
     return true;
   }