INT32-C. 符号付き整数演算がオーバーフローを引き起こさないことを保証する
整数オーバーフローは未定義の動作である。つまり、処理系は、符号付き整数オーバーフローをかなり自由に取り扱うことができるということである (「MSC15-C. 未定義の動作に依存しない」を参照)。たとえば、符号付き整数型を剰余として定義する処理系では、整数オーバーフローを検知する必要がない。処理系によっては、符号付き算術オーバーフローをトラップを生成したり、オーバーフローは決して発生しないと仮定した上でオブジェクトコードを生成することもできる。あるいは、コンテキストが異なれば違う動作をするコードを生成するかもしれない。たとえば、ローカルスコープで宣言された符号付き整数のループ変数がオーバーフローすることはありえない判断して性能の良いコードを生成する一方で、グローバル変数の場合はラップすると判断するような処理系も考えられる。
これらの理由から、符号付き整数に対する演算がオーバーフローを引き起こさないことを保証するのは重要である。中でも特に重要なのは、プログラムの外部から取得した次のような符号付き整数を使った演算である。
- ポインタ演算の整数オペランド (配列インデックス操作を含む)
- 可変長配列を宣言ときの代入式
- 角括弧
[]を後ろに伴う後置式、もしくは、配列オブジェクトの要素の添字による指定を行う角括弧[]の中の式 size_tもしくはrsize_t型を持つ関数の引数 (たとえばメモリ割り当て関数の引数)
整数演算の結果の値がその整数の表現形式で表現できないときにオーバーフローが発生する。オーバーフローを引き起こす可能性のある演算がどれであるかを次の表に示す。
|
演算子 |
オーバーフロー |
演算子 |
オーバーフロー |
演算子 |
オーバーフロー |
演算子 |
オーバーフロー |
|---|---|---|---|---|---|---|---|
|
|
する |
|
する |
|
する |
|
しない |
|
|
する |
|
する |
|
しない |
|
しない |
|
|
する |
|
する |
|
しない |
|
しない |
|
|
する |
|
する |
|
しない |
|
しない |
|
|
する |
|
する |
|
しない |
|
しない |
|
|
する |
|
しない |
|
しない |
|
しない |
|
|
する |
|
しない |
|
しない |
|
しない |
|
|
しない |
|
しない |
|
しない |
|
しない |
|
|
する |
|
しない |
|
する |
|
しない |
以降のセクションでは、整数オーバーフローを引き起こしやすい個々の演算について見ていく。int よりも小さい型の演算には整数拡張が適用される。また、算術演算が行われる前に、通常の算術型変換が適用され、オペランドが(暗黙的に)同等の型に変換される場合もある。セキュアな算術演算を実装する前に、整数変換のルールを理解しておくこと (「INT02-C. 整数変換のルールを理解する」を参照)。
処理系固有の詳細
GNU GCC の -fwrapv オプションを指定すると、符号無し整数と符号付き整数の両方で剰余演算を行う。
GNU GCC の -ftrapv オプションを指定すると、符号付き整数のオーバーフローが発生したときにトラップを生成する。トラップが発生すると、多くの場合プログラムは異常終了する。UNIX システムでは、そのような場合にシグナルが生成され、プロセスに送られる。
-fwrapv や -ftrapv を指定せずに GNU GCC を呼び出すと、符号付き整数はオーバーフローしないと仮定したオブジェクトコードを生成することになる。
アトミック整数
C 標準規格は符号付き atomic 整数の動作を規定している。規定の動作は、2の補数表現を使用している場は合オーバーフローの発生時に何事もなかったかのようにラップアラウンドする。つまり未定義の結果を生成しないというものである。動作は規定されているが、期待した結果ではなく符号無し整数の場合と同様のリスクが生じる恐れがある (「INT30-C. 符号無し整数の演算結果がラップアラウンドしないようにする」を参照)。つまり、符号付き atomic 整数のオーバーフローについても防止もしくは検知すべきである。
加算
加算は、2つの算術型のオペランドの間で、あるいはポインター型と整数型の間で行われる。本ルールは2つの算術型のオペランド同士の演算のみを対象とする(「ARR37-C. 配列以外のオブジェクトを指すポインタに対して整数の加算や減算を行わない」と「ARR30-C. 境界外を指すポインタや配列添字を生成したり使用したりしない」を参照)。
増分(incrementing)は 1 を足すのと同じである。
違反コード
次の違反コードは、符号付きオペランド si_a と si_b を加算するときに、符号付き整数オーバーフローが発生する可能性がある。
void func(signed int si_a, signed int si_b) {
signed int sum = si_a + si_b;
/* ... */
}
適合コード
次のコード例では、どのような整数表現形式を使用していても、加算でオーバーフローが発生しないことを保証している。
#include <limits.h>
void f(signed int si_a, signed int si_b) {
signed int sum;
if (((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
((si_b < 0) && (si_a < (INT_MIN - si_b)))) {
/* エラー処理 */
} else {
sum = si_a + si_b;
}
/* ... */
}
減算
減算は、2つの算術型オペランドの間、適合するオブジェクト型の修飾版または非修飾版へのポインタの間、またはオブジェクト型へのポインタと整数型の間で行われる。本ルールは算術型を持つオペランド同士の演算のみを対象とする。ポインタ演算に関しては次に挙げるルールを参照。「ARR36-C. 異なる配列を指す2つのポインタに対して減算や比較を行わない」、「ARR37-C. 配列以外のオブジェクトを指すポインタに対して整数の加算や減算を行わない」、「ARR38-C. 結果の値が有効な配列要素を指さないような、ポインタに対する整数の加算や減算を行わない」
減分 (decrementing) は、1 を引くのと同じである。
違反コード
次のコードは、符号付きオペランド si_a と si_b を減算するときに、符号付き整数オーバーフローを引き起こす可能性がある。
void func(signed int si_a, signed int si_b) {
signed int diff = si_a - si_b;
/* ... */
}
適合コード
このコードでは、減算のオペランドを事前に確認することで、整数の表現形式に関係なく、符号付き整数オーバーフローが発生しないことを保証している。
#include <limits.h>
void func(signed int si_a, signed int si_b) {
signed int diff;
if ((si_b > 0 && si_a < INT_MIN + si_b) ||
(si_b < 0 && si_a > INT_MAX + si_b)) {
/* エラー処理 */
} else {
diff = si_a - si_b;
}
/* ... */
}
乗算
乗算は2つの算術型オペランド間で行われる。
違反コード
次のコードは、符号付きのオペランド si_a と si_b を乗算するとき、符号付き整数オーバーフローを引き起こす可能性がある。
void func(signed int si_a, signed int si_b) {
signed int result = si_a * si_b;
/* ... */
}
適合コード
次の適合コードでは、long long が少なくとも int の2倍の幅を持つシステムにおいて、符号付き整数オーバーフローが発生しないことを保証している。
#include <stddef.h>
#include <assert.h>
#include <limits.h>
#include <inttypes.h>
extern size_t popcount(uintmax_t);
#define PRECISION(umax_value) popcount(umax_value)
void func(signed int si_a, signed int si_b) {
signed int result;
signed long long tmp;
assert(PRECISION(ULLONG_MAX) >= 2 * PRECISION(UINT_MAX));
tmp = (signed long long)si_a * (signed long long)si_b;
/*
* 積を32ビット整数で表現できない場合を
* エラー条件として処理する
*/
if ((tmp > INT_MAX) || (tmp < INT_MIN)) {
/* エラー処理 */
} else {
result = (int)tmp;
}
/* ... */
}
long long が int の2倍の精度を持たないとき、静的アサートは失敗する。PRECISION() マクロと popcount() 関数はいかなる整数型に対しても適切な精度を適用する。
適合コード
次のコードは int の少なくとの2倍の精度を持つ整数型が存在しない処理系も含む全ての規格合致処理系において使用することのできる移植性のある手法である。
#include <limits.h>
void func(signed int si_a, signed int si_b) {
signed int result;
if (si_a > 0) { /* si_a は正 */
if (si_b > 0) { /* si_a と si_b は正 */
if (si_a > (INT_MAX / si_b)) {
/* エラー処理 */
}
} else { /* si_a は正、 si_b はゼロ以下 */
if (si_b < (INT_MIN / si_a)) {
/* エラー処理 */
}
} /* si_a は正、si_b はゼロ以下 */
} else { /* si_a はゼロ以下 */
if (si_b > 0) { /* si_a is nonpositive, si_b is positive */
if (si_a < (INT_MIN / si_b)) {
/* エラー処理 */
}
} else { /* si_a と si_b はゼロ以下 */
if ( (si_a != 0) && (si_b < (INT_MAX / si_a))) {
/* エラー処理 */
}
}
}
result = si_a * si_b;
}
除算
除算は2つの算術型オペランドを使って行われる。被除数が符号付き整数型の最小値(負の値)に等しく、かつ除数が −1 に等しいときに2の補数の符号付き整数の除算を行うときにオーバーフローが発生する。除算演算はまた、ゼロ除算エラーにつながる可能性がある(「INT33-C. 除算および剰余演算がゼロ除算エラーを引き起こさないことを保証する」を参照)。
違反コード
次の違反コードは「INT33-C. 除算および剰余演算がゼロ除算エラーを引き起こさないことを保証する」に従いゼロ除算を防止しているが、2の補数表現における符号付き整数のオーバーフローは防止していない。
void func(signed long s_a, signed long s_b) {
signed long result;
if (s_b == 0) {
/* Handle error */
} else {
result = s_a / s_b;
}
/* ... */
}
処理系固有の詳細
x86-32 アーキテクチャでは、オーバーフローからフォールトが発生するため、サービス運用妨害攻撃に悪用される恐れがある。
適合コード
次の適合コードでは、符号付き整数オーバーフローもゼロ除算エラーも発生しないことを保証している。
#include <limits.h>
void func(signed long s_a, signed long s_b) {
signed long result;
if ((s_b == 0) || ((s_a == LONG_MIN) && (s_b == -1))) {
/* エラー処理 */
} else {
result = s_a / s_b;
}
/* ... */
}
剰余演算
剰余演算子は2つの符号付き整数を除算したときの余りを計算する。多くの処理系は剰余演算に除算と同じ命令を使用するため、剰余演算子もまたオーバーフローやゼロ除算の影響を受ける (「INT33-C. 除算および剰余演算がゼロ除算エラーを引き起こさないことを保証する」を参照)。
違反コード
多くのアーキテクチャでは剰余を除算オペレータの一種として実装しているため、オーバーフローを引き起こす可能性がある。被除数が符号付き整数型の最小値(負の値)、除数が −1 のときに剰余演算を行うとオーバーフローが発生する。数学的には 0 になる計算だが、計算機上ではオーバーフローが発生してしまうのである。次の違反コード例は「INT33-C. 除算および剰余演算がゼロ除算エラーを引き起こさないことを保証する」に従いゼロ除算の発生は防いでいるが、オーバーフローは発生してしまう。
void func(signed long s_a, signed long s_b) {
signed long result;
if (s_b == 0) {
/* エラー処理 */
} else {
result = s_a % s_b;
}
/* ... */
}
処理系固有の詳細
x86プラットフォームにおける符号付き整数オペランドに対する剰余演算は、除算オペレータと同様に idiv 命令を使って実装されている。LONG_MIN / -1 はオーバーフローを引き起こすため、このコードは LONG_MIN % -1 演算時に浮動小数点例外を通知する。
Microsoft Visual Studio 2013 において LONG_MIN を −1 で割ったときの剰余を計算すると、x86 および x64 アーキテクチャにおいてプログラムは異常終了する。GCC/Linux では浮動小数点例外を生成する。しかし、GCC 4.2.4 以降で最適化オプションを有効にした場合、LONG_MIN を −1 で剰余演算すると結果は 0 になる。
適合コード
次の適合コードでは、剰余演算のオペランドを確認し、オーバーフローエラーが発生しないことを保証している。
#include <limits.h>
void func(signed long s_a, signed long s_b) {
signed long result;
if ((s_b == 0 ) || ((s_a == LONG_MIN) && (s_b == -1))) {
/* エラー処理 */
} else {
result = s_a % s_b;
}
/* ... */
}
左シフト演算子
左シフト演算子は2つの整数オペランドを取る。E1 << E2 の結果は E1 をE2 ビット分左にシフトした値であり、空いたビットは 0 で埋められる。
C 標準規格の6.5.7節、第4パラグラフには次のように書かれている [ISO/IEC 9899:2011]。
E1が符号付き整数型と非負の値をもち、E1 × 2E2が結果の型で表現可能である場合、それが結果の値となる。それ以外の場合、その動作は未定義とする。
ほとんどすべての場合、負のビット数のシフトやオペランドのビット数以上のシフトはロジック上のエラーを意味する。この問題は「INT34-C. 負のビット数、あるいはオペランドのビット数以上シフトしない」で取りあげる。
違反コード
次の違反コードは、シフトされる数が負の値でなく、シフトするビット数が正しいことを確認してから左シフト演算を行う。マクロ PRECISION() と popcount() 関数はすべての整数型に対して正しい精度で演算する (「INT35-C. Use correct integer precisions」を参照)。このコードはオーバーフローのチェックを行っていないため、予期しない結果が発生することがある。
#include <limits.h>
#include <stddef.h>
#include <inttypes.h>
extern size_t popcount(uintmax_t);
#define PRECISION(umax_value) popcount(umax_value)
void func(signed long si_a, signed long si_b) {
signed long result;
if ((si_a < 0) || (si_b < 0) ||
(si_b >= PRECISION(ULONG_MAX)) {
/* エラー処理 */
} else {
result = si_a << si_b;
}
/* ... */
}
適合コード
次の適合コードは左シフト演算でオーバーフローが発生する可能性を排除している。
#include <limits.h>
#include <stddef.h>
#include <inttypes.h>
extern size_t popcount(uintmax_t);
#define PRECISION(umax_value) popcount(umax_value)
void func(signed long si_a, signed long si_b) {
signed long result;
if ((si_a < 0) || (si_b < 0) ||
(si_b >= PRECISION(ULONG_MAX)) ||
(si_a > (LONG_MAX >> si_b))) {
/* エラー処理 */
} else {
result = si_a << si_b;
}
/* ... */
}
単項マイナス演算
単項マイナス演算子は算術型オペランドを1つとる。符号付き整数型の最小値(負の値)をオペランドとして2の補数の単項マイナス演算を行うと、オーバーフローが発生する。
違反コード
次のコードは、符号付きオペランド s_a の単項マイナス演算時に符号付き整数オーバーフローが発生する可能性がある。
void func(signed long s_a) {
signed long result = -s_a;
/* ... */
}
適合コード
次の適合コードでは、問題が発生する可能性のあるマイナス演算を検査し、符号付き整数オーバーフローが発生しないことを保証している。
#include <limits.h>
void func(signed long s_a) {
signed long result;
if (s_a == LONG_MIN) {
/* エラー処理 */
} else {
result = -s_a;
}
/* ... */
}
リスク評価
整数のオーバーフローは、バッファオーバーフローや任意のコード実行につながる可能性がある。
|
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
|---|---|---|---|---|---|
|
INT32-C |
高 |
高 |
高 |
P9 |
L2 |
自動検出(最新の情報はこちら)
|
ツール |
バージョン |
チェッカー |
説明 |
|---|---|---|---|
| Coverity | 6.5 | TAINTED_STATIC | 実装済み |
|
5.0 |
CERT C Rule Packを使ってこのルールの違反を検出できる。具体的には、単項マイナス演算を行う前に、オペランドがその型の最小値と比較されているかどうかを確認する |
||
|
LDRA tool suite |
8.5.4 |
43 D |
部分的に実装済み |
| PRQA QA-C | 8.1 |
0278 |
実装済み |
関連するガイドライン
| CERT C Secure Coding Standard |
INT02-C. 整数変換のルールを理解する |
| CERT C++ Secure Coding Standard | INT32-CPP. Ensure that operations on signed integers do not result in overflow |
| CERT Oracle Secure Coding Standard for Java | INT00-J. 整数オーバーフローを回避するため明示的な範囲チェックを実行する |
| ISO/IEC TR 24772:2013 | Arithmetic Wrap-around Error [FIF] |
| ISO/IEC TS 17961 | Overflowing signed integers [intoflow] |
| MITRE CWE | CWE-129, Improper Validation of Array Index CWE-190, Integer Overflow or Wraparound |
参考資料
| [Dowd 2006] | Chapter 6, "C Language Issues" ("Arithmetic Boundary Conditions," pp. 211–223) |
| [ISO/IEC 9899:2011] | 6.5.5, "Multiplicative Operators" |
| [Seacord 2013b] | Chapter 5, "Integer Security" |
| [Viega 2005] | Section 5.2.7, "Integer Overflow" |
| [Warren 2002] | Chapter 2, "Basics" |
翻訳元
これは以下のページを翻訳したものです。
INT32-C. Ensure that operations on signed integers do not result in overflow (revision 237)
