﻿# Самая опасная функция в мире С/С\+\+

Проверяя много лет различные C/C\+\+ проекты, я заявляю: самая неудачная и опасная функция \- memset\(\)\. При использовании функции memset\(\) допускают наибольшее количество ошибок, в сравнении с использованием других функций\. Я понимаю, что мой вывод вряд ли потрясёт основы мироздания или невероятно ценен\. Однако я думаю, читателям будет интересно узнать, почему я пришел к такому заключению\.

![0360_The_most_dangerous_function_memset_ru/image1.png](https://import.viva64.com/docx/blog/0360_The_most_dangerous_function_memset_ru/image1.png)

## Здравствуйте

Меня зовут Андрей Карпов\. Я совмещаю много должностей и занятий\. Но основное, что я делаю, это рассказываю программистам о пользе, которую может приносить статический анализ кода\. Естественно я делаю это с корыстной целью, пытаясь заинтересовать читателей анализатором PVS\-Studio\. Впрочем, это не уменьшает интересность и полезность моих статей\.

Единственный вид рекламы, который может пробить чешуйчатую броню программистов, это демонстрация примеров ошибок, которые умеет находить PVS\-Studio\. С этой целью я проверяю большое количество открытых проектов и пишу статьи о результатах исследований\. Всеобщая выгода\. Открытые проекты становятся немного лучше, а у нашей компании появляются новые клиенты\.

Сейчас станет понятно, к чему я веду\. Занимаясь проверкой открытых проектов, я накопил большую [базу](https://pvs-studio.ru/ru/blog/examples/v501/) примеров ошибок\. И теперь, основываясь на ней, могу находить интересные закономерности\.

Например, одним из интересных наблюдений было, что программисты допускают ошибки при Copy\-Paste чаще всего в самом конце\. На эту тему предлагаю вниманию статью "[Эффект последней строки](https://pvs-studio.ru/ru/blog/posts/cpp/0260/)"\.

## Новое наблюдение

Теперь у меня есть ещё одно интересное наблюдение\. Используя те или иные функции, программисты могут допускать ошибки\. При этом вероятность допущения ошибки зависит от используемой функции\. Другими словами, какие\-то функции провоцируют ошибки, а какие\-то \-  нет\.

Так вот, я готов назвать функцию, при использовании которой есть наибольшая вероятность сесть в лужу\.

Итак, победитель на глючность \- функция memset\!

Как так получилось \- сложно сказать\. Видимо у неё неудачный интерфейс\. Плюс само её использование достаточно трудоемко и легко ошибиться, вычисляя значения фактических аргументов\.

Почетное второе место занимает функция printf\(\) и её разновидности\. Думаю, это никого не удивит\. Про опасность функции printf\(\) не писал только ленивый\. Возможно из\-за общеизвестности связанных с printf\(\) проблем, она и попала на второе место\.

Всего у меня в базе 9055 ошибок\. Это те ошибки, которые умеет находить анализатор PVS\-Studio\. Понятно, что он умеет далеко не всё\. Однако большое количество найденных ошибок позволяет мне быть уверенным в своих выводах\. Так вот, я посчитал, что с использованием функции memset\(\) связано 329 ошибок\.

Итого, около 3,6% ошибок в базе связано с функцией memset\(\)\. Это много\!

## Примеры

Давайте рассмотрим некоторые типовые примеры ошибок\. Рассматривая их, я думаю, вы согласитесь, что с функцией memset\(\) что\-то не так\. Она притягивает зло\.

Для начала освежим в памяти как объявлена эта функция:

void \* memset \( void \* ptr, int value, size\_t num \);

* ptr \- Pointer to the block of memory to fill\.
* value \- Value to be set\. The value is passed as an int, but the function fills the block of memory using the unsigned char conversion of this value\.
* num \- Number of bytes to be set to the value\. 'size\_t' is an unsigned integral type\.

**Пример N1 \(проект ReactOS\)**

```cpp
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\+\+ нельзя передавать массивы по значению \([подробнее](http://cpphints.com/hints/38)\)\. Аргумент 'dest' является не чем иным как обыкновенным указателем\. Поэтому оператор sizeof\(\) вычисляет размер указателя, а не массива\.

Вроде memset\(\) и не виноват\. Но с другой стороны, эта функция заполнит нулями только 4 или 8 байт \(экзотические архитектуры не в счёт\)\. Ошибка есть и произошла она при вызове функции memset\(\)\.

**Пример N2 \(проект Wolfenstein 3D\)**

```cpp
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\)**

```cpp
void MD5::finalize () {
  ...
  uint1 buffer[64];
  ...
  // Zeroize sensitive information
  memset (buffer, 0, sizeof(*buffer));
  ...
}
```

Очень распространенный паттерн ошибки, про который тем не менее осведомлено мало программистов\. Дело в том, что функция memset\(\) будет удалена компилятором\. Буфер после вызова memset\(\) более не используется\. И компилятор в целях оптимизации удаляет вызов функции\. С точки зрения языка C/C\+\+ это не оказывает никакого влияния на поведение программы\. Это действительно так\. То, что приватная информация останется в памяти, никак не повлияет на работу программы\. 

Это не ошибка компилятора\. И это не мои фантазии\. Компилятор действительно удаляет вызовы memset\(\)\. Каждый раз, когда я описываю эту ошибку уязвимости, я получаю письма, где со мной начинают спорить\. Я уже устал отвечать на эти письма\. Поэтому прошу всех сомневающихся, прежде чем начинать дискуссию, внимательно познакомиться со следующими материалами:

* Документация PVS\-Studio\. [V597](https://pvs-studio.ru/ru/docs/warnings/v597/)\.
* Mansour Moufid\. [Zero and forget \-\- caveats of zeroing memory in C](http://web.archive.org/web/20160304012102/http:/www.eliteraspberries.com/blog/2012/10/zero-and-forget--caveats-of-zeroing-memory-in-c.html)\.
* SEI CERT C Coding Standard\. [MSC06\-C](https://wiki.sei.cmu.edu/confluence/display/c/msc06-c.+beware+of+compiler+optimizations)\.
* [CWE\-14](https://cwe.mitre.org/data/definitions/14.html): Compiler Removal of Code to Clear Buffers
* [Перезаписывать память \- зачем?](https://pvs-studio.ru/ru/blog/posts/cpp/k0041/)

**Пример N4 \(проект Notepad\+\+\)**

```cpp
#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\)**

```cpp
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\)**

```cpp
static jpc_enc_tcmpt_t *tcmpt_create(....)
{
  ...
  memset(tcmpt->stepsizes, 0,
    sizeof(tcmpt->numstepsizes * sizeof(uint_fast16_t)));
  ...
}
```

Здесь присутствует лишний оператор sizeof\(\)\. Правильно размер вычислять так:

```cpp
tcmpt->numstepsizes * sizeof(uint_fast16_t)
```

Но написали лишний sizeof\(\) и получилась глупость:

```cpp
sizeof(tcmpt->numstepsizes * sizeof(uint_fast16_t))
```

Здесь оператор sizeof\(\) вычисляет размер типа size\_t\. Именно такой тип имеет выражение\.

Я знаю, что хочется возразить\. Уже не первый раз ошибка связана с оператором sizeof\(\)\. Т\.е\. программист ошибается, вычисляя размер буфера\. Однако причиной этих ошибок всё равно является функция memset\(\)\. Она устроена так, что приходится делать эти различные вычисления, в которых так легко ошибиться\.

**Пример N7 \(проект WinSCP\)**

```cpp
TForm * __fastcall TMessageForm::Create(....)
{
  ....
  LOGFONT AFont;
  ....   
  memset(&AFont, sizeof(AFont), 0);
  ....
}
```

Функция memset\(\) всеядна\. Поэтому спокойно отнесётся, если вы перепутаете 2 и 3 аргумент\. Именно так здесь и произошло\. Эта функция заполняет 0 байт\.

**Пример N8 \(проект Multi Theft Auto\)**

А вот ещё одна аналогичная ошибка\. Кажется, разработчики Win32 API пошутили, когда создали вот такой макрос: 

```cpp
#define RtlFillMemory(Destination,Length,Fill) \
  memset((Destination),(Fill),(Length))
```

По смыслу это альтернатива memset\(\)\. Но надо быть внимательным\. Обратите внимание, что меняется местами 2 и 3 аргумент\.

Когда начинают использовать RtlFillMemory\(\), то относятся к ней как к memset\(\)\. И думают, что параметры у них совпадают\. В результате возникают ошибки\. 

```cpp
#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\+\+ программе\. Пример достаточно длинный, поэтому можете в него не всматриваться\. Прочитайте описание ниже и всё станет понятно\.

```cpp
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](https://twitter.com/Code_Analysis)\.

## Примечание

Уже после публикации статьи, один из читателей прислал ссылку вот на эту интересную статью "[memset is Evil](https://web.archive.org/web/20170702122030/https:/augias.org/paercebal/tech_doc/doc.en/cp.memset_is_evil.html)"\. Решил поделиться ей с вами\. Что\-ж, ещё одно подтверждение опасности memset\(\)\.