Commit e64a00950e

Andrew Kelley <andrew@ziglang.org>
2024-08-05 02:48:08
fuzzer web ui: introduce entry points
so you can have somewhere to start browsing
1 parent 6e6164f
Changed files (8)
lib/compiler/test_runner.zig
@@ -1,8 +1,10 @@
 //! Default test runner for unit tests.
 const builtin = @import("builtin");
+
 const std = @import("std");
 const io = std.io;
 const testing = std.testing;
+const assert = std.debug.assert;
 
 pub const std_options = .{
     .logFn = log,
@@ -141,7 +143,9 @@ 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 = .{};
@@ -160,6 +164,10 @@ fn mainServer() !void {
                     };
                     if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput");
                     if (log_err_count != 0) @panic("error logs detected");
+                    if (first) {
+                        first = false;
+                        try server.serveU64Message(.fuzz_start_addr, entry_addr);
+                    }
                 }
             },
 
@@ -339,6 +347,7 @@ const FuzzerSlice = extern struct {
 };
 
 var is_fuzz_test: bool = undefined;
+var entry_addr: usize = 0;
 
 extern fn fuzzer_next() FuzzerSlice;
 extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
@@ -348,7 +357,10 @@ pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {
     @disableInstrumentation();
     if (crippled) return "";
     is_fuzz_test = true;
-    if (builtin.fuzz) return fuzzer_next().toSlice();
+    if (builtin.fuzz) {
+        if (entry_addr == 0) entry_addr = @returnAddress();
+        return fuzzer_next().toSlice();
+    }
     if (options.corpus.len == 0) return "";
     var prng = std.Random.DefaultPrng.init(testing.random_seed);
     const random = prng.random();
lib/fuzzer/wasm/main.zig
@@ -14,6 +14,7 @@ const js = struct {
     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 = .{
@@ -64,6 +65,7 @@ export fn message_end() void {
     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,
     }
 }
@@ -219,6 +221,19 @@ fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
     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);
+}
+
 var coverage = Coverage.init;
 var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
 /// Contains the most recent coverage update message, unmodified.
@@ -246,3 +261,14 @@ fn updateCoverage(
     @memcpy(coverage.directories.entries.items(.key), directories);
     try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
 }
+
+export fn sourceLocationLinkHtml(index: u32) String {
+    const sl = coverage_source_locations.items[index];
+    const file_name = coverage.stringAt(coverage.fileAt(sl.file).basename);
+
+    string_result.clearRetainingCapacity();
+    string_result.writer(gpa).print("{s}:{d}:{d}", .{
+        file_name, sl.line, sl.column,
+    }) catch @panic("OOM");
+    return String.init(string_result.items);
+}
lib/fuzzer/index.html
@@ -131,6 +131,7 @@
         <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">
lib/fuzzer/main.js
@@ -7,6 +7,7 @@
   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) {
@@ -30,7 +31,8 @@
           throw new Error("panic: " + msg);
       },
       emitSourceIndexChange: onSourceIndexChange,
-      emitCoverageUpdate: onCoverageUpdate,
+      emitCoverageUpdate: renderStats,
+      emitEntryPointsUpdate: renderStats,
     },
   }).then(function(obj) {
     wasm_exports = obj.instance.exports;
@@ -98,10 +100,6 @@
     render();
   }
 
-  function onCoverageUpdate() {
-    renderStats();
-  }
-
   function render() {
     domStatus.classList.add("hidden");
     domSectSource.classList.add("hidden");
@@ -120,9 +118,26 @@
     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.innerText = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i]));
+    }
+
+
     domSectStats.classList.remove("hidden");
   }
 
+  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);
   }
@@ -150,6 +165,13 @@
     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;
lib/std/Build/Fuzz/abi.zig
@@ -19,6 +19,7 @@ pub const SeenPcsHeader = extern struct {
 pub const ToClientTag = enum(u8) {
     source_index,
     coverage_update,
+    entry_points,
     _,
 };
 
@@ -53,3 +54,16 @@ pub const CoverageUpdateHeader = extern struct {
     unique_runs: u64 align(1),
     lowest_stack: u64 align(1),
 };
+
+/// Sent to the fuzzer web client when the set of entry points is updated.
+///
+/// Trailing:
+/// * one u32 index of source_locations per locs_len
+pub const EntryPointHeader = extern struct {
+    flags: Flags,
+
+    pub const Flags = packed struct(u32) {
+        tag: ToClientTag = .entry_points,
+        locs_len: u24,
+    };
+};
lib/std/Build/Fuzz/WebServer.zig
@@ -34,6 +34,8 @@ const CoverageMap = struct {
     mapped_memory: []align(std.mem.page_size) const u8,
     coverage: Coverage,
     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),
 
     fn deinit(cm: *CoverageMap, gpa: Allocator) void {
         std.posix.munmap(cm.mapped_memory);
@@ -47,6 +49,10 @@ const Msg = union(enum) {
         id: u64,
         run: *Step.Run,
     },
+    entry_point: struct {
+        coverage_id: u64,
+        addr: u64,
+    },
 };
 
 pub fn run(ws: *WebServer) void {
@@ -356,14 +362,20 @@ fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void {
     // 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;
-    try sendCoverageContext(ws, web_socket, &prev_unique_runs);
+    var prev_entry_points: usize = 0;
+    try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
     while (true) {
         ws.coverage_condition.timedWait(&ws.coverage_mutex, std.time.ns_per_ms * 500) catch {};
-        try sendCoverageContext(ws, web_socket, &prev_unique_runs);
+        try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
     }
 }
 
-fn sendCoverageContext(ws: *WebServer, web_socket: *std.http.WebSocket, prev_unique_runs: *usize) !void {
+fn sendCoverageContext(
+    ws: *WebServer,
+    web_socket: *std.http.WebSocket,
+    prev_unique_runs: *usize,
+    prev_entry_points: *usize,
+) !void {
     const coverage_maps = ws.coverage_files.values();
     if (coverage_maps.len == 0) return;
     // TODO: make each events URL correspond to one coverage map
@@ -407,6 +419,21 @@ fn sendCoverageContext(ws: *WebServer, web_socket: *std.http.WebSocket, prev_uni
 
         prev_unique_runs.* = unique_runs;
     }
+
+    if (prev_entry_points.* != coverage_map.entry_points.items.len) {
+        const header: abi.EntryPointHeader = .{
+            .flags = .{
+                .locs_len = @intCast(coverage_map.entry_points.items.len),
+            },
+        };
+        const iovecs: [2]std.posix.iovec_const = .{
+            makeIov(std.mem.asBytes(&header)),
+            makeIov(std.mem.sliceAsBytes(coverage_map.entry_points.items)),
+        };
+        try web_socket.writeMessagev(&iovecs, .binary);
+
+        prev_entry_points.* = coverage_map.entry_points.items.len;
+    }
 }
 
 fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
@@ -508,6 +535,10 @@ pub fn coverageRun(ws: *WebServer) void {
                 error.AlreadyReported => continue,
                 else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
             },
+            .entry_point => |entry_point| addEntryPoint(ws, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
+                error.AlreadyReported => continue,
+                else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
+            },
         };
         ws.msg_queue.clearRetainingCapacity();
     }
@@ -538,6 +569,7 @@ fn prepareTables(
         .coverage = std.debug.Coverage.init,
         .mapped_memory = undefined, // populated below
         .source_locations = undefined, // populated below
+        .entry_points = .{},
     };
     errdefer gop.value_ptr.coverage.deinit(gpa);
 
@@ -597,6 +629,32 @@ fn prepareTables(
     ws.coverage_condition.broadcast();
 }
 
+fn addEntryPoint(ws: *WebServer, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory }!void {
+    ws.coverage_mutex.lock();
+    defer ws.coverage_mutex.unlock();
+
+    const coverage_map = ws.coverage_files.getPtr(coverage_id).?;
+    const ptr = coverage_map.mapped_memory;
+    const pcs_bytes = ptr[@sizeOf(abi.SeenPcsHeader)..][0 .. coverage_map.source_locations.len * @sizeOf(usize)];
+    const pcs: []const usize = @alignCast(std.mem.bytesAsSlice(usize, pcs_bytes));
+    const index = std.sort.upperBound(usize, addr, pcs, {}, std.sort.asc(usize));
+    if (index >= pcs.len) {
+        log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
+            addr, pcs[0], pcs[pcs.len - 1],
+        });
+        return error.AlreadyReported;
+    }
+    if (false) {
+        const sl = coverage_map.source_locations[index];
+        const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
+        log.debug("server found entry point {s}:{d}:{d}", .{
+            file_name, sl.line, sl.column,
+        });
+    }
+    const gpa = ws.gpa;
+    try coverage_map.entry_points.append(gpa, @intCast(index));
+}
+
 fn makeIov(s: []const u8) std.posix.iovec_const {
     return .{
         .base = s.ptr,
lib/std/Build/Step/Run.zig
@@ -1427,6 +1427,7 @@ fn evalZigTest(
     var log_err_count: u32 = 0;
 
     var metadata: ?TestMetadata = null;
+    var coverage_id: ?u64 = null;
 
     var sub_prog_node: ?std.Progress.Node = null;
     defer if (sub_prog_node) |n| n.end();
@@ -1517,17 +1518,31 @@ fn evalZigTest(
             .coverage_id => {
                 const web_server = fuzz_context.?.web_server;
                 const msg_ptr: *align(1) const u64 = @ptrCast(body);
-                const coverage_id = msg_ptr.*;
+                coverage_id = msg_ptr.*;
                 {
                     web_server.mutex.lock();
                     defer web_server.mutex.unlock();
                     try web_server.msg_queue.append(web_server.gpa, .{ .coverage = .{
-                        .id = coverage_id,
+                        .id = coverage_id.?,
                         .run = run,
                     } });
                     web_server.condition.signal();
                 }
             },
+            .fuzz_start_addr => {
+                const web_server = fuzz_context.?.web_server;
+                const msg_ptr: *align(1) const u64 = @ptrCast(body);
+                const addr = msg_ptr.*;
+                {
+                    web_server.mutex.lock();
+                    defer web_server.mutex.unlock();
+                    try web_server.msg_queue.append(web_server.gpa, .{ .entry_point = .{
+                        .addr = addr,
+                        .coverage_id = coverage_id.?,
+                    } });
+                    web_server.condition.signal();
+                }
+            },
             else => {}, // ignore other messages
         }
 
lib/std/zig/Server.zig
@@ -32,6 +32,10 @@ pub const Message = struct {
         /// to store coverage information. The integer is a hash of the PCs
         /// stored within that file.
         coverage_id,
+        /// Body is a u64le that indicates the function pointer virtual memory
+        /// address of the fuzz unit test. This is used to provide a starting
+        /// point to view coverage.
+        fuzz_start_addr,
 
         _,
     };