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

EXP05-J. ひとつの式の中で同じ変数に2回以上書込みを行わない

Java言語仕様 15.7 「評価順序」には以下のように規定されている。

プログラミング言語Javaでは、演算子のオペランドは特定の順序(evaluation order)、つまり左から右へと評価されることが保証されている。

同じくJava言語仕様 15.7.3 「評価には括弧や優先順位が考慮される」には以下のように規定されている。

プログラミング言語Javaの実装では、括弧によって明示的に、また演算子の評価順序によって暗黙のうちに示される順序に従って評価を行わなければならない。

式に副作用が存在する場合、これら2つの要件は驚くべき結果をもたらす。オペランドの評価は、演算子の優先順位や評価順序をあらわす括弧の存在に関係なく、左から右へと行われる。しかし、「演算子」の評価は、その優先順位と括弧に従って行われる。

同一の式で、メモリに書き込み同時にそのメモリをリードしてはならない。また、同一の式で2度以上書込みを行ってもならない。メモリの読み書きは、代入式によって直接発生することもあれば、式中で呼ばれたメソッドの持つ副作用によって間接的に発生することもある。

違反コード (評価順序)

以下の違反コード例は、式中の副作用がどのように予期せぬ結果につながるかを示している。プログラマの意図は、閾値(threshold)に基づいたアクセス制御ロジックを実装することにある。各ユーザには点数が割り当てられ、この値が閾値より高いとアクセスが許可される。見ての通り、点数は単純なメソッドで計算することができる。get()メソッドは、アクセスを許可するユーザにはゼロ以外の値を返し、許可しないユーザにはゼロを返すことを意図している。

*演算子は+演算子よりも評価の優先順位が高いため、プログラマは最も右の部分式が最初に評価されること期待する。式が括弧でくくられているため、その期待はより確かであるように思える。これはしかし、get()メソッドがゼロを返すとき右辺が必ずゼロに評価されるという間違った結論に導いてしまう。最も右の部分式がnumber = get()であるため、プログラマは、numberにゼロが代入されることを期待するのである。それゆえ、左辺の部分式における条件式で、number の値が閾値の10より小さくなるため、許可されないユーザは拒否されると考える。

しかし、このプログラムは許可されていないユーザのアクセスを許してしまう。副作用を持つ部分式が左から右への評価順序に従うからである。

class BadPrecedence {
  public static void main(String[] args) {
    int number = 17;
    int[] threshold = new int[20];
    threshold[0] = 10;
    number = (number > threshold[0]? 0 : -2) 
             + ((31 * ++number) * (number = get()));
    // ... 
    if(number == 0) {
      System.out.println("Access granted");
    } else {
      System.out.println("Denied access"); // number = -2
    }
  }
  public static int get() {
    int number = 0;
    // 許可される場合は非ゼロを、それ以外の場合はゼロを代入する
    return number;
  }
}
違反コード (評価順序)

以下の違反コード例では、左から右へのオペランドの評価順序がプログラマの意図に沿うように、前述の式の順序を入れ替えている。

このコードは期待通りに動作するが、1つの式で3回numberに書き込むという杜撰なコードになっている。

int number = 17;
 
number = ((31 * ++number) * (number=get())) + (number > threshold[0]? 0 : -2);
適合コード (評価順序)

以下の適合コードでは、同じ処理を行うコードを副作用が存在しないように実装している。numberへの書き込みは1つの式につき1度しか行われない。最終的に得られる式は、部分式の評価順序を気にすることなく順序を入れ替えることができ、コードは理解しやすく保守しやすくなっている。

int number = 17;

final int authnum = get();
number = ((31 * (number + 1)) * authnum) + (authnum > threshold[0]? 0 : -2);
例外

EXP05-EX0: 増分演算子(++)と減分演算子(--)は、変数から値を読み込み、新しい値を変数に格納する。この動作は広く理解されており、このルールの例外とする。

EXP05-EX1: 論理演算子||&&はショートサーキット動作することが広く知られており、これらの演算子を含む式はこのルールの例外とする。以下のコードについて考えてみよう。

public void exampleFunction(InputStream in) {
  int i;
  // 1文字スキップして、次を処理する
  while ((i = in.read()) != -1 && (i = in.read()) != -1) {
    // ...
  }
 
}

条件式はこのルールに違反しているように見えるが、このコードはルールに違反しない。&&演算子の両辺の部分式がルールに違反していないからである。どちらの部分式も代入は1回だけ、副作用もひとつだけしか持たない(inからの文字の読み取り)。

リスク評価

副作用を持つ式の評価順序を理解せずにコードを書くと、期待と異なる結果が得られる可能性がある。

ルール 深刻度 可能性 修正コスト 優先度 レベル
EXP05-J P2 L3
自動検出

副作用が存在し、かつ演算子の優先度レベルが複数あるような式をすべて検出することは容易である。しかし、そのような正しさを判断することは一般に不可能である。ヒューリスティックな警告が役に立つ場合もあるだろう。

関連ガイドライン
CERT C Secure Coding Standard EXP30-C. Do not depend on order of evaluation between sequence points
CERT C++ Secure Coding Standard EXP30-CPP. Do not depend on order of evaluation between sequence points
ISO/IEC TR 24772:2010 Side-effects and Order of Evaluation [SAM]
参考文献
[JLS 2005] §15.7, Evaluation Order
  §15.7.3, Evaluation Respects Parentheses and Precedence
翻訳元

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

EXP05-J. Do not write more than once to the same variable within an expression (revision 85)

Top へ

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