【C言語】
配列の範囲外アクセスは
Off-by-one Errorが多い

Off-by-one Error オフバイワンエラー

warning: array subscript 10 is outside array bounds of ‘char[10]’

警告:配列の添字 10 は ‘char[10]’ の配列境界外です。
[-Warray-bounds]

■配列の添え字が一つ多い
(Off-by-one Error)

//gcc -Wall -Wextra  -O2
//warning: array subscript 10 is above array bounds of ‘int[10]’ 
//[-Warray-bounds]
int main(void){
    int     ary[10]={0};
    
    //ダメ:参照できるのはary[0]~ary[9]まで
    return  ary[10];
}

配列は ary[10]で宣言したら、
参照できるのはary[0]~ary[9]までです。
ary[10]は参照できません。
ary[10]を参照すると未定義動作となり何が起きるかわかりません。


■<と<=を間違えて一回多く回り過ぎる

//gcc -Wall -Wextra -O
//warning: iteration 10 invokes undefined behavior 
//[-Waggressive-loop-optimizations]
int main(void) {
    int     ary[10];
    
    //ダメ:一回まわりすぎ
    for(int i=0;i<=10;i++){
        ary[i] = 0;
    }
    return  ary[0];
}

一回多く回りすぎです。
i <= 10 ではなくて
i < 10 です。

バグの多くは境界で発生するので
以上・以下・未満・超えるを使いわけましょう。


■回り切ったカウンタで配列を参照してしまう

//gcc   -Wall -Wextra -O2
//warning: array subscript 10 is above array bounds of ‘int[10]’
//[-Warray-bounds]
int main(void){
    int     ary[10]={0};
    int     i;
    for(i=0;i<10;i++){
        ary[i] = 0;
    }
    
    //ダメ:回り切ったiは10になってる
    return  ary[i];
}

forループを回り切ると添字の i は10になっているので
ary[10] の配列範囲外となります。
回り切った時のiを添字に使う時は注意してください。


■添字のチェック間違い

//gcc -Wall -Wextra  -O2 
//warning: array subscript 4 is above array bounds of ‘int[4]’ 
//[-Warray-bounds]
#include <stdio.h>
int f(int idx){
    int buf[4] = {'A','B','C','D'} ;
    if(idx > 4) {//ダメ:idx == 4 を許容する
        return  'X' ;
    }
    return  buf[idx] ;
}
int main(void){
    printf("%c\n",f(4));
}

添字のチェック条件が誤っているので
idx が4の時も
制御が下へ行ってbuf[4]を参照してしまいます。
if(idx > 4) { ではなくて
if(idx >= 4) {でしょう。


■終端文字¥0の1byte考慮抜けてる

warning: ‘__builtin_memcpy’ writing 5 bytes into a region of size 4 overflows the destination

警告:サイズ4の領域へ5バイト書き込み
[-Wstringop-overflow=]

//warning: ‘__builtin_memcpy’ writing 5 bytes into a region of size 4 overflows the destination 
//[-Wstringop-overflow=]
#include    <stdio.h>
#include    <string.h>

int main(void) {
    char    buf[4];
    strcpy(buf,"1234");
    puts(buf);
}

終端文字¥0の考慮が抜けています。
コピー先が
4byteしかないのに
5byteコピーしました。

■if(配列[添字]&&範囲チェック)➡チェックを先に

(style) Array index ‘idx’ is used before limits check.
警告:配列添字を範囲チェック前に使った[(style)arrayIndexThenCheck]

//gcc f60.c -fsanitize=address
#include <stdio.h>
char *f(int idx){
    char *buf[4] = {"赤","青","黄",NULL} ;
    if(buf[idx] != NULL && (idx < 4)) {
        return  buf[idx] ;
    }
    return  "無効" ;
}
int main(void){
    printf("%s\n",f(0));
    printf("%s\n",f(1));
    printf("%s\n",f(2));
    printf("%s\n",f(3));
    printf("%s\n",f(4));
}

if((ary[idx]!=NULL) && (idx<MAX)){は
第一条件(ary[idx]!=NULL) と
第二条件 (idx<MAX)を
同時に評価するのではなく
左から順番に評価します。

このため(ary[idx]を参照してしまった後で
(idx<MAX)のチェックをしても手遅れです。

配列領域外参照が発生する可能性があり、
これは未定義動作なので
プログラムが異常終了するかもしれません。

筆者の環境下では以下のようになりました。
gcc -fsanitize=address⇒異常終了する
gcc -fsanitize=address -O2⇒正常終了する


➡修正後:if(範囲チェック&&配列[添字])

//gcc f60.c -fsanitize=address
#include <stdio.h>
char *f(int idx){
    char *buf[4] = {"赤","青","黄",NULL} ;
    if((idx < 4) && (buf[idx] != NULL)) {
        return  buf[idx] ;
    }
    return  "無効" ;
}
int main(void){
    printf("%s\n",f(0));
    printf("%s\n",f(1));
    printf("%s\n",f(2));
    printf("%s\n",f(3));
    printf("%s\n",f(4));
}

if((idx<MAX) && (ary[idx]!=NULL))と
記述すると
第一条件(idx<MAX) が成立しなければ
第二条件(ary[idx]!=NULL)は評価されないので
配列領域外参照は発生しません。


&配列[0]+byte数は多分バグ

int     main(void){
    int     buf[1024];

    //配列末尾のアドレスを求める間違った計算
    int     *tail = &buf[0] + (sizeof(int)*1024);
    for(int *p = buf; p < tail ;p++){
        *p = 0;
    }
}

➡&配列[0]+要素数←正しい

int     main(void){
    int     buf[1024];

    //配列末尾のアドレスを求める正しい計算
    int     *tail = &buf[0] + 1024;
    for(int *p = buf; p < tail ;p++){
        *p = 0;
    }
}

参考:

EXP08-C. ポインタ演算は正しく使用する

ARR30-C. 境界外を指すポインタや配列添字を生成したり使用したりしない

CWE-125: Out-of-bounds Read

CWE-787: Out-of-bounds Write

CWE-193: Off-by-one Error