メインコンテンツ

符号拡張文字の値の不適切な使用

符号拡張によるデータ型変換は予期しない動作を引き起こす

説明

この欠陥は、取りうる負の値を含む符号付きまたはプレーンな char 変数をより大きい整数データ型に変換 (またはそのような変換を行う算術演算を実行) し、結果として得られる値を次のいずれかの方法で使用した場合に発生します。

  • EOF との比較のため (== または != を使用)

  • 配列インデックスとして

  • isalpha() または isdigit() などの ctype.h にある文字処理関数の引数として

負の値をもつ符号付き char 変数を int などのより大きい型に変換した場合、符号ビットが保持されます (符号拡張)。符号ビットを考慮したと考えられる状況でも、これが特定の問題の原因となる場合があります。

たとえば、符号付き char の値 -1 は、文字 EOF (ファイルの終端) を表すことができますが、これは無効な文字です。char 型の変数 var がこの値を取得するとします。varchar 型の変数として扱う場合、この無効な文字値を考慮するために特別なコードの記述が必要になる場合があります。ただし、var++ などの (整数プロモーションを伴う) 演算を実行する場合、値が 0 になり、この値は意図せず有効な値 '\0' を表します。算術演算を通じて無効な値が有効な値に遷移したことになります。

-1 以外の負の値の場合でも、符号付き char から符号付き int への変換は別の問題につながる場合があります。たとえば、符号付き char 値 -126 は unsigned char 値 130 (拡張文字 '\202' に対応) と等価です。この値を char から int に変換する場合、符号ビットは保持されます。さらに、結果として得られる値を unsigned int にキャストすると、予想外に大きな値 4294967170 が得られます (32 ビットの int を仮定)。コードで最終的な unsigned int 型の変数が unsigned char 型の値 130 になることを想定している場合、予期しない結果になる可能性があります。

この問題の根本的原因は、より大きい型への変換時の符号拡張です。ほとんどのアーキテクチャでは、値を格納するために 2 の補数表現を使用します。この表現では、最上位ビットは値の符号を示します。より大きい型に変換する場合、符号を保持するために、この符号ビットをより大きい型のすべての先頭ビットにコピーすることによって変換が実行されます。たとえば、char 値 -3 は 11111101 として表現されます (8 ビットの char を仮定)。int に変換すると、表現は次のようになります。

11111111 11111111 11111111  11111101
値 -3 は、より大きい int 型で保持されます。ただし、unsigned int に変換すると、値 (4294967293) は元の char 値と同等な unsigned char と同じではなくなります。この問題を認識していない場合、コードで予期しない結果が生じる可能性があります。

リスク

以下の場合、Bug Finder は、char からより大きいデータ型への変換または変数をより大きいデータ型に暗黙的に変換する算術演算の後での変数の使用にフラグを設定します。

  • 変数値を EOF と比較する場合:

    char 値 -1 は無効な文字 EOF または有効な拡張文字の値 '\377' (unsigned char での等価値 255 に対応) を表す可能性があります。char 型の変数が int などのより大きい型にキャストされると、符号拡張により、EOF または '\377' のいずれかを表す char 値 -1 は EOF のみを表す int 値 -1 になります。unsigned char 値 255 を int 型の変数から復元することはできません。Bug Finder はこの状況にフラグを設定し、この変数を最初に unsigned char にキャストできるように (または、char から int への変換や EOF と比較する前の変換演算を避けることができるように) します。こうすることでのみ、EOF との比較が意味のあるものになります。符号拡張文字の値と EOF の比較を参照してください。

  • 変数値を配列インデックスとして使用する場合:

    char 型の変数が int などのより大きい型にキャストされると、符号拡張により、すべての負の値の符号は保持されます。負の値を直接使用して配列にアクセスすると、バッファー オーバーフローまたはバッファー アンダーフローの原因になります。負の値を考慮している場合でも、考慮の方法によっては、不適切な要素が配列から読み取られる可能性があります。配列インデックスとして使用される符号拡張文字の値を参照してください。

  • 変数値を引数として文字処理関数に渡す場合:

    C11 規格 (節 7.4) によると、unsigned char または EOF として表現できない整数引数を指定した場合、結果の動作は未定義になります。変換後の負の char 値は unsigned char または EOF として表現できないため、Bug Finder はこの状況にフラグを設定します。たとえば、符号付き char 値 -126 は unsigned char 値 130 と等価ですが、符号付き int 値 -126 は unsigned char または EOF として表現できません。

修正方法

より大きな整数データ型に変換する前に、符号付きまたはプレーンな char 値を明示的に unsigned char にキャストします。

char データ型を、文字を表現するためでなく、単純にメモリを節約するために小さいデータ型として使用する場合、符号拡張された char 値を使用すると、前述のリスクを回避できる可能性があります。その場合は、改めてレビューされないように結果またはコードにコメントを追加します。詳細は、以下を参照してください。

すべて展開する

#include <stdio.h>
#include <stdlib.h>
#define fatal_error() abort()

extern char parsed_token_buffer[20];

static int parser(char *buf)
{
    int c = EOF;
    if (buf && *buf) {
        c = *buf++;    
    }
    return c;
}

void func()
{
    if (parser(parsed_token_buffer) == EOF) { 
        /* Handle error */
        fatal_error();
    }
}

この例では、関数 parser によって文字列入力 buf を走査できます。文字列内のある文字の値が -1 の場合、これは EOF または有効な文字値 '\377' (unsigned char での等価値 255 に対応) のいずれかを表す可能性があります。int 型の変数 c に変換されると、その値は整数値 -1 になり、これは常に EOF です。その後の EOF との比較では、parser から返される値が実際に EOF であるかどうかが検出されません。

修正 — 変換前に unsigned char にキャスト

1 つの修正方法として、より大きな int 型に変換する前に、プレーンな char 型の値を unsigned char にキャストします。その後でのみ、parser の戻り値が本当に EOF であるかどうかをテストできます。

#include <stdio.h>
#include <stdlib.h>
#define fatal_error() abort()

extern char parsed_token_buffer[20];

static int parser(char *buf)
{
    int c = EOF;
    if (buf && *buf) {
        c = (unsigned char)*buf++;    
    }
    return c;
}

void func()
{
    if (parser(parsed_token_buffer) == EOF) { 
        /* Handle error */
        fatal_error();
    }
}
#include <limits.h>
#include <stddef.h>
#include <stdio.h>

#define NUL '\0'
#define SOH 1    /* start of heading */
#define STX 2    /* start of text */
#define ETX 3    /* end of text */
#define EOT 4    /* end of transmission */
#define ENQ 5    /* enquiry */
#define ACK 6    /* acknowledge */

static const int ascii_table[UCHAR_MAX + 1] =
{
      [0]=NUL,[1]=SOH, [2]=STX, [3]=ETX, [4]=EOT, [5]=ENQ,[6]=ACK,
      /* ... */
      [126] = '~',
      /* ... */
      [130/*-126*/]='\202',
      /* ... */
      [255 /*-1*/]='\377'
};

int lookup_ascii_table(char c)
{
    int i;
    i = (c < 0 ? -c : c);
    return ascii_table[i];
}

この例では、char 型の変数 cint 型の変数 i に変換されています。c が負の値をもつ場合、その値は i に代入される前に正の値に変換されます。ただし、この変換は、i が配列インデックスとして使用されるときに、予期しない値につながる可能性があります。次に例を示します。

  • c の値が無効な文字 EOF を表す -1 の場合、おそらくこの値を個別に扱う必要があります。ただし、この例では、c の値が -1 に等しいため、i の値が 1 になります。関数 lookup_ascii_table は、無効な文字値 EOF を考慮することなく、値 ascii_table[1] (つまり、SOH) を返します。

    char データ型を、文字を表現するためでなく、単純にメモリを節約するために小さいデータ型として使用する場合、この問題について心配する必要はありません。

  • c が負の値をもつ場合、i に代入されると、その符号は反転されます。ただし、i を使用して ascii_table の要素にアクセスする場合、この符号の反転により、予期しない値が読み取られる可能性があります。

    たとえば、c の値が -126 の場合、i の値は 126 になります。関数 lookup_ascii_table は値 ascii_table[126] (つまり、'~') を返しますが、おそらく想定している値は ascii_table[130] (つまり、'\202') です。

修正 – unsigned char へのキャスト

この問題を修正するには、char から int への変換を避けます。最初に、c が値 EOF であるかどうかをチェックします。その後、char 型の変数 c の値を unsigned char にキャストし、その結果を配列インデックスとして使用します。

#include <limits.h>
#include <stddef.h>
#include <stdio.h>

#define NUL '\0'
#define SOH 1    /* start of heading */
#define STX 2    /* start of text */
#define ETX 3    /* end of text */
#define EOT 4    /* end of transmission */
#define ENQ 5    /* enquiry */
#define ACK 6    /* acknowledge */

static const int ascii_table[UCHAR_MAX + 1] =
{
      [0]=NUL,[1]=SOH, [2]=STX, [3]=ETX, [4]=EOT, [5]=ENQ,[6]=ACK,
      /* ... */
      [126] = '~',
      /* ... */
      [130/*-126*/]='\202',
      /* ... */
      [255 /*-1*/]='\377'
};

int lookup_ascii_table(char c)
{
    int r = EOF;
    if (c != EOF) /* specific handling EOF, invalid character */
        r = ascii_table[(unsigned char)c]; /* cast to 'unsigned char' */
    return r;
}

結果情報

グループ: プログラミング
言語: C | C++
既定値: 手書きコードはオン、生成コードはオフ
コマンド ライン構文: CHARACTER_MISUSE
影響度: Medium

バージョン履歴

R2017a で導入