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

POS47-C. 非同期キャンセルが可能なスレッドを使用しない

スレッドの処理において、pthread は、即座にキャンセルするか、または特定のキャンセルポイントまでキャンセルを遅らせるかを設定できる。しかし、ほとんどのスレッドは安全に即座に (非同期に) キャンセルできないため、非同期にキャンセルするは危険である。

IEEE 標準規格には次のように記述されている。

安全にキャンセルできる関数に限り、非同期キャンセルが可能なスレッドから呼び出すことができる。

非同期のキャンセルは、スレッドを強制終了させるときにシグナルをスレッドに渡す場合と同じ道をたどるため、「CON37-C. マルチスレッドプログラムで signal() を呼び出さない」(「SIG02-C. 標準的な機能を実装する際はシグナルの使用を避ける」に密接に関連する)と同様の問題をもたらす。POS44-C と SIG02-C では、スレッドの突然のキャンセルがデータ競合状態をもたらす危険性に言及している。

違反コード

次の違反コードでは、worker スレッドが ab を繰り返し交換するという単純な処理を行っている。

このコードは単一のロックを使用している。global_lock ミューテックスは、ワーカースレッドとメインスレッドが変数 ab にアクセスする際に衝突しないことを保証する。

ワーカースレッドは、メインスレッドによってキャンセルされるまでab の値を繰り返し交換する。その後、メインスレッドは ab の現在の値を出力する。理想的には、一方の値は 5 で、もう一方の値は 10 になるべきである。

volatile int a = 5;
volatile int b = 10;

/* スレッドが a と b に安全にアクセスできるようにロックする */
pthread_mutex_t global_lock = PTHREAD_MUTEX_INITIALIZER;

void* worker_thread(void* dummy) {
  int i;
  int c;
  int result;

  if ((result = pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,&i)) != 0) {
    /* エラー処理 */
  }

  while (1) {
    if ((result = pthread_mutex_lock(&global_lock)) != 0) {
      /* エラー処理 */
    }
    c = b;
    b = a;
    a = c;
    if ((result = pthread_mutex_unlock(&global_lock)) != 0) {
      /* エラー処理 */
    }
  }
  return NULL;
}


int main(void) {
  int result;
  pthread_t worker;

  if ((result = pthread_create( &worker, NULL, worker_thread, NULL)) != 0) {
    /* エラー処理 */
  }
  
  /* .. しばらくの間ワーカスレッドが同時に実行される */

  /* 文字がいつ読み取られるかは分からないため、プログラムの実行は続くかもしれない */
  if ((result = pthread_cancel(worker)) != 0) {
    /* エラー処理 */
  }
  /* pthread_join はスレッドが終了するのを待つ */
  if ((result = pthread_join(worker, 0)) != 0) {
    /* エラー処理 */
  }

  if ((result = pthread_mutex_lock(&global_lock)) != 0) {
    /* エラー処理 */
  }
  printf("a: %i | b: %i", a, b);
  if ((result = pthread_mutex_unlock(&global_lock)) != 0) {
    /* エラー処理 */
  }

  return 0;
}

しかし、非同期キャンセルがいつ起こるかわからないため、このプログラムは競合状態を引き起こしやすい。global_lock ミューテックスが保持されている間にワーカーがキャンセルされた場合、global_lock ミューテックスは二度と解放されない。この場合、メインスレッドは global_lock を獲得しようとして永久に待機し、プログラムはデッドロックに陥る。

また、メインスレッドは pthread_setcanceltype() を呼び出す前に作業スレッドをキャンセルする可能性もある。この場合、ワーカースレッドが pthread_setcanceltype() を呼び出すまでキャンセルは延期される。

違反コード

次の例では、ワーカースレッドは割り込みが発生した場合に global_lock ミューテックスを解放するように設定している。

void release_global_lock(void* dummy) {
  int result;
  if ((result = pthread_mutex_unlock(&global_lock)) != 0) {
    /* エラー処理 */
  }
}

void* worker_thread(void* dummy) {
  int i;
  int c;
  int result;

  if ((result = pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,&i)) != 0) {
    /* エラー処理 */
  }

  while (1) {
    if ((result = pthread_mutex_lock(&global_lock)) != 0) {
      /* エラー処理 */
    }
    pthread_cleanup_push( release_global_lock, NULL);
    c = b;
    b = a;
    a = c;
    pthread_cleanup_pop(1);
  }
  return NULL;
}

非同期キャンセルはいつ起こるかわからないため、グローバル変数は依然として競合状態を引き起こしやすい。たとえば、ワーカースレッドは最終行(a = c)の直前でキャンセルされることもあり得る。その場合、b の直前の値は消失する。結果として、メインスレッドは ab が同じ値であると出力することになる。

pthread_setcanceltype() を呼び出す前にワーカースレッドがキャンセルされた場合、プログラムは依然として競合状態を引き起こしやすい。この場合、ワーカースレッドが pthread_setcanceltype() を呼び出すまでキャンセルは延期される。

さらに、可能性は低いものの、global_lock を獲得した後、pthread_cleanup_push() を呼び出す前にワーカースレッドがキャンセルされた場合、プログラムは依然としてデッドロックする可能性がある。この場合、ワーカースレッドは global_lock を保持したままキャンセルされ、プログラムはデッドロック状態となる。

適合コード

IEEE 標準規格には次のように記載されている。

最初にmain() を呼び出したスレッドも含め、新しく作成されるスレッドのキャンセル状態は PTHREAD_CANCEL_ENABLE、型は PTHREAD_CANCEL_DEFERRED であるものとする。

IEEE 標準規格によると、POSIX のデフォルトの状態は PTHREAD_CANCEL_DEFERRED であるため、適合コードにおいて pthread_setcanceltype() を呼び出す必要がない。

void* worker_thread(void* dummy) {
  int c;
  int result;

  while (1) {
    if ((result = pthread_mutex_lock(&global_lock)) != 0) {
      /* エラー処理 */
    }
    c = b;
    b = a;
    a = c;
    if ((result = pthread_mutex_unlock(&global_lock)) != 0) {
      /* エラー処理 */
    }

    /* 安全にキャンセルできる。キャンセルポイントを作成 */
    pthread_testcancel();
  }
  return NULL;
}

このコードは while ループ本体の末尾までワーカースレッドのキャンセルを制限しているため、作業スレッドはデータを a != b の状態に保つことができる。ゆえに、プログラムは a が 5 で b が 10、または a が 10 で b が 5 であると出力するが、ワーカースレッドがキャンセルされると、a と b は異なる値を持つことが常に明らかである。

違反コードに存在していたその他の競合状態は、ここでは引き起こされる可能性はない。ワーカースレッドがキャンセルの種類を変更することはないため、不適切に初期化される前にキャンセルさせられる可能性がない。また、global_lock ミューテックスを保持したままキャンセルさせられる可能性がないため、デッドロックの可能性もなく、ワーカースレッドはクリーンアップハンドラを登録する必要がない。

リスク評価

非同期にキャンセルできるスレッドの不適切な使用は、意図せぬデータの破損、リソースリーク、最悪の場合は予期しない結果を引き起こす可能性がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

POS47-C

P12

L1

関連するガイドライン
Java セキュアコーディングスタンダード CERT/Oracle 版 THI05-J. スレッドの強制終了にThread.stop()メソッドを使用しない
参考文献
[MKS] pthread_cancel() Man Page
[Open Group 2004] Threads Overview
翻訳元

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

POS47-C. Do not use threads that can be canceled asynchronously (revision 39)

Top へ

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