Compare commits

...

6 Commits

Author SHA1 Message Date
Sosuke Suzuki
748a680836 Implement Fuzzilli::logFile 2025-10-30 15:15:56 +09:00
Sosuke Suzuki
3e165c6302 Use cpp namespace instead of c linkage 2025-10-30 15:10:22 +09:00
Sosuke Suzuki
edbc38baa0 Exit if ANABLE_FUZZILLI is enabled without ENABLE_ASAN. 2025-10-30 10:35:36 +09:00
Sosuke Suzuki
423cf0be91 Enable obj.sanitize_coverage_trace_pc_guard for Zig 2025-10-30 10:33:07 +09:00
Sosuke Suzuki
e686b426e1 Add ENABLE_FUZZILI build flag 2025-10-30 10:09:19 +09:00
Martin Schwarzl
fef4601f01 bun simple fuzzilli integration 2025-10-20 11:39:52 +00:00
14 changed files with 874 additions and 13 deletions

View File

@@ -48,6 +48,7 @@ const BunBuildOptions = struct {
/// enable debug logs in release builds
enable_logs: bool = false,
enable_asan: bool,
enable_fuzzilli: bool,
enable_valgrind: bool,
use_mimalloc: bool,
tracy_callstack_depth: u16,
@@ -97,6 +98,7 @@ const BunBuildOptions = struct {
opts.addOption(bool, "baseline", this.isBaseline());
opts.addOption(bool, "enable_logs", this.enable_logs);
opts.addOption(bool, "enable_asan", this.enable_asan);
opts.addOption(bool, "enable_fuzzilli", this.enable_fuzzilli);
opts.addOption(bool, "enable_valgrind", this.enable_valgrind);
opts.addOption(bool, "use_mimalloc", this.use_mimalloc);
opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{}", .{this.reported_nodejs_version}));
@@ -271,6 +273,7 @@ pub fn build(b: *Build) !void {
.tracy_callstack_depth = b.option(u16, "tracy_callstack_depth", "") orelse 10,
.enable_logs = b.option(bool, "enable_logs", "Enable logs in release") orelse false,
.enable_asan = b.option(bool, "enable_asan", "Enable asan") orelse false,
.enable_fuzzilli = b.option(bool, "enable_fuzzilli", "Enable Fuzzilli fuzzing support (requires asan)") orelse false,
.enable_valgrind = b.option(bool, "enable_valgrind", "Enable valgrind") orelse false,
.use_mimalloc = b.option(bool, "use_mimalloc", "Use mimalloc as default allocator") orelse false,
.llvm_codegen_threads = b.option(u32, "llvm_codegen_threads", "Number of threads to use for LLVM codegen") orelse 1,
@@ -502,6 +505,7 @@ fn addMultiCheck(
.codegen_path = root_build_options.codegen_path,
.no_llvm = root_build_options.no_llvm,
.enable_asan = root_build_options.enable_asan,
.enable_fuzzilli = root_build_options.enable_fuzzilli,
.enable_valgrind = root_build_options.enable_valgrind,
.use_mimalloc = root_build_options.use_mimalloc,
.override_no_export_cpp_apis = root_build_options.override_no_export_cpp_apis,
@@ -626,6 +630,16 @@ fn configureObj(b: *Build, opts: *BunBuildOptions, obj: *Compile) void {
obj.step.dependOn(&fail_step.step);
}
}
if (opts.enable_fuzzilli) {
if (!opts.enable_asan) {
const fail_step = b.addFail("enable_fuzzilli requires enable_asan to be enabled. Please set -Denable_asan=true");
obj.step.dependOn(&fail_step.step);
}
if (!enableFastBuild(b)) {
obj.sanitize_coverage_trace_pc_guard = true;
}
}
obj.bundle_compiler_rt = false;
obj.bundle_ubsan_rt = false;

View File

@@ -51,6 +51,19 @@ if(ENABLE_ASAN)
)
endif()
# Coverage instrumentation should ONLY be enabled for Fuzzilli builds
if(ENABLE_FUZZILLI)
register_compiler_flags(
DESCRIPTION "Enable coverage instrumentation for fuzzing with Fuzzilli"
-fsanitize-coverage=trace-pc-guard
)
register_linker_flags(
DESCRIPTION "Link coverage instrumentation for Fuzzilli"
-fsanitize-coverage=trace-pc-guard
)
endif()
# --- Optimization level ---
if(DEBUG)
register_compiler_flags(

View File

@@ -127,6 +127,13 @@ if (NOT ENABLE_ASAN)
set(ENABLE_ZIG_ASAN OFF)
endif()
# Fuzzilli fuzzing support requires ASAN
optionx(ENABLE_FUZZILLI BOOL "If Fuzzilli fuzzing support should be enabled (requires ASAN)" DEFAULT OFF)
if(ENABLE_FUZZILLI AND NOT ENABLE_ASAN)
message(FATAL_ERROR "ENABLE_FUZZILLI requires ENABLE_ASAN to be enabled. Please set -DENABLE_ASAN=ON")
endif()
if(RELEASE AND LINUX AND CI AND NOT ENABLE_ASSERTIONS AND NOT ENABLE_ASAN)
set(DEFAULT_LTO ON)
else()

View File

@@ -684,6 +684,7 @@ register_command(
-Dcpu=${ZIG_CPU}
-Denable_logs=$<IF:$<BOOL:${ENABLE_LOGS}>,true,false>
-Denable_asan=$<IF:$<BOOL:${ENABLE_ZIG_ASAN}>,true,false>
-Denable_fuzzilli=$<IF:$<BOOL:${ENABLE_FUZZILLI}>,true,false>
-Denable_valgrind=$<IF:$<BOOL:${ENABLE_VALGRIND}>,true,false>
-Duse_mimalloc=$<IF:$<BOOL:${USE_MIMALLOC_AS_DEFAULT_ALLOCATOR}>,true,false>
-Dllvm_codegen_threads=${LLVM_ZIG_CODEGEN_THREADS}
@@ -886,6 +887,10 @@ if(DEBUG)
target_compile_definitions(${bun} PRIVATE BUN_DEBUG=1)
endif()
if(ENABLE_FUZZILLI)
target_compile_definitions(${bun} PRIVATE ENABLE_FUZZILLI=1)
endif()
if(APPLE)
target_compile_definitions(${bun} PRIVATE _DARWIN_NON_CANCELABLE=1)
endif()
@@ -978,6 +983,7 @@ if(NOT WIN32)
-Wno-unused-function
-Wno-c++23-lambda-attributes
-Wno-nullability-completeness
-Wno-deprecated-declarations
-Werror
)
else()
@@ -995,6 +1001,7 @@ if(NOT WIN32)
-Werror=sometimes-uninitialized
-Wno-c++23-lambda-attributes
-Wno-nullability-completeness
-Wno-deprecated-declarations
-Werror
)

View File

@@ -38,6 +38,7 @@
"build:ci": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_VERBOSE_MAKEFILE=ON -DCI=true -B build/release-ci --verbose --fresh",
"build:assert": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_ASSERTIONS=ON -DENABLE_LOGS=ON -B build/release-assert",
"build:asan": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -DENABLE_ASSERTIONS=ON -DENABLE_LOGS=OFF -DENABLE_ASAN=ON -DENABLE_LTO=OFF -B build/release-asan",
"build:fuzzilli": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Debug -DENABLE_FUZZILLI=ON -DENABLE_ASAN=ON -B build/debug-fuzzilli",
"build:logs": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -DENABLE_LOGS=ON -B build/release-logs",
"build:safe": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -DZIG_OPTIMIZE=ReleaseSafe -B build/release-safe",
"build:smol": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=MinSizeRel -B build/release-smol",

View File

@@ -92,17 +92,8 @@ private:
MACRO("UNSUBSCRIBE") \
#ifndef _WIN32
static constexpr std::array<const std::string, 35> HTTP_METHODS = {
#define MACRO(name) std::string {name},
FOR_EACH_HTTP_METHOD(MACRO)
#undef MACRO
};
static std::span<const std::string> getAllHttpMethods() {
return {HTTP_METHODS.data(), HTTP_METHODS.size()};
}
#else
// Windows, and older C++ can't do constexpr std::array<const std::string, 35>
// Use const char* for constexpr compatibility across all platforms
// constexpr std::string requires heap allocation and cannot be used at namespace scope
static constexpr std::array<const char*, 35> HTTP_METHODS = {
#define MACRO(name) name,
FOR_EACH_HTTP_METHOD(MACRO)
@@ -121,7 +112,6 @@ private:
});
return {methods.data(), methods.size()};
}
#endif
#undef FOR_EACH_HTTP_METHOD

View File

@@ -0,0 +1,312 @@
#include "FuzzilliREPRL.h"
#include "root.h"
#include "JavaScriptCore/JSGlobalObject.h"
#include "JavaScriptCore/CallFrame.h"
#include "JavaScriptCore/Identifier.h"
#include "wtf/text/WTFString.h"
#include "ZigGlobalObject.h"
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <cerrno>
#ifdef ENABLE_FUZZILLI
#include <sanitizer/asan_interface.h>
#endif
// Signal handler to ensure output is flushed before crash
static void fuzzilliSignalHandler(int sig) {
// Flush all output
fflush(stdout);
fflush(stderr);
fsync(STDOUT_FILENO);
fsync(STDERR_FILENO);
// Re-raise the signal with default handler
signal(sig, SIG_DFL);
raise(sig);
}
// Implementation of the global fuzzilli() function for Bun
// This function is used by Fuzzilli to:
// 1. Test crash detection with fuzzilli('FUZZILLI_CRASH', type)
// 2. Print output with fuzzilli('FUZZILLI_PRINT', value)
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES functionFuzzilli(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) {
JSC::VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
if (callFrame->argumentCount() < 1) {
return JSC::JSValue::encode(JSC::jsUndefined());
}
JSC::JSValue arg0 = callFrame->argument(0);
WTF::String command = arg0.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
if (command == "FUZZILLI_CRASH"_s) {
// Fuzzilli uses this to test crash detection
// The second argument is an integer specifying the crash type
int crashType = 0;
if (callFrame->argumentCount() >= 2) {
JSC::JSValue arg1 = callFrame->argument(1);
crashType = arg1.toInt32(globalObject);
}
// Print the crash type for debugging
fprintf(stdout, "FUZZILLI_CRASH: %d\n", crashType);
fflush(stdout);
// Trigger different types of crashes for testing (similar to V8 implementation)
switch (crashType) {
case 0:
// IMMEDIATE_CRASH - Simple abort
std::abort();
break;
case 1:
// CHECK failure - assertion in release builds
// Use __builtin_trap() for a direct crash
__builtin_trap();
break;
case 2:
// DCHECK failure - always crash (use trap instead of assert which is disabled in release)
__builtin_trap();
break;
case 3:
// Wild write - heap buffer overflow (will be caught by ASAN)
{
volatile char* buffer = new char[10];
buffer[20] = 'x'; // Write past the end - ASAN should catch this
// Don't delete to make it more obvious
}
break;
case 4:
// Use-after-free (will be caught by ASAN)
{
volatile char* buffer = new char[10];
delete[] buffer;
buffer[0] = 'x'; // Use after free - ASAN should catch this
}
break;
case 5:
// Null pointer dereference
{
volatile int* ptr = nullptr;
*ptr = 42;
}
break;
case 6:
// Stack buffer overflow (will be caught by ASAN)
{
volatile char buffer[10];
volatile char* p = const_cast<char*>(buffer);
p[20] = 'x'; // Write past stack buffer
}
break;
case 7:
// Double free (will be caught by ASAN)
{
char* buffer = new char[10];
delete[] buffer;
delete[] buffer; // Double free - ASAN should catch this
}
break;
case 8:
// Verify DEBUG or ASAN is enabled
#if defined(DEBUG) || __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
// Expected to be compiled with debug or ASAN, don't crash
fprintf(stdout, "DEBUG or ASAN is enabled\n");
fflush(stdout);
#else
// If neither DEBUG nor ASAN is enabled, crash to indicate misconfiguration
fprintf(stderr, "ERROR: Expected DEBUG or ASAN to be enabled\n");
fflush(stderr);
std::abort();
#endif
break;
default:
// Unknown crash type, just abort
std::abort();
break;
}
} else if (command == "FUZZILLI_PRINT"_s) {
if (callFrame->argumentCount() >= 2) {
String string = callFrame->argument(1).toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, { });
Fuzzilli::logFile().println(string);
Fuzzilli::logFile().flush();
}
}
return JSC::JSValue::encode(JSC::jsUndefined());
}
// Register the fuzzilli() function on a Bun global object
void Fuzzilli::registerFuzzilliFunction(Zig::GlobalObject* globalObject) {
JSC::VM& vm = globalObject->vm();
// Install signal handlers to ensure output is flushed before crashes
// This is important for ASAN output to be captured
signal(SIGABRT, fuzzilliSignalHandler);
signal(SIGSEGV, fuzzilliSignalHandler);
signal(SIGILL, fuzzilliSignalHandler);
signal(SIGFPE, fuzzilliSignalHandler);
globalObject->putDirectNativeFunction(
vm,
globalObject,
JSC::Identifier::fromString(vm, "fuzzilli"_s),
2, // max 2 arguments
functionFuzzilli,
JSC::ImplementationVisibility::Public,
JSC::NoIntrinsic,
JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete
);
}
WTF::FilePrintStream& Fuzzilli::logFile()
{
constexpr uint8_t REPRL_DWFD = 103;
static LazyNeverDestroyed<FilePrintStream> result;
static std::once_flag flag;
std::call_once(flag, []() {
if (FILE* file = fdopen(REPRL_DWFD, "w"))
result.construct(file, FilePrintStream::AdoptionMode::Adopt);
else {
result.construct(stdout, FilePrintStream::AdoptionMode::Borrow);
dataLogLn("Fuzzer output channel not available, printing to stdout instead.");
}
});
return result.get();
}
// ============================================================================
// Coverage instrumentation for Fuzzilli
// Based on workerd implementation
// Only enabled when ENABLE_FUZZILLI is set
// ============================================================================
extern "C" {
#ifdef ENABLE_FUZZILLI
#define SHM_SIZE 0x200000
#define MAX_EDGES ((SHM_SIZE - 4) * 8)
// Global variable used by sanitizer coverage to track the lowest stack address
// This needs to be provided when using custom coverage instrumentation
__attribute__((used)) uintptr_t __sancov_lowest_stack = 0;
struct shmem_data {
uint32_t num_edges;
unsigned char edges[];
};
// Global coverage data
static struct shmem_data* __shmem = nullptr;
static uint32_t* __edges_start = nullptr;
static uint32_t* __edges_stop = nullptr;
// Reset edge guards for next iteration
static void __sanitizer_cov_reset_edgeguards() {
if (!__edges_start || !__edges_stop) return;
uint64_t N = 0;
for (uint32_t* x = __edges_start; x < __edges_stop && N < MAX_EDGES; x++) {
*x = ++N;
}
}
// Called by the compiler to initialize coverage instrumentation
extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) {
// Avoid duplicate initialization
if (start == stop || *start) return;
if (__edges_start != nullptr || __edges_stop != nullptr) {
fprintf(stderr, "[COV] Coverage instrumentation is only supported for a single module\n");
_exit(-1);
}
__edges_start = start;
__edges_stop = stop;
// Map the shared memory region
const char* shm_key = getenv("SHM_ID");
if (!shm_key) {
fprintf(stderr, "[COV] no shared memory bitmap available, using malloc\n");
__shmem = (struct shmem_data*)malloc(SHM_SIZE);
if (!__shmem) {
fprintf(stderr, "[COV] Failed to allocate coverage bitmap\n");
_exit(-1);
}
memset(__shmem, 0, SHM_SIZE);
} else {
int fd = shm_open(shm_key, O_RDWR, S_IREAD | S_IWRITE);
if (fd <= -1) {
fprintf(stderr, "[COV] Failed to open shared memory region: %s\n", strerror(errno));
_exit(-1);
}
__shmem = (struct shmem_data*)mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (__shmem == MAP_FAILED) {
fprintf(stderr, "[COV] Failed to mmap shared memory region\n");
_exit(-1);
}
}
__sanitizer_cov_reset_edgeguards();
__shmem->num_edges = stop - start;
fprintf(stderr, "[COV] Coverage instrumentation initialized with %u edges\n", __shmem->num_edges);
}
// Called by the compiler for each edge
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
// There's a small race condition here: if this function executes in two threads for the same
// edge at the same time, the first thread might disable the edge (by setting the guard to zero)
// before the second thread fetches the guard value (and thus the index). However, our
// instrumentation ignores the first edge (see libcoverage.c) and so the race is unproblematic.
if (!__shmem) return;
uint32_t index = *guard;
// If this function is called before coverage instrumentation is properly initialized we want to return early.
if (!index) return;
__shmem->edges[index / 8] |= 1 << (index % 8);
*guard = 0;
}
// Function to reset coverage for next REPRL iteration
// This should be called after each script execution
extern "C" void Bun__REPRL__resetCoverage() {
if (__shmem && __edges_start && __edges_stop) {
__sanitizer_cov_reset_edgeguards();
}
}
#else
// Stub implementations when ENABLE_FUZZILLI is not enabled
extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) {
(void)start;
(void)stop;
}
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
(void)guard;
}
extern "C" void Bun__REPRL__resetCoverage() {
}
#endif // ENABLE_FUZZILLI
} // extern "C"

View File

@@ -0,0 +1,11 @@
#pragma once
#include "ZigGlobalObject.h"
#include <wtf/FilePrintStream.h>
namespace Fuzzilli {
void registerFuzzilliFunction(Zig::GlobalObject* globalObject);
WTF::FilePrintStream& logFile();
} // namespace Fuzzilli

View File

@@ -214,6 +214,11 @@
#include <unistd.h>
#endif
#ifdef ENABLE_FUZZILLI
#include "FuzzilliREPRL.h"
#endif
using namespace Bun;
BUN_DECLARE_HOST_FUNCTION(Bun__NodeUtil__jsParseArgs);
@@ -501,6 +506,13 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client,
Bun__setDefaultGlobalObject(globalObject);
JSC::gcProtect(globalObject);
#ifdef ENABLE_FUZZILLI
// Register fuzzilli() function if in fuzzilli mode
if (std::getenv("BUN_FUZZILLI_MODE")) {
Fuzzilli::registerFuzzilliFunction(static_cast<Zig::GlobalObject*>(globalObject));
}
#endif
vm.setOnComputeErrorInfo(computeErrorInfoWrapperToString);
vm.setOnComputeErrorInfoJSValue(computeErrorInfoWrapperToJSValue);
vm.setOnEachMicrotaskTick([](JSC::VM& vm) -> void {

View File

@@ -91,6 +91,7 @@ pub const PackCommand = @import("./cli/pack_command.zig").PackCommand;
pub const AuditCommand = @import("./cli/audit_command.zig").AuditCommand;
pub const InitCommand = @import("./cli/init_command.zig").InitCommand;
pub const WhyCommand = @import("./cli/why_command.zig").WhyCommand;
pub const FuzzilliCommand = @import("./cli/fuzzilli_command.zig").FuzzilliCommand;
pub const Arguments = @import("./cli/Arguments.zig");
@@ -619,6 +620,7 @@ pub const Command = struct {
RootCommandMatcher.case("prune") => .ReservedCommand,
RootCommandMatcher.case("list") => .ReservedCommand,
RootCommandMatcher.case("why") => .WhyCommand,
RootCommandMatcher.case("fuzzilli") => .FuzzilliCommand,
RootCommandMatcher.case("-e") => .AutoCommand,
@@ -928,6 +930,11 @@ pub const Command = struct {
try ExecCommand.exec(ctx);
} else Tag.printHelp(.ExecCommand, true);
},
.FuzzilliCommand => {
const ctx = try Command.init(allocator, log, .FuzzilliCommand);
try FuzzilliCommand.exec(ctx);
return;
},
}
}
@@ -963,6 +970,7 @@ pub const Command = struct {
PublishCommand,
AuditCommand,
WhyCommand,
FuzzilliCommand,
/// Used by crash reports.
///
@@ -1000,6 +1008,7 @@ pub const Command = struct {
.PublishCommand => 'k',
.AuditCommand => 'A',
.WhyCommand => 'W',
.FuzzilliCommand => 'F',
};
}
@@ -1313,7 +1322,7 @@ pub const Command = struct {
Output.flush();
},
else => {
HelpCommand.printWithReason(.explicit);
HelpCommand.printWithReason(.explicit, false);
},
}
}

View File

@@ -0,0 +1,66 @@
const std = @import("std");
const bun = @import("bun");
const Output = bun.Output;
const Global = bun.Global;
const Environment = bun.Environment;
const std_posix = std.posix;
const Command = bun.cli.Command;
extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int;
const Run = bun.bun_js.Run;
pub const FuzzilliCommand = struct {
pub fn exec(ctx: Command.Context) !void {
@branchHint(.cold);
if (!Environment.isPosix) {
Output.prettyErrorln("<r><red>error<r>: Fuzzilli mode is only supported on POSIX systems", .{});
Global.exit(1);
}
// Set an environment variable so we can detect fuzzilli mode in JavaScript
_ = setenv("BUN_FUZZILLI_MODE", "1", 1);
// Verify REPRL file descriptors are available
const REPRL_CRFD: c_int = 100;
verifyFd(REPRL_CRFD) catch {
Output.prettyErrorln("<r><red>error<r>: REPRL_CRFD (fd {d}) is not available. Run Bun under Fuzzilli.", .{REPRL_CRFD});
Output.prettyErrorln("<r><d>Example: fuzzilli --profile=bun /path/to/bun fuzzilli<r>", .{});
Global.exit(1);
};
// Always embed the REPRL script (it's small and not worth the runtime overhead)
const reprl_script = @embedFile("../js/internal/fuzzilli-reprl-minimal.ts");
// Create temp file for the script
var temp_dir = std.fs.cwd().openDir("/tmp", .{}) catch {
Output.prettyErrorln("<r><red>error<r>: Could not access /tmp directory", .{});
Global.exit(1);
};
defer temp_dir.close();
const temp_file_name = "bun-fuzzilli-reprl.js";
const temp_file = temp_dir.createFile(temp_file_name, .{ .truncate = true }) catch {
Output.prettyErrorln("<r><red>error<r>: Could not create temp file", .{});
Global.exit(1);
};
defer temp_file.close();
_ = temp_file.writeAll(reprl_script) catch {
Output.prettyErrorln("<r><red>error<r>: Could not write temp file", .{});
Global.exit(1);
};
Output.prettyErrorln("<r><d>[FUZZILLI] Temp file written, booting JS runtime<r>", .{});
// Run the temp file
const temp_path = "/tmp/bun-fuzzilli-reprl.js";
try Run.boot(ctx, temp_path, null);
}
fn verifyFd(fd: c_int) !void {
const stat = try std_posix.fstat(fd);
_ = stat;
}
};

View File

@@ -0,0 +1,71 @@
// Minimal REPRL wrapper - working baseline
// Will add APIs step-by-step
const REPRL_CRFD = 100; // Control read FD
const REPRL_CWFD = 101; // Control write FD
const REPRL_DRFD = 102; // Data read FD
const REPRL_DWFD = 103; // Data write FD
const fs = require("node:fs");
// Disable process.abort to prevent false positive crashes from fuzzer
if (process && process.abort) {
process.abort = undefined;
}
// Send HELO handshake FIRST - REPRLRun waits for this!
fs.writeSync(REPRL_CWFD, Buffer.from("HELO"));
// Read HELO response
const helo_response = Buffer.alloc(4);
fs.readSync(REPRL_CRFD, helo_response, 0, 4, null);
// Main REPRL loop
while (true) {
console.error("[REPRL] Waiting for command...");
// Read command
const cmd = Buffer.alloc(4);
const cmd_n = fs.readSync(REPRL_CRFD, cmd, 0, 4, null);
console.error(`[REPRL] Read command: ${cmd.toString()}, bytes: ${cmd_n}`);
if (cmd_n === 0) {
break; // EOF
}
if (cmd_n !== 4 || cmd.toString() !== "exec") {
throw new Error(`Invalid REPRL command: expected 'exec', got ${cmd.toString()}`);
}
// Read script size (8 bytes, little-endian)
const size_bytes = Buffer.alloc(8);
fs.readSync(REPRL_CRFD, size_bytes, 0, 8, null);
const script_size = Number(size_bytes.readBigUInt64LE(0));
// Read script data from REPRL_DRFD
const script_data = Buffer.alloc(script_size);
let total_read = 0;
while (total_read < script_size) {
const n = fs.readSync(REPRL_DRFD, script_data, total_read, script_size - total_read, null);
if (n === 0) break;
total_read += n;
}
const script = script_data.toString("utf8");
// Execute script
let exit_code = 0;
try {
(0, eval)(script);
} catch (e) {
console.log(`uncaught:${e}`);
exit_code = 1;
}
// Send status back
const status = exit_code << 8;
const status_bytes = Buffer.alloc(4);
status_bytes.writeUInt32LE(status, 0);
fs.writeSync(REPRL_CWFD, status_bytes);
}
export default {};

View File

@@ -0,0 +1,324 @@
// Comprehensive REPRL wrapper for Bun fuzzing with all runtime APIs exposed
// Based on workerd's approach to maximize fuzzing coverage
// https://bun.com/docs/runtime
const REPRL_CRFD = 100; // Control read FD
const REPRL_CWFD = 101; // Control write FD
const REPRL_DRFD = 102; // Data read FD
const REPRL_DWFD = 103; // Data write FD
const fs = require("node:fs");
// ============================================================================
// Expose ALL Bun APIs to globalThis for fuzzing
// https://bun.com/docs/runtime/bun-apis
// ============================================================================
// Bun global is already available
globalThis.Bun = Bun;
// File system APIs
globalThis.file = Bun.file;
globalThis.write = Bun.write;
// Process APIs
globalThis.spawn = Bun.spawn;
globalThis.spawnSync = Bun.spawnSync;
globalThis.$ = Bun.$;
globalThis.sleep = Bun.sleep;
globalThis.which = Bun.which;
// Crypto APIs
globalThis.password = Bun.password;
// Network APIs
globalThis.serve = Bun.serve;
globalThis.connect = Bun.connect;
globalThis.listen = Bun.listen;
// Compression
globalThis.deflateSync = Bun.deflateSync;
globalThis.gzipSync = Bun.gzipSync;
globalThis.inflateSync = Bun.inflateSync;
globalThis.gunzipSync = Bun.gunzipSync;
// Utilities
globalThis.inspect = Bun.inspect;
globalThis.nanoseconds = Bun.nanoseconds;
globalThis.readableStreamToArrayBuffer = Bun.readableStreamToArrayBuffer;
globalThis.readableStreamToBlob = Bun.readableStreamToBlob;
globalThis.readableStreamToJSON = Bun.readableStreamToJSON;
globalThis.readableStreamToText = Bun.readableStreamToText;
globalThis.resolveSync = Bun.resolveSync;
globalThis.resolve = Bun.resolve;
globalThis.FileSystemRouter = Bun.FileSystemRouter;
globalThis.Glob = Bun.Glob;
globalThis.Transpiler = Bun.Transpiler;
// Build/dev
globalThis.build = Bun.build;
globalThis.plugin = Bun.plugin;
// Env
globalThis.env = Bun.env;
globalThis.main = Bun.main;
globalThis.argv = Bun.argv;
globalThis.revision = Bun.revision;
globalThis.version = Bun.version;
// ============================================================================
// Web Standard APIs (already global but re-expose explicitly)
// https://bun.com/docs/runtime/web-apis
// ============================================================================
// Fetch API
globalThis.fetch = fetch;
globalThis.Request = Request;
globalThis.Response = Response;
globalThis.Headers = Headers;
// URL APIs
globalThis.URL = URL;
globalThis.URLSearchParams = URLSearchParams;
// Streams
globalThis.ReadableStream = ReadableStream;
globalThis.WritableStream = WritableStream;
globalThis.TransformStream = TransformStream;
globalThis.ByteLengthQueuingStrategy = ByteLengthQueuingStrategy;
globalThis.CountQueuingStrategy = CountQueuingStrategy;
globalThis.ReadableStreamBYOBReader = ReadableStreamBYOBReader;
globalThis.ReadableStreamDefaultReader = ReadableStreamDefaultReader;
// Text APIs
globalThis.TextEncoder = TextEncoder;
globalThis.TextDecoder = TextDecoder;
globalThis.atob = atob;
globalThis.btoa = btoa;
// Blob/File
globalThis.Blob = Blob;
globalThis.File = File;
globalThis.FormData = FormData;
// WebSocket
globalThis.WebSocket = WebSocket;
// Events
globalThis.EventTarget = EventTarget;
globalThis.Event = Event;
globalThis.CustomEvent = CustomEvent;
globalThis.MessageEvent = MessageEvent;
globalThis.ErrorEvent = ErrorEvent;
globalThis.CloseEvent = CloseEvent;
// Abort
globalThis.AbortController = AbortController;
globalThis.AbortSignal = AbortSignal;
// MessageChannel
globalThis.MessageChannel = MessageChannel;
globalThis.MessagePort = MessagePort;
globalThis.BroadcastChannel = BroadcastChannel;
// Timers
globalThis.setTimeout = setTimeout;
globalThis.setInterval = setInterval;
globalThis.clearTimeout = clearTimeout;
globalThis.clearInterval = clearInterval;
globalThis.setImmediate = setImmediate;
globalThis.clearImmediate = clearImmediate;
globalThis.queueMicrotask = queueMicrotask;
// Performance
globalThis.performance = performance;
globalThis.Performance = Performance;
// Crypto
globalThis.crypto = crypto;
globalThis.Crypto = Crypto;
globalThis.SubtleCrypto = SubtleCrypto;
globalThis.CryptoKey = CryptoKey;
// DOM Exception
globalThis.DOMException = DOMException;
// Structured clone
globalThis.structuredClone = structuredClone;
// Console
globalThis.console = console;
// Navigator
globalThis.navigator = navigator;
// HTML Rewriter
globalThis.HTMLRewriter = HTMLRewriter;
// ============================================================================
// Node.js Compatibility APIs
// https://bun.com/docs/runtime/nodejs-apis
// ============================================================================
globalThis.Buffer = Buffer;
globalThis.process = process;
globalThis.global = globalThis;
// Make common Node modules available
globalThis.require = require;
globalThis.__dirname = "/";
globalThis.__filename = "/fuzzilli.js";
// ============================================================================
// Mock implementations for features that would hang/fail in fuzzing
// ============================================================================
// Mock Bun.serve to avoid actually starting servers
globalThis.MOCK_SERVE = options => ({
url: new URL("http://localhost:3000"),
hostname: "localhost",
port: 3000,
development: false,
fetch: options?.fetch || (() => new Response("mock")),
stop: () => {},
reload: () => {},
ref: () => {},
unref: () => {},
requestIP: () => null,
publish: () => 0,
upgrade: () => false,
pendingWebsockets: 0,
});
// Mock subprocess operations
globalThis.MOCK_SPAWN = () => ({
pid: 12345,
stdin: new WritableStream(),
stdout: new ReadableStream(),
stderr: new ReadableStream(),
exited: Promise.resolve(0),
kill: () => true,
ref: () => {},
unref: () => {},
resourceUsage: () => ({ cpuTime: { user: 0, system: 0 }, maxRSS: 0 }),
});
// Mock file operations
globalThis.MOCK_FILE = {
name: "test.txt",
size: 1024,
type: "text/plain",
lastModified: Date.now(),
text: () => Promise.resolve("Mock file content"),
arrayBuffer: () => Promise.resolve(new ArrayBuffer(1024)),
json: () => Promise.resolve({ mock: "data" }),
blob: () => Promise.resolve(new Blob(["mock"], { type: "text/plain" })),
stream: () => new ReadableStream(),
slice: (start, end) => globalThis.MOCK_FILE,
writer: () => ({ write: () => 0, end: () => 0, flush: () => Promise.resolve() }),
};
// Mock SQLite database
globalThis.MOCK_DB = {
query: (sql, ...params) => ({
all: () => [{ id: 1, name: "test" }],
get: () => ({ id: 1, name: "test" }),
run: () => ({ changes: 1, lastInsertRowid: 1 }),
values: () => [[1, "test"]],
}),
prepare: sql => globalThis.MOCK_DB.query(sql),
exec: () => {},
close: () => {},
serialize: () => new Uint8Array(100),
loadExtension: () => {},
};
// Mock Glob
globalThis.MOCK_GLOB = {
scan: pattern => ({
[Symbol.asyncIterator]: async function* () {
yield "file1.txt";
yield "file2.txt";
},
}),
scanSync: () => ["file1.txt", "file2.txt"],
match: () => true,
};
// ============================================================================
// REPRL Protocol Loop
// ============================================================================
// Verify we're running under Fuzzilli before starting REPRL loop
// The Zig code should have already checked, but double-check here
try {
// Try to stat fd 100 to see if it exists
const stat = fs.fstatSync(REPRL_CRFD);
} catch (e) {
// FD doesn't exist - not running under Fuzzilli
console.error("ERROR: REPRL file descriptors not available. Must run under Fuzzilli.");
process.exit(1);
}
// Send HELO handshake
fs.writeSync(REPRL_CWFD, Buffer.from("HELO"));
// Read HELO response
const response = Buffer.alloc(4);
const responseBytes = fs.readSync(REPRL_CRFD, response, 0, 4, null);
if (responseBytes !== 4) {
throw new Error(`REPRL handshake failed: expected 4 bytes, got ${responseBytes}`);
}
// Main REPRL loop
while (true) {
// Read command
const cmd = Buffer.alloc(4);
const cmd_n = fs.readSync(REPRL_CRFD, cmd, 0, 4, null);
if (cmd_n === 0) {
// EOF
break;
}
if (cmd_n !== 4 || cmd.toString() !== "exec") {
throw new Error(`Invalid REPRL command: expected 'exec', got ${cmd.toString()}`);
}
// Read script size (8 bytes, little-endian)
const size_bytes = Buffer.alloc(8);
fs.readSync(REPRL_CRFD, size_bytes, 0, 8, null);
const script_size = Number(size_bytes.readBigUInt64LE(0));
// Read script data from REPRL_DRFD
const script_data = Buffer.alloc(script_size);
let total_read = 0;
while (total_read < script_size) {
const n = fs.readSync(REPRL_DRFD, script_data, total_read, script_size - total_read, null);
if (n === 0) break;
total_read += n;
}
const script = script_data.toString("utf8");
// Execute script
let exit_code = 0;
try {
// Use indirect eval to execute in global scope
(0, eval)(script);
} catch (e) {
// Print uncaught exception like workerd does
console.log(`uncaught:${e}`);
exit_code = 1;
}
// Send status back (4 bytes: exit code in REPRL format)
// Format: lower 8 bits = signal number, next 8 bits = exit code
const status = exit_code << 8;
const status_bytes = Buffer.alloc(4);
status_bytes.writeUInt32LE(status, 0);
fs.writeSync(REPRL_CWFD, status_bytes);
}
// Export to satisfy module bundler
export default {};

View File

@@ -0,0 +1,24 @@
import { describe, test, expect } from "bun:test";
import { spawn } from "bun";
import { bunExe } from "harness";
describe("fuzzilli command", () => {
test("bun fuzzilli command exists", () => {
// Just verify the command doesn't crash when invoked
// We can't actually test REPRL without setting up FDs
const result = spawn({
cmd: [bunExe(), "fuzzilli"],
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
BUN_DEBUG_QUIET_LOGS: "1",
},
});
// The command will fail because REPRL FDs aren't set up
// but it should at least start
expect(result).toBeDefined();
});
});