Проверил, помню ли я еще Си — TCP сервер с ошибками
18 сентября 2013
В свое время я довольно сильно увлекался всевозможной низкоуровщиной — ассемблерами (о чем как бы намекает доменное имя блога), сишечкой и так далее. Даже написал пару несложных драйверов для Windows. Но потом я открыл для себя Perl и понеслось.
Оказалось, что программы можно писать без ручного управления памятью, постоянной борьбы с переполнениями буферов и тп. Позже выяснилось, что выводить типы в уме и иметь побочные эффекты тоже не обязательно, и меня потянуло в сторону функциональщины. К Си, а также C++, я не возвращался (за исключением единичных случаев) в течение долгого времени.
А недавно я выбрал для легкого чтения перед сном книгу «Компьютерные вирусы и антивирусы, взгляд программиста». И такая ностальгия на меня нахлынула, что я решил сесть и написать небольшую программку на Си.
Программка банальнейшая — TCP сервер, спрашивающий имя пользователя и говорящий «привет, %пользователь»:
#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.
Метки: C/C++.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.