実践 fault injection framework
実践 fault injection framework
fault injection framework の存在自体は認知していたものの、実際に手を動かして試してみるといったことをしないまま日々を過ごしていたところ、ひょんなことからその利便性を体験することができたので、実際の利用方法や活用できそうなシナリオ、Tips (とよべるかどうかわからない情報) を残しておきます。
なお、一次情報はカーネルのソース・ドキュメントに十分存在するため、網羅性については諦めています。
fault injection framework とは
端的には「実行タイミング」における障害注入フレームワークであり、主に debugfs をインターフェイスとしてカーネルの実行時に注入の有無や条件を設定することが可能です。
対照的には、以下の最も単純な例に示すようにビルドタイミングにて障害注入を行うことも可能ですが、この極めてナイーブな例では条件を変える度
にビルドが必要となるなど、柔軟性に難点があります。
もちろん、何かしらのインターフェイスで操作可能な custom_should_fail() のような関数を用意して注入することも可能だとは思いますが、そこまでしたいなら素直に fault injection framework に乗っかるのが良いでしょう。
int foo() {
...
- if (bar())
+ if (true || bar())
return -ENOMEM;
...
return 0;
}
fault injection framework には、失敗させたい操作ごとに異なるトリガーが用意されています。
- failslab: slab allocation (kmalloc() などの kmem_cache_alloc() を叩くやつ) を失敗させる
- fail_page_alloc: page allocation を失敗させる
- fail_usercopy: ユーザー空間、カーネル空間におけるメモリのコピーを失敗させる
- fail_function: ビルド時に予めマークされている関数の戻り値にエラーを注入する
- そのほか多数
なお、基本的な利用方法のほとんどは fail* で共通であるため、今回はたまたま利用することになった failslab のみに焦点を当てることとします。 網羅的なガイドはConfigure fault-injection capabilities behaviorを参照ください。
failslab
failslab に限らず、fault injection の機能は前述の通り debugfs を通じて操作することとなり、特に failslab の場合には /sys/kernel/debug/failslab/* が操作対象となります。
- failslab に限らない、一般のプロパティ
- probability: slab allocation を失敗させる確率
- interval: 注入を発生させる間隔
- times: 注入する回数
- space: 初めての注入が発生するまでに割当を行うサイズ (slab allocation ごとに都度 allocation のサイズが減算され、これが 0 になるまでは注入が抑止される)
- verbose: 注入発生時の splat の詳細具合
- task-filter: タスクでのフィルタを実施するかどうか
- require-{start,end}: この範囲内のアドレスが stacktrace に出現したときにのみ発火する
- reject-{start,end}: この範囲内のアドレスが stacktrace に出現したときには発火を抑止する
- stacktrace-depth: stacktrace において {require,reject}-{start,end} のアドレスを探索する際、どの深さまで探索するか
- failslab 固有のプロパティ
- cache-filter: 特定の slab cache からの割当のみを対象とする
- ignore-gfp-wait: direct reclaim 可能な状況において注入をスキップするかどうか (捕捉: direct reclaim が可能なコンテキストでメモリの確保が失敗するシナリオは unlikely であるためこのようなオプションがあると想像しています。)
ドキュメントに記載のある failslab の利用例は以下のとおりであり、
- failslab の設定
- 対象の操作の実行
という流れになります。
#!/bin/bash
FAILTYPE=failslab
echo Y > /sys/kernel/debug/$FAILTYPE/task-filter
echo 10 > /sys/kernel/debug/$FAILTYPE/probability
echo 100 > /sys/kernel/debug/$FAILTYPE/interval
echo -1 > /sys/kernel/debug/$FAILTYPE/times
echo 0 > /sys/kernel/debug/$FAILTYPE/space
echo 2 > /sys/kernel/debug/$FAILTYPE/verbose
echo Y > /sys/kernel/debug/$FAILTYPE/ignore-gfp-wait
faulty_system()
{
bash -c "echo 1 > /proc/self/make-it-fail && exec $*"
}
if [ $# -eq 0 ]
then
echo "Usage: $0 modulename [ modulename ... ]"
exit 1
fi
for m in $*
do
echo inserting $m...
faulty_system modprobe $m
echo removing $m...
faulty_system modprobe -r $m
done
実践 failslab
今回障害を注入したいのは igc (Intel Ethernet driver) の probe パスにおける、特に igc_led_setup() の箇所です。
「ソースを書き換えれば (= ビルド時インジェクションすれば) 良いのでは」という指摘はたいへん最もなのですが、それだとカッコ悪い*のでなんとか実行時に注入したいのです。
発生タイミングを自由にコントロールできたほうがいいし、、という言い訳もあります。
* splat に表示されるカーネルバージョンに、dirty とか injection commit のコミットハッシュを載せたくないのです。
static int igc_probe(struct pci_dev *pdev,
const struct pci_device_id *ent)
{
...
err = register_netdev(netdev);
if (err)
goto err_register;
...
if (IS_ENABLED(CONFIG_IGC_LEDS)) {
err = igc_led_setup(adapter); /* <<<=== ここを失敗させたい */
if (err)
goto err_register;
}
return 0;
err_register:
igc_release_hw_control(adapter);
igc_ptp_stop(adapter);
...
err_ioremap:
free_netdev(netdev);
...
return err;
}
igc_led_setup() を見てみると、都合よく kcalloc() (kmalloc の亜種) が呼ばれているので、failslab を差し込めそうですね。
また、(probe コンテキストなので当たり前ではあるものの) GFP フラグが GFP_KERNEL であるため、ignore-gfp-wait には N を設定しなければならないとも想像できます。
なお、私は最初このことに気が付かず、長い間沼にハマっていました。
int igc_led_setup(struct igc_adapter *adapter)
{
struct net_device *netdev = adapter->netdev;
struct igc_led_classdev *leds;
int i, err;
mutex_init(&adapter->led_mutex);
leds = kcalloc(IGC_NUM_LEDS, sizeof(*leds), GFP_KERNEL);
if (!leds)
return -ENOMEM;
...
}
この時点で残っている問題は、どのようにして「igc_led_setup() の kcalloc() が呼ばれるまでは failslab を抑止しつつ、かつ、望むタイミングで failslab を発動させるか」です。
使えそうな道具としては probability, space, task-filter, cache-filter, {require,reject}-{start,end} などが考えられますが、いろいろうまくいかない試行を繰り返した結果、「発火させたいポイントが確定している場合には require-{start,end} がお手軽」だとわかりました。
まず、require-start については /proc/kallsyms でシンボルから引いたアドレスを設定してやればいいです。
そして require-end についても、require-start + 適当なオフセット を書き込んでやれば、(そして運が悪くても適当なオフセットを何度か試せば)、うまくいきます。
もちろん objdump みたいなツールを使って正確にサイズを測るのもいいと思います。
# grep " igc_led_setup" /proc/kallsyms
ffffffffc248c5b0 t igc_led_setup [igc]
実際に failslab を発火させ、期待したパスを実行させたときのセットアップは以下のとおりです。 require-{start,end} に加え、
- ENOMEM は一度出ればいいので times = 1
- 確実にエラーとなってほしいので probability = 100
- GFP_KERNEL でもエラーとなってほしいので ignore-gfp-wait = N
といったセットアップを行い、その後対象となる操作 (probe を引き起こす sysfs の操作) を行っています。 なお、その他のコンテキストでは同一のパスは呼ばれず、不意に times が消費されたりはしないことが確定しているので、task-filter の設定は行っていません。
#!/bin/bash -ex
FAILSLAB_PATH=/sys/kernel/debug/failslab/
DEVICE=0000:00:05.0
# START, END アドレスの取得 (オフセットは適当に 0x100)
START_ADDR=$(grep " igc_led_setup" /proc/kallsyms \
| awk '{printf("0x%s", $1)}')
END_ADDR=$(printf "0x%x" $((START_ADDR + 0x100)))
# 注入設定
echo $START_ADDR > $FAILSLAB_PATH/require-start
echo $END_ADDR > $FAILSLAB_PATH/require-end
echo 1 > $FAILSLAB_PATH/times
echo 100 > $FAILSLAB_PATH/probability
echo N > $FAILSLAB_PATH/ignore-gfp-wait
# igc_probe() 実行
echo $DEVICE > /sys/bus/pci/drivers/igc/bind
結果的に、failslab を用いて unlikely な ENOMEM を強制的に発生させることに成功しました。
また、それによって既存のバグ (net_device の unregister 忘れ) を顕在化させ、無事にカーネルをパニックさせることができました。🎉
splat が出ると嬉しい気持ちになりますよね。
kernel BUG at net/core/dev.c:12047!
Oops: invalid opcode: 0000 [#1] SMP NOPTI
CPU: 0 UID: 0 PID: 937 Comm: repro-igc-led-e Not tainted 6.17.0-rc4-enjuk-tnguy-00865-gc4940196ab02 #64 PREEMPT(voluntary)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
RIP: 0010:free_netdev+0x278/0x2b0
[...]
Call Trace:
<TASK>
igc_probe+0x370/0x910
local_pci_probe+0x3a/0x80
pci_device_probe+0xd1/0x200
[...]
なお、上記のバグはこちらのパッチで修正済みです。
failslab をはじめとした fault injection framework はカーネルのデバッグにも便利なので、ぜひ日々のカーネルハックにご活用ください。