19 смертных грехов, угрожающих безопасности программ - страница 27
Не вздумайте подавлять эти надоедливые предупреждения путем приведения типов. Теперь вы знаете, насколько это рискованно и как внимательно нужно все проверять. Найдите все приведения и убедитесь, что они безопасны. О приведениях и преобразованиях в языках С и С++ см. раздел «Операции приведения» выше.
Вот еще один пример:
int read(char* buf, size_t count) {
// Сделать что-то с памятью
}
...
while (true) {
BYTE buf[1024];
int skip = count – cbBytesRead;
if (skip > sizeof(buf))
skip = sizeof(buf);
if (read(buf, skip))
cbBytesRead += skip;
else
break;
...
В этом фрагменте значение skip сравнивается с 1024, и если оно меньше, то в буфер buf копируется skip байтов. Проблема в том, что если skip оказывается отрицательным (скажем, -2), то оно будет заведомо меньше 1024, так что функция read попытается скопировать–2 байта, а это значение, будучи представлено как целое без знака (тип size_t), равно без малого 4 Гб. Итак, read() копирует 4 Гб в буфер размером 1 Кб. Печально!
Еще один источник неприятностей – это оператор new в языке С++. Он сопряжен с неявным умножением:
Foo *p = new Foo(N);
Если N контролируется противником, то возможно переполнение внутри operator new при вычислении выражения N * sizeof(Foo);
С#
Хотя сам язык С# и не допускает прямых обращений к памяти, но иногда программа обращается к системным вызовам, помещенным в блок, помеченный ключевым словом unsafe (при этом ее еще надо компилировать с флагом /unsafe). Любые вычисления, результат которых передается системному вызову, нужно контролировать. Для этого полезно применить ключевое слово checked или – еще лучше – соответствующий флаг компилятора. Включите его и следите, не произойдет ли исключения. Напротив, ключевое слово unchecked используйте изредка, предварительно обдумав все последствия.
Java
В языке Java прямые обращения к памяти также запрещены, поэтому он не так опасен, как C/C++. Но проявлять беспечность все же не следует: как и в C/C++, в Java нет никакой защиты от переполнения целых, и легко можно допустить логические ошибки. О том, какие существуют программные решения, см. в разделе «Искупление греха».
Visual Basic и Visual Basic .NET
Visual Basic ухитрился превратить проблему переполнения целого в проблему отказа от обслуживания – примерно такую же ситуацию мы имеем при использовании ключевого слова checked в С#. Подозрение должны вызывать те места, где программист применяет механизм перехвата для игнорирования ошибок при работе с целыми. Убедитесь, что ошибки обрабатываются правильно. Следующее предложение в программе на Visual Basic (не Visual Basic .NET) свидетельствует о лености программиста, не пожелавшего обрабатывать ошибки, возникающие во время работы программы. Нехорошо это.
On Error Continue
Perl
Perl – замечательный язык, но математика чисел с плавающей точкой может преподнести неожиданности. По большей части Perl делает то, чего от него ожидают, но будьте осторожны: это очень многогранный язык. В особенности это относится к случаям, когда вы обращаетесь к модулям, являющимся тонкой оберткой вокруг системных вызовов.
Тестирование
Если на вход подаются строки символов, попробуйте задать размеры так, чтобы вызвать ошибку. Часто это происходит, если длина строки составляет 64К или 64К – 1 байтов. Также ошибки возможны для длин, равных 127, 128, 255 и 32К плюс–минус единица. Если вам удалось подобрать тест так, что прибавление единицы к числу вызвало изменение знака или обращение в нуль, значит, вы не зря потрудились.