Home > ラーニング > セキュアコーディング > C セキュアコーディングスタンダード > 02. 宣言と初期化 (DCL)
アサートは脆弱性につながるソフトウェアの欠陥を発見し排除するのに役に立つプログラム診断ツールである。(「MSC11-C. 診断テストはアサートを使って組み込む」を参照。)しかしながら、実行時 assert() マクロには、実行時のオーバーヘッドをともないかつabort()を呼び出すという制限が存在する。したがって、実行時 assert()マクロが有用なのは、間違った想定を特定するためだけであって、実行時のエラーチェックのためではない。それゆえ、実行時アサートは一般的にサーバープログラムや組込みシステムには向いていない。
静的アサートは C++ 0X 標準規格のドラフト [Becker 08] で新たに取り入れられた機能であり、次の形式をとる:
static_assert(constant-expression, string-literal);
C++ 0X 標準規格ドラフトによると、静的アサート宣言中のconstant-expressionは、コンパイル時にboolに変換することができる定数式とする。もし変換された式の値が真であれば、宣言は効果を発揮しない。そうでなければ、プログラムには欠陥が存在し、診断メッセージ(string-literal のテキストを含む)がコンパイル時に出力される。たとえば、
/* パスする */ static_assert( sizeof(int) <= sizeof(void*), "sizeof(int) <= sizeof(void*)" ); /* 失敗する */ static_assert( sizeof(double) <= sizeof(int), "sizeof(double) <= sizeof(int)" );
静的アサートは C99 では利用できない。しかし、ISO/IEC WG14 国際標準化ワーキンググループで、その機能を C1X に含めることが検討されている。
以下のコードでは、assert() マクロを使い、コードが正しく動作するために必須なメモリマップされた構造体の状態を診断している。
struct timer {
uint8_t MODE;
uint32_t DATA;
uint32_t COUNT;
};
int func(void) {
assert(offsetof(timer, DATA) == 4);
}
実行時アサートを使う方が何もしないよりはましだが、関数に組み込まれ、実行されなければ意味がない。つまり、多くの場合、関数が参照する実際の構造体からあまりにかけはなれているのである。診断が行われるのは、実行時のみであり、かつアサートを含むコードパスが実行された場合のみである。
定数式のみを含むアサートに関しては、次の例に示すように、プリプロセッサの条件文を使える処理系もある。
struct timer {
uint8_t MODE;
uint32_t DATA;
uint32_t COUNT;
};
#if (offsetof(timer, DATA) != 4)
#error "DATA must be at offset 4"
#endif
#error 指令を使うことで明解な診断メッセージを実現できる。このアプローチを使えばコンパイル時にアサートが評価されるため、実行時のペナルティーはない。
残念ながら、この解決法は移植性に欠ける。C99 は処理系がsizeof, offsetof, #if 条件中の列挙定数をサポートすることを要求しないからである。C99 の 6.10.1「条件付き取込み」によると、条件付き取込みを制御する式中にある全ての識別子は、マクロ名であるかそうでないかである。コンパイラの中には、条件中にあるこれらの構文を拡張として認めているものもあるが、ほとんどはそうではない。
以下の解決法は、static_assert の動作を移植性のある方法で疑似的に実現している。
#define JOIN(x, y) JOIN_AGAIN(x, y)
#define JOIN_AGAIN(x, y) x ## y
#define static_assert(e) \
typedef char JOIN(assertion_failed_at_line_, __LINE__) [(e) ? 1 : -1]
struct timer {
uint8_t MODE;
uint32_t DATA;
uint32_t COUNT;
};
static_assert(offsetof(struct timer, DATA) == 4);
static_assert マクロは定数式 e を引数にとる。e は条件演算子の第一オペランドとして評価される。e が非ゼロに評価されると、サイズが1の配列型が定義され、それ以外の場合、サイズが-1の配列型が定義される。負のサイズを持つ配列型を定義することは無効なため、結果として得られる型定義はコンパイラによって指摘される。配列名を使い、失敗したアサートの位置を示している。
JOIN() マクロは ## 演算子を使用している。C言語において、## 演算子を用いた場合にマクロがどのように展開されるかについては「PRE05-C. 字句の結合や文字列化を行う際にはマクロ置換をよくよく理解して行う」を参照のこと。
静的アサートを使うことで、プログラムが黙って機能しなくなったり実行時エラーを引き起こすかわりに、正しくない想定をコンパイル時に検査することができる。アサートはコンパイル時に行われるため、メモリスペースや時間の実行時コストは発生しない。アサートをファイル有効範囲やブロック有効範囲で使うことで、失敗時に有意味な診断エラーメッセージを出すことができる。
静的アサートの他の利用方法については、「FIO35-C. sizeof(int) == sizeof(char) の場合、EOF およびファイルエラーの検出には feof() と ferror() を使用する」を参照のこと。
静的アサートは脆弱性につながるソフトウェアの欠陥を発見し排除するのに役に立つプログラム診断ツールである。しかし、静的アサートがプログラム中にないからといってコードが間違っているということを意味するわけではない。
| レコメンデーション | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
|---|---|---|---|---|---|
| DCL03-C | 低 | 低 | 高 | P1 | L3 |
Compass/ROSE はただ単に assert() 呼び出しを見つけることでこのレコメンデーションの違反を検出できるはずだ。もし、アサートに関する全ての値がコンパイル時に決定しており、完全に評価できるのであれば、static_assert を使うべきであろう。
ただし、ROSE がマクロ呼び出しを認識できるという前提である。
DCL03-C. Use a static assertion to test the value of a constant expression