authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-09-11 23:41:51-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-09-11 23:41:51-07:00
logeccd06f5d01d05286691bc77e6d1e582bb14b7b1
treecacf35cfc07672e30623672a003efed8019db7b7
parent4fba7336a9038b4abf647caf822f89df717d3cc0
parente3f58bd5515ffd0039c7f5afde8b9d74dc5a24b5
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #21370 from ziglang/fuzz

rework fuzzing API to accept a function pointer parameter

14 files changed, 1042 insertions(+), 925 deletions(-)

lib/compiler/test_runner.zig+77-32
......@@ -145,31 +145,23 @@ fn mainServer() !void {
145145 .start_fuzzing => {
146146 if (!builtin.fuzz) unreachable;
147147 const index = try server.receiveBody_u32();
148 var first = true;
149148 const test_fn = builtin.test_functions[index];
150 while (true) {
151 testing.allocator_instance = .{};
152 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
153 log_err_count = 0;
154 is_fuzz_test = false;
155 test_fn.func() catch |err| switch (err) {
156 error.SkipZigTest => continue,
157 else => {
158 if (@errorReturnTrace()) |trace| {
159 std.debug.dumpStackTrace(trace.*);
160 }
161 std.debug.print("failed with error.{s}\n", .{@errorName(err)});
162 std.process.exit(1);
163 },
164 };
165 if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput");
166 if (log_err_count != 0) @panic("error logs detected");
167 if (first) {
168 first = false;
169 const entry_addr = @intFromPtr(test_fn.func);
170 try server.serveU64Message(.fuzz_start_addr, entry_addr);
171 }
172 }
149 const entry_addr = @intFromPtr(test_fn.func);
150 try server.serveU64Message(.fuzz_start_addr, entry_addr);
151 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
152 is_fuzz_test = false;
153 test_fn.func() catch |err| switch (err) {
154 error.SkipZigTest => return,
155 else => {
156 if (@errorReturnTrace()) |trace| {
157 std.debug.dumpStackTrace(trace.*);
158 }
159 std.debug.print("failed with error.{s}\n", .{@errorName(err)});
160 std.process.exit(1);
161 },
162 };
163 if (!is_fuzz_test) @panic("missed call to std.testing.fuzz");
164 if (log_err_count != 0) @panic("error logs detected");
173165 },
174166
175167 else => {
......@@ -349,19 +341,72 @@ const FuzzerSlice = extern struct {
349341
350342var is_fuzz_test: bool = undefined;
351343
352extern fn fuzzer_next() FuzzerSlice;
344extern fn fuzzer_start(testOne: *const fn ([*]const u8, usize) callconv(.C) void) void;
353345extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
354346extern fn fuzzer_coverage_id() u64;
355347
356pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {
348pub fn fuzz(
349 comptime testOne: fn ([]const u8) anyerror!void,
350 options: testing.FuzzInputOptions,
351) anyerror!void {
352 // Prevent this function from confusing the fuzzer by omitting its own code
353 // coverage from being considered.
357354 @disableInstrumentation();
358 if (crippled) return "";
355
356 // Some compiler backends are not capable of handling fuzz testing yet but
357 // we still want CI test coverage enabled.
358 if (crippled) return;
359
360 // Smoke test to ensure the test did not use conditional compilation to
361 // contradict itself by making it not actually be a fuzz test when the test
362 // is built in fuzz mode.
359363 is_fuzz_test = true;
364
365 // Ensure no test failure occurred before starting fuzzing.
366 if (log_err_count != 0) @panic("error logs detected");
367
368 // libfuzzer is in a separate compilation unit so that its own code can be
369 // excluded from code coverage instrumentation. It needs a function pointer
370 // it can call for checking exactly one input. Inside this function we do
371 // our standard unit test checks such as memory leaks, and interaction with
372 // error logs.
373 const global = struct {
374 fn fuzzer_one(input_ptr: [*]const u8, input_len: usize) callconv(.C) void {
375 @disableInstrumentation();
376 testing.allocator_instance = .{};
377 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
378 log_err_count = 0;
379 testOne(input_ptr[0..input_len]) catch |err| switch (err) {
380 error.SkipZigTest => return,
381 else => {
382 std.debug.lockStdErr();
383 if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace.*);
384 std.debug.print("failed with error.{s}\n", .{@errorName(err)});
385 std.process.exit(1);
386 },
387 };
388 if (log_err_count != 0) {
389 std.debug.lockStdErr();
390 std.debug.print("error logs detected\n", .{});
391 std.process.exit(1);
392 }
393 }
394 };
360395 if (builtin.fuzz) {
361 return fuzzer_next().toSlice();
396 const prev_allocator_state = testing.allocator_instance;
397 testing.allocator_instance = .{};
398 fuzzer_start(&global.fuzzer_one);
399 testing.allocator_instance = prev_allocator_state;
400 return;
362401 }
363 if (options.corpus.len == 0) return "";
364 var prng = std.Random.DefaultPrng.init(testing.random_seed);
365 const random = prng.random();
366 return options.corpus[random.uintLessThan(usize, options.corpus.len)];
402
403 // When the unit test executable is not built in fuzz mode, only run the
404 // provided corpus.
405 for (options.corpus) |input| {
406 try testOne(input);
407 }
408
409 // In case there is no provided corpus, also use an empty
410 // string as a smoke test.
411 try testOne("");
367412}
lib/fuzzer.zig+48-41
......@@ -28,7 +28,8 @@ fn logOverride(
2828 f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log");
2929}
3030
31export threadlocal var __sancov_lowest_stack: usize = std.math.maxInt(usize);
31/// Helps determine run uniqueness in the face of recursion.
32export threadlocal var __sancov_lowest_stack: usize = 0;
3233
3334export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void {
3435 handleCmp(@returnAddress(), arg1, arg2);
......@@ -220,7 +221,6 @@ const Fuzzer = struct {
220221 .n_runs = 0,
221222 .unique_runs = 0,
222223 .pcs_len = pcs.len,
223 .lowest_stack = std.math.maxInt(usize),
224224 };
225225 f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header));
226226 f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems * @sizeOf(usize));
......@@ -235,22 +235,41 @@ const Fuzzer = struct {
235235 };
236236 }
237237
238 fn next(f: *Fuzzer) ![]const u8 {
238 fn start(f: *Fuzzer) !void {
239239 const gpa = f.gpa;
240240 const rng = fuzzer.rng.random();
241241
242 if (f.recent_cases.entries.len == 0) {
243 // Prepare initial input.
244 try f.recent_cases.ensureUnusedCapacity(gpa, 100);
245 const len = rng.uintLessThanBiased(usize, 80);
246 try f.input.resize(gpa, len);
247 rng.bytes(f.input.items);
248 f.recent_cases.putAssumeCapacity(.{
249 .id = 0,
250 .input = try gpa.dupe(u8, f.input.items),
251 .score = 0,
252 }, {});
253 } else {
242 // Prepare initial input.
243 assert(f.recent_cases.entries.len == 0);
244 assert(f.n_runs == 0);
245 try f.recent_cases.ensureUnusedCapacity(gpa, 100);
246 const len = rng.uintLessThanBiased(usize, 80);
247 try f.input.resize(gpa, len);
248 rng.bytes(f.input.items);
249 f.recent_cases.putAssumeCapacity(.{
250 .id = 0,
251 .input = try gpa.dupe(u8, f.input.items),
252 .score = 0,
253 }, {});
254
255 const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
256
257 while (true) {
258 const chosen_index = rng.uintLessThanBiased(usize, f.recent_cases.entries.len);
259 const run = &f.recent_cases.keys()[chosen_index];
260 f.input.clearRetainingCapacity();
261 f.input.appendSliceAssumeCapacity(run.input);
262 try f.mutate();
263
264 @memset(f.pc_counters, 0);
265 __sancov_lowest_stack = std.math.maxInt(usize);
266 f.coverage.reset();
267
268 fuzzer_one(f.input.items.ptr, f.input.items.len);
269
270 f.n_runs += 1;
271 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
272
254273 if (f.n_runs % 10000 == 0) f.dumpStats();
255274
256275 const analysis = f.analyzeLastRun();
......@@ -301,7 +320,6 @@ const Fuzzer = struct {
301320 }
302321 }
303322
304 const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
305323 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
306324 }
307325
......@@ -317,26 +335,12 @@ const Fuzzer = struct {
317335 // This has to be done before deinitializing the deleted items.
318336 const doomed_runs = f.recent_cases.keys()[cap..];
319337 f.recent_cases.shrinkRetainingCapacity(cap);
320 for (doomed_runs) |*run| {
321 std.log.info("culling score={d} id={d}", .{ run.score, run.id });
322 run.deinit(gpa);
338 for (doomed_runs) |*doomed_run| {
339 std.log.info("culling score={d} id={d}", .{ doomed_run.score, doomed_run.id });
340 doomed_run.deinit(gpa);
323341 }
324342 }
325343 }
326
327 const chosen_index = rng.uintLessThanBiased(usize, f.recent_cases.entries.len);
328 const run = &f.recent_cases.keys()[chosen_index];
329 f.input.clearRetainingCapacity();
330 f.input.appendSliceAssumeCapacity(run.input);
331 try f.mutate();
332
333 f.n_runs += 1;
334 const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
335 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
336 _ = @atomicRmw(usize, &header.lowest_stack, .Min, __sancov_lowest_stack, .monotonic);
337 @memset(f.pc_counters, 0);
338 f.coverage.reset();
339 return f.input.items;
340344 }
341345
342346 fn visitPc(f: *Fuzzer, pc: usize) void {
......@@ -419,10 +423,13 @@ export fn fuzzer_coverage_id() u64 {
419423 return fuzzer.coverage_id;
420424}
421425
422export fn fuzzer_next() Fuzzer.Slice {
423 return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) {
424 error.OutOfMemory => @panic("out of memory"),
425 });
426var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C) void = undefined;
427
428export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void {
429 fuzzer_one = testOne;
430 fuzzer.start() catch |err| switch (err) {
431 error.OutOfMemory => fatal("out of memory", .{}),
432 };
426433}
427434
428435export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
......@@ -432,24 +439,24 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
432439 const pc_counters_start = @extern([*]u8, .{
433440 .name = "__start___sancov_cntrs",
434441 .linkage = .weak,
435 }) orelse fatal("missing __start___sancov_cntrs symbol");
442 }) orelse fatal("missing __start___sancov_cntrs symbol", .{});
436443
437444 const pc_counters_end = @extern([*]u8, .{
438445 .name = "__stop___sancov_cntrs",
439446 .linkage = .weak,
440 }) orelse fatal("missing __stop___sancov_cntrs symbol");
447 }) orelse fatal("missing __stop___sancov_cntrs symbol", .{});
441448
442449 const pc_counters = pc_counters_start[0 .. pc_counters_end - pc_counters_start];
443450
444451 const pcs_start = @extern([*]usize, .{
445452 .name = "__start___sancov_pcs1",
446453 .linkage = .weak,
447 }) orelse fatal("missing __start___sancov_pcs1 symbol");
454 }) orelse fatal("missing __start___sancov_pcs1 symbol", .{});
448455
449456 const pcs_end = @extern([*]usize, .{
450457 .name = "__stop___sancov_pcs1",
451458 .linkage = .weak,
452 }) orelse fatal("missing __stop___sancov_pcs1 symbol");
459 }) orelse fatal("missing __stop___sancov_pcs1 symbol", .{});
453460
454461 const pcs = pcs_start[0 .. pcs_end - pcs_start];
455462
lib/fuzzer/index.html deleted-161
......@@ -1,161 +0,0 @@
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Zig Build System Interface</title>
6 <style type="text/css">
7 body {
8 font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
9 color: #000000;
10 }
11 .hidden {
12 display: none;
13 }
14 table {
15 width: 100%;
16 }
17 a {
18 color: #2A6286;
19 }
20 pre{
21 font-family:"Source Code Pro",monospace;
22 font-size:1em;
23 background-color:#F5F5F5;
24 padding: 1em;
25 margin: 0;
26 overflow-x: auto;
27 }
28 :not(pre) > code {
29 white-space: break-spaces;
30 }
31 code {
32 font-family:"Source Code Pro",monospace;
33 font-size: 0.9em;
34 }
35 code a {
36 color: #000000;
37 }
38 kbd {
39 color: #000;
40 background-color: #fafbfc;
41 border-color: #d1d5da;
42 border-bottom-color: #c6cbd1;
43 box-shadow-color: #c6cbd1;
44 display: inline-block;
45 padding: 0.3em 0.2em;
46 font: 1.2em monospace;
47 line-height: 0.8em;
48 vertical-align: middle;
49 border: solid 1px;
50 border-radius: 3px;
51 box-shadow: inset 0 -1px 0;
52 cursor: default;
53 }
54
55 .l {
56 display: inline-block;
57 background: red;
58 width: 1em;
59 height: 1em;
60 border-radius: 1em;
61 }
62 .c {
63 background-color: green;
64 }
65
66 .tok-kw {
67 color: #333;
68 font-weight: bold;
69 }
70 .tok-str {
71 color: #d14;
72 }
73 .tok-builtin {
74 color: #0086b3;
75 }
76 .tok-comment {
77 color: #777;
78 font-style: italic;
79 }
80 .tok-fn {
81 color: #900;
82 font-weight: bold;
83 }
84 .tok-null {
85 color: #008080;
86 }
87 .tok-number {
88 color: #008080;
89 }
90 .tok-type {
91 color: #458;
92 font-weight: bold;
93 }
94
95 @media (prefers-color-scheme: dark) {
96 body {
97 background-color: #111;
98 color: #bbb;
99 }
100 pre {
101 background-color: #222;
102 color: #ccc;
103 }
104 a {
105 color: #88f;
106 }
107 code a {
108 color: #ccc;
109 }
110 .l {
111 background-color: red;
112 }
113 .c {
114 background-color: green;
115 }
116 .tok-kw {
117 color: #eee;
118 }
119 .tok-str {
120 color: #2e5;
121 }
122 .tok-builtin {
123 color: #ff894c;
124 }
125 .tok-comment {
126 color: #aa7;
127 }
128 .tok-fn {
129 color: #B1A0F8;
130 }
131 .tok-null {
132 color: #ff8080;
133 }
134 .tok-number {
135 color: #ff8080;
136 }
137 .tok-type {
138 color: #68f;
139 }
140 }
141 </style>
142 </head>
143 <body>
144 <p id="status">Loading JavaScript...</p>
145 <div id="sectStats" class="hidden">
146 <ul>
147 <li>Total Runs: <span id="statTotalRuns"></span></li>
148 <li>Unique Runs: <span id="statUniqueRuns"></span></li>
149 <li>Coverage: <span id="statCoverage"></span></li>
150 <li>Lowest Stack: <span id="statLowestStack"></span></li>
151 <li>Entry Points: <ul id="entryPointsList"></ul></li>
152 </ul>
153 </div>
154 <div id="sectSource" class="hidden">
155 <h2>Source Code</h2>
156 <pre><code id="sourceText"></code></pre>
157 </div>
158 <script src="main.js"></script>
159 </body>
160</html>
161
lib/fuzzer/main.js deleted-249
......@@ -1,249 +0,0 @@
1(function() {
2 const domStatus = document.getElementById("status");
3 const domSectSource = document.getElementById("sectSource");
4 const domSectStats = document.getElementById("sectStats");
5 const domSourceText = document.getElementById("sourceText");
6 const domStatTotalRuns = document.getElementById("statTotalRuns");
7 const domStatUniqueRuns = document.getElementById("statUniqueRuns");
8 const domStatCoverage = document.getElementById("statCoverage");
9 const domStatLowestStack = document.getElementById("statLowestStack");
10 const domEntryPointsList = document.getElementById("entryPointsList");
11
12 let wasm_promise = fetch("main.wasm");
13 let sources_promise = fetch("sources.tar").then(function(response) {
14 if (!response.ok) throw new Error("unable to download sources");
15 return response.arrayBuffer();
16 });
17 var wasm_exports = null;
18 var curNavSearch = null;
19 var curNavLocation = null;
20
21 const text_decoder = new TextDecoder();
22 const text_encoder = new TextEncoder();
23
24 domStatus.textContent = "Loading WebAssembly...";
25 WebAssembly.instantiateStreaming(wasm_promise, {
26 js: {
27 log: function(ptr, len) {
28 const msg = decodeString(ptr, len);
29 console.log(msg);
30 },
31 panic: function (ptr, len) {
32 const msg = decodeString(ptr, len);
33 throw new Error("panic: " + msg);
34 },
35 emitSourceIndexChange: onSourceIndexChange,
36 emitCoverageUpdate: onCoverageUpdate,
37 emitEntryPointsUpdate: renderStats,
38 },
39 }).then(function(obj) {
40 wasm_exports = obj.instance.exports;
41 window.wasm = obj; // for debugging
42 domStatus.textContent = "Loading sources tarball...";
43
44 sources_promise.then(function(buffer) {
45 domStatus.textContent = "Parsing sources...";
46 const js_array = new Uint8Array(buffer);
47 const ptr = wasm_exports.alloc(js_array.length);
48 const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
49 wasm_array.set(js_array);
50 wasm_exports.unpack(ptr, js_array.length);
51
52 window.addEventListener('popstate', onPopState, false);
53 onHashChange(null);
54
55 domStatus.textContent = "Waiting for server to send source location metadata...";
56 connectWebSocket();
57 });
58 });
59
60 function onPopState(ev) {
61 onHashChange(ev.state);
62 }
63
64 function onHashChange(state) {
65 history.replaceState({}, "");
66 navigate(location.hash);
67 if (state == null) window.scrollTo({top: 0});
68 }
69
70 function navigate(location_hash) {
71 domSectSource.classList.add("hidden");
72
73 curNavLocation = null;
74 curNavSearch = null;
75
76 if (location_hash.length > 1 && location_hash[0] === '#') {
77 const query = location_hash.substring(1);
78 const qpos = query.indexOf("?");
79 let nonSearchPart;
80 if (qpos === -1) {
81 nonSearchPart = query;
82 } else {
83 nonSearchPart = query.substring(0, qpos);
84 curNavSearch = decodeURIComponent(query.substring(qpos + 1));
85 }
86
87 if (nonSearchPart[0] == "l") {
88 curNavLocation = +nonSearchPart.substring(1);
89 renderSource(curNavLocation);
90 }
91 }
92
93 render();
94 }
95
96 function connectWebSocket() {
97 const host = document.location.host;
98 const pathname = document.location.pathname;
99 const isHttps = document.location.protocol === 'https:';
100 const match = host.match(/^(.+):(\d+)$/);
101 const defaultPort = isHttps ? 443 : 80;
102 const port = match ? parseInt(match[2], 10) : defaultPort;
103 const hostName = match ? match[1] : host;
104 const wsProto = isHttps ? "wss:" : "ws:";
105 const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
106 ws = new WebSocket(wsUrl);
107 ws.binaryType = "arraybuffer";
108 ws.addEventListener('message', onWebSocketMessage, false);
109 ws.addEventListener('error', timeoutThenCreateNew, false);
110 ws.addEventListener('close', timeoutThenCreateNew, false);
111 ws.addEventListener('open', onWebSocketOpen, false);
112 }
113
114 function onWebSocketOpen() {
115 //console.log("web socket opened");
116 }
117
118 function onWebSocketMessage(ev) {
119 wasmOnMessage(ev.data);
120 }
121
122 function timeoutThenCreateNew() {
123 ws.removeEventListener('message', onWebSocketMessage, false);
124 ws.removeEventListener('error', timeoutThenCreateNew, false);
125 ws.removeEventListener('close', timeoutThenCreateNew, false);
126 ws.removeEventListener('open', onWebSocketOpen, false);
127 ws = null;
128 setTimeout(connectWebSocket, 1000);
129 }
130
131 function wasmOnMessage(data) {
132 const jsArray = new Uint8Array(data);
133 const ptr = wasm_exports.message_begin(jsArray.length);
134 const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
135 wasmArray.set(jsArray);
136 wasm_exports.message_end();
137 }
138
139 function onSourceIndexChange() {
140 render();
141 if (curNavLocation != null) renderSource(curNavLocation);
142 }
143
144 function onCoverageUpdate() {
145 renderStats();
146 renderCoverage();
147 }
148
149 function render() {
150 domStatus.classList.add("hidden");
151 }
152
153 function renderStats() {
154 const totalRuns = wasm_exports.totalRuns();
155 const uniqueRuns = wasm_exports.uniqueRuns();
156 const totalSourceLocations = wasm_exports.totalSourceLocations();
157 const coveredSourceLocations = wasm_exports.coveredSourceLocations();
158 domStatTotalRuns.innerText = totalRuns;
159 domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)";
160 domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
161 domStatLowestStack.innerText = unwrapString(wasm_exports.lowestStack());
162
163 const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
164 resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
165 for (let i = 0; i < entryPoints.length; i += 1) {
166 const liDom = domEntryPointsList.children[i];
167 liDom.innerHTML = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i]));
168 }
169
170
171 domSectStats.classList.remove("hidden");
172 }
173
174 function renderCoverage() {
175 if (curNavLocation == null) return;
176 const sourceLocationIndex = curNavLocation;
177
178 for (let i = 0; i < domSourceText.children.length; i += 1) {
179 const childDom = domSourceText.children[i];
180 if (childDom.id != null && childDom.id[0] == "l") {
181 childDom.classList.add("l");
182 childDom.classList.remove("c");
183 }
184 }
185 const coveredList = unwrapInt32Array(wasm_exports.sourceLocationFileCoveredList(sourceLocationIndex));
186 for (let i = 0; i < coveredList.length; i += 1) {
187 document.getElementById("l" + coveredList[i]).classList.add("c");
188 }
189 }
190
191 function resizeDomList(listDom, desiredLen, templateHtml) {
192 for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
193 listDom.insertAdjacentHTML('beforeend', templateHtml);
194 }
195 while (desiredLen < listDom.childElementCount) {
196 listDom.removeChild(listDom.lastChild);
197 }
198 }
199
200 function percent(a, b) {
201 return ((Number(a) / Number(b)) * 100).toFixed(1);
202 }
203
204 function renderSource(sourceLocationIndex) {
205 const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex));
206 if (pathName.length === 0) return;
207
208 const h2 = domSectSource.children[0];
209 h2.innerText = pathName;
210 domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex));
211
212 domSectSource.classList.remove("hidden");
213
214 // Empirically, Firefox needs this requestAnimationFrame in order for the scrollIntoView to work.
215 requestAnimationFrame(function() {
216 const slDom = document.getElementById("l" + sourceLocationIndex);
217 if (slDom != null) slDom.scrollIntoView({
218 behavior: "smooth",
219 block: "center",
220 });
221 });
222 }
223
224 function decodeString(ptr, len) {
225 if (len === 0) return "";
226 return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
227 }
228
229 function unwrapInt32Array(bigint) {
230 const ptr = Number(bigint & 0xffffffffn);
231 const len = Number(bigint >> 32n);
232 if (len === 0) return new Uint32Array();
233 return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
234 }
235
236 function setInputString(s) {
237 const jsArray = text_encoder.encode(s);
238 const len = jsArray.length;
239 const ptr = wasm_exports.set_input_string(len);
240 const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
241 wasmArray.set(jsArray);
242 }
243
244 function unwrapString(bigint) {
245 const ptr = Number(bigint & 0xffffffffn);
246 const len = Number(bigint >> 32n);
247 return decodeString(ptr, len);
248 }
249})();
lib/fuzzer/wasm/main.zig deleted-428
......@@ -1,428 +0,0 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const abi = std.Build.Fuzz.abi;
4const gpa = std.heap.wasm_allocator;
5const log = std.log;
6const Coverage = std.debug.Coverage;
7const Allocator = std.mem.Allocator;
8
9const Walk = @import("Walk");
10const Decl = Walk.Decl;
11const html_render = @import("html_render");
12
13const js = struct {
14 extern "js" fn log(ptr: [*]const u8, len: usize) void;
15 extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
16 extern "js" fn emitSourceIndexChange() void;
17 extern "js" fn emitCoverageUpdate() void;
18 extern "js" fn emitEntryPointsUpdate() void;
19};
20
21pub const std_options: std.Options = .{
22 .logFn = logFn,
23};
24
25pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
26 _ = st;
27 _ = addr;
28 log.err("panic: {s}", .{msg});
29 @trap();
30}
31
32fn logFn(
33 comptime message_level: log.Level,
34 comptime scope: @TypeOf(.enum_literal),
35 comptime format: []const u8,
36 args: anytype,
37) void {
38 const level_txt = comptime message_level.asText();
39 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
40 var buf: [500]u8 = undefined;
41 const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
42 buf[buf.len - 3 ..][0..3].* = "...".*;
43 break :l &buf;
44 };
45 js.log(line.ptr, line.len);
46}
47
48export fn alloc(n: usize) [*]u8 {
49 const slice = gpa.alloc(u8, n) catch @panic("OOM");
50 return slice.ptr;
51}
52
53var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
54
55/// Resizes the message buffer to be the correct length; returns the pointer to
56/// the query string.
57export fn message_begin(len: usize) [*]u8 {
58 message_buffer.resize(gpa, len) catch @panic("OOM");
59 return message_buffer.items.ptr;
60}
61
62export fn message_end() void {
63 const msg_bytes = message_buffer.items;
64
65 const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
66 switch (tag) {
67 .source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
68 .coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
69 .entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
70 _ => unreachable,
71 }
72}
73
74export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
75 const tar_bytes = tar_ptr[0..tar_len];
76 log.debug("received {d} bytes of tar file", .{tar_bytes.len});
77
78 unpackInner(tar_bytes) catch |err| {
79 fatal("unable to unpack tar: {s}", .{@errorName(err)});
80 };
81}
82
83/// Set by `set_input_string`.
84var input_string: std.ArrayListUnmanaged(u8) = .{};
85var string_result: std.ArrayListUnmanaged(u8) = .{};
86
87export fn set_input_string(len: usize) [*]u8 {
88 input_string.resize(gpa, len) catch @panic("OOM");
89 return input_string.items.ptr;
90}
91
92/// Looks up the root struct decl corresponding to a file by path.
93/// Uses `input_string`.
94export fn find_file_root() Decl.Index {
95 const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
96 return file.findRootDecl();
97}
98
99export fn decl_source_html(decl_index: Decl.Index) String {
100 const decl = decl_index.get();
101
102 string_result.clearRetainingCapacity();
103 html_render.fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
104 fatal("unable to render source: {s}", .{@errorName(err)});
105 };
106 return String.init(string_result.items);
107}
108
109export fn lowestStack() String {
110 const header: *abi.CoverageUpdateHeader = @ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]);
111 string_result.clearRetainingCapacity();
112 string_result.writer(gpa).print("0x{d}", .{header.lowest_stack}) catch @panic("OOM");
113 return String.init(string_result.items);
114}
115
116export fn totalSourceLocations() usize {
117 return coverage_source_locations.items.len;
118}
119
120export fn coveredSourceLocations() usize {
121 const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..];
122 var count: usize = 0;
123 for (covered_bits) |byte| count += @popCount(byte);
124 return count;
125}
126
127export fn totalRuns() u64 {
128 const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
129 return header.n_runs;
130}
131
132export fn uniqueRuns() u64 {
133 const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
134 return header.unique_runs;
135}
136
137const String = Slice(u8);
138
139fn Slice(T: type) type {
140 return packed struct(u64) {
141 ptr: u32,
142 len: u32,
143
144 fn init(s: []const T) @This() {
145 return .{
146 .ptr = @intFromPtr(s.ptr),
147 .len = s.len,
148 };
149 }
150 };
151}
152
153fn unpackInner(tar_bytes: []u8) !void {
154 var fbs = std.io.fixedBufferStream(tar_bytes);
155 var file_name_buffer: [1024]u8 = undefined;
156 var link_name_buffer: [1024]u8 = undefined;
157 var it = std.tar.iterator(fbs.reader(), .{
158 .file_name_buffer = &file_name_buffer,
159 .link_name_buffer = &link_name_buffer,
160 });
161 while (try it.next()) |tar_file| {
162 switch (tar_file.kind) {
163 .file => {
164 if (tar_file.size == 0 and tar_file.name.len == 0) break;
165 if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
166 log.debug("found file: '{s}'", .{tar_file.name});
167 const file_name = try gpa.dupe(u8, tar_file.name);
168 if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
169 const pkg_name = file_name[0..pkg_name_end];
170 const gop = try Walk.modules.getOrPut(gpa, pkg_name);
171 const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
172 if (!gop.found_existing or
173 std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
174 std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
175 {
176 gop.value_ptr.* = file;
177 }
178 const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
179 assert(file == try Walk.add_file(file_name, file_bytes));
180 }
181 } else {
182 log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
183 }
184 },
185 else => continue,
186 }
187 }
188}
189
190fn fatal(comptime format: []const u8, args: anytype) noreturn {
191 var buf: [500]u8 = undefined;
192 const line = std.fmt.bufPrint(&buf, format, args) catch l: {
193 buf[buf.len - 3 ..][0..3].* = "...".*;
194 break :l &buf;
195 };
196 js.panic(line.ptr, line.len);
197}
198
199fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
200 const Header = abi.SourceIndexHeader;
201 const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
202
203 const directories_start = @sizeOf(Header);
204 const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String);
205 const files_start = directories_end;
206 const files_end = files_start + header.files_len * @sizeOf(Coverage.File);
207 const source_locations_start = files_end;
208 const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation);
209 const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len];
210
211 const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end]));
212 const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end]));
213 const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end]));
214
215 try updateCoverage(directories, files, source_locations, string_bytes);
216 js.emitSourceIndexChange();
217}
218
219fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
220 recent_coverage_update.clearRetainingCapacity();
221 recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM");
222 js.emitCoverageUpdate();
223}
224
225var entry_points: std.ArrayListUnmanaged(u32) = .{};
226
227fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void {
228 const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*);
229 entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM");
230 @memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..]));
231 js.emitEntryPointsUpdate();
232}
233
234export fn entryPoints() Slice(u32) {
235 return Slice(u32).init(entry_points.items);
236}
237
238/// Index into `coverage_source_locations`.
239const SourceLocationIndex = enum(u32) {
240 _,
241
242 fn haveCoverage(sli: SourceLocationIndex) bool {
243 return @intFromEnum(sli) < coverage_source_locations.items.len;
244 }
245
246 fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation {
247 return &coverage_source_locations.items[@intFromEnum(sli)];
248 }
249
250 fn sourceLocationLinkHtml(
251 sli: SourceLocationIndex,
252 out: *std.ArrayListUnmanaged(u8),
253 ) Allocator.Error!void {
254 const sl = sli.ptr();
255 try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)});
256 try sli.appendPath(out);
257 try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column });
258 }
259
260 fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void {
261 const sl = sli.ptr();
262 const file = coverage.fileAt(sl.file);
263 const file_name = coverage.stringAt(file.basename);
264 const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]);
265 try html_render.appendEscaped(out, dir_name);
266 try out.appendSlice(gpa, "/");
267 try html_render.appendEscaped(out, file_name);
268 }
269
270 fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index {
271 var buf: std.ArrayListUnmanaged(u8) = .{};
272 defer buf.deinit(gpa);
273 sli.appendPath(&buf) catch @panic("OOM");
274 return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null);
275 }
276
277 fn fileHtml(
278 sli: SourceLocationIndex,
279 out: *std.ArrayListUnmanaged(u8),
280 ) error{ OutOfMemory, SourceUnavailable }!void {
281 const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable;
282 const root_node = walk_file_index.findRootDecl().get().ast_node;
283 var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{};
284 defer annotations.deinit(gpa);
285 try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items);
286 html_render.fileSourceHtml(walk_file_index, out, root_node, .{
287 .source_location_annotations = annotations.items,
288 }) catch |err| {
289 fatal("unable to render source: {s}", .{@errorName(err)});
290 };
291 }
292};
293
294fn computeSourceAnnotations(
295 cov_file_index: Coverage.File.Index,
296 walk_file_index: Walk.File.Index,
297 annotations: *std.ArrayListUnmanaged(html_render.Annotation),
298 source_locations: []const Coverage.SourceLocation,
299) !void {
300 // Collect all the source locations from only this file into this array
301 // first, then sort by line, col, so that we can collect annotations with
302 // O(N) time complexity.
303 var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
304 defer locs.deinit(gpa);
305
306 for (source_locations, 0..) |sl, sli_usize| {
307 if (sl.file != cov_file_index) continue;
308 const sli: SourceLocationIndex = @enumFromInt(sli_usize);
309 try locs.append(gpa, sli);
310 }
311
312 std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct {
313 pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool {
314 _ = context;
315 const lhs_ptr = lhs.ptr();
316 const rhs_ptr = rhs.ptr();
317 if (lhs_ptr.line < rhs_ptr.line) return true;
318 if (lhs_ptr.line > rhs_ptr.line) return false;
319 return lhs_ptr.column < rhs_ptr.column;
320 }
321 }.lessThan);
322
323 const source = walk_file_index.get_ast().source;
324 var line: usize = 1;
325 var column: usize = 1;
326 var next_loc_index: usize = 0;
327 for (source, 0..) |byte, offset| {
328 if (byte == '\n') {
329 line += 1;
330 column = 1;
331 } else {
332 column += 1;
333 }
334 while (true) {
335 if (next_loc_index >= locs.items.len) return;
336 const next_sli = locs.items[next_loc_index];
337 const next_sl = next_sli.ptr();
338 if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break;
339 try annotations.append(gpa, .{
340 .file_byte_offset = offset,
341 .dom_id = @intFromEnum(next_sli),
342 });
343 next_loc_index += 1;
344 }
345 }
346}
347
348var coverage = Coverage.init;
349/// Index of type `SourceLocationIndex`.
350var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
351/// Contains the most recent coverage update message, unmodified.
352var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
353
354fn updateCoverage(
355 directories: []const Coverage.String,
356 files: []const Coverage.File,
357 source_locations: []const Coverage.SourceLocation,
358 string_bytes: []const u8,
359) !void {
360 coverage.directories.clearRetainingCapacity();
361 coverage.files.clearRetainingCapacity();
362 coverage.string_bytes.clearRetainingCapacity();
363 coverage_source_locations.clearRetainingCapacity();
364
365 try coverage_source_locations.appendSlice(gpa, source_locations);
366 try coverage.string_bytes.appendSlice(gpa, string_bytes);
367
368 try coverage.files.entries.resize(gpa, files.len);
369 @memcpy(coverage.files.entries.items(.key), files);
370 try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
371
372 try coverage.directories.entries.resize(gpa, directories.len);
373 @memcpy(coverage.directories.entries.items(.key), directories);
374 try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
375}
376
377export fn sourceLocationLinkHtml(index: SourceLocationIndex) String {
378 string_result.clearRetainingCapacity();
379 index.sourceLocationLinkHtml(&string_result) catch @panic("OOM");
380 return String.init(string_result.items);
381}
382
383/// Returns empty string if coverage metadata is not available for this source location.
384export fn sourceLocationPath(sli: SourceLocationIndex) String {
385 string_result.clearRetainingCapacity();
386 if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM");
387 return String.init(string_result.items);
388}
389
390export fn sourceLocationFileHtml(sli: SourceLocationIndex) String {
391 string_result.clearRetainingCapacity();
392 sli.fileHtml(&string_result) catch |err| switch (err) {
393 error.OutOfMemory => @panic("OOM"),
394 error.SourceUnavailable => {},
395 };
396 return String.init(string_result.items);
397}
398
399export fn sourceLocationFileCoveredList(sli_file: SourceLocationIndex) Slice(SourceLocationIndex) {
400 const global = struct {
401 var result: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
402 fn add(i: u32, want_file: Coverage.File.Index) void {
403 const src_loc_index: SourceLocationIndex = @enumFromInt(i);
404 if (src_loc_index.ptr().file == want_file) result.appendAssumeCapacity(src_loc_index);
405 }
406 };
407 const want_file = sli_file.ptr().file;
408 global.result.clearRetainingCapacity();
409
410 // This code assumes 64-bit elements, which is incorrect if the executable
411 // being fuzzed is not a 64-bit CPU. It also assumes little-endian which
412 // can also be incorrect.
413 comptime assert(abi.CoverageUpdateHeader.trailing[0] == .pc_bits_usize);
414 const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64);
415 const covered_bits = std.mem.bytesAsSlice(
416 u64,
417 recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)],
418 );
419 var sli: u32 = 0;
420 for (covered_bits) |elem| {
421 global.result.ensureUnusedCapacity(gpa, 64) catch @panic("OOM");
422 for (0..@bitSizeOf(u64)) |i| {
423 if ((elem & (@as(u64, 1) << @intCast(i))) != 0) global.add(sli, want_file);
424 sli += 1;
425 }
426 }
427 return Slice(SourceLocationIndex).init(global.result.items);
428}
lib/fuzzer/web/index.html created+161
......@@ -0,0 +1,161 @@
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Zig Build System Interface</title>
6 <style type="text/css">
7 body {
8 font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
9 color: #000000;
10 }
11 .hidden {
12 display: none;
13 }
14 table {
15 width: 100%;
16 }
17 a {
18 color: #2A6286;
19 }
20 pre{
21 font-family:"Source Code Pro",monospace;
22 font-size:1em;
23 background-color:#F5F5F5;
24 padding: 1em;
25 margin: 0;
26 overflow-x: auto;
27 }
28 :not(pre) > code {
29 white-space: break-spaces;
30 }
31 code {
32 font-family:"Source Code Pro",monospace;
33 font-size: 0.9em;
34 }
35 code a {
36 color: #000000;
37 }
38 kbd {
39 color: #000;
40 background-color: #fafbfc;
41 border-color: #d1d5da;
42 border-bottom-color: #c6cbd1;
43 box-shadow-color: #c6cbd1;
44 display: inline-block;
45 padding: 0.3em 0.2em;
46 font: 1.2em monospace;
47 line-height: 0.8em;
48 vertical-align: middle;
49 border: solid 1px;
50 border-radius: 3px;
51 box-shadow: inset 0 -1px 0;
52 cursor: default;
53 }
54
55 .l {
56 display: inline-block;
57 background: red;
58 width: 1em;
59 height: 1em;
60 border-radius: 1em;
61 }
62 .c {
63 background-color: green;
64 }
65
66 .tok-kw {
67 color: #333;
68 font-weight: bold;
69 }
70 .tok-str {
71 color: #d14;
72 }
73 .tok-builtin {
74 color: #0086b3;
75 }
76 .tok-comment {
77 color: #777;
78 font-style: italic;
79 }
80 .tok-fn {
81 color: #900;
82 font-weight: bold;
83 }
84 .tok-null {
85 color: #008080;
86 }
87 .tok-number {
88 color: #008080;
89 }
90 .tok-type {
91 color: #458;
92 font-weight: bold;
93 }
94
95 @media (prefers-color-scheme: dark) {
96 body {
97 background-color: #111;
98 color: #bbb;
99 }
100 pre {
101 background-color: #222;
102 color: #ccc;
103 }
104 a {
105 color: #88f;
106 }
107 code a {
108 color: #ccc;
109 }
110 .l {
111 background-color: red;
112 }
113 .c {
114 background-color: green;
115 }
116 .tok-kw {
117 color: #eee;
118 }
119 .tok-str {
120 color: #2e5;
121 }
122 .tok-builtin {
123 color: #ff894c;
124 }
125 .tok-comment {
126 color: #aa7;
127 }
128 .tok-fn {
129 color: #B1A0F8;
130 }
131 .tok-null {
132 color: #ff8080;
133 }
134 .tok-number {
135 color: #ff8080;
136 }
137 .tok-type {
138 color: #68f;
139 }
140 }
141 </style>
142 </head>
143 <body>
144 <p id="status">Loading JavaScript...</p>
145 <div id="sectStats" class="hidden">
146 <ul>
147 <li>Total Runs: <span id="statTotalRuns"></span></li>
148 <li>Unique Runs: <span id="statUniqueRuns"></span></li>
149 <li>Speed (Runs/Second): <span id="statSpeed"></span></li>
150 <li>Coverage: <span id="statCoverage"></span></li>
151 <li>Entry Points: <ul id="entryPointsList"></ul></li>
152 </ul>
153 </div>
154 <div id="sectSource" class="hidden">
155 <h2>Source Code</h2>
156 <pre><code id="sourceText"></code></pre>
157 </div>
158 <script src="main.js"></script>
159 </body>
160</html>
161
lib/fuzzer/web/main.js created+252
......@@ -0,0 +1,252 @@
1(function() {
2 const domStatus = document.getElementById("status");
3 const domSectSource = document.getElementById("sectSource");
4 const domSectStats = document.getElementById("sectStats");
5 const domSourceText = document.getElementById("sourceText");
6 const domStatTotalRuns = document.getElementById("statTotalRuns");
7 const domStatUniqueRuns = document.getElementById("statUniqueRuns");
8 const domStatSpeed = document.getElementById("statSpeed");
9 const domStatCoverage = document.getElementById("statCoverage");
10 const domEntryPointsList = document.getElementById("entryPointsList");
11
12 let wasm_promise = fetch("main.wasm");
13 let sources_promise = fetch("sources.tar").then(function(response) {
14 if (!response.ok) throw new Error("unable to download sources");
15 return response.arrayBuffer();
16 });
17 var wasm_exports = null;
18 var curNavSearch = null;
19 var curNavLocation = null;
20
21 const text_decoder = new TextDecoder();
22 const text_encoder = new TextEncoder();
23
24 domStatus.textContent = "Loading WebAssembly...";
25 WebAssembly.instantiateStreaming(wasm_promise, {
26 js: {
27 log: function(ptr, len) {
28 const msg = decodeString(ptr, len);
29 console.log(msg);
30 },
31 panic: function (ptr, len) {
32 const msg = decodeString(ptr, len);
33 throw new Error("panic: " + msg);
34 },
35 timestamp: function () {
36 return BigInt(new Date());
37 },
38 emitSourceIndexChange: onSourceIndexChange,
39 emitCoverageUpdate: onCoverageUpdate,
40 emitEntryPointsUpdate: renderStats,
41 },
42 }).then(function(obj) {
43 wasm_exports = obj.instance.exports;
44 window.wasm = obj; // for debugging
45 domStatus.textContent = "Loading sources tarball...";
46
47 sources_promise.then(function(buffer) {
48 domStatus.textContent = "Parsing sources...";
49 const js_array = new Uint8Array(buffer);
50 const ptr = wasm_exports.alloc(js_array.length);
51 const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
52 wasm_array.set(js_array);
53 wasm_exports.unpack(ptr, js_array.length);
54
55 window.addEventListener('popstate', onPopState, false);
56 onHashChange(null);
57
58 domStatus.textContent = "Waiting for server to send source location metadata...";
59 connectWebSocket();
60 });
61 });
62
63 function onPopState(ev) {
64 onHashChange(ev.state);
65 }
66
67 function onHashChange(state) {
68 history.replaceState({}, "");
69 navigate(location.hash);
70 if (state == null) window.scrollTo({top: 0});
71 }
72
73 function navigate(location_hash) {
74 domSectSource.classList.add("hidden");
75
76 curNavLocation = null;
77 curNavSearch = null;
78
79 if (location_hash.length > 1 && location_hash[0] === '#') {
80 const query = location_hash.substring(1);
81 const qpos = query.indexOf("?");
82 let nonSearchPart;
83 if (qpos === -1) {
84 nonSearchPart = query;
85 } else {
86 nonSearchPart = query.substring(0, qpos);
87 curNavSearch = decodeURIComponent(query.substring(qpos + 1));
88 }
89
90 if (nonSearchPart[0] == "l") {
91 curNavLocation = +nonSearchPart.substring(1);
92 renderSource(curNavLocation);
93 }
94 }
95
96 render();
97 }
98
99 function connectWebSocket() {
100 const host = document.location.host;
101 const pathname = document.location.pathname;
102 const isHttps = document.location.protocol === 'https:';
103 const match = host.match(/^(.+):(\d+)$/);
104 const defaultPort = isHttps ? 443 : 80;
105 const port = match ? parseInt(match[2], 10) : defaultPort;
106 const hostName = match ? match[1] : host;
107 const wsProto = isHttps ? "wss:" : "ws:";
108 const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
109 ws = new WebSocket(wsUrl);
110 ws.binaryType = "arraybuffer";
111 ws.addEventListener('message', onWebSocketMessage, false);
112 ws.addEventListener('error', timeoutThenCreateNew, false);
113 ws.addEventListener('close', timeoutThenCreateNew, false);
114 ws.addEventListener('open', onWebSocketOpen, false);
115 }
116
117 function onWebSocketOpen() {
118 //console.log("web socket opened");
119 }
120
121 function onWebSocketMessage(ev) {
122 wasmOnMessage(ev.data);
123 }
124
125 function timeoutThenCreateNew() {
126 ws.removeEventListener('message', onWebSocketMessage, false);
127 ws.removeEventListener('error', timeoutThenCreateNew, false);
128 ws.removeEventListener('close', timeoutThenCreateNew, false);
129 ws.removeEventListener('open', onWebSocketOpen, false);
130 ws = null;
131 setTimeout(connectWebSocket, 1000);
132 }
133
134 function wasmOnMessage(data) {
135 const jsArray = new Uint8Array(data);
136 const ptr = wasm_exports.message_begin(jsArray.length);
137 const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
138 wasmArray.set(jsArray);
139 wasm_exports.message_end();
140 }
141
142 function onSourceIndexChange() {
143 render();
144 if (curNavLocation != null) renderSource(curNavLocation);
145 }
146
147 function onCoverageUpdate() {
148 renderStats();
149 renderCoverage();
150 }
151
152 function render() {
153 domStatus.classList.add("hidden");
154 }
155
156 function renderStats() {
157 const totalRuns = wasm_exports.totalRuns();
158 const uniqueRuns = wasm_exports.uniqueRuns();
159 const totalSourceLocations = wasm_exports.totalSourceLocations();
160 const coveredSourceLocations = wasm_exports.coveredSourceLocations();
161 domStatTotalRuns.innerText = totalRuns;
162 domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)";
163 domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
164 domStatSpeed.innerText = wasm_exports.totalRunsPerSecond().toFixed(0);
165
166 const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
167 resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
168 for (let i = 0; i < entryPoints.length; i += 1) {
169 const liDom = domEntryPointsList.children[i];
170 liDom.innerHTML = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i]));
171 }
172
173
174 domSectStats.classList.remove("hidden");
175 }
176
177 function renderCoverage() {
178 if (curNavLocation == null) return;
179 const sourceLocationIndex = curNavLocation;
180
181 for (let i = 0; i < domSourceText.children.length; i += 1) {
182 const childDom = domSourceText.children[i];
183 if (childDom.id != null && childDom.id[0] == "l") {
184 childDom.classList.add("l");
185 childDom.classList.remove("c");
186 }
187 }
188 const coveredList = unwrapInt32Array(wasm_exports.sourceLocationFileCoveredList(sourceLocationIndex));
189 for (let i = 0; i < coveredList.length; i += 1) {
190 document.getElementById("l" + coveredList[i]).classList.add("c");
191 }
192 }
193
194 function resizeDomList(listDom, desiredLen, templateHtml) {
195 for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
196 listDom.insertAdjacentHTML('beforeend', templateHtml);
197 }
198 while (desiredLen < listDom.childElementCount) {
199 listDom.removeChild(listDom.lastChild);
200 }
201 }
202
203 function percent(a, b) {
204 return ((Number(a) / Number(b)) * 100).toFixed(1);
205 }
206
207 function renderSource(sourceLocationIndex) {
208 const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex));
209 if (pathName.length === 0) return;
210
211 const h2 = domSectSource.children[0];
212 h2.innerText = pathName;
213 domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex));
214
215 domSectSource.classList.remove("hidden");
216
217 // Empirically, Firefox needs this requestAnimationFrame in order for the scrollIntoView to work.
218 requestAnimationFrame(function() {
219 const slDom = document.getElementById("l" + sourceLocationIndex);
220 if (slDom != null) slDom.scrollIntoView({
221 behavior: "smooth",
222 block: "center",
223 });
224 });
225 }
226
227 function decodeString(ptr, len) {
228 if (len === 0) return "";
229 return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
230 }
231
232 function unwrapInt32Array(bigint) {
233 const ptr = Number(bigint & 0xffffffffn);
234 const len = Number(bigint >> 32n);
235 if (len === 0) return new Uint32Array();
236 return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
237 }
238
239 function setInputString(s) {
240 const jsArray = text_encoder.encode(s);
241 const len = jsArray.length;
242 const ptr = wasm_exports.set_input_string(len);
243 const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
244 wasmArray.set(jsArray);
245 }
246
247 function unwrapString(bigint) {
248 const ptr = Number(bigint & 0xffffffffn);
249 const len = Number(bigint >> 32n);
250 return decodeString(ptr, len);
251 }
252})();
lib/fuzzer/web/main.zig created+455
......@@ -0,0 +1,455 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const abi = std.Build.Fuzz.abi;
4const gpa = std.heap.wasm_allocator;
5const log = std.log;
6const Coverage = std.debug.Coverage;
7const Allocator = std.mem.Allocator;
8
9const Walk = @import("Walk");
10const Decl = Walk.Decl;
11const html_render = @import("html_render");
12
13/// Nanoseconds.
14var server_base_timestamp: i64 = 0;
15/// Milliseconds.
16var client_base_timestamp: i64 = 0;
17/// Relative to `server_base_timestamp`.
18var start_fuzzing_timestamp: i64 = undefined;
19
20const js = struct {
21 extern "js" fn log(ptr: [*]const u8, len: usize) void;
22 extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
23 extern "js" fn timestamp() i64;
24 extern "js" fn emitSourceIndexChange() void;
25 extern "js" fn emitCoverageUpdate() void;
26 extern "js" fn emitEntryPointsUpdate() void;
27};
28
29pub const std_options: std.Options = .{
30 .logFn = logFn,
31};
32
33pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
34 _ = st;
35 _ = addr;
36 log.err("panic: {s}", .{msg});
37 @trap();
38}
39
40fn logFn(
41 comptime message_level: log.Level,
42 comptime scope: @TypeOf(.enum_literal),
43 comptime format: []const u8,
44 args: anytype,
45) void {
46 const level_txt = comptime message_level.asText();
47 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
48 var buf: [500]u8 = undefined;
49 const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
50 buf[buf.len - 3 ..][0..3].* = "...".*;
51 break :l &buf;
52 };
53 js.log(line.ptr, line.len);
54}
55
56export fn alloc(n: usize) [*]u8 {
57 const slice = gpa.alloc(u8, n) catch @panic("OOM");
58 return slice.ptr;
59}
60
61var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
62
63/// Resizes the message buffer to be the correct length; returns the pointer to
64/// the query string.
65export fn message_begin(len: usize) [*]u8 {
66 message_buffer.resize(gpa, len) catch @panic("OOM");
67 return message_buffer.items.ptr;
68}
69
70export fn message_end() void {
71 const msg_bytes = message_buffer.items;
72
73 const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
74 switch (tag) {
75 .current_time => return currentTimeMessage(msg_bytes),
76 .source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
77 .coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
78 .entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
79 _ => unreachable,
80 }
81}
82
83export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
84 const tar_bytes = tar_ptr[0..tar_len];
85 log.debug("received {d} bytes of tar file", .{tar_bytes.len});
86
87 unpackInner(tar_bytes) catch |err| {
88 fatal("unable to unpack tar: {s}", .{@errorName(err)});
89 };
90}
91
92/// Set by `set_input_string`.
93var input_string: std.ArrayListUnmanaged(u8) = .{};
94var string_result: std.ArrayListUnmanaged(u8) = .{};
95
96export fn set_input_string(len: usize) [*]u8 {
97 input_string.resize(gpa, len) catch @panic("OOM");
98 return input_string.items.ptr;
99}
100
101/// Looks up the root struct decl corresponding to a file by path.
102/// Uses `input_string`.
103export fn find_file_root() Decl.Index {
104 const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
105 return file.findRootDecl();
106}
107
108export fn decl_source_html(decl_index: Decl.Index) String {
109 const decl = decl_index.get();
110
111 string_result.clearRetainingCapacity();
112 html_render.fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
113 fatal("unable to render source: {s}", .{@errorName(err)});
114 };
115 return String.init(string_result.items);
116}
117
118export fn totalSourceLocations() usize {
119 return coverage_source_locations.items.len;
120}
121
122export fn coveredSourceLocations() usize {
123 const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..];
124 var count: usize = 0;
125 for (covered_bits) |byte| count += @popCount(byte);
126 return count;
127}
128
129fn getCoverageUpdateHeader() *abi.CoverageUpdateHeader {
130 return @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
131}
132
133export fn totalRuns() u64 {
134 const header = getCoverageUpdateHeader();
135 return header.n_runs;
136}
137
138export fn uniqueRuns() u64 {
139 const header = getCoverageUpdateHeader();
140 return header.unique_runs;
141}
142
143export fn totalRunsPerSecond() f64 {
144 @setFloatMode(.optimized);
145 const header = getCoverageUpdateHeader();
146 const ns_elapsed: f64 = @floatFromInt(nsSince(start_fuzzing_timestamp));
147 const n_runs: f64 = @floatFromInt(header.n_runs);
148 return n_runs / (ns_elapsed / std.time.ns_per_s);
149}
150
151const String = Slice(u8);
152
153fn Slice(T: type) type {
154 return packed struct(u64) {
155 ptr: u32,
156 len: u32,
157
158 fn init(s: []const T) @This() {
159 return .{
160 .ptr = @intFromPtr(s.ptr),
161 .len = s.len,
162 };
163 }
164 };
165}
166
167fn unpackInner(tar_bytes: []u8) !void {
168 var fbs = std.io.fixedBufferStream(tar_bytes);
169 var file_name_buffer: [1024]u8 = undefined;
170 var link_name_buffer: [1024]u8 = undefined;
171 var it = std.tar.iterator(fbs.reader(), .{
172 .file_name_buffer = &file_name_buffer,
173 .link_name_buffer = &link_name_buffer,
174 });
175 while (try it.next()) |tar_file| {
176 switch (tar_file.kind) {
177 .file => {
178 if (tar_file.size == 0 and tar_file.name.len == 0) break;
179 if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
180 log.debug("found file: '{s}'", .{tar_file.name});
181 const file_name = try gpa.dupe(u8, tar_file.name);
182 if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
183 const pkg_name = file_name[0..pkg_name_end];
184 const gop = try Walk.modules.getOrPut(gpa, pkg_name);
185 const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
186 if (!gop.found_existing or
187 std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
188 std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
189 {
190 gop.value_ptr.* = file;
191 }
192 const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
193 assert(file == try Walk.add_file(file_name, file_bytes));
194 }
195 } else {
196 log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
197 }
198 },
199 else => continue,
200 }
201 }
202}
203
204fn fatal(comptime format: []const u8, args: anytype) noreturn {
205 var buf: [500]u8 = undefined;
206 const line = std.fmt.bufPrint(&buf, format, args) catch l: {
207 buf[buf.len - 3 ..][0..3].* = "...".*;
208 break :l &buf;
209 };
210 js.panic(line.ptr, line.len);
211}
212
213fn currentTimeMessage(msg_bytes: []u8) void {
214 client_base_timestamp = js.timestamp();
215 server_base_timestamp = @bitCast(msg_bytes[1..][0..8].*);
216}
217
218/// Nanoseconds passed since a server timestamp.
219fn nsSince(server_timestamp: i64) i64 {
220 const ms_passed = js.timestamp() - client_base_timestamp;
221 const ns_passed = server_base_timestamp - server_timestamp;
222 return ns_passed + ms_passed * std.time.ns_per_ms;
223}
224
225fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
226 const Header = abi.SourceIndexHeader;
227 const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
228
229 const directories_start = @sizeOf(Header);
230 const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String);
231 const files_start = directories_end;
232 const files_end = files_start + header.files_len * @sizeOf(Coverage.File);
233 const source_locations_start = files_end;
234 const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation);
235 const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len];
236
237 const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end]));
238 const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end]));
239 const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end]));
240
241 start_fuzzing_timestamp = header.start_timestamp;
242 try updateCoverage(directories, files, source_locations, string_bytes);
243 js.emitSourceIndexChange();
244}
245
246fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
247 recent_coverage_update.clearRetainingCapacity();
248 recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM");
249 js.emitCoverageUpdate();
250}
251
252var entry_points: std.ArrayListUnmanaged(u32) = .{};
253
254fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void {
255 const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*);
256 entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM");
257 @memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..]));
258 js.emitEntryPointsUpdate();
259}
260
261export fn entryPoints() Slice(u32) {
262 return Slice(u32).init(entry_points.items);
263}
264
265/// Index into `coverage_source_locations`.
266const SourceLocationIndex = enum(u32) {
267 _,
268
269 fn haveCoverage(sli: SourceLocationIndex) bool {
270 return @intFromEnum(sli) < coverage_source_locations.items.len;
271 }
272
273 fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation {
274 return &coverage_source_locations.items[@intFromEnum(sli)];
275 }
276
277 fn sourceLocationLinkHtml(
278 sli: SourceLocationIndex,
279 out: *std.ArrayListUnmanaged(u8),
280 ) Allocator.Error!void {
281 const sl = sli.ptr();
282 try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)});
283 try sli.appendPath(out);
284 try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column });
285 }
286
287 fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void {
288 const sl = sli.ptr();
289 const file = coverage.fileAt(sl.file);
290 const file_name = coverage.stringAt(file.basename);
291 const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]);
292 try html_render.appendEscaped(out, dir_name);
293 try out.appendSlice(gpa, "/");
294 try html_render.appendEscaped(out, file_name);
295 }
296
297 fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index {
298 var buf: std.ArrayListUnmanaged(u8) = .{};
299 defer buf.deinit(gpa);
300 sli.appendPath(&buf) catch @panic("OOM");
301 return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null);
302 }
303
304 fn fileHtml(
305 sli: SourceLocationIndex,
306 out: *std.ArrayListUnmanaged(u8),
307 ) error{ OutOfMemory, SourceUnavailable }!void {
308 const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable;
309 const root_node = walk_file_index.findRootDecl().get().ast_node;
310 var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{};
311 defer annotations.deinit(gpa);
312 try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items);
313 html_render.fileSourceHtml(walk_file_index, out, root_node, .{
314 .source_location_annotations = annotations.items,
315 }) catch |err| {
316 fatal("unable to render source: {s}", .{@errorName(err)});
317 };
318 }
319};
320
321fn computeSourceAnnotations(
322 cov_file_index: Coverage.File.Index,
323 walk_file_index: Walk.File.Index,
324 annotations: *std.ArrayListUnmanaged(html_render.Annotation),
325 source_locations: []const Coverage.SourceLocation,
326) !void {
327 // Collect all the source locations from only this file into this array
328 // first, then sort by line, col, so that we can collect annotations with
329 // O(N) time complexity.
330 var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
331 defer locs.deinit(gpa);
332
333 for (source_locations, 0..) |sl, sli_usize| {
334 if (sl.file != cov_file_index) continue;
335 const sli: SourceLocationIndex = @enumFromInt(sli_usize);
336 try locs.append(gpa, sli);
337 }
338
339 std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct {
340 pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool {
341 _ = context;
342 const lhs_ptr = lhs.ptr();
343 const rhs_ptr = rhs.ptr();
344 if (lhs_ptr.line < rhs_ptr.line) return true;
345 if (lhs_ptr.line > rhs_ptr.line) return false;
346 return lhs_ptr.column < rhs_ptr.column;
347 }
348 }.lessThan);
349
350 const source = walk_file_index.get_ast().source;
351 var line: usize = 1;
352 var column: usize = 1;
353 var next_loc_index: usize = 0;
354 for (source, 0..) |byte, offset| {
355 if (byte == '\n') {
356 line += 1;
357 column = 1;
358 } else {
359 column += 1;
360 }
361 while (true) {
362 if (next_loc_index >= locs.items.len) return;
363 const next_sli = locs.items[next_loc_index];
364 const next_sl = next_sli.ptr();
365 if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break;
366 try annotations.append(gpa, .{
367 .file_byte_offset = offset,
368 .dom_id = @intFromEnum(next_sli),
369 });
370 next_loc_index += 1;
371 }
372 }
373}
374
375var coverage = Coverage.init;
376/// Index of type `SourceLocationIndex`.
377var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
378/// Contains the most recent coverage update message, unmodified.
379var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
380
381fn updateCoverage(
382 directories: []const Coverage.String,
383 files: []const Coverage.File,
384 source_locations: []const Coverage.SourceLocation,
385 string_bytes: []const u8,
386) !void {
387 coverage.directories.clearRetainingCapacity();
388 coverage.files.clearRetainingCapacity();
389 coverage.string_bytes.clearRetainingCapacity();
390 coverage_source_locations.clearRetainingCapacity();
391
392 try coverage_source_locations.appendSlice(gpa, source_locations);
393 try coverage.string_bytes.appendSlice(gpa, string_bytes);
394
395 try coverage.files.entries.resize(gpa, files.len);
396 @memcpy(coverage.files.entries.items(.key), files);
397 try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
398
399 try coverage.directories.entries.resize(gpa, directories.len);
400 @memcpy(coverage.directories.entries.items(.key), directories);
401 try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
402}
403
404export fn sourceLocationLinkHtml(index: SourceLocationIndex) String {
405 string_result.clearRetainingCapacity();
406 index.sourceLocationLinkHtml(&string_result) catch @panic("OOM");
407 return String.init(string_result.items);
408}
409
410/// Returns empty string if coverage metadata is not available for this source location.
411export fn sourceLocationPath(sli: SourceLocationIndex) String {
412 string_result.clearRetainingCapacity();
413 if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM");
414 return String.init(string_result.items);
415}
416
417export fn sourceLocationFileHtml(sli: SourceLocationIndex) String {
418 string_result.clearRetainingCapacity();
419 sli.fileHtml(&string_result) catch |err| switch (err) {
420 error.OutOfMemory => @panic("OOM"),
421 error.SourceUnavailable => {},
422 };
423 return String.init(string_result.items);
424}
425
426export fn sourceLocationFileCoveredList(sli_file: SourceLocationIndex) Slice(SourceLocationIndex) {
427 const global = struct {
428 var result: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
429 fn add(i: u32, want_file: Coverage.File.Index) void {
430 const src_loc_index: SourceLocationIndex = @enumFromInt(i);
431 if (src_loc_index.ptr().file == want_file) result.appendAssumeCapacity(src_loc_index);
432 }
433 };
434 const want_file = sli_file.ptr().file;
435 global.result.clearRetainingCapacity();
436
437 // This code assumes 64-bit elements, which is incorrect if the executable
438 // being fuzzed is not a 64-bit CPU. It also assumes little-endian which
439 // can also be incorrect.
440 comptime assert(abi.CoverageUpdateHeader.trailing[0] == .pc_bits_usize);
441 const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64);
442 const covered_bits = std.mem.bytesAsSlice(
443 u64,
444 recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)],
445 );
446 var sli: u32 = 0;
447 for (covered_bits) |elem| {
448 global.result.ensureUnusedCapacity(gpa, 64) catch @panic("OOM");
449 for (0..@bitSizeOf(u64)) |i| {
450 if ((elem & (@as(u64, 1) << @intCast(i))) != 0) global.add(sli, want_file);
451 sli += 1;
452 }
453 }
454 return Slice(SourceLocationIndex).init(global.result.items);
455}
lib/init/src/main.zig+7-3
......@@ -27,7 +27,11 @@ test "simple test" {
2727}
2828
2929test "fuzz example" {
30 // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
31 const input_bytes = std.testing.fuzzInput(.{});
32 try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input_bytes));
30 const global = struct {
31 fn testOne(input: []const u8) anyerror!void {
32 // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
33 try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
34 }
35 };
36 try std.testing.fuzz(global.testOne, .{});
3337}
lib/std/Build/Fuzz.zig+2
......@@ -66,6 +66,8 @@ pub fn start(
6666 .coverage_files = .{},
6767 .coverage_mutex = .{},
6868 .coverage_condition = .{},
69
70 .base_timestamp = std.time.nanoTimestamp(),
6971 };
7072
7173 // For accepting HTTP connections.
lib/std/Build/Fuzz/WebServer.zig+20-5
......@@ -33,6 +33,9 @@ coverage_mutex: std.Thread.Mutex,
3333/// Signaled when `coverage_files` changes.
3434coverage_condition: std.Thread.Condition,
3535
36/// Time at initialization of WebServer.
37base_timestamp: i128,
38
3639const fuzzer_bin_name = "fuzzer";
3740const fuzzer_arch_os_abi = "wasm32-freestanding";
3841const fuzzer_cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
......@@ -43,6 +46,7 @@ const CoverageMap = struct {
4346 source_locations: []Coverage.SourceLocation,
4447 /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
4548 entry_points: std.ArrayListUnmanaged(u32),
49 start_timestamp: i64,
4650
4751 fn deinit(cm: *CoverageMap, gpa: Allocator) void {
4852 std.posix.munmap(cm.mapped_memory);
......@@ -87,6 +91,10 @@ pub fn run(ws: *WebServer) void {
8791 }
8892}
8993
94fn now(s: *const WebServer) i64 {
95 return @intCast(std.time.nanoTimestamp() - s.base_timestamp);
96}
97
9098fn accept(ws: *WebServer, connection: std.net.Server.Connection) void {
9199 defer connection.stream.close();
92100
......@@ -128,11 +136,11 @@ fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void {
128136 std.mem.eql(u8, request.head.target, "/debug") or
129137 std.mem.eql(u8, request.head.target, "/debug/"))
130138 {
131 try serveFile(ws, request, "fuzzer/index.html", "text/html");
139 try serveFile(ws, request, "fuzzer/web/index.html", "text/html");
132140 } else if (std.mem.eql(u8, request.head.target, "/main.js") or
133141 std.mem.eql(u8, request.head.target, "/debug/main.js"))
134142 {
135 try serveFile(ws, request, "fuzzer/main.js", "application/javascript");
143 try serveFile(ws, request, "fuzzer/web/main.js", "application/javascript");
136144 } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) {
137145 try serveWasm(ws, request, .ReleaseFast);
138146 } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) {
......@@ -217,7 +225,7 @@ fn buildWasmBinary(
217225
218226 const main_src_path: Build.Cache.Path = .{
219227 .root_dir = ws.zig_lib_directory,
220 .sub_path = "fuzzer/wasm/main.zig",
228 .sub_path = "fuzzer/web/main.zig",
221229 };
222230 const walk_src_path: Build.Cache.Path = .{
223231 .root_dir = ws.zig_lib_directory,
......@@ -381,6 +389,13 @@ fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void {
381389 ws.coverage_mutex.lock();
382390 defer ws.coverage_mutex.unlock();
383391
392 // On first connection, the client needs to know what time the server
393 // thinks it is to rebase timestamps.
394 {
395 const timestamp_message: abi.CurrentTime = .{ .base = ws.now() };
396 try web_socket.writeMessage(std.mem.asBytes(&timestamp_message), .binary);
397 }
398
384399 // On first connection, the client needs all the coverage information
385400 // so that subsequent updates can contain only the updated bits.
386401 var prev_unique_runs: usize = 0;
......@@ -406,7 +421,6 @@ fn sendCoverageContext(
406421 const seen_pcs = cov_header.seenBits();
407422 const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
408423 const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
409 const lowest_stack = @atomicLoad(usize, &cov_header.lowest_stack, .monotonic);
410424 if (prev_unique_runs.* != unique_runs) {
411425 // There has been an update.
412426 if (prev_unique_runs.* == 0) {
......@@ -417,6 +431,7 @@ fn sendCoverageContext(
417431 .files_len = @intCast(coverage_map.coverage.files.entries.len),
418432 .source_locations_len = @intCast(coverage_map.source_locations.len),
419433 .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
434 .start_timestamp = coverage_map.start_timestamp,
420435 };
421436 const iovecs: [5]std.posix.iovec_const = .{
422437 makeIov(std.mem.asBytes(&header)),
......@@ -431,7 +446,6 @@ fn sendCoverageContext(
431446 const header: abi.CoverageUpdateHeader = .{
432447 .n_runs = n_runs,
433448 .unique_runs = unique_runs,
434 .lowest_stack = lowest_stack,
435449 };
436450 const iovecs: [2]std.posix.iovec_const = .{
437451 makeIov(std.mem.asBytes(&header)),
......@@ -584,6 +598,7 @@ fn prepareTables(
584598 .mapped_memory = undefined, // populated below
585599 .source_locations = undefined, // populated below
586600 .entry_points = .{},
601 .start_timestamp = ws.now(),
587602 };
588603 errdefer gop.value_ptr.coverage.deinit(gpa);
589604
lib/std/Build/Fuzz/abi.zig+9-2
......@@ -13,7 +13,6 @@ pub const SeenPcsHeader = extern struct {
1313 n_runs: usize,
1414 unique_runs: usize,
1515 pcs_len: usize,
16 lowest_stack: usize,
1716
1817 /// Used for comptime assertions. Provides a mechanism for strategically
1918 /// causing compile errors.
......@@ -44,12 +43,19 @@ pub const SeenPcsHeader = extern struct {
4443};
4544
4645pub const ToClientTag = enum(u8) {
46 current_time,
4747 source_index,
4848 coverage_update,
4949 entry_points,
5050 _,
5151};
5252
53pub const CurrentTime = extern struct {
54 tag: ToClientTag = .current_time,
55 /// Number of nanoseconds that all other timestamps are in reference to.
56 base: i64 align(1),
57};
58
5359/// Sent to the fuzzer web client on first connection to the websocket URL.
5460///
5561/// Trailing:
......@@ -63,6 +69,8 @@ pub const SourceIndexHeader = extern struct {
6369 files_len: u32,
6470 source_locations_len: u32,
6571 string_bytes_len: u32,
72 /// When, according to the server, fuzzing started.
73 start_timestamp: i64 align(4),
6674
6775 pub const Flags = packed struct(u32) {
6876 tag: ToClientTag = .source_index,
......@@ -79,7 +87,6 @@ pub const CoverageUpdateHeader = extern struct {
7987 flags: Flags = .{},
8088 n_runs: u64,
8189 unique_runs: u64,
82 lowest_stack: u64,
8390
8491 pub const Flags = packed struct(u64) {
8592 tag: ToClientTag = .coverage_update,
lib/std/testing.zig+6-2
......@@ -1141,6 +1141,10 @@ pub const FuzzInputOptions = struct {
11411141 corpus: []const []const u8 = &.{},
11421142};
11431143
1144pub inline fn fuzzInput(options: FuzzInputOptions) []const u8 {
1145 return @import("root").fuzzInput(options);
1144/// Inline to avoid coverage instrumentation.
1145pub inline fn fuzz(
1146 comptime testOne: fn (input: []const u8) anyerror!void,
1147 options: FuzzInputOptions,
1148) anyerror!void {
1149 return @import("root").fuzz(testOne, options);
11461150}
lib/std/zig/tokenizer.zig+5-2
......@@ -1708,6 +1708,10 @@ test "invalid tabs and carriage returns" {
17081708 try testTokenize("\rpub\rswitch\r", &.{ .keyword_pub, .keyword_switch });
17091709}
17101710
1711test "fuzzable properties upheld" {
1712 return std.testing.fuzz(testPropertiesUpheld, .{});
1713}
1714
17111715fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void {
17121716 var tokenizer = Tokenizer.init(source);
17131717 for (expected_token_tags) |expected_token_tag| {
......@@ -1723,8 +1727,7 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v
17231727 try std.testing.expectEqual(source.len, last_token.loc.end);
17241728}
17251729
1726test "fuzzable properties upheld" {
1727 const source = std.testing.fuzzInput(.{});
1730fn testPropertiesUpheld(source: []const u8) anyerror!void {
17281731 const source0 = try std.testing.allocator.dupeZ(u8, source);
17291732 defer std.testing.allocator.free(source0);
17301733 var tokenizer = Tokenizer.init(source0);