You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
473 lines
18 KiB
473 lines
18 KiB
// Copyright 2023 The Centipede Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "./centipede/distill.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <numeric>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/base/thread_annotations.h"
|
|
#include "absl/container/flat_hash_set.h"
|
|
#include "absl/log/check.h"
|
|
#include "absl/log/log.h"
|
|
#include "absl/strings/str_cat.h"
|
|
#include "absl/strings/str_join.h"
|
|
#include "absl/synchronization/mutex.h"
|
|
#include "absl/time/time.h"
|
|
#include "./centipede/corpus_io.h"
|
|
#include "./centipede/environment.h"
|
|
#include "./centipede/feature.h"
|
|
#include "./centipede/feature_set.h"
|
|
#include "./centipede/periodic_action.h"
|
|
#include "./centipede/resource_pool.h"
|
|
#include "./centipede/rusage_profiler.h"
|
|
#include "./centipede/rusage_stats.h"
|
|
#include "./centipede/thread_pool.h"
|
|
#include "./centipede/util.h"
|
|
#include "./centipede/workdir.h"
|
|
#include "./common/blob_file.h"
|
|
#include "./common/defs.h"
|
|
#include "./common/hash.h"
|
|
#include "./common/logging.h"
|
|
#include "./common/remote_file.h"
|
|
#include "./common/status_macros.h"
|
|
|
|
namespace fuzztest::internal {
|
|
|
|
namespace {
|
|
|
|
// A corpus element. Consists of a fuzz test input and its matching features.
|
|
struct CorpusElt {
|
|
ByteArray input;
|
|
FeatureVec features;
|
|
|
|
CorpusElt(const ByteArray &input, FeatureVec features)
|
|
: input(input), features(std::move(features)) {}
|
|
|
|
// Movable, but not copyable for efficiency.
|
|
CorpusElt(const CorpusElt &) = delete;
|
|
CorpusElt &operator=(const CorpusElt &) = delete;
|
|
CorpusElt(CorpusElt &&) = default;
|
|
CorpusElt &operator=(CorpusElt &&) = default;
|
|
|
|
ByteArray PackedFeatures() const {
|
|
return PackFeaturesAndHash(input, features);
|
|
}
|
|
};
|
|
|
|
using CorpusEltVec = std::vector<CorpusElt>;
|
|
|
|
// The maximum number of threads reading input shards concurrently. This is
|
|
// mainly to prevent I/O congestion.
|
|
inline constexpr size_t kMaxReadingThreads = 50;
|
|
// The maximum number of threads writing shards concurrently. These in turn
|
|
// launch up to `kMaxReadingThreads` reading threads.
|
|
inline constexpr size_t kMaxWritingThreads = 100;
|
|
// A global cap on the total number of threads, both writing and reading. Unlike
|
|
// the other two limits, this one is purely to prevent too many threads in the
|
|
// process.
|
|
inline constexpr size_t kMaxTotalThreads = 5000;
|
|
static_assert(kMaxReadingThreads * kMaxWritingThreads <= kMaxTotalThreads);
|
|
|
|
inline constexpr MemSize kGB = 1024L * 1024L * 1024L;
|
|
// The total approximate amount of RAM to be shared by the concurrent threads.
|
|
// TODO(ussuri): Replace by a function of free RSS on the system.
|
|
inline constexpr RUsageMemory kRamQuota{/*mem_vsize=*/0, /*mem_vpeak=*/0,
|
|
/*mem_rss=*/25 * kGB};
|
|
// The amount of time that each thread will wait for enough RAM to be freed up
|
|
// by its concurrent siblings.
|
|
inline constexpr absl::Duration kRamLeaseTimeout = absl::Hours(5);
|
|
|
|
std::string LogPrefix(const Environment &env) {
|
|
return absl::StrCat("DISTILL[S.", env.my_shard_index, "]: ");
|
|
}
|
|
|
|
std::string LogPrefix() { return absl::StrCat("DISTILL[ALL]: "); }
|
|
|
|
// TODO(ussuri): Move the reader/writer classes to shard_reader.cc, rename it
|
|
// to corpus_io.cc, and reuse the new APIs where useful in the code base.
|
|
|
|
// A helper class for reading input corpus shards. Thread-safe.
|
|
class InputCorpusShardReader {
|
|
public:
|
|
InputCorpusShardReader(const Environment &env)
|
|
: workdir_{env}, log_prefix_{LogPrefix(env)} {}
|
|
|
|
MemSize EstimateRamFootprint(size_t shard_idx) const {
|
|
const auto corpus_path = workdir_.CorpusFilePaths().Shard(shard_idx);
|
|
const auto features_path = workdir_.FeaturesFilePaths().Shard(shard_idx);
|
|
const MemSize corpus_file_size = ValueOrDie(RemoteFileGetSize(corpus_path));
|
|
const MemSize features_file_size =
|
|
ValueOrDie(RemoteFileGetSize(features_path));
|
|
// Conservative compression factors for the two file types. These have been
|
|
// observed empirically for the Riegeli blob format. The legacy format is
|
|
// approximately 1:1, but use the stricter Riegeli numbers, as the legacy
|
|
// should be considered obsolete.
|
|
// TODO(b/322880269): Use the actual in-memory footprint once available.
|
|
constexpr double kMaxCorpusCompressionRatio = 5.0;
|
|
constexpr double kMaxFeaturesCompressionRatio = 10.0;
|
|
return corpus_file_size * kMaxCorpusCompressionRatio +
|
|
features_file_size * kMaxFeaturesCompressionRatio;
|
|
}
|
|
|
|
// Reads and returns a single shard's elements. Thread-safe.
|
|
CorpusEltVec ReadShard(size_t shard_idx) {
|
|
const auto corpus_path = workdir_.CorpusFilePaths().Shard(shard_idx);
|
|
const auto features_path = workdir_.FeaturesFilePaths().Shard(shard_idx);
|
|
VLOG(1) << log_prefix_ << "reading input shard " << shard_idx << ":\n"
|
|
<< VV(corpus_path) << "\n"
|
|
<< VV(features_path);
|
|
CorpusEltVec elts;
|
|
// Read elements from the current shard.
|
|
fuzztest::internal::ReadShard( //
|
|
corpus_path, features_path,
|
|
[&elts](ByteArray input, FeatureVec features) {
|
|
elts.emplace_back(std::move(input), std::move(features));
|
|
});
|
|
return elts;
|
|
}
|
|
|
|
private:
|
|
const WorkDir workdir_;
|
|
const std::string log_prefix_;
|
|
};
|
|
|
|
// A helper class for writing corpus shards. Thread-safe.
|
|
class CorpusShardWriter {
|
|
public:
|
|
// The writing stats so far.
|
|
struct Stats {
|
|
size_t num_total_elts = 0;
|
|
size_t num_written_elts = 0;
|
|
size_t num_written_batches = 0;
|
|
};
|
|
|
|
CorpusShardWriter(const Environment &env, bool append)
|
|
: workdir_{env},
|
|
log_prefix_{LogPrefix(env)},
|
|
corpus_path_{workdir_.DistilledCorpusFilePaths().MyShard()},
|
|
features_path_{workdir_.DistilledFeaturesFilePaths().MyShard()},
|
|
corpus_writer_{DefaultBlobFileWriterFactory()},
|
|
feature_writer_{DefaultBlobFileWriterFactory()} {
|
|
CHECK_OK(corpus_writer_->Open(corpus_path_, append ? "a" : "w"));
|
|
CHECK_OK(feature_writer_->Open(features_path_, append ? "a" : "w"));
|
|
}
|
|
|
|
virtual ~CorpusShardWriter() = default;
|
|
|
|
void WriteElt(CorpusElt elt) {
|
|
absl::MutexLock lock(&mu_);
|
|
WriteEltImpl(std::move(elt));
|
|
}
|
|
|
|
void WriteBatch(CorpusEltVec elts) {
|
|
absl::MutexLock lock(&mu_);
|
|
VLOG(1) << log_prefix_ << "writing " << elts.size()
|
|
<< " elements to output shard:\n"
|
|
<< VV(corpus_path_) << "\n"
|
|
<< VV(features_path_);
|
|
for (auto &elt : elts) {
|
|
WriteEltImpl(std::move(elt));
|
|
}
|
|
++stats_.num_written_batches;
|
|
}
|
|
|
|
Stats GetStats() const {
|
|
absl::MutexLock lock(&mu_);
|
|
return stats_;
|
|
}
|
|
|
|
protected:
|
|
// A behavior customization point: a derived class gets an opportunity to
|
|
// analyze and/or preprocess `elt` before it is written. For example, a
|
|
// derived class can trim the element's feature set before it is written, or
|
|
// choose to skip writing it entirely by returning `std::nullopt`.
|
|
virtual std::optional<CorpusElt> PreprocessElt(CorpusElt elt) {
|
|
return std::move(elt);
|
|
}
|
|
|
|
private:
|
|
void WriteEltImpl(CorpusElt elt) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_) {
|
|
++stats_.num_total_elts;
|
|
const auto preprocessed_elt = PreprocessElt(std::move(elt));
|
|
if (preprocessed_elt.has_value()) {
|
|
// Append to the distilled corpus and features files.
|
|
CHECK_OK(corpus_writer_->Write(preprocessed_elt->input));
|
|
CHECK_OK(feature_writer_->Write(preprocessed_elt->PackedFeatures()));
|
|
++stats_.num_written_elts;
|
|
}
|
|
}
|
|
|
|
// Const state.
|
|
const WorkDir workdir_;
|
|
const std::string log_prefix_;
|
|
const std::string corpus_path_;
|
|
const std::string features_path_;
|
|
|
|
// Mutable state.
|
|
mutable absl::Mutex mu_;
|
|
std::unique_ptr<BlobFileWriter> corpus_writer_ ABSL_GUARDED_BY(mu_);
|
|
std::unique_ptr<BlobFileWriter> feature_writer_ ABSL_GUARDED_BY(mu_);
|
|
Stats stats_ ABSL_GUARDED_BY(mu_);
|
|
};
|
|
|
|
// A distilling input filter:
|
|
// - Deduplicates byte-identical inputs: only the first one is allowed to pass.
|
|
// - Deduplicates feature-equivalent inputs: up to N from each equivalency set
|
|
// are allowed to pass.
|
|
// - Discards the specified set of "uninteresting" feature domains from the
|
|
// feature sets of filtered inputs.
|
|
class DistillingInputFilter {
|
|
public:
|
|
// An extension to the parent class's `Stats`.
|
|
struct Stats {
|
|
size_t num_total_elts = 0;
|
|
size_t num_byte_unique_elts = 0;
|
|
size_t num_feature_unique_elts = 0;
|
|
// The accumulated features of the distilled corpus so far, represents in
|
|
// the same compact textual form that Centipede uses in its fuzzing progress
|
|
// log messages, e.g.: "ft: 96331 cov: 81793 usr1: 5045 ...".
|
|
std::string coverage_str;
|
|
};
|
|
|
|
// `feature_equiv_redundancy` specifies how many inputs with equivalent
|
|
// feature sets are allowed to pass the filter. Any subsequent inputs with the
|
|
// equivalent set will be rejected.
|
|
// `should_discard_domains` specifies the domains that should be discarded
|
|
// from the feature set of a filtered input.
|
|
DistillingInputFilter( //
|
|
uint8_t feature_frequency_threshold,
|
|
const FeatureSet::FeatureDomainSet &domains_to_discard)
|
|
: seen_inputs_{},
|
|
seen_features_{
|
|
/*frequency_threshold=*/feature_frequency_threshold,
|
|
/*should_discard_domain=*/domains_to_discard,
|
|
} {}
|
|
|
|
std::optional<CorpusElt> FilterElt(CorpusElt elt) {
|
|
absl::MutexLock lock{&mu_};
|
|
|
|
++stats_.num_total_elts;
|
|
|
|
// Filter out approximately byte-identical inputs ("approximately" because
|
|
// we use hashes).
|
|
std::string hash = Hash(elt.input);
|
|
const auto [iter, inserted] = seen_inputs_.insert(std::move(hash));
|
|
if (!inserted) return std::nullopt;
|
|
++stats_.num_byte_unique_elts;
|
|
|
|
// Filter out feature-equivalent inputs.
|
|
seen_features_.PruneDiscardedDomains(elt.features);
|
|
if (!seen_features_.HasUnseenFeatures(elt.features)) return std::nullopt;
|
|
seen_features_.IncrementFrequencies(elt.features);
|
|
++stats_.num_feature_unique_elts;
|
|
|
|
return std::move(elt);
|
|
}
|
|
|
|
Stats GetStats() {
|
|
absl::MutexLock lock{&mu_};
|
|
std::stringstream ss;
|
|
ss << seen_features_;
|
|
stats_.coverage_str = std::move(ss).str();
|
|
return stats_;
|
|
}
|
|
|
|
private:
|
|
absl::Mutex mu_;
|
|
absl::flat_hash_set<std::string /*hash*/> seen_inputs_ ABSL_GUARDED_BY(mu_);
|
|
FeatureSet seen_features_ ABSL_GUARDED_BY(mu_);
|
|
Stats stats_ ABSL_GUARDED_BY(mu_);
|
|
};
|
|
|
|
// A helper class for writing distilled corpus shards. NOT thread-safe because
|
|
// all writes go to a single file.
|
|
class DistilledCorpusShardWriter : public CorpusShardWriter {
|
|
public:
|
|
DistilledCorpusShardWriter( //
|
|
const Environment &env, bool append, DistillingInputFilter &filter)
|
|
: CorpusShardWriter{env, append}, input_filter_{filter} {}
|
|
|
|
~DistilledCorpusShardWriter() override = default;
|
|
|
|
protected:
|
|
std::optional<CorpusElt> PreprocessElt(CorpusElt elt) override {
|
|
return input_filter_.FilterElt(std::move(elt));
|
|
}
|
|
|
|
private:
|
|
DistillingInputFilter &input_filter_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// Runs one independent distillation task. Reads shards in the order specified
|
|
// by `shard_indices`, distills inputs from them using `input_filter`, and
|
|
// writes the result to `WorkDir{env}.DistilledPath()`. Every task gets its own
|
|
// `env.my_shard_index`, and so every task creates its own independent distilled
|
|
// corpus file. `parallelism` is the maximum number of concurrent
|
|
// reading/writing threads. Values > 1 can cause non-determinism in which of the
|
|
// same-coverage inputs gets selected to be written to the output shard; set to
|
|
// 1 for tests.
|
|
void DistillToOneOutputShard( //
|
|
const Environment &env, //
|
|
const std::vector<size_t> &shard_indices, //
|
|
DistillingInputFilter &input_filter, //
|
|
ResourcePool<RUsageMemory> &ram_pool, //
|
|
int parallelism) {
|
|
LOG(INFO) << LogPrefix(env) << "Distilling to output shard "
|
|
<< env.my_shard_index << "; input shard indices:\n"
|
|
<< absl::StrJoin(shard_indices, ", ");
|
|
|
|
// Read and write the shards in parallel, but gate reading of each on the
|
|
// availability of free RAM to keep the peak RAM usage under control.
|
|
const size_t num_shards = shard_indices.size();
|
|
InputCorpusShardReader reader{env};
|
|
// NOTE: Always overwrite corpus and features files, never append.
|
|
DistilledCorpusShardWriter writer{env, /*append=*/false, input_filter};
|
|
|
|
{
|
|
ThreadPool threads{parallelism};
|
|
for (size_t shard_idx : shard_indices) {
|
|
threads.Schedule([shard_idx, &reader, &writer, &env, num_shards,
|
|
&ram_pool] {
|
|
const auto ram_lease = ram_pool.AcquireLeaseBlocking({
|
|
/*id=*/absl::StrCat("out_", env.my_shard_index, "/in_", shard_idx),
|
|
/*amount=*/
|
|
{/*mem_vsize=*/0, /*mem_vpeak=*/0,
|
|
/*mem_rss=*/reader.EstimateRamFootprint(shard_idx)},
|
|
/*timeout=*/kRamLeaseTimeout,
|
|
});
|
|
CHECK_OK(ram_lease.status());
|
|
|
|
CorpusEltVec shard_elts = reader.ReadShard(shard_idx);
|
|
// Reverse the order of elements. The intuition is as follows:
|
|
// * If the shard is the result of fuzzing with Centipede, the inputs
|
|
// that are closer to the end are more interesting, so we start there.
|
|
// * If the shard resulted from somethening else, the reverse order is
|
|
// not any better or worse than any other order.
|
|
std::reverse(shard_elts.begin(), shard_elts.end());
|
|
writer.WriteBatch(std::move(shard_elts));
|
|
const CorpusShardWriter::Stats shard_stats = writer.GetStats();
|
|
LOG(INFO) << LogPrefix(env)
|
|
<< "batches: " << shard_stats.num_written_batches << "/"
|
|
<< num_shards << " inputs: " << shard_stats.num_total_elts
|
|
<< " written: " << shard_stats.num_written_elts;
|
|
});
|
|
}
|
|
} // The threads join here.
|
|
|
|
LOG(INFO) << LogPrefix(env) << "Done distilling to output shard "
|
|
<< env.my_shard_index;
|
|
}
|
|
|
|
int Distill(const Environment &env, const DistillOptions &opts) {
|
|
RPROF_THIS_FUNCTION_WITH_TIMELAPSE( //
|
|
/*enable=*/ABSL_VLOG_IS_ON(1), //
|
|
/*timelapse_interval=*/absl::Seconds(ABSL_VLOG_IS_ON(2) ? 10 : 60), //
|
|
/*also_log_timelapses=*/ABSL_VLOG_IS_ON(10));
|
|
|
|
// Prepare the per-thread envs.
|
|
std::vector<Environment> envs_per_thread(env.num_threads, env);
|
|
for (size_t thread_idx = 0; thread_idx < env.num_threads; ++thread_idx) {
|
|
envs_per_thread[thread_idx].my_shard_index += thread_idx;
|
|
}
|
|
|
|
// Prepare the per-thread input shard indices. This assigns a randomized and
|
|
// shuffled subset of the input shards to each output shard writer. The subset
|
|
// sizes are roughly equal between the writers.
|
|
std::vector<std::vector<size_t>> shard_indices_per_thread(env.num_threads);
|
|
std::vector<size_t> all_shard_indices(env.total_shards);
|
|
std::iota(all_shard_indices.begin(), all_shard_indices.end(), 0);
|
|
Rng rng{GetRandomSeed(env.seed)};
|
|
std::shuffle(all_shard_indices.begin(), all_shard_indices.end(), rng);
|
|
size_t thread_idx = 0;
|
|
for (size_t shard_idx : all_shard_indices) {
|
|
shard_indices_per_thread[thread_idx].push_back(shard_idx);
|
|
thread_idx = (thread_idx + 1) % env.num_threads;
|
|
}
|
|
|
|
// Run the distillation threads in parallel.
|
|
{
|
|
// A global input filter shared by all output shard writers. The output
|
|
// shards will collectively contain a deduplicated set of byte- and
|
|
// feature-unique inputs.
|
|
DistillingInputFilter input_filter{
|
|
opts.feature_frequency_threshold,
|
|
env.MakeDomainDiscardMask(),
|
|
};
|
|
// A periodic logger of the global distillation progress. Runs on a separate
|
|
// thread.
|
|
PeriodicAction progress_logger{
|
|
[&input_filter]() {
|
|
const auto stats = input_filter.GetStats();
|
|
LOG(INFO) << LogPrefix() << stats.coverage_str
|
|
<< " inputs: " << stats.num_total_elts
|
|
<< " unique: " << stats.num_byte_unique_elts
|
|
<< " distilled: " << stats.num_feature_unique_elts;
|
|
},
|
|
// Seeing 0's at the beginning is not interesting, unless debugging.
|
|
// Likewise, increase the frequency --v >= 1 to aid debugging.
|
|
PeriodicAction::ConstDelayConstInterval(
|
|
absl::Seconds(ABSL_VLOG_IS_ON(1) ? 0 : 60),
|
|
absl::Seconds(ABSL_VLOG_IS_ON(1) ? 10 : 60)),
|
|
};
|
|
// The RAM pool shared between all the `DistillToOneOutputShard()` threads.
|
|
ResourcePool ram_pool{kRamQuota};
|
|
const size_t num_threads = std::min(env.num_threads, kMaxWritingThreads);
|
|
ThreadPool threads{static_cast<int>(num_threads)};
|
|
for (size_t thread_idx = 0; thread_idx < env.num_threads; ++thread_idx) {
|
|
threads.Schedule(
|
|
[&thread_env = envs_per_thread[thread_idx],
|
|
&thread_shard_indices = shard_indices_per_thread[thread_idx],
|
|
&input_filter, &progress_logger, &ram_pool]() {
|
|
DistillToOneOutputShard( //
|
|
thread_env, thread_shard_indices, input_filter, ram_pool,
|
|
kMaxReadingThreads);
|
|
// In addition to periodic progress reports, also log the progress
|
|
// after writing each output shard.
|
|
progress_logger.Nudge();
|
|
});
|
|
}
|
|
} // The threads join here.
|
|
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
void DistillForTests(const Environment &env,
|
|
const std::vector<size_t> &shard_indices) {
|
|
DistillingInputFilter input_filter{
|
|
/*feature_frequency_threshold=*/1,
|
|
env.MakeDomainDiscardMask(),
|
|
};
|
|
// Do not limit the max RAM.
|
|
ResourcePool ram_pool{RUsageMemory::Max()};
|
|
// Read the input shards sequentially and in order to ensure deterministic
|
|
// outputs.
|
|
DistillToOneOutputShard( //
|
|
env, shard_indices, input_filter, ram_pool, /*parallelism=*/1);
|
|
}
|
|
|
|
} // namespace fuzztest::internal
|
|
|