Pull to refresh

Comments 41

Есть ещё #pragma optimize в разных компиляторах, правда это не портабельно.
Так ведь это из «CERT C Coding Standard». Так что OK.
> Из нашего маленького исследования вытекает, что для очистки приватных данных нельзя полагаться на функцию memset().

Из исследования вытекает, что на неё нельзя полагаться, потому что компиляторы выкидывают её, если память больше не используются. Вывод — достаточно просто прочитать память после вывода memset(), и можно полагаться дальше. :) Или нет?
Конечно, если далее память используется, то компилятор не уберет вызов memset.
>«достаточно просто прочитать память после вывода memset»

А потом кто-то другой, работая над этим же местом, увидит «бессмысленный» код и удалит его.
Боюсь, если у участников проекта есть традиция молча тереть код, который они не понимают, конкретно этот случай может быть одним из самым безобидных примеров того, что может произойти в такой команде.
ну почему же молча, это спокойно пройдет код ревью, если про сию оптимизацию знал лишь автор кода. с комментарием типа «removed useless code»
Иногда проекты размазаны во времени и кто его знает, кто будет править этот код через 10 лет.
Смотря, что значит «прочитать». Если прочитать и как-то использовать, это одно. А если положить значение в локальную переменную и затем эту переменную не использовать — это другое. Высока вероятность, что компилятор выбросит эту локальную переменную А потом и memset().
Не стоит делать подобные «хакерские решения». Нужно вызывать правильные функции. :)
Если специализированная функция есть в стандарте языка, нет вопросов. А если стандарта нет, то я не улавливаю тонкую грань между «хакерским решением» и «правильной функцией». :)
Прочитайте статью до конца и вопросов у вас не будет.
Делать на пустом месте предположения о том, что я прочёл, а что — нет, не слишком вежливо с вашей стороны. :) Не могли бы вы пояснить, что вы имеете в виду?
Статья:
Начиная с C11 существует функция memset_s.
Вы пишете:
Если специализированная функция есть в стандарте языка, нет вопросов.
Следовательно вывод такой: если бы вы дочитали статью до конца, вопросов бы не было.
Поразительно. Я пишу «в таком-то упомянутом в статье случае нет вопросов». Вы из этого делаете вывод, что я это не прочитал, и у меня они есть. «Следовательно»? Как? :D

Раз вы прочитали статью до конца, то должны были заметить — после процитированного вами куска автор даёт две кастомные реализации. И очевидно, что мой комментарий о разнице «хакерских» и «правильных» методов относился именно к ситуации, когда стандартную функцию по какой-то причине использовать нельзя.
Если я не ошибаюсь, то memset_s и других «безопасные» функции придумала Микрософт, и пыталась протолкнуть в стандарт.
Они были добавлены, но как не обязательные. И скорее всего ни один компилятор кроме Visual Studio их не поддерживает.
условно, компилятор может выкинуть memset и сразу записать в читаемую переменную 0
Можно как вариант сделать размещающее выделение на «старом месте»?
А зачем? Наверняка есть масса хитрых способов заставить компилятор не оптимизировать вызов memset(). Но в чем смысл экзотических подходов?
Например, если сервер обслуживает несколько клиентов.
Тогда тем более надо взять и затереть наиболее надёжным способом. А не гадать, посчитает компилятор placement new использованием памяти или нет. Как по мне, вполне имеет право оптимизировать. Но я даже и не подумаю пытаться это выяснить и разобраться. Не понимаю я, откуда это желание перебирать движок через выхлопную трубу. :)
И с какой-то вероятностью (ошибка, неучтённая оптимизация) использовать данные старого клиента для нового? Ок.
Если писать на С++, то можно оставить публичным только конструктор, принимающий значения для инициализации полей структуры. Тогда выделение с размещением должно быть безопасным. Повторюсь — мне просто интересны возможные варианты, речь не идёт о том, как следует писать на работе.
inline void SecureWipeBuffer(char* buf, size_t n){ — linux
volatile char* p = buf;
asm volatile(«rep stosb»: "+c"(n), "+D"(p): «a»(0): «memory»);
}

// windows
PVOID SecureZeroMemory(
_In_ PVOID ptr,
_In_ SIZE_T cnt
);
Тогда уж так:
asm volatime(«mfence»:::«memory»)
чтобы компилятор не перемещал операции работы с памятью
А, невнимательно прочитал. Возражения снимаются.
Не хватает объяснения — почему компилятор не имеет права выбросить код предложенной memset_s?
Обратимся к первоисточнику с примером кода:

This compliant solution uses the volatile type qualifier to inform the compiler that the memory should be overwritten and that the call to the memset_s() function should not be optimized out. Unfortunately, this compliant solution may not be as efficient as possible because of the nature of the volatile type qualifier preventing the compiler from optimizing the code at all. Typically, some compilers are smart enough to replace calls to memset() with equivalent assembly instructions that are much more efficient than the memset() implementation. Implementing a memset_s() function as shown in the example may prevent the compiler from using the optimal assembly instructions and can result in less efficient code. Check compiler documentation and the assembly output from the compiler.

However, note that both calling functions and accessing volatile-qualified objects can still be optimized out (while maintaining strict conformance to the standard), so this compliant solution still might not work in some cases. The memset_s() function introduced in C11 is the preferred solution (see the following solution for more information). If memset_s() function is not yet available on your implementation, this compliant solution is the best alternative, and can be discarded once supported by your implementation.
Т.е. да, вариант не идеален, но если нет настоящей memset_s(), то хоть так… :)
Забавный, кстати, вы привели комментарий к memset_s. А вообще, и это уже не очень относится к теме, если задуматься, то вроде бы оптимальней заполнять память единицами, а не нулями. Ведь логический «ноль» — это эффект заряженного «конденсатора». В кавычках потому, что никакого конденсатора нет.
Здесь взаимоисключающие параграфы — в первом сказано (со ссылкой на Стандарт), что доступ к переменным, объявленным с квалификатором volatile, нельзя оптимизировать, а во втором — что он все равно может быть оптимизирован. Причина — в неправильном понимании требований Стандарта. Если переменная сама объявлена с квалификатором volatile, то доступ к ней оптимизировать запрещено, но если сама переменная объявлена без квалификатора volatile и доступ осуществляется через «указатель на volatile», запрета на оптимизацию нет. Последнее утверждение часто вызывает споры (пример), но тем не менее подкрепить возражения требованиями Стандарта никому не удается, а чего нет в Стандарте — не требование, а только точка зрения.

Поэтому все реализации якобы гарантированной перезаписи памяти через «указатели на volatile» полагаются на поддержку компилятора — если разработчики компилятора готовы сделать немного больше, чем требует Стандарт, то фокус удастся. Главный плюс использования указателей на volatile — они легко опознаваемы как разработчиками, так и компилятором, хорошо передают намерение авторов кода, но тем не менее такое решение не является абсолютно переносимым.
Очистка памяти выглядит довольно наивной техникой защиты приватных данных, т.к. виртуализация памяти и файлы подкачки не дают возможности полностью управлять физическим состоянием секрета во всей области памяти системы (в свопе вообще можно найти много интересного). Для размещения секретов нужно использовать правильные инструменты (например под .net — SecureString).
GCC 10.5 Release notes:

* -O42 (can't be disabled anymore) — removes all human-writen code, and replaces it with proper implementation googled using original symbol names

Только мне одному кажется что разработчики компилятора берут на себя слишком много не компилируя прямой _вызов_функции_?
Стандарт C++, тем не менее, позволяет проводить любые преобразования кода при условии, что сохраняется «наблюдаемое поведение» — последовательность чтений-записей в переменные, объявленные с квалификатором volatile, и вызовов функций ввода-вывода.
(прошу прощения за офтопик)

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

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

Подобные изменения, кроме всего, ломают совместимось — де-факто изменяют поведение стандартного API, потенциально делая уязвимыми массу старого ПО. На фоне сохранения в стандарте всяких strncpy() и strncat(), int-ов вместо size_t и off_t, char* вместо void* это выглядит, по меньшей мере, непоследовательно.
Кто будет решать, что «здраво», а что — нет? Стандарт на то и стандарт, что является описанием «как правильно», все доводы, не подкрепленные требованиями стандарта, являются только мнениями, какими бы «здравыми» они ни были.
Критерии здравости, каждый должен иметь свои, это залог того что все не рухнет в тар-тарары из-за какой-нибудь опечатки.
Мне тоже не особо нравится такое поведение, но оно скорее правильное. Единственное, о чём жалею, это что нельзя получить отчёт или набор варнингов типа «тупиковый граф выполнения» и «строки NN не были учтены при генерации кода. Чтобы изменить это поведение используйте #pragma ...».

Исторические так сложилось, что разрабатывали компиляторы под процессоры, а не наоборот (эх, где ты, SPARC...), а логика работы процессора определяется staсkholder'ами — производителями. Поэтому на наш родной реликт — С (и его внучков, унаследовавших многие черты) — приходится громоздить прагмы типа likely, чтобы приспособить дедушку С, родившемуся на одноядерных процессорах, дешёвом обращении к оперативка и килобайтах оперативки, к многоуровневым кешам, конвеерам и out-of-order выполнению
компилятор может работать как ему вздумается в рамках стандарта, в т.ч. и исходя из предположения о недостаточной компетентности программиста. Стандарт позволяет оптимизации, требуя лишь сохранение наблюдаемого поведения. Выделения/освобождения памяти по стандарту не являются наблюдаемым поведением.
Ну, про компетентность программиста, положим, вы придумали сами. Во всяком случае я надеюсь, что стандарт до такой ереси не опускается.

Вы правы в том, что, на самом деле, проблема в стандартах — именно они разрабатывают или допускают поведение, в отношении когорого я и высказал свое мнение.
касательно «недостаточной компетентности» — я это не придумал, просто немного другое имел в виду. Далеко не всякий программист может обеспечить оптимальность каждого из написанных им участков кода. Просто потому, что это противоречит требованиям к скорости разработки и читаемости. Так почему бы компилятору не взять на себя самую нудную, монотонную и сложную часть работы?
Sign up to leave a comment.