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

NUM01-J. 同一のデータに対してビット演算と算術演算の両方を行わない

NUM01-J. 同一のデータに対してビット演算と算術演算の両方を行わない

整数変数は多くの場合、数値とビット集合のどちらかを表現することを意図して用いられる。数値であれば算術演算のみが、ビット集合であればビット演算のみが行われなければならない。ビット演算子には、単項演算子の~や、二項演算子の<<>>>>>&^|がある。

同一のデータに対してビット演算と算術演算の両方を行うと、変数に格納されているデータが数値を意図しているのかビット集合を意図しているのかが分からなくなるし、データの意図に混乱が生じていることの徴候であることが多い。あいにくビット演算は、数値データに対する尚早な最適化 (premature optimization) として使われることも多い。このように演算子を使用しても、コードとしては有効でありコンパイルは通るが、可読性は低下する。

違反コード (左シフト)

左シフトおよび右シフト演算子はしばしば、2のべき乗の乗算や除算を行うために使われる。この方法は実質的には高速化にはならず、かえってコードの可読性や可搬性が低下することが多い。Java仮想マシンは通常、このような最適化を自動的に行うが、プログラマと違い、プログラムが実行されるプラットフォームに合った最適化を行うことができる。以下の違反コード例では、数値を保持することを意図した整数変数xに対して、ビット演算と数値演算の両方を行っている。結果として得られる尚早な最適化が行われた式では、プログラマの意図を反映して、5x + 1という値をxに代入している。

int compute(int x) {
  x += (x << 2) + 1;
  return x;
}
// ...

int x = compute(50);
違反コード (左シフト)

次の違反コード例では、変数ごとにビット演算子と算術演算子を分けている。変数 x はビット演算のみに使用され、変数 y は算術演算のみに使用されている。

int compute(int x) {
  int y = x << 2;
  x += y + 1;
  return x;
}
// ...

int x = compute(50);

演算自体は異なる変数に対して行われているが、実際のデータに対してはビット演算と算術演算の両方が行われているため、このコード例はルールに違反している。

適合コード (左シフト)

以下の適合コードでは、数値であるxの性質を反映するように代入式を変更することで、プログラマの意図をより明確に表現している。

int compute(int x) {
  return 5 * x + 1;
}
// ...

int x = compute(50);

この変更により、演算が整数オーバフローを引き起こさないように検査すべきであることがコードレビュー担当者に明らかとなる。オーバーフロー検査の必要性は、元の違反コードでは明らかではなかった (「NUM00-J. 整数オーバーフローを検出あるいは防止する 」も参照)。

違反コード (論理右シフト)

以下の違反コード例では、プログラマは x を 4 で割ることを意図している。パフォーマンスを最適化しようとして、除算演算の代わりに右シフトを使っている。

int compute(int x) {
  x >>>= 2;
  return x;
}
// ...

int x = compute(-50);

>>>= 演算子は論理右シフト演算である。シフトする値の符号に関係なく、左側のビットにゼロを詰める。このコードを実行すると、x は非常に大きな正の値になる (具体的には0x3FFFFFF3)。被除数 (この例では x) が負の値である場合、除算演算の代わりに論理右シフトを使うと、計算結果は間違った値になる。

違反コード (算術右シフト)

以下の違反コード例では、前述の違反コード例の間違いを、算術右シフト (>>= 演算子) を使うことで修正しようとしている。

int compute(int x) {
  x >>= 2;
  return x;
}
// ...

int x = compute(-50);

このコードの実行後、x の値は -12 ではなく -13 となる。算術右シフトでは計算結果は負の無限大の方向に切り捨てられる。一方、整数除算ではゼロに向かって切り捨てられる。

適合コード (右シフト)

以下の適合コードでは、右シフトを除算に置き換えている。

int compute(int x) {
  x /= 4
  return x;
}
// ...

int x = compute(-50);
違反コード

以下の違反コード例は、バイト配列から4つの値を取り出し、それらの値を整数変数 result に詰め込むことを意図している。このコード例における整数値は、数値ではなく、ビット集合を表現している。

// b[] は値 0xff に初期化されたバイト配列
byte[] b = new byte[] {-1, -1, -1, -1};
int result = 0;
for (int i = 0; i < 4; i++) {
  result = ((result << 8) + b[i]);
}

ビット演算において、バイト配列の要素 b[i] の値は符号拡張によって int に格上げされる。もしバイト配列の要素に負の値(たとえば 0xff)が入っていると、符号拡張により、符号ビットの 1intの上位24ビットに伝播する。byte が符号無し型であると想定している場合、この動作は予期せぬものであろう。このコード例では、格上げされた byte 値を result に加算するところで、バイト列を整数に詰め込んだ表現になっていない [FindBugs 2008]。

数値をバイト列にシリアライズする目的で同様の計算を行う方法についての詳細はNUM01-J-EX1を参照。

違反コード

以下の違反コード例では、加算を行う前に、格上げしたバイト配列の要素の上位24ビットをマスクしている。byteint をマスクするために必要なビット数は『Java 言語仕様』で規定されている。このコードは正しい結果を計算するが、同一のデータにビット演算と算術演算を行っている点でルールに違反している。

byte[] b = new byte[] {-1, -1, -1, -1};
int result = 0;
for (int i = 0; i < 4; i++) {
  result = ((result << 8) + (b[i] & 0xff));
}
適合コード

以下の適合コードでは、格上げしたバイト配列の要素の上位24ビットをマスクし、その計算結果を result の値と論理 OR 演算している。

byte[] b = new byte[] {-1, -1, -1, -1};
int result = 0;
for (int i = 0; i < 4; i++) {
  result = ((result << 8) | (b[i] & 0xff));
}

数値をバイト列にシリアライズする目的で同様の計算を行う方法についての詳細はNUM01-J-EX1を参照。

例外

NUM01-J-EX0: 定数式を構成するためであれば、ビット演算を使ってもよい。

int limit = (1 << 17) - 1; // 2^17 - 1 = 131071

コーディングスタイル上の問題ではあるが、上記のような定数式よりは16進定数として記述する方が望ましい。

int limit = 0x1FFFF; // 2^17 - 1 = 131071

NUM01-J-EX1: 通常は算術的に扱われるデータであっても、シリアライズ (serialization) や復元 (deserialization) の際にはビット演算を行ってよい。このような扱いはファイルやネットワークソケットからのデータ読み書きで必要となることが多い。また、バイトデータに詰め込まれたデータの読み書きにおいてもビット演算が許される。

int value = /* 目的の値 */
Byte[] bytes = new Byte[4];
for (int i = 0; i < bytes.length; i++) {
  bytes[i] = value >> (i*8) & 0xFF;
}
/* bytes[] は value と同じビット表現になっている */
リスク評価

同一の変数に対してビット演算と算術演算の両方を行うと、プログラマの意図が分かりにくくなり、コードの可読性が低下する。その結果、セキュリティ監査担当者やコードの保守担当者にとっては、脆弱性を排除し、データの完全性を保証するためにどのような検査を行うべきか判断することが困難になる。たとえば、オーバフローの検査は、数値演算を適用する数値データに対しては重要であるが、ビット演算のみを行う数値データに対してはそれほど重要ではない。

ルール

深刻度

可能性

自動検出

自動修正

優先度

レベル

NUM01-J

不可

P4

L3

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

2024.2

CERT.NUM01.BADSHIFT
CERT.NUM01.NCBAV
Avoid incorrect shift operations
Do not perform bitwise and arithmetic operations on the same data
関連ガイドライン

SEI CERT C コーディングスタンダード

INT14-C. 同じデータに対してビット単位の演算と算術演算を行わない

SEI CERT C++ Coding Standard

VOID INT14-CPP. Avoid performing bitwise and arithmetic operations on the same data

参考文献
[FindBugs 2008]
[Seacord 2015] Image result for video icon NUM01-J. Do not perform bitwise and arithmetic operations on the same data LiveLesson

[Steele 1977]


翻訳元

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

NUM01-J. Do not perform bitwise and arithmetic operations on the same data (revision 92)

Top へ

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