SIG01-C. シグナルハンドラの継続性に関して処理系定義の詳細を理解する
signal()
には処理系定義の動作がある。たとえば Windows 上での動作は多くの UNIX システム上での動作と異なる。
次のコード例は、このような処理系定義の動作を示したものである。
#include <stdio.h> #include <signal.h> volatile sig_atomic_t e_flag = 0; void handler(int signum) { e_flag = 1; } int main(void) { if (signal(SIGINT, handler) == SIG_ERR) { /* エラー処理 */ } while (!e_flag) {} puts("Escaped from first while ()"); e_flag = 0; while (!e_flag) {} puts("Escaped from second while ()"); return 0; }
多くの UNIX 系システムでは、シグナルハンドラが実行される時に自動的にハンドラを再登録する。つまり、ユーザによって定義されたシグナルハンドラは、明示的に削除されるまで登録されたままとなる。たとえば、以下のコードが Red Hat Linux 上の GCC 3.4.4 でコンパイルされ実行された場合、SIGINT
は 2 回とも handler
によって捕捉される。
% ./test ^C Escaped from first while () ^C Escaped from second while () %
Windows といくつかの UNIX システムでは、signal()
関数によってシグナルハンドラを登録しても、そのシグナルが送られた後は、デフォルトのハンドラに戻ってしまう。これは、シグナルハンドラが自動的には再登録されないことを意味する。たとえば、このコードが Microsoft Visual Studio 2005 バージョン 8.0 でコンパイルされた場合、最初の SIGINT
だけが handler
によって捕捉される。
> test.exe ^C Escaped from first while () ^C >
2 番目の SIGINT
はデフォルトの動作を実行する。つまりプログラムの実行を終了する。
アプリケーションにおいてシグナルハンドラを継続型にする必要があるかどうかに応じて、異なる動作を実行する必要がある。
継続型シグナルハンドラ
非同期シグナルは、プロセス外部の悪意ある者が発生させる可能性がある。このため、シグナルハンドラの継続性が開発者の意図に反している場合、たとえば、開発者が継続型シグナルハンドラを意図しているにもかかわらず実際には継続型でないといった場合、脆弱性が存在している可能性がある。
違反コード
以下のコード例は、Windows プラットフォーム上と、デフォルトではシグナルハンドラに継続性がない UNIX システム上ではハンドラの継続に失敗する。
void handler(int signum) { /* シグナルの処理 */ }
違反コード
継続型シグナルハンドラを作成する一般的な方法は、ハンドラ内から signal()
を呼び出すことであり、これにより、リセットされたシグナルはリセット解除される。
void handler(int signum) { if (signal(signum, handler) == SIG_ERR) { /* エラー処理 */ } /* シグナルの処理 */ }
残念ながら、このコードには依然として競合ウィンドウが存在する。この競合ウィンドウはホスト環境がシグナルハンドラをリセットしたときに開き、シグナルハンドラが signal()
を呼び出したときに閉じる。この期間中にプログラムに送信された 2 番目のシグナルは、デフォルトのシグナル動作を引き起こす。結果として、シグナルハンドラの継続性が達成されない(「SIG34-C. 割り込み可能なシグナルハンドラ内から signal() を呼び出さない」を参照)。
これを解決するには、シグナルがリセットされることを防がなければならない。残念ながら、Windows はこの問題に対する安全な解決策を提供していない。
適合コード (POSIX)
POSIX の sigaction()
関数は、C の signal()
関数と同様、シグナルにハンドラを割り当てると同時に、SA_RESETHAND
フラグを通じてシグナルハンドラの継続性を制御することができる(このフラグを設定しなければ、ハンドラは継続型になる)。
/* signal(SIGUSR1, handler) と同じ意味。シグナルハンドラを * 継続型にする */ struct sigaction act; act.sa_handler = handler; act.sa_flags = 0; if (sigemptyset(&act.sa_mask) != 0) { /* エラー処理 */ } if (sigaction(SIGUSR1, &act, NULL) != 0) { /* エラー処理 */ }
POSIX は sigaction()
の使用を推奨しており、signal()
の使用は推奨していない。残念ながら、sigaction()
は C 標準で定義されていないため、可搬性のある解決法ではない。
非継続型シグナルハンドラ
開発者が非継続型シグナルハンドラを意図しているのに、実際には シグナルハンドラが継続型であった場合、エラーが起こる可能性がある。
違反コード (UNIX)
以下のコード例は、シグナルハンドラのデフォルトの挙動が継続型であるシステム上では、ハンドラをデフォルトの動作に戻すことができない。
void handler(int signum) { /* シグナルの処理 */ }
適合コード (UNIX および Windows)
UNIX システム上でシグナルハンドラをリセットする C に準拠した方法は、ハンドラ自身のコードの 1 行目で、デフォルトのハンドラをシグナルに再度割り当てることである。ただし Windows は、シグナルハンドラの動作を自動的にデフォルトの動作に戻す。
void handler(int signum) { #ifndef WINDOWS if (signal(signum, SIG_DFL) == SIG_ERR) { /* エラー処理 */ } #endif /* シグナルの処理 */ }
UNIX に対してこの解決策を使用した場合、2 番目のシグナルが送信されることで攻撃者によって悪用されるような競合状態は発生しない。なぜなら、シグナルハンドラに送信される 2 番目のシグナルは、シグナルハンドラが signal(signum, SIG_DFL)
を呼び出す前に、単にハンドラを再度実行させ、結局 signal()
を呼び出すことになるためである。
この解決策は、「SIG34-C. 割り込み可能なシグナルハンドラ内から signal() を呼び出さない」に対する例外である。
適合コード (POSIX)
POSIX の sigaction()
関数は、C の signal()
関数と同様、シグナルにハンドラを割り当てると同時に、SA_RESETHAND
フラグを通じてシグナルハンドラの継続性を制御することができる(このフラグを設定することにより、ハンドラは非継続型になる)。
/* signal(SIGUSR1, handler) と同じ意味。シグナルハンドラを * 非継続型にする */ struct sigaction act; act.sa_handler = handler; act.sa_flags = SA_RESETHAND; if (sigemptyset(&act.sa_mask) != 0) { /* エラー処理 */ } if (sigaction(SIGUSR1, &act, NULL) != 0) { /* エラー処理 */ }
リスク評価
シグナルハンドラの継続性に関する処理系定義の詳細を理解していないと、予期せぬ動作が引き起こされる可能性がある。
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
SIG01-C |
低 |
低 |
低 |
P3 |
L3 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
Compass/ROSE |
|
|
|
PRQA QA-C | 8.1 | warncall for signal | 部分的に実装済み |
関連するガイドライン
CERT C++ Secure Coding Standard | SIG01-CPP. Understand implementation-specific details regarding signal handler persistence |
翻訳元
これは以下のページを翻訳したものです。
SIG01-C. Understand implementation-specific details regarding signal handler persistence (revision 72)