diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index 9bd5366af621..f0492b259a09 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -345,7 +345,7 @@ var is_fuzz_test: bool = undefined; extern fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void; extern fn fuzzer_init(cache_dir: FuzzerSlice) void; extern fn fuzzer_init_corpus_elem(input_ptr: [*]const u8, input_len: usize) void; -extern fn fuzzer_start(testOne: *const fn ([*]const u8, usize) callconv(.C) void) void; +extern fn fuzzer_start(testOne: *const fn ([*]const u8, usize) callconv(.C) void, usize, usize) void; extern fn fuzzer_coverage_id() u64; pub fn fuzz( @@ -398,6 +398,26 @@ pub fn fuzz( } } }; + + if (options.len_range.min > options.len_range.max) { + std.debug.lockStdErr(); + std.debug.print("input length range minimum greater than maximum\n", .{}); + std.process.exit(1); + } + + for (options.corpus, 0..) |input, i| { + if (input.len < options.len_range.min) { + std.debug.lockStdErr(); + std.debug.print("length of corpus entry {} is less than minimum size\n", .{i}); + std.process.exit(1); + } + if (input.len > options.len_range.max) { + std.debug.lockStdErr(); + std.debug.print("length of corpus entry {} is greater than maximum size\n", .{i}); + std.process.exit(1); + } + } + if (builtin.fuzz) { const prev_allocator_state = testing.allocator_instance; testing.allocator_instance = .{}; @@ -406,7 +426,11 @@ pub fn fuzz( for (options.corpus) |elem| fuzzer_init_corpus_elem(elem.ptr, elem.len); global.ctx = context; - fuzzer_start(&global.fuzzer_one); + fuzzer_start( + &global.fuzzer_one, + options.len_range.min, + options.len_range.max, + ); return; } @@ -416,7 +440,12 @@ pub fn fuzz( try testOne(context, input); } - // In case there is no provided corpus, also use an empty + // In case there is no provided corpus, also use a random // string as a smoke test. - try testOne(context, ""); + var random = std.Random.DefaultPrng.init(0); + const rng = random.random(); + const input = try testing.allocator.alloc(u8, options.len_range.min); + defer testing.allocator.free(input); + rng.bytes(input); + try testOne(context, input); } diff --git a/lib/fuzzer.zig b/lib/fuzzer.zig index 0c287c6afc14..59c607299060 100644 --- a/lib/fuzzer.zig +++ b/lib/fuzzer.zig @@ -130,6 +130,10 @@ const Fuzzer = struct { /// The file size corresponds to the capacity. The length is not stored /// and that is the next thing to work on! input: MemoryMappedList, + input_len_range: struct { + min: usize, + max: usize, + }, const Input = struct { bytes: []u8, @@ -217,7 +221,7 @@ const Fuzzer = struct { const i = f.corpus.items.len; var buf: [30]u8 = undefined; const input_sub_path = std.fmt.bufPrint(&buf, "{d}", .{i}) catch unreachable; - const input = f.corpus_directory.handle.readFileAlloc(gpa, input_sub_path, 1 << 31) catch |err| switch (err) { + var input = f.corpus_directory.handle.readFileAlloc(gpa, input_sub_path, 1 << 31) catch |err| switch (err) { error.FileNotFound => { // Make this one the next input. const input_file = f.corpus_directory.handle.createFile(input_sub_path, .{ @@ -240,6 +244,7 @@ const Fuzzer = struct { else => fatal("unable to read '{}{d}': {s}", .{ f.corpus_directory, i, @errorName(err) }), }; errdefer gpa.free(input); + input = gpa.realloc(input, f.input_len_range.max) catch fatal("unable to reallocate input '{}{d}'", .{ f.corpus_directory, i }); f.corpus.append(gpa, .{ .bytes = input, .last_traced_comparison = 0, @@ -273,7 +278,10 @@ const Fuzzer = struct { // If the corpus is empty, synthesize one input. if (f.corpus.items.len == 0) { - const len = rng.uintLessThanBiased(usize, 200); + const len = f.input_len_range.min + rng.uintLessThanBiased( + usize, + (f.input_len_range.max - f.input_len_range.min) + 1, + ); const slice = try gpa.alloc(u8, len); rng.bytes(slice); f.input.appendSliceAssumeCapacity(slice); @@ -309,8 +317,16 @@ const Fuzzer = struct { const rng = fuzzer.rng.random(); f.input.clearRetainingCapacity(); const old_input = f.corpus.items[corpus_index].bytes; + // In cases where we cannot add or remove bytes due to the bounds on input length, + // modify a byte instead. + const actual_mutation = if (old_input.len == f.input_len_range.min and mutation == .remove_byte) + .modify_byte + else if (old_input.len == f.input_len_range.max and mutation == .add_byte) + .modify_byte + else + mutation; f.input.ensureTotalCapacity(old_input.len + 1) catch @panic("mmap file resize failed"); - switch (mutation) { + switch (actual_mutation) { .remove_byte => { const omitted_index = rng.uintLessThanBiased(usize, old_input.len); f.input.appendSliceAssumeCapacity(old_input[0..omitted_index]); @@ -328,6 +344,8 @@ const Fuzzer = struct { f.input.appendSliceAssumeCapacity(old_input[modified_index..]); }, } + assert(f.input.items.len >= f.input_len_range.min); + assert(f.input.items.len <= f.input_len_range.max); runOne(f, corpus_index); } @@ -446,6 +464,7 @@ var fuzzer: Fuzzer = .{ .corpus = .empty, .corpus_directory = undefined, .traced_comparisons = .empty, + .input_len_range = undefined, }; /// Invalid until `fuzzer_init` is called. @@ -455,8 +474,16 @@ export fn fuzzer_coverage_id() u64 { var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C) void = undefined; -export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void { +export fn fuzzer_start( + testOne: @TypeOf(fuzzer_one), + min_input_length: usize, + max_input_length: usize, +) void { fuzzer_one = testOne; + fuzzer.input_len_range = .{ + .min = min_input_length, + .max = max_input_length, + }; fuzzer.start() catch |err| oom(err); } diff --git a/lib/std/testing.zig b/lib/std/testing.zig index a3b14d18bde2..36fbe9afe465 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -1165,6 +1165,10 @@ pub fn refAllDeclsRecursive(comptime T: type) void { pub const FuzzInputOptions = struct { corpus: []const []const u8 = &.{}, + len_range: struct { + min: usize = 0, + max: usize = 200, + } = .{}, }; /// Inline to avoid coverage instrumentation.