From 28d26b30e5fed7c23aa4467bcc5a26cbfab3f5d5 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sat, 18 Apr 2026 23:42:28 +0200 Subject: [PATCH 01/30] feat(fuzzer): add an internal LibAFL spike backend Link a minimal Rust/LibAFL static library into the native addon and expose internal sync/async spike entrypoints that reuse Jazzer.js's current JS execution model. This gives us a buildable Phase 0 backend for measuring the Rust/C++/Node boundary without changing the public engine surface yet. --- packages/fuzzer/CMakeLists.txt | 50 + packages/fuzzer/addon.cpp | 4 + packages/fuzzer/addon.ts | 18 + packages/fuzzer/libafl_spike.cpp | 452 +++++++++ packages/fuzzer/libafl_spike.h | 45 + packages/fuzzer/rust/Cargo.lock | 1370 ++++++++++++++++++++++++++ packages/fuzzer/rust/Cargo.toml | 20 + packages/fuzzer/rust/src/lib.rs | 181 ++++ packages/fuzzer/shared/callbacks.cpp | 4 + packages/fuzzer/shared/coverage.cpp | 15 + packages/fuzzer/shared/coverage.h | 6 + packages/fuzzer/shared/tracing.cpp | 66 ++ packages/fuzzer/shared/tracing.h | 9 + 13 files changed, 2240 insertions(+) create mode 100644 packages/fuzzer/libafl_spike.cpp create mode 100644 packages/fuzzer/libafl_spike.h create mode 100644 packages/fuzzer/rust/Cargo.lock create mode 100644 packages/fuzzer/rust/Cargo.toml create mode 100644 packages/fuzzer/rust/src/lib.rs diff --git a/packages/fuzzer/CMakeLists.txt b/packages/fuzzer/CMakeLists.txt index 0331affaa..a312c8c98 100644 --- a/packages/fuzzer/CMakeLists.txt +++ b/packages/fuzzer/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.15) project(jazzerjs) find_package(Patch REQUIRED) +find_program(CARGO_EXECUTABLE cargo REQUIRED) set(CMAKE_CXX_STANDARD 17) # mostly supported since GCC 7 set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -72,6 +73,55 @@ set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC}) target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB}) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(RUST_TARGET_TRIPLE "aarch64-unknown-linux-gnu") + else() + set(RUST_TARGET_TRIPLE "x86_64-unknown-linux-gnu") + endif() + set(RUST_STATICLIB_NAME "libjazzerjs_libafl_spike.a") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + if(CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64") + set(RUST_TARGET_TRIPLE "x86_64-apple-darwin") + elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "arm64") + set(RUST_TARGET_TRIPLE "aarch64-apple-darwin") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(RUST_TARGET_TRIPLE "aarch64-apple-darwin") + else() + set(RUST_TARGET_TRIPLE "x86_64-apple-darwin") + endif() + set(RUST_STATICLIB_NAME "libjazzerjs_libafl_spike.a") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(RUST_TARGET_TRIPLE "x86_64-pc-windows-msvc") + set(RUST_STATICLIB_NAME "jazzerjs_libafl_spike.lib") +endif() + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CARGO_PROFILE_DIR "debug") + set(CARGO_PROFILE_FLAG "") +else() + set(CARGO_PROFILE_DIR "release") + set(CARGO_PROFILE_FLAG "--release") +endif() + +set(RUST_CRATE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/rust") +set(RUST_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargo-target") +set(RUST_STATICLIB_PATH + "${RUST_TARGET_DIR}/${RUST_TARGET_TRIPLE}/${CARGO_PROFILE_DIR}/${RUST_STATICLIB_NAME}") + +add_custom_command( + OUTPUT ${RUST_STATICLIB_PATH} + COMMAND ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${RUST_TARGET_DIR} + ${CARGO_EXECUTABLE} build --manifest-path ${RUST_CRATE_DIR}/Cargo.toml + --target ${RUST_TARGET_TRIPLE} ${CARGO_PROFILE_FLAG} + WORKING_DIRECTORY ${RUST_CRATE_DIR} + DEPENDS ${RUST_CRATE_DIR}/Cargo.toml ${RUST_CRATE_DIR}/src/lib.rs + COMMENT "Building the LibAFL spike static library") + +add_custom_target(jazzerjs_libafl_spike ALL DEPENDS ${RUST_STATICLIB_PATH}) +add_dependencies(${PROJECT_NAME} jazzerjs_libafl_spike) +target_link_libraries(${PROJECT_NAME} ${RUST_STATICLIB_PATH}) + # We're not sure why but sometimes systems don't end up setting LLVM_TARGET_TRIPLE used in llvm's cmake to eventually # set COMPILER_RT_DEFAULT_TARGET which is necessary for compiler-rt to build # So this will either take it from an envvar or try to set it to a sane value until we can figure out why it's broken diff --git a/packages/fuzzer/addon.cpp b/packages/fuzzer/addon.cpp index b384ed220..e840bd3b6 100644 --- a/packages/fuzzer/addon.cpp +++ b/packages/fuzzer/addon.cpp @@ -16,6 +16,7 @@ #include "fuzzing_async.h" #include "fuzzing_sync.h" +#include "libafl_spike.h" #include "shared/callbacks.h" #include "shared/libfuzzer.h" @@ -61,6 +62,9 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) { exports["startFuzzing"] = Napi::Function::New(env); exports["startFuzzingAsync"] = Napi::Function::New(env); + exports["startLibAflSpike"] = Napi::Function::New(env); + exports["startLibAflSpikeAsync"] = + Napi::Function::New(env); RegisterCallbackExports(env, exports); return exports; diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index 96d5b4f4b..77862bb99 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -26,6 +26,11 @@ export type FuzzTargetCallback = ( ) => unknown; export type FuzzTarget = FuzzTargetAsyncOrValue | FuzzTargetCallback; export type FuzzOpts = string[]; +export type LibAflSpikeOptions = { + runs: number; + seed: number; + maxLen: number; +}; export type StartFuzzingSyncFn = ( fuzzFn: FuzzTarget, @@ -36,6 +41,15 @@ export type StartFuzzingAsyncFn = ( fuzzFn: FuzzTarget, fuzzOpts: FuzzOpts, ) => Promise; +export type StartLibAflSpikeSyncFn = ( + fuzzFn: FuzzTarget, + options: LibAflSpikeOptions, + jsStopCallback: (signal: number) => void, +) => Promise; +export type StartLibAflSpikeAsyncFn = ( + fuzzFn: FuzzTarget, + options: LibAflSpikeOptions, +) => Promise; type NativeAddon = { registerCoverageMap: (buffer: Buffer) => void; @@ -67,6 +81,10 @@ type NativeAddon = { startFuzzing: StartFuzzingSyncFn; startFuzzingAsync: StartFuzzingAsyncFn; + startLibAflSpike: StartLibAflSpikeSyncFn; + startLibAflSpikeAsync: StartLibAflSpikeAsyncFn; + clearCompareFeedbackMap: () => void; + countNonZeroCompareFeedbackSlots: () => number; }; function addonFilename(): string { diff --git a/packages/fuzzer/libafl_spike.cpp b/packages/fuzzer/libafl_spike.cpp new file mode 100644 index 000000000..bc7aabfd2 --- /dev/null +++ b/packages/fuzzer/libafl_spike.cpp @@ -0,0 +1,452 @@ +// Copyright 2026 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. + +#include "libafl_spike.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#define GetPID _getpid +#else +#include +#define GetPID getpid +#endif + +#include "shared/coverage.h" +#include "shared/libfuzzer.h" +#include "shared/tracing.h" +#include "utils.h" + +namespace { +constexpr int kExecutionContinue = 0; +constexpr int kExecutionFinding = 1; +constexpr int kExecutionStop = 2; +constexpr int kExecutionFatal = 3; + +constexpr int kSpikeOk = 0; +constexpr int kSpikeFoundFinding = 1; +constexpr int kSpikeStopped = 2; +constexpr int kSpikeFatal = 3; + +struct ParsedSpikeOptions { + uint64_t runs = 1000; + uint64_t seed = 1; + size_t max_len = 4096; +}; + +struct SyncFuzzTargetContext { + Napi::Env env; + Napi::Function target; + bool is_resolved; + Napi::Promise::Deferred deferred; + Napi::Function js_stop_callback; + volatile std::sig_atomic_t signal_status = 0; + volatile int sigints = 0; + std::jmp_buf execution_context; +}; + +struct AsyncFuzzTargetContext { + explicit AsyncFuzzTargetContext(Napi::Env env) + : deferred(Napi::Promise::Deferred::New(env)) {} + + std::thread native_thread; + Napi::Promise::Deferred deferred; + bool is_resolved = false; + bool is_done_called = false; + int run_status = kSpikeOk; + volatile int sigints = 0; + std::jmp_buf execution_context; +}; + +struct AsyncDataType { + const uint8_t *data; + size_t size; + std::promise *promise; + + AsyncDataType() = delete; +}; + +using AsyncFinalizerDataType = void; +void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, + AsyncFuzzTargetContext *context, + AsyncDataType *data); +using AsyncTsfn = Napi::TypedThreadSafeFunction; + +SyncFuzzTargetContext *gActiveSyncContext = nullptr; +AsyncFuzzTargetContext *gActiveAsyncContext = nullptr; +AsyncTsfn gAsyncTsfn; + +ParsedSpikeOptions ParseSpikeOptions(Napi::Env env, const Napi::Object &js_opts) { + ParsedSpikeOptions parsed; + + const auto runs = js_opts.Get("runs"); + const auto seed = js_opts.Get("seed"); + const auto max_len = js_opts.Get("maxLen"); + + if (!runs.IsNumber() || !seed.IsNumber() || !max_len.IsNumber()) { + throw Napi::Error::New( + env, + "The LibAFL spike expects an options object with numeric runs, seed, " + "and maxLen properties"); + } + + parsed.runs = runs.As().Int64Value(); + parsed.seed = seed.As().Int64Value(); + parsed.max_len = static_cast(max_len.As().Int64Value()); + + if (parsed.runs == 0) { + throw Napi::Error::New(env, "The LibAFL spike requires -runs to be > 0"); + } + if (parsed.max_len == 0) { + throw Napi::Error::New(env, + "The LibAFL spike requires -max_len to be > 0"); + } + + return parsed; +} + +JazzerLibAflSharedMaps SharedMapsForSpike(Napi::Env env) { + auto *edges = CoverageCounters(); + const auto edges_len = CoverageCountersSize(); + auto *cmp = CompareFeedbackMap(); + const auto cmp_len = CompareFeedbackMapSize(); + + if (edges == nullptr || edges_len == 0) { + throw Napi::Error::New(env, + "Coverage counters were not initialized before the " + "LibAFL spike started"); + } + + return {edges, edges_len, cmp, cmp_len}; +} + +void SyncSigintHandler(int signum) { + std::cerr << std::endl; + gActiveSyncContext->signal_status = signum; + if (gActiveSyncContext->sigints > 0) { + _Exit(libfuzzer::RETURN_CONTINUE); + } + gActiveSyncContext->sigints++; +} + +void SyncErrorSignalHandler(int signum) { + gActiveSyncContext->signal_status = signum; + std::longjmp(gActiveSyncContext->execution_context, signum); +} + +int ExecuteSyncInput(void *user_data, const uint8_t *data, size_t size) { + auto *context = static_cast(user_data); + auto scope = Napi::HandleScope(context->env); + + ClearCoverageCounters(); + ClearCompareFeedbackMap(); + + try { + auto buffer = Napi::Buffer::Copy(context->env, data, size); + if (setjmp(context->execution_context) == 0) { + auto result = context->target.Call({buffer}); + if (result.IsPromise()) { + AsyncReturnsHandler(); + } else { + SyncReturnsHandler(); + } + } + } catch (const Napi::Error &error) { + context->is_resolved = true; + context->deferred.Reject(error.Value()); + return kExecutionFinding; + } catch (std::exception &exception) { + std::cerr << "==" << static_cast(GetPID()) + << "== Jazzer.js: Unexpected Error: " << exception.what() + << std::endl; + libfuzzer::PrintCrashingInput(); + _Exit(libfuzzer::EXIT_ERROR_CODE); + } + + if (context->signal_status != 0) { + if (context->signal_status == SIGSEGV) { + std::cerr << "==" << static_cast(GetPID()) + << "== Segmentation Fault" << std::endl; + libfuzzer::PrintCrashingInput(); + _Exit(libfuzzer::EXIT_ERROR_SEGV); + } + + auto exit_code = Napi::Number::New(context->env, 0); + if (context->signal_status != SIGINT) { + exit_code = Napi::Number::New(context->env, context->signal_status); + } + + context->js_stop_callback.Call({exit_code}); + context->signal_status = 0; + return kExecutionStop; + } + + return kExecutionContinue; +} + +void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, + AsyncFuzzTargetContext *context, + AsyncDataType *data) { + try { + if (context->sigints > 0) { + data->promise->set_value(kExecutionStop); + context->deferred.Resolve(env.Undefined()); + context->is_resolved = true; + return; + } + + if (setjmp(context->execution_context) == SIGSEGV) { + std::cerr << "==" << static_cast(GetPID()) + << "== Segmentation Fault" << std::endl; + libfuzzer::PrintCrashingInput(); + _Exit(libfuzzer::EXIT_ERROR_SEGV); + } + + if (env == nullptr) { + data->promise->set_value(kExecutionFatal); + return; + } + + auto buffer = Napi::Buffer::Copy(env, data->data, data->size); + auto parameter_count = js_fuzz_callback.As() + .Get("length") + .As() + .Int32Value(); + + if (parameter_count > 1) { + context->is_done_called = false; + context->is_resolved = false; + auto done = Napi::Function::New( + env, [=](const Napi::CallbackInfo &info) { + if (context->is_resolved) { + return; + } + + if (context->is_done_called) { + context->deferred.Reject( + Napi::Error::New(env, "Expected done to be called once, but " + "it was called multiple times.") + .Value()); + context->is_resolved = true; + std::cerr << "Expected done to be called once, but it was called " + "multiple times." + << std::endl; + return; + } + + context->is_done_called = true; + auto has_error = !(info[0].IsNull() || info[0].IsUndefined()); + if (has_error) { + data->promise->set_value(kExecutionFinding); + context->deferred.Reject(info[0].As().Value()); + context->is_resolved = true; + } else { + data->promise->set_value(kExecutionContinue); + } + }); + + auto result = js_fuzz_callback.Call({buffer, done}); + if (result.IsPromise()) { + AsyncReturnsHandler(); + if (context->is_resolved) { + return; + } + if (!context->is_done_called) { + data->promise->set_value(kExecutionFinding); + } + context->deferred.Reject( + Napi::Error::New(env, "Internal fuzzer error - Either async or " + "done callback based fuzz tests allowed.") + .Value()); + context->is_resolved = true; + } else { + SyncReturnsHandler(); + } + return; + } + + auto result = js_fuzz_callback.Call({buffer}); + if (result.IsPromise()) { + AsyncReturnsHandler(); + auto js_promise = result.As(); + auto then = js_promise.Get("then").As(); + then.Call( + js_promise, + {Napi::Function::New(env, [=](const Napi::CallbackInfo &) { + data->promise->set_value(kExecutionContinue); + }), + Napi::Function::New(env, [=](const Napi::CallbackInfo &info) { + data->promise->set_value(kExecutionFinding); + context->deferred.Reject(info[0].As().Value()); + context->is_resolved = true; + })}); + } else { + SyncReturnsHandler(); + data->promise->set_value(kExecutionContinue); + } + } catch (const Napi::Error &error) { + if (context->is_resolved) { + return; + } + data->promise->set_value(kExecutionFinding); + context->deferred.Reject(error.Value()); + context->is_resolved = true; + } catch (const std::exception &exception) { + data->promise->set_value(kExecutionFatal); + auto message = std::string("Internal fuzzer error - ").append( + exception.what()); + context->deferred.Reject(Napi::Error::New(env, message).Value()); + context->is_resolved = true; + } +} + +void AsyncSigintHandler(int signum) { + std::cerr << std::endl; + if (gActiveAsyncContext->sigints > 0) { + _Exit(libfuzzer::RETURN_CONTINUE); + } + gActiveAsyncContext->sigints = signum; +} + +void AsyncErrorSignalHandler(int signum) { + std::longjmp(gActiveAsyncContext->execution_context, signum); +} + +int ExecuteAsyncInput(void *user_data, const uint8_t *data, size_t size) { + auto *context = static_cast(user_data); + + ClearCoverageCounters(); + ClearCompareFeedbackMap(); + + std::promise promise; + auto input = AsyncDataType{data, size, &promise}; + + auto future = promise.get_future(); + auto status = gAsyncTsfn.BlockingCall(&input); + if (status != napi_ok) { + Napi::Error::Fatal("StartLibAflSpikeAsync", + "TypedThreadSafeFunction.BlockingCall() failed"); + } + + try { + return future.get(); + } catch (std::exception &exception) { + std::cerr << "==" << static_cast(GetPID()) + << "== Jazzer.js: Unexpected Error: " << exception.what() + << std::endl; + libfuzzer::PrintCrashingInput(); + _Exit(libfuzzer::EXIT_ERROR_CODE); + } +} +} // namespace + +Napi::Value StartLibAflSpike(const Napi::CallbackInfo &info) { + if (info.Length() != 3 || !info[0].IsFunction() || !info[1].IsObject() || + !info[2].IsFunction()) { + throw Napi::Error::New( + info.Env(), + "Need three arguments, which must be the fuzz target function, a " + "LibAFL spike options object, and a stop callback"); + } + + auto options = ParseSpikeOptions(info.Env(), info[1].As()); + auto maps = SharedMapsForSpike(info.Env()); + + SyncFuzzTargetContext context = {info.Env(), + info[0].As(), + false, + Napi::Promise::Deferred::New(info.Env()), + info[2].As()}; + gActiveSyncContext = &context; + + signal(SIGINT, SyncSigintHandler); + signal(SIGSEGV, SyncErrorSignalHandler); + + JazzerLibAflSpikeOptions spike_options{options.runs, options.seed, + options.max_len}; + auto status = + jazzer_libafl_spike_run(&spike_options, &maps, ExecuteSyncInput, &context); + + signal(SIGINT, SIG_DFL); + signal(SIGSEGV, SIG_DFL); + gActiveSyncContext = nullptr; + + if (status == kSpikeFatal && !context.is_resolved) { + context.is_resolved = true; + context.deferred.Reject( + Napi::Error::New(info.Env(), "The LibAFL spike failed internally") + .Value()); + } + + if (!context.is_resolved) { + context.deferred.Resolve(context.env.Undefined()); + } + + return context.deferred.Promise(); +} + +Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info) { + if (info.Length() != 2 || !info[0].IsFunction() || !info[1].IsObject()) { + throw Napi::Error::New(info.Env(), + "Need two arguments, which must be the fuzz target " + "function and a LibAFL spike options object"); + } + + auto options = ParseSpikeOptions(info.Env(), info[1].As()); + auto maps = SharedMapsForSpike(info.Env()); + auto *context = new AsyncFuzzTargetContext(info.Env()); + + gAsyncTsfn = AsyncTsfn::New( + info.Env(), info[0].As(), "LibAflSpikeAsyncAddon", 0, 1, + context, + [](Napi::Env env, AsyncFinalizerDataType *, AsyncFuzzTargetContext *ctx) { + ctx->native_thread.join(); + if (ctx->run_status == kSpikeFatal && !ctx->is_resolved) { + ctx->deferred.Reject( + Napi::Error::New(env, "The LibAFL spike failed internally") + .Value()); + } else if (!ctx->is_resolved) { + ctx->deferred.Resolve(env.Undefined()); + } + delete ctx; + }); + + context->native_thread = std::thread( + [options, maps](AsyncFuzzTargetContext *ctx) { + gActiveAsyncContext = ctx; + signal(SIGSEGV, AsyncErrorSignalHandler); + signal(SIGINT, AsyncSigintHandler); + JazzerLibAflSpikeOptions spike_options{options.runs, options.seed, + options.max_len}; + ctx->run_status = jazzer_libafl_spike_run(&spike_options, &maps, + ExecuteAsyncInput, ctx); + signal(SIGINT, SIG_DFL); + signal(SIGSEGV, SIG_DFL); + gActiveAsyncContext = nullptr; + gAsyncTsfn.Release(); + }, + context); + + return context->deferred.Promise(); +} diff --git a/packages/fuzzer/libafl_spike.h b/packages/fuzzer/libafl_spike.h new file mode 100644 index 000000000..d25f16932 --- /dev/null +++ b/packages/fuzzer/libafl_spike.h @@ -0,0 +1,45 @@ +// Copyright 2026 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. + +#pragma once + +#include +#include +#include + +extern "C" { +struct JazzerLibAflSpikeOptions { + uint64_t runs; + uint64_t seed; + size_t max_len; +}; + +struct JazzerLibAflSharedMaps { + uint8_t *edges; + size_t edges_len; + uint8_t *cmp; + size_t cmp_len; +}; + +typedef int (*JazzerLibAflExecuteCallback)(void *user_data, const uint8_t *data, + size_t size); + +int jazzer_libafl_spike_run(const JazzerLibAflSpikeOptions *options, + const JazzerLibAflSharedMaps *maps, + JazzerLibAflExecuteCallback execute_one, + void *user_data); +} + +Napi::Value StartLibAflSpike(const Napi::CallbackInfo &info); +Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info); diff --git a/packages/fuzzer/rust/Cargo.lock b/packages/fuzzer/rust/Cargo.lock new file mode 100644 index 000000000..670a217fe --- /dev/null +++ b/packages/fuzzer/rust/Cargo.lock @@ -0,0 +1,1370 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary-int" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825297538d77367557b912770ca3083f778a196054b3ee63b22673c4a3cae0a5" + +[[package]] +name = "arbitrary-int" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993a810118f8f37e9c4411c86f1c4c940a09a7ab34b7bf2d88d06f50c553fab7" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitbybit" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec187a89ab07e209270175faf9e07ceb2755d984954e58a2296e325ddece2762" +dependencies = [ + "arbitrary-int 1.3.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "ctor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "dtor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "siphasher", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jazzerjs-libafl-spike" +version = "0.1.0" +dependencies = [ + "libafl", + "libafl_bolts", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libafl" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e13655171e69ad9094dd1be1948950a36d228f01a7cb9f6d8477090d98c6e4" +dependencies = [ + "ahash", + "arbitrary-int 2.1.1", + "backtrace", + "bincode", + "bitbybit", + "const_format", + "const_panic", + "fastbloom", + "fs2", + "hashbrown 0.16.1", + "libafl_bolts", + "libafl_derive", + "libc", + "libm", + "log", + "meminterval", + "nix", + "num-traits", + "postcard", + "regex", + "rustversion", + "serde", + "serde_json", + "serial_test", + "tuple_list", + "typed-builder", + "uuid", + "wait-timeout", + "winapi", + "windows", +] + +[[package]] +name = "libafl_bolts" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cbae44f69156f035ae2196b135ad27ea95020767e6787bfe45e8c2438c67b9" +dependencies = [ + "ahash", + "backtrace", + "ctor", + "erased-serde", + "hashbrown 0.16.1", + "hostname", + "libafl_derive", + "libc", + "log", + "mach2", + "miniz_oxide", + "nix", + "num_enum", + "once_cell", + "postcard", + "rand_core", + "rustversion", + "serde", + "serial_test", + "static_assertions", + "tuple_list", + "typeid", + "uds", + "uuid", + "wide", + "winapi", + "windows", + "windows-core", + "windows-result", + "xxhash-rust", +] + +[[package]] +name = "libafl_derive" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61adf76899bffdcd15ae7fea42b978e7df7cf9213aacdd8cdcda89e4bb3bc32d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "meminterval" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0f9a537564310a87dc77d5c88a407e27dd0aa740e070f0549439cfcc68fcfd" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serial_test" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +dependencies = [ + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tuple_list" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141fb9f71ee586d956d7d6e4d5a9ef8e946061188520140f7591b668841d502e" + +[[package]] +name = "typed-builder" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" + +[[package]] +name = "uds" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661" +dependencies = [ + "libc", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/packages/fuzzer/rust/Cargo.toml b/packages/fuzzer/rust/Cargo.toml new file mode 100644 index 000000000..1d25348da --- /dev/null +++ b/packages/fuzzer/rust/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "jazzerjs-libafl-spike" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +publish = false + +[lib] +name = "jazzerjs_libafl_spike" +crate-type = ["staticlib"] + +[profile.release] +panic = "abort" + +[profile.dev] +panic = "abort" + +[dependencies] +libafl = "0.15.4" +libafl_bolts = "0.15.4" diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs new file mode 100644 index 000000000..0648dc6ac --- /dev/null +++ b/packages/fuzzer/rust/src/lib.rs @@ -0,0 +1,181 @@ +use core::ffi::c_void; +use core::ptr; +use std::cell::Cell; + +use libafl::{ + corpus::{Corpus, InMemoryCorpus, Testcase}, + events::SimpleEventManager, + executors::{inprocess::InProcessExecutor, ExitKind}, + feedbacks::{CrashFeedback, MaxMapFeedback}, + fuzzer::{Fuzzer, StdFuzzer}, + inputs::{BytesInput, HasTargetBytes}, + monitors::SimpleMonitor, + mutators::{havoc_mutations::havoc_mutations, scheduled::HavocScheduledMutator}, + observers::{CanTrack, HitcountsMapObserver, StdMapObserver}, + schedulers::QueueScheduler, + stages::mutational::StdMutationalStage, + state::{HasCorpus, HasSolutions, StdState}, +}; +use libafl_bolts::{rands::StdRand, tuples::tuple_list, AsSlice}; + +const EXECUTION_CONTINUE: i32 = 0; +const EXECUTION_FINDING: i32 = 1; +const EXECUTION_STOP: i32 = 2; +const EXECUTION_FATAL: i32 = 3; + +const SPIKE_OK: i32 = 0; +const SPIKE_FOUND_FINDING: i32 = 1; +const SPIKE_STOPPED: i32 = 2; +const SPIKE_FATAL: i32 = 3; + +#[repr(C)] +pub struct JazzerLibAflSpikeOptions { + pub runs: u64, + pub seed: u64, + pub max_len: usize, +} + +#[repr(C)] +pub struct JazzerLibAflSharedMaps { + pub edges: *mut u8, + pub edges_len: usize, + pub cmp: *mut u8, + pub cmp_len: usize, +} + +pub type JazzerLibAflExecuteCallback = unsafe extern "C" fn( + user_data: *mut c_void, + data: *const u8, + size: usize, +) -> i32; + +fn clear_shared_map(ptr: *mut u8, len: usize) { + if ptr.is_null() || len == 0 { + return; + } + + unsafe { + ptr::write_bytes(ptr, 0, len); + } +} + +fn clamp_input_length(input: Vec, max_len: usize) -> Vec { + if input.len() <= max_len { + input + } else { + input[..max_len].to_vec() + } +} + +#[no_mangle] +pub unsafe extern "C" fn jazzer_libafl_spike_run( + options: *const JazzerLibAflSpikeOptions, + maps: *const JazzerLibAflSharedMaps, + execute_one: JazzerLibAflExecuteCallback, + user_data: *mut c_void, +) -> i32 { + if options.is_null() || maps.is_null() { + return SPIKE_FATAL; + } + + let options = &*options; + let maps = &*maps; + if maps.edges.is_null() || maps.edges_len == 0 { + return SPIKE_FATAL; + } + + let monitor = SimpleMonitor::new(|_| {}); + let mut mgr = SimpleEventManager::new(monitor); + + let edges_observer = HitcountsMapObserver::new(StdMapObserver::from_mut_ptr( + "edges", + maps.edges, + maps.edges_len, + )) + .track_indices(); + + let mut feedback = MaxMapFeedback::new(&edges_observer); + let mut objective = CrashFeedback::new(); + let mut state = match StdState::new( + StdRand::with_seed(options.seed), + InMemoryCorpus::new(), + InMemoryCorpus::new(), + &mut feedback, + &mut objective, + ) { + Ok(state) => state, + Err(_) => return SPIKE_FATAL, + }; + + if state + .corpus_mut() + .add(Testcase::new(BytesInput::new(vec![]))) + .is_err() + { + return SPIKE_FATAL; + } + + let mut fuzzer = StdFuzzer::new(QueueScheduler::new(), feedback, objective); + let mutator = HavocScheduledMutator::new(havoc_mutations()); + let mut stages = tuple_list!(StdMutationalStage::new(mutator)); + let stop_requested = Cell::new(false); + let fatal_error = Cell::new(false); + + let mut harness = |input: &BytesInput| { + clear_shared_map(maps.edges, maps.edges_len); + clear_shared_map(maps.cmp, maps.cmp_len); + + let bytes = clamp_input_length(input.target_bytes().as_slice().to_vec(), options.max_len); + let status = unsafe { execute_one(user_data, bytes.as_ptr(), bytes.len()) }; + match status { + EXECUTION_CONTINUE => ExitKind::Ok, + EXECUTION_FINDING => ExitKind::Crash, + EXECUTION_STOP => { + stop_requested.set(true); + ExitKind::Ok + } + EXECUTION_FATAL => { + fatal_error.set(true); + ExitKind::Ok + } + _ => { + fatal_error.set(true); + ExitKind::Ok + } + } + }; + + let mut executor = match InProcessExecutor::new( + &mut harness, + tuple_list!(edges_observer), + &mut fuzzer, + &mut state, + &mut mgr, + ) { + Ok(executor) => executor, + Err(_) => return SPIKE_FATAL, + }; + + for _ in 0..options.runs { + if fuzzer + .fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr) + .is_err() + { + return SPIKE_FATAL; + } + + if fatal_error.get() { + return SPIKE_FATAL; + } + + if state.solutions().count() > 0 { + return SPIKE_FOUND_FINDING; + } + + if stop_requested.get() { + return SPIKE_STOPPED; + } + } + + SPIKE_OK +} diff --git a/packages/fuzzer/shared/callbacks.cpp b/packages/fuzzer/shared/callbacks.cpp index 30365436e..ddbabab6b 100644 --- a/packages/fuzzer/shared/callbacks.cpp +++ b/packages/fuzzer/shared/callbacks.cpp @@ -30,4 +30,8 @@ void RegisterCallbackExports(Napi::Env env, Napi::Object exports) { exports["traceIntegerCompare"] = Napi::Function::New(env); exports["tracePcIndir"] = Napi::Function::New(env); + exports["clearCompareFeedbackMap"] = + Napi::Function::New(env); + exports["countNonZeroCompareFeedbackSlots"] = + Napi::Function::New(env); } diff --git a/packages/fuzzer/shared/coverage.cpp b/packages/fuzzer/shared/coverage.cpp index d1cb3a682..ad66dfeb2 100644 --- a/packages/fuzzer/shared/coverage.cpp +++ b/packages/fuzzer/shared/coverage.cpp @@ -15,6 +15,7 @@ #include #include +#include extern "C" { void __sanitizer_cov_8bit_counters_init(uint8_t *start, uint8_t *end); @@ -26,6 +27,7 @@ namespace { // Shared coverage counter buffer populated from JavaScript using Buffer. // Individual slices are registered with libFuzzer by RegisterNewCounters. uint8_t *gCoverageCounters = nullptr; +std::size_t gCoverageCountersSize = 0; // PC-Table is used by libFuzzer to keep track of program addresses // corresponding to coverage counters. The flags determine whether the @@ -102,6 +104,7 @@ void RegisterNewCounters(const Napi::CallbackInfo &info) { RegisterCounterRange(gCoverageCounters + old_num_counters, gCoverageCounters + new_num_counters); + gCoverageCountersSize = static_cast(new_num_counters); } // Register an independent coverage counter region for a single ES module. @@ -121,3 +124,15 @@ void RegisterModuleCounters(const Napi::CallbackInfo &info) { RegisterCounterRange(buf.Data(), buf.Data() + size); } + +uint8_t *CoverageCounters() { return gCoverageCounters; } + +std::size_t CoverageCountersSize() { return gCoverageCountersSize; } + +void ClearCoverageCounters() { + if (gCoverageCounters == nullptr || gCoverageCountersSize == 0) { + return; + } + + std::memset(gCoverageCounters, 0, gCoverageCountersSize); +} diff --git a/packages/fuzzer/shared/coverage.h b/packages/fuzzer/shared/coverage.h index ffbd7333a..42c126b09 100644 --- a/packages/fuzzer/shared/coverage.h +++ b/packages/fuzzer/shared/coverage.h @@ -13,8 +13,14 @@ // limitations under the License. #pragma once +#include +#include #include void RegisterCoverageMap(const Napi::CallbackInfo &info); void RegisterNewCounters(const Napi::CallbackInfo &info); void RegisterModuleCounters(const Napi::CallbackInfo &info); + +uint8_t *CoverageCounters(); +std::size_t CoverageCountersSize(); +void ClearCoverageCounters(); diff --git a/packages/fuzzer/shared/tracing.cpp b/packages/fuzzer/shared/tracing.cpp index ee68e55d0..ce98f6960 100644 --- a/packages/fuzzer/shared/tracing.cpp +++ b/packages/fuzzer/shared/tracing.cpp @@ -14,6 +14,10 @@ #include "tracing.h" +#include +#include +#include + // We expect these symbols to exist in the current plugin, provided either by // libfuzzer or by the native agent. extern "C" { @@ -26,6 +30,33 @@ void __sanitizer_cov_trace_const_cmp8_with_pc(uintptr_t called_pc, void __sanitizer_cov_trace_pc_indir_with_pc(void *caller_pc, uintptr_t callee); } +namespace { +constexpr std::size_t kCompareFeedbackMapSize = 1 << 16; +std::array gCompareFeedbackMap{}; + +void RecordCompareFeedback(uint64_t value) { + auto index = static_cast(value % kCompareFeedbackMapSize); + auto &slot = gCompareFeedbackMap[index]; + slot = slot == 255 ? 1 : static_cast(slot + 1); +} + +void RecordStringFeedback(uint64_t id, const std::string &first, + const std::string &second) { + uint64_t hash = id * 0x9e3779b185ebca87ULL; + const auto limit = std::min({first.size(), second.size(), 32}); + hash ^= static_cast(first.size()) << 32; + hash ^= static_cast(second.size()) << 1; + for (std::size_t i = 0; i < limit; ++i) { + hash ^= static_cast(static_cast(first[i])) + << ((i % 8) * 8); + hash ^= static_cast(static_cast(second[i])) + << (((i + 3) % 8) * 8); + RecordCompareFeedback(hash + i); + } + RecordCompareFeedback(hash); +} +} // namespace + // Record a comparison between two strings in the target that returned unequal. void TraceUnequalStrings(const Napi::CallbackInfo &info) { if (info.Length() != 3) { @@ -38,6 +69,8 @@ void TraceUnequalStrings(const Napi::CallbackInfo &info) { auto s1 = info[1].As().Utf8Value(); auto s2 = info[2].As().Utf8Value(); + RecordStringFeedback(id, s1, s2); + // strcmp returns zero on equality, and libfuzzer doesn't care about the // result beyond whether it's zero or not. __sanitizer_weak_hook_strcmp((void *)id, s1.c_str(), s2.c_str(), 1); @@ -55,6 +88,8 @@ void TraceStringContainment(const Napi::CallbackInfo &info) { auto needle = info[1].As().Utf8Value(); auto haystack = info[2].As().Utf8Value(); + RecordStringFeedback(id, needle, haystack); + // libFuzzer currently ignores the result, which allows us to simply pass a // valid but arbitrary pointer here instead of performing an actual strstr // operation. @@ -72,6 +107,8 @@ void TraceIntegerCompare(const Napi::CallbackInfo &info) { auto id = info[0].As().Int64Value(); auto arg1 = info[1].As().Int64Value(); auto arg2 = info[2].As().Int64Value(); + RecordCompareFeedback(static_cast(id) ^ static_cast(arg1) ^ + (static_cast(arg2) << 1)); __sanitizer_cov_trace_const_cmp8_with_pc(id, arg1, arg2); } @@ -83,5 +120,34 @@ void TracePcIndir(const Napi::CallbackInfo &info) { auto id = info[0].As().Int64Value(); auto state = info[1].As().Int64Value(); + RecordCompareFeedback(static_cast(id) ^ + (static_cast(state) << 1)); __sanitizer_cov_trace_pc_indir_with_pc((void *)id, state); } + +void ClearCompareFeedbackMap(const Napi::CallbackInfo &info) { + if (info.Length() != 0) { + throw Napi::Error::New(info.Env(), "This function does not accept arguments"); + } + + ClearCompareFeedbackMap(); +} + +Napi::Value CountNonZeroCompareFeedbackSlots(const Napi::CallbackInfo &info) { + if (info.Length() != 0) { + throw Napi::Error::New(info.Env(), "This function does not accept arguments"); + } + + const auto count = static_cast(std::count_if( + gCompareFeedbackMap.begin(), gCompareFeedbackMap.end(), + [](uint8_t value) { return value != 0; })); + return Napi::Number::New(info.Env(), count); +} + +uint8_t *CompareFeedbackMap() { return gCompareFeedbackMap.data(); } + +std::size_t CompareFeedbackMapSize() { return gCompareFeedbackMap.size(); } + +void ClearCompareFeedbackMap() { + std::memset(gCompareFeedbackMap.data(), 0, gCompareFeedbackMap.size()); +} diff --git a/packages/fuzzer/shared/tracing.h b/packages/fuzzer/shared/tracing.h index d85c8e854..744c603c4 100644 --- a/packages/fuzzer/shared/tracing.h +++ b/packages/fuzzer/shared/tracing.h @@ -13,9 +13,18 @@ // limitations under the License. #pragma once +#include +#include #include void TraceUnequalStrings(const Napi::CallbackInfo &info); void TraceStringContainment(const Napi::CallbackInfo &info); void TraceIntegerCompare(const Napi::CallbackInfo &info); void TracePcIndir(const Napi::CallbackInfo &info); + +void ClearCompareFeedbackMap(const Napi::CallbackInfo &info); +Napi::Value CountNonZeroCompareFeedbackSlots(const Napi::CallbackInfo &info); + +uint8_t *CompareFeedbackMap(); +std::size_t CompareFeedbackMapSize(); +void ClearCompareFeedbackMap(); From 367794a29118957c479905227b60fdc677dcca57 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sat, 18 Apr 2026 23:43:05 +0200 Subject: [PATCH 02/30] test(fuzzer): add Phase 0 LibAFL spike checks Add smoke tests for sync execution, async event-loop ordering, and shared compare feedback, plus a small benchmark harness that compares the raw libFuzzer entrypoint with the internal LibAFL spike. This gives us a repeatable way to validate the event-loop assumptions and measure the native boundary before adding a public engine switch. --- packages/fuzzer/libafl_spike.test.ts | 76 ++++++++++++++ packages/fuzzer/package.json | 1 + packages/fuzzer/spike/benchmark.js | 142 +++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 packages/fuzzer/libafl_spike.test.ts create mode 100644 packages/fuzzer/spike/benchmark.js diff --git a/packages/fuzzer/libafl_spike.test.ts b/packages/fuzzer/libafl_spike.test.ts new file mode 100644 index 000000000..95513a74c --- /dev/null +++ b/packages/fuzzer/libafl_spike.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2026 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. + */ + +import { addon } from "./addon"; +import { fuzzer } from "./fuzzer"; + +const spikeOptions = { runs: 32, seed: 1234, maxLen: 64 }; + +describe("LibAFL spike", () => { + it("runs synchronous fuzz targets through the native spike", async () => { + let invocations = 0; + + await addon.startLibAflSpike( + () => { + invocations++; + }, + spikeOptions, + () => undefined, + ); + + expect(invocations).toBeGreaterThan(0); + }); + + it("preserves async invocation ordering through the event loop", async () => { + let lastInvocationCount = 0; + let invocationCount = 1; + + await addon.startLibAflSpikeAsync(async () => { + const value = await new Promise((resolve) => { + queueMicrotask(() => { + setImmediate(() => resolve(invocationCount++)); + }); + }); + + if (value !== lastInvocationCount + 1) { + throw new Error( + `Invalid invocation order: received ${value}, last ${lastInvocationCount}`, + ); + } + + lastInvocationCount = value; + }, spikeOptions); + + expect(lastInvocationCount).toBeGreaterThan(0); + }); + + it("records compare feedback in the shared native map", async () => { + addon.clearCompareFeedbackMap(); + + await addon.startLibAflSpike( + (data: Buffer) => { + const text = data.toString("utf8"); + fuzzer.tracer.traceStrCmp(text, "jazzer", "===", 11); + fuzzer.tracer.traceNumberCmp(data.length, 7, "===", 12); + fuzzer.tracer.tracePcIndir(13, data.length); + }, + { runs: 1, seed: 9, maxLen: 16 }, + () => undefined, + ); + + expect(addon.countNonZeroCompareFeedbackSlots()).toBeGreaterThan(0); + }); +}); diff --git a/packages/fuzzer/package.json b/packages/fuzzer/package.json index dd895e26c..3ab0f7ee6 100644 --- a/packages/fuzzer/package.json +++ b/packages/fuzzer/package.json @@ -18,6 +18,7 @@ "scripts": { "prebuild": "cmake-js build --out build", "build": "node ../../scripts/build-fuzzer.js", + "benchmark:libafl-spike": "node spike/benchmark.js", "format:fix": "clang-format -i *.cpp shared/*.cpp shared/*.h", "lint": "find . -path ./build -prune -type f -o -iname '*.h' -o -iname '*.cpp' | xargs clang-tidy" }, diff --git a/packages/fuzzer/spike/benchmark.js b/packages/fuzzer/spike/benchmark.js new file mode 100644 index 000000000..e28416250 --- /dev/null +++ b/packages/fuzzer/spike/benchmark.js @@ -0,0 +1,142 @@ +/* + * Copyright 2026 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. + */ + +const { addon } = require("../dist/addon.js"); +const { fuzzer } = require("../dist/fuzzer.js"); + +const runs = Number(process.env.JAZZER_SPIKE_RUNS ?? "20000"); +const seed = Number(process.env.JAZZER_SPIKE_SEED ?? "1337"); +const maxLen = Number(process.env.JAZZER_SPIKE_MAX_LEN ?? "64"); + +const libFuzzerArgs = [ + "jazzer-libfuzzer-benchmark", + `-runs=${runs}`, + `-seed=${seed}`, + `-max_len=${maxLen}`, +]; +const spikeOptions = { runs, seed, maxLen }; + +async function measure(name, start) { + console.log(`\nRunning ${name}...`); + let invocations = 0; + const startedAt = process.hrtime.bigint(); + await start(() => { + invocations++; + }); + const elapsedSeconds = Number(process.hrtime.bigint() - startedAt) / 1e9; + return { + name, + invocations, + elapsedSeconds, + execsPerSecond: invocations / elapsedSeconds, + }; +} + +async function measureCompareHeavy(name, start) { + console.log(`\nRunning ${name}...`); + let invocations = 0; + const startedAt = process.hrtime.bigint(); + await start((data) => { + invocations++; + const text = data.toString("utf8"); + for (let i = 0; i < 32; i++) { + fuzzer.tracer.traceStrCmp(text, `cmp-${i}`, "===", i + 1); + fuzzer.tracer.traceNumberCmp(data.length, i, "===", i + 128); + fuzzer.tracer.tracePcIndir(i + 512, data.length ^ i); + } + }); + const elapsedSeconds = Number(process.hrtime.bigint() - startedAt) / 1e9; + return { + name, + invocations, + elapsedSeconds, + execsPerSecond: invocations / elapsedSeconds, + }; +} + +function printResult(result) { + console.log( + `${result.name.padEnd(28)} ${result.invocations + .toString() + .padStart( + 8, + )} execs ${result.elapsedSeconds.toFixed(3).padStart(8)} s ${result.execsPerSecond + .toFixed(0) + .padStart(10)} exec/s`, + ); +} + +async function main() { + console.log( + `Benchmarking with runs=${runs}, seed=${seed}, max_len=${maxLen}`, + ); + + const results = []; + results.push( + await measure("libFuzzer sync trivial", (target) => + addon.startFuzzing(target, libFuzzerArgs, () => undefined), + ), + ); + results.push( + await measure("LibAFL sync trivial", (target) => + addon.startLibAflSpike(target, spikeOptions, () => undefined), + ), + ); + results.push( + await measure("libFuzzer async trivial", (target) => + addon.startFuzzingAsync( + () => + new Promise((resolve) => { + target(); + setImmediate(resolve); + }), + libFuzzerArgs, + ), + ), + ); + results.push( + await measure("LibAFL async trivial", (target) => + addon.startLibAflSpikeAsync( + () => + new Promise((resolve) => { + target(); + setImmediate(resolve); + }), + spikeOptions, + ), + ), + ); + results.push( + await measureCompareHeavy("libFuzzer compare-heavy", (target) => + addon.startFuzzing(target, libFuzzerArgs, () => undefined), + ), + ); + results.push( + await measureCompareHeavy("LibAFL compare-heavy", (target) => + addon.startLibAflSpike(target, spikeOptions, () => undefined), + ), + ); + + console.log(""); + for (const result of results) { + printResult(result); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); From 0a83999c1af0da103197c173d8ec4d2a951cb481 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 00:26:45 +0200 Subject: [PATCH 03/30] feat(core): wire engine selection through core and addon APIs Add a first-class engine option that lets core dispatch between the existing libFuzzer backend and the internal LibAFL backend while keeping CLI and Jest flows compatible. This also adds strict LibAFL option parsing, engine alias normalization, and a stable startLibAfl/startLibAflAsync addon surface for follow-up backend work. --- packages/core/cli.ts | 12 ++- packages/core/core.ts | 59 +++++++++--- packages/core/options.test.ts | 52 ++++++++++ packages/core/options.ts | 138 ++++++++++++++++++++++++++- packages/core/utils.test.ts | 11 +++ packages/core/utils.ts | 2 + packages/fuzzer/addon.cpp | 2 + packages/fuzzer/addon.ts | 43 +++++++-- packages/fuzzer/fuzzer.ts | 5 + packages/fuzzer/libafl_spike.cpp | 8 ++ packages/fuzzer/libafl_spike.h | 2 + packages/fuzzer/libafl_spike.test.ts | 30 ++++-- packages/fuzzer/spike/benchmark.js | 18 +++- packages/fuzzer/tsconfig.json | 2 +- 14 files changed, 345 insertions(+), 39 deletions(-) diff --git a/packages/core/cli.ts b/packages/core/cli.ts index 13ccdf5bb..b1fff2a86 100644 --- a/packages/core/cli.ts +++ b/packages/core/cli.ts @@ -44,7 +44,7 @@ yargs(process.argv.slice(2)) 'Start a fuzzing run using the "fuzz" function exported by "target" ' + 'and use the directory "corpus" to store newly generated inputs. ' + 'Also pass the "-max_total_time" flag to the internal fuzzing engine ' + - "(libFuzzer) to stop the fuzzing run after 60 seconds.", + "to stop the fuzzing run after 60 seconds.", ) .epilogue("Happy fuzzing!") .command( @@ -56,7 +56,7 @@ yargs(process.argv.slice(2)) 'The "corpus" directory is optional and can be used to provide initial ' + "seed input. It is also used to store interesting inputs between fuzzing " + "runs.\n\n" + - "To pass options to the internal fuzzing engine (libFuzzer) use a " + + "To pass options to the internal fuzzing engine use a " + 'double-dash, "--", to mark the end of the normal fuzzer arguments. ' + "An example is shown in the examples section of this help message.", (yargs: Argv) => { @@ -177,6 +177,14 @@ yargs(process.argv.slice(2)) group: "Fuzzer:", type: "string", }) + .option("engine", { + alias: ["backend"], + defaultDescription: `${JSON.stringify(defaultCLIOptions.engine)}`, + describe: + "Fuzzing engine backend. Use 'libfuzzer' for the current default backend or 'afl' to run the LibAFL engine.", + group: "Fuzzer:", + type: "string", + }) .option("dryRun", { alias: ["dry_run", "d"], defaultDescription: `${JSON.stringify(defaultCLIOptions.dryRun)}`, diff --git a/packages/core/core.ts b/packages/core/core.ts index 4e7c0e8f3..87e8886b7 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -43,7 +43,12 @@ import { reportFinding, } from "./finding"; import { getJazzerJsGlobal, jazzerJs } from "./globals"; -import { buildFuzzerOption, OptionsManager } from "./options"; +import { + buildLibAflOptions, + buildLibFuzzerOptions, + OptionsManager, + resolveEngine, +} from "./options"; import { ensureFilepath, importModule } from "./utils"; // Remove temporary files on exit @@ -249,20 +254,40 @@ export async function startFuzzingNoInit( }; try { - const fuzzerOptions = buildFuzzerOption(options); - if (options.get("sync")) { - await fuzzer.fuzzer.startFuzzing( - fuzzFn, - fuzzerOptions, - // In synchronous mode, we cannot use the SIGINT handler in Node, - // because the event loop is blocked by the fuzzer, and the handler - // won't be called until the fuzzing process is finished. - // Hence, we pass a callback function to the native fuzzer and - // register a SIGINT handler there. - signalHandler, - ); + if (resolveEngine(options.get("engine")) === "libfuzzer") { + const fuzzerOptions = buildLibFuzzerOptions(options); + if (options.get("sync")) { + await fuzzer.fuzzer.startFuzzing( + fuzzFn, + fuzzerOptions, + // In synchronous mode, we cannot use the SIGINT handler in Node, + // because the event loop is blocked by the fuzzer, and the handler + // won't be called until the fuzzing process is finished. + // Hence, we pass a callback function to the native fuzzer and + // register a SIGINT handler there. + signalHandler, + ); + } else { + await fuzzer.fuzzer.startFuzzingAsync(fuzzFn, fuzzerOptions); + } } else { - await fuzzer.fuzzer.startFuzzingAsync(fuzzFn, fuzzerOptions); + const libAflOptions = buildLibAflOptions(options); + const libAflFuzzer = fuzzer.fuzzer as unknown as { + startLibAfl: ( + fuzzFn: FindingAwareFuzzTarget, + options: typeof libAflOptions, + jsStopCallback: (signal: number) => void, + ) => Promise; + startLibAflAsync: ( + fuzzFn: FindingAwareFuzzTarget, + options: typeof libAflOptions, + ) => Promise; + }; + if (options.get("sync")) { + await libAflFuzzer.startLibAfl(fuzzFn, libAflOptions, signalHandler); + } else { + await libAflFuzzer.startLibAflAsync(fuzzFn, libAflOptions); + } } // Fuzzing ended without a finding, due to -max_total_time or -runs. return reportFuzzingResult(undefined, options.get("expectedErrors")); @@ -369,6 +394,10 @@ export function asFindingAwareFuzzFn( ): FindingAwareFuzzTarget { function printAndDump(error: unknown): void { cleanErrorStack(error); + const shouldDumpWithLibFuzzer = + ( + globalThis as typeof globalThis & { options?: OptionsManager } + ).options?.get("engine") !== "libafl"; if ( !( error instanceof FuzzerSignalFinding && @@ -376,7 +405,7 @@ export function asFindingAwareFuzzFn( ) ) { printFinding(error); - if (dumpCrashingInput) { + if (dumpCrashingInput && shouldDumpWithLibFuzzer) { fuzzer.fuzzer.printAndDumpCrashingInput(); } } diff --git a/packages/core/options.test.ts b/packages/core/options.test.ts index d91a58a78..e8b38064c 100644 --- a/packages/core/options.test.ts +++ b/packages/core/options.test.ts @@ -15,12 +15,14 @@ */ import { + buildLibAflOptions, defaultCLIOptions, fromSnakeCase, fromSnakeCaseWithPrefix, Options, OptionsManager, OptionSource, + resolveEngine, spawnsSubprocess, validateKeySource, } from "./options"; @@ -314,6 +316,56 @@ describe("buildLibFuzzerOptions", () => { }); }); +describe("libafl options", () => { + it("normalizes engine aliases", () => { + expect(resolveEngine("libfuzzer")).toBe("libfuzzer"); + expect(resolveEngine("afl")).toBe("libafl"); + expect(resolveEngine("libafl")).toBe("libafl"); + expect(() => resolveEngine("unknown")).toThrow("Unknown fuzzing engine"); + }); + + it("builds structured LibAFL options from fuzzer options", () => { + const manager = new OptionsManager(OptionSource.DefaultCLIOptions).merge( + { + engine: "libafl", + timeout: 1234, + fuzzerOptions: [ + "corpus-main", + "corpus-seed", + "-runs=99", + "-seed=1337", + "-max_len=1024", + "-max_total_time=42", + "-artifact_prefix=/tmp/artifacts/", + ], + }, + OptionSource.CommandLineArguments, + ); + + expect(buildLibAflOptions(manager)).toEqual({ + runs: 99, + seed: 1337, + maxLen: 1024, + timeoutMillis: 1234, + maxTotalTimeSeconds: 42, + artifactPrefix: "/tmp/artifacts/", + corpusDirectories: ["corpus-main", "corpus-seed"], + }); + }); + + it("rejects unsupported options in LibAFL mode", () => { + const manager = new OptionsManager(OptionSource.DefaultCLIOptions).merge( + { + engine: "libafl", + fuzzerOptions: ["-fork=1"], + }, + OptionSource.CommandLineArguments, + ); + + expect(() => buildLibAflOptions(manager)).toThrow("not supported"); + }); +}); + function expectDefaultsExceptKeys( options: Options, source: OptionSource, diff --git a/packages/core/options.ts b/packages/core/options.ts index 66dfae2d7..bf80aedce 100644 --- a/packages/core/options.ts +++ b/packages/core/options.ts @@ -22,6 +22,16 @@ import * as tmp from "tmp"; import { useDictionaryByParams } from "./dictionary"; import { replaceAll } from "./utils"; +export type LibAflOptions = { + runs: number; + seed: number; + maxLen: number; + timeoutMillis: number; + maxTotalTimeSeconds: number; + artifactPrefix: string; + corpusDirectories: string[]; +}; + /** * Jazzer.js options structure expected by the fuzzer. * @@ -30,6 +40,8 @@ import { replaceAll } from "./utils"; * options. */ export interface Options { + // Fuzzing backend engine. + engine: "libfuzzer" | "libafl"; // Enable source code coverage report generation. coverage: boolean; // Directory to write coverage reports to. @@ -85,6 +97,7 @@ export type OptionsWithPrintableSource = { // These options can be set from the Jest fuzz test. const allowedFuzzTestOptions = [ + "engine", "dictionaryEntries", "fuzzerOptions", "sync", @@ -93,6 +106,7 @@ const allowedFuzzTestOptions = [ export type AllowedFuzzTestOptions = (typeof allowedFuzzTestOptions)[number]; export const defaultCLIOptions: Options = Object.freeze({ + engine: "libfuzzer", coverage: false, coverageDirectory: "coverage", coverageReporters: ["json", "text", "lcov", "clover"], // default Jest reporters @@ -149,6 +163,22 @@ export enum OptionSource { JestFuzzTestOptions, } +export type FuzzingEngine = Options["engine"]; + +export function resolveEngine(engine: string): FuzzingEngine { + switch (engine) { + case "libfuzzer": + return "libfuzzer"; + case "libafl": + case "afl": + return "libafl"; + default: + throw new Error( + `Unknown fuzzing engine '${engine}'. Supported engines are 'libfuzzer' and 'afl'.`, + ); + } +} + type DefaultSourceInfo = { name: string; transformKey: KeyFormatSource; @@ -421,7 +451,7 @@ function setProperty(obj: T, key: K, value: T[K]) { obj[key] = value; } -export function buildFuzzerOption(options: OptionsManager) { +export function buildLibFuzzerOptions(options: OptionsManager) { let params: string[] = []; params = optionDependentParams(options, params); params = forkedExecutionParams(params); @@ -436,6 +466,112 @@ export function buildFuzzerOption(options: OptionsManager) { return params; } +// Backwards-compatible alias for existing call sites. +export const buildFuzzerOption = buildLibFuzzerOptions; + +export function buildLibAflOptions(options: OptionsManager): LibAflOptions { + if (options.get("timeout") <= 0) { + throw new Error("timeout must be > 0"); + } + + let runs = options.get("mode") === "regression" ? 0 : 0; + let seed = 0; + let maxLen = 4096; + let maxTotalTimeSeconds = 0; + let artifactPrefix = ""; + const corpusDirectories: string[] = []; + + for (const option of options.get("fuzzerOptions")) { + if (!option.startsWith("-")) { + corpusDirectories.push(option); + continue; + } + + if (option.startsWith("-runs=")) { + runs = parsePositiveOrZeroInteger("runs", option.substring(6)); + continue; + } + if (option.startsWith("-seed=")) { + seed = parsePositiveOrZeroInteger("seed", option.substring(6)); + continue; + } + if (option.startsWith("-max_len=")) { + maxLen = parsePositiveInteger("max_len", option.substring(9)); + continue; + } + if (option.startsWith("-max_total_time=")) { + maxTotalTimeSeconds = parsePositiveOrZeroInteger( + "max_total_time", + option.substring(16), + ); + continue; + } + if (option.startsWith("-artifact_prefix=")) { + artifactPrefix = option.substring(17); + continue; + } + + throw new Error( + `Option '${option}' is not supported by the '${resolveEngine(options.get("engine"))}' engine`, + ); + } + + if (options.get("dictionaryEntries").length > 0) { + throw new Error( + "The 'libafl' engine currently does not support dictionaryEntries; use '-dict=...' in fuzzerOptions after support lands.", + ); + } + + printOptions(options); + if (process.env.JAZZER_DEBUG) { + console.error( + `DEBUG: [core] LibAFL options: ${JSON.stringify( + { + runs, + seed, + maxLen, + maxTotalTimeSeconds, + timeoutMillis: options.get("timeout"), + artifactPrefix, + corpusDirectories, + }, + null, + 2, + )}`, + ); + } + + return { + runs, + seed, + maxLen, + timeoutMillis: options.get("timeout"), + maxTotalTimeSeconds, + artifactPrefix, + corpusDirectories, + }; +} + +function parsePositiveInteger(name: string, value: string): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error( + `Option '${name}' must be a positive integer, got '${value}'`, + ); + } + return parsed; +} + +function parsePositiveOrZeroInteger(name: string, value: string): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed < 0) { + throw new Error( + `Option '${name}' must be a non-negative integer, got '${value}'`, + ); + } + return parsed; +} + export function printOptions(options: OptionsManager, infix = "") { if (process.env.JAZZER_DEBUG) { console.error( diff --git a/packages/core/utils.test.ts b/packages/core/utils.test.ts index 703364c79..de9fe920e 100644 --- a/packages/core/utils.test.ts +++ b/packages/core/utils.test.ts @@ -54,5 +54,16 @@ describe("core", () => { ], }); }); + + it("normalizes engine alias", () => { + const args = { + _: [], + corpus: [], + engine: "afl", + fuzzTarget: "filename.js", + }; + const options = prepareArgs(args); + expect(options.engine).toBe("libafl"); + }); }); }); diff --git a/packages/core/utils.ts b/packages/core/utils.ts index 5d439583e..20e5ed159 100644 --- a/packages/core/utils.ts +++ b/packages/core/utils.ts @@ -67,8 +67,10 @@ export function ensureFilepath(filePath: string): string { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function prepareArgs(args: any) { + const engine = args.engine === "afl" ? "libafl" : args.engine; const options = { ...args, + engine, fuzzTarget: ensureFilepath(args.fuzzTarget), fuzzerOptions: (args.corpus ?? []) .concat(args._) diff --git a/packages/fuzzer/addon.cpp b/packages/fuzzer/addon.cpp index e840bd3b6..aee3f94de 100644 --- a/packages/fuzzer/addon.cpp +++ b/packages/fuzzer/addon.cpp @@ -62,6 +62,8 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) { exports["startFuzzing"] = Napi::Function::New(env); exports["startFuzzingAsync"] = Napi::Function::New(env); + exports["startLibAfl"] = Napi::Function::New(env); + exports["startLibAflAsync"] = Napi::Function::New(env); exports["startLibAflSpike"] = Napi::Function::New(env); exports["startLibAflSpikeAsync"] = Napi::Function::New(env); diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index 77862bb99..ac500c373 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -26,10 +26,15 @@ export type FuzzTargetCallback = ( ) => unknown; export type FuzzTarget = FuzzTargetAsyncOrValue | FuzzTargetCallback; export type FuzzOpts = string[]; -export type LibAflSpikeOptions = { + +export type LibAflOptions = { runs: number; seed: number; maxLen: number; + timeoutMillis: number; + maxTotalTimeSeconds: number; + artifactPrefix: string; + corpusDirectories: string[]; }; export type StartFuzzingSyncFn = ( @@ -41,14 +46,14 @@ export type StartFuzzingAsyncFn = ( fuzzFn: FuzzTarget, fuzzOpts: FuzzOpts, ) => Promise; -export type StartLibAflSpikeSyncFn = ( +export type StartLibAflSyncFn = ( fuzzFn: FuzzTarget, - options: LibAflSpikeOptions, + options: LibAflOptions, jsStopCallback: (signal: number) => void, ) => Promise; -export type StartLibAflSpikeAsyncFn = ( +export type StartLibAflAsyncFn = ( fuzzFn: FuzzTarget, - options: LibAflSpikeOptions, + options: LibAflOptions, ) => Promise; type NativeAddon = { @@ -81,12 +86,19 @@ type NativeAddon = { startFuzzing: StartFuzzingSyncFn; startFuzzingAsync: StartFuzzingAsyncFn; - startLibAflSpike: StartLibAflSpikeSyncFn; - startLibAflSpikeAsync: StartLibAflSpikeAsyncFn; + startLibAfl?: StartLibAflSyncFn; + startLibAflAsync?: StartLibAflAsyncFn; + startLibAflSpike?: StartLibAflSyncFn; + startLibAflSpikeAsync?: StartLibAflAsyncFn; clearCompareFeedbackMap: () => void; countNonZeroCompareFeedbackSlots: () => number; }; +type LoadedAddon = NativeAddon & { + startLibAfl: StartLibAflSyncFn; + startLibAflAsync: StartLibAflAsyncFn; +}; + function addonFilename(): string { let dirName: string; if (fs.existsSync(path.join(__dirname, "prebuilds"))) { @@ -99,4 +111,19 @@ function addonFilename(): string { return path.join(dirName, `fuzzer-${process.platform}-${process.arch}.node`); } -export const addon: NativeAddon = require(addonFilename()); +const loadedAddon = require(addonFilename()) as NativeAddon; + +if (!loadedAddon.startLibAfl && loadedAddon.startLibAflSpike) { + loadedAddon.startLibAfl = loadedAddon.startLibAflSpike; +} +if (!loadedAddon.startLibAflAsync && loadedAddon.startLibAflSpikeAsync) { + loadedAddon.startLibAflAsync = loadedAddon.startLibAflSpikeAsync; +} + +if (!loadedAddon.startLibAfl || !loadedAddon.startLibAflAsync) { + throw new Error( + "The native addon does not export startLibAfl/startLibAflAsync", + ); +} + +export const addon: LoadedAddon = loadedAddon as LoadedAddon; diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index 1330bb44a..a00da42d2 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -22,6 +22,7 @@ export type { FuzzTarget, FuzzTargetAsyncOrValue, FuzzTargetCallback, + LibAflOptions, } from "./addon"; export interface Fuzzer { @@ -29,6 +30,8 @@ export interface Fuzzer { tracer: Tracer; startFuzzing: typeof addon.startFuzzing; startFuzzingAsync: typeof addon.startFuzzingAsync; + startLibAfl: typeof addon.startLibAfl; + startLibAflAsync: typeof addon.startLibAflAsync; printAndDumpCrashingInput: typeof addon.printAndDumpCrashingInput; printReturnInfo: typeof addon.printReturnInfo; } @@ -38,6 +41,8 @@ export const fuzzer: Fuzzer = { tracer: tracer, startFuzzing: addon.startFuzzing, startFuzzingAsync: addon.startFuzzingAsync, + startLibAfl: addon.startLibAfl, + startLibAflAsync: addon.startLibAflAsync, printAndDumpCrashingInput: addon.printAndDumpCrashingInput, printReturnInfo: addon.printReturnInfo, }; diff --git a/packages/fuzzer/libafl_spike.cpp b/packages/fuzzer/libafl_spike.cpp index bc7aabfd2..f95ad3761 100644 --- a/packages/fuzzer/libafl_spike.cpp +++ b/packages/fuzzer/libafl_spike.cpp @@ -450,3 +450,11 @@ Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info) { return context->deferred.Promise(); } + +Napi::Value StartLibAfl(const Napi::CallbackInfo &info) { + return StartLibAflSpike(info); +} + +Napi::Value StartLibAflAsync(const Napi::CallbackInfo &info) { + return StartLibAflSpikeAsync(info); +} diff --git a/packages/fuzzer/libafl_spike.h b/packages/fuzzer/libafl_spike.h index d25f16932..dc9e0e41d 100644 --- a/packages/fuzzer/libafl_spike.h +++ b/packages/fuzzer/libafl_spike.h @@ -43,3 +43,5 @@ int jazzer_libafl_spike_run(const JazzerLibAflSpikeOptions *options, Napi::Value StartLibAflSpike(const Napi::CallbackInfo &info); Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info); +Napi::Value StartLibAfl(const Napi::CallbackInfo &info); +Napi::Value StartLibAflAsync(const Napi::CallbackInfo &info); diff --git a/packages/fuzzer/libafl_spike.test.ts b/packages/fuzzer/libafl_spike.test.ts index 95513a74c..88ce0875d 100644 --- a/packages/fuzzer/libafl_spike.test.ts +++ b/packages/fuzzer/libafl_spike.test.ts @@ -17,17 +17,25 @@ import { addon } from "./addon"; import { fuzzer } from "./fuzzer"; -const spikeOptions = { runs: 32, seed: 1234, maxLen: 64 }; +const libAflOptions = { + runs: 32, + seed: 1234, + maxLen: 64, + timeoutMillis: 1000, + maxTotalTimeSeconds: 0, + artifactPrefix: "", + corpusDirectories: [], +}; describe("LibAFL spike", () => { it("runs synchronous fuzz targets through the native spike", async () => { let invocations = 0; - await addon.startLibAflSpike( + await addon.startLibAfl( () => { invocations++; }, - spikeOptions, + libAflOptions, () => undefined, ); @@ -38,7 +46,7 @@ describe("LibAFL spike", () => { let lastInvocationCount = 0; let invocationCount = 1; - await addon.startLibAflSpikeAsync(async () => { + await addon.startLibAflAsync(async () => { const value = await new Promise((resolve) => { queueMicrotask(() => { setImmediate(() => resolve(invocationCount++)); @@ -52,7 +60,7 @@ describe("LibAFL spike", () => { } lastInvocationCount = value; - }, spikeOptions); + }, libAflOptions); expect(lastInvocationCount).toBeGreaterThan(0); }); @@ -60,14 +68,22 @@ describe("LibAFL spike", () => { it("records compare feedback in the shared native map", async () => { addon.clearCompareFeedbackMap(); - await addon.startLibAflSpike( + await addon.startLibAfl( (data: Buffer) => { const text = data.toString("utf8"); fuzzer.tracer.traceStrCmp(text, "jazzer", "===", 11); fuzzer.tracer.traceNumberCmp(data.length, 7, "===", 12); fuzzer.tracer.tracePcIndir(13, data.length); }, - { runs: 1, seed: 9, maxLen: 16 }, + { + runs: 1, + seed: 9, + maxLen: 16, + timeoutMillis: 1000, + maxTotalTimeSeconds: 0, + artifactPrefix: "", + corpusDirectories: [], + }, () => undefined, ); diff --git a/packages/fuzzer/spike/benchmark.js b/packages/fuzzer/spike/benchmark.js index e28416250..23a848017 100644 --- a/packages/fuzzer/spike/benchmark.js +++ b/packages/fuzzer/spike/benchmark.js @@ -27,7 +27,15 @@ const libFuzzerArgs = [ `-seed=${seed}`, `-max_len=${maxLen}`, ]; -const spikeOptions = { runs, seed, maxLen }; +const libAflOptions = { + runs, + seed, + maxLen, + timeoutMillis: 1000, + maxTotalTimeSeconds: 0, + artifactPrefix: "", + corpusDirectories: [], +}; async function measure(name, start) { console.log(`\nRunning ${name}...`); @@ -92,7 +100,7 @@ async function main() { ); results.push( await measure("LibAFL sync trivial", (target) => - addon.startLibAflSpike(target, spikeOptions, () => undefined), + addon.startLibAfl(target, libAflOptions, () => undefined), ), ); results.push( @@ -109,13 +117,13 @@ async function main() { ); results.push( await measure("LibAFL async trivial", (target) => - addon.startLibAflSpikeAsync( + addon.startLibAflAsync( () => new Promise((resolve) => { target(); setImmediate(resolve); }), - spikeOptions, + libAflOptions, ), ), ); @@ -126,7 +134,7 @@ async function main() { ); results.push( await measureCompareHeavy("LibAFL compare-heavy", (target) => - addon.startLibAflSpike(target, spikeOptions, () => undefined), + addon.startLibAfl(target, libAflOptions, () => undefined), ), ); diff --git a/packages/fuzzer/tsconfig.json b/packages/fuzzer/tsconfig.json index 8ef3f91ff..c03b8347c 100644 --- a/packages/fuzzer/tsconfig.json +++ b/packages/fuzzer/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": ".", "outDir": "dist" }, - "exclude": ["build", "dist", "cmake-build-*"] + "exclude": ["build", "dist", "spike", "cmake-build-*"] } From 3bafae73c1b2b4f8653cadf6a7e449dfad675f28 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 00:38:05 +0200 Subject: [PATCH 04/30] feat(fuzzer): harden the LibAFL runtime bridge and timeout path Expand the LibAFL native bridge to use structured backend options, corpus seed directories, compare-map feedback, and max-total-time control. This also introduces fail-fast timeout handling with artifact persistence and maps timeout outcomes to dedicated LibAFL execution statuses. --- packages/fuzzer/libafl_spike.cpp | 571 +++++++++++++++++++++++------ packages/fuzzer/libafl_spike.h | 3 + packages/fuzzer/rust/src/lib.rs | 116 +++++- packages/fuzzer/shared/libfuzzer.h | 1 + 4 files changed, 568 insertions(+), 123 deletions(-) diff --git a/packages/fuzzer/libafl_spike.cpp b/packages/fuzzer/libafl_spike.cpp index f95ad3761..797364ddc 100644 --- a/packages/fuzzer/libafl_spike.cpp +++ b/packages/fuzzer/libafl_spike.cpp @@ -14,11 +14,21 @@ #include "libafl_spike.h" +#include +#include +#include +#include #include #include #include +#include +#include #include +#include #include +#include +#include +#include #include #include #include @@ -41,35 +51,74 @@ constexpr int kExecutionContinue = 0; constexpr int kExecutionFinding = 1; constexpr int kExecutionStop = 2; constexpr int kExecutionFatal = 3; +constexpr int kExecutionTimeout = 4; constexpr int kSpikeOk = 0; constexpr int kSpikeFoundFinding = 1; constexpr int kSpikeStopped = 2; constexpr int kSpikeFatal = 3; +constexpr int kSpikeFoundTimeout = 4; struct ParsedSpikeOptions { - uint64_t runs = 1000; + uint64_t runs = 0; uint64_t seed = 1; size_t max_len = 4096; + uint64_t timeout_millis = 5000; + uint64_t max_total_time_seconds = 0; + std::string artifact_prefix; + std::vector corpus_directories; +}; + +struct SyncWatchdogState { + std::thread thread; + std::mutex mutex; + std::condition_variable cv; + bool should_stop = false; + bool execution_armed = false; + std::chrono::steady_clock::time_point deadline; + std::vector current_input; }; struct SyncFuzzTargetContext { + SyncFuzzTargetContext(Napi::Env env, Napi::Function target, + Napi::Function js_stop_callback, + ParsedSpikeOptions options) + : env(env), target(target), is_resolved(false), + deferred(Napi::Promise::Deferred::New(env)), + js_stop_callback(js_stop_callback), options(std::move(options)) {} + Napi::Env env; Napi::Function target; bool is_resolved; Napi::Promise::Deferred deferred; Napi::Function js_stop_callback; + ParsedSpikeOptions options; + SyncWatchdogState watchdog; volatile std::sig_atomic_t signal_status = 0; volatile int sigints = 0; std::jmp_buf execution_context; }; +struct AsyncExecutionState { + std::promise promise; + std::atomic settled = false; +}; + +struct AsyncDataType { + std::vector data; + std::shared_ptr state; + + AsyncDataType() = delete; +}; + struct AsyncFuzzTargetContext { - explicit AsyncFuzzTargetContext(Napi::Env env) - : deferred(Napi::Promise::Deferred::New(env)) {} + explicit AsyncFuzzTargetContext(Napi::Env env, ParsedSpikeOptions options) + : deferred(Napi::Promise::Deferred::New(env)), options(std::move(options)) { + } std::thread native_thread; Napi::Promise::Deferred deferred; + ParsedSpikeOptions options; bool is_resolved = false; bool is_done_called = false; int run_status = kSpikeOk; @@ -77,14 +126,6 @@ struct AsyncFuzzTargetContext { std::jmp_buf execution_context; }; -struct AsyncDataType { - const uint8_t *data; - size_t size; - std::promise *promise; - - AsyncDataType() = delete; -}; - using AsyncFinalizerDataType = void; void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, AsyncFuzzTargetContext *context, @@ -97,30 +138,205 @@ SyncFuzzTargetContext *gActiveSyncContext = nullptr; AsyncFuzzTargetContext *gActiveAsyncContext = nullptr; AsyncTsfn gAsyncTsfn; -ParsedSpikeOptions ParseSpikeOptions(Napi::Env env, const Napi::Object &js_opts) { +std::string DigestInput(const uint8_t *data, size_t size) { + uint64_t hash = 1469598103934665603ULL; + for (size_t i = 0; i < size; ++i) { + hash ^= static_cast(data[i]); + hash *= 1099511628211ULL; + } + + std::array words{}; + for (auto &word : words) { + hash ^= hash >> 33; + hash *= 0xff51afd7ed558ccdULL; + hash ^= hash >> 33; + hash *= 0xc4ceb9fe1a85ec53ULL; + hash ^= hash >> 33; + word = static_cast(hash); + } + + std::ostringstream stream; + stream << std::hex << std::setfill('0'); + for (const auto word : words) { + stream << std::setw(8) << word; + } + return stream.str(); +} + +std::filesystem::path ArtifactPath(const std::string &artifact_prefix, + const std::string &kind, + const std::string &digest) { + const auto filename = kind + "-" + digest; + + if (artifact_prefix.empty()) { + return std::filesystem::current_path() / filename; + } + + const auto has_directory_semantics = + artifact_prefix.back() == '/' || artifact_prefix.back() == '\\'; + std::filesystem::path prefix_path(artifact_prefix); + if (has_directory_semantics || + (std::filesystem::exists(prefix_path) && + std::filesystem::is_directory(prefix_path))) { + return prefix_path / filename; + } + + return std::filesystem::path(artifact_prefix + filename); +} + +void WriteArtifact(const std::string &artifact_prefix, const std::string &kind, + const uint8_t *data, size_t size) { + if (data == nullptr) { + return; + } + + try { + const auto digest = DigestInput(data, size); + const auto artifact_path = ArtifactPath(artifact_prefix, kind, digest); + + if (!artifact_path.parent_path().empty()) { + std::filesystem::create_directories(artifact_path.parent_path()); + } + + std::ofstream output(artifact_path, + std::ios::binary | std::ios::out | std::ios::trunc); + if (!output.is_open()) { + std::cerr << "ERROR: Failed to open artifact file '" + << artifact_path.string() << "'" << std::endl; + return; + } + + if (size > 0) { + output.write(reinterpret_cast(data), + static_cast(size)); + } + if (!output.good()) { + std::cerr << "ERROR: Failed to write artifact file '" + << artifact_path.string() << "'" << std::endl; + return; + } + + std::cerr << "INFO: Wrote " << kind << " input to " + << artifact_path.string() << std::endl; + } catch (const std::exception &exception) { + std::cerr << "ERROR: Failed to persist " << kind + << " artifact: " << exception.what() << std::endl; + } +} + +[[noreturn]] void ExitOnTimeout(uint64_t timeout_millis, + const std::string &artifact_prefix, + const std::vector &input) { + std::cerr << "ERROR: Exceeded timeout of " << timeout_millis + << " ms for one fuzz target execution." << std::endl; + if (!input.empty()) { + WriteArtifact(artifact_prefix, "timeout", input.data(), input.size()); + } + _Exit(libfuzzer::EXIT_ERROR_TIMEOUT); +} + +[[noreturn]] void ExitWithUnexpectedError(const std::exception &exception) { + std::cerr << "==" << static_cast(GetPID()) + << "== Jazzer.js: Unexpected Error: " << exception.what() + << std::endl; + libfuzzer::PrintCrashingInput(); + _Exit(libfuzzer::EXIT_ERROR_CODE); +} + +void RejectDeferredIfNeeded(AsyncFuzzTargetContext *context, + const Napi::Value &error) { + if (context->is_resolved) { + return; + } + context->deferred.Reject(error); + context->is_resolved = true; +} + +bool TrySetExecutionStatus(const std::shared_ptr &state, + int status) { + bool expected = false; + if (!state->settled.compare_exchange_strong(expected, true, + std::memory_order_acq_rel, + std::memory_order_acquire)) { + return false; + } + state->promise.set_value(status); + return true; +} + +void ReportAsyncFinding(AsyncFuzzTargetContext *context, Napi::Env env, + const std::shared_ptr &state, + const Napi::Value &error, + const std::vector &input) { + if (TrySetExecutionStatus(state, kExecutionFinding)) { + if (!input.empty()) { + WriteArtifact(context->options.artifact_prefix, "crash", input.data(), + input.size()); + } + } + RejectDeferredIfNeeded(context, error); +} + +ParsedSpikeOptions ParseSpikeOptions(Napi::Env env, + const Napi::Object &js_opts) { ParsedSpikeOptions parsed; const auto runs = js_opts.Get("runs"); const auto seed = js_opts.Get("seed"); const auto max_len = js_opts.Get("maxLen"); - - if (!runs.IsNumber() || !seed.IsNumber() || !max_len.IsNumber()) { + const auto timeout_millis = js_opts.Get("timeoutMillis"); + const auto max_total_time_seconds = js_opts.Get("maxTotalTimeSeconds"); + const auto artifact_prefix = js_opts.Get("artifactPrefix"); + const auto corpus_directories = js_opts.Get("corpusDirectories"); + + if (!runs.IsNumber() || !seed.IsNumber() || !max_len.IsNumber() || + !timeout_millis.IsNumber() || !max_total_time_seconds.IsNumber() || + !artifact_prefix.IsString() || !corpus_directories.IsArray()) { throw Napi::Error::New( env, - "The LibAFL spike expects an options object with numeric runs, seed, " - "and maxLen properties"); + "The LibAFL backend expects an options object with runs, seed, " + "maxLen, timeoutMillis, maxTotalTimeSeconds, artifactPrefix, and " + "corpusDirectories"); } - parsed.runs = runs.As().Int64Value(); - parsed.seed = seed.As().Int64Value(); - parsed.max_len = static_cast(max_len.As().Int64Value()); + const auto runs_value = runs.As().Int64Value(); + const auto seed_value = seed.As().Int64Value(); + const auto max_len_value = max_len.As().Int64Value(); + const auto timeout_millis_value = timeout_millis.As().Int64Value(); + const auto max_total_time_seconds_value = + max_total_time_seconds.As().Int64Value(); - if (parsed.runs == 0) { - throw Napi::Error::New(env, "The LibAFL spike requires -runs to be > 0"); + if (runs_value < 0 || seed_value < 0 || max_len_value < 0 || + timeout_millis_value < 0 || max_total_time_seconds_value < 0) { + throw Napi::Error::New( + env, "The LibAFL options object does not allow negative values"); + } + + parsed.runs = static_cast(runs_value); + parsed.seed = static_cast(seed_value); + parsed.max_len = static_cast(max_len_value); + parsed.timeout_millis = static_cast(timeout_millis_value); + parsed.max_total_time_seconds = + static_cast(max_total_time_seconds_value); + parsed.artifact_prefix = artifact_prefix.As().Utf8Value(); + + const auto dirs = corpus_directories.As(); + for (uint32_t i = 0; i < dirs.Length(); ++i) { + auto dir = dirs.Get(i); + if (!dir.IsString()) { + throw Napi::Error::New( + env, "LibAFL corpusDirectories entries must be strings"); + } + parsed.corpus_directories.push_back(dir.As().Utf8Value()); } + if (parsed.max_len == 0) { throw Napi::Error::New(env, - "The LibAFL spike requires -max_len to be > 0"); + "The LibAFL backend requires maxLen to be > 0"); + } + if (parsed.timeout_millis == 0) { + throw Napi::Error::New( + env, "The LibAFL backend requires timeoutMillis to be > 0"); } return parsed; @@ -132,15 +348,111 @@ JazzerLibAflSharedMaps SharedMapsForSpike(Napi::Env env) { auto *cmp = CompareFeedbackMap(); const auto cmp_len = CompareFeedbackMapSize(); - if (edges == nullptr || edges_len == 0) { - throw Napi::Error::New(env, - "Coverage counters were not initialized before the " - "LibAFL spike started"); + if (edges == nullptr || edges_len == 0 || cmp == nullptr || cmp_len == 0) { + throw Napi::Error::New( + env, + "Coverage maps were not initialized before the LibAFL backend started"); } return {edges, edges_len, cmp, cmp_len}; } +void StartSyncWatchdog(SyncFuzzTargetContext *context) { + if (context->options.timeout_millis == 0) { + return; + } + + context->watchdog.thread = std::thread([context]() { + auto &watchdog = context->watchdog; + std::unique_lock lock(watchdog.mutex); + while (true) { + watchdog.cv.wait( + lock, [&watchdog] { return watchdog.should_stop || watchdog.execution_armed; }); + + if (watchdog.should_stop) { + return; + } + + const auto deadline = watchdog.deadline; + const auto resumed = watchdog.cv.wait_until( + lock, deadline, [&watchdog, deadline] { + return watchdog.should_stop || !watchdog.execution_armed || + watchdog.deadline != deadline; + }); + if (resumed) { + if (watchdog.should_stop) { + return; + } + continue; + } + + auto timed_out_input = watchdog.current_input; + lock.unlock(); + ExitOnTimeout(context->options.timeout_millis, + context->options.artifact_prefix, timed_out_input); + } + }); +} + +void ArmSyncWatchdog(SyncFuzzTargetContext *context, const uint8_t *data, + size_t size) { + if (context->options.timeout_millis == 0) { + return; + } + + auto &watchdog = context->watchdog; + std::lock_guard lock(watchdog.mutex); + watchdog.current_input.assign(data, data + size); + watchdog.deadline = + std::chrono::steady_clock::now() + + std::chrono::milliseconds(context->options.timeout_millis); + watchdog.execution_armed = true; + watchdog.cv.notify_one(); +} + +void DisarmSyncWatchdog(SyncFuzzTargetContext *context) { + if (context->options.timeout_millis == 0) { + return; + } + + auto &watchdog = context->watchdog; + std::lock_guard lock(watchdog.mutex); + watchdog.execution_armed = false; + watchdog.current_input.clear(); + watchdog.cv.notify_one(); +} + +void StopSyncWatchdog(SyncFuzzTargetContext *context) { + if (context->options.timeout_millis == 0) { + return; + } + + auto &watchdog = context->watchdog; + { + std::lock_guard lock(watchdog.mutex); + watchdog.should_stop = true; + watchdog.execution_armed = false; + } + watchdog.cv.notify_one(); + if (watchdog.thread.joinable()) { + watchdog.thread.join(); + } +} + +class ScopedSyncWatchdog { +public: + ScopedSyncWatchdog(SyncFuzzTargetContext *context, const uint8_t *data, + size_t size) + : context_(context) { + ArmSyncWatchdog(context_, data, size); + } + + ~ScopedSyncWatchdog() { DisarmSyncWatchdog(context_); } + +private: + SyncFuzzTargetContext *context_; +}; + void SyncSigintHandler(int signum) { std::cerr << std::endl; gActiveSyncContext->signal_status = signum; @@ -158,6 +470,7 @@ void SyncErrorSignalHandler(int signum) { int ExecuteSyncInput(void *user_data, const uint8_t *data, size_t size) { auto *context = static_cast(user_data); auto scope = Napi::HandleScope(context->env); + ScopedSyncWatchdog watchdog(context, data, size); ClearCoverageCounters(); ClearCompareFeedbackMap(); @@ -173,15 +486,12 @@ int ExecuteSyncInput(void *user_data, const uint8_t *data, size_t size) { } } } catch (const Napi::Error &error) { + WriteArtifact(context->options.artifact_prefix, "crash", data, size); context->is_resolved = true; context->deferred.Reject(error.Value()); return kExecutionFinding; - } catch (std::exception &exception) { - std::cerr << "==" << static_cast(GetPID()) - << "== Jazzer.js: Unexpected Error: " << exception.what() - << std::endl; - libfuzzer::PrintCrashingInput(); - _Exit(libfuzzer::EXIT_ERROR_CODE); + } catch (const std::exception &exception) { + ExitWithUnexpectedError(exception); } if (context->signal_status != 0) { @@ -207,10 +517,13 @@ int ExecuteSyncInput(void *user_data, const uint8_t *data, size_t size) { void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, AsyncFuzzTargetContext *context, - AsyncDataType *data) { + AsyncDataType *input) { + auto state = input->state; + const auto current_input = input->data; + try { if (context->sigints > 0) { - data->promise->set_value(kExecutionStop); + TrySetExecutionStatus(state, kExecutionStop); context->deferred.Resolve(env.Undefined()); context->is_resolved = true; return; @@ -224,11 +537,12 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, } if (env == nullptr) { - data->promise->set_value(kExecutionFatal); + TrySetExecutionStatus(state, kExecutionFatal); return; } - auto buffer = Napi::Buffer::Copy(env, data->data, data->size); + auto buffer = + Napi::Buffer::Copy(env, current_input.data(), current_input.size()); auto parameter_count = js_fuzz_callback.As() .Get("length") .As() @@ -236,7 +550,6 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, if (parameter_count > 1) { context->is_done_called = false; - context->is_resolved = false; auto done = Napi::Function::New( env, [=](const Napi::CallbackInfo &info) { if (context->is_resolved) { @@ -244,42 +557,36 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, } if (context->is_done_called) { - context->deferred.Reject( - Napi::Error::New(env, "Expected done to be called once, but " - "it was called multiple times.") - .Value()); - context->is_resolved = true; - std::cerr << "Expected done to be called once, but it was called " - "multiple times." - << std::endl; + auto error = Napi::Error::New( + env, + "Expected done to be called once, but it was called multiple times.") + .Value(); + ReportAsyncFinding(context, env, state, error, current_input); return; } context->is_done_called = true; - auto has_error = !(info[0].IsNull() || info[0].IsUndefined()); + const auto has_error = info.Length() > 0 && + !(info[0].IsNull() || info[0].IsUndefined()); if (has_error) { - data->promise->set_value(kExecutionFinding); - context->deferred.Reject(info[0].As().Value()); - context->is_resolved = true; + auto error = info[0]; + if (!error.IsObject()) { + error = Napi::Error::New(env, error.ToString()).Value(); + } + ReportAsyncFinding(context, env, state, error, current_input); } else { - data->promise->set_value(kExecutionContinue); + TrySetExecutionStatus(state, kExecutionContinue); } }); auto result = js_fuzz_callback.Call({buffer, done}); if (result.IsPromise()) { AsyncReturnsHandler(); - if (context->is_resolved) { - return; - } - if (!context->is_done_called) { - data->promise->set_value(kExecutionFinding); - } - context->deferred.Reject( - Napi::Error::New(env, "Internal fuzzer error - Either async or " - "done callback based fuzz tests allowed.") - .Value()); - context->is_resolved = true; + auto error = Napi::Error::New( + env, + "Internal fuzzer error - Either async or done callback based fuzz tests allowed.") + .Value(); + ReportAsyncFinding(context, env, state, error, current_input); } else { SyncReturnsHandler(); } @@ -294,30 +601,27 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, then.Call( js_promise, {Napi::Function::New(env, [=](const Napi::CallbackInfo &) { - data->promise->set_value(kExecutionContinue); + TrySetExecutionStatus(state, kExecutionContinue); }), Napi::Function::New(env, [=](const Napi::CallbackInfo &info) { - data->promise->set_value(kExecutionFinding); - context->deferred.Reject(info[0].As().Value()); - context->is_resolved = true; + auto error = info.Length() > 0 ? info[0] + : Napi::Error::New(env, "Unknown promise rejection") + .Value(); + if (!error.IsObject()) { + error = Napi::Error::New(env, error.ToString()).Value(); + } + ReportAsyncFinding(context, env, state, error, current_input); })}); } else { SyncReturnsHandler(); - data->promise->set_value(kExecutionContinue); + TrySetExecutionStatus(state, kExecutionContinue); } } catch (const Napi::Error &error) { - if (context->is_resolved) { - return; - } - data->promise->set_value(kExecutionFinding); - context->deferred.Reject(error.Value()); - context->is_resolved = true; + ReportAsyncFinding(context, env, state, error.Value(), current_input); } catch (const std::exception &exception) { - data->promise->set_value(kExecutionFatal); - auto message = std::string("Internal fuzzer error - ").append( - exception.what()); - context->deferred.Reject(Napi::Error::New(env, message).Value()); - context->is_resolved = true; + TrySetExecutionStatus(state, kExecutionFatal); + auto message = std::string("Internal fuzzer error - ").append(exception.what()); + RejectDeferredIfNeeded(context, Napi::Error::New(env, message).Value()); } } @@ -339,24 +643,35 @@ int ExecuteAsyncInput(void *user_data, const uint8_t *data, size_t size) { ClearCoverageCounters(); ClearCompareFeedbackMap(); - std::promise promise; - auto input = AsyncDataType{data, size, &promise}; + auto execution_state = std::make_shared(); + auto *input = new AsyncDataType{ + std::vector(data, data + size), + execution_state, + }; - auto future = promise.get_future(); - auto status = gAsyncTsfn.BlockingCall(&input); + auto future = execution_state->promise.get_future(); + auto status = gAsyncTsfn.BlockingCall(input); if (status != napi_ok) { - Napi::Error::Fatal("StartLibAflSpikeAsync", + delete input; + Napi::Error::Fatal("StartLibAflAsync", "TypedThreadSafeFunction.BlockingCall() failed"); } + if (context->options.timeout_millis > 0) { + auto timeout = std::chrono::milliseconds(context->options.timeout_millis); + if (future.wait_for(timeout) == std::future_status::timeout) { + ExitOnTimeout(context->options.timeout_millis, + context->options.artifact_prefix, input->data); + } + } + try { - return future.get(); - } catch (std::exception &exception) { - std::cerr << "==" << static_cast(GetPID()) - << "== Jazzer.js: Unexpected Error: " << exception.what() - << std::endl; - libfuzzer::PrintCrashingInput(); - _Exit(libfuzzer::EXIT_ERROR_CODE); + auto result = future.get(); + delete input; + return result; + } catch (const std::exception &exception) { + delete input; + ExitWithUnexpectedError(exception); } } } // namespace @@ -367,35 +682,58 @@ Napi::Value StartLibAflSpike(const Napi::CallbackInfo &info) { throw Napi::Error::New( info.Env(), "Need three arguments, which must be the fuzz target function, a " - "LibAFL spike options object, and a stop callback"); + "LibAFL options object, and a stop callback"); } auto options = ParseSpikeOptions(info.Env(), info[1].As()); auto maps = SharedMapsForSpike(info.Env()); - SyncFuzzTargetContext context = {info.Env(), - info[0].As(), - false, - Napi::Promise::Deferred::New(info.Env()), - info[2].As()}; + SyncFuzzTargetContext context(info.Env(), info[0].As(), + info[2].As(), + std::move(options)); gActiveSyncContext = &context; + StartSyncWatchdog(&context); signal(SIGINT, SyncSigintHandler); signal(SIGSEGV, SyncErrorSignalHandler); - JazzerLibAflSpikeOptions spike_options{options.runs, options.seed, - options.max_len}; + std::vector corpus_directories; + corpus_directories.reserve(options.corpus_directories.size()); + for (const auto &directory : options.corpus_directories) { + corpus_directories.push_back(directory.c_str()); + } + + JazzerLibAflSpikeOptions spike_options{ + options.runs, + options.seed, + options.max_len, + options.max_total_time_seconds, + corpus_directories.empty() ? nullptr : corpus_directories.data(), + corpus_directories.size(), + }; auto status = jazzer_libafl_spike_run(&spike_options, &maps, ExecuteSyncInput, &context); signal(SIGINT, SIG_DFL); signal(SIGSEGV, SIG_DFL); + StopSyncWatchdog(&context); gActiveSyncContext = nullptr; if (status == kSpikeFatal && !context.is_resolved) { context.is_resolved = true; context.deferred.Reject( - Napi::Error::New(info.Env(), "The LibAFL spike failed internally") + Napi::Error::New(info.Env(), "The LibAFL backend failed internally") + .Value()); + } else if (status == kSpikeFoundTimeout && !context.is_resolved) { + context.is_resolved = true; + context.deferred.Reject( + Napi::Error::New(info.Env(), + "Exceeded timeout while executing one fuzz input") + .Value()); + } else if (status == kSpikeFoundFinding && !context.is_resolved) { + context.is_resolved = true; + context.deferred.Reject( + Napi::Error::New(info.Env(), "The LibAFL backend found a crashing input") .Value()); } @@ -410,21 +748,31 @@ Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info) { if (info.Length() != 2 || !info[0].IsFunction() || !info[1].IsObject()) { throw Napi::Error::New(info.Env(), "Need two arguments, which must be the fuzz target " - "function and a LibAFL spike options object"); + "function and a LibAFL options object"); } auto options = ParseSpikeOptions(info.Env(), info[1].As()); auto maps = SharedMapsForSpike(info.Env()); - auto *context = new AsyncFuzzTargetContext(info.Env()); + auto *context = new AsyncFuzzTargetContext(info.Env(), std::move(options)); gAsyncTsfn = AsyncTsfn::New( - info.Env(), info[0].As(), "LibAflSpikeAsyncAddon", 0, 1, + info.Env(), info[0].As(), "LibAflAsyncAddon", 0, 1, context, [](Napi::Env env, AsyncFinalizerDataType *, AsyncFuzzTargetContext *ctx) { ctx->native_thread.join(); if (ctx->run_status == kSpikeFatal && !ctx->is_resolved) { ctx->deferred.Reject( - Napi::Error::New(env, "The LibAFL spike failed internally") + Napi::Error::New(env, "The LibAFL backend failed internally") + .Value()); + } else if (ctx->run_status == kSpikeFoundTimeout && !ctx->is_resolved) { + ctx->deferred.Reject( + Napi::Error::New( + env, "Exceeded timeout while executing one fuzz input") + .Value()); + } else if (ctx->run_status == kSpikeFoundFinding && !ctx->is_resolved) { + ctx->deferred.Reject( + Napi::Error::New(env, + "The LibAFL backend found a crashing input") .Value()); } else if (!ctx->is_resolved) { ctx->deferred.Resolve(env.Undefined()); @@ -433,12 +781,25 @@ Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info) { }); context->native_thread = std::thread( - [options, maps](AsyncFuzzTargetContext *ctx) { + [maps](AsyncFuzzTargetContext *ctx) { gActiveAsyncContext = ctx; signal(SIGSEGV, AsyncErrorSignalHandler); signal(SIGINT, AsyncSigintHandler); - JazzerLibAflSpikeOptions spike_options{options.runs, options.seed, - options.max_len}; + + std::vector corpus_directories; + corpus_directories.reserve(ctx->options.corpus_directories.size()); + for (const auto &directory : ctx->options.corpus_directories) { + corpus_directories.push_back(directory.c_str()); + } + + JazzerLibAflSpikeOptions spike_options{ + ctx->options.runs, + ctx->options.seed, + ctx->options.max_len, + ctx->options.max_total_time_seconds, + corpus_directories.empty() ? nullptr : corpus_directories.data(), + corpus_directories.size(), + }; ctx->run_status = jazzer_libafl_spike_run(&spike_options, &maps, ExecuteAsyncInput, ctx); signal(SIGINT, SIG_DFL); diff --git a/packages/fuzzer/libafl_spike.h b/packages/fuzzer/libafl_spike.h index dc9e0e41d..1cebc3f98 100644 --- a/packages/fuzzer/libafl_spike.h +++ b/packages/fuzzer/libafl_spike.h @@ -23,6 +23,9 @@ struct JazzerLibAflSpikeOptions { uint64_t runs; uint64_t seed; size_t max_len; + uint64_t max_total_time_seconds; + const char **corpus_directories; + size_t corpus_directories_len; }; struct JazzerLibAflSharedMaps { diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 0648dc6ac..7518c135e 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -1,12 +1,16 @@ -use core::ffi::c_void; +use core::ffi::{c_char, c_void}; use core::ptr; use std::cell::Cell; +use std::ffi::CStr; +use std::path::PathBuf; +use std::time::{Duration, Instant}; use libafl::{ corpus::{Corpus, InMemoryCorpus, Testcase}, events::SimpleEventManager, executors::{inprocess::InProcessExecutor, ExitKind}, - feedbacks::{CrashFeedback, MaxMapFeedback}, + feedback_or_fast, + feedbacks::{CrashFeedback, MaxMapFeedback, TimeoutFeedback}, fuzzer::{Fuzzer, StdFuzzer}, inputs::{BytesInput, HasTargetBytes}, monitors::SimpleMonitor, @@ -22,17 +26,22 @@ const EXECUTION_CONTINUE: i32 = 0; const EXECUTION_FINDING: i32 = 1; const EXECUTION_STOP: i32 = 2; const EXECUTION_FATAL: i32 = 3; +const EXECUTION_TIMEOUT: i32 = 4; const SPIKE_OK: i32 = 0; const SPIKE_FOUND_FINDING: i32 = 1; const SPIKE_STOPPED: i32 = 2; const SPIKE_FATAL: i32 = 3; +const SPIKE_FOUND_TIMEOUT: i32 = 4; #[repr(C)] pub struct JazzerLibAflSpikeOptions { pub runs: u64, pub seed: u64, pub max_len: usize, + pub max_total_time_seconds: u64, + pub corpus_directories: *const *const c_char, + pub corpus_directories_len: usize, } #[repr(C)] @@ -59,12 +68,21 @@ fn clear_shared_map(ptr: *mut u8, len: usize) { } } -fn clamp_input_length(input: Vec, max_len: usize) -> Vec { - if input.len() <= max_len { - input - } else { - input[..max_len].to_vec() +unsafe fn parse_corpus_directories(options: &JazzerLibAflSpikeOptions) -> Option> { + if options.corpus_directories.is_null() || options.corpus_directories_len == 0 { + return Some(Vec::new()); + } + + let mut result = Vec::with_capacity(options.corpus_directories_len); + let directories = std::slice::from_raw_parts(options.corpus_directories, options.corpus_directories_len); + for directory in directories { + if directory.is_null() { + return None; + } + let path = CStr::from_ptr(*directory).to_string_lossy().to_string(); + result.push(PathBuf::from(path)); } + Some(result) } #[no_mangle] @@ -75,15 +93,25 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( user_data: *mut c_void, ) -> i32 { if options.is_null() || maps.is_null() { + eprintln!("[libafl] fatal: null options or maps pointer"); return SPIKE_FATAL; } let options = &*options; let maps = &*maps; - if maps.edges.is_null() || maps.edges_len == 0 { + if maps.edges.is_null() || maps.edges_len == 0 || maps.cmp.is_null() || maps.cmp_len == 0 { + eprintln!("[libafl] fatal: shared maps are missing"); return SPIKE_FATAL; } + let corpus_dirs = match parse_corpus_directories(options) { + Some(dirs) => dirs, + None => { + eprintln!("[libafl] fatal: invalid corpus directories"); + return SPIKE_FATAL; + } + }; + let monitor = SimpleMonitor::new(|_| {}); let mut mgr = SimpleEventManager::new(monitor); @@ -93,9 +121,17 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( maps.edges_len, )) .track_indices(); + let cmp_observer = HitcountsMapObserver::new(StdMapObserver::from_mut_ptr( + "cmp", + maps.cmp, + maps.cmp_len, + )); - let mut feedback = MaxMapFeedback::new(&edges_observer); - let mut objective = CrashFeedback::new(); + let mut feedback = feedback_or_fast!( + MaxMapFeedback::new(&edges_observer), + MaxMapFeedback::new(&cmp_observer) + ); + let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new()); let mut state = match StdState::new( StdRand::with_seed(options.seed), InMemoryCorpus::new(), @@ -104,7 +140,10 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( &mut objective, ) { Ok(state) => state, - Err(_) => return SPIKE_FATAL, + Err(error) => { + eprintln!("[libafl] fatal: failed to create fuzzing state: {error:?}"); + return SPIKE_FATAL; + } }; if state @@ -112,6 +151,7 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( .add(Testcase::new(BytesInput::new(vec![]))) .is_err() { + eprintln!("[libafl] fatal: failed to seed empty testcase"); return SPIKE_FATAL; } @@ -120,13 +160,16 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( let mut stages = tuple_list!(StdMutationalStage::new(mutator)); let stop_requested = Cell::new(false); let fatal_error = Cell::new(false); + let timeout_found = Cell::new(false); let mut harness = |input: &BytesInput| { clear_shared_map(maps.edges, maps.edges_len); clear_shared_map(maps.cmp, maps.cmp_len); - let bytes = clamp_input_length(input.target_bytes().as_slice().to_vec(), options.max_len); - let status = unsafe { execute_one(user_data, bytes.as_ptr(), bytes.len()) }; + let bytes = input.target_bytes(); + let bytes = bytes.as_slice(); + let size = bytes.len().min(options.max_len); + let status = unsafe { execute_one(user_data, bytes.as_ptr(), size) }; match status { EXECUTION_CONTINUE => ExitKind::Ok, EXECUTION_FINDING => ExitKind::Crash, @@ -138,6 +181,10 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( fatal_error.set(true); ExitKind::Ok } + EXECUTION_TIMEOUT => { + timeout_found.set(true); + ExitKind::Timeout + } _ => { fatal_error.set(true); ExitKind::Ok @@ -147,27 +194,60 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( let mut executor = match InProcessExecutor::new( &mut harness, - tuple_list!(edges_observer), + tuple_list!(edges_observer, cmp_observer), &mut fuzzer, &mut state, &mut mgr, ) { Ok(executor) => executor, - Err(_) => return SPIKE_FATAL, + Err(error) => { + eprintln!("[libafl] fatal: failed to create executor: {error:?}"); + return SPIKE_FATAL; + } }; - for _ in 0..options.runs { - if fuzzer - .fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr) + if !corpus_dirs.is_empty() && state.must_load_initial_inputs() { + if state + .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &corpus_dirs) .is_err() { + eprintln!("[libafl] fatal: failed to load initial corpus inputs"); return SPIKE_FATAL; } + } + + let started_at = Instant::now(); + let max_total_time = if options.max_total_time_seconds == 0 { + None + } else { + Some(Duration::from_secs(options.max_total_time_seconds)) + }; + + let mut iterations = 0u64; + loop { + if options.runs != 0 && iterations >= options.runs { + break; + } + if let Some(max_total_time) = max_total_time { + if started_at.elapsed() >= max_total_time { + return SPIKE_STOPPED; + } + } + + if let Err(error) = fuzzer.fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr) { + eprintln!("[libafl] fatal: fuzz_one returned an error: {error:?}"); + return SPIKE_FATAL; + } + iterations = iterations.saturating_add(1); if fatal_error.get() { return SPIKE_FATAL; } + if timeout_found.get() { + return SPIKE_FOUND_TIMEOUT; + } + if state.solutions().count() > 0 { return SPIKE_FOUND_FINDING; } diff --git a/packages/fuzzer/shared/libfuzzer.h b/packages/fuzzer/shared/libfuzzer.h index d243d67c0..748b94c19 100644 --- a/packages/fuzzer/shared/libfuzzer.h +++ b/packages/fuzzer/shared/libfuzzer.h @@ -20,6 +20,7 @@ namespace libfuzzer { extern void (*PrintCrashingInput)(); const int EXIT_ERROR_CODE = 77; +const int EXIT_ERROR_TIMEOUT = 70; // Signals should exit with code 128+n, see // https://tldp.org/LDP/abs/html/exitcodes.html From 764932777eb695cbd574dd7fa9bb4c98db1de3f4 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 00:39:09 +0200 Subject: [PATCH 05/30] fix(core): validate LibAFL mode constraints explicitly Fail fast when users combine engine=libafl with regression mode and cover it with option tests. This prevents silently running incompatible mode combinations with undefined behavior. --- packages/core/options.test.ts | 12 ++++++++++++ packages/core/options.ts | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/core/options.test.ts b/packages/core/options.test.ts index e8b38064c..b911a1dfa 100644 --- a/packages/core/options.test.ts +++ b/packages/core/options.test.ts @@ -364,6 +364,18 @@ describe("libafl options", () => { expect(() => buildLibAflOptions(manager)).toThrow("not supported"); }); + + it("rejects regression mode in LibAFL mode", () => { + const manager = new OptionsManager(OptionSource.DefaultCLIOptions).merge( + { + engine: "libafl", + mode: "regression", + }, + OptionSource.CommandLineArguments, + ); + + expect(() => buildLibAflOptions(manager)).toThrow("fuzzing mode only"); + }); }); function expectDefaultsExceptKeys( diff --git a/packages/core/options.ts b/packages/core/options.ts index bf80aedce..df96544aa 100644 --- a/packages/core/options.ts +++ b/packages/core/options.ts @@ -470,11 +470,17 @@ export function buildLibFuzzerOptions(options: OptionsManager) { export const buildFuzzerOption = buildLibFuzzerOptions; export function buildLibAflOptions(options: OptionsManager): LibAflOptions { + if (options.get("mode") === "regression") { + throw new Error( + "The 'libafl' engine currently supports fuzzing mode only.", + ); + } + if (options.get("timeout") <= 0) { throw new Error("timeout must be > 0"); } - let runs = options.get("mode") === "regression" ? 0 : 0; + let runs = 0; let seed = 0; let maxLen = 4096; let maxTotalTimeSeconds = 0; From 9701989c3d3ebd51e694ce8ccab47259c262e03d Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 00:43:25 +0200 Subject: [PATCH 06/30] fix(fuzzer): make LibAFL -runs track executions, not fuzz_one loops Count completed target executions using LibAFL state metrics so the '-runs' option matches the libFuzzer expectation more closely during benchmarking and real fuzzing campaigns. --- packages/fuzzer/rust/src/lib.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 7518c135e..0f783fe43 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -18,7 +18,7 @@ use libafl::{ observers::{CanTrack, HitcountsMapObserver, StdMapObserver}, schedulers::QueueScheduler, stages::mutational::StdMutationalStage, - state::{HasCorpus, HasSolutions, StdState}, + state::{HasCorpus, HasExecutions, HasSolutions, StdState}, }; use libafl_bolts::{rands::StdRand, tuples::tuple_list, AsSlice}; @@ -223,9 +223,14 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( Some(Duration::from_secs(options.max_total_time_seconds)) }; - let mut iterations = 0u64; + let initial_executions = *state.executions(); loop { - if options.runs != 0 && iterations >= options.runs { + if options.runs != 0 + && state + .executions() + .saturating_sub(initial_executions) + >= options.runs + { break; } if let Some(max_total_time) = max_total_time { @@ -238,8 +243,6 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( eprintln!("[libafl] fatal: fuzz_one returned an error: {error:?}"); return SPIKE_FATAL; } - iterations = iterations.saturating_add(1); - if fatal_error.get() { return SPIKE_FATAL; } From 4683a48f4fa3d7d7609861599a5f929837910e10 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 00:43:48 +0200 Subject: [PATCH 07/30] docs(ci): ship dual-engine docs and Rust CI prerequisites Document the new engine selector and backend-specific fuzzerOptions behavior across README and fuzz-settings. Also install a Rust toolchain in release and test workflows so native LibAFL builds are deterministic on all supported CI jobs. --- .github/workflows/release.yaml | 2 ++ .github/workflows/run-all-tests-main.yaml | 2 ++ .github/workflows/run-all-tests-pr.yaml | 6 ++++ README.md | 8 +++-- docs/fuzz-settings.md | 43 +++++++++++++++++++++-- packages/fuzzer/README.md | 23 ++++++------ packages/fuzzer/package.json | 4 +-- 7 files changed, 71 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c3015629a..d6ae2e834 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -28,6 +28,8 @@ jobs: with: node-version: 22 cache: "npm" + - name: rust + uses: dtolnay/rust-toolchain@stable - name: MSVC (windows) uses: ilammy/msvc-dev-cmd@v1 if: contains(matrix.os, 'windows') diff --git a/.github/workflows/run-all-tests-main.yaml b/.github/workflows/run-all-tests-main.yaml index 6d415ebfd..f0c3a2c75 100644 --- a/.github/workflows/run-all-tests-main.yaml +++ b/.github/workflows/run-all-tests-main.yaml @@ -29,6 +29,8 @@ jobs: with: node-version: 22 cache: "npm" + - name: rust + uses: dtolnay/rust-toolchain@stable - name: install dependencies run: npm ci - name: build project diff --git a/.github/workflows/run-all-tests-pr.yaml b/.github/workflows/run-all-tests-pr.yaml index e657966d5..dc400b711 100644 --- a/.github/workflows/run-all-tests-pr.yaml +++ b/.github/workflows/run-all-tests-pr.yaml @@ -24,6 +24,8 @@ jobs: with: node-version: 22 cache: "npm" + - name: rust + uses: dtolnay/rust-toolchain@stable - name: install dependencies run: npm ci - name: install clang-tidy @@ -67,6 +69,8 @@ jobs: with: node-version: ${{ matrix.node }} cache: "npm" + - name: rust + uses: dtolnay/rust-toolchain@stable - name: MSVC (windows) uses: ilammy/msvc-dev-cmd@v1 if: contains(matrix.os, 'windows') @@ -95,6 +99,8 @@ jobs: with: node-version: 22 cache: "npm" + - name: rust + uses: dtolnay/rust-toolchain@stable - name: MSVC (windows) uses: ilammy/msvc-dev-cmd@v1 if: contains(matrix.os, 'windows') diff --git a/README.md b/README.md index 85873303e..e9a21f67f 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ Jazzer.js is a coverage-guided, in-process fuzzer for the [Node.js](https://nodejs.org) platform developed by -[Code Intelligence](https://www.code-intelligence.com). It is based on -[libFuzzer](https://llvm.org/docs/LibFuzzer.html) and brings many of its +[Code Intelligence](https://www.code-intelligence.com). It supports +[libFuzzer](https://llvm.org/docs/LibFuzzer.html) and +[LibAFL](https://github.com/AFLplusplus/LibAFL) backends and brings instrumentation-powered mutation features to the JavaScript ecosystem. ## Quickstart @@ -47,6 +48,9 @@ To use Jazzer.js in your own project follow these few simple steps: npx jazzer FuzzTarget ``` + To run with the LibAFL backend instead of the default libFuzzer backend, add + `--engine=afl`. + 4. Enjoy fuzzing! ## Usage diff --git a/docs/fuzz-settings.md b/docs/fuzz-settings.md index 43f3489a3..cef462ef0 100644 --- a/docs/fuzz-settings.md +++ b/docs/fuzz-settings.md @@ -589,13 +589,52 @@ JAZZER_FUZZ_ENTRY_POINT=buzz npx jazzer my-fuzz-file _Note:_ In Jest mode, this option cannot be set via environment variable. Instead use the native Jest flag `--testNamePattern` as described above. +### `engine` : [string] + +Default: "libfuzzer" + +Select the native fuzzing backend. + +- `libfuzzer`: use the existing libFuzzer backend. +- `afl` (alias for `libafl`): use the LibAFL backend. + +**CLI:** Select the backend with `--engine`, for example: + +```bash +npx jazzer my-fuzz-file --engine=afl +``` + +**Jest:** Set it in `.jazzerjsrc.json`: + +```json +{ + "engine": "afl" +} +``` + +_Note:_ The LibAFL backend currently supports _fuzzing_ mode only. + ### `fuzzerOptions` : [array\] Default: [] -Pass options to native fuzzing engine (Jazzer.js uses libFuzzer). +Pass options to the selected native fuzzing engine. + +For `engine=libfuzzer`, Jazzer.js supports the full libFuzzer-style argument +list. + +For `engine=afl`/`engine=libafl`, Jazzer.js currently supports these options: + +- `-runs=` +- `-seed=` +- `-max_len=` +- `-max_total_time=` +- `-artifact_prefix=` +- non-flag entries interpreted as corpus directories + +Unsupported engine-specific flags are rejected with an explicit error. -For a list of available options, see the +For the `libfuzzer` backend, see the [libFuzzer documentation](https://llvm.org/docs/LibFuzzer.html#options). To get a quick overview of all available options, call Jazzer.js with the libFuzzer argument `-help`. Here is an example for the CLI mode: diff --git a/packages/fuzzer/README.md b/packages/fuzzer/README.md index 9333484c9..ce8ef55a1 100644 --- a/packages/fuzzer/README.md +++ b/packages/fuzzer/README.md @@ -1,16 +1,17 @@ # @jazzer.js/fuzzer -This module provides a native Node.js addon which loads libfuzzer into Node.js. -Users can install it with `npm install`, which tries to download a prebuilt -shared object from GitHub but falls back to compilation on the user's machine if -there is no suitable binary. - -Loading the addon initializes libFuzzer and the sanitizer runtime. Users can -then start the fuzzer with the exported `startFuzzing` or `startFuzzingAsync` -functions; see [the test](fuzzer.test.ts) for an example. In sync mode -(`--sync`), the fuzzer runs on the main thread and blocks the event loop. In the -default async mode, libFuzzer runs on a separate native thread and communicates -with the JS event loop via a thread-safe function. +This module provides a native Node.js addon that hosts Jazzer.js fuzzing +backends inside Node.js. Users can install it with `npm install`, which tries to +download a prebuilt shared object from GitHub but falls back to compilation on +the user's machine if there is no suitable binary. + +Loading the addon initializes the sanitizer runtime and fuzzing hooks. Users can +start the libFuzzer backend with `startFuzzing` or `startFuzzingAsync`, and the +LibAFL backend with `startLibAfl` or `startLibAflAsync`; see +[the tests](fuzzer.test.ts) for examples. In sync mode (`--sync`), the fuzzer +runs on the main thread and blocks the event loop. In the default async mode, +the native backend runs on a separate thread and communicates with the JS event +loop via a thread-safe function. ## Development diff --git a/packages/fuzzer/package.json b/packages/fuzzer/package.json index 3ab0f7ee6..1eabb6ff2 100644 --- a/packages/fuzzer/package.json +++ b/packages/fuzzer/package.json @@ -1,7 +1,7 @@ { "name": "@jazzer.js/fuzzer", "version": "4.0.0", - "description": "Jazzer.js libfuzzer-based fuzzer for Node.js", + "description": "Jazzer.js native fuzzing backends for Node.js", "homepage": "https://github.com/CodeIntelligenceTesting/jazzer.js#readme", "author": "Code Intelligence", "license": "Apache-2.0", @@ -18,7 +18,7 @@ "scripts": { "prebuild": "cmake-js build --out build", "build": "node ../../scripts/build-fuzzer.js", - "benchmark:libafl-spike": "node spike/benchmark.js", + "benchmark:libafl": "node spike/benchmark.js", "format:fix": "clang-format -i *.cpp shared/*.cpp shared/*.h", "lint": "find . -path ./build -prune -type f -o -iname '*.h' -o -iname '*.cpp' | xargs clang-tidy" }, From 53d199c3f264738d507e6ef9cf5c29e7d8e527e1 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 01:04:49 +0200 Subject: [PATCH 08/30] test(engine): cover LibAFL backend behavior in integration suites Add a dedicated tests/engine package and extend the fuzz test harness with engine selection so CLI and Jest flows can run with --engine=afl. This locks in unsupported-option validation and timeout artifact behavior for both sync and async hangs under the LibAFL backend. --- tests/engine/engine.test.js | 138 +++++++++++++++++++++++ tests/engine/fuzz.js | 33 ++++++ tests/engine/jest_project/.gitignore | 3 + tests/engine/jest_project/jest.config.js | 22 ++++ tests/engine/jest_project/jest.fuzz.js | 31 +++++ tests/engine/package.json | 20 ++++ tests/helpers.js | 13 +++ 7 files changed, 260 insertions(+) create mode 100644 tests/engine/engine.test.js create mode 100644 tests/engine/fuzz.js create mode 100644 tests/engine/jest_project/.gitignore create mode 100644 tests/engine/jest_project/jest.config.js create mode 100644 tests/engine/jest_project/jest.fuzz.js create mode 100644 tests/engine/package.json diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js new file mode 100644 index 000000000..170ac966e --- /dev/null +++ b/tests/engine/engine.test.js @@ -0,0 +1,138 @@ +/* + * Copyright 2026 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. + */ + +const path = require("path"); + +const { + cleanCrashFilesIn, + FuzzingExitCode, + FuzzTestBuilder, + JestRegressionExitCode, + TimeoutExitCode, +} = require("../helpers.js"); + +describe("Engine selection", () => { + const testDirectory = __dirname; + const jestProjectDirectory = path.join(testDirectory, "jest_project"); + + beforeEach(async () => { + await cleanCrashFilesIn(testDirectory); + await cleanCrashFilesIn(jestProjectDirectory); + }); + + describe("CLI fuzzing", () => { + it("runs with the LibAFL backend", () => { + const fuzzTest = new FuzzTestBuilder() + .dir(testDirectory) + .fuzzEntryPoint("fuzz") + .disableBugDetectors([".*"]) + .engine("afl") + .runs(250) + .seed(1337) + .build() + .execute(); + + expect(fuzzTest.stderr).not.toContain("Unknown fuzzing engine"); + }); + + it("rejects unsupported libFuzzer options in LibAFL mode", () => { + const fuzzTest = new FuzzTestBuilder() + .dir(testDirectory) + .fuzzEntryPoint("fuzz") + .disableBugDetectors([".*"]) + .engine("afl") + .forkMode(1) + .runs(1) + .build(); + + expect(() => fuzzTest.execute()).toThrow(FuzzingExitCode); + }); + + it("fails fast on asynchronous hangs in LibAFL mode", async () => { + const fuzzTest = new FuzzTestBuilder() + .dir(testDirectory) + .fuzzEntryPoint("timeout_async") + .disableBugDetectors([".*"]) + .engine("afl") + .runs(1) + .timeout(200) + .build(); + + expect(() => fuzzTest.execute()).toThrow(TimeoutExitCode); + expect(fuzzTest.stderr).toContain("Exceeded timeout"); + const crashFiles = await cleanCrashFilesIn(testDirectory); + expect(crashFiles).toHaveLength(1); + expect(crashFiles[0]).toContain("timeout-"); + }); + + it("fails fast on synchronous hangs in LibAFL mode", async () => { + const fuzzTest = new FuzzTestBuilder() + .dir(testDirectory) + .fuzzEntryPoint("timeout_sync") + .disableBugDetectors([".*"]) + .engine("afl") + .sync(true) + .runs(1) + .timeout(200) + .build(); + + expect(() => fuzzTest.execute()).toThrow(TimeoutExitCode); + expect(fuzzTest.stderr).toContain("Exceeded timeout"); + const crashFiles = await cleanCrashFilesIn(testDirectory); + expect(crashFiles).toHaveLength(1); + expect(crashFiles[0]).toContain("timeout-"); + }); + }); + + describe("Jest integration", () => { + it("runs fuzzing mode with the LibAFL backend", async () => { + const fuzzTest = new FuzzTestBuilder() + .dir(jestProjectDirectory) + .disableBugDetectors([".*"]) + .engine("afl") + .jestRunInFuzzingMode(true) + .jestTestFile("jest.fuzz.js") + .jestTestName("afl engine smoke finding") + .runs(500) + .build(); + + expect(() => fuzzTest.execute()).toThrow(JestRegressionExitCode); + expect(fuzzTest.stdout + fuzzTest.stderr).toContain( + "AFL engine smoke finding", + ); + await cleanCrashFilesIn(jestProjectDirectory); + }); + + it("surfaces timeout failures in Jest fuzzing mode", async () => { + const fuzzTest = new FuzzTestBuilder() + .dir(jestProjectDirectory) + .disableBugDetectors([".*"]) + .engine("afl") + .jestRunInFuzzingMode(true) + .jestTestFile("jest.fuzz.js") + .jestTestName("afl engine timeout finding") + .timeout(200) + .runs(1) + .build(); + + expect(() => fuzzTest.execute()).toThrow(TimeoutExitCode); + expect(fuzzTest.stderr).toContain("Exceeded timeout"); + const crashFiles = await cleanCrashFilesIn(jestProjectDirectory); + expect(crashFiles).toHaveLength(1); + expect(crashFiles[0]).toContain("timeout-"); + }); + }); +}); diff --git a/tests/engine/fuzz.js b/tests/engine/fuzz.js new file mode 100644 index 000000000..63aab383e --- /dev/null +++ b/tests/engine/fuzz.js @@ -0,0 +1,33 @@ +/* + * Copyright 2026 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. + */ + +module.exports.fuzz = function (data) { + if (data.length > 1024 * 1024) { + throw new Error("Unexpectedly large input"); + } +}; + +module.exports.timeout_sync = function (_data) { + while (true) { + // Busy loop on purpose to exercise hard timeout handling. + } +}; + +module.exports.timeout_async = function (_data) { + return new Promise(() => { + // Never resolve on purpose to exercise cooperative timeout handling. + }); +}; diff --git a/tests/engine/jest_project/.gitignore b/tests/engine/jest_project/.gitignore new file mode 100644 index 000000000..ee9b755c4 --- /dev/null +++ b/tests/engine/jest_project/.gitignore @@ -0,0 +1,3 @@ +.jazzerjsrc.json +.cifuzz-corpus +jest.fuzz diff --git a/tests/engine/jest_project/jest.config.js b/tests/engine/jest_project/jest.config.js new file mode 100644 index 000000000..dd3b0bd12 --- /dev/null +++ b/tests/engine/jest_project/jest.config.js @@ -0,0 +1,22 @@ +/* + * Copyright 2026 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. + */ + +module.exports = { + testRunner: "@jazzer.js/jest-runner", + testEnvironment: "node", + testMatch: ["/*.fuzz.js"], + testTimeout: 60000, +}; diff --git a/tests/engine/jest_project/jest.fuzz.js b/tests/engine/jest_project/jest.fuzz.js new file mode 100644 index 000000000..dc560cb1a --- /dev/null +++ b/tests/engine/jest_project/jest.fuzz.js @@ -0,0 +1,31 @@ +/* + * Copyright 2026 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. + */ + +require("@jazzer.js/jest-runner"); + +describe("AFL engine", () => { + it.fuzz("afl engine smoke finding", (data) => { + if (data.length > 0) { + throw new Error("AFL engine smoke finding"); + } + }); + + it.fuzz("afl engine timeout finding", async (_data) => { + await new Promise(() => { + // Never resolve on purpose. + }); + }); +}); diff --git a/tests/engine/package.json b/tests/engine/package.json new file mode 100644 index 000000000..065c4016f --- /dev/null +++ b/tests/engine/package.json @@ -0,0 +1,20 @@ +{ + "name": "jazzerjs-engine-tests", + "version": "1.0.0", + "description": "Engine selection integration tests.", + "scripts": { + "fuzz": "jest" + }, + "devDependencies": { + "@jazzer.js/core": "file:../../packages/core", + "@jazzer.js/jest-runner": "file:../../packages/jest-runner", + "@types/jest": "^29.5.3", + "jest": "^29.6.2", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + }, + "jest": { + "testTimeout": 60000 + } +} diff --git a/tests/helpers.js b/tests/helpers.js index 8ca16ac41..f8e1cf691 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -49,6 +49,7 @@ class FuzzTest { expectedErrors, asJson, timeout, + engine, ) { this.logTestOutput = logTestOutput; this.includes = includes; @@ -74,6 +75,7 @@ class FuzzTest { this.expectedErrors = expectedErrors; this.asJson = asJson; this.timeout = timeout; + this.engine = engine; } // Runs the fuzz test in another process using `spawnSync`. @@ -104,6 +106,7 @@ class FuzzTest { if (this.verbose) options.push("--verbose"); if (this.dryRun !== undefined) options.push("--dry_run=" + this.dryRun); if (this.timeout !== undefined) options.push("--timeout=" + this.timeout); + if (this.engine !== undefined) options.push("--engine=" + this.engine); for (const include of this.includes) { options.push("-i=" + include); } @@ -177,6 +180,9 @@ class FuzzTest { if (this.verbose) { config.verbose = this.verbose; } + if (this.engine !== undefined) { + config.engine = this.engine; + } // Write jest config file even if it exists fs.writeFileSync( @@ -298,6 +304,7 @@ class FuzzTestBuilder { _expectedErrors = []; _asJson = false; _timeout = undefined; + _engine = undefined; /** * @param {boolean} logTestOutput - whether to print the output of the fuzz test to the console. @@ -502,6 +509,11 @@ class FuzzTestBuilder { return this; } + engine(engine) { + this._engine = engine; + return this; + } + build() { if (this._jestTestFile === "" && this._fuzzEntryPoint === "") { throw new Error("fuzzEntryPoint or jestTestFile are not set."); @@ -536,6 +548,7 @@ class FuzzTestBuilder { this._expectedErrors, this._asJson, this._timeout, + this._engine, ); } } From ef85c1791fdcf64650d84087a3318cb78502369e Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 13:15:13 +0200 Subject: [PATCH 09/30] feat(fuzzer): productionize the LibAFL runtime Rename the old spike backend, add regression replay, and surface\nLibAFL-native status output through the Node bridge. Make LibAFL the CLI default, add on-disk corpus handling plus\ndictionary-backed token mutations, and pin legacy tests to\nlibFuzzer where required. --- packages/core/cli.ts | 2 +- packages/core/dictionary.ts | 1 + packages/core/options.test.ts | 65 +- packages/core/options.ts | 33 +- packages/core/utils.test.ts | 12 + packages/core/utils.ts | 7 +- packages/fuzzer/CMakeLists.txt | 12 +- packages/fuzzer/addon.cpp | 5 +- packages/fuzzer/addon.ts | 11 +- .../{libafl_spike.cpp => libafl_runtime.cpp} | 607 +++++++++++++----- .../{libafl_spike.h => libafl_runtime.h} | 17 +- ...l_spike.test.ts => libafl_runtime.test.ts} | 8 +- packages/fuzzer/package.json | 2 +- .../fuzzer/{spike => runtime}/benchmark.js | 8 +- packages/fuzzer/rust/Cargo.lock | 2 +- packages/fuzzer/rust/Cargo.toml | 4 +- packages/fuzzer/rust/src/lib.rs | 327 ++++++++-- packages/fuzzer/tsconfig.json | 2 +- tests/bug-detectors/general.test.js | 4 + tests/code_coverage/coverage.test.js | 10 +- tests/done_callback/package.json | 4 +- tests/engine/engine.test.js | 48 ++ tests/engine/fuzz.js | 6 + tests/fork_mode/package.json | 4 +- tests/helpers.js | 2 +- tests/promise/package.json | 4 +- tests/string_compare/package.json | 4 +- tests/timeout/package.json | 4 +- tests/value_profiling/package.json | 4 +- 29 files changed, 936 insertions(+), 283 deletions(-) rename packages/fuzzer/{libafl_spike.cpp => libafl_runtime.cpp} (53%) rename packages/fuzzer/{libafl_spike.h => libafl_runtime.h} (72%) rename packages/fuzzer/{libafl_spike.test.ts => libafl_runtime.test.ts} (91%) rename packages/fuzzer/{spike => runtime}/benchmark.js (93%) diff --git a/packages/core/cli.ts b/packages/core/cli.ts index b1fff2a86..a471f6662 100644 --- a/packages/core/cli.ts +++ b/packages/core/cli.ts @@ -181,7 +181,7 @@ yargs(process.argv.slice(2)) alias: ["backend"], defaultDescription: `${JSON.stringify(defaultCLIOptions.engine)}`, describe: - "Fuzzing engine backend. Use 'libfuzzer' for the current default backend or 'afl' to run the LibAFL engine.", + "Fuzzing engine backend. Use 'afl' (alias 'libafl') for the default LibAFL backend or 'libfuzzer' to run the libFuzzer backend.", group: "Fuzzer:", type: "string", }) diff --git a/packages/core/dictionary.ts b/packages/core/dictionary.ts index 82e8c1373..1a9bcb326 100644 --- a/packages/core/dictionary.ts +++ b/packages/core/dictionary.ts @@ -37,6 +37,7 @@ export class Dictionary { } function getDictionary(): Dictionary { + globalThis.JazzerJS ??= new Map(); return getOrSetJazzerJsGlobal("dictionary", new Dictionary()); } diff --git a/packages/core/options.test.ts b/packages/core/options.test.ts index b911a1dfa..cecc6ab67 100644 --- a/packages/core/options.test.ts +++ b/packages/core/options.test.ts @@ -14,9 +14,14 @@ * limitations under the License. */ +import fs from "fs"; +import os from "os"; +import path from "path"; + import { buildLibAflOptions, defaultCLIOptions, + defaultJestOptions, fromSnakeCase, fromSnakeCaseWithPrefix, Options, @@ -72,6 +77,14 @@ describe("options", () => { }); describe("merge", () => { + it("uses LibAFL as default CLI engine", () => { + expect(defaultCLIOptions.engine).toBe("libafl"); + }); + + it("keeps libFuzzer as default Jest engine", () => { + expect(defaultJestOptions.engine).toBe("libfuzzer"); + }); + it("New options with lower priorities will not be added", () => { const baseOptions = OptionsManager.attachSource( defaultCLIOptions, @@ -343,6 +356,7 @@ describe("libafl options", () => { ); expect(buildLibAflOptions(manager)).toEqual({ + mode: "fuzzing", runs: 99, seed: 1337, maxLen: 1024, @@ -350,6 +364,7 @@ describe("libafl options", () => { maxTotalTimeSeconds: 42, artifactPrefix: "/tmp/artifacts/", corpusDirectories: ["corpus-main", "corpus-seed"], + dictionaryFiles: [], }); }); @@ -365,16 +380,62 @@ describe("libafl options", () => { expect(() => buildLibAflOptions(manager)).toThrow("not supported"); }); - it("rejects regression mode in LibAFL mode", () => { + it("supports regression mode in LibAFL mode", () => { const manager = new OptionsManager(OptionSource.DefaultCLIOptions).merge( { engine: "libafl", mode: "regression", + fuzzerOptions: ["corpus"], }, OptionSource.CommandLineArguments, ); - expect(() => buildLibAflOptions(manager)).toThrow("fuzzing mode only"); + expect(buildLibAflOptions(manager)).toEqual({ + mode: "regression", + runs: 0, + seed: 0, + maxLen: 4096, + timeoutMillis: 5000, + maxTotalTimeSeconds: 0, + artifactPrefix: "", + corpusDirectories: ["corpus"], + dictionaryFiles: [], + }); + }); + + it("supports dictionary entries in LibAFL mode", () => { + const tempDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), "jazzer-libafl-dict-"), + ); + const dictionaryPath = path.join(tempDirectory, "seed.dict"); + fs.writeFileSync(dictionaryPath, '"Amazing"\n'); + + try { + const manager = new OptionsManager(OptionSource.DefaultCLIOptions) + .merge( + { + engine: "libafl", + fuzzerOptions: ["corpus", `-dict=${dictionaryPath}`], + }, + OptionSource.CommandLineArguments, + ) + .merge( + { dictionaryEntries: ["banana"] }, + OptionSource.JestFuzzTestOptions, + ); + + const built = buildLibAflOptions(manager); + expect(built.corpusDirectories).toEqual(["corpus"]); + expect(built.dictionaryFiles).toHaveLength(1); + expect(fs.readFileSync(built.dictionaryFiles[0], "utf8")).toContain( + "\\x62\\x61\\x6e\\x61\\x6e\\x61", + ); + expect(fs.readFileSync(built.dictionaryFiles[0], "utf8")).toContain( + "Amazing", + ); + } finally { + fs.rmSync(tempDirectory, { force: true, recursive: true }); + } }); }); diff --git a/packages/core/options.ts b/packages/core/options.ts index df96544aa..3e864cbff 100644 --- a/packages/core/options.ts +++ b/packages/core/options.ts @@ -23,6 +23,7 @@ import { useDictionaryByParams } from "./dictionary"; import { replaceAll } from "./utils"; export type LibAflOptions = { + mode: "fuzzing" | "regression"; runs: number; seed: number; maxLen: number; @@ -30,6 +31,7 @@ export type LibAflOptions = { maxTotalTimeSeconds: number; artifactPrefix: string; corpusDirectories: string[]; + dictionaryFiles: string[]; }; /** @@ -106,7 +108,7 @@ const allowedFuzzTestOptions = [ export type AllowedFuzzTestOptions = (typeof allowedFuzzTestOptions)[number]; export const defaultCLIOptions: Options = Object.freeze({ - engine: "libfuzzer", + engine: "libafl", coverage: false, coverageDirectory: "coverage", coverageReporters: ["json", "text", "lcov", "clover"], // default Jest reporters @@ -129,6 +131,7 @@ export const defaultCLIOptions: Options = Object.freeze({ export const defaultJestOptions: Options = Object.freeze({ ...defaultCLIOptions, + engine: "libfuzzer", mode: "regression", }); @@ -470,24 +473,24 @@ export function buildLibFuzzerOptions(options: OptionsManager) { export const buildFuzzerOption = buildLibFuzzerOptions; export function buildLibAflOptions(options: OptionsManager): LibAflOptions { - if (options.get("mode") === "regression") { - throw new Error( - "The 'libafl' engine currently supports fuzzing mode only.", - ); - } - if (options.get("timeout") <= 0) { throw new Error("timeout must be > 0"); } + const normalizedFuzzerOptions = useDictionaryByParams( + options.get("fuzzerOptions"), + options.get("dictionaryEntries"), + ); + let runs = 0; let seed = 0; let maxLen = 4096; let maxTotalTimeSeconds = 0; let artifactPrefix = ""; const corpusDirectories: string[] = []; + const dictionaryFiles: string[] = []; - for (const option of options.get("fuzzerOptions")) { + for (const option of normalizedFuzzerOptions) { if (!option.startsWith("-")) { corpusDirectories.push(option); continue; @@ -516,23 +519,22 @@ export function buildLibAflOptions(options: OptionsManager): LibAflOptions { artifactPrefix = option.substring(17); continue; } + if (option.startsWith("-dict=")) { + dictionaryFiles.splice(0, dictionaryFiles.length, option.substring(6)); + continue; + } throw new Error( `Option '${option}' is not supported by the '${resolveEngine(options.get("engine"))}' engine`, ); } - if (options.get("dictionaryEntries").length > 0) { - throw new Error( - "The 'libafl' engine currently does not support dictionaryEntries; use '-dict=...' in fuzzerOptions after support lands.", - ); - } - printOptions(options); if (process.env.JAZZER_DEBUG) { console.error( `DEBUG: [core] LibAFL options: ${JSON.stringify( { + mode: options.get("mode"), runs, seed, maxLen, @@ -540,6 +542,7 @@ export function buildLibAflOptions(options: OptionsManager): LibAflOptions { timeoutMillis: options.get("timeout"), artifactPrefix, corpusDirectories, + dictionaryFiles, }, null, 2, @@ -548,6 +551,7 @@ export function buildLibAflOptions(options: OptionsManager): LibAflOptions { } return { + mode: options.get("mode"), runs, seed, maxLen, @@ -555,6 +559,7 @@ export function buildLibAflOptions(options: OptionsManager): LibAflOptions { maxTotalTimeSeconds, artifactPrefix, corpusDirectories, + dictionaryFiles, }; } diff --git a/packages/core/utils.test.ts b/packages/core/utils.test.ts index de9fe920e..e17a6d12c 100644 --- a/packages/core/utils.test.ts +++ b/packages/core/utils.test.ts @@ -36,6 +36,18 @@ describe("core", () => { }); }); describe("prepareArgs", () => { + it("does not add an undefined engine", () => { + const args = { + _: ["-some_arg=value"], + corpus: [], + fuzzTarget: "filename.js", + }; + const options = prepareArgs(args); + expect( + Object.prototype.hasOwnProperty.call(options, "engine"), + ).toBeFalsy(); + }); + it("converts fuzzer args to strings", () => { const args = { _: ["-some_arg=value", "-other_arg", 123], diff --git a/packages/core/utils.ts b/packages/core/utils.ts index 20e5ed159..4bca53ab6 100644 --- a/packages/core/utils.ts +++ b/packages/core/utils.ts @@ -67,15 +67,18 @@ export function ensureFilepath(filePath: string): string { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function prepareArgs(args: any) { - const engine = args.engine === "afl" ? "libafl" : args.engine; const options = { ...args, - engine, fuzzTarget: ensureFilepath(args.fuzzTarget), fuzzerOptions: (args.corpus ?? []) .concat(args._) .map((e: unknown) => e + ""), }; + if (options.engine !== undefined) { + options.engine = options.engine === "afl" ? "libafl" : options.engine; + } else { + delete options.engine; + } if (options.fuzzerOptions.length === 0) { delete options.fuzzerOptions; } diff --git a/packages/fuzzer/CMakeLists.txt b/packages/fuzzer/CMakeLists.txt index a312c8c98..9dad180e0 100644 --- a/packages/fuzzer/CMakeLists.txt +++ b/packages/fuzzer/CMakeLists.txt @@ -79,7 +79,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") else() set(RUST_TARGET_TRIPLE "x86_64-unknown-linux-gnu") endif() - set(RUST_STATICLIB_NAME "libjazzerjs_libafl_spike.a") + set(RUST_STATICLIB_NAME "libjazzerjs_libafl_runtime.a") elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") if(CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64") set(RUST_TARGET_TRIPLE "x86_64-apple-darwin") @@ -90,10 +90,10 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") else() set(RUST_TARGET_TRIPLE "x86_64-apple-darwin") endif() - set(RUST_STATICLIB_NAME "libjazzerjs_libafl_spike.a") + set(RUST_STATICLIB_NAME "libjazzerjs_libafl_runtime.a") elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(RUST_TARGET_TRIPLE "x86_64-pc-windows-msvc") - set(RUST_STATICLIB_NAME "jazzerjs_libafl_spike.lib") + set(RUST_STATICLIB_NAME "jazzerjs_libafl_runtime.lib") endif() if(CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -116,10 +116,10 @@ add_custom_command( --target ${RUST_TARGET_TRIPLE} ${CARGO_PROFILE_FLAG} WORKING_DIRECTORY ${RUST_CRATE_DIR} DEPENDS ${RUST_CRATE_DIR}/Cargo.toml ${RUST_CRATE_DIR}/src/lib.rs - COMMENT "Building the LibAFL spike static library") + COMMENT "Building the LibAFL runtime static library") -add_custom_target(jazzerjs_libafl_spike ALL DEPENDS ${RUST_STATICLIB_PATH}) -add_dependencies(${PROJECT_NAME} jazzerjs_libafl_spike) +add_custom_target(jazzerjs_libafl_runtime ALL DEPENDS ${RUST_STATICLIB_PATH}) +add_dependencies(${PROJECT_NAME} jazzerjs_libafl_runtime) target_link_libraries(${PROJECT_NAME} ${RUST_STATICLIB_PATH}) # We're not sure why but sometimes systems don't end up setting LLVM_TARGET_TRIPLE used in llvm's cmake to eventually diff --git a/packages/fuzzer/addon.cpp b/packages/fuzzer/addon.cpp index aee3f94de..e64444ec6 100644 --- a/packages/fuzzer/addon.cpp +++ b/packages/fuzzer/addon.cpp @@ -16,7 +16,7 @@ #include "fuzzing_async.h" #include "fuzzing_sync.h" -#include "libafl_spike.h" +#include "libafl_runtime.h" #include "shared/callbacks.h" #include "shared/libfuzzer.h" @@ -64,9 +64,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) { exports["startFuzzingAsync"] = Napi::Function::New(env); exports["startLibAfl"] = Napi::Function::New(env); exports["startLibAflAsync"] = Napi::Function::New(env); - exports["startLibAflSpike"] = Napi::Function::New(env); - exports["startLibAflSpikeAsync"] = - Napi::Function::New(env); RegisterCallbackExports(env, exports); return exports; diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index ac500c373..c2d9624cf 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -28,6 +28,7 @@ export type FuzzTarget = FuzzTargetAsyncOrValue | FuzzTargetCallback; export type FuzzOpts = string[]; export type LibAflOptions = { + mode: "fuzzing" | "regression"; runs: number; seed: number; maxLen: number; @@ -35,6 +36,7 @@ export type LibAflOptions = { maxTotalTimeSeconds: number; artifactPrefix: string; corpusDirectories: string[]; + dictionaryFiles: string[]; }; export type StartFuzzingSyncFn = ( @@ -88,8 +90,6 @@ type NativeAddon = { startFuzzingAsync: StartFuzzingAsyncFn; startLibAfl?: StartLibAflSyncFn; startLibAflAsync?: StartLibAflAsyncFn; - startLibAflSpike?: StartLibAflSyncFn; - startLibAflSpikeAsync?: StartLibAflAsyncFn; clearCompareFeedbackMap: () => void; countNonZeroCompareFeedbackSlots: () => number; }; @@ -113,13 +113,6 @@ function addonFilename(): string { const loadedAddon = require(addonFilename()) as NativeAddon; -if (!loadedAddon.startLibAfl && loadedAddon.startLibAflSpike) { - loadedAddon.startLibAfl = loadedAddon.startLibAflSpike; -} -if (!loadedAddon.startLibAflAsync && loadedAddon.startLibAflSpikeAsync) { - loadedAddon.startLibAflAsync = loadedAddon.startLibAflSpikeAsync; -} - if (!loadedAddon.startLibAfl || !loadedAddon.startLibAflAsync) { throw new Error( "The native addon does not export startLibAfl/startLibAflAsync", diff --git a/packages/fuzzer/libafl_spike.cpp b/packages/fuzzer/libafl_runtime.cpp similarity index 53% rename from packages/fuzzer/libafl_spike.cpp rename to packages/fuzzer/libafl_runtime.cpp index 797364ddc..f4718bcca 100644 --- a/packages/fuzzer/libafl_spike.cpp +++ b/packages/fuzzer/libafl_runtime.cpp @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "libafl_spike.h" +#include "libafl_runtime.h" +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +32,7 @@ #include #include #include +#include #include #include @@ -53,13 +56,19 @@ constexpr int kExecutionStop = 2; constexpr int kExecutionFatal = 3; constexpr int kExecutionTimeout = 4; -constexpr int kSpikeOk = 0; -constexpr int kSpikeFoundFinding = 1; -constexpr int kSpikeStopped = 2; -constexpr int kSpikeFatal = 3; -constexpr int kSpikeFoundTimeout = 4; +constexpr int kRuntimeOk = 0; +constexpr int kRuntimeFoundFinding = 1; +constexpr int kRuntimeStopped = 2; +constexpr int kRuntimeFatal = 3; +constexpr int kRuntimeFoundTimeout = 4; -struct ParsedSpikeOptions { +struct ParsedRuntimeOptions { + enum class Mode { + kFuzzing, + kRegression, + }; + + Mode mode = Mode::kFuzzing; uint64_t runs = 0; uint64_t seed = 1; size_t max_len = 4096; @@ -67,8 +76,70 @@ struct ParsedSpikeOptions { uint64_t max_total_time_seconds = 0; std::string artifact_prefix; std::vector corpus_directories; + std::vector dictionary_files; }; +std::string FormatDuration(std::chrono::steady_clock::duration duration) { + const auto total_seconds = + std::chrono::duration_cast(duration).count(); + const auto hours = total_seconds / 3600; + const auto minutes = (total_seconds % 3600) / 60; + const auto seconds = total_seconds % 60; + + std::ostringstream stream; + if (hours > 0) { + stream << hours << "h " << minutes << "m " << seconds << "s"; + } else if (minutes > 0) { + stream << minutes << "m " << seconds << "s"; + } else { + stream << seconds << "s"; + } + return stream.str(); +} + +std::string FormatRunLimit(uint64_t runs) { + if (runs == 0) { + return "unlimited"; + } + + return std::to_string(runs); +} + +std::string FormatTotalTimeLimit(uint64_t max_total_time_seconds) { + if (max_total_time_seconds == 0) { + return "unlimited"; + } + + return FormatDuration(std::chrono::seconds(max_total_time_seconds)); +} + +void PrintRegressionStart(const ParsedRuntimeOptions &options, + size_t replay_inputs) { + std::cerr << "[libafl::start] mode: regression, seed: " << options.seed + << ", replay_inputs: " << replay_inputs + << ", timeout: " << options.timeout_millis + << " ms, max_len: " << options.max_len + << ", runs: " << FormatRunLimit(options.runs) + << ", max_total_time: " + << FormatTotalTimeLimit(options.max_total_time_seconds) + << std::endl; +} + +void PrintRegressionDone(std::chrono::steady_clock::time_point started_at, + uint64_t executions, size_t replay_inputs) { + const auto elapsed = std::chrono::steady_clock::now() - started_at; + const auto elapsed_seconds = std::chrono::duration(elapsed).count(); + const auto execs_per_sec = elapsed_seconds > 0.0 + ? executions / elapsed_seconds + : static_cast(executions); + + std::cerr << "[libafl::done] mode: regression, run time: " + << FormatDuration(elapsed) << ", replay_inputs: " << replay_inputs + << ", executions: " << executions + << ", exec/sec: " << static_cast(execs_per_sec) + << std::endl; +} + struct SyncWatchdogState { std::thread thread; std::mutex mutex; @@ -82,7 +153,7 @@ struct SyncWatchdogState { struct SyncFuzzTargetContext { SyncFuzzTargetContext(Napi::Env env, Napi::Function target, Napi::Function js_stop_callback, - ParsedSpikeOptions options) + ParsedRuntimeOptions options) : env(env), target(target), is_resolved(false), deferred(Napi::Promise::Deferred::New(env)), js_stop_callback(js_stop_callback), options(std::move(options)) {} @@ -92,7 +163,7 @@ struct SyncFuzzTargetContext { bool is_resolved; Napi::Promise::Deferred deferred; Napi::Function js_stop_callback; - ParsedSpikeOptions options; + ParsedRuntimeOptions options; SyncWatchdogState watchdog; volatile std::sig_atomic_t signal_status = 0; volatile int sigints = 0; @@ -112,27 +183,26 @@ struct AsyncDataType { }; struct AsyncFuzzTargetContext { - explicit AsyncFuzzTargetContext(Napi::Env env, ParsedSpikeOptions options) - : deferred(Napi::Promise::Deferred::New(env)), options(std::move(options)) { - } + explicit AsyncFuzzTargetContext(Napi::Env env, ParsedRuntimeOptions options) + : deferred(Napi::Promise::Deferred::New(env)), + options(std::move(options)) {} std::thread native_thread; Napi::Promise::Deferred deferred; - ParsedSpikeOptions options; + ParsedRuntimeOptions options; bool is_resolved = false; bool is_done_called = false; - int run_status = kSpikeOk; + int run_status = kRuntimeOk; volatile int sigints = 0; std::jmp_buf execution_context; }; using AsyncFinalizerDataType = void; void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, - AsyncFuzzTargetContext *context, - AsyncDataType *data); -using AsyncTsfn = Napi::TypedThreadSafeFunction; + AsyncFuzzTargetContext *context, AsyncDataType *data); +using AsyncTsfn = + Napi::TypedThreadSafeFunction; SyncFuzzTargetContext *gActiveSyncContext = nullptr; AsyncFuzzTargetContext *gActiveAsyncContext = nullptr; @@ -175,9 +245,8 @@ std::filesystem::path ArtifactPath(const std::string &artifact_prefix, const auto has_directory_semantics = artifact_prefix.back() == '/' || artifact_prefix.back() == '\\'; std::filesystem::path prefix_path(artifact_prefix); - if (has_directory_semantics || - (std::filesystem::exists(prefix_path) && - std::filesystem::is_directory(prefix_path))) { + if (has_directory_semantics || (std::filesystem::exists(prefix_path) && + std::filesystem::is_directory(prefix_path))) { return prefix_path / filename; } @@ -186,7 +255,7 @@ std::filesystem::path ArtifactPath(const std::string &artifact_prefix, void WriteArtifact(const std::string &artifact_prefix, const std::string &kind, const uint8_t *data, size_t size) { - if (data == nullptr) { + if (data == nullptr && size != 0) { return; } @@ -229,9 +298,7 @@ void WriteArtifact(const std::string &artifact_prefix, const std::string &kind, const std::vector &input) { std::cerr << "ERROR: Exceeded timeout of " << timeout_millis << " ms for one fuzz target execution." << std::endl; - if (!input.empty()) { - WriteArtifact(artifact_prefix, "timeout", input.data(), input.size()); - } + WriteArtifact(artifact_prefix, "timeout", input.data(), input.size()); _Exit(libfuzzer::EXIT_ERROR_TIMEOUT); } @@ -269,18 +336,17 @@ void ReportAsyncFinding(AsyncFuzzTargetContext *context, Napi::Env env, const Napi::Value &error, const std::vector &input) { if (TrySetExecutionStatus(state, kExecutionFinding)) { - if (!input.empty()) { - WriteArtifact(context->options.artifact_prefix, "crash", input.data(), - input.size()); - } + WriteArtifact(context->options.artifact_prefix, "crash", input.data(), + input.size()); } RejectDeferredIfNeeded(context, error); } -ParsedSpikeOptions ParseSpikeOptions(Napi::Env env, - const Napi::Object &js_opts) { - ParsedSpikeOptions parsed; +ParsedRuntimeOptions ParseRuntimeOptions(Napi::Env env, + const Napi::Object &js_opts) { + ParsedRuntimeOptions parsed; + const auto mode = js_opts.Get("mode"); const auto runs = js_opts.Get("runs"); const auto seed = js_opts.Get("seed"); const auto max_len = js_opts.Get("maxLen"); @@ -288,21 +354,42 @@ ParsedSpikeOptions ParseSpikeOptions(Napi::Env env, const auto max_total_time_seconds = js_opts.Get("maxTotalTimeSeconds"); const auto artifact_prefix = js_opts.Get("artifactPrefix"); const auto corpus_directories = js_opts.Get("corpusDirectories"); + const auto dictionary_files = js_opts.Get("dictionaryFiles"); + + if (!mode.IsUndefined() && !mode.IsString()) { + throw Napi::Error::New( + env, "The LibAFL options object expects mode to be 'fuzzing' or " + "'regression'"); + } if (!runs.IsNumber() || !seed.IsNumber() || !max_len.IsNumber() || !timeout_millis.IsNumber() || !max_total_time_seconds.IsNumber() || - !artifact_prefix.IsString() || !corpus_directories.IsArray()) { + !artifact_prefix.IsString() || !corpus_directories.IsArray() || + !dictionary_files.IsArray()) { throw Napi::Error::New( - env, - "The LibAFL backend expects an options object with runs, seed, " - "maxLen, timeoutMillis, maxTotalTimeSeconds, artifactPrefix, and " - "corpusDirectories"); + env, "The LibAFL backend expects an options object with mode, runs, " + "seed, maxLen, timeoutMillis, maxTotalTimeSeconds, " + "artifactPrefix, corpusDirectories, and dictionaryFiles"); + } + + if (mode.IsString()) { + const auto mode_value = mode.As().Utf8Value(); + if (mode_value == "regression") { + parsed.mode = ParsedRuntimeOptions::Mode::kRegression; + } else if (mode_value == "fuzzing") { + parsed.mode = ParsedRuntimeOptions::Mode::kFuzzing; + } else { + throw Napi::Error::New( + env, "The LibAFL options object expects mode to be 'fuzzing' or " + "'regression'"); + } } const auto runs_value = runs.As().Int64Value(); const auto seed_value = seed.As().Int64Value(); const auto max_len_value = max_len.As().Int64Value(); - const auto timeout_millis_value = timeout_millis.As().Int64Value(); + const auto timeout_millis_value = + timeout_millis.As().Int64Value(); const auto max_total_time_seconds_value = max_total_time_seconds.As().Int64Value(); @@ -330,9 +417,18 @@ ParsedSpikeOptions ParseSpikeOptions(Napi::Env env, parsed.corpus_directories.push_back(dir.As().Utf8Value()); } + const auto dicts = dictionary_files.As(); + for (uint32_t i = 0; i < dicts.Length(); ++i) { + auto dict = dicts.Get(i); + if (!dict.IsString()) { + throw Napi::Error::New(env, + "LibAFL dictionaryFiles entries must be strings"); + } + parsed.dictionary_files.push_back(dict.As().Utf8Value()); + } + if (parsed.max_len == 0) { - throw Napi::Error::New(env, - "The LibAFL backend requires maxLen to be > 0"); + throw Napi::Error::New(env, "The LibAFL backend requires maxLen to be > 0"); } if (parsed.timeout_millis == 0) { throw Napi::Error::New( @@ -342,7 +438,7 @@ ParsedSpikeOptions ParseSpikeOptions(Napi::Env env, return parsed; } -JazzerLibAflSharedMaps SharedMapsForSpike(Napi::Env env) { +JazzerLibAflRuntimeSharedMaps SharedMapsForRuntime(Napi::Env env) { auto *edges = CoverageCounters(); const auto edges_len = CoverageCountersSize(); auto *cmp = CompareFeedbackMap(); @@ -357,6 +453,188 @@ JazzerLibAflSharedMaps SharedMapsForSpike(Napi::Env env) { return {edges, edges_len, cmp, cmp_len}; } +bool CollectRegressionCorpusFiles( + const std::vector &corpus_directories, + std::vector *files) { + for (const auto &directory : corpus_directories) { + const std::filesystem::path directory_path(directory); + std::error_code error; + + if (!std::filesystem::exists(directory_path, error)) { + if (error) { + std::cerr << "[libafl] fatal: failed to access corpus directory '" + << directory_path.string() << "': " << error.message() + << std::endl; + } else { + std::cerr << "[libafl] fatal: corpus directory does not exist: '" + << directory_path.string() << "'" << std::endl; + } + return false; + } + + if (!std::filesystem::is_directory(directory_path, error)) { + if (error) { + std::cerr << "[libafl] fatal: failed to inspect corpus directory '" + << directory_path.string() << "': " << error.message() + << std::endl; + } else { + std::cerr << "[libafl] fatal: corpus path is not a directory: '" + << directory_path.string() << "'" << std::endl; + } + return false; + } + + std::filesystem::recursive_directory_iterator iterator( + directory_path, + std::filesystem::directory_options::skip_permission_denied, error); + const auto end = std::filesystem::recursive_directory_iterator(); + if (error) { + std::cerr << "[libafl] fatal: failed to iterate corpus directory '" + << directory_path.string() << "': " << error.message() + << std::endl; + return false; + } + + for (; iterator != end; iterator.increment(error)) { + if (error) { + std::cerr << "[libafl] fatal: failed to iterate corpus directory '" + << directory_path.string() << "': " << error.message() + << std::endl; + return false; + } + + const auto is_regular_file = iterator->is_regular_file(error); + if (error) { + std::cerr << "[libafl] fatal: failed to inspect corpus entry '" + << iterator->path().string() << "': " << error.message() + << std::endl; + return false; + } + if (is_regular_file) { + files->push_back(iterator->path()); + } + } + } + + std::sort(files->begin(), files->end()); + return true; +} + +bool ReadRegressionInput(const std::filesystem::path &file_path, size_t max_len, + std::vector *input) { + input->clear(); + std::ifstream stream(file_path, std::ios::binary); + if (!stream.is_open()) { + std::cerr << "[libafl] fatal: failed to open corpus input '" + << file_path.string() << "'" << std::endl; + return false; + } + + constexpr size_t kChunkSize = 4096; + std::array buffer{}; + while (stream.good() && input->size() < max_len) { + const auto remaining = max_len - input->size(); + const auto to_read = static_cast( + std::min(remaining, buffer.size())); + stream.read(buffer.data(), to_read); + const auto bytes_read = stream.gcount(); + if (bytes_read <= 0) { + break; + } + input->insert(input->end(), buffer.begin(), buffer.begin() + bytes_read); + } + + if (stream.bad()) { + std::cerr << "[libafl] fatal: failed to read corpus input '" + << file_path.string() << "'" << std::endl; + return false; + } + + return true; +} + +bool ReachedMaxTotalTime(const ParsedRuntimeOptions &options, + std::chrono::steady_clock::time_point started_at) { + if (options.max_total_time_seconds == 0) { + return false; + } + return std::chrono::steady_clock::now() - started_at >= + std::chrono::seconds(options.max_total_time_seconds); +} + +int ReplayRegressionInputs( + const ParsedRuntimeOptions &options, + const std::function &execute_one) { + std::vector corpus_files; + if (!CollectRegressionCorpusFiles(options.corpus_directories, + &corpus_files)) { + return kRuntimeFatal; + } + + const auto started_at = std::chrono::steady_clock::now(); + const auto replay_inputs = corpus_files.size() + 1; + uint64_t executions = 0; + static constexpr uint8_t kEmptyInputByte = 0; + std::vector current_input; + + PrintRegressionStart(options, replay_inputs); + + auto execute_input = [&](const uint8_t *data, size_t size) -> int { + if (options.runs != 0 && executions >= options.runs) { + return kRuntimeOk; + } + if (ReachedMaxTotalTime(options, started_at)) { + return kRuntimeStopped; + } + + const auto status = execute_one(data, size); + executions++; + switch (status) { + case kExecutionContinue: + return kRuntimeOk; + case kExecutionFinding: + return kRuntimeFoundFinding; + case kExecutionStop: + return kRuntimeStopped; + case kExecutionFatal: + return kRuntimeFatal; + case kExecutionTimeout: + return kRuntimeFoundTimeout; + default: + std::cerr << "[libafl] fatal: unknown execution status: " << status + << std::endl; + return kRuntimeFatal; + } + }; + + auto status = execute_input(&kEmptyInputByte, 0); + if (status != kRuntimeOk) { + if (status == kRuntimeStopped) { + PrintRegressionDone(started_at, executions, replay_inputs); + } + return status; + } + + for (const auto &file_path : corpus_files) { + if (!ReadRegressionInput(file_path, options.max_len, ¤t_input)) { + return kRuntimeFatal; + } + + const auto *data = + current_input.empty() ? &kEmptyInputByte : current_input.data(); + status = execute_input(data, current_input.size()); + if (status != kRuntimeOk) { + if (status == kRuntimeStopped) { + PrintRegressionDone(started_at, executions, replay_inputs); + } + return status; + } + } + + PrintRegressionDone(started_at, executions, replay_inputs); + return kRuntimeOk; +} + void StartSyncWatchdog(SyncFuzzTargetContext *context) { if (context->options.timeout_millis == 0) { return; @@ -366,16 +644,17 @@ void StartSyncWatchdog(SyncFuzzTargetContext *context) { auto &watchdog = context->watchdog; std::unique_lock lock(watchdog.mutex); while (true) { - watchdog.cv.wait( - lock, [&watchdog] { return watchdog.should_stop || watchdog.execution_armed; }); + watchdog.cv.wait(lock, [&watchdog] { + return watchdog.should_stop || watchdog.execution_armed; + }); if (watchdog.should_stop) { return; } const auto deadline = watchdog.deadline; - const auto resumed = watchdog.cv.wait_until( - lock, deadline, [&watchdog, deadline] { + const auto resumed = + watchdog.cv.wait_until(lock, deadline, [&watchdog, deadline] { return watchdog.should_stop || !watchdog.execution_armed || watchdog.deadline != deadline; }); @@ -516,8 +795,7 @@ int ExecuteSyncInput(void *user_data, const uint8_t *data, size_t size) { } void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, - AsyncFuzzTargetContext *context, - AsyncDataType *input) { + AsyncFuzzTargetContext *context, AsyncDataType *input) { auto state = input->state; const auto current_input = input->data; @@ -541,8 +819,8 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, return; } - auto buffer = - Napi::Buffer::Copy(env, current_input.data(), current_input.size()); + auto buffer = Napi::Buffer::Copy(env, current_input.data(), + current_input.size()); auto parameter_count = js_fuzz_callback.As() .Get("length") .As() @@ -550,42 +828,41 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, if (parameter_count > 1) { context->is_done_called = false; - auto done = Napi::Function::New( - env, [=](const Napi::CallbackInfo &info) { - if (context->is_resolved) { - return; - } - - if (context->is_done_called) { - auto error = Napi::Error::New( - env, - "Expected done to be called once, but it was called multiple times.") - .Value(); - ReportAsyncFinding(context, env, state, error, current_input); - return; - } - - context->is_done_called = true; - const auto has_error = info.Length() > 0 && - !(info[0].IsNull() || info[0].IsUndefined()); - if (has_error) { - auto error = info[0]; - if (!error.IsObject()) { - error = Napi::Error::New(env, error.ToString()).Value(); - } - ReportAsyncFinding(context, env, state, error, current_input); - } else { - TrySetExecutionStatus(state, kExecutionContinue); - } - }); + auto done = Napi::Function::New(env, [=](const Napi::CallbackInfo &info) { + if (context->is_resolved) { + return; + } + + if (context->is_done_called) { + auto error = + Napi::Error::New(env, "Expected done to be called once, but it " + "was called multiple times.") + .Value(); + ReportAsyncFinding(context, env, state, error, current_input); + return; + } + + context->is_done_called = true; + const auto has_error = + info.Length() > 0 && !(info[0].IsNull() || info[0].IsUndefined()); + if (has_error) { + auto error = info[0]; + if (!error.IsObject()) { + error = Napi::Error::New(env, error.ToString()).Value(); + } + ReportAsyncFinding(context, env, state, error, current_input); + } else { + TrySetExecutionStatus(state, kExecutionContinue); + } + }); auto result = js_fuzz_callback.Call({buffer, done}); if (result.IsPromise()) { AsyncReturnsHandler(); - auto error = Napi::Error::New( - env, - "Internal fuzzer error - Either async or done callback based fuzz tests allowed.") - .Value(); + auto error = + Napi::Error::New(env, "Internal fuzzer error - Either async or " + "done callback based fuzz tests allowed.") + .Value(); ReportAsyncFinding(context, env, state, error, current_input); } else { SyncReturnsHandler(); @@ -598,20 +875,24 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, AsyncReturnsHandler(); auto js_promise = result.As(); auto then = js_promise.Get("then").As(); - then.Call( - js_promise, - {Napi::Function::New(env, [=](const Napi::CallbackInfo &) { - TrySetExecutionStatus(state, kExecutionContinue); - }), - Napi::Function::New(env, [=](const Napi::CallbackInfo &info) { - auto error = info.Length() > 0 ? info[0] - : Napi::Error::New(env, "Unknown promise rejection") - .Value(); - if (!error.IsObject()) { - error = Napi::Error::New(env, error.ToString()).Value(); - } - ReportAsyncFinding(context, env, state, error, current_input); - })}); + then.Call(js_promise, + {Napi::Function::New(env, + [=](const Napi::CallbackInfo &) { + TrySetExecutionStatus( + state, kExecutionContinue); + }), + Napi::Function::New(env, [=](const Napi::CallbackInfo &info) { + auto error = + info.Length() > 0 + ? info[0] + : Napi::Error::New(env, "Unknown promise rejection") + .Value(); + if (!error.IsObject()) { + error = Napi::Error::New(env, error.ToString()).Value(); + } + ReportAsyncFinding(context, env, state, error, + current_input); + })}); } else { SyncReturnsHandler(); TrySetExecutionStatus(state, kExecutionContinue); @@ -620,7 +901,8 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback, ReportAsyncFinding(context, env, state, error.Value(), current_input); } catch (const std::exception &exception) { TrySetExecutionStatus(state, kExecutionFatal); - auto message = std::string("Internal fuzzer error - ").append(exception.what()); + auto message = + std::string("Internal fuzzer error - ").append(exception.what()); RejectDeferredIfNeeded(context, Napi::Error::New(env, message).Value()); } } @@ -676,7 +958,7 @@ int ExecuteAsyncInput(void *user_data, const uint8_t *data, size_t size) { } } // namespace -Napi::Value StartLibAflSpike(const Napi::CallbackInfo &info) { +Napi::Value StartLibAfl(const Napi::CallbackInfo &info) { if (info.Length() != 3 || !info[0].IsFunction() || !info[1].IsObject() || !info[2].IsFunction()) { throw Napi::Error::New( @@ -685,8 +967,8 @@ Napi::Value StartLibAflSpike(const Napi::CallbackInfo &info) { "LibAFL options object, and a stop callback"); } - auto options = ParseSpikeOptions(info.Env(), info[1].As()); - auto maps = SharedMapsForSpike(info.Env()); + auto options = ParseRuntimeOptions(info.Env(), info[1].As()); + auto maps = SharedMapsForRuntime(info.Env()); SyncFuzzTargetContext context(info.Env(), info[0].As(), info[2].As(), @@ -697,43 +979,60 @@ Napi::Value StartLibAflSpike(const Napi::CallbackInfo &info) { signal(SIGINT, SyncSigintHandler); signal(SIGSEGV, SyncErrorSignalHandler); - std::vector corpus_directories; - corpus_directories.reserve(options.corpus_directories.size()); - for (const auto &directory : options.corpus_directories) { - corpus_directories.push_back(directory.c_str()); - } + auto status = kRuntimeOk; + if (context.options.mode == ParsedRuntimeOptions::Mode::kRegression) { + status = ReplayRegressionInputs( + context.options, [&context](const uint8_t *data, size_t size) { + return ExecuteSyncInput(&context, data, size); + }); + } else { + std::vector corpus_directories; + corpus_directories.reserve(context.options.corpus_directories.size()); + for (const auto &directory : context.options.corpus_directories) { + corpus_directories.push_back(directory.c_str()); + } + std::vector dictionary_files; + dictionary_files.reserve(context.options.dictionary_files.size()); + for (const auto &dictionary : context.options.dictionary_files) { + dictionary_files.push_back(dictionary.c_str()); + } - JazzerLibAflSpikeOptions spike_options{ - options.runs, - options.seed, - options.max_len, - options.max_total_time_seconds, - corpus_directories.empty() ? nullptr : corpus_directories.data(), - corpus_directories.size(), - }; - auto status = - jazzer_libafl_spike_run(&spike_options, &maps, ExecuteSyncInput, &context); + JazzerLibAflRuntimeOptions runtime_options{ + context.options.runs, + context.options.seed, + context.options.max_len, + context.options.timeout_millis, + context.options.max_total_time_seconds, + corpus_directories.empty() ? nullptr : corpus_directories.data(), + corpus_directories.size(), + dictionary_files.empty() ? nullptr : dictionary_files.data(), + dictionary_files.size(), + }; + status = jazzer_libafl_runtime_run(&runtime_options, &maps, + ExecuteSyncInput, &context); + } signal(SIGINT, SIG_DFL); signal(SIGSEGV, SIG_DFL); StopSyncWatchdog(&context); gActiveSyncContext = nullptr; - if (status == kSpikeFatal && !context.is_resolved) { + if (status == kRuntimeFatal && !context.is_resolved) { context.is_resolved = true; context.deferred.Reject( Napi::Error::New(info.Env(), "The LibAFL backend failed internally") .Value()); - } else if (status == kSpikeFoundTimeout && !context.is_resolved) { + } else if (status == kRuntimeFoundTimeout && !context.is_resolved) { context.is_resolved = true; context.deferred.Reject( Napi::Error::New(info.Env(), "Exceeded timeout while executing one fuzz input") .Value()); - } else if (status == kSpikeFoundFinding && !context.is_resolved) { + } else if (status == kRuntimeFoundFinding && !context.is_resolved) { context.is_resolved = true; context.deferred.Reject( - Napi::Error::New(info.Env(), "The LibAFL backend found a crashing input") + Napi::Error::New(info.Env(), + "The LibAFL backend found a crashing input") .Value()); } @@ -744,15 +1043,15 @@ Napi::Value StartLibAflSpike(const Napi::CallbackInfo &info) { return context.deferred.Promise(); } -Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info) { +Napi::Value StartLibAflAsync(const Napi::CallbackInfo &info) { if (info.Length() != 2 || !info[0].IsFunction() || !info[1].IsObject()) { throw Napi::Error::New(info.Env(), "Need two arguments, which must be the fuzz target " "function and a LibAFL options object"); } - auto options = ParseSpikeOptions(info.Env(), info[1].As()); - auto maps = SharedMapsForSpike(info.Env()); + auto options = ParseRuntimeOptions(info.Env(), info[1].As()); + auto maps = SharedMapsForRuntime(info.Env()); auto *context = new AsyncFuzzTargetContext(info.Env(), std::move(options)); gAsyncTsfn = AsyncTsfn::New( @@ -760,19 +1059,20 @@ Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info) { context, [](Napi::Env env, AsyncFinalizerDataType *, AsyncFuzzTargetContext *ctx) { ctx->native_thread.join(); - if (ctx->run_status == kSpikeFatal && !ctx->is_resolved) { + if (ctx->run_status == kRuntimeFatal && !ctx->is_resolved) { ctx->deferred.Reject( Napi::Error::New(env, "The LibAFL backend failed internally") .Value()); - } else if (ctx->run_status == kSpikeFoundTimeout && !ctx->is_resolved) { + } else if (ctx->run_status == kRuntimeFoundTimeout && + !ctx->is_resolved) { ctx->deferred.Reject( Napi::Error::New( env, "Exceeded timeout while executing one fuzz input") .Value()); - } else if (ctx->run_status == kSpikeFoundFinding && !ctx->is_resolved) { + } else if (ctx->run_status == kRuntimeFoundFinding && + !ctx->is_resolved) { ctx->deferred.Reject( - Napi::Error::New(env, - "The LibAFL backend found a crashing input") + Napi::Error::New(env, "The LibAFL backend found a crashing input") .Value()); } else if (!ctx->is_resolved) { ctx->deferred.Resolve(env.Undefined()); @@ -786,22 +1086,37 @@ Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info) { signal(SIGSEGV, AsyncErrorSignalHandler); signal(SIGINT, AsyncSigintHandler); - std::vector corpus_directories; - corpus_directories.reserve(ctx->options.corpus_directories.size()); - for (const auto &directory : ctx->options.corpus_directories) { - corpus_directories.push_back(directory.c_str()); + if (ctx->options.mode == ParsedRuntimeOptions::Mode::kRegression) { + ctx->run_status = ReplayRegressionInputs( + ctx->options, [ctx](const uint8_t *data, size_t size) { + return ExecuteAsyncInput(ctx, data, size); + }); + } else { + std::vector corpus_directories; + corpus_directories.reserve(ctx->options.corpus_directories.size()); + for (const auto &directory : ctx->options.corpus_directories) { + corpus_directories.push_back(directory.c_str()); + } + std::vector dictionary_files; + dictionary_files.reserve(ctx->options.dictionary_files.size()); + for (const auto &dictionary : ctx->options.dictionary_files) { + dictionary_files.push_back(dictionary.c_str()); + } + + JazzerLibAflRuntimeOptions runtime_options{ + ctx->options.runs, + ctx->options.seed, + ctx->options.max_len, + ctx->options.timeout_millis, + ctx->options.max_total_time_seconds, + corpus_directories.empty() ? nullptr : corpus_directories.data(), + corpus_directories.size(), + dictionary_files.empty() ? nullptr : dictionary_files.data(), + dictionary_files.size(), + }; + ctx->run_status = jazzer_libafl_runtime_run(&runtime_options, &maps, + ExecuteAsyncInput, ctx); } - - JazzerLibAflSpikeOptions spike_options{ - ctx->options.runs, - ctx->options.seed, - ctx->options.max_len, - ctx->options.max_total_time_seconds, - corpus_directories.empty() ? nullptr : corpus_directories.data(), - corpus_directories.size(), - }; - ctx->run_status = jazzer_libafl_spike_run(&spike_options, &maps, - ExecuteAsyncInput, ctx); signal(SIGINT, SIG_DFL); signal(SIGSEGV, SIG_DFL); gActiveAsyncContext = nullptr; @@ -811,11 +1126,3 @@ Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info) { return context->deferred.Promise(); } - -Napi::Value StartLibAfl(const Napi::CallbackInfo &info) { - return StartLibAflSpike(info); -} - -Napi::Value StartLibAflAsync(const Napi::CallbackInfo &info) { - return StartLibAflSpikeAsync(info); -} diff --git a/packages/fuzzer/libafl_spike.h b/packages/fuzzer/libafl_runtime.h similarity index 72% rename from packages/fuzzer/libafl_spike.h rename to packages/fuzzer/libafl_runtime.h index 1cebc3f98..c4440b498 100644 --- a/packages/fuzzer/libafl_spike.h +++ b/packages/fuzzer/libafl_runtime.h @@ -19,16 +19,19 @@ #include extern "C" { -struct JazzerLibAflSpikeOptions { +struct JazzerLibAflRuntimeOptions { uint64_t runs; uint64_t seed; size_t max_len; + uint64_t timeout_millis; uint64_t max_total_time_seconds; const char **corpus_directories; size_t corpus_directories_len; + const char **dictionary_files; + size_t dictionary_files_len; }; -struct JazzerLibAflSharedMaps { +struct JazzerLibAflRuntimeSharedMaps { uint8_t *edges; size_t edges_len; uint8_t *cmp; @@ -38,13 +41,11 @@ struct JazzerLibAflSharedMaps { typedef int (*JazzerLibAflExecuteCallback)(void *user_data, const uint8_t *data, size_t size); -int jazzer_libafl_spike_run(const JazzerLibAflSpikeOptions *options, - const JazzerLibAflSharedMaps *maps, - JazzerLibAflExecuteCallback execute_one, - void *user_data); +int jazzer_libafl_runtime_run(const JazzerLibAflRuntimeOptions *options, + const JazzerLibAflRuntimeSharedMaps *maps, + JazzerLibAflExecuteCallback execute_one, + void *user_data); } -Napi::Value StartLibAflSpike(const Napi::CallbackInfo &info); -Napi::Value StartLibAflSpikeAsync(const Napi::CallbackInfo &info); Napi::Value StartLibAfl(const Napi::CallbackInfo &info); Napi::Value StartLibAflAsync(const Napi::CallbackInfo &info); diff --git a/packages/fuzzer/libafl_spike.test.ts b/packages/fuzzer/libafl_runtime.test.ts similarity index 91% rename from packages/fuzzer/libafl_spike.test.ts rename to packages/fuzzer/libafl_runtime.test.ts index 88ce0875d..d276c3ec3 100644 --- a/packages/fuzzer/libafl_spike.test.ts +++ b/packages/fuzzer/libafl_runtime.test.ts @@ -18,6 +18,7 @@ import { addon } from "./addon"; import { fuzzer } from "./fuzzer"; const libAflOptions = { + mode: "fuzzing" as const, runs: 32, seed: 1234, maxLen: 64, @@ -25,10 +26,11 @@ const libAflOptions = { maxTotalTimeSeconds: 0, artifactPrefix: "", corpusDirectories: [], + dictionaryFiles: [], }; -describe("LibAFL spike", () => { - it("runs synchronous fuzz targets through the native spike", async () => { +describe("LibAFL runtime", () => { + it("runs synchronous fuzz targets through the native runtime", async () => { let invocations = 0; await addon.startLibAfl( @@ -76,6 +78,7 @@ describe("LibAFL spike", () => { fuzzer.tracer.tracePcIndir(13, data.length); }, { + mode: "fuzzing", runs: 1, seed: 9, maxLen: 16, @@ -83,6 +86,7 @@ describe("LibAFL spike", () => { maxTotalTimeSeconds: 0, artifactPrefix: "", corpusDirectories: [], + dictionaryFiles: [], }, () => undefined, ); diff --git a/packages/fuzzer/package.json b/packages/fuzzer/package.json index 1eabb6ff2..53ce9191a 100644 --- a/packages/fuzzer/package.json +++ b/packages/fuzzer/package.json @@ -18,7 +18,7 @@ "scripts": { "prebuild": "cmake-js build --out build", "build": "node ../../scripts/build-fuzzer.js", - "benchmark:libafl": "node spike/benchmark.js", + "benchmark:libafl": "node runtime/benchmark.js", "format:fix": "clang-format -i *.cpp shared/*.cpp shared/*.h", "lint": "find . -path ./build -prune -type f -o -iname '*.h' -o -iname '*.cpp' | xargs clang-tidy" }, diff --git a/packages/fuzzer/spike/benchmark.js b/packages/fuzzer/runtime/benchmark.js similarity index 93% rename from packages/fuzzer/spike/benchmark.js rename to packages/fuzzer/runtime/benchmark.js index 23a848017..8b08482ee 100644 --- a/packages/fuzzer/spike/benchmark.js +++ b/packages/fuzzer/runtime/benchmark.js @@ -17,9 +17,9 @@ const { addon } = require("../dist/addon.js"); const { fuzzer } = require("../dist/fuzzer.js"); -const runs = Number(process.env.JAZZER_SPIKE_RUNS ?? "20000"); -const seed = Number(process.env.JAZZER_SPIKE_SEED ?? "1337"); -const maxLen = Number(process.env.JAZZER_SPIKE_MAX_LEN ?? "64"); +const runs = Number(process.env.JAZZER_LIBAFL_RUNS ?? "20000"); +const seed = Number(process.env.JAZZER_LIBAFL_SEED ?? "1337"); +const maxLen = Number(process.env.JAZZER_LIBAFL_MAX_LEN ?? "64"); const libFuzzerArgs = [ "jazzer-libfuzzer-benchmark", @@ -28,6 +28,7 @@ const libFuzzerArgs = [ `-max_len=${maxLen}`, ]; const libAflOptions = { + mode: "fuzzing", runs, seed, maxLen, @@ -35,6 +36,7 @@ const libAflOptions = { maxTotalTimeSeconds: 0, artifactPrefix: "", corpusDirectories: [], + dictionaryFiles: [], }; async function measure(name, start) { diff --git a/packages/fuzzer/rust/Cargo.lock b/packages/fuzzer/rust/Cargo.lock index 670a217fe..dcaf48870 100644 --- a/packages/fuzzer/rust/Cargo.lock +++ b/packages/fuzzer/rust/Cargo.lock @@ -370,7 +370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "jazzerjs-libafl-spike" +name = "jazzerjs-libafl-runtime" version = "0.1.0" dependencies = [ "libafl", diff --git a/packages/fuzzer/rust/Cargo.toml b/packages/fuzzer/rust/Cargo.toml index 1d25348da..c11c2bbc2 100644 --- a/packages/fuzzer/rust/Cargo.toml +++ b/packages/fuzzer/rust/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "jazzerjs-libafl-spike" +name = "jazzerjs-libafl-runtime" version = "0.1.0" edition = "2021" license = "Apache-2.0" publish = false [lib] -name = "jazzerjs_libafl_spike" +name = "jazzerjs_libafl_runtime" crate-type = ["staticlib"] [profile.release] diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 0f783fe43..c4e453ba0 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -2,25 +2,34 @@ use core::ffi::{c_char, c_void}; use core::ptr; use std::cell::Cell; use std::ffi::CStr; +use std::fs; use std::path::PathBuf; use std::time::{Duration, Instant}; use libafl::{ - corpus::{Corpus, InMemoryCorpus, Testcase}, + corpus::{CachedOnDiskCorpus, Corpus, InMemoryCorpus}, events::SimpleEventManager, executors::{inprocess::InProcessExecutor, ExitKind}, feedback_or_fast, feedbacks::{CrashFeedback, MaxMapFeedback, TimeoutFeedback}, - fuzzer::{Fuzzer, StdFuzzer}, + fuzzer::{Evaluator, Fuzzer, StdFuzzer}, inputs::{BytesInput, HasTargetBytes}, - monitors::SimpleMonitor, - mutators::{havoc_mutations::havoc_mutations, scheduled::HavocScheduledMutator}, + monitors::{stats::ClientStatsManager, Monitor}, + mutators::{ + havoc_mutations::havoc_mutations, scheduled::HavocScheduledMutator, tokens_mutations, + Tokens, + }, observers::{CanTrack, HitcountsMapObserver, StdMapObserver}, - schedulers::QueueScheduler, + schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler}, stages::mutational::StdMutationalStage, - state::{HasCorpus, HasExecutions, HasSolutions, StdState}, + state::{HasCorpus, HasExecutions, HasMaxSize, HasSolutions, StdState}, + Error, HasMetadata, +}; +use libafl_bolts::{ + rands::StdRand, + tuples::{tuple_list, Merge}, + AsSlice, ClientId, }; -use libafl_bolts::{rands::StdRand, tuples::tuple_list, AsSlice}; const EXECUTION_CONTINUE: i32 = 0; const EXECUTION_FINDING: i32 = 1; @@ -28,35 +37,146 @@ const EXECUTION_STOP: i32 = 2; const EXECUTION_FATAL: i32 = 3; const EXECUTION_TIMEOUT: i32 = 4; -const SPIKE_OK: i32 = 0; -const SPIKE_FOUND_FINDING: i32 = 1; -const SPIKE_STOPPED: i32 = 2; -const SPIKE_FATAL: i32 = 3; -const SPIKE_FOUND_TIMEOUT: i32 = 4; +const RUNTIME_OK: i32 = 0; +const RUNTIME_FOUND_FINDING: i32 = 1; +const RUNTIME_STOPPED: i32 = 2; +const RUNTIME_FATAL: i32 = 3; +const RUNTIME_FOUND_TIMEOUT: i32 = 4; #[repr(C)] -pub struct JazzerLibAflSpikeOptions { +pub struct JazzerLibAflRuntimeOptions { pub runs: u64, pub seed: u64, pub max_len: usize, + pub timeout_millis: u64, pub max_total_time_seconds: u64, pub corpus_directories: *const *const c_char, pub corpus_directories_len: usize, + pub dictionary_files: *const *const c_char, + pub dictionary_files_len: usize, } #[repr(C)] -pub struct JazzerLibAflSharedMaps { +pub struct JazzerLibAflRuntimeSharedMaps { pub edges: *mut u8, pub edges_len: usize, pub cmp: *mut u8, pub cmp_len: usize, } -pub type JazzerLibAflExecuteCallback = unsafe extern "C" fn( - user_data: *mut c_void, - data: *const u8, - size: usize, -) -> i32; +pub type JazzerLibAflExecuteCallback = + unsafe extern "C" fn(user_data: *mut c_void, data: *const u8, size: usize) -> i32; + +struct LibAflMonitor; + +impl Monitor for LibAflMonitor { + fn display( + &mut self, + client_stats_manager: &mut ClientStatsManager, + event_msg: &str, + sender_id: ClientId, + ) -> Result<(), Error> { + let Some(event_name) = (match event_msg { + "Client Heartbeat" => Some("heartbeat"), + "Testcase" => Some("testcase"), + "Objective" => Some("objective"), + "Log" => Some("log"), + _ => None, + }) else { + return Ok(()); + }; + + let (run_time_pretty, corpus_size, objective_size, total_execs, execs_per_sec_pretty) = { + let global_stats = client_stats_manager.global_stats(); + ( + global_stats.run_time_pretty.clone(), + global_stats.corpus_size, + global_stats.objective_size, + global_stats.total_execs, + global_stats.execs_per_sec_pretty.clone(), + ) + }; + let mut user_stats = client_stats_manager + .client_stats_for(sender_id)? + .user_stats() + .iter() + .map(|(key, value)| format!("{key}: {value}")) + .collect::>(); + user_stats.sort(); + let extra = if user_stats.is_empty() { + String::new() + } else { + format!(", {}", user_stats.join(", ")) + }; + + eprintln!( + "[libafl::{event_name}] run time: {}, corpus: {}, objectives: {}, executions: {}, exec/sec: {}{extra}", + run_time_pretty, + corpus_size, + objective_size, + total_execs, + execs_per_sec_pretty, + ); + Ok(()) + } +} + +fn format_duration(duration: Duration) -> String { + let total_seconds = duration.as_secs(); + let hours = total_seconds / 3600; + let minutes = (total_seconds % 3600) / 60; + let seconds = total_seconds % 60; + + if hours > 0 { + format!("{hours}h {minutes}m {seconds}s") + } else if minutes > 0 { + format!("{minutes}m {seconds}s") + } else { + format!("{seconds}s") + } +} + +fn print_runtime_start(options: &JazzerLibAflRuntimeOptions, loaded_inputs: usize) { + let runs = if options.runs == 0 { + "unlimited".to_string() + } else { + options.runs.to_string() + }; + let max_total_time = if options.max_total_time_seconds == 0 { + "unlimited".to_string() + } else { + format_duration(Duration::from_secs(options.max_total_time_seconds)) + }; + + eprintln!( + "[libafl::start] mode: fuzzing, seed: {}, loaded_inputs: {}, timeout: {} ms, max_len: {}, runs: {}, max_total_time: {}", + options.seed, loaded_inputs, options.timeout_millis, options.max_len, runs, max_total_time, + ); +} + +fn print_runtime_done( + started_at: Instant, + executions: u64, + corpus_size: usize, + objective_size: usize, +) { + let elapsed = started_at.elapsed(); + let elapsed_seconds = elapsed.as_secs_f64(); + let execs_per_sec = if elapsed_seconds > 0.0 { + executions as f64 / elapsed_seconds + } else { + executions as f64 + }; + + eprintln!( + "[libafl::done] mode: fuzzing, run time: {}, corpus: {}, objectives: {}, executions: {}, exec/sec: {:.0}", + format_duration(elapsed), + corpus_size, + objective_size, + executions, + execs_per_sec, + ); +} fn clear_shared_map(ptr: *mut u8, len: usize) { if ptr.is_null() || len == 0 { @@ -68,13 +188,14 @@ fn clear_shared_map(ptr: *mut u8, len: usize) { } } -unsafe fn parse_corpus_directories(options: &JazzerLibAflSpikeOptions) -> Option> { +unsafe fn parse_corpus_directories(options: &JazzerLibAflRuntimeOptions) -> Option> { if options.corpus_directories.is_null() || options.corpus_directories_len == 0 { return Some(Vec::new()); } let mut result = Vec::with_capacity(options.corpus_directories_len); - let directories = std::slice::from_raw_parts(options.corpus_directories, options.corpus_directories_len); + let directories = + std::slice::from_raw_parts(options.corpus_directories, options.corpus_directories_len); for directory in directories { if directory.is_null() { return None; @@ -85,34 +206,91 @@ unsafe fn parse_corpus_directories(options: &JazzerLibAflSpikeOptions) -> Option Some(result) } +unsafe fn parse_dictionary_files(options: &JazzerLibAflRuntimeOptions) -> Option> { + if options.dictionary_files.is_null() || options.dictionary_files_len == 0 { + return Some(Vec::new()); + } + + let mut result = Vec::with_capacity(options.dictionary_files_len); + let files = std::slice::from_raw_parts(options.dictionary_files, options.dictionary_files_len); + for file in files { + if file.is_null() { + return None; + } + let path = CStr::from_ptr(*file).to_string_lossy().to_string(); + result.push(PathBuf::from(path)); + } + Some(result) +} + +fn resolve_main_corpus_directory( + corpus_dirs: &[PathBuf], + seed: u64, +) -> Result { + let directory = if let Some(first) = corpus_dirs.first() { + first.clone() + } else { + std::env::temp_dir().join(format!( + "jazzerjs-libafl-runtime-{}-{}", + std::process::id(), + seed, + )) + }; + fs::create_dir_all(&directory)?; + Ok(directory) +} + +fn load_dictionary_tokens(files: &[PathBuf]) -> Result { + if files.is_empty() { + return Ok(Tokens::new()); + } + + Tokens::new().add_from_files(files.iter()) +} + #[no_mangle] -pub unsafe extern "C" fn jazzer_libafl_spike_run( - options: *const JazzerLibAflSpikeOptions, - maps: *const JazzerLibAflSharedMaps, +pub unsafe extern "C" fn jazzer_libafl_runtime_run( + options: *const JazzerLibAflRuntimeOptions, + maps: *const JazzerLibAflRuntimeSharedMaps, execute_one: JazzerLibAflExecuteCallback, user_data: *mut c_void, ) -> i32 { if options.is_null() || maps.is_null() { eprintln!("[libafl] fatal: null options or maps pointer"); - return SPIKE_FATAL; + return RUNTIME_FATAL; } let options = &*options; let maps = &*maps; if maps.edges.is_null() || maps.edges_len == 0 || maps.cmp.is_null() || maps.cmp_len == 0 { eprintln!("[libafl] fatal: shared maps are missing"); - return SPIKE_FATAL; + return RUNTIME_FATAL; } let corpus_dirs = match parse_corpus_directories(options) { Some(dirs) => dirs, None => { eprintln!("[libafl] fatal: invalid corpus directories"); - return SPIKE_FATAL; + return RUNTIME_FATAL; + } + }; + let dictionary_files = match parse_dictionary_files(options) { + Some(files) => files, + None => { + eprintln!("[libafl] fatal: invalid dictionary files"); + return RUNTIME_FATAL; } }; - let monitor = SimpleMonitor::new(|_| {}); + let main_corpus_dir = match resolve_main_corpus_directory(&corpus_dirs, options.seed) { + Ok(directory) => directory, + Err(error) => { + eprintln!("[libafl] fatal: failed to prepare corpus directory: {error:?}"); + return RUNTIME_FATAL; + } + }; + + let monitor = LibAflMonitor; let mut mgr = SimpleEventManager::new(monitor); let edges_observer = HitcountsMapObserver::new(StdMapObserver::from_mut_ptr( @@ -121,20 +299,20 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( maps.edges_len, )) .track_indices(); - let cmp_observer = HitcountsMapObserver::new(StdMapObserver::from_mut_ptr( - "cmp", - maps.cmp, - maps.cmp_len, - )); - - let mut feedback = feedback_or_fast!( - MaxMapFeedback::new(&edges_observer), - MaxMapFeedback::new(&cmp_observer) - ); + let cmp_observer = + HitcountsMapObserver::new(StdMapObserver::from_mut_ptr("cmp", maps.cmp, maps.cmp_len)); + + let mut feedback = MaxMapFeedback::new(&edges_observer); let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new()); let mut state = match StdState::new( StdRand::with_seed(options.seed), - InMemoryCorpus::new(), + match CachedOnDiskCorpus::no_meta(&main_corpus_dir, 256) { + Ok(corpus) => corpus, + Err(error) => { + eprintln!("[libafl] fatal: failed to create on-disk corpus: {error:?}"); + return RUNTIME_FATAL; + } + }, InMemoryCorpus::new(), &mut feedback, &mut objective, @@ -142,22 +320,27 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( Ok(state) => state, Err(error) => { eprintln!("[libafl] fatal: failed to create fuzzing state: {error:?}"); - return SPIKE_FATAL; + return RUNTIME_FATAL; } }; + state.set_max_size(options.max_len); - if state - .corpus_mut() - .add(Testcase::new(BytesInput::new(vec![]))) - .is_err() - { - eprintln!("[libafl] fatal: failed to seed empty testcase"); - return SPIKE_FATAL; + match load_dictionary_tokens(&dictionary_files) { + Ok(tokens) => { + if !tokens.is_empty() { + state.add_metadata(tokens); + } + } + Err(error) => { + eprintln!("[libafl] fatal: failed to load dictionary tokens: {error:?}"); + return RUNTIME_FATAL; + } } - let mut fuzzer = StdFuzzer::new(QueueScheduler::new(), feedback, objective); - let mutator = HavocScheduledMutator::new(havoc_mutations()); - let mut stages = tuple_list!(StdMutationalStage::new(mutator)); + let scheduler = IndexesLenTimeMinimizerScheduler::new(&edges_observer, QueueScheduler::new()); + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + let mutator = HavocScheduledMutator::new(havoc_mutations().merge(tokens_mutations())); + let mut stages = tuple_list!(StdMutationalStage::new(mutator),); let stop_requested = Cell::new(false); let fatal_error = Cell::new(false); let timeout_found = Cell::new(false); @@ -202,7 +385,7 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( Ok(executor) => executor, Err(error) => { eprintln!("[libafl] fatal: failed to create executor: {error:?}"); - return SPIKE_FATAL; + return RUNTIME_FATAL; } }; @@ -212,10 +395,21 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( .is_err() { eprintln!("[libafl] fatal: failed to load initial corpus inputs"); - return SPIKE_FATAL; + return RUNTIME_FATAL; } } + if state.corpus().count() == 0 + && fuzzer + .add_input(&mut state, &mut executor, &mut mgr, BytesInput::new(vec![])) + .is_err() + { + eprintln!("[libafl] fatal: failed to seed empty testcase"); + return RUNTIME_FATAL; + } + + print_runtime_start(options, state.corpus().count()); + let started_at = Instant::now(); let max_total_time = if options.max_total_time_seconds == 0 { None @@ -224,41 +418,48 @@ pub unsafe extern "C" fn jazzer_libafl_spike_run( }; let initial_executions = *state.executions(); + let mut status = RUNTIME_OK; loop { if options.runs != 0 - && state - .executions() - .saturating_sub(initial_executions) - >= options.runs + && state.executions().saturating_sub(initial_executions) >= options.runs { break; } if let Some(max_total_time) = max_total_time { if started_at.elapsed() >= max_total_time { - return SPIKE_STOPPED; + status = RUNTIME_STOPPED; + break; } } if let Err(error) = fuzzer.fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr) { eprintln!("[libafl] fatal: fuzz_one returned an error: {error:?}"); - return SPIKE_FATAL; + return RUNTIME_FATAL; } if fatal_error.get() { - return SPIKE_FATAL; + return RUNTIME_FATAL; } if timeout_found.get() { - return SPIKE_FOUND_TIMEOUT; + return RUNTIME_FOUND_TIMEOUT; } if state.solutions().count() > 0 { - return SPIKE_FOUND_FINDING; + return RUNTIME_FOUND_FINDING; } if stop_requested.get() { - return SPIKE_STOPPED; + status = RUNTIME_STOPPED; + break; } } - SPIKE_OK + print_runtime_done( + started_at, + state.executions().saturating_sub(initial_executions), + state.corpus().count(), + state.solutions().count(), + ); + + status } diff --git a/packages/fuzzer/tsconfig.json b/packages/fuzzer/tsconfig.json index c03b8347c..2d70535b6 100644 --- a/packages/fuzzer/tsconfig.json +++ b/packages/fuzzer/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": ".", "outDir": "dist" }, - "exclude": ["build", "dist", "spike", "cmake-build-*"] + "exclude": ["build", "dist", "runtime", "cmake-build-*"] } diff --git a/tests/bug-detectors/general.test.js b/tests/bug-detectors/general.test.js index f94d5c79c..13fe74e29 100644 --- a/tests/bug-detectors/general.test.js +++ b/tests/bug-detectors/general.test.js @@ -176,6 +176,7 @@ describe("General tests", () => { .sync(false) .fuzzEntryPoint("ForkModeCallOriginalEvil") .dir(bugDetectorDirectory) + .engine("libfuzzer") .runs(200) .forkMode(3) .build(); @@ -195,6 +196,7 @@ describe("General tests", () => { .sync(false) .fuzzEntryPoint("ForkModeCallOriginalFriendly") .dir(bugDetectorDirectory) + .engine("libfuzzer") .runs(200) .forkMode(3) .build(); @@ -214,6 +216,7 @@ describe("General tests", () => { .sync(false) .fuzzEntryPoint("ForkModeCallOriginalEvilAsync") .dir(bugDetectorDirectory) + .engine("libfuzzer") .runs(10) .forkMode(3) .build(); @@ -233,6 +236,7 @@ describe("General tests", () => { .sync(false) .fuzzEntryPoint("ForkModeCallOriginalFriendlyAsync") .dir(bugDetectorDirectory) + .engine("libfuzzer") .runs(200) .forkMode(3) .build(); diff --git a/tests/code_coverage/coverage.test.js b/tests/code_coverage/coverage.test.js index 0d18f1dba..8d8a473a8 100644 --- a/tests/code_coverage/coverage.test.js +++ b/tests/code_coverage/coverage.test.js @@ -222,7 +222,15 @@ function executeFuzzTest( verbose = false, ) { removeCoverageDir(coverageOutputDir); - let options = ["jazzer", "fuzz", "-e", excludePattern, "--corpus", "corpus"]; + let options = [ + "jazzer", + "fuzz", + "--engine=libfuzzer", + "-e", + excludePattern, + "--corpus", + "corpus", + ]; // add dry run option if (dryRun) options.push("-d"); if (includeLib) { diff --git a/tests/done_callback/package.json b/tests/done_callback/package.json index 0d54782fb..50daba017 100644 --- a/tests/done_callback/package.json +++ b/tests/done_callback/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "An example showing how Jazzer.js handles callback based fuzz targets", "scripts": { - "fuzz": "jazzer fuzz --disableBugDetectors='.*' -x Error -- -runs=5000 -seed=2386907168", - "dryRun": "jazzer fuzz -- -runs=100 -seed=123456789" + "fuzz": "jazzer fuzz --engine=libfuzzer --disableBugDetectors='.*' -x Error -- -runs=5000 -seed=2386907168", + "dryRun": "jazzer fuzz --engine=libfuzzer -- -runs=100 -seed=123456789" }, "devDependencies": { "@jazzer.js/core": "file:../../packages/core" diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js index 170ac966e..0d47a57c1 100644 --- a/tests/engine/engine.test.js +++ b/tests/engine/engine.test.js @@ -14,6 +14,8 @@ * limitations under the License. */ +const { spawnSync } = require("child_process"); +const fs = require("fs"); const path = require("path"); const { @@ -46,6 +48,8 @@ describe("Engine selection", () => { .execute(); expect(fuzzTest.stderr).not.toContain("Unknown fuzzing engine"); + expect(fuzzTest.stderr).toContain("[libafl::start] mode: fuzzing"); + expect(fuzzTest.stderr).toContain("[libafl::done] mode: fuzzing"); }); it("rejects unsupported libFuzzer options in LibAFL mode", () => { @@ -61,6 +65,50 @@ describe("Engine selection", () => { expect(() => fuzzTest.execute()).toThrow(FuzzingExitCode); }); + it("supports regression mode in LibAFL mode", async () => { + const corpusDirectory = path.join(testDirectory, "regression_corpus"); + await fs.promises.rm(corpusDirectory, { force: true, recursive: true }); + await fs.promises.mkdir(corpusDirectory, { recursive: true }); + await fs.promises.writeFile( + path.join(corpusDirectory, "seed"), + "afl-regression-hit", + ); + + try { + const proc = spawnSync( + "npx", + [ + "jazzer", + "fuzz", + "-f", + "regression", + "--engine=afl", + "--mode=regression", + "--disable_bug_detectors=.*", + "--", + corpusDirectory, + ], + { + cwd: testDirectory, + env: { ...process.env }, + shell: true, + stdio: "pipe", + windowsHide: true, + }, + ); + + expect(proc.status).toBe(Number(FuzzingExitCode)); + const output = proc.stdout.toString() + proc.stderr.toString(); + expect(output).toContain("[libafl::start] mode: regression"); + expect(output).toContain("AFL regression finding"); + } finally { + await fs.promises.rm(corpusDirectory, { + force: true, + recursive: true, + }); + } + }); + it("fails fast on asynchronous hangs in LibAFL mode", async () => { const fuzzTest = new FuzzTestBuilder() .dir(testDirectory) diff --git a/tests/engine/fuzz.js b/tests/engine/fuzz.js index 63aab383e..dbef78c09 100644 --- a/tests/engine/fuzz.js +++ b/tests/engine/fuzz.js @@ -31,3 +31,9 @@ module.exports.timeout_async = function (_data) { // Never resolve on purpose to exercise cooperative timeout handling. }); }; + +module.exports.regression = function (data) { + if (data.toString() === "afl-regression-hit") { + throw new Error("AFL regression finding"); + } +}; diff --git a/tests/fork_mode/package.json b/tests/fork_mode/package.json index f04ed1d4b..41b9cf5da 100644 --- a/tests/fork_mode/package.json +++ b/tests/fork_mode/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "An example showing how to use libFuzzer's fork mode in Jazzer.js", "scripts": { - "fuzz": "jazzer fuzz --sync --disableBugDetectors='.*' -- -fork=3", - "dryRun": "jazzer fuzz --sync -- -fork=3 -runs=100 -seed=123456789" + "fuzz": "jazzer fuzz --engine=libfuzzer --sync --disableBugDetectors='.*' -- -fork=3", + "dryRun": "jazzer fuzz --engine=libfuzzer --sync -- -fork=3 -runs=100 -seed=123456789" }, "devDependencies": { "@jazzer.js/core": "file:../../packages/core" diff --git a/tests/helpers.js b/tests/helpers.js index f8e1cf691..aa23ea8f4 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -304,7 +304,7 @@ class FuzzTestBuilder { _expectedErrors = []; _asJson = false; _timeout = undefined; - _engine = undefined; + _engine = "libfuzzer"; /** * @param {boolean} logTestOutput - whether to print the output of the fuzz test to the console. diff --git a/tests/promise/package.json b/tests/promise/package.json index 0ec55ccc3..31fdd35de 100644 --- a/tests/promise/package.json +++ b/tests/promise/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "An example showing how Jazzer.js handles promise based fuzz targets", "scripts": { - "fuzz": "jazzer fuzz --fuzz_function fuzz_promise --disableBugDetectors='.*' -x Error -- -runs=5000 -seed=3088388356", - "dryRun": "jazzer fuzz --fuzz_function fuzz_promise -- -runs=1 -seed=123456789" + "fuzz": "jazzer fuzz --engine=libfuzzer --fuzz_function fuzz_promise --disableBugDetectors='.*' -x Error -- -runs=5000 -seed=3088388356", + "dryRun": "jazzer fuzz --engine=libfuzzer --fuzz_function fuzz_promise -- -runs=1 -seed=123456789" }, "devDependencies": { "@jazzer.js/core": "file:../../packages/core" diff --git a/tests/string_compare/package.json b/tests/string_compare/package.json index 76b5fbd16..5e93c4cc4 100644 --- a/tests/string_compare/package.json +++ b/tests/string_compare/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "An example showing how Jazzer.js handles string comparisons in the code", "scripts": { - "fuzz": "jazzer fuzz --sync --disableBugDetectors='.*' -x Error -- -runs=5000000 -seed=111994470", - "dryRun": "jazzer fuzz --sync -- -runs=100 -seed=123456789" + "fuzz": "jazzer fuzz --engine=libfuzzer --sync --disableBugDetectors='.*' -x Error -- -runs=5000000 -seed=111994470", + "dryRun": "jazzer fuzz --engine=libfuzzer --sync -- -runs=100 -seed=123456789" }, "devDependencies": { "@jazzer.js/core": "file:../../packages/core" diff --git a/tests/timeout/package.json b/tests/timeout/package.json index f35a9da32..00862b521 100644 --- a/tests/timeout/package.json +++ b/tests/timeout/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "Timeout test: checking that the handler for the SIGALRM signal does not return with error code.", "scripts": { - "timeout": "jazzer fuzz -f=timeout --timeout=1000 --disableBugDetectors='.*' -- -runs=5000 -seed=1234", - "fuzz": "jazzer fuzz --timeout=1000 -- -runs=5000 -seed=1234", + "timeout": "jazzer fuzz --engine=libfuzzer -f=timeout --timeout=1000 --disableBugDetectors='.*' -- -runs=5000 -seed=1234", + "fuzz": "jazzer fuzz --engine=libfuzzer --timeout=1000 -- -runs=5000 -seed=1234", "dryRun": "echo \"skipped\"" }, "devDependencies": { diff --git a/tests/value_profiling/package.json b/tests/value_profiling/package.json index 38b5e9ca9..fedb62155 100644 --- a/tests/value_profiling/package.json +++ b/tests/value_profiling/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "An example showing how Jazzer.js handles integer comparisons in the code", "scripts": { - "fuzz": "jazzer fuzz --sync --disableBugDetectors='.*' -x Error -- -runs=4000000 -seed=1428686921 -use_value_profile=1", - "dryRun": "jazzer fuzz --sync -- -use_value_profile=1 -runs=100 -seed=123456789" + "fuzz": "jazzer fuzz --engine=libfuzzer --sync --disableBugDetectors='.*' -x Error -- -runs=4000000 -seed=1428686921 -use_value_profile=1", + "dryRun": "jazzer fuzz --engine=libfuzzer --sync -- -use_value_profile=1 -runs=100 -seed=123456789" }, "devDependencies": { "@jazzer.js/core": "file:../../packages/core" From 6da049f0e9b367ae939954a41a9100124cb8fcea Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 13:16:11 +0200 Subject: [PATCH 10/30] test(bench): add a 30-second engine smoke benchmark Benchmark libFuzzer and LibAFL against qs, then report generated\ncorpus entries and regression-mode coverage from the saved corpus. Run the root Jest suites in band so local test runs stop saturating\ndeveloper machines. --- benchmarks/engine_smoke/.gitignore | 3 + benchmarks/engine_smoke/fuzz.js | 76 +++++++++ benchmarks/engine_smoke/package.json | 14 ++ benchmarks/engine_smoke/run.js | 182 ++++++++++++++++++++++ benchmarks/engine_smoke/seeds/basic.txt | 1 + benchmarks/engine_smoke/seeds/encoded.txt | 1 + benchmarks/engine_smoke/seeds/nested.txt | 1 + package.json | 4 +- 8 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 benchmarks/engine_smoke/.gitignore create mode 100644 benchmarks/engine_smoke/fuzz.js create mode 100644 benchmarks/engine_smoke/package.json create mode 100644 benchmarks/engine_smoke/run.js create mode 100644 benchmarks/engine_smoke/seeds/basic.txt create mode 100644 benchmarks/engine_smoke/seeds/encoded.txt create mode 100644 benchmarks/engine_smoke/seeds/nested.txt diff --git a/benchmarks/engine_smoke/.gitignore b/benchmarks/engine_smoke/.gitignore new file mode 100644 index 000000000..91b88bfe4 --- /dev/null +++ b/benchmarks/engine_smoke/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +package-lock.json +work/ diff --git a/benchmarks/engine_smoke/fuzz.js b/benchmarks/engine_smoke/fuzz.js new file mode 100644 index 000000000..690274041 --- /dev/null +++ b/benchmarks/engine_smoke/fuzz.js @@ -0,0 +1,76 @@ +/* + * Copyright 2026 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. + */ + +const qs = require("qs"); + +const { FuzzedDataProvider } = require("@jazzer.js/core"); + +module.exports.fuzz = function (data) { + const provider = new FuzzedDataProvider(data); + const input = provider.consumeRemainingAsString(); + + const parseOptions = { + allowDots: provider.consumeBoolean(), + allowEmptyArrays: provider.consumeBoolean(), + allowPrototypes: provider.consumeBoolean(), + arrayLimit: provider.consumeIntegralInRange(0, 32), + charset: provider.pickValue(["utf-8", "iso-8859-1"]), + charsetSentinel: provider.consumeBoolean(), + comma: provider.consumeBoolean(), + decodeDotInKeys: provider.consumeBoolean(), + depth: provider.consumeIntegralInRange(0, 16), + duplicates: provider.pickValue(["combine", "first", "last"]), + ignoreQueryPrefix: provider.consumeBoolean(), + interpretNumericEntities: provider.consumeBoolean(), + parameterLimit: provider.consumeIntegralInRange(1, 256), + parseArrays: provider.consumeBoolean(), + plainObjects: provider.consumeBoolean(), + strictDepth: provider.consumeBoolean(), + strictNullHandling: provider.consumeBoolean(), + }; + + let parsed; + try { + parsed = qs.parse(input, parseOptions); + } catch { + return; + } + + try { + qs.stringify(parsed, { + addQueryPrefix: provider.consumeBoolean(), + allowDots: provider.consumeBoolean(), + allowEmptyArrays: provider.consumeBoolean(), + arrayFormat: provider.pickValue([ + "indices", + "brackets", + "repeat", + "comma", + ]), + charset: provider.pickValue(["utf-8", "iso-8859-1"]), + charsetSentinel: provider.consumeBoolean(), + commaRoundTrip: provider.consumeBoolean(), + delimiter: provider.pickValue(["&", ";"]), + encode: provider.consumeBoolean(), + encodeDotInKeys: provider.consumeBoolean(), + indices: provider.consumeBoolean(), + skipNulls: provider.consumeBoolean(), + strictNullHandling: provider.consumeBoolean(), + }); + } catch { + // Smoke target: ignore library-level parse/stringify failures. + } +}; diff --git a/benchmarks/engine_smoke/package.json b/benchmarks/engine_smoke/package.json new file mode 100644 index 000000000..31b0156af --- /dev/null +++ b/benchmarks/engine_smoke/package.json @@ -0,0 +1,14 @@ +{ + "name": "jazzerjs-engine-smoke", + "version": "1.0.0", + "private": true, + "description": "Manual 30-second smoke benchmark for libFuzzer vs LibAFL.", + "scripts": { + "smoke": "node run.js" + }, + "devDependencies": { + "@jazzer.js/core": "file:../../packages/core", + "istanbul-lib-coverage": "^3.2.2", + "qs": "^6.14.0" + } +} diff --git a/benchmarks/engine_smoke/run.js b/benchmarks/engine_smoke/run.js new file mode 100644 index 000000000..6e2539e13 --- /dev/null +++ b/benchmarks/engine_smoke/run.js @@ -0,0 +1,182 @@ +/* + * Copyright 2026 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. + */ + +const { spawnSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const libCoverage = require("istanbul-lib-coverage"); + +const benchmarkDirectory = __dirname; +const workDirectory = path.join(benchmarkDirectory, "work"); +const fuzzTarget = path.join(benchmarkDirectory, "fuzz.js"); +const seedCorpusDirectory = path.join(benchmarkDirectory, "seeds"); +const seconds = Number.parseInt(process.argv[2] ?? "30", 10); + +function removeIfExists(targetPath) { + fs.rmSync(targetPath, { force: true, recursive: true }); +} + +function ensureDirectory(targetPath) { + fs.mkdirSync(targetPath, { recursive: true }); +} + +function runCommand(label, args, engineDirectory) { + console.log(`\n[smoke] ${label}`); + console.log(`[smoke] command: npx ${args.join(" ")}`); + ensureDirectory(engineDirectory); + const sanitizedLabel = label.replace(/[^a-z0-9]+/gi, "-").toLowerCase(); + const stdoutPath = path.join(engineDirectory, `${sanitizedLabel}.stdout.log`); + const stderrPath = path.join(engineDirectory, `${sanitizedLabel}.stderr.log`); + const stdoutFd = fs.openSync(stdoutPath, "w"); + const stderrFd = fs.openSync(stderrPath, "w"); + const proc = spawnSync("npx", args, { + cwd: benchmarkDirectory, + env: { ...process.env }, + shell: true, + stdio: ["ignore", stdoutFd, stderrFd], + windowsHide: true, + }); + fs.closeSync(stdoutFd); + fs.closeSync(stderrFd); + if (proc.status !== 0) { + throw new Error( + `${label} failed with exit code ${proc.status}\nSTDOUT (${stdoutPath}):\n${fs.readFileSync(stdoutPath, "utf8")}\nSTDERR (${stderrPath}):\n${fs.readFileSync(stderrPath, "utf8")}`, + ); + } + return { stdoutPath, stderrPath }; +} + +function countFiles(directory) { + if (!fs.existsSync(directory)) { + return 0; + } + return fs + .readdirSync(directory) + .filter((entry) => fs.lstatSync(path.join(directory, entry)).isFile()) + .length; +} + +function summarizeCoverage(coverageDirectory) { + const coverageFile = path.join(coverageDirectory, "coverage-final.json"); + const rawCoverage = JSON.parse(fs.readFileSync(coverageFile, "utf8")); + const coverageMap = libCoverage.createCoverageMap(rawCoverage); + const librarySummary = libCoverage.createCoverageSummary(); + const normalizedNeedle = `${path.sep}node_modules${path.sep}qs${path.sep}`; + + const files = coverageMap + .files() + .filter((filePath) => path.normalize(filePath).includes(normalizedNeedle)); + for (const filePath of files) { + librarySummary.merge(coverageMap.fileCoverageFor(filePath).toSummary()); + } + + return { + files: files.length, + lines: librarySummary.data.lines.pct, + branches: librarySummary.data.branches.pct, + functions: librarySummary.data.functions.pct, + statements: librarySummary.data.statements.pct, + }; +} + +function runSmoke(engine) { + const engineDirectory = path.join(workDirectory, engine); + const generatedCorpusDirectory = path.join( + engineDirectory, + "generated-corpus", + ); + const artifactDirectory = path.join(engineDirectory, "artifacts"); + const coverageDirectory = path.join(engineDirectory, "coverage"); + + removeIfExists(engineDirectory); + ensureDirectory(generatedCorpusDirectory); + ensureDirectory(artifactDirectory); + + runCommand( + `${engine} fuzzing`, + [ + "jazzer", + fuzzTarget, + generatedCorpusDirectory, + seedCorpusDirectory, + "--sync", + "--disable_bug_detectors=.*", + `--engine=${engine}`, + "-i=fuzz.js", + "-i=node_modules/qs/", + "--", + `-max_total_time=${seconds}`, + `-artifact_prefix=${artifactDirectory}${path.sep}`, + ], + engineDirectory, + ); + + removeIfExists(coverageDirectory); + runCommand( + `${engine} regression coverage`, + [ + "jazzer", + fuzzTarget, + generatedCorpusDirectory, + seedCorpusDirectory, + "--sync", + "--mode=regression", + "--coverage", + `--coverage_directory=${coverageDirectory}`, + "--coverage_reporters=json", + "--disable_bug_detectors=.*", + `--engine=${engine}`, + "-i=fuzz.js", + "-i=node_modules/qs/", + ], + engineDirectory, + ); + + return { + engine, + seconds, + generatedCorpusEntries: countFiles(generatedCorpusDirectory), + coverage: summarizeCoverage(coverageDirectory), + }; +} + +function printResult(result) { + console.log(`\n[smoke] ${result.engine}`); + console.log( + `[smoke] generated corpus entries: ${result.generatedCorpusEntries}`, + ); + console.log( + `[smoke] library coverage: lines=${result.coverage.lines}% branches=${result.coverage.branches}% functions=${result.coverage.functions}% statements=${result.coverage.statements}% across ${result.coverage.files} files`, + ); +} + +function main() { + ensureDirectory(workDirectory); + const results = [runSmoke("libfuzzer"), runSmoke("afl")]; + for (const result of results) { + printResult(result); + } + fs.writeFileSync( + path.join(workDirectory, "results.json"), + JSON.stringify(results, null, 2), + ); + console.log( + `\n[smoke] wrote machine-readable results to ${path.join(workDirectory, "results.json")}`, + ); +} + +main(); diff --git a/benchmarks/engine_smoke/seeds/basic.txt b/benchmarks/engine_smoke/seeds/basic.txt new file mode 100644 index 000000000..118274dd8 --- /dev/null +++ b/benchmarks/engine_smoke/seeds/basic.txt @@ -0,0 +1 @@ +a=b&c=d diff --git a/benchmarks/engine_smoke/seeds/encoded.txt b/benchmarks/engine_smoke/seeds/encoded.txt new file mode 100644 index 000000000..73bb8b819 --- /dev/null +++ b/benchmarks/engine_smoke/seeds/encoded.txt @@ -0,0 +1 @@ +utf8=%E2%9C%93&filters[color]=blue&filters[size]=xl&page=2 diff --git a/benchmarks/engine_smoke/seeds/nested.txt b/benchmarks/engine_smoke/seeds/nested.txt new file mode 100644 index 000000000..68cbd1817 --- /dev/null +++ b/benchmarks/engine_smoke/seeds/nested.txt @@ -0,0 +1 @@ +user[name]=alice&user[roles][]=admin&user[roles][]=author diff --git a/package.json b/package.json index db6f55df2..a3ec98eb2 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "test:default": "npm run test:jest", "test:linux:darwin": "npm run test:jest && cd tests && sh ../scripts/run_all.sh fuzz", "test:win32": "npm run test:jest && cd tests && ..\\scripts\\run_all.bat fuzz", - "test:jest": "jest && npm run test --ws --if-present", - "test:jest:coverage": "jest --coverage", + "test:jest": "jest --runInBand && npm run test --ws --if-present -- --runInBand", + "test:jest:coverage": "jest --coverage --runInBand", "test:jest:watch": "jest --watch", "example": "run-script-os", "example:linux:darwin": "cd examples && sh ../scripts/run_all.sh dryRun", From 934a610a0ec53a9f6f46f7b8f5f636fcdd66cc2b Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 13:38:53 +0200 Subject: [PATCH 11/30] feat(fuzzer): add LibAFL compare-guided mutation Log structured JS compare events, parse them through a shadow\nobserver, and feed LibAFL with I2S metadata plus promoted\ntokens for equality and containment hints. Cover the new path with runtime checks and CLI integration tests\nfor numeric guidance, token promotion, and dictionary-backed\nmutations. --- packages/fuzzer/addon.ts | 2 + packages/fuzzer/libafl_runtime.cpp | 14 +- packages/fuzzer/libafl_runtime.h | 3 + packages/fuzzer/libafl_runtime.test.ts | 2 + packages/fuzzer/rust/src/compare_log.rs | 190 ++++++++++++++++++++++++ packages/fuzzer/rust/src/lib.rs | 41 ++++- packages/fuzzer/shared/callbacks.cpp | 4 + packages/fuzzer/shared/tracing.cpp | 95 +++++++++++- packages/fuzzer/shared/tracing.h | 31 ++++ tests/engine/engine.test.js | 138 +++++++++++++++++ tests/engine/fuzz.js | 33 ++++ 11 files changed, 534 insertions(+), 19 deletions(-) create mode 100644 packages/fuzzer/rust/src/compare_log.rs diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index c2d9624cf..9f7ee6081 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -92,6 +92,8 @@ type NativeAddon = { startLibAflAsync?: StartLibAflAsyncFn; clearCompareFeedbackMap: () => void; countNonZeroCompareFeedbackSlots: () => number; + countCompareLogEntries: () => number; + countDroppedCompareLogEntries: () => number; }; type LoadedAddon = NativeAddon & { diff --git a/packages/fuzzer/libafl_runtime.cpp b/packages/fuzzer/libafl_runtime.cpp index f4718bcca..db103adea 100644 --- a/packages/fuzzer/libafl_runtime.cpp +++ b/packages/fuzzer/libafl_runtime.cpp @@ -443,14 +443,16 @@ JazzerLibAflRuntimeSharedMaps SharedMapsForRuntime(Napi::Env env) { const auto edges_len = CoverageCountersSize(); auto *cmp = CompareFeedbackMap(); const auto cmp_len = CompareFeedbackMapSize(); + auto *compare_log = CompareLog(); - if (edges == nullptr || edges_len == 0 || cmp == nullptr || cmp_len == 0) { + if (edges == nullptr || edges_len == 0 || cmp == nullptr || cmp_len == 0 || + compare_log == nullptr) { throw Napi::Error::New( env, "Coverage maps were not initialized before the LibAFL backend started"); } - return {edges, edges_len, cmp, cmp_len}; + return {edges, edges_len, cmp, cmp_len, compare_log}; } bool CollectRegressionCorpusFiles( @@ -765,9 +767,11 @@ int ExecuteSyncInput(void *user_data, const uint8_t *data, size_t size) { } } } catch (const Napi::Error &error) { - WriteArtifact(context->options.artifact_prefix, "crash", data, size); - context->is_resolved = true; - context->deferred.Reject(error.Value()); + if (!context->is_resolved) { + WriteArtifact(context->options.artifact_prefix, "crash", data, size); + context->is_resolved = true; + context->deferred.Reject(error.Value()); + } return kExecutionFinding; } catch (const std::exception &exception) { ExitWithUnexpectedError(exception); diff --git a/packages/fuzzer/libafl_runtime.h b/packages/fuzzer/libafl_runtime.h index c4440b498..2fad26912 100644 --- a/packages/fuzzer/libafl_runtime.h +++ b/packages/fuzzer/libafl_runtime.h @@ -18,6 +18,8 @@ #include #include +#include "shared/tracing.h" + extern "C" { struct JazzerLibAflRuntimeOptions { uint64_t runs; @@ -36,6 +38,7 @@ struct JazzerLibAflRuntimeSharedMaps { size_t edges_len; uint8_t *cmp; size_t cmp_len; + JazzerLibAflCompareLog *compare_log; }; typedef int (*JazzerLibAflExecuteCallback)(void *user_data, const uint8_t *data, diff --git a/packages/fuzzer/libafl_runtime.test.ts b/packages/fuzzer/libafl_runtime.test.ts index d276c3ec3..ef54f711b 100644 --- a/packages/fuzzer/libafl_runtime.test.ts +++ b/packages/fuzzer/libafl_runtime.test.ts @@ -92,5 +92,7 @@ describe("LibAFL runtime", () => { ); expect(addon.countNonZeroCompareFeedbackSlots()).toBeGreaterThan(0); + expect(addon.countCompareLogEntries()).toBeGreaterThan(0); + expect(addon.countDroppedCompareLogEntries()).toBe(0); }); }); diff --git a/packages/fuzzer/rust/src/compare_log.rs b/packages/fuzzer/rust/src/compare_log.rs new file mode 100644 index 000000000..2305219c4 --- /dev/null +++ b/packages/fuzzer/rust/src/compare_log.rs @@ -0,0 +1,190 @@ +use std::borrow::Cow; + +use libafl::{ + executors::ExitKind, + mutators::Tokens, + observers::{ + cmp::{CmpValues, CmpValuesMetadata, CmplogBytes}, + Observer, + }, + Error, HasMetadata, +}; +use libafl_bolts::Named; + +pub const COMPARE_LOG_ENTRY_BYTES: usize = 32; +pub const COMPARE_LOG_MAX_ENTRIES: usize = 1024; + +const MAX_PROMOTED_TOKENS_PER_EXEC: usize = 64; +const MAX_PROMOTED_TOKENS_TOTAL: usize = 1024; +const COMPARE_LOG_SIGNED_FLAG: u8 = 1 << 0; + +const COMPARE_KIND_INTEGER: u8 = 1; +const COMPARE_KIND_STRING_EQUALITY: u8 = 2; +const COMPARE_KIND_STRING_CONTAINMENT: u8 = 3; + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct JazzerLibAflCompareLogEntry { + pub kind: u8, + pub flags: u8, + pub left_len: u8, + pub right_len: u8, + pub left_value: u64, + pub right_value: u64, + pub left_bytes: [u8; COMPARE_LOG_ENTRY_BYTES], + pub right_bytes: [u8; COMPARE_LOG_ENTRY_BYTES], +} + +#[repr(C)] +#[derive(Debug)] +pub struct JazzerLibAflCompareLog { + pub used: u32, + pub dropped: u32, + pub entries: [JazzerLibAflCompareLogEntry; COMPARE_LOG_MAX_ENTRIES], +} + +#[derive(Clone, Debug)] +pub struct JazzerCompareLogObserver { + name: Cow<'static, str>, + compare_log: *mut JazzerLibAflCompareLog, +} + +impl JazzerCompareLogObserver { + pub fn new(compare_log: *mut JazzerLibAflCompareLog) -> Self { + Self { + name: Cow::Borrowed("jazzer-compare-log"), + compare_log, + } + } + + fn compare_log(&self) -> Option<&JazzerLibAflCompareLog> { + unsafe { self.compare_log.as_ref() } + } +} + +impl Named for JazzerCompareLogObserver { + fn name(&self) -> &Cow<'static, str> { + &self.name + } +} + +impl Observer for JazzerCompareLogObserver +where + S: HasMetadata, +{ + fn pre_exec(&mut self, state: &mut S, _input: &I) -> Result<(), Error> { + if let Some(metadata) = state.metadata_map_mut().get_mut::() { + metadata.list.clear(); + } + Ok(()) + } + + fn post_exec(&mut self, state: &mut S, _input: &I, _exit_kind: &ExitKind) -> Result<(), Error> { + let Some(compare_log) = self.compare_log() else { + return Ok(()); + }; + + let entry_count = usize::min(compare_log.used as usize, COMPARE_LOG_MAX_ENTRIES); + let mut cmp_values = Vec::with_capacity(entry_count); + let mut promoted_tokens = Vec::new(); + for entry in compare_log.entries.iter().take(entry_count) { + if let Some(value) = cmp_value_for_entry(entry) { + cmp_values.push(value); + } + if promoted_tokens.len() < MAX_PROMOTED_TOKENS_PER_EXEC { + if let Some(token) = promoted_token_for_entry(entry) { + promoted_tokens.push(token); + } + } + } + + let metadata = state.metadata_or_insert_with(CmpValuesMetadata::new); + metadata.list.clear(); + metadata.list.extend(cmp_values); + + if !promoted_tokens.is_empty() { + let tokens = state.metadata_or_insert_with(Tokens::new); + for token in promoted_tokens { + if tokens.len() >= MAX_PROMOTED_TOKENS_TOTAL { + break; + } + tokens.add_token(&token); + } + } + + Ok(()) + } +} + +fn cmp_value_for_entry(entry: &JazzerLibAflCompareLogEntry) -> Option { + match entry.kind { + COMPARE_KIND_INTEGER => Some(cmp_value_for_integer(entry)), + COMPARE_KIND_STRING_EQUALITY => cmp_value_for_string_equality(entry), + _ => None, + } +} + +fn promoted_token_for_entry(entry: &JazzerLibAflCompareLogEntry) -> Option> { + match entry.kind { + COMPARE_KIND_STRING_EQUALITY => token_from_entry(&entry.right_bytes, entry.right_len), + COMPARE_KIND_STRING_CONTAINMENT => token_from_entry(&entry.left_bytes, entry.left_len), + _ => None, + } +} + +fn token_from_entry(bytes: &[u8; COMPARE_LOG_ENTRY_BYTES], len: u8) -> Option> { + let len = usize::min(len as usize, COMPARE_LOG_ENTRY_BYTES); + if len == 0 { + return None; + } + Some(bytes[..len].to_vec()) +} + +fn cmp_value_for_string_equality(entry: &JazzerLibAflCompareLogEntry) -> Option { + let left_len = usize::min(entry.left_len as usize, COMPARE_LOG_ENTRY_BYTES); + let right_len = usize::min(entry.right_len as usize, COMPARE_LOG_ENTRY_BYTES); + if left_len == 0 || right_len == 0 { + return None; + } + + let mut left = [0; COMPARE_LOG_ENTRY_BYTES]; + left[..left_len].copy_from_slice(&entry.left_bytes[..left_len]); + let mut right = [0; COMPARE_LOG_ENTRY_BYTES]; + right[..right_len].copy_from_slice(&entry.right_bytes[..right_len]); + Some(CmpValues::Bytes(( + CmplogBytes::from_buf_and_len(left, left_len as u8), + CmplogBytes::from_buf_and_len(right, right_len as u8), + ))) +} + +fn cmp_value_for_integer(entry: &JazzerLibAflCompareLogEntry) -> CmpValues { + if entry.flags & COMPARE_LOG_SIGNED_FLAG != 0 { + cmp_value_for_signed_integer(entry.left_value as i64, entry.right_value as i64) + } else { + cmp_value_for_unsigned_integer(entry.left_value, entry.right_value) + } +} + +fn cmp_value_for_unsigned_integer(left: u64, right: u64) -> CmpValues { + if let (Ok(left), Ok(right)) = (u8::try_from(left), u8::try_from(right)) { + CmpValues::U8((left, right, false)) + } else if let (Ok(left), Ok(right)) = (u16::try_from(left), u16::try_from(right)) { + CmpValues::U16((left, right, false)) + } else if let (Ok(left), Ok(right)) = (u32::try_from(left), u32::try_from(right)) { + CmpValues::U32((left, right, false)) + } else { + CmpValues::U64((left, right, false)) + } +} + +fn cmp_value_for_signed_integer(left: i64, right: i64) -> CmpValues { + if let (Ok(left), Ok(right)) = (i8::try_from(left), i8::try_from(right)) { + CmpValues::U8((left as u8, right as u8, false)) + } else if let (Ok(left), Ok(right)) = (i16::try_from(left), i16::try_from(right)) { + CmpValues::U16((left as u16, right as u16, false)) + } else if let (Ok(left), Ok(right)) = (i32::try_from(left), i32::try_from(right)) { + CmpValues::U32((left as u32, right as u32, false)) + } else { + CmpValues::U64((left as u64, right as u64, false)) + } +} diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index c4e453ba0..6f2478a0e 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -1,3 +1,5 @@ +mod compare_log; + use core::ffi::{c_char, c_void}; use core::ptr; use std::cell::Cell; @@ -9,7 +11,7 @@ use std::time::{Duration, Instant}; use libafl::{ corpus::{CachedOnDiskCorpus, Corpus, InMemoryCorpus}, events::SimpleEventManager, - executors::{inprocess::InProcessExecutor, ExitKind}, + executors::{inprocess::InProcessExecutor, ExitKind, ShadowExecutor}, feedback_or_fast, feedbacks::{CrashFeedback, MaxMapFeedback, TimeoutFeedback}, fuzzer::{Evaluator, Fuzzer, StdFuzzer}, @@ -17,11 +19,11 @@ use libafl::{ monitors::{stats::ClientStatsManager, Monitor}, mutators::{ havoc_mutations::havoc_mutations, scheduled::HavocScheduledMutator, tokens_mutations, - Tokens, + I2SRandReplace, Tokens, }, observers::{CanTrack, HitcountsMapObserver, StdMapObserver}, schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler}, - stages::mutational::StdMutationalStage, + stages::{mutational::StdMutationalStage, shadow::ShadowTracingStage}, state::{HasCorpus, HasExecutions, HasMaxSize, HasSolutions, StdState}, Error, HasMetadata, }; @@ -31,6 +33,8 @@ use libafl_bolts::{ AsSlice, ClientId, }; +use crate::compare_log::{JazzerCompareLogObserver, JazzerLibAflCompareLog}; + const EXECUTION_CONTINUE: i32 = 0; const EXECUTION_FINDING: i32 = 1; const EXECUTION_STOP: i32 = 2; @@ -62,6 +66,7 @@ pub struct JazzerLibAflRuntimeSharedMaps { pub edges_len: usize, pub cmp: *mut u8, pub cmp_len: usize, + pub compare_log: *mut JazzerLibAflCompareLog, } pub type JazzerLibAflExecuteCallback = @@ -188,6 +193,16 @@ fn clear_shared_map(ptr: *mut u8, len: usize) { } } +fn clear_compare_log(ptr: *mut JazzerLibAflCompareLog) { + if ptr.is_null() { + return; + } + + unsafe { + ptr::write_bytes(ptr, 0, 1); + } +} + unsafe fn parse_corpus_directories(options: &JazzerLibAflRuntimeOptions) -> Option> { if options.corpus_directories.is_null() || options.corpus_directories_len == 0 { return Some(Vec::new()); @@ -262,7 +277,12 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let options = &*options; let maps = &*maps; - if maps.edges.is_null() || maps.edges_len == 0 || maps.cmp.is_null() || maps.cmp_len == 0 { + if maps.edges.is_null() + || maps.edges_len == 0 + || maps.cmp.is_null() + || maps.cmp_len == 0 + || maps.compare_log.is_null() + { eprintln!("[libafl] fatal: shared maps are missing"); return RUNTIME_FATAL; } @@ -339,8 +359,12 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let scheduler = IndexesLenTimeMinimizerScheduler::new(&edges_observer, QueueScheduler::new()); let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); - let mutator = HavocScheduledMutator::new(havoc_mutations().merge(tokens_mutations())); - let mut stages = tuple_list!(StdMutationalStage::new(mutator),); + let mutator = HavocScheduledMutator::new( + havoc_mutations() + .merge(tokens_mutations()) + .merge(tuple_list!(I2SRandReplace::new())), + ); + let mut stages = tuple_list!(ShadowTracingStage::new(), StdMutationalStage::new(mutator),); let stop_requested = Cell::new(false); let fatal_error = Cell::new(false); let timeout_found = Cell::new(false); @@ -348,6 +372,7 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let mut harness = |input: &BytesInput| { clear_shared_map(maps.edges, maps.edges_len); clear_shared_map(maps.cmp, maps.cmp_len); + clear_compare_log(maps.compare_log); let bytes = input.target_bytes(); let bytes = bytes.as_slice(); @@ -375,7 +400,7 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( } }; - let mut executor = match InProcessExecutor::new( + let executor = match InProcessExecutor::new( &mut harness, tuple_list!(edges_observer, cmp_observer), &mut fuzzer, @@ -388,6 +413,8 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( return RUNTIME_FATAL; } }; + let shadow_observer = JazzerCompareLogObserver::new(maps.compare_log); + let mut executor = ShadowExecutor::new(executor, tuple_list!(shadow_observer)); if !corpus_dirs.is_empty() && state.must_load_initial_inputs() { if state diff --git a/packages/fuzzer/shared/callbacks.cpp b/packages/fuzzer/shared/callbacks.cpp index ddbabab6b..59220f6b2 100644 --- a/packages/fuzzer/shared/callbacks.cpp +++ b/packages/fuzzer/shared/callbacks.cpp @@ -34,4 +34,8 @@ void RegisterCallbackExports(Napi::Env env, Napi::Object exports) { Napi::Function::New(env); exports["countNonZeroCompareFeedbackSlots"] = Napi::Function::New(env); + exports["countCompareLogEntries"] = + Napi::Function::New(env); + exports["countDroppedCompareLogEntries"] = + Napi::Function::New(env); } diff --git a/packages/fuzzer/shared/tracing.cpp b/packages/fuzzer/shared/tracing.cpp index ce98f6960..5172116ec 100644 --- a/packages/fuzzer/shared/tracing.cpp +++ b/packages/fuzzer/shared/tracing.cpp @@ -31,8 +31,10 @@ void __sanitizer_cov_trace_pc_indir_with_pc(void *caller_pc, uintptr_t callee); } namespace { -constexpr std::size_t kCompareFeedbackMapSize = 1 << 16; std::array gCompareFeedbackMap{}; +JazzerLibAflCompareLog gCompareLog{}; + +constexpr uint8_t kCompareLogSignedFlag = 1 << 0; void RecordCompareFeedback(uint64_t value) { auto index = static_cast(value % kCompareFeedbackMapSize); @@ -40,6 +42,55 @@ void RecordCompareFeedback(uint64_t value) { slot = slot == 255 ? 1 : static_cast(slot + 1); } +uint8_t ClampCompareBytesLength(std::size_t length) { + return static_cast(std::min(length, kCompareLogEntryBytes)); +} + +void CopyCompareBytes(uint8_t *destination, const std::string &source) { + const auto copied = ClampCompareBytesLength(source.size()); + std::memset(destination, 0, kCompareLogEntryBytes); + if (copied == 0) { + return; + } + std::memcpy(destination, source.data(), copied); +} + +JazzerLibAflCompareLogEntry *NextCompareLogEntry() { + if (gCompareLog.used >= kCompareLogMaxEntries) { + gCompareLog.dropped++; + return nullptr; + } + auto *entry = &gCompareLog.entries[gCompareLog.used++]; + std::memset(entry, 0, sizeof(*entry)); + return entry; +} + +void RecordIntegerCompareLog(int64_t left, int64_t right) { + auto *entry = NextCompareLogEntry(); + if (entry == nullptr) { + return; + } + entry->kind = static_cast(JazzerLibAflCompareKind::kInteger); + if (left < 0 || right < 0) { + entry->flags |= kCompareLogSignedFlag; + } + entry->left_value = static_cast(left); + entry->right_value = static_cast(right); +} + +void RecordStringCompareLog(JazzerLibAflCompareKind kind, + const std::string &left, const std::string &right) { + auto *entry = NextCompareLogEntry(); + if (entry == nullptr) { + return; + } + entry->kind = static_cast(kind); + entry->left_len = ClampCompareBytesLength(left.size()); + entry->right_len = ClampCompareBytesLength(right.size()); + CopyCompareBytes(entry->left_bytes, left); + CopyCompareBytes(entry->right_bytes, right); +} + void RecordStringFeedback(uint64_t id, const std::string &first, const std::string &second) { uint64_t hash = id * 0x9e3779b185ebca87ULL; @@ -70,6 +121,7 @@ void TraceUnequalStrings(const Napi::CallbackInfo &info) { auto s2 = info[2].As().Utf8Value(); RecordStringFeedback(id, s1, s2); + RecordStringCompareLog(JazzerLibAflCompareKind::kStringEquality, s1, s2); // strcmp returns zero on equality, and libfuzzer doesn't care about the // result beyond whether it's zero or not. @@ -89,6 +141,8 @@ void TraceStringContainment(const Napi::CallbackInfo &info) { auto haystack = info[2].As().Utf8Value(); RecordStringFeedback(id, needle, haystack); + RecordStringCompareLog(JazzerLibAflCompareKind::kStringContainment, needle, + haystack); // libFuzzer currently ignores the result, which allows us to simply pass a // valid but arbitrary pointer here instead of performing an actual strstr @@ -107,8 +161,10 @@ void TraceIntegerCompare(const Napi::CallbackInfo &info) { auto id = info[0].As().Int64Value(); auto arg1 = info[1].As().Int64Value(); auto arg2 = info[2].As().Int64Value(); - RecordCompareFeedback(static_cast(id) ^ static_cast(arg1) ^ + RecordCompareFeedback(static_cast(id) ^ + static_cast(arg1) ^ (static_cast(arg2) << 1)); + RecordIntegerCompareLog(arg1, arg2); __sanitizer_cov_trace_const_cmp8_with_pc(id, arg1, arg2); } @@ -127,7 +183,8 @@ void TracePcIndir(const Napi::CallbackInfo &info) { void ClearCompareFeedbackMap(const Napi::CallbackInfo &info) { if (info.Length() != 0) { - throw Napi::Error::New(info.Env(), "This function does not accept arguments"); + throw Napi::Error::New(info.Env(), + "This function does not accept arguments"); } ClearCompareFeedbackMap(); @@ -135,19 +192,43 @@ void ClearCompareFeedbackMap(const Napi::CallbackInfo &info) { Napi::Value CountNonZeroCompareFeedbackSlots(const Napi::CallbackInfo &info) { if (info.Length() != 0) { - throw Napi::Error::New(info.Env(), "This function does not accept arguments"); + throw Napi::Error::New(info.Env(), + "This function does not accept arguments"); } - const auto count = static_cast(std::count_if( - gCompareFeedbackMap.begin(), gCompareFeedbackMap.end(), - [](uint8_t value) { return value != 0; })); + const auto count = static_cast( + std::count_if(gCompareFeedbackMap.begin(), gCompareFeedbackMap.end(), + [](uint8_t value) { return value != 0; })); return Napi::Number::New(info.Env(), count); } +Napi::Value CountCompareLogEntries(const Napi::CallbackInfo &info) { + if (info.Length() != 0) { + throw Napi::Error::New(info.Env(), + "This function does not accept arguments"); + } + + return Napi::Number::New(info.Env(), gCompareLog.used); +} + +Napi::Value CountDroppedCompareLogEntries(const Napi::CallbackInfo &info) { + if (info.Length() != 0) { + throw Napi::Error::New(info.Env(), + "This function does not accept arguments"); + } + + return Napi::Number::New(info.Env(), gCompareLog.dropped); +} + uint8_t *CompareFeedbackMap() { return gCompareFeedbackMap.data(); } std::size_t CompareFeedbackMapSize() { return gCompareFeedbackMap.size(); } void ClearCompareFeedbackMap() { std::memset(gCompareFeedbackMap.data(), 0, gCompareFeedbackMap.size()); + ClearCompareLog(); } + +JazzerLibAflCompareLog *CompareLog() { return &gCompareLog; } + +void ClearCompareLog() { std::memset(&gCompareLog, 0, sizeof(gCompareLog)); } diff --git a/packages/fuzzer/shared/tracing.h b/packages/fuzzer/shared/tracing.h index 744c603c4..03bef091f 100644 --- a/packages/fuzzer/shared/tracing.h +++ b/packages/fuzzer/shared/tracing.h @@ -17,6 +17,33 @@ #include #include +constexpr std::size_t kCompareFeedbackMapSize = 1 << 16; +constexpr std::size_t kCompareLogEntryBytes = 32; +constexpr std::size_t kCompareLogMaxEntries = 1024; + +enum class JazzerLibAflCompareKind : uint8_t { + kInteger = 1, + kStringEquality = 2, + kStringContainment = 3, +}; + +struct JazzerLibAflCompareLogEntry { + uint8_t kind; + uint8_t flags; + uint8_t left_len; + uint8_t right_len; + uint64_t left_value; + uint64_t right_value; + uint8_t left_bytes[kCompareLogEntryBytes]; + uint8_t right_bytes[kCompareLogEntryBytes]; +}; + +struct JazzerLibAflCompareLog { + uint32_t used; + uint32_t dropped; + JazzerLibAflCompareLogEntry entries[kCompareLogMaxEntries]; +}; + void TraceUnequalStrings(const Napi::CallbackInfo &info); void TraceStringContainment(const Napi::CallbackInfo &info); void TraceIntegerCompare(const Napi::CallbackInfo &info); @@ -24,7 +51,11 @@ void TracePcIndir(const Napi::CallbackInfo &info); void ClearCompareFeedbackMap(const Napi::CallbackInfo &info); Napi::Value CountNonZeroCompareFeedbackSlots(const Napi::CallbackInfo &info); +Napi::Value CountCompareLogEntries(const Napi::CallbackInfo &info); +Napi::Value CountDroppedCompareLogEntries(const Napi::CallbackInfo &info); uint8_t *CompareFeedbackMap(); std::size_t CompareFeedbackMapSize(); void ClearCompareFeedbackMap(); +JazzerLibAflCompareLog *CompareLog(); +void ClearCompareLog(); diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js index 0d47a57c1..ce1f4ea2d 100644 --- a/tests/engine/engine.test.js +++ b/tests/engine/engine.test.js @@ -16,6 +16,7 @@ const { spawnSync } = require("child_process"); const fs = require("fs"); +const os = require("os"); const path = require("path"); const { @@ -26,6 +27,45 @@ const { TimeoutExitCode, } = require("../helpers.js"); +async function withTempGuidanceDirectory(callback) { + const directory = await fs.promises.mkdtemp( + path.join(os.tmpdir(), "jazzer-libafl-guidance-"), + ); + try { + return await callback(directory); + } finally { + await fs.promises.rm(directory, { force: true, recursive: true }); + } +} + +function runLibAflCli(cwd, entryPoint, extraFuzzerOptions = []) { + const proc = spawnSync( + "npx", + [ + "jazzer", + "fuzz.js", + "-f", + entryPoint, + "--engine=afl", + "--sync", + "--disable_bug_detectors=.*", + "--", + ...extraFuzzerOptions, + ], + { + cwd, + env: { ...process.env }, + shell: true, + stdio: "pipe", + windowsHide: true, + }, + ); + return { + status: proc.status, + output: proc.stdout.toString() + proc.stderr.toString(), + }; +} + describe("Engine selection", () => { const testDirectory = __dirname; const jestProjectDirectory = path.join(testDirectory, "jest_project"); @@ -109,6 +149,104 @@ describe("Engine selection", () => { } }); + it("finds integer comparisons with LibAFL compare guidance", async () => { + await withTempGuidanceDirectory(async (directory) => { + const corpusDirectory = path.join(directory, "numeric-corpus"); + await fs.promises.mkdir(corpusDirectory, { recursive: true }); + await fs.promises.writeFile( + path.join(corpusDirectory, "seed"), + Buffer.alloc(4), + ); + + const { status, output } = runLibAflCli( + testDirectory, + "guided_numeric", + [ + corpusDirectory, + "-runs=4000", + "-seed=1337", + "-max_len=16", + `-artifact_prefix=${directory}${path.sep}`, + ], + ); + + expect(status).toBe(Number(FuzzingExitCode)); + expect(output).toContain("AFL numeric guidance finding"); + }); + }); + + it("promotes equality targets into LibAFL tokens", async () => { + await withTempGuidanceDirectory(async (directory) => { + const corpusDirectory = path.join(directory, "equality-corpus"); + await fs.promises.mkdir(corpusDirectory, { recursive: true }); + await fs.promises.writeFile(path.join(corpusDirectory, "seed"), ""); + + const { status, output } = runLibAflCli( + testDirectory, + "guided_equality", + [ + corpusDirectory, + "-runs=4000", + "-seed=1441", + "-max_len=32", + `-artifact_prefix=${directory}${path.sep}`, + ], + ); + + expect(status).toBe(Number(FuzzingExitCode)); + expect(output).toContain("AFL equality guidance finding"); + }); + }); + + it("promotes containment needles into LibAFL tokens", async () => { + await withTempGuidanceDirectory(async (directory) => { + const corpusDirectory = path.join(directory, "containment-corpus"); + await fs.promises.mkdir(corpusDirectory, { recursive: true }); + await fs.promises.writeFile(path.join(corpusDirectory, "seed"), ""); + + const { status, output } = runLibAflCli( + testDirectory, + "guided_containment", + [ + corpusDirectory, + "-runs=4000", + "-seed=1777", + "-max_len=32", + `-artifact_prefix=${directory}${path.sep}`, + ], + ); + + expect(status).toBe(Number(FuzzingExitCode)); + expect(output).toContain("AFL containment guidance finding"); + }); + }); + + it("uses dictionaries with LibAFL token mutations", async () => { + await withTempGuidanceDirectory(async (directory) => { + const corpusDirectory = path.join(directory, "dictionary-corpus"); + const dictionaryPath = path.join(directory, "tokens.dict"); + await fs.promises.mkdir(corpusDirectory, { recursive: true }); + await fs.promises.writeFile(path.join(corpusDirectory, "seed"), ""); + await fs.promises.writeFile(dictionaryPath, '"from-dictionary"\n'); + + const { status, output } = runLibAflCli( + testDirectory, + "dictionary_target", + [ + corpusDirectory, + "-runs=4000", + "-seed=2333", + "-max_len=32", + `-dict=${dictionaryPath}`, + `-artifact_prefix=${directory}${path.sep}`, + ], + ); + + expect(status).toBe(Number(FuzzingExitCode)); + expect(output).toContain("AFL dictionary guidance finding"); + }); + }); + it("fails fast on asynchronous hangs in LibAFL mode", async () => { const fuzzTest = new FuzzTestBuilder() .dir(testDirectory) diff --git a/tests/engine/fuzz.js b/tests/engine/fuzz.js index dbef78c09..41d7097e8 100644 --- a/tests/engine/fuzz.js +++ b/tests/engine/fuzz.js @@ -37,3 +37,36 @@ module.exports.regression = function (data) { throw new Error("AFL regression finding"); } }; + +module.exports.guided_numeric = function (data) { + if (data.length < 4) { + return; + } + + const value = data.readUInt32LE(0); + if (Fuzzer.tracer.traceNumberCmp(value, 0x41424344, "===", 2001)) { + throw new Error("AFL numeric guidance finding"); + } +}; + +module.exports.guided_equality = function (data) { + const text = data.toString("utf8"); + Fuzzer.tracer.guideTowardsEquality(text, "libafl=eq", 2002); + if (text === "libafl=eq") { + throw new Error("AFL equality guidance finding"); + } +}; + +module.exports.guided_containment = function (data) { + const text = data.toString("utf8"); + Fuzzer.tracer.guideTowardsContainment("afl-token", text, 2003); + if (text.includes("afl-token")) { + throw new Error("AFL containment guidance finding"); + } +}; + +module.exports.dictionary_target = function (data) { + if (data.toString("utf8").includes("from-dictionary")) { + throw new Error("AFL dictionary guidance finding"); + } +}; From 34a703b27af90b9a05d71ae073839a253c8828d6 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 13:44:51 +0200 Subject: [PATCH 12/30] test: cap Jest worker usage Run the root Jest suites with --maxWorkers=25% instead of the\nprevious fully serial setup so local runs stay responsive without\nspiking CPU. --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a3ec98eb2..927e16cc2 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,9 @@ "test:default": "npm run test:jest", "test:linux:darwin": "npm run test:jest && cd tests && sh ../scripts/run_all.sh fuzz", "test:win32": "npm run test:jest && cd tests && ..\\scripts\\run_all.bat fuzz", - "test:jest": "jest --runInBand && npm run test --ws --if-present -- --runInBand", - "test:jest:coverage": "jest --coverage --runInBand", - "test:jest:watch": "jest --watch", + "test:jest": "jest --maxWorkers=25% && npm run test --ws --if-present -- --maxWorkers=25%", + "test:jest:coverage": "jest --coverage --maxWorkers=25%", + "test:jest:watch": "jest --watch --maxWorkers=25%", "example": "run-script-os", "example:linux:darwin": "cd examples && sh ../scripts/run_all.sh dryRun", "example:win32": "cd examples && ..\\scripts\\run_all.bat dryRun", From a31ef868990e0b65552ddf038e8f82b16ba076fa Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 14:08:06 +0200 Subject: [PATCH 13/30] feat(fuzzer): calibrate LibAFL queue entries Run LibAFL calibration before shadow tracing and mutation so queue\nentries always collect exec-time metadata in the Node runtime. --- packages/fuzzer/rust/src/lib.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 6f2478a0e..648ef369b 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -23,7 +23,9 @@ use libafl::{ }, observers::{CanTrack, HitcountsMapObserver, StdMapObserver}, schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler}, - stages::{mutational::StdMutationalStage, shadow::ShadowTracingStage}, + stages::{ + calibrate::CalibrationStage, mutational::StdMutationalStage, shadow::ShadowTracingStage, + }, state::{HasCorpus, HasExecutions, HasMaxSize, HasSolutions, StdState}, Error, HasMetadata, }; @@ -357,6 +359,7 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( } } + let calibration_stage = CalibrationStage::ignore_stability(&feedback); let scheduler = IndexesLenTimeMinimizerScheduler::new(&edges_observer, QueueScheduler::new()); let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); let mutator = HavocScheduledMutator::new( @@ -364,7 +367,11 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( .merge(tokens_mutations()) .merge(tuple_list!(I2SRandReplace::new())), ); - let mut stages = tuple_list!(ShadowTracingStage::new(), StdMutationalStage::new(mutator),); + let mut stages = tuple_list!( + calibration_stage, + ShadowTracingStage::new(), + StdMutationalStage::new(mutator), + ); let stop_requested = Cell::new(false); let fatal_error = Cell::new(false); let timeout_found = Cell::new(false); From d7076418b3a7111b7db6d546556a8a1d67738e78 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 14:10:53 +0200 Subject: [PATCH 14/30] feat(fuzzer): switch LibAFL to power queue scheduling Use PowerQueueScheduler::fast() under the minimizer wrapper and\nkeep corpus insertion on the scheduler path so power metadata is\nalways initialized. Preserve support for uninstrumented callbacks by marking one\nsynthetic edge only when a target leaves the coverage map empty. --- packages/fuzzer/rust/src/lib.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 648ef369b..3ebf36bde 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -22,7 +22,9 @@ use libafl::{ I2SRandReplace, Tokens, }, observers::{CanTrack, HitcountsMapObserver, StdMapObserver}, - schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler}, + schedulers::{ + powersched::PowerSchedule, IndexesLenTimeMinimizerScheduler, PowerQueueScheduler, + }, stages::{ calibrate::CalibrationStage, mutational::StdMutationalStage, shadow::ShadowTracingStage, }, @@ -205,6 +207,22 @@ fn clear_compare_log(ptr: *mut JazzerLibAflCompareLog) { } } +fn ensure_non_empty_edge_map(ptr: *mut u8, len: usize) { + if ptr.is_null() || len == 0 { + return; + } + + unsafe { + let map = std::slice::from_raw_parts_mut(ptr, len); + if map.iter().all(|slot| *slot == 0) { + // Power scheduling rejects corpus entries that never hit any edge. + // Preserve the old behavior for uninstrumented callbacks by marking + // one synthetic edge only when the target left the map untouched. + map[0] = 1; + } + } +} + unsafe fn parse_corpus_directories(options: &JazzerLibAflRuntimeOptions) -> Option> { if options.corpus_directories.is_null() || options.corpus_directories_len == 0 { return Some(Vec::new()); @@ -360,7 +378,10 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( } let calibration_stage = CalibrationStage::ignore_stability(&feedback); - let scheduler = IndexesLenTimeMinimizerScheduler::new(&edges_observer, QueueScheduler::new()); + let scheduler = IndexesLenTimeMinimizerScheduler::new( + &edges_observer, + PowerQueueScheduler::new(&mut state, &edges_observer, PowerSchedule::fast()), + ); let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); let mutator = HavocScheduledMutator::new( havoc_mutations() @@ -385,6 +406,7 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let bytes = bytes.as_slice(); let size = bytes.len().min(options.max_len); let status = unsafe { execute_one(user_data, bytes.as_ptr(), size) }; + ensure_non_empty_edge_map(maps.edges, maps.edges_len); match status { EXECUTION_CONTINUE => ExitKind::Ok, EXECUTION_FINDING => ExitKind::Crash, From e46a15b11fad015e6f15d9ebdea805bb2524f0c2 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 14:44:29 +0200 Subject: [PATCH 15/30] feat(fuzzer): use LibAFL power mutational stage Spend mutation budget according to LibAFL power scores while\nkeeping the existing scheduler, calibration, and compare-guided\nmutation stack unchanged. --- packages/fuzzer/rust/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 3ebf36bde..511d90b0d 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -25,9 +25,7 @@ use libafl::{ schedulers::{ powersched::PowerSchedule, IndexesLenTimeMinimizerScheduler, PowerQueueScheduler, }, - stages::{ - calibrate::CalibrationStage, mutational::StdMutationalStage, shadow::ShadowTracingStage, - }, + stages::{calibrate::CalibrationStage, shadow::ShadowTracingStage, StdPowerMutationalStage}, state::{HasCorpus, HasExecutions, HasMaxSize, HasSolutions, StdState}, Error, HasMetadata, }; @@ -391,7 +389,7 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let mut stages = tuple_list!( calibration_stage, ShadowTracingStage::new(), - StdMutationalStage::new(mutator), + StdPowerMutationalStage::new(mutator), ); let stop_requested = Cell::new(false); let fatal_error = Cell::new(false); From 4a8ccaad31ad373167f185c1c7e4fa1a0090b78b Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 14:47:18 +0200 Subject: [PATCH 16/30] test(bench): add LibAFL anomaly smoke checks Add manual compare-guided and async smoke runs that report wall\ntime and exec/sec, and fail only on obviously abnormal LibAFL\nbehavior. --- benchmarks/engine_smoke/anomaly.js | 189 ++++++++++++++++++++++++ benchmarks/engine_smoke/anomaly_fuzz.js | 32 ++++ benchmarks/engine_smoke/package.json | 3 +- 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 benchmarks/engine_smoke/anomaly.js create mode 100644 benchmarks/engine_smoke/anomaly_fuzz.js diff --git a/benchmarks/engine_smoke/anomaly.js b/benchmarks/engine_smoke/anomaly.js new file mode 100644 index 000000000..56791334e --- /dev/null +++ b/benchmarks/engine_smoke/anomaly.js @@ -0,0 +1,189 @@ +/* + * Copyright 2026 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. + */ + +const { spawnSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const benchmarkDirectory = __dirname; +const workDirectory = path.join(benchmarkDirectory, "work", "anomalies"); +const engineTarget = path.join( + benchmarkDirectory, + "..", + "..", + "tests", + "engine", + "fuzz.js", +); +const asyncTarget = path.join(benchmarkDirectory, "anomaly_fuzz.js"); + +function removeIfExists(targetPath) { + fs.rmSync(targetPath, { force: true, recursive: true }); +} + +function ensureDirectory(targetPath) { + fs.mkdirSync(targetPath, { recursive: true }); +} + +function runCommand(label, args, cwd, outputDirectory, expectedStatus = 0) { + console.log(`\n[anomaly] ${label}`); + console.log(`[anomaly] command: npx ${args.join(" ")}`); + ensureDirectory(outputDirectory); + const sanitizedLabel = label.replace(/[^a-z0-9]+/gi, "-").toLowerCase(); + const stdoutPath = path.join(outputDirectory, `${sanitizedLabel}.stdout.log`); + const stderrPath = path.join(outputDirectory, `${sanitizedLabel}.stderr.log`); + const stdoutFd = fs.openSync(stdoutPath, "w"); + const stderrFd = fs.openSync(stderrPath, "w"); + const startedAt = Date.now(); + const proc = spawnSync("npx", args, { + cwd, + env: { ...process.env }, + shell: true, + stdio: ["ignore", stdoutFd, stderrFd], + windowsHide: true, + }); + const elapsedMs = Date.now() - startedAt; + fs.closeSync(stdoutFd); + fs.closeSync(stderrFd); + + if (proc.status !== expectedStatus) { + throw new Error( + `${label} failed with exit code ${proc.status}\nSTDOUT (${stdoutPath}):\n${fs.readFileSync(stdoutPath, "utf8")}\nSTDERR (${stderrPath}):\n${fs.readFileSync(stderrPath, "utf8")}`, + ); + } + + return { + elapsedMs, + stderrPath, + stdoutPath, + }; +} + +function parseExecsPerSecond(stderrPath) { + const stderr = fs.readFileSync(stderrPath, "utf8"); + const match = stderr.match(/\[libafl::done\].*exec\/sec: ([0-9.]+)/); + if (!match) { + throw new Error(`No LibAFL done line found in ${stderrPath}`); + } + return Number.parseFloat(match[1]); +} + +function runGuidedNumericSmoke() { + const outputDirectory = path.join(workDirectory, "guided-numeric"); + const corpusDirectory = path.join(outputDirectory, "corpus"); + removeIfExists(outputDirectory); + ensureDirectory(corpusDirectory); + fs.writeFileSync(path.join(corpusDirectory, "seed"), Buffer.alloc(4)); + + const result = runCommand( + "guided numeric solve", + [ + "jazzer", + engineTarget, + "-f", + "guided_numeric", + "--engine=afl", + "--sync", + "--disable_bug_detectors=.*", + "--", + corpusDirectory, + "-runs=4000", + "-seed=1337", + "-max_len=16", + `-artifact_prefix=${outputDirectory}${path.sep}`, + ], + benchmarkDirectory, + outputDirectory, + 77, + ); + + const output = + fs.readFileSync(result.stdoutPath, "utf8") + + fs.readFileSync(result.stderrPath, "utf8"); + if (!output.includes("AFL numeric guidance finding")) { + throw new Error("Guided numeric smoke did not report the expected finding"); + } + + return { + name: "guided-numeric", + elapsedMs: result.elapsedMs, + }; +} + +function runAsyncSmoke() { + const outputDirectory = path.join(workDirectory, "async-smoke"); + const corpusDirectory = path.join(outputDirectory, "corpus"); + removeIfExists(outputDirectory); + ensureDirectory(corpusDirectory); + fs.writeFileSync(path.join(corpusDirectory, "seed"), "async-seed"); + + const result = runCommand( + "async throughput smoke", + [ + "jazzer", + asyncTarget, + "-f", + "async_smoke", + "--engine=afl", + "--disable_bug_detectors=.*", + "--", + corpusDirectory, + "-runs=2000", + "-seed=9001", + "-max_len=128", + `-artifact_prefix=${outputDirectory}${path.sep}`, + ], + benchmarkDirectory, + outputDirectory, + ); + + const execsPerSecond = parseExecsPerSecond(result.stderrPath); + if (execsPerSecond <= 0) { + throw new Error("Async smoke reported a non-positive exec/sec rate"); + } + if (result.elapsedMs > 30000) { + throw new Error( + `Async smoke took unexpectedly long: ${result.elapsedMs} ms`, + ); + } + + return { + name: "async-smoke", + elapsedMs: result.elapsedMs, + execsPerSecond, + }; +} + +function main() { + ensureDirectory(workDirectory); + const results = [runGuidedNumericSmoke(), runAsyncSmoke()]; + for (const result of results) { + const stats = [`elapsed_ms=${result.elapsedMs}`]; + if (result.execsPerSecond !== undefined) { + stats.push(`execs_per_second=${result.execsPerSecond}`); + } + console.log(`[anomaly] ${result.name}: ${stats.join(" ")}`); + } + fs.writeFileSync( + path.join(workDirectory, "results.json"), + JSON.stringify(results, null, 2), + ); + console.log( + `\n[anomaly] wrote machine-readable results to ${path.join(workDirectory, "results.json")}`, + ); +} + +main(); diff --git a/benchmarks/engine_smoke/anomaly_fuzz.js b/benchmarks/engine_smoke/anomaly_fuzz.js new file mode 100644 index 000000000..fd31e8624 --- /dev/null +++ b/benchmarks/engine_smoke/anomaly_fuzz.js @@ -0,0 +1,32 @@ +/* + * Copyright 2026 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. + */ + +module.exports.async_smoke = function (data) { + let checksum = 0; + for (const byte of data) { + checksum = ((checksum * 33) ^ byte) & 0xffff; + } + + return new Promise((resolve) => { + setImmediate(() => { + if (checksum === 0x1337) { + // Exercise an extra branch without turning this into a finding target. + checksum ^= data.length; + } + resolve(checksum); + }); + }); +}; diff --git a/benchmarks/engine_smoke/package.json b/benchmarks/engine_smoke/package.json index 31b0156af..aa448401c 100644 --- a/benchmarks/engine_smoke/package.json +++ b/benchmarks/engine_smoke/package.json @@ -4,7 +4,8 @@ "private": true, "description": "Manual 30-second smoke benchmark for libFuzzer vs LibAFL.", "scripts": { - "smoke": "node run.js" + "smoke": "node run.js", + "smoke:anomalies": "node anomaly.js" }, "devDependencies": { "@jazzer.js/core": "file:../../packages/core", From 7d1f6b551f0593df029bb371e3b5e38e41498030 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 17:34:26 +0200 Subject: [PATCH 17/30] feat(fuzzer): add structured LibAFL status output Wire LibAFL progress reporting into the custom runtime loop and\nrender aligned testcase, heartbeat, objective, and done output\nwith a shared formatter. Route finding details through the Rust monitor so artifacts and JS\nerror summaries stay in sync, and cover the new CLI output in the\nengine integration tests. --- packages/fuzzer/libafl_runtime.cpp | 133 +++++++++- packages/fuzzer/libafl_runtime.h | 10 + packages/fuzzer/rust/src/lib.rs | 390 ++++++++++++++++++++++++----- tests/engine/engine.test.js | 63 ++++- 4 files changed, 515 insertions(+), 81 deletions(-) diff --git a/packages/fuzzer/libafl_runtime.cpp b/packages/fuzzer/libafl_runtime.cpp index db103adea..4a45fe1f6 100644 --- a/packages/fuzzer/libafl_runtime.cpp +++ b/packages/fuzzer/libafl_runtime.cpp @@ -17,11 +17,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -207,6 +209,103 @@ using AsyncTsfn = SyncFuzzTargetContext *gActiveSyncContext = nullptr; AsyncFuzzTargetContext *gActiveAsyncContext = nullptr; AsyncTsfn gAsyncTsfn; +JazzerLibAflFindingInfo gFindingInfo{}; + +void ClearFindingInfo() { std::memset(&gFindingInfo, 0, sizeof(gFindingInfo)); } + +void CopyFindingField(char *destination, size_t destination_size, + const std::string &value) { + if (destination == nullptr || destination_size == 0) { + return; + } + + std::memset(destination, 0, destination_size); + const auto copied = std::min(destination_size - 1, value.size()); + if (copied > 0) { + std::memcpy(destination, value.data(), copied); + } +} + +std::string CollapseWhitespace(const std::string &value) { + std::string collapsed; + collapsed.reserve(value.size()); + + bool previous_was_space = false; + for (const auto character : value) { + if (std::isspace(static_cast(character)) != 0) { + if (!collapsed.empty() && !previous_was_space) { + collapsed.push_back(' '); + } + previous_was_space = true; + continue; + } + + collapsed.push_back(character); + previous_was_space = false; + } + + if (!collapsed.empty() && collapsed.back() == ' ') { + collapsed.pop_back(); + } + + return collapsed; +} + +std::string TrimStackFrame(const std::string &frame) { + const auto first = frame.find_first_not_of(" \t"); + if (first == std::string::npos) { + return ""; + } + + auto trimmed = frame.substr(first); + constexpr char kAtPrefix[] = "at "; + if (trimmed.rfind(kAtPrefix, 0) == 0) { + trimmed.erase(0, sizeof(kAtPrefix) - 1); + } + + if (!trimmed.empty() && trimmed.back() == ')') { + const auto open_paren = trimmed.rfind('('); + if (open_paren != std::string::npos && open_paren + 1 < trimmed.size()) { + return trimmed.substr(open_paren + 1, trimmed.size() - open_paren - 2); + } + } + + return trimmed; +} + +std::string DescribeJsError(Napi::Env env, const Napi::Value &error) { + std::string summary = error.ToString().Utf8Value(); + if (!error.IsObject()) { + return CollapseWhitespace(summary); + } + + const auto stack_value = error.As().Get("stack"); + if (!stack_value.IsString()) { + return CollapseWhitespace(summary); + } + + std::istringstream stream(stack_value.As().Utf8Value()); + std::string line; + std::getline(stream, line); + while (std::getline(stream, line)) { + const auto frame = TrimStackFrame(line); + if (frame.empty()) { + continue; + } + summary.append(" in ").append(frame); + break; + } + + return CollapseWhitespace(summary); +} + +void RecordFindingInfo(const std::string &artifact, + const std::string &summary) { + gFindingInfo.has_value = 1; + CopyFindingField(gFindingInfo.artifact, sizeof(gFindingInfo.artifact), + artifact); + CopyFindingField(gFindingInfo.summary, sizeof(gFindingInfo.summary), summary); +} std::string DigestInput(const uint8_t *data, size_t size) { uint64_t hash = 1469598103934665603ULL; @@ -253,10 +352,11 @@ std::filesystem::path ArtifactPath(const std::string &artifact_prefix, return std::filesystem::path(artifact_prefix + filename); } -void WriteArtifact(const std::string &artifact_prefix, const std::string &kind, - const uint8_t *data, size_t size) { +std::string WriteArtifact(const std::string &artifact_prefix, + const std::string &kind, const uint8_t *data, + size_t size, bool emit_info = true) { if (data == nullptr && size != 0) { - return; + return ""; } try { @@ -272,7 +372,7 @@ void WriteArtifact(const std::string &artifact_prefix, const std::string &kind, if (!output.is_open()) { std::cerr << "ERROR: Failed to open artifact file '" << artifact_path.string() << "'" << std::endl; - return; + return ""; } if (size > 0) { @@ -282,14 +382,18 @@ void WriteArtifact(const std::string &artifact_prefix, const std::string &kind, if (!output.good()) { std::cerr << "ERROR: Failed to write artifact file '" << artifact_path.string() << "'" << std::endl; - return; + return ""; } - std::cerr << "INFO: Wrote " << kind << " input to " - << artifact_path.string() << std::endl; + if (emit_info) { + std::cerr << "INFO: Wrote " << kind << " input to " + << artifact_path.string() << std::endl; + } + return artifact_path.filename().string(); } catch (const std::exception &exception) { std::cerr << "ERROR: Failed to persist " << kind << " artifact: " << exception.what() << std::endl; + return ""; } } @@ -336,8 +440,10 @@ void ReportAsyncFinding(AsyncFuzzTargetContext *context, Napi::Env env, const Napi::Value &error, const std::vector &input) { if (TrySetExecutionStatus(state, kExecutionFinding)) { - WriteArtifact(context->options.artifact_prefix, "crash", input.data(), - input.size()); + const auto artifact = + WriteArtifact(context->options.artifact_prefix, "crash", input.data(), + input.size(), false); + RecordFindingInfo(artifact, DescribeJsError(env, error)); } RejectDeferredIfNeeded(context, error); } @@ -444,15 +550,16 @@ JazzerLibAflRuntimeSharedMaps SharedMapsForRuntime(Napi::Env env) { auto *cmp = CompareFeedbackMap(); const auto cmp_len = CompareFeedbackMapSize(); auto *compare_log = CompareLog(); + auto *finding_info = &gFindingInfo; if (edges == nullptr || edges_len == 0 || cmp == nullptr || cmp_len == 0 || - compare_log == nullptr) { + compare_log == nullptr || finding_info == nullptr) { throw Napi::Error::New( env, "Coverage maps were not initialized before the LibAFL backend started"); } - return {edges, edges_len, cmp, cmp_len, compare_log}; + return {edges, edges_len, cmp, cmp_len, compare_log, finding_info}; } bool CollectRegressionCorpusFiles( @@ -768,7 +875,9 @@ int ExecuteSyncInput(void *user_data, const uint8_t *data, size_t size) { } } catch (const Napi::Error &error) { if (!context->is_resolved) { - WriteArtifact(context->options.artifact_prefix, "crash", data, size); + const auto artifact = WriteArtifact(context->options.artifact_prefix, + "crash", data, size, false); + RecordFindingInfo(artifact, DescribeJsError(context->env, error.Value())); context->is_resolved = true; context->deferred.Reject(error.Value()); } diff --git a/packages/fuzzer/libafl_runtime.h b/packages/fuzzer/libafl_runtime.h index 2fad26912..de1e25ed2 100644 --- a/packages/fuzzer/libafl_runtime.h +++ b/packages/fuzzer/libafl_runtime.h @@ -20,7 +20,16 @@ #include "shared/tracing.h" +constexpr std::size_t kFindingInfoArtifactBytes = 256; +constexpr std::size_t kFindingInfoSummaryBytes = 1024; + extern "C" { +struct JazzerLibAflFindingInfo { + uint8_t has_value; + char artifact[kFindingInfoArtifactBytes]; + char summary[kFindingInfoSummaryBytes]; +}; + struct JazzerLibAflRuntimeOptions { uint64_t runs; uint64_t seed; @@ -39,6 +48,7 @@ struct JazzerLibAflRuntimeSharedMaps { uint8_t *cmp; size_t cmp_len; JazzerLibAflCompareLog *compare_log; + JazzerLibAflFindingInfo *finding_info; }; typedef int (*JazzerLibAflExecuteCallback)(void *user_data, const uint8_t *data, diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 511d90b0d..b21a52290 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -2,21 +2,26 @@ mod compare_log; use core::ffi::{c_char, c_void}; use core::ptr; -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::ffi::CStr; use std::fs; +use std::io::IsTerminal; use std::path::PathBuf; +use std::rc::Rc; use std::time::{Duration, Instant}; use libafl::{ corpus::{CachedOnDiskCorpus, Corpus, InMemoryCorpus}, - events::SimpleEventManager, + events::{ProgressReporter, SimpleEventManager}, executors::{inprocess::InProcessExecutor, ExitKind, ShadowExecutor}, feedback_or_fast, feedbacks::{CrashFeedback, MaxMapFeedback, TimeoutFeedback}, fuzzer::{Evaluator, Fuzzer, StdFuzzer}, inputs::{BytesInput, HasTargetBytes}, - monitors::{stats::ClientStatsManager, Monitor}, + monitors::{ + stats::{ClientStatsManager, UserStats, UserStatsValue}, + Monitor, + }, mutators::{ havoc_mutations::havoc_mutations, scheduled::HavocScheduledMutator, tokens_mutations, I2SRandReplace, Tokens, @@ -49,6 +54,18 @@ const RUNTIME_STOPPED: i32 = 2; const RUNTIME_FATAL: i32 = 3; const RUNTIME_FOUND_TIMEOUT: i32 = 4; +const FINDING_INFO_ARTIFACT_BYTES: usize = 256; +const FINDING_INFO_SUMMARY_BYTES: usize = 1024; +const EXECUTION_FIELD_WIDTH: usize = 10; +const DEFAULT_MONITOR_TIMEOUT: Duration = Duration::from_secs(15); + +#[repr(C)] +pub struct JazzerLibAflFindingInfo { + pub has_value: u8, + pub artifact: [u8; FINDING_INFO_ARTIFACT_BYTES], + pub summary: [u8; FINDING_INFO_SUMMARY_BYTES], +} + #[repr(C)] pub struct JazzerLibAflRuntimeOptions { pub runs: u64, @@ -69,12 +86,64 @@ pub struct JazzerLibAflRuntimeSharedMaps { pub cmp: *mut u8, pub cmp_len: usize, pub compare_log: *mut JazzerLibAflCompareLog, + pub finding_info: *mut JazzerLibAflFindingInfo, } pub type JazzerLibAflExecuteCallback = unsafe extern "C" fn(user_data: *mut c_void, data: *const u8, size: usize) -> i32; -struct LibAflMonitor; +#[derive(Clone, Copy)] +struct RatioMetric { + numerator: u64, + denominator: u64, +} + +#[derive(Clone, Copy)] +struct ProgressSnapshot { + executions: u64, + edges: Option, + corpus_size: u64, + execs_per_sec: f64, + objective_size: u64, + stability: Option, + elapsed: Duration, +} + +struct MonitorState { + colors_enabled: bool, + last_progress: Option, +} + +#[derive(Clone, Copy)] +enum StatusEvent { + Testcase, + Heartbeat, + Objective, + Done, +} + +#[derive(Clone)] +struct LibAflMonitor { + state: Rc>, + finding_info: *mut JazzerLibAflFindingInfo, +} + +impl LibAflMonitor { + fn new(finding_info: *mut JazzerLibAflFindingInfo) -> (Self, Rc>) { + let state = Rc::new(RefCell::new(MonitorState { + colors_enabled: should_colorize_output(), + last_progress: None, + })); + + ( + Self { + state: state.clone(), + finding_info, + }, + state, + ) + } +} impl Monitor for LibAflMonitor { fn display( @@ -83,47 +152,33 @@ impl Monitor for LibAflMonitor { event_msg: &str, sender_id: ClientId, ) -> Result<(), Error> { - let Some(event_name) = (match event_msg { - "Client Heartbeat" => Some("heartbeat"), - "Testcase" => Some("testcase"), - "Objective" => Some("objective"), - "Log" => Some("log"), + let Some(event) = (match event_msg { + "Client Heartbeat" | "PerfMonitor" => Some(StatusEvent::Heartbeat), + "Testcase" => Some(StatusEvent::Testcase), + "Objective" => Some(StatusEvent::Objective), _ => None, }) else { return Ok(()); }; - let (run_time_pretty, corpus_size, objective_size, total_execs, execs_per_sec_pretty) = { - let global_stats = client_stats_manager.global_stats(); - ( - global_stats.run_time_pretty.clone(), - global_stats.corpus_size, - global_stats.objective_size, - global_stats.total_execs, - global_stats.execs_per_sec_pretty.clone(), - ) - }; - let mut user_stats = client_stats_manager - .client_stats_for(sender_id)? - .user_stats() - .iter() - .map(|(key, value)| format!("{key}: {value}")) - .collect::>(); - user_stats.sort(); - let extra = if user_stats.is_empty() { - String::new() - } else { - format!(", {}", user_stats.join(", ")) - }; + let snapshot = build_progress_snapshot(client_stats_manager, sender_id)?; + let colors_enabled = self.state.borrow().colors_enabled; + self.state.borrow_mut().last_progress = Some(snapshot); + + match event { + StatusEvent::Objective => { + let finding_info = read_finding_info(self.finding_info); + eprintln!( + "{}", + format_objective_line(snapshot.executions, finding_info, colors_enabled), + ); + } + StatusEvent::Testcase | StatusEvent::Heartbeat => { + eprintln!("{}", format_progress_line(event, snapshot, colors_enabled),); + } + StatusEvent::Done => unreachable!(), + } - eprintln!( - "[libafl::{event_name}] run time: {}, corpus: {}, objectives: {}, executions: {}, exec/sec: {}{extra}", - run_time_pretty, - corpus_size, - objective_size, - total_execs, - execs_per_sec_pretty, - ); Ok(()) } } @@ -135,53 +190,235 @@ fn format_duration(duration: Duration) -> String { let seconds = total_seconds % 60; if hours > 0 { - format!("{hours}h {minutes}m {seconds}s") + format!("{hours}h{minutes:02}m{seconds:02}s") } else if minutes > 0 { - format!("{minutes}m {seconds}s") + format!("{minutes}m{seconds:02}s") } else { format!("{seconds}s") } } -fn print_runtime_start(options: &JazzerLibAflRuntimeOptions, loaded_inputs: usize) { - let runs = if options.runs == 0 { - "unlimited".to_string() - } else { - options.runs.to_string() +fn should_colorize_output() -> bool { + if std::env::var_os("NO_COLOR").is_some() { + return false; + } + + if matches!(std::env::var("TERM"), Ok(term) if term == "dumb") { + return false; + } + + std::io::stderr().is_terminal() +} + +fn monitor_timeout() -> Duration { + match std::env::var("JAZZER_LIBAFL_MONITOR_TIMEOUT_MS") { + Ok(value) => value + .parse::() + .ok() + .filter(|timeout| *timeout > 0) + .map(Duration::from_millis) + .unwrap_or(DEFAULT_MONITOR_TIMEOUT), + Err(_) => DEFAULT_MONITOR_TIMEOUT, + } +} + +fn ratio_from_user_stat(user_stat: Option<&UserStats>) -> Option { + let UserStatsValue::Ratio(numerator, denominator) = user_stat?.value() else { + return None; }; - let max_total_time = if options.max_total_time_seconds == 0 { - "unlimited".to_string() + Some(RatioMetric { + numerator: *numerator, + denominator: *denominator, + }) +} + +fn format_ratio_metric(metric: Option) -> String { + let Some(metric) = metric else { + return " -/ - ( -%)".to_string(); + }; + + if metric.denominator == 0 { + return format!("{:>4}/{:<4} ( -%)", metric.numerator, metric.denominator); + } + + let percentage = metric.numerator.saturating_mul(100) / metric.denominator; + format!( + "{:>4}/{:<4} ({:>3}%)", + metric.numerator, metric.denominator, percentage + ) +} + +fn colorize_marker(marker: &str, sgr_code: &str, colors_enabled: bool) -> String { + if colors_enabled { + format!("\x1b[{sgr_code}m{marker}\x1b[0m") } else { - format_duration(Duration::from_secs(options.max_total_time_seconds)) + marker.to_string() + } +} + +fn marker_for_event(event: StatusEvent, colors_enabled: bool) -> String { + match event { + StatusEvent::Testcase => colorize_marker("[+]", "32", colors_enabled), + StatusEvent::Heartbeat => colorize_marker("[*]", "2", colors_enabled), + StatusEvent::Objective => colorize_marker("[!]", "1;31", colors_enabled), + StatusEvent::Done => colorize_marker("[=]", "34", colors_enabled), + } +} + +fn build_progress_snapshot( + client_stats_manager: &mut ClientStatsManager, + sender_id: ClientId, +) -> Result { + let (executions, corpus_size, execs_per_sec, objective_size, elapsed) = { + let global_stats = client_stats_manager.global_stats(); + ( + global_stats.total_execs, + global_stats.corpus_size, + global_stats.execs_per_sec, + global_stats.objective_size, + global_stats.run_time, + ) + }; + let client_stats = client_stats_manager.client_stats_for(sender_id)?; + Ok(ProgressSnapshot { + executions, + edges: ratio_from_user_stat(client_stats.get_user_stats("edges")), + corpus_size, + execs_per_sec, + objective_size, + stability: ratio_from_user_stat(client_stats.get_user_stats("stability")), + elapsed, + }) +} + +fn format_progress_line( + event: StatusEvent, + snapshot: ProgressSnapshot, + colors_enabled: bool, +) -> String { + format!( + "{} #{:4} | exec/s: {:>6.1} | obj: {:>3} | stab: {} | t: {}", + marker_for_event(event, colors_enabled), + snapshot.executions, + format_ratio_metric(snapshot.edges), + snapshot.corpus_size, + if snapshot.execs_per_sec.is_finite() { + snapshot.execs_per_sec + } else { + 0.0 + }, + snapshot.objective_size, + format_ratio_metric(snapshot.stability), + format_duration(snapshot.elapsed), + width = EXECUTION_FIELD_WIDTH, + ) +} + +#[derive(Clone)] +struct FindingInfo { + artifact: Option, + summary: Option, +} + +fn read_zero_terminated_string(bytes: &[u8]) -> Option { + let len = bytes + .iter() + .position(|byte| *byte == 0) + .unwrap_or(bytes.len()); + if len == 0 { + return None; + } + + Some(String::from_utf8_lossy(&bytes[..len]).into_owned()) +} + +fn read_finding_info(finding_info: *mut JazzerLibAflFindingInfo) -> FindingInfo { + let Some(finding_info) = (unsafe { finding_info.as_ref() }) else { + return FindingInfo { + artifact: None, + summary: None, + }; }; - eprintln!( - "[libafl::start] mode: fuzzing, seed: {}, loaded_inputs: {}, timeout: {} ms, max_len: {}, runs: {}, max_total_time: {}", - options.seed, loaded_inputs, options.timeout_millis, options.max_len, runs, max_total_time, - ); + if finding_info.has_value == 0 { + return FindingInfo { + artifact: None, + summary: None, + }; + } + + FindingInfo { + artifact: read_zero_terminated_string(&finding_info.artifact), + summary: read_zero_terminated_string(&finding_info.summary), + } +} + +fn format_objective_line( + executions: u64, + finding_info: FindingInfo, + colors_enabled: bool, +) -> String { + let artifact = finding_info + .artifact + .unwrap_or_else(|| "".to_string()); + let summary = finding_info + .summary + .unwrap_or_else(|| "finding".to_string()); + format!( + "{} #{:, + colors_enabled: bool, ) { let elapsed = started_at.elapsed(); let elapsed_seconds = elapsed.as_secs_f64(); let execs_per_sec = if elapsed_seconds > 0.0 { executions as f64 / elapsed_seconds } else { - executions as f64 + 0.0 }; + let edges = last_progress.and_then(|snapshot| snapshot.edges); eprintln!( - "[libafl::done] mode: fuzzing, run time: {}, corpus: {}, objectives: {}, executions: {}, exec/sec: {:.0}", + "{} #{:= options.runs { - break; + break "runs"; } if let Some(max_total_time) = max_total_time { if started_at.elapsed() >= max_total_time { status = RUNTIME_STOPPED; - break; + break "max_total_time"; } } @@ -504,15 +759,18 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( if stop_requested.get() { status = RUNTIME_STOPPED; - break; + break "stop_requested"; } - } + }; + let monitor_state = monitor_state.borrow(); print_runtime_done( + done_reason, started_at, - state.executions().saturating_sub(initial_executions), - state.corpus().count(), + *state.executions(), state.solutions().count(), + monitor_state.last_progress, + monitor_state.colors_enabled, ); status diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js index ce1f4ea2d..8d96b28fb 100644 --- a/tests/engine/engine.test.js +++ b/tests/engine/engine.test.js @@ -38,7 +38,7 @@ async function withTempGuidanceDirectory(callback) { } } -function runLibAflCli(cwd, entryPoint, extraFuzzerOptions = []) { +function runLibAflCli(cwd, entryPoint, extraFuzzerOptions = [], extraEnv = {}) { const proc = spawnSync( "npx", [ @@ -54,7 +54,7 @@ function runLibAflCli(cwd, entryPoint, extraFuzzerOptions = []) { ], { cwd, - env: { ...process.env }, + env: { ...process.env, ...extraEnv }, shell: true, stdio: "pipe", windowsHide: true, @@ -66,6 +66,10 @@ function runLibAflCli(cwd, entryPoint, extraFuzzerOptions = []) { }; } +function findOutputLine(output, prefix) { + return output.split(/\r?\n/).find((line) => line.startsWith(prefix)); +} + describe("Engine selection", () => { const testDirectory = __dirname; const jestProjectDirectory = path.join(testDirectory, "jest_project"); @@ -89,7 +93,57 @@ describe("Engine selection", () => { expect(fuzzTest.stderr).not.toContain("Unknown fuzzing engine"); expect(fuzzTest.stderr).toContain("[libafl::start] mode: fuzzing"); - expect(fuzzTest.stderr).toContain("[libafl::done] mode: fuzzing"); + expect(fuzzTest.stderr).toContain("[=] #"); + expect(fuzzTest.stderr).toContain("| DONE"); + }); + + it("prints aligned testcase, heartbeat, and done lines", async () => { + await withTempGuidanceDirectory(async (directory) => { + const { status, output } = runLibAflCli( + testDirectory, + "fuzz", + [ + "-max_total_time=1", + "-seed=1337", + "-max_len=32", + `-artifact_prefix=${directory}${path.sep}`, + ], + { JAZZER_LIBAFL_MONITOR_TIMEOUT_MS: "50" }, + ); + + expect(status).toBe(0); + const testcaseLine = findOutputLine(output, "[+]"); + const heartbeatLine = findOutputLine(output, "[*]"); + + expect(testcaseLine).toBeDefined(); + expect(heartbeatLine).toBeDefined(); + expect(testcaseLine.indexOf("| edges:")).toBe( + heartbeatLine.indexOf("| edges:"), + ); + expect(testcaseLine.indexOf("| corp:")).toBe( + heartbeatLine.indexOf("| corp:"), + ); + expect(testcaseLine.indexOf("| exec/s:")).toBe( + heartbeatLine.indexOf("| exec/s:"), + ); + expect(testcaseLine.indexOf("| obj:")).toBe( + heartbeatLine.indexOf("| obj:"), + ); + expect(testcaseLine.indexOf("| stab:")).toBe( + heartbeatLine.indexOf("| stab:"), + ); + expect(testcaseLine.indexOf("| t:")).toBe( + heartbeatLine.indexOf("| t:"), + ); + + expect(output).toContain("[=] #"); + expect(output).toContain("| DONE"); + expect(output).toContain("reason: max_total_time"); + expect(output).toContain("time: "); + expect(output).toContain("edges: "); + expect(output).toContain("crashes: 0"); + expect(output).toContain("speed: "); + }); }); it("rejects unsupported libFuzzer options in LibAFL mode", () => { @@ -195,6 +249,9 @@ describe("Engine selection", () => { expect(status).toBe(Number(FuzzingExitCode)); expect(output).toContain("AFL equality guidance finding"); + expect(output).toMatch( + /\[!\] #\d+\s+\| artifact: crash-[0-9a-f]+ \| Error: AFL equality guidance finding/, + ); }); }); From 3491c4e40d677fc72b32b68e9def3c0fb110e6a8 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 17:34:35 +0200 Subject: [PATCH 18/30] test(bench): parse the new LibAFL done block Read exec/s from the structured status summary so the anomaly smoke\nscript keeps working with the new LibAFL terminal UI. --- benchmarks/engine_smoke/anomaly.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/engine_smoke/anomaly.js b/benchmarks/engine_smoke/anomaly.js index 56791334e..0d46a6efd 100644 --- a/benchmarks/engine_smoke/anomaly.js +++ b/benchmarks/engine_smoke/anomaly.js @@ -74,7 +74,7 @@ function runCommand(label, args, cwd, outputDirectory, expectedStatus = 0) { function parseExecsPerSecond(stderrPath) { const stderr = fs.readFileSync(stderrPath, "utf8"); - const match = stderr.match(/\[libafl::done\].*exec\/sec: ([0-9.]+)/); + const match = stderr.match(/speed:\s+([0-9.]+) exec\/s/); if (!match) { throw new Error(`No LibAFL done line found in ${stderrPath}`); } From fa8da91d4a4e4fa5901325a479517b5d44d02e8f Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 17:42:51 +0200 Subject: [PATCH 19/30] feat(fuzzer): switch LibAFL start lines to [>] Render the LibAFL start banner with the new marker-based format in\nboth fuzzing and regression mode, and keep the same color/TTY\nrules as the rest of the UI. --- packages/fuzzer/libafl_runtime.cpp | 39 +++++++++++++++++++++++++----- packages/fuzzer/rust/src/lib.rs | 23 +++++++++++++++--- tests/engine/engine.test.js | 8 ++++-- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/packages/fuzzer/libafl_runtime.cpp b/packages/fuzzer/libafl_runtime.cpp index 4a45fe1f6..047ff32d5 100644 --- a/packages/fuzzer/libafl_runtime.cpp +++ b/packages/fuzzer/libafl_runtime.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,7 @@ #include #ifdef _WIN32 +#include #include #define GetPID _getpid #else @@ -115,14 +117,39 @@ std::string FormatTotalTimeLimit(uint64_t max_total_time_seconds) { return FormatDuration(std::chrono::seconds(max_total_time_seconds)); } +bool ShouldColorizeOutput() { + if (std::getenv("NO_COLOR") != nullptr) { + return false; + } + + const auto *term = std::getenv("TERM"); + if (term != nullptr && std::string(term) == "dumb") { + return false; + } + +#ifdef _WIN32 + return _isatty(_fileno(stderr)) != 0; +#else + return isatty(fileno(stderr)) != 0; +#endif +} + +std::string StartMarker() { + if (!ShouldColorizeOutput()) { + return "[>]"; + } + + return "\x1b[34m[>]\x1b[0m"; +} + void PrintRegressionStart(const ParsedRuntimeOptions &options, size_t replay_inputs) { - std::cerr << "[libafl::start] mode: regression, seed: " << options.seed - << ", replay_inputs: " << replay_inputs - << ", timeout: " << options.timeout_millis - << " ms, max_len: " << options.max_len - << ", runs: " << FormatRunLimit(options.runs) - << ", max_total_time: " + std::cerr << StartMarker() << " mode: regression | seed: " << options.seed + << " | loaded_inputs: " << replay_inputs + << " | timeout: " << options.timeout_millis + << " ms | max_len: " << options.max_len + << " | runs: " << FormatRunLimit(options.runs) + << " | max_total_time: " << FormatTotalTimeLimit(options.max_total_time_seconds) << std::endl; } diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index b21a52290..4f3716c1c 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -265,6 +265,10 @@ fn marker_for_event(event: StatusEvent, colors_enabled: bool) -> String { } } +fn start_marker(colors_enabled: bool) -> String { + colorize_marker("[>]", "34", colors_enabled) +} + fn build_progress_snapshot( client_stats_manager: &mut ClientStatsManager, sender_id: ClientId, @@ -404,7 +408,11 @@ fn print_runtime_done( ); } -fn print_runtime_start(options: &JazzerLibAflRuntimeOptions, loaded_inputs: usize) { +fn print_runtime_start( + options: &JazzerLibAflRuntimeOptions, + loaded_inputs: usize, + colors_enabled: bool, +) { let runs = if options.runs == 0 { "unlimited".to_string() } else { @@ -417,8 +425,14 @@ fn print_runtime_start(options: &JazzerLibAflRuntimeOptions, loaded_inputs: usiz }; eprintln!( - "[libafl::start] mode: fuzzing, seed: {}, loaded_inputs: {}, timeout: {} ms, max_len: {}, runs: {}, max_total_time: {}", - options.seed, loaded_inputs, options.timeout_millis, options.max_len, runs, max_total_time, + "{} mode: fuzzing | seed: {} | loaded_inputs: {} | timeout: {} ms | max_len: {} | runs: {} | max_total_time: {}", + start_marker(colors_enabled), + options.seed, + loaded_inputs, + options.timeout_millis, + options.max_len, + runs, + max_total_time, ); } @@ -711,7 +725,8 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( return RUNTIME_FATAL; } - print_runtime_start(options, state.corpus().count()); + let colors_enabled = monitor_state.borrow().colors_enabled; + print_runtime_start(options, state.corpus().count(), colors_enabled); let started_at = Instant::now(); let monitor_timeout = monitor_timeout(); diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js index 8d96b28fb..086d079b5 100644 --- a/tests/engine/engine.test.js +++ b/tests/engine/engine.test.js @@ -92,7 +92,9 @@ describe("Engine selection", () => { .execute(); expect(fuzzTest.stderr).not.toContain("Unknown fuzzing engine"); - expect(fuzzTest.stderr).toContain("[libafl::start] mode: fuzzing"); + expect(fuzzTest.stderr).toContain( + "[>] mode: fuzzing | seed: 1337 | loaded_inputs: 1 | timeout: 5000 ms | max_len: 4096 | runs: 250 | max_total_time: unlimited", + ); expect(fuzzTest.stderr).toContain("[=] #"); expect(fuzzTest.stderr).toContain("| DONE"); }); @@ -193,7 +195,9 @@ describe("Engine selection", () => { expect(proc.status).toBe(Number(FuzzingExitCode)); const output = proc.stdout.toString() + proc.stderr.toString(); - expect(output).toContain("[libafl::start] mode: regression"); + expect(output).toMatch( + /\[>] mode: regression \| seed: \d+ \| loaded_inputs: 2 \| timeout: 5000 ms \| max_len: 4096 \| runs: unlimited \| max_total_time: unlimited/, + ); expect(output).toContain("AFL regression finding"); } finally { await fs.promises.rm(corpusDirectory, { From 1ab6e0870df0b9fca3bf8d8997eb2182f7f4fcc7 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 17:48:45 +0200 Subject: [PATCH 20/30] feat(fuzzer): color LibAFL progress lines Color the whole testcase and heartbeat line once the fuzzing\ncampaign has started, while leaving seed corpus loading output\nunhighlighted. --- packages/fuzzer/rust/src/lib.rs | 54 +++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 4f3716c1c..bfa132b42 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -110,6 +110,7 @@ struct ProgressSnapshot { } struct MonitorState { + campaign_started: bool, colors_enabled: bool, last_progress: Option, } @@ -131,6 +132,7 @@ struct LibAflMonitor { impl LibAflMonitor { fn new(finding_info: *mut JazzerLibAflFindingInfo) -> (Self, Rc>) { let state = Rc::new(RefCell::new(MonitorState { + campaign_started: false, colors_enabled: should_colorize_output(), last_progress: None, })); @@ -162,7 +164,10 @@ impl Monitor for LibAflMonitor { }; let snapshot = build_progress_snapshot(client_stats_manager, sender_id)?; - let colors_enabled = self.state.borrow().colors_enabled; + let (campaign_started, colors_enabled) = { + let state = self.state.borrow(); + (state.campaign_started, state.colors_enabled) + }; self.state.borrow_mut().last_progress = Some(snapshot); match event { @@ -174,7 +179,10 @@ impl Monitor for LibAflMonitor { ); } StatusEvent::Testcase | StatusEvent::Heartbeat => { - eprintln!("{}", format_progress_line(event, snapshot, colors_enabled),); + eprintln!( + "{}", + format_progress_line(event, snapshot, colors_enabled, campaign_started), + ); } StatusEvent::Done => unreachable!(), } @@ -256,15 +264,28 @@ fn colorize_marker(marker: &str, sgr_code: &str, colors_enabled: bool) -> String } } -fn marker_for_event(event: StatusEvent, colors_enabled: bool) -> String { +fn marker_text(event: StatusEvent) -> &'static str { match event { - StatusEvent::Testcase => colorize_marker("[+]", "32", colors_enabled), - StatusEvent::Heartbeat => colorize_marker("[*]", "2", colors_enabled), - StatusEvent::Objective => colorize_marker("[!]", "1;31", colors_enabled), - StatusEvent::Done => colorize_marker("[=]", "34", colors_enabled), + StatusEvent::Testcase => "[+]", + StatusEvent::Heartbeat => "[*]", + StatusEvent::Objective => "[!]", + StatusEvent::Done => "[=]", } } +fn event_color_code(event: StatusEvent) -> &'static str { + match event { + StatusEvent::Testcase => "32", + StatusEvent::Heartbeat => "2", + StatusEvent::Objective => "1;31", + StatusEvent::Done => "34", + } +} + +fn marker_for_event(event: StatusEvent, colors_enabled: bool) -> String { + colorize_marker(marker_text(event), event_color_code(event), colors_enabled) +} + fn start_marker(colors_enabled: bool) -> String { colorize_marker("[>]", "34", colors_enabled) } @@ -299,10 +320,16 @@ fn format_progress_line( event: StatusEvent, snapshot: ProgressSnapshot, colors_enabled: bool, + highlight_full_line: bool, ) -> String { - format!( + let marker = if colors_enabled && !highlight_full_line { + marker_for_event(event, true) + } else { + marker_text(event).to_string() + }; + let line = format!( "{} #{:4} | exec/s: {:>6.1} | obj: {:>3} | stab: {} | t: {}", - marker_for_event(event, colors_enabled), + marker, snapshot.executions, format_ratio_metric(snapshot.edges), snapshot.corpus_size, @@ -315,7 +342,13 @@ fn format_progress_line( format_ratio_metric(snapshot.stability), format_duration(snapshot.elapsed), width = EXECUTION_FIELD_WIDTH, - ) + ); + + if colors_enabled && highlight_full_line { + format!("\x1b[{}m{}\x1b[0m", event_color_code(event), line) + } else { + line + } } #[derive(Clone)] @@ -727,6 +760,7 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let colors_enabled = monitor_state.borrow().colors_enabled; print_runtime_start(options, state.corpus().count(), colors_enabled); + monitor_state.borrow_mut().campaign_started = true; let started_at = Instant::now(); let monitor_timeout = monitor_timeout(); From 9ac61ae38693afa35dd0dab900fac4485016a657 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 18:03:16 +0200 Subject: [PATCH 21/30] fix(fuzzer): reduce LibAFL corpus-load chatter Only print testcase lines for power-of-two corpus milestones during\nseed loading, while keeping every new testcase visible once the\nfuzzing campaign has started. --- packages/fuzzer/rust/src/lib.rs | 7 ++++++ tests/engine/engine.test.js | 38 +++++++++++++++++++++++++++++++++ tests/engine/fuzz.js | 24 +++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index bfa132b42..5c44c3cfc 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -170,6 +170,13 @@ impl Monitor for LibAflMonitor { }; self.state.borrow_mut().last_progress = Some(snapshot); + if !campaign_started + && matches!(event, StatusEvent::Testcase) + && !snapshot.corpus_size.is_power_of_two() + { + return Ok(()); + } + match event { StatusEvent::Objective => { let finding_info = read_finding_info(self.finding_info); diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js index 086d079b5..76e24f6c4 100644 --- a/tests/engine/engine.test.js +++ b/tests/engine/engine.test.js @@ -148,6 +148,44 @@ describe("Engine selection", () => { }); }); + it("only reports power-of-two testcase milestones while loading corpus", async () => { + await withTempGuidanceDirectory(async (directory) => { + const corpusDirectory = path.join(directory, "seed-corpus"); + await fs.promises.mkdir(corpusDirectory, { recursive: true }); + + for (let i = 1; i <= 8; i++) { + await fs.promises.writeFile( + path.join(corpusDirectory, `seed-${i}.txt`), + Buffer.from([i]), + ); + } + + const { status, output } = runLibAflCli( + testDirectory, + "seed_progress", + [ + corpusDirectory, + "-runs=1", + "-seed=1337", + "-max_len=32", + `-artifact_prefix=${directory}${path.sep}`, + ], + ); + + expect(status).toBe(0); + const initOutput = output.split("[>] mode: fuzzing", 1)[0]; + const testcaseLines = initOutput + .split(/\r?\n/) + .filter((line) => line.startsWith("[+]")); + + expect(testcaseLines).toHaveLength(4); + expect(testcaseLines[0]).toContain("| corp: 1 |"); + expect(testcaseLines[1]).toContain("| corp: 2 |"); + expect(testcaseLines[2]).toContain("| corp: 4 |"); + expect(testcaseLines[3]).toContain("| corp: 8 |"); + }); + }); + it("rejects unsupported libFuzzer options in LibAFL mode", () => { const fuzzTest = new FuzzTestBuilder() .dir(testDirectory) diff --git a/tests/engine/fuzz.js b/tests/engine/fuzz.js index 41d7097e8..35f4a4f3a 100644 --- a/tests/engine/fuzz.js +++ b/tests/engine/fuzz.js @@ -70,3 +70,27 @@ module.exports.dictionary_target = function (data) { throw new Error("AFL dictionary guidance finding"); } }; + +module.exports.seed_progress = function (data) { + const firstByte = data[0] ?? 0; + switch (firstByte) { + case 1: + return; + case 2: + return; + case 3: + return; + case 4: + return; + case 5: + return; + case 6: + return; + case 7: + return; + case 8: + return; + default: + return; + } +}; From 7d1a6fd43572978c1568054c0df4992a52a40fdf Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 18:13:53 +0200 Subject: [PATCH 22/30] fix(fuzzer): polish LibAFL status edge cases Print the final seed-loading testcase before the start banner, color\nthe whole objective line, and stop treating signal 0 as a\nfuzzing finding during orderly shutdown. --- packages/core/core.ts | 3 +++ packages/fuzzer/rust/src/lib.rs | 45 +++++++++++++++++++++++++++++---- tests/engine/engine.test.js | 4 +-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/core/core.ts b/packages/core/core.ts index 87e8886b7..8d0831e2c 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -250,6 +250,9 @@ export async function startFuzzingNoInit( // Currently only SIGINT is handled this way, as SIGSEGV has to be handled // by the native addon and directly stops the process. const signalHandler = (signal: number): void => { + if (signal === 0) { + return; + } reportFinding(new FuzzerSignalFinding(signal), false); }; diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 5c44c3cfc..1048c40b5 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -358,6 +358,24 @@ fn format_progress_line( } } +fn maybe_print_final_init_testcase(state: &MonitorState, loaded_inputs: usize) { + let Some(snapshot) = state.last_progress else { + return; + }; + + if snapshot.corpus_size == 0 + || snapshot.corpus_size.is_power_of_two() + || snapshot.corpus_size != loaded_inputs as u64 + { + return; + } + + eprintln!( + "{}", + format_progress_line(StatusEvent::Testcase, snapshot, state.colors_enabled, false), + ); +} + #[derive(Clone)] struct FindingInfo { artifact: Option, @@ -408,14 +426,24 @@ fn format_objective_line( let summary = finding_info .summary .unwrap_or_else(|| "finding".to_string()); - format!( + let line = format!( "{} #{: { const corpusDirectory = path.join(directory, "seed-corpus"); await fs.promises.mkdir(corpusDirectory, { recursive: true }); - for (let i = 1; i <= 8; i++) { + for (let i = 1; i <= 6; i++) { await fs.promises.writeFile( path.join(corpusDirectory, `seed-${i}.txt`), Buffer.from([i]), @@ -182,7 +182,7 @@ describe("Engine selection", () => { expect(testcaseLines[0]).toContain("| corp: 1 |"); expect(testcaseLines[1]).toContain("| corp: 2 |"); expect(testcaseLines[2]).toContain("| corp: 4 |"); - expect(testcaseLines[3]).toContain("| corp: 8 |"); + expect(testcaseLines[3]).toContain("| corp: 6 |"); }); }); From 666a815470e3e579e97b910071168470db1e5585 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 18:25:18 +0200 Subject: [PATCH 23/30] fix(fuzzer): refine LibAFL status formatting Show the final loaded corpus entry before INITED, color the whole\nobjective line, and hide synthetic fallback edges behind the\n-/- placeholder for uninstrumented targets. --- packages/fuzzer/rust/src/lib.rs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 1048c40b5..178708990 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -112,6 +112,7 @@ struct ProgressSnapshot { struct MonitorState { campaign_started: bool, colors_enabled: bool, + last_edges_are_synthetic: bool, last_progress: Option, } @@ -134,6 +135,7 @@ impl LibAflMonitor { let state = Rc::new(RefCell::new(MonitorState { campaign_started: false, colors_enabled: should_colorize_output(), + last_edges_are_synthetic: false, last_progress: None, })); @@ -163,11 +165,16 @@ impl Monitor for LibAflMonitor { return Ok(()); }; - let snapshot = build_progress_snapshot(client_stats_manager, sender_id)?; - let (campaign_started, colors_enabled) = { + let (campaign_started, colors_enabled, last_edges_are_synthetic) = { let state = self.state.borrow(); - (state.campaign_started, state.colors_enabled) + ( + state.campaign_started, + state.colors_enabled, + state.last_edges_are_synthetic, + ) }; + let snapshot = + build_progress_snapshot(client_stats_manager, sender_id, last_edges_are_synthetic)?; self.state.borrow_mut().last_progress = Some(snapshot); if !campaign_started @@ -300,6 +307,7 @@ fn start_marker(colors_enabled: bool) -> String { fn build_progress_snapshot( client_stats_manager: &mut ClientStatsManager, sender_id: ClientId, + hide_edges: bool, ) -> Result { let (executions, corpus_size, execs_per_sec, objective_size, elapsed) = { let global_stats = client_stats_manager.global_stats(); @@ -314,7 +322,11 @@ fn build_progress_snapshot( let client_stats = client_stats_manager.client_stats_for(sender_id)?; Ok(ProgressSnapshot { executions, - edges: ratio_from_user_stat(client_stats.get_user_stats("edges")), + edges: if hide_edges { + None + } else { + ratio_from_user_stat(client_stats.get_user_stats("edges")) + }, corpus_size, execs_per_sec, objective_size, @@ -534,9 +546,9 @@ fn clear_finding_info(ptr: *mut JazzerLibAflFindingInfo) { } } -fn ensure_non_empty_edge_map(ptr: *mut u8, len: usize) { +fn ensure_non_empty_edge_map(ptr: *mut u8, len: usize) -> bool { if ptr.is_null() || len == 0 { - return; + return false; } unsafe { @@ -546,8 +558,11 @@ fn ensure_non_empty_edge_map(ptr: *mut u8, len: usize) { // Preserve the old behavior for uninstrumented callbacks by marking // one synthetic edge only when the target left the map untouched. map[0] = 1; + return true; } } + + false } unsafe fn parse_corpus_directories(options: &JazzerLibAflRuntimeOptions) -> Option> { @@ -735,7 +750,8 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let bytes = bytes.as_slice(); let size = bytes.len().min(options.max_len); let status = unsafe { execute_one(user_data, bytes.as_ptr(), size) }; - ensure_non_empty_edge_map(maps.edges, maps.edges_len); + let synthetic_edges = ensure_non_empty_edge_map(maps.edges, maps.edges_len); + monitor_state.borrow_mut().last_edges_are_synthetic = synthetic_edges; match status { EXECUTION_CONTINUE => ExitKind::Ok, EXECUTION_FINDING => ExitKind::Crash, From a350ede9927e11577bf664dddaf49c28a8d7e49f Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 18:37:30 +0200 Subject: [PATCH 24/30] feat(fuzzer): print INITED blocks for LibAFL Replace the single-line LibAFL start banner with a multiline\nINITED block in fuzzing and regression mode, including aligned\nfields and the agreed edge placeholder behavior. --- packages/fuzzer/libafl_runtime.cpp | 32 ++++++++++++++++++++++-------- packages/fuzzer/rust/src/lib.rs | 24 +++++++++++++++------- tests/engine/engine.test.js | 25 ++++++++++++++++------- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/packages/fuzzer/libafl_runtime.cpp b/packages/fuzzer/libafl_runtime.cpp index 047ff32d5..dc0ed068e 100644 --- a/packages/fuzzer/libafl_runtime.cpp +++ b/packages/fuzzer/libafl_runtime.cpp @@ -142,16 +142,32 @@ std::string StartMarker() { return "\x1b[34m[>]\x1b[0m"; } +std::string FormatInitedField(const std::string &label, + const std::string &value) { + std::ostringstream stream; + stream << " " << std::left << std::setw(15) << label << ' ' << value; + return stream.str(); +} + +std::string EmptyEdgesMetric() { return " -/ - ( -%)"; } + void PrintRegressionStart(const ParsedRuntimeOptions &options, size_t replay_inputs) { - std::cerr << StartMarker() << " mode: regression | seed: " << options.seed - << " | loaded_inputs: " << replay_inputs - << " | timeout: " << options.timeout_millis - << " ms | max_len: " << options.max_len - << " | runs: " << FormatRunLimit(options.runs) - << " | max_total_time: " - << FormatTotalTimeLimit(options.max_total_time_seconds) - << std::endl; + std::cerr + << StartMarker() << " INITED\n" + << FormatInitedField("mode:", "regression") << '\n' + << FormatInitedField("seed:", std::to_string(options.seed)) << '\n' + << FormatInitedField("loaded_inputs:", std::to_string(replay_inputs)) + << '\n' + << FormatInitedField("edges:", EmptyEdgesMetric()) << '\n' + << FormatInitedField("timeout:", + std::to_string(options.timeout_millis) + " ms") + << '\n' + << FormatInitedField("max_len:", std::to_string(options.max_len)) << '\n' + << FormatInitedField("runs:", FormatRunLimit(options.runs)) << '\n' + << FormatInitedField("max_total_time:", + FormatTotalTimeLimit(options.max_total_time_seconds)) + << std::endl; } void PrintRegressionDone(std::chrono::steady_clock::time_point started_at, diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 178708990..d3266b371 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -304,6 +304,10 @@ fn start_marker(colors_enabled: bool) -> String { colorize_marker("[>]", "34", colors_enabled) } +fn format_inited_field(label: &str, value: impl std::fmt::Display) -> String { + format!(" {label:<15} {value}") +} + fn build_progress_snapshot( client_stats_manager: &mut ClientStatsManager, sender_id: ClientId, @@ -491,6 +495,7 @@ fn print_runtime_done( fn print_runtime_start( options: &JazzerLibAflRuntimeOptions, loaded_inputs: usize, + edges: Option, colors_enabled: bool, ) { let runs = if options.runs == 0 { @@ -505,14 +510,16 @@ fn print_runtime_start( }; eprintln!( - "{} mode: fuzzing | seed: {} | loaded_inputs: {} | timeout: {} ms | max_len: {} | runs: {} | max_total_time: {}", + "{} INITED\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", start_marker(colors_enabled), - options.seed, - loaded_inputs, - options.timeout_millis, - options.max_len, - runs, - max_total_time, + format_inited_field("mode:", "fuzzing"), + format_inited_field("seed:", options.seed), + format_inited_field("loaded_inputs:", loaded_inputs), + format_inited_field("edges:", format_ratio_metric(edges)), + format_inited_field("timeout:", format!("{} ms", options.timeout_millis)), + format_inited_field("max_len:", options.max_len), + format_inited_field("runs:", runs), + format_inited_field("max_total_time:", max_total_time), ); } @@ -815,6 +822,9 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( print_runtime_start( options, state.corpus().count(), + monitor_state + .last_progress + .and_then(|snapshot| snapshot.edges), monitor_state.colors_enabled, ); } diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js index 4ed274d6d..83b0d9698 100644 --- a/tests/engine/engine.test.js +++ b/tests/engine/engine.test.js @@ -92,9 +92,14 @@ describe("Engine selection", () => { .execute(); expect(fuzzTest.stderr).not.toContain("Unknown fuzzing engine"); - expect(fuzzTest.stderr).toContain( - "[>] mode: fuzzing | seed: 1337 | loaded_inputs: 1 | timeout: 5000 ms | max_len: 4096 | runs: 250 | max_total_time: unlimited", - ); + expect(fuzzTest.stderr).toContain("[>] INITED"); + expect(fuzzTest.stderr).toContain(" mode: fuzzing"); + expect(fuzzTest.stderr).toContain(" seed: 1337"); + expect(fuzzTest.stderr).toContain(" loaded_inputs: 1"); + expect(fuzzTest.stderr).toContain(" timeout: 5000 ms"); + expect(fuzzTest.stderr).toContain(" max_len: 4096"); + expect(fuzzTest.stderr).toContain(" runs: 250"); + expect(fuzzTest.stderr).toContain(" max_total_time: unlimited"); expect(fuzzTest.stderr).toContain("[=] #"); expect(fuzzTest.stderr).toContain("| DONE"); }); @@ -173,7 +178,7 @@ describe("Engine selection", () => { ); expect(status).toBe(0); - const initOutput = output.split("[>] mode: fuzzing", 1)[0]; + const initOutput = output.split("[>] INITED", 1)[0]; const testcaseLines = initOutput .split(/\r?\n/) .filter((line) => line.startsWith("[+]")); @@ -233,9 +238,15 @@ describe("Engine selection", () => { expect(proc.status).toBe(Number(FuzzingExitCode)); const output = proc.stdout.toString() + proc.stderr.toString(); - expect(output).toMatch( - /\[>] mode: regression \| seed: \d+ \| loaded_inputs: 2 \| timeout: 5000 ms \| max_len: 4096 \| runs: unlimited \| max_total_time: unlimited/, - ); + expect(output).toContain("[>] INITED"); + expect(output).toContain(" mode: regression"); + expect(output).toMatch(/ seed:\s+\d+/); + expect(output).toContain(" loaded_inputs: 2"); + expect(output).toContain(" edges: -/ - ( -%)"); + expect(output).toContain(" timeout: 5000 ms"); + expect(output).toContain(" max_len: 4096"); + expect(output).toContain(" runs: unlimited"); + expect(output).toContain(" max_total_time: unlimited"); expect(output).toContain("AFL regression finding"); } finally { await fs.promises.rm(corpusDirectory, { From 7ad443c8793c7afb63cd08baacdfbe28bde6f7c8 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 18:50:40 +0200 Subject: [PATCH 25/30] fix(fuzzer): align LibAFL INITED fields Trim left-padding from INITED values so fields like edges align\ncleanly in fuzzing and regression mode, and tighten the engine\ntest around the corrected spacing. --- packages/fuzzer/libafl_runtime.cpp | 6 +++++- packages/fuzzer/rust/src/lib.rs | 3 ++- tests/engine/engine.test.js | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/fuzzer/libafl_runtime.cpp b/packages/fuzzer/libafl_runtime.cpp index dc0ed068e..bb4e46d6d 100644 --- a/packages/fuzzer/libafl_runtime.cpp +++ b/packages/fuzzer/libafl_runtime.cpp @@ -144,8 +144,12 @@ std::string StartMarker() { std::string FormatInitedField(const std::string &label, const std::string &value) { + const auto first = value.find_first_not_of(' '); + const auto trimmed = first == std::string::npos + ? std::string_view("") + : std::string_view(value).substr(first); std::ostringstream stream; - stream << " " << std::left << std::setw(15) << label << ' ' << value; + stream << " " << std::left << std::setw(15) << label << ' ' << trimmed; return stream.str(); } diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index d3266b371..38a731a3c 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -305,7 +305,8 @@ fn start_marker(colors_enabled: bool) -> String { } fn format_inited_field(label: &str, value: impl std::fmt::Display) -> String { - format!(" {label:<15} {value}") + let value = value.to_string(); + format!(" {label:<15} {}", value.trim_start()) } fn build_progress_snapshot( diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js index 83b0d9698..1b00502b3 100644 --- a/tests/engine/engine.test.js +++ b/tests/engine/engine.test.js @@ -96,6 +96,7 @@ describe("Engine selection", () => { expect(fuzzTest.stderr).toContain(" mode: fuzzing"); expect(fuzzTest.stderr).toContain(" seed: 1337"); expect(fuzzTest.stderr).toContain(" loaded_inputs: 1"); + expect(fuzzTest.stderr).toMatch(/ edges:\s{10}\S/); expect(fuzzTest.stderr).toContain(" timeout: 5000 ms"); expect(fuzzTest.stderr).toContain(" max_len: 4096"); expect(fuzzTest.stderr).toContain(" runs: 250"); @@ -242,7 +243,7 @@ describe("Engine selection", () => { expect(output).toContain(" mode: regression"); expect(output).toMatch(/ seed:\s+\d+/); expect(output).toContain(" loaded_inputs: 2"); - expect(output).toContain(" edges: -/ - ( -%)"); + expect(output).toContain(" edges: -/ - ( -%)"); expect(output).toContain(" timeout: 5000 ms"); expect(output).toContain(" max_len: 4096"); expect(output).toContain(" runs: unlimited"); From f6b1e0cdb90691e86315c904aba4c6392d880e70 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 19:57:53 +0200 Subject: [PATCH 26/30] feat(fuzzer): emit LibAFL pulses after idle periods Track the last printed status line in the runtime and emit [*] only after a quiet interval, instead of relying on LibAFL's own progress timer. --- packages/fuzzer/rust/src/lib.rs | 96 ++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 14 deletions(-) diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 38a731a3c..1d6ea6f29 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -12,7 +12,7 @@ use std::time::{Duration, Instant}; use libafl::{ corpus::{CachedOnDiskCorpus, Corpus, InMemoryCorpus}, - events::{ProgressReporter, SimpleEventManager}, + events::SimpleEventManager, executors::{inprocess::InProcessExecutor, ExitKind, ShadowExecutor}, feedback_or_fast, feedbacks::{CrashFeedback, MaxMapFeedback, TimeoutFeedback}, @@ -113,6 +113,7 @@ struct MonitorState { campaign_started: bool, colors_enabled: bool, last_edges_are_synthetic: bool, + last_status_output_at: Option, last_progress: Option, } @@ -136,6 +137,7 @@ impl LibAflMonitor { campaign_started: false, colors_enabled: should_colorize_output(), last_edges_are_synthetic: false, + last_status_output_at: None, last_progress: None, })); @@ -157,7 +159,6 @@ impl Monitor for LibAflMonitor { sender_id: ClientId, ) -> Result<(), Error> { let Some(event) = (match event_msg { - "Client Heartbeat" | "PerfMonitor" => Some(StatusEvent::Heartbeat), "Testcase" => Some(StatusEvent::Testcase), "Objective" => Some(StatusEvent::Objective), _ => None, @@ -192,15 +193,17 @@ impl Monitor for LibAflMonitor { format_objective_line(snapshot.executions, finding_info, colors_enabled), ); } - StatusEvent::Testcase | StatusEvent::Heartbeat => { + StatusEvent::Testcase => { eprintln!( "{}", format_progress_line(event, snapshot, colors_enabled, campaign_started), ); } - StatusEvent::Done => unreachable!(), + StatusEvent::Heartbeat | StatusEvent::Done => unreachable!(), } + self.state.borrow_mut().last_status_output_at = Some(Instant::now()); + Ok(()) } } @@ -352,7 +355,7 @@ fn format_progress_line( marker_text(event).to_string() }; let line = format!( - "{} #{:4} | exec/s: {:>6.1} | obj: {:>3} | stab: {} | t: {}", + "{} #{:4} | exec/s: {:>8.1} | obj: {:>3} | stab: {} | t: {}", marker, snapshot.executions, format_ratio_metric(snapshot.edges), @@ -375,7 +378,7 @@ fn format_progress_line( } } -fn maybe_print_final_init_testcase(state: &MonitorState, loaded_inputs: usize) { +fn maybe_print_final_init_testcase(state: &mut MonitorState, loaded_inputs: usize) { let Some(snapshot) = state.last_progress else { return; }; @@ -391,6 +394,68 @@ fn maybe_print_final_init_testcase(state: &MonitorState, loaded_inputs: usize) { "{}", format_progress_line(StatusEvent::Testcase, snapshot, state.colors_enabled, false), ); + state.last_status_output_at = Some(Instant::now()); +} + +fn build_idle_progress_snapshot( + state: &S, + started_at: Instant, + monitor_state: &MonitorState, +) -> ProgressSnapshot +where + S: HasCorpus + HasExecutions + HasSolutions, +{ + let executions = *state.executions(); + let elapsed = started_at.elapsed(); + let execs_per_sec = if elapsed.as_secs_f64() > 0.0 { + executions as f64 / elapsed.as_secs_f64() + } else { + 0.0 + }; + + ProgressSnapshot { + executions, + edges: monitor_state + .last_progress + .and_then(|snapshot| snapshot.edges), + corpus_size: state.corpus().count() as u64, + execs_per_sec, + objective_size: state.solutions().count() as u64, + stability: monitor_state + .last_progress + .and_then(|snapshot| snapshot.stability), + elapsed, + } +} + +fn maybe_emit_idle_heartbeat( + monitor_state: &mut MonitorState, + state: &S, + started_at: Instant, + monitor_timeout: Duration, +) where + S: HasCorpus + HasExecutions + HasSolutions, +{ + let Some(last_status_output_at) = monitor_state.last_status_output_at else { + return; + }; + + if last_status_output_at.elapsed() < monitor_timeout { + return; + } + + let snapshot = build_idle_progress_snapshot(state, started_at, monitor_state); + eprintln!( + "{}", + format_progress_line( + StatusEvent::Heartbeat, + snapshot, + monitor_state.colors_enabled, + true, + ), + ); + monitor_state.last_progress = Some(snapshot); + monitor_state.last_status_output_at = Some(Instant::now()); } #[derive(Clone)] @@ -818,8 +883,8 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( } { - let monitor_state = monitor_state.borrow(); - maybe_print_final_init_testcase(&monitor_state, state.corpus().count()); + let mut monitor_state = monitor_state.borrow_mut(); + maybe_print_final_init_testcase(&mut monitor_state, state.corpus().count()); print_runtime_start( options, state.corpus().count(), @@ -828,8 +893,9 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( .and_then(|snapshot| snapshot.edges), monitor_state.colors_enabled, ); + monitor_state.last_status_output_at = Some(Instant::now()); + monitor_state.campaign_started = true; } - monitor_state.borrow_mut().campaign_started = true; let started_at = Instant::now(); let monitor_timeout = monitor_timeout(); @@ -842,11 +908,6 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let initial_executions = *state.executions(); let mut status = RUNTIME_OK; let done_reason = loop { - if let Err(error) = mgr.maybe_report_progress(&mut state, monitor_timeout) { - eprintln!("[libafl] fatal: failed to report progress: {error:?}"); - return RUNTIME_FATAL; - } - if options.runs != 0 && state.executions().saturating_sub(initial_executions) >= options.runs { @@ -879,6 +940,13 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( status = RUNTIME_STOPPED; break "stop_requested"; } + + maybe_emit_idle_heartbeat( + &mut monitor_state.borrow_mut(), + &state, + started_at, + monitor_timeout, + ); }; let monitor_state = monitor_state.borrow(); From 654663fc551534e65ead354f9ff741debd90e0c6 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Sun, 19 Apr 2026 19:58:05 +0200 Subject: [PATCH 27/30] test(engine): cover LibAFL idle pulse timing Assert that pulses appear for quiet runs and stay suppressed during seed loading, even when the internal pulse timeout is shortened for tests. --- tests/engine/engine.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js index 1b00502b3..5a0f741ba 100644 --- a/tests/engine/engine.test.js +++ b/tests/engine/engine.test.js @@ -176,10 +176,12 @@ describe("Engine selection", () => { "-max_len=32", `-artifact_prefix=${directory}${path.sep}`, ], + { JAZZER_LIBAFL_MONITOR_TIMEOUT_MS: "1" }, ); expect(status).toBe(0); const initOutput = output.split("[>] INITED", 1)[0]; + expect(initOutput).not.toContain("[*]"); const testcaseLines = initOutput .split(/\r?\n/) .filter((line) => line.startsWith("[+]")); From a52710b84b2f12c123bbd4f6d4ac2134f0874daa Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Mon, 20 Apr 2026 02:09:01 +0200 Subject: [PATCH 28/30] fix(libafl): support lazy ESM coverage Allocate ESM counters from the shared global map and let libAFL observe its active length instead of a startup snapshot. This keeps lazy import() coverage deterministic via the existing edge ID strategy and locks in the behavior with integration tests. Also ignore host Babel config for runtime transforms so ESM instrumentation keeps module semantics intact. --- packages/fuzzer/addon.ts | 1 - packages/fuzzer/coverage.ts | 37 ++-- packages/fuzzer/libafl_runtime.cpp | 11 +- packages/fuzzer/libafl_runtime.h | 3 +- packages/fuzzer/rust/src/lib.rs | 66 ++++-- packages/fuzzer/shared/callbacks.cpp | 2 - packages/fuzzer/shared/coverage.cpp | 31 +-- packages/fuzzer/shared/coverage.h | 3 +- packages/instrumentor/edgeIdStrategy.ts | 207 +++++++++++------- packages/instrumentor/esm-loader.mts | 6 +- packages/instrumentor/esmSourceMaps.test.ts | 2 +- packages/instrumentor/instrument.ts | 8 + tests/esm_cjs_mixed/esm_cjs_mixed.test.js | 17 ++ .../esm_instrumentation.test.js | 17 ++ tests/esm_instrumentation/fuzz-lazy.mjs | 23 ++ 15 files changed, 288 insertions(+), 146 deletions(-) create mode 100644 tests/esm_instrumentation/fuzz-lazy.mjs diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index 9f7ee6081..8ce4db429 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -61,7 +61,6 @@ export type StartLibAflAsyncFn = ( type NativeAddon = { registerCoverageMap: (buffer: Buffer) => void; registerNewCounters: (oldNumCounters: number, newNumCounters: number) => void; - registerModuleCounters: (buffer: Buffer) => void; traceUnequalStrings: ( hookId: number, diff --git a/packages/fuzzer/coverage.ts b/packages/fuzzer/coverage.ts index acc807686..5efc3bc42 100644 --- a/packages/fuzzer/coverage.ts +++ b/packages/fuzzer/coverage.ts @@ -16,17 +16,23 @@ import { addon } from "./addon"; +type CoverageRangeAllocator = (filename: string, edgeCount: number) => number; + +function getCoverageRangeAllocator(): CoverageRangeAllocator { + const allocator = (globalThis as Record) + .__jazzer_reserveCoverageRange; + if (typeof allocator !== "function") { + throw new Error("Coverage range allocator was not initialized"); + } + return allocator as CoverageRangeAllocator; +} + export class CoverageTracker { private static readonly MAX_NUM_COUNTERS: number = 1 << 20; private static readonly INITIAL_NUM_COUNTERS: number = 1 << 9; private readonly coverageMap: Buffer; private currentNumCounters: number; - // Per-module counter buffers registered independently with libFuzzer. - // We must prevent GC from reclaiming these while libFuzzer still - // monitors the underlying memory. - private readonly moduleCounters: Buffer[] = []; - constructor() { this.coverageMap = Buffer.alloc(CoverageTracker.MAX_NUM_COUNTERS, 0); this.currentNumCounters = CoverageTracker.INITIAL_NUM_COUNTERS; @@ -71,16 +77,17 @@ export class CoverageTracker { return this.coverageMap.readUint8(edgeId); } - /** - * Allocate an independent counter buffer for a single module and - * register it with libFuzzer as a new coverage region. This lets - * each ESM module own its own counters without sharing global IDs. - */ - createModuleCounters(size: number): Buffer { - const buf = Buffer.alloc(size, 0); - this.moduleCounters.push(buf); - addon.registerModuleCounters(buf); - return buf; + createModuleCounters(filename: string, edgeCount: number): Buffer { + if (!Number.isInteger(edgeCount) || edgeCount < 0) { + throw new Error(`Invalid edge count: ${edgeCount}`); + } + if (edgeCount === 0) { + return Buffer.alloc(0); + } + + const firstEdgeId = getCoverageRangeAllocator()(filename, edgeCount); + this.enlargeCountersBufferIfNeeded(firstEdgeId + edgeCount - 1); + return this.coverageMap.subarray(firstEdgeId, firstEdgeId + edgeCount); } } diff --git a/packages/fuzzer/libafl_runtime.cpp b/packages/fuzzer/libafl_runtime.cpp index bb4e46d6d..f2073ddb0 100644 --- a/packages/fuzzer/libafl_runtime.cpp +++ b/packages/fuzzer/libafl_runtime.cpp @@ -593,20 +593,23 @@ ParsedRuntimeOptions ParseRuntimeOptions(Napi::Env env, JazzerLibAflRuntimeSharedMaps SharedMapsForRuntime(Napi::Env env) { auto *edges = CoverageCounters(); - const auto edges_len = CoverageCountersSize(); + const auto edges_capacity = CoverageCountersCapacity(); + auto *edges_size = CoverageCountersSizePointer(); auto *cmp = CompareFeedbackMap(); const auto cmp_len = CompareFeedbackMapSize(); auto *compare_log = CompareLog(); auto *finding_info = &gFindingInfo; - if (edges == nullptr || edges_len == 0 || cmp == nullptr || cmp_len == 0 || - compare_log == nullptr || finding_info == nullptr) { + if (edges == nullptr || edges_capacity == 0 || edges_size == nullptr || + cmp == nullptr || cmp_len == 0 || compare_log == nullptr || + finding_info == nullptr) { throw Napi::Error::New( env, "Coverage maps were not initialized before the LibAFL backend started"); } - return {edges, edges_len, cmp, cmp_len, compare_log, finding_info}; + return {edges, edges_capacity, edges_size, cmp, + cmp_len, compare_log, finding_info}; } bool CollectRegressionCorpusFiles( diff --git a/packages/fuzzer/libafl_runtime.h b/packages/fuzzer/libafl_runtime.h index de1e25ed2..56f7bdc32 100644 --- a/packages/fuzzer/libafl_runtime.h +++ b/packages/fuzzer/libafl_runtime.h @@ -44,7 +44,8 @@ struct JazzerLibAflRuntimeOptions { struct JazzerLibAflRuntimeSharedMaps { uint8_t *edges; - size_t edges_len; + size_t edges_capacity; + size_t *edges_size; uint8_t *cmp; size_t cmp_len; JazzerLibAflCompareLog *compare_log; diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 1d6ea6f29..43c3d4b32 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -8,6 +8,7 @@ use std::fs; use std::io::IsTerminal; use std::path::PathBuf; use std::rc::Rc; +use std::slice; use std::time::{Duration, Instant}; use libafl::{ @@ -26,7 +27,9 @@ use libafl::{ havoc_mutations::havoc_mutations, scheduled::HavocScheduledMutator, tokens_mutations, I2SRandReplace, Tokens, }, - observers::{CanTrack, HitcountsMapObserver, StdMapObserver}, + observers::{ + CanTrack, HitcountsMapObserver, StdMapObserver, VariableMapObserver, + }, schedulers::{ powersched::PowerSchedule, IndexesLenTimeMinimizerScheduler, PowerQueueScheduler, }, @@ -82,7 +85,8 @@ pub struct JazzerLibAflRuntimeOptions { #[repr(C)] pub struct JazzerLibAflRuntimeSharedMaps { pub edges: *mut u8, - pub edges_len: usize, + pub edges_capacity: usize, + pub edges_size: *mut usize, pub cmp: *mut u8, pub cmp_len: usize, pub compare_log: *mut JazzerLibAflCompareLog, @@ -619,23 +623,40 @@ fn clear_finding_info(ptr: *mut JazzerLibAflFindingInfo) { } } +fn edge_map_len(maps: &JazzerLibAflRuntimeSharedMaps) -> usize { + if maps.edges_size.is_null() { + 0 + } else { + unsafe { (*maps.edges_size).min(maps.edges_capacity) } + } +} + +fn has_non_zero_coverage(ptr: *mut u8, len: usize) -> bool { + if ptr.is_null() || len == 0 { + return false; + } + + unsafe { slice::from_raw_parts(ptr, len).iter().any(|slot| *slot != 0) } +} + fn ensure_non_empty_edge_map(ptr: *mut u8, len: usize) -> bool { + if has_non_zero_coverage(ptr, len) { + return false; + } + if ptr.is_null() || len == 0 { return false; } unsafe { - let map = std::slice::from_raw_parts_mut(ptr, len); - if map.iter().all(|slot| *slot == 0) { - // Power scheduling rejects corpus entries that never hit any edge. - // Preserve the old behavior for uninstrumented callbacks by marking - // one synthetic edge only when the target left the map untouched. - map[0] = 1; - return true; - } + let map = slice::from_raw_parts_mut(ptr, len); + // Power scheduling rejects corpus entries that never hit any edge. + // Preserve the old behavior for uninstrumented callbacks by marking + // one synthetic edge only when the target left every coverage region untouched. + map[0] = 1; } - false + true } unsafe fn parse_corpus_directories(options: &JazzerLibAflRuntimeOptions) -> Option> { @@ -713,7 +734,8 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let options = &*options; let maps = &*maps; if maps.edges.is_null() - || maps.edges_len == 0 + || maps.edges_capacity == 0 + || maps.edges_size.is_null() || maps.cmp.is_null() || maps.cmp_len == 0 || maps.compare_log.is_null() @@ -749,11 +771,14 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let (monitor, monitor_state) = LibAflMonitor::new(maps.finding_info); let mut mgr = SimpleEventManager::new(monitor); - let edges_observer = HitcountsMapObserver::new(StdMapObserver::from_mut_ptr( - "edges", - maps.edges, - maps.edges_len, - )) + let edges_observer = HitcountsMapObserver::new( + VariableMapObserver::from_mut_ptr( + "edges", + maps.edges, + maps.edges_capacity, + maps.edges_size, + ), + ) .track_indices(); let cmp_observer = HitcountsMapObserver::new(StdMapObserver::from_mut_ptr("cmp", maps.cmp, maps.cmp_len)); @@ -814,7 +839,7 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let timeout_found = Cell::new(false); let mut harness = |input: &BytesInput| { - clear_shared_map(maps.edges, maps.edges_len); + clear_shared_map(maps.edges, edge_map_len(maps)); clear_shared_map(maps.cmp, maps.cmp_len); clear_compare_log(maps.compare_log); clear_finding_info(maps.finding_info); @@ -823,7 +848,10 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run( let bytes = bytes.as_slice(); let size = bytes.len().min(options.max_len); let status = unsafe { execute_one(user_data, bytes.as_ptr(), size) }; - let synthetic_edges = ensure_non_empty_edge_map(maps.edges, maps.edges_len); + let synthetic_edges = ensure_non_empty_edge_map( + maps.edges, + edge_map_len(maps), + ); monitor_state.borrow_mut().last_edges_are_synthetic = synthetic_edges; match status { EXECUTION_CONTINUE => ExitKind::Ok, diff --git a/packages/fuzzer/shared/callbacks.cpp b/packages/fuzzer/shared/callbacks.cpp index 59220f6b2..235019fec 100644 --- a/packages/fuzzer/shared/callbacks.cpp +++ b/packages/fuzzer/shared/callbacks.cpp @@ -21,8 +21,6 @@ void RegisterCallbackExports(Napi::Env env, Napi::Object exports) { Napi::Function::New(env); exports["registerNewCounters"] = Napi::Function::New(env); - exports["registerModuleCounters"] = - Napi::Function::New(env); exports["traceUnequalStrings"] = Napi::Function::New(env); exports["traceStringContainment"] = diff --git a/packages/fuzzer/shared/coverage.cpp b/packages/fuzzer/shared/coverage.cpp index ad66dfeb2..fc5b0876d 100644 --- a/packages/fuzzer/shared/coverage.cpp +++ b/packages/fuzzer/shared/coverage.cpp @@ -25,8 +25,10 @@ void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg, namespace { // Shared coverage counter buffer populated from JavaScript using Buffer. -// Individual slices are registered with libFuzzer by RegisterNewCounters. +// It is preallocated on the JavaScript side; registerNewCounters grows the +// active prefix that the fuzzing backends should observe. uint8_t *gCoverageCounters = nullptr; +std::size_t gCoverageCountersCapacity = 0; std::size_t gCoverageCountersSize = 0; // PC-Table is used by libFuzzer to keep track of program addresses @@ -78,6 +80,7 @@ void RegisterCoverageMap(const Napi::CallbackInfo &info) { auto buf = info[0].As>(); gCoverageCounters = reinterpret_cast(buf.Data()); + gCoverageCountersCapacity = buf.Length(); } void RegisterNewCounters(const Napi::CallbackInfo &info) { @@ -98,6 +101,10 @@ void RegisterNewCounters(const Napi::CallbackInfo &info) { info.Env(), "new_num_counters must not be smaller than old_num_counters"); } + if (static_cast(new_num_counters) > gCoverageCountersCapacity) { + throw Napi::Error::New(info.Env(), + "new_num_counters exceeds the coverage map size"); + } if (new_num_counters == old_num_counters) { return; } @@ -107,28 +114,14 @@ void RegisterNewCounters(const Napi::CallbackInfo &info) { gCoverageCountersSize = static_cast(new_num_counters); } -// Register an independent coverage counter region for a single ES module. -// libFuzzer supports multiple disjoint counter regions; each call here -// hands it a fresh one. -void RegisterModuleCounters(const Napi::CallbackInfo &info) { - if (info.Length() != 1 || !info[0].IsBuffer()) { - throw Napi::Error::New(info.Env(), - "Need one argument: a Buffer of 8-bit counters"); - } - - auto buf = info[0].As>(); - auto size = buf.Length(); - if (size == 0) { - return; - } - - RegisterCounterRange(buf.Data(), buf.Data() + size); -} - uint8_t *CoverageCounters() { return gCoverageCounters; } +std::size_t CoverageCountersCapacity() { return gCoverageCountersCapacity; } + std::size_t CoverageCountersSize() { return gCoverageCountersSize; } +std::size_t *CoverageCountersSizePointer() { return &gCoverageCountersSize; } + void ClearCoverageCounters() { if (gCoverageCounters == nullptr || gCoverageCountersSize == 0) { return; diff --git a/packages/fuzzer/shared/coverage.h b/packages/fuzzer/shared/coverage.h index 42c126b09..ac84c7514 100644 --- a/packages/fuzzer/shared/coverage.h +++ b/packages/fuzzer/shared/coverage.h @@ -19,8 +19,9 @@ void RegisterCoverageMap(const Napi::CallbackInfo &info); void RegisterNewCounters(const Napi::CallbackInfo &info); -void RegisterModuleCounters(const Napi::CallbackInfo &info); uint8_t *CoverageCounters(); +std::size_t CoverageCountersCapacity(); std::size_t CoverageCountersSize(); +std::size_t *CoverageCountersSizePointer(); void ClearCoverageCounters(); diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts index dfc0c25e3..5ecac38ef 100644 --- a/packages/instrumentor/edgeIdStrategy.ts +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -40,6 +40,7 @@ if (process.listeners) { export interface EdgeIdStrategy { nextEdgeId(): number; + reserveEdgeRange(filename: string, idCount: number): number; startForSourceFile(filename: string): void; commitIdCount(filename: string): void; } @@ -52,6 +53,15 @@ export abstract class IncrementingEdgeIdStrategy implements EdgeIdStrategy { return this._nextEdgeId++; } + reserveEdgeRange(_filename: string, idCount: number): number { + if (!Number.isInteger(idCount) || idCount < 0) { + throw new Error(`Invalid edge count: ${idCount}`); + } + const firstId = this._nextEdgeId; + this._nextEdgeId += idCount; + return firstId; + } + abstract startForSourceFile(filename: string): void; abstract commitIdCount(filename: string): void; } @@ -76,6 +86,29 @@ interface EdgeIdInfo { idCount: number; } +function parseIdInfoLine(line: string): EdgeIdInfo { + const parts = line.split(","); + if (parts.length !== 3) { + throw new Error( + `Expected ID file line to be ,,, got ` + + `"${line}"`, + ); + } + return { + filename: parts[0], + firstId: parseInt(parts[1], 10), + idCount: parseInt(parts[2], 10), + }; +} + +function nextFreeId(idInfo: EdgeIdInfo[]): number { + if (idInfo.length === 0) { + return 0; + } + const last = idInfo[idInfo.length - 1]; + return last.firstId + last.idCount; +} + /** * A strategy for edge ID generation that synchronizes the IDs assigned to a source file * with other processes via the specified `idSyncFile`. The edge information stored as a @@ -95,93 +128,69 @@ export class FileSyncIdStrategy extends IncrementingEdgeIdStrategy { } startForSourceFile(filename: string): void { - // We resort to busy waiting since the `Transformer` required by istanbul's `hookRequire` - // must be a synchronous function returning the transformed code. - for (;;) { - const isLocked = lock.checkSync(this.idSyncFile); - if (isLocked) { - // If the ID sync file is already locked, wait for a random period of time - // between 0 and 100 milliseconds. Waiting for different periods reduces - // the chance of all processes wanting to acquire the lock at the same time. - this.wait(this.randomIntFromInterval(0, 100)); - continue; - } - try { - // Acquire the lock for the ID sync file and look for the initial edge ID and - // corresponding number of inserted counters. - this.releaseLockOnSyncFile = lock.lockSync(this.idSyncFile); - const idInfo = fs - .readFileSync(this.idSyncFile, "utf8") - .toString() - .split(os.EOL) - .filter((line) => line.length !== 0) - .map((line): EdgeIdInfo => { - const parts = line.split(","); - if (parts.length !== 3) { - lock.unlockSync(this.idSyncFile); - throw Error( - `Expected ID file line to be of the form ,,", got "${line}"`, - ); - } - return { - filename: parts[0], - firstId: parseInt(parts[1], 10), - idCount: parseInt(parts[2], 10), - }; - }); - const idInfoForFile = idInfo.filter( - (info) => info.filename === filename, - ); + const idInfo = this.acquireLockAndReadIdInfo(); + const idInfoForFile = idInfo.filter((info) => info.filename === filename); - switch (idInfoForFile.length) { - case 0: - // We are the first to encounter this source file and thus need to hold the lock - // until the file has been instrumented and we know the required number of edge IDs. - // - // Compute the next free ID as the maximum over the sums of first ID and ID count, starting at 0 if - // this is the first ID to be assigned. Since this is the only way new lines are added to - // the file, the maximum is always attained by the last line. - this.firstEdgeId = - idInfo.length !== 0 - ? idInfo[idInfo.length - 1].firstId + - idInfo[idInfo.length - 1].idCount - : 0; - break; - case 1: - // This source file has already been instrumented elsewhere, so we just return the first ID and - // ID count reported from there and release the lock right away. The caller is still expected - // to call commitIdCount. - this.firstEdgeId = idInfoForFile[0].firstId; - this.cachedIdCount = idInfoForFile[0].idCount; - this.releaseLockOnSyncFile(); - break; - default: - this.releaseLockOnSyncFile(); - console.error( - `ERROR: Multiple entries for ${filename} in ID sync file`, - ); - process.exit(FileSyncIdStrategy.fatalExitCode); - } + switch (idInfoForFile.length) { + case 0: + // Keep the lock until commitIdCount() records the final range. + this.firstEdgeId = nextFreeId(idInfo); + this.cachedIdCount = undefined; break; - } catch (e) { - // Retry to wait for the lock to be release it is acquired by another process - // in the time window between last successful check and trying to acquire it. - if (this.isLockAlreadyHeldError(e)) { - continue; - } + case 1: + this.firstEdgeId = idInfoForFile[0].firstId; + this.cachedIdCount = idInfoForFile[0].idCount; + this.releaseLock(); + break; + default: + this.releaseLock(); + console.error( + `ERROR: Multiple entries for ${filename} in ID sync file`, + ); + process.exit(FileSyncIdStrategy.fatalExitCode); + } - // Before rethrowing the exception, release the lock if we have already acquired it. - if (this.releaseLockOnSyncFile !== undefined) { - this.releaseLockOnSyncFile(); - } + this._nextEdgeId = this.firstEdgeId; + } - // Stop waiting for the lock if we encounter other errors. Also, rethrow the error. - throw e; + reserveEdgeRange(filename: string, idCount: number): number { + const idInfo = this.acquireLockAndReadIdInfo(); + try { + const idInfoForFile = idInfo.filter((info) => info.filename === filename); + switch (idInfoForFile.length) { + case 0: { + const firstId = nextFreeId(idInfo); + fs.appendFileSync( + this.idSyncFile, + `${filename},${firstId},${idCount}${os.EOL}`, + ); + this._nextEdgeId = Math.max(this._nextEdgeId, firstId + idCount); + return firstId; + } + case 1: + if (idInfoForFile[0].idCount !== idCount) { + throw new Error( + `${filename} has ${idCount} edges, but ` + + `${idInfoForFile[0].idCount} edges reserved in ` + + "ID sync file", + ); + } + this._nextEdgeId = Math.max( + this._nextEdgeId, + idInfoForFile[0].firstId + idCount, + ); + return idInfoForFile[0].firstId; + default: + console.error( + `ERROR: Multiple entries for ${filename} in ID sync file`, + ); + process.exit(FileSyncIdStrategy.fatalExitCode); } + } finally { + this.releaseLock(); } - - this._nextEdgeId = this.firstEdgeId; } + commitIdCount(filename: string): void { if (this.firstEdgeId === undefined) { throw Error("commitIdCount() is called before startForSourceFile()"); @@ -210,13 +219,43 @@ export class FileSyncIdStrategy extends IncrementingEdgeIdStrategy { this.idSyncFile, `${filename},${this.firstEdgeId},${usedIdsCount}${os.EOL}`, ); - this.releaseLockOnSyncFile(); - this.releaseLockOnSyncFile = undefined; + this.releaseLock(); this.firstEdgeId = undefined; this.cachedIdCount = undefined; } } + private acquireLockAndReadIdInfo(): EdgeIdInfo[] { + for (;;) { + if (lock.checkSync(this.idSyncFile)) { + this.wait(this.randomIntFromInterval(0, 100)); + continue; + } + try { + this.releaseLockOnSyncFile = lock.lockSync(this.idSyncFile); + return fs + .readFileSync(this.idSyncFile, "utf8") + .toString() + .split(os.EOL) + .filter((line) => line.length !== 0) + .map(parseIdInfoLine); + } catch (e) { + if (this.isLockAlreadyHeldError(e)) { + continue; + } + this.releaseLock(); + throw e; + } + } + } + + private releaseLock() { + if (this.releaseLockOnSyncFile !== undefined) { + this.releaseLockOnSyncFile(); + this.releaseLockOnSyncFile = undefined; + } + } + private wait(timeout: number) { // This is a workaround to synchronously sleep for a `timout` milliseconds. // The static Atomics.wait() method verifies that a given position in an Int32Array @@ -241,6 +280,10 @@ export class ZeroEdgeIdStrategy implements EdgeIdStrategy { return 0; } + reserveEdgeRange(_filename: string, _idCount: number): number { + return 0; + } + startForSourceFile(filename: string): void { // Nothing to do here } diff --git a/packages/instrumentor/esm-loader.mts b/packages/instrumentor/esm-loader.mts index 72ec199c4..5c4864062 100644 --- a/packages/instrumentor/esm-loader.mts +++ b/packages/instrumentor/esm-loader.mts @@ -167,6 +167,10 @@ function instrumentModule(code: string, filename: string): string | null { filename, sourceFileName: filename, sourceMaps: true, + // Ignore host-project Babel config so ESM instrumentation keeps the + // module format intact and doesn't inherit target-specific transforms. + babelrc: false, + configFile: false, plugins, sourceType: "module", }); @@ -187,7 +191,7 @@ function instrumentModule(code: string, filename: string): string | null { // SourceMapRegistry so that source-map-support can remap stack // traces back to the original source. const preambleLines = [ - `const ${COUNTER_ARRAY} = Fuzzer.coverageTracker.createModuleCounters(${edges});`, + `const ${COUNTER_ARRAY} = Fuzzer.coverageTracker.createModuleCounters(${JSON.stringify(filename)}, ${edges});`, ]; if (transformed.map) { diff --git a/packages/instrumentor/esmSourceMaps.test.ts b/packages/instrumentor/esmSourceMaps.test.ts index 27900356a..5101cbe11 100644 --- a/packages/instrumentor/esmSourceMaps.test.ts +++ b/packages/instrumentor/esmSourceMaps.test.ts @@ -52,7 +52,7 @@ function instrumentModule( } const preambleLines = [ - `const ${COUNTER_ARRAY} = Fuzzer.coverageTracker.createModuleCounters(${edges});`, + `const ${COUNTER_ARRAY} = Fuzzer.coverageTracker.createModuleCounters(${JSON.stringify(filename)}, ${edges});`, ]; let shiftedMap: SourceMap | null = null; diff --git a/packages/instrumentor/instrument.ts b/packages/instrumentor/instrument.ts index d1cd194f3..d7b66c879 100644 --- a/packages/instrumentor/instrument.ts +++ b/packages/instrumentor/instrument.ts @@ -104,6 +104,10 @@ export class Instrumentor { filename: string, map: SourceMap, ) => registry.registerSourceMap(filename, map); + (globalThis as Record).__jazzer_reserveCoverageRange = ( + filename: string, + idCount: number, + ) => this.idStrategy.reserveEdgeRange(filename, idCount); return this.sourceMapRegistry.installSourceMapSupport(); } @@ -187,6 +191,10 @@ export class Instrumentor { filename: filename, sourceFileName: filename, sourceMaps: true, + // Ignore host-project Babel config so Jazzer's runtime transforms stay + // deterministic and don't pick up polyfill injection from the fuzz target. + babelrc: false, + configFile: false, plugins: plugins, ...options, }); diff --git a/tests/esm_cjs_mixed/esm_cjs_mixed.test.js b/tests/esm_cjs_mixed/esm_cjs_mixed.test.js index c0e7519fc..66d3ce21b 100644 --- a/tests/esm_cjs_mixed/esm_cjs_mixed.test.js +++ b/tests/esm_cjs_mixed/esm_cjs_mixed.test.js @@ -47,4 +47,21 @@ describeOrSkip("Mixed CJS + ESM instrumentation", () => { fuzzTest.execute(); expect(fuzzTest.stderr).toContain("Found the mixed CJS+ESM secret!"); }); + + it("should report real edge coverage with the LibAFL backend", () => { + const fuzzTest = new FuzzTestBuilder() + .fuzzEntryPoint("fuzz") + .fuzzFile("fuzz.mjs") + .dir(__dirname) + .engine("afl") + .disableBugDetectors([".*"]) + .runs(1) + .seed(1337) + .build(); + + fuzzTest.execute(); + expect(fuzzTest.stderr).toContain("[>] INITED"); + expect(fuzzTest.stderr).toMatch(/\bedges:\s+\d+\/\d+/); + expect(fuzzTest.stderr).not.toContain("edges: -/ - ( -%)"); + }); }); diff --git a/tests/esm_instrumentation/esm_instrumentation.test.js b/tests/esm_instrumentation/esm_instrumentation.test.js index 957b3efee..45e6fc419 100644 --- a/tests/esm_instrumentation/esm_instrumentation.test.js +++ b/tests/esm_instrumentation/esm_instrumentation.test.js @@ -70,4 +70,21 @@ describeOrSkip("ESM instrumentation", () => { fuzzTest.execute(); expect(fuzzTest.stderr).toContain("Found the ESM secret!"); }); + + it("should report edges for a lazily imported ESM module in LibAFL", () => { + const fuzzTest = new FuzzTestBuilder() + .fuzzEntryPoint("fuzz") + .fuzzFile("fuzz-lazy.mjs") + .dir(__dirname) + .engine("afl") + .disableBugDetectors([".*"]) + .runs(1) + .seed(1337) + .build(); + + fuzzTest.execute(); + expect(fuzzTest.stderr).toContain("[>] INITED"); + expect(fuzzTest.stderr).toMatch(/\bedges:\s+\d+\/\d+/); + expect(fuzzTest.stderr).not.toContain("edges: -/ - ( -%)"); + }); }); diff --git a/tests/esm_instrumentation/fuzz-lazy.mjs b/tests/esm_instrumentation/fuzz-lazy.mjs new file mode 100644 index 000000000..614c3a2ed --- /dev/null +++ b/tests/esm_instrumentation/fuzz-lazy.mjs @@ -0,0 +1,23 @@ +/* + * Copyright 2026 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. + */ + +/** + * @param { Buffer } data + */ +export async function fuzz(data) { + const { checkSecret } = await import("./target.mjs"); + checkSecret(data.toString()); +} From c559d76d3a069b51a6a0c593a49c6e18c9342af9 Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Tue, 21 Apr 2026 20:31:43 +0200 Subject: [PATCH 29/30] feat(afl): show corpus initialization as [i] instead of [+] --- packages/fuzzer/rust/src/lib.rs | 20 +++++++++++++++----- tests/engine/engine.test.js | 4 ++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/fuzzer/rust/src/lib.rs b/packages/fuzzer/rust/src/lib.rs index 43c3d4b32..655c34c16 100644 --- a/packages/fuzzer/rust/src/lib.rs +++ b/packages/fuzzer/rust/src/lib.rs @@ -347,16 +347,26 @@ fn build_progress_snapshot( }) } +fn progress_marker(event: StatusEvent, in_campaign: bool, colors_enabled: bool) -> String { + let marker = if matches!(event, StatusEvent::Testcase) && !in_campaign { + "[i]" + } else { + marker_text(event) + }; + + colorize_marker(marker, event_color_code(event), colors_enabled) +} + fn format_progress_line( event: StatusEvent, snapshot: ProgressSnapshot, colors_enabled: bool, - highlight_full_line: bool, + in_campaign: bool, ) -> String { - let marker = if colors_enabled && !highlight_full_line { - marker_for_event(event, true) + let marker = if colors_enabled && !in_campaign { + progress_marker(event, false, true) } else { - marker_text(event).to_string() + progress_marker(event, in_campaign, false) }; let line = format!( "{} #{:4} | exec/s: {:>8.1} | obj: {:>3} | stab: {} | t: {}", @@ -375,7 +385,7 @@ fn format_progress_line( width = EXECUTION_FIELD_WIDTH, ); - if colors_enabled && highlight_full_line { + if colors_enabled && in_campaign { format!("\x1b[{}m{}\x1b[0m", event_color_code(event), line) } else { line diff --git a/tests/engine/engine.test.js b/tests/engine/engine.test.js index 5a0f741ba..9ef5d112a 100644 --- a/tests/engine/engine.test.js +++ b/tests/engine/engine.test.js @@ -120,7 +120,7 @@ describe("Engine selection", () => { ); expect(status).toBe(0); - const testcaseLine = findOutputLine(output, "[+]"); + const testcaseLine = findOutputLine(output, "[i]"); const heartbeatLine = findOutputLine(output, "[*]"); expect(testcaseLine).toBeDefined(); @@ -184,7 +184,7 @@ describe("Engine selection", () => { expect(initOutput).not.toContain("[*]"); const testcaseLines = initOutput .split(/\r?\n/) - .filter((line) => line.startsWith("[+]")); + .filter((line) => line.startsWith("[i]")); expect(testcaseLines).toHaveLength(4); expect(testcaseLines[0]).toContain("| corp: 1 |"); From d4aec3d0336d32af3dca182898d1736d6273028f Mon Sep 17 00:00:00 2001 From: Peter Samarin Date: Tue, 21 Apr 2026 20:38:30 +0200 Subject: [PATCH 30/30] feat(afl): print timeouts properly --- packages/fuzzer/libafl_runtime.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/fuzzer/libafl_runtime.cpp b/packages/fuzzer/libafl_runtime.cpp index f2073ddb0..694b0f7c9 100644 --- a/packages/fuzzer/libafl_runtime.cpp +++ b/packages/fuzzer/libafl_runtime.cpp @@ -346,6 +346,10 @@ std::string DescribeJsError(Napi::Env env, const Napi::Value &error) { return CollapseWhitespace(summary); } +std::string DescribeTimeout(uint64_t timeout_millis) { + return "timeout after " + std::to_string(timeout_millis) + " ms"; +} + void RecordFindingInfo(const std::string &artifact, const std::string &summary) { gFindingInfo.has_value = 1; @@ -449,7 +453,9 @@ std::string WriteArtifact(const std::string &artifact_prefix, const std::vector &input) { std::cerr << "ERROR: Exceeded timeout of " << timeout_millis << " ms for one fuzz target execution." << std::endl; - WriteArtifact(artifact_prefix, "timeout", input.data(), input.size()); + const auto artifact = + WriteArtifact(artifact_prefix, "timeout", input.data(), input.size()); + RecordFindingInfo(artifact, DescribeTimeout(timeout_millis)); _Exit(libfuzzer::EXIT_ERROR_TIMEOUT); }