From abb1b0c4d75acabf1742b951a1f739068c94a7bd Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 20 Nov 2025 23:37:31 -0800 Subject: [PATCH] test(ENG-21524): Fuzzilli Stop-Gap (#24826) ### What does this PR do? Adds [@mschwarzl's Fuzzilli Support PR](https://github.com/oven-sh/bun/pull/23862) with the changes necessary to be able to: - Run it in CI - Make no impact on `debug` and `release` mode. ### How did you verify your code works? --------- Co-authored-by: Martin Schwarzl Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dylan Conway --- build.zig | 11 + cmake/CompilerFlags.cmake | 17 ++ cmake/Options.cmake | 2 + cmake/targets/BuildBun.cmake | 1 + package.json | 1 + src/bun.js/bindings/FuzzilliREPRL.cpp | 286 ++++++++++++++++++++++++ src/bun.js/bindings/ZigGlobalObject.cpp | 9 + src/cli.zig | 18 +- src/cli/fuzzilli_command.zig | 63 ++++++ src/env.zig | 1 + src/js/eval/fuzzilli-reprl.ts | 91 ++++++++ 11 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 src/bun.js/bindings/FuzzilliREPRL.cpp create mode 100644 src/cli/fuzzilli_command.zig create mode 100644 src/js/eval/fuzzilli-reprl.ts diff --git a/build.zig b/build.zig index eb16d47401..3b50fd1fbe 100644 --- a/build.zig +++ b/build.zig @@ -32,6 +32,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, @@ -81,6 +82,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("{f}", .{this.reported_nodejs_version})); @@ -255,6 +257,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 instrumentation") 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, @@ -490,6 +493,7 @@ fn addMultiCheck( .no_llvm = root_build_options.no_llvm, .enable_asan = root_build_options.enable_asan, .enable_valgrind = root_build_options.enable_valgrind, + .enable_fuzzilli = root_build_options.enable_fuzzilli, .use_mimalloc = root_build_options.use_mimalloc, .override_no_export_cpp_apis = root_build_options.override_no_export_cpp_apis, }; @@ -605,13 +609,20 @@ fn configureObj(b: *Build, opts: *BunBuildOptions, obj: *Compile) void { obj.no_link_obj = opts.os != .windows; + if (opts.enable_asan and !enableFastBuild(b)) { if (@hasField(Build.Module, "sanitize_address")) { + if (opts.enable_fuzzilli) { + obj.sanitize_coverage_trace_pc_guard = true; + } obj.root_module.sanitize_address = true; } else { const fail_step = b.addFail("asan is not supported on this platform"); obj.step.dependOn(&fail_step.step); } + } else if (opts.enable_fuzzilli) { + const fail_step = b.addFail("fuzzilli requires asan"); + obj.step.dependOn(&fail_step.step); } obj.bundle_compiler_rt = false; obj.bundle_ubsan_rt = false; diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake index cff32fb166..bff19e1974 100644 --- a/cmake/CompilerFlags.cmake +++ b/cmake/CompilerFlags.cmake @@ -51,6 +51,23 @@ if(ENABLE_ASAN) ) endif() +if(ENABLE_FUZZILLI) + register_compiler_flags( + DESCRIPTION "Enable coverage instrumentation for fuzzing" + -fsanitize-coverage=trace-pc-guard + ) + + register_linker_flags( + DESCRIPTION "Link coverage instrumentation" + -fsanitize-coverage=trace-pc-guard + ) + + register_compiler_flags( + DESCRIPTION "Enable fuzzilli-specific code" + -DFUZZILLI_ENABLED + ) +endif() + # --- Optimization level --- if(DEBUG) register_compiler_flags( diff --git a/cmake/Options.cmake b/cmake/Options.cmake index ac6ce10c74..e54f6db166 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -127,6 +127,8 @@ if (NOT ENABLE_ASAN) set(ENABLE_ZIG_ASAN OFF) endif() +optionx(ENABLE_FUZZILLI BOOL "If fuzzilli support should be enabled" DEFAULT OFF) + if(RELEASE AND LINUX AND CI AND NOT ENABLE_ASSERTIONS AND NOT ENABLE_ASAN) set(DEFAULT_LTO ON) else() diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 43b061846b..6b23266ade 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -695,6 +695,7 @@ register_command( -Dcpu=${ZIG_CPU} -Denable_logs=$,true,false> -Denable_asan=$,true,false> + -Denable_fuzzilli=$,true,false> -Denable_valgrind=$,true,false> -Duse_mimalloc=$,true,false> -Dllvm_codegen_threads=${LLVM_ZIG_CODEGEN_THREADS} diff --git a/package.json b/package.json index c8bdbc36fa..ee34f430b8 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "bd:v": "(bun run --silent build:debug &> /tmp/bun.debug.build.log || (cat /tmp/bun.debug.build.log && rm -rf /tmp/bun.debug.build.log && exit 1)) && rm -f /tmp/bun.debug.build.log && ./build/debug/bun-debug", "bd": "BUN_DEBUG_QUIET_LOGS=1 bun --silent bd:v", "build:debug": "export COMSPEC=\"C:\\Windows\\System32\\cmd.exe\" && bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Debug -B build/debug --log-level=NOTICE", + "build:debug:fuzzilli": "export COMSPEC=\"C:\\Windows\\System32\\cmd.exe\" && bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Debug -B build/debug-fuzz -DENABLE_FUZZILLI=ON --log-level=NOTICE", "build:debug:noasan": "export COMSPEC=\"C:\\Windows\\System32\\cmd.exe\" && bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=OFF -B build/debug --log-level=NOTICE", "build:release": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -B build/release", "build:ci": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_VERBOSE_MAKEFILE=ON -DCI=true -B build/release-ci --verbose --fresh", diff --git a/src/bun.js/bindings/FuzzilliREPRL.cpp b/src/bun.js/bindings/FuzzilliREPRL.cpp new file mode 100644 index 0000000000..b3e0815271 --- /dev/null +++ b/src/bun.js/bindings/FuzzilliREPRL.cpp @@ -0,0 +1,286 @@ +#ifdef FUZZILLI_ENABLED +#include "JavaScriptCore/CallFrame.h" +#include "JavaScriptCore/Identifier.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include "ZigGlobalObject.h" +#include "root.h" +#include "wtf/text/WTFString.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define REPRL_DWFD 103 + +extern "C" { + +// 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(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 + // Expected to be compiled with debug or ASAN, don't crash + fprintf(stdout, "DEBUG or ASAN is enabled\n"); + fflush(stdout); + break; + + default: + // Unknown crash type, just abort + std::abort(); + break; + } + } else if (command == "FUZZILLI_PRINT"_s) { + // Optional: Print the second argument + if (callFrame->argumentCount() >= 2) { + JSC::JSValue arg1 = callFrame->argument(1); + WTF::String output = arg1.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined())); + + FILE* f = fdopen(REPRL_DWFD, "w"); + fprintf(f, "%s\n", output.utf8().data()); + fflush(f); + } + } + + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +// ============================================================================ +// Coverage instrumentation for Fuzzilli +// Based on workerd implementation +// Only enabled when ASAN is active +// ============================================================================ + +#define SHM_SIZE 0x200000 +#define MAX_EDGES ((SHM_SIZE - 4) * 8) + +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 +JSC_DEFINE_HOST_FUNCTION(jsResetCoverage, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*)) +{ + __sanitizer_cov_reset_edgeguards(); + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +// Register the fuzzilli() function on a Bun global object +void Bun__REPRL__registerFuzzilliFunctions(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); + + globalObject->putDirectNativeFunction( + vm, + globalObject, + JSC::Identifier::fromString(vm, "resetCoverage"_s), + 0, + jsResetCoverage, + JSC::ImplementationVisibility::Public, + JSC::NoIntrinsic, + JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +} // extern "C" + +#endif // FUZZILLI_ENABLED diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index c2d3b64f8a..a47cb93a25 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -260,6 +260,11 @@ extern "C" unsigned getJSCBytecodeCacheVersion() return getWebKitBytecodeCacheVersion(); } +// Declare fuzzilli function registration from FuzzilliREPRL.cpp +#ifdef FUZZILLI_ENABLED +extern "C" void Bun__REPRL__registerFuzzilliFunctions(Zig::GlobalObject*); +#endif + extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(const char* ptr, size_t length), bool evalMode) { static std::once_flag jsc_init_flag; @@ -503,6 +508,10 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client, Bun__setDefaultGlobalObject(globalObject); JSC::gcProtect(globalObject); +#ifdef FUZZILLI_ENABLED + Bun__REPRL__registerFuzzilliFunctions(static_cast(globalObject)); +#endif + vm.setOnComputeErrorInfo(computeErrorInfoWrapperToString); vm.setOnComputeErrorInfoJSValue(computeErrorInfoWrapperToJSValue); vm.setComputeLineColumnWithSourcemap(computeLineColumnWithSourcemap); diff --git a/src/cli.zig b/src/cli.zig index 63b605339c..204999fbe9 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -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"); @@ -626,6 +627,10 @@ pub const Command = struct { RootCommandMatcher.case("prune") => .ReservedCommand, RootCommandMatcher.case("list") => .PackageManagerCommand, RootCommandMatcher.case("why") => .WhyCommand, + RootCommandMatcher.case("fuzzilli") => if (bun.Environment.enable_fuzzilli) + .FuzzilliCommand + else + .AutoCommand, RootCommandMatcher.case("-e") => .AutoCommand, @@ -935,6 +940,15 @@ pub const Command = struct { try ExecCommand.exec(ctx); } else Tag.printHelp(.ExecCommand, true); }, + .FuzzilliCommand => { + if (bun.Environment.enable_fuzzilli) { + const ctx = try Command.init(allocator, log, .FuzzilliCommand); + try FuzzilliCommand.exec(ctx); + return; + } else { + return error.UnrecognizedCommand; + } + }, } } @@ -970,6 +984,7 @@ pub const Command = struct { PublishCommand, AuditCommand, WhyCommand, + FuzzilliCommand, /// Used by crash reports. /// @@ -1007,6 +1022,7 @@ pub const Command = struct { .PublishCommand => 'k', .AuditCommand => 'A', .WhyCommand => 'W', + .FuzzilliCommand => 'F', }; } @@ -1320,7 +1336,7 @@ pub const Command = struct { Output.flush(); }, else => { - HelpCommand.printWithReason(.explicit); + HelpCommand.printWithReason(.explicit, false); }, } } diff --git a/src/cli/fuzzilli_command.zig b/src/cli/fuzzilli_command.zig new file mode 100644 index 0000000000..f1c24005e0 --- /dev/null +++ b/src/cli/fuzzilli_command.zig @@ -0,0 +1,63 @@ +pub const FuzzilliCommand = if (bun.Environment.enable_fuzzilli) struct { + pub fn exec(ctx: Command.Context) !void { + @branchHint(.cold); + + if (!Environment.isPosix) { + Output.prettyErrorln("error: Fuzzilli mode is only supported on POSIX systems", .{}); + Global.exit(1); + } + + // Set an environment variable so we can detect fuzzilli mode in JavaScript + + // Verify REPRL file descriptors are available + const REPRL_CRFD: c_int = 100; + verifyFd(REPRL_CRFD) catch { + Output.prettyErrorln("error: REPRL_CRFD (fd {d}) is not available. Run Bun under Fuzzilli.", .{REPRL_CRFD}); + Output.prettyErrorln("Example: fuzzilli --profile=bun /path/to/bun fuzzilli", .{}); + Global.exit(1); + }; + + // Always embed the REPRL script (it's small and not worth the runtime overhead) + const reprl_script = @embedFile("../js/eval/fuzzilli-reprl.ts"); + + // Create temp file for the script + var temp_dir = bun.FD.cwd().openDir("/tmp", .{}) catch { + Output.prettyErrorln("error: 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("error: Could not create temp file", .{}); + Global.exit(1); + }; + defer temp_file.close(); + + _ = temp_file.writeAll(reprl_script) catch { + Output.prettyErrorln("error: Could not write temp file", .{}); + Global.exit(1); + }; + + Output.prettyErrorln("[FUZZILLI] Temp file written, booting JS runtime", .{}); + + // 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; + } +} else {}; + +const bun = @import("bun"); +const Environment = bun.Environment; +const Global = bun.Global; +const Output = bun.Output; +const Command = bun.cli.Command; +const Run = bun.bun_js.Run; + +const std = @import("std"); +const std_posix = std.posix; diff --git a/src/env.zig b/src/env.zig index be7e75dafd..528d4ef5a1 100644 --- a/src/env.zig +++ b/src/env.zig @@ -51,6 +51,7 @@ pub const dump_source = isDebug and !isTest; pub const base_path = build_options.base_path; pub const enable_logs = build_options.enable_logs; pub const enable_asan = build_options.enable_asan; +pub const enable_fuzzilli = build_options.enable_fuzzilli; pub const codegen_path = build_options.codegen_path; pub const codegen_embed = build_options.codegen_embed; diff --git a/src/js/eval/fuzzilli-reprl.ts b/src/js/eval/fuzzilli-reprl.ts new file mode 100644 index 0000000000..2cc125036d --- /dev/null +++ b/src/js/eval/fuzzilli-reprl.ts @@ -0,0 +1,91 @@ +// 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 fs = require("node:fs"); + +// Make common Node modules available +globalThis.require = require; +globalThis.__dirname = "/"; +globalThis.__filename = "/fuzzilli.js"; + +// ============================================================================ +// 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 + fs.fstatSync(REPRL_CRFD); +} catch { + // 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); + + resetCoverage(); +}