CON34-C. スレッド間で共有されるオブジェクトは適切な記憶域期間を持つように宣言する
あるスレッドから別のスレッドのスタックやスレッド内にのみ存在する局所的な変数にアクセスすると、スレッド同期化の制約内で複数のスレッドの実行が盛り込まれ、不正なメモリアクセスを引き起こす可能性がある。その結果、参照先のスタックフレームあるいはスレッドに対して局所的な変数は、他のスレッドがアクセスしようとした時点では有効でなくなっている可能性がある。共有される静的変数は、スレッド同期の仕組みによって保護されるべきである。しかし、自動(局所)変数を同じ方法で共有すべきではない。共有すると、参照先のスタックフレームのスレッドは実行を停止しなければならなくなるため、参照先のフレームが有効であることを保証できる他の方法を探す必要がある。データがスレッド間で共有されていない場合に、適切な記憶域期間を持つようにオブジェクトを宣言する方法については、「DCL30-C. 適切な記憶域期間を持つオブジェクトを宣言する」を参照すること。
他のスレッドインタフェースを使っている場合は、局所的なデータはスレッド間で安全に使用できるため、スレッド間でデータを共有する際にプログラマは必ずしも局所メモリ以外にデータをコピーする必要がないことに注意する。たとえば、The OpenMP® API Specification for Parallel Programming [OpenMP] の shared
キーワードを OpenMP のスレッド処理インタフェースと組み合わせて使用した場合、局所的な自動変数が有効なままかどうかを心配せずに局所的なメモリを共有できる。また、共有データを動的メモリにコピーすると、マルチスレッドのパフォーマンスの利点が完全に損なわれる。
違反コード (自動記憶域)
createThread()
関数は、スタックに整数を割り当てて void 型ポインタを渡し、childThread()
という新しいスレッドを生成する。スレッドの実行順序は交互であるため、val
は生存期間を過ぎたオブジェクトを参照している可能性があり、結果として子スレッドによる無効なメモリ位置へのアクセスが引き起こされる。
#include <threads.h> void *childThread(void *val) { /* スレッドの実行順序によっては、val が参照するオブジェクトは * 生存期間の終わりに到達していることもあり得るため、 * 間違った結果が出力される可能性がある */ int *res = (int *)val; printf("Result: %d\n", *res); return NULL; } void createThread(thrd_t *tid) { int val = 1; int result; if ((result = thrd_create(tid, childThread, &val)) != thrd_success) { /* エラー処理 */ } } int main(void) { thrd_t tid; int result; createThread(&tid); if ((result = thrd_join(tid, NULL)) != thrd_success) { /* エラー処理 */ } return 0; }
違反コード (スレッドに対して局所的な記憶域)
以下のコード例では、childThread()
の前に createThread()
が実行を終了するため、childThread()
は正しくないオブジェクトにアクセスする可能性がある。
__thread int val; void *childThread(void *val) { int *res = (int *)val; printf("Result: %d\n", *res); return NULL; } void *createThread(void *childTid) { thrd_t *tid = (thrd_t *)childTid; int result; val = 1; if ((result = thrd_create(tid, childThread, &val)) != thrd_success) { /* エラー処理 */ } return NULL; } int main(void) { thrd_t parentTid, childTid; int result; /* createThread() は childThread() の前に終了する可能性があり、 * createThread() に属するスレッドに対して局所的な変数 val は、 * childThread() の実行時には有効でない可能性がある */ if ((result = thrd_create(&parentTid, createThread, &childTid)) != thrd_success) { /* エラー処理 */ } if ((result = thrd_join(parentTid, NULL)) != thrd_success) { /* エラー処理 */ } if ((result = thrd_join(childTid, NULL)) != thrd_success) { /* エラー処理 */ } return 0; }
適合コード (記憶域の割り当て)
解決法の 1 つとして、親スレッドのスタックへポインタを渡す代わりに、ヒープに領域を割り当て、動的メモリにデータをコピーする方法がある。動的メモリに格納されたオブジェクトの生存期間は、それが解放されるまで続くため、子スレッドはオブジェクトが格納されている動的メモリへのアクセスが有効であることを保証できる。
void *childThread(void *val) { /* 正しく 1 を出力 */ int *res = (int *)val; printf("Result: %d\n", *res); free(res); return NULL; } void createThread(thrd_t *tid) { int result; /* データを動的メモリにコピー */ int *value = malloc(sizeof(int)); if (!value) { /* エラー処理 */ } *value = 1; if ((result = thrd_create(&tid, childThread, value)) != thrd_success) { /* エラー処理 */ } } int main(void) { thrd_t tid; int result; createThread(&tid); if ((result = thrd_join(tid, NULL)) != thrd_success) { /* エラー処理 */ } return 0; }
適合コード (静的記憶域)
別の解決法として、データを大域的な静的変数として格納する方法がある。スタック上に格納される局所的な自動変数とは異なり、静的変数はメモリ上のデータ領域に格納される。静的変数はプログラムが実行している間はメモリに残るため、スレッドは大域的な静的変数には安全にアクセスできる。
/* val を大域的な静的変数として宣言 */ int value; void *childThread(void *val) { /* 正しく 1 を出力 */ int *res = (int *)val; printf("Result: %d\n", *res); return NULL; } void createThread(thrd_t *tid) { value = 1; int result; if ((result = thrd_create(tid, childThread, &value)) != thrd_success) { /* エラー処理 */ } } int main(void) { thrd_t tid; createThread(&tid); int result; if ((result = thrd_join(tid, NULL)) != thrd_success) { /* エラー処理 */ } return 0; }
適合コード (自動記憶域)
別の解決法として、スレッド間で共有されている局所変数を、必ず thrd_join()
などのスレッド同期の仕組みを呼び出したときと同じかそれ以前のスタックフレームで宣言する方法がある。たとえば次の適合コードでは、val
は thrd_join()
の呼び出しが行われる main()
で宣言されている。親スレッドは、子スレッドが完了するまで待ってから実行を継続するため、子スレッドはまだ生存期間内にあるオブジェクトにアクセスしていることが保証される。
void *childThread(void *val) { /* 正しく 1 を出力 */ int *res = (int *)val; printf("Result: %d\n", *res); return NULL; } void createThread(thrd_t *tid, int *val) { int result = thrd_create(tid, childThread, val); if (result != thrd_success) { /* エラー処理 */ } } int main(void) { /* val を thrd_join と同じ関数内で宣言 */ int val = 1; int result; thrd_t tid; createThread(&tid, &val); if ((result = thrd_join(tid, NULL)) != thrd_success) { /* エラー処理 */ } return 0; }
適合コード (スレッドに対して局所的な記憶域)
別の解決法として、val
をスレッド内にのみ存在する局所変数として、thrd_join()
などのスレッド同期の仕組みの呼び出しと組み合わせて宣言する方法がある。親スレッドは、子スレッドが完了するまで待ってから実行を継続するため、子スレッドはまだ生存期間内にあるオブジェクトにアクセスしていることが保証される。
/* val を大域的な静的変数として宣言 */ __thread int val; void *childThread(void *val) { /* 正しく 1 を出力 */ int *res = (int *)val; printf("Result: %d\n", *res); return NULL; } void createThread(thrd_t *tid) { val = 1; int result = thrd_create(tid, childThread, &val); if (result != thrd_success) { /* エラー処理 */ } } int main(void) { thrd_t tid; int result; createThread(&tid); if ((result = thrd_join(tid, NULL)) != thrd_success) { /* エラー処理 */ } return 0; }
リスク評価
他のスレッドのスタックを参照するスレッドは、関数ポインタや戻り先アドレスなど、スタック上の重要な情報を上書きするおそれがある。しかし、他のスレッドのスタックを参照しているという誤りだけでは、攻撃者はそのコードを悪用するのは難しい。プログラマが他のスレッドの局所変数へアクセスしようとしていても、コンパイラは警告を発しないため、プログラマはコンパイル時に誤りの可能性を検出できない可能性がある。解析ツールで同時実行や競合状態に関する問題を診断するのは難しいため、この誤りの修正コストは高い。
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
CON34-C |
中 |
中 |
高 |
P4 |
L3 |
参考資料
[Bryant 2003] | Chapter 13, "Concurrent Programming" |
[OpenMP] | The OpenMP® API Specification for Parallel Programming |
翻訳元
これは以下のページを翻訳したものです。
CON34-C. Declare objects shared between threads with appropriate storage durations (revision 60)