aboutsummaryrefslogtreecommitdiff
path: root/lib/std/Build/Fuzz.zig
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2024-08-04 00:16:28 -0700
committerAndrew Kelley <andrew@ziglang.org>2024-08-07 00:48:32 -0700
commit517cfb0dd1e2b5b8efc8e90ce4e5593a38fa158c (patch)
treeb630ec6fa767f2aaf6932472a8acb85ac5089cf5 /lib/std/Build/Fuzz.zig
parent5f92a036f9a9a137e4276d0f605e4cb940eca3a7 (diff)
downloadzig-517cfb0dd1e2b5b8efc8e90ce4e5593a38fa158c.tar.gz
zig-517cfb0dd1e2b5b8efc8e90ce4e5593a38fa158c.zip
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
Diffstat (limited to 'lib/std/Build/Fuzz.zig')
-rw-r--r--lib/std/Build/Fuzz.zig209
1 files changed, 197 insertions, 12 deletions
diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig
index 46d9bfc8fd..0ff82f3677 100644
--- a/lib/std/Build/Fuzz.zig
+++ b/lib/std/Build/Fuzz.zig
@@ -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,
+ 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,
- listen_address: std.net.Address,
- fuzz_run_steps: []const *Step.Run,
+ /// 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;
},
};
}