Подтвердить что ты не робот

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 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 код вызывает (!!) в strcpy(), а memcpy() работает хорошо.
  • С gcc.exe в Windows код работает хорошо, независимо от уровня оптимизации.

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

  • (Похоже) минимальный вход для воспроизведения аварии - 9 байт (включая нулевой терминатор) или 1+sizeof(p->c). С этой длиной (или дольше) гарантируется сбой (Дорогой я...).
  • Даже если я выделил дополнительное пространство (до 1 МБ) в malloc(), это не поможет. Вышеуказанные действия не меняются вообще.
  • strncpy() ведет себя точно так же, даже с правильной длиной, переданной его третьему аргументу.
  • Указатель не имеет значения. Если член структуры 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 = '\0';
    return r;
}

Вопросы:

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

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


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

objdump


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

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

4b9b3361

Ответ 1

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

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

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

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

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

    Внимание. Это полностью отключит проверку переполнения буфера во всей программе.

Ответ 2

Что вы делаете, это поведение undefined.

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

Точка, даже если вы выделяете правильный размер памяти с помощью malloc(), компилятору разрешено предполагать, что вы никогда не будете использовать больше sizeof int64_t в своем вызове strcpy() или memcpy(). Потому что вы отправляете адрес c (aka 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);
}

Примечание. Для стандартной цитаты см. этот ответ.

Ответ 3

Ответ пока не обсуждался подробно о том, почему этот код может быть или не быть undefined.

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

Но обратите внимание, что, основываясь на обсуждении -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 не исследовал этот случай).

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


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

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

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

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

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


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

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

Еще одним осложняющим фактором является то, что стандарт не предоставляет определение для "массива" , ни для "объекта массива". Но, чтобы сократить длинную историю, объект, выделенный 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-байтовому распределению. ИМХО простота этого привлекательна, по сравнению с тем, что обсуждалось в моем предыдущем разделе.

Ответ 4

Это все из-за -D_FORTIFY_SOURCE=2 намеренного сбоя в том, что он решает, небезопасно.

Некоторые дистрибутивы build 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() независимо от уровня -O?

ИДК.

gcc полностью встраивает его в 8B load/store + 1B load/store. (Похоже на пропущенную оптимизацию, он должен знать, что malloc не изменял его в стеке, поэтому он мог просто сохранить его от непосредственных снова вместо перезагрузки. (Или лучше сохранить значение 8B в регистре.)

Ответ 5

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

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

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

вы явно попадаете в нераспределенную область памяти или где-то записываемую, если вам повезет...

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

Я бы:

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

test.c: 19: 10: warning: передающий аргумент 1 из 'puts' из несовместимого указателя ty pe [-Wincompatible-pointer-types]      ставит (& p > с);

Мое предложение

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

Ответ 6

Ваш указатель p- > c является причиной сбоя.
Сначала инициализируйте структуру с размером "unsigned long long" плюс размер "* p".
Второй указатель инициализации p- > c с требуемым размером области. Сделать копию операции: strcpy (p- > c, str);
Наконец, свободные сначала свободные (p- > c) и свободные (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);