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 код работает хорошо, независимо от уровня оптимизации.
Также я нашел некоторые другие черты кода:
Я пробовал все эти компиляторы, и они не имели значения:
- 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()
. Я не использую его в своем проекте, но только код примера.
Ответы
Ответ 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);