メインコンテンツ

CWE Rule 825

Expired Pointer Dereference

R2023a 以降

説明

ルールの説明

The program dereferences a pointer that contains a location for memory that was previously valid, but is no longer valid.

Polyspace 実装

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

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

  • 以前に割り当て解除したポインターの解放

  • 前の操作によって無効になった環境ポインター

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

  • putenv ファミリ関数の引数としての自動変数の使用

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

すべて展開する

問題

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

  • 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();
}
問題

この問題は、メモリのブロックが、関数 free を使用して中間割り当てなしで、連続して解放された場合に発生します。

リスク

malloccalloc、または realloc を使用してポインターに動的メモリが割り当てられている場合、ポインターはヒープ上のメモリ位置を指します。このポインターに対して関数 free を使用すると、メモリの関連ブロックが再割り当て用に解放されます。メモリのこのブロックを解放しようとすると、セグメンテーション違反が発生する可能性があります。

修正方法

修正方法は欠陥の根本原因によって異なります。最初の割り当て解除と 2 番目の割り当て解除の間でメモリ ブロックをポインターに割り当てることを意図しているかどうかを確認します。そうでない場合は、2 番目の free ステータスを削除します。

メモリ ブロックを解放した後、対応するポインターに NULL を割り当てることをお勧めします。ポインターを解放する前に、NULL 値かどうかをチェックしてエラーを処理します。この方法により、既に解放されているブロックを解放するのを避けることができます。

例 — 以前に割り当て解除したポインターの解放エラー
#include <stdlib.h>

void allocate_and_free(void)
{

    int* pi = (int*)malloc(sizeof(int));
    if (pi == NULL) return;

    *pi = 2;
    free(pi);
    free (pi);  //Noncompliant
    /* Defect: pi has already been freed */
}

最初の free ステートメントにより、pi が参照するメモリのブロックが解放されます。pi に対する 2 番目の free ステートメントにより、既に解放されたメモリが解放されます。

修正 — 重複する割り当て解除を削除

1 つの修正方法として、2 番目の free ステートメントを削除することができます。

#include <stdlib.h>

void allocate_and_free(void)
{

    int* pi = (int*)malloc(sizeof(int));
    if (pi == NULL) return;

    *pi = 2;
    free(pi);
    /* Fix: remove second deallocation */
 }
例 — サイズがゼロの可能性のある、過去に再割り当てされたポインターの解放
#include <stdlib.h>
  
void reshape(char *buf, size_t size) {
  char *reshaped_buf = (char *)realloc(buf, size);
  if (reshaped_buf == NULL) {
    free(buf);  //Noncompliant
  }
}

この例では、関数 reshape() の引数 size がゼロになり、その結果、realloc() でサイズがゼロの再割り当てが行われる可能性があります。GNU® ライブラリなどの一部の実装では、サイズがゼロの再割り当てによってメモリが解放されると、二重解放の欠陥につながります。

修正 – サイズがゼロの再割り当てを防止する

1 つの修正方法として、使用する前に realloc() のサイズ引数の値がゼロかどうかをチェックします。サイズ引数がゼロの場合は、メモリを再割り当てするのではなく、単に解放します。

#include <stdlib.h>

void reshape(char *buf, size_t size) {
  if (size != 0) {
    char *reshaped_buf = (char *)realloc(buf, size);
    if (reshaped_buf == NULL) {
      free(buf);
    }
  }
  else {
    free(buf);
  }

}
問題

この問題は、ホスト環境での操作によって環境が変更された後に、"main()" の第 3 引数を使用して環境にアクセスした場合に発生します。ホストされている環境では、多くの C 実装で非標準の構文がサポートされます。

main (int argc, char *argv[], char *envp[])
setenv または putenv ファミリ関数の呼び出しにより、*envp の参照先の環境に変更が加えられます。

リスク

setenv または putenv ファミリ関数の呼び出しによって環境が変更されると、環境のメモリが再割り当てされる可能性があります。そのホストされている環境のポインターが更新されず、正しくない場所を指す場合があります。このポインターを呼び出すと、予期しない結果が返されたり、プログラムの異常終了が発生したりすることがあります。

修正方法

ホストされている環境のポインターを使用しないようにします。代わりに、グローバル外部変数を使用します。Linux® では environ、Windows® では _environ_wenviron、またはこれらと同等のグローバル外部変数を使用します。環境を変更すると、これらの変数が更新されます。

例 — ポインター envp による環境へのアクセス
#include <stdio.h>
#include <stdlib.h>

extern int check_arguments(int argc, char **argv, char **envp);
extern void use_envp(char **envp);

/* envp is from main function */
int func(char **envp) 
{
    /* Call to setenv may cause environment
     *memory to be reallocated 
     */
    if (setenv(("MY_NEW_VAR"),("new_value"),1) != 0) 
    {
        /* Handle error */
        return -1;
    }
    /* envp not updated after call to setenv, and may
     *point to incorrect location.
     **/
    if (envp != ((void *)0)) {  //Noncompliant
        use_envp(envp);
/* No defect on second access to
*envp because defect already raised */
    }
    return 0;
}

void  main(int argc, char **argv, char **envp)
{
    if (check_arguments(argc, argv, envp))
    {
        (void)func(envp);
    }
}

この例では、環境のメモリが再割り当てされる可能性のある setenv の呼び出しの後に、func() 内で envp にアクセスしています。envp は、setenv によって環境が変更された後には更新されないため、正しくない場所を指す可能性があります。前のコード行で既に欠陥が報告されているため、use_envp() が呼び出されたときには欠陥が報告されません。

修正 — グローバル外部変数 environ を使用

1 つの修正方法として、setenv の呼び出し後に必ず更新される変数を使用して環境にアクセスします。たとえば、次のコードでは、ポインター envpmain() からも使用可能ですが、環境へのアクセスは func() 内でグローバル外部変数 environ を通じて行います。

#include <stdio.h>
#include <stdlib.h>
extern char **environ;

extern int check_arguments(int argc, char **argv, char **envp);
extern void use_envp(char **envp);

int func(void)
{
    if (setenv(("MY_NEW_VAR"), ("new_value"),1) != 0) {
        /* Handle error */
        return -1;
    }
  /* Use global external variable environ
   *which is always updated after a call to setenv */
    
    if (environ != NULL) { 
        use_envp(environ);
    }
    return 0;
}

void  main(int argc, char **argv, char **envp)
{
    if (check_arguments(argc, argv, envp))
    {
        (void)func();
    }
} 
問題

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

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

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

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

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

  • (C++11 以降) 関数が、参照によって関数のローカル変数を取得するラムダ式オブジェクトを返す。

欠陥は、関数 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);
}
問題

この問題は、putenv ファミリ関数の引数が自動持続期間をもつローカル変数である場合に発生します。

リスク

関数 putenv(char *string) は、引数のコピーを作成するのではなく、指定された引数を指すポインターを環境配列に挿入します。引数が自動変数の場合、putenv() 呼び出しを含む関数から値が返された後、メモリが上書きされる可能性があります。その後、別の関数が getenv() を呼び出した場合、適切にデリファレンスできないスコープ外の変数のアドレスが返されます。このスコープ外の変数は、環境変数が予期しない値を取る、プログラムが応答を停止する、任意のコード実行を可能にする脆弱性を生むなどの原因になる可能性があります。

修正方法

環境変数の設定/設定解除に setenv()/unsetenv() を使用します。または、putenv ファミリ関数の引数と動的に割り当てられたメモリを使用するか、アプリケーションに再入要件がない場合は引数と静的存続期間を使用します。たとえば、再帰や割り込みのないシングルスレッド実行には再入は必要ありません。実行中の呼び出し (再入) はできません。

例 — putenv() の引数としての自動変数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SIZE1024 1024

void func(int var)
{
    char env[SIZE1024];
    int retval = sprintf(env, "TEST=%s", var ? "1" : "0");
    if (retval <= 0) {
        /* Handle error */
    }
	/* Environment variable TEST is set using putenv().
	The argument passed to putenv is an automatic variable. */
    retval = putenv(env);   //Noncompliant
    if (retval != 0) {
        /* Handle error */
    }
}
              

この例では、sprintf() は文字列 TEST=varenv に格納します。その後、環境変数 TEST の値が putenv() を使用して var に設定されます。env は自動変数であるので、TEST の値は func() が値を返すときに変化する可能性があります。

修正 — putenv() の引数に static 変数を使用

env を静的存続期間の変数として宣言します。env のメモリ位置は、func() が値を返した後でも、プログラムの存続期間中は上書きされません。

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

#define SIZE1024 1024 
void func(int var)
{
	/* static duration variable */
    static char env[SIZE1024]; 
    int retval = sprintf(env,"TEST=%s", var ? "1" : "0");
    if (retval <= 0) {
        /* Handle error */
    }
	
	/* Environment variable TEST is set using putenv() */
    retval=putenv(env);   
	if (retval != 0) {
        /* Handle error */
    }
}
修正 — setenv() を使用して環境変数値を設定

TEST の値を var に設定するために、setenv() を使用します。

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

#define SIZE1024 1024 

void func(int var)
{
	/* Environment variable TEST is set using setenv() */
    int retval = setenv("TEST", var ? "1" : "0", 1); 
	
    if (retval != 0) {
        /* Handle error */
    }
}
問題

この問題は、メモリのブロックを関数 free を使用して解放した後で、そのブロックにアクセスした場合に発生します。

リスク

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

修正方法

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

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

例 — 前に解放したポインターの使用エラー
#include <stdlib.h>
#include <stdio.h>
 int increment_content_of_address(int base_val, int shift)
   { 
    int j;
    int* pi = (int*)malloc(sizeof(int));
    if (pi == NULL) return 0;

    *pi = base_val;
    free(pi);

    j = *pi + shift; //Noncompliant
    /* Defect: Reading a freed pointer */
 
    return j;
   }

free ステートメントにより、pi が参照するメモリのブロックが解放されます。そのため、free ステートメント後の pi のデリファレンスは有効ではありません。

修正 — 使用後にポインターを解放

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

#include <stdlib.h>

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

    *pi = base_val;

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

    /* Fix: The pointer is freed after its last use */
    free(pi);               
    return j;
}

チェック情報

カテゴリ: Pointer Issues

バージョン履歴

R2023a で導入