MSC06-C. コンパイラの最適化に注意する
C 標準 5.1.2.3 節には次のように記されている [ISO/IEC 9899:2011]。
抽象計算機では、すべての式は意味規則で規定するとおりに評価する。実際の処理系では、値が使用されないこと、及び(関数の呼出し又はボラタイルオブジェクトのアクセスによって起こる副作用を含め)副作用が必要とされないことが保証できる式は、評価しなくてよい。
この規定により、コンパイラは、プログラムをビルドする時、未使用または不要であると見なしたコードを削除することが許されている。この仕組みは通常は有益であるが、ある目的で (多くの場合セキュリティに関する目的で) 追加されたコードが、コンパイラによって不要と見なされ削除されてしまうことがある。
違反コード (memset())
想定外かつ望ましくないコンパイラによる最適化の一例として、たとえば、センシティブデータの格納に使用していた記憶領域を上書きする操作が挙げられる。センシティブデータを扱う場合、そのようなデータに対する操作が必ず意図通りに実行されることを確認するよう常に注意しなくてはならない。コードを削除してもプログラムの動作が変わらないとオプティマイザが判断すると、コンパイラの最適化レベルによっては、そのようなコードが削除されることがある。以下の違反コード例では、書き込み後に変数へのアクセスが行われていないため、最適化処理により(プログラマがセンシティブデータを格納するメモリをクリアするために用意した)memset() の呼び出しが削除される可能性がある。このようなコンパイラ固有の動作や、それがどの最適化レベルで生じるかについては、コンパイラのマニュアルで確認せよ。
void getPassword(void) {
char pwd[64];
if (GetPassword(pwd, sizeof(pwd))) {
/* パスワードの検査、セキュアな操作など */
}
memset(pwd, 0, sizeof(pwd));
}
このレコメンデーションで取り上げるすべての適合コードについて、最適化されたリリースビルドで生成したアセンブリコードに対し、メモリクリアするコードや、関数呼び出しのコードが削除されていないかどうか確認することを強く推奨する。
違反コード (Touching Memory)
以下の違反コード例は、memset() を呼び出したあとで再度バッファにアクセスしている。この手法によってコンパイラが最適化により memset() の呼び出しを削除することを防止できる場合もあるが、すべての処理系で通用するわけではない。たとえば、MIPSpro コンパイラやバージョン 3 以降の GCC では、最初の 1 バイトにだけ 0 をセットし、残りの領域には変更を加えない。特定のプラットフォームにおけるこのような動作については、コンパイラのマニュアルで確認すること。
void getPassword(void) {
char pwd[64];
if (retrievePassword(pwd, sizeof(pwd))) {
/* パスワードの検査、セキュアな操作など */
}
memset(pwd, 0, sizeof(pwd));
*(volatile char*)pwd= *(volatile char*)pwd;
}
違反コード (Windows)
以下のコード例は、複数のバージョンの Microsoft Visual Studio コンパイラが提供する ZeroMemory() 関数を使用している。
void getPassword(void) {
char pwd[64];
if (retrievePassword(pwd, sizeof(pwd))) {
/* パスワードの検査、セキュアな操作など */
}
ZeroMemory(pwd, sizeof(pwd));
}
memset() 呼び出しと同様、ZeroMemory() 呼び出しも最適化により削除されることがある。
適合コード (Windows)
以下の適合コードは、複数のバージョンの Microsoft Visual Studio コンパイラが提供する SecureZeroMemory() 関数を使用している。SecureZeroMemory() 関数のマニュアルによると、メモリの内容を 0 で埋めるこの関数呼び出しをコンパイラが最適化で削除しないことが保証されている。
void getPassword(void) {
char pwd[64];
if (retrievePassword(pwd, sizeof(pwd))) {
/* パスワードの検査、セキュアな操作など */
}
SecureZeroMemory(pwd, sizeof(pwd));
}
適合コード (Windows)
次の適合コードの #pragma 指示文は、囲まれたコードを最適化しないようにコンパイラに指示する。#pragma 指示文は Microsoft Visual Studio の複数のバージョンおよびその他のコンパイラで使用できる。コンパイラのマニュアルで、この指示文を利用できることと、指示文により最適化が行われないことを確認しよう。
void getPassword(void) {
char pwd[64];
if (retrievePassword(pwd, sizeof(pwd))) {
/* パスワードの検査、セキュアな操作など */
}
#pragma optimize("", off)
memset(pwd, 0, sizeof(pwd));
#pragma optimize("", on)
}
適合コード (C99)
以下の適合コードでは、volatile 型修飾子を使用し、メモリ内容を上書きする必要があること、また memset_s() 関数を最適化により削除すべきではないことをコンパイラに通知している。volatile 型修飾子の使用により、コンパイラのコード最適化が完全に抑止されるため、このコードはそれほど効率的であるとはいえない。一般に、memset() の呼び出しを、より効率のよい同等なアセンブリ命令に置き換えるようなコンパイラも存在する。下記の例に示すような memset_s() 関数を実装すると、コンパイラは最適なアセンブリ命令を使用できなくなり、その結果コードの効率が悪化する可能性がある。コンパイラのマニュアルとそのアセンブリ出力を確認する必要があるだろう。
/* memset_s.c */
errno_t memset_s(void *v, rsize_t smax, int c, rsize_t n) {
if (v == NULL) return EINVAL;
if (smax > RSIZE_MAX) return EINVAL;
if (n > smax) return EINVAL;
volatile unsigned char *p = v;
while (smax-- && n--) {
*p++ = c;
}
return 0;
}
/* getPassword.c */
extern errno_t memset_s(void *v, rsize_t smax, int c, rsize_t n);
void getPassword(void) {
char pwd[64];
if (retrievePassword(pwd, sizeof(pwd))) {
/* パスワードの検査、セキュアな操作など */
}
if (memset_s(pwd, sizeof(pwd), 0, sizeof(pwd)) != 0) {
/* エラー処理 */
}
}
しかし、関数の呼び出しと volatile 修飾されたオブジェクトへのアクセスは最適化によって削除される可能性がある (そのような場合でも言語規格には厳格に合致している)。したがって、この適合コードでは対応できない場合もある。C11 で導入された memset_s() 関数を使った解決法が推奨される (次の適合コードを参照)。もし使用する処理系がまだ memset_s() に対応していないのであれば、この適合コードが最も適しており、memset_s() がサポートされれば変更すればよいだろう。
適合コード (C11)
C 標準には memset_s 関数が含まれている。 K.3.7.4.1 節の第4パラグラフには次のような記述がある [ISO/IEC 9899:2011]。
memsetとは異なり、memset_s関数の呼び出しは、(5.1.2.3) に記述する抽象計算機の規則に厳密に従って評価しなければならない。つまり、memset_s関数の呼び出しにおいて、sとnが示すメモリが未来においてアクセス可能であり、つまりcが示す値を含んでいなければならないということである。
void getPassword(void) {
char pwd[64];
if (retrievePassword(pwd, sizeof(pwd))) {
/* パスワードの検査、セキュアな操作など */
}
memset_s(pwd, 0, sizeof(pwd));
}
違反コード
ごくまれにではあるが、空の無限ループの使用が避けられない場合があるかもしれない。たとえば、sleep(3) や同等の関数をサポートしないプラットフォームでは空のループが必要になるかもしれない。他にも OS カーネルにおいて、通常のスケジューラ機能が利用できる前に開始したタスクからは sleep(3) や同等の関数を利用できないことがある。ループの中で何も処理を行わない空の無限ループを使用するのはあまり良い解決方法であるとは言えない。なぜなら、CPU サイクルを浪費するだけで何ら役に立つ操作を行わないからである。最適化を行うコンパイラでは、そのようなループを取り除いて予期せぬ結果をもたらす可能性がある。C 標準規格の 6.8.5 節、第6パラグラフには次のように規定されている [ISO/IEC 9899:2011]。
繰返し文において、制御式が定数式でなく、入出力操作を行わず、volatile オブジェクトに対するアクセスを行わず、同期やアトミック操作を繰返し文の本体、制御式、あるいは式3 (for 文の場合)の中で行わない場合、処理系はそのような繰返し文が終了すると仮定することができる157。
157) これは、たとえ文の終了が証明できない場合であっても、空のループを取り除くなどの変換をコンパイラが行うことを認めることを意図している。
以下の違反コード例では、ループ内でいかなる命令も実行することなく、繰返しループを実行するアイドルタスクを実装している。最適化を行うコンパイラはこのような while ループを取り除いてしまう場合がある。
static int always = 1;
int main(void) {
while (always) { }
}
適合コード (while)
ループが最適化によって削除されるのを防ぐために、以下の適合コードでは定数式 (1) を while ループの制御式として使用している。
int main(void) {
while (1) { }
}
適合コード (for)
C 標準規格の 6.8.5.3 節、第2パラグラフによると、for ループの式2を省略することで、式が非ゼロの定数と置き換えられる。
int main(void) {
for (;;) { }
}
リスク評価
コンパイラの最適化によってメモリ内容を消去するコードが削除された場合、攻撃者がセンシティブデータにアクセスできる可能性がある。
|
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
|---|---|---|---|---|---|
|
MSC06-C |
中 |
中 |
中 |
P8 |
L2 |
自動検出(最新の情報はこちら)
Tool |
Version |
Checker |
Description |
|---|---|---|---|
| CodeSonar | 4.0 | BADFUNC.MEMSET | Use of memset |
関連するガイドライン
| CERT C++ Secure Coding Standard | MSC06-CPP. Be aware of compiler optimization when dealing with sensitive data |
| CERT Oracle Secure Coding Standard for Java | MSC01-J. Do not use an empty infinite loop |
| MITRE CWE | CWE-14, Compiler removal of code to clear buffers |
参考情報
| [ISO/IEC 9899:2011] | Subclause 6.8.5, "Iteration Statements" Subclause K.3.7.4.1, "The memset_s Function" |
| [MSDN] | "SecureZeroMemory" "Optimize (C/C++)" |
| [US-CERT] | "MEMSET" |
| [Wheeler 2003] | Section 11.4, "Specially Protect Secrets (Passwords and Keys) in User Memory" |
翻訳元
これは以下のページを翻訳したものです。
MSC06-C. Be aware of compiler optimization (revision 78)
