Tech-Talk: Performantes Sandboxing durch Bytecode-Manipulation
Um Studierenden in Programmiermodulen, wie OOP1 & 2, schnelles Feedback geben zu können, verwendet Michael Faes einen selbst entwickelten Grading-Server. Der Grading-Prozess wurde bereits in einem früheren Tech-Talk gezeigt.
Um sicherzustellen das keine falsche Lösungen oder böswillige Abgaben zu Problemen führen, wurde der Java Security Manager verwendet. Dieser ist aber bereits seit vier Jahren deprecated und wurde nun in Java 24 endgültig entfernt. In diesem Tech-Talk wird ein neuer Ansatz für das Sandboxing von Studenten-Lösungen vorgestellt, welcher Bytecode-Manipulation verwendet.
Der bisherige Ansatz, wie AutoFeedback Studierendenabgaben evaluierte, basiert auf Containern. Pro Abgabe wurde ein Container gestartet. Da dieser Ansatz CI / CD Pipelines (z.B. bei GitLab) ähnelt, wurde der Core von AutoFeedback leicht modifiziert, um in solchen Pipelines laufen zu können.
Sandboxing ohne Java Security Manager
Der Security Manager sorgte dafür, dass alle Klassen / Funktionen ihn erst fragen mussten, ob der Aufruf gestattet ist. Ein Sandboxing ohne Security Manager muss folgende Ziele erreichen.
- Sicherheit
- Tests sollen nicht bestanden werden ohne (zuverlässiges) Liefern der erwarteten Ausgabe.
- Studenten sollen keine Möglichkeit haben, Informationen über die Testfälle zu erhalten.
- Das Test-System soll nicht störbar / komprimittierbar sein. Das enthält Löschen von Files, unbegrenzter Ressourcenverbrauch, etc.
- Breites Einsatzgebiet:
Studierende sollen übliche Klassen von Java Libraries verwenden können (java.util, java.io, etc.) - Performance
Für das Auswerten der Studenten-Test-Suites werden pro Abgabe dutzende von Testläufen mit jeweils hunderten von Programmen ausgeführt. Resultate sollen innert wenigen Minuten vorliegen.
Der Ansatz ist es das Sandboxing mit Bytecode-Manipulation umzusetzen.
Potenziell böswillige Abgaben werden erst zu Bytecode kompiliert. Der Bytecode wird dann bereinigt, auflösen von Endlosschleifen, ersetzen von nicht-erlaubten Funktionsaufrufen und in einer JVM ausgeführt.

Der Aufbau des Grading-Servers ist unten visualisiert.
Test-Runner teilen sich eine JVM, welche nur bei Crashes neugestartet wird. Die Tests werden jeweils in einem neuen Thread ausgeführt.

Bytecode Manipulation
Illegale Funktionen
Um die Nutzung von problematischen Funktionen zu verhindern (zum Beispiel File Zugriffe) wird eine Whitelist verwendet. In dieser werden erlaubte Konstruktoren und Methoden overload-genau angegeben. Man kann auch alles einer Klasse erlauben (java.lang.Integer.*
).
Wenn im Bytecode nun ein Methoden-/Konstruktorenaufruf erkannt wird, wird geprüft, ob dieser Aufruf in der Whitelist ist. Wenn nicht, wird der Aufruf mit einem Stück Bytecode ersetzt, das eine SecurityException wirft.

Endlosschleifen
Um Endlosschleifen zu verhindern oder generell Programme, die länger laufen als erlaubt, wird ein Timeout verwendet; wenn der Test zu lange läuft, wird der entsprechende Thread gestoppt. Das ist aber einfacher gesagt als getan. In Java kann man Threads nicht stoppen, wenn sie das nicht wollen. Die Methode `Thread.stop()`, welche für über 20 Jahre deprecated war, wurde in Java 20 entfernt.
Die Threads müssen jetzt überprüfen, ob sie unterbrochen wurden, und sich dann selbst beenden.
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
Nun kann man einen solchen Check am Ende jedes Schleifenblocks mit Bytecode-Manipulation einfügen. Da gibt es aber wieder ein Problem, im Bytecode gibt es keine Schleifen, nur Jump-Instructions.
Um das Problem zu lösen wird ein Control Flow Graph aus den Basic Blocks des Bytecodes gebaut. In diesem Graph wird dann nach Zyklen respektive Back Edges mittels topologischer Sortierung gesucht. Bei jedem Basic Block, der mit einer Back Edge aufhört, wird am Ende der Interrupted-Check als Bytecode eingefügt.
Tests können jetzt beliebig abgebrochen werden.

Endlosrekursion
Im Gegensatz zu Endlosschleifen braucht es zum Verhindern von Endlosrekursion keinen speziellen Mechanismus; der Call-Stack in Java ist begrenzt, d. h. Programme mit Endlosrekursion brechen recht schnell mit einem `StackOverflowError` ab. Allerdings wurde in der Diskussion klar, dass nicht nur Endlosrekursion, sondern auch endliche, aber sehr lange laufende Rekursion ein Problem sein kann (Beispiel naiver Fibonacci-Algorithmus). Solche kann verhindert werden, indem am Anfang von jeder Methode ebenfalls ein `isInterrupted`-Check eingefügt wird.
Tech-Talk: Michael Faes (michael.faes@fhnw.ch), Dozent für Informatik, HSI
Blog Beitrag: Janic Berger (janic.berger@fhnw.ch), MSE Student und Assistent am IMVS
Kommentare
Keine Kommentare erfasst zu Tech-Talk: Performantes Sandboxing durch Bytecode-Manipulation