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

MEM12-C. リソースの使用および解放の最中に発生するエラーが原因で関数を終了する場合に、Goto 連鎖の使用を検討する

MEM12-C. リソースの使用および解放の最中に発生するエラーが原因で関数を終了する場合に、Goto 連鎖の使用を検討する

多くの場合、関数は複数のリソースを割り当てる必要がある。このような関数が操作に失敗し、割り当てられていたリソースのすべてを解放せずに復帰した場合、メモリリークを引き起こす可能性がある。リソースの1つ(またはすべて)を解放し忘れるのはよくあるエラーである。goto 文の連鎖を使用し、解放されたリソースの秩序を守りつつ体系的に関数を終了することが、最も簡潔で問題のない解決法である。

違反コード

以下のコード例では、関数が途中で終了する可能性のあるすべての場合に対して終了コードが書かれている。fin2 をクローズし損ねるとリソースリークが発生し、ファイル記述子を開いたままにしてしまうことに注意。

この例では、errno_tNOERR が、「DCL09-C. errno エラーコードを返す関数は返り値を errno_t 型として定義する」に従って定義済みであると仮定している。返り値の型が errno_t の errno の値を返す関数を宣言している。別の方法として、errno_tintNOERR をゼロとして定義するやり方もある。

typedef struct object {   // 汎用的な構造体 -- 内容は問わない
  int propertyA, propertyB, propertyC;
} object_t;

errno_t do_something(void){
  FILE *fin1, *fin2;
  object_t *obj;
  errno_t ret_val;
  
  fin1 = fopen("some_file", "r");
  if (fin1 == NULL) {
    return errno;
  }

  fin2 = fopen("some_other_file", "r");
  if (fin2 == NULL) {
    fclose(fin1);
    return errno;
  }

  obj = malloc(sizeof(object_t));
  if (obj == NULL) {
    ret_val = errno;
    fclose(fin1);
    return ret_val;  // fin2 を閉じ忘れた !!
  }

  // ... 通常の処理 ...

  fclose(fin1);
  fclose(fin2);
  free(obj);
  return NOERR;
}

これは小さなコード例だが、より大きいコード例では、この種のエラーの検出がさらに困難になる。

適合コード

以下の修正版では、エラー処理専用のコードを goto 文の連鎖に置き換えている。エラーがなければ、制御の流れは SUCCESS ラベルまで進み、すべてのリソースを解放して NOERR を返す。エラーが発生すると、返り値は errno に設定され、制御の流れは失敗を示す適切なラベルまでジャンプし、復帰する前に該当するリソースが解放される。

// ... 上記の構造体と同じものを想定 ...

errno_t do_something(void) {
  FILE *fin1, *fin2;
  object_t *obj;
  errno_t ret_val = NOERR; // 成功時の返り値で初期化

  fin1 = fopen("some_file", "r");
  if (fin == NULL) {
    ret_val = errno;
    goto FAIL_FIN1;
  }

  fin2 = fopen("some_other_file", "r");
  if (fin2 == NULL) {
    ret_val = errno;
    goto FAIL_FIN2;
  }

  obj = malloc(sizeof(object_t));
  if (obj == NULL) {
    ret_val = errno;
    goto FAIL_OBJ;
  }

  // ... 通常の処理 ...

SUCCESS:     // すべてを後処理
  free(obj);

FAIL_OBJ:   // それ以外の場合は、開いたリソースのみ閉じる
  fclose(fin2);

FAIL_FIN2:
  fclose(fin1);

FAIL_FIN1:
  return ret_val;
}

この方法はコードがより明確なため有益である。プログラマは関数エラーごとに類似のコードを記述し直す必要はない。

適合コード (Linux カーネルの copy_process())

goto 文の連鎖の効果を示す例の中には、かなり大規模なものもある。以下の適合コードは Linux カーネルからの抜粋である。これはカーネルのバージョン 2.6.29 の kernel/fork.ccopy_process 関数である。

この関数は、内部関数でエラーコードが発生した場合に後処理を実行するために 17 個の goto ラベル(ここに表示されていないものもある)を使用する。エラーが発生しなかった場合、プログラムは新しいプロセス p へのポインタを返す。エラーが発生した場合は、プログラムは特定の goto ラベルに制御を渡し、その時点で実行が成功している関数部分の後処理を行い、まだ実行されていない関数部分については後処理は行わないようにする。その結果、正しく開くことができたリソースだけを実際に閉じることになる。

この抜粋内のすべてのコメントは、ここに表示されていないカーネル内の追加コードを示すために追加した。

static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					struct pt_regs *regs,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid,
					int trace)
{
  int retval;
  struct task_struct *p;
  int cgroup_callbacks_done = 0;

  if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
    return ERR_PTR(-EINVAL);

  /* ... */

  retval = security_task_create(clone_flags);
  if (retval)
    goto fork_out;

  retval = -ENOMEM;
  p = dup_task_struct(current);
  if (!p)
    goto fork_out;

  /* ... */

  /* すべてのプロセス情報のコピー */
  if ((retval = copy_semundo(clone_flags, p)))
    goto bad_fork_cleanup_audit;
  if ((retval = copy_files(clone_flags, p)))
    goto bad_fork_cleanup_semundo;
  if ((retval = copy_fs(clone_flags, p)))
    goto bad_fork_cleanup_files;
  if ((retval = copy_sighand(clone_flags, p)))
    goto bad_fork_cleanup_fs;
  if ((retval = copy_signal(clone_flags, p)))
    goto bad_fork_cleanup_sighand;
  if ((retval = copy_mm(clone_flags, p)))
    goto bad_fork_cleanup_signal;
  if ((retval = copy_namespaces(clone_flags, p)))
    goto bad_fork_cleanup_mm;
  if ((retval = copy_io(clone_flags, p)))
    goto bad_fork_cleanup_namespaces;
  retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
  if (retval)
    goto bad_fork_cleanup_io;

  /* ... */

  return p;

  /* ... 後処理ここから開始 ... */

bad_fork_cleanup_io:
  put_io_context(p->io_context);
bad_fork_cleanup_namespaces:
  exit_task_namespaces(p);
bad_fork_cleanup_mm:
  if (p->mm)
    mmput(p->mm);
bad_fork_cleanup_signal:
  cleanup_signal(p);
bad_fork_cleanup_sighand:
  __cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:
  exit_fs(p); /* blocking */
bad_fork_cleanup_files:
  exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
  exit_sem(p);
bad_fork_cleanup_audit:
  audit_free(p);

  /* ... 他の後処理 ... */

fork_out:
  return ERR_PTR(retval);
}
リスク評価

割り当てられたメモリを解放しなかった場合、または開いたファイルを閉じなかった場合、メモリリークが発生し、予期せぬ結果が生じることがある。

レコメンデーション

深刻度

可能性

修正コスト

優先度

レベル

MEM12-C

P4

L3

参考資料
Linux カーネルソースコード (v2.6.xx) 2.6.29, kernel/fork.c, the copy_process() Function
[Seacord 2013] Chapter 4, "Dynamic Memory Management"
翻訳元

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

MEM12-C. Consider using a Goto-Chain when leaving a function on error when using and releasing resources (revision 31)

Top へ

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