Skip to content

Commit abb1b0c

Browse files
markovejnovicmschwarzlautofix-ci[bot]dylan-conway
authored
test(ENG-21524): Fuzzilli Stop-Gap (#24826)
### What does this PR do? Adds [@mschwarzl's Fuzzilli Support PR](#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 <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dylan Conway <[email protected]>
1 parent 274e01c commit abb1b0c

File tree

11 files changed

+499
-1
lines changed

11 files changed

+499
-1
lines changed

build.zig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const BunBuildOptions = struct {
3232
/// enable debug logs in release builds
3333
enable_logs: bool = false,
3434
enable_asan: bool,
35+
enable_fuzzilli: bool,
3536
enable_valgrind: bool,
3637
use_mimalloc: bool,
3738
tracy_callstack_depth: u16,
@@ -81,6 +82,7 @@ const BunBuildOptions = struct {
8182
opts.addOption(bool, "baseline", this.isBaseline());
8283
opts.addOption(bool, "enable_logs", this.enable_logs);
8384
opts.addOption(bool, "enable_asan", this.enable_asan);
85+
opts.addOption(bool, "enable_fuzzilli", this.enable_fuzzilli);
8486
opts.addOption(bool, "enable_valgrind", this.enable_valgrind);
8587
opts.addOption(bool, "use_mimalloc", this.use_mimalloc);
8688
opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{f}", .{this.reported_nodejs_version}));
@@ -255,6 +257,7 @@ pub fn build(b: *Build) !void {
255257
.tracy_callstack_depth = b.option(u16, "tracy_callstack_depth", "") orelse 10,
256258
.enable_logs = b.option(bool, "enable_logs", "Enable logs in release") orelse false,
257259
.enable_asan = b.option(bool, "enable_asan", "Enable asan") orelse false,
260+
.enable_fuzzilli = b.option(bool, "enable_fuzzilli", "Enable fuzzilli instrumentation") orelse false,
258261
.enable_valgrind = b.option(bool, "enable_valgrind", "Enable valgrind") orelse false,
259262
.use_mimalloc = b.option(bool, "use_mimalloc", "Use mimalloc as default allocator") orelse false,
260263
.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(
490493
.no_llvm = root_build_options.no_llvm,
491494
.enable_asan = root_build_options.enable_asan,
492495
.enable_valgrind = root_build_options.enable_valgrind,
496+
.enable_fuzzilli = root_build_options.enable_fuzzilli,
493497
.use_mimalloc = root_build_options.use_mimalloc,
494498
.override_no_export_cpp_apis = root_build_options.override_no_export_cpp_apis,
495499
};
@@ -605,13 +609,20 @@ fn configureObj(b: *Build, opts: *BunBuildOptions, obj: *Compile) void {
605609

606610
obj.no_link_obj = opts.os != .windows;
607611

612+
608613
if (opts.enable_asan and !enableFastBuild(b)) {
609614
if (@hasField(Build.Module, "sanitize_address")) {
615+
if (opts.enable_fuzzilli) {
616+
obj.sanitize_coverage_trace_pc_guard = true;
617+
}
610618
obj.root_module.sanitize_address = true;
611619
} else {
612620
const fail_step = b.addFail("asan is not supported on this platform");
613621
obj.step.dependOn(&fail_step.step);
614622
}
623+
} else if (opts.enable_fuzzilli) {
624+
const fail_step = b.addFail("fuzzilli requires asan");
625+
obj.step.dependOn(&fail_step.step);
615626
}
616627
obj.bundle_compiler_rt = false;
617628
obj.bundle_ubsan_rt = false;

cmake/CompilerFlags.cmake

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,23 @@ if(ENABLE_ASAN)
5151
)
5252
endif()
5353

54+
if(ENABLE_FUZZILLI)
55+
register_compiler_flags(
56+
DESCRIPTION "Enable coverage instrumentation for fuzzing"
57+
-fsanitize-coverage=trace-pc-guard
58+
)
59+
60+
register_linker_flags(
61+
DESCRIPTION "Link coverage instrumentation"
62+
-fsanitize-coverage=trace-pc-guard
63+
)
64+
65+
register_compiler_flags(
66+
DESCRIPTION "Enable fuzzilli-specific code"
67+
-DFUZZILLI_ENABLED
68+
)
69+
endif()
70+
5471
# --- Optimization level ---
5572
if(DEBUG)
5673
register_compiler_flags(

cmake/Options.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ if (NOT ENABLE_ASAN)
127127
set(ENABLE_ZIG_ASAN OFF)
128128
endif()
129129

130+
optionx(ENABLE_FUZZILLI BOOL "If fuzzilli support should be enabled" DEFAULT OFF)
131+
130132
if(RELEASE AND LINUX AND CI AND NOT ENABLE_ASSERTIONS AND NOT ENABLE_ASAN)
131133
set(DEFAULT_LTO ON)
132134
else()

cmake/targets/BuildBun.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,7 @@ register_command(
695695
-Dcpu=${ZIG_CPU}
696696
-Denable_logs=$<IF:$<BOOL:${ENABLE_LOGS}>,true,false>
697697
-Denable_asan=$<IF:$<BOOL:${ENABLE_ZIG_ASAN}>,true,false>
698+
-Denable_fuzzilli=$<IF:$<BOOL:${ENABLE_FUZZILLI}>,true,false>
698699
-Denable_valgrind=$<IF:$<BOOL:${ENABLE_VALGRIND}>,true,false>
699700
-Duse_mimalloc=$<IF:$<BOOL:${USE_MIMALLOC_AS_DEFAULT_ALLOCATOR}>,true,false>
700701
-Dllvm_codegen_threads=${LLVM_ZIG_CODEGEN_THREADS}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"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",
3434
"bd": "BUN_DEBUG_QUIET_LOGS=1 bun --silent bd:v",
3535
"build:debug": "export COMSPEC=\"C:\\Windows\\System32\\cmd.exe\" && bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Debug -B build/debug --log-level=NOTICE",
36+
"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",
3637
"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",
3738
"build:release": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -B build/release",
3839
"build:ci": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_VERBOSE_MAKEFILE=ON -DCI=true -B build/release-ci --verbose --fresh",
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
#ifdef FUZZILLI_ENABLED
2+
#include "JavaScriptCore/CallFrame.h"
3+
#include "JavaScriptCore/Identifier.h"
4+
#include "JavaScriptCore/JSGlobalObject.h"
5+
#include "ZigGlobalObject.h"
6+
#include "root.h"
7+
#include "wtf/text/WTFString.h"
8+
#include <cerrno>
9+
#include <csignal>
10+
#include <cstdlib>
11+
#include <cstring>
12+
#include <fcntl.h>
13+
#include <sanitizer/asan_interface.h>
14+
#include <sys/mman.h>
15+
#include <sys/stat.h>
16+
#include <unistd.h>
17+
18+
#define REPRL_DWFD 103
19+
20+
extern "C" {
21+
22+
// Signal handler to ensure output is flushed before crash
23+
static void fuzzilliSignalHandler(int sig)
24+
{
25+
// Flush all output
26+
fflush(stdout);
27+
fflush(stderr);
28+
fsync(STDOUT_FILENO);
29+
fsync(STDERR_FILENO);
30+
31+
// Re-raise the signal with default handler
32+
signal(sig, SIG_DFL);
33+
raise(sig);
34+
}
35+
36+
// Implementation of the global fuzzilli() function for Bun
37+
// This function is used by Fuzzilli to:
38+
// 1. Test crash detection with fuzzilli('FUZZILLI_CRASH', type)
39+
// 2. Print output with fuzzilli('FUZZILLI_PRINT', value)
40+
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES functionFuzzilli(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)
41+
{
42+
JSC::VM& vm = globalObject->vm();
43+
auto scope = DECLARE_THROW_SCOPE(vm);
44+
45+
if (callFrame->argumentCount() < 1) {
46+
return JSC::JSValue::encode(JSC::jsUndefined());
47+
}
48+
49+
JSC::JSValue arg0 = callFrame->argument(0);
50+
WTF::String command = arg0.toWTFString(globalObject);
51+
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
52+
53+
if (command == "FUZZILLI_CRASH"_s) {
54+
// Fuzzilli uses this to test crash detection
55+
// The second argument is an integer specifying the crash type
56+
int crashType = 0;
57+
if (callFrame->argumentCount() >= 2) {
58+
JSC::JSValue arg1 = callFrame->argument(1);
59+
crashType = arg1.toInt32(globalObject);
60+
}
61+
62+
// Print the crash type for debugging
63+
fprintf(stdout, "FUZZILLI_CRASH: %d\n", crashType);
64+
fflush(stdout);
65+
66+
// Trigger different types of crashes for testing (similar to V8 implementation)
67+
switch (crashType) {
68+
case 0:
69+
// IMMEDIATE_CRASH - Simple abort
70+
std::abort();
71+
break;
72+
73+
case 1:
74+
// CHECK failure - assertion in release builds
75+
// Use __builtin_trap() for a direct crash
76+
__builtin_trap();
77+
break;
78+
79+
case 2:
80+
// DCHECK failure - always crash (use trap instead of assert which is disabled in release)
81+
__builtin_trap();
82+
break;
83+
84+
case 3:
85+
// Wild write - heap buffer overflow (will be caught by ASAN)
86+
{
87+
volatile char* buffer = new char[10];
88+
buffer[20] = 'x'; // Write past the end - ASAN should catch this
89+
// Don't delete to make it more obvious
90+
}
91+
break;
92+
93+
case 4:
94+
// Use-after-free (will be caught by ASAN)
95+
{
96+
volatile char* buffer = new char[10];
97+
delete[] buffer;
98+
buffer[0] = 'x'; // Use after free - ASAN should catch this
99+
}
100+
break;
101+
102+
case 5:
103+
// Null pointer dereference
104+
{
105+
volatile int* ptr = nullptr;
106+
*ptr = 42;
107+
}
108+
break;
109+
110+
case 6:
111+
// Stack buffer overflow (will be caught by ASAN)
112+
{
113+
volatile char buffer[10];
114+
volatile char* p = const_cast<char*>(buffer);
115+
p[20] = 'x'; // Write past stack buffer
116+
}
117+
break;
118+
119+
case 7:
120+
// Double free (will be caught by ASAN)
121+
{
122+
char* buffer = new char[10];
123+
delete[] buffer;
124+
delete[] buffer; // Double free - ASAN should catch this
125+
}
126+
break;
127+
128+
case 8:
129+
// Verify DEBUG or ASAN is enabled
130+
// Expected to be compiled with debug or ASAN, don't crash
131+
fprintf(stdout, "DEBUG or ASAN is enabled\n");
132+
fflush(stdout);
133+
break;
134+
135+
default:
136+
// Unknown crash type, just abort
137+
std::abort();
138+
break;
139+
}
140+
} else if (command == "FUZZILLI_PRINT"_s) {
141+
// Optional: Print the second argument
142+
if (callFrame->argumentCount() >= 2) {
143+
JSC::JSValue arg1 = callFrame->argument(1);
144+
WTF::String output = arg1.toWTFString(globalObject);
145+
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
146+
147+
FILE* f = fdopen(REPRL_DWFD, "w");
148+
fprintf(f, "%s\n", output.utf8().data());
149+
fflush(f);
150+
}
151+
}
152+
153+
return JSC::JSValue::encode(JSC::jsUndefined());
154+
}
155+
156+
// ============================================================================
157+
// Coverage instrumentation for Fuzzilli
158+
// Based on workerd implementation
159+
// Only enabled when ASAN is active
160+
// ============================================================================
161+
162+
#define SHM_SIZE 0x200000
163+
#define MAX_EDGES ((SHM_SIZE - 4) * 8)
164+
165+
struct shmem_data {
166+
uint32_t num_edges;
167+
unsigned char edges[];
168+
};
169+
170+
// Global coverage data
171+
static struct shmem_data* __shmem = nullptr;
172+
static uint32_t* __edges_start = nullptr;
173+
static uint32_t* __edges_stop = nullptr;
174+
175+
// Reset edge guards for next iteration
176+
static void __sanitizer_cov_reset_edgeguards()
177+
{
178+
if (!__edges_start || !__edges_stop) return;
179+
uint64_t N = 0;
180+
for (uint32_t* x = __edges_start; x < __edges_stop && N < MAX_EDGES; x++) {
181+
*x = ++N;
182+
}
183+
}
184+
185+
// Called by the compiler to initialize coverage instrumentation
186+
extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop)
187+
{
188+
// Avoid duplicate initialization
189+
if (start == stop || *start) return;
190+
191+
if (__edges_start != nullptr || __edges_stop != nullptr) {
192+
fprintf(stderr, "[COV] Coverage instrumentation is only supported for a single module\n");
193+
_exit(-1);
194+
}
195+
196+
__edges_start = start;
197+
__edges_stop = stop;
198+
199+
// Map the shared memory region
200+
const char* shm_key = getenv("SHM_ID");
201+
if (!shm_key) {
202+
fprintf(stderr, "[COV] no shared memory bitmap available, using malloc\n");
203+
__shmem = (struct shmem_data*)malloc(SHM_SIZE);
204+
if (!__shmem) {
205+
fprintf(stderr, "[COV] Failed to allocate coverage bitmap\n");
206+
_exit(-1);
207+
}
208+
memset(__shmem, 0, SHM_SIZE);
209+
} else {
210+
int fd = shm_open(shm_key, O_RDWR, S_IREAD | S_IWRITE);
211+
if (fd <= -1) {
212+
fprintf(stderr, "[COV] Failed to open shared memory region: %s\n", strerror(errno));
213+
_exit(-1);
214+
}
215+
216+
__shmem = (struct shmem_data*)mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
217+
if (__shmem == MAP_FAILED) {
218+
fprintf(stderr, "[COV] Failed to mmap shared memory region\n");
219+
_exit(-1);
220+
}
221+
}
222+
223+
__sanitizer_cov_reset_edgeguards();
224+
__shmem->num_edges = stop - start;
225+
fprintf(stderr, "[COV] Coverage instrumentation initialized with %u edges\n", __shmem->num_edges);
226+
}
227+
228+
// Called by the compiler for each edge
229+
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t* guard)
230+
{
231+
// There's a small race condition here: if this function executes in two threads for the same
232+
// edge at the same time, the first thread might disable the edge (by setting the guard to zero)
233+
// before the second thread fetches the guard value (and thus the index). However, our
234+
// instrumentation ignores the first edge (see libcoverage.c) and so the race is unproblematic.
235+
if (!__shmem) return;
236+
uint32_t index = *guard;
237+
// If this function is called before coverage instrumentation is properly initialized we want to return early.
238+
if (!index) return;
239+
__shmem->edges[index / 8] |= 1 << (index % 8);
240+
*guard = 0;
241+
}
242+
243+
// Function to reset coverage for next REPRL iteration
244+
// This should be called after each script execution
245+
JSC_DEFINE_HOST_FUNCTION(jsResetCoverage, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*))
246+
{
247+
__sanitizer_cov_reset_edgeguards();
248+
return JSC::JSValue::encode(JSC::jsUndefined());
249+
}
250+
251+
// Register the fuzzilli() function on a Bun global object
252+
void Bun__REPRL__registerFuzzilliFunctions(Zig::GlobalObject* globalObject)
253+
{
254+
JSC::VM& vm = globalObject->vm();
255+
256+
// Install signal handlers to ensure output is flushed before crashes
257+
// This is important for ASAN output to be captured
258+
signal(SIGABRT, fuzzilliSignalHandler);
259+
signal(SIGSEGV, fuzzilliSignalHandler);
260+
signal(SIGILL, fuzzilliSignalHandler);
261+
signal(SIGFPE, fuzzilliSignalHandler);
262+
263+
globalObject->putDirectNativeFunction(
264+
vm,
265+
globalObject,
266+
JSC::Identifier::fromString(vm, "fuzzilli"_s),
267+
2, // max 2 arguments
268+
functionFuzzilli,
269+
JSC::ImplementationVisibility::Public,
270+
JSC::NoIntrinsic,
271+
JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete);
272+
273+
globalObject->putDirectNativeFunction(
274+
vm,
275+
globalObject,
276+
JSC::Identifier::fromString(vm, "resetCoverage"_s),
277+
0,
278+
jsResetCoverage,
279+
JSC::ImplementationVisibility::Public,
280+
JSC::NoIntrinsic,
281+
JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete);
282+
}
283+
284+
} // extern "C"
285+
286+
#endif // FUZZILLI_ENABLED

0 commit comments

Comments
 (0)