🦁

TOCTOUを忘れずに設計・実装する

2022/04/24に公開

TL;DR

意外に知られていない、そして知っていてもつい忘れてしまう TOCTOU について説明します。

  • 正しい知識がバグを防ぐ
  • シニアエンジニアでも理解していないことがある
  • 設計の考慮漏れで引き起こされることも多い
  • 非機能要件を機能分割手法で設計すると起きやすい
  • 実装で埋め込み易く気付き難い
変更履歴

2022.05.28 読んでいただける人が増えてきたので、雑だった記載をリライトし図を追加しました。

TOCTOUとは

TOCTOU(または TOCTTOU)は、Time of check to time of use.の略で、チェックのタイミングと使用のタイミングに、ズレがあると発生する問題を言います。

この問題は、ファイル I/O のセキュリティで良く語られますが、実際には Race condition(競合状態)に関する問題です。 つまり、トランザクション境界や排他制御などと同様に、同時実行制御に関する一般的な考慮点なのですが、知名度が低いため忘れられがちです。

このバグは、設計においては、同時実行制御の問題を理解していないと、問題の存在に気づくことができません。 実装・テストにおいては、同時実効制御は、本質的にテストが難しいため、問題を見逃す傾向にあります。 これらの結果として、プロジェクトの後半またはカットオーバー後に問題を引き起こす、というのが典型的なパターンです。

設計で見かける問題

外部仕様の失敗

誰でも知っている有名な例として、次のような受注システムの在庫不整合の問題があります。

  1. 在庫数をチェックする
  2. 受注して在庫を更新する

シーケンス図で表すと、次のような処理です。

このように、処理を独立した 2 つの外部仕様として設計すると、在庫がマイナスになる可能性があります。 これは、在庫のチェックと更新のタイミングが一致していない、典型的な TOCTOU です。 もちろん、上記の問題を回避する手段は、いくつか考えることができます。

これらの解決方法は、『システムの複雑性が増す』『不必要に在庫を確保し販売機会を失う』などの懸念から、あまり採用されません。 一般的には、注文処理のトランザクションの中で、『在庫数の確認(とロック)』と『在庫更新』を行うことで TOCTOU を回避します。 つまり設計者は、注文APIの中で在庫確認を再度行うことを明確にする必要があります。 そんな自明のことを書きたくないという意見も聞きますが、開発者はエスパーではありませんので、書いてない思いを全て拾うことはできません。

TOCTOU を回避する設計をシーケンス図で表すと、次のようになります(DBMS の悲観ロックを用いない場合)。

機能分割で見る問題

機能分割(モジュール分割)による設計を取り入れている際には、分割する観点として『共通機能分割』があります。 もちろん、これは正しい観点ですが、機能を細かく分割し過ぎた結果として TOCTOU の原因にならないかを、十分に考慮する必要があります。 この傾向は、特に非機能要件で良く見られるため、 非機能はユースケース実装と一体化して使うように設計する(API化しない) という基本を外すと高確率で TOCTOU の原因になります。

これを言い換えると『非機能を単機能で公開しない』ということです。 文章にすると当たり前ですが、なぜか機能分割の設計手法を採用している場合は、機能要件と非機能要件が、システムの機能として同列に扱われることが少なくありません。 その結果、非機能要件が独立した単機能として提供され、TOCTOU 問題の原因となります。

これまでに見た例では、次のような機能がユースケース実装から分離され、ユースケース実装では同等のチェックされていないものがありました。

  • 機能ごとの認可チェック API
  • リモーティング先の死活チェック API
  • ファイルの存在チェックや権限チェック API

非機能要件は、あくまで内部仕様であり、ユースケースから分離して利用するものではないことをに注意し、AOP やライブラリとして設計するようにしましょう。

実装でみかける問題

仮に、上記のような問題のない設計がされていたとしても、実装がそれを実現できていないと意味がありません。 実装で見かける問題は、 チェックと処理が不可分操作になっていない という、そのものの問題です。

具体的な例を上げると、次のようなものです。

  • ユーザーやファイルの権限チェックと作成・更新・削除が別機能になっている
  • 悲観ロックや DBMS の制約を用いない、重複チェックからの更新

残念ながら、大半の開発者は、 チェックと処理を連続で書けば問題ない と考えているフシがありますが、TOCTOU はそのような問題ではありません。 チェックと処理を連続して実装した場合でも、チェックの直後に、他の処理によって更新が発生している可能性があります。 この問題に対して、同期メソッドや multex で対処している例も見かけますが、プロセス外や他システムからの同時処理に対しては守られていませんので、これも本質的な対応ではありません。

一般的に不可分操作を実現する場合には、OS、CPU または DMBS や TP モニターなどのミドルウェアのサポートが必要になり、『テスト・アンド・セット』『コンペア・アンド・スワップ』『フェッチ・アンド・アッド』『悲観ロック』『ツーフェーズコミット』など、一貫性を守るために用意された仕組みを使う必要があります (一般開発者が独自の発想で作った不可分操作には全く意味がありません)。 開発者が行うべきことは、不可分操作を提供しているシステムコール、ライブラリ、フレームワークを正しく使いこなす能力です。 もし、不可分操作が提供されていないにも関わらず、設計が不可分操作を要求している場合は、それを無理に作っても良い結果を生むことはありません。

まとめ

一般的な認知度が低い、TOCTOU について取り上げてみました。 この問題は、設計・実装の両面から引き起こされる問題ですが、意識していないと見落としがちな問題です。 なぜなら、そもそも TOCTOU の認知度が低いこともありますが、それよりも設計・実装の両面において表面上は間違いないように見えてしまうことにあります。 また場合によっては、綺麗な設計・実装を目指した結果、TOCTOU の原因になっていることすらあります。

TOCTOU は、その多くで再現性が低く、稼働してから数年後に発覚するようなケースもあります。 TOCTOU によるバグを回避するためには、設計や実装時に同時実行制御を意識し、問題を持ち込まない以外に方法はないと考えます。 正しい知識が、バグを防ぐ。

Discussion