Compare commits

...

11 Commits

Author SHA1 Message Date
autofix-ci[bot]
d6ad32c38f [autofix.ci] apply automated fixes 2025-11-19 02:55:13 +00:00
Marko Vejnovic
ed17aaf19c Let claude do god knows what 2025-11-18 18:53:32 -08:00
Marko Vejnovic
d3302a9e8f Fix comms issues 2025-11-18 17:41:54 -08:00
Marko Vejnovic
e0f0ec4ec0 Aggresively vibing 2025-11-18 17:19:14 -08:00
Marko Vejnovic
860d67e2c7 Debug what's going on with fuzzilli 2025-11-17 23:40:20 -08:00
Marko Vejnovic
b9643d21e9 Add fuzzilli TTY 2025-11-17 23:01:24 -08:00
autofix-ci[bot]
3adc9ef81a [autofix.ci] apply automated fixes 2025-11-18 00:14:57 +00:00
Marko Vejnovic
98b5b58e10 Get fuzzilli to run us 2025-11-17 16:13:07 -08:00
Marko Vejnovic
34123e3143 fix segfaults 2025-11-17 14:27:09 -08:00
Marko Vejnovic
2f4008f80b Start working on a repl 2025-11-17 14:27:09 -08:00
Marko Vejnovic
bb1b93c6fb test(fuzzing): Start adding fuzzilli
Co-authored-by: mschwarzl <mschwarzl@cloudflare.com>
2025-11-17 14:27:09 -08:00
22 changed files with 832 additions and 90 deletions

View File

@@ -18,6 +18,22 @@ const OperatingSystem = @import("src/env.zig").OperatingSystem;
const pathRel = fs.path.relative;
/// When updating this, make sure to adjust SetupZig.cmake
const recommended_zig_version = "0.14.0";
// comptime {
// if (!std.mem.eql(u8, builtin.zig_version_string, recommended_zig_version)) {
// @compileError(
// "" ++
// "Bun requires Zig version " ++ recommended_zig_version ++ ", but you have " ++
// builtin.zig_version_string ++ ". This is automatically configured via Bun's " ++
// "CMake setup. You likely meant to run `bun run build`. If you are trying to " ++
// "upgrade the Zig compiler, edit ZIG_COMMIT in cmake/tools/SetupZig.cmake or " ++
// "comment this error out.",
// );
// }
// }
const zero_sha = "0000000000000000000000000000000000000000";
const BunBuildOptions = struct {
@@ -33,6 +49,7 @@ const BunBuildOptions = struct {
enable_logs: bool = false,
enable_asan: bool,
enable_valgrind: bool,
enable_fuzzilli: bool,
use_mimalloc: bool,
tracy_callstack_depth: u16,
reported_nodejs_version: Version,
@@ -82,6 +99,7 @@ const BunBuildOptions = struct {
opts.addOption(bool, "enable_logs", this.enable_logs);
opts.addOption(bool, "enable_asan", this.enable_asan);
opts.addOption(bool, "enable_valgrind", this.enable_valgrind);
opts.addOption(bool, "enable_fuzzilli", this.enable_fuzzilli);
opts.addOption(bool, "use_mimalloc", this.use_mimalloc);
opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{f}", .{this.reported_nodejs_version}));
opts.addOption(bool, "zig_self_hosted_backend", this.no_llvm);
@@ -255,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 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,
@@ -489,6 +508,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,
@@ -613,6 +633,9 @@ fn configureObj(b: *Build, opts: *BunBuildOptions, obj: *Compile) void {
obj.step.dependOn(&fail_step.step);
}
}
if (opts.enable_fuzzilli) {
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()
if(ENABLE_FUZZILLI)
# Enable coverage instrumentation for fuzzing with 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
)
endif()
# --- Optimization level ---
if(DEBUG)
register_compiler_flags(

View File

@@ -202,4 +202,6 @@ optionx(ERROR_LIMIT STRING "Maximum number of errors to show when compiling C++
# bun.zig, and enables C++ code to also be aware of the option.
set(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR ON)
optionx(ENABLE_FUZZILLI BOOL "If Fuzzilli support should be enabled" DEFAULT OFF)
list(APPEND CMAKE_ARGS -DCMAKE_EXPORT_COMPILE_COMMANDS=ON)

View File

@@ -915,6 +915,16 @@ if(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR)
target_compile_definitions(${bun} PRIVATE USE_MIMALLOC=1)
endif()
if(ENABLE_FUZZILLI)
target_compile_definitions(${bun} PRIVATE BUN_FUZZILLI_ENABLED=1)
target_sources(${bun} PRIVATE
${CWD}/src/fuzzilli/client.cpp
${CWD}/src/fuzzilli/session.cpp
${CWD}/src/fuzzilli/log.cpp
${CWD}/src/fuzzilli/reprl.cpp
)
endif()
target_compile_definitions(${bun} PRIVATE
_HAS_EXCEPTIONS=0
LIBUS_USE_OPENSSL=1

View File

@@ -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 -DENABLE_FUZZILLI=ON -B build/debug --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",

View File

@@ -36,7 +36,6 @@ MessagePortChannelProviderImpl::MessagePortChannelProviderImpl() = default;
MessagePortChannelProviderImpl::~MessagePortChannelProviderImpl()
{
ASSERT_NOT_REACHED();
}
void MessagePortChannelProviderImpl::createNewMessagePortChannel(const MessagePortIdentifier& local, const MessagePortIdentifier& remote)

View File

@@ -1,89 +0,0 @@
#include <ctime>
#include <fstream>
#include <iostream>
using namespace std;
#include "root.h"
#include "ZigGlobalObject.h"
#include "Path.h"
#include "DOMURL.h"
#include "headers-cpp.h"
#include <JavaScriptCore/CallFrame.h>
#include <JavaScriptCore/JSArrayBufferViewInlines.h>
int main() {
time_t rawtime;
struct tm *timeinfo;
char buf[80];
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(buf, 80, "%Y-%m-%d %H:%M:%s", timeinfo);
cout << "// Auto-generated by src/bun.js/headergen/sizegen.cpp at " << buf
<< ".\n";
cout << "// These are the byte sizes for the different object types with "
"bindings in JavaScriptCore.\n";
cout << "// This allows us to safely return stack allocated C++ types to "
"Zig.\n";
cout << "// It is only safe to do this when these sizes are correct.\n";
cout << "// That means:\n";
cout << "// 1. We can't dynamically link JavaScriptCore\n";
cout << "// 2. It's important that this is run whenever JavaScriptCore is "
"updated or the bindings on the Zig side change.\n";
cout << "// Failure to do so will lead to undefined behavior and probably "
"some frustrated people.\n";
cout << "// --- Regenerate this: --- \n";
cout << "// 1. \"make headers\"\n";
cout << "// 2. \"make sizegen\"\n";
cout << "// 3. \"make headers\"\n";
cout << "// ------------------------\n";
cout << "// You can verify the numbers written in this file at runtime via "
"the `extern`d types\n";
cout << "// Run \"headers\" twice because it uses these values "
"in the output. That's how all the bJSC__.* types are created - from "
"these values. \n";
int i = 0;
int len = 31 - 3;
for (i = 0; i < len; i++) {
cout << "pub const " << names[i] << " = " << sizes[i] << ";\n";
cout << "pub const " << names[i] << "_align = " << aligns[i] << ";\n";
}
cout << "pub const Bun_FFI_PointerOffsetToArgumentsList = "
<< JSC::CallFrame::argumentOffset(0) << ";\n";
cout << "pub const Bun_FFI_PointerOffsetToTypedArrayVector = "
<< JSC::JSArrayBufferView::offsetOfVector() << ";\n";
cout << "pub const Bun_FFI_PointerOffsetToTypedArrayLength = "
<< JSC::JSArrayBufferView::offsetOfLength() << ";\n";
cout << "pub const Bun_CallFrame__codeBlock = ";
cout << static_cast<int>(JSC::CallFrameSlot::codeBlock) << ";\n";
cout << "pub const Bun_CallFrame__callee = ";
cout << static_cast<int>(JSC::CallFrameSlot::callee) << ";\n";
cout << "pub const Bun_CallFrame__argumentCountIncludingThis = ";
cout << static_cast<int>(JSC::CallFrameSlot::argumentCountIncludingThis)
<< ";\n";
cout << "pub const Bun_CallFrame__thisArgument = ";
cout << static_cast<int>(JSC::CallFrameSlot::thisArgument) << ";\n";
cout << "pub const Bun_CallFrame__firstArgument = ";
cout << static_cast<int>(JSC::CallFrameSlot::firstArgument) << ";\n";
cout << "pub const Bun_CallFrame__size = ";
cout << sizeof(JSC::CallFrame) << ";\n";
cout << "pub const Bun_CallFrame__align = ";
cout << alignof(JSC::CallFrame) << ";\n";
return 0;
}

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");
@@ -624,6 +625,7 @@ pub const Command = struct {
RootCommandMatcher.case("prune") => .ReservedCommand,
RootCommandMatcher.case("list") => .PackageManagerCommand,
RootCommandMatcher.case("why") => .WhyCommand,
RootCommandMatcher.case("fuzzilli") => .FuzzilliCommand,
RootCommandMatcher.case("-e") => .AutoCommand,
@@ -933,6 +935,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;
},
}
}
@@ -968,6 +975,7 @@ pub const Command = struct {
PublishCommand,
AuditCommand,
WhyCommand,
FuzzilliCommand,
/// Used by crash reports.
///
@@ -1005,6 +1013,7 @@ pub const Command = struct {
.PublishCommand => 'k',
.AuditCommand => 'A',
.WhyCommand => 'W',
.FuzzilliCommand => 'F',
};
}
@@ -1317,6 +1326,9 @@ pub const Command = struct {
Output.pretty(intro_text, .{});
Output.flush();
},
.FuzzilliCommand => {
HelpCommand.printWithReason(.explicit, false);
},
else => {
HelpCommand.printWithReason(.explicit);
},

View File

@@ -0,0 +1,62 @@
/// C++ function that runs the Fuzzilli REPRL loop
/// Takes a callback pointer for executing JavaScript
extern "c" fn bun__fuzzilli__begin_with_global(callback: ?*const anyopaque) void;
/// Callback invoked by C++ to execute a JavaScript script
/// Returns 0 on success, non-zero on exception or error
fn executeScript(script_ptr: [*c]const u8, script_len: c_ulong) callconv(.c) c_int {
const script_slice = script_ptr[0..script_len];
// Get path to current bun executable
var exe_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
const exe_path = std.fs.selfExePath(&exe_path_buf) catch {
bun.Output.printErrorln("[Zig] ERROR: Failed to get self exe path", .{});
return 1;
};
// Use `bun -e` to execute the script in a fresh process
// This provides complete state isolation between executions
const argv = [_][]const u8{
exe_path,
"-e",
script_slice,
};
var child = std.process.Child.init(&argv, bun.default_allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
const term = child.spawnAndWait() catch |err| {
bun.Output.printErrorln("[Zig] ERROR: Failed to spawn: {}", .{err});
return 1;
};
// Return 0 for success, 1 for any failure
return switch (term) {
.Exited => |code| if (code == 0) 0 else 1,
else => 1,
};
}
pub const FuzzilliCommand = struct {
pub fn exec(_: bun.cli.Command.Context) !void {
bun.Output.printErrorln("[Zig] FuzzilliCommand.exec() called", .{});
// Initialize JSC
bun.Output.printErrorln("[Zig] Initializing JSC", .{});
JSC.initialize(false);
bun.Output.printErrorln("[Zig] JSC initialized", .{});
// Call C++ to handle REPRL protocol, passing our execute callback
bun.Output.printErrorln("[Zig] Calling bun__fuzzilli__begin_with_global()", .{});
bun__fuzzilli__begin_with_global(@ptrCast(&executeScript));
bun.Output.printErrorln("[Zig] bun__fuzzilli__begin_with_global() returned (should never happen)", .{});
}
};
const std = @import("std");
const bun = @import("bun");
const JSC = bun.jsc;

55
src/fuzzilli/client.cpp Normal file
View File

@@ -0,0 +1,55 @@
#include "log.hpp"
#include <cstring>
#ifdef BUN_FUZZILLI_ENABLED
#include "client.hpp"
#include <cstdlib>
#include <string_view>
#include <unistd.h>
namespace bun::fuzzilli {
Client::Client(Log& log, ClientConfig config)
: m_config(std::move(config)),
m_log(log)
{
}
Client::~Client()
{
}
std::size_t Client::forceRead(int fd, std::span<char> buffer, std::size_t maxBytes)
{
const ssize_t res = read(fd, buffer.data(), maxBytes);
if (res < 0) {
m_log << "Error reading from fd " << fd << " -- " << strerror(errno) << "\n";
std::abort();
}
return static_cast<std::size_t>(res);
}
void Client::forceWrite(int fd, std::string_view data)
{
const int written = write(fd, data.data(), data.size());
if (written != static_cast<int>(data.size())) {
m_log << "Error writing to fd " << fd << "\n";
std::abort();
}
}
void Client::sendCommand(std::string_view command)
{
forceWrite(m_config.commandWriteFD, command);
}
void Client::sendData(std::string_view data)
{
forceWrite(m_config.dataWriteFD, data);
}
} // namespace bun::fuzzilli
#endif // BUN_FUZZILLI_ENABLED

83
src/fuzzilli/client.hpp Normal file
View File

@@ -0,0 +1,83 @@
#pragma once
#include "log.hpp"
#include <array>
#include <string_view>
#include <span>
#include <ranges>
namespace bun::fuzzilli {
struct ClientConfig {
int commandReadFD;
int commandWriteFD;
int dataReadFD;
int dataWriteFD;
static constexpr ClientConfig defaultConfig()
{
return {
.commandReadFD = 100,
.commandWriteFD = 101,
.dataReadFD = 102,
.dataWriteFD = 103,
};
}
};
/// @brief A client that connects to the Fuzzilli runner.
class Client {
private:
static constexpr auto defaultMaxCmdSize = 4 * 1024;
static constexpr auto defaultMaxDataSize = 4 * 1024 * 1024;
public:
Client(Log& log, ClientConfig config = ClientConfig::defaultConfig());
~Client();
void sendCommand(std::string_view);
void sendData(std::string_view);
template<typename It>
std::size_t receiveFd(It it, int fd, std::size_t numBytes)
{
static constexpr std::size_t bufSize = 128;
std::array<char, bufSize> buffer;
std::size_t written = 0;
while (written < numBytes) {
std::size_t toRead = std::min(bufSize, numBytes - written);
std::size_t count = forceRead(fd, buffer, toRead);
if (count == 0) break; // EOF or error
it = std::ranges::copy_n(buffer.begin(), count, it).out;
written += count;
}
return written;
}
template<typename It>
std::size_t receiveCommand(It it, std::size_t maxSize = defaultMaxCmdSize)
{
m_log << "Receiving command up to " << maxSize << " bytes\n";
return receiveFd(it, m_config.commandReadFD, maxSize);
}
template<typename It>
std::size_t receiveData(It it, std::size_t maxSize = defaultMaxDataSize)
{
m_log << "Receiving data up to " << maxSize << " bytes\n";
return receiveFd(it, m_config.dataReadFD, maxSize);
}
private:
std::size_t forceRead(int fd, std::span<char> buffer, std::size_t maxBytes);
void forceWrite(int fd, std::string_view data);
ClientConfig m_config;
Log& m_log;
};
} // namespace bun::fuzzilli

94
src/fuzzilli/coverage.c Normal file
View File

@@ -0,0 +1,94 @@
// SanitzerCoverage-based coverage collection code for libcoverage.
// Copy+paste this code into the JavaScript shell binary.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
//
// BEGIN FUZZING CODE
//
#define REPRL_CRFD 100
#define REPRL_CWFD 101
#define REPRL_DRFD 102
#define REPRL_DWFD 103
#define SHM_SIZE 0x200000
#define MAX_EDGES ((SHM_SIZE - 4) * 8)
#define CHECK(cond) if (!(cond)) { fprintf(stderr, "\"" #cond "\" failed\n"); _exit(-1); }
struct shmem_data {
uint32_t num_edges;
unsigned char edges[];
};
struct shmem_data* __shmem;
uint32_t *__edges_start, *__edges_stop;
void __sanitizer_cov_reset_edgeguards() {
uint64_t N = 0;
for (uint32_t *x = __edges_start; x < __edges_stop && N < MAX_EDGES; x++)
*x = ++N;
}
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
// Avoid duplicate initialization
if (start == stop || *start)
return;
if (__edges_start != NULL || __edges_stop != NULL) {
fprintf(stderr, "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) {
puts("[COV] no shared memory bitmap available, skipping");
__shmem = (struct shmem_data*) malloc(SHM_SIZE);
} else {
int fd = shm_open(shm_key, O_RDWR, S_IREAD | S_IWRITE);
if (fd <= -1) {
fprintf(stderr, "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, "Failed to mmap shared memory region\n");
_exit(-1);
}
}
__sanitizer_cov_reset_edgeguards();
__shmem->num_edges = stop - start;
printf("[COV] edge counters initialized. Shared memory: %s with %u edges\n", shm_key, __shmem->num_edges);
}
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.
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;
}
//
// END FUZZING CODE
//

View File

@@ -0,0 +1,45 @@
#include "data_stream.hpp"
#include <cstdlib>
#include <memory>
#include <sys/mman.h>
namespace bun::fuzzilli {
MmapDataStreamBuf::MmapDataStreamBuf(int fd) : m_mapping([&] {
void* addr = mmap(nullptr, reprlMaxDataSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
// TODO(markoejnovic): Log
std::abort();
}
return reinterpret_cast<char*>(addr);
}()) {
setg(m_mapping, m_mapping, m_mapping + reprlMaxDataSize);
}
MmapDataStreamBuf::~MmapDataStreamBuf() {
if (m_mapping != nullptr) {
munmap(m_mapping, reprlMaxDataSize);
}
}
DataStream DataStream::fromEnv(int dataReadFd) {
return {
[dataReadFd] -> std::unique_ptr<DataStreamBuf> {
if (const char* shmKey = getenv("SHM_ID")) {
const std::int32_t fd = shm_open(shmKey, O_RDWR, S_IREAD | S_IWRITE);
if (fd < 0) {
// TODO(markoejnovic): Log
std::abort();
}
return std::make_unique<MmapDataStreamBuf>(fd);
}
// Otherwise, we will be reading from the data stream.
return std::make_unique<FileDataStreamBuf>(dataReadFd);
}()
};
}
} // namespace bun::fuzzilli

View File

@@ -0,0 +1,52 @@
#pragma once
#include <memory>
#include <streambuf>
#include <variant>
namespace bun::fuzzilli {
/// @brief Represents the stream of data exchanged by fuzzilli and bun.
/// Fuzzilli will send us data through this stream.
struct DataStreamBuf : public std::streambuf {};
struct MmapDataStreamBuf : public DataStreamBuf {
private:
static constexpr std::size_t reprlMaxDataSize = 16 << 20;
char* m_mapping = nullptr;
public:
MmapDataStreamBuf(int fd);
~MmapDataStreamBuf();
};
/// @note This borrows the file descriptor; it does not take ownership of it.
struct FileDataStreamBuf : public DataStreamBuf {
constexpr FileDataStreamBuf(int fd)
: m_fd(fd)
{
}
private:
int m_fd;
};
struct DataStream {
public:
/// @brief Create the fuzzilli DataStream from the environment.
///
/// Reads `envp["SHM_ID"]` to determine whether to use shared memory or a file descriptor.
/// If `SHM_ID` is set, uses shared memory; otherwise, falls back to file descriptor.
///
/// @note Borrows the file descriptor.
static DataStream fromEnv(int dataReadFd);
private:
constexpr DataStream(std::unique_ptr<DataStreamBuf>&& buf)
: m_buf(std::move(buf)) {};
std::unique_ptr<DataStreamBuf> m_buf;
};
} // namespace bun::fuzzilli

36
src/fuzzilli/log.cpp Normal file
View File

@@ -0,0 +1,36 @@
#include "log.hpp"
namespace bun::fuzzilli {
Log::Log(std::filesystem::path const& path)
: m_fd([&]() {
const int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd < 0) [[unlikely]] {
std::abort();
}
return fd;
}())
{
}
Log& Log::operator<<(std::string_view message)
{
const auto forceWrite = [this](std::string_view message) {
const ssize_t bytesWritten = write(m_fd, message.data(), message.size());
if (bytesWritten < 0 || static_cast<size_t>(bytesWritten) != message.size()) [[unlikely]] {
std::abort();
}
};
forceWrite(message);
fsync(m_fd);
return *this;
}
Log& Log::operator<<(std::int64_t message)
{
return (*this << std::to_string(message));
}
} // namespace bun::fuzzilli

26
src/fuzzilli/log.hpp Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include <filesystem>
namespace bun::fuzzilli {
class Log {
public:
constexpr Log(int fd)
: m_fd(fd)
{
}
Log(std::filesystem::path const& path);
Log& operator<<(std::string_view message);
Log& operator<<(std::int64_t message);
Log(const Log&) = delete;
Log& operator=(const Log&) = delete;
Log(Log&&) = delete;
Log& operator=(Log&&) = delete;
private:
int m_fd;
};
} // namespace bun::fuzzilli

84
src/fuzzilli/reprl.cpp Normal file
View File

@@ -0,0 +1,84 @@
#include "reprl.hpp"
#include "JavaScriptCore/Heap.h"
#include "JavaScriptCore/JSLock.h"
#include "JavaScriptCore/Completion.h"
#include "JavaScriptCore/InitializeThreading.h"
#include "JavaScriptCore/SourceCode.h"
#include "JavaScriptCore/SourceOrigin.h"
#include "JavaScriptCore/JSGlobalObject.h"
#include "wtf/NakedPtr.h"
#include <span>
namespace bun::fuzzilli {
Reprl::Reprl() : m_vm(JSC::VM::create(JSC::HeapType::Large)) {
fprintf(stderr, "[Fuzzilli] Reprl() constructor started\n");
fprintf(stderr, "[Fuzzilli] VM created\n");
// Acquire heap access before creating the global object
fprintf(stderr, "[Fuzzilli] About to acquire heap access\n");
m_vm->heap.acquireAccess();
fprintf(stderr, "[Fuzzilli] Heap access acquired\n");
fprintf(stderr, "[Fuzzilli] About to acquire JS lock\n");
JSC::JSLockHolder locker(m_vm.get());
fprintf(stderr, "[Fuzzilli] JS lock acquired\n");
// Use vanilla JSC::JSGlobalObject instead of Zig::GlobalObject
// This avoids needing the full Bun VirtualMachine infrastructure
fprintf(stderr, "[Fuzzilli] About to create global object structure\n");
auto* structure = JSC::JSGlobalObject::createStructure(m_vm.get(), JSC::jsNull());
if (!structure) {
fprintf(stderr, "[Fuzzilli] ERROR: Failed to create global object structure\n");
std::abort();
}
fprintf(stderr, "[Fuzzilli] Global object structure created\n");
fprintf(stderr, "[Fuzzilli] About to create global object\n");
m_globalObject = JSC::JSGlobalObject::create(m_vm.get(), structure);
if (!m_globalObject) {
fprintf(stderr, "[Fuzzilli] ERROR: Failed to create global object\n");
std::abort();
}
fprintf(stderr, "[Fuzzilli] Global object created successfully\n");
fprintf(stderr, "[Fuzzilli] Reprl() constructor completed\n");
}
Reprl::~Reprl() {
}
int Reprl::execute(std::string_view script) {
JSC::JSLockHolder locker(m_vm.get());
auto* globalObject = m_globalObject;
// Create the source code
auto sourceCode = JSC::SourceCode(
JSC::StringSourceProvider::create(
WTF::String::fromUTF8(std::span { script.data(), script.length() }),
JSC::SourceOrigin(),
WTF::String(),
JSC::SourceTaintedOrigin::Untainted,
WTF::TextPosition(),
JSC::SourceProviderSourceType::Program
)
);
// Evaluate the script
WTF::NakedPtr<JSC::Exception> exception;
JSC::evaluate(globalObject, sourceCode, JSC::JSValue(), exception);
if (exception) {
// Script threw an exception - return non-zero status
return 1;
}
return 0;
}
void Reprl::reset() {
JSC::JSLockHolder locker(m_vm.get());
m_vm->heap.collectSync(JSC::CollectionScope::Full);
}
} // namespace bun::fuzzilli

23
src/fuzzilli/reprl.hpp Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include "JavaScriptCore/VM.h"
#include "JavaScriptCore/JSGlobalObject.h"
#include "wtf/Ref.h"
#include <string_view>
namespace bun::fuzzilli {
class Reprl {
public:
Reprl();
~Reprl();
int execute(std::string_view script);
void reset();
private:
Ref<JSC::VM> m_vm;
JSC::JSGlobalObject* m_globalObject;
};
} // namespace bun::fuzzilli

191
src/fuzzilli/session.cpp Normal file
View File

@@ -0,0 +1,191 @@
#include "log.hpp"
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include "session.hpp"
#include "client.hpp"
#include <string_view>
namespace bun::fuzzilli {
/// @brief Represents a long-running Fuzzilli session.
///
/// Fuzzilli will instantiate ONE bun instance, and this class manages that. Note that the same bun
/// instance will be used for multiple fuzzing inputs.
class FuzzilliSession {
private:
static constexpr std::string_view heloMessage = "HELO";
public:
FuzzilliSession() noexcept {
exchangeHelo();
}
~FuzzilliSession() {}
private:
Log m_log { "/tmp/fuzzilli-bun.log" };
Client m_client { m_log };
void exchangeHelo() {
m_log << "[Session] Starting HELO exchange\n";
m_client.sendCommand(heloMessage);
m_log << "[Session] Sent HELO to Fuzzilli\n";
std::string response(heloMessage.size(), '\0');
response.resize(m_client.receiveCommand(response.begin(), heloMessage.size()));
m_log << "[Session] Received HELO response from Fuzzilli: '" << response << "' (length: " << response.size() << ")\n";
if (response != heloMessage) {
m_log << "[Session] ERROR: Invalid HELO response from Fuzzilli: '" << response << "'\n";
std::abort();
}
m_log << "[Session] HELO exchange completed successfully\n";
}
};
} // namespace bun::fuzzilli
// Type definition for the Zig callback
using ExecuteCallback = int (*)(const char* script, size_t length);
// Global callback pointer set by Zig
static ExecuteCallback g_execute_callback = nullptr;
extern "C" void bun__fuzzilli__begin_with_global(void* callback_ptr) {
fprintf(stderr, "[C++] bun__fuzzilli__begin_with_global() entered\n");
fflush(stderr);
g_execute_callback = reinterpret_cast<ExecuteCallback>(callback_ptr);
if (!g_execute_callback) {
fprintf(stderr, "[C++] ERROR: Execute callback is null!\n");
_exit(-1);
}
bun::fuzzilli::Log log("/tmp/fuzzilli-bun.log");
log << "[Main] ========================================\n";
log << "[Main] bun__fuzzilli__begin() called\n";
log << "[Main] ========================================\n";
fprintf(stderr, "[C++] About to create FuzzilliSession\n");
fflush(stderr);
log << "[Main] Creating FuzzilliSession for HELO exchange\n";
bun::fuzzilli::FuzzilliSession session;
log << "[Main] FuzzilliSession created successfully\n";
fprintf(stderr, "[C++] FuzzilliSession created\n");
fflush(stderr);
static constexpr auto REPRL_CRFD = 100;
static constexpr auto REPRL_CWFD = 101;
static constexpr auto REPRL_DRFD = 102;
log << "[Main] Entering REPRL loop\n";
log << "[Main] REPRL FDs - CRFD: " << REPRL_CRFD << ", CWFD: " << REPRL_CWFD << ", DRFD: " << REPRL_DRFD << "\n";
int iteration = 0;
while (true) {
iteration++;
log << "[Loop] ==================== Iteration " << iteration << " ====================\n";
// Check if control FD is still valid
int fd_status = fcntl(REPRL_CRFD, F_GETFL);
if (fd_status == -1) {
log << "[Loop] ERROR: Control FD is invalid, errno: " << errno << " (" << strerror(errno) << ")\n";
_exit(-1);
}
// Read action (4 bytes, should be 'exec' = 0x63657865)
unsigned action = 0;
ssize_t nread = read(REPRL_CRFD, &action, 4);
log << "[Loop] Read action: " << nread << " bytes\n";
fflush(0);
if (nread != 4 || action != 0x63657865) { // 'exec'
log << "[Loop] ERROR: Unknown action (expected 'exec'), nread=" << nread << "\n";
_exit(-1);
}
log << "[Loop] Received 'exec' action\n";
// Read script size (8 bytes)
size_t script_size = 0;
nread = read(REPRL_CRFD, &script_size, 8);
log << "[Loop] Read script size: " << nread << " bytes, size: " << script_size << " bytes\n";
if (nread != 8) {
log << "[Loop] ERROR: Failed to read script size (got " << nread << " bytes instead of 8)\n";
_exit(-1);
}
if (script_size > 10 * 1024 * 1024) { // 10MB sanity check
log << "[Loop] WARNING: Very large script size: " << script_size << " bytes\n";
}
// Allocate buffer for script
char* buf = reinterpret_cast<char*>(malloc(script_size + 1));
if (!buf) {
log << "[Loop] ERROR: Failed to allocate " << script_size << " bytes for script\n";
_exit(-1);
}
memset(buf, 0, script_size + 1);
// Read script data
char* source_buffer_tail = buf;
ssize_t remaining = static_cast<ssize_t>(script_size);
log << "[Loop] Reading " << remaining << " bytes of script data from FD " << REPRL_DRFD << "\n";
size_t total_read = 0;
while (remaining > 0) {
ssize_t rv = read(REPRL_DRFD, source_buffer_tail, static_cast<size_t>(remaining));
log << "[Loop] Read chunk: " << rv << " bytes (remaining: " << remaining << ", total read: " << total_read << ")\n";
if (rv <= 0) {
log << "[Loop] ERROR: Failed to read script data (rv=" << rv << ", errno=" << errno << ": " << strerror(errno) << ")\n";
free(buf);
_exit(-1);
}
remaining -= rv;
source_buffer_tail += rv;
total_read += rv;
}
buf[script_size] = '\0';
log << "[Loop] Script data read successfully (total: " << total_read << " bytes)\n";
if (script_size > 0) {
size_t preview_len = std::min(script_size, size_t(200));
log << "[Loop] Script preview (first " << preview_len << " chars): " << std::string_view(buf, preview_len);
if (script_size > preview_len) {
log << "...";
}
log << "\n";
}
// Execute the script via Zig callback
log << "[Loop] Calling Zig execute callback\n";
int status = g_execute_callback(buf, script_size);
// Clean up the script buffer
free(buf);
log << "[Loop] Freed script buffer\n";
log << "[Loop] Execution status: " << status << "\n";
// Send back the status to Fuzzilli (4 bytes as per REPRL protocol)
log << "[Loop] Sending status " << status << " to Fuzzilli on FD " << REPRL_CWFD << "\n";
ssize_t status_written = write(REPRL_CWFD, &status, 4);
if (status_written != 4) {
log << "[Loop] ERROR: Failed to write status (wrote " << status_written << " bytes instead of 4)\n";
_exit(1);
}
log << "[Loop] Status sent successfully (" << status_written << " bytes)\n";
log << "[Loop] Iteration " << iteration << " complete\n";
}
}

9
src/fuzzilli/session.hpp Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
/// Type definition for the Zig execute callback
/// Takes a script buffer and its length, returns exit status (0 = success, non-zero = failure/exception)
typedef int (*FuzzilliExecuteCallback)(const char* script, unsigned long length);
/// Begins the Fuzzilli REPRL loop using the provided callback for script execution
/// @param callback_ptr Pointer to the Zig execute callback function
extern "C" void bun__fuzzilli__begin_with_global(void* callback_ptr);

0
src/fuzzilli/shmem.cpp Normal file
View File

11
src/fuzzilli/shmem.hpp Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
#include <cstdint>
namespace bun::fuzzilli {
/// @brief Data shared between the fuzilling runner and the fuzzed process.
struct Shmem {
};
} // namespace bun::fuzzilli