JPCERT コーディネーションセンター

CON32-C. 複数スレッドによる隣接データへのアクセスが必要な場合データ競合を防止する

複数のスレッドが共有変数に対するアクセスや変更を行わねばならない場合、隣接メモリ内の他の変数に対してそれらの操作を意図せずに行ってしまうことがある。これは、1 バイトで複数の変数を保持するなど変数をコンパクトに格納する際の弊害として発生するが、一方でワードマシン上ではよく行われる最適化である。コンパイラは、複数のビットフィールドをアドレス可能な 1 バイトもしくはワードに格納してもよいため、ビットフィールドは特にこの影響を受けやすい。したがって、データ競合は、複数のスレッドが 1 つの変数にアクセスするときだけでなく、同じバイトやワードアドレスを共有する他の変数に対しても発生しうる。

並列プログラミングにおいてデータ競合を防ぐツールの 1 つがミューテックスである。すべてのスレッドが適切に使用していれば、ミューテックスは共有変数を安全かつセキュアにアクセスするすべを提供する。しかし、アクセスしているスレッドがミューテックスを制御していない場合、同時にアクセスされる他の変数に対しては何の保証もない。

残念ながら、ある変数と一緒にどの変数が隣り合わせに格納されているかを調べる可搬性のある方法は存在しない。

別のアプローチとして、共有体の中に long 変数やパディングと一緒に並列アクセスされる変数を埋め込み、そのアドレスでアクセスされるのがメモリ上で隣り合う変数だけになるよう保証する方法がある。この方法により、メモリ上で隣り合う変数がアクセスされたり変更されたりする際に他の変数が影響を受けないよう効果的に保証できる。

違反コード (C99)

以下のコード例では、2 つのスレッドが、構造体の 2 つのメンバを同時に変更している。

struct multi_threaded_flags {
  unsigned char flag1;
  unsigned char flag2;
};
 
struct multi_threaded_flags flags;
 
void thread1(void) {
  flags.flag1 = 1;
}

void thread2(void) {
  flags.flag2 = 2;
}

このコードは問題ないように見えるが、flag1flag2 が同じワードに格納される可能性がある。これら 2 つの変数に対する代入を行うスレッドが交互に実行され、値の格納が交互に行われると、一方のフラグだけが意図したとおりに設定され、もう一方は以前のままになることが考えられる。これは、どちらの文字も同じワード(プロセッサが操作できる最小単位)を使って表現されているためである。C99 は、これらのフラグの同時変更について何の保証もしていない。

各スレッドが別々のオブジェクトを変更している場合であっても、メモリ内では同じワードを変更している可能性がある。これは「CON00-C. 複数のスレッドによる競合状態を避ける」で説明されている問題とよく似ているが、同じメモリ位置が変更されていることがすぐにはわからないため、この例のほうが診断が難しくなる可能性がある。

適合コード (C11)

C11 に適合するプラットフォーム上で動作する場合、同じコードが適合コードになる。C99 とは異なり、C11 はメモリ位置を明示的に定義し、セクション 3.14.2 [ISO/IEC 9899:2011] で次の注意を示している。

注1: 2 つの実行スレッドが、互いに干渉することなく別々のメモリ位置を更新したり、メモリ位置にアクセスしたりすることができる。

違反コード (ビットフィールド)

隣接ビットフィールドが 1 つのメモリ位置に格納される可能性がある。その結果、別々のスレッドでの隣接ビットフィールドの変更が、C11 でも未定義動作になる。

struct multi_threaded_flags {
  unsigned int flag1 : 2;
  unsigned int flag2 : 2;
};

struct multi_threaded_flags flags;

void thread1(void) {
  flags.flag1 = 1;
}

void thread2(void) {
  flags.flag2 = 2;
}

C 言語規格 [ISO/IEC 9899:2011] セクション 3.14.3 には次のように記載されている。

注2: ビットフィールドおよび隣接非ビットフィールドメンバは、別々のメモリ位置にある。一方が入れ子にされた構造体宣言で宣言されもう一方がそうでない場合や、両方がサイズ 0 のビットフィールド宣言によって区別されている場合、両方が非ビットフィールドメンバ宣言によって区別されている場合は、同じことが 2 つのビットフィールドに適用される。ビットフィールド間で宣言されているすべてのメンバも(サイズ 0 ではない)ビットフィールドである場合は、これら間に挟まれるビットフィールドのサイズとは関係なく、同じ構造体内の 2 つの非アトミックビットフィールドの同時更新は安全ではない。

たとえば、次の一連のイベントが発生する可能性がある。

Thread 1: register 0 = flags
Thread 1: register 0 &= ~mask(flag1)
Thread 2: register 0 = flags
Thread 2: register 0 &= ~mask(flag2)
Thread 1: register 0 |= 1 << shift(flag1)
Thread 1: flags = register 0
Thread 2: register 0 |= 2 << shift(flag2)
Thread 2: flags = register 0
適合コード (ビットフィールド)

以下の適合コードでは、ミューテックスを持つフラグへのアクセスをすべて保護し、データ競合を防いでいる。最後に、フラグは共有体の中で long の隣に埋め込まれており、静的アサートによりフラグが long 以上の領域を占有しないことを保証している。この方法により、C11 に適合していないプラットフォーム上のビットフィールドでミューテックスがチェックしていないデータがアクセスされたり変更されることを防止する。

struct multi_threaded_flags {
  unsigned int flag1 : 2;
  unsigned int flag2 : 2;
};

union mtf_protect {
  struct multi_threaded_flags s;
  long padding;
};

static_assert(sizeof(long) >= sizeof(struct multi_threaded_flags));

struct mtf_mutex {
  union mtf_protect u;
  mtx_t mutex;
};

struct mtf_mutex flags;

void thread1(void) {
  int result;
  if ((result = mtx_lock(&flags.mutex)) == thrd_error) {
    /* エラー処理 */
  }
  flags.u.s.flag1 = 1;
  if ((result = mtx_unlock(&flags.mutex)) == thrd_error) {
    /* エラー処理 */
  }
}

void thread2(void) {
  int result;
  if ((result = mtx_lock(&flags.mutex)) == thrd_error) {
    /* エラー処理 */
  }
  flags.u.s.flag2 = 2;
  if ((result = mtx_unlock(&flags.mutex)) == thrd_error) {
    /* エラー処理 */
  }
}

静的アサートの詳細については、「DCL03-C. 定数式の値をテストするには静的アサートを使う」を参照のこと。

リスク評価

競合ウィンドウは小さいが、データの誤解釈が原因で代入や式が正しく評価されず、実行状態の悪影響を与えたり意図しない情報漏えいにつながる可能性がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

CON32-C

P8

L2

自動検出
ツール バージョン チェッカー 説明
Coverity 6.5 RACE_CONDITION 実装済み
参考資料
[ISO/IEC 9899:2011] Section 3.14, "Memory Location"
翻訳元

これは以下のページを翻訳したものです。

CON32-C. Prevent data races when adjacent data must be accessed by multiple threads (revision 59)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter