mirror of
https://github.com/oven-sh/bun
synced 2026-02-21 08:12:21 +00:00
Compare commits
4 Commits
claude/fix
...
user/marko
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e578f277e9 | ||
|
|
b11d24fe14 | ||
|
|
6bc7d2ef63 | ||
|
|
d775f2912f |
@@ -49,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,
|
||||
@@ -98,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);
|
||||
@@ -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 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,
|
||||
@@ -505,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,
|
||||
@@ -629,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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
348
src/bun.js/bindings/Fuzzilli.cpp
Normal file
348
src/bun.js/bindings/Fuzzilli.cpp
Normal file
@@ -0,0 +1,348 @@
|
||||
#include "JavaScriptCore/CallFrame.h"
|
||||
#include "JavaScriptCore/Identifier.h"
|
||||
#include "JavaScriptCore/Intrinsic.h"
|
||||
#include "JavaScriptCore/JITOperations.h"
|
||||
#include "JavaScriptCore/JSGlobalObject.h"
|
||||
#include "JavaScriptCore/JSObject.h"
|
||||
#include "JavaScriptCore/ThrowScope.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
#include "root.h"
|
||||
#include "wtf/Platform.h"
|
||||
#include "wtf/text/ASCIILiteral.h"
|
||||
#include "wtf/text/StringView.h"
|
||||
#include "wtf/text/WTFString.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <expected>
|
||||
#include <fcntl.h>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
#include <signal.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include "util/functional.hpp"
|
||||
|
||||
#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
|
||||
#include <sanitizer/asan_interface.h>
|
||||
#endif
|
||||
|
||||
namespace SysSignal {
|
||||
|
||||
extern "C" void SysSignal__handler(int sig) noexcept
|
||||
{
|
||||
std::ranges::for_each(std::array { STDOUT_FILENO, STDERR_FILENO }, [](int fd) { fsync(fd); });
|
||||
|
||||
signal(sig, SIG_DFL);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
void Register() noexcept
|
||||
{
|
||||
static constexpr std::array kSignalsToHandle = { SIGABRT, SIGSEGV, SIGILL, SIGFPE };
|
||||
std::ranges::for_each(kSignalsToHandle, [](int sig) { signal(sig, SysSignal__handler); });
|
||||
}
|
||||
|
||||
} // namespace SignalHandlers
|
||||
|
||||
/// @brief Functions supported to to be called from Fuzzilli. These are
|
||||
/// Fuzzilli-specific functions.
|
||||
namespace FuzziliJsApi {
|
||||
|
||||
namespace Messages {
|
||||
|
||||
/// @brief Force the program to crash in a specific way.
|
||||
struct ForceCrash {
|
||||
enum class Mode : std::uint8_t {
|
||||
ImmediateCrash,
|
||||
BuiltinTrap,
|
||||
DCheckFailure,
|
||||
OutOfBoundsWrite,
|
||||
UseAfterFree,
|
||||
NullPointerDereference,
|
||||
};
|
||||
|
||||
Mode m_mode;
|
||||
|
||||
void dispatch()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Crash the program if Address Sanitizer is not enabled.
|
||||
struct EnsureAsanEnabled {};
|
||||
|
||||
} // namespace messages
|
||||
|
||||
/// @brief The variant type representing all possible messages.
|
||||
using Message = std::variant<Messages::ForceCrash,
|
||||
Messages::EnsureAsanEnabled>;
|
||||
|
||||
inline void serviceMessage(const Message& message)
|
||||
{
|
||||
std::visit(Util::Functional::Overloaded {
|
||||
[](const Messages::ForceCrash& msg) -> void {
|
||||
switch (msg.m_mode) {
|
||||
case Messages::ForceCrash::Mode::ImmediateCrash:
|
||||
std::abort();
|
||||
case Messages::ForceCrash::Mode::BuiltinTrap:
|
||||
__builtin_trap();
|
||||
case Messages::ForceCrash::Mode::DCheckFailure:
|
||||
assert(false);
|
||||
case Messages::ForceCrash::Mode::OutOfBoundsWrite: {
|
||||
volatile char* p = static_cast<volatile char*>(std::malloc(1));
|
||||
p[-1] = 'A';
|
||||
std::free(const_cast<char*>(p));
|
||||
break;
|
||||
}
|
||||
case Messages::ForceCrash::Mode::UseAfterFree: {
|
||||
volatile char* p = static_cast<volatile char*>(std::malloc(1));
|
||||
std::free(const_cast<char*>(p));
|
||||
p[0] = 'A';
|
||||
break;
|
||||
}
|
||||
case Messages::ForceCrash::Mode::NullPointerDereference: {
|
||||
volatile std::uint64_t* p = nullptr;
|
||||
while (true)
|
||||
p = reinterpret_cast<volatile std::uint64_t*>(*p);
|
||||
}
|
||||
}
|
||||
},
|
||||
[](const Messages::EnsureAsanEnabled&) -> void {
|
||||
} },
|
||||
message);
|
||||
}
|
||||
|
||||
std::expected<Message, std::string> parseMessageFromJS(JSC::JSGlobalObject* go, JSC::JSObject* object)
|
||||
{
|
||||
struct MessageParser {
|
||||
WTF::ASCIILiteral m_name;
|
||||
std::expected<Message, std::string> (*m_parser)(JSC::JSGlobalObject*, JSC::JSObject*, JSC::VM& vm);
|
||||
};
|
||||
static constexpr std::array messageParsersByType {
|
||||
MessageParser {
|
||||
"forceCrash"_s,
|
||||
[](JSC::JSGlobalObject* go, JSC::JSObject* object, JSC::VM& vm) -> std::expected<Message, std::string> {
|
||||
using Mode = Messages::ForceCrash::Mode;
|
||||
struct ModeStringEntry {
|
||||
WTF::ASCIILiteral m_string;
|
||||
Mode m_mode;
|
||||
};
|
||||
|
||||
static constexpr std::array modeStringEntries {
|
||||
ModeStringEntry { "immediateCrash"_s, Mode::ImmediateCrash },
|
||||
ModeStringEntry { "builtinTrap"_s, Mode::BuiltinTrap },
|
||||
ModeStringEntry { "dcheckFailure"_s, Mode::DCheckFailure },
|
||||
ModeStringEntry { "outOfBoundsWrite"_s, Mode::OutOfBoundsWrite },
|
||||
ModeStringEntry { "useAfterFree"_s, Mode::UseAfterFree },
|
||||
ModeStringEntry { "nullPointerDereference"_s, Mode::NullPointerDereference },
|
||||
};
|
||||
|
||||
auto jsMode = object->getIfPropertyExists(
|
||||
go,
|
||||
JSC::Identifier::fromString(vm, "mode"_s));
|
||||
if (jsMode.isUndefined() || !jsMode.isString()) {
|
||||
return std::unexpected("Invalid forceCrash message: missing or invalid 'mode' property");
|
||||
}
|
||||
|
||||
auto modeIt = std::ranges::find_if(
|
||||
modeStringEntries,
|
||||
[&](const ModeStringEntry& entry) {
|
||||
return jsMode.toWTFString(go) == entry.m_string;
|
||||
});
|
||||
if (modeIt == modeStringEntries.end()) {
|
||||
return std::unexpected(std::format("Invalid forceCrash message: unknown mode '{}'", jsMode.toWTFString(go).utf8().toStdString()));
|
||||
}
|
||||
|
||||
return Messages::ForceCrash { .m_mode = modeIt->m_mode };
|
||||
},
|
||||
},
|
||||
MessageParser {
|
||||
"ensureAsanEnabled"_s,
|
||||
[](JSC::JSGlobalObject* go, JSC::JSObject* object, JSC::VM& vm) -> std::expected<Message, std::string> {
|
||||
return Messages::EnsureAsanEnabled {};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
JSC::VM& vm = go->vm();
|
||||
auto typeStr = object->getIfPropertyExists(go, JSC::Identifier::fromString(vm, "type"_s));
|
||||
if (typeStr.isUndefined() || !typeStr.isString()) {
|
||||
return std::unexpected("Invalid message: missing or invalid 'type' property");
|
||||
}
|
||||
|
||||
for (const auto& parser : messageParsersByType) {
|
||||
if (typeStr.toWTFString(go) == parser.m_name) {
|
||||
return parser.m_parser(go, object, vm);
|
||||
}
|
||||
}
|
||||
|
||||
return std::unexpected(std::format("Invalid message: unknown message type '{}'", typeStr.toWTFString(go).utf8().toStdString()));
|
||||
}
|
||||
|
||||
JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES fuzzilli(JSC::JSGlobalObject* go, JSC::CallFrame* cf)
|
||||
{
|
||||
JSC::VM& vm = go->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (cf->argumentCount() != 1) {
|
||||
JSC::JSValue error = JSC::jsString(vm, "Invalid argument count provided. Must provide one argument."_s);
|
||||
return JSC::JSValue::encode(error);
|
||||
}
|
||||
}
|
||||
|
||||
void Register(Zig::GlobalObject* go)
|
||||
{
|
||||
// Install signal handlers to ensure output is flushed before crashes.
|
||||
SysSignal::Register();
|
||||
|
||||
go->putDirectNativeFunction(go->vm(), go, JSC::Identifier::fromString(go->vm(), "fuzzilli"_s),
|
||||
1, fuzzilli, JSC::ImplementationVisibility::Public,
|
||||
JSC::NoIntrinsic,
|
||||
JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete);
|
||||
}
|
||||
|
||||
} // namespace FuzziliJsApi
|
||||
|
||||
namespace Bun::Fuzzilli {
|
||||
|
||||
namespace Coverage {
|
||||
|
||||
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t* guard)
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace Coverage
|
||||
|
||||
} // namespace Bun::Fuzzilli
|
||||
|
||||
// ============================================================================
|
||||
// Coverage instrumentation for Fuzzilli
|
||||
// Based on workerd implementation
|
||||
// Only enabled when ASAN is active
|
||||
// ============================================================================
|
||||
|
||||
#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
|
||||
|
||||
#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
|
||||
extern "C" void Bun__REPRL__resetCoverage()
|
||||
{
|
||||
if (__shmem && __edges_start && __edges_stop) {
|
||||
__sanitizer_cov_reset_edgeguards();
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Stub implementations when ASAN 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 // ASAN
|
||||
|
||||
} // extern "C"
|
||||
17
src/bun.js/bindings/Fuzzilli.hpp
Normal file
17
src/bun.js/bindings/Fuzzilli.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef BUN_FUZZILLI_HPP
|
||||
#define BUN_FUZZILLI_HPP
|
||||
|
||||
#include "ZigGlobalObject.h"
|
||||
|
||||
namespace Bun::Fuzzilli {
|
||||
|
||||
namespace Js {
|
||||
|
||||
/// @brief Register the Fuzzilli-specific runtime functions on the provided global object.
|
||||
void Register(Zig::GlobalObject* go);
|
||||
|
||||
} // namespace Js
|
||||
|
||||
} // namespace Bun::Fuzzilli
|
||||
|
||||
#endif // BUN_FUZZILLI_HPP
|
||||
303
src/bun.js/bindings/FuzzilliREPRL.cpp
Normal file
303
src/bun.js/bindings/FuzzilliREPRL.cpp
Normal file
@@ -0,0 +1,303 @@
|
||||
#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>
|
||||
|
||||
#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
|
||||
#include <sanitizer/asan_interface.h>
|
||||
#endif
|
||||
|
||||
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<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) {
|
||||
// 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()));
|
||||
|
||||
fprintf(stdout, "FUZZILLI_PRINT: %s\n", output.utf8().data());
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
// Register the fuzzilli() function on a Bun global object
|
||||
void Bun__REPRL__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);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Coverage instrumentation for Fuzzilli
|
||||
// Based on workerd implementation
|
||||
// Only enabled when ASAN is active
|
||||
// ============================================================================
|
||||
|
||||
#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
|
||||
|
||||
#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
|
||||
extern "C" void Bun__REPRL__resetCoverage()
|
||||
{
|
||||
if (__shmem && __edges_start && __edges_stop) {
|
||||
__sanitizer_cov_reset_edgeguards();
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Stub implementations when ASAN 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 // ASAN
|
||||
|
||||
} // extern "C"
|
||||
@@ -258,6 +258,9 @@ extern "C" unsigned getJSCBytecodeCacheVersion()
|
||||
return getWebKitBytecodeCacheVersion();
|
||||
}
|
||||
|
||||
// Declare fuzzilli function registration from FuzzilliREPRL.cpp
|
||||
extern "C" void Bun__REPRL__registerFuzzilliFunction(Zig::GlobalObject*);
|
||||
|
||||
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;
|
||||
@@ -501,6 +504,11 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client,
|
||||
Bun__setDefaultGlobalObject(globalObject);
|
||||
JSC::gcProtect(globalObject);
|
||||
|
||||
// Register fuzzilli() function if in fuzzilli mode
|
||||
if (std::getenv("BUN_FUZZILLI_MODE")) {
|
||||
Bun__REPRL__registerFuzzilliFunction(static_cast<Zig::GlobalObject*>(globalObject));
|
||||
}
|
||||
|
||||
vm.setOnComputeErrorInfo(computeErrorInfoWrapperToString);
|
||||
vm.setOnComputeErrorInfoJSValue(computeErrorInfoWrapperToJSValue);
|
||||
vm.setComputeLineColumnWithSourcemap(computeLineColumnWithSourcemap);
|
||||
|
||||
26
src/bun.js/bindings/util/functional.hpp
Normal file
26
src/bun.js/bindings/util/functional.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef BUN_UTIL_FUNCTIONAL_HPP
|
||||
#define BUN_UTIL_FUNCTIONAL_HPP
|
||||
|
||||
namespace Util::Functional {
|
||||
|
||||
/// @brief Combines multiple lambda expressions into a single callable object.
|
||||
///
|
||||
/// Example:
|
||||
/// @code{.cpp}
|
||||
/// auto visitor = Overloaded {
|
||||
/// [](int i) { /* handle int */ },
|
||||
/// [](const std::string& str) { /* handle string */ },
|
||||
/// [](double d) { /* handle double */ }
|
||||
/// };
|
||||
///
|
||||
/// std::variant<int, std::string, double> var = /* ... */;
|
||||
/// std::visit(visitor, var);
|
||||
/// @endcode
|
||||
template<typename... Lambdas>
|
||||
struct Overloaded : Lambdas... {
|
||||
using Lambdas::operator()...;
|
||||
};
|
||||
|
||||
} // namespace util::functional
|
||||
|
||||
#endif // BUN_UTIL_FUNCTIONAL_HPP
|
||||
11
src/cli.zig
11
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");
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1318,7 +1327,7 @@ pub const Command = struct {
|
||||
Output.flush();
|
||||
},
|
||||
else => {
|
||||
HelpCommand.printWithReason(.explicit);
|
||||
HelpCommand.printWithReason(.explicit, false);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
66
src/cli/fuzzilli_command.zig
Normal file
66
src/cli/fuzzilli_command.zig
Normal file
@@ -0,0 +1,66 @@
|
||||
extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
71
src/js/internal/fuzzilli-reprl-minimal.ts
Normal file
71
src/js/internal/fuzzilli-reprl-minimal.ts
Normal 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 {};
|
||||
324
src/js/internal/fuzzilli-reprl.ts
Normal file
324
src/js/internal/fuzzilli-reprl.ts
Normal 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 {};
|
||||
24
test/js/bun/fuzzilli/basic.test.ts
Normal file
24
test/js/bun/fuzzilli/basic.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { spawn } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user