Вебинар: Использование статических анализаторов кода при разработке безопасного ПО - 19.12
Проверяя много лет различные C/C++ проекты, я заявляю: самая неудачная и опасная функция - memset(). При использовании функции memset() допускают наибольшее количество ошибок, в сравнении с использованием других функций. Я понимаю, что мой вывод вряд ли потрясёт основы мироздания или невероятно ценен. Однако я думаю, читателям будет интересно узнать, почему я пришел к такому заключению.
Меня зовут Андрей Карпов. Я совмещаю много должностей и занятий. Но основное, что я делаю, это рассказываю программистам о пользе, которую может приносить статический анализ кода. Естественно я делаю это с корыстной целью, пытаясь заинтересовать читателей анализатором PVS-Studio. Впрочем, это не уменьшает интересность и полезность моих статей.
Единственный вид рекламы, который может пробить чешуйчатую броню программистов, это демонстрация примеров ошибок, которые умеет находить PVS-Studio. С этой целью я проверяю большое количество открытых проектов и пишу статьи о результатах исследований. Всеобщая выгода. Открытые проекты становятся немного лучше, а у нашей компании появляются новые клиенты.
Сейчас станет понятно, к чему я веду. Занимаясь проверкой открытых проектов, я накопил большую базу примеров ошибок. И теперь, основываясь на ней, могу находить интересные закономерности.
Например, одним из интересных наблюдений было, что программисты допускают ошибки при Copy-Paste чаще всего в самом конце. На эту тему предлагаю вниманию статью "Эффект последней строки".
Теперь у меня есть ещё одно интересное наблюдение. Используя те или иные функции, программисты могут допускать ошибки. При этом вероятность допущения ошибки зависит от используемой функции. Другими словами, какие-то функции провоцируют ошибки, а какие-то - нет.
Так вот, я готов назвать функцию, при использовании которой есть наибольшая вероятность сесть в лужу.
Итак, победитель на глючность - функция memset!
Как так получилось - сложно сказать. Видимо у неё неудачный интерфейс. Плюс само её использование достаточно трудоемко и легко ошибиться, вычисляя значения фактических аргументов.
Почетное второе место занимает функция printf() и её разновидности. Думаю, это никого не удивит. Про опасность функции printf() не писал только ленивый. Возможно из-за общеизвестности связанных с printf() проблем, она и попала на второе место.
Всего у меня в базе 9055 ошибок. Это те ошибки, которые умеет находить анализатор PVS-Studio. Понятно, что он умеет далеко не всё. Однако большое количество найденных ошибок позволяет мне быть уверенным в своих выводах. Так вот, я посчитал, что с использованием функции memset() связано 329 ошибок.
Итого, около 3,6% ошибок в базе связано с функцией memset(). Это много!
Давайте рассмотрим некоторые типовые примеры ошибок. Рассматривая их, я думаю, вы согласитесь, что с функцией memset() что-то не так. Она притягивает зло.
Для начала освежим в памяти как объявлена эта функция:
void * memset ( void * ptr, int value, size_t num );
Пример N1 (проект ReactOS)
void
Mapdesc::identify( REAL dest[MAXCOORDS][MAXCOORDS] )
{
memset( dest, 0, sizeof( dest ) );
for( int i=0; i != hcoords; i++ )
dest[i][i] = 1.0;
}
Ошибка в том, что в C и в C++ нельзя передавать массивы по значению (подробнее). Аргумент 'dest' является не чем иным как обыкновенным указателем. Поэтому оператор sizeof() вычисляет размер указателя, а не массива.
Вроде memset() и не виноват. Но с другой стороны, эта функция заполнит нулями только 4 или 8 байт (экзотические архитектуры не в счёт). Ошибка есть и произошла она при вызове функции memset().
Пример N2 (проект Wolfenstein 3D)
typedef struct cvar_s {
char *name;
...
struct cvar_s *hashNext;
} cvar_t;
void Cvar_Restart_f( void ) {
cvar_t *var;
...
memset( var, 0, sizeof( var ) );
...
}
Похожая ошибка. Допущена она скорее всего по невнимательности. Переменная 'var' является указателем. А значит memset() вновь обнулит только часть структуры. На практике будет обнулён только член 'name'.
Пример N3 (проект SMTP Client)
void MD5::finalize () {
...
uint1 buffer[64];
...
// Zeroize sensitive information
memset (buffer, 0, sizeof(*buffer));
...
}
Очень распространенный паттерн ошибки, про который тем не менее осведомлено мало программистов. Дело в том, что функция memset() будет удалена компилятором. Буфер после вызова memset() более не используется. И компилятор в целях оптимизации удаляет вызов функции. С точки зрения языка C/C++ это не оказывает никакого влияния на поведение программы. Это действительно так. То, что приватная информация останется в памяти, никак не повлияет на работу программы.
Это не ошибка компилятора. И это не мои фантазии. Компилятор действительно удаляет вызовы memset(). Каждый раз, когда я описываю эту ошибку уязвимости, я получаю письма, где со мной начинают спорить. Я уже устал отвечать на эти письма. Поэтому прошу всех сомневающихся, прежде чем начинать дискуссию, внимательно познакомиться со следующими материалами:
Пример N4 (проект Notepad++)
#define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];
...
DockingManager::DockingManager()
{
...
memset(_iContMap, -1, CONT_MAP_MAX);
...
}
Часто забывают, что третий аргумент функции memset() это не количество элементов, а размер буфера в байтах. Именно так и произошло в приведенном выше фрагменте кода. В результате, заполнена будет только четверть буфера (при условии, что размер типа 'int' равен 4 байтам).
Пример N5 (проект Newton Game Dynamics)
dgCollisionCompoundBreakable::dgCollisionCompoundBreakable(....)
{
...
dgInt32 faceOffsetHitogram[256];
dgSubMesh* mainSegmenst[256];
...
memset(faceOffsetHitogram, 0, sizeof(faceOffsetHitogram));
memset(mainSegmenst, 0, sizeof(faceOffsetHitogram));
...
}
Имеем дело с опечаткой. Скорее всего кто-то поленился два раза набирать вызов функции memset(). Продублировали строчку. В одном месте заменили 'faceOffsetHitogram' на 'mainSegmenst', а в другом забыли.
Получается, что sizeof() вычисляет размер не того массива, который заполняется нулями. Вроде как функция memset() никак не виновата. Но неправильно будет работать именно она.
Пример N6 (проект CxImage)
static jpc_enc_tcmpt_t *tcmpt_create(....)
{
...
memset(tcmpt->stepsizes, 0,
sizeof(tcmpt->numstepsizes * sizeof(uint_fast16_t)));
...
}
Здесь присутствует лишний оператор sizeof(). Правильно размер вычислять так:
tcmpt->numstepsizes * sizeof(uint_fast16_t)
Но написали лишний sizeof() и получилась глупость:
sizeof(tcmpt->numstepsizes * sizeof(uint_fast16_t))
Здесь оператор sizeof() вычисляет размер типа size_t. Именно такой тип имеет выражение.
Я знаю, что хочется возразить. Уже не первый раз ошибка связана с оператором sizeof(). Т.е. программист ошибается, вычисляя размер буфера. Однако причиной этих ошибок всё равно является функция memset(). Она устроена так, что приходится делать эти различные вычисления, в которых так легко ошибиться.
Пример N7 (проект WinSCP)
TForm * __fastcall TMessageForm::Create(....)
{
....
LOGFONT AFont;
....
memset(&AFont, sizeof(AFont), 0);
....
}
Функция memset() всеядна. Поэтому спокойно отнесётся, если вы перепутаете 2 и 3 аргумент. Именно так здесь и произошло. Эта функция заполняет 0 байт.
Пример N8 (проект Multi Theft Auto)
А вот ещё одна аналогичная ошибка. Кажется, разработчики Win32 API пошутили, когда создали вот такой макрос:
#define RtlFillMemory(Destination,Length,Fill) \
memset((Destination),(Fill),(Length))
По смыслу это альтернатива memset(). Но надо быть внимательным. Обратите внимание, что меняется местами 2 и 3 аргумент.
Когда начинают использовать RtlFillMemory(), то относятся к ней как к memset(). И думают, что параметры у них совпадают. В результате возникают ошибки.
#define FillMemory RtlFillMemory
LPCTSTR __stdcall GetFaultReason ( EXCEPTION_POINTERS * pExPtrs )
{
....
PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&g_stSymbol ;
FillMemory ( pSym , NULL , SYM_BUFF_SIZE ) ;
....
}
NULL есть ни что иное, как 0. Поэтому функция memset() заполнила 0 байт.
Пример N9 (проект IPP Samples)
Я думаю, вы понимаете, что примеры ошибок я могу приводить долго. Но это не очень интересно, так как они будут весьма однообразны и похожи на уже показанные в статье. Но ещё один случай давайте рассмотрим.
Хотя некоторые из приведенных выше ошибок были найдены в кода на языке C++, к C++ они никакого отношения не имеют. Другими словами, это ошибки возникают при программировании в стиле языка C.
Следующая ошибка связана как раз с неправильным использованием memset() в C++ программе. Пример достаточно длинный, поэтому можете в него не всматриваться. Прочитайте описание ниже и всё станет понятно.
class _MediaDataEx {
...
virtual bool TryStrongCasting(
pDynamicCastFunction pCandidateFunction) const;
virtual bool TryWeakCasting(
pDynamicCastFunction pCandidateFunction) const;
};
Status VC1Splitter::Init(SplitterParams& rInit)
{
MediaDataEx::_MediaDataEx *m_stCodes;
...
m_stCodes = (MediaDataEx::_MediaDataEx *)
ippsMalloc_8u(START_CODE_NUMBER*2*sizeof(Ipp32s)+
sizeof(MediaDataEx::_MediaDataEx));
...
memset(m_stCodes, 0,
(START_CODE_NUMBER*2*sizeof(Ipp32s)+
sizeof(MediaDataEx::_MediaDataEx)));
...
}
Функция memset() используется для инициализации массива, состоящих из объектов класса. Самая большая беда в том, что класс содержит виртуальные функции. Соответственно функция memset() не только обнуляет поля класса, но и указатель на таблицу виртуальных методов (vptr). К чему это приведёт неизвестно. Но ничего хорошего в этом точно нет. Нельзя так обращаться с классами.
Как видите, функция memset() имеет крайне неудачный интерфейс. В результате, функция memset() больше всех остальных провоцирует появление ошибок. Будьте бдительны!
Я не готов сейчас сказать, как можно использовать моё наблюдение. Но надеюсь, вам было интересно познакомиться с этой заметкой. Возможно теперь, используя memset(), вы будете более внимательны. И это уже хорошо.
Спасибо всем за внимание и подписывайтесь на мой твиттер @Code_Analysis.
Уже после публикации статьи, один из читателей прислал ссылку вот на эту интересную статью "memset is Evil". Решил поделиться ей с вами. Что-ж, ещё одно подтверждение опасности memset().
0