2 ポイント 投稿者 GN⁺ 2024-09-15 | 1件のコメント | WhatsAppで共有
  • 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-*.jar agent jarをダウンロードする
    • Javaコマンドラインに-javaagent:path/to/flowtracker.jarを追加する
    • FlowTrackerを妨げる一部のJVM最適化を無効にするため、java -jar flowtracker.jar jvmoptsの出力もコマンドラインに追加する
    • デフォルトではFlowTrackerは8011番ポートでWebサーバーを起動するので、ブラウザでhttp://localhost:8011/を開けばよい
  • より詳しい設定オプションはUSAGE.mdにある

内部動作: バイトコード計測とTrackerモデル

  • FlowTrackerは、JVMがクラスをロードするときにclassファイル、つまりバイトコードへコードを注入する計測agentである
  • 注入されたコードは、プログラムがデータを読み取り、受け渡し、書き込むあいだ、メモリ内データとその出所のマッピングを維持する
  • 追跡対象の中心は、Stringcharbyte[]のようなテキスト・バイナリデータであり、数値・構造化データ・計算結果のデータが主眼ではない
  • 使用される方法:
    • 一部のJDKメソッド呼び出しを、FlowTracker版のメソッド呼び出しに置き換える
    • 入力と出力を追跡するため、JDKの中核的な箇所にコードを注入する
    • メソッド内部のローカル変数とスタック値を追跡するため、データフロー解析とより深い計測を行う
    • メソッド呼び出しの前後と、呼び出されたメソッドの開始・終了時にコードを追加し、ThreadLocalで引数と戻り値を追跡する
  • Trackerデータモデル

    • Tracker: 追跡対象オブジェクトの内容と出所情報を保持する
    • content: InputStreamOutputStreamを通過した全バイトのようなデータ
    • source: 内容の特定範囲を、別のtrackerの特定範囲に関連付ける
    • TrackerRepository: 関心のあるオブジェクトと対応するTrackerを結び付ける、大きなグローバルMap<Object, Tracker>を保持する
    • TrackerPoint: tracker内のある位置を指し、1つのbyteの出所のような単一のprimitive値を表す

基本的な計測: JDKフックとASM

  • FlowTrackerは、特定のJDKメソッドが呼び出されたときにhookメソッド呼び出しを挿入し、Trackerを最新の状態に保つ
  • 最も単純な例はSystem.arraycopy
    • java.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.FileInputStreamjava.io.FileOutputStreamsun.nio.ch.FileChannelImplsun.nio.ch.IOUtilsun.nio.ch.NioSocketImplのようなJDKのI/O関連クラスにhookを追加する
  • 関連実装:

primitive値の追跡とメソッド内部データフロー解析

  • byteのようなprimitive値は、オブジェクトのようなidentityを持たないため、TrackerRepositoryMapキーとして安全に追跡できない
  • FlowTrackerは、primitive値の出所をメソッド内部のローカル変数に別途保存するようコードを書き換える
  • たとえば、byte b = x[1]の後ではArrayHook.getElementTracker(x, 1)bのtrackerを取得し、y[2] = bのときArrayHook.setElementTracker(y, 2, bTracker)で先の配列に出所を記録する
  • このためにFlowTrackerは、ASMの解析機能の上でシンボリックインタープリテーション(symbolic interpretation) を行う
  • メソッドの各地点で、ローカル変数とスタックの値がどこから来てどこへ行くのかをモデル化する
  • 関連実装:
  • すべてのprimitive値を追跡するわけではなく、焦点はbytecharで、intlongはそれより限定的に扱う

メソッド呼び出しをまたぐデータフロー

  • メソッド内部の解析だけでは、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 メソッドがメソッド実行のたびに呼び出されないよう ConstantDynamicJEP 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.mdbreakStringInterning で設定できる
  • 関連実装:

追跡されない値のフォールバック

  • 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件のコメント

 
GN⁺ 2024-09-15
Hacker News のコメント
  • いいですね。同じ方向性で Clojure 向けのツール FlowStorm を作りました http://www.flow-storm.org/
    計測には計測エージェントではなく公式 Clojure コンパイラのフォークを使い、開発中にコンパイラを簡単に差し替えられるという Clojure の特性を利用して追加のバイトコードを挿入しています
    Clojure プログラムの実行記録で興味深いのは、ほとんどの値が不変なので、ポインタを保持するだけでスナップショットを取れることです
    元記事のデモが Web アプリ探索なので、興味のある人向けに FlowStorm で Web アプリをデバッグするデモも置いておきます https://www.youtube.com/watch?v=h8AFpZkAwPo
    • 本当にすばらしい。なぜ JavaFX を選んだのか気になります。JavaFX を選んだあとで cljfx も見てみましたか?
    • いいですね。値の追跡にデータ構造メタデータを使うやり方も好きかどうか気になります
  • 本当にすごい
    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
  • HTML 要素を、その値をデータベースに追加した SQL 文までさかのぼって追跡するデモが印象的です
    今後こうしたツールが、バグを追跡するときの第一防衛線になる様子は十分に想像できます
    • ありがとう
      FlowTracker を開発する中で、多くの作業は特定のサンプルプログラムで追跡が動くようにするところから生まれました
      目標とする結果は分かっていましたが、特定の例を動かすためにどの低レベルの仕組みをサポートする必要があるのかは予測しづらく、データが通過する JDK やライブラリの内部実装の詳細に左右されることがよくありました
      しかし、HTML 要素がそのデータを DB に入れた SQL スクリプトにつながるのは、そういうものではありませんでした
      期待したり意図して作ったものではなく、ただそうなっただけで、だから私もかなり驚きましたし、このアプローチでさらに何ができるのか楽しみになりました
    • 考えてみると、データの出所と真正性を追跡する標準的な方法があったなら、本当に多くの問題が防げて、多くのビジネスルールもより簡単に表現できた気がします
      データが一時的なものなのか、再び記録されるべきものなのかを追跡する方法もあるとよさそうです
      こうした制約を前もって多く記述できるほど良いですね
  • 全体像や活用方法をまだ完全に理解したわけではありませんが、すべてを検査できる Smalltalk 環境を思い出します
    Smalltalk ではすべてがオブジェクトとメッセージなので、さかのぼって追跡し、相互作用できます
  • とてもすばらしい。デモ動画もよく、見慣れないコードベースを掘り下げるときに確実に役立ちそうです
  • 数年前に似たような概念を実験したことがあります[1]。JavaScript のソースマップのようなものを HTML に適用したいと思っていました
    さらに拡張する時間は取れませんでしたが、Web 開発ツールはこの種のフルスタック帰属追跡から大きな恩恵を受けると思います
    ただ、こうした解決策を既存のフレームワークに統合するのは大きな課題に感じます
    [1] HTML Source Maps - https://github.com/connorjclark/html-source-maps https://docs.google.com/document/d/19XYWiPL9h9vA6QcOrGV9Nfkr...
  • 良い意味で、プログラムをデバッグするときに単に「なぜここにないのか?」と尋ねていた Eve-lang のデモを思い出しました。すばらしい仕事です
    https://www.youtube.com/watch?v=TWAMr72VaaU&t=164s そして https://witheve.com/
  • 記憶が正しければ、Java プログラムで SQL インジェクションを動的に見つける似たようなツールについての論文がありました。これは同じツールですか?
    • いいえ、おそらく別のツールだったと思います
      FlowTracker がしていることを拡張すれば、SQL や他のインジェクション脆弱性も見つけられます。なので、思い浮かべているツールが似たアプローチを使っていた可能性はあります
  • かつて、インターネット越しにデータを追跡することを想像していました。たとえば画像がどこから来て、どの CDN にあったのか、といったことです
    あるいは「この文字列は生成された瞬間から自分の画面に届くまでに何を見てきたのか」という質問です
    これはその方向への一歩に見えます
  • このツールを VSCode で、理解しようとしているプロジェクトと一緒に動かしてみようとしているところです
    今は少し中断しなければなりませんが、動くようにしていろいろ触ってみるのが楽しみです