TkrzwのFedora用のパッケージを作ってくれている人から不具合報告を貰っていた。その不具合はPowerPC 64ビットの特定の環境でしか起こらないらしい。その不具合が手元で再現できないので、何が問題なのかもわからず、しばらく直せないでいた。その原因をやっと昨日つきとめて、自分の浅はかさを知るとともに、システムプログラミングの奥深さを感じた。その経緯の与太話である。
Fedoraのパッケージを登録するということは、基本的にはFedoraがサポートする全ての環境で動作することが求められるということだ。CPUの種類はx86、x64を始めとして様々あり、32ビットアドレスだったりビッグエンディアンだったりすることもある。自分の環境では問題ないのに他の環境で動かないという報告を受け取ると、手元では再現できないものだから、修正にかなり手こずる。
32ビット環境でポインタがオーバーフローするとか、エンディアンの違いを吸収するロジックを入れ忘れていたとか、特定の環境で未実装のシステムコールを使っていたとか、GCCのオプションが特定の環境で未実装だったりとかというのが「あるある」だ。それらは比較的すばやく直せるのだが、それより複雑なものには本当に頭を悩ませられる。報告者にログを送ってもらって、その情報だけから原因を推測して、パッチを書いて、報告者に追試をお願いするという手順を繰り返さねばならない。多くの場合、ラッキーで有り難いことに、そのイテレーションに根気よく付き合ってくれる人達がいる。彼らのおかげで、ついには不具合は修正される。
PowerPCの64ビットでリトルエンディアンの環境でデータベースファイルが壊れるという不具合が数ヶ月前に報告されていた。PowerPCとはいえ、64ビットでリトルエンディアンならば、私の主な開発環境であるx64とそんなに変わらないはずだ。少なくともアドレス幅とエンディアンは同じなのだから。それでも、実際不具合は起きているのだ。ともかく、ログを見て、思いついたパッチを書いては報告者に送り、その度に「failed」「failed」との返事をいただく期間を過ごした。主にGitHub上で行われたそのやり取りを見ていた別の人が、Fedoraのサーバにアカウントを作ってくれて、自分でテストできるようにしてくれた。ありがたや。手元で再現する手順を確立さえすれば、バグの98%は直ったも同然である。
しかし、問題はここからだった。PowerPCの64ビットでリトルエンディアンの環境にいるのに、全てのテストが正常動作して、不具合が起こらないのだ。それなのに、Fedoraの自動ビルドシステムに通すと、make checkがエラーになるという。正直ここで諦めかけた。ただ、ビルドシステムの環境と私が貰った環境は微妙な差異があるそうで、少なくともCPUのコア数とLinuxのマイナーバージョンが違うらしい。よって、まずはシングルスレッドでもビルドシステム上で不具合が発生するテストケースを考えて、コア数とレースコンディションにまつわる問題の可能性を排除した。すると、残るはLinuxのバージョンの影響ということになる。すなわち、システムコールの挙動に微妙な差異があるだろうということだ。不可思議な挙動をするように見えるシステムコールと言えばforkとsocketとmmapだが、今回はmmapが怪しいので、その辺を重点的に調査した。
結論としては、少なくともPowerPC上で、mmapの挙動がLinux 5.10とLinux 5.14の間で異なるということだ。これが、環境によってこの不具合が起きたり起きなかったりすることの原因である。そして、Linux 5.10の方で不具合が起きてしまうのは、Tkrzw 1.0.11でメモリリークの不具合を直した際に私が入れ込んでしまった別のバグのせいである。この不具合を説明するための前提はどうしても複雑になってしまうので、箇条書きする。
- 全ての入出力をメモリマップI/Oで行うファイルクラスMemoryMapParallelFileというのを実装している。
- ファイルクラスはTruncateメソッドをサポートし、ファイルを任意のサイズに変更できる仕様である。
- mmapは長さがゼロのマップを許さないので、Truncateの際のマップの最低サイズをOSのPAGE_SIZEにしていた。
- Tkrzwのハッシュデータベースはレコードの開始位置を4096バイトにしているので、4096バイト以下のデータベースファイルは作られない。
- x86やx64の多くの環境では、PAGE_SIZEは4096だが、PowerPCでは65536である。
- よって、PowerPC上では、Truncateにより、マップのサイズよりファイルのサイズが小さくなる可能性がある。ここがバグの根本。
- マップされているがファイルサイズを超えた部分の読み書きは、ファイルサイズの末尾と同一ページ内であれば、セグメンテーションフォルトを起こさない。
- マップされているがファイルサイズを超えた同一ページ部分の読み書きは、Linux 5.14以上では問題なく行われるが、Linux 5.10では、書き込みは成功するが、読み込みは常にゼロコードで埋められたデータを返す
以上の理由により、PowerPC 64ビットのLinux 5.10ではデータベースが壊れる可能性があり、その潜在的不具合は他の環境では発現しないのだ。存外、ページサイズ周りの処理はバグを入れやすく、またそれが発見しにくいので注意せねば。あと、mmap周りも同様で、POSIX仕様で明記されていないコーナーケースで嵌まることがあるので、行き過ぎた想定をせずに保守的にコードを書くのが大事だ。自分の開発環境でテストが通るからって大丈夫だと思いこむのが浅はかなのだ。仕様に書いてあること以外は前提としてはいけない。
対策としては、そもそもマップサイズをページサイズに合わせるのをやめるのが良いだろう。空のマップが作れない問題に関しては、マップサイズが0のときのみダミーのサイズのマップを作ることで対応する。その対策を施したところ、全ての環境で正常動作するようになった。晴れてFedoraに登録申請してもらえるそうだ。ついでに言えば、Debianのパッケージも申請されている。
まとめ。TkrzwのFedoraパッケージがもうすぐ登録される。その対応の過程でバグをいくつか踏んだが、協力者のおかげで何とか乗り越えることができた。オープンソースとコミュニティの力に感謝だ。