HN公開: FlowTracker — Javaプログラムのデータフローを追跡
(github.com/coekie)- FlowTrackerは、Javaプログラムがデータを読み取り、操作し、書き込む過程を追跡するJava agentで、出力がどの入力・ファイル・ネットワーク・コード定数に由来するのかを関連付けて表示する
- 実行中のプログラムを観察してファイルおよびネットワークI/Oを表示し、特に入力と出力の対応関係を追跡することで、Javaプログラムの出力が何を意味し、なぜ生成されたのかを理解できるようにする
- Spring PetClinicデモでは、HTTPレスポンスのヘッダー、Thymeleafテンプレート、データベースの値、SQL挿入スクリプトまでたどり、ソフトウェアスタックの複数の層を探索できる
- 内部的には、JVMのロード時点でバイトコードを計測し、JDKメソッドのフック、データフロー解析、
ThreadLocalベースの呼び出し追跡、ClassOriginTrackerを組み合わせて、文字列・文字・バイトを中心とした出所マッピングを維持する - 現在の状態は本番運用の準備が整ったものというより概念実証に近く、一部のサンプルプログラムではうまく動作したが、すべてのプログラムに適しているわけではなく、大きなオーバーヘッドにより実行速度が大幅に低下する
FlowTrackerが追跡するもの
- FlowTrackerは、Javaプログラム内でデータがどのように読み取られ、受け渡され、変換され、書き込まれるかを追跡するJava agentである
- ファイルやネットワークI/Oを見せるだけでなく、プログラムの出力がどの入力に由来するのかを関連付けて表示する
- 目的は、Javaプログラムの出力が何を意味するのか、そしてプログラムがなぜその出力を書き出したのかを理解することにある
- 現在のプロジェクトは、この観点からプログラムの動作を見ることでどのような洞察が得られるかを探るproof-of-conceptである
デモ: Spring PetClinicでHTTPレスポンスの出所を追跡
- FlowTracker PetClinic demoでは、Spring PetClinicがHTTPリクエストを処理し、テンプレートとデータベースデータに基づいてHTMLページを生成する過程をブラウザで見られる
- 画面にはPetClinicがネットワークに送信したHTTPレスポンスが表示され、レスポンス本文の一部をクリックすると、下側のビューでその部分がどこから来たのかを確認できる
- 左側のツリー、またはモバイルでは左下のボタンから、追跡された入力・出所または出力・シンクを選択できる
-
HTTP処理層
"HTTP/1.1"やHTTPヘッダーをクリックすると、そのレスポンス部分がorg.apache.coyoteパッケージのApache Coyoteクラスで生成されたことが分かる- FlowTrackerは、どのコードがどの出力を作ったのかを表示する
-
Thymeleafテンプレート層
"html"や"head"のようなHTMLタグ名をクリックすると、そのHTML部分がlayout.htmlファイルに由来することが分かるlayout.htmlをクリックした後、下側の色付き+ボタンを押すと、そのファイルに由来するすべての部分が同じ色で表示される- 下にスクロールすると、レスポンスの一部が別のファイルである
ownerDetails.htmlに由来していることを確認できる <または>文字をクリックすると、その文字がThymeleafテンプレートライブラリによって書き込まれたことが分かる
-
データベース値の出所
- HTMLページのテーブルには、データベースに由来する情報が含まれている
- テーブル内の
Georgeをクリックすると、その値がデータベース由来であるというレベルを超えて、最初にデータベースへ値を挿入したSQLスクリプトまで追跡される - このデモでSQLスクリプトまで追跡されるのは、インメモリデータベースを使用しており、データベースの内容がJVMの外へ出ていないためである
MySQLデモとフレームワーク非依存性
- 同じPetClinicデモをMySQLデータベースで実行すると、値はデータベース接続地点まで追跡される
- この場合、以前に値を作成するために送信されたSQLクエリと、MySQL JDBCドライバがデータベースと通信する詳細を確認できる
- FlowTracker PetClinic mysql demoは、FlowTrackerがデータベースSSL接続を通じて送信された復号済みの内容も傍受することを示している
- Spring PetClinicはあくまで一例であり、FlowTrackerは特定のフレームワークやライブラリに依存しない
- javac demoは、Javaコンパイラを観察して、生成されたclassファイル形式とその中のバイトコードを理解するうえでFlowTrackerがどのように役立つかを示している
使用方法と注意事項
- 現在のFlowTrackerは、本番運用可能な状態というよりproof-of-conceptに近い
- 複数のサンプルプログラムでうまく動作したが、すべてのプログラムで問題なく動作する保証はない
- 大きなオーバーヘッドが追加されるため、プログラムの実行は大幅に遅くなる
- 使用手順:
- Github releases pagesから
flowtracker-*.jaragent jarをダウンロードする - Javaコマンドラインに
-javaagent:path/to/flowtracker.jarを追加する - FlowTrackerを妨げる一部のJVM最適化を無効にするため、
java -jar flowtracker.jar jvmoptsの出力もコマンドラインに追加する - デフォルトではFlowTrackerは8011番ポートでWebサーバーを起動するので、ブラウザで
http://localhost:8011/を開けばよい
- Github releases pagesから
- より詳しい設定オプションは
USAGE.mdにある
内部動作: バイトコード計測とTrackerモデル
- FlowTrackerは、JVMがクラスをロードするときにclassファイル、つまりバイトコードへコードを注入する計測agentである
- 注入されたコードは、プログラムがデータを読み取り、受け渡し、書き込むあいだ、メモリ内データとその出所のマッピングを維持する
- 追跡対象の中心は、
String、char、byte[]のようなテキスト・バイナリデータであり、数値・構造化データ・計算結果のデータが主眼ではない - 使用される方法:
- 一部のJDKメソッド呼び出しを、FlowTracker版のメソッド呼び出しに置き換える
- 入力と出力を追跡するため、JDKの中核的な箇所にコードを注入する
- メソッド内部のローカル変数とスタック値を追跡するため、データフロー解析とより深い計測を行う
- メソッド呼び出しの前後と、呼び出されたメソッドの開始・終了時にコードを追加し、
ThreadLocalで引数と戻り値を追跡する
-
Trackerデータモデル
Tracker: 追跡対象オブジェクトの内容と出所情報を保持するcontent:InputStreamやOutputStreamを通過した全バイトのようなデータsource: 内容の特定範囲を、別のtrackerの特定範囲に関連付けるTrackerRepository: 関心のあるオブジェクトと対応するTrackerを結び付ける、大きなグローバルMap<Object, Tracker>を保持するTrackerPoint: tracker内のある位置を指し、1つのbyteの出所のような単一のprimitive値を表す
基本的な計測: JDKフックとASM
- FlowTrackerは、特定のJDKメソッドが呼び出されたときにhookメソッド呼び出しを挿入し、
Trackerを最新の状態に保つ - 最も単純な例は
System.arraycopyjava.lang.System.arraycopy呼び出しをcom.coekie.flowtracker.hook.SystemHook.arraycopy呼び出しに置き換えるSystemHookは実際のarraycopyを呼び出した後、TrackerRepositoryから元配列・先配列のtrackerを取得し、先のtrackerが元を指すように更新する
- このような計測にはASMバイトコード操作ライブラリを使用する
- ほとんどのhookは呼び出し側ではなく、JDKメソッド内部の呼び出される側に追加される
- たとえば、
FileInputStream.read(byte[])の末尾にFileInputStreamHook.afterReadByteArray呼び出しを追加する - この計測は、ASMの
AdviceAdapterを使うアノテーションベースの独自マイクロフレームワークとして実装されている
- たとえば、
- FlowTrackerは、
java.io.FileInputStream、java.io.FileOutputStream、sun.nio.ch.FileChannelImpl、sun.nio.ch.IOUtil、sun.nio.ch.NioSocketImplのようなJDKのI/O関連クラスにhookを追加する - 関連実装:
primitive値の追跡とメソッド内部データフロー解析
byteのようなprimitive値は、オブジェクトのようなidentityを持たないため、TrackerRepositoryのMapキーとして安全に追跡できない- FlowTrackerは、primitive値の出所をメソッド内部のローカル変数に別途保存するようコードを書き換える
- たとえば、
byte b = x[1]の後ではArrayHook.getElementTracker(x, 1)でbのtrackerを取得し、y[2] = bのときArrayHook.setElementTracker(y, 2, bTracker)で先の配列に出所を記録する - このためにFlowTrackerは、ASMの解析機能の上でシンボリックインタープリテーション(symbolic interpretation) を行う
- メソッドの各地点で、ローカル変数とスタックの値がどこから来てどこへ行くのかをモデル化する
- 関連実装:
- FlowValueとArrayLoadValue
- MergedValue: if文やloopのような制御フローによって値が複数の位置から来る可能性がある状況を処理する
- FlowInterpreter: ASM
Interpreter拡張としてバイトコード命令を解釈し、適切なFlowValueを作る - StoreとArrayStore
- FlowTransformer: 全体の解析と計測プロセスを駆動する
- すべてのprimitive値を追跡するわけではなく、焦点は
byteとcharで、intとlongはそれより限定的に扱う
メソッド呼び出しをまたぐデータフロー
- メソッド内部の解析だけでは、primitive値が別のメソッドの引数や戻り値として流れる状況を処理できない
- FlowTrackerは、
Invocationに引数と戻り値のPointTrackerを保存し、メソッド呼び出しの直前にこれをThreadLocalへ入れる - 呼び出されたメソッドの開始地点では、
Invocation.start(...)でThreadLocalの情報を取り出し、primitive引数の出所を利用できる - この方式により、
out.write(b)のようにprimitive値をメソッドへ渡す場合でも、write(byte value)内部でvalueのtrackerを引き継げる - 関連実装:
コード自体をデータの出所として扱う
- FlowTrackerが追跡する主な出所は、I/Oとコード自体から来る値である
- コード由来の値には、
'a'、"abc"のような primitive およびString定数が含まれる - こうした定数については、クラスごとに
ClassOriginTrackerを作成し、クラスと定数参照をテキストで表現した内容を保持する - 定数が参照されると、その値の tracker はこのテキスト表現内の位置を指す
- このモデルでは、定数をコードのテキスト表現から読み取られたもののように扱うため、I/O追跡モデルと似たものになる
- パフォーマンス上の理由から、
constantPointメソッドがメソッド実行のたびに呼び出されないよう ConstantDynamic(JEP 309)を使っている - 関連実装:
String リテラルの処理と制約
Stringリテラルでは新しいStringのコピーを作成し、String.value内のbyte[]をClassOriginTrackerに関連付けるString s = "abc";のような文は、String s = StringHook.constantString("abc", 1234, 81);の形に書き換えられる- この方式は、JVMが通常提供する String interning の保証 を壊す
- 本来、同じ
String定数のすべての出現は同じインスタンスを参照しなければならない - instrumentation 後は、この保証に依存するコードが壊れる可能性がある
- 本来、同じ
- FlowTrackerはこの問題を軽減するため、いくつかの仕組みを設けている
- ConstantDynamic を使い、同じ行の同じ
Stringリテラルが複数回実行されても、毎回同じインスタンスを返す - 一部の
stringA == stringB式をObjects.equals(stringA, stringB)に書き換え、特定の観点では同じインスタンスのように見えるようにする java.lang.*のような一部パッケージではStringリテラルの追跡を無効化する- この動作は
USAGE.mdのbreakStringInterningで設定できる
- ConstantDynamic を使い、同じ行の同じ
- 関連実装:
追跡されない値のフォールバック
- FlowTrackerはプログラム内のすべての値を追跡するわけではない
- その理由として、パフォーマンス上の懸念、未実装の部分、関連性の低い値、複数の出所の組み合わせから生じる値を表現するにはより複雑なデータモデルが必要であることが挙げられる
- それまで追跡されていなかった値が、追跡を開始すべき場所に到達すると、定数と同様に
ClassOriginTrackerに関連付けられ、その位置は"<?>"として表現される - たとえば配列長は追跡されないため、
write(array.length)が呼ばれると、Invocationにはwrite呼び出し箇所のコード位置を指すPointTrackerが渡される - その結果、バイナリ形式の出力では元の出所が見えなくても、周囲の追跡済み文字列やコード位置を通じて、その値の意味を素早く解釈できる場合がある
さらに掘り下げられる実装トピック
- MergedValue は、分岐やループを通過する値の追跡を扱っており、データフロー解析でもっとも難しい部分のひとつとされる
- String concatenation は indification(JEP 280)を通じて、
StringConcatFactoryが返すMethodHandleに hook を追加して処理している - ソースコードの探索、Vineflower によるデコンパイル、バイトコードとソース行の対応付けも実装に含まれている
- ClassLoader の構成は、bootclasspath 依存関係やアプリケーションとの競合を避けつつ、shading や nested jar なしで高速な開発サイクルを維持することに重点を置いている
- フィールドに保存された primitive 値の追跡も実装に含まれている
- フロントエンドは Jetty と JAX-RS ベースの Web サーバー、および Svelte ベースの Web UI で構成されている
1件のコメント
Hacker News のコメント
計測には計測エージェントではなく公式 Clojure コンパイラのフォークを使い、開発中にコンパイラを簡単に差し替えられるという Clojure の特性を利用して追加のバイトコードを挿入しています
Clojure プログラムの実行記録で興味深いのは、ほとんどの値が不変なので、ポインタを保持するだけでスナップショットを取れることです
元記事のデモが Web アプリ探索なので、興味のある人向けに FlowStorm で Web アプリをデバッグするデモも置いておきます https://www.youtube.com/watch?v=h8AFpZkAwPo
Java/JVM エコシステムのツールが優れていてうれしいです。こんなに驚いたのは、以前 jitwatch を見たとき以来でした https://github.com/AdoptOpenJDK/jitwatch
FlowTracker は、検証されていないユーザー入力や秘密値がプログラム内でどう流れるかを追跡し、漏えいしたり検証なしに使われたりしないようにする汚染解析を少し思い起こさせます
検索キーワードは “dynamic taint tracking/analysis” です
https://github.com/gmu-swe/phosphor
https://github.com/soot-oss/SootUp
https://github.com/feliam/klee-taint
今後こうしたツールが、バグを追跡するときの第一防衛線になる様子は十分に想像できます
FlowTracker を開発する中で、多くの作業は特定のサンプルプログラムで追跡が動くようにするところから生まれました
目標とする結果は分かっていましたが、特定の例を動かすためにどの低レベルの仕組みをサポートする必要があるのかは予測しづらく、データが通過する JDK やライブラリの内部実装の詳細に左右されることがよくありました
しかし、HTML 要素がそのデータを DB に入れた SQL スクリプトにつながるのは、そういうものではありませんでした
期待したり意図して作ったものではなく、ただそうなっただけで、だから私もかなり驚きましたし、このアプローチでさらに何ができるのか楽しみになりました
データが一時的なものなのか、再び記録されるべきものなのかを追跡する方法もあるとよさそうです
こうした制約を前もって多く記述できるほど良いですね
Smalltalk ではすべてがオブジェクトとメッセージなので、さかのぼって追跡し、相互作用できます
さらに拡張する時間は取れませんでしたが、Web 開発ツールはこの種のフルスタック帰属追跡から大きな恩恵を受けると思います
ただ、こうした解決策を既存のフレームワークに統合するのは大きな課題に感じます
[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 そして https://witheve.com/
FlowTracker がしていることを拡張すれば、SQL や他のインジェクション脆弱性も見つけられます。なので、思い浮かべているツールが似たアプローチを使っていた可能性はあります
あるいは「この文字列は生成された瞬間から自分の画面に届くまでに何を見てきたのか」という質問です
これはその方向への一歩に見えます
今は少し中断しなければなりませんが、動くようにしていろいろ触ってみるのが楽しみです