найнебезпечніша функція в світі З/З++

memset()
Перевіряючи багато років різні 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 );
  • 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)
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 {
...
віртуальний bool TryStrongCasting(
pDynamicCastFunction pCandidateFunction) const;
віртуальний 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.

Ще я веду ресурс C++Hints, де ділюся різними корисними порадами, які приходять мені в голову. Підписуйтесь.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.