ライブパッチング(kpatch/livepatch)の仕組み
再起動なしでカーネルの脆弱性を塞げる。ftraceで関数を丸ごと差し替え、全タスクが安全点を通った瞬間に切り替える、その整合性の作り方を原理から解説します。
- 1.ライブパッチは関数まるごとを新版に差し替える手法。ftraceのフックで旧関数の入口から新関数へジャンプさせ、再起動なしにバグや脆弱性を修正する。
- 2.整合性の鍵はタスクの安全点。実行中のスレッドがどれも旧関数のコールスタック上にいない瞬間(KLP_PATCHEDへの遷移)を全タスクについて確認してから新版を有効化する。
- 3.差し替え単位は関数なので、データ構造のレイアウト変更やセマンティクスの大改修は原理的に苦手。あくまで小さな緊急修正に向く。
「再起動できない」という制約から逆算する
カーネルに脆弱性が見つかったら、本来は修正版を入れて再起動すれば終わりです。だが数千台のサーバーや、停止コストの高い基幹システムでは、その「再起動」こそが最大の障害になります。パッチ適用のたびにサービスを落とせない——この制約から逆算して生まれたのがライブパッチングです。
発想は単純で、走り続けているカーネルの中で、問題のある関数だけを新しい関数に差し替える。プロセスを止めず、メモリ上の状態を保ったまま、コードの一部分だけをすり替えます。Red Hat の kpatch と SUSE の kGraft が源流で、両者の知見が上流の livepatch(CONFIG_LIVEPATCH)として Linux カーネル本体に統合されました。
ただし「走っているコードを書き換える」は本質的に危うい操作です。差し替えの瞬間に旧関数を実行中のスレッドがいたらどうなるのか。この整合性をどう担保するかが、ライブパッチの技術的な核心になります。
関数リダイレクト:ftraceフックで入口を奪う
ライブパッチが差し替える単位は関数です。命令を1個書き換えるのではなく、対象関数の入口で実行の流れを新版へ丸ごと逸らすのが基本戦略です。これを支えるのが ftrace の仕組みです。
カーネルを -pg(mcount/fentry)付きでビルドすると、各関数の先頭にトレース用のフック呼び出しが埋め込まれます。平時はこの箇所が nop に書き換えられて無効化されていますが、ftrace はここを足場にして任意のハンドラを差し込めます。livepatch はこの ftrace ハンドラを使い、旧関数の入口に来た実行を新関数の先頭へリダイレクトします。
[呼び出し元] ─call→ 旧関数の先頭
│ fentryフック(ftrace)
▼
livepatch ftraceハンドラ
│ 復帰アドレス(ip)を新関数へ書き換え
▼
新関数の本体を実行 ─return→ 呼び出し元へ
ポイントは、旧関数の本体を実行する前にハンドラへ寄り道し、戻り先(命令ポインタ)を新関数の先頭にすり替えることです。これにより呼び出し元から見れば、何事もなかったように新しいコードが走ります。命令列を直接 jmp で潰す素朴な方式と違い、ftrace 経由なので着脱が安全で、同じ箇所を複数の利用者(トレーサとパッチ)が共有する調停も効きます。フックを置く土台は、関数入口の mcount/fentry を足場に実行を奪う ftrace のトレース機構 そのものです。
脆弱性修正は数行の差分でも、コンパイラの最適化でレジスタ割り当てや命令順序は大きく変わります。数命令だけを器用に書き換える方式は、その文脈依存性ゆえに脆い。関数を丸ごと新版に置き換えれば、新版は独立にコンパイル・検証でき、差し替えは「入口を奪う」一点に集約されます。境界が関数という明確な単位に揃うのが利点です。
整合性の難所:旧関数のスタック上にいるタスク
関数の入口を奪うだけなら簡単です。難しいのは、切り替えの瞬間にすでに旧関数の内部を実行中のタスクの扱いです。入口フックはこれから入る呼び出ししか捕まえられません。すでに旧関数の途中まで進み、コールスタックに旧関数のフレームを積んでいるスレッドは、そのまま旧コードを走り続けます。
新旧が混在しても無害なパッチなら問題ありませんが、新旧で前提が食い違う修正——たとえばロック取得順序の変更や、関数間で受け渡すデータの意味の変更——では、旧版で取ったロックを新版の規約で扱う、といった矛盾が起き得ます。だからライブパッチは「新版だけが、または旧版だけが、首尾一貫して使われている」状態を保証しなければなりません。
ここで使われるのが**タスクの安全点(safe point)**という考え方です。
一貫性モデル:全タスクが安全点を通るのを待つ
上流の livepatch が採る整合性確保は、ハイブリッド一貫性モデルと呼ばれます。kpatch のスタック検査と kGraft のタスク単位切り替えを合わせたもので、概略は次の流れです。
- パッチを有効化すると、全タスクに「まだ移行(transition)中」の印を付ける。各タスクは個別に
KLP_UNPATCHEDからKLP_PATCHEDへ遷移する状態を持つ。 - カーネルは各タスクごとに、そのタスクのコールスタックを巻き戻して調べ、パッチ対象の旧関数がスタック上に1つも載っていないことを確認する。載っていなければ、そのタスクを安全に新版側へ切り替える。
- スタック上に旧関数がまだ載っているタスクは、まだ切り替えない。次にカーネルへ入った折り——システムコール復帰時やスケジューラによる切り替え時など、自然な安全点で再判定し、条件が満たされ次第切り替える。
- すべてのタスクが切り替わったら移行は完了。以後は全タスクが新版を使う。
つまり瞬時の全体置換ではなく、タスクごとに安全な瞬間を待って徐々に移すわけです。各タスクの切り替え点は、そのタスクが旧関数のフレームを持たない瞬間に限られるため、「新版で動くタスクと旧版で動くタスクが、それぞれ自分の版で一貫している」状態が保たれます。
カーネル内のループに長く居座るカーネルスレッドや、UNINTERRUPTIBLE で深く待っているタスクは、いつまでも安全点に到達せず移行が完了しないことがあります。この場合パッチは「移行中」のまま宙づりになります。対策として、カーネルスレッドに明示的な譲歩点(klp_update_patch_state を呼ぶ地点)を持たせる、force で強制完了させる(ただし整合性保証を一部放棄する)、といった逃げ道が用意されています。「適用=即完了」とは限らない点が運用上の落とし穴です。
旧関数を実行中のタスクがいなくなるのを待つこの構図は、旧版を読む者が全員いなくなってから古い版を解放する RCU の grace period と発想がよく似ています。「並行して動く読み手が安全に抜けるのを待つ」というパターンの一族です。
適用範囲の制約:なぜ「何でも直せる」わけではないのか
ライブパッチが関数単位の差し替えであるという原理から、苦手分野が論理的に決まります。
| 修正の種類 | ライブパッチ適用 | 理由 |
|---|---|---|
| 関数本体のロジック修正 | 得意 | 新関数に置換すれば完結する |
| 脆弱性の緊急修正(数行) | 得意 | 影響範囲が局所的で安全点も通りやすい |
| 構造体レイアウトの変更 | 原理的に困難 | 稼働中の既存インスタンスを作り直せない |
| 新フィールドの追加 | 困難 | shadow変数等の回避策が要る |
| 関数間セマンティクスの大改修 | 危険 | 新旧の一貫性を安全点で保証しきれない |
| 初期化済みの初期化コード | 対象外 | 起動時に一度走った後は呼ばれない |
最大の壁はデータ構造です。ライブパッチが差し替えるのはコード(関数)であって、すでにメモリ上に存在するデータのインスタンスではありません。構造体にフィールドを足したくても、稼働中の何千個の既存インスタンスを遡って拡張することはできません。この限界に対しては、既存構造体の外に追加データを括り付ける**シャドウ変数(shadow variable)**という仕組みが用意されていますが、パッチ作成者が手で整合性を設計する必要があり、自動ではありません。
ライブパッチはあくまで再起動までの時間を稼ぐ橋渡しです。次の計画停止で正規の修正カーネルへ入れ替えるのが本筋で、パッチを際限なく積み続ける運用は想定されていません。緊急の脆弱性を塞いで安全に再起動の窓を待つ——それがライブパッチの正しい使い方です。
関連技術との位置づけ
ライブパッチは「再起動なしでカーネルを変える」という点で eBPF と並びますが、目的が異なります。eBPF が観測やフック点での振る舞い追加に向くのに対し、ライブパッチは既存関数のバグそのものの修正を狙います。後者は対象が任意のカーネル関数で、しかも挙動を変えてしまうため、前述の一貫性保証が不可欠になります。
なお、差し替えるべき新関数は事前にビルドしてカーネルへ持ち込む必要があり、シンボルの解決には 動的リンク/シンボル解決 と同じく、対象カーネルの正しいシンボルアドレスへ結び付ける作業が伴います。kpatch のツール群は、修正前後のカーネルをビルドして差分のある関数だけを抽出し、それをパッチモジュールへ固める、という流れでこれを自動化します。
核は三段で押さえます。(1) ftrace の fentry フックで旧関数の入口を奪い、戻り先を新関数へすり替える関数リダイレクト。(2) 全タスクのコールスタックを調べ、旧関数を実行中のタスクが居なくなる安全点でタスクごとに切り替える一貫性モデル。(3) 差し替え単位が関数なので、構造体レイアウト変更やデータ移行は原理的に苦手。この三点と、「恒久対策ではなく再起動までの橋渡し」という運用上の位置づけを言えれば十分です。
まとめ
ライブパッチング(kpatch/livepatch)は、稼働中のカーネルで関数を丸ごと新版へ差し替えることで、再起動なしにバグや脆弱性を修正します。差し替えは ftrace のフック機構 を足場に、旧関数の入口で実行を新関数へリダイレクトして行います。整合性の鍵はタスクの安全点で、全タスクのコールスタックを調べ、旧関数を実行中のタスクが居なくなった瞬間にタスクごとへ切り替える——RCU の grace period に似た「安全に抜けるのを待つ」発想です。切り替えの自然な契機はスケジューラによる切り替えやシステムコール復帰。差し替え単位が関数であるがゆえ、データ構造のレイアウト変更には弱く、シャドウ変数で補います。あくまで再起動までの橋渡しとして、緊急の局所修正に使うのが本筋です。
OS Article
ライブパッチング(kpatch/livepatch)の仕組みを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ライブパッチング
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
整合性の鍵はタスクの安全点。実行中のスレッドがどれも旧関数のコールスタック上にいない瞬間(KLP_PATCHEDへの遷移)を全タスクについて確認してから新版を有効化する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ライブパッチング / livepatch」に近いか確認する。
- 強みである「ライブパッチは関数まるごとを新版に差し替える手法。ftraceのフックで旧関数の入口から新関数へジャンプさせ、再起動なしにバグや脆弱性を修正する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。