ビルド成功、警告なし実行、テストケースのSKIPは、それぞれ異なる結果を意味します。カーネル検証では、入力、実行パス、環境前提、結果を1つの記録に収める必要があります。
| 参照目的 | 参照箇所 |
| 実行パスの特定 | ログとDynamic Debug |
| 境界条件の検証 | KUnit、kselftest、sanitizer、ファジーテスト |
| 失敗分岐と納品証拠 | 障害注入、再現記録 |
printk とコンソールレベル
pr_err、pr_info、pr_debug などはログのエントリポイントを提供し、dev_* 形式はデバイスコンテキストを伴います。ログがカーネルバッファに記録される段階と、最終的にコンソールに出力される段階は異なります。console_loglevel はコンソール出力をフィルタリングするだけで、低いレベルの記録をすべて削除するわけではありません。
ログレベルの数値が小さいほど重大です。console_loglevel=4 は通常、数値が4より小さいメッセージ、つまり error およびそれより重大なレベルを出力します。この設定を「システムにはこれらのログしか存在しない」と記述すべきではありません。高頻度パスのログはタイミングとパフォーマンスを変更するため、レート制限を行うか、適切なトレースポイントに切り替える必要があります。
Dynamic Debug で既存の呼び出しポイントを制御
dynamic debug は、対応する呼び出しポイントがサポートされ、コンパイルされている場合に限り、モジュール、関数、ファイル、行番号、またはその他の条件に基づいて pr_debug/dev_dbg などの情報を有効にします。任意のデバッグ文がない関数に、ありもしないエントリログを追加することはできません。
現在のドキュメントでは /proc/dynamic_debug/control を提供しています。debugfs が有効化され、マウントされている場合は /sys/kernel/debug/dynamic_debug/control も存在する可能性があります。実際存在する制御ファイルを読み取り、エントリと元のフラグを確認してから、限定的な範囲で変更を行ってください。Dynamic debug
例えば、コマンド文字列 module my_driver +p は、対応するモジュールエントリを有効にすることを意味します。-p は印刷を無効にし、f/l/t などのフラグは関数、行番号、スレッド情報を追加します。復元時は以前の状態に戻し、他者が有効にしたデバッグポイントを無条件に消去しないでください。
KUnit と kselftest は異なる境界をチェックする
KUnit はカーネル環境内で内部関数またはサブシステムのロジックを検証し、UML、QEMU、またはテスト用カーネルで実行できます。kselftest は通常、ユーザー空間からカーネルインターフェースと動作をチェックし、一部のスイートはテストモジュールと連携します。これらは補完関係にあります。KUnit 入門、kselftest
KUnit には KUNIT_CASE 配列だけでなく、スイートの登録と Kconfig/Kbuild での接続も必要です。以下に、バッファの範囲チェックを例として挙げます。容量が capacity で、アクセス範囲が [offset, offset+length) の場合、長さが0で offset がちょうど capacity に等しいことは許可されますが、範囲外アクセスは禁止されます。offset + length <= capacity を直接判断すると、符号なし加算のラップにより、不正な範囲が合法と誤認される可能性があります。
kb_range_test.c として保存:
#include <kunit/test.h>
#include <linux/module.h>
#include <linux/limits.h>
static bool range_fits(size_t capacity, size_t offset, size_t length)
{
return offset <= capacity && length <= capacity - offset;
}
static void range_boundary_test(struct kunit *test)
{
KUNIT_EXPECT_TRUE(test, range_fits(4096, 4092, 4));
KUNIT_EXPECT_FALSE(test, range_fits(4096, 4092, 5));
KUNIT_EXPECT_TRUE(test, range_fits(4096, 4096, 0));
KUNIT_EXPECT_FALSE(test, range_fits(4096, 4097, 0));
KUNIT_EXPECT_TRUE(test, range_fits(0, 0, 0));
}
static void range_overflow_test(struct kunit *test)
{
KUNIT_EXPECT_FALSE(test, range_fits(4096, SIZE_MAX - 3, 4));
KUNIT_EXPECT_TRUE(test, range_fits(SIZE_MAX, SIZE_MAX, 0));
KUNIT_EXPECT_FALSE(test, range_fits(SIZE_MAX, SIZE_MAX, 1));
}
static struct kunit_case range_cases[] = {
KUNIT_CASE(range_boundary_test),
KUNIT_CASE(range_overflow_test),
{}
};
static struct kunit_suite range_suite = {
.name = "kb-range",
.test_cases = range_cases,
};
kunit_test_suite(range_suite);
MODULE_LICENSE("GPL");
第一グループは排他的上限と空の範囲の契約をチェックし、第二グループは最大整数付近での加算リスクをチェックします。offset <= capacity は、後の減算がアンダーフローしないことも保証します。C言語の && のショートサーキット評価により、2番目の項はこの前提が成立した場合のみ計算されます。このヘルパー関数は数値範囲のみを検証し、ポインタの有効性、ページへのアクセス可能性、またはデバイスがDMAを実行する権限を持っているかどうかは保証しません。
テストソースディレクトリに接続する際、対応する Kconfig エントリと Kbuild ルールを追加し、上位の設定とビルドでこのディレクトリを含めてください:
config KB_RANGE_KUNIT_TEST
tristate "Range boundary tests" if !KUNIT_ALL_TESTS
depends on KUNIT
default KUNIT_ALL_TESTS
obj-$(CONFIG_KB_RANGE_KUNIT_TEST) += kb_range_test.o
このテストが接続されたクリーンなソースツリーでは、独立したビルドディレクトリで .kunitconfig を使用して CONFIG_KUNIT=y と CONFIG_KB_RANGE_KUNIT_TEST=y を有効にし、スイート名でフィルタリングできます。フィルタはビルドに含まれているテストのみを選択し、設定の有効化に代わるものではありません。
./tools/testing/kunit/kunit.py run --build_dir=.kunit-range --kunitconfig=.kunitconfig kb-range
デフォルトの UML パスは、現在のホストにテストモジュールをロードするのではなく、ユーザー空間Linuxテストカーネルをビルドして実行します。ソースツリーに互換性のないビルド成果物が既に存在する場合は、別途準備したクリーンなソースコピーを使用してください。チュートリアルコマンドのために、メンテナンス中のカーネルツリーで直接 mrproper を実行しないでください。KUnit 設定、ビルド、フィルタリング
組み込みテストとモジュールテストの実行タイミングは異なります。kunit.py はテストの設定、ビルド、実行を支援できますが、初回実行には完全なテストカーネルのビルドが必要になる場合があります。単にCファイルをコンパイルしただけでは、テストが実行されたことにはなりません。
結果の解釈:SKIP と PASS を別々に記録
selftests の TARGETS はスイートをフィルタリングできますが、ネットワーク、BPF などのテストには対応するカーネル設定、権限、依存関係が必要であり、テスト環境を変更する可能性があります。変更に関連するスイートを優先して実行し、SKIP の理由を記録してください。「すべてスキップ」は「すべてパス」ではありません。
sanitizer、ロックチェック、ブレークポイントはそれぞれ証拠を提供する
KASAN は適用可能な範囲外アクセスと解放後使用(use-after-free)に焦点を当て、KCSAN はデータ競合の発見を支援し、lockdep はロック依存関係などの制約をチェックします。これらは設定、実行パス、サンプリングメカニズムの影響を受けるため、問題が報告されなかったことをエラーがないことの証明として扱うことはできません。
KGDB は、適切な設定とデバッグリンクの下で GDB を使用してカーネルをデバッグすることを許可します。SysRq-g はターゲットカーネルを停止させてデバッガを待機させる可能性があり、回復手段があるテスト環境に適しています。日常の読み取り専用診断コマンドとして直接実行すべきではありません。
ブレークポイントとログはスケジューリングのタイミングを変更します。再現が難しい競合状態は、トレースポイント、最小入力、繰り返しトリガーと組み合わせて、発生順序を保存し、最後のスタックのみを保持しないでください。
syzkaller と LTP
syzkaller はシステムコール記述、リソース関係、カバレッジフィードバックを使用してプログラムを生成および変異させ、設定されたターゲット環境でクラッシュ、ハングなどの異常を検出し、最小化と再現を試みます。QEMU は一般的な実行バックエンドですが、唯一ではありません。自動バイセクトにも対応するインフラストラクチャが必要であり、任意の実行をすべてバイセクトを含むとは限りません。syzkaller
LTP はシステムインターフェースなどの回帰テストコレクションを提供し、カーネルとlibcの相互作用の検証を補完するのに適しています。テストケースの失敗は、カーネル、環境、権限、またはテスト前提に起因する可能性があるため、総失敗数だけを報告するのではなく、具体的な結果とバージョンを保持してください。LTP
障害注入:制御不能な失敗を検証可能な入力に変える
メモリ割り当て、I/O、関数戻り値などの注入ポイントは、クリーンアップ、リトライ、ロールバックパスをチェックするのに役立ちます。フレームワークの probability、interval、times、task-filter およびフィルタ条件は、トリガーに共同で影響します。failslab/interval を単に 100 に設定しても、任意の割り当ての100回ごとに必ず1回失敗するとは限りません。障害注入
| 段階 | 確認すべき事実 |
| 注入前 | ターゲットタスク/パスが限定され、ベースライン動作が明確 |
| 注入中 | 予期された失敗が実際に発生し、クリーンアップとロールバックが検証可能 |
| 注入後 | リソースが解放され、関連パラメータが復元 |
理論的な例では、現在のホストでグローバル注入を有効にする必要はありません。テストVMの方が再現と回復が容易です。
取得順序に従って1つずつ失敗し、所有しているリソースのみを解放するか確認
初期化が context、buffer、registration を順に取得し、すべて成功してからオブジェクトを呼び出し元に公開すると仮定します。最初の割り当てのみを1回失敗させるのではなく、各ステップでの失敗を個別にテストする必要があります。
| 失敗位置 | 失敗前に所有 | ロールバック順序 |
| context | なし | 取得していないリソースを解放しない |
| buffer | context | context |
| registration | context、buffer | buffer → context |
| すべて成功して終了 | 3つのリソース、かつオブジェクトが公開済み | 外部公開を停止してから、リソース依存関係に従ってクリーンアップ |
以下は決定論的なローカルライフサイクルモデルです。fail_at 回目の取得がリソースの確立前に失敗し、成功したオブジェクトの所有権は呼び出し元に渡されます。終了時は、公開を取消してからリソースを解放します。ホストの failslab を有効にしたり、実際のデバイスの非同期コールバックをシミュレートしたりしません。
class Resources:
def __init__(self, fail_at=None):
self.fail_at = fail_at
self.attempts = 0
self.live = set()
self.freed = []
self.published = None
def acquire(self, name):
self.attempts += 1
if self.attempts == self.fail_at:
raise MemoryError(name)
assert name not in self.live
self.live.add(name)
return name
def release(self, name):
assert self.published is None and name in self.live
self.live.remove(name)
self.freed.append(name)
def create(self):
acquired = []
try:
for name in ("context", "buffer", "registration"):
acquired.append(self.acquire(name))
except MemoryError:
for name in reversed(acquired):
self.release(name)
raise
self.published = tuple(acquired)
return self.published
def destroy(self, owned):
assert self.published is owned
self.published = None
for name in reversed(owned):
self.release(name)
for failure, expected in [(1, []), (2, ["context"]), (3, ["buffer", "context"])]:
resources = Resources(failure)
try:
resources.create()
except MemoryError:
pass
else:
raise AssertionError("目標失敗未发生")
assert resources.attempts == failure
assert not resources.live and resources.published is None
assert resources.freed == expected
resources = Resources()
owned = resources.create()
assert resources.live == set(owned) and not resources.freed
resources.destroy(owned)
assert not resources.live and resources.published is None
assert resources.freed == ["registration", "buffer", "context"]
ここでは、関数の戻り値の失敗だけでなく、目標注入ポイントに実際に到達したか、後続の取得が継続していないか、リソースがリークまたは重複解放されていないか、未完成のオブジェクトが外部に可視でないかもチェックします。これらがエラーパスの検証可能な不変条件です。
実際のドライバの登録操作では、戻り値の前にワークが開始されるか、外部オブジェクトが参照を保持し続ける可能性があります。公開を取消した後、新しいリクエストの取消、保留中の操作の待機、およびコールバックの終了を待ち、使用されている buffer/context を解放する必要があります。free 文を機械的に逆順に実行するだけでは不十分です。API の確率とフィルタパラメータを注入してパスをトリガーし、リソースと可視性のチェックでそのパスが正しいかどうかを判断します。これらは互いに代替できません。
失敗ポイントを異なるリソースの取得後に配置し、クリーンアップ方法を切り替えます。残りのリソース数だけでなく、依存関係にあるオブジェクトがまだ生存している場合に、それが依存しているオブジェクトを事前に解放していないかもチェックします。
図を準備しています
失敗が異なる位置で発生した場合、クリーンアップ責任はどのように変化するか?エラーパスでは、取得したリソースのみを解放し、使用依存関係に従って取消す必要があります。最終的なリソース数が0でも、途中でまだ使用されているバッファを先に解放していないことを証明するには不十分です。
{"id":"failure-injection-rollback","title":"失敗が異なる位置で発生した場合、クリーンアップ責任はどのように変化するか?","summary":"エラーパスでは、取得したリソースのみを解放し、使用依存関係に従って取消す必要があります。最終的なリソース数が0でも、途中でまだ使用されているバッファを先に解放していないことを証明するには不十分です。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label for=\"failAt\" data-i18n=\"failAt\"></label><select id=\"failAt\"><option value=\"0\" data-i18n=\"buffer\"></option><option value=\"1\" data-i18n=\"irq\"></option><option value=\"2\" selected data-i18n=\"interface\"></option><option value=\"3\" data-i18n=\"success\"></option></select><label for=\"policy\" class=\"caption\" data-i18n=\"policy\"></label><select id=\"policy\"><option value=\"correct\" data-i18n=\"correct\"></option><option value=\"leak\" data-i18n=\"leak\"></option><option value=\"forward\" data-i18n=\"forward\"></option></select><div class=\"actions\"><button id=\"run\" data-i18n=\"run\"></button><button id=\"remove\" data-i18n=\"remove\"></button></div><div class=\"track-row\"><span data-i18n=\"resources\"></span><div id=\"resources\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"remaining\"></span><strong id=\"remaining\"></strong></div><div><span data-i18n=\"hazards\"></span><strong id=\"hazards\"></strong></div></div><div class=\"track-row\"><span data-i18n=\"trace\"></span><div id=\"trace\" class=\"track\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.caption{display:block;margin:14px 0 5px}#trace{display:grid;grid-template-columns:1fr}#trace .cell{min-height:28px;font:11px var(--viz-font-ui)}.track#resources .cell{min-height:60px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let live=[false,false,false],hazards=0,trace=[],success=false;const names=['buffer','IRQ','interface'];function cleanup(){const policy=$('#policy').value,order=policy==='forward'?[0,1,2]:[2,1,0];for(const i of order){if(!live[i])continue;if(policy==='leak'&&i===0){trace.push('KEEP buffer');continue;}if((i===0&&live[1])||(i===1&&live[2])){hazards++;trace.push('FREE '+names[i]+' !');}else trace.push('FREE '+names[i]);live[i]=false;}}function draw(){cells('resources',live.map((v,i)=>({text:names[i]+' '+(v?'●':'×'),cls:v?'gold':'empty'})));cells('trace',Array.from({length:7},(_,i)=>({text:trace[i]??'—',cls:trace[i]?.includes('!')?'gold':trace[i]?'on':'empty'})));set('remaining',live.filter(Boolean).length);set('hazards',hazards);$('#remove').disabled=!success;set('explanation',t('note'));}$('#run').onclick=()=>{live=[false,false,false];hazards=0;trace=[];const fail=+$('#failAt').value;for(let i=0;i<3;i++){if(i===fail){trace.push('FAIL '+names[i]);break;}live[i]=true;trace.push('GET '+names[i]);}success=fail===3;if(!success)cleanup();draw();};$('#remove').onclick=()=>{cleanup();success=false;draw();};draw();","audio":false,"strings":{"buffer":"buffer を要求する前","correct":"逆依存関係クリーンアップ","failAt":"失敗が発生した位置","forward":"要求順に解放","hazards":"解放順序リスク回数","heading":"失敗が異なる位置で発生した場合、クリーンアップ責任はどのように変化するか?","interface":"interface を登録する前","intro":"probe の失敗位置とロールバック戦略を選択し、モデルを再度実行します。残留リソースと解放順序のリスクを比較します。成功パスでは明示的な解除も可能です。","irq":"IRQ を要求する前","leak":"buffer 解放の漏れ","note":"「すべて成功」を選択して解除するか、interface の前で失敗させてみてください。逆順クリーンアップ、解放漏れ、誤った順序の結果は異なり、正常な probe の戻り値が成功したことだけを検証することはできません。","policy":"ロールバック戦略","remaining":"残留リソース数","remove":"成功したデバイスを解除","reset":"リセット","resources":"有効なリソースと依存関係","run":"選択したシナリオを実行","scope":"玩具 probe:buffer → IRQ が buffer を使用 → 外部 interface が IRQ に依存。失敗は、選択されたステップでリソースを取得する前に発生します。誤った戦略はモデル内でのみデモされ、実際のカーネル障害はトリガーされません。並行処理の詳細は省略。「順序リスク」は、依存関係が有効な状態でリソースが解放されたことを示します。","scopeLabel":"モデル範囲と規約","success":"すべて成功","trace":"今回の取得とクリーンアップ記録"}}
納品可能な再現記録
正確なカーネルバージョンまたはコミット、.config、起動パラメータ、モジュールと taint 状態、最小入力、実際の動作、期待される動作、および元のログを記録します。修正が特定の設定下でのみ検証される場合は、その境界を明確にしてください。
これらの証拠を ビルドとモジュール の成果物に接続して初めて、テストが目標コードを実行していることを確認でき、別の同名のカーネルではないことを保証できます。