Skip to content

Commit 30b7241

Browse files
authored
Add FuzzTest and use it to fuzz types (#7246)
FuzzTest is a state-of-the-art fuzzing framework. It supports writing property-based fuzz targets very similar to googletest unit tests, where the framework provides the arguments to the test function drawn from a given domain. These fuzz tests are run continuously for one second each when running the normal googletest unit tests, but FuzzTest also supports building in "fuzzing mode" where the tests can be run continuously with coverage-guided mutations until they find a bug. Add FuzzTest as a third_party dependency that is not built by default. To build FuzzTest and the fuzz tests that use it, set the CMake variable `BUILD_FUZZTEST=ON`. To build in fuzzing mode, additionally set the CMake variable `FUZZTEST_FUZZING_MODE=ON`. One of FuzzTest's key features is its support for domain combinators, which combine simple domains into more complex domains. For example, the domain `VariantOf(InRange(0, 10), Arbitrary<std::string>())` produces a std::variant that either holds an integer between 0 and 10 or an arbitrary string. The set of available combinators is powerful enough to build domains for arbitrarily structured types. Use domain combinators to define a domain of WebAssembly type definitions. The implementation of this domain follows the same general structure as the existing heap type fuzzer: it chooses the size of rec groups, then it chooses the supertypes and hierarchies for all the definitions, then it generates the particular definitions. The difference is that all random choices are made by the FuzzTest framework rather than our own code. Whenever the domains of future choices will depend on the outcome of the current choice, we use the `FlatMap` combinator to make a choice from the current domain, then pass it to a continuation that finishes constructing the final domain of types. This leads to strange continuation-passing code, but allows us to recursively construct the domain of type definitions. The current implementation of the type definition domain is not ideal: the tree of choices used to produce a particular set of type definitions is deeper and narrower than it could be. Since a mutation of one choice in the tree requires regenerating and changing the subtree of choices rooted at the changed choice, having a narrower tree than necessary means that small mutations are not as diverse as they could be and having a deeper tree means that many mutations are larger than they could be. The quality of the domain construction will be improved in the future.
1 parent d0321bc commit 30b7241

13 files changed

+1537
-65
lines changed

.flake8

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ ignore =
66
E241,
77
; line break after binary operator
88
W504
9-
exclude = third_party,./test/emscripten,./test/spec,./test/wasm-install,./test/lit
9+
exclude = third_party,./test/emscripten,./test/spec,./test/wasm-install,./test/lit,./_deps

.github/workflows/ci.yml

+28
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,34 @@ jobs:
147147
- name: test
148148
run: python check.py --binaryen-bin=out/bin
149149

150+
# Copied and modified from build-clang
151+
build-fuzztest:
152+
name: clang with fuzztest
153+
runs-on: ubuntu-latest
154+
steps:
155+
- uses: actions/setup-python@v5
156+
with:
157+
python-version: '3.x'
158+
- uses: actions/checkout@v4
159+
with:
160+
submodules: true
161+
- name: install ninja
162+
run: sudo apt-get install ninja-build
163+
- name: install v8
164+
run: |
165+
npm install jsvu -g
166+
jsvu --os=default --engines=v8
167+
- name: install Python dev dependencies
168+
run: pip3 install -r requirements-dev.txt
169+
- name: cmake
170+
run: |
171+
mkdir -p out
172+
cmake -S . -B out -G Ninja -DCMAKE_INSTALL_PREFIX=out/install -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DBUILD_FUZZTEST=ON
173+
- name: build
174+
run: cmake --build out -v
175+
- name: test
176+
run: python check.py --binaryen-bin=out/bin
177+
150178
# TODO(sbc): Find a way to reduce the duplicate between these sanitizer jobs
151179
build-asan:
152180
name: asan

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ CMakeFiles
2121
/.ninja_log
2222
/bin/
2323
/lib/
24+
/_deps/
25+
/dist/
2426
/config.h
2527
/emcc-build
2628
compile_commands.json

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "test/spec/testsuite"]
55
path = test/spec/testsuite
66
url = https://github.com/WebAssembly/testsuite.git
7+
[submodule "third_party/fuzztest"]
8+
path = third_party/fuzztest
9+
url = https://github.com/google/fuzztest

CMakeLists.txt

+70-49
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ option(BYN_ENABLE_LTO "Build with LTO" Off)
3636
# Turn this off to avoid the dependency on gtest.
3737
option(BUILD_TESTS "Build GTest-based tests" ON)
3838

39+
option(BUILD_FUZZTEST "Build fuzztest-based tests and fuzzers" OFF)
40+
3941
# Turn this off to build only the library.
4042
option(BUILD_TOOLS "Build tools" ON)
4143

@@ -161,8 +163,8 @@ endfunction()
161163

162164
function(binaryen_add_executable name sources)
163165
add_executable(${name} ${sources})
164-
target_link_libraries(${name} Threads::Threads)
165-
target_link_libraries(${name} binaryen)
166+
target_link_libraries(${name} PRIVATE Threads::Threads)
167+
target_link_libraries(${name} PRIVATE binaryen)
166168
binaryen_setup_rpath(${name})
167169
install(TARGETS ${name} DESTINATION ${CMAKE_INSTALL_BINDIR})
168170
endfunction()
@@ -258,7 +260,10 @@ if(MSVC)
258260
else() # MSVC
259261

260262
add_compile_flag("-fno-omit-frame-pointer")
261-
add_compile_flag("-fno-rtti")
263+
if(NOT BUILD_FUZZTEST)
264+
# fuzztest depends on RTTIs.
265+
add_compile_flag("-fno-rtti")
266+
endif()
262267
if(WIN32)
263268
add_compile_flag("-D_GNU_SOURCE")
264269
add_compile_flag("-D__STDC_FORMAT_MACROS")
@@ -276,10 +281,6 @@ else() # MSVC
276281
# explicitly undefine it:
277282
add_nondebug_compile_flag("-UNDEBUG")
278283
endif()
279-
if(NOT APPLE AND NOT "${CMAKE_CXX_FLAGS}" MATCHES "-fsanitize")
280-
# This flag only applies to shared libraries so don't use add_link_flag
281-
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
282-
endif()
283284
endif()
284285

285286
if(EMSCRIPTEN)
@@ -416,6 +417,25 @@ else() # MSVC
416417
add_compile_flag("-Wno-deprecated-declarations")
417418
endif()
418419

420+
if(BUILD_FUZZTEST)
421+
add_compile_flag("-DFUZZTEST")
422+
fuzztest_setup_fuzzing_flags()
423+
424+
# Enabling fuzzing mode turns on sanitizers, which turn on additional
425+
# warnings. To keep the build working, do not treat these warnings as
426+
# errors.
427+
add_compile_flag("-Wno-error=maybe-uninitialized")
428+
add_compile_flag("-Wno-error=uninitialized")
429+
add_compile_flag("-Wno-error=array-bounds")
430+
add_compile_flag("-Wno-error=stringop-overread")
431+
add_compile_flag("-Wno-error=missing-field-initializers")
432+
endif()
433+
434+
if(NOT APPLE AND NOT "${CMAKE_CXX_FLAGS}" MATCHES "-fsanitize")
435+
# This flag only applies to shared libraries so don't use add_link_flag
436+
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
437+
endif()
438+
419439
endif()
420440

421441
# Declare libbinaryen
@@ -483,83 +503,84 @@ endif()
483503
if(EMSCRIPTEN)
484504
# binaryen.js WebAssembly variant
485505
add_executable(binaryen_wasm ${binaryen_SOURCES})
486-
target_link_libraries(binaryen_wasm binaryen)
487-
target_link_libraries(binaryen_wasm "-sFILESYSTEM")
488-
target_link_libraries(binaryen_wasm "-sEXPORT_NAME=Binaryen")
489-
target_link_libraries(binaryen_wasm "-sNODERAWFS=0")
506+
target_link_libraries(binaryen_wasm PRIVATE binaryen)
507+
target_link_libraries(binaryen_wasm PRIVATE "-sFILESYSTEM")
508+
target_link_libraries(binaryen_wasm PRIVATE "-sEXPORT_NAME=Binaryen")
509+
target_link_libraries(binaryen_wasm PRIVATE "-sNODERAWFS=0")
490510
# Do not error on the repeated NODERAWFS argument
491-
target_link_libraries(binaryen_wasm "-Wno-unused-command-line-argument")
511+
target_link_libraries(binaryen_wasm PRIVATE "-Wno-unused-command-line-argument")
492512
# Emit a single file for convenience of people using binaryen.js as a library,
493513
# so they only need to distribute a single file.
494514
if(EMSCRIPTEN_ENABLE_SINGLE_FILE)
495-
target_link_libraries(binaryen_wasm "-sSINGLE_FILE")
496-
endif()
497-
target_link_libraries(binaryen_wasm "-sEXPORT_ES6")
498-
target_link_libraries(binaryen_wasm "-sEXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,stringToAscii")
499-
target_link_libraries(binaryen_wasm "-sEXPORTED_FUNCTIONS=_malloc,_free")
500-
target_link_libraries(binaryen_wasm "--post-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.js-post.js")
501-
target_link_libraries(binaryen_wasm "-msign-ext")
502-
target_link_libraries(binaryen_wasm "-mbulk-memory")
503-
target_link_libraries(binaryen_wasm optimized "--closure=1")
515+
target_link_libraries(binaryen_wasm PRIVATE "-sSINGLE_FILE")
516+
endif()
517+
target_link_libraries(binaryen_wasm PRIVATE "-sEXPORT_ES6")
518+
target_link_libraries(binaryen_wasm PRIVATE "-sEXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,stringToAscii")
519+
target_link_libraries(binaryen_wasm PRIVATE "-sEXPORTED_FUNCTIONS=_malloc,_free")
520+
target_link_libraries(binaryen_wasm PRIVATE "--post-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.js-post.js")
521+
target_link_libraries(binaryen_wasm PRIVATE "-msign-ext")
522+
target_link_libraries(binaryen_wasm PRIVATE "-mbulk-memory")
523+
target_link_libraries(binaryen_wasm PRIVATE optimized "--closure=1")
504524
# TODO: Fix closure warnings! (#5062)
505-
target_link_libraries(binaryen_wasm optimized "-Wno-error=closure")
506-
target_link_libraries(binaryen_wasm optimized "-flto")
507-
target_link_libraries(binaryen_wasm debug "--profiling")
525+
target_link_libraries(binaryen_wasm PRIVATE optimized "-Wno-error=closure")
526+
target_link_libraries(binaryen_wasm PRIVATE optimized "-flto")
527+
target_link_libraries(binaryen_wasm PRIVATE debug "--profiling")
508528
# Avoid catching exit as that can confuse error reporting in Node,
509529
# see https://github.com/emscripten-core/emscripten/issues/17228
510-
target_link_libraries(binaryen_wasm "-sNODEJS_CATCH_EXIT=0")
530+
target_link_libraries(binaryen_wasm PRIVATE "-sNODEJS_CATCH_EXIT=0")
511531
install(TARGETS binaryen_wasm DESTINATION ${CMAKE_INSTALL_BINDIR})
512532

513533
# binaryen.js JavaScript variant
514534
add_executable(binaryen_js ${binaryen_SOURCES})
515-
target_link_libraries(binaryen_js binaryen)
516-
target_link_libraries(binaryen_js "-sWASM=0")
517-
target_link_libraries(binaryen_js "-sWASM_ASYNC_COMPILATION=0")
535+
target_link_libraries(binaryen_js PRIVATE binaryen)
536+
target_link_libraries(binaryen_js PRIVATE "-sWASM=0")
537+
target_link_libraries(binaryen_js PRIVATE "-sWASM_ASYNC_COMPILATION=0")
538+
518539
if(${CMAKE_CXX_COMPILER_VERSION} STREQUAL "6.0.1")
519540
# only valid with fastcomp and WASM=0
520-
target_link_libraries(binaryen_js "-sELIMINATE_DUPLICATE_FUNCTIONS")
541+
target_link_libraries(binaryen_js PRIVATE "-sELIMINATE_DUPLICATE_FUNCTIONS")
521542
endif()
522543
# Disabling filesystem and setting web environment for js_of_ocaml
523544
# so it doesn't try to detect the "node" environment
524545
if(JS_OF_OCAML)
525-
target_link_libraries(binaryen_js "-sFILESYSTEM=0")
526-
target_link_libraries(binaryen_js "-sENVIRONMENT=web,worker")
546+
target_link_libraries(binaryen_js PRIVATE "-sFILESYSTEM=0")
547+
target_link_libraries(binaryen_js PRIVATE "-sENVIRONMENT=web,worker")
527548
else()
528-
target_link_libraries(binaryen_js "-sFILESYSTEM=1")
549+
target_link_libraries(binaryen_js PRIVATE "-sFILESYSTEM=1")
529550
endif()
530-
target_link_libraries(binaryen_js "-sNODERAWFS=0")
551+
target_link_libraries(binaryen_js PRIVATE "-sNODERAWFS=0")
531552
# Do not error on the repeated NODERAWFS argument
532-
target_link_libraries(binaryen_js "-Wno-unused-command-line-argument")
553+
target_link_libraries(binaryen_js PRIVATE "-Wno-unused-command-line-argument")
533554
if(EMSCRIPTEN_ENABLE_SINGLE_FILE)
534-
target_link_libraries(binaryen_js "-sSINGLE_FILE")
555+
target_link_libraries(binaryen_js PRIVATE "-sSINGLE_FILE")
535556
endif()
536-
target_link_libraries(binaryen_js "-sEXPORT_NAME=Binaryen")
557+
target_link_libraries(binaryen_js PRIVATE "-sEXPORT_NAME=Binaryen")
537558
# Currently, js_of_ocaml can only process ES5 code
538559
if(JS_OF_OCAML)
539-
target_link_libraries(binaryen_js "-sEXPORT_ES6=0")
560+
target_link_libraries(binaryen_js PRIVATE "-sEXPORT_ES6=0")
540561
else()
541-
target_link_libraries(binaryen_js "-sEXPORT_ES6=1")
562+
target_link_libraries(binaryen_js PRIVATE "-sEXPORT_ES6=1")
542563
endif()
543-
target_link_libraries(binaryen_js "-sEXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,stringToAscii")
544-
target_link_libraries(binaryen_js "-sEXPORTED_FUNCTIONS=_malloc,_free")
545-
target_link_libraries(binaryen_js "--post-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.js-post.js")
564+
target_link_libraries(binaryen_js PRIVATE "-sEXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,stringToAscii")
565+
target_link_libraries(binaryen_js PRIVATE "-sEXPORTED_FUNCTIONS=_malloc,_free")
566+
target_link_libraries(binaryen_js PRIVATE "--post-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.js-post.js")
546567
# js_of_ocaml needs a specified variable with special comment to provide the library to consumers
547568
if(JS_OF_OCAML)
548-
target_link_libraries(binaryen_js "--extern-pre-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.jsoo-extern-pre.js")
569+
target_link_libraries(binaryen_js PRIVATE "--extern-pre-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.jsoo-extern-pre.js")
549570
endif()
550-
target_link_libraries(binaryen_js optimized "--closure=1")
571+
target_link_libraries(binaryen_js PRIVATE optimized "--closure=1")
551572
# Currently, js_of_ocaml can only process ES5 code
552573
if(JS_OF_OCAML)
553-
target_link_libraries(binaryen_js optimized "--closure-args=\"--language_out=ECMASCRIPT5\"")
574+
target_link_libraries(binaryen_js PRIVATE optimized "--closure-args=\"--language_out=ECMASCRIPT5\"")
554575
endif()
555576
# TODO: Fix closure warnings! (#5062)
556-
target_link_libraries(binaryen_js optimized "-Wno-error=closure")
557-
target_link_libraries(binaryen_js optimized "-flto")
558-
target_link_libraries(binaryen_js debug "--profiling")
559-
target_link_libraries(binaryen_js debug "-sASSERTIONS")
577+
target_link_libraries(binaryen_js PRIVATE optimized "-Wno-error=closure")
578+
target_link_libraries(binaryen_js PRIVATE optimized "-flto")
579+
target_link_libraries(binaryen_js PRIVATE debug "--profiling")
580+
target_link_libraries(binaryen_js PRIVATE debug "-sASSERTIONS")
560581
# Avoid catching exit as that can confuse error reporting in Node,
561582
# see https://github.com/emscripten-core/emscripten/issues/17228
562-
target_link_libraries(binaryen_js "-sNODEJS_CATCH_EXIT=0")
583+
target_link_libraries(binaryen_js PRIVATE "-sNODEJS_CATCH_EXIT=0")
563584
install(TARGETS binaryen_js DESTINATION ${CMAKE_INSTALL_BINDIR})
564585
endif()
565586

check.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ def run_version_tests():
4343
print('[ checking --version ... ]\n')
4444

4545
not_executable_suffix = ['.DS_Store', '.txt', '.js', '.ilk', '.pdb', '.dll', '.wasm', '.manifest']
46-
not_executable_prefix = ['binaryen-lit', 'binaryen-unittests']
46+
executable_prefix = ['wasm']
4747
bin_files = [os.path.join(shared.options.binaryen_bin, f) for f in os.listdir(shared.options.binaryen_bin)]
4848
executables = [f for f in bin_files if os.path.isfile(f) and
4949
not any(f.endswith(s) for s in not_executable_suffix) and
50-
not any(os.path.basename(f).startswith(s) for s in not_executable_prefix)]
50+
any(os.path.basename(f).startswith(s) for s in executable_prefix)]
5151
executables = sorted(executables)
5252
assert len(executables)
5353

test/gtest/CMakeLists.txt

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/googletest/googletest/include)
1+
if(BUILD_FUZZTEST)
2+
include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/fuzztest)
3+
else()
4+
include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/googletest/googletest/include)
5+
endif()
26

37
set(unittest_SOURCES
48
arena.cpp
@@ -21,10 +25,21 @@ set(unittest_SOURCES
2125
validator.cpp
2226
)
2327

28+
if(BUILD_FUZZTEST)
29+
set(unittest_SOURCES ${unittest_SOURCES} type-domains.cpp)
30+
endif()
31+
2432
# suffix_tree.cpp includes LLVM header using std::iterator (deprecated in C++17)
2533
if (NOT MSVC)
2634
set_source_files_properties(suffix_tree.cpp PROPERTIES COMPILE_FLAGS -Wno-deprecated-declarations)
2735
endif()
2836

37+
enable_testing()
38+
include(GoogleTest)
2939
binaryen_add_executable(binaryen-unittests "${unittest_SOURCES}")
30-
target_link_libraries(binaryen-unittests gtest gtest_main)
40+
if(BUILD_FUZZTEST)
41+
link_fuzztest(binaryen-unittests)
42+
gtest_discover_tests(binaryen-unittests)
43+
else()
44+
target_link_libraries(binaryen-unittests PRIVATE gtest gtest_main)
45+
endif()

test/gtest/type-builder.cpp

+39
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
#include "wasm-type.h"
66
#include "gtest/gtest.h"
77

8+
#ifdef FUZZTEST
9+
#include "type-domains.h"
10+
#endif
11+
812
using namespace wasm;
913

1014
TEST_F(TypeTest, TypeBuilderGrowth) {
@@ -1056,6 +1060,41 @@ TEST_F(TypeTest, TestHeapTypeRelations) {
10561060
}
10571061
}
10581062

1063+
#ifdef FUZZTEST
1064+
1065+
void TestHeapTypeRelationsFuzz(std::pair<HeapType, HeapType> pair) {
1066+
auto [a, b] = pair;
1067+
auto lub = HeapType::getLeastUpperBound(a, b);
1068+
auto otherLub = HeapType::getLeastUpperBound(b, a);
1069+
EXPECT_EQ(lub, otherLub);
1070+
if (lub) {
1071+
EXPECT_EQ(a.getTop(), b.getTop());
1072+
EXPECT_EQ(a.getBottom(), b.getBottom());
1073+
EXPECT_TRUE(HeapType::isSubType(a, *lub));
1074+
EXPECT_TRUE(HeapType::isSubType(b, *lub));
1075+
} else {
1076+
EXPECT_NE(a.getTop(), b.getTop());
1077+
EXPECT_NE(a.getBottom(), b.getBottom());
1078+
}
1079+
if (a == b) {
1080+
EXPECT_EQ(lub, a);
1081+
EXPECT_EQ(lub, b);
1082+
} else if (lub && *lub == b) {
1083+
EXPECT_TRUE(HeapType::isSubType(a, b));
1084+
EXPECT_FALSE(HeapType::isSubType(b, a));
1085+
} else if (lub && *lub == a) {
1086+
EXPECT_FALSE(HeapType::isSubType(a, b));
1087+
EXPECT_TRUE(HeapType::isSubType(b, a));
1088+
} else if (lub) {
1089+
EXPECT_FALSE(HeapType::isSubType(a, b));
1090+
EXPECT_FALSE(HeapType::isSubType(b, a));
1091+
}
1092+
}
1093+
FUZZ_TEST(TypeFuzzTest, TestHeapTypeRelationsFuzz)
1094+
.WithDomains(ArbitraryHeapTypePair());
1095+
1096+
#endif // FUZZTEST
1097+
10591098
TEST_F(TypeTest, TestSubtypeErrors) {
10601099
Type anyref = Type(HeapType::any, Nullable);
10611100
Type eqref = Type(HeapType::eq, Nullable);

0 commit comments

Comments
 (0)