JPCERT コーディネーションセンター

FIO14-J. プログラムの終了時には適切なクリーンアップを行う

回復不可能なロジック上のエラーなど、ある種のエラーを検知した場合の対処方針としては、状態が不定のままプログラムの実行を続けることでデータが破壊される危険を冒すよりも、ただちにシステムをシャットダウンし、安定した状態でプログラムを再スタートする方が適切であろう。[ISO/IEC TR 24772:2010]のセクション6.46, "Termination Strategy [REU]" には以下のように記されている。

障害(fault)が検知された場合、システムがそれに応ずる方法は複数存在する。最も迅速かつ分かりやすい方法は、フェイルハード(fail hard)である。これは、フェイルファスト(fail fast)やフェイルストップ(fail stop)とも呼ばれ、検知された障害への対応として、システムをただちに停止することである。別の対応はフェイルソフト(fail soft)である。システムは障害が存在したまま動作を続けるが、パフォーマンスは低下するであろう。高可用性が求められる環境、たとえば電話交換機センターや電子商取引サイトなど「常に稼働する」アプリケーションでは、フェイルソフトアプローチがとられることが多い。フェイルソフトアプローチでとられる実際の対応は、安全性を重視するシステムか、セキュリティを重視するシステムかによって異なる。航空管制、信号機、医療用モニタリングシステムなどのフェールセーフシステムでは、通常運用時に求められる要件を満たそうとするよりもむしろ、障害によって引き起こされる被害や危険を抑えようとするであろう。安全にフェイルするシステム、たとえば暗号システムなどでは、サービス運用を停止することで、障害が検知された場合に最大限セキュリティを維持しようとするであろう。

また、次のような記述もある。

障害への対応は、障害が発生した箇所の重要性に依存する。プログラムが複数のタスクから構成されている場合、重要なタスクもあれば、そうでないものもあるだろう。タスクが重要であれば、それ以外のプログラムによってタスクが再起動されるかもしれない。理想的には、障害を検知したタスクは、自身が確保したシステムのリソースを他のプログラムが利用できるようにするか、リソースをクリアして停止するか、あるいはプログラム全体を停止するのがよい。タスクが終了するまでの遅延やタスクが終了シグナルを無視できるかどうかは明確に定められるべきである。障害に対して一貫した対応ができないと、脆弱性につながる可能性がある。

Javaにはプログラムを終了する方法が2つある。Runtime.exit() (System.exit()と同じ)と、Runtime.halt() である。

Runtime.exit()

Runtime.exit() はプログラムを終了させる標準的な方法である。Java API 仕様 [API 2006]には次のように記されている。

現在実行している Java 仮想マシンを終了するために、シャットダウンシーケンスを開始する。通常、このメソッドは復帰することはない。引数はステータスコードを表し、通例、ゼロ以外のステータスコードは異常終了を示す。

仮想マシンのシャットダウンシーケンスは2段階で構成される。第1段階では、すべての登録済みのシャットダウンフックを(存在する場合)、特に指定しない順序で起動し、終了するまで同時に実行することができる。第2段階では、終了時のファイナライズが有効になっている場合、呼び出されていないすべてのファイナライザが実行される。これが終了すると、仮想マシンは停止する。

仮想マシンがシャットダウンシーケンスを開始したあとにこのメソッドが呼び出されると、シャットダウンフックがこのメソッドを実行している場合は無期限にブロックされる。シャットダウンフックがすでに実行されており終了時のファイナライズが有効になっている場合、ステータスがゼロ以外の場合は仮想マシンを指定したステータスコードで停止し、そうでない場合は無期限にブロックする。

このメソッドを呼び出すには、System.exit() メソッドが一般的かつ簡便である。

Runtime.addShutdownHook() メソッドを使用することで、プログラムの終了時に Runtime.exit() に別の動作もさせることができる。このメソッドは、初期化されているが開始されてはいない、単一の Thread オブジェクトを引数に取る。このスレッドは、JVMがシャットダウンするときに実行される。JVMは通常、一定の時間でシャットダウンするため、これらのメソッドは長時間動作するべきではなく、またユーザインタラクションも試みるべきではない。

Runtime.halt()

Runtime.halt() も同様の動作をするが、シャットダウンフックやファイナライザは実行しない。Java API 仕様 [API 2006] には以下のように記されている。

現在実行中の Java 仮想マシンを強制終了する。このメソッドは通常、復帰しない。

このメソッドの使用には細心の注意が必要である。exit メソッドとは異なり、このメソッドではシャットダウンフックを起動せず、終了時のファイナライズが有効な場合であっても呼び出されていないファイナライザを実行しない。すでにシャットダウンシーケンスが開始されている場合、このメソッドは実行中のシャットダウンフックやファイナライザを待機せずに終了する。

Javaではプログラム終了時に、書き出されていないバッファ付きデータの書出しや、オープンされたファイルのクローズは行われないため、プログラムがこれを行う必要がある。また、共有ロックを解放するなど外部リソースのクリーンアップもプログラムで行わなくてはならない。

違反コード

以下の違反コード例では、ファイルを新規作成し、テキストをファイルに書き込み、Runtime.exit() を使用してプログラムを突然終了している。そのためファイルは、テキストが実際に書き込まれることなくクローズされるかもしれない。

public class CreateFile {
  public static void main(String[] args)
                          throws FileNotFoundException {
    final PrintStream out =
        new PrintStream(new BufferedOutputStream(
                        new FileOutputStream("foo.txt")));
    out.println("hello");
    Runtime.getRuntime().exit(1);
  }
}
適合コード (close())

以下の適合コードでは、プログラムを終了する前に、明示的にファイルをクローズしている。

public class CreateFile {
  public static void main(String[] args)
                          throws FileNotFoundException {
    final PrintStream out =
    new PrintStream(new BufferedOutputStream(
        new FileOutputStream("foo.txt")));
    try {
      out.println("hello");
    } finally {
    try {
      out.close();
   } catch (IOException x) {
     // エラー処理
   }
    }
    Runtime.getRuntime().exit(1);
  }
}
適合コード (シャットダウンフック)

以下の適合コードは、シャットダウンフックを追加してファイルをクローズする。このフックは Runtime.exit() によって起動され、JVMが終了する前に呼び出される。

public class CreateFile {
  public static void main(String[] args)
                          throws FileNotFoundException {
    final PrintStream out =
        new PrintStream(new BufferedOutputStream(
                        new FileOutputStream("foo.txt")));
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        public void run() {
          out.close();
        }
    }));
    out.println("hello");
    Runtime.getRuntime().exit(1);
  }
}
違反コード (Runtime.halt())

以下の違反コード例は、Runtime.exit() の代わりに Runtime.halt() を呼び出している。Runtime.halt() メソッドはシャットダウンフックを起動せずにJVMを停止する。したがって、ファイルが適切に書き出されず、クローズもされない。

public class CreateFile {
  public static void main(String[] args)
                          throws FileNotFoundException {
    final PrintStream out =
          new PrintStream(new BufferedOutputStream(
                          new FileOutputStream("foo.txt")));
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        public void run() {
          out.close();
        }
    }));
    out.println("hello");
    Runtime.getRuntime().halt(1);
  }
}
違反コード (シグナル)

ユーザが ctrl + c を押したり、kill コマンドを使用して強制的にプログラムを終了させた場合、JVMは突然終了する。このイベントは捕捉できないにしても、プログラムは終了する前に必要なクリーンアップ動作を実施すべきである。以下の違反コード例ではこのクリーンアップを行っていない。

public class InterceptExit {
  public static void main(String[] args)
                          throws FileNotFoundException {
    InputStream in = null;
    try {
      in = new FileInputStream("file");
      System.out.println("Regular code block");
      // ctrl + c キーが押されるなど、プログラムが突然終了させられる
      System.out.println("This never executes");
    } finally {
      if (in != null) {
        try {
          in.close();  // この行も決して実行されない
        } catch (IOException x) {
          // エラー処理
        }
      }
    }
  }
}
適合コード (addShutdownHook())

java.lang.Runtime クラスの addShutdownHook() メソッドを使用して、プログラムの突然の終了時にクリーンアップ処理を行うこと。JVMは、突然終了させられると、シャットダウンフックのスレッドを開始する。シャットダウンフックはJVMの他のスレッドと並行実行される。

Java API 仕様 [API 2006]のクラス Runtime、メソッド addShutdownHook には次のように記されている。

「シャットダウンフック」は初期化されただけで起動していないスレッドである。仮想マシンがシャットダウンシーケンスを開始すると、すべての登録済みシャットダウンフックを、指定されていない順序で起動し並行して実行する。フックがすべて終了すると、終了時のファイナライズが有効である場合はすべての呼び出されていないファイナライザを実行する。最後に、仮想マシンは停止する。いったんシャットダウンシーケンスを起動すると、新しいシャットダウンフックを登録したり、以前に登録したフックの登録を解除したりすることはできない。

シャットダウン時にはJVMがセンシティブな状態にあるかもしれないので、いくつか注意すべき事項がある。シャットダウンフックは以下の条件を満たすべきである。

複数のシャットダウン処理の間の競合状態やデッドロックを避けるためには、単一のシャットダウンフックを使用し、1つのスレッドで複数のシャットダウン処理を順番に実行するほうがよい [Goetz 2006a]。

以下の適合コードに、シャットダウンフックを追加する標準的な方法を示す。

public class Hook {

  public static void main(String[] args) {
    try {
      final InputStream in = new FileInputStream("file");
      Runtime.getRuntime().addShutdownHook(new Thread() {
          public void run() {
            // シャットダウンイベントをログに保存し、すべてのリソースをクローズする
            in.close();
          }
      });

     // ...
    } catch (IOException x) {
      // エラー処理
    } catch (FileNotFoundException x) {
      // エラー処理
    }
  }
}

JVM は外部要因によって中断させられることがある。たとえば、SIGKILL シグナル(POSIX)や TerminateProcess コール(Windows)などが外部から送られたり、ネイティブメソッドが原因でメモリが破壊されたりした場合など。このような場合、JVMはフックが意図通りに実行されることを保証できず、シャットダウンフックは想定通りに実行されないかもしれない。

リスク評価

プログラム終了時に必要なクリーンアップが行われないと、システムが矛盾した状態に陥ることがある。

ルール 深刻度 可能性 修正コスト 優先度 レベル
FIO14-J P12 L1
関連ガイドライン
The CERT C Secure Coding Standard ERR04-C. Choose an appropriate termination strategy
The CERT C++ Secure Coding Standard ERR04-CPP. Choose an appropriate termination strategy
ISO/IEC TR 24772:2010 Termination Strategy [REU]
MITRE CWE CWE-705. Incorrect control flow scoping
参考文献
[API 06] Class Runtime
[ISO/IEC TR 24772:2010] Section 6.46, Termination Strategy [REU]
翻訳元

これは以下のページを翻訳したものです。

FIO14-J. Perform proper cleanup at program termination (revision 42)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter