2 Punkte von GN⁺ 2024-09-15 | 1 Kommentare | Auf WhatsApp teilen
  • 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 und ClassOriginTracker, 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 Paket org.apache.coyote erzeugt wurde
    • FlowTracker zeigt an, welcher Code welche Ausgabe erzeugt hat
  • Thymeleaf-Template-Ebene

    • Klickt man auf HTML-Tag-Namen wie "html" oder "head", sieht man, dass dieser HTML-Teil aus der Datei layout.html stammt
    • Nach einem Klick auf layout.html lassen 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
  • 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-*.jar von den Github releases pages herunterladen
    • Der Java-Kommandozeile -javaagent:path/to/flowtracker.jar hinzufügen
    • Um einige JVM-Optimierungen zu deaktivieren, die FlowTracker stören, auch die Ausgabe von java -jar flowtracker.jar jvmopts zur Kommandozeile hinzufügen
    • Standardmäßig startet FlowTracker einen Webserver auf Port 8011; daher einfach im Browser http://localhost:8011/ öffnen
  • 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, char und byte[]; 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 ThreadLocal zu verfolgen
  • Tracker-Datenmodell

    • Tracker: speichert den Inhalt des verfolgten Objekts und Informationen über dessen Ursprung
    • content: Daten wie alle Bytes, die durch einen InputStream oder OutputStream gelaufen sind
    • source: verknüpft einen bestimmten Bereich des Inhalts mit einem bestimmten Bereich eines anderen Trackers
    • TrackerRepository: verwaltet ein großes globales Map<Object, Tracker>, das relevante Objekte mit ihrem Tracker verknüpft
    • TrackerPoint: verweist auf eine Position innerhalb eines Trackers und stellt einen einzelnen primitiven Wert dar, etwa den Ursprung eines einzelnen byte

Grundlegende Instrumentierung: JDK-Hooks und ASM

  • FlowTracker hält den Tracker aktuell, indem es Hook-Methodenaufrufe einfügt, wenn bestimmte JDK-Methoden aufgerufen werden
  • Das einfachste Beispiel ist System.arraycopy
    • Aufrufe von java.lang.System.arraycopy werden durch Aufrufe von com.coekie.flowtracker.hook.SystemHook.arraycopy ersetzt
    • SystemHook ruft das eigentliche arraycopy auf und holt anschließend den Tracker des Quell- und Ziel-Arrays aus dem TrackerRepository, um den Ziel-Tracker so zu aktualisieren, dass er auf die Quelle zeigt
  • 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 von FileInputStreamHook.afterReadByteArray eingefügt
    • Diese Instrumentierung ist als annotationsbasiertes eigenes Mikro-Framework mit ASMs AdviceAdapter implementiert
  • FlowTracker fügt Hooks zu den I/O-bezogenen JDK-Klassen java.io.FileInputStream, java.io.FileOutputStream, sun.nio.ch.FileChannelImpl, sun.nio.ch.IOUtil und sun.nio.ch.NioSocketImpl hinzu
  • Relevante Implementierungen:

Verfolgung von primitive-Werten und Analyse des Datenflusses innerhalb von Methoden

  • Primitive-Werte wie byte haben im Gegensatz zu Objekten keine Identity und können daher nicht sicher als Map-Schlüssel im TrackerRepository verfolgt 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 mit ArrayHook.getElementTracker(x, 1) der Tracker von b geholt, und bei y[2] = b wird mit ArrayHook.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 passende FlowValue erzeugt
    • Store und ArrayStore
    • FlowTransformer: steuert den gesamten Analyse- und Instrumentierungsprozess
  • Es werden nicht alle Primitive-Werte verfolgt; der Fokus liegt auf byte und char, während int und long nur 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 PointTracker der Argumente und Rückgabewerte in Invocation und legt diese direkt vor dem Methodenaufruf in einem ThreadLocal ab
  • Am Einstiegspunkt der aufgerufenen Methode werden die Informationen aus dem ThreadLocal mit Invocation.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 von value innerhalb von write(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 ClassOriginTracker erstellt, 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 constantPoint nicht 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 das byte[] in String.value mit dem ClassOriginTracker verknüpft
  • Eine Anweisung wie String s = "abc"; wird in die Form String 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 == stringB werden zu Objects.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 breakStringInterning in USAGE.md konfigurieren
  • 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 ClassOriginTracker verknüpft und diese Position als "<?>" dargestellt
  • Da zum Beispiel Array-Längen nicht verfolgt werden, wird beim Aufruf von write(array.length) an Invocation ein PointTracker übergeben, der auf die Code-Position des write-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 StringConcatFactory zurückgegebenen MethodHandle hinzugefü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

 
GN⁺ 2024-09-15
Meinungen auf Hacker News
  • Sehr cool. Ich habe ein Tool in eine ähnliche Richtung für Clojure gebaut: FlowStorm http://www.flow-storm.org/
    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
    • Wirklich cool. Ich frage mich, warum du JavaFX gewählt hast. Hast du dir nach der Entscheidung für JavaFX auch cljfx angesehen?
    • Schön. Ich frage mich, ob dir auch der Ansatz gefällt, Datenstruktur-Metadaten zur Werteverfolgung zu verwenden.
  • Wirklich beeindruckend.
    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
  • Die Demo, in der ein HTML-Element bis zu der SQL-Anweisung zurückverfolgt wird, die seinen Wert in die Datenbank eingefügt hat, ist beeindruckend.
    Ich kann mir gut vorstellen, dass solche Tools künftig die erste Verteidigungslinie beim Aufspüren von Bugs werden.
    • Danke.
      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.
    • Wenn man darüber nachdenkt, hätte eine Standardmethode zur Verfolgung von Herkunft und Wahrhaftigkeit von Daten wirklich viele Probleme verhindern können, und viele Business-Regeln wären leichter auszudrücken gewesen.
      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.
  • Ich verstehe das Gesamtbild oder die Einsatzweise noch nicht vollständig, aber es erinnert mich an eine Smalltalk-Umgebung, in der man alles inspizieren kann.
    In Smalltalk ist alles ein Objekt und eine Nachricht, sodass man Dinge zurückverfolgen und mit ihnen interagieren kann.
  • Sehr cool. Auch das Demo-Video ist gut, und es wirkt definitiv nützlich, wenn man sich in eine unbekannte Codebase einarbeitet.
  • Vor ein paar Jahren habe ich mit einem ähnlichen Konzept experimentiert[1]. Ich wollte so etwas wie JavaScript-Source-Maps auf HTML anwenden.
    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...
  • Im positiven Sinne erinnert mich das an die Eve-lang-Demo, in der man beim Debuggen eines Programms einfach fragte: „Warum ist es nicht hier?“ Tolle Arbeit.
    https://www.youtube.com/watch?v=TWAMr72VaaU&t=164s und https://witheve.com/
  • Wenn ich mich recht erinnere, gab es ein Paper über ein ähnliches Tool, das SQL-Injection in Java-Programmen dynamisch findet. Ist das dasselbe Tool?
    • Nein, das war vermutlich ein anderes Tool.
      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.
  • Ich habe mir einmal vorgestellt, Daten über das Internet hinweg zu verfolgen. Zum Beispiel, woher ein Bild kam und auf welchem CDN es lag.
    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.
  • Ich versuche gerade, dieses Tool in VSCode zusammen mit dem Projekt laufen zu lassen, das ich verstehen möchte.
    Im Moment muss ich kurz pausieren, aber ich freue mich darauf, es zum Laufen zu bringen und damit herumzuspielen.