LINUX.ORG.RU

Почему компилятор разрешает такой код??

 


0

4
#include <iostream>

int main()
{
    int n = 5;
    int *a = new int [n];

    for (int i = 0; i < n; ++i)
        a[i] = i;

    delete[] a;

    for (int i = 0; i < n; ++i)
        std::cout << a[i] << std::endl;

    return 0;
}
$ g++ h2.cpp -o h2 -Wall -Wextra -pedantic
$ ./h2
1603630097
5
-751897228
153970139
4
$ cppcheck h2.cpp
Checking h2.cpp ...
$

И даже чекер не видит явной ошибки О_о

Потому что не используешь -fsanitize=address

=================================================================
==20941==ERROR: AddressSanitizer: heap-use-after-free on address 0x603000000010 at pc 0x55c53abe2338 bp 0x7ffd0b47ddb0 sp 0x7ffd0b47dda8
READ of size 4 at 0x603000000010 thread T0
    #0 0x55c53abe2337 in main (/tmp/a.out+0x1337)
    #1 0x7f911ab9bd09 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x23d09)
    #2 0x55c53abe2169 in _start (/tmp/a.out+0x1169)

0x603000000010 is located 0 bytes inside of 20-byte region [0x603000000010,0x603000000024)
freed by thread T0 here:
    #0 0x7f911afc5127 in operator delete[](void*) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:163
    #1 0x55c53abe22dc in main (/tmp/a.out+0x12dc)
    #2 0x7f911ab9bd09 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x23d09)

previously allocated by thread T0 here:
    #0 0x7f911afc47a7 in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:102
    #1 0x55c53abe2253 in main (/tmp/a.out+0x1253)
    #2 0x7f911ab9bd09 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x23d09)

SUMMARY: AddressSanitizer: heap-use-after-free (/tmp/a.out+0x1337) in main
Shadow bytes around the buggy address:
  0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c067fff8000: fa fa[fd]fd fd fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==20941==ABORTING
fluorite ★★★★★
()
Ответ на: комментарий от fluorite

Да, теперь компилируется, но падает при исполнении (странно, а почему бы не выкидывать ошибку уже при компиляции?)

Но вопрос в силе: почему по дефолту компилятор такое позволяет? В этом же нет никакого смысла.

alex1101
() автор топика
Ответ на: комментарий от alex1101

Потому что asan был написан Константином Серебряным только лет 10 назад, что по меркам плюсов - вчера.

https://www.youtube.com/watch?v=vKtNwALHb2k

fluorite ★★★★★
()
Последнее исправление: fluorite (всего исправлений: 1)

valgrind спасёт отца русской демократии.

На этапе компиляции в общем случае поймать такое невозможно, так как можно указатели «расщепить» или проманипулировать другим каким способом (за’xor’ить дважды etc).

bugfixer ★★★★★
()
Ответ на: комментарий от alex1101

Но вопрос в силе: почему по дефолту компилятор такое позволяет? В этом же нет никакого смысла.

Потому что случаи могут быть сложнее так что компилятор не сможет понять что произошёл use after free.

X512 ★★★★★
()

вообще-то компилятор локально (вне контекста) компилирует выражения, если такой контекст для компиляции не нужен.

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

и потом откуда видно, какова семантика delete[]?, он может быть переопределен и не делать вообще ничего.

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

alysnix ★★★
()
Ответ на: комментарий от alex1101

Это оно?

Да, классический use-after-free.

указатели «расщепить»

Что ты имеешь в виду?

Разделить на части. Например сохранить старшие 32 бита в одном месте, младшие в другом, в нужный момент собрать обратно etc. Отследить такое в общем случае невозможно.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Например сохранить старшие 32 бита в одном месте, младшие в другом, в нужный момент собрать обратно

Жесть, а зачем такое может понадобиться в принципе?)

alex1101
() автор топика
Ответ на: комментарий от X512

Зато в Расте утечки памяти считаются нормальным поведением.

Для их создания нужно явно написать mem::forget() или создать цикл из Arc/Rc. И, нет, они не считаются «нормальным поведением». Не всё что safe - нормально. Логические баги тоже могут быть без unsafe, но они - не нормальное поведение.

Так что не надо тут тень на плетень наводить.

red75prim ★★★
()
Последнее исправление: red75prim (всего исправлений: 1)
Ответ на: комментарий от red75prim

Для их создания нужно явно написать mem::forget()

Я не совсем понял зачем это нужно. Чтобы передать указать в программу на Си и потом оттуда вызвать free()? Или настоящая необратимая утечка памяти? Если второе – то это фундаментальный баг в дизайне.

или создать цикл из Arc/Rc

Что на практике запросто может произойти. Другие языки вроде Python используют трассирующий сборщик мусора в дополнение к счётчику ссылок чтобы избежать утечек.

X512 ★★★★★
()
Ответ на: комментарий от alex1101
  auto x = new T;

  if (some_func()) delete x;
  
  x._some_field = 0;

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

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

alysnix ★★★
()

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

Кстати хорошим тоном является обнулять указатели после delete, что бы сегфолт прилетал сразу.

AntonI ★★★★★
()
Ответ на: комментарий от AntonI

Кстати хорошим тоном является обнулять указатели после delete, что бы сегфолт прилетал сразу.

Только вот особо умный оптимизирующий компилятор может удалять эти обнуления так разименовывание нуля – UB.

X512 ★★★★★
()

И даже чекер не видит явной ошибки О_о

Visual Studio 2022 выдаёт предупреждение C6001: https://learn.microsoft.com/ru-ru/cpp/code-quality/c6001

https://imgur.com/a/raQEsoR

Можно выставить, чтобы предупреждения трактовались как ошибки, тогда не будет компилироваться…

fsb4000 ★★★★★
()
Последнее исправление: fsb4000 (всего исправлений: 3)

— Доктор, у меня болит, когда я делаю так.
— А вы так больше не делайте.

Да, С/С++ позволяют выстрелить себе в ногу. Факт, известный лет пятьдесят как. И?

alegz ★★★★
()

Кстати g++ тоже ругается. Ты просто неправильно компилируешь.

error: dereference of possibly-NULL 'a' [CWE-690] [-Werror=analyzer-possible-null-dereference]
    9 |         a[i] = i;
      |         ~~~~~^~~

https://gcc.godbolt.org/z/bbsxd8Wh4

Нужно так:

g++ h2.cpp -o h2 -Wall -Wextra -pedantic -fanalyzer -Werror

Ну и с санитайзерами как было сказано выше…

fsb4000 ★★★★★
()
Ответ на: комментарий от X512

Или настоящая необратимая утечка памяти? Если второе – то это фундаментальный баг в дизайне.

Это способ не вызывать деструктор «забытого» объекта. Иногда это может пригодиться (когда ресурсы освобождаются внешним кодом, например).

Что на практике запросто может произойти.

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

Ладно, закругляюсь, а то будут ругаться, что «лезут тут всякие со своим растом».

red75prim ★★★
()
Последнее исправление: red75prim (всего исправлений: 1)
Ответ на: комментарий от AntonI

Кстати хорошим тоном является обнулять указатели после delete, что бы сегфолт прилетал сразу.

Вообще говоря - не факт что в прод сборке возымеет хоть какой-то эффект. Например, я уверен, что с O3 (и даже O2) компилятор любые memwrites через «this» в деструкторе выкинет нафиг…

bugfixer ★★★★★
()
Ответ на: комментарий от AntonI

А если ты нулишь указатель не в объекте для которого деструктор а какой то ещё?

Мопед не мой:-)

Идея, на самом деле, очень здравая :-) Хотя и не бесплатная. Мысль которую я пытаюсь донести - далеко не всегда работает, закладываться на это не стОит.

bugfixer ★★★★★
()
Ответ на: комментарий от X512

Речь про потенциальное разыменовывание к указателя посте удаления и зануления.

О птичках. Тут намедни выяснилось что даже чтение (не разыменовывание!) указателя после free / delete - это UB. Я даже до commetee members дошёл чтобы докопаться до истины - таки да, это именно то что имелось в виду.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Мнэ…ну вот скажем я потом где то проверяю, если указатель нулевой- выделяю память или ещё что то осмысленное делаю.

Неужто зануление компилятор уберёт? По моим представлениям не должен, это же обычное присваивание…

AntonI ★★★★★
()
Ответ на: комментарий от alex1101

Один и тот же указатель можно сохранить в нескольких переменных (полях классов, глобальных и локальных переменных). Ещё это может происходить в разных функциях, по разным условиям, в циклах и т. д.

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

Затем для одной переменной делают delete и все остальные указатели становятся невалидны. Чтобы такое поймать нужно просчитывать все варианты исполнения целой программы, что в общем случае NP полная задача и для программы длинее hello world ты не дождёшься окончания компиляции.

Поэтому решили не делать обработку одного частного случая (где delete и использование сразу той же переменной в одной функции), потому что 99% ситуаций всё равно будет пропущено. Решили сконцентрироваться на оптимизациях и т. п.

Есть статические анализаторы (типа того же PVS Studio), которые ловят 100500 частных случаев и таким образом покрывают больше.

Есть языки со специально ограниченной семантикой (например, в том же Rust есть ограничения на inferior mutability), чтобы анализ работы с памятью перестал быть np полной задачей или хотя бы мог быть проведен в рамках всегда одной функции.

KivApple ★★★★★
()
Ответ на: комментарий от AntonI

Мнэ…ну вот скажем я потом где то проверяю, если указатель нулевой- выделяю память или ещё что то осмысленное делаю.

Так сработает, но если где-то забыли проверить указатель на ноль или забыли занулить указатель после удаления и перед проверкой, то компилятор может начать генерировать странный код.

X512 ★★★★★
()
Последнее исправление: X512 (всего исправлений: 1)
Ответ на: комментарий от KivApple

Чтобы такое поймать нужно просчитывать все варианты исполнения целой программы, что в общем случае NP полная задача

Не, это даже не NP полная задача, это - практически нерешаемая задача. Даже на компьютерах с ограниченной памятью программа может перебрать 2^(количество бит виртуальной памяти компьютера) перед тем как остановиться, и это не считая взаимодействия с внешним миром.

Даже простейшие машины Тьюринга демонстрируют сложное поведение. Например, существует программа для машины Тьюринга с 2-мя символами и 6-ю состояниями, которая останавливается только после 10↑↑15 шагов (это - 10 возвести в 10-ю степень 15 раз). См. busy beaver.

red75prim ★★★
()
Ответ на: комментарий от AntonI

Неужто зануление компилятор уберёт? По моим представлениям не должен, это же обычное присваивание…

В подавляющем большинстве случаев - не уберёт. И по факту присваивание (не важно - зануление или нет) - это единственная legit operation над указателем after free.

bugfixer ★★★★★
()
Ответ на: комментарий от AntonI

Это ещё цветочки, иногда приходится каждый бит экономить.

Кстати, если знать что указатель пришёл из malloc() - как мин 2 лишних бита у нас есть (он будет выровнен по 4 байтам, а то и больше). Более того - я реально видел код это использующий, причём довольно старенький (конец 80-х .. начало 90-х). Я к тому что идея ни в одном глазу не нова.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Это понятно что умеючи можно и хрен сломать, недоумение вызывает то что очевидно некорректная программа не просто компилируется, но даже предупреждения не выводится. Понятно что компилятор «по стандарту не обязан», но он много чего не обязан из того что на практике реализовано, например проверять форматные строки

cobold ★★★★★
()
Ответ на: комментарий от cobold

по-взрослому этим статические анализаторы занимаются.

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

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

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

alysnix ★★★
()
Ответ на: комментарий от Shushundr

Разве такая дополнительная типизация не потребует дополнительного синтаксиса?

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

alysnix ★★★
()
Ответ на: комментарий от alex1101

Можно пример?

Элементарно:

int * a = new int[n];
foo(a);
... a[i] ...

Тело foo находится в другой единице компиляции и компилятору недоступно. Что делает функция foo? Неизвестно. Она может освободить память, на которую указывает a, тогда a[i] делать нельзя, а может и не освобождает, тогда a[i] делать можно.

debugger ★★★★★
()