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

DCL00-J. クラスの初期化を循環させない

DCL00-J. クラスの初期化を循環させない

『Java言語仕様』§12.4「クラスやインタフェースの初期化」には次のように記されている[JLS 2005]。

クラスの初期化(initialization)とは、その静的初期化子とそのクラスの中に宣言されているstaticフィールド(クラス変数)に対する初期化子の実行からなる処理である。

したがって、staticフィールドが存在すると、クラスの初期化が開始される。しかし、staticフィールドの初期化子は、別のクラスが初期化されていることに依存する場合が考えられ、その場合、初期化が循環することになる。

Java言語仕様§8.3.2.1「クラス変数の初期化子」には次のように記されている[JLS 2005]。

static変数がfinalとして宣言されており、かつコンパイル時の定数値で初期化される場合、それは実行時に最初に初期化される。

この一文は、後で初期化されるstatic finalフィールドの値を使用するインスタンスには当てはまらない。フィールドを static final 宣言するだけでは、値が読み取られる前に完全に初期化されることを保証できない。

プログラム一般、セキュリティに配慮すべきプログラムでは特に、クラスの初期化を循環させてはならない。

違反コード (クラス内循環)

以下の違反コード例には、クラス内の初期化循環がある。

public class Cycle {
  private final int balance;
  private static final Cycle c = new Cycle();
  private static final int deposit = (int) (Math.random() * 100); // ランダムな金額を口座に預金

  public Cycle() {
    balance = deposit - 10; // 預金額から手数料を引く
  }

  public static void main(String[] args) {
    System.out.println("The account balance is: " + c.balance);
  }
}

Cycleクラスでは private static final クラス変数を宣言しており、このクラス変数はCycleクラスのインスタンスに初期化される。静的初期化子(static initializer)は、static 宣言されたクラスメンバやコンストラクタの最初の呼び出しより前に1度だけ呼び出されることが保証されている。

プログラマの意図は、預金額(deposit)から手数料(10)を引き、口座の残額(balance)を計算することにある。しかし、クラス変数cの初期化はdepositフィールドが初期化される前に行われる。なぜなら、クラス変数cの初期化はソースコード上、depositフィールドより前に現れるからである。したがって、cの静的初期化時に呼び出されるコンストラクタから見えるdepositの値は、初期値0であって、ランダムな値ではない。結果として、残額(balance)の計算結果は常に-10になってしまう。

Java言語仕様§12.4.2で説明されている詳細な初期化手順のステップ3では、このような再帰的な初期化の循環を実装上無視してもよいと認めている[JLS 2014]。

適合コード (クラス内循環)

以下の適合コードでは、クラスCycleのメンバフィールドの初期化順序を変更し、初期化の循環を発生させずにフィールドが初期化されるようにしている。具体的には、ソースコード上、cの初期化をdepositの初期化の後に置き、depositの初期化が完了した後でクラスが初期化されるようにしている。

public class Cycle {
  private final int balance;
  private static final int deposit = (int) (Math.random() * 100); // Random deposit
  private static final Cycle c = new Cycle();  // 必要なフィールドが初期化された後で実行される
  public Cycle() {
    balance = deposit - 10; // 預金額から手数料を引く
  }

  public static void main(String[] args) {
    System.out.println("The account balance is: " + c.balance);
  }
}

このような初期化循環の問題は、関係するフィールドが増えるほど気づかれにくくなるため、プログラムの制御フローにこのような循環が発生しないようにすることは重要である。

上述の適合コードのように修正すれば初期化循環は防げるが、宣言の順序に依存しており脆弱である。コードを後日メンテナンスするプログラマは、プログラムが正しく動作するにはこの宣言順序を保たなくてはならないことに気づかないかもしれない。したがって、このような依存性が存在することはコードの中にはっきりコメントとして残しておくべきである。

違反コード (クラス間循環)

以下の違反コード例では、静的変数を持つ2つのクラスを宣言しており、これらの変数は互いに依存している。2つのクラスをあわせて眺めると循環が存在することは明白だが、別々に眺めれば容易に見落としてしまうであろう。

class A {
  public static final int a = B.b + 1;
  // ...
}

class B {
  public static final int b = A.a + 1;
  // ...
}

クラスの初期化順序が異なれば、計算されるA.aB.bの値も異なる。クラスAが先に初期化される場合、A.aの値は2になり、B.bの値は1になる。クラスBが先に初期化される場合、値は逆になる。

適合コード (クラス間循環)

以下の適合コードでは、変数の依存関係を解消することでクラス間の循環を断ち切っている。

class A {
  public static final int a = 2;
  // ...
}

class B {
  public static final int b = A.a + 1;
  // ...
}

循環が発生しないため、どちらのクラスが先に初期化されるかに関係なく、初期値は常にA.a = 2B.b = 3になる。

違反コード

以下の違反コード例のプログラマは、1つ目のクラスの静的変数を2つ目のクラスの静的メソッドにより初期化しようとしているが、そのメソッドは1つ目のクラスの静的メソッドに依存している。

class A {
  public static int a = B.b();
  public static int c() { return 1; }
}

class B {
  public static int b() { return A.c(); }
}

このコードは、Oracle JVMでは、AとBのどちらが先にロードされるかに関わらず、A.aが正しく1に初期化される。しかし、Java言語仕様はA.aが正しく初期化されることを保証していない。さらに、この初期化循環により、システムの保守性は損なわれ、変更時に予期せぬ壊れ方をしやすくなる。

適合コード

以下の適合コードでは、c()メソッドをBクラスで実装し、循環を断ち切っている。

class A {
  public static int a = B.b();
}

class B {
  public static int b() { return B.c(); }
  public static int c() { return 1; }
}
リスク評価

初期化循環は予期せぬ結果をもたらす可能性がある。

ルール

深刻度

可能性

自動検出

自動修正

優先度

レベル

DCL00-J

不可

P2

L3

自動検出
ツール バージョン チェッカー 説明
CodeSonar

9.0p0

JAVA.STRUCT.SE.ASSERT
JAVA.STRUCT.UA

JAVA.STRUCT.UA.DEFAULT
Assertion contains side effects
Useless assignment
Useless assignment to default
Parasoft Jtest 2024.2 CERT.DCL00.ACD Ensure that files do not contain cyclical dependencies
PVS-Studio

7.38

V6050
SonarQube 9.9

S2390

Classes should not access their own subclasses during initialization
関連ガイドライン

SEI CERT C++ Coding Standard

VOID DCL14-CPP. Avoid assumptions about the initialization order between translation units

ISO/IEC TR 24772:2010

Initialization of Variables [LAV]

MITRE CWE

CWE-665, Improper Initialization

Bibliography

[Bloch 2005]

Puzzle 49, "Larger than Life"

[JLS 2005]

§8.3.2.1, "Initializers for Class Variables"

§12.4, "Initialization of Classes and Interfaces"
[Seacord 2015]

Image result for video icon DCL00-J. Prevent class initialization cycles LiveLesson

翻訳元

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

DCL00-J. Prevent class initialization cycles (revision 147)

Top へ

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