Иногда флеймы на профильных ресурсах оказываются очень полезны. Вот свежий пример с LOR-а: "Навеяно свежей дырой в Xorg".
В двух словах: после освобождения указателя (через free в C или delete в C++) с указателем ничего нельзя делать (кроме как присвоить ему новое валидное значение).
Т.е. не то, что разыменовывать нельзя (это-то как раз понятно), но и вообще ничего нельзя: ни распечатать, ни сравнить с чем-нибудь (пусть даже и с NULL/nullptr), ни преобразовать в uintptr_t. НИЧЕГО. Любые попытки сделать что-то подобное есть UB.
Говорят, что у этого есть даже какое-то логическое обоснование. Но мне осознать сие не получается :)
Вышесказанное означает, что если у вас написано что-то подобное:
void data_cleanup(data * ptr) { if(ptr != NULL) { ... // Какие-то действия по очистке. free(ptr); } else { ... // Какие-то другие действия. Допустим, просто // печать в лог о том, что data_cleanup был вызван. } ... // Еще какие-то действия, которые не зависят от // значения ptr. // А здесь нужно еще что-то сделать если ptr не был NULL. if(ptr != NULL) { // (1) ... } } |
то поздравляю, у вас в коде UB. И, судя по тому, как безжалостно компиляторостроители начинают UB эксплуатировать, рано или поздно случится какая-нибудь бяка.
Полагаю, что выйти из ситуации можно вот так:
void data_cleanup(data * ptr) { int not_null = ptr != NULL; if(not_null) { ... // Какие-то действия по очистке. free(ptr); } else { ... // Какие-то другие действия. Допустим, просто // печать в лог о том, что data_cleanup был вызван. } ... // Еще какие-то действия, которые не зависят от // значения ptr. // А здесь нужно еще что-то сделать если ptr не был NULL. if(not_null) { ... } } |
Но это же нужно быть очень внимательным, чтобы не наступать на подобные грабли.
К счастью, в C++ при использовании умных указателей вроде shared_ptr и unique_ptr все не так страшно. Но вот если нужно записать что-то на чистой ламповой Сишечке или на старых плюсах, в которых умных указателей нет... То грустно.
Данное обсуждение заставило вспомнить еще про одну засаду с голыми указателями. Еще, если кто-то не знал или забыл, в Си и C++ нельзя просто так сравнивать на больше/меньше указатели одного типа. Грубо говоря, если у нас есть указатели a и b одного типа, то выражение (a<b) будет определено, только если a и b указывают на элементы одного массива (либо на элемент за последним элементом этого массива).
Правда, в C++, насколько я понимаю, можно воспользоваться std::less или std::greater из стандартной библиотеки. Поскольку для подобных компараторов определены специализации для указателей. Например, по поводу std::less на cppreference сказано буквально следующее: "A specialization of std::less for any pointer type yields the implementation-defined strict total order, even if the built-in < operator does not."
Вот ведь, а я всю жизнь указатели одного типа сравнивал между собой и даже не задумывался. Подобное сравнение есть, например, в SObjectizer-е. Нужно будет в следующем релизе поправить.
3 комментария:
Записывать ноль в указатель сразу после деаллокации всегда была частью т.н. "лучших практик". (Особенно, когда не было умных указателей.) Но даже деструктор unique_ptr (по крайней мере, в реализации GCC) зануляет указатель на освобождённую память! (Хотя, казалось бы, зачем?)
поддержка экзотических архитектур кмк. ну вот я могу представить архитектуру проца, где есть регистры, а память, ну скажем, через SPI подключена. и каждый malloc создаёт внутренний канал к этой памяти, закодированный в указателе. при обращении (чтении указателя или тем более разыменовании) используется канал. и допустим поддерживаются perf-counters, т.е. каждое чтение/запись считается. free этот канал убивает и следующее обращение к указателю при попытке апдейта статистики - падает.
либо авторы стандарта пытались любой ценой запретить use-after-free. что бы даже в голову никому не могло придти хоть как-то указатель после free пользовать.
а срач, канеш, эпичный. да и кейс действительно интересный
@NickViz
> поддержка экзотических архитектур кмк.
Там упоминалась эта причина. И я ее понял следующим образом: были архитектуры, на которых вообще все операции с указателями (адресами) должны были выполняться через специальные регистры процессора. Т.е. даже чтобы сравнить два указателя их нужно было загрузить в эти регистры, а потом уже сравнить значения в двух специальных регистрах.
При этом корректность значения указателя проверялась аппаратно при выполнении операции загрузки значения в регистр. Так что если память была освобождена, то указатель переставал быть валидным и его нельзя было даже загрузить в специальный регистр.
Там же говорилось, что таких архитектур уже нет. Но стандарт оставляет возможность для адаптации языка если таковые архитектуры появятся в будущем.
Отправить комментарий