【C言語】
strcpyとstrNcpyの安全な使い方
~メモリ破壊対策に転送先サイズはドーンと取る~

warning: argument to ‘sizeof’ in ‘strncpy’ call is the same expression as the source;

警告:転送元のサイズではなく転送先のサイズでは?
[-Wsizeof-pointer-memaccess]

■この記事の概要

strcpy()はメモリ破壊の危険性が高いので使用禁止とし
変わりにstrncpy()を推奨するプロジェクトがあります。

しかしstrncpy()にすれば安全というわけではありません。

一番重要なのは
コピー元より
コピー先の容量が
必要十分大きい事を保証することです。

strncpyの第三引数に何を記述するか
決めているプロジェクトはほぼ皆無、
というか一律に決められません。

単純にstrcpy()を禁止しただけでは
strncpy()の使い方が混乱し
メモリ破壊が発生したり
暴走が発生したりします。


■結論:strcpyの安全な使い方はケチらない事

//コピー先のサイズはケチらずドーンと取る事
#include <stdio.h>
#include <string.h>
void OK(char *s){
    char    buf[1024];//コピー先をドーンと取る
    strcpy(buf,s);
    puts(buf);
}
void NG(char *s){
    char    buf[4]; //コピー先をケチるとメモリ破壊
    strcpy(buf,s);
    puts(buf);
}
int main(void){
    OK("Jugemu,Jugemu,Goko,no,Surikire");
    NG("Jugemu,Jugemu,Goko,no,Surikire");
}

strcpyやstrncpyコピー先のサイズはドーンと大きく取りましょう。
几帳面な人はピッタリなサイズを指定したがりますがケッチってはいけません。
ケチるとメモリ破壊のリスクが高まります。

とは言え職場によってはスタックオーバーフローの危険があるので先輩に適切なサイズを確認しましょう。

■sizeof(コピー元)でメモリ破壊

//sizeof(コピー元)でメモリ破壊
// warning: 'strncpy' size argument is too large; 
// destination buffer has size 4, 
// but size argument is 1024 
// [-Wfortify-source]
#include <stdio.h>
#include <string.h>
int main(void){
    char dst[4];
    char src[1024] = "123";    
    //コピー先よりコピー元が大きいのでメモリ破壊する
    strncpy(dst,src,sizeof(src));   
    puts(dst); 
}

このコードでは
コピー先のサイズ コピー元のサイズ
なので
メモリ破壊が発生します。

■sizeof(コピー先)で尻切れトンボの暴走

//sizeof(コピー先)で尻切れトンボ
//warning: ‘strncpy’ output may be truncated 
//  copying 16 bytes 
//  from a string of length 20 
//  [-Wstringop-truncation]
#include <stdio.h>
#include <string.h>

//コピーサイズがコピー先のサイズで尻切れトンボ
int main(void) {
    char dst[16]={0};
    char src[] = "I'll give you $10000";    
    
    puts(src);
    strncpy(dst,src,sizeof(dst));  
    puts(dst);  //終端文字が無いので暴走する  
}

このコードでは
コピー先に終端文字が設定されません。
このためコピー先を文字列として参照すると
終端文字が見つからず暴走します。

■一文字コピーにわざわざstrncpyを使わない事

//1文字コピーにわざわざstrncpyを使わない事
#include <stdio.h>
#include <string.h>
void OK(char *s){
    s[0] = 'O';     //〇:直接代入する
    puts(s);
}
void NG(char *s){
    strncpy(s,"X",1);//X:わざわざstrncpyを使ってる
    puts(s);
}
int main(void){
    char sa[] = "AAAA"; 
    OK(sa);    
    char sb[] = "BBBB"; 
    NG(sb);
}

1文字しか転送しないのにstrncpyを使う人がいますが、直接代入すれば済むので
わざわざstrncpy関数をコールするのは止めましょう。

一文字コピーにsizeofを使うと意図しない結果になる

//一文字コピーにsizeofを使うと意図しない結果になる
#include <stdio.h>
#include <string.h>

void NG(char *s1,char *s2){
    //sizeof('X')で1を期待しない事
    //CとC++で結果が違う
    strncpy(s1,"X",sizeof('X'));//1文字コピーしたかった
    puts(s1);

    //sizeof("X")で1を期待しない事
    strncpy(s2,"X",sizeof("X"));//1文字コピーしたかった
    puts(s2);
}
int main(void){
    char s1[] = "1234";
    char s2[] = "5678";
    NG(s1,s2);
}

sizeof(‘X’)シングルクォーテーションは
C言語ではsizeof(int)と同じで4になります。
C++ではsizeof(char)と同じで1になります。sizeof(“X”)ダブルクォーテーションは
終端文字\0が加わるので2になります。
いずれも1を期待すると予期せぬ結果となります。

■strncpyは終端文字¥0を忘れやすい

//strncpyは終端文字¥0を忘れやすい
//[-Wstringop-truncation]
#include <stdio.h>
#include <string.h>
void NG(void){
    char    dsname[4] ;
    //終端文字\0を忘れたのか意図的なのか分からない
    strncpy(dsname,"1234",4);   
    puts(dsname);   //終端文字を期待すると暴走するかもしれない  
} 
void OK(void){
    char    dsname[4] ;
    //[終端文字\0を意図的にぬく]とコメントを書く
    strncpy(dsname,"1234",4);   
    printf("%4.4s\n",dsname);//終端文字がなくても文字数指定で大丈夫
} 
int main(void){
    OK();
    NG();
}

コピー元の長さが第三引数と同じなので
コピー先には終端文字\0が設定されません。

プログラマが意図的に終端文字を抜いたのか
うっかり終端文字の考慮を忘れたのか
読み手にはわかりにくいです。

■strncpyは第2引数が小さくても第3引数が大きいとメモリ破壊する

//strncpyは第2引数が小さくても第3引数が大きいとメモリ破壊する
//warning: 'strncpy' size argument is too large; 
//destination buffer has size 456, 
//but size argument is 789 
//[-Wfortify-source]
#include <stdio.h>
#include <string.h>
int main(void){
    char    dst[456] ;
    //#1 < #3 ならば
    //#2 < #1 でも、メモリ破壊が発生する
    strncpy(dst,"123",789);
    puts(dst);
}

問題なさそうですが
コピー先を789byteになるまで終端文字¥0で埋めるのでメモリ破壊が発生します。

■ポインターのサイズしかコピーしない

//ポインターのサイズしかコピーしない
#include <stdio.h>
#include <string.h>

void NG(char src[]){
    char dst[1024]={0};
    
    printf("src=%s\n",src);
    //ポインタのサイズ(ILP32=4,LP64=8)しかコピーしない
    strncpy(dst,src,sizeof(src));  
    printf("dst=%s\n",dst); 
}
int main(void){
    NG("1234567890123456");
}

sizeof(src)と
strlen(src)は似ていますが違います。

このコードの場合
strlen(src)は文字列の長さですが
sizeof(src)はポインタのサイズです
ポインタのサイズは
ILP32=4,LP64=8固定です。

■strcpyの代わりにstrncpyを使っているが遅いだけで間違いやすい

//strcpyの代わりにstrncpyを使っているが
//  遅いだけで間違いやすい
#include <stdio.h>
#include <string.h>
void    NG(char *src){   
    char    dst[1024] ;
    //  遅いだけで記述量が多く間違いやすい
    strncpy(dst,src,strlen(src)+1); 
    puts(dst); 
}
void    OK(char *src){   
    char    dst[1024] ;
    //  単純
    strcpy(dst,src); 
    puts(dst); 
}
int main(void){
    char *src = "Jugemu,Jugemu,Goko,no,Surikire";
    OK(src);
    NG(src);
}

strncpy(dst,src,strlen(src)+1);は
strcpy(dst,src);と比較して
処理速度が遅くなるだけで
安全でも何でもありません。

コピー元が長すぎると
strncpy(dst,src,strlen(src)+1);は
strcpy(dst,src);と
同じようにメモリ破壊します。

記述量も多いので終端文字\0の分
+1も忘れがちです。

参考:

STR03-C. null 終端バイト文字列を不注意に切り捨てない