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

MSC37-C. 非 void 型関数の制御が関数定義の最終行に到達しないことを保証する

void 型関数の実行が、return 文を評価することなく閉じ中括弧 (}) まで到達した場合、関数呼出しの戻り値の使用は未定義の動作となる(C 標準、附属書 J.2「未定義の動作」の 88 を参照)。

違反コード

以下の違反コード例では、strcmp() に渡された 2 つの文字列が等しくない場合に、制御が checkpass() 関数の最終行に到達する。これは未定義の動作となる。return 文を通らない制御パスにおける checkpass 関数の返り値はコンパイラによって異なるだろう。

#include <string.h>
#include <stdio.h>

int checkpass(const char *password) {
  if (strcmp(password, "pass") == 0) {
    return 1;
  }
}

void func(const char *userinput) {
  if (checkpass(userinput)) {
    printf("Success\n");
  }
}

このエラーは多くの場合コンパイラが警告する(「MSC00-C. 高い警告レベルでのコンパイルで警告が出ないようにする」を参照)。

適合コード

次の適合コードでは、checkpass() 関数は常に return 文を実行する。

#include <string.h>
#include <stdio.h>

int checkpass(const char *password) {
  if (strcmp(password, "pass") == 0) {
    return 1;
  }
  return 0;
}

void func(const char *userinput) {
  if (checkpass(userinput)) {
    printf("Success!\n");
  }
}
違反コード

以下のコード例では、配列 input に整数 delim が含まれていない場合、getlen() 関数の最終行に制御が到達する。この場合 getlen() 関数の戻り値は不定となるが、この戻り値は配列のインデックスとして使われているため、バッファオーバーフローが発生する可能性がある。

#include <stddef.h>

size_t getlen(const int *input, size_t maxlen, int delim) {
  for (size_t i = 0; i < maxlen; ++i) {
    if (input[i] == delim) {
      return i;
    }
  }
}

void func(int userdata) {
  size_t i;
  int data[] = { 1, 1, 1 };
  i = getlen(data, sizeof(data), 0);
  data[i] = userdata;
}
処理系固有の詳細 (GCC)

本ルールに違反すると予期せぬ結果をもたらすことがある。次のコード例で考えてみよう。

#include <stdio.h>

size_t getlen(const int *input, size_t maxlen, int delim) {
  for (size_t i = 0; i < maxlen; ++i) {
    if (input[i] == delim) {
      return i;
    }
  }
}

int main(int argc, char **argv) {
  size_t i;
  int data[] = { 1, 1, 1 };

  i = getlen(data, sizeof(data), 0);
  printf("Returned: %zu\n", i);
  data[i] = 0;

  return 0;
}

このプログラムを GCC で -Wall オプションを付けてコンパイルすると、ほとんどのバージョンで次の警告が出力される。

example.c: In function 'getlen':
example.c:12: warning: control reaches end of non-void function

input 引数で渡された配列のどの要素も delim の値に一致しない。Linux 上の GCC 4.4.3 でコンパイルして実行すると、制御は getlen() 関数の終わりに達し、5 を返す。これは data 配列に対する境界外書込みを引き起こす。

適合コード

以下の適合コードでは、getlen() のインターフェイスを変更した。結果の値はユーザが指定したポインタに格納し、関数の返り値はエラーの状態を表している。この種のエラーを処理する最善の方法は、アプリケーションやエラーの種類によって異なる(エラー処理については「ERR00-C. エラー処理には一貫性のある方針を採用する」を参照)。

int getlen(const int *input, size_t maxlen, int delim,
           size_t *result) {
  for (size_t i = 0; i < maxlen; ++i) {
    if (input[i] == delim) {
      if (result != NULL) {
        *result = i;
      }
      return 0;
    }
  }
  return -1;
}

void func(int userdata) {
  size_t i;
  int data[] = {1, 1, 1};
  if (getlen(data, sizeof(data), 0, &i) != 0) {
    /* エラー処理 */
  } else {
    data[i] = userdata;
  }
}
例外

MSC37-EX1: C 標準 5.1.2.2.3節の第1パラグラフには「main 関数を終了する } に到達した場合、main 関数は、値 0 を返す」と規定されている[ISO/IEC 9899:2011]。つまり main() 関数の場合は、制御が return 文を実行せずに終わりまで到達しても構わない。

リスク評価

void 型関数の実行において return 文を評価せずに関数の終わりまで制御が到達している場合、その戻り値を使用すると、バッファオーバーフローやプログラムの予期せぬ動作を引き起こす可能性がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

MSC37-C

P9

L2

自動検出
ツール バージョン チェッカー 説明
CodeSonar 4.0 LANG.STRUCT.MRS Missing Return Statement
PRQA QA-C v8.2 2888  
SonarQube Plugin 3.1 S935  
関連するガイドライン
CERT C コーディグスタンダード MSC01-C. 論理的な完全性を追求する
参考文献
[ISO/IEC 9899:2011] 5.1.2.2.3, "Program Termination"
翻訳元

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

MSC37-C. Ensure that control never reaches the end of a non-void function (revision 52)

Top へ

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