strcpy () / strncpy () аварийно завершает работу на элементе структуры с дополнительным пространством, когда оптимизация включена в Unix?

при написании проекта я столкнулся со странной проблемой.

это минимальный код, который мне удалось написать, чтобы воссоздать проблему. Я намеренно сохраняю фактическую строку вместо чего-то другого, с достаточным выделенным пространством.

// #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 на OSX, код падает с strcpy().
  • С gcc/clang -O2 или -O3 на Ubuntu / Fedora / WSL код аварии (!!) at strcpy(), а memcpy() работает хорошо.
  • С gcc.exe в Windows код хорошо работает независимо от уровня оптимизации.

также я нашел некоторые другие черты код:

  • (похоже) минимальный входной сигнал для воспроизведения катастрофы-это 9 байт (включая нуль-Терминатор), или 1+sizeof(p->c). С этой длиной (или более длинной) авария гарантирована (Дорогой я ...).
  • даже если я выделяю дополнительное пространство (до 1 МБ) в malloc(), это не поможет. Вышеуказанное поведение не изменилось.
  • strncpy() ведет себя точно так же, даже с правильной длины поставляются в 3-м аргументом.
  • указатель, похоже, не имеет значения. Если член структуры char *c превращается в long long c (или int64_t), поведение остается прежним. (Обновление: уже изменено).
  • сообщение об аварии не выглядит регулярным. Много дополнительной информации дается вместе.

    crash

я пробовал все эти компиляторы, и они не имели никакого значения:

  • GCC 5.4.0 (Ubuntu / Fedora / OS X / WSL, все 64-разрядные)
  • GCC 6.3.0 (только Ubuntu)
  • GCC 7.2.0 (Android, norepro???) (Этот является ли GCC от c4droid также)
  • 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 = '';
    return r;
}

вопросы:

  • почему strcpy() провал? Как это возможно?
  • почему он терпит неудачу, только если оптимизация о?
  • почему memcpy() не зависимо от -O уровня??

*если вы хотите обсудить о нарушении доступа к члену структуры, pleast head over здесь.


часть objdump -dвывод аварийного исполняемого файла (на WSL):

objdump


П. С. Изначально я хотел написать структуру, последний элемент которого является указателем на динамически выделенного пространства (для строки.) Когда я пишу структуру в файл, я не могу написать указатель. Я должен написать настоящую строку. Поэтому я придумал это решение: принудительно хранить строку вместо указателя.

также, пожалуйста, не жалуйтесь о gets(). Я не использую его в своем проекте, но только пример кода выше.

6 ответов


я воспроизвел эту проблему на своем 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;
}

ваш пакет struct заставляет GCC верить, что элемент c - это ровно 8 байт.

и memcpy_chk потерпит неудачу, если длина копирования больше чем четвертый аргумент!

Итак, есть 2 решения:

  • изменить структуру

  • использование параметров компиляции -D_FORTIFY_SOURCE=0(любит gcc test.c -O3 -D_FORTIFY_SOURCE=0 -o ./test), чтобы отключить функции укрепления.

    осторожностью: это полностью отключит проверку переполнения буфера во всей программе!!


что вы делаете-это неопределенное поведение.

компилятор может предположить, что вы никогда не будете использовать больше, чем sizeof int64_t для переменной int64_t c. Поэтому, если вы попытаетесь написать больше, чем sizeof int64_t(Он же sizeof c) on c, у вас будет проблема вне границ в вашем коде. Это так, потому что sizeof "aaaaaaaa">sizeof int64_t.

дело в том, даже если вы выделяете правильный размер памяти с помощью malloc(), компилятор может предположить, что вы никогда не будете использовать более sizeof int64_t в своем strcpy() или memcpy() звонок. Потому что вы отправляете адрес c (Он же int64_t c).

TL; DR: вы пытаетесь скопировать 9 байтов в тип, состоящий из 8 байтов (мы предполагаем, что байт является октетом). (От @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);
}

Примечание: Для стандартной цитаты, пожалуйста, обратитесь к этой ответ.


нет ответа еще подробно говорили о том, почему этот код может или не может быть неопределенным поведением.

стандарт недооценен в этой области, и есть предложение активно исправить его. В соответствии с этим предложением этот код не будет неопределенным поведением, и компиляторы, генерирующие код, который аварийно завершает работу, не будут соответствовать обновленному стандарту. (Я возвращаюсь к этому в моем заключительном пункте ниже).

но обратите внимание, что на основе обсуждения -D_FORTIFY_SOURCE=2 in другие ответы, похоже, это поведение намеренно со стороны участвующих разработчиков.


я буду говорить на основе следующего фрагмента:

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

теперь все три x z w обратитесь к тому же местоположению памяти и будет иметь то же значение и то же представление. Но компилятор обрабатывает z иначе x. (Компилятор также обрабатывает w по-разному к одному из этих двух, хотя мы не знаем, как ОП не исследовал этот случай).

эта тема называется указатель на происхождение. Это означает ограничение, для которого значение указателя может находиться в диапазоне. Компилятор принимает z как имеющий происхождение только за y->c, тогда как x имеет происхождение по всему 9-байтовому распределению.


текущий стандарт C не очень хорошо определяет происхождение. Такие правила, как вычитание указателя может происходить только между двумя указателями к тому же объекту массива пример правило провенанс. Еще одно правило происхождения относится к обсуждаемому нами коду, C 6.5.6 / 8:

когда выражение с целочисленным типом добавляется или вычитается из Указателя, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта array, а массив достаточно велик, результат указывает на элемент, смещенный от исходного элемента так, что разность индексов результирующего и исходного элементов массива равна целочисленному выражению. Другими словами, если выражение P точки i - й элемент объекта массива, выражения (P)+N (что эквивалентно N+(P)) и (P)-N (где N имеет значение n) указывают, соответственно,i+n-й и i−n - й элементы объекта array, при условии, что они существуют. Более того, если выражение P указывает на последний элемент массива объекта, выражение (P)+1 указывает на один мимо последнего элемента объекта массива, и если выражение Q указывает на один последний элемент объекта массива, выражение (Q)-1 указывает на последний элемент объекта array. Если и операнд указателя, и результат указывают на элементы одного и того же объекта массива или на один последний элемент объекта массива, оценка не должна вызывать переполнения; в противном случае поведение не определено. Если результат указывает один мимо последнего элемент объекта массива, он не должен использоваться в качестве операнда унарного * оператор, который оценивается.

обоснование для проверки границ strcpy, memcpy всегда возвращается к этому правилу - эти функции определяются так, как если бы они были серией назначений символов из базового указателя, который увеличивается, чтобы добраться до следующего символа, а увеличение указателя покрывается (P)+1 как уже обсуждалось в этой правило.

обратите внимание, что термин "объект массива" может применяться к объекту, который не был объявлен как массив. Это прописано в 6.5.6/7:

для целей настоящих операторов, указатель на объект, который не является элементом массива ведет себя так же, как указатель на первый элемент массива длины с типом объекта, его тип элемента.


большой вопрос здесь: что такое "массив объект"? В этом коде это y->c, *y, или фактический 9-байтовый объект, возвращенный malloc?

важно то, что стандарт не проливает никакого света на этот вопрос. Всякий раз, когда у нас есть объекты с подобъектами, стандарт не говорит, относится ли 6.5.6/8 к объекту или подобъекту.

еще одним осложняющим фактором является то, что стандарт не содержит определения для"array", ни для "объекта массива". Но резать долго короче говоря, объект, выделенный malloc описывается как "массив" в разных местах стандарта, поэтому кажется, что 9-байтовый объект здесь является допустимым кандидатом на "объект массива". (На самом деле это только такой кандидат для случая использования 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 здесь, но char *r = (char *)&arr; нет. На самом деле я много лет назад продвигал этот взгляд в своих постах. Но я больше этого не делаю: в моем опыте попыток защитить эту позицию, она просто не может быть сделана самосогласованной, слишком много проблемных сценариев. И даже если бы это можно было сделать самосогласованным, факт остается фактом: стандарт этого не уточняет. В лучшем случае это мнение должно иметь статус предложения.


чтобы закончить, я бы рекомендовал читать N2090: уточнение происхождения указателя (проект отчета о дефекте или предложение для C2x).

их предложение заключается в том, что происхождение всегда относится к выделение. Это делает спорными все сложности объектов и подобъектов. Нет никаких перераспределений. В этом предложении все x z w идентичны и могут использоваться для диапазона по всему 9-байтовому распределению. ИМХО простота этого привлекательна, по сравнению с тем, что обсуждалось в моем предыдущем разделе.


это все из-за -D_FORTIFY_SOURCE=2 умышленно сбой на том, что он решает небезопасно.

некоторые дистрибутивы строят gcc с -D_FORTIFY_SOURCE=2 включен по умолчанию. Некоторые-нет. Это объясняет все различия между разными компиляторами. Вероятно, те, которые не аварийно завершают работу, будут, если вы создадите свой код с помощью -O3 -D_FORTIFY_SOURCE=2.

почему он терпит неудачу, только если оптимизация включена?

_FORTIFY_SOURCE требуется компиляция с оптимизацией (-O) чтобы отслеживать размеры объектов с помощью приведений / назначений указателей. См.слайды из этого разговора подробнее о _FORTIFY_SOURCE.

почему strcpy () терпит неудачу? Как это возможно?

вызовы gcc __memcpy_chk на strcpy только с -D_FORTIFY_SOURCE=2. передает 8 как размер целевого объекта, потому что он думает, что вы имеете в виду / что он может выяснить из исходного кода, который вы ему дали. То же самое для strncpy вызов __strncpy_chk.

__memcpy_chk прерывание по назначению. _FORTIFY_SOURCE может выходить за пределы вещей, которые UB в C и запрещать вещи, которые выглядят потенциально опасные. Это дает ему лицензию, чтобы решить, что ваш код небезопасен. (Как указывали другие, гибкий член массива как последний член вашей структуры и / или объединение с гибким членом массива-это то, как вы должны выразить то, что вы делаете в 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));
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

(от gcc7.2 -O3 -Wall в проводнике компилятора Godbolt).


почему memcpy() не зависимо от


зачем все усложнять? Overcomplexifying, как вы делаете, дает просто больше места для неопределено поведение, в этой части:

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

предупреждение: передача аргумента 1 'puts' из несовместимого указателя ty pe [- Wincompatible-типы указателей] puts (&p->c);

вы явно в конечном итоге в нераспределенной области памяти или где-то записывается, если Вам ПОВЕЗЕТ...

оптимизация или нет может изменить значения адресов, и он может работать (так как адреса совпадают), или нет. Ты просто не может делать то, что вы хотите сделать (в основном ложь компилятору)

Я:

  • выделите только то, что нужно для структуры, не учитывайте длину строки внутри, это бесполезно
  • не используйте gets как это небезопасно и отживающие
  • использовать strdup вместо подверженных ошибкам memcpy код, который вы используете так как ты управляешь струнами. strdup не забудем выделить нуль-Терминатор,и установим его в целевой для вас.
  • не забудьте освободить дублированную строку
  • читать предупреждения, put(&p->c) неопределено поведение

ваш указатель p->c является причиной аварии.
Сначала инициализируйте структуру с размером "unsigned long long" плюс размер "*p".
Второй инициализируйте указатель p - >c требуемым размером области. Сделать копию операции: strcpy (p->c, str);
Наконец, освободите сначала free (p - >c) и free(p).
Я думаю, дело было в этом.
[EDIT]
Я буду настаивать. Причина ошибки заключается в том, что его структура резервирует пространство только для указателя, но не выделяет указатель для данные, которые будут скопированы.
посмотреть

int main() 
{
    pack *p;
    char str[1024];
    gets(str);
    size_t len_struc = sizeof(*p) + sizeof(unsigned long long);
    p = malloc(len_struc);
    p->c = malloc(strlen(str));
    strcpy(p->c, str); // This do not crashes!
    puts(&p->c);
    free(p->c);
    free(p);
    return 0;
}

[EDIT2]
Это не традиционный способ хранения данных, но это работает:

    pack2 *p;
    char str[9] = "aaaaaaaa"; // Input
    size_t len = sizeof(pack) + (strlen(str) + 1);
    p = malloc(len);
    // Version 1: crash
    strcpy((char*)p + sizeof(pack), str);
    free(p);