| ... | ... | @@ -68,8 +68,7 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void { |
| 68 | 68 | const len = cases_ptr[0]; |
| 69 | 69 | const val_size_in_bits = cases_ptr[1]; |
| 70 | 70 | const cases = cases_ptr[2..][0..len]; |
| 71 | | _ = val; |
| 72 | | fuzzer.visitPc(pc); |
| 71 | fuzzer.traceValue(pc ^ val); |
| 73 | 72 | _ = val_size_in_bits; |
| 74 | 73 | _ = cases; |
| 75 | 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 | 77 | } |
| 79 | 78 | |
| 80 | 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 | 81 | _ = callee; |
| 83 | | fuzzer.visitPc(pc); |
| 82 | //const pc = @returnAddress(); |
| 83 | //fuzzer.traceValue(pc ^ callee); |
| 84 | 84 | //std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee }); |
| 85 | 85 | } |
| 86 | 86 | |
| 87 | 87 | fn handleCmp(pc: usize, arg1: u64, arg2: u64) void { |
| 88 | | fuzzer.visitPc(pc ^ arg1 ^ arg2); |
| 88 | fuzzer.traceValue(pc ^ arg1 ^ arg2); |
| 89 | 89 | //std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 }); |
| 90 | 90 | } |
| 91 | 91 | |
| 92 | 92 | const Fuzzer = struct { |
| 93 | | gpa: Allocator, |
| 94 | 93 | rng: std.Random.DefaultPrng, |
| 95 | | input: std.ArrayListUnmanaged(u8), |
| 96 | 94 | pcs: []const usize, |
| 97 | 95 | pc_counters: []u8, |
| 98 | 96 | n_runs: usize, |
| 99 | | recent_cases: RunMap, |
| 100 | | /// Data collected from code coverage instrumentation from one execution of |
| 101 | | /// the test function. |
| 102 | | coverage: Coverage, |
| 97 | traced_comparisons: std.AutoArrayHashMapUnmanaged(usize, void), |
| 103 | 98 | /// Tracks which PCs have been seen across all runs that do not crash the fuzzer process. |
| 104 | 99 | /// Stored in a memory-mapped file so that it can be shared with other |
| 105 | 100 | /// processes and viewed while the fuzzer is running. |
| ... | ... | @@ -108,42 +103,25 @@ const Fuzzer = struct { |
| 108 | 103 | /// Identifies the file name that will be used to store coverage |
| 109 | 104 | /// information, available to other processes. |
| 110 | 105 | coverage_id: u64, |
| 106 | unit_test_name: []const u8, |
| 111 | 107 | |
| 112 | | const RunMap = std.ArrayHashMapUnmanaged(Run, void, Run.HashContext, false); |
| 113 | | |
| 114 | | const Coverage = struct { |
| 115 | | pc_table: std.AutoArrayHashMapUnmanaged(usize, void), |
| 116 | | run_id_hasher: std.hash.Wyhash, |
| 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, |
| 108 | /// The index corresponds to the file name within the f/ subdirectory. |
| 109 | /// The string is the input. |
| 110 | /// This data is read-only; it caches what is on the filesystem. |
| 111 | corpus: std.ArrayListUnmanaged(Input), |
| 112 | corpus_directory: std.Build.Cache.Directory, |
| 128 | 113 | |
| 129 | | const Id = u64; |
| 130 | | |
| 131 | | const HashContext = struct { |
| 132 | | pub fn eql(ctx: HashContext, a: Run, b: Run, b_index: usize) bool { |
| 133 | | _ = b_index; |
| 134 | | _ = ctx; |
| 135 | | return a.id == b.id; |
| 136 | | } |
| 137 | | pub fn hash(ctx: HashContext, a: Run) u32 { |
| 138 | | _ = ctx; |
| 139 | | return @truncate(a.id); |
| 140 | | } |
| 141 | | }; |
| 114 | /// The next input that will be given to the testOne function. When the |
| 115 | /// current process crashes, this memory-mapped file is used to recover the |
| 116 | /// input. |
| 117 | /// |
| 118 | /// The file size corresponds to the capacity. The length is not stored |
| 119 | /// and that is the next thing to work on! |
| 120 | input: MemoryMappedList, |
| 142 | 121 | |
| 143 | | fn deinit(run: *Run, gpa: Allocator) void { |
| 144 | | gpa.free(run.input); |
| 145 | | run.* = undefined; |
| 146 | | } |
| 122 | const Input = struct { |
| 123 | bytes: []u8, |
| 124 | last_traced_comparison: usize, |
| 147 | 125 | }; |
| 148 | 126 | |
| 149 | 127 | const Slice = extern 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 | 143 | fn init(f: *Fuzzer, cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []const usize) !void { |
| 171 | 144 | f.cache_dir = cache_dir; |
| 172 | 145 | f.pc_counters = pc_counters; |
| ... | ... | @@ -186,7 +159,6 @@ const Fuzzer = struct { |
| 186 | 159 | .read = true, |
| 187 | 160 | .truncate = false, |
| 188 | 161 | }); |
| 189 | | defer coverage_file.close(); |
| 190 | 162 | const n_bitset_elems = (pcs.len + @bitSizeOf(usize) - 1) / @bitSizeOf(usize); |
| 191 | 163 | comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize); |
| 192 | 164 | comptime assert(SeenPcsHeader.trailing[1] == .pc_addr); |
| ... | ... | @@ -228,156 +200,196 @@ const Fuzzer = struct { |
| 228 | 200 | } |
| 229 | 201 | } |
| 230 | 202 | |
| 231 | | fn analyzeLastRun(f: *Fuzzer) Analysis { |
| 232 | | return .{ |
| 233 | | .id = f.coverage.run_id_hasher.final(), |
| 234 | | .score = f.coverage.pc_table.count(), |
| 235 | | }; |
| 203 | fn initNextInput(f: *Fuzzer) void { |
| 204 | while (true) { |
| 205 | const i = f.corpus.items.len; |
| 206 | var buf: [30]u8 = undefined; |
| 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 | 245 | fn start(f: *Fuzzer) !void { |
| 239 | | const gpa = f.gpa; |
| 240 | 246 | const rng = fuzzer.rng.random(); |
| 241 | 247 | |
| 242 | | // Prepare initial input. |
| 243 | | assert(f.recent_cases.entries.len == 0); |
| 248 | // Grab the corpus which is namespaced based on `unit_test_name`. |
| 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 | 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 | 275 | 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(); |
| 276 | const chosen_index = rng.uintLessThanBiased(usize, f.corpus.items.len); |
| 277 | const modification = rng.enumValue(Mutation); |
| 278 | f.mutateAndRunOne(chosen_index, modification); |
| 279 | } |
| 280 | } |
| 263 | 281 | |
| 264 | | @memset(f.pc_counters, 0); |
| 265 | | __sancov_lowest_stack = std.math.maxInt(usize); |
| 266 | | f.coverage.reset(); |
| 282 | /// `x` represents a possible branch. It is the PC address of the possible |
| 283 | /// branch site, hashed together with the value(s) used that determine to |
| 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; |
| 271 | | _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic); |
| 296 | fn mutateAndRunOne(f: *Fuzzer, corpus_index: usize, mutation: Mutation) void { |
| 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(); |
| 276 | | const gop = f.recent_cases.getOrPutAssumeCapacity(.{ |
| 277 | | .id = analysis.id, |
| 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 | | } |
| 325 | f.traced_comparisons.clearRetainingCapacity(); |
| 326 | @memset(f.pc_counters, 0); |
| 327 | __sancov_lowest_stack = std.math.maxInt(usize); |
| 322 | 328 | |
| 323 | | _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); |
| 324 | | } |
| 329 | fuzzer_one(@volatileCast(f.input.items.ptr), f.input.items.len); |
| 325 | 330 | |
| 326 | | if (f.recent_cases.entries.len >= 100) { |
| 327 | | const Context = struct { |
| 328 | | values: []const Run, |
| 329 | | pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { |
| 330 | | return ctx.values[b_index].score < ctx.values[a_index].score; |
| 331 | | } |
| 332 | | }; |
| 333 | | f.recent_cases.sortUnstable(Context{ .values = f.recent_cases.keys() }); |
| 334 | | const cap = 50; |
| 335 | | // This has to be done before deinitializing the deleted items. |
| 336 | | const doomed_runs = f.recent_cases.keys()[cap..]; |
| 337 | | f.recent_cases.shrinkRetainingCapacity(cap); |
| 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); |
| 341 | | } |
| 331 | f.n_runs += 1; |
| 332 | _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic); |
| 333 | |
| 334 | // Track code coverage from all runs. |
| 335 | comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize); |
| 336 | const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.items[@sizeOf(SeenPcsHeader)..]); |
| 337 | const remainder = f.pcs.len % @bitSizeOf(usize); |
| 338 | const aligned_len = f.pcs.len - remainder; |
| 339 | const seen_pcs = header_end_ptr[0..aligned_len]; |
| 340 | const pc_counters = std.mem.bytesAsSlice([@bitSizeOf(usize)]u8, f.pc_counters[0..aligned_len]); |
| 341 | const V = @Vector(@bitSizeOf(usize), u8); |
| 342 | const zero_v: V = @splat(0); |
| 343 | var fresh = false; |
| 344 | var superset = true; |
| 345 | |
| 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 { |
| 353 | | for (f.recent_cases.keys()[0..@min(f.recent_cases.entries.len, 5)], 0..) |run, i| { |
| 354 | | std.log.info("best[{d}] id={x} score={d} input: '{}'", .{ |
| 355 | | i, run.id, run.score, std.zig.fmtEscapes(run.input), |
| 356 | | }); |
| 365 | // First check if this is a better version of an already existing |
| 366 | // input, replacing that input. |
| 367 | if (superset or f.traced_comparisons.entries.len >= f.corpus.items[corpus_index].last_traced_comparison) { |
| 368 | const new_input = gpa.realloc(f.corpus.items[corpus_index].bytes, f.input.items.len) catch |err| oom(err); |
| 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 { |
| 361 | | const gpa = f.gpa; |
| 362 | | const rng = fuzzer.rng.random(); |
| 378 | if (!fresh) return; |
| 363 | 379 | |
| 364 | | if (f.input.items.len == 0) { |
| 365 | | const len = rng.uintLessThanBiased(usize, 80); |
| 366 | | try f.input.resize(gpa, len); |
| 367 | | rng.bytes(f.input.items); |
| 368 | | return; |
| 369 | | } |
| 380 | // Input is already committed to the file system, we just need to open a new file |
| 381 | // for the next input. |
| 382 | // Pre-add it to the corpus list so that it does not get redundantly picked up. |
| 383 | f.corpus.append(gpa, .{ |
| 384 | .bytes = gpa.dupe(u8, @volatileCast(f.input.items)) catch |err| oom(err), |
| 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); |
| 372 | | if (index < f.input.items.len) { |
| 373 | | f.input.items[index] = rng.int(u8); |
| 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 | | } |
| 390 | // TODO: also mark input as "hot" so it gets prioritized for checking mutations above others. |
| 391 | |
| 392 | _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); |
| 381 | 393 | } |
| 382 | 394 | }; |
| 383 | 395 | |
| ... | ... | @@ -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 | 424 | var fuzzer: Fuzzer = .{ |
| 408 | | .gpa = general_purpose_allocator.allocator(), |
| 409 | 425 | .rng = std.Random.DefaultPrng.init(0), |
| 410 | | .input = .{}, |
| 426 | .input = undefined, |
| 411 | 427 | .pcs = undefined, |
| 412 | 428 | .pc_counters = undefined, |
| 413 | 429 | .n_runs = 0, |
| 414 | | .recent_cases = .{}, |
| 415 | | .coverage = undefined, |
| 416 | 430 | .cache_dir = undefined, |
| 417 | 431 | .seen_pcs = undefined, |
| 418 | 432 | .coverage_id = undefined, |
| 433 | .unit_test_name = &.{}, |
| 434 | .corpus = .empty, |
| 435 | .corpus_directory = undefined, |
| 436 | .traced_comparisons = .empty, |
| 419 | 437 | }; |
| 420 | 438 | |
| 421 | 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 | 445 | |
| 428 | 446 | export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void { |
| 429 | 447 | fuzzer_one = testOne; |
| 430 | | fuzzer.start() catch |err| switch (err) { |
| 431 | | error.OutOfMemory => fatal("out of memory", .{}), |
| 432 | | }; |
| 448 | fuzzer.start() catch |err| oom(err); |
| 449 | } |
| 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 | 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 | 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 | 500 | /// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping. |
| 476 | 501 | pub const MemoryMappedList = struct { |
| 477 | 502 | /// Contents of the list. |
| ... | ... | @@ -483,6 +508,8 @@ pub const MemoryMappedList = struct { |
| 483 | 508 | items: []align(std.heap.page_size_min) volatile u8, |
| 484 | 509 | /// How many bytes this list can hold without allocating additional memory. |
| 485 | 510 | capacity: usize, |
| 511 | /// The file is kept open so that it can be resized. |
| 512 | file: std.fs.File, |
| 486 | 513 | |
| 487 | 514 | pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList { |
| 488 | 515 | const ptr = try std.posix.mmap( |
| ... | ... | @@ -494,11 +521,52 @@ pub const MemoryMappedList = struct { |
| 494 | 521 | 0, |
| 495 | 522 | ); |
| 496 | 523 | return .{ |
| 524 | .file = file, |
| 497 | 525 | .items = ptr[0..length], |
| 498 | 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 | 570 | /// Append the slice of items to the list. |
| 503 | 571 | /// Asserts that the list can hold the additional items. |
| 504 | 572 | pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void { |
| ... | ... | @@ -509,6 +577,24 @@ pub const MemoryMappedList = struct { |
| 509 | 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 | 598 | /// Append a value to the list `n` times. |
| 513 | 599 | /// Never invalidates element pointers. |
| 514 | 600 | /// The function is inline so that a comptime-known `value` parameter will |
| ... | ... | @@ -520,4 +606,26 @@ pub const MemoryMappedList = struct { |
| 520 | 606 | @memset(l.items.ptr[l.items.len..new_len], value); |
| 521 | 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 | }; |