commit eccd06f5d01d05286691bc77e6d1e582bb14b7b1 (tree)
parent 4fba7336a9038b4abf647caf822f89df717d3cc0
Author: Andrew Kelley <andrew@ziglang.org>
Date: Wed, 11 Sep 2024 23:41:51 -0700
Merge pull request #21370 from ziglang/fuzz
rework fuzzing API to accept a function pointer parameter
Diffstat:
14 files changed, 1042 insertions(+), 925 deletions(-)
diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig
@@ -145,31 +145,23 @@ fn mainServer() !void {
.start_fuzzing => {
if (!builtin.fuzz) unreachable;
const index = try server.receiveBody_u32();
- var first = true;
const test_fn = builtin.test_functions[index];
- while (true) {
- testing.allocator_instance = .{};
- defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
- log_err_count = 0;
- is_fuzz_test = false;
- test_fn.func() catch |err| switch (err) {
- error.SkipZigTest => continue,
- else => {
- if (@errorReturnTrace()) |trace| {
- std.debug.dumpStackTrace(trace.*);
- }
- std.debug.print("failed with error.{s}\n", .{@errorName(err)});
- std.process.exit(1);
- },
- };
- if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput");
- 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);
- }
- }
+ const entry_addr = @intFromPtr(test_fn.func);
+ try server.serveU64Message(.fuzz_start_addr, entry_addr);
+ defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
+ is_fuzz_test = false;
+ test_fn.func() catch |err| switch (err) {
+ error.SkipZigTest => return,
+ else => {
+ if (@errorReturnTrace()) |trace| {
+ std.debug.dumpStackTrace(trace.*);
+ }
+ std.debug.print("failed with error.{s}\n", .{@errorName(err)});
+ std.process.exit(1);
+ },
+ };
+ if (!is_fuzz_test) @panic("missed call to std.testing.fuzz");
+ if (log_err_count != 0) @panic("error logs detected");
},
else => {
@@ -349,19 +341,72 @@ const FuzzerSlice = extern struct {
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_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();
- 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;
+
+ // 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) {
- 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);
- const random = prng.random();
- return options.corpus[random.uintLessThan(usize, options.corpus.len)];
+
+ // When the unit test executable is not built in fuzz mode, only run the
+ // provided corpus.
+ 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("");
}
diff --git a/lib/fuzzer.zig b/lib/fuzzer.zig
@@ -28,7 +28,8 @@ fn logOverride(
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 {
handleCmp(@returnAddress(), arg1, arg2);
@@ -220,7 +221,6 @@ const Fuzzer = struct {
.n_runs = 0,
.unique_runs = 0,
.pcs_len = pcs.len,
- .lowest_stack = std.math.maxInt(usize),
};
f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header));
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 rng = fuzzer.rng.random();
- if (f.recent_cases.entries.len == 0) {
- // Prepare initial input.
- try f.recent_cases.ensureUnusedCapacity(gpa, 100);
- const len = rng.uintLessThanBiased(usize, 80);
- try f.input.resize(gpa, len);
- rng.bytes(f.input.items);
- f.recent_cases.putAssumeCapacity(.{
- .id = 0,
- .input = try gpa.dupe(u8, f.input.items),
- .score = 0,
- }, {});
- } else {
+ // Prepare initial input.
+ assert(f.recent_cases.entries.len == 0);
+ assert(f.n_runs == 0);
+ try f.recent_cases.ensureUnusedCapacity(gpa, 100);
+ const len = rng.uintLessThanBiased(usize, 80);
+ try f.input.resize(gpa, len);
+ rng.bytes(f.input.items);
+ f.recent_cases.putAssumeCapacity(.{
+ .id = 0,
+ .input = try gpa.dupe(u8, f.input.items),
+ .score = 0,
+ }, {});
+
+ 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();
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);
}
@@ -317,26 +335,12 @@ const Fuzzer = struct {
// This has to be done before deinitializing the deleted items.
const doomed_runs = f.recent_cases.keys()[cap..];
f.recent_cases.shrinkRetainingCapacity(cap);
- for (doomed_runs) |*run| {
- std.log.info("culling score={d} id={d}", .{ run.score, run.id });
- run.deinit(gpa);
+ for (doomed_runs) |*doomed_run| {
+ std.log.info("culling score={d} id={d}", .{ doomed_run.score, doomed_run.id });
+ 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 {
@@ -419,10 +423,13 @@ export fn fuzzer_coverage_id() u64 {
return fuzzer.coverage_id;
}
-export fn fuzzer_next() Fuzzer.Slice {
- return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) {
- error.OutOfMemory => @panic("out of memory"),
- });
+var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C) void = undefined;
+
+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 {
@@ -432,24 +439,24 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
const pc_counters_start = @extern([*]u8, .{
.name = "__start___sancov_cntrs",
.linkage = .weak,
- }) orelse fatal("missing __start___sancov_cntrs symbol");
+ }) orelse fatal("missing __start___sancov_cntrs symbol", .{});
const pc_counters_end = @extern([*]u8, .{
.name = "__stop___sancov_cntrs",
.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 pcs_start = @extern([*]usize, .{
.name = "__start___sancov_pcs1",
.linkage = .weak,
- }) orelse fatal("missing __start___sancov_pcs1 symbol");
+ }) orelse fatal("missing __start___sancov_pcs1 symbol", .{});
const pcs_end = @extern([*]usize, .{
.name = "__stop___sancov_pcs1",
.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];
diff --git a/lib/fuzzer/index.html b/lib/fuzzer/index.html
@@ -1,161 +0,0 @@
-<!doctype html>
-<html>
- <head>
- <meta charset="utf-8">
- <title>Zig Build System Interface</title>
- <style type="text/css">
- body {
- font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
- color: #000000;
- }
- .hidden {
- display: none;
- }
- table {
- width: 100%;
- }
- a {
- color: #2A6286;
- }
- pre{
- font-family:"Source Code Pro",monospace;
- font-size:1em;
- background-color:#F5F5F5;
- padding: 1em;
- margin: 0;
- overflow-x: auto;
- }
- :not(pre) > code {
- white-space: break-spaces;
- }
- code {
- font-family:"Source Code Pro",monospace;
- font-size: 0.9em;
- }
- code a {
- color: #000000;
- }
- kbd {
- color: #000;
- background-color: #fafbfc;
- border-color: #d1d5da;
- border-bottom-color: #c6cbd1;
- box-shadow-color: #c6cbd1;
- display: inline-block;
- padding: 0.3em 0.2em;
- font: 1.2em monospace;
- line-height: 0.8em;
- vertical-align: middle;
- border: solid 1px;
- border-radius: 3px;
- box-shadow: inset 0 -1px 0;
- cursor: default;
- }
-
- .l {
- display: inline-block;
- background: red;
- width: 1em;
- height: 1em;
- border-radius: 1em;
- }
- .c {
- background-color: green;
- }
-
- .tok-kw {
- color: #333;
- font-weight: bold;
- }
- .tok-str {
- color: #d14;
- }
- .tok-builtin {
- color: #0086b3;
- }
- .tok-comment {
- color: #777;
- font-style: italic;
- }
- .tok-fn {
- color: #900;
- font-weight: bold;
- }
- .tok-null {
- color: #008080;
- }
- .tok-number {
- color: #008080;
- }
- .tok-type {
- color: #458;
- font-weight: bold;
- }
-
- @media (prefers-color-scheme: dark) {
- body {
- background-color: #111;
- color: #bbb;
- }
- pre {
- background-color: #222;
- color: #ccc;
- }
- a {
- color: #88f;
- }
- code a {
- color: #ccc;
- }
- .l {
- background-color: red;
- }
- .c {
- background-color: green;
- }
- .tok-kw {
- color: #eee;
- }
- .tok-str {
- color: #2e5;
- }
- .tok-builtin {
- color: #ff894c;
- }
- .tok-comment {
- color: #aa7;
- }
- .tok-fn {
- color: #B1A0F8;
- }
- .tok-null {
- color: #ff8080;
- }
- .tok-number {
- color: #ff8080;
- }
- .tok-type {
- color: #68f;
- }
- }
- </style>
- </head>
- <body>
- <p id="status">Loading JavaScript...</p>
- <div id="sectStats" class="hidden">
- <ul>
- <li>Total Runs: <span id="statTotalRuns"></span></li>
- <li>Unique Runs: <span id="statUniqueRuns"></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>
- </ul>
- </div>
- <div id="sectSource" class="hidden">
- <h2>Source Code</h2>
- <pre><code id="sourceText"></code></pre>
- </div>
- <script src="main.js"></script>
- </body>
-</html>
-
diff --git a/lib/fuzzer/main.js b/lib/fuzzer/main.js
@@ -1,249 +0,0 @@
-(function() {
- const domStatus = document.getElementById("status");
- const domSectSource = document.getElementById("sectSource");
- const domSectStats = document.getElementById("sectStats");
- const domSourceText = document.getElementById("sourceText");
- const domStatTotalRuns = document.getElementById("statTotalRuns");
- const domStatUniqueRuns = document.getElementById("statUniqueRuns");
- const domStatCoverage = document.getElementById("statCoverage");
- const domStatLowestStack = document.getElementById("statLowestStack");
- const domEntryPointsList = document.getElementById("entryPointsList");
-
- let wasm_promise = fetch("main.wasm");
- let sources_promise = fetch("sources.tar").then(function(response) {
- if (!response.ok) throw new Error("unable to download sources");
- return response.arrayBuffer();
- });
- var wasm_exports = null;
- var curNavSearch = null;
- var curNavLocation = null;
-
- const text_decoder = new TextDecoder();
- const text_encoder = new TextEncoder();
-
- domStatus.textContent = "Loading WebAssembly...";
- WebAssembly.instantiateStreaming(wasm_promise, {
- js: {
- log: function(ptr, len) {
- const msg = decodeString(ptr, len);
- console.log(msg);
- },
- panic: function (ptr, len) {
- const msg = decodeString(ptr, len);
- throw new Error("panic: " + msg);
- },
- emitSourceIndexChange: onSourceIndexChange,
- emitCoverageUpdate: onCoverageUpdate,
- emitEntryPointsUpdate: renderStats,
- },
- }).then(function(obj) {
- wasm_exports = obj.instance.exports;
- window.wasm = obj; // for debugging
- domStatus.textContent = "Loading sources tarball...";
-
- sources_promise.then(function(buffer) {
- domStatus.textContent = "Parsing sources...";
- const js_array = new Uint8Array(buffer);
- const ptr = wasm_exports.alloc(js_array.length);
- const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
- wasm_array.set(js_array);
- wasm_exports.unpack(ptr, js_array.length);
-
- window.addEventListener('popstate', onPopState, false);
- onHashChange(null);
-
- domStatus.textContent = "Waiting for server to send source location metadata...";
- connectWebSocket();
- });
- });
-
- function onPopState(ev) {
- onHashChange(ev.state);
- }
-
- function onHashChange(state) {
- history.replaceState({}, "");
- navigate(location.hash);
- if (state == null) window.scrollTo({top: 0});
- }
-
- function navigate(location_hash) {
- domSectSource.classList.add("hidden");
-
- curNavLocation = null;
- curNavSearch = null;
-
- if (location_hash.length > 1 && location_hash[0] === '#') {
- const query = location_hash.substring(1);
- const qpos = query.indexOf("?");
- let nonSearchPart;
- if (qpos === -1) {
- nonSearchPart = query;
- } else {
- nonSearchPart = query.substring(0, qpos);
- curNavSearch = decodeURIComponent(query.substring(qpos + 1));
- }
-
- if (nonSearchPart[0] == "l") {
- curNavLocation = +nonSearchPart.substring(1);
- renderSource(curNavLocation);
- }
- }
-
- render();
- }
-
- function connectWebSocket() {
- const host = document.location.host;
- const pathname = document.location.pathname;
- const isHttps = document.location.protocol === 'https:';
- const match = host.match(/^(.+):(\d+)$/);
- const defaultPort = isHttps ? 443 : 80;
- const port = match ? parseInt(match[2], 10) : defaultPort;
- const hostName = match ? match[1] : host;
- const wsProto = isHttps ? "wss:" : "ws:";
- const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
- ws = new WebSocket(wsUrl);
- ws.binaryType = "arraybuffer";
- ws.addEventListener('message', onWebSocketMessage, false);
- ws.addEventListener('error', timeoutThenCreateNew, false);
- ws.addEventListener('close', timeoutThenCreateNew, false);
- ws.addEventListener('open', onWebSocketOpen, false);
- }
-
- function onWebSocketOpen() {
- //console.log("web socket opened");
- }
-
- function onWebSocketMessage(ev) {
- wasmOnMessage(ev.data);
- }
-
- function timeoutThenCreateNew() {
- ws.removeEventListener('message', onWebSocketMessage, false);
- ws.removeEventListener('error', timeoutThenCreateNew, false);
- ws.removeEventListener('close', timeoutThenCreateNew, false);
- ws.removeEventListener('open', onWebSocketOpen, false);
- ws = null;
- setTimeout(connectWebSocket, 1000);
- }
-
- function wasmOnMessage(data) {
- const jsArray = new Uint8Array(data);
- const ptr = wasm_exports.message_begin(jsArray.length);
- const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
- wasmArray.set(jsArray);
- wasm_exports.message_end();
- }
-
- function onSourceIndexChange() {
- render();
- if (curNavLocation != null) renderSource(curNavLocation);
- }
-
- function onCoverageUpdate() {
- renderStats();
- renderCoverage();
- }
-
- function render() {
- domStatus.classList.add("hidden");
- }
-
- function renderStats() {
- const totalRuns = wasm_exports.totalRuns();
- const uniqueRuns = wasm_exports.uniqueRuns();
- const totalSourceLocations = wasm_exports.totalSourceLocations();
- const coveredSourceLocations = wasm_exports.coveredSourceLocations();
- domStatTotalRuns.innerText = totalRuns;
- domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)";
- domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
- domStatLowestStack.innerText = unwrapString(wasm_exports.lowestStack());
-
- const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
- resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
- for (let i = 0; i < entryPoints.length; i += 1) {
- const liDom = domEntryPointsList.children[i];
- liDom.innerHTML = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i]));
- }
-
-
- domSectStats.classList.remove("hidden");
- }
-
- function renderCoverage() {
- if (curNavLocation == null) return;
- const sourceLocationIndex = curNavLocation;
-
- for (let i = 0; i < domSourceText.children.length; i += 1) {
- const childDom = domSourceText.children[i];
- if (childDom.id != null && childDom.id[0] == "l") {
- childDom.classList.add("l");
- childDom.classList.remove("c");
- }
- }
- const coveredList = unwrapInt32Array(wasm_exports.sourceLocationFileCoveredList(sourceLocationIndex));
- for (let i = 0; i < coveredList.length; i += 1) {
- document.getElementById("l" + coveredList[i]).classList.add("c");
- }
- }
-
- function resizeDomList(listDom, desiredLen, templateHtml) {
- for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
- listDom.insertAdjacentHTML('beforeend', templateHtml);
- }
- while (desiredLen < listDom.childElementCount) {
- listDom.removeChild(listDom.lastChild);
- }
- }
-
- function percent(a, b) {
- return ((Number(a) / Number(b)) * 100).toFixed(1);
- }
-
- function renderSource(sourceLocationIndex) {
- const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex));
- if (pathName.length === 0) return;
-
- const h2 = domSectSource.children[0];
- h2.innerText = pathName;
- domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex));
-
- domSectSource.classList.remove("hidden");
-
- // Empirically, Firefox needs this requestAnimationFrame in order for the scrollIntoView to work.
- requestAnimationFrame(function() {
- const slDom = document.getElementById("l" + sourceLocationIndex);
- if (slDom != null) slDom.scrollIntoView({
- behavior: "smooth",
- block: "center",
- });
- });
- }
-
- function decodeString(ptr, len) {
- if (len === 0) return "";
- return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
- }
-
- function unwrapInt32Array(bigint) {
- const ptr = Number(bigint & 0xffffffffn);
- const len = Number(bigint >> 32n);
- if (len === 0) return new Uint32Array();
- return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
- }
-
- function setInputString(s) {
- const jsArray = text_encoder.encode(s);
- const len = jsArray.length;
- const ptr = wasm_exports.set_input_string(len);
- const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
- wasmArray.set(jsArray);
- }
-
- function unwrapString(bigint) {
- const ptr = Number(bigint & 0xffffffffn);
- const len = Number(bigint >> 32n);
- return decodeString(ptr, len);
- }
-})();
diff --git a/lib/fuzzer/wasm/main.zig b/lib/fuzzer/wasm/main.zig
@@ -1,428 +0,0 @@
-const std = @import("std");
-const assert = std.debug.assert;
-const abi = std.Build.Fuzz.abi;
-const gpa = std.heap.wasm_allocator;
-const log = std.log;
-const Coverage = std.debug.Coverage;
-const Allocator = std.mem.Allocator;
-
-const Walk = @import("Walk");
-const Decl = Walk.Decl;
-const html_render = @import("html_render");
-
-const js = struct {
- extern "js" fn log(ptr: [*]const u8, len: usize) void;
- extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
- extern "js" fn emitSourceIndexChange() void;
- extern "js" fn emitCoverageUpdate() void;
- extern "js" fn emitEntryPointsUpdate() void;
-};
-
-pub const std_options: std.Options = .{
- .logFn = logFn,
-};
-
-pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
- _ = st;
- _ = addr;
- log.err("panic: {s}", .{msg});
- @trap();
-}
-
-fn logFn(
- comptime message_level: log.Level,
- comptime scope: @TypeOf(.enum_literal),
- comptime format: []const u8,
- args: anytype,
-) void {
- const level_txt = comptime message_level.asText();
- const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
- var buf: [500]u8 = undefined;
- const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
- buf[buf.len - 3 ..][0..3].* = "...".*;
- break :l &buf;
- };
- js.log(line.ptr, line.len);
-}
-
-export fn alloc(n: usize) [*]u8 {
- const slice = gpa.alloc(u8, n) catch @panic("OOM");
- return slice.ptr;
-}
-
-var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
-
-/// Resizes the message buffer to be the correct length; returns the pointer to
-/// the query string.
-export fn message_begin(len: usize) [*]u8 {
- message_buffer.resize(gpa, len) catch @panic("OOM");
- return message_buffer.items.ptr;
-}
-
-export fn message_end() void {
- const msg_bytes = message_buffer.items;
-
- const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
- switch (tag) {
- .source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
- .coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
- .entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
- _ => unreachable,
- }
-}
-
-export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
- const tar_bytes = tar_ptr[0..tar_len];
- log.debug("received {d} bytes of tar file", .{tar_bytes.len});
-
- unpackInner(tar_bytes) catch |err| {
- fatal("unable to unpack tar: {s}", .{@errorName(err)});
- };
-}
-
-/// Set by `set_input_string`.
-var input_string: std.ArrayListUnmanaged(u8) = .{};
-var string_result: std.ArrayListUnmanaged(u8) = .{};
-
-export fn set_input_string(len: usize) [*]u8 {
- input_string.resize(gpa, len) catch @panic("OOM");
- return input_string.items.ptr;
-}
-
-/// Looks up the root struct decl corresponding to a file by path.
-/// Uses `input_string`.
-export fn find_file_root() Decl.Index {
- const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
- return file.findRootDecl();
-}
-
-export fn decl_source_html(decl_index: Decl.Index) String {
- const decl = decl_index.get();
-
- string_result.clearRetainingCapacity();
- html_render.fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
- fatal("unable to render source: {s}", .{@errorName(err)});
- };
- 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 {
- return coverage_source_locations.items.len;
-}
-
-export fn coveredSourceLocations() usize {
- const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..];
- var count: usize = 0;
- for (covered_bits) |byte| count += @popCount(byte);
- return count;
-}
-
-export fn totalRuns() u64 {
- const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
- return header.n_runs;
-}
-
-export fn uniqueRuns() u64 {
- const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
- return header.unique_runs;
-}
-
-const String = Slice(u8);
-
-fn Slice(T: type) type {
- return packed struct(u64) {
- ptr: u32,
- len: u32,
-
- fn init(s: []const T) @This() {
- return .{
- .ptr = @intFromPtr(s.ptr),
- .len = s.len,
- };
- }
- };
-}
-
-fn unpackInner(tar_bytes: []u8) !void {
- var fbs = std.io.fixedBufferStream(tar_bytes);
- var file_name_buffer: [1024]u8 = undefined;
- var link_name_buffer: [1024]u8 = undefined;
- var it = std.tar.iterator(fbs.reader(), .{
- .file_name_buffer = &file_name_buffer,
- .link_name_buffer = &link_name_buffer,
- });
- while (try it.next()) |tar_file| {
- switch (tar_file.kind) {
- .file => {
- if (tar_file.size == 0 and tar_file.name.len == 0) break;
- if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
- log.debug("found file: '{s}'", .{tar_file.name});
- const file_name = try gpa.dupe(u8, tar_file.name);
- if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
- const pkg_name = file_name[0..pkg_name_end];
- const gop = try Walk.modules.getOrPut(gpa, pkg_name);
- const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
- if (!gop.found_existing or
- std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
- std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
- {
- gop.value_ptr.* = file;
- }
- const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
- assert(file == try Walk.add_file(file_name, file_bytes));
- }
- } else {
- log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
- }
- },
- else => continue,
- }
- }
-}
-
-fn fatal(comptime format: []const u8, args: anytype) noreturn {
- var buf: [500]u8 = undefined;
- const line = std.fmt.bufPrint(&buf, format, args) catch l: {
- buf[buf.len - 3 ..][0..3].* = "...".*;
- break :l &buf;
- };
- js.panic(line.ptr, line.len);
-}
-
-fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
- const Header = abi.SourceIndexHeader;
- const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
-
- const directories_start = @sizeOf(Header);
- const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String);
- const files_start = directories_end;
- const files_end = files_start + header.files_len * @sizeOf(Coverage.File);
- const source_locations_start = files_end;
- const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation);
- const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len];
-
- const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_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]));
-
- try updateCoverage(directories, files, source_locations, string_bytes);
- js.emitSourceIndexChange();
-}
-
-fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
- recent_coverage_update.clearRetainingCapacity();
- recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM");
- js.emitCoverageUpdate();
-}
-
-var entry_points: std.ArrayListUnmanaged(u32) = .{};
-
-fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void {
- const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*);
- entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM");
- @memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..]));
- js.emitEntryPointsUpdate();
-}
-
-export fn entryPoints() Slice(u32) {
- return Slice(u32).init(entry_points.items);
-}
-
-/// Index into `coverage_source_locations`.
-const SourceLocationIndex = enum(u32) {
- _,
-
- fn haveCoverage(sli: SourceLocationIndex) bool {
- return @intFromEnum(sli) < coverage_source_locations.items.len;
- }
-
- fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation {
- return &coverage_source_locations.items[@intFromEnum(sli)];
- }
-
- fn sourceLocationLinkHtml(
- sli: SourceLocationIndex,
- out: *std.ArrayListUnmanaged(u8),
- ) Allocator.Error!void {
- const sl = sli.ptr();
- try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)});
- try sli.appendPath(out);
- try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column });
- }
-
- fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void {
- const sl = sli.ptr();
- const file = coverage.fileAt(sl.file);
- const file_name = coverage.stringAt(file.basename);
- const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]);
- try html_render.appendEscaped(out, dir_name);
- try out.appendSlice(gpa, "/");
- try html_render.appendEscaped(out, file_name);
- }
-
- fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index {
- var buf: std.ArrayListUnmanaged(u8) = .{};
- defer buf.deinit(gpa);
- sli.appendPath(&buf) catch @panic("OOM");
- return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null);
- }
-
- fn fileHtml(
- sli: SourceLocationIndex,
- out: *std.ArrayListUnmanaged(u8),
- ) error{ OutOfMemory, SourceUnavailable }!void {
- const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable;
- const root_node = walk_file_index.findRootDecl().get().ast_node;
- var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{};
- defer annotations.deinit(gpa);
- try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items);
- html_render.fileSourceHtml(walk_file_index, out, root_node, .{
- .source_location_annotations = annotations.items,
- }) catch |err| {
- fatal("unable to render source: {s}", .{@errorName(err)});
- };
- }
-};
-
-fn computeSourceAnnotations(
- cov_file_index: Coverage.File.Index,
- walk_file_index: Walk.File.Index,
- annotations: *std.ArrayListUnmanaged(html_render.Annotation),
- source_locations: []const Coverage.SourceLocation,
-) !void {
- // Collect all the source locations from only this file into this array
- // first, then sort by line, col, so that we can collect annotations with
- // O(N) time complexity.
- var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
- defer locs.deinit(gpa);
-
- for (source_locations, 0..) |sl, sli_usize| {
- if (sl.file != cov_file_index) continue;
- const sli: SourceLocationIndex = @enumFromInt(sli_usize);
- try locs.append(gpa, sli);
- }
-
- std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct {
- pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool {
- _ = context;
- const lhs_ptr = lhs.ptr();
- const rhs_ptr = rhs.ptr();
- if (lhs_ptr.line < rhs_ptr.line) return true;
- if (lhs_ptr.line > rhs_ptr.line) return false;
- return lhs_ptr.column < rhs_ptr.column;
- }
- }.lessThan);
-
- const source = walk_file_index.get_ast().source;
- var line: usize = 1;
- var column: usize = 1;
- var next_loc_index: usize = 0;
- for (source, 0..) |byte, offset| {
- if (byte == '\n') {
- line += 1;
- column = 1;
- } else {
- column += 1;
- }
- while (true) {
- if (next_loc_index >= locs.items.len) return;
- const next_sli = locs.items[next_loc_index];
- const next_sl = next_sli.ptr();
- if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break;
- try annotations.append(gpa, .{
- .file_byte_offset = offset,
- .dom_id = @intFromEnum(next_sli),
- });
- next_loc_index += 1;
- }
- }
-}
-
-var coverage = Coverage.init;
-/// Index of type `SourceLocationIndex`.
-var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
-/// Contains the most recent coverage update message, unmodified.
-var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
-
-fn updateCoverage(
- directories: []const Coverage.String,
- files: []const Coverage.File,
- source_locations: []const Coverage.SourceLocation,
- string_bytes: []const u8,
-) !void {
- coverage.directories.clearRetainingCapacity();
- coverage.files.clearRetainingCapacity();
- coverage.string_bytes.clearRetainingCapacity();
- coverage_source_locations.clearRetainingCapacity();
-
- try coverage_source_locations.appendSlice(gpa, source_locations);
- try coverage.string_bytes.appendSlice(gpa, string_bytes);
-
- try coverage.files.entries.resize(gpa, files.len);
- @memcpy(coverage.files.entries.items(.key), files);
- try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
-
- try coverage.directories.entries.resize(gpa, directories.len);
- @memcpy(coverage.directories.entries.items(.key), directories);
- try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
-}
-
-export fn sourceLocationLinkHtml(index: SourceLocationIndex) String {
- string_result.clearRetainingCapacity();
- index.sourceLocationLinkHtml(&string_result) catch @panic("OOM");
- return String.init(string_result.items);
-}
-
-/// Returns empty string if coverage metadata is not available for this source location.
-export fn sourceLocationPath(sli: SourceLocationIndex) String {
- string_result.clearRetainingCapacity();
- if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM");
- return String.init(string_result.items);
-}
-
-export fn sourceLocationFileHtml(sli: SourceLocationIndex) String {
- string_result.clearRetainingCapacity();
- sli.fileHtml(&string_result) catch |err| switch (err) {
- error.OutOfMemory => @panic("OOM"),
- error.SourceUnavailable => {},
- };
- return String.init(string_result.items);
-}
-
-export fn sourceLocationFileCoveredList(sli_file: SourceLocationIndex) Slice(SourceLocationIndex) {
- const global = struct {
- var result: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
- fn add(i: u32, want_file: Coverage.File.Index) void {
- const src_loc_index: SourceLocationIndex = @enumFromInt(i);
- if (src_loc_index.ptr().file == want_file) result.appendAssumeCapacity(src_loc_index);
- }
- };
- const want_file = sli_file.ptr().file;
- global.result.clearRetainingCapacity();
-
- // This code assumes 64-bit elements, which is incorrect if the executable
- // being fuzzed is not a 64-bit CPU. It also assumes little-endian which
- // can also be incorrect.
- comptime assert(abi.CoverageUpdateHeader.trailing[0] == .pc_bits_usize);
- const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64);
- const covered_bits = std.mem.bytesAsSlice(
- u64,
- recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)],
- );
- var sli: u32 = 0;
- for (covered_bits) |elem| {
- global.result.ensureUnusedCapacity(gpa, 64) catch @panic("OOM");
- for (0..@bitSizeOf(u64)) |i| {
- if ((elem & (@as(u64, 1) << @intCast(i))) != 0) global.add(sli, want_file);
- sli += 1;
- }
- }
- return Slice(SourceLocationIndex).init(global.result.items);
-}
diff --git a/lib/fuzzer/web/index.html b/lib/fuzzer/web/index.html
@@ -0,0 +1,161 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Zig Build System Interface</title>
+ <style type="text/css">
+ body {
+ font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
+ color: #000000;
+ }
+ .hidden {
+ display: none;
+ }
+ table {
+ width: 100%;
+ }
+ a {
+ color: #2A6286;
+ }
+ pre{
+ font-family:"Source Code Pro",monospace;
+ font-size:1em;
+ background-color:#F5F5F5;
+ padding: 1em;
+ margin: 0;
+ overflow-x: auto;
+ }
+ :not(pre) > code {
+ white-space: break-spaces;
+ }
+ code {
+ font-family:"Source Code Pro",monospace;
+ font-size: 0.9em;
+ }
+ code a {
+ color: #000000;
+ }
+ kbd {
+ color: #000;
+ background-color: #fafbfc;
+ border-color: #d1d5da;
+ border-bottom-color: #c6cbd1;
+ box-shadow-color: #c6cbd1;
+ display: inline-block;
+ padding: 0.3em 0.2em;
+ font: 1.2em monospace;
+ line-height: 0.8em;
+ vertical-align: middle;
+ border: solid 1px;
+ border-radius: 3px;
+ box-shadow: inset 0 -1px 0;
+ cursor: default;
+ }
+
+ .l {
+ display: inline-block;
+ background: red;
+ width: 1em;
+ height: 1em;
+ border-radius: 1em;
+ }
+ .c {
+ background-color: green;
+ }
+
+ .tok-kw {
+ color: #333;
+ font-weight: bold;
+ }
+ .tok-str {
+ color: #d14;
+ }
+ .tok-builtin {
+ color: #0086b3;
+ }
+ .tok-comment {
+ color: #777;
+ font-style: italic;
+ }
+ .tok-fn {
+ color: #900;
+ font-weight: bold;
+ }
+ .tok-null {
+ color: #008080;
+ }
+ .tok-number {
+ color: #008080;
+ }
+ .tok-type {
+ color: #458;
+ font-weight: bold;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ body {
+ background-color: #111;
+ color: #bbb;
+ }
+ pre {
+ background-color: #222;
+ color: #ccc;
+ }
+ a {
+ color: #88f;
+ }
+ code a {
+ color: #ccc;
+ }
+ .l {
+ background-color: red;
+ }
+ .c {
+ background-color: green;
+ }
+ .tok-kw {
+ color: #eee;
+ }
+ .tok-str {
+ color: #2e5;
+ }
+ .tok-builtin {
+ color: #ff894c;
+ }
+ .tok-comment {
+ color: #aa7;
+ }
+ .tok-fn {
+ color: #B1A0F8;
+ }
+ .tok-null {
+ color: #ff8080;
+ }
+ .tok-number {
+ color: #ff8080;
+ }
+ .tok-type {
+ color: #68f;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <p id="status">Loading JavaScript...</p>
+ <div id="sectStats" class="hidden">
+ <ul>
+ <li>Total Runs: <span id="statTotalRuns"></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>Entry Points: <ul id="entryPointsList"></ul></li>
+ </ul>
+ </div>
+ <div id="sectSource" class="hidden">
+ <h2>Source Code</h2>
+ <pre><code id="sourceText"></code></pre>
+ </div>
+ <script src="main.js"></script>
+ </body>
+</html>
+
diff --git a/lib/fuzzer/web/main.js b/lib/fuzzer/web/main.js
@@ -0,0 +1,252 @@
+(function() {
+ const domStatus = document.getElementById("status");
+ const domSectSource = document.getElementById("sectSource");
+ const domSectStats = document.getElementById("sectStats");
+ const domSourceText = document.getElementById("sourceText");
+ const domStatTotalRuns = document.getElementById("statTotalRuns");
+ const domStatUniqueRuns = document.getElementById("statUniqueRuns");
+ const domStatSpeed = document.getElementById("statSpeed");
+ const domStatCoverage = document.getElementById("statCoverage");
+ const domEntryPointsList = document.getElementById("entryPointsList");
+
+ let wasm_promise = fetch("main.wasm");
+ let sources_promise = fetch("sources.tar").then(function(response) {
+ if (!response.ok) throw new Error("unable to download sources");
+ return response.arrayBuffer();
+ });
+ var wasm_exports = null;
+ var curNavSearch = null;
+ var curNavLocation = null;
+
+ const text_decoder = new TextDecoder();
+ const text_encoder = new TextEncoder();
+
+ domStatus.textContent = "Loading WebAssembly...";
+ WebAssembly.instantiateStreaming(wasm_promise, {
+ js: {
+ log: function(ptr, len) {
+ const msg = decodeString(ptr, len);
+ console.log(msg);
+ },
+ panic: function (ptr, len) {
+ const msg = decodeString(ptr, len);
+ throw new Error("panic: " + msg);
+ },
+ timestamp: function () {
+ return BigInt(new Date());
+ },
+ emitSourceIndexChange: onSourceIndexChange,
+ emitCoverageUpdate: onCoverageUpdate,
+ emitEntryPointsUpdate: renderStats,
+ },
+ }).then(function(obj) {
+ wasm_exports = obj.instance.exports;
+ window.wasm = obj; // for debugging
+ domStatus.textContent = "Loading sources tarball...";
+
+ sources_promise.then(function(buffer) {
+ domStatus.textContent = "Parsing sources...";
+ const js_array = new Uint8Array(buffer);
+ const ptr = wasm_exports.alloc(js_array.length);
+ const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
+ wasm_array.set(js_array);
+ wasm_exports.unpack(ptr, js_array.length);
+
+ window.addEventListener('popstate', onPopState, false);
+ onHashChange(null);
+
+ domStatus.textContent = "Waiting for server to send source location metadata...";
+ connectWebSocket();
+ });
+ });
+
+ function onPopState(ev) {
+ onHashChange(ev.state);
+ }
+
+ function onHashChange(state) {
+ history.replaceState({}, "");
+ navigate(location.hash);
+ if (state == null) window.scrollTo({top: 0});
+ }
+
+ function navigate(location_hash) {
+ domSectSource.classList.add("hidden");
+
+ curNavLocation = null;
+ curNavSearch = null;
+
+ if (location_hash.length > 1 && location_hash[0] === '#') {
+ const query = location_hash.substring(1);
+ const qpos = query.indexOf("?");
+ let nonSearchPart;
+ if (qpos === -1) {
+ nonSearchPart = query;
+ } else {
+ nonSearchPart = query.substring(0, qpos);
+ curNavSearch = decodeURIComponent(query.substring(qpos + 1));
+ }
+
+ if (nonSearchPart[0] == "l") {
+ curNavLocation = +nonSearchPart.substring(1);
+ renderSource(curNavLocation);
+ }
+ }
+
+ render();
+ }
+
+ function connectWebSocket() {
+ const host = document.location.host;
+ const pathname = document.location.pathname;
+ const isHttps = document.location.protocol === 'https:';
+ const match = host.match(/^(.+):(\d+)$/);
+ const defaultPort = isHttps ? 443 : 80;
+ const port = match ? parseInt(match[2], 10) : defaultPort;
+ const hostName = match ? match[1] : host;
+ const wsProto = isHttps ? "wss:" : "ws:";
+ const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
+ ws = new WebSocket(wsUrl);
+ ws.binaryType = "arraybuffer";
+ ws.addEventListener('message', onWebSocketMessage, false);
+ ws.addEventListener('error', timeoutThenCreateNew, false);
+ ws.addEventListener('close', timeoutThenCreateNew, false);
+ ws.addEventListener('open', onWebSocketOpen, false);
+ }
+
+ function onWebSocketOpen() {
+ //console.log("web socket opened");
+ }
+
+ function onWebSocketMessage(ev) {
+ wasmOnMessage(ev.data);
+ }
+
+ function timeoutThenCreateNew() {
+ ws.removeEventListener('message', onWebSocketMessage, false);
+ ws.removeEventListener('error', timeoutThenCreateNew, false);
+ ws.removeEventListener('close', timeoutThenCreateNew, false);
+ ws.removeEventListener('open', onWebSocketOpen, false);
+ ws = null;
+ setTimeout(connectWebSocket, 1000);
+ }
+
+ function wasmOnMessage(data) {
+ const jsArray = new Uint8Array(data);
+ const ptr = wasm_exports.message_begin(jsArray.length);
+ const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
+ wasmArray.set(jsArray);
+ wasm_exports.message_end();
+ }
+
+ function onSourceIndexChange() {
+ render();
+ if (curNavLocation != null) renderSource(curNavLocation);
+ }
+
+ function onCoverageUpdate() {
+ renderStats();
+ renderCoverage();
+ }
+
+ function render() {
+ domStatus.classList.add("hidden");
+ }
+
+ function renderStats() {
+ const totalRuns = wasm_exports.totalRuns();
+ const uniqueRuns = wasm_exports.uniqueRuns();
+ const totalSourceLocations = wasm_exports.totalSourceLocations();
+ const coveredSourceLocations = wasm_exports.coveredSourceLocations();
+ domStatTotalRuns.innerText = totalRuns;
+ domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)";
+ domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
+ domStatSpeed.innerText = wasm_exports.totalRunsPerSecond().toFixed(0);
+
+ const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
+ resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
+ for (let i = 0; i < entryPoints.length; i += 1) {
+ const liDom = domEntryPointsList.children[i];
+ liDom.innerHTML = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i]));
+ }
+
+
+ domSectStats.classList.remove("hidden");
+ }
+
+ function renderCoverage() {
+ if (curNavLocation == null) return;
+ const sourceLocationIndex = curNavLocation;
+
+ for (let i = 0; i < domSourceText.children.length; i += 1) {
+ const childDom = domSourceText.children[i];
+ if (childDom.id != null && childDom.id[0] == "l") {
+ childDom.classList.add("l");
+ childDom.classList.remove("c");
+ }
+ }
+ const coveredList = unwrapInt32Array(wasm_exports.sourceLocationFileCoveredList(sourceLocationIndex));
+ for (let i = 0; i < coveredList.length; i += 1) {
+ document.getElementById("l" + coveredList[i]).classList.add("c");
+ }
+ }
+
+ function resizeDomList(listDom, desiredLen, templateHtml) {
+ for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
+ listDom.insertAdjacentHTML('beforeend', templateHtml);
+ }
+ while (desiredLen < listDom.childElementCount) {
+ listDom.removeChild(listDom.lastChild);
+ }
+ }
+
+ function percent(a, b) {
+ return ((Number(a) / Number(b)) * 100).toFixed(1);
+ }
+
+ function renderSource(sourceLocationIndex) {
+ const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex));
+ if (pathName.length === 0) return;
+
+ const h2 = domSectSource.children[0];
+ h2.innerText = pathName;
+ domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex));
+
+ domSectSource.classList.remove("hidden");
+
+ // Empirically, Firefox needs this requestAnimationFrame in order for the scrollIntoView to work.
+ requestAnimationFrame(function() {
+ const slDom = document.getElementById("l" + sourceLocationIndex);
+ if (slDom != null) slDom.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
+ });
+ }
+
+ function decodeString(ptr, len) {
+ if (len === 0) return "";
+ return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
+ }
+
+ function unwrapInt32Array(bigint) {
+ const ptr = Number(bigint & 0xffffffffn);
+ const len = Number(bigint >> 32n);
+ if (len === 0) return new Uint32Array();
+ return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
+ }
+
+ function setInputString(s) {
+ const jsArray = text_encoder.encode(s);
+ const len = jsArray.length;
+ const ptr = wasm_exports.set_input_string(len);
+ const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
+ wasmArray.set(jsArray);
+ }
+
+ function unwrapString(bigint) {
+ const ptr = Number(bigint & 0xffffffffn);
+ const len = Number(bigint >> 32n);
+ return decodeString(ptr, len);
+ }
+})();
diff --git a/lib/fuzzer/web/main.zig b/lib/fuzzer/web/main.zig
@@ -0,0 +1,455 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const abi = std.Build.Fuzz.abi;
+const gpa = std.heap.wasm_allocator;
+const log = std.log;
+const Coverage = std.debug.Coverage;
+const Allocator = std.mem.Allocator;
+
+const Walk = @import("Walk");
+const Decl = Walk.Decl;
+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 {
+ extern "js" fn log(ptr: [*]const u8, len: usize) void;
+ extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
+ extern "js" fn timestamp() i64;
+ extern "js" fn emitSourceIndexChange() void;
+ extern "js" fn emitCoverageUpdate() void;
+ extern "js" fn emitEntryPointsUpdate() void;
+};
+
+pub const std_options: std.Options = .{
+ .logFn = logFn,
+};
+
+pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
+ _ = st;
+ _ = addr;
+ log.err("panic: {s}", .{msg});
+ @trap();
+}
+
+fn logFn(
+ comptime message_level: log.Level,
+ comptime scope: @TypeOf(.enum_literal),
+ comptime format: []const u8,
+ args: anytype,
+) void {
+ const level_txt = comptime message_level.asText();
+ const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
+ var buf: [500]u8 = undefined;
+ const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
+ buf[buf.len - 3 ..][0..3].* = "...".*;
+ break :l &buf;
+ };
+ js.log(line.ptr, line.len);
+}
+
+export fn alloc(n: usize) [*]u8 {
+ const slice = gpa.alloc(u8, n) catch @panic("OOM");
+ return slice.ptr;
+}
+
+var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
+
+/// Resizes the message buffer to be the correct length; returns the pointer to
+/// the query string.
+export fn message_begin(len: usize) [*]u8 {
+ message_buffer.resize(gpa, len) catch @panic("OOM");
+ return message_buffer.items.ptr;
+}
+
+export fn message_end() void {
+ const msg_bytes = message_buffer.items;
+
+ const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
+ switch (tag) {
+ .current_time => return currentTimeMessage(msg_bytes),
+ .source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
+ .coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
+ .entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
+ _ => unreachable,
+ }
+}
+
+export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
+ const tar_bytes = tar_ptr[0..tar_len];
+ log.debug("received {d} bytes of tar file", .{tar_bytes.len});
+
+ unpackInner(tar_bytes) catch |err| {
+ fatal("unable to unpack tar: {s}", .{@errorName(err)});
+ };
+}
+
+/// Set by `set_input_string`.
+var input_string: std.ArrayListUnmanaged(u8) = .{};
+var string_result: std.ArrayListUnmanaged(u8) = .{};
+
+export fn set_input_string(len: usize) [*]u8 {
+ input_string.resize(gpa, len) catch @panic("OOM");
+ return input_string.items.ptr;
+}
+
+/// Looks up the root struct decl corresponding to a file by path.
+/// Uses `input_string`.
+export fn find_file_root() Decl.Index {
+ const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
+ return file.findRootDecl();
+}
+
+export fn decl_source_html(decl_index: Decl.Index) String {
+ const decl = decl_index.get();
+
+ string_result.clearRetainingCapacity();
+ html_render.fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
+ fatal("unable to render source: {s}", .{@errorName(err)});
+ };
+ return String.init(string_result.items);
+}
+
+export fn totalSourceLocations() usize {
+ return coverage_source_locations.items.len;
+}
+
+export fn coveredSourceLocations() usize {
+ const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..];
+ var count: usize = 0;
+ for (covered_bits) |byte| count += @popCount(byte);
+ return count;
+}
+
+fn getCoverageUpdateHeader() *abi.CoverageUpdateHeader {
+ return @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
+}
+
+export fn totalRuns() u64 {
+ const header = getCoverageUpdateHeader();
+ return header.n_runs;
+}
+
+export fn uniqueRuns() u64 {
+ const header = getCoverageUpdateHeader();
+ 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);
+
+fn Slice(T: type) type {
+ return packed struct(u64) {
+ ptr: u32,
+ len: u32,
+
+ fn init(s: []const T) @This() {
+ return .{
+ .ptr = @intFromPtr(s.ptr),
+ .len = s.len,
+ };
+ }
+ };
+}
+
+fn unpackInner(tar_bytes: []u8) !void {
+ var fbs = std.io.fixedBufferStream(tar_bytes);
+ var file_name_buffer: [1024]u8 = undefined;
+ var link_name_buffer: [1024]u8 = undefined;
+ var it = std.tar.iterator(fbs.reader(), .{
+ .file_name_buffer = &file_name_buffer,
+ .link_name_buffer = &link_name_buffer,
+ });
+ while (try it.next()) |tar_file| {
+ switch (tar_file.kind) {
+ .file => {
+ if (tar_file.size == 0 and tar_file.name.len == 0) break;
+ if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
+ log.debug("found file: '{s}'", .{tar_file.name});
+ const file_name = try gpa.dupe(u8, tar_file.name);
+ if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
+ const pkg_name = file_name[0..pkg_name_end];
+ const gop = try Walk.modules.getOrPut(gpa, pkg_name);
+ const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
+ if (!gop.found_existing or
+ std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
+ std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
+ {
+ gop.value_ptr.* = file;
+ }
+ const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
+ assert(file == try Walk.add_file(file_name, file_bytes));
+ }
+ } else {
+ log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
+ }
+ },
+ else => continue,
+ }
+ }
+}
+
+fn fatal(comptime format: []const u8, args: anytype) noreturn {
+ var buf: [500]u8 = undefined;
+ const line = std.fmt.bufPrint(&buf, format, args) catch l: {
+ buf[buf.len - 3 ..][0..3].* = "...".*;
+ break :l &buf;
+ };
+ 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 {
+ const Header = abi.SourceIndexHeader;
+ const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
+
+ const directories_start = @sizeOf(Header);
+ const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String);
+ const files_start = directories_end;
+ const files_end = files_start + header.files_len * @sizeOf(Coverage.File);
+ const source_locations_start = files_end;
+ const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation);
+ const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len];
+
+ const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_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]));
+
+ start_fuzzing_timestamp = header.start_timestamp;
+ try updateCoverage(directories, files, source_locations, string_bytes);
+ js.emitSourceIndexChange();
+}
+
+fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
+ recent_coverage_update.clearRetainingCapacity();
+ recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM");
+ js.emitCoverageUpdate();
+}
+
+var entry_points: std.ArrayListUnmanaged(u32) = .{};
+
+fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void {
+ const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*);
+ entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM");
+ @memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..]));
+ js.emitEntryPointsUpdate();
+}
+
+export fn entryPoints() Slice(u32) {
+ return Slice(u32).init(entry_points.items);
+}
+
+/// Index into `coverage_source_locations`.
+const SourceLocationIndex = enum(u32) {
+ _,
+
+ fn haveCoverage(sli: SourceLocationIndex) bool {
+ return @intFromEnum(sli) < coverage_source_locations.items.len;
+ }
+
+ fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation {
+ return &coverage_source_locations.items[@intFromEnum(sli)];
+ }
+
+ fn sourceLocationLinkHtml(
+ sli: SourceLocationIndex,
+ out: *std.ArrayListUnmanaged(u8),
+ ) Allocator.Error!void {
+ const sl = sli.ptr();
+ try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)});
+ try sli.appendPath(out);
+ try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column });
+ }
+
+ fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void {
+ const sl = sli.ptr();
+ const file = coverage.fileAt(sl.file);
+ const file_name = coverage.stringAt(file.basename);
+ const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]);
+ try html_render.appendEscaped(out, dir_name);
+ try out.appendSlice(gpa, "/");
+ try html_render.appendEscaped(out, file_name);
+ }
+
+ fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index {
+ var buf: std.ArrayListUnmanaged(u8) = .{};
+ defer buf.deinit(gpa);
+ sli.appendPath(&buf) catch @panic("OOM");
+ return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null);
+ }
+
+ fn fileHtml(
+ sli: SourceLocationIndex,
+ out: *std.ArrayListUnmanaged(u8),
+ ) error{ OutOfMemory, SourceUnavailable }!void {
+ const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable;
+ const root_node = walk_file_index.findRootDecl().get().ast_node;
+ var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{};
+ defer annotations.deinit(gpa);
+ try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items);
+ html_render.fileSourceHtml(walk_file_index, out, root_node, .{
+ .source_location_annotations = annotations.items,
+ }) catch |err| {
+ fatal("unable to render source: {s}", .{@errorName(err)});
+ };
+ }
+};
+
+fn computeSourceAnnotations(
+ cov_file_index: Coverage.File.Index,
+ walk_file_index: Walk.File.Index,
+ annotations: *std.ArrayListUnmanaged(html_render.Annotation),
+ source_locations: []const Coverage.SourceLocation,
+) !void {
+ // Collect all the source locations from only this file into this array
+ // first, then sort by line, col, so that we can collect annotations with
+ // O(N) time complexity.
+ var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
+ defer locs.deinit(gpa);
+
+ for (source_locations, 0..) |sl, sli_usize| {
+ if (sl.file != cov_file_index) continue;
+ const sli: SourceLocationIndex = @enumFromInt(sli_usize);
+ try locs.append(gpa, sli);
+ }
+
+ std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct {
+ pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool {
+ _ = context;
+ const lhs_ptr = lhs.ptr();
+ const rhs_ptr = rhs.ptr();
+ if (lhs_ptr.line < rhs_ptr.line) return true;
+ if (lhs_ptr.line > rhs_ptr.line) return false;
+ return lhs_ptr.column < rhs_ptr.column;
+ }
+ }.lessThan);
+
+ const source = walk_file_index.get_ast().source;
+ var line: usize = 1;
+ var column: usize = 1;
+ var next_loc_index: usize = 0;
+ for (source, 0..) |byte, offset| {
+ if (byte == '\n') {
+ line += 1;
+ column = 1;
+ } else {
+ column += 1;
+ }
+ while (true) {
+ if (next_loc_index >= locs.items.len) return;
+ const next_sli = locs.items[next_loc_index];
+ const next_sl = next_sli.ptr();
+ if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break;
+ try annotations.append(gpa, .{
+ .file_byte_offset = offset,
+ .dom_id = @intFromEnum(next_sli),
+ });
+ next_loc_index += 1;
+ }
+ }
+}
+
+var coverage = Coverage.init;
+/// Index of type `SourceLocationIndex`.
+var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
+/// Contains the most recent coverage update message, unmodified.
+var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
+
+fn updateCoverage(
+ directories: []const Coverage.String,
+ files: []const Coverage.File,
+ source_locations: []const Coverage.SourceLocation,
+ string_bytes: []const u8,
+) !void {
+ coverage.directories.clearRetainingCapacity();
+ coverage.files.clearRetainingCapacity();
+ coverage.string_bytes.clearRetainingCapacity();
+ coverage_source_locations.clearRetainingCapacity();
+
+ try coverage_source_locations.appendSlice(gpa, source_locations);
+ try coverage.string_bytes.appendSlice(gpa, string_bytes);
+
+ try coverage.files.entries.resize(gpa, files.len);
+ @memcpy(coverage.files.entries.items(.key), files);
+ try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
+
+ try coverage.directories.entries.resize(gpa, directories.len);
+ @memcpy(coverage.directories.entries.items(.key), directories);
+ try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
+}
+
+export fn sourceLocationLinkHtml(index: SourceLocationIndex) String {
+ string_result.clearRetainingCapacity();
+ index.sourceLocationLinkHtml(&string_result) catch @panic("OOM");
+ return String.init(string_result.items);
+}
+
+/// Returns empty string if coverage metadata is not available for this source location.
+export fn sourceLocationPath(sli: SourceLocationIndex) String {
+ string_result.clearRetainingCapacity();
+ if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM");
+ return String.init(string_result.items);
+}
+
+export fn sourceLocationFileHtml(sli: SourceLocationIndex) String {
+ string_result.clearRetainingCapacity();
+ sli.fileHtml(&string_result) catch |err| switch (err) {
+ error.OutOfMemory => @panic("OOM"),
+ error.SourceUnavailable => {},
+ };
+ return String.init(string_result.items);
+}
+
+export fn sourceLocationFileCoveredList(sli_file: SourceLocationIndex) Slice(SourceLocationIndex) {
+ const global = struct {
+ var result: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
+ fn add(i: u32, want_file: Coverage.File.Index) void {
+ const src_loc_index: SourceLocationIndex = @enumFromInt(i);
+ if (src_loc_index.ptr().file == want_file) result.appendAssumeCapacity(src_loc_index);
+ }
+ };
+ const want_file = sli_file.ptr().file;
+ global.result.clearRetainingCapacity();
+
+ // This code assumes 64-bit elements, which is incorrect if the executable
+ // being fuzzed is not a 64-bit CPU. It also assumes little-endian which
+ // can also be incorrect.
+ comptime assert(abi.CoverageUpdateHeader.trailing[0] == .pc_bits_usize);
+ const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64);
+ const covered_bits = std.mem.bytesAsSlice(
+ u64,
+ recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)],
+ );
+ var sli: u32 = 0;
+ for (covered_bits) |elem| {
+ global.result.ensureUnusedCapacity(gpa, 64) catch @panic("OOM");
+ for (0..@bitSizeOf(u64)) |i| {
+ if ((elem & (@as(u64, 1) << @intCast(i))) != 0) global.add(sli, want_file);
+ sli += 1;
+ }
+ }
+ return Slice(SourceLocationIndex).init(global.result.items);
+}
diff --git a/lib/init/src/main.zig b/lib/init/src/main.zig
@@ -27,7 +27,11 @@ test "simple test" {
}
test "fuzz example" {
- // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
- const input_bytes = std.testing.fuzzInput(.{});
- try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input_bytes));
+ const global = struct {
+ fn testOne(input: []const u8) anyerror!void {
+ // 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, .{});
}
diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig
@@ -66,6 +66,8 @@ pub fn start(
.coverage_files = .{},
.coverage_mutex = .{},
.coverage_condition = .{},
+
+ .base_timestamp = std.time.nanoTimestamp(),
};
// For accepting HTTP connections.
diff --git a/lib/std/Build/Fuzz/WebServer.zig b/lib/std/Build/Fuzz/WebServer.zig
@@ -33,6 +33,9 @@ coverage_mutex: std.Thread.Mutex,
/// Signaled when `coverage_files` changes.
coverage_condition: std.Thread.Condition,
+/// Time at initialization of WebServer.
+base_timestamp: i128,
+
const fuzzer_bin_name = "fuzzer";
const fuzzer_arch_os_abi = "wasm32-freestanding";
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,
/// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
entry_points: std.ArrayListUnmanaged(u32),
+ start_timestamp: i64,
fn deinit(cm: *CoverageMap, gpa: Allocator) void {
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 {
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/"))
{
- 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
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")) {
try serveWasm(ws, request, .ReleaseFast);
} 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 = .{
.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 = .{
.root_dir = ws.zig_lib_directory,
@@ -381,6 +389,13 @@ fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void {
ws.coverage_mutex.lock();
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
// so that subsequent updates can contain only the updated bits.
var prev_unique_runs: usize = 0;
@@ -406,7 +421,6 @@ fn sendCoverageContext(
const seen_pcs = cov_header.seenBits();
const n_runs = @atomicLoad(usize, &cov_header.n_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) {
// There has been an update.
if (prev_unique_runs.* == 0) {
@@ -417,6 +431,7 @@ fn sendCoverageContext(
.files_len = @intCast(coverage_map.coverage.files.entries.len),
.source_locations_len = @intCast(coverage_map.source_locations.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 = .{
makeIov(std.mem.asBytes(&header)),
@@ -431,7 +446,6 @@ fn sendCoverageContext(
const header: abi.CoverageUpdateHeader = .{
.n_runs = n_runs,
.unique_runs = unique_runs,
- .lowest_stack = lowest_stack,
};
const iovecs: [2]std.posix.iovec_const = .{
makeIov(std.mem.asBytes(&header)),
@@ -584,6 +598,7 @@ fn prepareTables(
.mapped_memory = undefined, // populated below
.source_locations = undefined, // populated below
.entry_points = .{},
+ .start_timestamp = ws.now(),
};
errdefer gop.value_ptr.coverage.deinit(gpa);
diff --git a/lib/std/Build/Fuzz/abi.zig b/lib/std/Build/Fuzz/abi.zig
@@ -13,7 +13,6 @@ pub const SeenPcsHeader = extern struct {
n_runs: usize,
unique_runs: usize,
pcs_len: usize,
- lowest_stack: usize,
/// Used for comptime assertions. Provides a mechanism for strategically
/// causing compile errors.
@@ -44,12 +43,19 @@ pub const SeenPcsHeader = extern struct {
};
pub const ToClientTag = enum(u8) {
+ current_time,
source_index,
coverage_update,
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.
///
/// Trailing:
@@ -63,6 +69,8 @@ pub const SourceIndexHeader = extern struct {
files_len: u32,
source_locations_len: u32,
string_bytes_len: u32,
+ /// When, according to the server, fuzzing started.
+ start_timestamp: i64 align(4),
pub const Flags = packed struct(u32) {
tag: ToClientTag = .source_index,
@@ -79,7 +87,6 @@ pub const CoverageUpdateHeader = extern struct {
flags: Flags = .{},
n_runs: u64,
unique_runs: u64,
- lowest_stack: u64,
pub const Flags = packed struct(u64) {
tag: ToClientTag = .coverage_update,
diff --git a/lib/std/testing.zig b/lib/std/testing.zig
@@ -1141,6 +1141,10 @@ pub const FuzzInputOptions = struct {
corpus: []const []const u8 = &.{},
};
-pub inline fn fuzzInput(options: FuzzInputOptions) []const u8 {
- return @import("root").fuzzInput(options);
+/// Inline to avoid coverage instrumentation.
+pub inline fn fuzz(
+ comptime testOne: fn (input: []const u8) anyerror!void,
+ options: FuzzInputOptions,
+) anyerror!void {
+ return @import("root").fuzz(testOne, options);
}
diff --git a/lib/std/zig/tokenizer.zig b/lib/std/zig/tokenizer.zig
@@ -1708,6 +1708,10 @@ test "invalid tabs and carriage returns" {
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 {
var tokenizer = Tokenizer.init(source);
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);
}
-test "fuzzable properties upheld" {
- const source = std.testing.fuzzInput(.{});
+fn testPropertiesUpheld(source: []const u8) anyerror!void {
const source0 = try std.testing.allocator.dupeZ(u8, source);
defer std.testing.allocator.free(source0);
var tokenizer = Tokenizer.init(source0);