プロジェクトを書いているときに、奇妙な問題に遭遇しました。
これは、問題を再現するために作成した最小限のコードです。十分なスペースを割り当てて、実際の文字列を他の場所に意図的に格納しています。
_// #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h> // For offsetof()
typedef struct _pack{
// The type of `c` doesn't matter as long as it's inside of a struct.
int64_t c;
} pack;
int main(){
pack *p;
char str[9] = "aaaaaaaa"; // Input
size_t len = offsetof(pack, c) + (strlen(str) + 1);
p = malloc(len);
// Version 1: crash
strcpy((char*)&(p->c), str);
// Version 2: crash
strncpy((char*)&(p->c), str, strlen(str)+1);
// Version 3: works!
memcpy((char*)&(p->c), str, strlen(str)+1);
// puts((char*)&(p->c));
free(p);
return 0;
}
_
上記のコードは私を混乱させています:
gcc/clang -O0
_を使用すると、strcpy()
とmemcpy()
の両方がLinux/WSLで機能し、以下のputs()
は入力したものをすべて提供します。clang -O0
_ on OSXを使用すると、コードはstrcpy()
でクラッシュします。gcc/clang -O2
_または_-O3
_ buntu/Fedora/WSLの場合、コードがクラッシュする(!!) at strcpy()
、memcpy()
はうまく機能します。gcc.exe
_を使用すると、コードは最適化レベルが何であってもうまく機能します。また、私はコードの他の特徴をいくつか見つけました:
1+sizeof(p->c)
。その長さ(またはそれ以上)でクラッシュが保証されます(Dear me ...)。malloc()
に余分なスペース(最大1MB)を割り当てても、役に立ちません。上記の動作はまったく変わりません。strncpy()
は、3番目の引数に正しい長さが指定されていても、まったく同じように動作します。char *c
_が_long long c
_(または_int64_t
_)に変更されても、動作は変わりません。 (更新:すでに変更されています)。クラッシュメッセージは定期的に表示されません。多くの追加情報が一緒に与えられます。
私はこれらのコンパイラをすべて試しましたが、違いはありませんでした。
さらに、このカスタム文字列コピー関数は、標準の関数とまったく同じであり、上記のコンパイラー構成でうまく機能します。
_char* my_strcpy(char *d, const char* s){
char *r = d;
while (*s){
*(d++) = *(s++);
}
*d = '\0';
return r;
}
_
strcpy()
が失敗するのはなぜですか?どうすればいいの?-O
_レベルに関係なくmemcpy()
が失敗しないのはなぜですか?*構造体メンバーのアクセス違反について話し合いたい場合は、- ここ をご覧ください。
_objdump -d
_のクラッシュする実行可能ファイルの出力(WSL上)の一部:
追伸最初に、構造を記述します。その最後の項目は、動的に割り当てられたスペース(文字列用)へのポインターです。構造体をファイルに書き込むときに、ポインタを書き込むことができません。実際の文字列を書く必要があります。だから私はこの解決策を思いつきました:ポインターの代わりに文字列を強制的に保存します。
また、gets()
について文句を言わないでください。私のプロジェクトでは使用していませんが、上記のサンプルコードのみを使用しています。
私はこの問題をUbuntu 16.10で再現しましたが、何か面白いものを見つけました。
gcc -O3 -o ./test ./test.c
を使用してコンパイルすると、入力が8バイトを超えるとプログラムがクラッシュします。
逆転した後、GCCがstrcpy
をmemcpy_chkに置き換えたことがわかりました。これを参照してください。
// decompile from IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
int *v3; // rbx
int v4; // edx
unsigned int v5; // eax
signed __int64 v6; // rbx
char *v7; // rax
void *v8; // r12
const char *v9; // rax
__int64 _0; // [rsp+0h] [rbp+0h]
unsigned __int64 vars408; // [rsp+408h] [rbp+408h]
vars408 = __readfsqword(0x28u);
v3 = (int *)&_0;
gets(&_0, argv, envp);
do
{
v4 = *v3;
++v3;
v5 = ~v4 & (v4 - 16843009) & 0x80808080;
}
while ( !v5 );
if ( !((unsigned __int16)~(_Word)v4 & (unsigned __int16)(v4 - 257) & 0x8080) )
v5 >>= 16;
if ( !((unsigned __int16)~(_Word)v4 & (unsigned __int16)(v4 - 257) & 0x8080) )
v3 = (int *)((char *)v3 + 2);
v6 = (char *)v3 - __CFADD__((_BYTE)v5, (_BYTE)v5) - 3 - (char *)&_0; // strlen
v7 = (char *)malloc(v6 + 9);
v8 = v7;
v9 = (const char *)_memcpy_chk(v7 + 8, &_0, v6 + 1, 8LL); // Forth argument is 8!!
puts(v9);
free(v8);
return 0;
}
構造体パックにより、GCCは要素c
が正確に8バイト長であると認識します。
そして、コピー長が4番目の引数よりも大きい場合、memcpy_chk
は失敗します。
したがって、2つの解決策があります。
構造を変更する
コンパイルオプション-D_FORTIFY_SOURCE=0
(gcc test.c -O3 -D_FORTIFY_SOURCE=0 -o ./test
のような)を使用して、要塞化機能をオフにします。
注意:これにより、プログラム全体のバッファオーバーフローチェックが完全に無効になります。
あなたがしていることは未定義の動作です。
コンパイラーは、変数メンバーsizeof int64_t
にint64_t c
を超えて使用しないことを前提としています。したがって、c
にsizeof int64_t
(aka sizeof c
)より多くを書き込もうとすると、コードに範囲外の問題が発生します。これは、sizeof "aaaaaaaa"
> sizeof int64_t
のためです。
重要なのは、malloc()
を使用して正しいメモリサイズを割り当てたとしても、コンパイラはstrcpy()
またはmemcpy()
呼び出しでsizeof int64_t
を超えて使用しないことを前提としています。 c
(別名int64_t c
)のアドレスを送信するからです。
TL; DR:8バイトで構成されるタイプに9バイトをコピーしようとしています(1バイトがオクテットであると想定しています)。 (from @ Kcvin )
同様のものが必要な場合は、C99の柔軟な配列メンバーを使用します。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
size_t size;
char str[];
} string;
int main(void) {
char str[] = "aaaaaaaa";
size_t len_str = strlen(str);
string *p = malloc(sizeof *p + len_str + 1);
if (!p) {
return 1;
}
p->size = len_str;
strcpy(p->str, str);
puts(p->str);
strncpy(p->str, str, len_str + 1);
puts(p->str);
memcpy(p->str, str, len_str + 1);
puts(p->str);
free(p);
}
注:標準的な見積もりについては、 this の回答を参照してください。
このコードが未定義の動作である場合とそうでない場合がある理由について、詳細な回答はまだありません。
この領域では標準が十分に指定されておらず、修正するための提案が活発に行われています。その提案の下では、このコードは未定義の動作ではなく、クラッシュするコードを生成するコンパイラーは、更新された標準に準拠できません。 (私は私の最後の段落でこれを再訪します)。
ただし、他の回答での_-D_FORTIFY_SOURCE=2
_の説明に基づくと、この動作は、関係する開発者の意図によるものであるようです。
次のスニペットに基づいて話をします。
_char *x = malloc(9);
pack *y = (pack *)x;
char *z = (char *)&y->c;
char *w = (char *)y;
_
これで、x
z
w
の3つすべてが同じメモリロケーションを参照し、同じ値と同じ表現になります。ただし、コンパイラはz
をx
とは異なる方法で処理します。 (また、コンパイラーはw
をこれら2つのうちの1つとは異なる方法で処理します。ただし、OPがそのケースを調査しなかったため、どちらを使用するかはわかりません).
このトピックはポインタの来歴と呼ばれます。これは、ポインター値がどのオブジェクトに及ぶかを制限することを意味します。コンパイラはz
を_y->c
_にのみ起源があると見なしていますが、x
は9バイトの割り当て全体に起源があるとしています。
現在のC標準では、出所があまり明確に指定されていません。 ポインター減算は同じ配列オブジェクトへの2つのポインター間でのみ発生する可能性がありますなどのルールは、来歴ルールの例です。もう1つの起源規則は、ここで説明しているコードに適用されるもので、C 6.5.6/8です。
整数型の式がポインタに加算またはポインタから減算されると、結果はポインタオペランドの型になります。ポインターオペランドが配列オブジェクトの要素を指し、配列が十分に大きい場合、結果は元の要素からの要素オフセットを指し、結果の配列要素と元の配列要素の添え字の差が整数式と等しくなるようにします。つまり、式
P
が配列オブジェクトのi
番目の要素を指している場合、式_(P)+N
_(同等にN+(P)
)および_(P)-N
_(ここでN
の値はn
)は、配列オブジェクトの_i+n
_- thおよび_i−n
_- th要素をそれぞれポイントしますそれらは存在します。さらに、式Pが配列オブジェクトの最後の要素を指す場合、式_(P)+1
_は配列オブジェクトの最後の要素の1つ後を指し、式Q
は最後の要素の1つ前を指す場合配列オブジェクトの場合、式_(Q)-1
_は配列オブジェクトの最後の要素を指します。ポインターオペランドと結果の両方が同じ配列オブジェクトの要素を指している場合、または配列オブジェクトの最後の要素の1つ前を指している場合、評価によってオーバーフローが発生することはありません。それ以外の場合、動作は未定義です。結果が配列オブジェクトの最後の要素の1つ先を指している場合、その結果は、評価される単項_*
_演算子のオペランドとして使用されません。
strcpy
、memcpy
の境界チェックの正当性は常にこのルールに戻ります-これらの関数は、取得するためにインクリメントされるベースポインターからの一連の文字割り当てであるかのように動作するように定義されていますこのルールで説明されているように、ポインタの増分は_(P)+1
_によってカバーされます。
「配列オブジェクト」という用語は、配列として宣言されていないオブジェクトに適用される場合があることに注意してください。これは6.5.6/7で詳しく説明されています。
これらの演算子の目的では、配列の要素ではないオブジェクトへのポインターは、オブジェクトの型を要素型として、長さが1の配列の最初の要素へのポインターと同じように動作します。
ここでの大きな質問は次のとおりです:「配列オブジェクト」とは何ですか?このコードでは、それは_y->c
_、_*y
_、またはmallocによって返される実際の9バイトオブジェクトですか?
重要なことに、この基準はこの問題にまったく光を当てていません。サブオブジェクトを持つオブジェクトがある場合は常に、標準では6.5.6/8がオブジェクトとサブオブジェクトのどちらを参照しているかについては言及していません。
さらに複雑な要素は 標準では「配列」の定義は提供されていません でも「配列オブジェクト」でもありません。しかし、長い話を簡単に言うと、malloc
によって割り当てられたオブジェクトは、規格のさまざまな場所で「配列」として記述されているため、ここでの9バイトのオブジェクトは「配列オブジェクト」。 (実際、これはonlyであり、x
を使用して9バイトの割り当てを反復処理する場合の候補です。これは、だれもが合法であることに同意すると思います)。
注:このセクションは非常に推論的であり、ここでコンパイラによって選択されたソリューションが自己矛盾している理由についての議論を提供しようとします
_&y->c
_が起源が_int64_t
_サブオブジェクトであることを意味するという引数couldが作成されます。しかし、これはすぐに困難につながります。たとえば、y
には_*y
_の起源がありますか?もしそうなら、_(char *)y
_は来歴__*y
_をまだ持っているはずですが、これは6.3.2.3/7の規則に矛盾し、ポインターを別の型にキャストして元のポインターを返す必要があります(ただし、配置に違反していません)。
カバーしないもう1つのことは、出所の重複です。ポインターは、同じ値であるが起源が小さい(それが起源が大きいのサブセットである)ポインターと等しくない場合がありますか?
さらに、サブオブジェクトが配列である場合にも同じ原則を適用すると、次のようになります。
_char arr[2][2];
char *r = (char *)arr;
++r; ++r; ++r; // undefined behavior - exceeds bounds of arr[0]
_
arr
はこのコンテキストでは_&arr[0]
_を意味すると定義されているため、_&X
_の起源がX
の場合、r
は実際には配列の最初の行-おそらく驚くべき結果。
ここでchar *r = (char *)arr;
はUBにつながると言うことができますが、char *r = (char *)&arr;
はUBにつながりません。実際、私は何年も前に私の投稿でこの見解を宣伝してきました。しかし、私はもうやっていません。この立場を擁護しようとした私の経験では、それを自己矛盾のないものにすることはできません。問題のシナリオが多すぎます。そして、それを自己矛盾のないものにすることができたとしても、規格がそれを指定していないという事実が残っています。せいぜい、このビューはプロポーザルのステータスを持つべきです。
最後に、 N2090:ポインターの来歴の明確化(欠陥レポートのドラフトまたはC2xの提案) を読むことをお勧めします。
彼らの提案は、来歴は常にan allocationに適用されるというものです。これは、オブジェクトとサブオブジェクトのすべての複雑さを疑わしくレンダリングします。サブ割り当てはありません。この提案では、x
z
w
はすべて同一であり、9バイトの割り当て全体に使用できます。私の前のセクションで説明したものと比較して、このシンプルさは魅力的です。
これは、_-D_FORTIFY_SOURCE=2
_が安全でないと判断したもので意図的にクラッシュするためです。
一部のディストリビューションでは、デフォルトで_-D_FORTIFY_SOURCE=2
_を有効にしてgccをビルドします。一部ではありません。これは、異なるコンパイラ間のすべての違いを説明しています。おそらく、_-O3 -D_FORTIFY_SOURCE=2
_でコードをビルドすると、通常はクラッシュしないものになります。
最適化がオンの場合にのみ失敗するのはなぜですか?
__FORTIFY_SOURCE
_は、ポインタキャスト/割り当てを通じてオブジェクトサイズを追跡するために、最適化(_-O
_)を使用してコンパイルする必要があります。 __FORTIFY_SOURCE
_の詳細については、 この講演のスライド を参照してください。
Strcpy()が失敗するのはなぜですか?どうすればいいの?
gccはstrcpy
に対して___memcpy_chk
_を_-D_FORTIFY_SOURCE=2
_でのみ呼び出します。 これは、ターゲットオブジェクトのサイズとして_8
_を渡します。これは、それがあなたが意図していることだからです/ソースコードから把握できることあなたがそれを与えました。 ___strncpy_chk
_を呼び出すstrncpy
についても同様です。
___memcpy_chk
_は意図的に中止されます。 __FORTIFY_SOURCE
_は、CのUBを超えて、潜在的に危険なに見えるものを許可しない可能性があります。これにより、コードが安全でないと判断するライセンスが与えられます。 (他の人が指摘したように、構造体の最後のメンバーとしての柔軟な配列メンバー、および/または柔軟な配列メンバーとの共用体は、Cで行っていることを表現する方法です。)
gccは、チェックが常に失敗することを警告します。
_In function 'strcpy',
inlined from 'main' at <source>:18:9:
/usr/include/x86_64-linux-gnu/bits/string3.h:110:10: warning: call to __builtin___memcpy_chk will always overflow destination buffer
return __builtin___strcpy_chk (__dest, __src, __bos (__dest));
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
_
(Godboltコンパイラエクスプローラの _gcc7.2 -O3 -Wall
_から )。
_
-O
_レベルに関係なくmemcpy()
が失敗しないのはなぜですか?
IDK。
gccは、8Bロード/ストア+ 1Bロード/ストアを完全にインライン化します。 (最適化に失敗したように見えます。mallocがスタック上でそれを変更しなかったので、リロードせずにイミディエートからそれを格納できることを知っている必要があります。(または、8B値をレジスターに保持することをお勧めします。)
なぜ物事を複雑にするのですか?あなたがしているように過度に複雑化すると、その部分で未定義の動作のためのより多くのスペースが与えられます:
_memcpy((char*)&p->c, str, strlen(str)+1);
puts((char*)&p->c);
_
警告:互換性のないポインタty peから 'puts'の引数1を渡します[-Wincompatible-pointer-types] puts(&p-> c);
運が良ければ、割り当てられていないメモリ領域または書き込み可能な場所に明らかに到達します...
最適化するかしないかでアドレスの値が変更される場合があり、アドレスが一致するために機能する場合と機能しない場合があります。あなただけできませんあなたがやりたいことを行います(基本的にはコンパイラに依存)
私は...するだろう:
gets
は危険で陳腐化するため、使用しないでくださいstrdup
コードの代わりにmemcpy
を使用します。 strdup
は、nulターミネーターを割り当てることを忘れず、ターゲットに設定します。put(&p->c)
は未定義の動作ですtest.c:19:10:警告:互換性のないポインターからの 'puts'の引数1を渡しますpe [-Wincompatible-pointer-types] puts(&p-> c);
私の提案
_int main(){
pack *p = malloc(sizeof(pack));
char str[1024];
fgets(str,sizeof(str),stdin);
p->c = strdup(str);
puts(p->c);
free(p->c);
free(p);
return 0;
}
_