| ... | @@ -68,8 +68,7 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void { | ... | @@ -68,8 +68,7 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void { |
| 68 | const len = cases_ptr[0]; | 68 | const len = cases_ptr[0]; |
| 69 | const val_size_in_bits = cases_ptr[1]; | 69 | const val_size_in_bits = cases_ptr[1]; |
| 70 | const cases = cases_ptr[2..][0..len]; | 70 | const cases = cases_ptr[2..][0..len]; |
| 71 | _ = val; | 71 | fuzzer.traceValue(pc ^ val); |
| 72 | fuzzer.visitPc(pc); | | |
| 73 | _ = val_size_in_bits; | 72 | _ = val_size_in_bits; |
| 74 | _ = cases; | 73 | _ = cases; |
| 75 | //std.log.debug("0x{x}: switch on value {d} ({d} bits) with {d} cases", .{ | 74 | //std.log.debug("0x{x}: switch on value {d} ({d} bits) with {d} cases", .{ |
| ... | @@ -78,28 +77,24 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void { | ... | @@ -78,28 +77,24 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void { |
| 78 | } | 77 | } |
| 79 | | 78 | |
| 80 | export fn __sanitizer_cov_trace_pc_indir(callee: usize) void { | 79 | export fn __sanitizer_cov_trace_pc_indir(callee: usize) void { |
| 81 | const pc = @returnAddress(); | 80 | // Not valuable because we already have pc tracing via 8bit counters. |
| 82 | _ = callee; | 81 | _ = callee; |
| 83 | fuzzer.visitPc(pc); | 82 | //const pc = @returnAddress(); |
| | 83 | //fuzzer.traceValue(pc ^ callee); |
| 84 | //std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee }); | 84 | //std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee }); |
| 85 | } | 85 | } |
| 86 | | 86 | |
| 87 | fn handleCmp(pc: usize, arg1: u64, arg2: u64) void { | 87 | fn handleCmp(pc: usize, arg1: u64, arg2: u64) void { |
| 88 | fuzzer.visitPc(pc ^ arg1 ^ arg2); | 88 | fuzzer.traceValue(pc ^ arg1 ^ arg2); |
| 89 | //std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 }); | 89 | //std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 }); |
| 90 | } | 90 | } |
| 91 | | 91 | |
| 92 | const Fuzzer = struct { | 92 | const Fuzzer = struct { |
| 93 | gpa: Allocator, | | |
| 94 | rng: std.Random.DefaultPrng, | 93 | rng: std.Random.DefaultPrng, |
| 95 | input: std.ArrayListUnmanaged(u8), | | |
| 96 | pcs: []const usize, | 94 | pcs: []const usize, |
| 97 | pc_counters: []u8, | 95 | pc_counters: []u8, |
| 98 | n_runs: usize, | 96 | n_runs: usize, |
| 99 | recent_cases: RunMap, | 97 | traced_comparisons: std.AutoArrayHashMapUnmanaged(usize, void), |
| 100 | /// Data collected from code coverage instrumentation from one execution of | | |
| 101 | /// the test function. | | |
| 102 | coverage: Coverage, | | |
| 103 | /// Tracks which PCs have been seen across all runs that do not crash the fuzzer process. | 98 | /// Tracks which PCs have been seen across all runs that do not crash the fuzzer process. |
| 104 | /// Stored in a memory-mapped file so that it can be shared with other | 99 | /// Stored in a memory-mapped file so that it can be shared with other |
| 105 | /// processes and viewed while the fuzzer is running. | 100 | /// processes and viewed while the fuzzer is running. |
| ... | @@ -108,42 +103,25 @@ const Fuzzer = struct { | ... | @@ -108,42 +103,25 @@ const Fuzzer = struct { |
| 108 | /// Identifies the file name that will be used to store coverage | 103 | /// Identifies the file name that will be used to store coverage |
| 109 | /// information, available to other processes. | 104 | /// information, available to other processes. |
| 110 | coverage_id: u64, | 105 | coverage_id: u64, |
| | 106 | unit_test_name: []const u8, |
| 111 | | 107 | |
| 112 | const RunMap = std.ArrayHashMapUnmanaged(Run, void, Run.HashContext, false); | 108 | /// The index corresponds to the file name within the f/ subdirectory. |
| 113 | | 109 | /// The string is the input. |
| 114 | const Coverage = struct { | 110 | /// This data is read-only; it caches what is on the filesystem. |
| 115 | pc_table: std.AutoArrayHashMapUnmanaged(usize, void), | 111 | corpus: std.ArrayListUnmanaged(Input), |
| 116 | run_id_hasher: std.hash.Wyhash, | 112 | corpus_directory: std.Build.Cache.Directory, |
| 117 | | | |
| 118 | fn reset(cov: *Coverage) void { | | |
| 119 | cov.pc_table.clearRetainingCapacity(); | | |
| 120 | cov.run_id_hasher = std.hash.Wyhash.init(0); | | |
| 121 | } | | |
| 122 | }; | | |
| 123 | | | |
| 124 | const Run = struct { | | |
| 125 | id: Id, | | |
| 126 | input: []const u8, | | |
| 127 | score: usize, | | |
| 128 | | 113 | |
| 129 | const Id = u64; | 114 | /// The next input that will be given to the testOne function. When the |
| 130 | | 115 | /// current process crashes, this memory-mapped file is used to recover the |
| 131 | const HashContext = struct { | 116 | /// input. |
| 132 | pub fn eql(ctx: HashContext, a: Run, b: Run, b_index: usize) bool { | 117 | /// |
| 133 | _ = b_index; | 118 | /// The file size corresponds to the capacity. The length is not stored |
| 134 | _ = ctx; | 119 | /// and that is the next thing to work on! |
| 135 | return a.id == b.id; | 120 | input: MemoryMappedList, |
| 136 | } | | |
| 137 | pub fn hash(ctx: HashContext, a: Run) u32 { | | |
| 138 | _ = ctx; | | |
| 139 | return @truncate(a.id); | | |
| 140 | } | | |
| 141 | }; | | |
| 142 | | 121 | |
| 143 | fn deinit(run: *Run, gpa: Allocator) void { | 122 | const Input = struct { |
| 144 | gpa.free(run.input); | 123 | bytes: []u8, |
| 145 | run.* = undefined; | 124 | last_traced_comparison: usize, |
| 146 | } | | |
| 147 | }; | 125 | }; |
| 148 | | 126 | |
| 149 | const Slice = extern struct { | 127 | const Slice = extern struct { |
| ... | @@ -162,11 +140,6 @@ const Fuzzer = struct { | ... | @@ -162,11 +140,6 @@ const Fuzzer = struct { |
| 162 | } | 140 | } |
| 163 | }; | 141 | }; |
| 164 | | 142 | |
| 165 | const Analysis = struct { | | |
| 166 | score: usize, | | |
| 167 | id: Run.Id, | | |
| 168 | }; | | |
| 169 | | | |
| 170 | fn init(f: *Fuzzer, cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []const usize) !void { | 143 | fn init(f: *Fuzzer, cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []const usize) !void { |
| 171 | f.cache_dir = cache_dir; | 144 | f.cache_dir = cache_dir; |
| 172 | f.pc_counters = pc_counters; | 145 | f.pc_counters = pc_counters; |
| ... | @@ -186,7 +159,6 @@ const Fuzzer = struct { | ... | @@ -186,7 +159,6 @@ const Fuzzer = struct { |
| 186 | .read = true, | 159 | .read = true, |
| 187 | .truncate = false, | 160 | .truncate = false, |
| 188 | }); | 161 | }); |
| 189 | defer coverage_file.close(); | | |
| 190 | const n_bitset_elems = (pcs.len + @bitSizeOf(usize) - 1) / @bitSizeOf(usize); | 162 | const n_bitset_elems = (pcs.len + @bitSizeOf(usize) - 1) / @bitSizeOf(usize); |
| 191 | comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize); | 163 | comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize); |
| 192 | comptime assert(SeenPcsHeader.trailing[1] == .pc_addr); | 164 | comptime assert(SeenPcsHeader.trailing[1] == .pc_addr); |
| ... | @@ -228,156 +200,196 @@ const Fuzzer = struct { | ... | @@ -228,156 +200,196 @@ const Fuzzer = struct { |
| 228 | } | 200 | } |
| 229 | } | 201 | } |
| 230 | | 202 | |
| 231 | fn analyzeLastRun(f: *Fuzzer) Analysis { | 203 | fn initNextInput(f: *Fuzzer) void { |
| 232 | return .{ | 204 | while (true) { |
| 233 | .id = f.coverage.run_id_hasher.final(), | 205 | const i = f.corpus.items.len; |
| 234 | .score = f.coverage.pc_table.count(), | 206 | var buf: [30]u8 = undefined; |
| 235 | }; | 207 | const input_sub_path = std.fmt.bufPrint(&buf, "{d}", .{i}) catch unreachable; |
| | 208 | const input = f.corpus_directory.handle.readFileAlloc(gpa, input_sub_path, 1 << 31) catch |err| switch (err) { |
| | 209 | error.FileNotFound => { |
| | 210 | // Make this one the next input. |
| | 211 | const input_file = f.corpus_directory.handle.createFile(input_sub_path, .{ |
| | 212 | .exclusive = true, |
| | 213 | .truncate = false, |
| | 214 | .read = true, |
| | 215 | }) catch |e| switch (e) { |
| | 216 | error.PathAlreadyExists => continue, |
| | 217 | else => fatal("unable to create '{}{d}: {s}", .{ f.corpus_directory, i, @errorName(err) }), |
| | 218 | }; |
| | 219 | errdefer input_file.close(); |
| | 220 | // Initialize the mmap for the current input. |
| | 221 | f.input = MemoryMappedList.create(input_file, 0, std.heap.page_size_max) catch |e| { |
| | 222 | fatal("unable to init memory map for input at '{}{d}': {s}", .{ |
| | 223 | f.corpus_directory, i, @errorName(e), |
| | 224 | }); |
| | 225 | }; |
| | 226 | break; |
| | 227 | }, |
| | 228 | else => fatal("unable to read '{}{d}': {s}", .{ f.corpus_directory, i, @errorName(err) }), |
| | 229 | }; |
| | 230 | errdefer gpa.free(input); |
| | 231 | f.corpus.append(gpa, .{ |
| | 232 | .bytes = input, |
| | 233 | .last_traced_comparison = 0, |
| | 234 | }) catch |err| oom(err); |
| | 235 | } |
| | 236 | } |
| | 237 | |
| | 238 | fn addCorpusElem(f: *Fuzzer, input: []const u8) !void { |
| | 239 | try f.corpus.append(gpa, .{ |
| | 240 | .bytes = try gpa.dupe(u8, input), |
| | 241 | .last_traced_comparison = 0, |
| | 242 | }); |
| 236 | } | 243 | } |
| 237 | | 244 | |
| 238 | fn start(f: *Fuzzer) !void { | 245 | fn start(f: *Fuzzer) !void { |
| 239 | const gpa = f.gpa; | | |
| 240 | const rng = fuzzer.rng.random(); | 246 | const rng = fuzzer.rng.random(); |
| 241 | | 247 | |
| 242 | // Prepare initial input. | 248 | // Grab the corpus which is namespaced based on `unit_test_name`. |
| 243 | assert(f.recent_cases.entries.len == 0); | 249 | { |
| | 250 | if (f.unit_test_name.len == 0) fatal("test runner never set unit test name", .{}); |
| | 251 | const sub_path = try std.fmt.allocPrint(gpa, "f/{s}", .{f.unit_test_name}); |
| | 252 | f.corpus_directory = .{ |
| | 253 | .handle = f.cache_dir.makeOpenPath(sub_path, .{}) catch |err| |
| | 254 | fatal("unable to open corpus directory 'f/{s}': {s}", .{ sub_path, @errorName(err) }), |
| | 255 | .path = sub_path, |
| | 256 | }; |
| | 257 | initNextInput(f); |
| | 258 | } |
| | 259 | |
| 244 | assert(f.n_runs == 0); | 260 | 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 | | 261 | |
| 255 | const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]); | 262 | // If the corpus is empty, synthesize one input. |
| | 263 | if (f.corpus.items.len == 0) { |
| | 264 | const len = rng.uintLessThanBiased(usize, 200); |
| | 265 | const slice = try gpa.alloc(u8, len); |
| | 266 | rng.bytes(slice); |
| | 267 | f.input.appendSliceAssumeCapacity(slice); |
| | 268 | try f.corpus.append(gpa, .{ |
| | 269 | .bytes = slice, |
| | 270 | .last_traced_comparison = 0, |
| | 271 | }); |
| | 272 | runOne(f, 0); |
| | 273 | } |
| 256 | | 274 | |
| 257 | while (true) { | 275 | while (true) { |
| 258 | const chosen_index = rng.uintLessThanBiased(usize, f.recent_cases.entries.len); | 276 | const chosen_index = rng.uintLessThanBiased(usize, f.corpus.items.len); |
| 259 | const run = &f.recent_cases.keys()[chosen_index]; | 277 | const modification = rng.enumValue(Mutation); |
| 260 | f.input.clearRetainingCapacity(); | 278 | f.mutateAndRunOne(chosen_index, modification); |
| 261 | f.input.appendSliceAssumeCapacity(run.input); | 279 | } |
| 262 | try f.mutate(); | 280 | } |
| 263 | | 281 | |
| 264 | @memset(f.pc_counters, 0); | 282 | /// `x` represents a possible branch. It is the PC address of the possible |
| 265 | __sancov_lowest_stack = std.math.maxInt(usize); | 283 | /// branch site, hashed together with the value(s) used that determine to |
| 266 | f.coverage.reset(); | 284 | /// where it branches. |
| | 285 | fn traceValue(f: *Fuzzer, x: usize) void { |
| | 286 | errdefer |err| oom(err); |
| | 287 | try f.traced_comparisons.put(gpa, x, {}); |
| | 288 | } |
| 267 | | 289 | |
| 268 | fuzzer_one(f.input.items.ptr, f.input.items.len); | 290 | const Mutation = enum { |
| | 291 | remove_byte, |
| | 292 | modify_byte, |
| | 293 | add_byte, |
| | 294 | }; |
| 269 | | 295 | |
| 270 | f.n_runs += 1; | 296 | fn mutateAndRunOne(f: *Fuzzer, corpus_index: usize, mutation: Mutation) void { |
| 271 | _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic); | 297 | const rng = fuzzer.rng.random(); |
| | 298 | f.input.clearRetainingCapacity(); |
| | 299 | const old_input = f.corpus.items[corpus_index].bytes; |
| | 300 | f.input.ensureTotalCapacity(old_input.len + 1) catch @panic("mmap file resize failed"); |
| | 301 | switch (mutation) { |
| | 302 | .remove_byte => { |
| | 303 | const omitted_index = rng.uintLessThanBiased(usize, old_input.len); |
| | 304 | f.input.appendSliceAssumeCapacity(old_input[0..omitted_index]); |
| | 305 | f.input.appendSliceAssumeCapacity(old_input[omitted_index + 1 ..]); |
| | 306 | }, |
| | 307 | .modify_byte => { |
| | 308 | const modified_index = rng.uintLessThanBiased(usize, old_input.len); |
| | 309 | f.input.appendSliceAssumeCapacity(old_input); |
| | 310 | f.input.items[modified_index] = rng.int(u8); |
| | 311 | }, |
| | 312 | .add_byte => { |
| | 313 | const modified_index = rng.uintLessThanBiased(usize, old_input.len); |
| | 314 | f.input.appendSliceAssumeCapacity(old_input[0..modified_index]); |
| | 315 | f.input.appendAssumeCapacity(rng.int(u8)); |
| | 316 | f.input.appendSliceAssumeCapacity(old_input[modified_index..]); |
| | 317 | }, |
| | 318 | } |
| | 319 | runOne(f, corpus_index); |
| | 320 | } |
| 272 | | 321 | |
| 273 | if (f.n_runs % 10000 == 0) f.dumpStats(); | 322 | fn runOne(f: *Fuzzer, corpus_index: usize) void { |
| | 323 | const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]); |
| 274 | | 324 | |
| 275 | const analysis = f.analyzeLastRun(); | 325 | f.traced_comparisons.clearRetainingCapacity(); |
| 276 | const gop = f.recent_cases.getOrPutAssumeCapacity(.{ | 326 | @memset(f.pc_counters, 0); |
| 277 | .id = analysis.id, | 327 | __sancov_lowest_stack = std.math.maxInt(usize); |
| 278 | .input = undefined, | | |
| 279 | .score = undefined, | | |
| 280 | }); | | |
| 281 | if (gop.found_existing) { | | |
| 282 | //std.log.info("duplicate analysis: score={d} id={d}", .{ analysis.score, analysis.id }); | | |
| 283 | if (f.input.items.len < gop.key_ptr.input.len or gop.key_ptr.score == 0) { | | |
| 284 | gpa.free(gop.key_ptr.input); | | |
| 285 | gop.key_ptr.input = try gpa.dupe(u8, f.input.items); | | |
| 286 | gop.key_ptr.score = analysis.score; | | |
| 287 | } | | |
| 288 | } else { | | |
| 289 | std.log.info("unique analysis: score={d} id={d}", .{ analysis.score, analysis.id }); | | |
| 290 | gop.key_ptr.* = .{ | | |
| 291 | .id = analysis.id, | | |
| 292 | .input = try gpa.dupe(u8, f.input.items), | | |
| 293 | .score = analysis.score, | | |
| 294 | }; | | |
| 295 | | | |
| 296 | { | | |
| 297 | // Track code coverage from all runs. | | |
| 298 | comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize); | | |
| 299 | const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.items[@sizeOf(SeenPcsHeader)..]); | | |
| 300 | const remainder = f.pcs.len % @bitSizeOf(usize); | | |
| 301 | const aligned_len = f.pcs.len - remainder; | | |
| 302 | const seen_pcs = header_end_ptr[0..aligned_len]; | | |
| 303 | const pc_counters = std.mem.bytesAsSlice([@bitSizeOf(usize)]u8, f.pc_counters[0..aligned_len]); | | |
| 304 | const V = @Vector(@bitSizeOf(usize), u8); | | |
| 305 | const zero_v: V = @splat(0); | | |
| 306 | | | |
| 307 | for (header_end_ptr[0..pc_counters.len], pc_counters) |*elem, *array| { | | |
| 308 | const v: V = array.*; | | |
| 309 | const mask: usize = @bitCast(v != zero_v); | | |
| 310 | _ = @atomicRmw(usize, elem, .Or, mask, .monotonic); | | |
| 311 | } | | |
| 312 | if (remainder > 0) { | | |
| 313 | const i = pc_counters.len; | | |
| 314 | const elem = &seen_pcs[i]; | | |
| 315 | var mask: usize = 0; | | |
| 316 | for (f.pc_counters[i * @bitSizeOf(usize) ..][0..remainder], 0..) |byte, bit_index| { | | |
| 317 | mask |= @as(usize, @intFromBool(byte != 0)) << @intCast(bit_index); | | |
| 318 | } | | |
| 319 | _ = @atomicRmw(usize, elem, .Or, mask, .monotonic); | | |
| 320 | } | | |
| 321 | } | | |
| 322 | | 328 | |
| 323 | _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); | 329 | fuzzer_one(@volatileCast(f.input.items.ptr), f.input.items.len); |
| 324 | } | | |
| 325 | | 330 | |
| 326 | if (f.recent_cases.entries.len >= 100) { | 331 | f.n_runs += 1; |
| 327 | const Context = struct { | 332 | _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic); |
| 328 | values: []const Run, | 333 | |
| 329 | pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { | 334 | // Track code coverage from all runs. |
| 330 | return ctx.values[b_index].score < ctx.values[a_index].score; | 335 | comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize); |
| 331 | } | 336 | const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.items[@sizeOf(SeenPcsHeader)..]); |
| 332 | }; | 337 | const remainder = f.pcs.len % @bitSizeOf(usize); |
| 333 | f.recent_cases.sortUnstable(Context{ .values = f.recent_cases.keys() }); | 338 | const aligned_len = f.pcs.len - remainder; |
| 334 | const cap = 50; | 339 | const seen_pcs = header_end_ptr[0..aligned_len]; |
| 335 | // This has to be done before deinitializing the deleted items. | 340 | const pc_counters = std.mem.bytesAsSlice([@bitSizeOf(usize)]u8, f.pc_counters[0..aligned_len]); |
| 336 | const doomed_runs = f.recent_cases.keys()[cap..]; | 341 | const V = @Vector(@bitSizeOf(usize), u8); |
| 337 | f.recent_cases.shrinkRetainingCapacity(cap); | 342 | const zero_v: V = @splat(0); |
| 338 | for (doomed_runs) |*doomed_run| { | 343 | var fresh = false; |
| 339 | std.log.info("culling score={d} id={d}", .{ doomed_run.score, doomed_run.id }); | 344 | var superset = true; |
| 340 | doomed_run.deinit(gpa); | 345 | |
| 341 | } | 346 | for (header_end_ptr[0..pc_counters.len], pc_counters) |*elem, *array| { |
| | 347 | const v: V = array.*; |
| | 348 | const mask: usize = @bitCast(v != zero_v); |
| | 349 | const prev = @atomicRmw(usize, elem, .Or, mask, .monotonic); |
| | 350 | fresh = fresh or (prev | mask) != prev; |
| | 351 | superset = superset and (prev | mask) != mask; |
| | 352 | } |
| | 353 | if (remainder > 0) { |
| | 354 | const i = pc_counters.len; |
| | 355 | const elem = &seen_pcs[i]; |
| | 356 | var mask: usize = 0; |
| | 357 | for (f.pc_counters[i * @bitSizeOf(usize) ..][0..remainder], 0..) |byte, bit_index| { |
| | 358 | mask |= @as(usize, @intFromBool(byte != 0)) << @intCast(bit_index); |
| 342 | } | 359 | } |
| | 360 | const prev = @atomicRmw(usize, elem, .Or, mask, .monotonic); |
| | 361 | fresh = fresh or (prev | mask) != prev; |
| | 362 | superset = superset and (prev | mask) != mask; |
| 343 | } | 363 | } |
| 344 | } | | |
| 345 | | | |
| 346 | fn visitPc(f: *Fuzzer, pc: usize) void { | | |
| 347 | errdefer |err| oom(err); | | |
| 348 | try f.coverage.pc_table.put(f.gpa, pc, {}); | | |
| 349 | f.coverage.run_id_hasher.update(std.mem.asBytes(&pc)); | | |
| 350 | } | | |
| 351 | | 364 | |
| 352 | fn dumpStats(f: *Fuzzer) void { | 365 | // First check if this is a better version of an already existing |
| 353 | for (f.recent_cases.keys()[0..@min(f.recent_cases.entries.len, 5)], 0..) |run, i| { | 366 | // input, replacing that input. |
| 354 | std.log.info("best[{d}] id={x} score={d} input: '{}'", .{ | 367 | if (superset or f.traced_comparisons.entries.len >= f.corpus.items[corpus_index].last_traced_comparison) { |
| 355 | i, run.id, run.score, std.zig.fmtEscapes(run.input), | 368 | const new_input = gpa.realloc(f.corpus.items[corpus_index].bytes, f.input.items.len) catch |err| oom(err); |
| 356 | }); | 369 | f.corpus.items[corpus_index] = .{ |
| | 370 | .bytes = new_input, |
| | 371 | .last_traced_comparison = f.traced_comparisons.count(), |
| | 372 | }; |
| | 373 | @memcpy(new_input, @volatileCast(f.input.items)); |
| | 374 | _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); |
| | 375 | return; |
| 357 | } | 376 | } |
| 358 | } | | |
| 359 | | 377 | |
| 360 | fn mutate(f: *Fuzzer) !void { | 378 | if (!fresh) return; |
| 361 | const gpa = f.gpa; | | |
| 362 | const rng = fuzzer.rng.random(); | | |
| 363 | | 379 | |
| 364 | if (f.input.items.len == 0) { | 380 | // Input is already committed to the file system, we just need to open a new file |
| 365 | const len = rng.uintLessThanBiased(usize, 80); | 381 | // for the next input. |
| 366 | try f.input.resize(gpa, len); | 382 | // Pre-add it to the corpus list so that it does not get redundantly picked up. |
| 367 | rng.bytes(f.input.items); | 383 | f.corpus.append(gpa, .{ |
| 368 | return; | 384 | .bytes = gpa.dupe(u8, @volatileCast(f.input.items)) catch |err| oom(err), |
| 369 | } | 385 | .last_traced_comparison = f.traced_comparisons.entries.len, |
| | 386 | }) catch |err| oom(err); |
| | 387 | f.input.deinit(); |
| | 388 | initNextInput(f); |
| 370 | | 389 | |
| 371 | const index = rng.uintLessThanBiased(usize, f.input.items.len * 3); | 390 | // TODO: also mark input as "hot" so it gets prioritized for checking mutations above others. |
| 372 | if (index < f.input.items.len) { | 391 | |
| 373 | f.input.items[index] = rng.int(u8); | 392 | _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); |
| 374 | } else if (index < f.input.items.len * 2) { | | |
| 375 | _ = f.input.orderedRemove(index - f.input.items.len); | | |
| 376 | } else if (index < f.input.items.len * 3) { | | |
| 377 | try f.input.insert(gpa, index - f.input.items.len * 2, rng.int(u8)); | | |
| 378 | } else { | | |
| 379 | unreachable; | | |
| 380 | } | | |
| 381 | } | 393 | } |
| 382 | }; | 394 | }; |
| 383 | | 395 | |
| ... | @@ -402,20 +414,26 @@ fn oom(err: anytype) noreturn { | ... | @@ -402,20 +414,26 @@ fn oom(err: anytype) noreturn { |
| 402 | } | 414 | } |
| 403 | } | 415 | } |
| 404 | | 416 | |
| 405 | var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init; | 417 | var debug_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init; |
| | 418 | |
| | 419 | const gpa = switch (builtin.mode) { |
| | 420 | .Debug => debug_allocator.allocator(), |
| | 421 | .ReleaseFast, .ReleaseSmall, .ReleaseSafe => std.heap.smp_allocator, |
| | 422 | }; |
| 406 | | 423 | |
| 407 | var fuzzer: Fuzzer = .{ | 424 | var fuzzer: Fuzzer = .{ |
| 408 | .gpa = general_purpose_allocator.allocator(), | | |
| 409 | .rng = std.Random.DefaultPrng.init(0), | 425 | .rng = std.Random.DefaultPrng.init(0), |
| 410 | .input = .{}, | 426 | .input = undefined, |
| 411 | .pcs = undefined, | 427 | .pcs = undefined, |
| 412 | .pc_counters = undefined, | 428 | .pc_counters = undefined, |
| 413 | .n_runs = 0, | 429 | .n_runs = 0, |
| 414 | .recent_cases = .{}, | | |
| 415 | .coverage = undefined, | | |
| 416 | .cache_dir = undefined, | 430 | .cache_dir = undefined, |
| 417 | .seen_pcs = undefined, | 431 | .seen_pcs = undefined, |
| 418 | .coverage_id = undefined, | 432 | .coverage_id = undefined, |
| | 433 | .unit_test_name = &.{}, |
| | 434 | .corpus = .empty, |
| | 435 | .corpus_directory = undefined, |
| | 436 | .traced_comparisons = .empty, |
| 419 | }; | 437 | }; |
| 420 | | 438 | |
| 421 | /// Invalid until `fuzzer_init` is called. | 439 | /// Invalid until `fuzzer_init` is called. |
| ... | @@ -427,9 +445,11 @@ var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C | ... | @@ -427,9 +445,11 @@ var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C |
| 427 | | 445 | |
| 428 | export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void { | 446 | export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void { |
| 429 | fuzzer_one = testOne; | 447 | fuzzer_one = testOne; |
| 430 | fuzzer.start() catch |err| switch (err) { | 448 | fuzzer.start() catch |err| oom(err); |
| 431 | error.OutOfMemory => fatal("out of memory", .{}), | 449 | } |
| 432 | }; | 450 | |
| | 451 | export fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void { |
| | 452 | fuzzer.unit_test_name = name_ptr[0..name_len]; |
| 433 | } | 453 | } |
| 434 | | 454 | |
| 435 | export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { | 455 | export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { |
| ... | @@ -472,6 +492,11 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { | ... | @@ -472,6 +492,11 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { |
| 472 | fatal("unable to init fuzzer: {s}", .{@errorName(err)}); | 492 | fatal("unable to init fuzzer: {s}", .{@errorName(err)}); |
| 473 | } | 493 | } |
| 474 | | 494 | |
| | 495 | export fn fuzzer_init_corpus_elem(input_ptr: [*]const u8, input_len: usize) void { |
| | 496 | fuzzer.addCorpusElem(input_ptr[0..input_len]) catch |err| |
| | 497 | fatal("failed to add corpus element: {s}", .{@errorName(err)}); |
| | 498 | } |
| | 499 | |
| 475 | /// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping. | 500 | /// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping. |
| 476 | pub const MemoryMappedList = struct { | 501 | pub const MemoryMappedList = struct { |
| 477 | /// Contents of the list. | 502 | /// Contents of the list. |
| ... | @@ -483,6 +508,8 @@ pub const MemoryMappedList = struct { | ... | @@ -483,6 +508,8 @@ pub const MemoryMappedList = struct { |
| 483 | items: []align(std.heap.page_size_min) volatile u8, | 508 | items: []align(std.heap.page_size_min) volatile u8, |
| 484 | /// How many bytes this list can hold without allocating additional memory. | 509 | /// How many bytes this list can hold without allocating additional memory. |
| 485 | capacity: usize, | 510 | capacity: usize, |
| | 511 | /// The file is kept open so that it can be resized. |
| | 512 | file: std.fs.File, |
| 486 | | 513 | |
| 487 | pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList { | 514 | pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList { |
| 488 | const ptr = try std.posix.mmap( | 515 | const ptr = try std.posix.mmap( |
| ... | @@ -494,11 +521,52 @@ pub const MemoryMappedList = struct { | ... | @@ -494,11 +521,52 @@ pub const MemoryMappedList = struct { |
| 494 | 0, | 521 | 0, |
| 495 | ); | 522 | ); |
| 496 | return .{ | 523 | return .{ |
| | 524 | .file = file, |
| 497 | .items = ptr[0..length], | 525 | .items = ptr[0..length], |
| 498 | .capacity = capacity, | 526 | .capacity = capacity, |
| 499 | }; | 527 | }; |
| 500 | } | 528 | } |
| 501 | | 529 | |
| | 530 | pub fn create(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList { |
| | 531 | try file.setEndPos(capacity); |
| | 532 | return init(file, length, capacity); |
| | 533 | } |
| | 534 | |
| | 535 | pub fn deinit(l: *MemoryMappedList) void { |
| | 536 | l.file.close(); |
| | 537 | std.posix.munmap(@volatileCast(l.items.ptr[0..l.capacity])); |
| | 538 | l.* = undefined; |
| | 539 | } |
| | 540 | |
| | 541 | /// Modify the array so that it can hold at least `additional_count` **more** items. |
| | 542 | /// Invalidates element pointers if additional memory is needed. |
| | 543 | pub fn ensureUnusedCapacity(l: *MemoryMappedList, additional_count: usize) !void { |
| | 544 | return l.ensureTotalCapacity(l.items.len + additional_count); |
| | 545 | } |
| | 546 | |
| | 547 | /// If the current capacity is less than `new_capacity`, this function will |
| | 548 | /// modify the array so that it can hold at least `new_capacity` items. |
| | 549 | /// Invalidates element pointers if additional memory is needed. |
| | 550 | pub fn ensureTotalCapacity(l: *MemoryMappedList, new_capacity: usize) !void { |
| | 551 | if (l.capacity >= new_capacity) return; |
| | 552 | |
| | 553 | const better_capacity = growCapacity(l.capacity, new_capacity); |
| | 554 | return l.ensureTotalCapacityPrecise(better_capacity); |
| | 555 | } |
| | 556 | |
| | 557 | pub fn ensureTotalCapacityPrecise(l: *MemoryMappedList, new_capacity: usize) !void { |
| | 558 | if (l.capacity >= new_capacity) return; |
| | 559 | |
| | 560 | std.posix.munmap(@volatileCast(l.items.ptr[0..l.capacity])); |
| | 561 | try l.file.setEndPos(new_capacity); |
| | 562 | l.* = try init(l.file, l.items.len, new_capacity); |
| | 563 | } |
| | 564 | |
| | 565 | /// Invalidates all element pointers. |
| | 566 | pub fn clearRetainingCapacity(l: *MemoryMappedList) void { |
| | 567 | l.items.len = 0; |
| | 568 | } |
| | 569 | |
| 502 | /// Append the slice of items to the list. | 570 | /// Append the slice of items to the list. |
| 503 | /// Asserts that the list can hold the additional items. | 571 | /// Asserts that the list can hold the additional items. |
| 504 | pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void { | 572 | pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void { |
| ... | @@ -509,6 +577,24 @@ pub const MemoryMappedList = struct { | ... | @@ -509,6 +577,24 @@ pub const MemoryMappedList = struct { |
| 509 | @memcpy(l.items[old_len..][0..items.len], items); | 577 | @memcpy(l.items[old_len..][0..items.len], items); |
| 510 | } | 578 | } |
| 511 | | 579 | |
| | 580 | /// Extends the list by 1 element. |
| | 581 | /// Never invalidates element pointers. |
| | 582 | /// Asserts that the list can hold one additional item. |
| | 583 | pub fn appendAssumeCapacity(l: *MemoryMappedList, item: u8) void { |
| | 584 | const new_item_ptr = l.addOneAssumeCapacity(); |
| | 585 | new_item_ptr.* = item; |
| | 586 | } |
| | 587 | |
| | 588 | /// Increase length by 1, returning pointer to the new item. |
| | 589 | /// The returned pointer becomes invalid when the list is resized. |
| | 590 | /// Never invalidates element pointers. |
| | 591 | /// Asserts that the list can hold one additional item. |
| | 592 | pub fn addOneAssumeCapacity(l: *MemoryMappedList) *volatile u8 { |
| | 593 | assert(l.items.len < l.capacity); |
| | 594 | l.items.len += 1; |
| | 595 | return &l.items[l.items.len - 1]; |
| | 596 | } |
| | 597 | |
| 512 | /// Append a value to the list `n` times. | 598 | /// Append a value to the list `n` times. |
| 513 | /// Never invalidates element pointers. | 599 | /// Never invalidates element pointers. |
| 514 | /// The function is inline so that a comptime-known `value` parameter will | 600 | /// The function is inline so that a comptime-known `value` parameter will |
| ... | @@ -520,4 +606,26 @@ pub const MemoryMappedList = struct { | ... | @@ -520,4 +606,26 @@ pub const MemoryMappedList = struct { |
| 520 | @memset(l.items.ptr[l.items.len..new_len], value); | 606 | @memset(l.items.ptr[l.items.len..new_len], value); |
| 521 | l.items.len = new_len; | 607 | l.items.len = new_len; |
| 522 | } | 608 | } |
| | 609 | |
| | 610 | /// Resize the array, adding `n` new elements, which have `undefined` values. |
| | 611 | /// The return value is a slice pointing to the newly allocated elements. |
| | 612 | /// Never invalidates element pointers. |
| | 613 | /// The returned pointer becomes invalid when the list is resized. |
| | 614 | /// Asserts that the list can hold the additional items. |
| | 615 | pub fn addManyAsSliceAssumeCapacity(l: *MemoryMappedList, n: usize) []volatile u8 { |
| | 616 | assert(l.items.len + n <= l.capacity); |
| | 617 | const prev_len = l.items.len; |
| | 618 | l.items.len += n; |
| | 619 | return l.items[prev_len..][0..n]; |
| | 620 | } |
| | 621 | |
| | 622 | /// Called when memory growth is necessary. Returns a capacity larger than |
| | 623 | /// minimum that grows super-linearly. |
| | 624 | fn growCapacity(current: usize, minimum: usize) usize { |
| | 625 | var new = current; |
| | 626 | while (true) { |
| | 627 | new = std.mem.alignForward(usize, new + new / 2, std.heap.page_size_max); |
| | 628 | if (new >= minimum) return new; |
| | 629 | } |
| | 630 | } |
| 523 | }; | 631 | }; |