← На главную

Проверил, помню ли я еще Си – TCP сервер с ошибками

В свое время я довольно сильно увлекался всевозможной низкоуровщиной – ассемблерами (о чем как бы намекает доменное имя блога), сишечкой и так далее. Даже написал пару несложных драйверов для Windows. Но потом я открыл для себя Perl и понеслось.

Оказалось, что программы можно писать без ручного управления памятью, постоянной борьбы с переполнениями буферов и тп. Позже выяснилось, что выводить типы в уме и иметь побочные эффекты тоже не обязательно, и меня потянуло в сторону функциональщины. К Си, а также C++, я не возвращался (за исключением единичных случаев) в течение долгого времени.

А недавно я выбрал для легкого чтения перед сном книгу «Компьютерные вирусы и антивирусы, взгляд программиста». И такая ностальгия на меня нахлынула, что я решил сесть и написать небольшую программку на Си.

Программка банальнейшая – TCP сервер, спрашивающий имя пользователя и говорящий «привет, %пользователь»:

#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <stdio.h> #include <unistd.h> #include <errno.h> void handle_error(char* err_fun) { printf("%s() failed, errno = %d", err_fun, errno); exit(1); } void child_proc(int sock) { size_t name_len; char msg[] = "What is your name?\n"; char name_buff[256]; char format_buff[256]; write(sock, msg, sizeof(msg)); name_len = read(sock, name_buff, sizeof(name_buff)); if(-1 == name_len) { handle_error("read"); } while(name_len > 0 && ( '\n' == name_buff[name_len-1] || '\r' == name_buff[name_len-1])) { name_len--; } name_buff[name_len] = 0; sprintf(format_buff, "Hello, %s!\n", name_buff); write(sock, format_buff, strlen(format_buff)); close(sock); exit(0); } int main() { struct sockaddr_in addr; int accepted_socket; int fork_pid; int listen_socket = socket(AF_INET, SOCK_STREAM, 0); if(-1 == listen_socket) { handle_error("socket"); } addr.sin_family = AF_INET; addr.sin_port = htons(31337); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(-1 == bind(listen_socket, (struct sockaddr*)&addr, sizeof(addr))){ handle_error("bind"); } if(-1 == listen(listen_socket, 100)) { handle_error("listen"); } for(;;) { accepted_socket = accept(listen_socket, 0, 0); if(-1 == accepted_socket) { handle_error("accept"); } fork_pid = fork(); if(-1 == fork_pid) { handle_error("fork"); } else if(0 == fork_pid) { close(listen_socket); child_proc(accepted_socket); } else { close(accepted_socket); } } exit(0); }

Как выяснилось, что-то еще помнится. Писал я все это хозяйство минут тридцать, попутно заполняя пробелы в знаниях с помощью man-страниц и томика Стивенса, который в свое время я прочитал на одном дыхании и до сих пор бережно храню на почетной верхней книжной полке. Эх, наверное, все-таки, прикольно быть сишником…

Вы, конечно же, без труда найдете в этом коде три грубейшие ошибки. Я подскажу первую. Тут есть переполнение буфера. Пользователь может ввести имя длиной 256 байт, которые в сочетании с еще с девятью байтами форматной строки "Hello, %s!\n" переполнят буфер format_buff. Кстати, я обнаружил, что современные версии GCC вставляют рантайм проверки на переполнение буфера. В свое время еще говорили про хардварные защиты, что дескать все новые процессоры будут как-то автоматически детектить выполнение кода в стеке или что-то в этом роде. Кто следит за темой, переполнение буфера еще существует вообще?

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

Дополнение: Быстрее всех ошибки нашли smirn0v и rulexec. Молодцы! Описание ошибок вы найдете в заметке Типичные ошибки в сетевых приложениях на C/C++. Также вас может заинтересовать пост Асинхронная работа с сокетами на C/C++ с libevent.