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

MET12-J. ファイナライザは使わない

ガベージコレクタはオブジェクトが到達不能であると判断してから、そのメモリ領域を回収するまでの間に、オブジェクトのファイナライザメソッドを呼び出す。ファイナライザメソッドを実行することで、ストリーム、ファイル、ネットワーク接続といった、ガベージコレクタの通常動作では自動的に解放されないかもしれないリソースを解放することができる。

ファイナライザには様々な問題が存在するため、例外的場合にのみ使うべきである。以下にそれらの問題を挙げる。

こういった問題が存在するため、新規に開発するクラスでファイナライザを使うべきでない。

違反コード (スーパークラスの finalizer())

ファイナライザを使用するスーパークラスを拡張する際には、さらなる制限が課される。JDK 1.5およびそれ以前のバージョンで問題になった例について考えてみよう。以下の違反コードでは、Swing の JFrame オブジェクトのための16MBのバッファを割り当てている。JFrameのAPIにはfinalize()メソッドは実装されていないが、JFrame が継承する AWT.Frame には、finalize() が実装されている。MyFrameオブジェクトが到達不能になった場合、JFrameによって継承されたfinalize()メソッドの中でバイト配列のバッファを参照しているかもしれないため、ガベージコレクタはこのバッファのメモリ領域を回収することができない。つまり、クラスMyFrameに継承されたfinalize()メソッドの実行が少なくとも完了するまでの間は、バッファは存在し続けるに違いなく、次のガベージコレクションのサイクルまで回収できない。

class MyFrame extends JFrame {
  private byte[] buffer = new byte[16 * 1024 * 1024];
  // 少なくとも2度のガベージコレクションサイクルの間存在し続ける
}
適合コード (スーパークラスの finalizer())

スーパークラスでfinalize()メソッドを定義する場合、ただちにガベージコレクション可能なオブジェクトと、ファイナライザの終了に依存するオブジェクトは明確に分けるべきである。以下に示す解決法では、bufferは到達不能になれば直ちに回収することができる。

class MyFrame {
  private JFrame frame;
  private byte[] buffer = new byte[16 * 1024 * 1024]; // 分離している
}
違反コード (System.runFinalizersOnExit())

以下の違反コードでは、System.runFinalizersOnExit()メソッドを使用し、ガベージコレクションを独自に実行している。このメソッドはスレッド安全性上の問題から廃止されていることに注意。詳細は「MET02-J. 非推奨(deprecated)あるいは廃止された(obsolete)クラスやメソッドを使用しない」を参照。

Java API [API 2006]には System クラスの runFinalizersOnExit()メソッドについて、以下のように記述されている。

終了時のファイナライズを有効または無効にする。これを実行することによって、自動的に呼び出されていないファイナライザを持つすべてのオブジェクトのファイナライザが呼び出され、Java Runtime の終了前に実行されるようになる。デフォルトでは終了時のファイナライズは無効である。

クラスSubClassprotected finalize()メソッドをオーバーライドしており、クリーンアップ処理を行う。このファイナライザはsuper.finalize()を呼び出し、スーパークラスもファイナライズされることを保証している。BaseClassは、SubClassにおいてオーバーライドされているdoLogic()メソッドを呼び出してしまう。この呼出しによりSubClassへの参照が復活し、SubClass がガベージコレクションされるのを妨げるのみならず、SubClassのファイナライザが呼び出されなくなり、ファイナライザでリリースされるべきリソースがリリースされなくなる。「MET05-J. コンストラクタにおいてオーバーライド可能なメソッドを呼び出さない」で詳しく説明したように、サブクラスのファイナライザが重要なリソースをリリースした後で、スーパクラスからサブクラスのメソッドを呼び出すと、オブジェクトが不整合な状態になってしまうかもしれない。場合によっては、NullPointerExceptionを引き起こす。

class BaseClass {
  protected void finalize() throws Throwable {
    System.out.println("Superclass finalize!");
    doLogic();
  }

  public void doLogic() throws Throwable {
    System.out.println("This is super-class!");
  }
}

class SubClass extends BaseClass {
  private Date d; // 可変インスタンスフィールド

  protected SubClass() {
    d = new Date();
  }

  protected void finalize() throws Throwable {
    System.out.println("Subclass finalize!");
    try {
      //  リソースをクリアする
      d = null;
    } finally {
      super.finalize();  // BaseClass のファイナライザを呼び出す
    }
  }

  public void doLogic() throws Throwable {
    // ここで割り当てられたリソースは回収されない

    // オブジェクトの矛盾した状態
    System.out.println(
        "This is sub-class! The date object is: " + d);
    // 'd' は既に null
  }
}

public class BadUse {
  public static void main(String[] args) {
    try {
      BaseClass bc = new SubClass();
      // ファイナライザの動作をシミュレートする
      System.runFinalizersOnExit(true);
    } catch (Throwable t) {
      // エラー処理
    }
  }
}

このコードは以下を出力する

Subclass finalize!
Superclass finalize!
This is sub-class! The date object is: null
適合コード

Joshua Bloch [Bloch 2008] は、クラスを、その生存期間を越えたら使用不可能な状態にするような stop() メソッドを明示的に実装することを提唱している。クラスのプライベートフィールドを使ってそのクラスが使用不可能であることを表すことができる。すべてのクラスメソッドは、クラスに対する操作を行う前にまず、このフィールドをチェックしなくてはならない。この手法は、「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」の適合コードで取り上げた「初期化フラグ」を使った手法と同じである。

例外

MET12-EX0: ネイティブコードを使ったプログラムを作成する場合にはファイナライザを使用してもよい。その理由は、他の言語で書かれたコードによって使用されるメモリをガベージコレクションすることは出来ないからであり、オブジェクトの生存期間も分からないからである。繰り返しになるが、ネイティブプロセスにおいて、すぐにリソースを解放する必要があるような重要なジョブを実行すべきではない。

finalize()をオーバーライドするサブクラスは、スーパークラスのファイナライザも明示的に呼び出さなくてはならない。finalize()が自動的に連鎖して呼び出されることはない。これを正しく行う方法を以下に示す。

protected void finalize() throws Throwable {
  try {
    //...
  } finally {
    super.finalize();
  }
}

よりコストのかかる解決法としては、アノニマスクラスを宣言し、finalize()メソッドがスーパークラスに対して実行されることを保証するというやり方もある。この解決法はfinal宣言されていないパブリッククラスに適用できる。「Finalizer Guardian を使用することによって、finalize をオーバーライドしたサブクラスが明示的に super.finalize を呼び出していない場合であっても、super.finazlie が強制的に呼び出されるようになる」[JLS 2005]。

public class Foo {
  // finalizeGuardian オブジェクトは、外側の Foo オブジェクトをファイナライズする。
  private final Object finalizerGuardian = new Object() {
    protected void finalize() throws Throwable {
      // 外側の Foo オブジェクトをファイナライズ
    }
  };
  //...
}

ネイティブコードを使用する場合、ファイナライズの順序が問題になる場合がある。たとえば、オブジェクトAがオブジェクトBを(直接あるいはリフレクションにより)参照しており、Bが先にファイナライズされる場合、Aのファイナライザはネイティブコードのダングリングポインタを参照することになるかもしれない。ファイナライザを決まった順序で呼び出すためには、Aのファイナライザが終了するまでBが到達可能であることを保証する。これを実現するには、グローバルな状態変数にBへの参照を保存しておき、Aのファイナライザが実行されたらこれを削除する。java.lang.refの参照を使用するというやり方もある。

MET12-EX1: 「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」で詳しく説明したように、ファイナライザ攻撃を防止するために、クラスに空のファイナライザを用意することがある。

リスク評価

ファイナライザを誤用すると、ガベージコレクションされるはずのオブジェクトが回収されなくなり、サービス運用妨害の脆弱性につながる恐れがある。

ルール 深刻度 可能性 修正コスト 優先度 レベル
MET12-J P8 L2
関連する脆弱性

AXIS2-4163 には、Axis ウェブサービスフレームワークのfinalize()メソッドの脆弱性について記されている。ファイナライザが、クリーンアップ処理を行う前に誤ってsuper.finalize()を呼び出しているため、ガベージコレクタが実行されるとき、GlassFishの中でエラーが発生する。

関連ガイドライン
MITRE CWE CWE-586. Explicit call to Finalize()
  CWE-583. finalize() method declared public
  CWE-568. finalize() method without super.finalize()
参考文献
[API 2006] finalize()
[Bloch 2008] Item 7. Avoid finalizers
[Boehm 2005]  
[Coomes 2007] "Sneaky" Memory Retention
[Darwin 2004] Section 9.5, The Finalize Method
[Flanagan 2005] Section 3.3, Destroying and Finalizing Objects
[JLS 2005] §12.6, Finalization of Class Instances
翻訳元

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

MET12-J. Do not use finalizers (revision 118)

Top へ

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