6.10. 競合状態を避ける

「競合状態」は次のように定義されています。 「イベント同士が動作する相対的なタイミングが、思いもよらない依存関係に 陥ってしまった危険な動作状態」 [FOLDOC]。 競合状態は通常、1 つ以上のプロセスが共有リソース(ファイルや変数等)にアクセス する時に伴う現象で、複数のアクセスを適切に制御できなくなります。

通常プロセスはアトミックには動作しません。別のプロセスは基本的に 2 つの命令間 に割り込みます。 安全が必要となるプログラムのプロセスが、この割り込みに備えていなければ、別の プロセスが妨害できる可能性がありますん。 安全が必要なプログラムが動いている間に、別プロセスのコードがいくつ動いて、 それがどんな操作の組み合わせであっても、プログラムは正確に動かなければいけ ません。

競合状態の問題は、2 つのカテゴリに分類できます。

6.10.1. シーケンス(非アトミック)問題

一般的に、任意のコードが 2 つの操作間で実行されると操作の組み合わせによって 機能しなくなるものすべてを、注意深くチェックしなければいけません。

共有している変数をロードしたりセーブする場合、普通は独立した操作で実行し、 アトミックな操作にはなっていません。 どういうことかと言うと、「増分する変数」の操作は、通常ロードして、増分して、 保存するという操作に置き換えます。したがって、変数のメモリを他のプロセス と共有していれば、増分の操作に干渉してしまうかもしれません。

安全が必要なプログラムは、要求を許可すべきなのかを判断し、許可できれば 実行します。 そのプログラムが判断にもとづいて動作する前に、信頼できないユーザがその判断 結果を使って、何かを変更する手段があってはいけません。 この種の競合状態は「チェック時が使用時(time of check - time of use (TOCTOU))」競合状態と呼ばれる時もあります。

6.10.1.1. ファイルシステムのアトミックな実行

アトミックな動作の実行が機能しなくなる問題は、ファイルシステムでも度々発生 します。 ファイルシステムというものは、たくさんのプログラムが共有しているリソースです。 プログラムには、他のプログラムがリソースを使うと干渉を受けてしまうものが あります。 安全が必要なプログラムでは、リクエストが許可されるかどうかの判断に際して open(2) に先立って access(2) を実行するのは止めてください。理由は、ユーザが この呼び出しの間にファイルを移動して、そのかわりに自分で選んだファイルに シンボリックリンクを張ってしまう恐れがあるからです。 安全が必要なプログラムは、実効 id や ファイルシステム id を設定せずに、直接 open を呼び出した方が賢明です。 access(2)の安全な使用も可能ですが、それはユーザがそのファイルやファイル システムのルートからのパスにそったディレクトリに影響を与えられない場合に限り ます。

ファイルを作成する時には、O_CREAT | O_EXCL モードを使ってオープンし、 パーミッションをきつく制限した(現在のユーザに限定した)ものだけを許可 しなければいけません。 また、open が失敗した場合にも備える必要があります。 ファイルを open できる必要があるなら(たとえば、サービス拒否攻撃を防ぐため)、 (1)「ランダム」なファイル名、(2)上記のように開く、(3)open が成功したら繰り返さ ない、ということを毎時行なわなければなりません。

普通のプログラムがファイルをきちんと作成しないと、セキュリティ上の弱点に なる可能性があります。 たとえば、「joe」というテキストエディタは、「DEADJOE」という、シンボリック リンクに関する脆弱性を抱えています。 joe をイレギュラーに終了した場合(システムクラッシュや xterm を閉じる、 ネットワーク接続が切れる等)、joe が開いていたバッファを「DEADJOE」と いうファイルに無条件で追加します。 root が通常 joe を使うディレクトリの中で DEADJOE のシンボリックリンクを作成 するとやられてしまうかもしれません。 こうなると、joe はゴミデータをもしかすると機密事項を含んでいるファイルに追加 するようになって、結果としてサービス拒否になったり、悪意のないアクセスが発生 したりします。

他の例として、ファイルのメタ情報をいろいろ操作する作業を行う場合(オーナーの 変更、ファイルの状態確認、パーミッションビットの変更等)、まずファイルを 開いて、開いたファイルに対して操作してください。 つまりこれは、chown()や chgrp()、chmod()のようなファイル名を受けとる関数では なく、fchown()や fstat()、fchmod()システムコールを使うことを意味しています。 こうすることで、プログラムが動作している間にファイルの置き換わりを防げます (おそらく競合状態も)。 たとえば、あるファイルを閉じてから、chmod()を使ってパーミッションを変更する と、攻撃者はその 2 ステップ間にそのファイルを移動もしくは削除し、別のファイル に対してシンボリックリンクを張ってしまえるかもしれません(たとえば、 /etc/passwd に対して)。 他の興味深いファイルの 1 つとして /dev/zero があります。このファイルは 無限大のデータストリームを入力としてプログラムに渡せます。攻撃者が 途中でファイルを「切り替え」たなら、危険な結末になるやもしれません。

しかし、さらに面倒なことがあります。ファイルを作成する時は、できるだけ 最低の権限を与えた上で、望むならもっと権限を広げるように変更しなければいけ ない点です。 一般的には、umask か open 時のパラメタを使って、ユーザやそのユーザのグループ が最初にアクセスした時に制限をかける必要があります。 たとえば、あるファイルを作成し、最初は誰でも読める状態から「誰でも読める」 ビットを落とそうとすると、攻撃者はパーミッションビットが OK である間に ファイルを開こうとします。 たいていの Unix ライクなシステムでは、パーミッションは open 時にチェック されるだけなので、意図したものより高い特権を攻撃者が持つ結果になるかも しれません。

一般的に Unix ライクなシステムにおいて、複数のユーザがあるディレクトリに 書き込みができるなら、そのディレクトリに「sticky」ビットを設定した方が 良いでしょう。sticky なディレクトリを実現した方が具合が良くなります。 しかし、この問題を完全に避けるなら、信頼できる特別なプロセスだけがアクセス できるディレクトリを作る(慎重に実装する)方が、より優れています。 これまでの Unix で一時的に使用するディレクトリ(/tmp や /var/tmp)は、普通 「sticky」ディレクトリとして実現されていますが、それでもセキュリティ上の あらゆる問題が表面化しています。次からその点を見ていきましょう。

6.10.1.2. テンポラリ・ファイル

テンポラリ・ファイルを作成する時に、アトミックな操作を正しく実行する上での 問題が顕著に現れます。 これまで Unix ライクなシステムでは、テンポラリ・ファイルは /tmp もしくは /var/tmp ディレクトリに作ってきており、ユーザすべてが共有していました。 安全が必要なプログラムが動作している間、他のファイル(たとえば、/etc/passwd) に対するシンボリックリンクをテンポラリ・ディレクトリに作成する罠を攻撃者は 仕掛けてきました。 攻撃者の狙いは、安全が必要なプログラムが、ある特定のファイル名が存在しないと 判断する状況を作り上げてから、攻撃者が別のファイルへのシンボリックリンクを 張って、安全が必要なプログラムにある操作を実行させる状態です(実際は意図して いないファイルを開いてしまっている)。 この方法でよく重要なファイルが壊されたり、修正されたりします。 普通のファイルを作成するようなこの手の攻撃のバリエーションはたくさんあります。 この攻撃は、安全なプログラムで使用するテンポラリ・ファイルが存在するのと同じ ディレクトリに、攻撃者がファイルシステム・オブジェクトを作成できる (さもなければアクセスできる)という仮定にもとづいています。

共有ディレクトリにファイルを作成する上で共通の問題点は、使用を予定している ファイル名が、作成時に既に存在していないことを保証しなければいけない点です。 ファイルを作成する「前」にチェックするのは効き目がありません。理由は、 チェック後かつファイルの作成前に、別のプロセスがそのファイル名でファイルを 作成できてしまうからです。 「予測不可能」もしくは「ユニーク」なファイル名を使うのも、およそ効果があり ません。 それは、名前の推測が成功するまで、別プロセスが何度でも推測できるからです。

基本的に、共有している(sticky をかけてある)ディレクトリでテンポラリ・ ファイルを作成するには、次のことを繰り返し行う必要があります。(1)「ランダム」 なファイル名を作成すること、(2)O_CREAT | O_EXCL を使って open し、 パーミッションできつい制限をかけること、(3)open が成功したなら、繰り返さない こと、です。

1997 年版の「Single Unix Specification」によると、任意にテンポラリ・ファイル を作成するのに望ましい方法は、tmpfile(3)を使う、となっています。 tmpfile(3)関数はテンポラリ・ファイルを作成し、それに対応したストリームを open し、そのストリームのディスクリプタを返します(失敗すると NULL を返します)。 あいにく、ファイルが安全に作成される保証は仕様上一切ありません。 このドキュメントの旧版で、実装すべてが安全かどうか確信できないので心配だ、 と述べました。 その後、古い System V システムで tmpfile(3)の実装が安全ではないことがわかって います(tmpnam(3) と tempnam(3)も同様に安全でない)。 もちろん tmpfile(3)を実装しているライブラリは、そのようなファイルを安全に 作成すべきですが、ユーザはシステムのライブラリにセキュリティ上の欠陥がある ことを、必ずしも気付くわけではありません。場合によってはその件について、 何も打つ手がない時もあります。

Kris Kennaway 氏は、テンポラリ・ファイルの作成に当たって、一般に mkstemp(3) の使用を推奨しています。 テンポラリ・ファイルを作るなら、自分自身で関数を作り上げて利用するよりも、 よく知らたライブラリを使った方が良い、というのが理屈です。そしてこの関数は よく知られた使い方を採用しています。 これはかなりもっともな見解です。 mkstemp(3)を使うなら、私はこれに加えて必ず umask(2)を使って、テンポラリ・ ファイルのパーミッションが所有者だけになるように制限をかけます。 これは mkstemp(3)の実装(基本的には古い物)には、テンポラリ・ファイルを すべてのユーザに対して、読み書き可能にしているものがあるからです。この状態 になると攻撃者は、このディレクトリにプライベートなデータを読み書き可能に なります。 多少厄介なのは、mkstemp(3)が直接には TMP や TMPDIR といった環境変数をサポート していない点です(下記で論じます)。そこで環境変数をサポートしたいとなると、 自分でサポートできるようにコードを追加しなければいけません。 ここで、環境変数をサポートした C で書いた mkstemp(3)の使い方を示したプログラム を掲載します。これで、TMP もしくは TMPDIR のサポートを追加することで、直接 両者の操作が可能になります。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

void failure(msg) {
 fprintf(stderr, "%s\n", msg);
 exit(1);
}

/*
 * Given a "pattern" for a temporary filename
 * (starting with the directory location and ending in XXXXXX),
 * create the file and return it.
 * This routines unlinks the file, so normally it won't appear in
 * a directory listing.
 * The pattern will be changed to show the final filename.
 */

FILE *create_tempfile(char *temp_filename_pattern)
{
 int temp_fd;
 mode_t old_mode;
 FILE *temp_file;

 old_mode = umask(077);  /* Create file with restrictive permissions */
 temp_fd = mkstemp(temp_filename_pattern);
 (void) umask(old_mode);
 if (temp_fd == -1) {
   failure("Couldn't open temporary file");
 }
 if (!(temp_file = fdopen(temp_fd, "w+b"))) {
   failure("Couldn't create temporary file's file descriptor");
 }
 if (unlink(temp_filename_pattern) == -1) {
   failure("Couldn't unlink temporary file");
 }
 return temp_file;
}


/*
 * Given a "tag" (a relative filename ending in XXXXXX),
 * create a temporary file using the tag.  The file will be created
 * in the directory specified in the environment variables
 * TMPDIR or TMP, if defined and we aren't setuid/setgid, otherwise
 * it will be created in /tmp.  Note that root (and su'd to root)
 * _will_ use TMPDIR or TMP, if defined.
 * 
 */
FILE *smart_create_tempfile(char *tag)
{
 char *tmpdir = NULL;
 char *pattern;
 FILE *result;

 if ((getuid()==geteuid()) && (getgid()==getegid())) {
   if (! ((tmpdir=getenv("TMPDIR")))) {
     tmpdir=getenv("TMP");
   }
 }
 if (!tmpdir) {tmpdir = "/tmp";}

 pattern = malloc(strlen(tmpdir)+strlen(tag)+2);
 if (!pattern) {
   failure("Could not malloc tempfile pattern");
 }
 strcpy(pattern, tmpdir);
 strcat(pattern, "/");
 strcat(pattern, tag);
 result = create_tempfile(pattern);
 free(pattern);
 return result;
}



main() {
 int c;
 FILE *demo_temp_file1;
 FILE *demo_temp_file2;
 char demo_temp_filename1[] = "/tmp/demoXXXXXX";
 char demo_temp_filename2[] = "second-demoXXXXXX";

 demo_temp_file1 = create_tempfile(demo_temp_filename1);
 demo_temp_file2 = smart_create_tempfile(demo_temp_filename2);
 fprintf(demo_temp_file2, "This is a test.\n");
 printf("Printing temporary file contents:\n");
 rewind(demo_temp_file2);
 while (  (c=fgetc(demo_temp_file2)) != EOF) {
   putchar(c);
 }
 putchar('\n');
 printf("Exiting; you'll notice that there are no temporary files on exit.\n");
}

Kennaway 氏は、mkstemp(3)を使えないなら、mkdtemp(3)を使ってディレクトリを つくるように推奨しています。こうすれば、外部から守れます。 最終的に、安全でない mktemp(3)を使わなければならないなら、予測できない文字を たくさん使うようにも提案しています。 10 文字がお勧めです(libc が許せば)。 こうすれば、ファイル名は簡単には推測できなくなります(6 文字だと、5 文字は PID で取られてしまうので、ランダムに残されたのは 1 文字だけになってしまいます。 これでは攻撃者に簡単に競合状態をしかけられてしまいます)。 これに加えて tmpnam(3)の利用も避けるように提案します。スレッドが動いていて tmpnam(3)を使用をすると、どうなるのかわかりません。また TMP_MAX を越えて使用 すると(実用上、1 つのループ内で使用しなければいけません)正しい動作が保証 できません。

概して mktemp(3) や tmpnam(3)のような、安全でない関数の使用は避けるべきです。 使用するなら、セキュリティを脅かす点に特別な処置を講じたり、安全なライブラリ の実装のテストをインストールの一環として行ってください。 問題がいろいろあってもなお、/tmp や誰でも書ける(もしくはグループを信頼して いないなら、グループで書ける)ディレクトリにファイルを作って、mk*temp()を使い たくないなら(たとえば、名前が事前にわかっているファイルを意図して)、 常に O_CREAT と O_EXCL フラグを付けて open()を呼び出し、 返り値をチェックしてください。 open()が失敗したなら、その時は適切に後処理してください(たとえば、exit する)。

GNOME のプログラミング・ガイドラインでは、ファイルシステム・オブジェクトを 共有の(テンポラリの)ディレクトリに作成する場合、下記の C コードを推奨 しています。これは最小限のセキュリティでファイルを作成するのが目的です。
 char *filename;
 int fd;

 do {
   filename = tempnam (NULL, "foo");
   fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600);
   free (filename);
 } while (fd == -1);
ここで注目なのは、安全でない関数の tempnam(3)が使われていても、ループ内部 で O_CREAT と O_EXCL を使って、セキュリティ上の弱点をカバーしている点です。 注目して欲しいのは、ファイル名を free()する必要があるところです。 処理が終わったら、close()や unlink()すべきです。 標準 C 入出力ライブラリを使いたいなら、fdopen()を「w+b」で使用して、ファイル ディスクリプタを FILE * に変えてください。 この解決方法は、NFS version 2 (v2)のシステムではうまくいかないでしょう。理由 は、古い NFS が O_EXCL をきちんとサポートしていないからです。 この解決方法にはちょっとした欠点もあって、tempnam を安全に使わないと、 コンパイラやセキュリティ・スキャナがうるさく警告を出すかもしれません。 これは mkstemp(3)では問題となりません。

シェルスクリプトでテンポラリ・ファイルが必要ならば、パイプを使ってローカル ディレクトリ(たとえば、ユーザのホームディレクトリ内のどれかに)や、場合に よってはカレントディレクトリを利用するのが適切でしょう。 こうすれば、ユーザが許可しない限りは共有はありえません。 どうしても /tmp のような共有ディレクトリにテンポラリファイルを作りたい、 もしくは必要なら、従来からのシェル上のテクニックを使って、ファイル名のひな形 にプロセス id を組み込み、いつも通りに「>」でファイルを作っては、いけません。 シェルスクリプトは「$$」を使って pid を示しますが、攻撃者は簡単に pid を 特定もしくは推測できます。そうして攻撃者は、同じ名前で事前にファイルを作成 したり、リンクしたりしてしまいます。 つまり、下記の「ありがち」なシェルスクリプトは、安全ではありません
   echo "This is a test" > /tmp/test$$  # DON'T DO THIS.

シェルスクリプトでテンポラリファイルが必要でかつ、/tmp に置きたい場合は、 mktemp(1)が解決方法になるのと思います。mktemp(1)はシェルスクリプトでの利用 を前提にしています。 mktemp(1)と mktemp(3)は別物で、mktemp(1)は安全です。 正直言うと、私はシェルスクリプトで共有ディレクトリにテンポラリファイルを しょっちゅう作っているわけではありません。 そのようなファイルをプライベートなディレクトリに作成するか、パイプを使うか する方が好ましいと思います。 しかしどうしても必要なら、mktemp(1)でひな形を作って、O_EXCL でファイルや ディレクトリを作成し、最終的にファイル名を返すようにします。 O_EXCL を使えば、/tmp のような共有ディレクトリでも安全になります(ただし NFS version 2 を使っていなければ)。 ここで、正しい例として mktemp(1) を Bourne シェルで利用してみます。 この例は mktemp(1)の man からそのまま持ってきました。
 # Simple use of mktemp(1), where the script should quit
 # if it can't get a safe temporary file:

   TMPFILE=`mktemp /tmp/$0.XXXXXX` || exit 1
   echo "program output" >> $TMPFILE

  # Simple example, if you want to catch the error:

   TMPFILE=`mktemp -q /tmp/$0.XXXXXX`
   if [ $? -ne 0 ]; then
      echo "$0: Can't create temp file, exiting..."
      exit 1
   fi

テンポラリファイル名は、再利用しないでください(つまり削除して再作成します)。 いかに「安全な」テンポラリのファイル名を最初に得られたとしてもです。 攻撃者は、オリジナルのファイル名を見つけて、二度目に再利用する前に乗っ取って しまうかもしれません。 もちろん適切なパーミッションを常にかけてください。 たとえば、誰でも、もしくはあるグループがそのファイルにアクセスする必要が あるなら、そのアクセスだけを許可してください。さもなければ、モードを 0600 に しておいてください(すなわち、所有者だけが読み書きできように)。

きちんと後始末をしてください。終了処理を使うか、UNIX ファイルシステムの実際 の処理方法を利用して、作成とともにファイルを unlink()してください。そうすると ディレクトリ・エントリは消えてしまいますが、ファイル自体はファイルを指し示す 最後のファイル・ディスクリプタが閉じるまではアクセスできるようになっています。 そうすれば、プログラム内からはファイル・ディスクリプタ経由でファイルにアクセス し続けられます。 ファイルを unlink するのは、コードをメンテナンスするのに非常に役立ちます。 ファイルはプログラムがクラッシュしたとしても自動的に削除されます。 すぐに unlink すると管理者がディスクスペースがどのくらいあるかがわかりにくく なるという問題も多少はあります。それは単純に名前ではファイルシステムを見られ なくなるからです。

環境変数の TMP や TMPDIR の値が確実に信頼できるところから得られ、コードが Unix ライクなシステム向けなら、それらの環境変数を尊重してもよいかもしれません。 そうすれば、ユーザはテンポラリファイルをホームディレクトリ下のサブディレクトリ のような共有していないディレクトリに移せます(そしてここで論じた問題を回避 できます)。 Bastille の最近のバージョンでは、ユーザ間で共有を減らすように、これらの変数を 設定できるようになっています。 残念ながら、ユーザは TMP や TMPDIR に共有ディレクトリ(たとえば /tmp)を設定 しているケースが多く、依然として安全が必要なプログラムでは、これらの環境変数 が設定してあっても、正しくテンポラリファイルを作成する必要があります。 GNOME の解決方法には長所が 1 つあります。少なくともあるシステムでは、 tempnam(3)は自動的に TMPDIR を利用しますが、mkstemp(3)で同様なことをするには、 さらにコードを書かなければならないからです。 テンポラリディレクトリ用にさらに環境変数(TEMP の ような)を作らないように してください。特にアプリケーション毎に別の環境変数名を作らないでください (たとえば、「MYAPP_TEMP」のように使わないこと)。 作成してしまうと、システム管理がとても複雑になってしまいます。特定のアプリ ケーション用に専用のテンポラリファイルを望んでいるユーザが、そのアプリケー ションを動かす時に環境変数を独自に設定できてしまいます。 もちろん、これらの環境変数が信頼できないソースで設定されてしまったなら、 これらを無視しなければいけません。Section 4.2.3 にある アドバイスに従うなら、どのみちそうなるでしょう。

これらのテクニックは、テンポラリディレクトリが NFS version 2 (NFSv2)でマウント した、リモートのディレクトリであるとうまく動きません。それは NFSv2 がきちんと O_EXCL をサポートしていないからです。 詳しいことは Section 6.10.2.1 を見てください。 NFS version 3 以降では O_EXCL をきちんとサポートしています。テンポラリ ディレクトリは、いつもローカルに作成するか、NFS を使ってマウントするなら、常に NFS version 3 以降を使うのが解りやすい解決策です。 NFS v2 で安全にテンポラリファイルを作成するには、link(2) と stat(2)を使用 しますが、面倒です。これについては、Section 6.10.2.1 に 詳しい情報があります。

それはさておき、FreeBSD が最近になって mk*temp()系でファイル名に pid を 付けないようにした点は、注目に値します。pid ではなく、base-62 でエンコード したランダムな値に完全に置き換えました。このことによって「デフォルト」の 6 文字分を使用したテンポラリファイルが大幅に増加しました。つまり、6 文字 を使った mktemp(3)でさえ、頻繁に使用しなければ、名前の推測に対してかなり (確率的にも) 安全になりました。 しかしここでも教えにならうなら、彼らが取り組んでいる問題を回避するでしょう。

テンポラリファイルについての情報の多くは、 Kris Kennaway 氏が 2000 年 12月15日に Bugtraq へテンポラリファイルについて投稿した記事 によっています。

6.10.2. ロックをかける

プログラムがあるもの(たとえば、ファイルやデバイス、あるサーバ・プロセスの存在) に対して、排他的な権限を確保しなければならない状況がよくあります。 リソースをロックするシステムはどれでも、よくあるロックの問題、つまりデッド ロック(「死の抱擁(deadly embrace)」)やライブロック、そしてプログラムがロックを 後片づけしない場合は「取り残された」ロックの解放に対処しなければいけません。 デッドロックは、それぞれのプログラムがリソースが解放されるのを待って、身動きが とれない場合に発生します。 たとえば、デッドロックは、プロセス 1 がリソース A をロックしつつ、リソース B が解放されるのを待っている状態で、プロセス 2 がリソース B をロックしつつ、 リソース A が解放されるのを待っている時に起こります。 デッドロックの多くは、複数のリソースをロックするプロセスすべてが、同じ順序付け (たとえば、ロックする名前をアルファベット順にする)でロックを行えば簡単に回避 できます。

6.10.2.1. ファイルをロックに使用する

Unix ライクなシステムでリソースをロックするには、これまではファイルを作って ロックを実現していました。これが非常に移植性がある方法だからです。 またこの方法では、ロックの残骸を簡単に「修復」できます。理由は、管理者が ファイルシステムを見れば、どんなロックが設定してあるのかわかるからです。 ロックの残骸は、プログラム自身が後片づけに失敗したり(たとえば、クラッシュ したり、誤動作したりした場合)、システム全体がクラッシュした場合に起こります。 これらは「アドバイザリ」(強制(mandatory)ではありません)ロックであることに 注意してください。リソースを必要としているプロセスすべてはこのロックを協調 して使わなければいけません。 【訳註:ファイルのロック機能には、強制ロック(mandatory locking)と アドバイザリ・ロック(advisory locking)があります。違いは、前者はカーネルが プロセスを監視しロック操作を行うので、プロセス間の依存関係を越えてロックが 可能であるのに対して、後者はプロセス自身がロック操作を行うので、そのプロセス の制御外のものに対してはロックが無効となります。 詳しくは、カーネル付属の ドキュメントの linux/Documentation/mandatory.txt を参照してください】

しかし、避けなければならない落とし穴があります。 まず、以前から使われている C プログラムのやり方を使わないようにしてください。 その方法では、create()もしくはそれと等価の open()を呼び出し、open()のモード を O_WRONLY | O_CREAT | O_TRUNC としてファイルのモードを 0(パーミッションなし) とします。 通常のファイルシステム上で、一般ユーザが行うのであれば問題はありませんが、 ユーザが root の特権を持っている場合には、ファイルのロックに失敗します。 root はファイルが既に存在していても、常にこの操作が実行できてしまいます。 実際、古い Unix バージョンでは、いにしえのエディタである 「ed」がこの特徴的な問題を抱えていました。ときおり、パスワードファイルの一部 がユーザのファイルになってしまう現象がありました[Rochkind 1985, 22]。 そうするかわりに、プロセスに使うロックをローカルのファイルシステム上に作る なら、open()に O_WRONLY | O_CREAT | O_EXCL フラグをつけて使用すべきです (また一方では、パーミッションはつけなければ、同じユーザの他のプロセスはロック を獲得できません)。 O_EXCL は、正式には「排他的な」ファイルの作成に使用されます。これはローカル のファイルシステム上で root に対しても効果があります[Rochkind 1985, 27]。

次に問題となるのが、ロックファイルを NFS でマウントしたファイルシステム上に 作成する場合です。NFS version 2 が、通常のファイルが持っている機能を完全には サポートしていない点が問題です。 これは、クライアントが「ローカル」にあることを仮定して動作する場合も問題となり ます。クライアントによっては、ローカルでディスクを持たないものやすべてのファイルが NFS 経由でリモートマウントしている ものもあるからです。 open(2) のマニュアルでこのケースの扱いを説明 しています(root のプログラムの扱いも説明してあります)。

"……プログラムが open(2)の O_CREAT と O_EXCL に依存している 場合、ロック機能を動かすと競合状態になることがあります。ロックファイルを使って アトミックにファイルロックを実現するには、同じファイルシステム上にユニークな ファイルを作成し(たとえば、ホスト名や pid を組み合わせて)、link(2)を使って そのロックファイルにリンクを張ります。そして stat(2)を使って、そのユニークな ファイルに対してリンク・カウントが 2 まで増えているかをチェックします。link(2) システムコールの返り値は使用しないでください。"

どう考えてもこの解決策では、すべてのプログラムが協調してロックを行わないと うまく動作しません。協調していないプログラムが干渉してもうまくありません。 特に、ファイルのロックに使っているディレクトリで、ファイルを作成・削除できる パーミッションを許可してはいけません。

NFS version 3 には O_EXCL モードを open(2)でサポートする機能が追加してあり ます。IETF RFC 1813 を見て、特に「CREATE」に対する「モード」値の「EXCLUSIVE」 をよく見てください。 残念なことに、現状ではみんながみんな NFS version 3 以上に移行しているわけ ではありません。したがって、移植性が必要なプログラムでは、この機能を頼みに できません。 ただし長期的にみれば、この問題が解決する望みもあります。

ローカルマシン上に存在するデバイスやプロセスをロックするなら、標準的な約束事を 守ってみてください。 Filesystem Hierarchy Standard (FHS)の利用を推奨します。 Linux システムは広く FHS を参考にしているだけでなく、他の Unix ライクな システムのアイディアも盛り込もうとしています。 FHS はファイルのロックについても説明しており、名前の付け方、置き方、ファイル の標準的な内容について盛り込んでいます[FHS 1997]。 マシンでサーバを 2 つ以上実行していないかを単に確かめたいなら、通常は /var/run/NAME.pid としてプロセスの識別子を作成し、その中身には pid を入れて おきます。 同じような状況で /var/lock のデバイス用ロックファイルのように、ロックファイル を作成すべきです。 この解決方法では、プログラムが予期せずにハングアップすると、関連したファイル が残ったままになってしまう点が欠点と言えば欠点です。しかしそれが普通のやり方 なので、他のシステムツールを使って簡単に解決できます。

協調して動作しているプログラムが、ファイルを使ってロックを提供するのに、 同じディレクトリ名を使うだけでなく、実体も同じディレクトリを使用するのが大切 です。 ネットワークを利用しているシステムでは、これが問題となります。FHS でははっきり と、/var/run と /var/lock は共有しない、/var/mail は共有できると言及しています。 つまり、単独のマシン上で動作するロックが必要で、他のマシンから影響を受けない なら、/var/run のような共有しないディレクトリを使用してください (たとえば、それぞれのマシン独自でサーバが動作するのを許可したい場合)。 しかし、マシンすべてでネットワークにあるファイルを共有し、ロックに従いたい なら、共有しているディレクトリを使う必要があります。/var/mail はそんな ディレクトリ場所の 1 つです。FHS のセクション 2 にこの話題についてのさらに 詳しい情報があります。

6.10.2.2. ロックを使う他の解決方法

もちろん、ロックするのにファイルを使う必要はありません。 ネットワークサーバならこの点についてほとんど悩む必要はありません。あるポート に接続してくる動作をロックとして扱うだけでよいからです。つまり、あるポートに 既に接続しているサーバがあれば、もうそれ以上そのポートにサーバは接続できなく なります。

ロックを行う別の解決方法として、POSIX のレコード・ロックがあります。これは fcntl(2) を使って「任意ロック(discretionary lock)」として実装している方法 です。 これらは任意に使えます。つまりこれらを使うには、ロックを必要としている プログラムが協調して動作していなければなりません(ファイルを使ってロック を行うのと同じように)。 POSIX レコード・ロックを推奨するのには、理由がたくさんあります。 POSIX レコード・ロックは、ほとんどすべての Unix ライクなプラットフォーム でサポートしていて(POSIX.1 で公式に推奨しています)、ファイルの一部(ファイル 全体ではなく)をロックでき、読み書きそれぞれのロックを扱えます。 それにも増して、プロセスが死んだとしてもロックが自動的に解除されます。通常 これが望ましい動作です。

強制ロックも使えます。これは System V の強制ロック技術をベースにしています。 ロックされたファイルの setgid ビットは設定してあるが、グループの実行ビットが 設定されていないファイルにだけ適用されます。 また、強制ファイルロックを許可するには、ファイルシステムをマウントしないと いけません。 この場合、read(2) と write(2)それぞれが、ロックする時にチェックされます。 このやり方はアドバイザリ・ロックよりも徹底しているので、遅くなります。 また、強制ロックは、他の Unix ライクなシステムに広く移植されているわけでは ありません(Linux と System V ベースのシステムでは利用できますが、その他は必ず しもそうではありません)。 root 特権を持つプロセスも、強制ロックで止められますので、サービス拒否攻撃 の原因になります。