programing

strcpy()/strncpy()는 유닉스에서 최적화를 설정할 때 여분의 공간이 있는 구조 부재에서 충돌합니까?

newstyles 2023. 10. 11. 20:34

strcpy()/strncpy()는 유닉스에서 최적화를 설정할 때 여분의 공간이 있는 구조 부재에서 충돌합니까?

프로젝트를 쓸 때, 저는 이상한 문제에 부딪혔습니다.

문제를 다시 만들기 위해 작성한 최소한의 코드입니다.저는 의도적으로 공간을 충분히 할당한 다른 것 대신 실제 문자열을 저장하고 있습니다.

// #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()/WSL합니다.puts()제가 입력한 내용은 아래와 같습니다.
  • 와 함께clang -O0 OSX에서 코드는 다음과 충돌합니다.strcpy().
  • 와 함께gcc/clang -O2아니면-O3 Ubuntu/Fedora/WSL에서 코드가 충돌합니다(!!!).strcpy(),하는 동안에memcpy()잘 통합니다.
  • 와 함께gcc.exe윈도우에서는 최적화 수준이 무엇이든 코드가 잘 작동합니다.

그리고 코드의 몇가지 다른 특징들을 발견했습니다.

  • 충돌을 재현하기 위한 최소 입력은 9바이트(종료자 0 포함) 또는1+sizeof(p->c) ( 그 그 정도 길이면 (혹은 그 이상이면) 충돌이 보장됩니다 (사랑하는...)

  • ( 1MB)에 여유 을malloc()이 되지 않습니다그건 도움이 안 돼.위의 행동은 전혀 변하지 않습니다.

  • strncpy()는 세 번째 인수에 정확한 길이를 제공하더라도 완전히 동일하게 동작합니다.

  • 포인터는 중요하지 않은 것 같습니다. ifchar *c다로 .long long c(또는int64_t됩니다.), 됩니다: (Update: 이미 변경됨).

  • 충돌 메시지가 규칙적으로 보이지 않습니다.많은 추가 정보가 함께 주어집니다.

    crash

이 모든 컴파일러를 시도해 보았지만 별 차이가 없었습니다.

  • GCC 5.4.0 (Ubuntu/Fedora/OS X/WSL, 모두 64비트)
  • GCC 6.3.0 (Ubuntu만 해당)
  • GCC 7.2.0 (안드로이드, norepro???) (C4droid의 GCC입니다.)
  • Clang 5.0.0 (Ubuntu/OS X)
  • MinGW GCC 6.3.0 (Windows 7/10, 둘 다 x64)

또한 이 사용자 지정 문자열 복사 기능은 표준과 정확히 유사하며 위에서 언급한 모든 컴파일러 구성에서 잘 작동합니다.

char* my_strcpy(char *d, const char* s){
    char *r = d;
    while (*s){
        *(d++) = *(s++);
    }
    *d = '\0';
    return r;
}

질문:

  • 왜 그럴까요?strcpy()실패? 어떻게 그럴 수 있죠?
  • 최적화가 켜져 있는 경우에만 실패하는 이유는 무엇입니까?
  • 는 왜요?memcpy()아랑곳하지 않고 실패하다-O레벨??

*Struct Member 접근 위반에 대해 상담하실 분은 이쪽으로 오시면 됩니다.


objdump -d 파일의 (WSL 에서) :

objdump


추신. 처음에는 구조를 작성하려고 하는데, 마지막 항목은 동적으로 할당된 공간(스트링의 경우)에 대한 포인터입니다.struct to file을 쓸 때 포인터를 쓸 수 없습니다.나는 실제 문자열을 써야 합니다.그래서 저는 이 해결책을 생각해냈습니다: 포인터 대신 문자열을 강제로 저장하는 것입니다.

에 .gets()만 사용합니다 저는 프로젝트에서 사용하지 않고 위의 예시 코드만 사용합니다.

당신이 하는 일은 정의되지 않은 행동입니다.

가 할 수 있습니다.sizeof int64_trint64_t c. 그래서 만약 당신이 더 많은 것을 쓰려고 노력한다면.sizeof int64_t ()sizeof cc 초과 에 한계 를합니다.이런 것은 ㅇsizeof "aaaaaaaa">sizeof int64_t.

을 하여 정확한 malloc()이 , 것이라고 할 수 .sizeof int64_tn에strcpy()아니면memcpy()해요의 에 전화를 합니다. 왜냐하면 당신이 주소를 보내주거든요.c 명)int64_t c).

TL;DR: 9바이트를 8바이트로 구성된 유형으로 복사하려고 합니다(바이트가 옥텟이라고 가정합니다).(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);
}

참고: 표준 견적은 이 답변을 참조하시기 바랍니다.

Ubuntu 16.10에서 이 문제를 재현했는데 흥미로운 점을 발견했습니다.

을 사용하여 컴파일할 경우gcc -O3 -o ./test ./test.c이 8 합니다.

가 에 GCC 했습니다를 대체한 되었습니다.strcpymemcpy_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로 요소가 GCC 을 믿게 합니다.c길이가 정확히 8바이트입니다.

그리고.memcpy_chk복사 길이가 네 번째 인수보다 크면 실패합니다!

다음과 같은 두 가지 솔루션이 있습니다.

  • 구조 수정

  • 하기 -D_FORTIFY_SOURCE=0()gcc test.c -O3 -D_FORTIFY_SOURCE=0 -o ./test강화 기능을 해제합니다.

    주의:이렇게 하면 전체 프로그램에서 버퍼 오버플로 검사가 완전히 비활성화됩니다.!!

이 코드가 정의되지 않은 동작일 수도 있고 그렇지 않을 수도 있는 이유에 대해서는 아직 자세한 답변이 없습니다.

이 부분은 기준이 과소 지정되어 있고, 이를 수정하기 위해 적극적인 제안이 있습니다.이 제안 하에서 이 코드는 정의되지 않은 동작이 아닐 것이며 충돌하는 코드를 생성하는 컴파일러는 업데이트된 표준을 준수하지 못할 것입니다. (아래의 마무리 문단에서 이 내용을 다시 살펴봅니다.

하지만 주의할 점은 다음과 같은 논의를 바탕으로 한 것입니다.-D_FORTIFY_SOURCE=2다른 답변들을 보면, 이 행동은 관련된 개발자들의 의도적인 행동인 것 같습니다.


다음 토막글을 바탕으로 이야기하겠습니다.

char *x = malloc(9);
pack *y = (pack *)x;
char *z = (char *)&y->c;
char *w = (char *)y;

x z w동일한 메모리 위치를 참조하면 동일한 값과 동일한 표현을 가질 수 있습니다.z에 대해 방식으로x .(합니다)를 다룹니다.w비록 OP가 그 사례를 탐구하지 않았기 때문에 우리는 알 수 없지만, 그 둘 중 하나와 다르게.)

이 주제를 포인터 프로방스라고 합니다.포인터 값의 범위가 제한되는 객체를 의미합니다.컴파일러가 다음을 수행하고 있습니다.zr 있는 처럼.y->c에, 에.x는 전체 9바이트 할당에 대해 입증력을 가지고 있습니다.


현재 C 표준은 증명력을 잘 명시하지 않습니다.포인터 빼기와 같은 규칙은 동일한 배열 개체에 대한포인터 사이에서만 발생할 수 있습니다. 이러한 규칙은 프로방스 규칙의 한 예입니다.또 다른 증명 규칙은 우리가 논의하고 있는 코드인 C 6.5.6/8에 적용되는 규칙입니다.

포인터에 정수 유형을 포함하는 식을 추가하거나 포인터에서 뺄 때 결과는 포인터 피연산자의 유형을 갖습니다.포인터 피연산자가 배열 개체의 요소를 가리키고 배열이 충분히 큰 경우 결과 배열 요소와 원래 배열 요소의 첨자 차이가 정수 식과 같도록 원래 요소에서 오프셋된 요소를 가리킵니다. 만약 즉라면,P에 대한 지적합니다.i- 열 번째 식 - ,(P)+Nl적으로, 으로)N+(P)및 ) (P)-N()N다.n는 ) 는를 .i+n에 -i−n배열 개체의 - 요소가 존재하는 경우.한,식 P가면,(P)+1 지나며고인 Q합니다를 을 가리킵니다.(Q)-1배열 개체의 마지막 요소를 가리킵니다.포인터 피연산자와 결과가 모두 동일한 배열 개체의 요소를 가리키거나 배열 개체의 마지막 요소를 가리키면 평가에서 오버플로가 발생하지 않습니다. 그렇지 않으면 동작이 정의되지 않습니다.보다 1 , 단 보다 1는 안 *평가되는 연산자.

의 에 대한 .strcpy,memcpy 이 이 은 다음 포인터에서 의 증가분은다.합니다.(P)+1본 규칙에서 논하는 바와 같이

"array object"라는 용어는 배열로 선언되지 않은 개체에 적용될 수 있습니다.이 내용은 6.5.6/7에 나와 있습니다.

이러한 연산자의 목적을 위해 배열의 요소가 아닌 개체에 대한 포인터는 개체 유형을 요소 유형으로 하는 길이 1의 배열의 첫 번째 요소에 대한 포인터와 동일하게 동작합니다.


여기서 중요한 질문은 "배열 개체"란 무엇인가 하는 것입니다.이 코드에서는?y->c,*y malloc 에 9 에 ? malloc 에 9?

결정적으로, 표준은 이 문제를 전혀 조명하지 않습니다.하위 객체가 있는 객체가 있을 때마다 표준은 6.5.6/8이 객체를 가리키는 것인지 하위 객체를 가리키는 것인지를 말하지 않습니다.

더 복잡한 요인은 표준이 "array"와 "array object"에 대한 정의를 제공하지 않는다는 것입니다.하지만 간단히 말하면, 그 물체는 다음과 같이 할당된 것입니다.malloc표준의 여러 곳에서 "array"로 표현되므로 여기서 9바이트 개체는 "array object"의 유효한 후보인 것으로 보입니다. (실제로 이는 을 사용하는 경우에 해당하는 유일한 후보입니다.x모든 사람들이 합법적이라고 동의할 것이라고 생각하는 9바이트 할당을 반복합니다.


참고: 이 섹션은 매우 추측성이 풍부하며, 왜 여기 있는 컴파일러들이 선택한 솔루션이 자체적으로 일관성을 갖지 못하는지에 대한 주장을 제시하고자 합니다.

다음과 같은 주장이 제기될 수 있습니다.&y->c는,입니다.int64_t소개체 .하지만 이것은 곧바로 어려움으로 이어집니다.를 들면,면,까?y*y? 그렇다면.(char *)y다가 .*y그러나 이는 다른 유형 및 뒤로 포인터를 캐스팅하면 원래 포인터를 반환해야 한다는 6.3.2.3/7 규칙과 모순됩니다(정렬을 위반하지 않는 한).

그것이 다루지 않는 또 다른 것은 중복되는 증거입니다.포인터를 같은 값이지만 더 작은 프로빈스(큰 프로빈스의 부분 집합)의 포인터와 비교할 수 있습니까?

또한 하위 객체가 배열인 경우에 동일한 원리를 적용하면 다음과 같습니다.

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로 서 UB되지만은char *r = (char *)&arr;하지 않다. 에 이 .사실 저는 수년 전 제 게시물에 이 견해를 홍보하곤 했습니다.하지만 저는 더 이상 그렇게 생각하지 않습니다. 제가 이 입장을 방어하려고 노력해본 경험으로 볼 때, 그것은 자기 일관성을 가질 수 없고, 너무 많은 문제 시나리오들이 있습니다.그리고 그것이 자기 일관성을 가질 수 있다고 하더라도, 그 기준에는 그것이 명시되어 있지 않다는 사실이 남아 있습니다.기껏해야 이 견해는 제안의 지위를 가져야 합니다.


마무리하려면 N2090: Clarifying Pointer Provenance(Draft Defect Report 또는 Proposal for C2x)를 읽어보는 것이 좋습니다.

그들의 제안은 프로방스는 항상 할당에 적용된다는 것입니다.이렇게 하면 객체와 하위 객체의 모든 복잡성이 제거됩니다.하위 할당이 없습니다.이 제안서에서, 모든.x z w는 동일하며 전체 9바이트 할당 범위에 사용될 수 있습니다.이전 섹션에서 논의했던 것과 비교할 때 단순성이 매력적입니다.

은 모두 은 입니다.-D_FORTIFY_SOURCE=2안전하지 않다고 판단되는 것에 의도적으로 충돌하는 것입니다.

은 은 gcc 를.-D_FORTIFY_SOURCE=2기본적으로 활성화됩니다.어떤 사람들은 그렇지 않습니다.이것은 서로 다른 컴파일러 간의 모든 차이점을 설명해 줍니다.h로 것입니다.-O3 -D_FORTIFY_SOURCE=2.

최적화가 켜져 있는 경우에만 실패하는 이유는 무엇입니까?

_FORTIFY_SOURCE합니다()-O포인터 캐스트/할당을 통해 개체 크기를 추적합니다.자세한 내용은 이 강연의 슬라이드를 참조하십시오._FORTIFY_SOURCE.

strcpy()가 실패하는 이유는 무엇입니까?어떻게?

를 호출합니다.__memcpy_chk위해서strcpy-D_FORTIFY_SOURCE=2. 이것은 대상 개체의 크기로 전달되는데, 이는 사용자가 생각하는 의미 / 사용자가 제공한 소스 코드로부터 알 수 있는 것이기 때문입니다.동일한 거래strncpyg__strncpy_chk.

__memcpy_chk고의의 낙태_FORTIFY_SOURCEC에서 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 컴파일러 탐색기에서).


는 왜요?memcpy()아랑곳하지 않고 실패하다-O벨?

몰라.

gcc는 단지 8B 로드/스토어 + 1B 로드/스토어를 완전히 정렬합니다. (최적화를 놓친 것 같습니다. malloc이 스택에서 수정하지 않았기 때문에 다시 로드하는 대신 즉시에서 다시 저장할 수 있다는 것을 알아야 합니다. (또는 8B 값을 레지스터에 유지하는 것이 좋습니다.)

왜 일을 복잡하게 만드나요?너무 복잡해지는 것처럼 이 부분에서 정의되지 않은 동작을 수행할 수 있는 공간이 더 넓어집니다.

memcpy((char*)&p->c, str, strlen(str)+1);
puts((char*)&p->c);

경고: 호환되지 않는 포인터 유형 [-Win compatible-pointer-type] puts(&p->c)에서 'puts'의 인수 1을 통과합니다;

당신은 분명히 할당되지 않은 기억 영역이나 운이 좋다면 쓸 수 있는 곳으로 가게 될 것입니다.

최적화 여부에 따라 주소 값이 변경될 수 있으며, 주소가 일치하기 때문에 작동할 수도 있고 그렇지 않을 수도 있습니다.당신은 단지 당신이 하고 싶은 일을 할 수 없습니다(기본적으로 컴파일러에게 거짓말을 함).

다음과 같습니다.

  • 구조에 필요한 것만 할당하고, 안에 있는 끈의 길이를 고려하지 마십시오. 소용이 없습니다.
  • gets안전하지 하기 때문에
  • 사용하다strdup에 잘 걸리는d memcpy문자열을 처리하기 때문에 사용하는 코드입니다.strdup하는 것을 않을 l-터미네이터이고,입니다.
  • 중복된 문자열을 해제하는 것을 잊지 마십시오.
  • .put(&p->c)되지 않은 입니다.

test.c:19:10: warning: 호환되지 않는 포인터 유형 [-Win compatible-pointer-type] puts(&p->c)에서 'puts'의 인수 1을 통과합니다;

나의 프러포즈

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;
}

포인터 p->c가 충돌의 원인입니다.
먼저 "부호가 없는 긴" 크기에 "*p" 크기의 구조를 초기화합니다.
두 번째로 포인터 p->c를 필요한 영역 크기로 초기화합니다.작업 복사본 만들기: strcpy(p->c, str);
마지막으로 첫 번째 무료(p->c)와 무료(p).
이거였던 것 같아요.
[편집]
제가 주장하겠습니다.오류의 원인은 구조가 포인터를 위한 공간만 예약하고 복사할 데이터를 포함하도록 포인터를 할당하지 않기 때문입니다.
.

본심의{포장 *p;charstr[1024];gets(str);size_tlen_struct = size of (*p) + size of (signed long);p = malloc(len_struc);p->c = malloc(strlen(str));strcpy(p->c, str); // 충돌하지 않습니다!풋(&p->c);free(p->c);무료(p);0을 반환합니다.}

[EDIT2]
이 방법은 데이터를 저장하는 전통적인 방법은 아니지만 다음과 같이 작동합니다.

팩2 *p;charstr[9] = "aaaaaaaaaa"; // 입력size_tlen = (팩) + (strlen(strl)) + 1의 크기;p = malloc(len);// 버전 1: 충돌strcpy((char*)p + (pack)의 크기, str);무료(p);

언급URL : https://stackoverflow.com/questions/47220212/strcpy-strncpy-crashes-on-structure-member-with-extra-space-when-optimizatio