カーネルがたまに落ちる原因を5日かけて追いかけた

当時の開発ログを見返しながらのメモ。

2026年6月、x86 のベアメタル版がたまに落ちるようになっていた。

■症状

AI の推論を担当する小さなプログラムを kill して再起動させる、を繰り返していると、たまにカーネル本体がページフォルトで止まる。

=== KERNEL EXCEPTION ===  Page Fault  err=0x00000002
EIP=knl_wait_release_tmout+0x11

落ちる場所は、タイマで待っているタスクを起こす処理。

■仮説1:古いタイマ

kill したタスクのタイマが残っていて発火しているのでは、と考えて 6/10 に KILL-TIMER-RACE として課題表に登録。
でも落ちた瞬間のタイマの待ち行列はいつもきれい。「壊れた関数ポインタ」に見えた値も、8バイトの時刻を4バイトのポインタとして読み違えていただけ。ハズレ。

■原因その1:TSS.RSP0

CPU がユーザ側からカーネル側に入るとき、どのスタックにレジスタを積むかを覚えておく場所。
ディスパッチャはスタックは切り替えているのに、RSP0 は起動時に設定したきり一度も更新していなかった。kill されたタスクのスタックが解放されたあとも RSP0 はそこを指したまま。次の割り込みで CPU が解放済みのメモリに書き込んで、別の誰かの戻り先を壊す。

切り替えのたびに RSP0 を更新するようにした(アセンブラで21行)。
直す前は2〜6回の kill で落ちていたのが、20回続けても大丈夫に。

■でもまだ落ちる

頻度は10分の1くらいになったけど、ゼロにならない。

・「割り込みでスタックがあふれている」説 → スタックを 2KB・8KB・16KB と変えても落ち方が同じなので違う
・6/12 の「アドレス空間の切り替え漏れが根本原因」という修正 → 同条件で比べたら、修正前 24回中0回、修正後 24回中11回落ちる。悪化していたので即取り消し

■原因その2:使い回された枠

6/14、やっと再現して記録を追ったら、犯人はタスク管理表の使い回された枠。
kill したタスクの枠に新しいタスクが入った直後に、前の住人のタイマが遅れて発火して、新しい住人の中身を書き換えていた。

最初の「古いタイマ」説は半分当たりだった。被害者は死んだタスクではなく、その枠を引き継いだ新しいタスクの方。
タイマ発火時に「そのタスクがまだ本当に待っているか」を確認して、待っていなければ何もしないようにした。

■メモ

犯人が2人いて、ほぼ同じ落ち方をしていた。1つ直しても「まだ直っていない」ように見える。
修正が効いたかどうかは、直す前と同じ日・同じ手順で比べないと分からない。

※リポジトリ: https://github.com/monyuonyu/p-kernel

※この記事は、p-kernel の開発を手伝ってもらっている AI(Claude)と一緒に書いています。

コメントを書く