Skip to content

Commit

Permalink
initial support for integrated fuzzing
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
andrewrk committed Jul 22, 2024
1 parent b149d8f commit 5058efa
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 53 deletions.
Empty file added lib/fuzzer.zig
Empty file.
12 changes: 6 additions & 6 deletions lib/std/mem.zig
Original file line number Diff line number Diff line change
Expand Up @@ -636,18 +636,20 @@ test lessThan {
try testing.expect(lessThan(u8, "", "a"));
}

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

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

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

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

if (a.len != b.len) return false;
if (a.len == 0 or a.ptr == b.ptr) return true;
Expand Down
3 changes: 3 additions & 0 deletions src/Builtin.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ optimize_mode: std.builtin.OptimizeMode,
error_tracing: bool,
valgrind: bool,
sanitize_thread: bool,
fuzz: bool,
pic: bool,
pie: bool,
strip: bool,
Expand Down Expand Up @@ -185,6 +186,7 @@ pub fn append(opts: @This(), buffer: *std.ArrayList(u8)) Allocator.Error!void {
\\pub const have_error_return_tracing = {};
\\pub const valgrind_support = {};
\\pub const sanitize_thread = {};
\\pub const fuzz = {};
\\pub const position_independent_code = {};
\\pub const position_independent_executable = {};
\\pub const strip_debug_info = {};
Expand All @@ -199,6 +201,7 @@ pub fn append(opts: @This(), buffer: *std.ArrayList(u8)) Allocator.Error!void {
opts.error_tracing,
opts.valgrind,
opts.sanitize_thread,
opts.fuzz,
opts.pic,
opts.pie,
opts.strip,
Expand Down
93 changes: 60 additions & 33 deletions src/Compilation.zig
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ debug_compile_errors: bool,
incremental: bool,
job_queued_compiler_rt_lib: bool = false,
job_queued_compiler_rt_obj: bool = false,
job_queued_fuzzer_lib: bool = false,
job_queued_update_builtin_zig: bool,
alloc_failure_occurred: bool = false,
formatted_panics: bool = false,
Expand Down Expand Up @@ -231,6 +232,10 @@ compiler_rt_lib: ?CRTFile = null,
/// Populated when we build the compiler_rt_obj object. A Job to build this is indicated
/// by setting `job_queued_compiler_rt_obj` and resolved before calling linker.flush().
compiler_rt_obj: ?CRTFile = null,
/// Populated when we build the libfuzzer static library. A Job to build this
/// is indicated by setting `job_queued_fuzzer_lib` and resolved before
/// calling linker.flush().
fuzzer_lib: ?CRTFile = null,

glibc_so_files: ?glibc.BuiltSharedObjects = null,
wasi_emulated_libs: []const wasi_libc.CRTFile,
Expand Down Expand Up @@ -799,6 +804,7 @@ pub const MiscTask = enum {
libcxx,
libcxxabi,
libtsan,
libfuzzer,
wasi_libc_crt_file,
compiler_rt,
zig_libc,
Expand Down Expand Up @@ -887,6 +893,7 @@ pub const cache_helpers = struct {
hh.add(mod.red_zone);
hh.add(mod.sanitize_c);
hh.add(mod.sanitize_thread);
hh.add(mod.fuzz);
hh.add(mod.unwind_tables);
hh.add(mod.structured_cfg);
hh.addListOfBytes(mod.cc_argv);
Expand Down Expand Up @@ -1302,6 +1309,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
const any_unwind_tables = options.config.any_unwind_tables or options.root_mod.unwind_tables;
const any_non_single_threaded = options.config.any_non_single_threaded or !options.root_mod.single_threaded;
const any_sanitize_thread = options.config.any_sanitize_thread or options.root_mod.sanitize_thread;
const any_fuzz = options.config.any_fuzz or options.root_mod.fuzz;

const link_eh_frame_hdr = options.link_eh_frame_hdr or any_unwind_tables;
const build_id = options.build_id orelse .none;
Expand Down Expand Up @@ -1563,6 +1571,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
comp.config.any_unwind_tables = any_unwind_tables;
comp.config.any_non_single_threaded = any_non_single_threaded;
comp.config.any_sanitize_thread = any_sanitize_thread;
comp.config.any_fuzz = any_fuzz;

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

if (comp.config.any_fuzz and capable_of_building_compiler_rt) {
if (is_exe_or_dyn_lib) {
log.debug("queuing a job to build libfuzzer", .{});
comp.job_queued_fuzzer_lib = true;
}
}

if (!comp.skip_linker_dependencies and is_exe_or_dyn_lib and
!comp.config.link_libc and capable_of_building_zig_libc)
{
Expand Down Expand Up @@ -1956,6 +1972,9 @@ pub fn destroy(comp: *Compilation) void {
if (comp.compiler_rt_obj) |*crt_file| {
crt_file.deinit(gpa);
}
if (comp.fuzzer_lib) |*crt_file| {
crt_file.deinit(gpa);
}
if (comp.libc_static_lib) |*crt_file| {
crt_file.deinit(gpa);
}
Expand Down Expand Up @@ -2721,6 +2740,7 @@ pub fn emitLlvmObject(
.is_small = comp.root_mod.optimize_mode == .ReleaseSmall,
.time_report = comp.time_report,
.sanitize_thread = comp.config.any_sanitize_thread,
.fuzz = comp.config.any_fuzz,
.lto = comp.config.lto,
});
}
Expand Down Expand Up @@ -3641,15 +3661,9 @@ fn performAllTheWorkInner(
break;
}

if (comp.job_queued_compiler_rt_lib) {
comp.job_queued_compiler_rt_lib = false;
buildCompilerRtOneShot(comp, .Lib, &comp.compiler_rt_lib, main_progress_node);
}

if (comp.job_queued_compiler_rt_obj) {
comp.job_queued_compiler_rt_obj = false;
buildCompilerRtOneShot(comp, .Obj, &comp.compiler_rt_obj, main_progress_node);
}
buildCompilerRtOneShot(comp, &comp.job_queued_compiler_rt_lib, "compiler_rt.zig", .compiler_rt, .Lib, &comp.compiler_rt_lib, main_progress_node);
buildCompilerRtOneShot(comp, &comp.job_queued_compiler_rt_obj, "compiler_rt.zig", .compiler_rt, .Obj, &comp.compiler_rt_obj, main_progress_node);
buildCompilerRtOneShot(comp, &comp.job_queued_fuzzer_lib, "fuzzer.zig", .libfuzzer, .Lib, &comp.fuzzer_lib, main_progress_node);
}

const JobError = Allocator.Error;
Expand Down Expand Up @@ -4655,23 +4669,27 @@ fn workerUpdateWin32Resource(

fn buildCompilerRtOneShot(
comp: *Compilation,
job_queued: *bool,
root_source_name: []const u8,
misc_task: MiscTask,
output_mode: std.builtin.OutputMode,
out: *?CRTFile,
prog_node: std.Progress.Node,
) void {
if (!job_queued.*) return;
job_queued.* = false;

comp.buildOutputFromZig(
"compiler_rt.zig",
root_source_name,
output_mode,
out,
.compiler_rt,
misc_task,
prog_node,
) catch |err| switch (err) {
error.SubCompilationFailed => return, // error reported already
else => comp.lockAndSetMiscFailure(
.compiler_rt,
"unable to build compiler_rt: {s}",
.{@errorName(err)},
),
else => comp.lockAndSetMiscFailure(misc_task, "unable to build {s}: {s}", .{
@tagName(misc_task), @errorName(err),
}),
};
}

Expand Down Expand Up @@ -5602,23 +5620,32 @@ pub fn addCCArgs(
try argv.append("-mthumb");
}

if (mod.sanitize_c and !mod.sanitize_thread) {
try argv.append("-fsanitize=undefined");
try argv.append("-fsanitize-trap=undefined");
// It is very common, and well-defined, for a pointer on one side of a C ABI
// to have a different but compatible element type. Examples include:
// `char*` vs `uint8_t*` on a system with 8-bit bytes
// `const char*` vs `char*`
// `char*` vs `unsigned char*`
// Without this flag, Clang would invoke UBSAN when such an extern
// function was called.
try argv.append("-fno-sanitize=function");
} else if (mod.sanitize_c and mod.sanitize_thread) {
try argv.append("-fsanitize=undefined,thread");
try argv.append("-fsanitize-trap=undefined");
try argv.append("-fno-sanitize=function");
} else if (!mod.sanitize_c and mod.sanitize_thread) {
try argv.append("-fsanitize=thread");
{
var san_arg: std.ArrayListUnmanaged(u8) = .{};
const prefix = "-fsanitize=";
if (mod.sanitize_c) {
if (san_arg.items.len == 0) try san_arg.appendSlice(arena, prefix);
try san_arg.appendSlice(arena, "undefined,");
try argv.append("-fsanitize-trap=undefined");
// It is very common, and well-defined, for a pointer on one side of a C ABI
// to have a different but compatible element type. Examples include:
// `char*` vs `uint8_t*` on a system with 8-bit bytes
// `const char*` vs `char*`
// `char*` vs `unsigned char*`
// Without this flag, Clang would invoke UBSAN when such an extern
// function was called.
try argv.append("-fno-sanitize=function");
}
if (mod.sanitize_thread) {
if (san_arg.items.len == 0) try san_arg.appendSlice(arena, prefix);
try san_arg.appendSlice(arena, "thread,");
}
if (mod.fuzz) {
if (san_arg.items.len == 0) try san_arg.appendSlice(arena, prefix);
try san_arg.appendSlice(arena, "fuzzer-no-link,");
}
// Chop off the trailing comma and append to argv.
if (san_arg.popOrNull()) |_| try argv.append(san_arg.items);
}

if (mod.red_zone) {
Expand Down
3 changes: 3 additions & 0 deletions src/Compilation/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ any_non_single_threaded: bool,
/// per-Module setting.
any_error_tracing: bool,
any_sanitize_thread: bool,
any_fuzz: bool,
pie: bool,
/// If this is true then linker code is responsible for making an LLVM IR
/// Module, outputting it to an object file, and then linking that together
Expand Down Expand Up @@ -82,6 +83,7 @@ pub const Options = struct {
ensure_libcpp_on_non_freestanding: bool = false,
any_non_single_threaded: bool = false,
any_sanitize_thread: bool = false,
any_fuzz: bool = false,
any_unwind_tables: bool = false,
any_dyn_libs: bool = false,
any_c_source_files: bool = false,
Expand Down Expand Up @@ -486,6 +488,7 @@ pub fn resolve(options: Options) ResolveError!Config {
.any_non_single_threaded = options.any_non_single_threaded,
.any_error_tracing = any_error_tracing,
.any_sanitize_thread = options.any_sanitize_thread,
.any_fuzz = options.any_fuzz,
.root_error_tracing = root_error_tracing,
.pie = pie,
.lto = lto,
Expand Down
13 changes: 13 additions & 0 deletions src/Package/Module.zig
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ stack_protector: u32,
red_zone: bool,
sanitize_c: bool,
sanitize_thread: bool,
fuzz: bool,
unwind_tables: bool,
cc_argv: []const []const u8,
/// (SPIR-V) whether to generate a structured control flow graph or not
Expand Down Expand Up @@ -92,6 +93,7 @@ pub const CreateOptions = struct {
unwind_tables: ?bool = null,
sanitize_c: ?bool = null,
sanitize_thread: ?bool = null,
fuzz: ?bool = null,
structured_cfg: ?bool = null,
};
};
Expand All @@ -106,6 +108,7 @@ pub const ResolvedTarget = struct {
/// At least one of `parent` and `resolved_target` must be non-null.
pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
if (options.inherited.sanitize_thread == true) assert(options.global.any_sanitize_thread);
if (options.inherited.fuzz == true) assert(options.global.any_fuzz);
if (options.inherited.single_threaded == false) assert(options.global.any_non_single_threaded);
if (options.inherited.unwind_tables == true) assert(options.global.any_unwind_tables);
if (options.inherited.error_tracing == true) assert(options.global.any_error_tracing);
Expand Down Expand Up @@ -210,6 +213,12 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
break :b false;
};

const fuzz = b: {
if (options.inherited.fuzz) |x| break :b x;
if (options.parent) |p| break :b p.fuzz;
break :b false;
};

const code_model = b: {
if (options.inherited.code_model) |x| break :b x;
if (options.parent) |p| break :b p.code_model;
Expand Down Expand Up @@ -337,6 +346,7 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
.red_zone = red_zone,
.sanitize_c = sanitize_c,
.sanitize_thread = sanitize_thread,
.fuzz = fuzz,
.unwind_tables = unwind_tables,
.cc_argv = options.cc_argv,
.structured_cfg = structured_cfg,
Expand All @@ -359,6 +369,7 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
.error_tracing = error_tracing,
.valgrind = valgrind,
.sanitize_thread = sanitize_thread,
.fuzz = fuzz,
.pic = pic,
.pie = options.global.pie,
.strip = strip,
Expand Down Expand Up @@ -427,6 +438,7 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
.red_zone = red_zone,
.sanitize_c = sanitize_c,
.sanitize_thread = sanitize_thread,
.fuzz = fuzz,
.unwind_tables = unwind_tables,
.cc_argv = &.{},
.structured_cfg = structured_cfg,
Expand Down Expand Up @@ -485,6 +497,7 @@ pub fn createLimited(gpa: Allocator, options: LimitedOptions) Allocator.Error!*P
.red_zone = undefined,
.sanitize_c = undefined,
.sanitize_thread = undefined,
.fuzz = undefined,
.unwind_tables = undefined,
.cc_argv = undefined,
.structured_cfg = undefined,
Expand Down
6 changes: 6 additions & 0 deletions src/codegen/llvm.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,7 @@ pub const Object = struct {
is_small: bool,
time_report: bool,
sanitize_thread: bool,
fuzz: bool,
lto: bool,
};

Expand Down Expand Up @@ -1287,6 +1288,7 @@ pub const Object = struct {
options.is_small,
options.time_report,
options.sanitize_thread,
options.fuzz,
options.lto,
null,
emit_bin_path,
Expand All @@ -1311,6 +1313,7 @@ pub const Object = struct {
options.is_small,
options.time_report,
options.sanitize_thread,
options.fuzz,
options.lto,
options.asm_path,
emit_bin_path,
Expand Down Expand Up @@ -2982,6 +2985,9 @@ pub const Object = struct {
if (owner_mod.sanitize_thread) {
try attributes.addFnAttr(.sanitize_thread, &o.builder);
}
if (owner_mod.fuzz) {
try attributes.addFnAttr(.optforfuzzing, &o.builder);
}
const target = owner_mod.resolved_target.result;
if (target.cpu.model.llvm_name) |s| {
try attributes.addFnAttr(.{ .string = .{
Expand Down
1 change: 1 addition & 0 deletions src/codegen/llvm/bindings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub const TargetMachine = opaque {
is_small: bool,
time_report: bool,
tsan: bool,
sancov: bool,
lto: bool,
asm_filename: ?[*:0]const u8,
bin_filename: ?[*:0]const u8,
Expand Down
Loading

0 comments on commit 5058efa

Please sign in to comment.