2023-10-29
The Zig Build
System is a cross platform build system used for the Zig programming
language. It can also be used as a C/C++ build system largely
independent of the Zig language (you still have to write the
build.zig
in Zig of course).
The following is a cookbook for some of the things I've found useful with the build system but which weren't immediately obvious for me:
If you search for how to test coverage in a Zig project, you'll
probably come across this issue: ziglang/zig#352.
They discuss adding it as a core feature to zig test
(i.e.:
--coverage
without any external binaries necessary). At the
bottom, someone mentions that you can run kcov
on the test binary outside of the build system. That was promising, but
I had a hard time discovering how I could automatically capture it
(i.e.: zig build test -Dcoverage=true
).
Enter std.zig.Build.Compile.setExecCmd
... And, that does
nothing: ziglang/zig#17756.
However, there's another method:
const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const unit_tests_step = b.step("test-unit", "Run unit tests");
unit_tests_step.dependOn(&run_unit_tests.step);
const coverage = b.addSystemCommand(&.{
"kcov",
b.fmt("--include-pattern={s}", .{b.pathFromRoot("src")}),
b.pathFromRoot("kcov-out"),
});
coverage.addFileArg(unit_tests.getEmittedBin());
const coverage_run = b.step("coverage", "Run unit tests while capturing coverage.");
coverage_run.dependOn(&coverage.step);
This can be run with zig build coverage
.
There's some prior work on this1, but it seems to not be
entirely functional right now because of LLVM version differences
between AFL++ and Zig. Here's how
I'm running fuzzing through zig build fuzz
:
const fuzz = b.addExecutable(.{
.name = "fuzz",
.root_source_file = .{ .path = "fuzz/main.zig" },
.target = target,
.optimize = optimize,
});
const afl_dir = b.cache_root.join(b.allocator, &.{"afl"}) catch @panic("OOM");
const afl_input = if (std.fs.accessAbsolute(afl_dir, .{})) |_|
"-"
else |_|
b.pathFromRoot("src/data/request");
const run_fuzz = b.addSystemCommand(&.{
"afl-fuzz", "-n",
"-i", afl_input,
"-o", afl_dir,
"--",
});
run_fuzz.addFileArg(fuzz.getEmittedBin());
const run_fuzz_step = b.step("fuzz", "Run AFL++ on the fuzz program.");
run_fuzz_step.dependOn(&run_fuzz.step);
fuzz/main.zig
takes input from
std.io.getStdIn()
and pipes it into the functionality I
want to test. In my case, that's an HTTP/1.1 connection loop, but it can
be anything. Just make certain that
b.pathFromRoot("src/data/request")
is renamed to point to a
directory containing your valid input data. AFL++ will
mutate that input data to find crashes with your program2.
Note that this uses black box fuzzing with no instrumentation. I have yet to get AFL++ to instrument a Zig executable properly. This supposedly makes AFL++ less effective, but considering that it's already found three crashes for me, I'm still happy with the current functionality.