メインコンテンツ

CERT C++: EXP54-CPP

Do not access an object outside of its lifetime

説明

ルール定義

有効期間外でオブジェクトにアクセスしないようにします。1

Polyspace 実装

ルール チェッカーは以下の問題をチェックします。

  • 未初期化ポインター

  • 未初期化変数

  • 前に解放したポインターの使用

  • スタック変数へのポインターまたは参照が範囲外

  • 有効期間が一時的なオブジェクトにアクセスしています

チェッカーの拡張

チェッカーは次の方法で拡張できます。

すべて展開する

問題

未初期化ポインターは、デリファレンスの前にポインターにアドレスが割り当てられていない場合に発生します。

リスク

ポインターにアドレスが明示的に割り当てられていない場合、そのポインターは予測できない位置を指します。

修正方法

修正方法は欠陥の根本原因によって異なります。たとえば、アドレスをポインターに割り当てたが、その割り当てに到達不能である場合があります。

多くの場合、結果の詳細には欠陥につながる一連のイベントが表示されます。そのシーケンス内のどのイベントについても修正を実装できます。結果の詳細にイベント履歴が表示されない場合は、ソース コード内で右クリック オプションを使用して逆のトレースを行い、これまでの関連するイベントを確認できます。Polyspace デスクトップ ユーザー インターフェイスでの Bug Finder の結果の解釈も参照してください。

以下の修正例を参照してください。ポインターを宣言するときはポインターを NULL に初期化することをお勧めします。

問題を修正しない場合は、改めてレビューされないように結果またはコードにコメントを追加します。詳細は、以下を参照してください。

例 - 未初期化ポインター エラー
#include <stdlib.h>

int* assign_pointer(int* prev)
{
    int j = 42;
    int* pi;

    if (prev == nullptr) 
      {
        pi = new int;
        if (pi == nullptr) return NULL;
      }
    *pi = j;  //Noncompliant

    return pi;
}

prevnullptr でない場合、ポインター pi にはアドレスが割り当てられません。しかし、prevnullptr かどうかにかかわりなく、pi はすべての実行パスでデリファレンスされます。

修正 — すべての実行パスでポインターを初期化

1 つの修正方法として、prevnullptr でない場合に pi にアドレスを割り当てることができます。または、宣言中に pinullptr として初期化します。

#include <cstdlib>

int* assign_pointer(int* prev)
{
    int j = 42;
/*Fix: Initialize pointers by using nullptr during declaration*/
    int* pi = nullptr;

    if (prev == NULL) 
       {
        pi = new int;
        if (pi == nullptr) return NULL;
       } 
    /* Fix: Initialize pi in branches of if statement  */
    else 
        pi = prev;              
    *pi = j;
    return pi;
}
問題

未初期化変数は、変数の値が読み取られる前にその変数が初期化されていない場合に発生します。

リスク

変数が明示的に初期化されていない場合、変数値は予測できません。変数が特定の値をもつことは期待できません。

修正方法

修正方法は欠陥の根本原因によって異なります。たとえば、値を変数に割り当てたがその割り当てに到達不能であるか、条件付きステートメントの 2 つの分岐のいずれかで値を変数に割り当てた可能性があります。到達不能コードまたは割り当ての欠落を修正します。

多くの場合、結果の詳細には欠陥につながる一連のイベントが表示されます。そのシーケンス内のどのイベントについても修正を実装できます。結果の詳細にイベント履歴が表示されない場合は、ソース コード内で右クリック オプションを使用して逆のトレースを行い、これまでの関連するイベントを確認できます。Polyspace デスクトップ ユーザー インターフェイスでの Bug Finder の結果の解釈も参照してください。

以下の修正例を参照してください。宣言時に変数を初期化することをお勧めします。

問題を修正しない場合は、改めてレビューされないように結果またはコードにコメントを追加します。詳細は、以下を参照してください。

例 - 未初期化変数エラー
int get_sensor_value(void)
{
    extern int getsensor(void);
    int command;
    int val;

    command = getsensor();
    if (command == 2) 
      {
        val = getsensor();
      }

    return val; //Noncompliant
}

command が 2 でない場合、変数 val には値が割り当てられません。この場合、関数 get_sensor_value の戻り値は未定です。

修正 — 宣言に際しての初期化

1 つの修正方法として、一部の実行パスで初期化が回避されることのないように、宣言時に val の初期化を行います。

int get_sensor_value(void)
{
    extern int getsensor(void);
    int command;
    /* Fix: Initialize val */
    int val=0;

    command = getsensor();
    if (command == 2) 
      {
        val = getsensor();
      }

    return val;              
 }

val には初期値 0 が割り当てられます。command が 2 と等しくない場合、関数 get_sensor_value はこの値を返します。

問題

前に解放したポインターの使用は、メモリのブロックを、たとえば、関数 freedelete 演算子を使用して割り当て解除した後に、そのメモリ ブロックにアクセスした場合に発生します。

リスク

関数の malloccallocrealloc または演算子 new を使用してポインターに動的メモリが割り当てられている場合、ポインターはヒープ上のメモリ位置を指します。このポインターに対して関数 free または delete 演算子を使用すると、メモリの関連ブロックが割り当て解除されます。メモリのこのブロックにアクセスしようとすると、予測できない動作やセグメンテーション違反が発生する可能性があります。

修正方法

修正方法は欠陥の根本原因によって異なります。メモリを後で割り当て解除することを意図しているのか、またはアクセスする前に別のメモリ ブロックをポインターに割り当てることを意図しているのかを確認します。

メモリ ブロックを割り当て解除した後、対応するポインターに nullptr を割り当てることをお勧めします。ポインターをデリファレンスする前に、nullptr かどうかをチェックしてエラーを処理します。この方法により、割り当て解除されたブロックにアクセスするのを避けることができます。

例 - 前に解放したポインターの使用エラー
#include <cstdlib>
int increment_content_of_address(int base_val, int shift)
   { 
    int j;
    int* pi = new int;
    if (pi == NULL) return 0;

    *pi = base_val;
    delete pi;

    j = *pi + shift; //Noncompliant
 
    return j;
   }

delete 演算子は、pi が参照しているメモリのブロックを割り当て解除します。そのため、delete pi; ステートメント後の pi のデリファレンスは有効ではありません。

修正 — 使用後のポインターの割り当て解除

1 つの修正方法として、最後のインスタンスがアクセスされた後にのみポインター pi を割り当て解除することができます。

#include <cstdlib>

int increment_content_of_address(int base_val, int shift)
{
    int j;
    int* pi = new int;
    if (pi == NULL) return 0;

    *pi = base_val;

    j = *pi + shift;
    *pi = 0;

    /* Fix: The pointer is deallocated after its last use */
    delete pi;               
    return j;
}
修正 — std::unique_ptr を使用

別の修正方法として、生のポインターの代わりに、std::unique_ptr を使用することができます。std::unique_ptr などのスマート ポインターは、独自のリソースを管理します。スマート ポインターは明示的に割り当て解除する必要がないため、割り当て解除後に誤ってアクセスされることはありません。

#include <cstdlib>
#include <memory>

int increment_content_of_address(int base_val, int shift)
{
    int j;
    /* Fix: A smart pointer is used*/
    std::unique_ptr<int>   pi(new int(3));
    if (pi == nullptr) return 0;

    *pi = base_val;

    j = *pi + shift;
    *pi = 0;
    return j;
}
問題

スタック変数へのポインターまたは参照が範囲外は、ローカル変数へのポインターまたは参照が変数のスコープを逸脱している場合に発生します。次に例を示します。

  • 関数が、ローカル変数を指すポインターを返す。

  • 関数が代入 globPtr = &locVar を実行する。globPtr はグローバル ポインター変数、locVar はローカル変数です。

  • 関数が代入 *paramPtr = &locVar を実行する。paramPtr は関数パラメーター (つまり int** ポインターなど)、locVar はローカルの int 変数です。

  • C++ メソッドが代入 memPtr = &locVar を実行する。memPtr はメソッドが属するクラスのポインター データ メンバー、locVar はメソッドから見てローカルな変数です。

欠陥は、関数 alloca を使用して割り当てたメモリにも適用されます。この欠陥は静的なローカル変数には適用されません。Polyspace は、関数定義に含まれるローカル オブジェクトは同じスコープ内にあると仮定します。

リスク

ローカル変数にはスタック上のアドレスが割り当てられます。ローカル変数のスコープがいったん終了すると、このアドレスは再利用可能になります。このアドレスを使用して変数のスコープ外にあるローカル変数値にアクセスすると、予期しない動作を引き起こす可能性があります。

ローカル変数を指すポインターが変数のスコープを逸脱していると、Polyspace Bug Finder™ によってその欠陥が強調表示されます。この欠陥は、ポインターに格納されているアドレスが使用されていない場合でも発生します。コードを保守可能なものにするため、ポインターが変数のスコープを逸脱しないようにすることをお勧めします。ポインター内のアドレスが現在使用されていない場合でも、関数の他の使用者がそのアドレスを使用することで動作が未定義となる可能性があります。

修正方法

ローカル変数へのポインターまたは参照が変数スコープを逸脱しないようにします。

例 - ローカル変数を指すポインターが関数から返される
void func2(int *ptr) {
    *ptr = 0;
}

int* func1(void) {
    int ret = 0; //Noncompliant
    return &ret ;
}
void main(void) {
    int* ptr = func1() ;
    func2(ptr) ;
}

この例では、func1 はローカル変数 ret を指すポインターを返します。

main では、ptr はローカル変数のアドレスを指します。ret のスコープは func1 に制限されているため、func2 内で ptr がアクセスされると、そのアクセスは無効になります。

例 - ローカル変数を指すポインターがラムダ式によってエスケープする
auto createAdder(int amountToAdd) {
  int addThis = amountToAdd; //Noncompliant
  auto adder = [&] (int initialAmount) {
      return (initialAmount + addThis);
  };
  return adder;
}
 
void func() {
  auto AddByTwo = createAdder(2);
  int res = AddByTwo(10);
}

この例では、関数 createAdder で、参照によってローカル変数 addThis を取得するラムダ式 adder を定義しています。addThis のスコープは関数 createAdder に制限されます。createAdder によって返されたオブジェクトが呼び出されると、変数 addThis に対する参照がスコープ外からアクセスされます。この方法でアクセスが行われると、addThis の値は未定義となります。

修正 – 参照ではなくラムダ式でのコピーによってローカル変数を取得

関数がラムダ式オブジェクトを返す場合は、ラムダ式でローカル変数を参照によって取得しないようにします。代わりにコピーによって変数を取得します。

コピーによって取得された変数の有効期間はラムダ オブジェクトと同じです。しかし、参照によって取得された変数の有効期間は、多くの場合、ラムダ オブジェクト自体よりも短くなります。ラムダ オブジェクトを使用すると、スコープ外からアクセスされるこれらの変数は値が未定義となります。

auto createAdder(int amountToAdd) {
  int addThis = amountToAdd;
  auto adder = [=] (int initialAmount) {
      return (initialAmount + addThis);
  };
  return adder;
}
 
void func() {
  auto AddByTwo = createAdder(2);
  int res = AddByTwo(10);
}
問題

[有効期間が一時的なオブジェクトにアクセスしています] は、関数呼び出しから返された、有効期間が一時的なオブジェクトに対して読み取りまたは書き込みを試みると発生します。関数から返された、配列が含まれている構造体または共用体では、それらの配列メンバーは一時オブジェクトです。一時オブジェクトの有効期間が終了するのは、以下のとおりです。

  • C11 規格で定義されているように、その呼び出しを含む完全な式または完全な宣言子が終了したとき。

  • C90 および C99 規格で定義されているように、次のシーケンス ポイントの後。シーケンス ポイントとは、プログラムの実行において、すべての前の評価が完了し、以降の評価がまだ開始されていない時点のことです。

C++ コードでは、有効期間が一時的なオブジェクトに書き込みを行う場合のみ、[有効期間が一時的なオブジェクトにアクセスしています] によって欠陥が報告されます。

有効期間が一時的なオブジェクトがアドレスで返された場合、欠陥は報告されません。

リスク

有効期間が一時的なオブジェクトの変更は未定義の動作であり、プログラムの異常終了と移植性の問題の原因になる可能性があります。

修正方法

関数呼び出しから返されたオブジェクトをローカル変数に代入します。有効期間が一時的なオブジェクトの内容がこの変数にコピーされます。安全にこれを変更できるようになります。

例 - 関数呼び出しから返された、有効期間が一時的なオブジェクトの変更
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>

#define SIZE6 6

struct S_Array
{
    int t;
    int a[SIZE6];
};

struct S_Array func_temp(void);

/* func_temp() returns a struct value containing
* an array with a temporary lifetime.
*/
int func(void) {
 
/*Writing to temporary lifetime object is
 undefined behavior
 */
    return ++(func_temp().a[0]); //Noncompliant
}

void main(void) {
    (void)func();
}

この例では、func_temp() は配列メンバー a をもつ構造体を値で返します。このメンバーの有効期間は一時的です。これをインクリメントすることは未定義の動作です。

修正 — 書き込み前にローカル変数に戻り値を代入

1 つの修正方法として、func_temp() の呼び出しの戻り値をローカル変数に代入します。一時オブジェクト a の内容がこの変数にコピーされ、これを安全にインクリメントできます。

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

#define SIZE6 6

struct S_Array
{
    int t;
    int a[SIZE6];
};

struct S_Array func_temp(void);

int func(void) {

/* Assign object returned by function call to 
 *local variable
 */
    struct S_Array s = func_temp(); 

/* Local variable can safely be
 *incremented
 */
    ++(s.a[0]);                                           
    return s.a[0];
}

void main(void) {
    (void)func();
}

チェック情報

グループ: 02.式 (EXP)

バージョン履歴

R2019a で導入

すべて展開する


1 This software has been created by MathWorks incorporating portions of: the “SEI CERT-C Website,” © 2017 Carnegie Mellon University, the SEI CERT-C++ Web site © 2017 Carnegie Mellon University, ”SEI CERT C Coding Standard – Rules for Developing safe, Reliable and Secure systems – 2016 Edition,” © 2016 Carnegie Mellon University, and “SEI CERT C++ Coding Standard – Rules for Developing safe, Reliable and Secure systems in C++ – 2016 Edition” © 2016 Carnegie Mellon University, with special permission from its Software Engineering Institute.

ANY MATERIAL OF CARNEGIE MELLON UNIVERSITY AND/OR ITS SOFTWARE ENGINEERING INSTITUTE CONTAINED HEREIN IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.

This software and associated documentation has not been reviewed nor is it endorsed by Carnegie Mellon University or its Software Engineering Institute.