以下は、2016年5月時点の Linux カーネル文書、Documentation/livepatch/livepatch.txt の kanda.motohiro@gmail.com による全訳です。原文と同じ、GPL v2 で公開します。
===========
ライブパッチ
===========
この文書は、カーネルライブパッチに関する基本的情報を概説します。
目次
1. 動機
2. Kprobes, Ftrace, ライブパッチ
3. 一貫性モデル
4. ライブパッチモジュール
4.1 新しい関数
4.2 メタデータ
4.3 ライブパッチモジュールの扱い
5. ライブパッチのライフサイクル
5.1 登録
5.2 有効化
5.3 無効化
5.4 登録解除
6 Sysfs
7 制限
1.動機
======
ユーザがシステムをリブートしたがらない状況がたくさんあります。そのシステムが複雑な科学計算をしているからかもしれませんし、繁忙期に高負荷の元にあるからかもしれません。システムを実行中のままとすることに加えて、ユーザは同時に安定しセキュアなシステムを欲しがります。ライブパッチは、関数呼び出しをリダイレクト可能とすることでユーザに両方を与えます。そして、システムリブートなしで、致命的な関数を修正できます。
2. Kprobes, Ftrace, ライブパッチ
================================
Linuxカーネルには、コード実行をリダイレクトすることに直接関連する機構が複数あります。それは、カーネルプローブ、関数トレース、そしてライブパッチです。
+ カーネルプローブは最も一般的です。任意の命令の代わりにブレークポイント命令を置くことでコードをリダイレクトできます。
+ 関数トレースは、関数のエントリポイントに近いところにあるあらかじめ決められた場所からコードを呼びます。この場所は、'-pg' gcc オプションによって、コンパイラが生成します。
+ ライブパッチは典型的には、関数引数やスタックがいかなる方法によっても変更される前の、関数エントリの一番初めにおいてコードをリダイレクトする必要があります。
三つのアプローチは全て既存コードを実行時に変更する必要があります。なので、それらはお互いの存在に気がついている必要があり、お互いの足を踏まない必要があります。これらの問題のほとんどは、ダイナミック ftrace フレームワークを基礎とすることで解決します。kprobe は、関数エントリがプローブされるときに、ftrace ハンドラとして登録されます。CONFIG_KPROBES_ON_FTRACE を参照下さい。同様に、ライブパッチの代替関数はカスタム ftrace ハンドラの助けを借りて呼ばれます。ただし、制限がいくつかあります。以下を参照。
3. 一貫性モデル
====================
関数は理由があるからそこにあります。入力引数があり、ロックを取ったり放したりし、あるデータを決められた方法で読み、処理し、時には書くこともあり、戻り値を持つでしょう。言葉を代えて言えば、それぞれの関数は定義されたセマンティックスを持ちます。
多くの修正は、変更される関数のセマンティックスを変えません。例えば、それは NULLポインタや境界チェックを追加します。欠けているメモリバリアを追加することで競合をなおします。あるいは、クリティカルセクションのまわりに何らかのロックを追加します。これらの変更のほとんどは自己完結しており、関数はシステムの残りの部分に対して、自分を同じ方法で提示します。この場合、関数は独立して一つづつ更新することができるでしょう。
しかし、より複雑な修正もあります。例えば、あるパッチは、同時に複数の関数で、ロックの順序を変えるかもしれません。または、あるパッチはある一時的な構造体達の意味を交換し、全ての関連する関数を更新するかもしれません。この場合、影響のある単位(スレッド、カーネル全体)は、その関数全ての新しいバージョンを同時に使い始める必要があります。さらに、交換は、そうすることが安全な時にだけ起きなくてはいけません。例えば、影響のあるロックが放されている時、あるいはその時に、変更された構造体にデータが何も格納されていない時。
どのように関数に安全に適用するかという理論はかなり複雑です。目的は、いわゆる一貫性モデルを定義することです。それは、システムが一貫したままで、新らしい実装を使うことができる条件を定義しようと試みます。理論はまだ終わっていません。以下の議論を参照下さい。
http://thread.gmane.org/gmane.linux.kernel/1823033/focus=1828189
現在の一貫性モデルはとても単純です。それは、古い方か新しい方か、どちらかの関数が呼ばれることを保証します。しかし、いろいろな関数は何の同期もなしに、一つづつリダイレクトされます。
言葉を代えて言えば、現在の実装は、「決して」呼び出しの途中で振る舞いを変えません。その理由は、それがメモリ上の関数全体を書き換えるからでは「ない」からです。そうでなく、関数は一番最初でリダイレクトされます。しかしこのリダイレクションは、同じパッチからの他の何らかの関数がまだリダイレクトされていないときにも直ちに使われます。
以下の「制限」の節も参照下さい。
4.ライブパッチモジュール
===================
ライブパッチはカーネルモジュールを使って配布されます。以下を参照下さい。
samples/livepatch/livepatch-sample.c
モジュールは、私達が置換したい関数の新しい実装を含みます。それに加えて、それは元と新しい実装の関係を記述したある構造体を定義します。そして、ライブパッチモジュールがロードされた時に、カーネルに新しいコードを開始させるためのコードがあります。また、ライブパッチモジュールが削除される前に後始末をするコードもあります。次の節でこれら全てをより詳しく説明します。
4.1 新しい関数
------------------
関数の新しいバージョンは、典型的には元のソースから単にコピーされます。良い習慣は、名前にプレフィックスをつけることで、元のものと区別がつくようにすることです。例えば、バックトレースにおいて。また、それらは、static と宣言されても良いです。なぜならば、それらは直接呼ばれることはないので、グローバルに見える必要はないからです。
パッチは、実際に変更される関数だけを含みます。しかしそれは、元のソースファイルの中で、ローカルにしかアクセスできない、関数やデータをアクセスしたいことがあるかもしれません。これは、生成されたライブパッチモジュールにある特別のリロケーションセクションによって解決されます。詳しくは、
Documentation/livepatch/module-elf-format.txt を参照下さい。
4.2 メタデータ
------------
パッチはいくつかの構造体で説明され、それは情報を三つのレベルに分割します。
+ パッチされるそれぞれの関数ごとに、klp_func 構造体 が定義されます。それは特定の関数の元と新しい関数の関係を記述します。
この構造体は、元の関数の名前を文字列として含みます。その関数アドレスは、実行時に kallsyms によって見つけられます。
次に、それは新しい関数のアドレスを含みます。それは、関数ポインタを代入することで直接定義されます。新しい関数は典型的には同じソースファイルに定義されることに注意下さい。オプショナルなパラメタとして、同じ名前を持つ関数の曖昧さを無くすために、kallsyms データベース内のシンボル位置を使うこともできます。これはデータベース内の絶対位置ではなく、特定のオブジェクト( vmlinux あるいはカーネルモジュール )でそれが見つかった順序です。kallsyms は、シンボルをオブジェクト名によって探すことができるのに注意下さい。
+ klp_object構造体は、その同じオブジェクト内のパッチされる関数の配列を定義します。ここでオブジェクトは、vmlinux (NULL) か、モジュール名です。
この構造体は、それぞれのオブジェクトについて関数をグループし、一緒に扱う助けをします。パッチされるモジュールは、パッチ自身よりも後でロードされることもあるのに注意下さい。関連する関数は、それが使用可能になったときにだけパッチされます。
+ klp_patch 構造体は、パッチされるオブジェクト(struct klp_object)の配列を定義します。
この構造体は、全てのパッチされる関数を、一貫して、そして最終的に同期して、扱います。パッチ全体は、パッチされるシンボルが全て見つかったときにだけ適用されます。唯一の例外は、まだロードされていないオブジェクト(カーネルモジュール)からのシンボルです。または、もしより複雑な一貫性モデルがサポートされた時には、限られたユニット(スレッド、カーネル全体)は、パッチ全体からの新しいコードを、それが安全な状態にある時にだけ見るでしょう。
4.3 ライブパッチモジュールの扱い
------------------------------
通常の振る舞いは、そのライブパッチモジュールがロードされた時にその新しい関数が使われるようになることです。そのために、module init() 関数は、そのパッチ(struct klp_patch)を登録して、有効にします。この二つの操作についてより詳しくは、以下の「ライブパッチのライフサイクル」の節を参照下さい。
モジュール削除は、関連する関数の使用者がない時に限り安全です。immediate 一貫性モデルはこれを検出できないため、ライブパッチモジュールは削除できません。以下の「制限」を見てください。
5. ライブパッチのライフサイクル
=======================
ライブパッチを行うことは四つの基本的操作を定義します。それが、ライブパッチ登録、有効化、無効化、そして登録解除というライフサイクルを定義します。これがこのように行われるのには、いくつかの理由があります。
最初に、パッチは、既にロードされているオブジェクトの、パッチされる全てのシンボルが、見つかっている時に限り適用されます。ある関数がリダイレクトされる前にこの判定をする方が、エラーの扱いがずっと簡単です。
二つ目に、immediate 一貫性モデルは、パッチが取り消された時に、新しいコードの中で眠っている人が誰も居ないことは保証しません。これは、新しいコードは「永遠に」残り続ける必要が有ることを意味します。もしコードがあるならば、パッチはもう一度適用できます。なので、一度だけ行われる操作と、パッチが再度有効になる(適用される)時に繰り返される必要のある操作を分割するのは意味の有ることです。
三つ目に、より複雑な一貫性モデルが使われた時に、システム全体が移行するにはある程度の時間がかかるかもしれません。パッチ取り消しは、ライブパッチモジュールの削除を長過ぎる時間、ブロックするかもしれません。なので、明示的に呼ぶことのできる個別の操作を使ってパッチを取り消すのは便利なことです。しかし、ライブパッチモジュールが実際に削除されるまでは、全ての情報を削除するのは意味のないことです。
5.1 登録
-----------------
パッチのそれぞれは、最初に klp_register_patch() を使って登録されなくてはいけません。これによって、そのパッチは、ライブパッチフレームワークに知られます。同時に、準備的な計算とチェックもいくらか行います。
特に、そのパッチは、既知のパッチのリストに加えられます。パッチされる関数のアドレスが、その名前に従って見つけられます。「新しい関数」の節で述べた特別のリロケーションが適用されます。/sys/kernel/livepatch/<name> の下に、対応するエントリが作成されます。操作のどれかが失敗したら、パッチは拒絶されます。
5.2 有効化
-------------
登録されたパッチは、klp_enable_patch() を呼ぶか、'1' を /sys/kernel/livepatch/<name>/enabled に書くことで有効化できます。システムは、この時点で、パッチされる関数の新しい実装を使い始めます。
特に、もし元の関数が初めてパッチされる時には、関数ごとの klp_ops 構造体が作られ、ユニバーサル ftrace ハンドラが登録されます。
関数は複数回パッチされることがあります。ある関数に対しては、ftrace ハンドラは一度だけ登録されます。以降のパッチは、klp_ops 構造体のリスト(`func_stack` を参照下さい)にエントリを加えるだけです。最後に加えられたエントリが、ftrace によって選択され、アクティブな関数置換になります。
パッチは、登録された順序とは異なる順序で有効化されることがあるのに注意下さい。
5.3 無効化
-------------
有効化されたパッチは、klp_disable_patch() を呼ぶか、'0' を /sys/kernel/livepatch/<name>/enabled に書くことで無効化されることがあります。この時点で、以前に有効化されたパッチからのコードもしくは、元のコードのどちらかが使われます。
この時、無効化されるパッチに関連する全ての関数(klp_func 構造体)は対応する klp_ops 構造体から削除されます。func_stack リストが空になったら、ftrace ハンドラは登録解除され、klp_ops 構造体は解放されます。
パッチは、有効化された時と正確に逆の順序で無効化されなくてはいけません。そうすることで、問題と実装がずっと簡単になります。
5.4 登録解除
-------------
無効化されたパッチは、klp_unregister_patch() を呼ぶことで登録解除することができます。これは、そのパッチが無効化され、そのコードがもう使われなくなった時にだけ、行うことができます。それは、ライブパッチモジュールがアンロードされる前に呼ばなくてはいけません。
この時点で、全ての関連する sys-fs エントリは削除され、そのパッチは、既知のパッチのリストから削除されます。
6 Sysfs
========
登録されたパッチについての情報は、/sys/kernel/livepatch の下にあります。パッチは、そこに書くことで、有効、無効にできます。
詳しくは、Documentation/ABI/testing/sysfs-kernel-livepatch を参照下さい。
7 制限
==============
現在のライブパッチの実装はいくつかの制限があります。
+ パッチはパッチされる関数のセマンティックを変えてはいけません。
現在の実装は、古いもしくは新しい関数のどちらかが呼ばれることだけを保証します。関数は一つづつパッチされます。それは、パッチがその関数のセマンティックを変えては「いけない」ことを意味します。
+ データ構造はパッチできません。
データ構造をバージョン化したり、どのような手段にせよ、ある構造体から他のに移行することはサポートされません。
より複雑な一貫性モデルができたら、なんらかの回避策ができるでしょう。例えば、データ構造はアラインされているので、新しいメンバのためにホールを使うことができるでしょう。あるいは、既存のメンバを他のことに使うこともできるでしょう。
今のところ、変更された構造体のより一般的なサポートを追加する計画はありません。
+ トレースできる関数しかパッチできません。
ライブパッチはダイナミック ftrace を基礎とします。特に、ftrace あるいはライブパッチ ftrace ハンドラを実装する関数はパッチできません。さもないと、コードは無限ループになるでしょう。潜在的な誤りは、問題となる関数を "notrace" と宣言することで避けられます。
+ __schedule() にインラインされるものは全てパッチできません。
switch_to マクロは、__schedule() にインラインされます。それはマクロの途中で、二つのプロセスの間でコンテキストを移ります。それは、x86_64 バージョンでは、RIP を退避しません(32ビットバージョンとは違って)。その代わり、現在使われている __schedule()/switch_to() が両方のプロセスを扱います。
さて、二つの異なるタスクがあるとします。一つは元の __schedule() を呼びます。そのレジスタは定められた順序で格納され、それは switch_to マクロでスリープに入り、どれか他のタスクが、元の __schedule() を使って回復されます。次に、二つ目のタスクが、patched__schedule() を呼びます。それはそこでスリープに入り、最初のタスクが、patched__schedule() によりピックアップされます。その RSP が回復され、ここでレジスタも回復されるはずです。しかし、新しい patched__schedule() では順序が異なっています。なので。。。
この制限を除くための作業が進行中です。
+ ライブパッチモジュールは削除できません。
現在の実装は、関数を一番最初にリダイレクトするだけです。その関数が使われているのかどうかを判定しません。言葉を代えて言えば、それは関数が呼ばれた時は知っていますが、その関数が戻る時は知りません。なので、ライブパッチモジュールが安全に削除できる時を決定できません。
これは、より複雑な一貫性モデルがサポートされたらきっと解決するでしょう。アイデアは、パッチに安全な状態は、そのパッチを除くためにも安全な状態を意味するはずだということです。
パッチ自身は、/sys/kernel/livepatch/<patch>/enabled にゼロを書くことで無効にできることに注意下さい。それは、新しいコードはもう呼ばれなくなるようにします。しかし、誰かが新しいコードのどこかで眠っていることがないのは保証しません。
+ ライブパッチはダイナミック ftrace がその関数の一番最初に位置するときにだけ、安定して動きます。
その関数は、スタックや関数引数がいかなる方法でも変更される前にリダイレクトされる必要があります。例えば、ライブパッチは、x86_64 では、-fentry gcc コンパイラオプションを使うことが必要です。
一つの例外は、PPC ポートです。それは、相対アドレッシングと TOC を使います。それぞれの関数は、ftrace ハンドラを呼ぶことができるようになる前に、TOC を処理して、LR を退避しなくてはいけません。戻る時には、この操作は元に戻されなくてはいけません。幸運にも、一般的な ftrace コードは同じ問題を持ち、この全ては ftrace レベルで扱われます。
+ ftrace フレームワークを使う kretprobes はパッチされる関数と共存できません。
kretprobes もライブパッチも、戻りアドレスを変更する ftrace ハンドラを使います。最初の使用者が勝ちます。ハンドラが既に他のものによって使われている時には、プローブもしくはパッチは拒絶されます。
+ 元の関数の kprobes はそのコードが新しい実装にリダイレクトされる時には無視されます。
この状況で警告を加えるための作業が進行中です。
以上