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

EXP35-C. 一時的な生存期間を持つオブジェクトを変更しない

C 標準には、一時的な生存期間を持つオブジェクトを変更すると未定義の動作となると記載されている。この定義は C99 標準の仕様とは異なる (C99 標準では関数呼び出しの結果を変更したり、次の副作用完了点の後にアクセスしたりすることが未定義の動作であると定義されている)。一時オブジェクトの生存期間は完全な式または完全な宣言子を持つ評価が終了する時点で完了し、関数呼び出しの結果にアクセスできるようになるためである。

C の関数は配列を返すことはできないが、配列を含む構造体や共用体を返すことはできる。したがって、関数の返り値に配列が含まれる場合は、その関数呼び出しを含む式中で配列を変更してはならない。C99 では、アクセスも禁じられている。

違反コード

以下のコード例では、関数呼び出しによって返された struct から配列を取得し、最初の値をインクリメントしようとしている。配列は変更されているため、未定義の動作が引き起こされる。

#include <stdio.h>

struct X { int a[6]; };

struct X addressee(void) {
  struct X result = { 1, 2, 3, 4, 5, 6 };
  return result;
}

int main(void) {
  printf("%x", ++(addressee().a[0]));
  return 0;
}
適合コード

以下の適合コードでは、addressee() が返した構造体は、printf() 関数を呼び出す前に変数 my_x に格納されている。

#include <stdio.h>

struct X { int a[6]; };

struct X addressee(void) {
  struct X result = { 1, 2, 3, 4, 5, 6 };
  return result;
}

int main(void) {
  struct X my_x = addressee();
  printf("%x", ++(my_x.a[0]));
  return 0;
}
違反コード (C99)

以下のコード例では、関数呼び出しによって返された struct から配列を取得しようとしている。

#include <stdio.h>

struct X { char a[6]; };

struct X addressee(void) {
  struct X result = { "world" };
  return result;
}

int main(void) {
  printf("Hello, %s!\n", addressee().a);
  return 0;
}

このコードは、以下の C 固有の三つの性質に由来する C99 の問題がある。

  1. C では、返り値の生存期間は次の副作用完了点で終了する。したがって、printf() が呼び出された時点では、addressee() 呼び出しが返した構造体が有効ではなくなっており、データが上書きされているかもしれない。
  2. C では、関数の引数は値で渡される。そのため、引数として渡されるすべてのオブジェクトに対してコピーが作られる。たとえば、"Hello, %s!\n" へのポインタのコピーが作られる。多くの状況では、これらのコピーは、実引数が副作用完了点の影響を受けることから保護する。
  3. 最後に C では、配列が関数の引数として渡される場合、配列をポインタに暗黙的に変換する。つまり、addressee().a へのポインタのコピーが作られ、このポインタが printf() に渡される。しかし、配列データ自身はコピーされず、printf() が呼び出されたときにはすでに存在しないかもしれない。

したがって、上記のコードで printf() の第2引数が参照するメモリの内容は不定である。

このコードの動作は C 標準で定義されている。一時オブジェクトの生存期間がそれを含む完全な式まで拡張されるためである。

処理系固有の詳細

Microsoft Visual C++ Version 8.0 では、このコードは問題なくコンパイルでき、エラーなく実行できる。GCC コンパイラバージョン 4.2 では、-Wall スイッチを使用している場合警告が出力され、Linux で実行するとセグメンテーション違反が起きる。しかし、GCC コンパイラで --std=c99 フラグを指定すると警告は出ず、エラーなしに実行される。

適合コード (C99)

以下の適合コードでは、addressee() が返した構造体は、printf() 関数を呼び出す前に変数 my_x に格納されている。

#include <stdio.h>

struct X { char a[6]; };

struct X addressee(void) {
  struct X result = { "world" };
  return result;
}

int main(void) {
  struct X my_x = addressee();
  printf("Hello, %s!\n", my_x.a);
  return 0;
}
リスク評価

次の副作用完了点のあとで、関数の中の配列へのアクセスや変更をしようとすると、予期しないまたはおそらく意図しないプログラム動作を引き起こす可能性がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

EXP35-C

P4

L3

自動検出

ツール

バージョン

チェッカー

説明

GCC

V. 4.3.5

 

-Wall フラグを使うと、違反を検出できる。

Splint

V. 3.1.1

 

 

関連するガイドライン
ISO/IEC TR 24772:2013 Dangling References to Stack Frames [DCM]
Side-effects and Order of Evaluation [SAM]
翻訳元

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

EXP35-C. Do not modify objects with temporary lifetime (revision 64)

Top へ

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