Skip to content

Adding fuzzing capabilities for C3 projects. #3505

Description

@alexveden

Hi, I'd like to propose adding fuzzing capabilities into C3 language. I made some preliminary research and POC that confirms that LLVM libFuzzer is viable with C3. I will post a WIP PR for this, see #3506.
But currently I'd like to discuss the bigger picture and benefits of native fuzzing can bring on the table in C3.

Rationale

I think fuzzing capabilities is a must for system languages, especially for those who allow unsafe operations with pointers, system libs, and other C code. C3 standard library can get a lot of edge case coverage especially when dealing with user input processing: regex, JSON/xml, and other file formats.

With that said, LLVM provides tools and capabilities out of the box, it looks like the fuzzing integration is relatively straightforward and requires small changes. I made a throwaway AI driven prototype that confirms that fuzzing is possible in C3, but I can definitely miss some knock-on effects of it. So I'd like to start discussion about fuzzing here.

Levels of fuzzing support

Stage 1: Bare minimum

Just implementing a compiler flag than enables fuzzers (same path as sanitizers): c3c compile --sanitize=fuzzer --no-entry fuzz_target.c3 -o fuzz

This flag can allows user to run hackish fuzzing of the c3 programs, like this:

module fuzz_target;

extern fn void abort() @cname("abort");

// Called by libFuzzer for every input. Returns 0 on success; a crash or
// abort() reports a finding.
fn int llvm_fuzzer_test_one_input(char* data, usz size) @export("LLVMFuzzerTestOneInput")
{
    // Protocol: [0..2] = "GET", [3] = '/', [4] = body length, [5..] = body bytes.
    if (size < 5) return 0;
    if (data[0] != 'G' || data[1] != 'E' || data[2] != 'T') return 0;
    if (data[3] != '/') return 0;
    usz len = (usz)data[4];
    if (size >= 5 + len) return 0; // body complete: input is fine.
    // Planted bug: truncated body accepted. In real code this would be an
    // out-of-bounds read; here it is simulated so the fuzzer terminates
    // with a reportable crash.
    abort();
}   
c3c compile --sanitize=fuzzer --cc=clang --no-entry fuzz_target.c3 -o fuzz

mkdir corpus
printf 'GET /abc\n' > corpus/seed1
printf 'hello\n' > corpus/seed2

./fuzz -max_total_time=30 ./corpus   # coverage-guided run, flags provided by libFuzzer
./fuzz <crash-file>                  # replay a finding, one shot, exact what triggered crash

Stage 2: Fuzz runner

Implementing machinery for running fuzz testing similar to unit tests and benchmarks. It could be simplified command that prepares fuzz binary and fuzz corpus.

CLI examples:

c3c fuzz test/fuzz/fuzz_foo.c3 '<libFuzzer args>' # runs individual fuzz app indefinitely
c3c fuzz # runs full fuzz suite, with time limits, say 1 min per test

Follow up notes and ideas

  • LibFuzzer ships its own main() function, so the main convention there that each fuzz case has to be a standalone executable. So the C3 program should be compiled with --no-entry.
  • Not all platform support fuzzing, but mainly Linux/Windows/MacOS do
  • LLVM libFuzzer uses branch tracking for testing all function code paths. It's not just randomly generated input, it's deliberately shaped input.
  • Fuzz data is not just a random stream of bytes, but it can be used as source of deterministic randomization. Which can open a whole new page of deterministic simulation testing in C3.
  • Unlike the AFL++ fuzzer, libFuzzer provides mode focused fuzzing, right into the weak spots of the project, instead end-to-end fuzzing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions