Die größte Herausforderung ist direkt ersichtlich: libFuzzer ist ein C/C++-Tool. Es funktioniert, indem es wiederholt eine einzige Funktion, LLVMFuzzerTestOneInput(), aufruft und ihr mutierte Daten zur Verfügung stellt. Mein Ziel, JSON::XS, ist ein Perl-Modul.
Mein erster Gedanke war, einfach die zugrunde liegende C-Implementierung (die Datei XS.xs) direkt zu testen. Nach Durchsicht des C-Codes wurde jedoch klar, dass dies nicht funktionieren würde. Der Code ist stark mit der Perl-Umgebung verflochten. Er enthält Perl-Header (perl.h, EXTERN.h) und ruft viele Perl-Funktionen auf.
Das Entfernen dieser Abhängigkeiten, um eine eigenständige C-Funktion zu erstellen, wäre schwierig und fehleranfällig. Der Testfall würde sich von der ursprünglichen Implementierung unterscheiden, wodurch möglicherweise echte Fehler verdeckt oder neue eingeführt würden.
Daher entschied ich mich, das Modul innerhalb eines laufenden Perl-Interpreters zu testen. Das bedeutet, dass meine Implementierung nicht nur das Modul, sondern auch den gesamten Perl-Interpreter einbetten und im Grunde genommen in eine C-Funktion einbinden muss.
Dieser sogenannte Fuzz-Harness ist ein kleines C-Programm, das in diesem Fall drei Aufgaben erfüllt:
- Initialisiert einen eingebetteten Perl-Interpreter.
- Lädt ein einfaches Perl-Wrapper-Skript zur Fehlerbehandlung.
- Akzeptiert Rohdaten von libFuzzer und leitet sie an die zu testende Perl-Funktion weiter.
Bevor ich jedoch beginnen konnte, war ein zusätzlicher Schritt erforderlich.
Schritt 1: Der Perl-Wrapper zur Fehlerbehandlung
Warum benötige ich einen "zusätzlichen Schritt" mit einem Perl-Wrapper?
Der Fuzzer generiert Tausende von Eingaben, von denen die meisten ungültiges JSON sind. Die Funktion decode_json würde für diese Eingaben korrekt einen Fehler (eine Ausnahme) melden. Wenn ich diese Ausnahmen nicht behandle, unterbrechen sie den Testlauf und der Fuzzer kann nicht fortfahren. Mich interessieren nur Abstürze aufgrund von Speicherverletzungen, nicht einfache Parsing-Fehler.
Um dieses Problem zu lösen, habe ich ein kleines Perl-Skript (json_eval.pl) verwendet, das den Aufruf in einen eval-Block einbindet. Dieser fungiert als einfacher Ausnahmehandler und unterdrückt die erwarteten Fehler.
use JSON::XS;
sub do_json {
eval {
decode_json("@_");
} or do {
return;
};
}
Dieses Skript stellt eine neue, sichere Funktion namens do_json bereit, die ich aus dem C-Harness aufrufen kann.
Schritt 2: Das C-Harness zum Einbetten von Perl
Nun zum Hauptprogramm in C. Zunächst muss der eingebettete Perl-Interpreter initialisiert und die Bereinigung sichergestellt werden. Ich verwende die Hilfsfunktionen __attribute__((constructor)) und __attribute__((destructor)), damit dies automatisch und nur einmal beim Start ausgeführt wird.
Dieser Code (basierend auf der perlembed-Dokumentation) weist einen Interpreter zu, analysiert das Skript json_eval.pl und führt es aus. Die Funktion xs_init ist gemäß der Dokumentation definiert und hier nicht aufgeführt.
#include <EXTERN.h>
#include <perl.h>
EXTERN_C void xs_init (pTHX);
static PerlInterpreter *my_perl;
// Wird einmal beim Start des Programms aufgerufen.
__attribute__((constructor))
static void init_perl_fuzzer() {
char *my_argv[] = {"", "json_eval.pl"};
my_perl = perl_alloc();
perl_construct(my_perl);
perl_parse(my_perl, xs_init, 2, my_argv, (char **) NULL);
perl_run(my_perl);
}
// Wird einmal beim Beenden des Programms aufgerufen.
__attribute__((destructor))
static void destroy_perl_fuzzer() {
perl_destruct(my_perl);
perl_free(my_perl);
}
Als Nächstes benötigte ich eine C-Funktion, um die Perl-Funktion do_json aufzurufen. Dazu muss die Perl-C-API verwendet werden, um den Stack einzurichten, die JSON-Zeichenkette als Argument zu übergeben und den Aufruf auszuführen.
static void PerlJson(const char *const json, const STRLEN len) {
dSP;
ENTER;
SAVETMPS;
PUSHMARK(SP);
// Fuzzer-Daten auf den Perl-Stack schieben
XPUSHs(sv_2mortal(newSVpvn(json, len)));
PUTBACK;
// Die Funktion "do_json" aufrufen, Ergebnis verwerfen (G_EVAL)
call_pv("do_json", G_EVAL);
SPAGAIN;
PUTBACK;
FREETMPS;
LEAVE;
}
Schließlich habe ich die von libFuzzer benötigte Funktion LLVMFuzzerTestOneInput implementiert. Diese ist jetzt recht einfach: Sie schiebt die Daten aus dem Fuzzer direkt in meine PerlJson- Funktion.
int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
char *json = (char *) Data;
PerlJson(json, Size);
return 0;
}