This commit is contained in:
Prokop Randacek 2024-11-12 19:08:40 +01:00
parent 3d05a4631a
commit 26dc65a345
No known key found for this signature in database
GPG Key ID: 25C91C6F81E82453
8 changed files with 328 additions and 351 deletions

View File

@ -341,7 +341,10 @@ const FuzzerSlice = extern struct {
var is_fuzz_test: bool = undefined;
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,
options: *const std.testing.FuzzInputOptions,
) void;
extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
extern fn fuzzer_coverage_id() u64;
@ -395,7 +398,7 @@ pub fn fuzz(
if (builtin.fuzz) {
const prev_allocator_state = testing.allocator_instance;
testing.allocator_instance = .{};
fuzzer_start(&global.fuzzer_one);
fuzzer_start(&global.fuzzer_one, &options);
testing.allocator_instance = prev_allocator_state;
return;
}

View File

@ -3,7 +3,6 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const fatal = std.process.fatal;
const check = @import("fuzzer/main.zig").check;
const Fuzzer = @import("fuzzer/main.zig").Fuzzer;
const Slice = @import("fuzzer/main.zig").Slice;
const fc = @import("fuzzer/feature_capture.zig");
@ -12,8 +11,6 @@ const fc = @import("fuzzer/feature_capture.zig");
var log_file: ?std.fs.File = null;
var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .{};
var fuzzer: Fuzzer = undefined;
// ==== llvm callbacks ====
@ -65,13 +62,20 @@ export fn fuzzer_coverage_id() u64 {
return fuzzer.coverage_id;
}
/// Called before each invocation of the user's code
export fn fuzzer_next(options: *const std.testing.FuzzInputOptions) Slice {
// TODO: probably just call fatal instead of propagating errors up here
return Slice.fromZig(fuzzer.next(options));
export fn fuzzer_start(
testOne: *const fn ([*]const u8, usize) callconv(.C) void,
options: *const std.testing.FuzzInputOptions,
) void {
fuzzer.start(testOne, options.*) catch |e| switch (e) {
error.OutOfMemory => {
std.debug.print("fuzzer OOM\n", .{});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
},
};
}
/// Called once
export fn fuzzer_init(cache_dir_struct: Slice) void {
// setup log file as soon as possible
const cache_dir_path = cache_dir_struct.toZig();
@ -110,22 +114,33 @@ export fn fuzzer_init(cache_dir_struct: Slice) void {
const pcs = pcs_start[0 .. pcs_end - pcs_start];
fuzzer = Fuzzer.init(general_purpose_allocator.allocator(), cache_dir, pc_counters, pcs);
fuzzer = Fuzzer.init(
cache_dir,
pc_counters,
pcs,
) catch |e| switch (e) {
error.OutOfMemory => {
std.debug.print("fuzzer OOM\n", .{});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
std.process.exit(1);
},
};
}
export fn fuzzer_deinit() void {
fuzzer.deinit();
}
export fn fuzzer_deinit() void {}
// ==== log ====
pub const std_options = .{
pub const std_options = std.Options{
.logFn = logOverride,
.log_level = .debug,
};
fn setupLogFile(cachedir: std.fs.Dir) void {
log_file = check(@src(), cachedir.createFile("tmp/libfuzzer.log", .{}), .{});
log_file = cachedir.createFile("tmp/libfuzzer.log", .{}) catch
@panic("create log file failed"); // cant log details because log file is not setup
}
fn logOverride(

View File

@ -13,7 +13,7 @@
const std = @import("std");
const assert = std.debug.assert;
const check = @import("main.zig").check;
const fatal = std.process.fatal;
const MemoryMappedList = @import("memory_mapped_list.zig").MemoryMappedList;
/// maximum 2GiB of input data should be enough. 32th bit is delete flag
@ -68,14 +68,14 @@ pub fn init(dir: std.fs.Dir, pc_digest: u64) InputPoolPosix {
const buffer_file_path = "v/" ++ hex_digest ++ "buffer";
const meta_file_path = "v/" ++ hex_digest ++ "meta";
const buffer_file = check(@src(), dir.createFile(buffer_file_path, .{
const buffer_file = dir.createFile(buffer_file_path, .{
.read = true,
.truncate = false,
}), .{ .file = buffer_file_path });
const meta_file = check(@src(), dir.createFile(meta_file_path, .{
}) catch |e| fatal("create file at '{s}' failed: {}", .{ buffer_file_path, e });
const meta_file = dir.createFile(meta_file_path, .{
.read = true,
.truncate = false,
}), .{ .file = meta_file_path });
}) catch |e| fatal("create file at '{s}' failed: {}", .{ meta_file_path, e });
const buffer = MemoryMappedList(u8).init(buffer_file, std.math.maxInt(Index));
var meta = MemoryMappedList(u32).init(meta_file, std.math.maxInt(Index));

View File

@ -2,6 +2,7 @@
// (buffer + meta file pair in the .zig-cache/v/ directory)
const std = @import("std");
const fatal = std.process.fatal;
const InputPool = @import("input_pool.zig").InputPool;
@ -12,16 +13,16 @@ pub fn main() void {
const pc_digest_str = args.next();
if (cache_dir_path == null or pc_digest_str == null or args.next() != null) {
std.process.fatal("usage: {s} CACHE_DIR PC_DIGEST\n", .{bin.?});
fatal("usage: {s} CACHE_DIR PC_DIGEST\n", .{bin.?});
}
// std.fmt.hex actually produces the hex number in the opposite order than
// parseInt reads...
const pc_digest = @byteSwap(std.fmt.parseInt(u64, pc_digest_str.?, 16) catch |e|
std.process.fatal("invalid pc digest: {}", .{e}));
fatal("invalid pc digest: {}", .{e}));
const cache_dir = std.fs.cwd().makeOpenPath(cache_dir_path.?, .{}) catch |e|
std.process.fatal("invalid cache dir: {}", .{e});
fatal("invalid cache dir: {}", .{e});
std.log.info("cache_dir: {s}", .{cache_dir_path.?});
std.log.info("pc_digest: {x}", .{@byteSwap(pc_digest)});

View File

@ -1,54 +1,26 @@
const builtin = @import("builtin");
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const SeenPcsHeader = std.Build.Fuzz.abi.SeenPcsHeader;
const MemoryMappedList = @import("memory_mapped_list.zig").MemoryMappedList;
const builtin = @import("builtin");
const mutate = @import("mutate.zig");
const InputPool = @import("input_pool.zig").InputPool;
const MemoryMappedList = @import("memory_mapped_list.zig").MemoryMappedList;
const feature_capture = @import("feature_capture.zig");
// current unused
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const Options = std.testing.FuzzInputOptions;
const Prng = std.Random.DefaultPrng;
const SeenPcsHeader = std.Build.Fuzz.abi.SeenPcsHeader;
const assert = std.debug.assert;
const fatal = std.process.fatal;
const Testee = *const fn ([*]const u8, usize) callconv(.C) void;
const InitialFeatureBufferCap = 64;
// currently unused
export threadlocal var __sancov_lowest_stack: usize = std.math.maxInt(usize);
/// Returns error union payload or void if error set
fn StripError(comptime T: type) type {
return switch (@typeInfo(T)) {
.error_union => |eu| eu.payload,
.error_set => void,
else => @compileError("no error to strip"),
};
}
/// Checks that the value is not error. If it is error, it logs the args and
/// terminates
pub fn check(src: std.builtin.SourceLocation, v: anytype, args: anytype) StripError(@TypeOf(v)) {
return v catch |e| {
var buffer: [4096]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buffer);
var cw = std.io.countingWriter(fbs.writer());
const w = cw.writer();
if (@typeInfo(@TypeOf(args)).@"struct".fields.len != 0) {
w.writeAll(" (") catch {};
inline for (@typeInfo(@TypeOf(args)).@"struct".fields, 0..) |field, i| {
const Field = @TypeOf(@field(args, field.name));
if (i != 0) {
w.writeAll(", ") catch {};
}
if (Field == []const u8 or Field == []u8) {
w.print("{s}='{s}'", .{ field.name, @field(args, field.name) }) catch {};
} else {
w.print("{s}={any}", .{ field.name, @field(args, field.name) }) catch {};
}
}
w.writeAll(")") catch {};
}
std.process.fatal("{s}:{}: {s}{s}", .{ src.file, src.line, @errorName(e), buffer[0..cw.bytes_written] });
};
}
/// Type for passing slices across extern functions where we can't use zig
/// types
pub const Slice = extern struct {
@ -71,14 +43,13 @@ fn createFileBail(dir: std.fs.Dir, sub_path: []const u8, flags: std.fs.File.Crea
return dir.createFile(sub_path, flags) catch |err| switch (err) {
error.FileNotFound => {
const dir_name = std.fs.path.dirname(sub_path).?;
check(@src(), dir.makePath(dir_name), .{ .dir_name = dir_name });
return check(@src(), dir.createFile(sub_path, flags), .{ .sub_path = sub_path, .flags = flags });
dir.makePath(dir_name) catch |e| fatal("makePath '{s}' failed: {}", .{ dir_name, e });
return dir.createFile(sub_path, flags) catch |e| fatal("createFile '{s}' failed: {}", .{ sub_path, e });
},
else => |e| std.process.fatal("create file '{s}' failed: {}", .{ sub_path, e }),
else => |e| fatal("create file '{s}' failed: {}", .{ sub_path, e }),
};
}
/// Sorts array of features
fn sort(a: []u32) void {
std.mem.sort(u32, a, void{}, std.sort.asc(u32));
}
@ -110,6 +81,14 @@ test uniq {
try std.testing.expectEqualSlices(u32, &[_]u32{ 0, 1, 2, 3, 4 }, cropped);
}
/// sorted and dedeuplicated
fn getLastRunFeatures() []u32 {
var features = feature_capture.values();
sort(features);
features = uniq(features);
return features;
}
pub const CmpResult = struct { only_a: u32, only_b: u32, both: u32 };
/// Compares two sorted lists of features
@ -169,8 +148,8 @@ test cmp {
/// Merges the second sorted list of features into the first list of sorted
/// features
fn merge(dest: *std.ArrayList(u32), src: []const u32) !void {
// TODO: can be in O(n) time and O(1) space
fn merge(dest: *ArrayList(u32), src: []const u32) error{OutOfMemory}!void {
// TODO: can be in O(n) time and O(1) extra space
try dest.appendSlice(src);
sort(dest.items);
dest.items = uniq(dest.items);
@ -178,7 +157,7 @@ fn merge(dest: *std.ArrayList(u32), src: []const u32) !void {
fn hashPCs(pcs: []const usize) u64 {
var hasher = std.hash.Wyhash.init(0);
hasher.update(std.mem.asBytes(pcs));
hasher.update(std.mem.sliceAsBytes(pcs));
return hasher.final();
}
@ -206,7 +185,7 @@ fn initCoverageFile(cache_dir: std.fs.Dir, coverage_file_path: []const u8, pcs:
const existing_len = seen_pcs.items.len;
if (existing_len != 0 and existing_len != bytes_len)
std.process.fatal("coverage file '{s}' is invalid (wrong length)", .{coverage_file_path});
fatal("coverage file '{s}' is invalid (wrong length)", .{coverage_file_path});
if (existing_len != 0) {
// check existing file is ok
@ -214,7 +193,7 @@ fn initCoverageFile(cache_dir: std.fs.Dir, coverage_file_path: []const u8, pcs:
const existing_pcs = std.mem.bytesAsSlice(usize, existing_pcs_bytes);
for (existing_pcs, pcs) |old, new| {
if (old != new) {
std.process.fatal("coverage file '{s}' is invalid (pc missmatch)", .{coverage_file_path});
fatal("coverage file '{s}' is invalid (pc missmatch)", .{coverage_file_path});
}
}
} else {
@ -275,280 +254,255 @@ fn incrementNumberOfRuns(seen_pcs: MemoryMappedList(u8)) void {
_ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
}
const InitialFeatureBufferCap = 64;
fn initialCorpusRandom(ip: *InputPool, rng: *Prng) void {
var buffer: [256]u8 = undefined;
for (0..256) |len| {
const slice = buffer[0..len];
rng.fill(slice);
ip.insertString(slice);
}
// TODO: could prune
}
fn selectInputIndex(ip: *InputPool, rng: *Prng) InputPool.Index {
const len = ip.len();
assert(len != 0);
const index = rng.next() % len;
return @intCast(index);
}
fn selectAndCopyInput(
a: Allocator,
ip: *InputPool,
rng: *Prng,
input_: ArrayListUnmanaged(u8),
) !ArrayListUnmanaged(u8) {
var input = input_;
const new_input_index = selectInputIndex(ip, rng);
const new_input = ip.getString(new_input_index);
// manual slice copy since appendSlice doesn't take volatile slice
input.clearRetainingCapacity();
try input.ensureTotalCapacity(a, new_input.len);
input.items.len = new_input.len;
@memcpy(input.items, new_input);
return input;
}
fn logNewFeatures(
seen_pcs: MemoryMappedList(u8),
features: []u32,
mutation_seed: u64,
total_features: usize,
) void {
var buffer: [128]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
var ar = ArrayList(u8).init(fba.allocator());
mutate.writeMutation(mutation_seed, ar.writer()) catch {};
std.log.info("new unique run: F:{} \tT:{} \t{s}", .{
features.len,
total_features,
ar.items,
});
incrementUniqueRuns(seen_pcs);
}
fn checksum(str: []const u8) u8 {
// this is very bad checksum but since we run the user's code a lot, it
// will eventually catch when they do it.
var c: u8 = 0;
for (str) |s| {
c ^= s;
}
return c;
}
fn collectPcCounterFeatures(pc_counters: []u8) void {
for (pc_counters, 0..) |counter, i_| {
if (counter != 0) {
const i: u32 = @intCast(i_);
// TODO: does this do a lot of collisions?
feature_capture.newFeature(std.hash.uint32(i));
}
}
}
fn beforeRun(pc_counters: []u8, feature_buffer: []u32) void {
@memset(pc_counters, 0);
feature_capture.prepare(feature_buffer);
}
fn growFeatureBuffer(a: Allocator, feature_buffer: *ArrayListUnmanaged(u32)) !void {
// avoid copying data
const new_size = feature_buffer.items.len * 2;
feature_buffer.clearRetainingCapacity();
try feature_buffer.ensureTotalCapacity(a, new_size);
}
fn runInput(
a: Allocator,
test_one: Testee,
feature_buffer: *ArrayListUnmanaged(u32),
pc_counters: []u8,
input: []const u8,
) !void {
// loop for run retry
while (true) {
beforeRun(pc_counters, feature_buffer.items);
test_one(input.ptr, input.len);
collectPcCounterFeatures(pc_counters);
if (feature_capture.is_full()) {
try growFeatureBuffer(a, feature_buffer);
// rerun same input with larger buffer
continue;
}
break;
}
}
/// Returns true when the new features are interesting
fn analyzeFeatures(ip: *InputPool, features: []u32, input: []const u8, all_features: []const u32) bool {
const analysis = cmp(features, all_features);
if (analysis.only_a > 0) {
ip.insertString(input);
return true;
}
return false; // boring input
}
fn mergeInput(
a: Allocator,
seen_pcs: MemoryMappedList(u8),
all_features: *ArrayListUnmanaged(u32),
features: []u32,
pc_counters: []u8,
) error{OutOfMemory}!void {
var ar = all_features.toManaged(a);
try merge(&ar, features);
all_features.* = ar.moveToUnmanaged();
updateGlobalCoverage(pc_counters, seen_pcs);
}
pub const Fuzzer = struct {
gpa: Allocator,
rng: std.Random.DefaultPrng,
cache_dir: std.fs.Dir,
input_pool: InputPool,
mutate_scratch: ArrayListUnmanaged(u8) = .{},
mutation_seed: u64 = undefined,
mutation_len: usize = undefined,
current_input: ArrayListUnmanaged(u8) = .{},
current_input_checksum: u8 = undefined,
feature_buffer: []u32 = undefined,
all_features: ArrayListUnmanaged(u32) = .{},
// given to us by LLVM
pcs: []const usize,
pc_counters: []u8, // same length as pcs
n_runs: usize = 0,
/// Tracks which PCs have been seen across all runs that do not crash the fuzzer process.
/// Stored in a memory-mapped file so that it can be shared with other
/// processes and viewed while the fuzzer is running.
seen_pcs: MemoryMappedList(u8),
/// Identifies the file name that will be used to store coverage
/// information, available to other processes.
coverage_id: u64,
first_run: bool = true,
cache_dir: std.fs.Dir,
/// When we boot, we need to iterate over all corpus inputs and run them
/// once, populating initial feature set. When we are walking the corpus,
/// this variable stores current input index. After the walk is done, we
/// set it to null
corpus_walk: ?usize = null,
pub fn init(gpa: Allocator, cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []usize) Fuzzer {
pub fn init(cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []usize) error{OutOfMemory}!Fuzzer {
assert(pc_counters.len == pcs.len);
// Choose a file name for the coverage based on a hash of the PCs that
// will be stored within.
const pc_digest = hashPCs(pcs);
const coverage_id = pc_digest;
const hex_digest = std.fmt.hex(pc_digest);
const coverage_file_path = "v/" ++ hex_digest ++ "coverage";
const feature_buffer = check(@src(), gpa.alloc(u32, InitialFeatureBufferCap), .{});
const seen_pcs = initCoverageFile(cache_dir, coverage_file_path, pcs);
const input_pool = InputPool.init(cache_dir, pc_digest);
return .{
.gpa = gpa,
.rng = std.Random.DefaultPrng.init(0),
.coverage_id = coverage_id,
.cache_dir = cache_dir,
.pcs = pcs,
.pc_counters = pc_counters,
.seen_pcs = seen_pcs,
.feature_buffer = feature_buffer,
.input_pool = input_pool,
.cache_dir = cache_dir,
.coverage_id = hashPCs(pcs),
};
}
pub fn deinit(f: *Fuzzer) void {
f.input_pool.deinit();
f.seen_pcs.deinit();
}
pub fn start(f: *Fuzzer, test_one: Testee, options: Options) error{OutOfMemory}!void {
// we are a well behaved program
greet();
defer farewell();
fn readOptions(f: *Fuzzer, options: *const std.testing.FuzzInputOptions) void {
// Otherwise the options corpus would be re-added every time we restart
// the fuzzer
if (f.input_pool.len() == 0) {
for (options.corpus) |input| {
f.input_pool.insertString(input);
}
}
}
var rng = Prng.init(0);
var gpa_impl: std.heap.GeneralPurposeAllocator(.{}) = .{};
const a = gpa_impl.allocator();
fn makeUpInitialCorpus(f: *Fuzzer) void {
var buffer: [256]u8 = undefined;
for (0..256) |len| {
const slice = buffer[0..len];
f.rng.fill(slice);
f.input_pool.insertString(slice);
}
// TODO: prune
}
var input: ArrayListUnmanaged(u8) = .{};
defer input.deinit(a);
fn pickInput(f: *Fuzzer) InputPool.Index {
const input_pool_len = f.input_pool.len();
assert(input_pool_len != 0);
var mutate_scratch: ArrayListUnmanaged(u8) = .{};
defer mutate_scratch.deinit(a);
if (f.corpus_walk) |w| {
if (w == input_pool_len) {
std.log.info("corpus walk done after walking {} inputs", .{w});
f.corpus_walk = null;
} else {
f.corpus_walk = w + 1;
return @intCast(w);
}
}
var all_features: ArrayListUnmanaged(u32) = .{};
defer all_features.deinit(a);
const index = f.rng.next() % input_pool_len;
return @intCast(index);
}
var feature_buffer = try ArrayListUnmanaged(u32).initCapacity(a, InitialFeatureBufferCap);
defer feature_buffer.deinit(a);
fn doMutation(f: *Fuzzer) void {
if (f.corpus_walk != null) return;
// Choose a file name for the coverage based on a hash of the PCs that
// will be stored within.
const hex_digest = std.fmt.hex(f.coverage_id);
const coverage_file_path = "v/" ++ hex_digest ++ "coverage";
f.mutation_seed = f.rng.next();
f.mutate_scratch.clearRetainingCapacity();
var ip = InputPool.init(f.cache_dir, f.coverage_id);
defer ip.deinit();
var ar_scratch = f.mutate_scratch.toManaged(f.gpa);
var ar_input = f.current_input.toManaged(f.gpa);
check(@src(), mutate.mutate(&ar_input, f.mutation_seed, &ar_scratch), .{ .seed = f.mutation_seed });
f.mutate_scratch = ar_scratch.moveToUnmanaged();
f.current_input = ar_input.moveToUnmanaged();
}
// Tracks which PCs have been seen across all runs that do not crash the fuzzer process.
// Stored in a memory-mapped file so that it can be shared with other
// processes and viewed while the fuzzer is running.
const seen_pcs = initCoverageFile(f.cache_dir, coverage_file_path, f.pcs);
fn undoMutate(f: *Fuzzer) void {
if (f.corpus_walk != null) return;
std.log.info("Coverage id is {s}", .{&hex_digest});
var ar_scratch = f.mutate_scratch.toManaged(f.gpa);
var ar_input = f.current_input.toManaged(f.gpa);
mutate.mutateReverse(&ar_input, f.mutation_seed, &ar_scratch);
f.mutate_scratch = ar_scratch.moveToUnmanaged();
f.current_input = ar_input.moveToUnmanaged();
f.mutate_scratch.clearRetainingCapacity();
}
fn checksum(str: []const u8) u8 {
// this is very bad checksum but since we run the user's code a lot, it
// will probably eventually catch when they do it.
var c: u8 = 0;
for (str) |s| {
c ^= s;
}
return c;
}
fn collectPcCounterFeatures(f: *Fuzzer) void {
for (f.pc_counters, 0..) |counter, i_| {
if (counter != 0) {
const i: u32 = @intCast(i_);
// TODO: does this do a lot of collisions?
feature_capture.newFeature(std.hash.uint32(i));
}
}
}
fn growFeatureBuffer(f: *Fuzzer) void {
// we dont need to copy over the data so we try to resize and
// fallback to new blank allocation
const new_size = f.feature_buffer.len * 2;
if (!f.gpa.resize(f.feature_buffer, new_size)) {
std.log.info("growing feature buffer to {}", .{new_size});
const new_feature_buffer = check(@src(), f.gpa.alloc(u32, new_size), .{ .size = new_size });
f.gpa.free(f.feature_buffer);
f.feature_buffer = new_feature_buffer;
} else {
std.log.info("growing feature buffer to {} (resize)", .{new_size});
}
}
fn analyzeLastRun(f: *Fuzzer) void {
var features = feature_capture.values();
sort(features);
features = uniq(features);
const analysis = cmp(features, f.all_features.items);
if (analysis.only_a == 0) {
return; // bad input
}
if (f.corpus_walk == null) {
var buffer: [256]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
var ar = std.ArrayList(u8).init(fba.allocator());
mutate.writeMutation(f.mutation_seed, ar.writer()) catch {};
std.log.info("new unique run: F:{} \tN:{} \tC:{} \tM:{} \tT:{} \t{s}", .{
features.len,
analysis.only_a,
analysis.both,
analysis.only_b,
f.all_features.items.len + analysis.only_a,
ar.items,
});
incrementUniqueRuns(f.seen_pcs);
f.input_pool.insertString(f.current_input.items);
}
var ar = f.all_features.toManaged(f.gpa);
check(@src(), merge(&ar, features), .{});
f.all_features = ar.moveToUnmanaged();
updateGlobalCoverage(f.pc_counters, f.seen_pcs);
}
fn selectAndMutate(f: *Fuzzer) void {
const input_index = f.pickInput();
const input_extra = f.input_pool.getString(input_index);
const input = input_extra[0..input_extra.len];
f.current_input.clearRetainingCapacity();
// manual slice append since appendSlice doesn't take volatile slice
check(@src(), f.current_input.ensureTotalCapacity(f.gpa, input.len), .{ .input_len = input.len });
f.current_input.items.len = input.len;
@memcpy(f.current_input.items, input);
f.doMutation();
f.current_input_checksum = checksum(f.current_input.items);
}
fn beforeRun(f: *Fuzzer) void {
@memset(f.pc_counters, 0);
feature_capture.prepare(f.feature_buffer);
}
fn firstRun(f: *Fuzzer, options: *const std.testing.FuzzInputOptions) void {
f.readOptions(options);
std.log.info(
\\ starting to fuzz with initial corpus of {}
\\ F - this input features
\\ N - this input new features
\\ C - this input features already discovered
\\ M - features this input missed but discovered by other
\\ T - new total unique features
, .{f.input_pool.len()});
if (f.input_pool.len() == 0) {
f.makeUpInitialCorpus();
\\Fuzzer booted with a initial corpus of size {}
\\F - this input features
\\T - new unique features
, .{ip.len()});
if (ip.len() == 0) {
initialCorpusRandom(&ip, &rng);
}
std.log.info("starting corpus walk", .{});
f.corpus_walk = 0;
}
pub fn next(f: *Fuzzer, options: *const std.testing.FuzzInputOptions) []const u8 {
incrementNumberOfRuns(f.seen_pcs);
for (options.corpus) |inp| {
try runInput(a, test_one, &feature_buffer, f.pc_counters, inp);
const features = getLastRunFeatures();
if (analyzeFeatures(&ip, features, inp, all_features.items)) {
try mergeInput(a, seen_pcs, &all_features, features, f.pc_counters);
}
}
if (f.first_run) {
f.first_run = false;
f.firstRun(options);
} else {
if (f.current_input_checksum != checksum(f.current_input.items)) {
// TODO: report the input? it is not very useful since it was written to
// fuzzer main loop
while (true) {
incrementNumberOfRuns(seen_pcs);
input = try selectAndCopyInput(a, &ip, &rng, input);
const mutation_seed = rng.next();
assert(mutate_scratch.items.len == 0);
try mutate.mutate(&input, mutation_seed, &mutate_scratch, a);
const input_checksum = checksum(input.items);
try runInput(a, test_one, &feature_buffer, f.pc_counters, input.items);
if (input_checksum != checksum(input.items)) {
// report the input? it is not very useful since it was written to
@panic("user code mutated input!");
}
f.collectPcCounterFeatures();
if (feature_capture.is_full()) {
f.growFeatureBuffer();
// rerun same input with larger buffer
f.beforeRun();
return f.current_input.items;
const features = getLastRunFeatures();
if (analyzeFeatures(&ip, features, input.items, all_features.items)) {
logNewFeatures(seen_pcs, features, mutation_seed, all_features.items.len);
try mergeInput(a, seen_pcs, &all_features, features, f.pc_counters);
}
f.analyzeLastRun();
f.undoMutate();
mutate.mutateReverse(&input, mutation_seed, &mutate_scratch);
}
f.selectAndMutate();
f.beforeRun();
return f.current_input.items;
}
};
fn greet() void {
const epoch_seconds = std.time.epoch.EpochSeconds{
.secs = @intCast(std.time.timestamp()),
};
const day_seconds = epoch_seconds.getDaySeconds();
const hour_of_day = day_seconds.getHoursIntoDay();
std.log.info("Good {s}", .{switch (hour_of_day) {
0...11 => "morning",
12...19 => "afternoon",
else => "evening",
}});
}
fn farewell() void {
std.log.info("Farewell", .{});
}

View File

@ -8,7 +8,7 @@
const std = @import("std");
const assert = std.debug.assert;
const check = @import("main.zig").check;
const fatal = std.process.fatal;
pub fn MemoryMappedList(comptime T: type) type {
return struct {
@ -21,19 +21,19 @@ pub fn MemoryMappedList(comptime T: type) type {
const Self = @This();
pub fn init(f: std.fs.File, size: usize) Self {
const slice_cap = check(@src(), f.getEndPos(), .{});
const slice_cap = f.getEndPos() catch |e| fatal("getendpos failed: {}", .{e});
const items_cap = @divExact(slice_cap, @sizeOf(T)); // crash here is probably a corrupt file
assert(size >= slice_cap);
const slice: []align(std.mem.page_size) u8 = check(@src(), std.posix.mmap(
const slice: []align(std.mem.page_size) u8 = std.posix.mmap(
null,
size, // unused virtual address space on linux is cheap
std.posix.PROT.READ | std.posix.PROT.WRITE,
.{ .TYPE = .SHARED },
f.handle,
0,
), .{ .len = size, .fd = f.handle });
) catch |e| fatal("mmap(len={},fd={}) failed: {}", .{ size, f.handle, e });
assert(slice.len == size);
@ -55,10 +55,8 @@ pub fn MemoryMappedList(comptime T: type) type {
// use it until the end of the program. Even this msync is more of
// a politeness than a necessity:
// https://stackoverflow.com/questions/31539208/posix-shared-memory-and-msync
check(@src(), std.posix.msync(start8[0..len8], std.posix.MSF.ASYNC), .{
.ptr = start8,
.len = len8,
});
std.posix.msync(start8[0..len8], std.posix.MSF.ASYNC) catch |e|
fatal("msync failed: {}", .{e});
}
pub fn append(self: *Self, item: T) void {
@ -84,7 +82,7 @@ pub fn MemoryMappedList(comptime T: type) type {
const total = self.items.len + additional_count;
const new_size = total * @sizeOf(T);
check(@src(), std.posix.ftruncate(self.file.handle, new_size), .{ .size = new_size });
std.posix.ftruncate(self.file.handle, new_size) catch |e| fatal("ftruncate failed: {}", .{e});
}
};
}

View File

@ -16,6 +16,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Rng = std.Random.DefaultPrng;
const ArrayList = std.ArrayList;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const test_data: [128]u8 = blk: {
var b: [128]u8 = undefined;
@ -44,12 +45,16 @@ const Mutation = union(enum) {
const MutationSequence = std.BoundedArray(Mutation, 8);
pub fn mutate(str: *ArrayList(u8), seed: u64, scr: *ArrayList(u8)) error{OutOfMemory}!void {
pub fn mutate(str: *ArrayListUnmanaged(u8), seed: u64, scr: *ArrayListUnmanaged(u8), gpa: Allocator) error{OutOfMemory}!void {
var a = str.toManaged(gpa);
var b = scr.toManaged(gpa);
const muts = generateRandomMutationSequence(Rng.init(seed));
try executeMutation(str, muts, scr);
try executeMutation(&a, muts, &b);
str.* = a.moveToUnmanaged();
scr.* = b.moveToUnmanaged();
}
pub fn mutateReverse(str: *ArrayList(u8), seed: u64, scr: *ArrayList(u8)) void {
pub fn mutateReverse(str: *ArrayListUnmanaged(u8), seed: u64, scr: *ArrayListUnmanaged(u8)) void {
const muts = generateRandomMutationSequence(Rng.init(seed));
executeMutationReverse(str, muts, scr);
}
@ -81,13 +86,13 @@ fn executeMutation(str: *ArrayList(u8), muts: MutationSequence, scr: *ArrayList(
.erase_bytes => |a| try mutateEraseBytes(str, scr, a.index, a.len),
.insert_byte => |a| try mutateInsertByte(str, a.index, a.byte),
.insert_repeated_byte => |a| try mutateInsertRepeatedByte(str, a.index, a.len, a.byte),
.change_byte => |a| try mutateChangeByte(str, scr, a.index, a.byte),
.change_bit => |a| mutateChangeBit(str, a.index, a.bit),
.change_byte => |a| try mutateChangeByte(str.items, scr, a.index, a.byte),
.change_bit => |a| mutateChangeBit(str.items, a.index, a.bit),
}
}
}
fn executeMutationReverse(str: *ArrayList(u8), muts: MutationSequence, scr: *ArrayList(u8)) void {
fn executeMutationReverse(str: *ArrayListUnmanaged(u8), muts: MutationSequence, scr: *ArrayListUnmanaged(u8)) void {
const slice = muts.slice();
for (0..slice.len) |i| {
const mut = slice[slice.len - i - 1];
@ -96,8 +101,8 @@ fn executeMutationReverse(str: *ArrayList(u8), muts: MutationSequence, scr: *Arr
.erase_bytes => |a| mutateEraseBytesReverse(str, scr, a.index),
.insert_byte => |a| mutateInsertByteReverse(str, a.index),
.insert_repeated_byte => |a| mutateInsertRepeatedByteReverse(str, a.index, a.len),
.change_byte => |a| mutateChangeByteReverse(str, scr, a.index),
.change_bit => |a| mutateChangeBitReverse(str, a.index, a.bit),
.change_byte => |a| mutateChangeByteReverse(str.items, scr, a.index),
.change_bit => |a| mutateChangeBitReverse(str.items, a.index, a.bit),
}
}
}
@ -138,13 +143,13 @@ pub fn writeMutation(seed: u64, writer: anytype) !void {
}
}
fn mutateChangeBit(str: *ArrayList(u8), index: u32, bit: u3) void {
if (str.items.len == 0) return;
fn mutateChangeBit(str: []u8, index: u32, bit: u3) void {
if (str.len == 0) return;
const mask = @as(u8, 1) << bit;
str.items[index % str.items.len] ^= mask;
str[index % str.len] ^= mask;
}
fn mutateChangeBitReverse(str: *ArrayList(u8), index: u32, bit: u3) void {
fn mutateChangeBitReverse(str: []u8, index: u32, bit: u3) void {
return mutateChangeBit(str, index, bit);
}
@ -161,23 +166,23 @@ test "mutate change bit" {
const index: u32 = @truncate(rng.next());
const bit: u3 = @truncate(rng.next());
mutateChangeBit(&str, index, bit);
mutateChangeBitReverse(&str, index, bit);
mutateChangeBitReverse(str.items, index, bit);
try std.testing.expectEqualStrings(test_data[0..l], str.items);
try std.testing.expectEqual(0, scr.items.len);
}
}
}
fn mutateChangeByte(str: *ArrayList(u8), scr: *ArrayList(u8), index: u32, byte: u8) !void {
if (str.items.len == 0) return;
const target = &str.items[index % str.items.len];
fn mutateChangeByte(str: []u8, scr: *ArrayList(u8), index: u32, byte: u8) !void {
if (str.len == 0) return;
const target = &str[index % str.len];
try scr.append(target.*);
target.* = byte;
}
fn mutateChangeByteReverse(str: *ArrayList(u8), scr: *ArrayList(u8), index: u32) void {
if (str.items.len == 0) return;
str.items[index % str.items.len] = scr.pop();
fn mutateChangeByteReverse(str: []u8, scr: *ArrayListUnmanaged(u8), index: u32) void {
if (str.len == 0) return;
str[index % str.len] = scr.pop();
}
test "mutate change byte" {
@ -192,7 +197,7 @@ test "mutate change byte" {
for (0..1000) |_| {
const index: u32 = @truncate(rng.next());
const byte: u8 = @truncate(rng.next());
try mutateChangeByte(&str, &scr, index, byte);
try mutateChangeByte(str.items, &scr, index, byte);
mutateChangeByteReverse(&str, &scr, index);
try std.testing.expectEqualStrings(test_data[0..l], str.items);
try std.testing.expectEqual(0, scr.items.len);
@ -214,7 +219,7 @@ fn mutateInsertRepeatedByte(str: *ArrayList(u8), index: u32, len_: u8, byte: u8)
@memset(str.items[insert_index..][0..len], byte);
}
fn mutateInsertRepeatedByteReverse(str: *ArrayList(u8), index: u32, len_: u8) void {
fn mutateInsertRepeatedByteReverse(str: *ArrayListUnmanaged(u8), index: u32, len_: u8) void {
const len = @min(24, @max(1, len_));
const str_len = str.items.len - len;
const insert_index = index % (str_len + 1);
@ -247,7 +252,7 @@ fn mutateInsertByte(str: *ArrayList(u8), index: u32, byte: u8) !void {
return mutateInsertRepeatedByte(str, index, 1, byte);
}
fn mutateInsertByteReverse(str: *ArrayList(u8), index: u32) void {
fn mutateInsertByteReverse(str: *ArrayListUnmanaged(u8), index: u32) void {
return mutateInsertRepeatedByteReverse(str, index, 1);
}
@ -292,7 +297,7 @@ fn mutateEraseBytes(str: *ArrayList(u8), scr: *ArrayList(u8), index: u32, len_:
str.items.len -= len;
}
fn mutateEraseBytesReverse(str: *ArrayList(u8), scr: *ArrayList(u8), index: u32) void {
fn mutateEraseBytesReverse(str: *ArrayListUnmanaged(u8), scr: *ArrayListUnmanaged(u8), index: u32) void {
const len = scr.pop();
if (len == 0) return;
const erase_index = index % (str.items.len + 1);

View File

@ -131,12 +131,13 @@ fn accept(ws: *WebServer, connection: std.net.Server.Connection) void {
}
}
pub const staticFileMap = std.StaticStringMap(struct { path: []const u8, mime: []const u8 }).initComptime(.{
.{ "/", .{ .path = "fuzzer/web/index.html", .mime = "text/html" } },
.{ "/debug", .{ .path = "fuzzer/web/index.html", .mime = "text/html" } },
.{ "/debug/", .{ .path = "fuzzer/web/index.html", .mime = "text/html" } },
.{ "/main.js", .{ .path = "fuzzer/web/main.js", .mime = "application/javascript" } },
.{ "/debug/main.js", .{ .path = "fuzzer/web/main.js", .mime = "application/javascript" } },
const FileMapEntry = struct { path: []const u8, mime: []const u8 };
pub const staticFileMap = std.StaticStringMap(FileMapEntry).initComptime(&.{
.{ "/", FileMapEntry{ .path = "fuzzer/web/index.html", .mime = "text/html" } },
.{ "/debug", FileMapEntry{ .path = "fuzzer/web/index.html", .mime = "text/html" } },
.{ "/debug/", FileMapEntry{ .path = "fuzzer/web/index.html", .mime = "text/html" } },
.{ "/main.js", FileMapEntry{ .path = "fuzzer/web/main.js", .mime = "application/javascript" } },
.{ "/debug/main.js", FileMapEntry{ .path = "fuzzer/web/main.js", .mime = "application/javascript" } },
});
fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void {