четверг, 16 февраля 2023 г.

[prog.wow] Век живи, век учись: с указателем после освобождения ничего нельзя делать

Иногда флеймы на профильных ресурсах оказываются очень полезны. Вот свежий пример с 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 комментария:

Dmitry Igrishin комментирует...

Записывать ноль в указатель сразу после деаллокации всегда была частью т.н. "лучших практик". (Особенно, когда не было умных указателей.) Но даже деструктор unique_ptr (по крайней мере, в реализации GCC) зануляет указатель на освобождённую память! (Хотя, казалось бы, зачем?)

NickViz комментирует...

поддержка экзотических архитектур кмк. ну вот я могу представить архитектуру проца, где есть регистры, а память, ну скажем, через SPI подключена. и каждый malloc создаёт внутренний канал к этой памяти, закодированный в указателе. при обращении (чтении указателя или тем более разыменовании) используется канал. и допустим поддерживаются perf-counters, т.е. каждое чтение/запись считается. free этот канал убивает и следующее обращение к указателю при попытке апдейта статистики - падает.

либо авторы стандарта пытались любой ценой запретить use-after-free. что бы даже в голову никому не могло придти хоть как-то указатель после free пользовать.

а срач, канеш, эпичный. да и кейс действительно интересный

eao197 комментирует...

@NickViz

> поддержка экзотических архитектур кмк.

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

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

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