Проверка корректности входной информации
Что бы вы ни программировали, не думайте, что приложение будет получать только те данные, на которые рассчитывает. Пренебрежение этим принципом- пример отсутствия защитного программирования. Хочется надеяться, что профессиональный программист, разрабатывающий коммерческую программу, всегда ему следует. Однако часто это правило игнорируют. В работе [Miller et al. 1995] описывается, как авторы генерировали случайный набор входных данных и подавали его на вход всевозможных стандартных утилит UNIX от разных производителей. При этом им удалось «сломать» (с дампом памяти) или «подвесить» (в бесконечном цикле) от 6 до 43% тестируемых программ (в зависимости от производителя;. В семи исследованных коммерческих системах частота отказов составила 23%
Вывод ясен: если такие результаты получены при тестировании зрелых программ, которые принято считать программами «промышленного качества», то те более необходимо защищаться и подвергать сомнению все места в программе, где неожиданные входные данные могут привести к нежелательным результатам. Рассмотрим несколько примеров, когда неожиданные данные оказываются источником ошибок.
Две самые распространенные причины краха приложений - это переполнение буфера и сбитые указатели. В вышеупомянутом исследовании именно эти две ошибки послужили причиной большинства сбоев. Можно сказать, что в сетевых программах переполнение буфера должно быть редким явлением, так как при обращении к системным вызовам, выполняющим чтение (read, recv, recvfrom, readv и readmsg), всегда необходимо указывать размер буфера. Но вы увидите далее как легко допустить такую ошибку. (Это рассмотрено в замечании к строке 42 программы shutdown.с в совете 16.)
Чтобы понять, как это происходит, разработаем функцию readline, использовавшуюся в совете 9. Поставленная задача - написать функцию, которая считывает из сокета в буфер одну строку, заканчивающуюся символом новой строки, и дописывает в конец двоичный нуль. На начало буфера указывает параметр buf.
#include "etcp.h"
int readline( SOCKET s, char *buf, size_t len );
Возвращаемое значение: число прочитанных байтов или -1 в случае ошибки.
Первая попытка реализации, которую надо отбросить сразу, похожа на следующий код:
while ( recv( fd, , &с, 1, 0 ) == 1 )
{
*bufptr++ = с;
if ( с == "\n" )
break;
}
/* Проверка ошибок, добавление завершающего нуля и т.д. */
Прежде всего, многократные вызовы recv совсем неэффективны, поскольку при каждом вызове нужно два переключения - в режим ядра и обратно.
Примечание: Но иногда приходится писать и такой код - смотрите, например, функцию readcrlf в листинге 3.10.
Важнее, однако, то, что нет контроля переполнения буфера, чтобы понять, как аналогичная ошибка может вкрасться и в более рациональную реализацию, следует рассмотреть такой фрагмент:
static char *bp;
static int cnt = 0;
static char b[ 1500 ];
char c;
for ( ; ; )
{
if (cnt-- <= 0)
{
cnt = recv( fd, b, sizeof( b ), 0 );
if ( cnt < 0 )
return -1;
if ( cnt == 0 )
return 0;
bp = b;
}
c = *bp++;
*bufptr++ = c;
if ( c ==”\n” )
{
*bufptr = “\0”;
break;
}
}
В этой реализации нет неэффективности первого решения. Теперь считывается большой блок данных в промежуточный буфер, а затем по одному копируются байты в окончательный буфер; по ходу производится поиск символа новой строки. Но при этом в коде присутствует та же ошибка, что и раньше. Не проверяется переполнение буфера, на который указывает переменная bufptr. Можно было бы и не писать универсальную функцию чтения строки; такой код - вместе с ошибкой - легко мог бы быть частью какой-то большей функции.
А теперь напишем настоящую реализацию (листинг 2.31).
Листинг 2.31. Неправильная реализация readline
readline.с
1 int readline( SOCKET fd, char *bufptr, size_t len )
2 {
3 char *bufx = bufptr;
4 static char *bp;
5 static int cnt = 0;
6 static char b[ 1500 ];
7 char c;
8 while ( --len > 0 )
9 {
10 if ( --cnt <= 0 )
11 {
12 cnt = recv( fd, b, sizeof( b ), 0 );
13 if ( cnt < 0 )
14 return -1;
15 if ( cnt == 0 )
16 return 0;
17 bp = b;
18 }
19 с = *bp++;
20 *bufptr++ = c;
21 if ( с == "\n" )
22 {
23 *bufptr = "\
24 return bufptr - bufx;
25 )
26 }
27 set_errno( EMSGSIZE ) ;
28 return -1;
29 }
На первый взгляд, все хорошо. Размер буфера передается readline и во внешнем цикле проверяется, не превышен ли он. Если размер превышен, то переменной errno присваивается значение EMSGSIZE и возвращается -1.
Чтобы понять, в чем ошибка, представьте, что функция вызывается так:
rc = readline( s, buffer, 10 );
и при этом из сокета читается строка
123456789<nl>
Когда в c записывается символ новой строки, значение len равно нулю. Это означает, что данный байт последний из тех, что готовы принять. В строке 20 помещаете символ новой строки в буфер и продвигаете указатель bufptr за конец буфера. Ошибка возникает в строке 23, где записывается нулевой байт за границу буфера.
Заметим, что похожая ошибка имеет место и во внутреннем цикле. Чтобы увидеть ее, представьте, что при входе в функцию readline значение cnt равно нулю и recv возвращает один байт. Что происходит дальше? Можно назвать это «опустошением» (underflow) буфера.
Этот пример показывает, как легко допустить ошибки, связанные с переполнением буфера, даже предполагая, что все контролируется. В листинге 2.32 приведена окончательная, правильная версия readline.
Листинг 2.32. Окончательная версия readline
1 int readline( SOCKET fd, char *bufptr, size_t len )
2 {
3 char *bufx = bufptr;
4 static char *bp;
5 static int cnt = 0;
6 static char b[ 1500 ];
7 char c;
8 while ( --len > 0 )
9 {
10 if ( --cnt <= 0 )
11 {
12 cnt = recv( fd, b, sizeof ( b ), 0 );
13 if ( cnt < 0 )
14 {
15 if ( errno == EINTR )
16 {
17 len++; /*Уменьшим на 1 в заголовке while.*/
18 continue;
19 }
20 return –1;
21 }
22 if ( cnt == 0)
23 return 0;
24 bp = b;
25 }
26 с = *bp++;
27 *bufptr++ = с;
28 if ( с == "\n" )
29 {
30 *bufptr = "\0";
31 return bufptr - bufx;
32 }
33 }
34 set_errno( EMSGSIZE ) ;
35 return -1;
36 }
Единственная разница между этой и предыдущей версиями в том, что уменьшаются значения len и cnt до проверки, а не после. Также проверяется, не вернула ли recv значение EINTR. Если это так, то вызов следует повторить. При уменьшении len до использования появляется гарантия, что для нулевого байта всегда останется место. А, уменьшая cnt, можно получить некоторую уверенность, что данные не будут читаться из пустого буфера.