Zig Build System

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:

Coverage

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.

Fuzzing

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.


  1. https://www.ryanliptak.com/blog/fuzzing-zig-code/↩︎

  2. Case Study #1↩︎