zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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:
Mlib/compiler/test_runner.zig | 109++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mlib/fuzzer.zig | 89+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Dlib/fuzzer/index.html | 161-------------------------------------------------------------------------------
Dlib/fuzzer/main.js | 249-------------------------------------------------------------------------------
Dlib/fuzzer/wasm/main.zig | 428-------------------------------------------------------------------------------
Alib/fuzzer/web/index.html | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/fuzzer/web/main.js | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/fuzzer/web/main.zig | 455+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/init/src/main.zig | 10+++++++---
Mlib/std/Build/Fuzz.zig | 2++
Mlib/std/Build/Fuzz/WebServer.zig | 25++++++++++++++++++++-----
Mlib/std/Build/Fuzz/abi.zig | 11+++++++++--
Mlib/std/testing.zig | 8++++++--
Mlib/std/zig/tokenizer.zig | 7+++++--
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(&timestamp_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);