Basic fuzzing

What is fuzzing?

(Skip this background stuff)

It’s intentionally blasting random (even invalid and malformed) data at your codebase, usually through its public interface, in an attempt to crash the code or expose unexpected bahavior, memory leaks, security vulnerabilities, or other trouble that might not be easy to predict or exercise by other means.

Historically, it seems more common to fuzz-test applications, within test harnesses, mimicking input to the application through its command-line args, socket interfaces, or other user interface controls. In zig, it is easy to fuzz test closer to the unit-test domain, and this is the flavor of fuzzing that will be covered in this document. Using zig tools to construct more sophistocated or higher-level tests should be a natural next step from there.

(Note: “fuzzing” and “fuzz testing” are synonyms, not mysteriously different things.)

How to do it in zig?

The zig toolset includes fuzz test tools, including std.testing.fuzz and std.testing.Smith as well as zig build support for running the onslaught.

One approach is to write your app/lib code and (conventional) test code, as usual, then run and tune your test code to achieve good coverage, and then, finally, author your fuzz test code. It is not convenient (to say the least) to instrument your core code or your test scaffolding for fuzzing, so it’s best to plan for that and keep your fuzz scaffold design to a minimum; it’s nice if you can repurpose some of your general testing code for fuzzing. More details and demonstration of this later.

Who is “smith”?

It’s not the name of the guy who came up with it. Think of a “smithy” - a blacksmith’s workshop or forge. “smith” refers to the test data generator. Fuzzing is about sending randomized data, so the smith’s job is to serve meaningful random data, in the types your fuzz code requests, from a stream of random bytes according to smith functions you call to constrain and type that randomness. For instance,

smith.valueRangeAtMost(0, 100);

will return a random value between 0 and 100 (but it will do so in a way appropriate to fuzz testing - you do not want to use random.intRangeLessThan(), as you won’t get the intelligent coverage you want and your implementation will be slower).

Basics

If you wrote a reverse() function and then this test…

test "reverse" {
   var v1 = "hello".*;
   try std.testing.expectEqualStrings(reverse(&v1), "olleh");
   var v2 = "godbye".*;
   try std.testing.expectEqualStrings(reverse(&v2), "eybdog");
}

…you may not be persuaded to fuzz-test this, since:

  1. this is on the simple, deterministic side,
  2. it doesn’t involve allocations (that might be missing deallocations in corner-cases)
  3. it doesn’t involve parallel processing (that might be prone to deadlocks, starvation, access control problems, etc.), and
  4. it doesn’t have an obvious security-vulnerability profile (it may not even be exposed to user input according to your application design)
(Want to see the poor-man's implementation of reverse()...?)
pub fn reverse(str: []u8) []u8 {
   var c:u8 = 0;
   for (0..str.len/2) |i| {
      c = str[str.len - i - 1];
      str[str.len - i - 1] = str[i];
      str[i] = c;
   }
   return str;
}

Imagine that a little mistake here could have resulted in only even- or odd-length strings reversing correctly; conventional testing might not have exercised the error-prone case, but oracle-based fuzz testing, treated below, with lots of variably-lengthed strings, would expose the flaw.

But let’s try anyway; it’s overly simple for demonstration.

test "reverse2" {
   const t = struct {
      var data: [32]u8 = undefined;
      fn fuzz_reversal(_: void, smith: *std.testing.Smith) !void {
         const len = smith.slice(&data); // len will be between 0 and 32
         _ = reverse(data[0..len]);
      }
   };
   try std.testing.fuzz({}, t.fuzz_reversal, .{});
}

Then, we run the fuzzer:

zig build test -Doptimize=ReleaseSafe --summary line --fuzz=10K

That’s it! Notice that, unlike normal unit testing, comparing outputs to expectations is not required (e.g., with the expectEqualStrings() used above). Rather, crashing or causing memory leaks or thread deadlocks is a useful “output”. It’s pretty easy to determine by inspection that this contrived example appears safe and will never “crash”. If we changed the function signature of reverse() to return ![]u8, instead, and implemented it to return errors if the input buffer didn’t meet certain conditions (e.g., lowercase ASCII letters only), then we’d want to silently accept and discard those errors, rather than propagating them beyond our fuzz_reversal() function because they represent proper handling of that erroneous state. However, if we allocated memory within reverse() and propagated an OutOfMemory error, it would be appropriate to:

         _ = try reverse(gpa, data[0..len]);

… this would propagate the OutOfMemory error from the fuzz_reversal() method, called by the fuzzer, and result in halting notification in the terminal.

Or, perhaps you don’t detect any leaks using std.testing.allocator, using your unit tests, but you want to make sure there aren’t corner cases where leaking does occur. You could use std.testing.allocator while fuzzing, for this purpose, but note that the performance will take a real dive, of course.

zig build test ... --fuzz

The line of code…

   try std.testing.fuzz({}, t.fuzz_reversal, .{});

…is the standard call to start the testing. The first arg is for
FuzzInputOptions, not currently used, and the last arg is for a context struct you can define and provide for your fuzz function (first arg (_: void) in our fuzz_reversal).

It’s normal to run the fuzzer as:

zig build test -Doptimize=ReleaseSafe --summary line --fuzz

You want ReleaseSafe in order to test your optimized code, and in order to run the test efficiently and maximize your coverage and penetration. In the above test, we set --fuzz=10k, limiting the number of iterations of calls to our fuzz_reversal() function to 10,000. Otherwise, the fuzzer runs until the cows come home, which is often what you want. If you’re instrumenting code, to make sure you’ve got it scaffolded aright, you might use --fuzz=10 or some other low limit, so you don’t spill your terminal.

Note that, by default, values are accumulated across multiple runs. After several limited runs, output might look like:

======= FUZZING REPORT =======
Step: run test
Fuzz test: "main.test.reverse fuzz" (16729ee6e81554aa)
Runs: 1573702 -> 1574704
Unique runs: 4 -> 4
Coverage: 217/9487 -> 217/9487 (2.29%)
------------------------------

Even with the individual allocations in our test harness, a million runs like this take almost no time to run.

Weights

Assume you (think you) know that your reverse() function is much more likely to be called with strings between 8 and 40 characters long (but you still want to test lengths below and above). If you want to commit more resources to exercising that 8-40 range, you can assign weights. Here we’ll use the baselineWeights for the byte values themselves, but we’ll tip the scale for the lengths of the strings generated by the smith:

const Weight = std.Build.abi.fuzz.Weight;
const Smith = std.testing.Smith;
test "reverse3" {
   const t = struct {
      var data: [100]u8 = undefined; // tested lengths could be up to 100
      const len_weights: []const Weight = &.{
         .rangeAtMost(u8, 0, 100, 1),
         .rangeAtMost(u8, 8, 40, 10), // 8-40 about 10x more likely
      };
      fn fuzz_reversal(_: void, smith: *Smith) !void {
         const len = smith.sliceWeighted(&data, len_weights, Smith.baselineWeights(u8));
         _ = reverse(data[0..len]);
      }
   };
   try std.testing.fuzz({}, t.fuzz_reversal, .{});
}

Oracle-based fuzz testing

In many cases, you can actually test output against expectations. This is called oracle-based fuzzing. Let’s do so here by refining our unit test, then using it for a fuzz test:

const Smith = std.testing.Smith;
test "reverse4" {
   const t = struct {
      var data: [100]u8 = undefined;
      var ref: [100]u8 = undefined;
      fn reversal(str: [] u8) !void {
         @memcpy(ref[0..str.len], str);
         std.mem.reverse(u8, ref[0..str.len]); // reference reversal
         try std.testing.expectEqualStrings(reverse(str), ref[0..str.len]);
      }
      fn fuzz_reversal(_: void, smith: *Smith) !void {
         const len = smith.slice(&data);
         try reversal(data[0..len]);
      }
   };
   // one-off unit tests:
   var v1 = "hello".*;
   try t.reversal(&v1);
   // fuzz test:
   try std.testing.fuzz({}, t.fuzz_reversal, .{});
}

Note that separating the reversal() function from the fuzz_reversal() function allows expectation-based unit-testing code to be re-used by the fuzz test scaffolding. Then, you can instrument your unit test code as needed, but let the fuzzer hammer the code with random input afterward.

Boilerplate example

If you create a new directory and use zig init to produce a boilerplate, you’ll find an example fuzz test in src/main.zig. Be sure to use 0.16 or greater for the new smith-based fuzz support documented in this article.

More on Smith

std.testing.Smith documents Smith’s interface, but some highlights…

Eos

eos stands for “end of stream”. It’s used to terminate a “stream” of input data. The fuzzer runs mega-multiple iterations of calls on your fuzz scaffolding function, but, within the function, you can run a loop, simulating input data until this “eos”, like so:

    while (!smith.eos()) switch (smith.value(enum { add_data, dup_data })) {
        .add_data => {
            const slice = try list.addManyAsSlice(gpa, smith.value(u4));
            smith.bytes(slice);
        },
        .dup_data => {
            // ....

This is from the example code generated by zig init. The while (!smith.eos()) loop will iterate some random number of times before breaking out. Variations are also provided; eosWeightedSimple(), for instance, allows you to designate how likely you’d like a false (“not end- of-stream”) versus a true (“yes end-of-stream”). Normally, the construct is:

    while (!smith.eosWeightedSimple(9, 1)) {

for example, in which the loop is 9 times more likely to “continue” than to
break out. Notice the ! at the beginning, to follow the pattern, which reads
“while not end-of-stream”.

value()

In that boilerplate example, smith.value() is used to branch between emulating data acquisition (add_data) and running a duplication scheme (dup_data) and, within the add_data branch, it’s used again to fetch a random u4 value. Again, these are pre-generated random inputs, fetched by smith.value(); you don’t want to re-create the wheel by employing a random number generator yourself.

Bounded values

Of course, you may need values pulled from a specified range. For that, use valueRangeAtMost() and valueRangeLessThan() or the hash flavors of each of those. index() is another flavor, for random indices. Or…

Weights

valueWeighted() is another variant; this allows you to distribute the probability of getting one or another value from the range, based on the weights you assign. boolWeighted() functions like the weighted eos*() functions, introduced above, allowing you to specify the relative likelihood of a true value versus a false value.

Bytes

Perhaps one of the most common needs is for random bytes. bytes(), bytesWeighted(), and bytesWeightedWithHash() provide random bytes from the stream for this purpose.

Slices/buffers

Random slices can be generated with slice*() flavors; note that you have to provide the allocated (or stack) space for the data, of course.

Tests in various source files

As with other tests, fuzzing blocks can be in files other than main.zig or root.zig, but you either have to use addTest() in build.zig or reference the tests from within a main or root test:

const foo = @import("foo");
test {
    _ = foo; // explicitly reference foo so its tests will run
}

Alternately, you can use refAllDecls(); note that the import assignments must be pub in this case:

pub const foo = @import("foo");
pub const bar = @import("bar");
test {
    std.testing.refAllDecls(@This()); // implicitly reference all the pub decls
}

Running / terminal output

Note that zig build test just runs zig test under the hood, normally, but for fuzzing, zig build test --fuzz must be used, and, as shown above, the -Doptimize=ReleaseSafe arg is almost certainly wanted for fuzzing, so that you’re fuzzing your release build. When using zig test, you’ll get summary output by default, but you need the --summary line argument (or --summary all or --summary new) to see output when using zig build test.

Note that all fuzz scaffolds are run through once during a normal call to zig test or zig build test. However, fuzz input seems to prime with lots of zero-values at the start of the stream, resulting in, e.g., a while (!smith.eos()) test failing right away, and never exercising your code. You could fudge, but the best bet is to do as illustrated above, and make sure you can run as much of your test scaffolding code as possible in a normal test, so that your confidence in the fuzz run is high.

Web interface

You can also watch output at the web interface (e.g., at http://[::1]:46785/ if you pass --webui=[::1]:46785 to your zig build test)

Crash- vs. Expectation-testing (oracle-based fuzzing)

Again, Fuzzing to crash (without any std.testing.expect*() calls) is perfectly legitimate - don’t shy from fuzzing for lack of clear expectation comparisons. My first use-case was fuzz-testing a custom FFT algorithm - FFT is reversible, though (especially in my case) the reversal is lossy; nevertheless, boundaried comparisons could be constructed. I took an entropy approach, starting with simple periodic data where frequencies were fuzz-random-chosen, layering more fuzz-random-chosen frequencies, then perturbing from pure frequencies with fuzz. But I also included a pure-fuzz, pure-chaos test - sending fuzz into the FFT merely to exercise it to crash, if possible, or to leak memory. In that case, there was no expectation-comparison to be done, for, without periodicity, an FFT result can’t be reversed with any predictability.

Another pattern is to test validity at the end of a fuzz iteration. You may not be able to test accuracy (with an expectation comparison), but asserting the integrity of your state could expose problem code.

To Add

  • Crash dumps and re-running failures
  • Multi-threaded fuzzing
  • Fuzzing infinite mode with multiple tests
  • FuzzInputOptions.corpus

References

32 Likes

Well, there’s a start. Plenty probably wants whittling - please feel free to suggest/direct me, but hopefully it can become a helpful resource.

This is great. More goodies from 0.16. Thanks for the docs.

I got couple questions.

  1. When running fuzz via zig build test -Doptimize=ReleaseSafe --fuzz, are the tests not using smith also run by that many times as the tests using smith? Is there a way to separate the two groups of tests?

  2. Can you add more explanation on the weighted calls? I’m confused about some of the expected behaviors the calls.

E.g. test valueRangeAtMost in Zig Documentation. Line 815,

   try std.testing.expectEqual(0, smith.valueRangeAtMost(u8, 0, 100));

The 3rd value in the smith stream is 200. Shouldn’t the expected value be 100 since the AtMost range is (0, 100)? And 200 is greater than the max 100.

More documentation on the API calls would be great. Thanks!

Others may have more complete answers, but my try:

  1. Your function that you provide to std.testing.fuzz(...); is the function that will be called repetitively. Anything in that function body will be run repetitively. Your normal test {} blocks will not be run during fuzzing.
  2. I intend to augment the doc with more on weighting. You may be confused about specific details; perhaps enumerate them? For the valueRangeAtMost() call in your example, there’s no specific weighting distribution; you’d use valueWeighted() to provide an array of distribution weights. However, I’m surprised that you got a 200 out of that - that could be a bug. I haven’t seen that, but it’s a little difficult to instrument fuzz code; perhaps you figured out the trick, and have clear verification of the produced value, and can prove it’s a bug. If there’s any other detail that would help me, I could try myself after my 12-hour flight and 5-hour drive. Just heading home from the other side of the earth. :slight_smile:

Re: “more documentation on the API calls” - I agree that a little more could be helpful in the actual code/std-docs, but I don’t think I want to over-document that level of detail in this doc. We’re all being patient with the pre-1.0 flux in the meantime. Almost all of the functions in Smith are quite short and pretty easy to understand by analyzing the code. baselineWeights() is the obvious exception, and understanding how the hashing works takes a little brain power, but it’s normally fine to trust the automatic hash management and just call the functions that do not include hash in their names.

  1. That’s great. Good to know the fuzz tests and normal tests are separated out.

  2. Sorry. I jumped too far. I used valueRangeAtMost() as an example because it calls valueRangeAtMostWithHash which calls `valueWeightedWithHash(.., &.{.rangeAtMost(T, at_least, at_most, 1)}, ..). The weighted version seems to be central in the calls, and it would be nice to explain how the weighted parameter works.

Oh, I didn’t run the tests. I was just looking at the tests trying to figure out what the expected behaviors of the API. The testing call at line 815 does return 0. It just seems that returning 0 is wrong (i.e. the test is wrong as well). The smith’s stream value is 200 at line 807. Comparing 200 against the range smith.valueRangeAtMost(u8, 0, 100), it exceeds the upper limit 100 so the return value should be limited to 100, rather than returning 0, which is the lower limit.

Regarding documentation, just having the parameters and expected behaviors/outcome spelt out would be great. Also not every single function needs documentation. Just document one function out of a group of similar functions would be enough.

Thanks for all the work. Fuzzing is extremely helpful.

I see. Yes, I’ve found the std test code to be an excellent way to get an understanding of the code, as well. In this case, ymmv. Here, constructInput() is used to build contrived input streams, and I think this test you point out is demonstrating that, in fact, since the input stream had a 200 in it next, but the value requested was 0-100, that 0 had to be returned. I don’t know quite enough about the internals of fuzzing science itself, but normally I think a stream of random bits (rather than a bunch of separate rng calls) is used to provide the data, and when data can’t “fit” (given bitwidth wanted and some bits available), fallbacks like 0 are needed. Something like that. If the range 5-100 was specified, then perhaps(?) 5 would be supplied? A deeper dig would be required. But I’m pretty sure what this is testing is the ability of the function to return NOT 200, even though that’s what’s in the stream. That is, I think it’s doing what it’s supposed to be doing here.

I’ll try to augment the documentation with everything under my To Add section in due course. I’ll consider beefing up the API function detail a little.

Nice post! I definitely think fuzzing could use some more documentation and hope to write some up soon. I just want to clear up some things I see,

Instrumentation is just code that the compiler inserts when the test is built in fuzzing mode which reports to the fuzzer different behavior the code is doing, namely the blocks it hits. @disableInstrumentation only applies to the current block, and does not recurse through function calls or affect logging or printing in any way, there are just some fixes that need made to the build runner regarding handling stderr from fuzz tests. (I created #35882 to document the builtin.)

The values from Smith are usually not pre-generated (i.e. using Smith.in). Instead, they are dynamically generated by making calls into the fuzzer for every value. This is also where weights become integral to the implementation as under the hood every requested value is a u64 (or filling in a []u8) which is constrained to the actual type’s range by the weights.

The example fuzz test could also be improved by using Smith.slice, though if you intended to demonstrate other parts of the interface that is also fine. Also, expectation-testing is better known as oracle-based fuzz testing. I also recommend pointing towards using master for fuzzing for the time being due to the important usability fixes in #32033.

The main purpose of the Smith.in mode is for rerunning previous inputs generated by the fuzzer. Hence, It is unspecified (except for eos which must eventually return true) which value the Smith should return in the case of an invalid value or end-of-stream, just that it is a valid value. The implementation simply opts to choose the first value of the first weight range if it reads an invalid value.

3 Likes

Thank you @gooncreeper! I eagerly invite all input and correction.

“some fixes that need made” - meaning that, due to bug(s), traces (in the form of std.testing.print or std.log.*) that are not working right now will work in the future? Does that mean that a trace in my test code will spit millionfold when a fuzz is properly underway?

Ok, am I to understand, then, that a random value is actually generated every call (rather than just pulled from a random bistream already generated), BUT that, even if you ask for a u2, a u64 will be generated, and the other 62 bits will ultimately just be thrown away? Is the only advantage to smith-generated values, then, the use made of the hash values, for future calls? (otherwise, that is, one would have similar performance if they just used a rng themselves, rather than smith calls?) Well, I guess the other important bit is the record, so that you can reproduce a crash, for instance, using Smith.in. (I’ll need to add treatment of that, too.)

You’re talking about my example, above, not the example generated by zig init, I presume?

Thank you with the terminology help and other feedback; I’ll make some adjustments accordingly.

Yes, though even as it is, each write to stderr comes with a significant performance penalty due to the syscall and leads to unbounded memory usage in the build runner as it collects all the test’s stderr.

Another note on performance is to aim to avoid using std.testing.allocator during fuzz testing since heap allocation is relatively slow, instead using a std.heap.FixedBufferAllocator on a stack buffer or, if need be, a heap buffer provided in the context pre-allocated by the test.

Instead of generating completely random inputs each time, fuzz testing makes small changes (called mutations) to previous inputs. If the new input is interesting, for example it hits a new block, it saves it and it becomes a candidate to be mutated for future runs.

Hashes are used to identify the sources of values so that a new request does not reuse a value meant for a different type of request. The regular methods already use a hash value based off the return address, so the only real purpose of calling the hash variants directly is in the case of inlining.

This is not the case due to the use of weights. A set of weights ([]const Weight) which is empty has no values which are allowed to be returned. Each Weight added allows for a range of values to be returned with a given probability. In the example of a u2, the Smith would ask the fuzzer to generate a u64 with the only weight being [0, 3], which allows only the values in the range of a u2 to be returned by the fuzzer.

That is correct.

1 Like

This is great, thank you. I confess to only having know about as much as I needed to successfully blast my use-case; it was effective and encouraging, but I’m still a learner, too. Sometimes that position makes a better documenter, so I lend my try…

Right, makes sense, and, for both of these reasons, that’s why I thought that disableInstrumentation (my misconception of it) was a good thing. I’ll alter this doc to discourage tracing in fuzz; onus is on the fuzzer user. Finally, I’ll mention the value of preferring FixedBufferAllocator / stack buffering or pre-allocated heap over typical thoughtless use of std.testing.allocator in the hotpath.

I’m finally researching generative and mutation fuzzers, evolution and grammar-based fuzzers, white- and gray- and black-box, un/structured input, oracles (and poets and couriers?!), etc.; I don’t want this basic doc to become an article covering everything and the kitchen sink, but I do need to morph it a little in light of some pretty important bits. Thanks for the helpful direction.

Completed significant re-write of many sections based on feedback. Still more to do, to be sure. I’ll probably take one more pass at it after a couple of days and feedback. That might button it up for the foreseeable future. There’s so much that could be added, but then it turns into an article and gets too far beyond the basics. I guess.

1 Like

Important warning you MUST run fuzzer with -Doptimize=ReleaseSafe, it crashes in debug mode

That’s true right now (and has been for a little while), but in this doc I only mentioned ReleaseSafe as important for the “other” reason - your optimized compilation is probably what you want to be fuzzing, normally, anyway. I didn’t mention the crash here because I expect that to go away soon. Thank you for mentioning it as a comment, though, as it’s certainly true for now.

I’m finding that a number of the concepts around fuzzing are very similar to what I’ve used professionally in hardware verification as “constrained random” testing. As such I find myself needing to break through the language barrier a bit.

  • Is it fair to say that a Smith is just a source of random values?
  • Would you expect to write Smiths for complex data types, and they would build on std.testing.Smith? Would I write a PacketSmith if I wanted to generate a stream of randomised network packets?
  • I don’t see how this is true from the current implementation. It looks like values are int generated directly from the hashes right now. Is this description how it might work in the future? Doesn’t the Smith need to understand higher level constructs to do mutations?
  • Are fuzz tests reproducible? Currently the Smith seems to use the return address to seed the hash. Won’t that change when you recompile after a fix, making your tests different next time? You wouldn’t actually know if you fixed the problem or not.

That’s not quite how I’d phrase it. I’d say a smith is a tool that fashions random bytes into the types you want.

You could, I’m sure, but I think it’s more likely you’d just use std.testing.Smith to generate your data. I think, especially for something like network packets, it would only be a line or two of code. You want to rely on the fuzz engine to, e.g., evolve the randomness to exercise more “interesting” prospects. You can use weights to lean possible random values to your wishes. But the most primal random data maintained by the fuzzer is, in fact, a random byte stream. You probably want your “randomized network packets” to even include malformed data, to ensure that your code handles such gracefully.

There are a few things in that @gooncreeper comment that you might be wondering about here. Take “if the new input is interesting, for example it hits a new block, it saves it and it becomes a candidate to be mutated for future runs.” - this I can say is working correctly, I’m pretty sure. That is, if fuzzing runs for 10 seconds or 10 minutes, then fails, you get some info spit out. Then, if you run again (another zig build ... --fuzz), the internal log is used by the fuzzer to try input “like that” again, to see if it can cause the failure again (perhaps much sooner). If, between runs, you went and “fixed” your code, to handle the failure gracefully, then successful seconds or minutes of running, thereafter, should give you some confidence that your fix succeeded, because the fuzzer should have essentially re-tried the failing scenario.

If you’re referring to how the hashing works (or fails to work as you’d expect), you might need to post some code detail or something to help demonstrate the problem. But I don’t fully understand how (intelligent?) mutation works, nor have I used hash variants of the smith functions directly (I haven’t needed to employ inlining at all yet).

This is probably a detail for @gooncreeper or another to weigh in on, but I think of it like this: your test code ran a sequence of Smith calls which gave you random data in the types you wanted, to feed to your codebase. If the stream of bytes which fed that Smith, and generated your data, is known, it can be used to re-create the same chain of events that led to the failure. I imagine a mutation could look like: “use the same stream of bytes, but hash differently at this critical point, or that critical point (only)”… but I haven’t looked carefully at the code that might do this.

Yes, that’s my full expectation, but I haven’t yet tested this enough to have included it in this doc. It’s a high priority, imo, so… when I get back to this, I’ll have more. In the meantime, another may have more useful input for you. To answer your final question there – I’d think that if your code change was isolated enough that the input data remained of the same type/byte-size, then an exact (re-)input stream should still result in success only if your fix was really a fix. For me, a failure during fuzzing just aims my eyes to the trouble, and I usually follow up with manual analysis of that bit of code, debugging, unit-testing to try to explicitly replicate it, and fixing it that way… as if I’d accidentally stumbled upon the code and had a stack trace to know where to look. But it might have taken me years to accidentally stumble, where fuzz testing compacted that into seconds.

Thanks for the lengthy response. Appreciate it.

I’m struggling here. Going back to the packet example… A packet will have a header, length, payload and checksum. Weights aren’t enough because the fields have inter-dependencies. If it’s trivially driven by random bytes for all fields, even with weights, the likelihood of getting a valid packet is vanishingly small. That’s very bad for coverage as you’ll only test your “invalid packet” path.

Hence my question about a PacketSmith. Something that shapes that randomness into a probably-valid packet (certainly not always valid). I’m coming from things like QuickCheck (Haskell) where you’d write a generator function for a type that would take an RNG as input, and return a randomised instance[1]. So I struggle to see how you wouldn’t have something to do that job if you were wanting to fuzz code that took any kind of non-trivial structure as input.

Maybe this should be a different thread though. It’s less about the guide and more about particular applications of the library.


Re: Reproducibility – I guess what I’m missing, and this is a bit more relevant for the guide, is what the debug loop is going to look like. If something needs an hour of fuzzing (or even 10 mins) to find a failure case, then I’ll want a quick way to reproduce that failure in a debugger, or maybe built with debug prints enabled. Great if the stack trace tells you all you need, but if it doesn’t a quick reproduction is vital.

Maybe having an example that finds a rare failure, and showing how to debug/fix it would be good.


  1. Hmmmm… Maybe I should try to write a Zig version. I wonder if that’s possible. You could probably comptime a lot of it, and test shrinking is really nice. ↩︎

1 Like

Keep in mind I’m a relative noob. I do not have significant experience with other fuzzers, and my research suggested an amazing variety of approaches (and divergent terminology, etc.) in that space, since that thunderstorm experience back in the 60s or whatever. Indeed, my “real-world” cases (only a couple) have been highly “shaped”, using weights and wielding the Smith to do my bidding to give me in-range values (in addition to out-of-range values). But, in my case at least, I didn’t need to create a Smith, I just had to “use” the std Smith…

Yes, I would think at least the checksum – you’d probably want to actually provide calculated checksums, or you’re only going to see an extremely tiny fraction of packets get beyond the front door. Length, likewise. I’d guess most of the “random” would go into header (perhaps still shaped) and payload (perhaps the most resilient). Nevertheless, fuzzing completely random data into the hole for a little while might be useful in turning up a case or two handling invalid data, and I think that’s in the spirit of fuzzing, too.

I think it’s correct to say that, in its current state, the zig fuzzer is good for black-box fuzzing and forms of data-generation testing. I did not cover, in this basic doc, whether there are plans for true gray-box testing with mutations that are triggered by actual code-path analysis, because I really don’t know! Maybe @gooncreeper will chime in. There is, of course, nothing of the automated white-box sophistication that big-box tools provide.

Agreed! I need to dig into that and augment this document with a section on “what to do with the failure output (to reproduce the failure, etc.)”. I’ve only scratched the surface there myself.

1 Like

Thanks so much!! Excited to try this out

In the sense that you are generating unknown values, yes; however, not quite in the sense of how they are generated.

If you are asking if you should break the generation of complex types into smaller reusable functions, that is generally a good idea; and yes, they would use Smith as it is the the root source of getting values from the fuzzer. Take as an example the compiler’s own AstSmith which generates Zig ASTs (abstract syntax trees, essentially Zig code). It is currently used to fuzz test AST-parsing and zig fmt.

Not quite, Smith is not doing any value generation. Most of the time, it calls into the fuzzer (example) to generate values, though it is also able to read a pregenerated input (Smith.in) for rerunning inputs. Hashes are only used as a way to give the fuzzer context on the source of different values so it can more effectively mutate them. They don’t get included in the inputs, even which the fuzzer stores on disk for if it gets restarted, in any way. The implementation of the fuzzer logic and mutations can be found here.

Doesn’t the Smith need to understand higher level constructs to do mutations?

By default, the fuzzer already has several details on the construction of the input, from which value requests are end-of-streams, bytes, etc., from hashes, and from which blocks the input hits. However, the fuzzer expects that you give some effort in guiding it to construct interesting inputs, for example by using weights. The more you guide the fuzzer, the more efficient your fuzzing campaign (test) will be. For example,

an efficient input generator for such a case would request a bool which decides if the checksum will be populated with the correct value for the fuzzer. It would weigh at runtime the values of other fields based off what makes sense for the already generated dependencies.

No matter the fuzzer implementation, a test where more inputs lead to interesting behavior is going to be more efficient than the contrary, which is why Zig fuzzing leans into the smith-based approach.

When a fuzz test crashes the input gets saved to .zig-cache/f/crash (this path is also printed when the test crashes). You can copy the file into the module with you fuzz test and use the .corpus option with the input included using @embedFile (more detailed post). The corpus is automatically run with the test even in non-fuzzing mode.

7 Likes

Could anyone help to dig corpus?
what is the combination of it with smith?

I have a case when I need to generate specific sets of data for encoding so I could either pass [“42”, “53”] => .int or [“42”, “text”] => .string.

or corpus is used only during a regular test?