fuzz.zig (15497B) - Raw
1 // Server timestamp. 2 var start_fuzzing_timestamp: i64 = undefined; 3 4 const js = struct { 5 extern "fuzz" fn requestSources() void; 6 extern "fuzz" fn ready() void; 7 8 extern "fuzz" fn updateStats(html_ptr: [*]const u8, html_len: usize) void; 9 extern "fuzz" fn updateEntryPoints(html_ptr: [*]const u8, html_len: usize) void; 10 extern "fuzz" fn updateSource(html_ptr: [*]const u8, html_len: usize) void; 11 extern "fuzz" fn updateCoverage(covered_ptr: [*]const SourceLocationIndex, covered_len: u32) void; 12 }; 13 14 pub fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void { 15 Walk.files.clearRetainingCapacity(); 16 Walk.decls.clearRetainingCapacity(); 17 Walk.modules.clearRetainingCapacity(); 18 recent_coverage_update.clearRetainingCapacity(); 19 selected_source_location = null; 20 21 js.requestSources(); 22 23 const Header = abi.fuzz.SourceIndexHeader; 24 const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*); 25 26 const directories_start = @sizeOf(Header); 27 const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String); 28 const files_start = directories_end; 29 const files_end = files_start + header.files_len * @sizeOf(Coverage.File); 30 const source_locations_start = files_end; 31 const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation); 32 const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len]; 33 34 const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end])); 35 const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end])); 36 const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end])); 37 38 start_fuzzing_timestamp = header.start_timestamp; 39 try updateCoverageSources(directories, files, source_locations, string_bytes); 40 js.ready(); 41 } 42 43 var coverage = Coverage.init; 44 /// Index of type `SourceLocationIndex`. 45 var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .empty; 46 /// Contains the most recent coverage update message, unmodified. 47 var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, .of(u64)) = .empty; 48 49 fn updateCoverageSources( 50 directories: []const Coverage.String, 51 files: []const Coverage.File, 52 source_locations: []const Coverage.SourceLocation, 53 string_bytes: []const u8, 54 ) !void { 55 coverage.directories.clearRetainingCapacity(); 56 coverage.files.clearRetainingCapacity(); 57 coverage.string_bytes.clearRetainingCapacity(); 58 coverage_source_locations.clearRetainingCapacity(); 59 60 try coverage_source_locations.appendSlice(gpa, source_locations); 61 try coverage.string_bytes.appendSlice(gpa, string_bytes); 62 63 try coverage.files.entries.resize(gpa, files.len); 64 @memcpy(coverage.files.entries.items(.key), files); 65 try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items }); 66 67 try coverage.directories.entries.resize(gpa, directories.len); 68 @memcpy(coverage.directories.entries.items(.key), directories); 69 try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items }); 70 } 71 72 pub fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void { 73 recent_coverage_update.clearRetainingCapacity(); 74 recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM"); 75 try updateStats(); 76 try updateCoverage(); 77 } 78 79 var entry_points: std.ArrayListUnmanaged(SourceLocationIndex) = .empty; 80 81 pub fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void { 82 const header: abi.fuzz.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.fuzz.EntryPointHeader)].*); 83 const slis: []align(1) const SourceLocationIndex = @ptrCast(msg_bytes[@sizeOf(abi.fuzz.EntryPointHeader)..]); 84 assert(slis.len == header.locsLen()); 85 try entry_points.resize(gpa, slis.len); 86 @memcpy(entry_points.items, slis); 87 try updateEntryPoints(); 88 } 89 90 /// Index into `coverage_source_locations`. 91 const SourceLocationIndex = enum(u32) { 92 _, 93 94 fn haveCoverage(sli: SourceLocationIndex) bool { 95 return @intFromEnum(sli) < coverage_source_locations.items.len; 96 } 97 98 fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation { 99 return &coverage_source_locations.items[@intFromEnum(sli)]; 100 } 101 102 fn sourceLocationLinkHtml( 103 sli: SourceLocationIndex, 104 out: *std.ArrayList(u8), 105 focused: bool, 106 ) error{OutOfMemory}!void { 107 const sl = sli.ptr(); 108 try out.print(gpa, "<code{s}>", .{ 109 @as([]const u8, if (focused) " class=\"status-running\"" else ""), 110 }); 111 try sli.appendPath(out); 112 try out.print(gpa, ":{d}:{d} </code><button class=\"linkish\" onclick=\"wasm_exports.fuzzSelectSli({d});\">View</button>", .{ 113 sl.line, 114 sl.column, 115 @intFromEnum(sli), 116 }); 117 } 118 119 fn appendPath(sli: SourceLocationIndex, out: *std.ArrayList(u8)) error{OutOfMemory}!void { 120 const sl = sli.ptr(); 121 const file = coverage.fileAt(sl.file); 122 const file_name = coverage.stringAt(file.basename); 123 const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]); 124 try html_render.appendEscaped(out, dir_name); 125 try out.appendSlice(gpa, "/"); 126 try html_render.appendEscaped(out, file_name); 127 } 128 129 fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index { 130 var buf: std.ArrayListUnmanaged(u8) = .empty; 131 defer buf.deinit(gpa); 132 sli.appendPath(&buf) catch @panic("OOM"); 133 return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null); 134 } 135 136 fn fileHtml( 137 sli: SourceLocationIndex, 138 out: *std.ArrayListUnmanaged(u8), 139 ) error{ OutOfMemory, SourceUnavailable }!void { 140 const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable; 141 const root_node = walk_file_index.findRootDecl().get().ast_node; 142 var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .empty; 143 defer annotations.deinit(gpa); 144 try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items); 145 html_render.fileSourceHtml(walk_file_index, out, root_node, .{ 146 .source_location_annotations = annotations.items, 147 }) catch |err| { 148 fatal("unable to render source: {s}", .{@errorName(err)}); 149 }; 150 } 151 }; 152 153 fn computeSourceAnnotations( 154 cov_file_index: Coverage.File.Index, 155 walk_file_index: Walk.File.Index, 156 annotations: *std.ArrayListUnmanaged(html_render.Annotation), 157 source_locations: []const Coverage.SourceLocation, 158 ) !void { 159 // Collect all the source locations from only this file into this array 160 // first, then sort by line, col, so that we can collect annotations with 161 // O(N) time complexity. 162 var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .empty; 163 defer locs.deinit(gpa); 164 165 for (source_locations, 0..) |sl, sli_usize| { 166 if (sl.file != cov_file_index) continue; 167 const sli: SourceLocationIndex = @enumFromInt(sli_usize); 168 try locs.append(gpa, sli); 169 } 170 171 std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct { 172 pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool { 173 _ = context; 174 const lhs_ptr = lhs.ptr(); 175 const rhs_ptr = rhs.ptr(); 176 if (lhs_ptr.line < rhs_ptr.line) return true; 177 if (lhs_ptr.line > rhs_ptr.line) return false; 178 return lhs_ptr.column < rhs_ptr.column; 179 } 180 }.lessThan); 181 182 const source = walk_file_index.get_ast().source; 183 var line: usize = 1; 184 var column: usize = 1; 185 var next_loc_index: usize = 0; 186 for (source, 0..) |byte, offset| { 187 if (byte == '\n') { 188 line += 1; 189 column = 1; 190 } else { 191 column += 1; 192 } 193 while (true) { 194 if (next_loc_index >= locs.items.len) return; 195 const next_sli = locs.items[next_loc_index]; 196 const next_sl = next_sli.ptr(); 197 if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break; 198 try annotations.append(gpa, .{ 199 .file_byte_offset = offset, 200 .dom_id = @intFromEnum(next_sli), 201 }); 202 next_loc_index += 1; 203 } 204 } 205 } 206 207 export fn fuzzUnpackSources(tar_ptr: [*]u8, tar_len: usize) void { 208 const tar_bytes = tar_ptr[0..tar_len]; 209 log.debug("received {d} bytes of sources.tar", .{tar_bytes.len}); 210 211 unpackSourcesInner(tar_bytes) catch |err| { 212 fatal("unable to unpack sources.tar: {s}", .{@errorName(err)}); 213 }; 214 } 215 216 fn unpackSourcesInner(tar_bytes: []u8) !void { 217 var tar_reader: std.Io.Reader = .fixed(tar_bytes); 218 var file_name_buffer: [1024]u8 = undefined; 219 var link_name_buffer: [1024]u8 = undefined; 220 var it: std.tar.Iterator = .init(&tar_reader, .{ 221 .file_name_buffer = &file_name_buffer, 222 .link_name_buffer = &link_name_buffer, 223 }); 224 while (try it.next()) |tar_file| { 225 switch (tar_file.kind) { 226 .file => { 227 if (tar_file.size == 0 and tar_file.name.len == 0) break; 228 if (std.mem.endsWith(u8, tar_file.name, ".zig")) { 229 log.debug("found file: '{s}'", .{tar_file.name}); 230 const file_name = try gpa.dupe(u8, tar_file.name); 231 if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| { 232 const pkg_name = file_name[0..pkg_name_end]; 233 const gop = try Walk.modules.getOrPut(gpa, pkg_name); 234 const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len); 235 if (!gop.found_existing or 236 std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or 237 std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name)) 238 { 239 gop.value_ptr.* = file; 240 } 241 const file_bytes = tar_reader.take(@intCast(tar_file.size)) catch unreachable; 242 it.unread_file_bytes = 0; // we have read the whole thing 243 assert(file == try Walk.add_file(file_name, file_bytes)); 244 } 245 } else { 246 log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name}); 247 } 248 }, 249 else => continue, 250 } 251 } 252 } 253 254 fn updateStats() error{OutOfMemory}!void { 255 @setFloatMode(.optimized); 256 257 if (recent_coverage_update.items.len == 0) return; 258 259 const hdr: *abi.fuzz.CoverageUpdateHeader = @ptrCast(@alignCast( 260 recent_coverage_update.items[0..@sizeOf(abi.fuzz.CoverageUpdateHeader)], 261 )); 262 263 const covered_src_locs: usize = n: { 264 var n: usize = 0; 265 const covered_bits = recent_coverage_update.items[@sizeOf(abi.fuzz.CoverageUpdateHeader)..]; 266 for (covered_bits) |byte| n += @popCount(byte); 267 break :n n; 268 }; 269 const total_src_locs = coverage_source_locations.items.len; 270 271 const avg_speed: f64 = speed: { 272 const ns_elapsed: f64 = @floatFromInt(nsSince(start_fuzzing_timestamp)); 273 const n_runs: f64 = @floatFromInt(hdr.n_runs); 274 break :speed n_runs / (ns_elapsed / std.time.ns_per_s); 275 }; 276 277 const html = try std.fmt.allocPrint(gpa, 278 \\<span slot="stat-total-runs">{d}</span> 279 \\<span slot="stat-unique-runs">{d} ({d:.1}%)</span> 280 \\<span slot="stat-coverage">{d} / {d} ({d:.1}%)</span> 281 \\<span slot="stat-speed">{d:.0}</span> 282 , .{ 283 hdr.n_runs, 284 hdr.unique_runs, 285 @as(f64, @floatFromInt(hdr.unique_runs)) / @as(f64, @floatFromInt(hdr.n_runs)), 286 covered_src_locs, 287 total_src_locs, 288 @as(f64, @floatFromInt(covered_src_locs)) / @as(f64, @floatFromInt(total_src_locs)), 289 avg_speed, 290 }); 291 defer gpa.free(html); 292 293 js.updateStats(html.ptr, html.len); 294 } 295 296 fn updateEntryPoints() error{OutOfMemory}!void { 297 var html: std.ArrayList(u8) = .empty; 298 defer html.deinit(gpa); 299 for (entry_points.items) |sli| { 300 try html.appendSlice(gpa, "<li>"); 301 try sli.sourceLocationLinkHtml(&html, selected_source_location == sli); 302 try html.appendSlice(gpa, "</li>\n"); 303 } 304 js.updateEntryPoints(html.items.ptr, html.items.len); 305 } 306 307 fn updateCoverage() error{OutOfMemory}!void { 308 if (recent_coverage_update.items.len == 0) return; 309 const want_file = (selected_source_location orelse return).ptr().file; 310 311 var covered: std.ArrayListUnmanaged(SourceLocationIndex) = .empty; 312 defer covered.deinit(gpa); 313 314 // This code assumes 64-bit elements, which is incorrect if the executable 315 // being fuzzed is not a 64-bit CPU. It also assumes little-endian which 316 // can also be incorrect. 317 comptime assert(abi.fuzz.CoverageUpdateHeader.trailing[0] == .pc_bits_usize); 318 const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64); 319 const covered_bits = std.mem.bytesAsSlice( 320 u64, 321 recent_coverage_update.items[@sizeOf(abi.fuzz.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)], 322 ); 323 var sli: SourceLocationIndex = @enumFromInt(0); 324 for (covered_bits) |elem| { 325 try covered.ensureUnusedCapacity(gpa, 64); 326 for (0..@bitSizeOf(u64)) |i| { 327 if ((elem & (@as(u64, 1) << @intCast(i))) != 0) { 328 if (sli.ptr().file == want_file) { 329 covered.appendAssumeCapacity(sli); 330 } 331 } 332 sli = @enumFromInt(@intFromEnum(sli) + 1); 333 } 334 } 335 336 js.updateCoverage(covered.items.ptr, covered.items.len); 337 } 338 339 fn updateSource() error{OutOfMemory}!void { 340 if (recent_coverage_update.items.len == 0) return; 341 const file_sli = selected_source_location.?; 342 var html: std.ArrayListUnmanaged(u8) = .empty; 343 defer html.deinit(gpa); 344 file_sli.fileHtml(&html) catch |err| switch (err) { 345 error.OutOfMemory => |e| return e, 346 error.SourceUnavailable => {}, 347 }; 348 js.updateSource(html.items.ptr, html.items.len); 349 } 350 351 var selected_source_location: ?SourceLocationIndex = null; 352 353 /// This function is not used directly by `main.js`, but a reference to it is 354 /// emitted by `SourceLocationIndex.sourceLocationLinkHtml`. 355 export fn fuzzSelectSli(sli: SourceLocationIndex) void { 356 if (!sli.haveCoverage()) return; 357 selected_source_location = sli; 358 updateEntryPoints() catch @panic("out of memory"); // highlights the selected one green 359 updateSource() catch @panic("out of memory"); 360 updateCoverage() catch @panic("out of memory"); 361 } 362 363 const std = @import("std"); 364 const Allocator = std.mem.Allocator; 365 const Coverage = std.debug.Coverage; 366 const abi = std.Build.abi; 367 const assert = std.debug.assert; 368 const gpa = std.heap.wasm_allocator; 369 370 const Walk = @import("Walk"); 371 const html_render = @import("html_render"); 372 373 const nsSince = @import("main.zig").nsSince; 374 const Slice = @import("main.zig").Slice; 375 const fatal = @import("main.zig").fatal; 376 const log = std.log; 377 const String = Slice(u8);