Skip to content

Commit 8a73fc8

Browse files
authored
Merge pull request #25981 from mlugg/macos-fuzz-2
make the fuzzer vaguely work on macOS
2 parents a9568ed + a87b533 commit 8a73fc8

File tree

21 files changed

+876
-534
lines changed

21 files changed

+876
-534
lines changed

lib/build-web/fuzz.zig

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -228,20 +228,21 @@ fn unpackSourcesInner(tar_bytes: []u8) !void {
228228
if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
229229
log.debug("found file: '{s}'", .{tar_file.name});
230230
const file_name = try gpa.dupe(u8, tar_file.name);
231-
if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
232-
const pkg_name = file_name[0..pkg_name_end];
233-
const gop = try Walk.modules.getOrPut(gpa, pkg_name);
234-
const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
235-
if (!gop.found_existing or
236-
std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
237-
std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
238-
{
239-
gop.value_ptr.* = file;
240-
}
241-
const file_bytes = tar_reader.take(@intCast(tar_file.size)) catch unreachable;
242-
it.unread_file_bytes = 0; // we have read the whole thing
243-
assert(file == try Walk.add_file(file_name, file_bytes));
244-
}
231+
// This is a hack to guess modules from the tar file contents. To handle modules
232+
// properly, the build system will need to change the structure here to have one
233+
// directory per module. This in turn requires compiler enhancements to allow
234+
// the build system to actually discover the required information.
235+
const mod_name, const is_module_root = p: {
236+
if (std.mem.find(u8, file_name, "std/")) |i| break :p .{ "std", std.mem.eql(u8, file_name[i + 4 ..], "std.zig") };
237+
if (std.mem.endsWith(u8, file_name, "/builtin.zig")) break :p .{ "builtin", true };
238+
break :p .{ "root", std.mem.endsWith(u8, file_name, "/root.zig") };
239+
};
240+
const gop = try Walk.modules.getOrPut(gpa, mod_name);
241+
const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
242+
if (!gop.found_existing or is_module_root) gop.value_ptr.* = file;
243+
const file_bytes = tar_reader.take(@intCast(tar_file.size)) catch unreachable;
244+
it.unread_file_bytes = 0; // we have read the whole thing
245+
assert(file == try Walk.add_file(file_name, file_bytes));
245246
} else {
246247
log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
247248
}

lib/compiler/test_runner.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ fn mainServer() !void {
184184
const test_fn = builtin.test_functions[index];
185185
const entry_addr = @intFromPtr(test_fn.func);
186186

187-
try server.serveU64Message(.fuzz_start_addr, entry_addr);
187+
try server.serveU64Message(.fuzz_start_addr, fuzz_abi.fuzzer_unslide_address(entry_addr));
188188
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
189189
is_fuzz_test = false;
190190
fuzz_test_index = index;

lib/fuzzer.zig

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,18 @@ const Executable = struct {
116116
"failed to init memory map for coverage file '{s}': {t}",
117117
.{ &coverage_file_name, e },
118118
);
119-
map.appendSliceAssumeCapacity(mem.asBytes(&abi.SeenPcsHeader{
119+
map.appendSliceAssumeCapacity(@ptrCast(&abi.SeenPcsHeader{
120120
.n_runs = 0,
121121
.unique_runs = 0,
122122
.pcs_len = pcs.len,
123123
}));
124124
map.appendNTimesAssumeCapacity(0, pc_bitset_usizes * @sizeOf(usize));
125-
map.appendSliceAssumeCapacity(mem.sliceAsBytes(pcs));
125+
// Relocations have been applied to `pcs` so it contains runtime addresses (with slide
126+
// applied). We need to translate these to the virtual addresses as on disk.
127+
for (pcs) |pc| {
128+
const pc_vaddr = fuzzer_unslide_address(pc);
129+
map.appendSliceAssumeCapacity(@ptrCast(&pc_vaddr));
130+
}
126131
return map;
127132
} else {
128133
const size = coverage_file.getEndPos() catch |e| panic(
@@ -215,7 +220,16 @@ const Executable = struct {
215220
.{ self.pc_counters.len, pcs.len },
216221
);
217222

218-
self.pc_digest = std.hash.Wyhash.hash(0, mem.sliceAsBytes(pcs));
223+
self.pc_digest = digest: {
224+
// Relocations have been applied to `pcs` so it contains runtime addresses (with slide
225+
// applied). We need to translate these to the virtual addresses as on disk.
226+
var h: std.hash.Wyhash = .init(0);
227+
for (pcs) |pc| {
228+
const pc_vaddr = fuzzer_unslide_address(pc);
229+
h.update(@ptrCast(&pc_vaddr));
230+
}
231+
break :digest h.final();
232+
};
219233
self.shared_seen_pcs = getCoverageFile(cache_dir, pcs, self.pc_digest);
220234

221235
return self;
@@ -622,6 +636,14 @@ export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void {
622636
}
623637
}
624638

639+
export fn fuzzer_unslide_address(addr: usize) usize {
640+
const si = std.debug.getSelfDebugInfo() catch @compileError("unsupported");
641+
const slide = si.getModuleSlide(std.debug.getDebugInfoAllocator(), addr) catch |err| {
642+
std.debug.panic("failed to find virtual address slide: {t}", .{err});
643+
};
644+
return addr - slide;
645+
}
646+
625647
/// Helps determine run uniqueness in the face of recursion.
626648
/// Currently not used by the fuzzer.
627649
export threadlocal var __sancov_lowest_stack: usize = 0;
@@ -1185,13 +1207,13 @@ const Mutation = enum {
11851207
const j = rng.uintAtMostBiased(usize, corpus[splice_i].len - len);
11861208
out.appendSliceAssumeCapacity(corpus[splice_i][j..][0..len]);
11871209
},
1188-
.@"const" => out.appendSliceAssumeCapacity(mem.asBytes(
1210+
.@"const" => out.appendSliceAssumeCapacity(@ptrCast(
11891211
&data_ctx[rng.uintLessThanBiased(usize, data_ctx.len)],
11901212
)),
1191-
.small => out.appendSliceAssumeCapacity(mem.asBytes(
1213+
.small => out.appendSliceAssumeCapacity(@ptrCast(
11921214
&mem.nativeTo(data_ctx[0], rng.int(SmallValue), data_ctx[1]),
11931215
)),
1194-
.few => out.appendSliceAssumeCapacity(mem.asBytes(
1216+
.few => out.appendSliceAssumeCapacity(@ptrCast(
11951217
&fewValue(rng, data_ctx[0], data_ctx[1]),
11961218
)),
11971219
}

lib/std/Build/Fuzz.zig

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,14 @@ fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutO
383383
errdefer gop.value_ptr.coverage.deinit(fuzz.gpa);
384384

385385
const rebuilt_exe_path = run_step.rebuilt_executable.?;
386-
var debug_info = std.debug.Info.load(fuzz.gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| {
386+
const target = run_step.producer.?.rootModuleTarget();
387+
var debug_info = std.debug.Info.load(
388+
fuzz.gpa,
389+
rebuilt_exe_path,
390+
&gop.value_ptr.coverage,
391+
target.ofmt,
392+
target.cpu.arch,
393+
) catch |err| {
387394
log.err("step '{s}': failed to load debug information for '{f}': {s}", .{
388395
run_step.step.name, rebuilt_exe_path, @errorName(err),
389396
});
@@ -479,9 +486,23 @@ fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReporte
479486
if (false) {
480487
const sl = coverage_map.source_locations[index];
481488
const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
482-
log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
483-
addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
484-
});
489+
if (pcs.len == 1) {
490+
log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{
491+
addr, file_name, sl.line, sl.column,
492+
});
493+
} else if (index == 0) {
494+
log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{
495+
addr, file_name, sl.line, sl.column, pcs[index + 1],
496+
});
497+
} else if (index == pcs.len - 1) {
498+
log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{
499+
addr, file_name, sl.line, sl.column, index, pcs[index - 1],
500+
});
501+
} else {
502+
log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
503+
addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
504+
});
505+
}
485506
}
486507
try coverage_map.entry_points.append(fuzz.gpa, @intCast(index));
487508
}

lib/std/Build/Step/CheckObject.zig

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -729,10 +729,10 @@ const MachODumper = struct {
729729
imports: std.ArrayListUnmanaged([]const u8) = .empty,
730730

731731
fn parse(ctx: *ObjectContext) !void {
732-
var it = ctx.getLoadCommandIterator();
732+
var it = try ctx.getLoadCommandIterator();
733733
var i: usize = 0;
734-
while (it.next()) |cmd| {
735-
switch (cmd.cmd()) {
734+
while (try it.next()) |cmd| {
735+
switch (cmd.hdr.cmd) {
736736
.SEGMENT_64 => {
737737
const seg = cmd.cast(macho.segment_command_64).?;
738738
try ctx.segments.append(ctx.gpa, seg);
@@ -771,14 +771,13 @@ const MachODumper = struct {
771771
return mem.sliceTo(@as([*:0]const u8, @ptrCast(ctx.strtab.items.ptr + off)), 0);
772772
}
773773

774-
fn getLoadCommandIterator(ctx: ObjectContext) macho.LoadCommandIterator {
775-
const data = ctx.data[@sizeOf(macho.mach_header_64)..][0..ctx.header.sizeofcmds];
776-
return .{ .ncmds = ctx.header.ncmds, .buffer = data };
774+
fn getLoadCommandIterator(ctx: ObjectContext) !macho.LoadCommandIterator {
775+
return .init(&ctx.header, ctx.data[@sizeOf(macho.mach_header_64)..]);
777776
}
778777

779-
fn getLoadCommand(ctx: ObjectContext, cmd: macho.LC) ?macho.LoadCommandIterator.LoadCommand {
780-
var it = ctx.getLoadCommandIterator();
781-
while (it.next()) |lc| if (lc.cmd() == cmd) {
778+
fn getLoadCommand(ctx: ObjectContext, cmd: macho.LC) !?macho.LoadCommandIterator.LoadCommand {
779+
var it = try ctx.getLoadCommandIterator();
780+
while (try it.next()) |lc| if (lc.hdr.cmd == cmd) {
782781
return lc;
783782
};
784783
return null;
@@ -872,9 +871,9 @@ const MachODumper = struct {
872871
\\LC {d}
873872
\\cmd {s}
874873
\\cmdsize {d}
875-
, .{ index, @tagName(lc.cmd()), lc.cmdsize() });
874+
, .{ index, @tagName(lc.hdr.cmd), lc.hdr.cmdsize });
876875

877-
switch (lc.cmd()) {
876+
switch (lc.hdr.cmd) {
878877
.SEGMENT_64 => {
879878
const seg = lc.cast(macho.segment_command_64).?;
880879
try writer.writeByte('\n');
@@ -1592,9 +1591,9 @@ const MachODumper = struct {
15921591
.headers => {
15931592
try ObjectContext.dumpHeader(ctx.header, writer);
15941593

1595-
var it = ctx.getLoadCommandIterator();
1594+
var it = try ctx.getLoadCommandIterator();
15961595
var i: usize = 0;
1597-
while (it.next()) |cmd| {
1596+
while (try it.next()) |cmd| {
15981597
try ObjectContext.dumpLoadCommand(cmd, i, writer);
15991598
try writer.writeByte('\n');
16001599

@@ -1615,7 +1614,7 @@ const MachODumper = struct {
16151614
.dyld_weak_bind,
16161615
.dyld_lazy_bind,
16171616
=> {
1618-
const cmd = ctx.getLoadCommand(.DYLD_INFO_ONLY) orelse
1617+
const cmd = try ctx.getLoadCommand(.DYLD_INFO_ONLY) orelse
16191618
return step.fail("no dyld info found", .{});
16201619
const lc = cmd.cast(macho.dyld_info_command).?;
16211620

@@ -1649,7 +1648,7 @@ const MachODumper = struct {
16491648
},
16501649

16511650
.exports => blk: {
1652-
if (ctx.getLoadCommand(.DYLD_INFO_ONLY)) |cmd| {
1651+
if (try ctx.getLoadCommand(.DYLD_INFO_ONLY)) |cmd| {
16531652
const lc = cmd.cast(macho.dyld_info_command).?;
16541653
if (lc.export_size > 0) {
16551654
const data = ctx.data[lc.export_off..][0..lc.export_size];

lib/std/Build/Step/Compile.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,6 +1932,11 @@ pub fn rebuildInFuzzMode(c: *Compile, gpa: Allocator, progress_node: std.Progres
19321932
c.step.result_error_bundle.deinit(gpa);
19331933
c.step.result_error_bundle = std.zig.ErrorBundle.empty;
19341934

1935+
if (c.step.result_failed_command) |cmd| {
1936+
gpa.free(cmd);
1937+
c.step.result_failed_command = null;
1938+
}
1939+
19351940
const zig_args = try getZigArgs(c, true);
19361941
const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false, null, gpa);
19371942
return maybe_output_bin_path.?;

lib/std/Build/Step/Run.zig

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,12 @@ pub fn rerunInFuzzMode(
11401140
.output_file, .output_directory => unreachable,
11411141
}
11421142
}
1143+
1144+
if (run.step.result_failed_command) |cmd| {
1145+
fuzz.gpa.free(cmd);
1146+
run.step.result_failed_command = null;
1147+
}
1148+
11431149
const has_side_effects = false;
11441150
const rand_int = std.crypto.random.int(u64);
11451151
const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
@@ -1150,7 +1156,7 @@ pub fn rerunInFuzzMode(
11501156
.web_server = null, // only needed for time reports
11511157
.ttyconf = fuzz.ttyconf,
11521158
.unit_test_timeout_ns = null, // don't time out fuzz tests for now
1153-
.gpa = undefined, // not used by `runCommand`
1159+
.gpa = fuzz.gpa,
11541160
}, .{
11551161
.unit_test_index = unit_test_index,
11561162
.fuzz = fuzz,
@@ -1870,7 +1876,10 @@ fn pollZigTest(
18701876
// test. For instance, if the test runner leaves this much time between us requesting a test to
18711877
// start and it acknowledging the test starting, we terminate the child and raise an error. This
18721878
// *should* never happen, but could in theory be caused by some very unlucky IB in a test.
1873-
const response_timeout_ns = @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
1879+
const response_timeout_ns: ?u64 = ns: {
1880+
if (fuzz_context != null) break :ns null; // don't timeout fuzz tests
1881+
break :ns @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
1882+
};
18741883

18751884
const stdout = poller.reader(.stdout);
18761885
const stderr = poller.reader(.stderr);

lib/std/Build/abi.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ pub const fuzz = struct {
145145
pub extern fn fuzzer_init_test(test_one: TestOne, unit_test_name: Slice) void;
146146
pub extern fn fuzzer_new_input(bytes: Slice) void;
147147
pub extern fn fuzzer_main(limit_kind: LimitKind, amount: u64) void;
148+
pub extern fn fuzzer_unslide_address(addr: usize) usize;
148149

149150
pub const Slice = extern struct {
150151
ptr: [*]const u8,

lib/std/Io/Writer.zig

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,16 +270,17 @@ fn writeSplatHeaderLimitFinish(
270270
remaining -= copy_len;
271271
if (remaining == 0) break :v;
272272
}
273-
for (data[0 .. data.len - 1]) |buf| if (buf.len != 0) {
274-
const copy_len = @min(header.len, remaining);
275-
vecs[i] = buf;
273+
for (data[0 .. data.len - 1]) |buf| {
274+
if (buf.len == 0) continue;
275+
const copy_len = @min(buf.len, remaining);
276+
vecs[i] = buf[0..copy_len];
276277
i += 1;
277278
remaining -= copy_len;
278279
if (remaining == 0) break :v;
279280
if (vecs.len - i == 0) break :v;
280-
};
281+
}
281282
const pattern = data[data.len - 1];
282-
if (splat == 1) {
283+
if (splat == 1 or remaining < pattern.len) {
283284
vecs[i] = pattern[0..@min(remaining, pattern.len)];
284285
i += 1;
285286
break :v;
@@ -915,7 +916,16 @@ pub fn sendFileHeader(
915916
if (new_end <= w.buffer.len) {
916917
@memcpy(w.buffer[w.end..][0..header.len], header);
917918
w.end = new_end;
918-
return header.len + try w.vtable.sendFile(w, file_reader, limit);
919+
const file_bytes = w.vtable.sendFile(w, file_reader, limit) catch |err| switch (err) {
920+
error.ReadFailed, error.WriteFailed => |e| return e,
921+
error.EndOfStream, error.Unimplemented => |e| {
922+
// These errors are non-fatal, so if we wrote any header bytes, we will report that
923+
// and suppress this error. Only if there was no header may we return the error.
924+
if (header.len != 0) return header.len;
925+
return e;
926+
},
927+
};
928+
return header.len + file_bytes;
919929
}
920930
const buffered_contents = limit.slice(file_reader.interface.buffered());
921931
const n = try w.vtable.drain(w, &.{ header, buffered_contents }, 1);

lib/std/debug.zig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const root = @import("root");
2121
pub const Dwarf = @import("debug/Dwarf.zig");
2222
pub const Pdb = @import("debug/Pdb.zig");
2323
pub const ElfFile = @import("debug/ElfFile.zig");
24+
pub const MachOFile = @import("debug/MachOFile.zig");
2425
pub const Info = @import("debug/Info.zig");
2526
pub const Coverage = @import("debug/Coverage.zig");
2627
pub const cpu_context = @import("debug/cpu_context.zig");
@@ -1366,7 +1367,7 @@ test printLineFromFile {
13661367

13671368
/// The returned allocator should be thread-safe if the compilation is multi-threaded, because
13681369
/// multiple threads could capture and/or print stack traces simultaneously.
1369-
fn getDebugInfoAllocator() Allocator {
1370+
pub fn getDebugInfoAllocator() Allocator {
13701371
// Allow overriding the debug info allocator by exposing `root.debug.getDebugInfoAllocator`.
13711372
if (@hasDecl(root, "debug") and @hasDecl(root.debug, "getDebugInfoAllocator")) {
13721373
return root.debug.getDebugInfoAllocator();

0 commit comments

Comments
 (0)