INT02-C. 整数変換のルールを理解する
整数変換は、キャストの結果明示的に起こることもあれば、演算に必要なため暗黙的に起こることもある。整数変換は一般に、プログラムを正しく実行するために必要とされるが、場合によってはデータの欠損や誤解釈につながることもある。オペランド値の適合する型への変換は、値または表現に影響しない。
C の整数変換規則は、C コンパイラがどのように整数変換を扱うべきかを定義している。これらの規則には、整数拡張(integer promotions)、整数変換の順位(integer conversion rank)、および 通常の算術型変換(usual arithmetic conversions) が含まれる。整数変換規則の意図は、整数変換の結果いつも同じ値が得られること、そして他の演算への影響を最小限におさえること、である。C が標準化される前の実装においては、符号を維持することが一般に好まれていた。
整数拡張 (Integer Promotions)
int
より小さな整数型は、演算時に整数拡張される。元の型のすべての値を int
で 表現できる場合、小さな型の値を int
に変換する。それ以外の場合、unsigned int
に変換する。整数拡張は通常の算術型変換の一部として、引数式や、単項 +
、-
、~
演算子, シフト演算子のオペランドに対して適用される。以下に示すコードは、整数拡張の適用例を示している。
char c1, c2; c1 = c1 + c2;
整数拡張は、変数c1
と c2
がそれぞれ int
に整数拡張されることを要求する。この2つの int
の値は加算され、和は char
型に収まるように切り捨てられる。整数拡張が行われるのは、計算途中の値がオーバーフローして算術エラーが起こることを防ぐためである。
signed char cresult, c1, c2, c3; c1 = 100; c2 = 3; c3 = 4; cresult = c1 * c2 / c3;
この例では、(演算子の優先順位の規則に従って) c1
の値が c2
と乗算され、その積が次に c3
の値で除算されている。signed char
が8ビットの値として表現されると仮定すると、c1
と c2
の積である 300 という値は表現することができない。しかし、整数拡張の結果、c1
、c2
、c3
の値はそれぞれ int
に変換され、式全体は正しく評価される。結果の値は切り捨てられ、cresult
に格納される。最終結果の 75 は signed char
型の範囲にあるため、int
から signed char
への変換ではデータの欠損は発生しない。
整数変換の順位 (Integer Conversion Rank)
すべての整数型は整数変換の順位を持ち、これによってどのように変換が行われるかが決まる。この順位は、各整数型が、より低い順位の型と少なくとも同じビット数を持つように定められている。C 言語規格セクション 6.3.1.1 では、次のように整数変換の順位を定義している[ISO/IEC 9899:2011]。
- 2つの符号付き整数型は、同じ表現を持つ場合であっても、同じ順位をもってはならない。
- 符号付き整数型は、より小さい精度の符号付き整数型より高い順位をもたなければならない。
long long int
型はlong int
型より高い順位をもたなければならない。long int
型はint
型より高い順位をもたなければならない。int
型はshort int
型より高い順位をもたなければならない。short int
型はsigned char
型より高い順位をもたなければならない。- ある符号無し整数型に対し、対応する符号付き整数型があれば、両方の型は同じ順位をもたなければならない。
- 標準整数型は、同じ幅の拡張整数型より高い順位をもたなければならない。
char
型は、signed char
型およびunsigned char
型と同じ順位をもたなければならない。_Bool
型の順位は、他のすべての標準整数型の順位より低くなければならない。- 列挙型の順位は、適合する整数型の順位と等しくなければならない。
- 精度の等しい拡張符号付き整数型同士の順位は処理系定義とするが、整数変換の順位を定める他の規則に従わなければならない。
- すべての整数型
T1
、T2
、T3
について、T1
がT2
より高い順位をもち、かつT2
がT3
より高い順位をもつならば、T1
はT3
より高い順位をもたなければならない。
整数変換の順位は通常の算術型変換で使用される。異なる整数型が混在している場合に、どのような変換が必要となるか整数変換の順位にもとづいて決定される。
通常の算術型変換
二項演算子の2つのオペランドや条件演算子 (?:
) の第二、第三オペランドは共通の型の値として扱われる。通常の算術型変換は、ここで使われる共通の型を導出するための規則である。型変換は2つのオペランドの型が異なる場合に行われ、一方もしくは両方のオペランドが変換される。算術オペランドをとる演算子の多くは、通常の算術型変換にしたがって型変換を行う。両方のオペランドに整数拡張が適用され、その後で以下の規則が適用される。
- 両方のオペランドが同じ型をもつ場合、更なる型変換は行わない。
- そうでない場合、両方のオペランドが符号付き整数型をもつ、又は両方のオペランドが符号無し整数型をもつならば、整数変換の順位の低い方の型を、高い方の型に変換する。
- そうでない場合、符号無し整数型を持つオペランドが、他方のオペランドの整数変換の順位より高い又は等しい順位をもつならば、符号付き整数型をもつオペランドを、符号無し整数型をもつオペランドの型に変換する。
- そうでない場合、符号付き整数型をもつオペランドの型が、符号無し整数型をもつオペランドの型のすべての値を表現できるならば、符号無し整数型をもつオペランドを、符号付き整数型をもつオペランドの型に変換する。
- そうでない場合、両方のオペランドを、符号付き整数型をもつオペランドの型に対応する符号無し整数型に変換する。
例
以下では、8ビット char
、32ビット int
、64ビット long long
の処理系でコードがコンパイルされると仮定する。
signed char sc = SCHAR_MAX; unsigned char uc = UCHAR_MAX; signed long long sll = sc + uc;
この例では、signed char sc
と unsigned char uc
はどちらも整数拡張される。元の型のすべての値が int
で表現できるため、整数拡張の過程で両方の値は自動的に int
型に変換される。通常の算術型変換では、もし両方のオペランドの型が等しくなければさらに変換が行われる可能性がある。この例では、実際の加算演算は32ビット int
の値に対して行われる。演算結果の値は signed long long
型整数に格納されるが、演算自体はそれに影響されず、32ビット int
での演算となることに注意。加算結果の32ビット値は単純に符号拡張され64ビットの値として signed long long
型整数に格納される。
signed char
の精度が7ビットであり、unsigned char
の精度が8ビットであると仮定した場合、この演算はまったく安全に行える。しかし、もしコンパイラが signed char
と unsigned char
をそれぞれ 31ビットと32ビットの精度で表現した場合、変数 uc
は signed int
ではなく、unsigned int
に変換する必要がある。通常の算術型変換の結果、signed int
は符号無し型に変換され、加算は2つの unsigned int
型の値に対して行われる。また、uc
は UCHAR_MAX
に等しく、これは UINT_MAX
に等しくなるため、この例の加算はオーバーフローを引き起こす結果となる。結果の値はゼロ拡張され、sll
に割り当てられた64ビットの領域に格納される。
違反コード (比較)
異なる型が混在する演算には注意する必要がある。以下のコード例では整数拡張の特異性を示している。
int si = -1; unsigned int ui = 1; printf("%d\n", si < ui);
このコード例では、比較演算子のオペランドとして signed int
型および unsigned int
型の値をとる。変換規則によって、si
の値は unsigned int
型に変換される。−1 は unsigned int
型の値として表現できないため、C 言語規格セクション 6.3.1.3、第2パラグラフ [ISO/IEC 9899:2011] に従い、UINT_MAX
に変換される。
新しい型で表現できない場合、新しい型が符号無し整数型であれば、新しい型で表現しうる最大の数に1加えた数を加えることまたは減じることを、新しい型の範囲に入るまで繰り返すことによって得られる値に変換する。
その結果、UINT_MAX
は 1より小さくはないため、プログラムは 0 を出力する。
適合コード
比較が signed int
型で行われるように強制することで、直観通りの結果を出力するよう修正することができる。
int si = -1; unsigned ui = 1; printf("%d\n", si < (int)ui);
このプログラムは期待通り 1
を出力する。この例で (int)ui
としてよいのは、ui
の値が int
で表現できることがあらかじめ分かっているからだ、という点に注意。あらかじめ分かっていないのであれば、以下のように書きかえる必要がある。
int si = /* 符号付きの何かしらの値 */; unsigned ui = /* 符号無しの何かしらの値 */; printf("%d\n", (si < 0 || (unsigned)si < ui));
違反コード
以下のコード例は、int
型より小さい整数型に対してビット単位の演算を行うとどのような予期せぬ結果がもたらされるかを示している。
uint8_t port = 0x5a; uint8_t result_8 = ( ~port ) >> 4;
この例では、port
のビット補数が最初に計算され、その後4ビット右シフトしている。これらの演算はどちらも8ビット符号無し整数に対して行われたとすると、result_8
は 0x0a
という値をとる。ただし、以下の結果では、port
は最初に signed int
型に拡張される (int
型が 32 ビットの典型的なアーキテクチャ)。
式 |
種類 |
値 |
備考 |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
値がマイナスかどうかは処理系定義である。 |
|
|
|
|
適合コード
以下の適合コードでは、port
のビット補数は8ビットに変換される。これにより、result_8
には期待通り 0x0aU
が割り当てられる。
uint8_t port = 0x5a; uint8_t result_8 = (uint8_t) (~port) >> 4;
リスク評価
整数変換規則について正しく理解していないとエラーにつながり、これが攻撃可能な脆弱性を引き起こす恐れがある。とくに次のような場合に重大なリスクとなる: 小さな型に変換する場合(明示的なキャストや代入)、符号無しの型から符号付きの型への変換、負の値から符号無しの型への変換。
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
INT02-C |
中 |
中 |
中 |
P8 |
L2 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
ECLAIR |
1.1 |
utypbtws |
実装済み |
LDRA tool suite |
V. 8.5.4 |
52 S |
実装済み |
PRQA QA-C | 8.1 |
0290 |
実装済み |
関連する脆弱性
Adobe Flashの脆弱性 VU#159523 は、Flashが符号付き整数を calloc()
に渡すために生じている。攻撃者はこの整数を制御し、負の値を渡すことができる。calloc()
の引数は符号無しの size_t
型として扱われるため、負の値は大きな正の値に変換される。変換された正の値は一般にメモリ割り当てには大きすぎる値であり、その結果、calloc()
は NULL
を返すことになる。
関連するガイドライン
CERT C++ Coding Standard | INT02-CPP. Understand integer conversion rules |
ISO/IEC TR 24772:2013 | Numeric Conversion Errors [FLC] |
MISRA C:2012 | Rule 10.1 (required) Rule 10.3 (required) Rule 10.4 (required) Rule 10.6 (required) Rule 10.7 (required) Rule 10.8 (required) |
MITRE CWE | CWE-192, Integer coercion error CWE-197, Numeric truncation error |
参考資料
[Dowd 2006] | Chapter 6, "C Language Issues" ("Type Conversions," pp. 223–270) |
[Seacord 2013] |
Chapter 5, "Integer Security" |
翻訳元
これは以下のページを翻訳したものです。
INT02-C. Understand integer conversion rules (revision 99)