zig

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

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);