SIG30-C. シグナルハンドラ内では非同期安全な関数のみを呼び出す
シグナルハンドラ内では非同期安全な関数のみを呼び出すこと。厳密に合致するプログラムでは、C 標準ライブラリ関数 abort()、_Exit()、および signal() をシグナルハンドラ内から呼び出すことができる。
C 標準 [ISO/IEC 9899:2011] セクション 7.14.1.1、第 5 段落には、シグナルが abort() または raise() 関数の呼び出し以外によって発生し、次の条件が該当する場合には、動作は未定義になると規定している。
シグナル処理ルーチンが、この規格で規定するライブラリ中の関数を呼び出す場合。ただし、
abort関数、_Exit関数、又は、シグナル処理ルーチンの呼出しを発生させたシグナルに対応するシグナル番号を第 1実引数としてsignal関数自身を呼び出す場合を除く。
多くのシステムでは、非同期安全な関数の実装固有のリストを定義している。これらの関数はシグナルハンドラ内で呼び出すこともできる。この制約は、アプリケーション内で定義した関数だけでなく、ライブラリ関数にも適用される。
C Rationale のセクション 7.14.1.1 [ISO/IEC 2003] には以下のように記載されている。
シグナルが発生すると、プログラムの通常の制御フローは中断される。シグナルハンドラによって捕捉されているシグナルが発生した場合、そのハンドラが呼び出される。ハンドラが終了すると、シグナルが発生した時点からプログラムは続行する。この処理は、ライブラリ関数実行時にシグナルが発生し、シグナルハンドラが同じライブラリ関数を呼び出す場合、問題を引き起こす可能性がある。
一般に、I/O 関数をシグナルハンドラ内で呼び出すのは安全ではない。シグナルハンドラ内で I/O 関数を使用する場合は、使用するシステムの非同期安全な関数を事前に確認すること。
違反コード
以下のコード例では、log_message() 関数により、シグナルハンドラから C 標準ライブラリ関数 fprintf() が呼び出される。関数 free() もまた非同期安全ではなく、シグナルハンドラから呼び出すことは本ルールに違反する。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
enum { MAXLINE = 1024 };
char *info = NULL;
void log_message(void) {
fprintf(stderr, info); /* 違反 */
}
void handler(int signum) {
log_message();
free(info); /* 違反 */
info = NULL;
}
int main(void) {
if (signal(SIGINT, handler) == SIG_ERR) {
/* エラー処理 */
}
info = (char*)malloc(MAXLINE);
if (info == NULL) {
/* エラー処理 */
}
while (1) {
/* メインループ */
log_message();
/* さらにプログラムコードが続く */
}
return 0;
}
適合コード
シグナルハンドラはできる限り簡潔にすべきであり、単にフラグを設定するだけですぐ return するのが理想である。以下の適合コードでは、volatile sig_atomic_t タイプのフラグを設定して return し、log_message() および free() 関数は main() から直接呼び出される。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
enum { MAXLINE = 1024 };
volatile sig_atomic_t eflag = 0;
char *info = NULL;
void log_message(void) {
fprintf(stderr, info);
}
void handler(int signum) {
eflag = 1;
}
int main(void) {
if (signal(SIGINT, handler) == SIG_ERR) {
/* エラー処理 */
}
info = (char*)malloc(MAXLINE);
if (info == NULL) {
/* エラー処理 */
}
while (!eflag) {
/* メインループ */
log_message();
/* さらにプログラムコードが続く */
}
log_message();
free(info);
info = NULL;
return 0;
}
違反コード
シグナルハンドラ内から longjmp() 関数を呼び出した結果、非同期安全でない関数が呼び出されると、未定義の動作につながり、プログラムの完全性が損なわれる可能性がある。このため、longjmp() も POSIX の siglongjmp() も、シグナルハンドラ内から呼び出してはならない。
以下は、古いバージョンの sendmail の脆弱性 [VU #834865] に類似のコード例である。コードの目的は main() のループでコードを実行し、データのログも記録することである。SIGINT を受信すると、プログラムはループから抜け、エラーを記録して終了する。
攻撃者は、log_message() 中の2番目の if 文が実行される直前に SIGINT を生成することで、このコードを悪用できる可能性がある。longjmp() によりプログラムの制御は main() に戻り、log_message() が再び呼び出される。しかし、今度は最初の if 文が実行されず(割り込みにより buf が NULL に設定されていないため)、結果としてプログラムは buf0 が参照する無効なメモリに書き込みを行うことになる。
#include <setjmp.h>
#include <signal.h>
#include <stdlib.h>
enum { MAXLINE = 1024 };
static jmp_buf env;
void handler(int signum) {
longjmp(env, 1); /* 違反 */
}
void log_message(char *info1, char *info2) {
static char *buf = NULL;
static size_t bufsize;
char buf0[MAXLINE];
if (buf == NULL) {
buf = buf0;
bufsize = sizeof(buf0);
}
/*
* メッセージを buf に入れる。buf の大きさが不足していたら、メッセージデータを
* 格納するのに十分な領域をヒープメモリから割り当て直して処理する。
*/
/*** VULNERABILITY IF SIGINT RAISED HERE ***/
if (buf == buf0) {
buf = NULL;
}
}
int main(void) {
if (signal(SIGINT, handler) == SIG_ERR) {
/* エラー処理 */
}
char *info1;
char *info2;
/* info1 と info2 はユーザ入力からきている */
if (setjmp(env) == 0) {
while (1) {
/* メインループ */
log_message(info1, info2);
/* さらにプログラムコードが続く */
}
}
else {
log_message(info1, info2);
}
return 0;
}
適合コード
以下の適合コードでは、longjmp() の呼び出しを削除している。代わりに、シグナルハンドラは volatile sig_atomic_t 型のエラーフラグを設定している。
#include <signal.h>
#include <stdlib.h>
enum { MAXLINE = 1024 };
volatile sig_atomic_t eflag = 0;
void handler(int signum) {
eflag = 1;
}
void log_message(char *info1, char *info2) {
static char *buf = NULL;
static size_t bufsize;
char buf0[MAXLINE];
if (buf == NULL) {
buf = buf0;
bufsize = sizeof(buf0);
}
/*
* メッセージを buf に入れる。buf の大きさが不足していたら、メッセージデータを
* 格納するのに十分な領域をヒープメモリから割り当て直して処理する。
*/
if (buf == buf0) {
buf = NULL;
}
}
int main(void) {
if (signal(SIGINT, handler) == SIG_ERR) {
/* エラー処理 */
}
char *info1;
char *info2;
/* info1 と info2 はユーザ入力からきている */
while (!eflag) {
/* メインループ */
log_message(info1, info2);
/* さらにプログラムコードが続く */
}
log_message(info1, info2);
return 0;
}
違反コード
以下のコード例では、int_handler() 関数を使って SIGINT に対応する処理を実行し、次に SIGTERM を発生させる。しかし、raise() 関数を再帰的に呼び出しており、未定義の動作となる。
void term_handler(int signum) {
/* SIGTERM 固有の処理 */
}
void int_handler(int signum) {
/* SIGINT 固有の処理 */
if (raise(SIGTERM) != 0) { /* 違反 */
/* エラー処理 */
}
}
int main(void) {
if (signal(SIGTERM, term_handler) == SIG_ERR) {
/* エラー処理 */
}
if (signal(SIGINT, int_handler) == SIG_ERR) {
/* エラー処理 */
}
/* プログラムコード */
if (raise(SIGINT) != 0) {
/* エラー処理 */
}
/* さらにコードが続く */
return EXIT_SUCCESS;
}
適合コード
以下の適合コードでは、handler() 内で raise() 関数を呼び出す代わりに、log_msg() を直接呼び出している。
#include <signal.h>
void log_msg(int signum) {
/* async-safe なエラー記録ルーチン */
}
void handler(int signum) {
/* SIGINT 固有の処理 */
log_msg(SIGUSR1);
}
int main(void) {
if (signal(SIGUSR1, log_msg) == SIG_ERR) {
/* エラー処理 */
}
if (signal(SIGINT, handler) == SIG_ERR) {
/* エラー処理 */
}
/* プログラムコード */
if (raise(SIGINT) != 0) {
/* エラー処理 */
}
/* さらにコードが続く */
return 0;
}
違反コード (POSIX)
POSIX 標準は、シグナルハンドラ内から raise() を呼び出すことに関しては C と相反した立場をとる。POSIX 標準 [Open Group 2004] では、raise() 関数、kill() 関数、pthread_kill() 関数、sigqueue() 関数の呼び出しによりシグナルが発生した場合、signal() を使って登録されたシグナルハンドラが、raise() 関数を呼び出すことを禁止している。しかし、シグナルハンドラ内で raise() 関数を安全に呼び出すことは許可している。結果として、signal() を使って登録されたシグナルハンドラ内で POSIX アプリケーションが raise() を呼び出すことが安全かどうかは明らかでないが、sigaction() を使って登録されたシグナルハンドラ内で raise() を呼び出すことは安全だといえる。
以下のコード例では、シグナルハンドラは signal() を使って登録され、raise() はシグナルハンドラ内で呼び出される。
#include <signal.h>
void log_msg(int signum) {
/* エラーメッセージの記録 */
}
void handler(int signum) {
/* SIGINT 固有の処理 */
if (raise(SIGUSR1) != 0) { /* 違反 */
/* エラー処理 */
}
}
int main(void) {
signal(SIGUSR1, log_msg);
signal(SIGINT, handler);
/* プログラムコード */
if (raise(SIGINT) != 0) {
/* エラー処理 */
}
/* さらにコードが続く */
return 0;
}
処理系固有の詳細
POSIX
Open Group Base Specifications [Open Group 2004] から引用した次の表に、一連の非同期安全な関数が定義されている。アプリケーションのシグナルハンドラは制約なしにこれらの関数を呼び出せる。
非同期安全な関数
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
この表にない関数はすべて、シグナルに関しては安全でないとされている。IEEE 標準1003.1-2001 により定義される関数はすべて、シグナルが発生している場合、シグナルハンドラから呼び出されても、あるいはシグナルハンドラによって割り込まれても、定義通りに動作する。ただし例外が1つある。シグナルが安全でない関数に割り込み、シグナルハンドラが安全でない関数を呼び出した場合、動作は未定義である。
raise() は非同期安全な関数のリストに含まれているが、abort() または raise() 関数の呼び出しによってシグナルが発生する場合、シグナルハンドラ内で呼び出してはならない。
C 標準 [ISO/IEC 9899:2011] セクション 7.14.1.1、第 4 段落には次のように記載されている。
シグナルが、
abort関数又はraise関数の呼出しの結果として発生する場合、シグナル処理ルーチンは、raise関数を呼び出してはならない。
(附属書 J 「未定義の動作」の 131 も参照。)
OpenBSD
OpenBSD の signal() のマニュアルには、OpenBSD で非同期安全である関数がさらにいくつか列挙されている。ただし、これらの関数は「他のシステムではおそらく非同期安全ではない」ことに注意。たとえば、snprintf()、vsnprintf()、(syslog_data struct 構造体がローカル変数として初期化されている場合の) syslog_r() など。
適合コード (POSIX)
以下の適合コードでは、シグナルハンドラは sigaction() を使って登録されるため、シグナルハンドラ内での raise() の使用は安全といえる。
#include <signal.h>
void log_msg(int signum) {
/* async-safe なエラー記録ルーチン */
}
void handler(int signum) {
/* SIGINT 固有の処理 */
if (raise(SIGUSR1) != 0) {
/* エラー処理 */
}
}
int main(void) {
struct sigaction act;
act.sa_flags = 0;
if (sigemptyset(&act.sa_mask) != 0) {
/* エラー処理 */
}
act.sa_handler = log_msg;
if (sigaction(SIGUSR1, &act, NULL) != 0) {
/* エラー処理 */
}
act.sa_handler = handler;
if (sigaction(SIGINT, &act, NULL) != 0) {
/* エラー処理 */
}
/* プログラムコード */
if (raise(SIGINT) != 0) {
/* エラー処理 */
}
/* さらにコードが続く */
return 0;
}
POSIX は sigaction() の使用を推奨しており、signal() の使用は推奨していない。残念ながら、sigaction() は C 標準で定義されていないため、可搬性のある解決法ではない。
リスク評価
シグナルハンドラから非同期安全でない関数を呼び出すと、権限の昇格やその他の攻撃を引き起こす可能性がある。
|
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
|---|---|---|---|---|---|
|
SIG30-C |
高 |
高 |
中 |
P18 |
L1 |
自動検出(最新の情報はこちら)
|
ツール |
バージョン |
チェッカー |
説明 |
|---|---|---|---|
| Compass/ROSE | 単一ファイルのプログラムの場合このルールへの違反を検出できる。 | ||
|
LDRA tool suite |
V. 8.5.4 |
88 D |
実装済み |
|
Splint |
V. 3.1.1 |
|
|
関連する脆弱性
シグナルハンドリング関連の脆弱性の概要、悪用、防止に関する Zalewski の論文 [Zalewski 2001] を参照のこと。VU #834865 はこのルールの違反が原因で生じる脆弱性についてとり上げている。
シグナルハンドラ内での longjmp() 関数の使用が重大な脆弱性を引き起こしたもう1つの注目すべき事例として、wu-ftpd 2.4 [Greenman 1997] がある。wu-ftpd では、あるシグナルハンドラ内で EUID をゼロに設定している。2番目のシグナルが最初のシグナルに割り込むと、longjmp() の呼び出しが行われ、プログラムはメインスレッドに戻るがユーザの権限は降格されないままになる。昇格した権限は、さらなる攻撃に悪用される可能性がある。
関連するガイドライン
| CERT C++ Secure Coding Standard | SIG30-CPP. Call only asynchronous-safe functions within signal handlers |
| ISO/IEC TR 17961 (ドラフト) | Calling functions in the C Standard Library other than abort, _Exit, and signal from within a signal handler [asyncsig] |
| MITRE CWE | CWE-479, Unsafe function call from a signal handler |
参考資料
| [Dowd 2006] | Chapter 13, "Synchronization and State" |
| [ISO/IEC 2003] | Section 5.2.3, "Signals and Interrupts" Section 7.14.1.1, "The signal Function" |
| [ISO/IEC 9899:2011] | Section 7.14, "Signal Handling <signal.h>" |
| [Open Group 2004] | longjmp() |
| [OpenBSD] | signal() Man Page |
| [Zalewski 2001] | "Delivering Signals for Fun and Profit" |
翻訳元
これは以下のページを翻訳したものです。
SIG30-C. Call only asynchronous-safe functions within signal handlers (revision 113)



