fuzzing: progress towards web UI

* libfuzzer: close file after mmap
* fuzzer/main.js: connect with EventSource and debug dump the messages.
  currently this prints how many fuzzer runs have been attempted to
  console.log.
* extract some `std.debug.Info` logic into `std.debug.Coverage`.
  Prepares for consolidation across multiple different executables which
  share source files, and makes it possible to send all the
  PC/SourceLocation mapping data with 4 memcpy'd arrays.
* std.Build.Fuzz:
  - spawn a thread to watch the message queue and signal event
    subscribers.
  - track coverage map data
  - respond to /events URL with EventSource messages on a timer
This commit is contained in:
Andrew Kelley
2024-08-04 00:16:28 -07:00
parent 5f92a036f9
commit 517cfb0dd1
8 changed files with 480 additions and 167 deletions

View File

@@ -6,6 +6,7 @@ const assert = std.debug.assert;
const fatal = std.process.fatal;
const Allocator = std.mem.Allocator;
const log = std.log;
const Coverage = std.debug.Coverage;
const Fuzz = @This();
const build_runner = @import("root");
@@ -53,17 +54,30 @@ pub fn start(
.global_cache_directory = global_cache_directory,
.zig_lib_directory = zig_lib_directory,
.zig_exe_path = zig_exe_path,
.msg_queue = .{},
.mutex = .{},
.listen_address = listen_address,
.fuzz_run_steps = fuzz_run_steps,
.msg_queue = .{},
.mutex = .{},
.condition = .{},
.coverage_files = .{},
.coverage_mutex = .{},
.coverage_condition = .{},
};
// For accepting HTTP connections.
const web_server_thread = std.Thread.spawn(.{}, WebServer.run, .{&web_server}) catch |err| {
fatal("unable to spawn web server thread: {s}", .{@errorName(err)});
};
defer web_server_thread.join();
// For polling messages and sending updates to subscribers.
const coverage_thread = std.Thread.spawn(.{}, WebServer.coverageRun, .{&web_server}) catch |err| {
fatal("unable to spawn coverage thread: {s}", .{@errorName(err)});
};
defer coverage_thread.join();
{
const fuzz_node = prog_node.start("Fuzzing", fuzz_run_steps.len);
defer fuzz_node.end();
@@ -88,14 +102,38 @@ pub const WebServer = struct {
global_cache_directory: Build.Cache.Directory,
zig_lib_directory: Build.Cache.Directory,
zig_exe_path: []const u8,
/// Messages from fuzz workers. Protected by mutex.
msg_queue: std.ArrayListUnmanaged(Msg),
mutex: std.Thread.Mutex,
listen_address: std.net.Address,
fuzz_run_steps: []const *Step.Run,
/// Messages from fuzz workers. Protected by mutex.
msg_queue: std.ArrayListUnmanaged(Msg),
/// Protects `msg_queue` only.
mutex: std.Thread.Mutex,
/// Signaled when there is a message in `msg_queue`.
condition: std.Thread.Condition,
coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap),
/// Protects `coverage_files` only.
coverage_mutex: std.Thread.Mutex,
/// Signaled when `coverage_files` changes.
coverage_condition: std.Thread.Condition,
const CoverageMap = struct {
mapped_memory: []align(std.mem.page_size) const u8,
coverage: Coverage,
fn deinit(cm: *CoverageMap, gpa: Allocator) void {
std.posix.munmap(cm.mapped_memory);
cm.coverage.deinit(gpa);
cm.* = undefined;
}
};
const Msg = union(enum) {
coverage_id: u64,
coverage: struct {
id: u64,
run: *Step.Run,
},
};
fn run(ws: *WebServer) void {
@@ -162,6 +200,10 @@ pub const WebServer = struct {
std.mem.eql(u8, request.head.target, "/debug/sources.tar"))
{
try serveSourcesTar(ws, request);
} else if (std.mem.eql(u8, request.head.target, "/events") or
std.mem.eql(u8, request.head.target, "/debug/events"))
{
try serveEvents(ws, request);
} else {
try request.respond("not found", .{
.status = .not_found,
@@ -384,6 +426,58 @@ pub const WebServer = struct {
try file.writeAll(std.mem.asBytes(&header));
}
fn serveEvents(ws: *WebServer, request: *std.http.Server.Request) !void {
var send_buffer: [0x4000]u8 = undefined;
var response = request.respondStreaming(.{
.send_buffer = &send_buffer,
.respond_options = .{
.extra_headers = &.{
.{ .name = "content-type", .value = "text/event-stream" },
},
.transfer_encoding = .none,
},
});
ws.coverage_mutex.lock();
defer ws.coverage_mutex.unlock();
if (getStats(ws)) |stats| {
try response.writer().print("data: {d}\n\n", .{stats.n_runs});
} else {
try response.writeAll("data: loading debug information\n\n");
}
try response.flush();
while (true) {
ws.coverage_condition.timedWait(&ws.coverage_mutex, std.time.ns_per_ms * 500) catch {};
if (getStats(ws)) |stats| {
try response.writer().print("data: {d}\n\n", .{stats.n_runs});
try response.flush();
}
}
}
const Stats = struct {
n_runs: u64,
};
fn getStats(ws: *WebServer) ?Stats {
const coverage_maps = ws.coverage_files.values();
if (coverage_maps.len == 0) return null;
// TODO: make each events URL correspond to one coverage map
const ptr = coverage_maps[0].mapped_memory;
const SeenPcsHeader = extern struct {
n_runs: usize,
deduplicated_runs: usize,
pcs_len: usize,
lowest_stack: usize,
};
const header: *const SeenPcsHeader = @ptrCast(ptr[0..@sizeOf(SeenPcsHeader)]);
return .{
.n_runs = @atomicLoad(usize, &header.n_runs, .monotonic),
};
}
fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
const gpa = ws.gpa;
@@ -471,6 +565,95 @@ pub const WebServer = struct {
.name = "cache-control",
.value = "max-age=0, must-revalidate",
};
fn coverageRun(ws: *WebServer) void {
ws.mutex.lock();
defer ws.mutex.unlock();
while (true) {
ws.condition.wait(&ws.mutex);
for (ws.msg_queue.items) |msg| switch (msg) {
.coverage => |coverage| prepareTables(ws, coverage.run, coverage.id) catch |err| switch (err) {
error.AlreadyReported => continue,
else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
},
};
ws.msg_queue.clearRetainingCapacity();
}
}
fn prepareTables(
ws: *WebServer,
run_step: *Step.Run,
coverage_id: u64,
) error{ OutOfMemory, AlreadyReported }!void {
const gpa = ws.gpa;
ws.coverage_mutex.lock();
defer ws.coverage_mutex.unlock();
const gop = try ws.coverage_files.getOrPut(gpa, coverage_id);
if (gop.found_existing) {
// We are fuzzing the same executable with multiple threads.
// Perhaps the same unit test; perhaps a different one. In any
// case, since the coverage file is the same, we only have to
// notice changes to that one file in order to learn coverage for
// this particular executable.
return;
}
errdefer _ = ws.coverage_files.pop();
gop.value_ptr.* = .{
.coverage = std.debug.Coverage.init,
.mapped_memory = undefined, // populated below
};
errdefer gop.value_ptr.coverage.deinit(gpa);
const rebuilt_exe_path: Build.Cache.Path = .{
.root_dir = Build.Cache.Directory.cwd(),
.sub_path = run_step.rebuilt_executable.?,
};
var debug_info = std.debug.Info.load(gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| {
log.err("step '{s}': failed to load debug information for '{}': {s}", .{
run_step.step.name, rebuilt_exe_path, @errorName(err),
});
return error.AlreadyReported;
};
defer debug_info.deinit(gpa);
const coverage_file_path: Build.Cache.Path = .{
.root_dir = run_step.step.owner.cache_root,
.sub_path = "v/" ++ std.fmt.hex(coverage_id),
};
var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| {
log.err("step '{s}': failed to load coverage file '{}': {s}", .{
run_step.step.name, coverage_file_path, @errorName(err),
});
return error.AlreadyReported;
};
defer coverage_file.close();
const file_size = coverage_file.getEndPos() catch |err| {
log.err("unable to check len of coverage file '{}': {s}", .{ coverage_file_path, @errorName(err) });
return error.AlreadyReported;
};
const mapped_memory = std.posix.mmap(
null,
file_size,
std.posix.PROT.READ,
.{ .TYPE = .SHARED },
coverage_file.handle,
0,
) catch |err| {
log.err("failed to map coverage file '{}': {s}", .{ coverage_file_path, @errorName(err) });
return error.AlreadyReported;
};
gop.value_ptr.mapped_memory = mapped_memory;
ws.coverage_condition.broadcast();
}
};
fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog_node: std.Progress.Node) void {
@@ -493,16 +676,16 @@ fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog
build_runner.printErrorMessages(gpa, &compile.step, ttyconf, stderr, false) catch {};
}
if (result) |rebuilt_bin_path| {
run.rebuilt_executable = rebuilt_bin_path;
} else |err| switch (err) {
error.MakeFailed => {},
const rebuilt_bin_path = result catch |err| switch (err) {
error.MakeFailed => return,
else => {
std.debug.print("step '{s}': failed to rebuild in fuzz mode: {s}\n", .{
log.err("step '{s}': failed to rebuild in fuzz mode: {s}", .{
compile.step.name, @errorName(err),
});
return;
},
}
};
run.rebuilt_executable = rebuilt_bin_path;
}
fn fuzzWorkerRun(
@@ -524,11 +707,13 @@ fn fuzzWorkerRun(
std.debug.lockStdErr();
defer std.debug.unlockStdErr();
build_runner.printErrorMessages(gpa, &run.step, ttyconf, stderr, false) catch {};
return;
},
else => {
std.debug.print("step '{s}': failed to rebuild '{s}' in fuzz mode: {s}\n", .{
log.err("step '{s}': failed to rerun '{s}' in fuzz mode: {s}", .{
run.step.name, test_name, @errorName(err),
});
return;
},
};
}