Clean up the crash minimizer interface and rewrite the implementation. This is for minimizing crashes during corpus database update, which needs crash signature matching. PiperOrigin-RevId: 992342097
diff --git a/centipede/BUILD b/centipede/BUILD index 6e1f7a1..3c045ca 100644 --- a/centipede/BUILD +++ b/centipede/BUILD
@@ -380,11 +380,10 @@ ":stop", ":thread_pool", ":util", - ":workdir", "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/synchronization", "@com_google_fuzztest//common:defs", - "@com_google_fuzztest//common:hash", "@com_google_fuzztest//common:logging", ], ) @@ -775,6 +774,7 @@ ":minimize_crash", ":pc_info", ":periodic_action", + ":runner_result", ":seed_corpus_maker_lib", ":stats", ":stop", @@ -1522,9 +1522,9 @@ ":minimize_crash", ":runner_result", ":stop", - ":util", - ":workdir", "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/random", "@abseil-cpp//absl/types:span", "@com_google_fuzztest//common:defs", "@com_google_fuzztest//common:test_util",
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc index e6ba1eb..4cd9f75 100644 --- a/centipede/centipede_interface.cc +++ b/centipede/centipede_interface.cc
@@ -26,6 +26,7 @@ #include <memory> #include <string> #include <string_view> +#include <system_error> // NOLINT #include <utility> #include <vector> @@ -55,6 +56,7 @@ #include "./centipede/minimize_crash.h" #include "./centipede/pc_info.h" #include "./centipede/periodic_action.h" +#include "./centipede/runner_result.h" #include "./centipede/seed_corpus_maker_lib.h" #include "./centipede/stats.h" #include "./centipede/stop.h" @@ -109,6 +111,58 @@ } } +void MinimizeCrash(const Environment& env, + CentipedeCallbacksFactory& callbacks_factory, + StopCondition& stop_condition) { + ByteArray crashy_input; + ReadFromLocalFile(env.minimize_crash_file_path, crashy_input); + + BatchResult batch_result; + { + ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env, + stop_condition); + CreateLocalDirRemovedAtExit(TemporaryLocalDirPath()); + if (scoped_callbacks.callbacks()->Execute(env.binary, {crashy_input}, + batch_result)) { + FUZZTEST_LOG(ERROR) << "The original crashy input did not crash; exiting"; + stop_condition.RequestStop(EXIT_FAILURE, + "The original crashy input did not crash"); + return; + } + } + + const auto result = + MinimizeCrash(crashy_input, env, callbacks_factory, + batch_result.failure_signature(), stop_condition); + if (!result.has_value()) { + stop_condition.RequestStop(EXIT_FAILURE, "No smaller crash found"); + return; + } + + const auto output_dir = + std::filesystem::path{WorkDir{env}.CrashReproducerDirPaths().MyShard()}; + const auto mkdir_status = RemoteMkdir(output_dir.string()); + if (!mkdir_status.ok()) { + const auto error_msg = + absl::StrCat("Failed to create the minimized crasher dir ", + output_dir.string(), ": ", mkdir_status); + FUZZTEST_LOG(ERROR) << error_msg; + stop_condition.RequestStop(EXIT_FAILURE, error_msg); + return; + } + const auto result_input_hash = Hash(result->input); + const auto output_path = (output_dir / result_input_hash).string(); + const auto write_status = RemoteFileSetContents(output_path, result->input); + if (!write_status.ok()) { + const auto error_msg = + absl::StrCat("Failed to write the minimized crasher to ", output_path, + ": ", write_status); + FUZZTEST_LOG(ERROR) << error_msg; + stop_condition.RequestStop(EXIT_FAILURE, error_msg); + return; + } +} + // Loads corpora from work dirs provided in `env.args`, if there are two args // provided, analyzes differences. If there is one arg provided, reports the // function coverage. Returns EXIT_SUCCESS on success, EXIT_FAILURE otherwise. @@ -755,9 +809,7 @@ } if (!env.minimize_crash_file_path.empty()) { - ByteArray crashy_input; - ReadFromLocalFile(env.minimize_crash_file_path, crashy_input); - MinimizeCrash(crashy_input, env, callbacks_factory, *stop_condition); + MinimizeCrash(env, callbacks_factory, *stop_condition); return SaveStopReasonAndGetExitCode(); }
diff --git a/centipede/minimize_crash.cc b/centipede/minimize_crash.cc index 4399dcb..66ef9a5 100644 --- a/centipede/minimize_crash.cc +++ b/centipede/minimize_crash.cc
@@ -17,12 +17,14 @@ #include <algorithm> #include <cstddef> #include <cstdlib> -#include <filesystem> // NOLINT +#include <optional> #include <string> #include <string_view> +#include <utility> #include <vector> #include "absl/base/thread_annotations.h" +#include "absl/container/flat_hash_set.h" #include "absl/synchronization/mutex.h" #include "./centipede/centipede_callbacks.h" #include "./centipede/environment.h" @@ -31,64 +33,57 @@ #include "./centipede/stop.h" #include "./centipede/thread_pool.h" #include "./centipede/util.h" -#include "./centipede/workdir.h" #include "./common/defs.h" -#include "./common/hash.h" #include "./common/logging.h" // IWYU pragma: keep namespace fuzztest::internal { -// Work queue for the minimizer. +namespace { + +// The minimizer state shared by all worker threads. // Thread-safe. -struct MinimizerWorkQueue { +class MinimizerState { public: - // Creates the queue. - // `crash_dir_path` is the directory path where new crashers are written. - // `crasher` is the initial crashy input. - MinimizerWorkQueue(const std::string_view crash_dir_path, - const ByteArray crasher) - : crash_dir_path_(crash_dir_path), crashers_{ByteArray(crasher)} { - std::filesystem::create_directory(crash_dir_path_); + MinimizerState(size_t capacity, ByteSpan initial_crasher) + : capacity_(capacity), + crashers_{{initial_crasher.begin(), initial_crasher.end()}} {} + + std::vector<ByteArray> GetCurrentCrashers() const { + absl::MutexLock lock(mutex_); + return {crashers_.begin(), crashers_.end()}; } - // Returns up to `max_num_crashers` most recently added crashers. - std::vector<ByteArray> GetRecentCrashers(size_t max_num_crashers) { + void AddCrasher(ByteArray crasher, std::string description) { absl::MutexLock lock(mutex_); - size_t num_crashers_to_return = - std::min(crashers_.size(), max_num_crashers); - return {crashers_.end() - num_crashers_to_return, crashers_.end()}; + if (!result_.has_value() || crasher.size() < result_->input.size()) { + result_ = {crasher, std::move(description)}; + } + crashers_.insert(std::move(crasher)); + while (crashers_.size() > capacity_) { + crashers_.erase(std::max_element( + crashers_.begin(), crashers_.end(), + [](const auto& a, const auto& b) { return a.size() < b.size(); })); + } } - // Adds `crasher` to the queue, writes it to `crash_dir_path_/Hash(crasher)`. - // The crasher must be smaller than the original one. - void AddCrasher(ByteArray crasher) { + std::optional<MinimizeCrashResult> GetResult() const { absl::MutexLock lock(mutex_); - FUZZTEST_CHECK_LT(crasher.size(), crashers_.front().size()); - crashers_.emplace_back(crasher); - // Write the crasher to disk. - auto hash = Hash(crasher); - auto dir = crash_dir_path_; - std::string file_path = dir.append(hash); - WriteToLocalFile(file_path, crasher); - } - - // Returns true if new smaller crashes were found. - bool SmallerCrashesFound() const { - absl::MutexLock lock(mutex_); - return crashers_.size() > 1; + return result_; } private: mutable absl::Mutex mutex_; - const std::filesystem::path crash_dir_path_; - std::vector<ByteArray> crashers_ ABSL_GUARDED_BY(mutex_); + size_t capacity_ ABSL_GUARDED_BY(mutex_); + // Keep at most `capacity_` crashers to avoid being stuck in local minimum. + absl::flat_hash_set<ByteArray> crashers_ ABSL_GUARDED_BY(mutex_); + std::optional<MinimizeCrashResult> result_ ABSL_GUARDED_BY(mutex_); }; -// Performs a minimization loop in one thread. -static void MinimizeCrash(const Environment& env, - CentipedeCallbacksFactory& callbacks_factory, - MinimizerWorkQueue& queue, - StopCondition& stop_condition) { +void MinimizeCrashInOneThread(const Environment& env, + CentipedeCallbacksFactory& callbacks_factory, + std::string_view crash_signature, + MinimizerState& state, + StopCondition& stop_condition) { ScopedCentipedeCallbacks scoped_callback(callbacks_factory, env, stop_condition); auto callbacks = scoped_callback.callbacks(); @@ -96,84 +91,86 @@ size_t num_batches = env.num_runs / env.batch_size; for (size_t i = 0; i < num_batches; ++i) { + if (stop_condition.ShouldStop()) break; FUZZTEST_LOG_EVERY_POW_2(INFO) << "[" << i << "] Minimizing... Interrupt to stop"; - if (stop_condition.ShouldStop()) break; - // Get up to kMaxNumCrashersToGet most recent crashers. We don't want just - // the most recent crasher to avoid being stuck in local minimum. - constexpr size_t kMaxNumCrashersToGet = 20; - const auto recent_crashers = queue.GetRecentCrashers(kMaxNumCrashersToGet); - FUZZTEST_CHECK(!recent_crashers.empty()); + + const auto crashers = state.GetCurrentCrashers(); + FUZZTEST_CHECK(!crashers.empty()); // Compute the minimal known crasher size. - size_t min_known_size = recent_crashers.front().size(); - for (const auto &crasher : recent_crashers) { + size_t min_known_size = crashers.front().size(); + for (const auto& crasher : crashers) { min_known_size = std::min(min_known_size, crasher.size()); } + std::vector<ByteSpan> smaller_mutants; // Create several mutants that are smaller than the current smallest one. // // Currently, we do this by calling the vanilla mutator and // discarding all inputs that are too large. - // TODO(kcc): modify the Mutate() interface such that max_len can be passed. // + // TODO(xinhaoyuan): modify the Mutate() interface such that size hint can + // be passed. const std::vector<Mutant> mutants = callbacks->Mutate( - GetMutationInputRefsFromDataInputs(recent_crashers), env.batch_size); - std::vector<ByteSpan> smaller_mutants; - for (const auto &m : mutants) { - if (m.data.size() < min_known_size) smaller_mutants.push_back(m.data); + GetMutationInputRefsFromDataInputs(crashers), env.batch_size); + for (const auto& m : mutants) { + if (m.data.size() < min_known_size) { + smaller_mutants.push_back(m.data); + } } - // Execute all mutants. If a new crasher is found, add it to `queue`. - if (!callbacks->Execute(env.binary, smaller_mutants, batch_result)) { - size_t crash_inputs_idx = batch_result.num_outputs_read(); - FUZZTEST_CHECK_LT(crash_inputs_idx, smaller_mutants.size()); - const auto new_crasher = smaller_mutants[crash_inputs_idx]; - FUZZTEST_LOG(INFO) << "Crasher: size: " << new_crasher.size() << ": " - << AsPrintableString(new_crasher, /*max_len=*/40); - queue.AddCrasher({new_crasher.begin(), new_crasher.end()}); + if (smaller_mutants.empty()) { + continue; } + + // Try smaller mutants first to minimize the size of the new crasher. + std::sort(smaller_mutants.begin(), smaller_mutants.end(), + [](const auto& a, const auto& b) { return a.size() < b.size(); }); + + // Execute all mutants. If a new crasher is found, add it to `state`. + if (callbacks->Execute(env.binary, smaller_mutants, batch_result)) { + continue; + } + + if (batch_result.failure_signature() != crash_signature) { + continue; + } + + size_t crash_inputs_idx = batch_result.num_outputs_read(); + FUZZTEST_CHECK_LT(crash_inputs_idx, smaller_mutants.size()); + const auto& new_crasher = smaller_mutants[crash_inputs_idx]; + FUZZTEST_LOG(INFO) << "Crasher: size: " << new_crasher.size() << ": " + << AsPrintableString(new_crasher, /*max_len=*/40); + state.AddCrasher({new_crasher.begin(), new_crasher.end()}, + std::move(batch_result.failure_description())); } } -void MinimizeCrash(ByteSpan crashy_input, const Environment& env, - CentipedeCallbacksFactory& callbacks_factory, - StopCondition& stop_condition) { - ScopedCentipedeCallbacks scoped_callback(callbacks_factory, env, - stop_condition); - auto callbacks = scoped_callback.callbacks(); +} // namespace - FUZZTEST_LOG(INFO) << "MinimizeCrash: trying the original crashy input"; - CreateLocalDirRemovedAtExit(TemporaryLocalDirPath()); - - BatchResult batch_result; - ByteArray original_crashy_input(crashy_input.begin(), crashy_input.end()); - if (callbacks->Execute(env.binary, {original_crashy_input}, batch_result)) { - FUZZTEST_LOG(INFO) << "The original crashy input did not crash; exiting"; - stop_condition.RequestStop(EXIT_FAILURE, - "The original crashy input did not crash"); - return; - } - +std::optional<MinimizeCrashResult> MinimizeCrash( + ByteSpan crashy_input, const Environment& env, + CentipedeCallbacksFactory& callbacks_factory, + std::string_view crash_signature, StopCondition& stop_condition) { FUZZTEST_LOG(INFO) << "Starting the crash minimization loop in " - << env.num_threads << "threads"; + << env.num_threads << " threads"; - MinimizerWorkQueue queue(WorkDir{env}.CrashReproducerDirPaths().MyShard(), - original_crashy_input); + // Minimize with 20 intermediate crashers empirically - may be adjusted later. + MinimizerState state(/*capacity=*/20, crashy_input); { ThreadPool threads{static_cast<int>(env.num_threads)}; for (size_t i = 0; i < env.num_threads; ++i) { - threads.Schedule([&env, &callbacks_factory, &queue, &stop_condition]() { + threads.Schedule([&env, &callbacks_factory, &state, &stop_condition, + crash_signature]() { CreateLocalDirRemovedAtExit(TemporaryLocalDirPath()); - MinimizeCrash(env, callbacks_factory, queue, stop_condition); + MinimizeCrashInOneThread(env, callbacks_factory, crash_signature, state, + stop_condition); }); } } // The threads join here. - if (stop_condition.StopRequested()) return; - if (!queue.SmallerCrashesFound()) { - stop_condition.RequestStop(EXIT_FAILURE, "Smaller crashes not found"); - } + return state.GetResult(); } } // namespace fuzztest::internal
diff --git a/centipede/minimize_crash.h b/centipede/minimize_crash.h index a7e21d5..5cc20b2 100644 --- a/centipede/minimize_crash.h +++ b/centipede/minimize_crash.h
@@ -15,6 +15,10 @@ #ifndef THIRD_PARTY_CENTIPEDE_MINIMIZE_CRASH_H_ #define THIRD_PARTY_CENTIPEDE_MINIMIZE_CRASH_H_ +#include <optional> +#include <string> +#include <string_view> + #include "./centipede/centipede_callbacks.h" #include "./centipede/environment.h" #include "./centipede/stop.h" @@ -22,14 +26,19 @@ namespace fuzztest::internal { -// Tries to minimize `crashy_input`. +struct MinimizeCrashResult { + ByteArray input; + std::string description; +}; + +// Tries to minimize `crashy_input` with `crash_signature`. // Uses `callbacks_factory` to create `env.num_threads` workers. -// Requests to stop with EXIT_FAILURE using `stop_condition` if no smaller -// crasher was found or if the original input didn't crash. Stores the newly -// found crashy inputs in `WorkDir{env}.CrashReproducerDirPath()`. -void MinimizeCrash(ByteSpan crashy_input, const Environment& env, - CentipedeCallbacksFactory& callbacks_factory, - StopCondition& stop_condition); +// Returns a minimized crash if found, otherwise nullopt. +// Stops when `stop_condition` is requested/due. +std::optional<MinimizeCrashResult> MinimizeCrash( + ByteSpan crashy_input, const Environment& env, + CentipedeCallbacksFactory& callbacks_factory, + std::string_view crash_signature, StopCondition& stop_condition); } // namespace fuzztest::internal
diff --git a/centipede/minimize_crash_test.cc b/centipede/minimize_crash_test.cc index 80dc5c0..5b27047 100644 --- a/centipede/minimize_crash_test.cc +++ b/centipede/minimize_crash_test.cc
@@ -15,32 +15,33 @@ #include "./centipede/minimize_crash.h" #include <cstdlib> -#include <filesystem> // NOLINT #include <string> #include <string_view> -#include <vector> +#include <utility> #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/base/nullability.h" +#include "absl/container/flat_hash_set.h" +#include "absl/random/random.h" #include "absl/types/span.h" #include "./centipede/centipede_callbacks.h" #include "./centipede/environment.h" #include "./centipede/runner_result.h" #include "./centipede/stop.h" -#include "./centipede/util.h" -#include "./centipede/workdir.h" #include "./common/defs.h" #include "./common/test_util.h" namespace fuzztest::internal { namespace { +using ::testing::UnorderedElementsAre; + // A mock for CentipedeCallbacks. class MinimizerMock : public CentipedeCallbacks { public: - MinimizerMock(const Environment& env) - : CentipedeCallbacks(env, internal_stop_condition_) {} + MinimizerMock(const Environment& env, StopCondition& stop_condition) + : CentipedeCallbacks(env, stop_condition) {} // Runs FuzzMe() on every input, imitates failure if FuzzMe() returns true. bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs, @@ -49,6 +50,11 @@ for (auto input : inputs) { if (FuzzMe(input)) { batch_result.exit_code() = EXIT_FAILURE; + // Set signature differently to test signature matching behavior. + batch_result.failure_signature() = + input[0] == 'f' ? "signature one" : "signature two"; + batch_result.failure_description() = + input[0] == 'f' ? "description one" : "description two"; return false; } ++batch_result.num_outputs_read(); @@ -57,18 +63,16 @@ } private: - // Returns true on inputs that look like 'f???u???z', false otherwise. - // The minimal input on which this function returns true is 'fuz'. + // Returns true on inputs that look like '[fz]+', false otherwise. + // The minimal input on which this function returns true is 'f' or 'z', with + // different crash signatures. bool FuzzMe(ByteSpan data) { if (data.empty()) return false; - if (data.front() == 'f' && data[data.size() / 2] == 'u' && - data.back() == 'z') { - return true; + for (const auto c : data) { + if (c != 'f' && c != 'z') return false; } - return false; + return true; } - - StopCondition internal_stop_condition_; }; // Factory that creates/destroys MinimizerMock. @@ -76,56 +80,78 @@ public: CentipedeCallbacks* absl_nonnull create( const Environment& env, StopCondition& stop_condition) override { - return new MinimizerMock(env); + return new MinimizerMock(env, stop_condition); } void destroy(CentipedeCallbacks *cb) override { delete cb; } }; -TEST(MinimizeTest, MinimizeTest) { - TempDir tmp_dir{test_info_->name()}; +TEST(MinimizeTest, FailsWhenCrasherCannotBeMinimized) { Environment env; - env.workdir = tmp_dir.path(); env.num_runs = 100000; - const WorkDir wd{env}; MinimizerMockFactory factory; StopCondition stop_condition; StopCondition::StopRequest stop_request; - // Test with a non-crashy input. + const ByteArray expected_minimized = {'f'}; stop_request = {}; - MinimizeCrash({1, 2, 3}, env, factory, stop_condition); - (void)stop_condition.StopRequested(&stop_request); - EXPECT_EQ(stop_request.exit_code, EXIT_FAILURE); + EXPECT_FALSE(MinimizeCrash(expected_minimized, env, factory, "signature one", + stop_condition) + .has_value()); + EXPECT_FALSE(stop_condition.StopRequested()); +} - ByteArray expected_minimized = {'f', 'u', 'z'}; +TEST(MinimizeTest, FailsWhenSignatureDoesNotMatch) { + Environment env; + env.num_runs = 100000; + MinimizerMockFactory factory; + StopCondition stop_condition; + StopCondition::StopRequest stop_request; - // Test with a crashy input that can't be minimized further. - stop_condition.ClearStopRequest(); + ByteArray original_crasher = {'f', 'f', 'f', 'f', 'f', 'f', + 'z', 'z', 'z', 'z', 'z', 'z'}; stop_request = {}; - MinimizeCrash(expected_minimized, env, factory, stop_condition); - (void)stop_condition.StopRequested(&stop_request); - EXPECT_EQ(stop_request.exit_code, EXIT_FAILURE); + EXPECT_FALSE(MinimizeCrash(original_crasher, env, factory, "bad signature", + stop_condition) + .has_value()); + EXPECT_FALSE(stop_condition.StopRequested()); +} - // Test the actual minimization. - ByteArray original_crasher = {'f', '.', '.', '.', '.', '.', '.', '.', - '.', '.', '.', 'u', '.', '.', '.', '.', - '.', '.', '.', '.', '.', '.', 'z'}; - stop_condition.ClearStopRequest(); - stop_request = {}; - MinimizeCrash(original_crasher, env, factory, stop_condition); - (void)stop_condition.StopRequested(&stop_request); - EXPECT_EQ(stop_request.exit_code, EXIT_SUCCESS); - // Collect the new crashers from the crasher dir. - std::vector<ByteArray> crashers; - for (auto const &dir_entry : std::filesystem::directory_iterator{ - wd.CrashReproducerDirPaths().MyShard()}) { - ByteArray crasher; - const std::string &path = dir_entry.path(); - ReadFromLocalFile(path, crasher); - EXPECT_LT(crasher.size(), original_crasher.size()); - crashers.push_back(crasher); +TEST(MinimizeTest, MinimizesWithSignature) { + TempDir tmp_dir{test_info_->name()}; + Environment env; + env.num_runs = 100000; + MinimizerMockFactory factory; + + ByteArray original_crasher = {'f', 'f', 'f', 'f', 'f', 'f', + 'z', 'z', 'z', 'z', 'z', 'z'}; + constexpr size_t kNumTrials = 30; + absl::BitGen rng; + absl::flat_hash_set<ByteArray> minimized_crashers; + StopCondition stop_condition; + for (size_t i = 0; i < kNumTrials; ++i) { + env.seed = rng(); + auto result = MinimizeCrash(original_crasher, env, factory, "signature one", + stop_condition); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->description, "description one"); + EXPECT_FALSE(stop_condition.StopRequested()); + minimized_crashers.insert(std::move(result->input)); } - EXPECT_THAT(crashers, testing::Contains(expected_minimized)); + EXPECT_THAT(minimized_crashers, UnorderedElementsAre(ByteArray{'f'})); + + minimized_crashers.clear(); + ByteArray original_crasher_alt = {'z', 'z', 'z', 'z', 'z', 'z', + 'f', 'f', 'f', 'f', 'f', 'f'}; + for (size_t i = 0; i < kNumTrials; ++i) { + env.seed = rng(); + auto result = MinimizeCrash(original_crasher_alt, env, factory, + "signature two", stop_condition); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->description, "description two"); + EXPECT_FALSE(stop_condition.StopRequested()); + minimized_crashers.insert(std::move(result->input)); + } + EXPECT_THAT(minimized_crashers, UnorderedElementsAre(ByteArray{'z'})); } } // namespace
diff --git a/centipede/test_util.sh b/centipede/test_util.sh index 6983a52..672bb54 100644 --- a/centipede/test_util.sh +++ b/centipede/test_util.sh
@@ -104,7 +104,8 @@ local -r file="$2" local -r expected_found="$3" # Make the shell option change below local. - local -r saved_opts="$(set +o)" + local -r saved_short_opts="$-" + local -r saved_long_opts="$(set +o)" set -o pipefail if ! fileop ls "${file}" > /dev/null; then die "Expected file ${file} doesn't exist" @@ -127,7 +128,8 @@ die "^^^ File ${file} contains unexpected regex /${regex}/" fi fi - eval "${saved_opts}" + eval "${saved_long_opts}" + set -"${saved_short_opts}" } # Makes sure that string "$1" exists in file "$2". Works for local and CNS.
diff --git a/centipede/testing/minimize_crash_test.sh b/centipede/testing/minimize_crash_test.sh index 7505164..eed107d 100755 --- a/centipede/testing/minimize_crash_test.sh +++ b/centipede/testing/minimize_crash_test.sh
@@ -16,7 +16,7 @@ # Tests the --minimize_crash flag. -set -eu +set -euo pipefail if ! [[ -e "${TEST_UTIL_SH:=$(dirname "$0")/../test_util.sh}" ]]; then echo "TEST_UTIL_SH must be set to the location of :test_util_sh" >&2