вторник, 19 июля 2011 г.

[prog.bugs] А жуки все лезут и лезут. На этот раз приплюснутые

Прошло совсем немного спокойных дней после очень нервной bug’s hunting week, как опять полезли злобные жуки, в самых неожиданных местах (в коде, который эксплуатировался годами) и в самый неподходящий момент. Причем оба бага, о которых я расскажу, являются просто таки классическими C/C++ багами.

Первый баг очень простой:

std::string
hex_char( char ch )
{
   char v[5];
   std::sprintf( v, "%02x", ch );
   return std::string(v);
}

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

Если кто-то не силен в С++ или не заметил еще проблемы, раскрою причину. Тип char в большинстве случаев (если не указывать компилятору специальный ключик) знаковый. При передаче ch в sprintf происходит его преобразование в int. Т.е., если в ch находился символ из диапазона 128..255 (например, 0xFF), то при передаче в sprintf получится значение 0xFFFFFFFF (для 32-х битового int-а). Соответственно, текстовое представление этого значения никак не поместится в v. И, в лучшем случае, программа сразу упадет. Что она и начала делать, когда я с ее помощью стал проверять изменения, внесенные в совсем-совсем другой части большого программного комплекса.

Второй баг так же не сложен. Но вот его маскировка в течении трех лет намного интереснее. Впрочем, начнем по порядку. Ошибка находится в следующем фрагменте:

std::string msg;
std::vector< wchar_t > wbuf;
...
msg = ACE_Wide_To_Ascii::convert( &( wbuf.front() ) );

В чем здесь смысл – из какого-то источника берется последовательность байт, которая является набором Unicode-символов. Эта последовательность сохраняется в wbuf. А затем Unicode-строка передается в статический метод convert ACE-овского класса ACE_Wide_To_Ascii. Данный метод имеет следующий прототип:

/// Converts an wchar_t string to ascii and returns a new string.
static char *convert (const wchar_t *wstr);

Сразу расскажу, что здесь к чему. Во-первых, convert возвращает указатель на динамически размещенную в памяти строку. Эту память затем нужно освобождать вручную. Чего не делалось – получалась утечка. Во-вторых, convert не добавляет к результирующей строке завершающий 0-символ. Поэтому результат convert-а нельзя отдавать в basic_string::operator=(), т.к. operator=() определяет конец строки как раз по 0-символу. Но 0-символа не было, поэтому получался проход по памяти.

Для меня очевидно, что эти две проблемы не возникли бы вообще, если бы разработчик заглянул внутрь ACE_Wide_To_Ascii::convert. Мне вот из комментария к методу было не понятно, как метод работает и что возвращает. Пришлось залезть в исходник и разобраться. Почему этого не сделал автор кода – теперь уже не узнать :(

Но самое интересное не в факте существования бага. Интересно другое – долгое время этот код успешно проходил unit-тестирование. Т.е. он изначально был покрыт unit-тестом и все время проверялся. Но ошибка не проявлялась.

Оказалось, что вначале unit-тесты писались с использованием Boost.Test. При этом unit-тесты отрабатывали тихо и гладко, никаких проблем не диагностировалось. Затем Boost.Test выбросили, unit-тесты чуть подкорректировали – и… И ошибка не проявлялась еще в течении нескольких лет.

И вот что подозрительно. Как только я запустил полный билд библиотеки с прогоном всех тестов, проблема с проходом по памяти сразу же всплыла наружу. Почему же она не была выявлена ранее? Либо у разработчика при прогоне тестов звезды располагались совсем иначе и проход по памяти по счастливому стечению обстоятельств не происходил. Либо разработчик вообще не запускал unit-тесты – т.е. увидел, что компиляция проходит успешно, а запускать не стал. К сожалению, правды сейчас не узнать.

В очередной раз убеждаюсь, что C++ – это классный язык, если разработку ведет совсем маленькая команда хороших специалистов. Но стоит к разработке присоединиться парочке чайников или, что еще хуже, раздолбаев, как C++ный проект превращается в минное поле.

4 комментария:

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

Первый баг говорит о том что автора мало били линейкой по рукам за использование функции sprintf вместо snprintf.

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

> В очередной раз убеждаюсь, что C++ – это классный язык, если разработку ведет совсем маленькая команда хороших специалистов. Но стоит к разработке присоединиться парочке чайников или, что еще хуже, раздолбаев, как C++ный проект превращается в минное поле.

Хех. Идеальных программистов не бывает и в достаточно большом проекте даже хороший специалист по С++ допустит пару ошибок которые выльются в вот такие вот бомбы. Вывод - чем меньше С и С++ в проекте, тем лучше для всех.

Анонимный комментирует...

"Первый баг говорит о том что автора мало били линейкой по рукам за использование функции sprintf вместо snprintf."

Опять же, это проблема языка С - почему то автор утверждает что это С++ - непонятно.
Если бы это было реализовано на С++ (std::stringstream, formatters) то подобного бага бы не возникло.

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

присоединяюсь к idispatch - стоит ли экономить на спичках? Если использовать C++-ные потоки и строки, то обоих багов не будет. А если уж приходится использовать c-строки, то надо быть внимательным...

А функция, которая создает объект и возвращает голый указатель на него - вообще моветон по моему. Стоило бы возвращать бы какой-нить auto_ptr (или умный указатель из ace - если он там есть)