diff --git a/docs/architecture.md b/docs/architecture.md index f7a0547d..7d57dcce 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -78,3 +78,10 @@ possible to decide if a loaded module should be instrumented or not. **Consequences**: Independence of JavaScript engine, fine-grained instrumentation control + +## Visualization + +The most crucial inner mechanisms that make Jazzer.js work are highlighted in +the below Figure: + +![DetailOvervoew](pictures/jazzer-detail_overview.jpg "Detail overview") diff --git a/docs/pictures/jazzer-detail_overview.jpg b/docs/pictures/jazzer-detail_overview.jpg new file mode 100644 index 00000000..c08a8c72 Binary files /dev/null and b/docs/pictures/jazzer-detail_overview.jpg differ diff --git a/jest.config.js b/jest.config.js index 5d0848f3..30b2e4ba 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,6 +23,7 @@ module.exports = { "packages/fuzzer/build", "tests/code_coverage", ], + testMatch: ["/packages/**/*.test.[jt]s"], collectCoverageFrom: ["packages/**/*.ts"], coveragePathIgnorePatterns: ["/node_modules/", "/dist/"], }; diff --git a/package-lock.json b/package-lock.json index cbecadc7..f8e3711a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1768,9 +1768,9 @@ } }, "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "dev": true }, "node_modules/axios": { @@ -9951,9 +9951,9 @@ "dev": true }, "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "dev": true }, "axios": { diff --git a/packages/core/core.ts b/packages/core/core.ts index ecc28421..09ea3590 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -102,7 +102,8 @@ export async function startFuzzing(options: Options) { undefined, options.expectedErrors, options.coverageDirectory, - options.coverageReporters + options.coverageReporters, + options.sync ); }, (err: unknown) => { @@ -110,7 +111,8 @@ export async function startFuzzing(options: Options) { err, options.expectedErrors, options.coverageDirectory, - options.coverageReporters + options.coverageReporters, + options.sync ); } ); @@ -186,8 +188,10 @@ function stopFuzzing( err: unknown, expectedErrors: string[], coverageDirectory: string, - coverageReporters: reports.ReportType[] + coverageReporters: reports.ReportType[], + sync: boolean ) { + const stopFuzzing = sync ? Fuzzer.stopFuzzing : Fuzzer.stopFuzzingAsync; if (process.env.JAZZER_DEBUG) { trackedHooks.categorizeUnknown(HookManager.hooks).print(); } @@ -211,7 +215,7 @@ function stopFuzzing( console.error( `ERROR: Received no error, but expected one of [${expectedErrors}].` ); - Fuzzer.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); + stopFuzzing(ERROR_UNEXPECTED_CODE); } return; } @@ -221,13 +225,13 @@ function stopFuzzing( const name = errorName(err); if (expectedErrors.includes(name)) { console.error(`INFO: Received expected error "${name}".`); - Fuzzer.stopFuzzingAsync(ERROR_EXPECTED_CODE); + stopFuzzing(ERROR_EXPECTED_CODE); } else { printError(err); console.error( `ERROR: Received error "${name}" is not in expected errors [${expectedErrors}].` ); - Fuzzer.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); + stopFuzzing(ERROR_UNEXPECTED_CODE); } return; } @@ -235,7 +239,7 @@ function stopFuzzing( // Error found, but no specific one expected. This case is used for normal // fuzzing runs, so no dedicated exit code is given to the stop fuzzing function. printError(err); - Fuzzer.stopFuzzingAsync(); + stopFuzzing(); } function errorName(error: unknown): string { @@ -296,7 +300,6 @@ function buildFuzzerOptions(options: Options): string[] { } const inSeconds = Math.ceil(options.timeout / 1000); opts = opts.concat(`-timeout=${inSeconds}`); - return [prepareLibFuzzerArg0(opts), ...opts]; } diff --git a/packages/fuzzer/CMakeLists.txt b/packages/fuzzer/CMakeLists.txt index 6983744b..50e3a319 100644 --- a/packages/fuzzer/CMakeLists.txt +++ b/packages/fuzzer/CMakeLists.txt @@ -4,18 +4,26 @@ project(jazzerjs) find_package(Patch REQUIRED) -set(CMAKE_CXX_STANDARD 17) # mostly supported since GCC 7 +set(CMAKE_CXX_STANDARD 17) # mostly supported since GCC 7 set(CMAKE_CXX_STANDARD_REQUIRED ON) set(LLVM_ENABLE_LLD TRUE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) +# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24: +if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") + cmake_policy(SET CMP0135 NEW) +endif() # To help with development, let's write compile_commands.json unconditionally. set(CMAKE_EXPORT_COMPILE_COMMANDS 1) # As per the cmake-js README, we need the following to build on Windows: -if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) +if(MSVC + AND CMAKE_JS_NODELIB_DEF + AND CMAKE_JS_NODELIB_TARGET) # Generate node.lib - execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) + execute_process( + COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} + /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") @@ -46,14 +54,15 @@ endif() # releases.) add_definitions(-DNAPI_VERSION=4) -add_library(${PROJECT_NAME} SHARED +add_library( + ${PROJECT_NAME} SHARED "addon.cpp" "shared/callbacks.cpp" "shared/coverage.cpp" "shared/sanitizer_symbols.cpp" "shared/tracing.cpp" - "start_fuzzing_sync.cpp" - "start_fuzzing_async.cpp" + "fuzzing_sync.cpp" + "fuzzing_async.cpp" "utils.cpp" ${CMAKE_JS_SRC}) set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") @@ -62,66 +71,75 @@ target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB}) # Download and build compiler-rt, which contains libfuzzer. include(ExternalProject) -ExternalProject_Add(compiler-rt +ExternalProject_Add( + compiler-rt URL https://github.com/CodeIntelligenceTesting/llvm-project-jazzer/archive/refs/tags/2022-11-25.tar.gz - URL_HASH SHA256=e691dc9b45c35713fa67c613d352b646f30cab5d35d15abfcf77cc004a3befdb + URL_HASH + SHA256=e691dc9b45c35713fa67c613d352b646f30cab5d35d15abfcf77cc004a3befdb SOURCE_SUBDIR compiler-rt - CMAKE_ARGS - # compiler-rt usually initializes the sanitizer runtime by means of a pointer - # in the .preinit_array section; since .preinit_array isn't supported for - # shared objects like our Node plugin, disable it here. - -DCMAKE_CXX_FLAGS="-DSANITIZER_CAN_USE_PREINIT_ARRAY=0" - # No need to build all the sanitizers; the UBSan standalone runtime is built - # by default. - -DCOMPILER_RT_SANITIZERS_TO_BUILD="" - # Don't build libc++ into the fuzzer; our own code is C++ as well, so we're - # going to link against a C++ runtime anyway. - -DCOMPILER_RT_USE_LIBCXX=OFF - # Use the same build type as the parent project. - -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - # We only need libfuzzer from the compiler-rt project. - BUILD_COMMAND ${CMAKE_COMMAND} --build --target ${LIBFUZZER_TARGET} - # Skip the install step because it tries to copy files to a hardcoded path in "/usr". + CMAKE_ARGS # compiler-rt usually initializes the sanitizer runtime by means of + # a pointer in the .preinit_array section; since .preinit_array + # isn't supported for shared objects like our Node plugin, disable + # it here. + -DCMAKE_CXX_FLAGS="-DSANITIZER_CAN_USE_PREINIT_ARRAY=0" + # No need to build all the sanitizers; the UBSan standalone runtime + # is built by default. + -DCOMPILER_RT_SANITIZERS_TO_BUILD="" + # Don't build libc++ into the fuzzer; our own code is C++ as well, + # so we're going to link against a C++ runtime anyway. + -DCOMPILER_RT_USE_LIBCXX=OFF + # Use the same build type as the parent project. + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + # We only need libfuzzer from the compiler-rt project. + BUILD_COMMAND ${CMAKE_COMMAND} --build --target + ${LIBFUZZER_TARGET} + # Skip the install step because it tries to copy files to a hardcoded path in + # "/usr". INSTALL_COMMAND "" # Tell CMake about the libfuzzer libraries that are built as part of the # external project. Some CMake generators fail if we later depend on the # libraries without declaring them here (including ninja). - BUILD_BYPRODUCTS - /${LIBFUZZER_STATIC_LIB_PATH}) + BUILD_BYPRODUCTS /${LIBFUZZER_STATIC_LIB_PATH}) # Make our plugin depend on and link against libfuzzer. add_dependencies(${PROJECT_NAME} compiler-rt) ExternalProject_Get_Property(compiler-rt BINARY_DIR) ExternalProject_Get_Property(compiler-rt SOURCE_DIR) -target_include_directories(${PROJECT_NAME} PRIVATE ${SOURCE_DIR}/compiler-rt/lib) +target_include_directories(${PROJECT_NAME} + PRIVATE ${SOURCE_DIR}/compiler-rt/lib) # We may want to include additional libraries here. For example, # libclang_rt.fuzzer_interceptors-x86_64.a contains # https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/fuzzer/FuzzerInterceptors.cpp, -# i.e., fuzzer-friendly overrides for some common libc functions. However, -# there is a challenge with this particular library: we're not in the binary, -# so we can't intercept libc. +# i.e., fuzzer-friendly overrides for some common libc functions. However, there +# is a challenge with this particular library: we're not in the binary, so we +# can't intercept libc. # # Remember to add any libraries mentioned here to the BUILD_BYPRODUCTS of the # external compiler-rt project above. if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_link_libraries(${PROJECT_NAME} - -Wl,-whole-archive - ${BINARY_DIR}/${LIBFUZZER_STATIC_LIB_PATH} - -Wl,-no-whole-archive) + target_link_libraries( + ${PROJECT_NAME} -Wl,-whole-archive + ${BINARY_DIR}/${LIBFUZZER_STATIC_LIB_PATH} -Wl,-no-whole-archive) elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") - target_link_libraries(${PROJECT_NAME} - -Wl,-all_load - ${BINARY_DIR}/${LIBFUZZER_STATIC_LIB_PATH} - -Wl,-noall_load) + target_link_libraries( + ${PROJECT_NAME} -Wl,-all_load ${BINARY_DIR}/${LIBFUZZER_STATIC_LIB_PATH} + -Wl,-noall_load) elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") # Force MSVC to do an MT build, suggested by cmake-js cmake_policy(SET CMP0091 NEW) - target_link_libraries(${PROJECT_NAME} "$") + target_link_libraries( + ${PROJECT_NAME} + "$") endif() -# Avoid downloading the full nodejs headers, and bundle a much more lightweight copy instead -if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) +# Avoid downloading the full nodejs headers, and bundle a much more lightweight +# copy instead +if(MSVC + AND CMAKE_JS_NODELIB_DEF + AND CMAKE_JS_NODELIB_TARGET) # Generate node.lib - execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) + execute_process( + COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} + /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) endif() diff --git a/packages/fuzzer/addon.cpp b/packages/fuzzer/addon.cpp index 5a46967c..99ff33e2 100644 --- a/packages/fuzzer/addon.cpp +++ b/packages/fuzzer/addon.cpp @@ -14,8 +14,8 @@ #include -#include "start_fuzzing_async.h" -#include "start_fuzzing_sync.h" +#include "fuzzing_async.h" +#include "fuzzing_sync.h" #include "shared/callbacks.h" @@ -27,16 +27,30 @@ void PrintVersion(const Napi::CallbackInfo &info) { << " using Node-API version " << napi_version << std::endl; } -// Initialize the module by populating its JS exports with pointers to our C++ -// functions. +// This code is defining a function called "Init" which is used to initialize a +// Node.js addon module. The function takes two arguments, an "env" object, and +// an "exports" object. +// The "exports" object is an instance of the `Napi::Object` class, which is +// used to define the exports of the Node.js addon module. The code is adding +// properties to the "exports" object, where each property is a JavaScript +// function that corresponds to a C++ function. +// `RegisterCallbackExports` links more functions needed, like coverage tracking +// capabilities. Napi::Object Init(Napi::Env env, Napi::Object exports) { exports["printVersion"] = Napi::Function::New(env); exports["startFuzzing"] = Napi::Function::New(env); exports["startFuzzingAsync"] = Napi::Function::New(env); exports["stopFuzzingAsync"] = Napi::Function::New(env); + exports["stopFuzzing"] = Napi::Function::New(env); RegisterCallbackExports(env, exports); return exports; } +// Macro that exports the "Init" function as the entry point of the addon module +// named "myPackage". When this addon is imported in a Node.js script, the +// "Init" function will be executed to define the exports of the addon. +// This effectively allows us to do this from the Node.js side of things: +// const jazzerjs = require('jazzerjs'); +// jazzerjs.printVersion NODE_API_MODULE(jazzerjs, Init) diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index 84cf75fb..d7b936a3 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -60,6 +60,7 @@ type NativeAddon = { startFuzzing: StartFuzzingSyncFn; startFuzzingAsync: StartFuzzingAsyncFn; stopFuzzingAsync: (status?: number) => void; + stopFuzzing: (status?: number) => void; }; export const addon: NativeAddon = bind("jazzerjs"); diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index 25ef92a6..0cda36f5 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -30,6 +30,7 @@ export interface Fuzzer { startFuzzing: typeof addon.startFuzzing; startFuzzingAsync: typeof addon.startFuzzingAsync; stopFuzzingAsync: typeof addon.stopFuzzingAsync; + stopFuzzing: typeof addon.stopFuzzing; } export const fuzzer: Fuzzer = { @@ -38,6 +39,7 @@ export const fuzzer: Fuzzer = { startFuzzing: addon.startFuzzing, startFuzzingAsync: addon.startFuzzingAsync, stopFuzzingAsync: addon.stopFuzzingAsync, + stopFuzzing: addon.stopFuzzing, }; export type { CoverageTracker } from "./coverage"; diff --git a/packages/fuzzer/start_fuzzing_async.cpp b/packages/fuzzer/fuzzing_async.cpp similarity index 96% rename from packages/fuzzer/start_fuzzing_async.cpp rename to packages/fuzzer/fuzzing_async.cpp index a3456290..0fd0a63a 100644 --- a/packages/fuzzer/start_fuzzing_async.cpp +++ b/packages/fuzzer/fuzzing_async.cpp @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "napi.h" +#include #include #include @@ -23,8 +25,8 @@ #define GetPID getpid #endif +#include "fuzzing_async.h" #include "shared/libfuzzer.h" -#include "start_fuzzing_async.h" #include "utils.h" namespace { @@ -89,7 +91,7 @@ int FuzzCallbackAsync(const uint8_t *Data, size_t Size) { // cleanup including libfuzzer exit handlers. _Exit(libfuzzer::ExitErrorCode); } - return 0; + return EXIT_SUCCESS; } // This function is the callback that gets executed in the addon's main thread @@ -164,6 +166,7 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function jsFuzzCallback, // not, an appropriate error, describing the illegal return value, // can be set. As everything is executed on the main event loop, no // synchronization is needed. + AsyncReturnsHandler(); if (context->is_resolved) { return; } @@ -176,6 +179,8 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function jsFuzzCallback, "done callback based fuzz tests allowed.") .Value()); context->is_resolved = true; + } else { + SyncReturnsHandler(); } return; } @@ -186,6 +191,7 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function jsFuzzCallback, // resolving the fuzzer promise and continue fuzzing. Otherwise, resolve // and continue directly. if (result.IsPromise()) { + AsyncReturnsHandler(); auto jsPromise = result.As(); auto then = jsPromise.Get("then").As(); then.Call( @@ -203,6 +209,7 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function jsFuzzCallback, std::make_exception_ptr(JSException())); })}); } else { + SyncReturnsHandler(); data->promise->set_value(nullptr); } } else { @@ -285,15 +292,12 @@ Napi::Value StartFuzzingAsync(const Napi::CallbackInfo &info) { } void StopFuzzingAsync(const Napi::CallbackInfo &info) { - int exitCode = libfuzzer::ExitErrorCode; + int exitCode = StopFuzzingHandleExit(info); - if (info[0].IsNumber()) { - exitCode = info[0].As().Int32Value(); - } else { - // If a dedicated status code is provided, the run is executed as internal - // test and the crashing input does not need to be printed/saved. - libfuzzer::PrintCrashingInput(); - } + // If we ran in async mode and we only ever encountered synchronous results + // we'll give an indicator that running in synchronous mode is likely + // benefical + ReturnValueInfo(false); // We call _Exit to immediately terminate the process without performing any // cleanup including libfuzzer exit handlers. These handlers print information diff --git a/packages/fuzzer/start_fuzzing_async.h b/packages/fuzzer/fuzzing_async.h similarity index 92% rename from packages/fuzzer/start_fuzzing_async.h rename to packages/fuzzer/fuzzing_async.h index 076c52ef..0c344507 100644 --- a/packages/fuzzer/start_fuzzing_async.h +++ b/packages/fuzzer/fuzzing_async.h @@ -17,4 +17,4 @@ #include Napi::Value StartFuzzingAsync(const Napi::CallbackInfo &info); -void StopFuzzingAsync(const Napi::CallbackInfo &info); \ No newline at end of file +void StopFuzzingAsync(const Napi::CallbackInfo &info); diff --git a/packages/fuzzer/start_fuzzing_sync.cpp b/packages/fuzzer/fuzzing_sync.cpp similarity index 73% rename from packages/fuzzer/start_fuzzing_sync.cpp rename to packages/fuzzer/fuzzing_sync.cpp index d9ed0af0..577db48c 100644 --- a/packages/fuzzer/start_fuzzing_sync.cpp +++ b/packages/fuzzer/fuzzing_sync.cpp @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "start_fuzzing_sync.h" +#include "fuzzing_sync.h" +#include "shared/libfuzzer.h" #include "utils.h" +#include #include namespace { @@ -49,18 +51,25 @@ int FuzzCallbackSync(const uint8_t *Data, size_t Size) { // modify it (else the fuzzer will abort); moreover, we don't know when // the JS buffer is going to be garbage-collected. But it would still be // nice for efficiency if we could use a pointer instead of copying. + // auto data = Napi::Buffer::Copy(gFuzzTarget->env, Data, Size); - gFuzzTarget->target.Call({data}); - return 0; + auto result = gFuzzTarget->target.Call({data}); + + if (result.IsPromise()) { + AsyncReturnsHandler(); + } else { + SyncReturnsHandler(); + } + return EXIT_SUCCESS; } // Start libfuzzer with a JS fuzz target. // -// This is a JS-enabled version of libfuzzer's main function (see FuzzerMain.cpp -// in the compiler-rt source). It takes the fuzz target, which must be a JS -// function taking a single data argument, as its first parameter; the fuzz -// target's return value is ignored. The second argument is an array of -// (command-line) arguments to pass to libfuzzer. +// This is a JS-enabled version of libfuzzer's main function (see +// FuzzerMain.cpp in the compiler-rt source). It takes the fuzz target, which +// must be a JS function taking a single data argument, as its first +// parameter; the fuzz target's return value is ignored. The second argument +// is an array of (command-line) arguments to pass to libfuzzer. void StartFuzzing(const Napi::CallbackInfo &info) { if (info.Length() != 2 || !info[0].IsFunction() || !info[1].IsArray()) { throw Napi::Error::New(info.Env(), @@ -80,3 +89,19 @@ void StartFuzzing(const Napi::CallbackInfo &info) { // when we return. gFuzzTarget = {}; } + +void StopFuzzing(const Napi::CallbackInfo &info) { + int exitCode = StopFuzzingHandleExit(info); + + // If we ran in async mode and we only ever encountered synchronous results + // we'll give an indicator that running in synchronous mode is likely + // benefical + ReturnValueInfo(true); + + // We call _Exit to immediately terminate the process without performing any + // cleanup including libfuzzer exit handlers. These handlers print information + // about the native libfuzzer target which is neither relevant nor actionable + // for JavaScript developers. We provide the relevant crash information + // such as the error message and stack trace in Jazzer.js CLI. + _Exit(exitCode); +} diff --git a/packages/fuzzer/start_fuzzing_sync.h b/packages/fuzzer/fuzzing_sync.h similarity index 86% rename from packages/fuzzer/start_fuzzing_sync.h rename to packages/fuzzer/fuzzing_sync.h index b99e2727..edb9ada2 100644 --- a/packages/fuzzer/start_fuzzing_sync.h +++ b/packages/fuzzer/fuzzing_sync.h @@ -16,4 +16,5 @@ #include -void StartFuzzing(const Napi::CallbackInfo &info); \ No newline at end of file +void StartFuzzing(const Napi::CallbackInfo &info); +void StopFuzzing(const Napi::CallbackInfo &info); diff --git a/packages/fuzzer/utils.cpp b/packages/fuzzer/utils.cpp index 4a89f875..6aec2b08 100644 --- a/packages/fuzzer/utils.cpp +++ b/packages/fuzzer/utils.cpp @@ -14,6 +14,10 @@ #include "utils.h" #include "napi.h" +#include "shared/libfuzzer.h" +#include + +#define btoa(x) ((x) ? "true" : "false") void StartLibFuzzer(const std::vector &args, fuzzer::UserCallback fuzzCallback) { @@ -27,6 +31,9 @@ void StartLibFuzzer(const std::vector &args, fuzzer::FuzzerDriver(&argc, &argv, fuzzCallback); } +// Constructs a libfuzzer usable string array based on an array +// object originating from a `Napi::CallbackInfo` object that is +// pre-filled by the caller. std::vector LibFuzzerArgs(Napi::Env env, const Napi::Array &jsArgs) { std::vector fuzzer_args; @@ -40,3 +47,61 @@ std::vector LibFuzzerArgs(Napi::Env env, } return fuzzer_args; } + +// The following two small functions serve as a simple mechanism for keeping +// track of encountered return values in the fuzzed target function. IFF both +// `exclAsyncReturns` and `exclSyncReturns` are toggled the `mixedReturns` is +// enabled. These toggles are used to inform the user about a potential +// performance benefit when fuzzing asynchronously but only synchronous return +// values are observed during a campaign. In such cases a user will be informed +// about this once libfuzzer exits, e.g. due to a crash, or timeout. +bool exclSyncReturns = false, exclAsyncReturns = false, mixedReturns = false; +void AsyncReturnsHandler() { + exclAsyncReturns = true; + if (exclSyncReturns) { + mixedReturns = true; + } +} + +void SyncReturnsHandler() { + exclSyncReturns = true; + if (exclAsyncReturns) { + mixedReturns = true; + } +} + +void ReturnValueInfo(bool is_sync_runner) { + if (!is_sync_runner) { + if (exclSyncReturns && !mixedReturns) { + std::cerr + << "\n== Jazzer.js:\n" + << " Exclusively observed synchronous return values from fuzzed " + "function." + << " Fuzzing in synchronous mode seems benefical!\n" + << " To enable it, append a `--sync` to your Jazzer.js invocation." + << std::endl; + } + } else { + if (mixedReturns) { + std::cerr << "\n== Jazzer.js:\n" + << " Observed asynchronous return values from " + "fuzzed function." + << " Fuzzing in asynchronous mode seems benefical!\n" + << " Remove the `--sync` flag from your Jazzer.js invocation." + << std::endl; + } + } +} + +int StopFuzzingHandleExit(const Napi::CallbackInfo &info) { + int exitCode = libfuzzer::ExitErrorCode; + + if (info[0].IsNumber()) { + exitCode = info[0].As().Int32Value(); + } else { + // If a dedicated status code is provided, the run is executed as internal + // test and the crashing input does not need to be printed/saved. + libfuzzer::PrintCrashingInput(); + } + return exitCode; +} diff --git a/packages/fuzzer/utils.h b/packages/fuzzer/utils.h index 5628603e..b9b771b1 100644 --- a/packages/fuzzer/utils.h +++ b/packages/fuzzer/utils.h @@ -23,3 +23,8 @@ void StartLibFuzzer(const std::vector &args, fuzzer::UserCallback fuzzCallback); std::vector LibFuzzerArgs(Napi::Env env, const Napi::Array &jsArgs); + +int StopFuzzingHandleExit(const Napi::CallbackInfo &info); +void AsyncReturnsHandler(); +void SyncReturnsHandler(); +void ReturnValueInfo(bool); diff --git a/tests/FuzzedDataProvider/fuzz.js b/tests/FuzzedDataProvider/fuzz.js index 9595da7f..029dbc8d 100644 --- a/tests/FuzzedDataProvider/fuzz.js +++ b/tests/FuzzedDataProvider/fuzz.js @@ -1,5 +1,5 @@ /* - * Copyright 2022 Code Intelligence GmbH + * Copyright 2023 Code Intelligence GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/code_coverage/.gitignore b/tests/code_coverage/.gitignore new file mode 100644 index 00000000..32747612 --- /dev/null +++ b/tests/code_coverage/.gitignore @@ -0,0 +1,3 @@ +# Fuzzer coverage +**/coverage[0-9]*/ +sample_fuzz_test/.jazzerjsrc.json diff --git a/tests/return_values/asyncRunnerAsyncReturns/fuzz.js b/tests/return_values/asyncRunnerAsyncReturns/fuzz.js new file mode 100644 index 00000000..dadc8756 --- /dev/null +++ b/tests/return_values/asyncRunnerAsyncReturns/fuzz.js @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * 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 + * + * http://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. + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ +const code = require("../exampleCode/code"); + +let syncCtr = 0; +let asyncCtr = 0; + +/** + * @param { Buffer } data + */ +module.exports.fuzz = function (data) { + if (data.length < 16) { + return; + } + + const name = code.encrypt(data.readInt32BE(0), code.ReturnType.ASYNC); + if (name instanceof Promise) { + asyncCtr += 1; + } else { + syncCtr += 1; + } + if (asyncCtr + syncCtr > 100) { + throw Error("Mixed return values condition reached!"); + } + return name; +}; diff --git a/tests/return_values/asyncRunnerAsyncReturns/package.json b/tests/return_values/asyncRunnerAsyncReturns/package.json new file mode 100644 index 00000000..a6770fe5 --- /dev/null +++ b/tests/return_values/asyncRunnerAsyncReturns/package.json @@ -0,0 +1,12 @@ +{ + "name": "jazzerjs-string-compare-async-async-info", + "version": "1.0.0", + "description": "An example showing how Jazzer.js handles string comparisons in the code", + "scripts": { + "fuzz": "jazzer fuzz -- -max_total_time=60", + "dryRun": "jazzer fuzz -- -runs=100 -seed=123456789 " + }, + "devDependencies": { + "@jazzer.js/core": "file:../../../packages/core" + } +} diff --git a/tests/return_values/asyncRunnerMixedReturns/fuzz.js b/tests/return_values/asyncRunnerMixedReturns/fuzz.js new file mode 100644 index 00000000..36ba7142 --- /dev/null +++ b/tests/return_values/asyncRunnerMixedReturns/fuzz.js @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * 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 + * + * http://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. + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ +const code = require("../exampleCode/code"); + +let syncCtr = 0; +let asyncCtr = 0; + +/** + * @param { Buffer } data + */ +module.exports.fuzz = function (data) { + if (data.length < 16) { + return; + } + + const name = code.encrypt(data.readInt32BE(0), code.ReturnType.MIXED); + if (name instanceof Promise) { + asyncCtr += 1; + } else { + syncCtr += 1; + } + if (asyncCtr + syncCtr > 100) { + throw Error("Mixed return values condition reached!"); + } + return name; +}; diff --git a/tests/return_values/asyncRunnerMixedReturns/package.json b/tests/return_values/asyncRunnerMixedReturns/package.json new file mode 100644 index 00000000..1dce3d1c --- /dev/null +++ b/tests/return_values/asyncRunnerMixedReturns/package.json @@ -0,0 +1,12 @@ +{ + "name": "jazzerjs-string-compare-async-mixed-no-info", + "version": "1.0.0", + "description": "An example showing how Jazzer.js handles string comparisons in the code", + "scripts": { + "fuzz": "jazzer fuzz -- -max_total_time=60 ", + "dryRun": "jazzer fuzz -- -runs=100 -seed=123456789 " + }, + "devDependencies": { + "@jazzer.js/core": "file:../../../packages/core" + } +} diff --git a/tests/return_values/asyncRunnerSyncReturns/fuzz.js b/tests/return_values/asyncRunnerSyncReturns/fuzz.js new file mode 100644 index 00000000..e98fe057 --- /dev/null +++ b/tests/return_values/asyncRunnerSyncReturns/fuzz.js @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * 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 + * + * http://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. + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ +const code = require("../exampleCode/code"); + +let syncCtr = 0; +let asyncCtr = 0; + +/** + * @param { Buffer } data + */ +module.exports.fuzz = function (data) { + if (data.length < 16) { + return; + } + + const name = code.encrypt(data.readInt32BE(0), code.ReturnType.SYNC); + if (name instanceof Promise) { + asyncCtr += 1; + } else { + syncCtr += 1; + } + if (asyncCtr + syncCtr > 100) { + throw Error("Mixed return values condition reached!"); + } + return name; +}; diff --git a/tests/return_values/asyncRunnerSyncReturns/package.json b/tests/return_values/asyncRunnerSyncReturns/package.json new file mode 100644 index 00000000..443fc406 --- /dev/null +++ b/tests/return_values/asyncRunnerSyncReturns/package.json @@ -0,0 +1,12 @@ +{ + "name": "jazzerjs-string-compare-async-sync-info", + "version": "1.0.0", + "description": "An example showing how Jazzer.js handles string comparisons in the code", + "scripts": { + "fuzz": "jazzer fuzz -- -max_total_time=60 ", + "dryRun": "jazzer fuzz -- -runs=100 -seed=123456789 " + }, + "devDependencies": { + "@jazzer.js/core": "file:../../../packages/core" + } +} diff --git a/tests/return_values/exampleCode/code.js b/tests/return_values/exampleCode/code.js new file mode 100644 index 00000000..0804e74e --- /dev/null +++ b/tests/return_values/exampleCode/code.js @@ -0,0 +1,40 @@ +const ReturnType = { + SYNC: "sync", + ASYNC: "async", + MIXED: "mixed", +}; + +exports.ReturnType = ReturnType; + +/** + * @param {number} n + */ +exports.encrypt = function encrypt(n, return_type) { + const ret = n ^ 0x11223344; + switch (return_type) { + case ReturnType.SYNC: + return ret; + case ReturnType.ASYNC: + return new Promise((resolve) => { + setImmediate(() => { + resolve(ret); + }); + }); + case ReturnType.MIXED: { + const syncOrAsync = Math.random() >= 0.5; + // Synchronous result + if (syncOrAsync) { + return ret; + } else { + // Asynchronous result + return new Promise((resolve) => { + setImmediate(() => { + resolve(ret); + }); + }); + } + } + default: + return ret; + } +}; diff --git a/tests/return_values/package.json b/tests/return_values/package.json new file mode 100644 index 00000000..10e62159 --- /dev/null +++ b/tests/return_values/package.json @@ -0,0 +1,11 @@ +{ + "name": "jazzerjs-sync-async-detection", + "version": "1.0.0", + "description": "Unit tests for detection of sync and async behavior.", + "scripts": { + "fuzz": "jest" + }, + "devDependencies": { + "@jazzer.js/core": "file:../../packages/core/" + } +} diff --git a/tests/return_values/return_values.test.js b/tests/return_values/return_values.test.js new file mode 100644 index 00000000..061081b6 --- /dev/null +++ b/tests/return_values/return_values.test.js @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * 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 + * + * http://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. + */ + +/* eslint no-undef: 0 */ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { spawnSync } = require("child_process"); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const path = require("path"); +const SyncInfo = + "Exclusively observed synchronous return values from fuzzed function. Fuzzing in synchronous mode seems benefical!"; +const AsyncInfo = + "Observed asynchronous return values from fuzzed function. Fuzzing in asynchronous mode seems benefical!"; + +// current working directory +const testDirectory = __dirname; + +describe("Execute a sync runner", () => { + it("Expect a hint due to async and sync return values", () => { + const testCaseDir = path.join(testDirectory, "syncRunnerMixedReturns"); + const log = executeFuzzTest(true, false, testCaseDir); + expect(log).toContain(AsyncInfo.trim()); + }); + it("Expect a hint due to exclusively async return values", () => { + const testCaseDir = path.join(testDirectory, "syncRunnerAsyncReturns"); + const log = executeFuzzTest(true, false, testCaseDir); + expect(log.trim()).toContain(AsyncInfo.trim()); + }); + it("Expect no hint due to strict synchronous return values", () => { + const testCaseDir = path.join(testDirectory, "syncRunnerSyncReturns"); + const log = executeFuzzTest(true, false, testCaseDir); + expect(log.includes(SyncInfo)).toBeFalsy(); + expect(log.includes(AsyncInfo)).toBeFalsy(); + }); +}); + +describe("Execute a async runner", () => { + it("Expect no hint due to async and sync return values", () => { + const testCaseDir = path.join(testDirectory, "asyncRunnerMixedReturns"); + const log = executeFuzzTest(false, false, testCaseDir); + expect(log.includes(SyncInfo)).toBeFalsy(); + expect(log.includes(AsyncInfo)).toBeFalsy(); + }); + it("Expect a hint due to exclusively sync return values", () => { + const testCaseDir = path.join(testDirectory, "asyncRunnerSyncReturns"); + const log = executeFuzzTest(false, false, testCaseDir); + expect(log.trim()).toContain(SyncInfo.trim()); + }); + it("Expect no hint due to strict asynchronous return values", () => { + const testCaseDir = path.join(testDirectory, "asyncRunnerAsyncReturns"); + const log = executeFuzzTest(false, false, testCaseDir); + expect(log.includes(SyncInfo)).toBeFalsy(); + expect(log.includes(AsyncInfo)).toBeFalsy(); + }); +}); + +function executeFuzzTest(sync, verbose, dir) { + let options = ["jazzer", "fuzz"]; + // Specify mode + if (sync) options.push("--sync"); + options.push("--"); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const process = spawnSync("npx", options, { + stdio: "pipe", + cwd: dir, + shell: true, + windowsHide: true, + }); + let stdout = process.output.toString(); + if (verbose) console.log(stdout); + return stdout; +} diff --git a/tests/return_values/syncRunnerAsyncReturns/fuzz.js b/tests/return_values/syncRunnerAsyncReturns/fuzz.js new file mode 100644 index 00000000..dadc8756 --- /dev/null +++ b/tests/return_values/syncRunnerAsyncReturns/fuzz.js @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * 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 + * + * http://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. + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ +const code = require("../exampleCode/code"); + +let syncCtr = 0; +let asyncCtr = 0; + +/** + * @param { Buffer } data + */ +module.exports.fuzz = function (data) { + if (data.length < 16) { + return; + } + + const name = code.encrypt(data.readInt32BE(0), code.ReturnType.ASYNC); + if (name instanceof Promise) { + asyncCtr += 1; + } else { + syncCtr += 1; + } + if (asyncCtr + syncCtr > 100) { + throw Error("Mixed return values condition reached!"); + } + return name; +}; diff --git a/tests/return_values/syncRunnerAsyncReturns/package.json b/tests/return_values/syncRunnerAsyncReturns/package.json new file mode 100644 index 00000000..c23cda03 --- /dev/null +++ b/tests/return_values/syncRunnerAsyncReturns/package.json @@ -0,0 +1,12 @@ +{ + "name": "jazzerjs-string-compare-sync-async-info", + "version": "1.0.0", + "description": "An example showing how Jazzer.js handles string comparisons in the code", + "scripts": { + "fuzz": "jazzer fuzz --sync -- -max_total_time=60 ", + "dryRun": "jazzer fuzz --sync -- -runs=100 -seed=123456789 " + }, + "devDependencies": { + "@jazzer.js/core": "file:../../../packages/core" + } +} diff --git a/tests/return_values/syncRunnerMixedReturns/fuzz.js b/tests/return_values/syncRunnerMixedReturns/fuzz.js new file mode 100644 index 00000000..36ba7142 --- /dev/null +++ b/tests/return_values/syncRunnerMixedReturns/fuzz.js @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * 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 + * + * http://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. + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ +const code = require("../exampleCode/code"); + +let syncCtr = 0; +let asyncCtr = 0; + +/** + * @param { Buffer } data + */ +module.exports.fuzz = function (data) { + if (data.length < 16) { + return; + } + + const name = code.encrypt(data.readInt32BE(0), code.ReturnType.MIXED); + if (name instanceof Promise) { + asyncCtr += 1; + } else { + syncCtr += 1; + } + if (asyncCtr + syncCtr > 100) { + throw Error("Mixed return values condition reached!"); + } + return name; +}; diff --git a/tests/return_values/syncRunnerMixedReturns/package.json b/tests/return_values/syncRunnerMixedReturns/package.json new file mode 100644 index 00000000..14db3d38 --- /dev/null +++ b/tests/return_values/syncRunnerMixedReturns/package.json @@ -0,0 +1,12 @@ +{ + "name": "jazzerjs-string-compare-sync-mixed-info", + "version": "1.0.0", + "description": "An example showing how Jazzer.js handles string comparisons in the code", + "scripts": { + "fuzz": "jazzer fuzz --sync -- -max_total_time=60 ", + "dryRun": "jazzer fuzz -- -runs=100 -seed=123456789 " + }, + "devDependencies": { + "@jazzer.js/core": "file:../../../packages/core" + } +} diff --git a/tests/return_values/syncRunnerSyncReturns/fuzz.js b/tests/return_values/syncRunnerSyncReturns/fuzz.js new file mode 100644 index 00000000..e98fe057 --- /dev/null +++ b/tests/return_values/syncRunnerSyncReturns/fuzz.js @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * 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 + * + * http://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. + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ +const code = require("../exampleCode/code"); + +let syncCtr = 0; +let asyncCtr = 0; + +/** + * @param { Buffer } data + */ +module.exports.fuzz = function (data) { + if (data.length < 16) { + return; + } + + const name = code.encrypt(data.readInt32BE(0), code.ReturnType.SYNC); + if (name instanceof Promise) { + asyncCtr += 1; + } else { + syncCtr += 1; + } + if (asyncCtr + syncCtr > 100) { + throw Error("Mixed return values condition reached!"); + } + return name; +}; diff --git a/tests/return_values/syncRunnerSyncReturns/package.json b/tests/return_values/syncRunnerSyncReturns/package.json new file mode 100644 index 00000000..7e536f03 --- /dev/null +++ b/tests/return_values/syncRunnerSyncReturns/package.json @@ -0,0 +1,13 @@ +{ + "name": "jazzerjs-string-compare-sync-sync-no-info", + "version": "1.0.0", + "description": "An example showing how Jazzer.js handles string comparisons in the code", + "scripts": { + "fuzz": "jazzer fuzz --sync -- -max_total_time=60 ", + "dryRun": "jazzer fuzz -- -runs=100 -seed=123456789 " + }, + "devDependencies": { + "@jazzer.js/core": "file:../../../packages/core", + "@jazzer.js/jest-runner": "file:../../../packages/jest-runner/" + } +}