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 – это язык культа страданий во имя страданий.

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

TCC не реализует современные стандарты C. Про него можно вообще забыть в данном контексте. Но даже и в нём вряд ли зануление указателя даст какой-либо ущерб к производительности.

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

Например, если бы

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

Причем тут mmap,

При том, что до mmap было совершенно безболезненно юзать память после free, об это так в книжках и писали — плохо, но можно.

goto на free это типичный паттерн

Это пока free один, но их бывает много, часть уже вызвана, часть ещё вызывать рано, так как свалилось в ошибку перед malloc.

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

А что ещё с таким указателем можно делать?

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

vodz ★★★★★
()
Ответ на: комментарий от hateyoufeel
#include <stdio.h>
#include <stdlib.h>

int main(void){

    int *j = malloc(1024);
    printf("%p\n", j);
    free(j);
    printf("%p\n", j);
    
    j = (int) 0x00000000;
    printf("%p\n", j);
    j = (int*) 0xFFFFFFFF;
    printf("%p\n", j);
    j = (int*) 0xDEADBEEF;
    printf("%p\n", j);

    return 0;
}
0x1f842a0
0x1f842a0
(nil)
0xffffffff
0xdeadbeef

Это может поменяться в следующей же версии компилятора.

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

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

успехов в изучении трудных наук )

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

При том, что до mmap было совершенно безболезненно юзать память после free, об это так в книжках и писали — плохо, но можно.

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

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

С такой «логикой» можно int-ы передавать для %f (или double в %d), ведь они же могут «автоматически» кастоваться

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

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

Я надеюсь, ты на C профессионально не программируешь? Потому что тебя стоит уволить в этом случае.

Компилятор без проблем может выкинуть этот printf(), например. Или вообще всю программу. И это не будет багом.

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

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

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

6.3.2.3 (1)

A pointer to void may be converted to or from a pointer to any incomplete or object type. A pointer to any incomplete or object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.

Это покрывает в том числе неявные касты.

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

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

Нет там никакого мусора, логика работы malloc в книжках описывалась скрупулёзно, если её учитывать, что free не портит память вообще, ибо работает с памятью на указатель на один ранее выдавамой при возврате malloc, а портит только последующие malloc и то с оговорками что во что вложенно (но это совсем уже высший пилотаж и мало где это писалось), то и работало всё. То что память портится — это как раз с mmap-а всё и пошло и к сожалению только всё перешло в разряд магии и страшилок. Отсюда и всякие магические представления о порче самого значения указателя. Ведь это легко сделать как раз при взятии памяти под него другим ранее вызванным malloc.

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

Я всё ещё не понимаю, в чём именно проблема. Printf() внутри берёт значение из va_arg и кастует его к void*, а потом к intptr_t (скорее всего). Так что UB тут точно нету.

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

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

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

такой sanitizer не будет нарушать стандарт. Есть ли в этом смысл? сомневаюсь

В смысле, сомневаешься? Use after free ловить только так и надо. У чувака передача указателя в левую функцию. Если это не printf, а какая-нибудь шняга из сторонней библиотеки, которую компилятор не видит? То-то же!

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

Я всё ещё не понимаю, в чём именно проблема.

8 The conversion specifiers and their meanings are:
p The argument shall be a pointer to void.

9 If any argument is not the correct type for the corresponding conversion specification, the behavior is undefined.

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

А можешь показать на примерах, где в старых поделках так делали?

Потому что это дичь какая-то. И причем здесь вообще ммап?

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

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

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

А теперь представь кучу таких ситуаций в 1 секунду. Ну и как? Безопасно пользоваться освободившимся куском?

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

use after free ловят обычно в аллокаторе. Но если там что-то более-серьёзное типа msan, оно должно отслеживать хозяина памяти, а не code flow вокруг значения указателя. Хотя можно было бы наверно добавить и к значению тег, тогда смысл будет

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

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

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

Мне лень, но вообще надо смотреть в раздел стандарта про variadic функции. Там должны быть правила приведения типов при передаче в такие функции. Например, точно знаю, что все целочисленные типы меньше int автоматически приводятся к int. Вполне возможно там про указатели тоже есть что-то. То есть гарантирую даёт не стандарт printf, а более общий стандарт variadic функций частным случаем которых является printf.

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

А можешь показать на примерах, где в старых поделках так делали?

Я не волшебник и по желанию какого-то там чела из Инета могу ненапрягаясь тот час вытащить из колпака любой пример на то, что когда-то где-то читал и попадалось. Не хотите верить — да на здоровье. На предмет, как и почему так работает malloc всегда и в том числе и сейчас пишут много и почему-то все считают, что другие ничего не понимают: «Ты не догоняешь как работает маллок и фри.» - ну да, ну да... Какие листики, какие клеточки? Я сам писал кучу реализаций libmalloc, как на sbrk, так и потом на mmap, как и вместе кстати.

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

Детский сад. Есть понятие фрагментации, есть дефграгментация (угадайте с трёх раз, где она проводится, в free или в malloc), есть возврат системе памяти при освободившейся памяти величиной запрошенного блока, есть флаги mallopt(), меняющие принцип аллокации...

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

И тем не менее, здесь ни слова о том, что

значение указателя <…> является неопределённым

Значение отлично определено. Не определено лишь поведение, возникающее при разыменовании этого значения.

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

У «не определено» может быть 2 смысла:

  1. Стандарт ничего не говорит про значение
  2. Он называет его indeterminate

Значение отлично определено.

В том смысле, что явно сказано какое — да. Только indeterminate.

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

Содержимое освобождаемой памяти легко может изменить сам free банально храня в ней элементы связанного списка

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

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

Компилятор без проблем может выкинуть этот printf(), например. Или вообще всю программу. И это не будет багом.

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

#include <stdio.h>
#include <stdlib.h>

int main(void){

    int *j = malloc(sizeof(int));
    printf("%p %d\n", j, j[0]); // ok
    free(j);
    printf("%p\n", j); // ok

    printf("%p %d\n", j, j[0]); // UB

    return 0;
}

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

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

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

Перечитай цитату из стандарта ещё раз.

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

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

И ты тоже перечитай цитату из стандарта ещё раз. Использование самого значения является UB.

UB возникает тогда, когда мы действуем, а не тогда, когда храним.

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

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

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

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

Это зависит от имплементации менеджера памяти. Фактически же перед куском занятой памяти минимально необходимо хранить лишь его длину 1x size_t, иметь связанный список занятых блоков памяти для базовой реализации malloc/free нет потребности. А вот свободные блоки памяти уже нужно выстраивать в список, чтобы потом malloc в нём мог искать откуда выделить память. Таким образом в свободной памяти лежит минимум 1x void* + 1x size_t (связанный список), а возможно даже 2x void* и 1x size_t (двунаправленный связанный список удобнее для удаления-вставки в произвольную позицию).

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

Я это пишу по опыту написания игрушечного менеджера памяти для игрушечной ОС много лет назад - мой free разрушал данные. А так как это не противоречит стандарту, ничто не мешает это делать и не игрушечным libc. Тем более что 30 лет назад об эффективности использования памяти беспокоились гораздо больше, а описанный подход уменьшает оверхед аллокаций (а ещё 30 лет назад никому не нужно было выравнивание malloc больше чем на sizeof size_t, потому что не было всяких SSE).

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

Компилятор выдаст тебе говно вместо работающей программы. Что непонятного-то?

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

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

а присвоить указателю произвольное значение мы можем?

Сегодня у нас, видимо, публичные чтения стандарта:

An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.

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

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

Удивительно, что вы писали менеджер памяти, скорее всего до mmap и не понимаете, что список не может расширяться после free, ибо malloc не отдает разные куски по определению, потому связанный список может только сократиться. И причём тут SSE? Я же с первого раза сказал, что теперь юзать память после free нельзя в принципе, так как и память может быть munmap-нута и даже всякие __attribute__(malloc/free) теперь могут после оптимизатора дествительно сделать с этой памятью что угодно.

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

вопрос вот в чем

#include <stdio.h>
#include <stdlib.h>

int main(void){

    int *ptr = (int*) 0xDEADBEEF;

    printf("%p\n", 0xDEADBEEF); // так можно
    printf("%p\n", ptr); // а так уже нельзя?

    ptr = malloc(sizeof(int));
    free(ptr);
    printf("%p\n", ptr); // а так вапще низя никада!

    return 0;
}

да?)

olelookoe ★★★
()
Ответ на: комментарий от olelookoe
printf("%p\n", 0xDEADBEEF); // так можно

нет, нельзя :D

fail.c: In function ‘main’:
fail.c:8:14: warning: format ‘%p’ expects argument of type ‘void *’, but argument 2 has type ‘unsigned int’ [-Wformat=]
    8 |     printf("%p\n", 0xDEADBEEF); // так можно
      |             ~^     ~~~~~~~~~~
      |              |     |
      |              |     unsigned int
      |              void *
      |             %d
cumvillain
()
Ответ на: комментарий от vodz

С опциями нам с тобой еще рано разбираться. Ты основы не понимаешь. Нахватался умных слов, а сути не видишь.

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

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

hibou ★★★★★
()
Ответ на: комментарий от cumvillain
#include <stdio.h>
#include <stdlib.h>

int main(void){

    int *ptr = (int*) 0xDEADBEEF;

    printf("%p\n", (void*) 0xDEADBEEF); // так можно
    printf("%p\n", ptr); // а так уже нельзя?

    ptr = malloc(sizeof(int));
    free(ptr);
    printf("%p\n", ptr); // а так вапще низя никада!

    return 0;
}

пойдет? )

olelookoe ★★★
()
Ответ на: комментарий от olelookoe
int *ptr = (int*) 0xDEADBEEF;

Согласно стандарту, тут implementation defined.

(void*) 0xDEADBEEF

Как и тут.

printf("%p\n", ptr); // а так уже нельзя?

Можно, если ptr, который ты получил кастом, валидный.

printf("%p\n", ptr); // а так вапще низя никада!

Согласно стандарту, нельзя.

cumvillain
()