LINUX.ORG.RU

Навеяно свежей дырой в Xorg

 , ,


9

7

Привет, ЛОР!

Ты, наверное, уже видел свежую дыру в Xorg, патч для которой выглядит буквально вот так:

-        else
+        else {
             free(to->button->xkb_acts);
+            to->button->xkb_acts = NULL;
+        }

В связи с этим у меня возник вопрос: а почему в стандартной библиотеке C нет макроса SAFE_FREE()?

#define SAFE_FREE(ptr) do{free(ptr);(ptr)=NULL;}while(0)

Напомню, что значение указателя после вызова free() является неопределённым согласно стандарту. Не только значение памяти, на которое он указывает, но и значение самого указателя, и работа с ним представляет собой жуткое undefined behaviour, а значит единственное что можно сделать – занулить его.

Так вот, почему даже таких банальных вещей нет? Я уже не говорю про строковый тип, а то даже Эдичка тут строки не осилил.

Моя гипотеза тут: C – это язык культа страданий во имя страданий.

Ответ на: комментарий от unsigned

Насчёт реже - сомневаюсь, но то, что легче найти, бесспорно. Надо бы в си++ сделать аналог (safe и внутри него unsafe, так как должна быть совместимость).

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

А как его сломать? У меня clang выкидывает аллокацию и напрямую суёт 666 в printf.

А никак. Работает на 99,999999999% архитектур и компиляторах, однако не отменяет того факта, что это UB.

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

Работает на 99,999999999% архитектур и компиляторах, однако не отменяет того факта, что это UB.

А.. ну так-то да. Я думал, какой-нибудь компилятор этот код ломает напрочь.

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

Если в программе есть UB, значит твоя программа некорректна

Так-то оно так, но в этом конкретном случае я не до конца понимаю какую практическую пользу из конкретно этого UB можно извлечь в принципе.

и все предположения компилятора идут лесом.

Как компилятор вообще хоть что-то может предполагать о free(), учитывая что я могу подсунуть свою в runtime?

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

Разработчики компилятора в определённом смысле заложники разработчиков апликух, и без оных не стоят вообще ничего. Компилятор который генерит код взрывающийся на ровном месте будет мгновенно выкинут на помойку. Так же как и (например) машина с ресурсом педали тормоза в 10 нажатий, и никакие отмазки вида «ну у нас же там в инструкции мелким шрифтом написано» не помогут.

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

Так-то оно так, но в этом конкретном случае я не до конца понимаю какую практическую пользу из конкретно этого UB можно извлечь в принципе.

Как и от большей части UB, практической пользы абсолютный ноль. Это C, его разумом не понять. Он нужен в основном во имя страданий.

Как компилятор вообще хоть что-то может предполагать о free(), учитывая что я могу подсунуть свою в runtime?

Легко. В стандарте языка написано, что он может, значит он может.

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

Это C, его разумом не понять. Он нужен в основном во имя страданий.

Вы не котиков не любите, Вы их готовить не умеете.

Легко. В стандарте языка написано, что он может, значит он может.

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

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

Пример из жизни: QObject::connect. Принимает сырые указатели, сохраняет. Достаточно крови попортило.

Гм, не очень представляю, как образом это может попортить кровь, он же отлеживает время жизни объектов? Или речь о вариантах с лямбдами?

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

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

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

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

он же отлеживает время жизни объектов?

В целом да, но есть нюансы. Мы ловили тонкие рейсы не раз. Емнип, обработчик вызывался в процессе разрушения объекта-ресивера, но до деструктора родительского QObject, так что Qt еще не считал его мертвым.

Или речь о вариантах с лямбдами?

На это тоже напарывались, но быстро научились указывать контекст.

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

Так-то оно так, но в этом конкретном случае я не до конца понимаю какую практическую пользу из конкретно этого UB можно извлечь в принципе.

Например, код

p = malloc(...):
...
free(p);
q = malloc(...);
if(p == q) {
  magick_run();
}

Компилятор имеет право убрать проверку на p == q, а не выполнять magick_run в зависимости от того, попал ли новый объект на место старого.

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

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

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

Компилятор имеет право убрать проверку на p == q, а не выполнять magick_run в зависимости от того, попал ли новый объект на место старого.

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

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

Ага, но в этом пункте нет про UB. А в предыдущем есть. И про equal там тоже есть.

Это, на самом деле, отдельная проблема, что стандарт написан членом.

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

Ага, но в этом пункте нет про UB.

Вот и отлично!

А в предыдущем есть.

Так он не релевантен.

Это, на самом деле, отдельная проблема, что стандарт написан членом.

Какая радость для тех, кто читает жёппой))0)0

Но вообще тут всё ок с написанием. Вот почему некоторые, когда хотят узнать про поведение выражения p == q, лезут в раздел про операторы </<=/>/>= — это вопрос.

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

https://eel.is/c++draft/expr.eq#3.1

(3.1)

If one pointer represents the address of a complete object, and another pointer represents the address one past the last element of a different complete object, the result of the comparison is unspecified.

(3.2)

Otherwise, if the pointers are both null, both point to the same function, or both represent the same address, they compare equal.

(3.3)

Otherwise, the pointers compare unequal.

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

а каков смысл этого?

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

Например

if(f(x)) { do_p(p); free(p); }

do_something_else();

if(some_else(y)) { g(p); }

Так как компилятор видит, что g(p) не может быть вычислено после free(p), то если some_else не имеет побочных эффектов, то компилятор может запомнить значение f(x) и если оно истинно, то не вычислять some_else.

Или может сразу после free(p) использовать регистр, в котором хранилось значение p, для каких-то других целей (тогда если программист допустил ошибку и f(x) и some_else(y) одновременно истинны, то в g будет передано некой произвольное значение).

и написал такой вот проверочный код

Этот код некорректен. Можешь перед освобождением преобразовать в число или строку и сравнивать эти числа или строки.

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

Так как компилятор видит, что g(p) не может быть вычислено после free(p), то если some_else не имеет побочных эффектов, то компилятор может запомнить значение f(x) и если оно истинно, то не вычислять some_else.

откуда у «компилятора» такие мыслищи???

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

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

if(f(x)) { do_p(p); free(p); }
do_something_else();
if(some_else(y)) { g(p); }
alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от monk

Компилятор имеет право убрать проверку на p == q, а не выполнять magick_run в зависимости от того, попал ли новый объект на место старого.

a malloc может вернуть еще и NULL. причем оба раза. и получается уже не сравнение двух адресов, а NULL==NULL что есть тождественное равенство, и надо ветку выполнять.

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

Не. В стандарте под сравнением с NULL имеется ввиду сравнение именно с константой NULL.

не об это речь. а речь о том, что

auto p = malloc(100);
auto pp = malloc(100);
if (p == pp) do_something();
else do_else();

тут нельзя полагать что p и pp разные. они могут быть и одинаковыми и равными null. то есть нельзя выкидывать do_something и оставлять только do_else при оптимизации

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

тут нельзя полагать что p и pp разные. они могут быть и одинаковыми и равными null. то есть нельзя выкидывать do_something и оставлять только do_else при оптимизации

Почему?

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

что почему? если хипменеджер нормально алокировал оба куска - то адреса разные, если не может аллокировать - там два нула. то есть при оптимизации нельзя считать что равенство p==pp никогда не выполняется и выбрасывать true ветку

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

Хотя ты прав, да. Я из-за этого треда уже столько в стандарт втыкал, у меня в голове всё спуталось.

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

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

если вставить вот это в compiler explorer

extern void func1( void* fp); ///типа внешняя функция, чтобы не заоптимизировалось в нуль.

void test__test() {
   auto lp = malloc(1000);
   auto lpp = malloc(1000);
   func1(lp);
   if (lp == lpp) func1(lpp);
}

то clang при -02 выкидывает второй вызов func1, считая что указатели не могут быть одинаковыми. но поскольку по стандарту malloc может вернуть и NULL, то такое утверждение слишком сильно.

test__test():                        # @test__test()
        push    rax
        mov     edi, 1000
        call    malloc@PLT
        mov     rdi, rax
        pop     rax
        jmp     func1(void*)@PLT                  
alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от alysnix

но поскольку по стандарту malloc может вернуть и NULL, то такое утверждение слишком сильно.

https://discourse.llvm.org/t/why-we-assume-malloc-always-returns-a-non-null-pointer-in-instruction-combing/36076/10

В общем, если нет разыменования, то компилятор трактует вызов malloc как вызов такой версии malloc, которая никогда не возвращает NULL (допустимо по стандарту).

Вплоть до того, что

int main() {
   int* lpp = (int *)malloc(-1);
   if (lpp == NULL) printf("nil\n");
}

никогда не вызывает printf.

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

Значение указателя не может быть использовано. Передача в функцию является использованием.

Мне трудно описАть насколько это для меня бессмысленно. При первой возможности пообщаюсь со «власть имеющими» - бред же конкретный… Хочется верить что имеем дело с неправильной интерпретацией стандарта. Или наличием адекватных людей среди разработкиков компиляторов «кладущих» на инструкции «сверху».

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

Это после free тоже запрещено.

Hold on. Кто-то ссылался на 7.20.3(.2). И где здесь что-то написано про «низзя»? Я на неправильную pdf’ку смотрю?

The free function causes the space pointed to by ptr to be deallocated, that is, made available for further allocation. If ptr is a null pointer, no action occurs. Otherwise, if the argument does not match a pointer earlier returned by the calloc, malloc, or realloc function, or if the space has been deallocated by a call to free or realloc, the behavior is undefined.
bugfixer ★★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от hateyoufeel

Всё что написано выше (а) правда (б) имеет смысл и (в) из этого ну никак не следует тезис об

По сути же получается, что практически любое сравнение указателей (или даже печать значения указателя) – это потенциальный UB

Не по сути, а по стандарту лул :DDDD

ПыСы. Это я пытался в треде ссылку на конкретный параграф найти где говорится что-то большее про использование указателя чем разыменование after free() (дабы нашим «знатокам» предьявить)…

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

На правильную. В ней на странице 502:

J.2 Undefined behavior
...
- The value of a pointer that refers to space deallocated by a call to the free or realloc function is used (7.20.3).

То есть нельзя использовать значение указателя, ссылающегося на место, попавшее в free или realloc.

monk ★★★★★
()