mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 06:13:07 +00:00
Merge pull request #21370 from ziglang/fuzz
rework fuzzing API to accept a function pointer parameter
This commit is contained in:
commit
eccd06f5d0
@ -145,31 +145,23 @@ fn mainServer() !void {
|
|||||||
.start_fuzzing => {
|
.start_fuzzing => {
|
||||||
if (!builtin.fuzz) unreachable;
|
if (!builtin.fuzz) unreachable;
|
||||||
const index = try server.receiveBody_u32();
|
const index = try server.receiveBody_u32();
|
||||||
var first = true;
|
|
||||||
const test_fn = builtin.test_functions[index];
|
const test_fn = builtin.test_functions[index];
|
||||||
while (true) {
|
const entry_addr = @intFromPtr(test_fn.func);
|
||||||
testing.allocator_instance = .{};
|
try server.serveU64Message(.fuzz_start_addr, entry_addr);
|
||||||
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
|
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
|
||||||
log_err_count = 0;
|
is_fuzz_test = false;
|
||||||
is_fuzz_test = false;
|
test_fn.func() catch |err| switch (err) {
|
||||||
test_fn.func() catch |err| switch (err) {
|
error.SkipZigTest => return,
|
||||||
error.SkipZigTest => continue,
|
else => {
|
||||||
else => {
|
if (@errorReturnTrace()) |trace| {
|
||||||
if (@errorReturnTrace()) |trace| {
|
std.debug.dumpStackTrace(trace.*);
|
||||||
std.debug.dumpStackTrace(trace.*);
|
}
|
||||||
}
|
std.debug.print("failed with error.{s}\n", .{@errorName(err)});
|
||||||
std.debug.print("failed with error.{s}\n", .{@errorName(err)});
|
std.process.exit(1);
|
||||||
std.process.exit(1);
|
},
|
||||||
},
|
};
|
||||||
};
|
if (!is_fuzz_test) @panic("missed call to std.testing.fuzz");
|
||||||
if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput");
|
if (log_err_count != 0) @panic("error logs detected");
|
||||||
if (log_err_count != 0) @panic("error logs detected");
|
|
||||||
if (first) {
|
|
||||||
first = false;
|
|
||||||
const entry_addr = @intFromPtr(test_fn.func);
|
|
||||||
try server.serveU64Message(.fuzz_start_addr, entry_addr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
else => {
|
else => {
|
||||||
@ -349,19 +341,72 @@ const FuzzerSlice = extern struct {
|
|||||||
|
|
||||||
var is_fuzz_test: bool = undefined;
|
var is_fuzz_test: bool = undefined;
|
||||||
|
|
||||||
extern fn fuzzer_next() FuzzerSlice;
|
extern fn fuzzer_start(testOne: *const fn ([*]const u8, usize) callconv(.C) void) void;
|
||||||
extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
|
extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
|
||||||
extern fn fuzzer_coverage_id() u64;
|
extern fn fuzzer_coverage_id() u64;
|
||||||
|
|
||||||
pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {
|
pub fn fuzz(
|
||||||
|
comptime testOne: fn ([]const u8) anyerror!void,
|
||||||
|
options: testing.FuzzInputOptions,
|
||||||
|
) anyerror!void {
|
||||||
|
// Prevent this function from confusing the fuzzer by omitting its own code
|
||||||
|
// coverage from being considered.
|
||||||
@disableInstrumentation();
|
@disableInstrumentation();
|
||||||
if (crippled) return "";
|
|
||||||
|
// Some compiler backends are not capable of handling fuzz testing yet but
|
||||||
|
// we still want CI test coverage enabled.
|
||||||
|
if (crippled) return;
|
||||||
|
|
||||||
|
// Smoke test to ensure the test did not use conditional compilation to
|
||||||
|
// contradict itself by making it not actually be a fuzz test when the test
|
||||||
|
// is built in fuzz mode.
|
||||||
is_fuzz_test = true;
|
is_fuzz_test = true;
|
||||||
|
|
||||||
|
// Ensure no test failure occurred before starting fuzzing.
|
||||||
|
if (log_err_count != 0) @panic("error logs detected");
|
||||||
|
|
||||||
|
// libfuzzer is in a separate compilation unit so that its own code can be
|
||||||
|
// excluded from code coverage instrumentation. It needs a function pointer
|
||||||
|
// it can call for checking exactly one input. Inside this function we do
|
||||||
|
// our standard unit test checks such as memory leaks, and interaction with
|
||||||
|
// error logs.
|
||||||
|
const global = struct {
|
||||||
|
fn fuzzer_one(input_ptr: [*]const u8, input_len: usize) callconv(.C) void {
|
||||||
|
@disableInstrumentation();
|
||||||
|
testing.allocator_instance = .{};
|
||||||
|
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
|
||||||
|
log_err_count = 0;
|
||||||
|
testOne(input_ptr[0..input_len]) catch |err| switch (err) {
|
||||||
|
error.SkipZigTest => return,
|
||||||
|
else => {
|
||||||
|
std.debug.lockStdErr();
|
||||||
|
if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace.*);
|
||||||
|
std.debug.print("failed with error.{s}\n", .{@errorName(err)});
|
||||||
|
std.process.exit(1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (log_err_count != 0) {
|
||||||
|
std.debug.lockStdErr();
|
||||||
|
std.debug.print("error logs detected\n", .{});
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
if (builtin.fuzz) {
|
if (builtin.fuzz) {
|
||||||
return fuzzer_next().toSlice();
|
const prev_allocator_state = testing.allocator_instance;
|
||||||
|
testing.allocator_instance = .{};
|
||||||
|
fuzzer_start(&global.fuzzer_one);
|
||||||
|
testing.allocator_instance = prev_allocator_state;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (options.corpus.len == 0) return "";
|
|
||||||
var prng = std.Random.DefaultPrng.init(testing.random_seed);
|
// When the unit test executable is not built in fuzz mode, only run the
|
||||||
const random = prng.random();
|
// provided corpus.
|
||||||
return options.corpus[random.uintLessThan(usize, options.corpus.len)];
|
for (options.corpus) |input| {
|
||||||
|
try testOne(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case there is no provided corpus, also use an empty
|
||||||
|
// string as a smoke test.
|
||||||
|
try testOne("");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,8 @@ fn logOverride(
|
|||||||
f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log");
|
f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log");
|
||||||
}
|
}
|
||||||
|
|
||||||
export threadlocal var __sancov_lowest_stack: usize = std.math.maxInt(usize);
|
/// Helps determine run uniqueness in the face of recursion.
|
||||||
|
export threadlocal var __sancov_lowest_stack: usize = 0;
|
||||||
|
|
||||||
export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void {
|
export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void {
|
||||||
handleCmp(@returnAddress(), arg1, arg2);
|
handleCmp(@returnAddress(), arg1, arg2);
|
||||||
@ -220,7 +221,6 @@ const Fuzzer = struct {
|
|||||||
.n_runs = 0,
|
.n_runs = 0,
|
||||||
.unique_runs = 0,
|
.unique_runs = 0,
|
||||||
.pcs_len = pcs.len,
|
.pcs_len = pcs.len,
|
||||||
.lowest_stack = std.math.maxInt(usize),
|
|
||||||
};
|
};
|
||||||
f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header));
|
f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header));
|
||||||
f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems * @sizeOf(usize));
|
f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems * @sizeOf(usize));
|
||||||
@ -235,22 +235,41 @@ const Fuzzer = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next(f: *Fuzzer) ![]const u8 {
|
fn start(f: *Fuzzer) !void {
|
||||||
const gpa = f.gpa;
|
const gpa = f.gpa;
|
||||||
const rng = fuzzer.rng.random();
|
const rng = fuzzer.rng.random();
|
||||||
|
|
||||||
if (f.recent_cases.entries.len == 0) {
|
// Prepare initial input.
|
||||||
// Prepare initial input.
|
assert(f.recent_cases.entries.len == 0);
|
||||||
try f.recent_cases.ensureUnusedCapacity(gpa, 100);
|
assert(f.n_runs == 0);
|
||||||
const len = rng.uintLessThanBiased(usize, 80);
|
try f.recent_cases.ensureUnusedCapacity(gpa, 100);
|
||||||
try f.input.resize(gpa, len);
|
const len = rng.uintLessThanBiased(usize, 80);
|
||||||
rng.bytes(f.input.items);
|
try f.input.resize(gpa, len);
|
||||||
f.recent_cases.putAssumeCapacity(.{
|
rng.bytes(f.input.items);
|
||||||
.id = 0,
|
f.recent_cases.putAssumeCapacity(.{
|
||||||
.input = try gpa.dupe(u8, f.input.items),
|
.id = 0,
|
||||||
.score = 0,
|
.input = try gpa.dupe(u8, f.input.items),
|
||||||
}, {});
|
.score = 0,
|
||||||
} else {
|
}, {});
|
||||||
|
|
||||||
|
const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const chosen_index = rng.uintLessThanBiased(usize, f.recent_cases.entries.len);
|
||||||
|
const run = &f.recent_cases.keys()[chosen_index];
|
||||||
|
f.input.clearRetainingCapacity();
|
||||||
|
f.input.appendSliceAssumeCapacity(run.input);
|
||||||
|
try f.mutate();
|
||||||
|
|
||||||
|
@memset(f.pc_counters, 0);
|
||||||
|
__sancov_lowest_stack = std.math.maxInt(usize);
|
||||||
|
f.coverage.reset();
|
||||||
|
|
||||||
|
fuzzer_one(f.input.items.ptr, f.input.items.len);
|
||||||
|
|
||||||
|
f.n_runs += 1;
|
||||||
|
_ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
|
||||||
|
|
||||||
if (f.n_runs % 10000 == 0) f.dumpStats();
|
if (f.n_runs % 10000 == 0) f.dumpStats();
|
||||||
|
|
||||||
const analysis = f.analyzeLastRun();
|
const analysis = f.analyzeLastRun();
|
||||||
@ -301,7 +320,6 @@ const Fuzzer = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
|
|
||||||
_ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
|
_ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,26 +335,12 @@ const Fuzzer = struct {
|
|||||||
// This has to be done before deinitializing the deleted items.
|
// This has to be done before deinitializing the deleted items.
|
||||||
const doomed_runs = f.recent_cases.keys()[cap..];
|
const doomed_runs = f.recent_cases.keys()[cap..];
|
||||||
f.recent_cases.shrinkRetainingCapacity(cap);
|
f.recent_cases.shrinkRetainingCapacity(cap);
|
||||||
for (doomed_runs) |*run| {
|
for (doomed_runs) |*doomed_run| {
|
||||||
std.log.info("culling score={d} id={d}", .{ run.score, run.id });
|
std.log.info("culling score={d} id={d}", .{ doomed_run.score, doomed_run.id });
|
||||||
run.deinit(gpa);
|
doomed_run.deinit(gpa);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chosen_index = rng.uintLessThanBiased(usize, f.recent_cases.entries.len);
|
|
||||||
const run = &f.recent_cases.keys()[chosen_index];
|
|
||||||
f.input.clearRetainingCapacity();
|
|
||||||
f.input.appendSliceAssumeCapacity(run.input);
|
|
||||||
try f.mutate();
|
|
||||||
|
|
||||||
f.n_runs += 1;
|
|
||||||
const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
|
|
||||||
_ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
|
|
||||||
_ = @atomicRmw(usize, &header.lowest_stack, .Min, __sancov_lowest_stack, .monotonic);
|
|
||||||
@memset(f.pc_counters, 0);
|
|
||||||
f.coverage.reset();
|
|
||||||
return f.input.items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visitPc(f: *Fuzzer, pc: usize) void {
|
fn visitPc(f: *Fuzzer, pc: usize) void {
|
||||||
@ -419,10 +423,13 @@ export fn fuzzer_coverage_id() u64 {
|
|||||||
return fuzzer.coverage_id;
|
return fuzzer.coverage_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn fuzzer_next() Fuzzer.Slice {
|
var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C) void = undefined;
|
||||||
return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) {
|
|
||||||
error.OutOfMemory => @panic("out of memory"),
|
export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void {
|
||||||
});
|
fuzzer_one = testOne;
|
||||||
|
fuzzer.start() catch |err| switch (err) {
|
||||||
|
error.OutOfMemory => fatal("out of memory", .{}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
|
export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
|
||||||
@ -432,24 +439,24 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
|
|||||||
const pc_counters_start = @extern([*]u8, .{
|
const pc_counters_start = @extern([*]u8, .{
|
||||||
.name = "__start___sancov_cntrs",
|
.name = "__start___sancov_cntrs",
|
||||||
.linkage = .weak,
|
.linkage = .weak,
|
||||||
}) orelse fatal("missing __start___sancov_cntrs symbol");
|
}) orelse fatal("missing __start___sancov_cntrs symbol", .{});
|
||||||
|
|
||||||
const pc_counters_end = @extern([*]u8, .{
|
const pc_counters_end = @extern([*]u8, .{
|
||||||
.name = "__stop___sancov_cntrs",
|
.name = "__stop___sancov_cntrs",
|
||||||
.linkage = .weak,
|
.linkage = .weak,
|
||||||
}) orelse fatal("missing __stop___sancov_cntrs symbol");
|
}) orelse fatal("missing __stop___sancov_cntrs symbol", .{});
|
||||||
|
|
||||||
const pc_counters = pc_counters_start[0 .. pc_counters_end - pc_counters_start];
|
const pc_counters = pc_counters_start[0 .. pc_counters_end - pc_counters_start];
|
||||||
|
|
||||||
const pcs_start = @extern([*]usize, .{
|
const pcs_start = @extern([*]usize, .{
|
||||||
.name = "__start___sancov_pcs1",
|
.name = "__start___sancov_pcs1",
|
||||||
.linkage = .weak,
|
.linkage = .weak,
|
||||||
}) orelse fatal("missing __start___sancov_pcs1 symbol");
|
}) orelse fatal("missing __start___sancov_pcs1 symbol", .{});
|
||||||
|
|
||||||
const pcs_end = @extern([*]usize, .{
|
const pcs_end = @extern([*]usize, .{
|
||||||
.name = "__stop___sancov_pcs1",
|
.name = "__stop___sancov_pcs1",
|
||||||
.linkage = .weak,
|
.linkage = .weak,
|
||||||
}) orelse fatal("missing __stop___sancov_pcs1 symbol");
|
}) orelse fatal("missing __stop___sancov_pcs1 symbol", .{});
|
||||||
|
|
||||||
const pcs = pcs_start[0 .. pcs_end - pcs_start];
|
const pcs = pcs_start[0 .. pcs_end - pcs_start];
|
||||||
|
|
||||||
|
|||||||
@ -146,8 +146,8 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Total Runs: <span id="statTotalRuns"></span></li>
|
<li>Total Runs: <span id="statTotalRuns"></span></li>
|
||||||
<li>Unique Runs: <span id="statUniqueRuns"></span></li>
|
<li>Unique Runs: <span id="statUniqueRuns"></span></li>
|
||||||
|
<li>Speed (Runs/Second): <span id="statSpeed"></span></li>
|
||||||
<li>Coverage: <span id="statCoverage"></span></li>
|
<li>Coverage: <span id="statCoverage"></span></li>
|
||||||
<li>Lowest Stack: <span id="statLowestStack"></span></li>
|
|
||||||
<li>Entry Points: <ul id="entryPointsList"></ul></li>
|
<li>Entry Points: <ul id="entryPointsList"></ul></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -5,8 +5,8 @@
|
|||||||
const domSourceText = document.getElementById("sourceText");
|
const domSourceText = document.getElementById("sourceText");
|
||||||
const domStatTotalRuns = document.getElementById("statTotalRuns");
|
const domStatTotalRuns = document.getElementById("statTotalRuns");
|
||||||
const domStatUniqueRuns = document.getElementById("statUniqueRuns");
|
const domStatUniqueRuns = document.getElementById("statUniqueRuns");
|
||||||
|
const domStatSpeed = document.getElementById("statSpeed");
|
||||||
const domStatCoverage = document.getElementById("statCoverage");
|
const domStatCoverage = document.getElementById("statCoverage");
|
||||||
const domStatLowestStack = document.getElementById("statLowestStack");
|
|
||||||
const domEntryPointsList = document.getElementById("entryPointsList");
|
const domEntryPointsList = document.getElementById("entryPointsList");
|
||||||
|
|
||||||
let wasm_promise = fetch("main.wasm");
|
let wasm_promise = fetch("main.wasm");
|
||||||
@ -32,6 +32,9 @@
|
|||||||
const msg = decodeString(ptr, len);
|
const msg = decodeString(ptr, len);
|
||||||
throw new Error("panic: " + msg);
|
throw new Error("panic: " + msg);
|
||||||
},
|
},
|
||||||
|
timestamp: function () {
|
||||||
|
return BigInt(new Date());
|
||||||
|
},
|
||||||
emitSourceIndexChange: onSourceIndexChange,
|
emitSourceIndexChange: onSourceIndexChange,
|
||||||
emitCoverageUpdate: onCoverageUpdate,
|
emitCoverageUpdate: onCoverageUpdate,
|
||||||
emitEntryPointsUpdate: renderStats,
|
emitEntryPointsUpdate: renderStats,
|
||||||
@ -158,7 +161,7 @@
|
|||||||
domStatTotalRuns.innerText = totalRuns;
|
domStatTotalRuns.innerText = totalRuns;
|
||||||
domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)";
|
domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)";
|
||||||
domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
|
domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
|
||||||
domStatLowestStack.innerText = unwrapString(wasm_exports.lowestStack());
|
domStatSpeed.innerText = wasm_exports.totalRunsPerSecond().toFixed(0);
|
||||||
|
|
||||||
const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
|
const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
|
||||||
resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
|
resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
|
||||||
@ -10,9 +10,17 @@ const Walk = @import("Walk");
|
|||||||
const Decl = Walk.Decl;
|
const Decl = Walk.Decl;
|
||||||
const html_render = @import("html_render");
|
const html_render = @import("html_render");
|
||||||
|
|
||||||
|
/// Nanoseconds.
|
||||||
|
var server_base_timestamp: i64 = 0;
|
||||||
|
/// Milliseconds.
|
||||||
|
var client_base_timestamp: i64 = 0;
|
||||||
|
/// Relative to `server_base_timestamp`.
|
||||||
|
var start_fuzzing_timestamp: i64 = undefined;
|
||||||
|
|
||||||
const js = struct {
|
const js = struct {
|
||||||
extern "js" fn log(ptr: [*]const u8, len: usize) void;
|
extern "js" fn log(ptr: [*]const u8, len: usize) void;
|
||||||
extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
|
extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
|
||||||
|
extern "js" fn timestamp() i64;
|
||||||
extern "js" fn emitSourceIndexChange() void;
|
extern "js" fn emitSourceIndexChange() void;
|
||||||
extern "js" fn emitCoverageUpdate() void;
|
extern "js" fn emitCoverageUpdate() void;
|
||||||
extern "js" fn emitEntryPointsUpdate() void;
|
extern "js" fn emitEntryPointsUpdate() void;
|
||||||
@ -64,6 +72,7 @@ export fn message_end() void {
|
|||||||
|
|
||||||
const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
|
const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
|
.current_time => return currentTimeMessage(msg_bytes),
|
||||||
.source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
|
.source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
|
||||||
.coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
|
.coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
|
||||||
.entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
|
.entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
|
||||||
@ -106,13 +115,6 @@ export fn decl_source_html(decl_index: Decl.Index) String {
|
|||||||
return String.init(string_result.items);
|
return String.init(string_result.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn lowestStack() String {
|
|
||||||
const header: *abi.CoverageUpdateHeader = @ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]);
|
|
||||||
string_result.clearRetainingCapacity();
|
|
||||||
string_result.writer(gpa).print("0x{d}", .{header.lowest_stack}) catch @panic("OOM");
|
|
||||||
return String.init(string_result.items);
|
|
||||||
}
|
|
||||||
|
|
||||||
export fn totalSourceLocations() usize {
|
export fn totalSourceLocations() usize {
|
||||||
return coverage_source_locations.items.len;
|
return coverage_source_locations.items.len;
|
||||||
}
|
}
|
||||||
@ -124,16 +126,28 @@ export fn coveredSourceLocations() usize {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn getCoverageUpdateHeader() *abi.CoverageUpdateHeader {
|
||||||
|
return @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
|
||||||
|
}
|
||||||
|
|
||||||
export fn totalRuns() u64 {
|
export fn totalRuns() u64 {
|
||||||
const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
|
const header = getCoverageUpdateHeader();
|
||||||
return header.n_runs;
|
return header.n_runs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn uniqueRuns() u64 {
|
export fn uniqueRuns() u64 {
|
||||||
const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
|
const header = getCoverageUpdateHeader();
|
||||||
return header.unique_runs;
|
return header.unique_runs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export fn totalRunsPerSecond() f64 {
|
||||||
|
@setFloatMode(.optimized);
|
||||||
|
const header = getCoverageUpdateHeader();
|
||||||
|
const ns_elapsed: f64 = @floatFromInt(nsSince(start_fuzzing_timestamp));
|
||||||
|
const n_runs: f64 = @floatFromInt(header.n_runs);
|
||||||
|
return n_runs / (ns_elapsed / std.time.ns_per_s);
|
||||||
|
}
|
||||||
|
|
||||||
const String = Slice(u8);
|
const String = Slice(u8);
|
||||||
|
|
||||||
fn Slice(T: type) type {
|
fn Slice(T: type) type {
|
||||||
@ -196,6 +210,18 @@ fn fatal(comptime format: []const u8, args: anytype) noreturn {
|
|||||||
js.panic(line.ptr, line.len);
|
js.panic(line.ptr, line.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn currentTimeMessage(msg_bytes: []u8) void {
|
||||||
|
client_base_timestamp = js.timestamp();
|
||||||
|
server_base_timestamp = @bitCast(msg_bytes[1..][0..8].*);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nanoseconds passed since a server timestamp.
|
||||||
|
fn nsSince(server_timestamp: i64) i64 {
|
||||||
|
const ms_passed = js.timestamp() - client_base_timestamp;
|
||||||
|
const ns_passed = server_base_timestamp - server_timestamp;
|
||||||
|
return ns_passed + ms_passed * std.time.ns_per_ms;
|
||||||
|
}
|
||||||
|
|
||||||
fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
|
fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
|
||||||
const Header = abi.SourceIndexHeader;
|
const Header = abi.SourceIndexHeader;
|
||||||
const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
|
const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
|
||||||
@ -212,6 +238,7 @@ fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
|
|||||||
const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end]));
|
const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end]));
|
||||||
const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end]));
|
const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end]));
|
||||||
|
|
||||||
|
start_fuzzing_timestamp = header.start_timestamp;
|
||||||
try updateCoverage(directories, files, source_locations, string_bytes);
|
try updateCoverage(directories, files, source_locations, string_bytes);
|
||||||
js.emitSourceIndexChange();
|
js.emitSourceIndexChange();
|
||||||
}
|
}
|
||||||
@ -27,7 +27,11 @@ test "simple test" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test "fuzz example" {
|
test "fuzz example" {
|
||||||
// Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
|
const global = struct {
|
||||||
const input_bytes = std.testing.fuzzInput(.{});
|
fn testOne(input: []const u8) anyerror!void {
|
||||||
try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input_bytes));
|
// Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
|
||||||
|
try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try std.testing.fuzz(global.testOne, .{});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,6 +66,8 @@ pub fn start(
|
|||||||
.coverage_files = .{},
|
.coverage_files = .{},
|
||||||
.coverage_mutex = .{},
|
.coverage_mutex = .{},
|
||||||
.coverage_condition = .{},
|
.coverage_condition = .{},
|
||||||
|
|
||||||
|
.base_timestamp = std.time.nanoTimestamp(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// For accepting HTTP connections.
|
// For accepting HTTP connections.
|
||||||
|
|||||||
@ -33,6 +33,9 @@ coverage_mutex: std.Thread.Mutex,
|
|||||||
/// Signaled when `coverage_files` changes.
|
/// Signaled when `coverage_files` changes.
|
||||||
coverage_condition: std.Thread.Condition,
|
coverage_condition: std.Thread.Condition,
|
||||||
|
|
||||||
|
/// Time at initialization of WebServer.
|
||||||
|
base_timestamp: i128,
|
||||||
|
|
||||||
const fuzzer_bin_name = "fuzzer";
|
const fuzzer_bin_name = "fuzzer";
|
||||||
const fuzzer_arch_os_abi = "wasm32-freestanding";
|
const fuzzer_arch_os_abi = "wasm32-freestanding";
|
||||||
const fuzzer_cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
|
const fuzzer_cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
|
||||||
@ -43,6 +46,7 @@ const CoverageMap = struct {
|
|||||||
source_locations: []Coverage.SourceLocation,
|
source_locations: []Coverage.SourceLocation,
|
||||||
/// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
|
/// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
|
||||||
entry_points: std.ArrayListUnmanaged(u32),
|
entry_points: std.ArrayListUnmanaged(u32),
|
||||||
|
start_timestamp: i64,
|
||||||
|
|
||||||
fn deinit(cm: *CoverageMap, gpa: Allocator) void {
|
fn deinit(cm: *CoverageMap, gpa: Allocator) void {
|
||||||
std.posix.munmap(cm.mapped_memory);
|
std.posix.munmap(cm.mapped_memory);
|
||||||
@ -87,6 +91,10 @@ pub fn run(ws: *WebServer) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn now(s: *const WebServer) i64 {
|
||||||
|
return @intCast(std.time.nanoTimestamp() - s.base_timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
fn accept(ws: *WebServer, connection: std.net.Server.Connection) void {
|
fn accept(ws: *WebServer, connection: std.net.Server.Connection) void {
|
||||||
defer connection.stream.close();
|
defer connection.stream.close();
|
||||||
|
|
||||||
@ -128,11 +136,11 @@ fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void {
|
|||||||
std.mem.eql(u8, request.head.target, "/debug") or
|
std.mem.eql(u8, request.head.target, "/debug") or
|
||||||
std.mem.eql(u8, request.head.target, "/debug/"))
|
std.mem.eql(u8, request.head.target, "/debug/"))
|
||||||
{
|
{
|
||||||
try serveFile(ws, request, "fuzzer/index.html", "text/html");
|
try serveFile(ws, request, "fuzzer/web/index.html", "text/html");
|
||||||
} else if (std.mem.eql(u8, request.head.target, "/main.js") or
|
} else if (std.mem.eql(u8, request.head.target, "/main.js") or
|
||||||
std.mem.eql(u8, request.head.target, "/debug/main.js"))
|
std.mem.eql(u8, request.head.target, "/debug/main.js"))
|
||||||
{
|
{
|
||||||
try serveFile(ws, request, "fuzzer/main.js", "application/javascript");
|
try serveFile(ws, request, "fuzzer/web/main.js", "application/javascript");
|
||||||
} else if (std.mem.eql(u8, request.head.target, "/main.wasm")) {
|
} else if (std.mem.eql(u8, request.head.target, "/main.wasm")) {
|
||||||
try serveWasm(ws, request, .ReleaseFast);
|
try serveWasm(ws, request, .ReleaseFast);
|
||||||
} else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) {
|
} else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) {
|
||||||
@ -217,7 +225,7 @@ fn buildWasmBinary(
|
|||||||
|
|
||||||
const main_src_path: Build.Cache.Path = .{
|
const main_src_path: Build.Cache.Path = .{
|
||||||
.root_dir = ws.zig_lib_directory,
|
.root_dir = ws.zig_lib_directory,
|
||||||
.sub_path = "fuzzer/wasm/main.zig",
|
.sub_path = "fuzzer/web/main.zig",
|
||||||
};
|
};
|
||||||
const walk_src_path: Build.Cache.Path = .{
|
const walk_src_path: Build.Cache.Path = .{
|
||||||
.root_dir = ws.zig_lib_directory,
|
.root_dir = ws.zig_lib_directory,
|
||||||
@ -381,6 +389,13 @@ fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void {
|
|||||||
ws.coverage_mutex.lock();
|
ws.coverage_mutex.lock();
|
||||||
defer ws.coverage_mutex.unlock();
|
defer ws.coverage_mutex.unlock();
|
||||||
|
|
||||||
|
// On first connection, the client needs to know what time the server
|
||||||
|
// thinks it is to rebase timestamps.
|
||||||
|
{
|
||||||
|
const timestamp_message: abi.CurrentTime = .{ .base = ws.now() };
|
||||||
|
try web_socket.writeMessage(std.mem.asBytes(×tamp_message), .binary);
|
||||||
|
}
|
||||||
|
|
||||||
// On first connection, the client needs all the coverage information
|
// On first connection, the client needs all the coverage information
|
||||||
// so that subsequent updates can contain only the updated bits.
|
// so that subsequent updates can contain only the updated bits.
|
||||||
var prev_unique_runs: usize = 0;
|
var prev_unique_runs: usize = 0;
|
||||||
@ -406,7 +421,6 @@ fn sendCoverageContext(
|
|||||||
const seen_pcs = cov_header.seenBits();
|
const seen_pcs = cov_header.seenBits();
|
||||||
const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
|
const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
|
||||||
const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
|
const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
|
||||||
const lowest_stack = @atomicLoad(usize, &cov_header.lowest_stack, .monotonic);
|
|
||||||
if (prev_unique_runs.* != unique_runs) {
|
if (prev_unique_runs.* != unique_runs) {
|
||||||
// There has been an update.
|
// There has been an update.
|
||||||
if (prev_unique_runs.* == 0) {
|
if (prev_unique_runs.* == 0) {
|
||||||
@ -417,6 +431,7 @@ fn sendCoverageContext(
|
|||||||
.files_len = @intCast(coverage_map.coverage.files.entries.len),
|
.files_len = @intCast(coverage_map.coverage.files.entries.len),
|
||||||
.source_locations_len = @intCast(coverage_map.source_locations.len),
|
.source_locations_len = @intCast(coverage_map.source_locations.len),
|
||||||
.string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
|
.string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
|
||||||
|
.start_timestamp = coverage_map.start_timestamp,
|
||||||
};
|
};
|
||||||
const iovecs: [5]std.posix.iovec_const = .{
|
const iovecs: [5]std.posix.iovec_const = .{
|
||||||
makeIov(std.mem.asBytes(&header)),
|
makeIov(std.mem.asBytes(&header)),
|
||||||
@ -431,7 +446,6 @@ fn sendCoverageContext(
|
|||||||
const header: abi.CoverageUpdateHeader = .{
|
const header: abi.CoverageUpdateHeader = .{
|
||||||
.n_runs = n_runs,
|
.n_runs = n_runs,
|
||||||
.unique_runs = unique_runs,
|
.unique_runs = unique_runs,
|
||||||
.lowest_stack = lowest_stack,
|
|
||||||
};
|
};
|
||||||
const iovecs: [2]std.posix.iovec_const = .{
|
const iovecs: [2]std.posix.iovec_const = .{
|
||||||
makeIov(std.mem.asBytes(&header)),
|
makeIov(std.mem.asBytes(&header)),
|
||||||
@ -584,6 +598,7 @@ fn prepareTables(
|
|||||||
.mapped_memory = undefined, // populated below
|
.mapped_memory = undefined, // populated below
|
||||||
.source_locations = undefined, // populated below
|
.source_locations = undefined, // populated below
|
||||||
.entry_points = .{},
|
.entry_points = .{},
|
||||||
|
.start_timestamp = ws.now(),
|
||||||
};
|
};
|
||||||
errdefer gop.value_ptr.coverage.deinit(gpa);
|
errdefer gop.value_ptr.coverage.deinit(gpa);
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,6 @@ pub const SeenPcsHeader = extern struct {
|
|||||||
n_runs: usize,
|
n_runs: usize,
|
||||||
unique_runs: usize,
|
unique_runs: usize,
|
||||||
pcs_len: usize,
|
pcs_len: usize,
|
||||||
lowest_stack: usize,
|
|
||||||
|
|
||||||
/// Used for comptime assertions. Provides a mechanism for strategically
|
/// Used for comptime assertions. Provides a mechanism for strategically
|
||||||
/// causing compile errors.
|
/// causing compile errors.
|
||||||
@ -44,12 +43,19 @@ pub const SeenPcsHeader = extern struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const ToClientTag = enum(u8) {
|
pub const ToClientTag = enum(u8) {
|
||||||
|
current_time,
|
||||||
source_index,
|
source_index,
|
||||||
coverage_update,
|
coverage_update,
|
||||||
entry_points,
|
entry_points,
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const CurrentTime = extern struct {
|
||||||
|
tag: ToClientTag = .current_time,
|
||||||
|
/// Number of nanoseconds that all other timestamps are in reference to.
|
||||||
|
base: i64 align(1),
|
||||||
|
};
|
||||||
|
|
||||||
/// Sent to the fuzzer web client on first connection to the websocket URL.
|
/// Sent to the fuzzer web client on first connection to the websocket URL.
|
||||||
///
|
///
|
||||||
/// Trailing:
|
/// Trailing:
|
||||||
@ -63,6 +69,8 @@ pub const SourceIndexHeader = extern struct {
|
|||||||
files_len: u32,
|
files_len: u32,
|
||||||
source_locations_len: u32,
|
source_locations_len: u32,
|
||||||
string_bytes_len: u32,
|
string_bytes_len: u32,
|
||||||
|
/// When, according to the server, fuzzing started.
|
||||||
|
start_timestamp: i64 align(4),
|
||||||
|
|
||||||
pub const Flags = packed struct(u32) {
|
pub const Flags = packed struct(u32) {
|
||||||
tag: ToClientTag = .source_index,
|
tag: ToClientTag = .source_index,
|
||||||
@ -79,7 +87,6 @@ pub const CoverageUpdateHeader = extern struct {
|
|||||||
flags: Flags = .{},
|
flags: Flags = .{},
|
||||||
n_runs: u64,
|
n_runs: u64,
|
||||||
unique_runs: u64,
|
unique_runs: u64,
|
||||||
lowest_stack: u64,
|
|
||||||
|
|
||||||
pub const Flags = packed struct(u64) {
|
pub const Flags = packed struct(u64) {
|
||||||
tag: ToClientTag = .coverage_update,
|
tag: ToClientTag = .coverage_update,
|
||||||
|
|||||||
@ -1141,6 +1141,10 @@ pub const FuzzInputOptions = struct {
|
|||||||
corpus: []const []const u8 = &.{},
|
corpus: []const []const u8 = &.{},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub inline fn fuzzInput(options: FuzzInputOptions) []const u8 {
|
/// Inline to avoid coverage instrumentation.
|
||||||
return @import("root").fuzzInput(options);
|
pub inline fn fuzz(
|
||||||
|
comptime testOne: fn (input: []const u8) anyerror!void,
|
||||||
|
options: FuzzInputOptions,
|
||||||
|
) anyerror!void {
|
||||||
|
return @import("root").fuzz(testOne, options);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1708,6 +1708,10 @@ test "invalid tabs and carriage returns" {
|
|||||||
try testTokenize("\rpub\rswitch\r", &.{ .keyword_pub, .keyword_switch });
|
try testTokenize("\rpub\rswitch\r", &.{ .keyword_pub, .keyword_switch });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "fuzzable properties upheld" {
|
||||||
|
return std.testing.fuzz(testPropertiesUpheld, .{});
|
||||||
|
}
|
||||||
|
|
||||||
fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void {
|
fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void {
|
||||||
var tokenizer = Tokenizer.init(source);
|
var tokenizer = Tokenizer.init(source);
|
||||||
for (expected_token_tags) |expected_token_tag| {
|
for (expected_token_tags) |expected_token_tag| {
|
||||||
@ -1723,8 +1727,7 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v
|
|||||||
try std.testing.expectEqual(source.len, last_token.loc.end);
|
try std.testing.expectEqual(source.len, last_token.loc.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "fuzzable properties upheld" {
|
fn testPropertiesUpheld(source: []const u8) anyerror!void {
|
||||||
const source = std.testing.fuzzInput(.{});
|
|
||||||
const source0 = try std.testing.allocator.dupeZ(u8, source);
|
const source0 = try std.testing.allocator.dupeZ(u8, source);
|
||||||
defer std.testing.allocator.free(source0);
|
defer std.testing.allocator.free(source0);
|
||||||
var tokenizer = Tokenizer.init(source0);
|
var tokenizer = Tokenizer.init(source0);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user