warning: iteration 1024 invokes undefined behavior
警告:ループカウンタ 1024 で未定義動作
[-Waggressive-loop-optimizations]
[gcc -fsanitize=addres]
[gcc -Wall -Wextra -O3]
■この記事の概要
メモリ破壊とは、簡単に言うと
主に配列からはみ出た領域に書き込みする事です。
この記事では、C言語におけるメモリ破壊の原因と影響、
strcpyやstrncpyなどの関数を使用する際の注意点を
サンプルコードを用いて詳しく解説しています。
GCCの警告とエラーを踏まえたプログラムの
安全性向上についても紹介します。
■文字列操作関数系のメモリ破壊
➡strcpy
#include <stdio.h>
#include <string.h>
//gcc -Wall -O
//warning: ‘__builtin___strcpy_chk’ writing 94 bytes
//into a region of size 16 overflows the destination
//[-Wstringop-overflow=]
int main(void) {
char buf[16];
char *jugemu = "寿限無寿限無五劫のすり切れ海砂利水魚の水行末雲来末風来末~~~";
strcpy(buf, jugemu);
puts(buf);
}この問題は
gcc -Wall -Wextra -Oで検出できます。
※ーOで最適化しないと検出できません。
参考:
汝が
“foo”とタイプするところで
“supercalifragilisticexpialidocious”とタイプする者が、
いつの日か必ずいるからである
➡strncpy
#include <stdio.h>
#include <string.h>
//clang -Wall
//warning: 'strncpy' size argument is too large;
//destination buffer has size 4,
//but size argument is 1024
//[-Wfortify-source]
int main(void) {
char buf[4];
strncpy(buf,"abc",1024);
puts(buf);
}4byte領域に”abc”3byteと\0をコピーするので
問題なさそうですが、
第三引数の1024がくせ者です。
➡strcat
#include <stdio.h>
#include <string.h>
//gcc strcat.c -Wall -O2
// warning: ‘__builtin___strcat_chk’ forming offset [4, 6] is
//out of the bounds
//[0, 4] of object ‘buf1’ with type ‘char[4]’
//[-Warray-bounds]
int main(void){
char buf1[] = "abc";
char buf2[] = "def";
strcat(buf1,buf2);
puts(buf1);
}
第一引数に第二引数を加える領域がありません。
➡memcpy
#include <stdio.h>
#include <string.h>
//gcc -Wall -Wextra -O -fsanitize=address
//warning: ‘__builtin_memcpy’ forming offset [4, 1023] is
//out of the bounds [0, 4]
//[-Warray-bounds]
int main(void){
char buf[1024];
//OK:"abc"の後ろは0で埋まる
strncpy(buf,"abc",sizeof(buf));
puts(buf);
//NG:"def"の後ろを領域外参照する
memcpy(buf,"def",sizeof(buf));
puts(buf);
}
文字列定数”def”の後ろを参照してしまいます。
➡sprintf
#include <stdio.h>
//gcc -fsanitize=address
int main(void){
char input[4] = "abcd";//終端文字が無い
char output[5];
sprintf(output,"%s",input);
puts(output);
}
inputに終端文字\0が無いので
暴走して
\0を見つけるまでoutputに書き込みします。
■標準ライブラリ関数系のメモリ破壊
➡scanf
#include <stdio.h>
//gcc -fsanitize=address
int main(void){
printf("4桁の数字を入れてね");
char buf[4];//終端文字を考慮していない
scanf("%s",buf);
printf("%sが入力されました\n",buf);
}
4桁の数字文字列を収容するには終端文字\0を入れて5byte必要です。
➡fgets
#include <stdio.h>
//warning:
//‘fgets’ writing 8192 bytes into a region of size 4
//overflows the destination
//[-Wstringop-overflow=]
int main(void){
int num;
char buf[4];
printf("4桁の数字を入れてね");
fgets(buf,BUFSIZ,stdin);
sscanf(buf,"%d",&num);
printf("%dが入力されました\n",num);
}
4桁の数字文字列を収容するには終端文字\0を入れて5byte必要です。
■ループ系のメモリ破壊
➡<と<=を間違える
static int ary[1024];
int main(void){
for(int i = 0 ; i <= 1024 ;i++){//★NG
ary[i] = 0 ;
}
}static int ary[1024];
int main(void){
for(int i = 0 ; i < 1024 ;i++){
ary[i] = 0 ;
}
}修正例は<に変更する。
➡回り切ったカウンタを添字に使う
//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を添字に使う時は注意してください。
■配列系のメモリ破壊
➡配列の添え字が一つ多い
(Off-by-one Error)
warning: array subscript 10 is outside array bounds of ‘char[10]’
警告:配列の添字 10 は ‘char[10]’ の配列境界外です。
[-Warray-bounds]
//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]を参照すると未定義動作となり何が起きるかわかりません。
➡要素数とサイズを間違える
#include <stdint.h>
static char aryC[1024];
static int aryI[1024];
int main(void){
//たまたま上手くいく例
for(uint32_t i = 0 ; i < sizeof(aryC) ;i++){
aryC[i] = 0 ;
}
//配列の要素数とサイズは違う
for(uint32_t i = 0 ; i < sizeof(aryI) ;i++){//★NG
aryI[i] = 0 ;
}
}要素数はサイズではありません。
sizeof(aryI) != 1024
sizeof(aryI) == sizeof(int)*1024
です。
このコードではLP64モードの場合、4096回転するので
配列のメモリ破壊が発生します。
➡修正例
#include <stdint.h>
static char aryC[1024];
static int aryI[1024];
int main(void){
const uint32_t maxC = sizeof(aryC)/sizeof(aryC[0]);
for(uint32_t i = 0 ; i < maxC ;i++){
aryC[i] = 0 ;
}
const uint32_t maxI = sizeof(aryI)/sizeof(aryI[0]);
for(uint32_t i = 0 ; i < maxI ;i++){
aryI[i] = 0 ;
}
}
配列の要素数を求める定石 sizeof(配列)/sizeof(配列[0])を使います。
➡配列[-1]を参照
warning: array subscript -1 is below array bounds of ‘char[10]’
警告:配列[-1]を参照した
[-fsanitize=address -O]
[-Warray-bounds]
//clang -fsanitize=address -O
#include <stdio.h>
int main(void) {
int idx = -1 ;
char buf[10]="0123456789";
printf("%c\n",buf[idx]);
}buf[-1]を参照してしまいます。
プログラムが異常終了するかどうかは
運しだいです。
➡降順ループで配列[-1]を参照
//gcc -fsanitize=address
#include <stdio.h>
#include <string.h>
int main(void){
char str[] = "ABCDEFG";
for(int i = strlen(str);i>=0;i--){
printf("src[%d]=:%x:=%c:\n",
i-1,
str[i-1],
str[i-1]
);
}
printf("\n");
}降順ループのこの例ではiが0の時
src[i-1]で
src[-1]を参照してしまいます。
■終端文字系のメモリ破壊
➡¥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コピーしました。
■エラーチェックの不良
➡添字のチェック間違い
//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) {でしょう。
➡チェックの順番
(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));
}