Skip to content

Commit 5058efa

Browse files
committed
initial support for integrated fuzzing
* Add the `-ffuzz` and `-fno-fuzz` CLI arguments. * Detect fuzz testing flags from zig cc. * Set the correct clang flags when fuzz testing is requested. It can be combined with TSAN and UBSAN. * Compilation: build fuzzer library when needed which is currently an empty zig file. * Add optforfuzzing to every function in the llvm backend for modules that have requested fuzzing. * In ZigLLVMTargetMachineEmitToFile, add the optimization passes for sanitizer coverage. * std.mem.eql uses a naive implementation optimized for fuzzing when builtin.fuzz is true. Tracked by #20702
1 parent b149d8f commit 5058efa

File tree

11 files changed

+133
-53
lines changed

11 files changed

+133
-53
lines changed

lib/fuzzer.zig

Whitespace-only changes.

lib/std/mem.zig

+6-6
Original file line numberDiff line numberDiff line change
@@ -636,18 +636,20 @@ test lessThan {
636636
try testing.expect(lessThan(u8, "", "a"));
637637
}
638638

639-
const backend_can_use_eql_bytes = switch (builtin.zig_backend) {
639+
const eqlBytes_allowed = switch (builtin.zig_backend) {
640640
// The SPIR-V backend does not support the optimized path yet.
641641
.stage2_spirv64 => false,
642642
// The RISC-V does not support vectors.
643643
.stage2_riscv64 => false,
644-
else => true,
644+
// The naive memory comparison implementation is more useful for fuzzers to
645+
// find interesting inputs.
646+
else => !builtin.fuzz,
645647
};
646648

647649
/// Compares two slices and returns whether they are equal.
648650
pub fn eql(comptime T: type, a: []const T, b: []const T) bool {
649651
if (@sizeOf(T) == 0) return true;
650-
if (!@inComptime() and std.meta.hasUniqueRepresentation(T) and backend_can_use_eql_bytes) return eqlBytes(sliceAsBytes(a), sliceAsBytes(b));
652+
if (!@inComptime() and std.meta.hasUniqueRepresentation(T) and eqlBytes_allowed) return eqlBytes(sliceAsBytes(a), sliceAsBytes(b));
651653

652654
if (a.len != b.len) return false;
653655
if (a.len == 0 or a.ptr == b.ptr) return true;
@@ -660,9 +662,7 @@ pub fn eql(comptime T: type, a: []const T, b: []const T) bool {
660662

661663
/// std.mem.eql heavily optimized for slices of bytes.
662664
fn eqlBytes(a: []const u8, b: []const u8) bool {
663-
if (!backend_can_use_eql_bytes) {
664-
return eql(u8, a, b);
665-
}
665+
comptime assert(eqlBytes_allowed);
666666

667667
if (a.len != b.len) return false;
668668
if (a.len == 0 or a.ptr == b.ptr) return true;

src/Builtin.zig

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ optimize_mode: std.builtin.OptimizeMode,
1010
error_tracing: bool,
1111
valgrind: bool,
1212
sanitize_thread: bool,
13+
fuzz: bool,
1314
pic: bool,
1415
pie: bool,
1516
strip: bool,
@@ -185,6 +186,7 @@ pub fn append(opts: @This(), buffer: *std.ArrayList(u8)) Allocator.Error!void {
185186
\\pub const have_error_return_tracing = {};
186187
\\pub const valgrind_support = {};
187188
\\pub const sanitize_thread = {};
189+
\\pub const fuzz = {};
188190
\\pub const position_independent_code = {};
189191
\\pub const position_independent_executable = {};
190192
\\pub const strip_debug_info = {};
@@ -199,6 +201,7 @@ pub fn append(opts: @This(), buffer: *std.ArrayList(u8)) Allocator.Error!void {
199201
opts.error_tracing,
200202
opts.valgrind,
201203
opts.sanitize_thread,
204+
opts.fuzz,
202205
opts.pic,
203206
opts.pie,
204207
opts.strip,

src/Compilation.zig

+60-33
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ debug_compile_errors: bool,
190190
incremental: bool,
191191
job_queued_compiler_rt_lib: bool = false,
192192
job_queued_compiler_rt_obj: bool = false,
193+
job_queued_fuzzer_lib: bool = false,
193194
job_queued_update_builtin_zig: bool,
194195
alloc_failure_occurred: bool = false,
195196
formatted_panics: bool = false,
@@ -231,6 +232,10 @@ compiler_rt_lib: ?CRTFile = null,
231232
/// Populated when we build the compiler_rt_obj object. A Job to build this is indicated
232233
/// by setting `job_queued_compiler_rt_obj` and resolved before calling linker.flush().
233234
compiler_rt_obj: ?CRTFile = null,
235+
/// Populated when we build the libfuzzer static library. A Job to build this
236+
/// is indicated by setting `job_queued_fuzzer_lib` and resolved before
237+
/// calling linker.flush().
238+
fuzzer_lib: ?CRTFile = null,
234239

235240
glibc_so_files: ?glibc.BuiltSharedObjects = null,
236241
wasi_emulated_libs: []const wasi_libc.CRTFile,
@@ -799,6 +804,7 @@ pub const MiscTask = enum {
799804
libcxx,
800805
libcxxabi,
801806
libtsan,
807+
libfuzzer,
802808
wasi_libc_crt_file,
803809
compiler_rt,
804810
zig_libc,
@@ -887,6 +893,7 @@ pub const cache_helpers = struct {
887893
hh.add(mod.red_zone);
888894
hh.add(mod.sanitize_c);
889895
hh.add(mod.sanitize_thread);
896+
hh.add(mod.fuzz);
890897
hh.add(mod.unwind_tables);
891898
hh.add(mod.structured_cfg);
892899
hh.addListOfBytes(mod.cc_argv);
@@ -1302,6 +1309,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
13021309
const any_unwind_tables = options.config.any_unwind_tables or options.root_mod.unwind_tables;
13031310
const any_non_single_threaded = options.config.any_non_single_threaded or !options.root_mod.single_threaded;
13041311
const any_sanitize_thread = options.config.any_sanitize_thread or options.root_mod.sanitize_thread;
1312+
const any_fuzz = options.config.any_fuzz or options.root_mod.fuzz;
13051313

13061314
const link_eh_frame_hdr = options.link_eh_frame_hdr or any_unwind_tables;
13071315
const build_id = options.build_id orelse .none;
@@ -1563,6 +1571,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
15631571
comp.config.any_unwind_tables = any_unwind_tables;
15641572
comp.config.any_non_single_threaded = any_non_single_threaded;
15651573
comp.config.any_sanitize_thread = any_sanitize_thread;
1574+
comp.config.any_fuzz = any_fuzz;
15661575

15671576
const lf_open_opts: link.File.OpenOptions = .{
15681577
.linker_script = options.linker_script,
@@ -1908,6 +1917,13 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
19081917
}
19091918
}
19101919

1920+
if (comp.config.any_fuzz and capable_of_building_compiler_rt) {
1921+
if (is_exe_or_dyn_lib) {
1922+
log.debug("queuing a job to build libfuzzer", .{});
1923+
comp.job_queued_fuzzer_lib = true;
1924+
}
1925+
}
1926+
19111927
if (!comp.skip_linker_dependencies and is_exe_or_dyn_lib and
19121928
!comp.config.link_libc and capable_of_building_zig_libc)
19131929
{
@@ -1956,6 +1972,9 @@ pub fn destroy(comp: *Compilation) void {
19561972
if (comp.compiler_rt_obj) |*crt_file| {
19571973
crt_file.deinit(gpa);
19581974
}
1975+
if (comp.fuzzer_lib) |*crt_file| {
1976+
crt_file.deinit(gpa);
1977+
}
19591978
if (comp.libc_static_lib) |*crt_file| {
19601979
crt_file.deinit(gpa);
19611980
}
@@ -2721,6 +2740,7 @@ pub fn emitLlvmObject(
27212740
.is_small = comp.root_mod.optimize_mode == .ReleaseSmall,
27222741
.time_report = comp.time_report,
27232742
.sanitize_thread = comp.config.any_sanitize_thread,
2743+
.fuzz = comp.config.any_fuzz,
27242744
.lto = comp.config.lto,
27252745
});
27262746
}
@@ -3641,15 +3661,9 @@ fn performAllTheWorkInner(
36413661
break;
36423662
}
36433663

3644-
if (comp.job_queued_compiler_rt_lib) {
3645-
comp.job_queued_compiler_rt_lib = false;
3646-
buildCompilerRtOneShot(comp, .Lib, &comp.compiler_rt_lib, main_progress_node);
3647-
}
3648-
3649-
if (comp.job_queued_compiler_rt_obj) {
3650-
comp.job_queued_compiler_rt_obj = false;
3651-
buildCompilerRtOneShot(comp, .Obj, &comp.compiler_rt_obj, main_progress_node);
3652-
}
3664+
buildCompilerRtOneShot(comp, &comp.job_queued_compiler_rt_lib, "compiler_rt.zig", .compiler_rt, .Lib, &comp.compiler_rt_lib, main_progress_node);
3665+
buildCompilerRtOneShot(comp, &comp.job_queued_compiler_rt_obj, "compiler_rt.zig", .compiler_rt, .Obj, &comp.compiler_rt_obj, main_progress_node);
3666+
buildCompilerRtOneShot(comp, &comp.job_queued_fuzzer_lib, "fuzzer.zig", .libfuzzer, .Lib, &comp.fuzzer_lib, main_progress_node);
36533667
}
36543668

36553669
const JobError = Allocator.Error;
@@ -4655,23 +4669,27 @@ fn workerUpdateWin32Resource(
46554669

46564670
fn buildCompilerRtOneShot(
46574671
comp: *Compilation,
4672+
job_queued: *bool,
4673+
root_source_name: []const u8,
4674+
misc_task: MiscTask,
46584675
output_mode: std.builtin.OutputMode,
46594676
out: *?CRTFile,
46604677
prog_node: std.Progress.Node,
46614678
) void {
4679+
if (!job_queued.*) return;
4680+
job_queued.* = false;
4681+
46624682
comp.buildOutputFromZig(
4663-
"compiler_rt.zig",
4683+
root_source_name,
46644684
output_mode,
46654685
out,
4666-
.compiler_rt,
4686+
misc_task,
46674687
prog_node,
46684688
) catch |err| switch (err) {
46694689
error.SubCompilationFailed => return, // error reported already
4670-
else => comp.lockAndSetMiscFailure(
4671-
.compiler_rt,
4672-
"unable to build compiler_rt: {s}",
4673-
.{@errorName(err)},
4674-
),
4690+
else => comp.lockAndSetMiscFailure(misc_task, "unable to build {s}: {s}", .{
4691+
@tagName(misc_task), @errorName(err),
4692+
}),
46754693
};
46764694
}
46774695

@@ -5602,23 +5620,32 @@ pub fn addCCArgs(
56025620
try argv.append("-mthumb");
56035621
}
56045622

5605-
if (mod.sanitize_c and !mod.sanitize_thread) {
5606-
try argv.append("-fsanitize=undefined");
5607-
try argv.append("-fsanitize-trap=undefined");
5608-
// It is very common, and well-defined, for a pointer on one side of a C ABI
5609-
// to have a different but compatible element type. Examples include:
5610-
// `char*` vs `uint8_t*` on a system with 8-bit bytes
5611-
// `const char*` vs `char*`
5612-
// `char*` vs `unsigned char*`
5613-
// Without this flag, Clang would invoke UBSAN when such an extern
5614-
// function was called.
5615-
try argv.append("-fno-sanitize=function");
5616-
} else if (mod.sanitize_c and mod.sanitize_thread) {
5617-
try argv.append("-fsanitize=undefined,thread");
5618-
try argv.append("-fsanitize-trap=undefined");
5619-
try argv.append("-fno-sanitize=function");
5620-
} else if (!mod.sanitize_c and mod.sanitize_thread) {
5621-
try argv.append("-fsanitize=thread");
5623+
{
5624+
var san_arg: std.ArrayListUnmanaged(u8) = .{};
5625+
const prefix = "-fsanitize=";
5626+
if (mod.sanitize_c) {
5627+
if (san_arg.items.len == 0) try san_arg.appendSlice(arena, prefix);
5628+
try san_arg.appendSlice(arena, "undefined,");
5629+
try argv.append("-fsanitize-trap=undefined");
5630+
// It is very common, and well-defined, for a pointer on one side of a C ABI
5631+
// to have a different but compatible element type. Examples include:
5632+
// `char*` vs `uint8_t*` on a system with 8-bit bytes
5633+
// `const char*` vs `char*`
5634+
// `char*` vs `unsigned char*`
5635+
// Without this flag, Clang would invoke UBSAN when such an extern
5636+
// function was called.
5637+
try argv.append("-fno-sanitize=function");
5638+
}
5639+
if (mod.sanitize_thread) {
5640+
if (san_arg.items.len == 0) try san_arg.appendSlice(arena, prefix);
5641+
try san_arg.appendSlice(arena, "thread,");
5642+
}
5643+
if (mod.fuzz) {
5644+
if (san_arg.items.len == 0) try san_arg.appendSlice(arena, prefix);
5645+
try san_arg.appendSlice(arena, "fuzzer-no-link,");
5646+
}
5647+
// Chop off the trailing comma and append to argv.
5648+
if (san_arg.popOrNull()) |_| try argv.append(san_arg.items);
56225649
}
56235650

56245651
if (mod.red_zone) {

src/Compilation/Config.zig

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ any_non_single_threaded: bool,
3232
/// per-Module setting.
3333
any_error_tracing: bool,
3434
any_sanitize_thread: bool,
35+
any_fuzz: bool,
3536
pie: bool,
3637
/// If this is true then linker code is responsible for making an LLVM IR
3738
/// Module, outputting it to an object file, and then linking that together
@@ -82,6 +83,7 @@ pub const Options = struct {
8283
ensure_libcpp_on_non_freestanding: bool = false,
8384
any_non_single_threaded: bool = false,
8485
any_sanitize_thread: bool = false,
86+
any_fuzz: bool = false,
8587
any_unwind_tables: bool = false,
8688
any_dyn_libs: bool = false,
8789
any_c_source_files: bool = false,
@@ -486,6 +488,7 @@ pub fn resolve(options: Options) ResolveError!Config {
486488
.any_non_single_threaded = options.any_non_single_threaded,
487489
.any_error_tracing = any_error_tracing,
488490
.any_sanitize_thread = options.any_sanitize_thread,
491+
.any_fuzz = options.any_fuzz,
489492
.root_error_tracing = root_error_tracing,
490493
.pie = pie,
491494
.lto = lto,

src/Package/Module.zig

+13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ stack_protector: u32,
2626
red_zone: bool,
2727
sanitize_c: bool,
2828
sanitize_thread: bool,
29+
fuzz: bool,
2930
unwind_tables: bool,
3031
cc_argv: []const []const u8,
3132
/// (SPIR-V) whether to generate a structured control flow graph or not
@@ -92,6 +93,7 @@ pub const CreateOptions = struct {
9293
unwind_tables: ?bool = null,
9394
sanitize_c: ?bool = null,
9495
sanitize_thread: ?bool = null,
96+
fuzz: ?bool = null,
9597
structured_cfg: ?bool = null,
9698
};
9799
};
@@ -106,6 +108,7 @@ pub const ResolvedTarget = struct {
106108
/// At least one of `parent` and `resolved_target` must be non-null.
107109
pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
108110
if (options.inherited.sanitize_thread == true) assert(options.global.any_sanitize_thread);
111+
if (options.inherited.fuzz == true) assert(options.global.any_fuzz);
109112
if (options.inherited.single_threaded == false) assert(options.global.any_non_single_threaded);
110113
if (options.inherited.unwind_tables == true) assert(options.global.any_unwind_tables);
111114
if (options.inherited.error_tracing == true) assert(options.global.any_error_tracing);
@@ -210,6 +213,12 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
210213
break :b false;
211214
};
212215

216+
const fuzz = b: {
217+
if (options.inherited.fuzz) |x| break :b x;
218+
if (options.parent) |p| break :b p.fuzz;
219+
break :b false;
220+
};
221+
213222
const code_model = b: {
214223
if (options.inherited.code_model) |x| break :b x;
215224
if (options.parent) |p| break :b p.code_model;
@@ -337,6 +346,7 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
337346
.red_zone = red_zone,
338347
.sanitize_c = sanitize_c,
339348
.sanitize_thread = sanitize_thread,
349+
.fuzz = fuzz,
340350
.unwind_tables = unwind_tables,
341351
.cc_argv = options.cc_argv,
342352
.structured_cfg = structured_cfg,
@@ -359,6 +369,7 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
359369
.error_tracing = error_tracing,
360370
.valgrind = valgrind,
361371
.sanitize_thread = sanitize_thread,
372+
.fuzz = fuzz,
362373
.pic = pic,
363374
.pie = options.global.pie,
364375
.strip = strip,
@@ -427,6 +438,7 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
427438
.red_zone = red_zone,
428439
.sanitize_c = sanitize_c,
429440
.sanitize_thread = sanitize_thread,
441+
.fuzz = fuzz,
430442
.unwind_tables = unwind_tables,
431443
.cc_argv = &.{},
432444
.structured_cfg = structured_cfg,
@@ -485,6 +497,7 @@ pub fn createLimited(gpa: Allocator, options: LimitedOptions) Allocator.Error!*P
485497
.red_zone = undefined,
486498
.sanitize_c = undefined,
487499
.sanitize_thread = undefined,
500+
.fuzz = undefined,
488501
.unwind_tables = undefined,
489502
.cc_argv = undefined,
490503
.structured_cfg = undefined,

src/codegen/llvm.zig

+6
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,7 @@ pub const Object = struct {
11011101
is_small: bool,
11021102
time_report: bool,
11031103
sanitize_thread: bool,
1104+
fuzz: bool,
11041105
lto: bool,
11051106
};
11061107

@@ -1287,6 +1288,7 @@ pub const Object = struct {
12871288
options.is_small,
12881289
options.time_report,
12891290
options.sanitize_thread,
1291+
options.fuzz,
12901292
options.lto,
12911293
null,
12921294
emit_bin_path,
@@ -1311,6 +1313,7 @@ pub const Object = struct {
13111313
options.is_small,
13121314
options.time_report,
13131315
options.sanitize_thread,
1316+
options.fuzz,
13141317
options.lto,
13151318
options.asm_path,
13161319
emit_bin_path,
@@ -2982,6 +2985,9 @@ pub const Object = struct {
29822985
if (owner_mod.sanitize_thread) {
29832986
try attributes.addFnAttr(.sanitize_thread, &o.builder);
29842987
}
2988+
if (owner_mod.fuzz) {
2989+
try attributes.addFnAttr(.optforfuzzing, &o.builder);
2990+
}
29852991
const target = owner_mod.resolved_target.result;
29862992
if (target.cpu.model.llvm_name) |s| {
29872993
try attributes.addFnAttr(.{ .string = .{

src/codegen/llvm/bindings.zig

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ pub const TargetMachine = opaque {
9393
is_small: bool,
9494
time_report: bool,
9595
tsan: bool,
96+
sancov: bool,
9697
lto: bool,
9798
asm_filename: ?[*:0]const u8,
9899
bin_filename: ?[*:0]const u8,

0 commit comments

Comments
 (0)