diff --git a/CMakePresets.json b/CMakePresets.json index f2df4ba9c7c1..6d8bc50b70fa 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -187,6 +187,20 @@ "Halide_LLVM_ROOT": "/opt/homebrew/opt/llvm" } }, + { + "name": "macOS-fuzz", + "displayName": "macOS (Fuzz)", + "description": "macOS fuzzing build", + "inherits": "macOS", + "toolchainFile": "${sourceDir}/cmake/toolchain.macos-homebrew.cmake", + "cacheVariables": { + "CMAKE_C_FLAGS": "-fsanitize=fuzzer-no-link -O1 -g3", + "CMAKE_CXX_FLAGS": "-fsanitize=fuzzer-no-link -O1 -g3", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=fuzzer", + "CMAKE_MODULE_LINKER_FLAGS": "-fsanitize=fuzzer", + "CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=fuzzer" + } + }, { "name": "macOS-vcpkg", "inherits": [ diff --git a/Makefile b/Makefile index c668cf20fdcd..4e5cb36364f8 100644 --- a/Makefile +++ b/Makefile @@ -1284,6 +1284,7 @@ PERFORMANCE_TESTS = $(shell ls $(ROOT_DIR)/test/performance/*.cpp) ERROR_TESTS = $(shell ls $(ROOT_DIR)/test/error/*.cpp) WARNING_TESTS = $(shell ls $(ROOT_DIR)/test/warning/*.cpp) RUNTIME_TESTS = $(shell ls $(ROOT_DIR)/test/runtime/*.cpp) +FUZZ_TESTS = $(filter-out %halide_fuzz_main.cpp, $(shell ls $(ROOT_DIR)/test/fuzz/*.cpp)) GENERATOR_EXTERNAL_TESTS := $(shell ls $(ROOT_DIR)/test/generator/*test.cpp) GENERATOR_EXTERNAL_TEST_GENERATOR := $(shell ls $(ROOT_DIR)/test/generator/*_generator.cpp) TUTORIALS = $(filter-out %_generate.cpp, $(shell ls $(ROOT_DIR)/tutorial/*.cpp)) @@ -1297,6 +1298,7 @@ test_performance: $(PERFORMANCE_TESTS:$(ROOT_DIR)/test/performance/%.cpp=perform test_error: $(ERROR_TESTS:$(ROOT_DIR)/test/error/%.cpp=error_%) test_warning: $(WARNING_TESTS:$(ROOT_DIR)/test/warning/%.cpp=warning_%) test_runtime: $(RUNTIME_TESTS:$(ROOT_DIR)/test/runtime/%.cpp=runtime_%) +test_fuzz: $(FUZZ_TESTS:$(ROOT_DIR)/test/fuzz/%.cpp=fuzz_%) test_tutorial: $(TUTORIALS:$(ROOT_DIR)/tutorial/%.cpp=tutorial_%) test_valgrind: $(CORRECTNESS_TESTS:$(ROOT_DIR)/test/correctness/%.cpp=valgrind_%) test_avx512: $(CORRECTNESS_TESTS:$(ROOT_DIR)/test/correctness/%.cpp=avx512_%) @@ -1392,6 +1394,7 @@ build_tests: $(CORRECTNESS_TESTS:$(ROOT_DIR)/test/correctness/%.cpp=$(BIN_DIR)/c $(ERROR_TESTS:$(ROOT_DIR)/test/error/%.cpp=$(BIN_DIR)/error_%) \ $(WARNING_TESTS:$(ROOT_DIR)/test/warning/%.cpp=$(BIN_DIR)/warning_%) \ $(RUNTIME_TESTS:$(ROOT_DIR)/test/runtime/%.cpp=$(BIN_DIR)/runtime_%) \ + $(FUZZ_TESTS:$(ROOT_DIR)/test/fuzz/%.cpp=$(BIN_DIR)/fuzz_%) \ $(GENERATOR_EXTERNAL_TESTS:$(ROOT_DIR)/test/generator/%_aottest.cpp=$(BIN_DIR)/$(TARGET)/generator_aot_%) \ $(GENERATOR_EXTERNAL_TESTS:$(ROOT_DIR)/test/generator/%_jittest.cpp=$(BIN_DIR)/generator_jit_%) \ $(MULLAPUDI2016_TESTS:$(ROOT_DIR)/test/autoschedulers/mullapudi2016/%.cpp=$(BIN_DIR)/mullapudi2016_%) \ @@ -1470,6 +1473,9 @@ $(BIN_DIR)/$(TARGET)/correctness_opencl_runtime: $(ROOT_DIR)/test/correctness/op $(BIN_DIR)/performance_%: $(ROOT_DIR)/test/performance/%.cpp $(TEST_DEPS) $(CXX) $(TEST_CXX_FLAGS) $(OPTIMIZE) $< -I$(INCLUDE_DIR) -I$(ROOT_DIR)/src/runtime -I$(ROOT_DIR)/test/common $(TEST_LD_FLAGS) -o $@ +$(BIN_DIR)/fuzz_%: $(ROOT_DIR)/test/fuzz/%.cpp $(ROOT_DIR)/test/fuzz/halide_fuzz_main.cpp $(ROOT_DIR)/test/fuzz/fuzz_helpers.h $(ROOT_DIR)/test/fuzz/halide_fuzz_main.h $(TEST_DEPS) + $(CXX) $(TEST_CXX_FLAGS) -I$(ROOT_DIR)/src/runtime -I$(ROOT_DIR)/test/common $(OPTIMIZE_FOR_BUILD_TIME) $(filter %.cpp,$^) -I$(INCLUDE_DIR) $(TEST_LD_FLAGS) -o $@ -DHALIDE_FUZZER_BACKEND=0 + # Error tests that link against libHalide $(BIN_DIR)/error_%: $(ROOT_DIR)/test/error/%.cpp $(TEST_DEPS) $(CXX) $(TEST_CXX_FLAGS) -I$(ROOT_DIR)/src/runtime -I$(ROOT_DIR)/test/common $(OPTIMIZE_FOR_BUILD_TIME) $< -I$(INCLUDE_DIR) $(TEST_LD_FLAGS) -o $@ @@ -2063,6 +2069,11 @@ quiet_correctness_%: $(BIN_DIR)/correctness_% @-mkdir -p $(TMP_DIR) @cd $(TMP_DIR) ; ( $(CURDIR)/$< 2>stderr_$*.txt > stdout_$*.txt && echo -n . ) || ( echo ; echo FAILED TEST: $* ; cat stdout_$*.txt stderr_$*.txt ; false ) +fuzz_%: $(BIN_DIR)/fuzz_% + @-mkdir -p $(TMP_DIR) + cd $(TMP_DIR) ; $(CURDIR)/$< + @-echo + valgrind_%: $(BIN_DIR)/correctness_% @-mkdir -p $(TMP_DIR) cd $(TMP_DIR) ; valgrind --error-exitcode=-1 $(CURDIR)/$< diff --git a/cmake/toolchain.macos-homebrew.cmake b/cmake/toolchain.macos-homebrew.cmake new file mode 100644 index 000000000000..60bac1270558 --- /dev/null +++ b/cmake/toolchain.macos-homebrew.cmake @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.28) + +set(CMAKE_C_COMPILER /opt/homebrew/opt/llvm@21/bin/clang) +set(CMAKE_CXX_COMPILER /opt/homebrew/opt/llvm@21/bin/clang++) + +if (NOT DEFINED CMAKE_SYSROOT) + execute_process( + COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE CMAKE_SYSROOT + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) +endif () + +set(CMAKE_SYSROOT "${CMAKE_SYSROOT}" CACHE PATH "") + +if (NOT DEFINED XC_TOOLCHAIN_PATH) + execute_process( + COMMAND xcrun --find clang + OUTPUT_VARIABLE XC_TOOLCHAIN_PATH + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) + cmake_path(SET XC_TOOLCHAIN_PATH NORMALIZE "${XC_TOOLCHAIN_PATH}/../../..") +endif () + +set(XC_TOOLCHAIN_PATH "${XC_TOOLCHAIN_PATH}" CACHE PATH "") + +set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES + "${CMAKE_SYSROOT}/usr/include" + "${XC_TOOLCHAIN_PATH}/usr/include" + "${CMAKE_SYSROOT}/System/Library/Frameworks" + "${CMAKE_SYSROOT}/System/Library/SubFrameworks" +) + +set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES + "${CMAKE_SYSROOT}/usr/include/c++/v1" + ${CMAKE_C_STANDARD_INCLUDE_DIRECTORIES} +) diff --git a/run-clang-tidy.sh b/run-clang-tidy.sh index 7f261529a959..155ad32179c9 100755 --- a/run-clang-tidy.sh +++ b/run-clang-tidy.sh @@ -111,27 +111,7 @@ export CMAKE_EXPORT_COMPILE_COMMANDS=ON export Halide_LLVM_ROOT="${CLANG_TIDY_LLVM_INSTALL_DIR}" if [[ $(${CC} --version) =~ .*Homebrew.* ]]; then - # Homebrew clang 21 is badly misconfigured and needs help finding the - # system headers, even though it uses system libc++ by default. - SDKROOT="$(xcrun --show-sdk-path)" - # TOOLCHAINROOT="$(xcrun --show-toolchain-path)" - TOOLCHAINROOT="$(cd "$(dirname "$(xcrun --find clang)")"/../.. && pwd)" - RCDIR="$(xcrun clang -print-resource-dir)" - cat >"${CLANG_TIDY_BUILD_DIR}/toolchain.cmake" <log_spaces(); // C++17 right fold - (debug(0) << ... << args) << "\n"; + auto dump = [](auto &&arg) { + debug(0) << arg; + }; + (dump(args), ...); + debug(0) << "\n"; } ~BoundsLogger() { @@ -2144,7 +2148,11 @@ class BoxesTouched : public IRGraphVisitor { void log_line(Args &&...args) { debug(0) << self->log_spaces(); // C++17 right fold - (debug(0) << ... << args) << "\n"; + auto dump = [](auto &&arg) { + debug(0) << arg; + }; + (dump(args), ...); + debug(0) << "\n"; } BoxesTouchedLogger(BoxesTouched *self, const char *pretty_function) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9edfd0476cbf..e41d7081b66a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -67,36 +67,7 @@ endif () # FIXME: failing_with_issue is dead code :) -# Ensure that basic sanitizer flags are supported; -# - Address sanitizer is often used in conjunction with fuzzing as it will detect -# common high severity bugs. This sanitizer is used as a "default" for fuzzing -# when the sanitizer isn't otherwise specified. -# - Fuzzer sanitizer will link against libfuzzer and is currently only supported -# on clang/msvc and isn't supported with GCC. If you need to use these fuzzers -# with a GCC based project you should consider looking into the LIB_FUZZING_ENGINE -# env variable defined in `test/fuzz/CMakeLists.txt`. -set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=fuzzer,address") -set(CMAKE_REQUIRED_FLAGS "-fsanitize=fuzzer-no-link,address") -check_cxx_source_compiles([[ -#include -#include -extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, std::size_t Size) { - return 0; -} -]] HAS_FUZZ_FLAGS) - -if (NOT HAS_FUZZ_FLAGS) - message(VERBOSE "Compiler does not support libfuzzer sanitizer.") -else () - message(VERBOSE "Compiler supports libfuzzer sanitizer.") -endif () - -# Note that we want to default WITH_TEST_FUZZ to OFF, even if HAS_FUZZ_FLAGS -# is true: just because our compiler supports fuzzing doesn't mean we want to -# build the fuzz tests, because they won't really build properly without the -# right preset specified. -Halide_feature(WITH_TEST_FUZZ "Build fuzz tests" AUTO - DEPENDS HAS_FUZZ_FLAGS) +Halide_feature(WITH_TEST_FUZZ "Build fuzz tests" ON) if (WITH_TEST_FUZZ) add_subdirectory(fuzz) endif () diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt index 3d96a1d6ee97..65376c7ebeb7 100644 --- a/test/fuzz/CMakeLists.txt +++ b/test/fuzz/CMakeLists.txt @@ -1,3 +1,10 @@ +if (NOT LIB_FUZZING_ENGINE) + set(LIB_FUZZING_ENGINE "$ENV{LIB_FUZZING_ENGINE}") +endif () + +set(LIB_FUZZING_ENGINE "${LIB_FUZZING_ENGINE}" + CACHE STRING "Extra link libraries arguments for fuzz tests.") + tests(GROUPS fuzz SOURCES bounds.cpp @@ -15,25 +22,31 @@ tests(GROUPS fuzz USE_EXIT_CODE_ONLY ) +# Check whether the configured toolchain can compile a libfuzzer test harness. +# If this fails, the user likely needs to set up their environment to use a +# compatible fuzzing engine (e.g. by setting up a toolchain file). +check_cxx_source_compiles([[ +#include +#include +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, std::size_t Size) { + return 0; +} +]] HAVE_LIBFUZZER_FLAGS) -# By default we are going to use the libfuzzer engine. However if -# LIB_FUZZING_ENGINE is declared you can override the fuzzing engine to one of; -# - Centipede -# - Hongfuzz -# - AFL++ -# - etc. -set(LIB_FUZZING_ENGINE "$ENV{LIB_FUZZING_ENGINE}" - CACHE STRING "Compiler flags necessary to link the fuzzing engine of choice e.g. libfuzzer, afl etc.") +add_library(Halide_fuzz INTERFACE) +add_library(Halide::fuzz ALIAS Halide_fuzz) -foreach(fuzzer "fuzz_bounds" "fuzz_cse") - target_link_libraries(${fuzzer} PRIVATE Halide::Halide) +if (NOT HAVE_LIBFUZZER_FLAGS) + if (LIB_FUZZING_ENGINE) + message(FATAL_ERROR "Cannot set LIB_FUZZING_ENGINE when not building with -fsanitize=fuzzer or a compatible fuzzing engine.") + endif () + target_sources(Halide_fuzz INTERFACE halide_fuzz_main.cpp halide_fuzz_main.h) + target_compile_definitions(Halide_fuzz INTERFACE HALIDE_FUZZER_BACKEND=HALIDE_FUZZER_BACKEND_STDLIB) +else () + target_link_libraries(Halide_fuzz INTERFACE ${LIB_FUZZING_ENGINE}) + target_compile_definitions(Halide_fuzz INTERFACE HALIDE_FUZZER_BACKEND=HALIDE_FUZZER_BACKEND_LIBFUZZER) +endif () - # Allow OSS-fuzz to manage flags directly - if (LIB_FUZZING_ENGINE) - target_link_libraries(${fuzzer} PRIVATE "${LIB_FUZZING_ENGINE}") - else () - # By default just build with address-sanitizers/libfuzzer for local testing - target_compile_options(${fuzzer} PRIVATE -fsanitize=fuzzer-no-link) - target_link_options(${fuzzer} PRIVATE -fsanitize=fuzzer) - endif () -endforeach() +foreach (fuzzer IN LISTS TEST_NAMES) + target_link_libraries("${fuzzer}" PRIVATE Halide::fuzz) +endforeach () diff --git a/test/fuzz/bounds.cpp b/test/fuzz/bounds.cpp index 67c66410d6e8..2a42df653fb5 100644 --- a/test/fuzz/bounds.cpp +++ b/test/fuzz/bounds.cpp @@ -1,7 +1,6 @@ #include "Halide.h" #include "fuzz_helpers.h" #include -#include #include #include #include @@ -26,13 +25,13 @@ std::string fuzz_var(int i) { // This is modified for each round. static Type global_var_type = Int(32); -Expr random_var(FuzzedDataProvider &fdp) { - int fuzz_count = fdp.ConsumeIntegralInRange(0, fuzz_var_count - 1); +Expr random_var(FuzzingContext &fuzz) { + int fuzz_count = fuzz.ConsumeIntegralInRange(0, fuzz_var_count - 1); return Variable::make(global_var_type, fuzz_var(fuzz_count)); } -Type random_type(FuzzedDataProvider &fdp, int width) { - Type t = fdp.PickValueInArray(fuzz_types); +Type random_type(FuzzingContext &fuzz, int width) { + Type t = fuzz.PickValueInArray(fuzz_types); if (width > 1) { t = t.with_lanes(width); @@ -40,7 +39,7 @@ Type random_type(FuzzedDataProvider &fdp, int width) { return t; } -int get_random_divisor(FuzzedDataProvider &fdp, Type t) { +int get_random_divisor(FuzzingContext &fuzz, Type t) { std::vector divisors = {t.lanes()}; for (int dd = 2; dd < t.lanes(); dd++) { if (t.lanes() % dd == 0) { @@ -48,41 +47,41 @@ int get_random_divisor(FuzzedDataProvider &fdp, Type t) { } } - return pick_value_in_vector(fdp, divisors); + return fuzz.PickValueInVector(divisors); } -Expr random_leaf(FuzzedDataProvider &fdp, Type t, bool overflow_undef = false, bool imm_only = false) { +Expr random_leaf(FuzzingContext &fuzz, Type t, bool overflow_undef = false, bool imm_only = false) { if (t.is_int() && t.bits() == 32) { overflow_undef = true; } if (t.is_scalar()) { - if (!imm_only && fdp.ConsumeBool()) { - auto v1 = random_var(fdp); + if (!imm_only && fuzz.ConsumeBool()) { + auto v1 = random_var(fuzz); return cast(t, v1); } else if (overflow_undef) { // For Int(32), we don't care about correctness during // overflow, so just use numbers that are unlikely to // overflow. - return cast(t, fdp.ConsumeIntegralInRange(-128, 127)); + return cast(t, fuzz.ConsumeIntegralInRange(-128, 127)); } else { - return cast(t, fdp.ConsumeIntegral()); + return cast(t, fuzz.ConsumeIntegral()); } } else { - int lanes = get_random_divisor(fdp, t); - if (fdp.ConsumeBool()) { - auto e1 = random_leaf(fdp, t.with_lanes(t.lanes() / lanes), overflow_undef); - auto e2 = random_leaf(fdp, t.with_lanes(t.lanes() / lanes), overflow_undef); + int lanes = get_random_divisor(fuzz, t); + if (fuzz.ConsumeBool()) { + auto e1 = random_leaf(fuzz, t.with_lanes(t.lanes() / lanes), overflow_undef); + auto e2 = random_leaf(fuzz, t.with_lanes(t.lanes() / lanes), overflow_undef); return Ramp::make(e1, e2, lanes); } else { - auto e1 = random_leaf(fdp, t.with_lanes(t.lanes() / lanes), overflow_undef); + auto e1 = random_leaf(fuzz, t.with_lanes(t.lanes() / lanes), overflow_undef); return Broadcast::make(e1, lanes); } } } -Expr random_expr(FuzzedDataProvider &fdp, Type t, int depth, bool overflow_undef = false); +Expr random_expr(FuzzingContext &fuzz, Type t, int depth, bool overflow_undef = false); -Expr random_condition(FuzzedDataProvider &fdp, Type t, int depth, bool maybe_scalar) { +Expr random_condition(FuzzingContext &fuzz, Type t, int depth, bool maybe_scalar) { static make_bin_op_fn make_bin_op[] = { EQ::make, NE::make, @@ -92,80 +91,80 @@ Expr random_condition(FuzzedDataProvider &fdp, Type t, int depth, bool maybe_sca GE::make, }; - if (maybe_scalar && fdp.ConsumeBool()) { + if (maybe_scalar && fuzz.ConsumeBool()) { t = t.element_of(); } - Expr a = random_expr(fdp, t, depth); - Expr b = random_expr(fdp, t, depth); - return fdp.PickValueInArray(make_bin_op)(a, b); + Expr a = random_expr(fuzz, t, depth); + Expr b = random_expr(fuzz, t, depth); + return fuzz.PickValueInArray(make_bin_op)(a, b); } -Expr random_expr(FuzzedDataProvider &fdp, Type t, int depth, bool overflow_undef) { +Expr random_expr(FuzzingContext &fuzz, Type t, int depth, bool overflow_undef) { if (t.is_int() && t.bits() == 32) { overflow_undef = true; } if (depth-- <= 0) { - return random_leaf(fdp, t, overflow_undef); + return random_leaf(fuzz, t, overflow_undef); } std::function operations[] = { [&]() { - return random_leaf(fdp, t); + return random_leaf(fuzz, t); }, [&]() { - auto c = random_condition(fdp, t, depth, true); - auto e1 = random_expr(fdp, t, depth, overflow_undef); - auto e2 = random_expr(fdp, t, depth, overflow_undef); + auto c = random_condition(fuzz, t, depth, true); + auto e1 = random_expr(fuzz, t, depth, overflow_undef); + auto e2 = random_expr(fuzz, t, depth, overflow_undef); // Don't use Select::make: we want to use select() here to // ensure that the condition and values match types. return select(c, e1, e2); }, [&]() { if (t.lanes() != 1) { - int lanes = get_random_divisor(fdp, t); - auto e1 = random_expr(fdp, t.with_lanes(t.lanes() / lanes), depth, overflow_undef); + int lanes = get_random_divisor(fuzz, t); + auto e1 = random_expr(fuzz, t.with_lanes(t.lanes() / lanes), depth, overflow_undef); return Broadcast::make(e1, lanes); } // If we got here, try again. - return random_expr(fdp, t, depth, overflow_undef); + return random_expr(fuzz, t, depth, overflow_undef); }, [&]() { if (t.lanes() != 1) { - int lanes = get_random_divisor(fdp, t); - auto e1 = random_expr(fdp, t.with_lanes(t.lanes() / lanes), depth, overflow_undef); - auto e2 = random_expr(fdp, t.with_lanes(t.lanes() / lanes), depth, overflow_undef); + int lanes = get_random_divisor(fuzz, t); + auto e1 = random_expr(fuzz, t.with_lanes(t.lanes() / lanes), depth, overflow_undef); + auto e2 = random_expr(fuzz, t.with_lanes(t.lanes() / lanes), depth, overflow_undef); return Ramp::make(e1, e2, lanes); } // If we got here, try again. - return random_expr(fdp, t, depth, overflow_undef); + return random_expr(fuzz, t, depth, overflow_undef); }, [&]() { if (t.is_bool()) { - auto e1 = random_expr(fdp, t, depth); + auto e1 = random_expr(fuzz, t, depth); return Not::make(e1); } // If we got here, try again. - return random_expr(fdp, t, depth, overflow_undef); + return random_expr(fuzz, t, depth, overflow_undef); }, [&]() { if (t.is_bool()) { - return random_condition(fdp, random_type(fdp, t.lanes()), depth, false); + return random_condition(fuzz, random_type(fuzz, t.lanes()), depth, false); } // If we got here, try again. - return random_expr(fdp, t, depth, overflow_undef); + return random_expr(fuzz, t, depth, overflow_undef); }, [&]() { // Get a random type that isn't t or int32 (int32 can overflow and we don't care about that). - // Note also that the FuzzedDataProvider doesn't actually promise to return a random distribution -- + // Note also that the FuzzingContext doesn't actually promise to return a random distribution -- // it can (e.g.) decide to just return 0 for all data, forever -- so this loop has no guarantee // of eventually finding a different type. To remedy this, we'll just put a limit on the retries. int count = 0; Type subtype; do { - subtype = random_type(fdp, t.lanes()); + subtype = random_type(fuzz, t.lanes()); } while (++count < 10 && (subtype == t || (subtype.is_int() && subtype.bits() == 32))); - auto e1 = random_expr(fdp, subtype, depth, overflow_undef); + auto e1 = random_expr(fuzz, subtype, depth, overflow_undef); return Cast::make(t, e1); }, [&]() { @@ -179,9 +178,9 @@ Expr random_expr(FuzzedDataProvider &fdp, Type t, int depth, bool overflow_undef Div::make, Mod::make, }; - make_bin_op_fn maker = fdp.PickValueInArray(make_bin_op); - Expr a = random_expr(fdp, t, depth, overflow_undef); - Expr b = random_expr(fdp, t, depth, overflow_undef); + make_bin_op_fn maker = fuzz.PickValueInArray(make_bin_op); + Expr a = random_expr(fuzz, t, depth, overflow_undef); + Expr b = random_expr(fuzz, t, depth, overflow_undef); return maker(a, b); }, [&]() { @@ -192,16 +191,16 @@ Expr random_expr(FuzzedDataProvider &fdp, Type t, int depth, bool overflow_undef }; // Boolean operations -- both sides must be cast to booleans, // and then we must cast the result back to 't'. - make_bin_op_fn maker = fdp.PickValueInArray(make_bin_op); - Expr a = random_expr(fdp, t, depth, overflow_undef); - Expr b = random_expr(fdp, t, depth, overflow_undef); + make_bin_op_fn maker = fuzz.PickValueInArray(make_bin_op); + Expr a = random_expr(fuzz, t, depth, overflow_undef); + Expr b = random_expr(fuzz, t, depth, overflow_undef); Type bool_with_lanes = Bool(t.lanes()); a = cast(bool_with_lanes, a); b = cast(bool_with_lanes, b); return cast(t, maker(a, b)); }, }; - return fdp.PickValueInArray(operations)(); + return fuzz.PickValueInArray(operations)(); } // These are here to enable copy of failed output expressions @@ -285,7 +284,7 @@ Expr c(Variable::make(global_var_type, fuzz_var(2))); Expr d(Variable::make(global_var_type, fuzz_var(3))); Expr e(Variable::make(global_var_type, fuzz_var(4))); -Interval random_interval(FuzzedDataProvider &fdp, Type t) { +Interval random_interval(FuzzingContext &fuzz, Type t) { Interval interval; int min_value = -128; @@ -319,10 +318,10 @@ Interval random_interval(FuzzedDataProvider &fdp, Type t) { max_value = std::min(max_value, 128); // change the min_value for the calculation of max - min_value = fdp.ConsumeIntegralInRange(min_value, max_value); + min_value = fuzz.ConsumeIntegralInRange(min_value, max_value); interval.min = cast(t, min_value); - max_value = fdp.ConsumeIntegralInRange(min_value, max_value); + max_value = fuzz.ConsumeIntegralInRange(min_value, max_value); interval.max = cast(t, max_value); if (min_value > max_value || (interval.is_bounded() && can_prove(interval.min > interval.max))) { @@ -337,7 +336,7 @@ Interval random_interval(FuzzedDataProvider &fdp, Type t) { return interval; } -int sample_interval(FuzzedDataProvider &fdp, const Interval &interval) { +int sample_interval(FuzzingContext &fuzz, const Interval &interval) { // Values chosen so intervals don't repeatedly produce signed_overflow when simplified. int min_value = -128; int max_value = 128; @@ -364,7 +363,7 @@ int sample_interval(FuzzedDataProvider &fdp, const Interval &interval) { } } - return fdp.ConsumeIntegralInRange(min_value, max_value); + return fuzz.ConsumeIntegralInRange(min_value, max_value); } bool test_bounds(Expr test, const Interval &interval, Type t, const map &vars) { @@ -416,18 +415,31 @@ bool test_bounds(Expr test, const Interval &interval, Type t, const map vars; for (int i = 0; i < fuzz_var_count; i++) { vars[fuzz_var(i)] = Expr(); } + // Don't test expressions with potentially overflowing casts to signed + // ints. This is known to be broken (See + // https://github.com/halide/Halide/pull/7814) + bool contains_risky_cast = false; + visit_with(test, [&](auto *self, const Cast *op) { + contains_risky_cast |= (op->type.is_int() && + op->type.bits() >= 32 && + !op->type.can_represent(op->value.type())); + }); + if (contains_risky_cast) { + return true; + } + for (int i = 0; i < trials; i++) { Scope scope; for (auto v = vars.begin(); v != vars.end(); v++) { // This type is used because the variables will be this type for a given round. - Interval interval = random_interval(fdp, global_var_type); + Interval interval = random_interval(fuzz, global_var_type); scope.push(v->first, interval); } @@ -456,7 +468,7 @@ bool test_expression_bounds(FuzzedDataProvider &fdp, Expr test, int trials, int for (int j = 0; j < samples_per_trial; j++) { for (std::map::iterator v = vars.begin(); v != vars.end(); v++) { Interval interval = scope.get(v->first); - v->second = cast(global_var_type, sample_interval(fdp, interval)); + v->second = cast(global_var_type, sample_interval(fuzz, interval)); } if (!test_bounds(test, interval, test.type(), vars)) { @@ -474,9 +486,7 @@ bool test_expression_bounds(FuzzedDataProvider &fdp, Expr test, int trials, int } // namespace -extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { - FuzzedDataProvider fdp(data, size); - +FUZZ_TEST(bounds, FuzzingContext &fuzz) { // Number of random expressions to test. const int count = 100; // Depth of the randomly generated expression trees. @@ -488,18 +498,17 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { std::array vector_widths = {1, 2, 3, 4, 6, 8}; for (int n = 0; n < count; n++) { - int width = fdp.PickValueInArray(vector_widths); + int width = fuzz.PickValueInArray(vector_widths); // This is the type that will be the innermost (leaf) value type. - Type expr_type = random_type(fdp, width); - Type var_type = random_type(fdp, 1); + Type expr_type = random_type(fuzz, width); + Type var_type = random_type(fuzz, 1); global_var_type = var_type; // Generate a random expr... - Expr test = random_expr(fdp, expr_type, depth); - if (!test_expression_bounds(fdp, test, trials, samples)) { + Expr test = random_expr(fuzz, expr_type, depth); + if (!test_expression_bounds(fuzz, test, trials, samples)) { return 1; } } - std::cout << "Success!\n"; return 0; } diff --git a/test/fuzz/cse.cpp b/test/fuzz/cse.cpp index 1874bef94968..0c15b74e8921 100644 --- a/test/fuzz/cse.cpp +++ b/test/fuzz/cse.cpp @@ -2,7 +2,6 @@ #include "fuzz_helpers.h" #include -#include #include using namespace Halide; @@ -15,14 +14,14 @@ using std::vector; // *not* int32 -- because we want to test CSE, not the simplifier's // overflow behavior, and using int32 can end up with results // containing signed_integer_overflow(), which is not helpful here. -Expr random_expr(FuzzedDataProvider &fdp, int depth, vector> &exprs) { +Expr random_expr(FuzzingContext &fuzz, int depth, vector> &exprs) { if (depth <= 0) { - return i16(fdp.ConsumeIntegralInRange(-5, 4)); + return i16(fuzz.ConsumeIntegralInRange(-5, 4)); } - if (!exprs.empty() && fdp.ConsumeBool()) { + if (!exprs.empty() && fuzz.ConsumeBool()) { // Reuse an existing expression that was generated under conditions at // least as strict as our current depth limit. - auto p = pick_value_in_vector(fdp, exprs); + auto p = fuzz.PickValueInVector(exprs); if (p.second <= depth) { return p.first; } @@ -41,45 +40,44 @@ Expr random_expr(FuzzedDataProvider &fdp, int depth, vector> &ex return Variable::make(Int(16), "z"); }, [&]() { - Expr next = random_expr(fdp, depth - 1, exprs); - next += random_expr(fdp, depth - 1, exprs); + Expr next = random_expr(fuzz, depth - 1, exprs); + next += random_expr(fuzz, depth - 1, exprs); return next; }, [&]() { - Expr a = random_expr(fdp, depth - 2, exprs); - Expr b = random_expr(fdp, depth - 2, exprs); - Expr c = random_expr(fdp, depth - 2, exprs); - Expr d = random_expr(fdp, depth - 2, exprs); + Expr a = random_expr(fuzz, depth - 2, exprs); + Expr b = random_expr(fuzz, depth - 2, exprs); + Expr c = random_expr(fuzz, depth - 2, exprs); + Expr d = random_expr(fuzz, depth - 2, exprs); return select(a > b, c, d); }, [&]() { - Expr a = random_expr(fdp, depth - 1, exprs); - Expr b = random_expr(fdp, depth - 1, exprs); + Expr a = random_expr(fuzz, depth - 1, exprs); + Expr b = random_expr(fuzz, depth - 1, exprs); return i16(Let::make("x", a, b)); }, [&]() { - Expr a = random_expr(fdp, depth - 1, exprs); - Expr b = random_expr(fdp, depth - 1, exprs); + Expr a = random_expr(fuzz, depth - 1, exprs); + Expr b = random_expr(fuzz, depth - 1, exprs); return i16(Let::make("y", a, b)); }, [&]() { - Expr a = random_expr(fdp, depth - 1, exprs); - Expr b = random_expr(fdp, depth - 1, exprs); + Expr a = random_expr(fuzz, depth - 1, exprs); + Expr b = random_expr(fuzz, depth - 1, exprs); return i16(Let::make("z", a, b)); }, [&]() { - return i16(fdp.ConsumeIntegralInRange(-5, 4)); + return i16(fuzz.ConsumeIntegralInRange(-5, 4)); }, }; - Expr next = fdp.PickValueInArray(build_next_expr)(); + Expr next = fuzz.PickValueInArray(build_next_expr)(); exprs.emplace_back(next, depth); return next; } -extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { - FuzzedDataProvider fdp(data, size); +FUZZ_TEST(cse, FuzzingContext &fuzz) { vector> exprs; - Expr orig = random_expr(fdp, 5, exprs); + Expr orig = random_expr(fuzz, 5, exprs); Expr csed = common_subexpression_elimination(orig); @@ -92,7 +90,9 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { // Don't use can_prove, because it recursively calls cse, which just confuses matters. Expr result = simplify(check); - assert(is_const_one(result)); + if (is_const_one(result)) { + return 0; + } - return 0; + return 1; } diff --git a/test/fuzz/fuzz_helpers.h b/test/fuzz/fuzz_helpers.h index 24e302f26aa2..5b9740070be8 100644 --- a/test/fuzz/fuzz_helpers.h +++ b/test/fuzz/fuzz_helpers.h @@ -1,15 +1,100 @@ #ifndef HALIDE_FUZZ_HELPERS_H_ #define HALIDE_FUZZ_HELPERS_H_ -#include "fuzzer/FuzzedDataProvider.h" +#define HALIDE_FUZZER_BACKEND_STDLIB 0 +#define HALIDE_FUZZER_BACKEND_LIBFUZZER 1 + +#ifndef HALIDE_FUZZER_BACKEND +#error "HALIDE_FUZZER_BACKEND not defined, defaulting to libFuzzer" +#endif + +/////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include #include +#if HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_LIBFUZZER +#include "fuzzer/FuzzedDataProvider.h" // IWYU pragma: export +#elif HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_STDLIB +#include "halide_fuzz_main.h" +#include +#endif + namespace Halide { -template -inline T pick_value_in_vector(FuzzedDataProvider &fdp, std::vector &vec) { - return vec[fdp.ConsumeIntegralInRange(0, vec.size() - 1)]; -} +#if HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_LIBFUZZER +class FuzzingContext : public FuzzedDataProvider { +public: + using FuzzedDataProvider::FuzzedDataProvider; + template + T PickValueInVector(std::vector &vec) { + return vec[ConsumeIntegralInRange(0, vec.size() - 1)]; + } +}; +#elif HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_STDLIB +// IMPORTANT: we don't use std::*_distribution because they are not portable across standard libraries +class FuzzingContext { +public: + using RandomEngine = std::mt19937_64; + using SeedType = RandomEngine::result_type; + + explicit FuzzingContext(SeedType seed) : rng(seed) { + } + + template + T ConsumeIntegral() { + return static_cast(rng()); + } + + template + T ConsumeIntegralInRange(T min, T max) { + // If this proves too slow, there are smarter things we can do: + // https://lemire.me/blog/2019/06/06/nearly-divisionless-random-integer-generation-on-various-systems/ + if (max < min) { + return min; + } + return min + rng() % (max - min + 1); + } + + bool ConsumeBool() { + return rng() & 1; + } + + template + T PickValueInVector(std::vector &vec) { + return vec[ConsumeIntegralInRange(static_cast(0), vec.size() - 1)]; + } + + template + auto PickValueInArray(T &array) -> decltype(auto) { + return array[ConsumeIntegralInRange(static_cast(0), std::size(array) - 1)]; + } + +private: + RandomEngine rng; +}; +#endif + } // namespace Halide +#if HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_LIBFUZZER +#define FUZZ_TEST(name, signature) \ + static int name##_fuzz_test(signature); \ + extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { \ + FuzzingContext fdp(data, size); \ + return name##_fuzz_test(fdp); \ + } \ + static int name##_fuzz_test(signature) +#elif HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_STDLIB +#define FUZZ_TEST(name, signature) \ + static int name##_fuzz_test(signature); \ + int main(int argc, char **argv) { \ + return Halide::fuzz_main(argc, argv, name##_fuzz_test); \ + } \ + static int name##_fuzz_test(signature) +#endif + #endif // HALIDE_FUZZ_HELPERS_H_ diff --git a/test/fuzz/halide_fuzz_main.cpp b/test/fuzz/halide_fuzz_main.cpp new file mode 100644 index 000000000000..085500f4ac7b --- /dev/null +++ b/test/fuzz/halide_fuzz_main.cpp @@ -0,0 +1,145 @@ +#include "halide_fuzz_main.h" +#include "fuzz_helpers.h" + +#include +#include +#include +#include +#include + +namespace { + +template +T initialize_rng() { + constexpr auto kStateWords = T::state_size * sizeof(typename T::result_type) / sizeof(uint32_t); + std::vector random(kStateWords); + std::generate(random.begin(), random.end(), std::random_device{}); + std::seed_seq seed_seq(random.begin(), random.end()); + return T{seed_seq}; +} + +void print_usage(const char *argv0) { + std::cerr + << "Usage: " << argv0 << " [options] [seed]\n" + << "\n" + << "Options:\n" + << " -runs=N Number of fuzz iterations (default: 10000)\n" + << " -timeout=N (ignored, accepted for libFuzzer compatibility)\n" + << " -max_total_time=N (ignored, accepted for libFuzzer compatibility)\n" + << " -help Print this help message and exit\n" + << "\n" + << "If a single non-option argument is given, it is used as the RNG seed.\n" + << "Options may use '-' or '--' prefixes.\n"; +} + +// Strip one or two leading dashes from arg. Returns nullptr if arg doesn't +// start with '-'. +const char *strip_dashes(const char *arg) { + if (arg[0] != '-') { + return nullptr; + } + arg++; + if (arg[0] == '-') { + arg++; + } + return arg; +} + +// Try to parse "key=N" where key matches `name`. On match, stores the parsed +// positive integer in *out and returns true. On parse error, prints a message +// and exits. Returns false if `body` doesn't start with `name=`. +bool parse_positive_int_flag(const char *body, const char *name, int *out) { + size_t len = strlen(name); + if (strncmp(body, name, len) != 0 || body[len] != '=') { + return false; + } + const char *val = body + len + 1; + int n = 0; + std::istringstream iss(val); + if (!(iss >> n) || !iss.eof() || n <= 0) { + std::cerr << "Error: -" << name << " requires a strictly positive integer, got '" << val << "'\n\n"; + print_usage("fuzz_test"); + exit(1); + } + *out = n; + return true; +} + +} // namespace + +namespace Halide { + +int fuzz_main(int argc, char **argv, FuzzFunction main_fn) { + int runs = 10000; + FuzzingContext::SeedType explicit_seed = 0; + bool has_explicit_seed = false; + + // First pass: check for -help anywhere. + for (int i = 1; i < argc; i++) { + const char *body = strip_dashes(argv[i]); + if (body && strcmp(body, "help") == 0) { + print_usage(argv[0]); + return 0; + } + } + + // Second pass: parse all arguments. + for (int i = 1; i < argc; i++) { + if (const char *body = strip_dashes(argv[i])) { + int dummy = 0; + if (parse_positive_int_flag(body, "runs", &runs)) { + continue; + } + if (parse_positive_int_flag(body, "timeout", &dummy)) { + std::cerr << "Warning: -timeout is accepted but ignored.\n"; + continue; + } + if (parse_positive_int_flag(body, "max_total_time", &dummy)) { + std::cerr << "Warning: -max_total_time is accepted but ignored.\n"; + continue; + } + std::cerr << "Error: unknown option '" << argv[i] << "'\n\n"; + print_usage(argv[0]); + return 1; + } + + // Positional argument: must be the only one and must be a seed. + if (has_explicit_seed) { + std::cerr << "Error: unexpected extra argument '" << argv[i] << "'\n\n"; + print_usage(argv[0]); + return 1; + } + std::istringstream iss(argv[i]); + if (argv[i][0] == '-' || !(iss >> explicit_seed) || !iss.eof()) { + std::cerr << "Error: seed must be a non-negative integer, got '" << argv[i] << "'\n\n"; + print_usage(argv[0]); + return 1; + } + has_explicit_seed = true; + } + + if (has_explicit_seed) { + // Single run with the given seed. + std::cerr << "Seed: " << explicit_seed << "\n" + << std::flush; + FuzzingContext ctx{explicit_seed}; + return main_fn(ctx); + } + + auto seed_generator = initialize_rng(); + + for (int i = 0; i < runs; i++) { + auto seed = seed_generator(); + std::cerr << "Seed: " << seed << "\n" + << std::flush; + FuzzingContext ctx{seed}; + int result = main_fn(ctx); + if (result != 0) { + return result; + } + } + + return 0; +} + +} // namespace Halide diff --git a/test/fuzz/halide_fuzz_main.h b/test/fuzz/halide_fuzz_main.h new file mode 100644 index 000000000000..ca9af5aa61ea --- /dev/null +++ b/test/fuzz/halide_fuzz_main.h @@ -0,0 +1,11 @@ +#ifndef HALIDE_FUZZ_STDLIB_MAIN_H +#define HALIDE_FUZZ_STDLIB_MAIN_H + +namespace Halide { +class FuzzingContext; +using FuzzFunction = int (*)(FuzzingContext &); + +int fuzz_main(int argc, char **argv, FuzzFunction main_fn); +} // namespace Halide + +#endif // HALIDE_FUZZ_STDLIB_MAIN_H