HN vorgestellt: FlowTracker – Datenflussverfolgung in Java-Programmen
(github.com/coekie)- FlowTracker ist ein Java-Agent, der verfolgt, wie Java-Programme Daten lesen, verarbeiten und schreiben, und sichtbar macht, aus welchen Eingaben, Dateien, Netzwerken oder Code-Konstanten eine Ausgabe stammt
- Er beobachtet ein laufendes Programm, zeigt Datei- und Netzwerk-I/O an und verfolgt insbesondere die Zuordnung zwischen Ein- und Ausgabe, damit verständlich wird, was die Ausgabe eines Java-Programms bedeutet und warum sie erzeugt wurde
- In der Spring-PetClinic-Demo lässt sich über Header der HTTP-Antwort, Thymeleaf-Templates, Datenbankwerte bis hin zu SQL-Einfügeskripten navigieren und so mehrere Ebenen des Software-Stacks erkunden
- Intern instrumentiert es Bytecode beim Laden durch die JVM und kombiniert JDK-Methoden-Hooks, Datenflussanalyse,
ThreadLocal-basierte Aufrufverfolgung undClassOriginTracker, um ein Ursprungs-Mapping mit Fokus auf Strings, Zeichen und Bytes aufrechtzuerhalten - Der aktuelle Stand ist eher ein Proof of Concept als produktionsreif; in einigen Beispielprogrammen funktioniert es gut, ist aber nicht für alle Programme geeignet und verursacht einen großen Overhead, wodurch die Ausführung deutlich langsamer wird
Was FlowTracker verfolgt
- FlowTracker ist ein Java-Agent, der verfolgt, wie Daten innerhalb eines Java-Programms gelesen, weitergegeben, umgewandelt und geschrieben werden
- Er zeigt nicht nur Datei- und Netzwerk-I/O, sondern verknüpft auch die Ausgabe eines Programms mit den Eingaben, aus denen sie entstanden ist
- Ziel ist es zu verstehen, was die Ausgabe eines Java-Programms bedeutet und warum das Programm genau diese Ausgabe geschrieben hat
- Das aktuelle Projekt ist ein Proof of Concept, der untersucht, welche Erkenntnisse sich aus dieser Sicht auf das Programmverhalten gewinnen lassen
Demo: Ursprung von HTTP-Antworten in Spring PetClinic nachverfolgen
- Die FlowTracker PetClinic demo macht im Browser sichtbar, wie Spring PetClinic HTTP-Anfragen verarbeitet und auf Basis von Templates und Datenbankdaten HTML-Seiten erzeugt
- Auf dem Bildschirm wird die HTTP-Antwort angezeigt, die PetClinic über das Netzwerk gesendet hat; klickt man auf einen Teil des Antwortinhalts, sieht man in der unteren Ansicht, woher dieser Teil stammt
- Über den linken Baum oder auf Mobilgeräten über die Schaltfläche links unten lassen sich verfolgte Eingaben/Ursprünge oder Ausgaben/Sinks auswählen
-
HTTP-Verarbeitungsebene
- Klickt man auf
"HTTP/1.1"oder einen HTTP-Header, sieht man, dass dieser Teil der Antwort in Apache-Coyote-Klassen aus dem Paketorg.apache.coyoteerzeugt wurde - FlowTracker zeigt an, welcher Code welche Ausgabe erzeugt hat
- Klickt man auf
-
Thymeleaf-Template-Ebene
- Klickt man auf HTML-Tag-Namen wie
"html"oder"head", sieht man, dass dieser HTML-Teil aus der Dateilayout.htmlstammt - Nach einem Klick auf
layout.htmllassen sich mit der farbigen+-Schaltfläche unten alle Teile, die aus dieser Datei stammen, in derselben Farbe markieren - Scrollt man nach unten, sieht man, dass Teile der Antwort aus einer anderen Datei,
ownerDetails.html, stammen - Klickt man auf die Zeichen
<oder>, sieht man, dass diese Zeichen von der Thymeleaf-Template-Bibliothek geschrieben wurden
- Klickt man auf HTML-Tag-Namen wie
-
Ursprung von Datenbankwerten
- Die Tabelle auf der HTML-Seite enthält Informationen aus der Datenbank
- Klickt man in der Tabelle auf
George, lässt sich der Wert nicht nur bis zur Datenbank zurückverfolgen, sondern sogar bis zu dem SQL-Skript, das ihn ursprünglich in die Datenbank eingefügt hat - Dass sich in dieser Demo bis zum SQL-Skript zurückverfolgen lässt, liegt daran, dass eine In-Memory-Datenbank verwendet wird und der Datenbankinhalt die JVM nie verlässt
MySQL-Demo und Framework-Unabhängigkeit
- Führt man dieselbe PetClinic-Demo mit einer MySQL-Datenbank aus, lassen sich Werte bis zum Punkt der Datenbankverbindung zurückverfolgen
- In diesem Fall sieht man die zuvor gesendeten SQL-Abfragen, mit denen die Werte erzeugt wurden, sowie Details dazu, wie der MySQL-JDBC-Treiber mit der Datenbank kommuniziert
- Die FlowTracker PetClinic mysql demo zeigt außerdem, dass FlowTracker die über die SSL-Datenbankverbindung übertragenen entschlüsselten Inhalte abfängt
- Spring PetClinic ist nur ein Beispiel; FlowTracker hängt nicht von einem bestimmten Framework oder einer bestimmten Bibliothek ab
- Die javac demo zeigt, wie FlowTracker beim Beobachten des Java-Compilers hilft, das erzeugte class-Dateiformat und den darin enthaltenen Bytecode zu verstehen
Verwendung und Hinweise
- Der aktuelle Stand von FlowTracker ist eher ein Proof of Concept als produktionsreif
- Es hat in mehreren Beispielprogrammen gut funktioniert, aber es gibt keine Garantie, dass es in allen Programmen gut funktioniert
- Es verursacht viel Overhead, sodass Programme deutlich langsamer ausgeführt werden
- Vorgehensweise:
- Das Agent-JAR
flowtracker-*.jarvon den Github releases pages herunterladen - Der Java-Kommandozeile
-javaagent:path/to/flowtracker.jarhinzufügen - Um einige JVM-Optimierungen zu deaktivieren, die FlowTracker stören, auch die Ausgabe von
java -jar flowtracker.jar jvmoptszur Kommandozeile hinzufügen - Standardmäßig startet FlowTracker einen Webserver auf Port 8011; daher einfach im Browser
http://localhost:8011/öffnen
- Das Agent-JAR
- Weitere Konfigurationsoptionen stehen in
USAGE.md
Interne Funktionsweise: Bytecode-Instrumentierung und Tracker-Modell
- FlowTracker ist ein Instrumentierungs-Agent, der beim Laden von Klassen durch die JVM Code in class-Dateien, also in den Bytecode, injiziert
- Der injizierte Code hält beim Lesen, Weitergeben und Schreiben von Daten im Programm ein Mapping zwischen Daten im Speicher und ihrem Ursprung aufrecht
- Im Fokus der Verfolgung stehen Text- und Binärdaten wie
String,charundbyte[]; Zahlen, strukturierte Daten und berechnete Daten stehen nicht im Zentrum - Verwendete Methoden:
- Einige JDK-Methodenaufrufe werden durch Aufrufe von FlowTracker-Versionen dieser Methoden ersetzt
- An zentralen Stellen im JDK wird Code injiziert, um Eingaben und Ausgaben zu verfolgen
- Um lokale Variablen und Stack-Werte innerhalb von Methoden zu verfolgen, werden Datenflussanalyse und tiefere Instrumentierung eingesetzt
- Vor und nach Methodenaufrufen sowie am Anfang und Ende aufgerufener Methoden wird Code hinzugefügt, um Argumente und Rückgabewerte mit
ThreadLocalzu verfolgen
-
Tracker-Datenmodell
Tracker: speichert den Inhalt des verfolgten Objekts und Informationen über dessen Ursprungcontent: Daten wie alle Bytes, die durch einenInputStreamoderOutputStreamgelaufen sindsource: verknüpft einen bestimmten Bereich des Inhalts mit einem bestimmten Bereich eines anderen TrackersTrackerRepository: verwaltet ein großes globalesMap<Object, Tracker>, das relevante Objekte mit ihremTrackerverknüpftTrackerPoint: verweist auf eine Position innerhalb eines Trackers und stellt einen einzelnen primitiven Wert dar, etwa den Ursprung eines einzelnenbyte
Grundlegende Instrumentierung: JDK-Hooks und ASM
- FlowTracker hält den
Trackeraktuell, indem es Hook-Methodenaufrufe einfügt, wenn bestimmte JDK-Methoden aufgerufen werden - Das einfachste Beispiel ist
System.arraycopy- Aufrufe von
java.lang.System.arraycopywerden durch Aufrufe voncom.coekie.flowtracker.hook.SystemHook.arraycopyersetzt SystemHookruft das eigentlichearraycopyauf und holt anschließend den Tracker des Quell- und Ziel-Arrays aus demTrackerRepository, um den Ziel-Tracker so zu aktualisieren, dass er auf die Quelle zeigt
- Aufrufe von
- Für diese Instrumentierung wird die ASM-Bytecode-Manipulationsbibliothek verwendet
- Die meisten Hooks werden nicht auf der Aufruferseite, sondern auf der Empfängerseite innerhalb der JDK-Methode hinzugefügt
- So wird zum Beispiel am Ende von
FileInputStream.read(byte[])ein Aufruf vonFileInputStreamHook.afterReadByteArrayeingefügt - Diese Instrumentierung ist als annotationsbasiertes eigenes Mikro-Framework mit ASMs
AdviceAdapterimplementiert
- So wird zum Beispiel am Ende von
- FlowTracker fügt Hooks zu den I/O-bezogenen JDK-Klassen
java.io.FileInputStream,java.io.FileOutputStream,sun.nio.ch.FileChannelImpl,sun.nio.ch.IOUtilundsun.nio.ch.NioSocketImplhinzu - Relevante Implementierungen:
Verfolgung von primitive-Werten und Analyse des Datenflusses innerhalb von Methoden
- Primitive-Werte wie
bytehaben im Gegensatz zu Objekten keine Identity und können daher nicht sicher alsMap-Schlüssel imTrackerRepositoryverfolgt werden - FlowTracker schreibt den Code so um, dass die Herkunft von Primitive-Werten separat in lokalen Variablen innerhalb der Methode gespeichert wird
- Nach
byte b = x[1]wird zum Beispiel mitArrayHook.getElementTracker(x, 1)der Tracker vonbgeholt, und beiy[2] = bwird mitArrayHook.setElementTracker(y, 2, bTracker)die Herkunft im Ziel-Array vermerkt - Dafür führt FlowTracker auf Basis der Analysefunktionen von ASM eine symbolische Interpretation durch
- Dabei wird für jeden Punkt einer Methode modelliert, woher die Werte in lokalen Variablen und auf dem Stack kommen und wohin sie fließen
- Relevante Implementierungen:
- FlowValue und ArrayLoadValue
- MergedValue: verarbeitet Situationen, in denen Werte aufgrund von Kontrollfluss wie
if-Anweisungen oder Schleifen aus mehreren Positionen kommen können - FlowInterpreter: Erweiterung von ASM
Interpreter, die Bytecode-Befehle interpretiert und passendeFlowValueerzeugt - Store und ArrayStore
- FlowTransformer: steuert den gesamten Analyse- und Instrumentierungsprozess
- Es werden nicht alle Primitive-Werte verfolgt; der Fokus liegt auf
byteundchar, währendintundlongnur eingeschränkter behandelt werden
Datenfluss über Methodenaufrufe hinweg
- Eine Analyse nur innerhalb einer Methode kann nicht abdecken, wenn Primitive-Werte als Argumente und Rückgabewerte in andere Methoden fließen
- FlowTracker speichert die
PointTrackerder Argumente und Rückgabewerte inInvocationund legt diese direkt vor dem Methodenaufruf in einemThreadLocalab - Am Einstiegspunkt der aufgerufenen Methode werden die Informationen aus dem
ThreadLocalmitInvocation.start(...)ausgelesen, sodass die Herkunft von Primitive-Argumenten verwendet werden kann - So kann auch in Fällen, in denen Primitive-Werte an Methoden übergeben werden, etwa bei
out.write(b), der Tracker vonvalueinnerhalb vonwrite(byte value)weitergereicht werden - Relevante Implementierungen:
Code selbst als Datenquelle behandeln
- Die wichtigsten Quellen, die FlowTracker verfolgt, sind I/O und Werte aus dem Code selbst
- Zu den aus dem Code stammenden Werten gehören primitive Konstanten und
String-Konstanten wie'a'und"abc" - Für solche Konstanten wird pro Klasse ein
ClassOriginTrackererstellt, der eine textuelle Darstellung der Klasse und der Konstantenreferenzen darin speichert - Wenn auf eine Konstante verwiesen wird, zeigt der Tracker dieses Werts auf die Position innerhalb dieser textuellen Darstellung
- Dieses Modell behandelt Konstanten so, als würden sie aus der textuellen Darstellung des Codes gelesen, und ähnelt damit dem I/O-Tracking-Modell
- Aus Performance-Gründen wird ConstantDynamic (JEP 309) verwendet, damit die Methode
constantPointnicht bei jeder Methodenausführung aufgerufen wird - Zugehörige Implementierungen:
Verarbeitung von String-Literalen und Einschränkungen
- Bei String-Literalen wird eine neue
String-Kopie erstellt und dasbyte[]inString.valuemit demClassOriginTrackerverknüpft - Eine Anweisung wie
String s = "abc";wird in die FormString s = StringHook.constantString("abc", 1234, 81);umgeschrieben - Dieser Ansatz bricht die String-Interning-Garantie, die die JVM normalerweise bietet
- Ursprünglich müssen alle Vorkommen derselben String-Konstante auf dieselbe Instanz verweisen
- Nach der Instrumentierung kann Code brechen, der sich auf diese Garantie verlässt
- FlowTracker enthält einige Vorkehrungen, um dieses Problem zu verringern
- Durch den Einsatz von ConstantDynamic wird selbst bei mehrfacher Ausführung desselben String-Literals in derselben Zeile jedes Mal dieselbe Instanz zurückgegeben
- Einige Ausdrücke wie
stringA == stringBwerden zuObjects.equals(stringA, stringB)umgeschrieben, sodass sie aus bestimmten Perspektiven wie dieselbe Instanz wirken - In einigen Paketen wie
java.lang.*wird das Tracking von String-Literalen deaktiviert - Dieses Verhalten lässt sich über
breakStringInterninginUSAGE.mdkonfigurieren
- Zugehörige Implementierungen:
Fallback für nicht verfolgte Werte
- FlowTracker verfolgt nicht alle Werte eines Programms
- Gründe dafür sind Performance-Bedenken, noch nicht implementierte Bereiche, Werte mit geringer Relevanz und die Tatsache, dass für Werte, die aus der Kombination mehrerer Quellen entstehen, ein komplexeres Datenmodell nötig wäre
- Wenn ein zuvor nicht verfolgter Wert eine Stelle erreicht, an der das Tracking beginnen muss, wird er ähnlich wie Konstanten mit dem
ClassOriginTrackerverknüpft und diese Position als"<?>"dargestellt - Da zum Beispiel Array-Längen nicht verfolgt werden, wird beim Aufruf von
write(array.length)anInvocationeinPointTrackerübergeben, der auf die Code-Position deswrite-Aufrufs zeigt - Dadurch kann man bei Ausgaben in Binärformaten zwar die ursprüngliche Quelle nicht sehen, den Bedeutungszusammenhang eines Werts aber oft trotzdem schnell über die umliegenden verfolgten Strings und die Code-Position erfassen
Weitere behandelbare Implementierungsthemen
- MergedValue behandelt die Verfolgung von Werten, die durch Verzweigungen und Schleifen laufen, und gilt als der schwierigste Teil der Datenflussanalyse
- String concatenation wird über indification (JEP 280) verarbeitet, indem ein Hook zu dem von
StringConcatFactoryzurückgegebenenMethodHandlehinzugefügt wird - Das Auffinden von Quellcode, die Dekompilierung mit Vineflower und die Verknüpfung von Bytecode mit Quellzeilen sind ebenfalls Teil der Implementierung
- Die ClassLoader-Konfiguration ist darauf ausgerichtet, bootclasspath-Abhängigkeiten und Konflikte mit der Anwendung zu vermeiden und zugleich einen schnellen Entwicklungszyklus ohne Shading und verschachtelte JARs beizubehalten
- Die Verfolgung von in Feldern gespeicherten primitive-Werten ist ebenfalls Teil der Implementierung
- Das Frontend besteht aus einem auf Jetty und JAX-RS basierenden Webserver sowie einer auf Svelte basierenden Web-UI
1 Kommentare
Meinungen auf Hacker News
Für die Instrumentierung verwende ich statt eines Instrumentierungs-Agenten einen Fork des offiziellen Clojure-Compilers und nutze die Clojure-Eigenschaft, den Compiler während der Entwicklung leicht austauschen zu können, um zusätzlichen Bytecode einzufügen.
Interessant an der Aufzeichnung der Ausführung von Clojure-Programmen ist, dass die meisten Werte unveränderlich sind, sodass man Snapshots erstellen kann, indem man nur Pointer behält.
Da die Demo im ursprünglichen Beitrag durch eine Web-App navigiert, lasse ich für Interessierte auch eine Demo hier, wie man mit FlowStorm eine Web-App debuggt: https://www.youtube.com/watch?v=h8AFpZkAwPo
Ich mag die hervorragenden Tools im Java/JVM-Ökosystem. So erstaunt war ich zuletzt, als ich früher jitwatch gesehen habe: https://github.com/AdoptOpenJDK/jitwatch
FlowTracker erinnert mich ein wenig an Taint Analysis, bei der verfolgt wird, wie ungeprüfte Benutzereingaben oder Geheimwerte durch ein Programm fließen, damit sie nicht leaken oder ungeprüft verwendet werden.
Der Suchbegriff lautet „dynamic taint tracking/analysis“.
https://github.com/gmu-swe/phosphor
https://github.com/soot-oss/SootUp
https://github.com/feliam/klee-taint
Ich kann mir gut vorstellen, dass solche Tools künftig die erste Verteidigungslinie beim Aufspüren von Bugs werden.
Bei der Entwicklung von FlowTracker entstand ein großer Teil der Arbeit daraus, das Tracking für bestimmte Beispielprogramme zum Laufen zu bringen.
Ich kannte das gewünschte Ergebnis, aber es war schwer vorherzusagen, welche Low-Level-Mechanismen unterstützt werden mussten, damit ein bestimmtes Beispiel funktioniert; oft hing es von internen Implementierungsdetails der JDK oder von Bibliotheken ab, durch die die Daten liefen.
Dass das HTML-Element mit dem SQL-Skript verknüpft wurde, das die Daten in die DB eingefügt hat, war aber nicht so.
Es war weder erwartet noch beabsichtigt, sondern passierte einfach so; deshalb war ich selbst ziemlich überrascht und freue mich darauf zu sehen, was man mit diesem Ansatz noch machen kann.
Es wäre auch gut, eine Möglichkeit zu haben, nachzuverfolgen, ob Daten flüchtig sind oder erneut gespeichert werden müssen.
Je mehr solcher Einschränkungen man im Vorfeld beschreiben kann, desto besser.
In Smalltalk ist alles ein Objekt und eine Nachricht, sodass man Dinge zurückverfolgen und mit ihnen interagieren kann.
Ich hatte nicht die Zeit, es weiter auszubauen, aber Web-Development-Tools könnten meiner Meinung nach stark von dieser Art Full-Stack-Zuordnungstracking profitieren.
Allerdings fühlt sich die Integration solcher Lösungen in bestehende Frameworks wie eine große Herausforderung an.
[1] HTML Source Maps - https://github.com/connorjclark/html-source-maps https://docs.google.com/document/d/19XYWiPL9h9vA6QcOrGV9Nfkr...
https://www.youtube.com/watch?v=TWAMr72VaaU&t=164s und https://witheve.com/
Wenn man das, was FlowTracker macht, erweitert, kann man damit auch SQL- oder andere Injection-Schwachstellen finden. Es ist also möglich, dass das Tool, an das du denkst, einen ähnlichen Ansatz verwendet hat.
Oder Fragen wie: „Was hat dieser String gesehen, seit er erzeugt wurde, bis er auf meinem Bildschirm angekommen ist?“
Das wirkt wie ein Schritt in diese Richtung.
Im Moment muss ich kurz pausieren, aber ich freue mich darauf, es zum Laufen zu bringen und damit herumzuspielen.