mirror of
https://github.com/ziglang/zig.git
synced 2024-11-24 13:20:14 +00:00
upkeep
This commit is contained in:
parent
3d05a4631a
commit
26dc65a345
@ -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;
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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));
|
||||
|
@ -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)});
|
||||
|
@ -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", .{});
|
||||
}
|
||||
|
@ -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});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user