authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-31 22:54:05-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-08-07 00:48:32-07:00
loge0ffac4e3c2271a617616760f3084f5f01fb0785
tree5769c9280878ebb3cc5f76345cf00050012e8198
parentffc050e0557c5d951cd293ca365ed0cd3cdf83db

introduce a web interface for fuzzing

* new .zig-cache subdirectory: 'v' - stores coverage information with filename of hash of PCs that want coverage. This hash is a hex encoding of the 64-bit coverage ID. * build runner * fixed bug in file system inputs when a compile step has an overridden zig_lib_dir field set. * set some std lib options optimized for the build runner - no side channel mitigations - no Transport Layer Security - no crypto fork safety * add a --port CLI arg for choosing the port the fuzzing web interface listens on. it defaults to choosing a random open port. * introduce a web server, and serve a basic single page application - shares wasm code with autodocs - assets are created live on request, for convenient development experience. main.wasm is properly cached if nothing changes. - sources.tar comes from file system inputs (introduced with the `--watch` feature) * receives coverage ID from test runner and sends it on a thread-safe queue to the WebServer. * test runner - takes a zig cache directory argument now, for where to put coverage information. - sends coverage ID to parent process * fuzzer - puts its logs (in debug mode) in .zig-cache/tmp/libfuzzer.log - computes coverage_id and makes it available with `fuzzer_coverage_id` exported function. - the memory-mapped coverage file is now namespaced by the coverage id in hex encoding, in `.zig-cache/v` * tokenizer - add a fuzz test to check that several properties are upheld

13 files changed, 872 insertions(+), 62 deletions(-)

lib/compiler/build_runner.zig+28-1
...@@ -17,6 +17,12 @@ const runner = @This();...@@ -17,6 +17,12 @@ const runner = @This();
17pub const root = @import("@build");17pub const root = @import("@build");
18pub const dependencies = @import("@dependencies");18pub const dependencies = @import("@dependencies");
1919
20pub const std_options: std.Options = .{
21 .side_channels_mitigations = .none,
22 .http_disable_tls = true,
23 .crypto_fork_safety = false,
24};
25
20pub fn main() !void {26pub fn main() !void {
21 // Here we use an ArenaAllocator backed by a page allocator because a build is a short-lived,27 // Here we use an ArenaAllocator backed by a page allocator because a build is a short-lived,
22 // one shot program. We don't need to waste time freeing memory and finding places to squish28 // one shot program. We don't need to waste time freeing memory and finding places to squish
...@@ -106,6 +112,7 @@ pub fn main() !void {...@@ -106,6 +112,7 @@ pub fn main() !void {
106 var watch = false;112 var watch = false;
107 var fuzz = false;113 var fuzz = false;
108 var debounce_interval_ms: u16 = 50;114 var debounce_interval_ms: u16 = 50;
115 var listen_port: u16 = 0;
109116
110 while (nextArg(args, &arg_idx)) |arg| {117 while (nextArg(args, &arg_idx)) |arg| {
111 if (mem.startsWith(u8, arg, "-Z")) {118 if (mem.startsWith(u8, arg, "-Z")) {
...@@ -203,6 +210,14 @@ pub fn main() !void {...@@ -203,6 +210,14 @@ pub fn main() !void {
203 next_arg, @errorName(err),210 next_arg, @errorName(err),
204 });211 });
205 };212 };
213 } else if (mem.eql(u8, arg, "--port")) {
214 const next_arg = nextArg(args, &arg_idx) orelse
215 fatalWithHint("expected u16 after '{s}'", .{arg});
216 listen_port = std.fmt.parseUnsigned(u16, next_arg, 10) catch |err| {
217 fatal("unable to parse port '{s}' as unsigned 16-bit integer: {s}\n", .{
218 next_arg, @errorName(err),
219 });
220 };
206 } else if (mem.eql(u8, arg, "--debug-log")) {221 } else if (mem.eql(u8, arg, "--debug-log")) {
207 const next_arg = nextArgOrFatal(args, &arg_idx);222 const next_arg = nextArgOrFatal(args, &arg_idx);
208 try debug_log_scopes.append(next_arg);223 try debug_log_scopes.append(next_arg);
...@@ -403,7 +418,19 @@ pub fn main() !void {...@@ -403,7 +418,19 @@ pub fn main() !void {
403 else => return err,418 else => return err,
404 };419 };
405 if (fuzz) {420 if (fuzz) {
406 Fuzz.start(&run.thread_pool, run.step_stack.keys(), run.ttyconf, main_progress_node);421 const listen_address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable;
422 try Fuzz.start(
423 gpa,
424 arena,
425 global_cache_directory,
426 zig_lib_directory,
427 zig_exe,
428 &run.thread_pool,
429 run.step_stack.keys(),
430 run.ttyconf,
431 listen_address,
432 main_progress_node,
433 );
407 }434 }
408435
409 if (!watch) return cleanExit();436 if (!watch) return cleanExit();
lib/compiler/test_runner.zig+20-2
...@@ -28,6 +28,7 @@ pub fn main() void {...@@ -28,6 +28,7 @@ pub fn main() void {
28 @panic("unable to parse command line args");28 @panic("unable to parse command line args");
2929
30 var listen = false;30 var listen = false;
31 var opt_cache_dir: ?[]const u8 = null;
3132
32 for (args[1..]) |arg| {33 for (args[1..]) |arg| {
33 if (std.mem.eql(u8, arg, "--listen=-")) {34 if (std.mem.eql(u8, arg, "--listen=-")) {
...@@ -35,13 +36,18 @@ pub fn main() void {...@@ -35,13 +36,18 @@ pub fn main() void {
35 } else if (std.mem.startsWith(u8, arg, "--seed=")) {36 } else if (std.mem.startsWith(u8, arg, "--seed=")) {
36 testing.random_seed = std.fmt.parseUnsigned(u32, arg["--seed=".len..], 0) catch37 testing.random_seed = std.fmt.parseUnsigned(u32, arg["--seed=".len..], 0) catch
37 @panic("unable to parse --seed command line argument");38 @panic("unable to parse --seed command line argument");
39 } else if (std.mem.startsWith(u8, arg, "--cache-dir")) {
40 opt_cache_dir = arg["--cache-dir=".len..];
38 } else {41 } else {
39 @panic("unrecognized command line argument");42 @panic("unrecognized command line argument");
40 }43 }
41 }44 }
4245
43 fba.reset();46 fba.reset();
44 if (builtin.fuzz) fuzzer_init();47 if (builtin.fuzz) {
48 const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument");
49 fuzzer_init(FuzzerSlice.fromSlice(cache_dir));
50 }
4551
46 if (listen) {52 if (listen) {
47 return mainServer() catch @panic("internal test runner failure");53 return mainServer() catch @panic("internal test runner failure");
...@@ -60,6 +66,11 @@ fn mainServer() !void {...@@ -60,6 +66,11 @@ fn mainServer() !void {
60 });66 });
61 defer server.deinit();67 defer server.deinit();
6268
69 if (builtin.fuzz) {
70 const coverage_id = fuzzer_coverage_id();
71 try server.serveU64Message(.coverage_id, coverage_id);
72 }
73
63 while (true) {74 while (true) {
64 const hdr = try server.receiveMessage();75 const hdr = try server.receiveMessage();
65 switch (hdr.tag) {76 switch (hdr.tag) {
...@@ -316,15 +327,22 @@ const FuzzerSlice = extern struct {...@@ -316,15 +327,22 @@ const FuzzerSlice = extern struct {
316 ptr: [*]const u8,327 ptr: [*]const u8,
317 len: usize,328 len: usize,
318329
330 /// Inline to avoid fuzzer instrumentation.
319 inline fn toSlice(s: FuzzerSlice) []const u8 {331 inline fn toSlice(s: FuzzerSlice) []const u8 {
320 return s.ptr[0..s.len];332 return s.ptr[0..s.len];
321 }333 }
334
335 /// Inline to avoid fuzzer instrumentation.
336 inline fn fromSlice(s: []const u8) FuzzerSlice {
337 return .{ .ptr = s.ptr, .len = s.len };
338 }
322};339};
323340
324var is_fuzz_test: bool = undefined;341var is_fuzz_test: bool = undefined;
325342
326extern fn fuzzer_next() FuzzerSlice;343extern fn fuzzer_next() FuzzerSlice;
327extern fn fuzzer_init() void;344extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
345extern fn fuzzer_coverage_id() u64;
328346
329pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {347pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {
330 @disableInstrumentation();348 @disableInstrumentation();
lib/docs/wasm/main.zig+2-2
...@@ -53,7 +53,7 @@ export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {...@@ -53,7 +53,7 @@ export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
53 const tar_bytes = tar_ptr[0..tar_len];53 const tar_bytes = tar_ptr[0..tar_len];
54 //log.debug("received {d} bytes of tar file", .{tar_bytes.len});54 //log.debug("received {d} bytes of tar file", .{tar_bytes.len});
5555
56 unpack_inner(tar_bytes) catch |err| {56 unpackInner(tar_bytes) catch |err| {
57 fatal("unable to unpack tar: {s}", .{@errorName(err)});57 fatal("unable to unpack tar: {s}", .{@errorName(err)});
58 };58 };
59}59}
...@@ -750,7 +750,7 @@ export fn decl_type_html(decl_index: Decl.Index) String {...@@ -750,7 +750,7 @@ export fn decl_type_html(decl_index: Decl.Index) String {
750750
751const Oom = error{OutOfMemory};751const Oom = error{OutOfMemory};
752752
753fn unpack_inner(tar_bytes: []u8) !void {753fn unpackInner(tar_bytes: []u8) !void {
754 var fbs = std.io.fixedBufferStream(tar_bytes);754 var fbs = std.io.fixedBufferStream(tar_bytes);
755 var file_name_buffer: [1024]u8 = undefined;755 var file_name_buffer: [1024]u8 = undefined;
756 var link_name_buffer: [1024]u8 = undefined;756 var link_name_buffer: [1024]u8 = undefined;
lib/fuzzer.zig+58-15
...@@ -17,7 +17,8 @@ fn logOverride(...@@ -17,7 +17,8 @@ fn logOverride(
17 args: anytype,17 args: anytype,
18) void {18) void {
19 const f = if (log_file) |f| f else f: {19 const f = if (log_file) |f| f else f: {
20 const f = fuzzer.dir.createFile("libfuzzer.log", .{}) catch @panic("failed to open fuzzer log file");20 const f = fuzzer.cache_dir.createFile("tmp/libfuzzer.log", .{}) catch
21 @panic("failed to open fuzzer log file");
21 log_file = f;22 log_file = f;
22 break :f f;23 break :f f;
23 };24 };
...@@ -114,7 +115,10 @@ const Fuzzer = struct {...@@ -114,7 +115,10 @@ const Fuzzer = struct {
114 /// Stored in a memory-mapped file so that it can be shared with other115 /// Stored in a memory-mapped file so that it can be shared with other
115 /// processes and viewed while the fuzzer is running.116 /// processes and viewed while the fuzzer is running.
116 seen_pcs: MemoryMappedList,117 seen_pcs: MemoryMappedList,
117 dir: std.fs.Dir,118 cache_dir: std.fs.Dir,
119 /// Identifies the file name that will be used to store coverage
120 /// information, available to other processes.
121 coverage_id: u64,
118122
119 const SeenPcsHeader = extern struct {123 const SeenPcsHeader = extern struct {
120 n_runs: usize,124 n_runs: usize,
...@@ -189,18 +193,31 @@ const Fuzzer = struct {...@@ -189,18 +193,31 @@ const Fuzzer = struct {
189 id: Run.Id,193 id: Run.Id,
190 };194 };
191195
192 fn init(f: *Fuzzer, dir: std.fs.Dir) !void {196 fn init(f: *Fuzzer, cache_dir: std.fs.Dir) !void {
193 f.dir = dir;197 const flagged_pcs = f.flagged_pcs;
198
199 f.cache_dir = cache_dir;
200
201 // Choose a file name for the coverage based on a hash of the PCs that will be stored within.
202 const pc_digest = d: {
203 var hasher = std.hash.Wyhash.init(0);
204 for (flagged_pcs) |flagged_pc| {
205 hasher.update(std.mem.asBytes(&flagged_pc.addr));
206 }
207 break :d f.coverage.run_id_hasher.final();
208 };
209 f.coverage_id = pc_digest;
210 const hex_digest = std.fmt.hex(pc_digest);
211 const coverage_file_path = "v/" ++ hex_digest;
194212
195 // Layout of this file:213 // Layout of this file:
196 // - Header214 // - Header
197 // - list of PC addresses (usize elements)215 // - list of PC addresses (usize elements)
198 // - list of hit flag, 1 bit per address (stored in u8 elements)216 // - list of hit flag, 1 bit per address (stored in u8 elements)
199 const coverage_file = dir.createFile("coverage", .{217 const coverage_file = createFileBail(cache_dir, coverage_file_path, .{
200 .read = true,218 .read = true,
201 .truncate = false,219 .truncate = false,
202 }) catch |err| fatal("unable to create coverage file: {s}", .{@errorName(err)});220 });
203 const flagged_pcs = f.flagged_pcs;
204 const n_bitset_elems = (flagged_pcs.len + 7) / 8;221 const n_bitset_elems = (flagged_pcs.len + 7) / 8;
205 const bytes_len = @sizeOf(SeenPcsHeader) + flagged_pcs.len * @sizeOf(usize) + n_bitset_elems;222 const bytes_len = @sizeOf(SeenPcsHeader) + flagged_pcs.len * @sizeOf(usize) + n_bitset_elems;
206 const existing_len = coverage_file.getEndPos() catch |err| {223 const existing_len = coverage_file.getEndPos() catch |err| {
...@@ -217,7 +234,8 @@ const Fuzzer = struct {...@@ -217,7 +234,8 @@ const Fuzzer = struct {
217 fatal("unable to init coverage memory map: {s}", .{@errorName(err)});234 fatal("unable to init coverage memory map: {s}", .{@errorName(err)});
218 };235 };
219 if (existing_len != 0) {236 if (existing_len != 0) {
220 const existing_pcs = std.mem.bytesAsSlice(usize, f.seen_pcs.items[@sizeOf(SeenPcsHeader)..][0 .. flagged_pcs.len * @sizeOf(usize)]);237 const existing_pcs_bytes = f.seen_pcs.items[@sizeOf(SeenPcsHeader)..][0 .. flagged_pcs.len * @sizeOf(usize)];
238 const existing_pcs = std.mem.bytesAsSlice(usize, existing_pcs_bytes);
221 for (existing_pcs, flagged_pcs, 0..) |old, new, i| {239 for (existing_pcs, flagged_pcs, 0..) |old, new, i| {
222 if (old != new.addr) {240 if (old != new.addr) {
223 fatal("incompatible existing coverage file (differing PC at index {d}: {x} != {x})", .{241 fatal("incompatible existing coverage file (differing PC at index {d}: {x} != {x})", .{
...@@ -380,6 +398,21 @@ const Fuzzer = struct {...@@ -380,6 +398,21 @@ const Fuzzer = struct {
380 }398 }
381};399};
382400
401fn createFileBail(dir: std.fs.Dir, sub_path: []const u8, flags: std.fs.File.CreateFlags) std.fs.File {
402 return dir.createFile(sub_path, flags) catch |err| switch (err) {
403 error.FileNotFound => {
404 const dir_name = std.fs.path.dirname(sub_path).?;
405 dir.makePath(dir_name) catch |e| {
406 fatal("unable to make path '{s}': {s}", .{ dir_name, @errorName(e) });
407 };
408 return dir.createFile(sub_path, flags) catch |e| {
409 fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(e) });
410 };
411 },
412 else => fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(err) }),
413 };
414}
415
383fn oom(err: anytype) noreturn {416fn oom(err: anytype) noreturn {
384 switch (err) {417 switch (err) {
385 error.OutOfMemory => @panic("out of memory"),418 error.OutOfMemory => @panic("out of memory"),
...@@ -397,25 +430,35 @@ var fuzzer: Fuzzer = .{...@@ -397,25 +430,35 @@ var fuzzer: Fuzzer = .{
397 .n_runs = 0,430 .n_runs = 0,
398 .recent_cases = .{},431 .recent_cases = .{},
399 .coverage = undefined,432 .coverage = undefined,
400 .dir = undefined,433 .cache_dir = undefined,
401 .seen_pcs = undefined,434 .seen_pcs = undefined,
435 .coverage_id = undefined,
402};436};
403437
438/// Invalid until `fuzzer_init` is called.
439export fn fuzzer_coverage_id() u64 {
440 return fuzzer.coverage_id;
441}
442
404export fn fuzzer_next() Fuzzer.Slice {443export fn fuzzer_next() Fuzzer.Slice {
405 return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) {444 return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) {
406 error.OutOfMemory => @panic("out of memory"),445 error.OutOfMemory => @panic("out of memory"),
407 });446 });
408}447}
409448
410export fn fuzzer_init() void {449export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
411 if (module_count_8bc == 0) fatal("__sanitizer_cov_8bit_counters_init was never called", .{});450 if (module_count_8bc == 0) fatal("__sanitizer_cov_8bit_counters_init was never called", .{});
412 if (module_count_pcs == 0) fatal("__sanitizer_cov_pcs_init was never called", .{});451 if (module_count_pcs == 0) fatal("__sanitizer_cov_pcs_init was never called", .{});
413452
414 // TODO: move this to .zig-cache/f453 const cache_dir_path = cache_dir_struct.toZig();
415 const fuzz_dir = std.fs.cwd().makeOpenPath("f", .{ .iterate = true }) catch |err| {454 const cache_dir = if (cache_dir_path.len == 0)
416 fatal("unable to open fuzz directory 'f': {s}", .{@errorName(err)});455 std.fs.cwd()
417 };456 else
418 fuzzer.init(fuzz_dir) catch |err| fatal("unable to init fuzzer: {s}", .{@errorName(err)});457 std.fs.cwd().makeOpenPath(cache_dir_path, .{ .iterate = true }) catch |err| {
458 fatal("unable to open fuzz directory '{s}': {s}", .{ cache_dir_path, @errorName(err) });
459 };
460
461 fuzzer.init(cache_dir) catch |err| fatal("unable to init fuzzer: {s}", .{@errorName(err)});
419}462}
420463
421/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.464/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.
lib/fuzzer/index.html created+76
...@@ -0,0 +1,76 @@
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Zig Documentation</title>
6 <style type="text/css">
7 body {
8 font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
9 color: #000000;
10 }
11 .tok-kw {
12 color: #333;
13 font-weight: bold;
14 }
15 .tok-str {
16 color: #d14;
17 }
18 .tok-builtin {
19 color: #0086b3;
20 }
21 .tok-comment {
22 color: #777;
23 font-style: italic;
24 }
25 .tok-fn {
26 color: #900;
27 font-weight: bold;
28 }
29 .tok-null {
30 color: #008080;
31 }
32 .tok-number {
33 color: #008080;
34 }
35 .tok-type {
36 color: #458;
37 font-weight: bold;
38 }
39
40 @media (prefers-color-scheme: dark) {
41 body {
42 background-color: #111;
43 color: #bbb;
44 }
45 .tok-kw {
46 color: #eee;
47 }
48 .tok-str {
49 color: #2e5;
50 }
51 .tok-builtin {
52 color: #ff894c;
53 }
54 .tok-comment {
55 color: #aa7;
56 }
57 .tok-fn {
58 color: #B1A0F8;
59 }
60 .tok-null {
61 color: #ff8080;
62 }
63 .tok-number {
64 color: #ff8080;
65 }
66 .tok-type {
67 color: #68f;
68 }
69 }
70 </style>
71 </head>
72 <body>
73 <script src="main.js"></script>
74 </body>
75</html>
76
lib/fuzzer/main.js created+40
...@@ -0,0 +1,40 @@
1(function() {
2 let wasm_promise = fetch("main.wasm");
3 let sources_promise = fetch("sources.tar").then(function(response) {
4 if (!response.ok) throw new Error("unable to download sources");
5 return response.arrayBuffer();
6 });
7 var wasm_exports = null;
8
9 const text_decoder = new TextDecoder();
10 const text_encoder = new TextEncoder();
11
12 WebAssembly.instantiateStreaming(wasm_promise, {
13 js: {
14 log: function(ptr, len) {
15 const msg = decodeString(ptr, len);
16 console.log(msg);
17 },
18 panic: function (ptr, len) {
19 const msg = decodeString(ptr, len);
20 throw new Error("panic: " + msg);
21 },
22 },
23 }).then(function(obj) {
24 wasm_exports = obj.instance.exports;
25 window.wasm = obj; // for debugging
26
27 sources_promise.then(function(buffer) {
28 const js_array = new Uint8Array(buffer);
29 const ptr = wasm_exports.alloc(js_array.length);
30 const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
31 wasm_array.set(js_array);
32 wasm_exports.unpack(ptr, js_array.length);
33 });
34 });
35
36 function decodeString(ptr, len) {
37 if (len === 0) return "";
38 return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
39 }
40})();
lib/fuzzer/wasm/main.zig created+99
...@@ -0,0 +1,99 @@
1const std = @import("std");
2const assert = std.debug.assert;
3
4const Walk = @import("Walk");
5
6const gpa = std.heap.wasm_allocator;
7const log = std.log;
8
9const js = struct {
10 extern "js" fn log(ptr: [*]const u8, len: usize) void;
11 extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
12};
13
14pub const std_options: std.Options = .{
15 .logFn = logFn,
16};
17
18pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
19 _ = st;
20 _ = addr;
21 log.err("panic: {s}", .{msg});
22 @trap();
23}
24
25fn logFn(
26 comptime message_level: log.Level,
27 comptime scope: @TypeOf(.enum_literal),
28 comptime format: []const u8,
29 args: anytype,
30) void {
31 const level_txt = comptime message_level.asText();
32 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
33 var buf: [500]u8 = undefined;
34 const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
35 buf[buf.len - 3 ..][0..3].* = "...".*;
36 break :l &buf;
37 };
38 js.log(line.ptr, line.len);
39}
40
41export fn alloc(n: usize) [*]u8 {
42 const slice = gpa.alloc(u8, n) catch @panic("OOM");
43 return slice.ptr;
44}
45
46export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
47 const tar_bytes = tar_ptr[0..tar_len];
48 log.debug("received {d} bytes of tar file", .{tar_bytes.len});
49
50 unpackInner(tar_bytes) catch |err| {
51 fatal("unable to unpack tar: {s}", .{@errorName(err)});
52 };
53}
54
55fn unpackInner(tar_bytes: []u8) !void {
56 var fbs = std.io.fixedBufferStream(tar_bytes);
57 var file_name_buffer: [1024]u8 = undefined;
58 var link_name_buffer: [1024]u8 = undefined;
59 var it = std.tar.iterator(fbs.reader(), .{
60 .file_name_buffer = &file_name_buffer,
61 .link_name_buffer = &link_name_buffer,
62 });
63 while (try it.next()) |tar_file| {
64 switch (tar_file.kind) {
65 .file => {
66 if (tar_file.size == 0 and tar_file.name.len == 0) break;
67 if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
68 log.debug("found file: '{s}'", .{tar_file.name});
69 const file_name = try gpa.dupe(u8, tar_file.name);
70 if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
71 const pkg_name = file_name[0..pkg_name_end];
72 const gop = try Walk.modules.getOrPut(gpa, pkg_name);
73 const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
74 if (!gop.found_existing or
75 std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
76 std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
77 {
78 gop.value_ptr.* = file;
79 }
80 const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
81 assert(file == try Walk.add_file(file_name, file_bytes));
82 }
83 } else {
84 log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
85 }
86 },
87 else => continue,
88 }
89 }
90}
91
92fn fatal(comptime format: []const u8, args: anytype) noreturn {
93 var buf: [500]u8 = undefined;
94 const line = std.fmt.bufPrint(&buf, format, args) catch l: {
95 buf[buf.len - 3 ..][0..3].* = "...".*;
96 break :l &buf;
97 };
98 js.panic(line.ptr, line.len);
99}
lib/std/Build.zig+8-4
...@@ -2300,22 +2300,26 @@ pub const LazyPath = union(enum) {...@@ -2300,22 +2300,26 @@ pub const LazyPath = union(enum) {
2300 }2300 }
23012301
2302 pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath {2302 pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath {
2303 return lazy_path.join(b.allocator, sub_path) catch @panic("OOM");
2304 }
2305
2306 pub fn join(lazy_path: LazyPath, arena: Allocator, sub_path: []const u8) Allocator.Error!LazyPath {
2303 return switch (lazy_path) {2307 return switch (lazy_path) {
2304 .src_path => |src| .{ .src_path = .{2308 .src_path => |src| .{ .src_path = .{
2305 .owner = src.owner,2309 .owner = src.owner,
2306 .sub_path = b.pathResolve(&.{ src.sub_path, sub_path }),2310 .sub_path = try fs.path.resolve(arena, &.{ src.sub_path, sub_path }),
2307 } },2311 } },
2308 .generated => |gen| .{ .generated = .{2312 .generated => |gen| .{ .generated = .{
2309 .file = gen.file,2313 .file = gen.file,
2310 .up = gen.up,2314 .up = gen.up,
2311 .sub_path = b.pathResolve(&.{ gen.sub_path, sub_path }),2315 .sub_path = try fs.path.resolve(arena, &.{ gen.sub_path, sub_path }),
2312 } },2316 } },
2313 .cwd_relative => |cwd_relative| .{2317 .cwd_relative => |cwd_relative| .{
2314 .cwd_relative = b.pathResolve(&.{ cwd_relative, sub_path }),2318 .cwd_relative = try fs.path.resolve(arena, &.{ cwd_relative, sub_path }),
2315 },2319 },
2316 .dependency => |dep| .{ .dependency = .{2320 .dependency => |dep| .{ .dependency = .{
2317 .dependency = dep.dependency,2321 .dependency = dep.dependency,
2318 .sub_path = b.pathResolve(&.{ dep.sub_path, sub_path }),2322 .sub_path = try fs.path.resolve(arena, &.{ dep.sub_path, sub_path }),
2319 } },2323 } },
2320 };2324 };
2321 }2325 }
lib/std/Build/Fuzz.zig+438-17
...@@ -1,59 +1,479 @@...@@ -1,59 +1,479 @@
1const builtin = @import("builtin");
1const std = @import("../std.zig");2const std = @import("../std.zig");
2const Fuzz = @This();3const Build = std.Build;
3const Step = std.Build.Step;4const Step = std.Build.Step;
4const assert = std.debug.assert;5const assert = std.debug.assert;
5const fatal = std.process.fatal;6const fatal = std.process.fatal;
7const Allocator = std.mem.Allocator;
8const log = std.log;
9
10const Fuzz = @This();
6const build_runner = @import("root");11const build_runner = @import("root");
712
8pub fn start(13pub fn start(
14 gpa: Allocator,
15 arena: Allocator,
16 global_cache_directory: Build.Cache.Directory,
17 zig_lib_directory: Build.Cache.Directory,
18 zig_exe_path: []const u8,
9 thread_pool: *std.Thread.Pool,19 thread_pool: *std.Thread.Pool,
10 all_steps: []const *Step,20 all_steps: []const *Step,
11 ttyconf: std.io.tty.Config,21 ttyconf: std.io.tty.Config,
22 listen_address: std.net.Address,
12 prog_node: std.Progress.Node,23 prog_node: std.Progress.Node,
13) void {24) Allocator.Error!void {
14 const count = block: {25 const fuzz_run_steps = block: {
15 const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0);26 const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0);
16 defer rebuild_node.end();27 defer rebuild_node.end();
17 var count: usize = 0;
18 var wait_group: std.Thread.WaitGroup = .{};28 var wait_group: std.Thread.WaitGroup = .{};
19 defer wait_group.wait();29 defer wait_group.wait();
30 var fuzz_run_steps: std.ArrayListUnmanaged(*Step.Run) = .{};
31 defer fuzz_run_steps.deinit(gpa);
20 for (all_steps) |step| {32 for (all_steps) |step| {
21 const run = step.cast(Step.Run) orelse continue;33 const run = step.cast(Step.Run) orelse continue;
22 if (run.fuzz_tests.items.len > 0 and run.producer != null) {34 if (run.fuzz_tests.items.len > 0 and run.producer != null) {
23 thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, ttyconf, rebuild_node });35 thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, ttyconf, rebuild_node });
24 count += 1;36 try fuzz_run_steps.append(gpa, run);
25 }37 }
26 }38 }
27 if (count == 0) fatal("no fuzz tests found", .{});39 if (fuzz_run_steps.items.len == 0) fatal("no fuzz tests found", .{});
28 rebuild_node.setEstimatedTotalItems(count);40 rebuild_node.setEstimatedTotalItems(fuzz_run_steps.items.len);
29 break :block count;41 break :block try arena.dupe(*Step.Run, fuzz_run_steps.items);
30 };42 };
3143
32 // Detect failure.44 // Detect failure.
33 for (all_steps) |step| {45 for (fuzz_run_steps) |run| {
34 const run = step.cast(Step.Run) orelse continue;46 assert(run.fuzz_tests.items.len > 0);
35 if (run.fuzz_tests.items.len > 0 and run.rebuilt_executable == null)47 if (run.rebuilt_executable == null)
36 fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});48 fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});
37 }49 }
3850
51 var web_server: WebServer = .{
52 .gpa = gpa,
53 .global_cache_directory = global_cache_directory,
54 .zig_lib_directory = zig_lib_directory,
55 .zig_exe_path = zig_exe_path,
56 .msg_queue = .{},
57 .mutex = .{},
58 .listen_address = listen_address,
59 .fuzz_run_steps = fuzz_run_steps,
60 };
61
62 const web_server_thread = std.Thread.spawn(.{}, WebServer.run, .{&web_server}) catch |err| {
63 fatal("unable to spawn web server thread: {s}", .{@errorName(err)});
64 };
65 defer web_server_thread.join();
66
39 {67 {
40 const fuzz_node = prog_node.start("Fuzzing", count);68 const fuzz_node = prog_node.start("Fuzzing", fuzz_run_steps.len);
41 defer fuzz_node.end();69 defer fuzz_node.end();
42 var wait_group: std.Thread.WaitGroup = .{};70 var wait_group: std.Thread.WaitGroup = .{};
43 defer wait_group.wait();71 defer wait_group.wait();
4472
45 for (all_steps) |step| {73 for (fuzz_run_steps) |run| {
46 const run = step.cast(Step.Run) orelse continue;
47 for (run.fuzz_tests.items) |unit_test_index| {74 for (run.fuzz_tests.items) |unit_test_index| {
48 assert(run.rebuilt_executable != null);75 assert(run.rebuilt_executable != null);
49 thread_pool.spawnWg(&wait_group, fuzzWorkerRun, .{ run, unit_test_index, ttyconf, fuzz_node });76 thread_pool.spawnWg(&wait_group, fuzzWorkerRun, .{
77 run, &web_server, unit_test_index, ttyconf, fuzz_node,
78 });
50 }79 }
51 }80 }
52 }81 }
5382
54 fatal("all fuzz workers crashed", .{});83 log.err("all fuzz workers crashed", .{});
55}84}
5685
86pub const WebServer = struct {
87 gpa: Allocator,
88 global_cache_directory: Build.Cache.Directory,
89 zig_lib_directory: Build.Cache.Directory,
90 zig_exe_path: []const u8,
91 /// Messages from fuzz workers. Protected by mutex.
92 msg_queue: std.ArrayListUnmanaged(Msg),
93 mutex: std.Thread.Mutex,
94 listen_address: std.net.Address,
95 fuzz_run_steps: []const *Step.Run,
96
97 const Msg = union(enum) {
98 coverage_id: u64,
99 };
100
101 fn run(ws: *WebServer) void {
102 var http_server = ws.listen_address.listen(.{
103 .reuse_address = true,
104 }) catch |err| {
105 log.err("failed to listen to port {d}: {s}", .{ ws.listen_address.in.getPort(), @errorName(err) });
106 return;
107 };
108 const port = http_server.listen_address.in.getPort();
109 log.info("web interface listening at http://127.0.0.1:{d}/", .{port});
110
111 while (true) {
112 const connection = http_server.accept() catch |err| {
113 log.err("failed to accept connection: {s}", .{@errorName(err)});
114 return;
115 };
116 _ = std.Thread.spawn(.{}, accept, .{ ws, connection }) catch |err| {
117 log.err("unable to spawn connection thread: {s}", .{@errorName(err)});
118 connection.stream.close();
119 continue;
120 };
121 }
122 }
123
124 fn accept(ws: *WebServer, connection: std.net.Server.Connection) void {
125 defer connection.stream.close();
126
127 var read_buffer: [8000]u8 = undefined;
128 var server = std.http.Server.init(connection, &read_buffer);
129 while (server.state == .ready) {
130 var request = server.receiveHead() catch |err| switch (err) {
131 error.HttpConnectionClosing => return,
132 else => {
133 log.err("closing http connection: {s}", .{@errorName(err)});
134 return;
135 },
136 };
137 serveRequest(ws, &request) catch |err| switch (err) {
138 error.AlreadyReported => return,
139 else => |e| {
140 log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(e) });
141 return;
142 },
143 };
144 }
145 }
146
147 fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void {
148 if (std.mem.eql(u8, request.head.target, "/") or
149 std.mem.eql(u8, request.head.target, "/debug") or
150 std.mem.eql(u8, request.head.target, "/debug/"))
151 {
152 try serveFile(ws, request, "fuzzer/index.html", "text/html");
153 } else if (std.mem.eql(u8, request.head.target, "/main.js") or
154 std.mem.eql(u8, request.head.target, "/debug/main.js"))
155 {
156 try serveFile(ws, request, "fuzzer/main.js", "application/javascript");
157 } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) {
158 try serveWasm(ws, request, .ReleaseFast);
159 } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) {
160 try serveWasm(ws, request, .Debug);
161 } else if (std.mem.eql(u8, request.head.target, "/sources.tar") or
162 std.mem.eql(u8, request.head.target, "/debug/sources.tar"))
163 {
164 try serveSourcesTar(ws, request);
165 } else {
166 try request.respond("not found", .{
167 .status = .not_found,
168 .extra_headers = &.{
169 .{ .name = "content-type", .value = "text/plain" },
170 },
171 });
172 }
173 }
174
175 fn serveFile(
176 ws: *WebServer,
177 request: *std.http.Server.Request,
178 name: []const u8,
179 content_type: []const u8,
180 ) !void {
181 const gpa = ws.gpa;
182 // The desired API is actually sendfile, which will require enhancing std.http.Server.
183 // We load the file with every request so that the user can make changes to the file
184 // and refresh the HTML page without restarting this server.
185 const file_contents = ws.zig_lib_directory.handle.readFileAlloc(gpa, name, 10 * 1024 * 1024) catch |err| {
186 log.err("failed to read '{}{s}': {s}", .{ ws.zig_lib_directory, name, @errorName(err) });
187 return error.AlreadyReported;
188 };
189 defer gpa.free(file_contents);
190 try request.respond(file_contents, .{
191 .extra_headers = &.{
192 .{ .name = "content-type", .value = content_type },
193 cache_control_header,
194 },
195 });
196 }
197
198 fn serveWasm(
199 ws: *WebServer,
200 request: *std.http.Server.Request,
201 optimize_mode: std.builtin.OptimizeMode,
202 ) !void {
203 const gpa = ws.gpa;
204
205 var arena_instance = std.heap.ArenaAllocator.init(gpa);
206 defer arena_instance.deinit();
207 const arena = arena_instance.allocator();
208
209 // Do the compilation every request, so that the user can edit the files
210 // and see the changes without restarting the server.
211 const wasm_binary_path = try buildWasmBinary(ws, arena, optimize_mode);
212 // std.http.Server does not have a sendfile API yet.
213 const file_contents = try std.fs.cwd().readFileAlloc(gpa, wasm_binary_path, 10 * 1024 * 1024);
214 defer gpa.free(file_contents);
215 try request.respond(file_contents, .{
216 .extra_headers = &.{
217 .{ .name = "content-type", .value = "application/wasm" },
218 cache_control_header,
219 },
220 });
221 }
222
223 fn buildWasmBinary(
224 ws: *WebServer,
225 arena: Allocator,
226 optimize_mode: std.builtin.OptimizeMode,
227 ) ![]const u8 {
228 const gpa = ws.gpa;
229
230 const main_src_path: Build.Cache.Path = .{
231 .root_dir = ws.zig_lib_directory,
232 .sub_path = "fuzzer/wasm/main.zig",
233 };
234 const walk_src_path: Build.Cache.Path = .{
235 .root_dir = ws.zig_lib_directory,
236 .sub_path = "docs/wasm/Walk.zig",
237 };
238
239 var argv: std.ArrayListUnmanaged([]const u8) = .{};
240
241 try argv.appendSlice(arena, &.{
242 ws.zig_exe_path,
243 "build-exe",
244 "-fno-entry",
245 "-O",
246 @tagName(optimize_mode),
247 "-target",
248 "wasm32-freestanding",
249 "-mcpu",
250 "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext",
251 "--cache-dir",
252 ws.global_cache_directory.path orelse ".",
253 "--global-cache-dir",
254 ws.global_cache_directory.path orelse ".",
255 "--name",
256 "fuzzer",
257 "-rdynamic",
258 "--dep",
259 "Walk",
260 try std.fmt.allocPrint(arena, "-Mroot={}", .{main_src_path}),
261 try std.fmt.allocPrint(arena, "-MWalk={}", .{walk_src_path}),
262 "--listen=-",
263 });
264
265 var child = std.process.Child.init(argv.items, gpa);
266 child.stdin_behavior = .Pipe;
267 child.stdout_behavior = .Pipe;
268 child.stderr_behavior = .Pipe;
269 try child.spawn();
270
271 var poller = std.io.poll(gpa, enum { stdout, stderr }, .{
272 .stdout = child.stdout.?,
273 .stderr = child.stderr.?,
274 });
275 defer poller.deinit();
276
277 try sendMessage(child.stdin.?, .update);
278 try sendMessage(child.stdin.?, .exit);
279
280 const Header = std.zig.Server.Message.Header;
281 var result: ?[]const u8 = null;
282 var result_error_bundle = std.zig.ErrorBundle.empty;
283
284 const stdout = poller.fifo(.stdout);
285
286 poll: while (true) {
287 while (stdout.readableLength() < @sizeOf(Header)) {
288 if (!(try poller.poll())) break :poll;
289 }
290 const header = stdout.reader().readStruct(Header) catch unreachable;
291 while (stdout.readableLength() < header.bytes_len) {
292 if (!(try poller.poll())) break :poll;
293 }
294 const body = stdout.readableSliceOfLen(header.bytes_len);
295
296 switch (header.tag) {
297 .zig_version => {
298 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
299 return error.ZigProtocolVersionMismatch;
300 }
301 },
302 .error_bundle => {
303 const EbHdr = std.zig.Server.Message.ErrorBundle;
304 const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body));
305 const extra_bytes =
306 body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len];
307 const string_bytes =
308 body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len];
309 // TODO: use @ptrCast when the compiler supports it
310 const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes);
311 const extra_array = try arena.alloc(u32, unaligned_extra.len);
312 @memcpy(extra_array, unaligned_extra);
313 result_error_bundle = .{
314 .string_bytes = try arena.dupe(u8, string_bytes),
315 .extra = extra_array,
316 };
317 },
318 .emit_bin_path => {
319 const EbpHdr = std.zig.Server.Message.EmitBinPath;
320 const ebp_hdr = @as(*align(1) const EbpHdr, @ptrCast(body));
321 if (!ebp_hdr.flags.cache_hit) {
322 log.info("source changes detected; rebuilt wasm component", .{});
323 }
324 result = try arena.dupe(u8, body[@sizeOf(EbpHdr)..]);
325 },
326 else => {}, // ignore other messages
327 }
328
329 stdout.discard(body.len);
330 }
331
332 const stderr = poller.fifo(.stderr);
333 if (stderr.readableLength() > 0) {
334 const owned_stderr = try stderr.toOwnedSlice();
335 defer gpa.free(owned_stderr);
336 std.debug.print("{s}", .{owned_stderr});
337 }
338
339 // Send EOF to stdin.
340 child.stdin.?.close();
341 child.stdin = null;
342
343 switch (try child.wait()) {
344 .Exited => |code| {
345 if (code != 0) {
346 log.err(
347 "the following command exited with error code {d}:\n{s}",
348 .{ code, try Build.Step.allocPrintCmd(arena, null, argv.items) },
349 );
350 return error.WasmCompilationFailed;
351 }
352 },
353 .Signal, .Stopped, .Unknown => {
354 log.err(
355 "the following command terminated unexpectedly:\n{s}",
356 .{try Build.Step.allocPrintCmd(arena, null, argv.items)},
357 );
358 return error.WasmCompilationFailed;
359 },
360 }
361
362 if (result_error_bundle.errorMessageCount() > 0) {
363 const color = std.zig.Color.auto;
364 result_error_bundle.renderToStdErr(color.renderOptions());
365 log.err("the following command failed with {d} compilation errors:\n{s}", .{
366 result_error_bundle.errorMessageCount(),
367 try Build.Step.allocPrintCmd(arena, null, argv.items),
368 });
369 return error.WasmCompilationFailed;
370 }
371
372 return result orelse {
373 log.err("child process failed to report result\n{s}", .{
374 try Build.Step.allocPrintCmd(arena, null, argv.items),
375 });
376 return error.WasmCompilationFailed;
377 };
378 }
379
380 fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void {
381 const header: std.zig.Client.Message.Header = .{
382 .tag = tag,
383 .bytes_len = 0,
384 };
385 try file.writeAll(std.mem.asBytes(&header));
386 }
387
388 fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
389 const gpa = ws.gpa;
390
391 var arena_instance = std.heap.ArenaAllocator.init(gpa);
392 defer arena_instance.deinit();
393 const arena = arena_instance.allocator();
394
395 var send_buffer: [0x4000]u8 = undefined;
396 var response = request.respondStreaming(.{
397 .send_buffer = &send_buffer,
398 .respond_options = .{
399 .extra_headers = &.{
400 .{ .name = "content-type", .value = "application/x-tar" },
401 cache_control_header,
402 },
403 },
404 });
405 const w = response.writer();
406
407 const DedupeTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
408 var dedupe_table: DedupeTable = .{};
409 defer dedupe_table.deinit(gpa);
410
411 for (ws.fuzz_run_steps) |run_step| {
412 const compile_step_inputs = run_step.producer.?.step.inputs.table;
413 for (compile_step_inputs.keys(), compile_step_inputs.values()) |dir_path, *file_list| {
414 try dedupe_table.ensureUnusedCapacity(gpa, file_list.items.len);
415 for (file_list.items) |sub_path| {
416 // Special file "." means the entire directory.
417 if (std.mem.eql(u8, sub_path, ".")) continue;
418 const joined_path = try dir_path.join(arena, sub_path);
419 _ = dedupe_table.getOrPutAssumeCapacity(joined_path);
420 }
421 }
422 }
423
424 const deduped_paths = dedupe_table.keys();
425 const SortContext = struct {
426 pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool {
427 _ = this;
428 return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) {
429 .lt => true,
430 .gt => false,
431 .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path),
432 };
433 }
434 };
435 std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
436
437 for (deduped_paths) |joined_path| {
438 var file = joined_path.root_dir.handle.openFile(joined_path.sub_path, .{}) catch |err| {
439 log.err("failed to open {}: {s}", .{ joined_path, @errorName(err) });
440 continue;
441 };
442 defer file.close();
443
444 const stat = file.stat() catch |err| {
445 log.err("failed to stat {}: {s}", .{ joined_path, @errorName(err) });
446 continue;
447 };
448 if (stat.kind != .file)
449 continue;
450
451 const padding = p: {
452 const remainder = stat.size % 512;
453 break :p if (remainder > 0) 512 - remainder else 0;
454 };
455
456 var file_header = std.tar.output.Header.init();
457 file_header.typeflag = .regular;
458 try file_header.setPath(joined_path.root_dir.path orelse ".", joined_path.sub_path);
459 try file_header.setSize(stat.size);
460 try file_header.updateChecksum();
461 try w.writeAll(std.mem.asBytes(&file_header));
462 try w.writeFile(file);
463 try w.writeByteNTimes(0, padding);
464 }
465
466 // intentionally omitting the pointless trailer
467 //try w.writeByteNTimes(0, 512 * 2);
468 try response.end();
469 }
470
471 const cache_control_header: std.http.Header = .{
472 .name = "cache-control",
473 .value = "max-age=0, must-revalidate",
474 };
475};
476
57fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog_node: std.Progress.Node) void {477fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog_node: std.Progress.Node) void {
58 const gpa = run.step.owner.allocator;478 const gpa = run.step.owner.allocator;
59 const stderr = std.io.getStdErr();479 const stderr = std.io.getStdErr();
...@@ -88,6 +508,7 @@ fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog...@@ -88,6 +508,7 @@ fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog
88508
89fn fuzzWorkerRun(509fn fuzzWorkerRun(
90 run: *Step.Run,510 run: *Step.Run,
511 web_server: *WebServer,
91 unit_test_index: u32,512 unit_test_index: u32,
92 ttyconf: std.io.tty.Config,513 ttyconf: std.io.tty.Config,
93 parent_prog_node: std.Progress.Node,514 parent_prog_node: std.Progress.Node,
...@@ -98,7 +519,7 @@ fn fuzzWorkerRun(...@@ -98,7 +519,7 @@ fn fuzzWorkerRun(
98 const prog_node = parent_prog_node.start(test_name, 0);519 const prog_node = parent_prog_node.start(test_name, 0);
99 defer prog_node.end();520 defer prog_node.end();
100521
101 run.rerunInFuzzMode(unit_test_index, prog_node) catch |err| switch (err) {522 run.rerunInFuzzMode(web_server, unit_test_index, prog_node) catch |err| switch (err) {
102 error.MakeFailed => {523 error.MakeFailed => {
103 const stderr = std.io.getStdErr();524 const stderr = std.io.getStdErr();
104 std.debug.lockStdErr();525 std.debug.lockStdErr();
lib/std/Build/Step.zig+2-1
...@@ -559,7 +559,8 @@ fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool) !?[]const u8 {...@@ -559,7 +559,8 @@ fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool) !?[]const u8 {
559 },559 },
560 .zig_lib => zl: {560 .zig_lib => zl: {
561 if (s.cast(Step.Compile)) |compile| {561 if (s.cast(Step.Compile)) |compile| {
562 if (compile.zig_lib_dir) |lp| {562 if (compile.zig_lib_dir) |zig_lib_dir| {
563 const lp = try zig_lib_dir.join(arena, sub_path);
563 try addWatchInput(s, lp);564 try addWatchInput(s, lp);
564 break :zl;565 break :zl;
565 }566 }
lib/std/Build/Step/Run.zig+38-14
...@@ -205,6 +205,7 @@ pub fn enableTestRunnerMode(run: *Run) void {...@@ -205,6 +205,7 @@ pub fn enableTestRunnerMode(run: *Run) void {
205 run.stdio = .zig_test;205 run.stdio = .zig_test;
206 run.addArgs(&.{206 run.addArgs(&.{
207 std.fmt.allocPrint(arena, "--seed=0x{x}", .{b.graph.random_seed}) catch @panic("OOM"),207 std.fmt.allocPrint(arena, "--seed=0x{x}", .{b.graph.random_seed}) catch @panic("OOM"),
208 std.fmt.allocPrint(arena, "--cache-dir={s}", .{b.cache_root.path orelse ""}) catch @panic("OOM"),
208 "--listen=-",209 "--listen=-",
209 });210 });
210}211}
...@@ -845,7 +846,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -845,7 +846,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
845 );846 );
846}847}
847848
848pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress.Node) !void {849pub fn rerunInFuzzMode(
850 run: *Run,
851 web_server: *std.Build.Fuzz.WebServer,
852 unit_test_index: u32,
853 prog_node: std.Progress.Node,
854) !void {
849 const step = &run.step;855 const step = &run.step;
850 const b = step.owner;856 const b = step.owner;
851 const arena = b.allocator;857 const arena = b.allocator;
...@@ -877,7 +883,10 @@ pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress....@@ -877,7 +883,10 @@ pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress.
877 const has_side_effects = false;883 const has_side_effects = false;
878 const rand_int = std.crypto.random.int(u64);884 const rand_int = std.crypto.random.int(u64);
879 const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);885 const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
880 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, unit_test_index);886 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, .{
887 .unit_test_index = unit_test_index,
888 .web_server = web_server,
889 });
881}890}
882891
883fn populateGeneratedPaths(892fn populateGeneratedPaths(
...@@ -952,13 +961,18 @@ fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term...@@ -952,13 +961,18 @@ fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term
952 };961 };
953}962}
954963
964const FuzzContext = struct {
965 web_server: *std.Build.Fuzz.WebServer,
966 unit_test_index: u32,
967};
968
955fn runCommand(969fn runCommand(
956 run: *Run,970 run: *Run,
957 argv: []const []const u8,971 argv: []const []const u8,
958 has_side_effects: bool,972 has_side_effects: bool,
959 output_dir_path: []const u8,973 output_dir_path: []const u8,
960 prog_node: std.Progress.Node,974 prog_node: std.Progress.Node,
961 fuzz_unit_test_index: ?u32,975 fuzz_context: ?FuzzContext,
962) !void {976) !void {
963 const step = &run.step;977 const step = &run.step;
964 const b = step.owner;978 const b = step.owner;
...@@ -977,7 +991,7 @@ fn runCommand(...@@ -977,7 +991,7 @@ fn runCommand(
977 var interp_argv = std.ArrayList([]const u8).init(b.allocator);991 var interp_argv = std.ArrayList([]const u8).init(b.allocator);
978 defer interp_argv.deinit();992 defer interp_argv.deinit();
979993
980 const result = spawnChildAndCollect(run, argv, has_side_effects, prog_node, fuzz_unit_test_index) catch |err| term: {994 const result = spawnChildAndCollect(run, argv, has_side_effects, prog_node, fuzz_context) catch |err| term: {
981 // InvalidExe: cpu arch mismatch995 // InvalidExe: cpu arch mismatch
982 // FileNotFound: can happen with a wrong dynamic linker path996 // FileNotFound: can happen with a wrong dynamic linker path
983 if (err == error.InvalidExe or err == error.FileNotFound) interpret: {997 if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
...@@ -1113,7 +1127,7 @@ fn runCommand(...@@ -1113,7 +1127,7 @@ fn runCommand(
11131127
1114 try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);1128 try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);
11151129
1116 break :term spawnChildAndCollect(run, interp_argv.items, has_side_effects, prog_node, fuzz_unit_test_index) catch |e| {1130 break :term spawnChildAndCollect(run, interp_argv.items, has_side_effects, prog_node, fuzz_context) catch |e| {
1117 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;1131 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
11181132
1119 return step.fail("unable to spawn interpreter {s}: {s}", .{1133 return step.fail("unable to spawn interpreter {s}: {s}", .{
...@@ -1133,7 +1147,7 @@ fn runCommand(...@@ -1133,7 +1147,7 @@ fn runCommand(
11331147
1134 const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items;1148 const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items;
11351149
1136 if (fuzz_unit_test_index != null) {1150 if (fuzz_context != null) {
1137 try step.handleChildProcessTerm(result.term, cwd, final_argv);1151 try step.handleChildProcessTerm(result.term, cwd, final_argv);
1138 return;1152 return;
1139 }1153 }
...@@ -1298,12 +1312,12 @@ fn spawnChildAndCollect(...@@ -1298,12 +1312,12 @@ fn spawnChildAndCollect(
1298 argv: []const []const u8,1312 argv: []const []const u8,
1299 has_side_effects: bool,1313 has_side_effects: bool,
1300 prog_node: std.Progress.Node,1314 prog_node: std.Progress.Node,
1301 fuzz_unit_test_index: ?u32,1315 fuzz_context: ?FuzzContext,
1302) !ChildProcResult {1316) !ChildProcResult {
1303 const b = run.step.owner;1317 const b = run.step.owner;
1304 const arena = b.allocator;1318 const arena = b.allocator;
13051319
1306 if (fuzz_unit_test_index != null) {1320 if (fuzz_context != null) {
1307 assert(!has_side_effects);1321 assert(!has_side_effects);
1308 assert(run.stdio == .zig_test);1322 assert(run.stdio == .zig_test);
1309 }1323 }
...@@ -1357,7 +1371,7 @@ fn spawnChildAndCollect(...@@ -1357,7 +1371,7 @@ fn spawnChildAndCollect(
1357 var timer = try std.time.Timer.start();1371 var timer = try std.time.Timer.start();
13581372
1359 const result = if (run.stdio == .zig_test)1373 const result = if (run.stdio == .zig_test)
1360 evalZigTest(run, &child, prog_node, fuzz_unit_test_index)1374 evalZigTest(run, &child, prog_node, fuzz_context)
1361 else1375 else
1362 evalGeneric(run, &child);1376 evalGeneric(run, &child);
13631377
...@@ -1383,7 +1397,7 @@ fn evalZigTest(...@@ -1383,7 +1397,7 @@ fn evalZigTest(
1383 run: *Run,1397 run: *Run,
1384 child: *std.process.Child,1398 child: *std.process.Child,
1385 prog_node: std.Progress.Node,1399 prog_node: std.Progress.Node,
1386 fuzz_unit_test_index: ?u32,1400 fuzz_context: ?FuzzContext,
1387) !StdIoResult {1401) !StdIoResult {
1388 const gpa = run.step.owner.allocator;1402 const gpa = run.step.owner.allocator;
1389 const arena = run.step.owner.allocator;1403 const arena = run.step.owner.allocator;
...@@ -1394,8 +1408,8 @@ fn evalZigTest(...@@ -1394,8 +1408,8 @@ fn evalZigTest(
1394 });1408 });
1395 defer poller.deinit();1409 defer poller.deinit();
13961410
1397 if (fuzz_unit_test_index) |index| {1411 if (fuzz_context) |fuzz| {
1398 try sendRunTestMessage(child.stdin.?, .start_fuzzing, index);1412 try sendRunTestMessage(child.stdin.?, .start_fuzzing, fuzz.unit_test_index);
1399 } else {1413 } else {
1400 run.fuzz_tests.clearRetainingCapacity();1414 run.fuzz_tests.clearRetainingCapacity();
1401 try sendMessage(child.stdin.?, .query_test_metadata);1415 try sendMessage(child.stdin.?, .query_test_metadata);
...@@ -1437,7 +1451,7 @@ fn evalZigTest(...@@ -1437,7 +1451,7 @@ fn evalZigTest(
1437 }1451 }
1438 },1452 },
1439 .test_metadata => {1453 .test_metadata => {
1440 assert(fuzz_unit_test_index == null);1454 assert(fuzz_context == null);
1441 const TmHdr = std.zig.Server.Message.TestMetadata;1455 const TmHdr = std.zig.Server.Message.TestMetadata;
1442 const tm_hdr = @as(*align(1) const TmHdr, @ptrCast(body));1456 const tm_hdr = @as(*align(1) const TmHdr, @ptrCast(body));
1443 test_count = tm_hdr.tests_len;1457 test_count = tm_hdr.tests_len;
...@@ -1466,7 +1480,7 @@ fn evalZigTest(...@@ -1466,7 +1480,7 @@ fn evalZigTest(
1466 try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);1480 try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
1467 },1481 },
1468 .test_results => {1482 .test_results => {
1469 assert(fuzz_unit_test_index == null);1483 assert(fuzz_context == null);
1470 const md = metadata.?;1484 const md = metadata.?;
14711485
1472 const TrHdr = std.zig.Server.Message.TestResults;1486 const TrHdr = std.zig.Server.Message.TestResults;
...@@ -1500,6 +1514,16 @@ fn evalZigTest(...@@ -1500,6 +1514,16 @@ fn evalZigTest(
15001514
1501 try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);1515 try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
1502 },1516 },
1517 .coverage_id => {
1518 const web_server = fuzz_context.?.web_server;
1519 const msg_ptr: *align(1) const u64 = @ptrCast(body);
1520 const coverage_id = msg_ptr.*;
1521 {
1522 web_server.mutex.lock();
1523 defer web_server.mutex.unlock();
1524 try web_server.msg_queue.append(web_server.gpa, .{ .coverage_id = coverage_id });
1525 }
1526 },
1503 else => {}, // ignore other messages1527 else => {}, // ignore other messages
1504 }1528 }
15051529
lib/std/zig/Server.zig+18-6
...@@ -28,6 +28,10 @@ pub const Message = struct {...@@ -28,6 +28,10 @@ pub const Message = struct {
28 /// The remaining bytes is the file path relative to that prefix.28 /// The remaining bytes is the file path relative to that prefix.
29 /// The prefixes are hard-coded in Compilation.create (cwd, zig lib dir, local cache dir)29 /// The prefixes are hard-coded in Compilation.create (cwd, zig lib dir, local cache dir)
30 file_system_inputs,30 file_system_inputs,
31 /// Body is a u64le that indicates the file path within the cache used
32 /// to store coverage information. The integer is a hash of the PCs
33 /// stored within that file.
34 coverage_id,
3135
32 _,36 _,
33 };37 };
...@@ -180,6 +184,14 @@ pub fn serveMessage(...@@ -180,6 +184,14 @@ pub fn serveMessage(
180 try s.out.writevAll(iovecs[0 .. bufs.len + 1]);184 try s.out.writevAll(iovecs[0 .. bufs.len + 1]);
181}185}
182186
187pub fn serveU64Message(s: *Server, tag: OutMessage.Tag, int: u64) !void {
188 const msg_le = bswap(int);
189 return s.serveMessage(.{
190 .tag = tag,
191 .bytes_len = @sizeOf(u64),
192 }, &.{std.mem.asBytes(&msg_le)});
193}
194
183pub fn serveEmitBinPath(195pub fn serveEmitBinPath(
184 s: *Server,196 s: *Server,
185 fs_path: []const u8,197 fs_path: []const u8,
...@@ -187,7 +199,7 @@ pub fn serveEmitBinPath(...@@ -187,7 +199,7 @@ pub fn serveEmitBinPath(
187) !void {199) !void {
188 try s.serveMessage(.{200 try s.serveMessage(.{
189 .tag = .emit_bin_path,201 .tag = .emit_bin_path,
190 .bytes_len = @as(u32, @intCast(fs_path.len + @sizeOf(OutMessage.EmitBinPath))),202 .bytes_len = @intCast(fs_path.len + @sizeOf(OutMessage.EmitBinPath)),
191 }, &.{203 }, &.{
192 std.mem.asBytes(&header),204 std.mem.asBytes(&header),
193 fs_path,205 fs_path,
...@@ -201,7 +213,7 @@ pub fn serveTestResults(...@@ -201,7 +213,7 @@ pub fn serveTestResults(
201 const msg_le = bswap(msg);213 const msg_le = bswap(msg);
202 try s.serveMessage(.{214 try s.serveMessage(.{
203 .tag = .test_results,215 .tag = .test_results,
204 .bytes_len = @as(u32, @intCast(@sizeOf(OutMessage.TestResults))),216 .bytes_len = @intCast(@sizeOf(OutMessage.TestResults)),
205 }, &.{217 }, &.{
206 std.mem.asBytes(&msg_le),218 std.mem.asBytes(&msg_le),
207 });219 });
...@@ -209,14 +221,14 @@ pub fn serveTestResults(...@@ -209,14 +221,14 @@ pub fn serveTestResults(
209221
210pub fn serveErrorBundle(s: *Server, error_bundle: std.zig.ErrorBundle) !void {222pub fn serveErrorBundle(s: *Server, error_bundle: std.zig.ErrorBundle) !void {
211 const eb_hdr: OutMessage.ErrorBundle = .{223 const eb_hdr: OutMessage.ErrorBundle = .{
212 .extra_len = @as(u32, @intCast(error_bundle.extra.len)),224 .extra_len = @intCast(error_bundle.extra.len),
213 .string_bytes_len = @as(u32, @intCast(error_bundle.string_bytes.len)),225 .string_bytes_len = @intCast(error_bundle.string_bytes.len),
214 };226 };
215 const bytes_len = @sizeOf(OutMessage.ErrorBundle) +227 const bytes_len = @sizeOf(OutMessage.ErrorBundle) +
216 4 * error_bundle.extra.len + error_bundle.string_bytes.len;228 4 * error_bundle.extra.len + error_bundle.string_bytes.len;
217 try s.serveMessage(.{229 try s.serveMessage(.{
218 .tag = .error_bundle,230 .tag = .error_bundle,
219 .bytes_len = @as(u32, @intCast(bytes_len)),231 .bytes_len = @intCast(bytes_len),
220 }, &.{232 }, &.{
221 std.mem.asBytes(&eb_hdr),233 std.mem.asBytes(&eb_hdr),
222 // TODO: implement @ptrCast between slices changing the length234 // TODO: implement @ptrCast between slices changing the length
...@@ -251,7 +263,7 @@ pub fn serveTestMetadata(s: *Server, test_metadata: TestMetadata) !void {...@@ -251,7 +263,7 @@ pub fn serveTestMetadata(s: *Server, test_metadata: TestMetadata) !void {
251263
252 return s.serveMessage(.{264 return s.serveMessage(.{
253 .tag = .test_metadata,265 .tag = .test_metadata,
254 .bytes_len = @as(u32, @intCast(bytes_len)),266 .bytes_len = @intCast(bytes_len),
255 }, &.{267 }, &.{
256 std.mem.asBytes(&header),268 std.mem.asBytes(&header),
257 // TODO: implement @ptrCast between slices changing the length269 // TODO: implement @ptrCast between slices changing the length
lib/std/zig/tokenizer.zig+45
...@@ -1840,3 +1840,48 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v...@@ -1840,3 +1840,48 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v
1840 try std.testing.expectEqual(source.len, last_token.loc.start);1840 try std.testing.expectEqual(source.len, last_token.loc.start);
1841 try std.testing.expectEqual(source.len, last_token.loc.end);1841 try std.testing.expectEqual(source.len, last_token.loc.end);
1842}1842}
1843
1844test "fuzzable properties upheld" {
1845 const source = std.testing.fuzzInput(.{});
1846 const source0 = try std.testing.allocator.dupeZ(u8, source);
1847 defer std.testing.allocator.free(source0);
1848 var tokenizer = Tokenizer.init(source0);
1849 var tokenization_failed = false;
1850 while (true) {
1851 const token = tokenizer.next();
1852
1853 // Property: token end location after start location (or equal)
1854 try std.testing.expect(token.loc.end >= token.loc.start);
1855
1856 switch (token.tag) {
1857 .invalid => {
1858 tokenization_failed = true;
1859
1860 // Property: invalid token always ends at newline or eof
1861 try std.testing.expect(source0[token.loc.end] == '\n' or source0[token.loc.end] == 0);
1862 },
1863 .eof => {
1864 // Property: EOF token is always 0-length at end of source.
1865 try std.testing.expectEqual(source0.len, token.loc.start);
1866 try std.testing.expectEqual(source0.len, token.loc.end);
1867 break;
1868 },
1869 else => continue,
1870 }
1871 }
1872
1873 if (source0.len > 0) for (source0, source0[1..][0..source0.len]) |cur, next| {
1874 // Property: No null byte allowed except at end.
1875 if (cur == 0) {
1876 try std.testing.expect(tokenization_failed);
1877 }
1878 // Property: No ASCII control characters other than \n and \t are allowed.
1879 if (std.ascii.isControl(cur) and cur != '\n' and cur != '\t') {
1880 try std.testing.expect(tokenization_failed);
1881 }
1882 // Property: All '\r' must be followed by '\n'.
1883 if (cur == '\r' and next != '\n') {
1884 try std.testing.expect(tokenization_failed);
1885 }
1886 };
1887}