ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Vivariumとは何か〜Rubyのための次世代セキュリティと可観測性ツール〜

Vivariumというツールを作っている。前の記事で少しだけ紹介した。

udzura.hatenablog.jp

ツナギメオフライン ベンキョウカイ #8 でも開発中のものを紹介した。

udzura.jp

本記事ではVivariumというツール自体についてと、今実現できている機能を紹介する。

まず意味と読み方

「小さな生き物が暮らす自然環境を、ガラスケースなどの空間内に再現した生態系」ってGeminiが言っていた。生き物飼育系の方はそういう環境を普通に「ビバリウム」と呼んでいるそうで、Vivariumもビバリウムと読んでよさそう。

なお英語に寄せるなら「ヴァイヴァリアム」である。

できること

発表にもある通り、昨今のサプライチェーンアタックやその他各種攻撃に対して何かしら対策できないかな、と思って旅をしていたんだ。その途中でeBPFを知ったんだよ。いやもともと知っていたけど、思い出した。

github.com

2020年ごろにlibbccのbindingであるRbBCCを作った際には、たとえばBPF LSMのような機能は出たばかりで世で使われているカーネルで利用するのは難しかった記憶がある。あれから6年経って、その辺りの機能も普及してきたところで、改めて昔の思いつきを実装し直してみた。

Vivariumを使うと、Rubyプログラムの以下のようなイベントを観察することができる:

  • ファイル操作
  • プロセス実行
  • 共有ライブラリのロード
  • ネットワーク接続
  • DNS クエリ
  • SSL/TLS 通信
  • 環境変数の操作
  • 権限・kill・セキュリティ操作...
  • 観察対象とした Ruby メソッドの呼び出し

また、 Kernel#system のようなメソッドで起動した外部のプログラムの挙動もそのまま観察することができる。たとえば curl コマンドを打てば名前解決やソケット作成、TLS通信のログがそのまま観察できる。

断っておくと、一部のイベントはユーザランドの操作なので、バイパスの方法はどうしても存在するだろう。DNS クエリの監視一つ取っても、今はUDP/53しか監視しておらず、回避策はいくつか考えられそう*1。ただ、Vivariumとしてはある程度幅広くイベントをトレースすることで、そのプログラムがどういう挙動をしているか浮き出させることを目的としている。一種のスイスチーズモデルのようなものだ。したがって、一部のイベントをなんとかバイパスしたところで、攻撃をしたこと自体を完全に隠蔽するのは難しくなるよう期待している。


Vivariumは以下のような用途を考えている。

デバッグ

複雑な既存コードを読み取るより、そもそも起こったイベントを追いかけた方がデバッグしやすいパターンはいくらでもあると思われる。まず思いつく用途だと思う。

CIなどによる動的解析

最初に述べたように、安全な環境を用意した上で、ライブラリをインストールし、その挙動を外から観察するという用途も考えられる。挙動の中で、見覚えのないドメインへのDNSクエリ、HTTPS通信の発生、妙なシステムコール操作などの怪しい兆候を確認したら、コードベース含みより詳細に見ていくこともできるだろう。

また、Vivariumが吐き出すログは全イベントを含むとそれなりに大きいが、とはいえ、Skillなどを作成してAIに読み取らせるという運用もできそう。

Runtime Security

さらに、近年のeBPFの機能は、カーネルイベントに対し読み取りだけでなく以下のような副作用を発生させることができる。

  • SIGKILL等のシグナルを強制的に発生させる
  • 当該システムコールの返却値を変える
  • LSM hookの場合、当該操作をそもそも禁止する

したがっていわゆるRuntime Securityの分野、実行中のプログラムにそもそも禁止アクションを取らせないように守る用途にも使えるかもしれない。

Rubyのイベント含め観察するという性質上オーバヘッドが無視しづらいのが難点ではある。一定の性能劣化が許容できる用途で使うか、あるいはRubyレベルのイベントは取得せず、カーネルレベルのイベントだけ追うようにすればオーバヘッドが軽減されるので、実用に足るかもしれない。

アーキテクチャ

発表で掲示した図を引用する。

全体図(一部ざっくり省略しているが)

基本的に

  • まず vivariumd というデーモンを立てる
  • 観察対象のクライアントから vivariumd のUNIXドメインソケット経由でPIDを登録する
  • その登録したプロセスのイベントがUNIXドメインソケット経由でストリームされる

という感じで使う。

図ではRuby、(ユーザランド)ライブラリ、システムコール等がフラットに送られているように描いているが、ここで言う「Rubyイベント」の送り方が技ポイントである。具体的にはRubyのTracePointで送っていて、こういう感じで送っている:

TracePoint.new(:call, :c_call, :return, :c_return) do |tp|
  file_arg = fit_string(tp.path, SPAN_FILE_ARG_MAX)
  case tp.event
  when :call, :c_call
    Vivarium::Usdt.start(tp.defined_class.to_s, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
  when :return, :c_return
    Vivarium::Usdt.stop(tp.defined_class.to_s, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
  end
end

vivarium_usdt というgemを別途用意しており、メソッドの呼び出しと戻りをUSDTで送りつけている。

USDT(User Statically-Defined Tracing)とはユーザー空間のアプリケーションの特定のアドレスにあらかじめマーカーを埋め込み、カーネルからその実行をトレース可能にする技術。ちなみにユーザランドのライブラリ関数の開始位置をマーカーにするのがuprobe。

Ruby自体にもUSDTはある(DTrace probeがそのままUSDTになる)が、ビルドオプションで有効無効が決まる上に、ほぼ有効になっていない。今回は自分でUSDT入りの拡張ライブラリを用意して require して使っている。

こうすることで、Rubyイベント(USDT)、ユーザランドのライブラリイベント(uprobe)、システムイベント(kfunc/tracepoint/LSM hook)が同等の粒度でeBPF内部で取得でき、受け取った時のktimeで並べることで簡単に正しい順序で観察できる。また、TracePointの操作は比較的重いとはいえ、内部の処理としてはUSDTをキックするだけにして、オーバヘッドをなるべく小さくしている。

余談

VivariumはRuby特化ではあるが、USDTを経由して低コストでユーザランドのイベントを送りつけるというアイデア自体は、どういう言語でもできると思う。Pythonなら sys.settrace() などが使える模様。

ちなみにVivariumではRustで拡張を書いて probe というcrateを用いている。当該コード

制限事項など

以下あたりは本ツールの制限事項になる。

  • そもそも、Linuxでないと動かない
  • 機能のうち多くを BPF LSM に依存しているが、BPF LSMが最初から有効になっているディストリビューションはまだ多くない
    • configでは有効になってることが多いので、その場合起動パラメータを変えれば使える。

ちなみに中央の vivariumd の起動には特権が必要だが、観察対象のプログラムは一般ユーザでも問題ない。


結び

VivariumのアイデアはまさにRubyKaigi 2026でRuby::Boxのトークを聴いて手を動かし始めた。

最初、特定のBoxの挙動を追いかけるものを作ろうとしていて、実際プロトタイプはできたのだが、作っているうちにもっと色々使えるなと思って今に至る。

6年前に面白いかな〜と思って調べた eBPF や RbBCC などの技術が、今新しく形になって、さらに、それが「AIなら使えるっしょ!」みたいな設計のツールなのは、自分のこと*2だが面白い。

ちなみに似たような目的のツールでは、たとえば kntrl と言うものがあるそう。これはこれで気になるので試したい。

kntrl.dev

Vivariumに関して、他にもこう言うツールがあるよとか、使ってみたけどここはこうした方がいいとか、色々意見が欲しいかもしれない。

*1:無論漏れにくいようにカバー範囲は広げるが

*2:AIがかなりの部分書いたわけで、自分で考えたツールだけど、自分で書いたツールなのか?という話もある