← Commit list

1a64fe359

Denys Vlasenko
Denys Vlasenko
2026-01-24
SUCCESS Duration: 142s

Subject

httpd: implement POSTDATA read timeout, and -K KILLSECS CGI lifetime control

Binary

No binary (build failed)

Architecture & Compiler Flags

Architecture: x86_64-linux-gnu Compiler: gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0
gcc -Wp,-MD,applets/.usage.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer      -Iinclude -Iinclude -o applets/usage applets/usage.c

Build Log

(log yok)

Commit Diff GitHub →

networking/httpd.c+110 -59
@@ -268,6 +268,7 @@
//usage: " [-c CONFFILE]"
//usage: " [-p [IP:]PORT]"
//usage: " [-M MAXCONN]"
+//usage: IF_FEATURE_HTTPD_CGI(" [-K KILLSEC]")
//usage: IF_FEATURE_HTTPD_SETUID(" [-u USER[:GRP]]")
//usage: IF_FEATURE_HTTPD_BASIC_AUTH(" [-r REALM]")
//usage: " [-h HOME]\n"
@@ -279,6 +280,8 @@
//usage: "\n -v[v] Verbose"
//usage: "\n -p [IP:]PORT Bind to IP:PORT (default *:"STR(CONFIG_FEATURE_HTTPD_PORT_DEFAULT)")"
//usage: "\n -M NUM Pause if NUM connections are open (default 256)"
+//usage: IF_FEATURE_HTTPD_CGI(
+//usage: "\n -K NUM Kill CGIs after NUM seconds")
//usage: IF_FEATURE_HTTPD_SETUID(
//usage: "\n -u USER[:GRP] Set uid/gid after binding to port")
//usage: IF_FEATURE_HTTPD_BASIC_AUTH(
@@ -299,6 +302,10 @@
* (HEADER_READ_TIMEOUT)
* - if client stops consuming its result ("stalled receiver" attack),
* the server will drop the connection (DATA_WRITE_TIMEOUT)
+ * - POSTDATA for POST method to CGI must arrive at least once
+ * per DATA_READ_TIMEOUT
+ * - killing CGIs which are obviously stuck if -K 60:
+ * "if CGI isn't done in 1 minute, it's fishy. SIGTERM+SIGKILL"
*
* To limit the number of simultaneously running CGI children,
* you may use the helper tool, httpd_ratelimit_cgi.c:
@@ -309,15 +316,13 @@
*
* TODO:
* - do not allow all MAXCONN connections be taken up by the same remote IP.
- * - CGI I/O loop lacks full timeout protection (may be abused by slow POST client?),
- * see cgi_io_loop_and_exit()
* - sanity: add a limit how big POSTDATA can be? (-P 1024: "megabyte+ of POSTDATA is insanity")
* currently we only do: if (POST_length > INT_MAX) HTTP_BAD_REQUEST
- * - sanity: kill CGIs which are obviously stuck? (-K 60: "if CGI isn't done in 1 minute, it's fishy. SIGTERM+SIGKILL")
* - sanity: measure CGI memory consumption (how?), kill when way too big?
*/
#define HEADER_READ_TIMEOUT 30
#define DATA_WRITE_TIMEOUT 60
+#define DATA_READ_TIMEOUT 60
#include "libbb.h"
@@ -483,7 +488,11 @@ struct globals {
smallint flg_deny_all;
#if ENABLE_FEATURE_HTTPD_GZIP
/* client can handle gzip / we are going to send gzip */
+ smallint accept_gzip;
smallint content_gzip;
+#endif
+#if ENABLE_FEATURE_HTTPD_CGI
+ smallint cgi_output;
#endif
time_t last_mod;
#if ENABLE_FEATURE_HTTPD_ETAG
@@ -528,6 +537,13 @@ struct globals {
#define sizeof_hdr_buf COMMON_BUFSIZE
char *hdr_ptr;
int hdr_cnt;
+#if ENABLE_FEATURE_HTTPD_CGI || ENABLE_FEATURE_HTTPD_PROXY
+ int POST_len;
+#endif
+#if ENABLE_FEATURE_HTTPD_CGI
+ unsigned cgi_kill_timeout;
+ pid_t cgi_pid;
+#endif
#if ENABLE_FEATURE_HTTPD_ETAG
char etag[sizeof("'%llx-%llx'") + 2 * sizeof(long long)*3];
#endif
@@ -543,8 +559,10 @@ struct globals {
#define verbose (G.verbose )
#define flg_deny_all (G.flg_deny_all )
#if ENABLE_FEATURE_HTTPD_GZIP
+# define accept_gzip (G.accept_gzip )
# define content_gzip (G.content_gzip )
#else
+# define accept_gzip 0
# define content_gzip 0
#endif
#define bind_addr_or_port (G.bind_addr_or_port)
@@ -1386,8 +1404,8 @@ static unsigned get_line(void)
#if ENABLE_FEATURE_HTTPD_CGI || ENABLE_FEATURE_HTTPD_PROXY
/* gcc 4.2.1 fares better with NOINLINE */
-static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post_len) NORETURN;
-static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post_len)
+static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr) NORETURN;
+static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr)
{
enum { FROM_CGI = 1, TO_CGI = 2 }; /* indexes in pfd[] */
struct pollfd pfd[3];
@@ -1401,12 +1419,14 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post
// is coming than we expected. We may give *some part* of that
// extra data to CGI.
- //if (hdr_cnt > post_len) {
+ /* We often have the start of POSTDATA buffered in hdr_buf already
+ * by the header lines reading logic */
+ //if (hdr_cnt > G.POST_len) {
// /* We got more POSTDATA from network than we expected */
- // hdr_cnt = post_len;
+ // hdr_cnt = G.POST_len;
//}
- post_len -= hdr_cnt;
- /* post_len - number of POST bytes not yet read from network */
+ G.POST_len -= hdr_cnt;
+ //bb_error_msg("G.POST_len:%d", G.POST_len);
/* NB: breaking out of this loop jumps to log_and_exit() */
out_cnt = 0;
@@ -1432,15 +1452,22 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post
pfd[TO_CGI].revents = 0; /* needed! */
if (toCgi_wr && hdr_cnt <= 0) {
- if (post_len > 0) {
+ if (G.POST_len > 0) {
/* Expect more POST data from network */
pfd[0].fd = 0;
+ /* Kill ourselves if no data arrives in N seconds */
+ alarm(DATA_READ_TIMEOUT);
} else {
- /* post_len <= 0 && hdr_cnt <= 0:
+ /* G.POST_len <= 0 && hdr_cnt <= 0:
* no more POST data to CGI,
* let CGI see EOF on CGI's stdin */
- if (toCgi_wr != fromCgi_rd)
+ if (!ENABLE_FEATURE_HTTPD_PROXY || (toCgi_wr != fromCgi_rd)) {
close(toCgi_wr);
+ alarm(G.cgi_kill_timeout);
+ } else {
+ /* proxying a socket, there is no CGI */
+ alarm(0);
+ }
toCgi_wr = 0;
}
}
@@ -1448,9 +1475,6 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post
/* Now wait on the set of sockets */
/* Poll whether TO_CGI is ready to accept writes *only* if we have some POSTDATA to give to it */
count = safe_poll(pfd, hdr_cnt > 0 ? TO_CGI+1 : FROM_CGI+1, -1);
-//FIXME:do not allow the client to stall POSTDATA:
-//the timeout must not be infinite, should be similar to DATA_WRITE_TIMEOUT
-//("give some data at least once a minute", better "give >=N bytes once minute")
if (count <= 0) {
#if 0 /* This doesn't work since we SIG_IGNed SIGCHLD (which means kernel auto-reaps our children) */
@@ -1483,25 +1507,25 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post
hdr_cnt -= count;
} else {
/* EOF/broken pipe to CGI, stop piping POST data */
- hdr_cnt = post_len = 0;
+ hdr_cnt = G.POST_len = 0;
}
}
-
+ else /* After one poll(), we either write to CGI, or read from network. Never both */
if (pfd[0].revents) {
- /* post_len > 0 && hdr_cnt == 0 here */
+ /* G.POST_len > 0 && hdr_cnt == 0 here */
/* We expect data, prev data portion is eaten by CGI
* and there *is* POSTDATA to read from the peer
*/
- //count = post_len > (int)sizeof_hdr_buf ? (int)sizeof_hdr_buf : post_len;
+ //count = G.POST_len > (int)sizeof_hdr_buf ? (int)sizeof_hdr_buf : G.POST_len;
//count = safe_read(STDIN_FILENO, hdr_buf, count);
count = safe_read(STDIN_FILENO, hdr_buf, sizeof_hdr_buf);
if (count > 0) {
hdr_cnt = count;
hdr_ptr = hdr_buf;
- post_len -= count; /* can go negative (peer wrote more than expected POSTDATA) */
+ G.POST_len -= count; /* can go negative (peer wrote more than expected POSTDATA) */
} else {
/* no more POST data can be read */
- post_len = 0;
+ G.POST_len = 0;
}
}
@@ -1546,6 +1570,13 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post
memmove(iobuf + 1, iobuf, out_cnt);
out_cnt += 1;
memcpy(iobuf, HTTP_200, 9);
+ if (verbose) {
+ char *end = iobuf + 9;
+ int cnt = out_cnt - 9;
+ while ((unsigned char)*end >= ' ' && (unsigned char)*end < 0x7f && cnt > 0)
+ end++, cnt--;
+ bb_error_msg("cgi response:'%.*s'", (int)(end - (iobuf + 9)), iobuf + 9);
+ }
}
//NB: Apache has no such autodetection. It always adds its own HTTP/1.x header,
//unless the CGI name starts with "nph-", in which case it passes its output verbatim to network.
@@ -1556,10 +1587,12 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post
/* no, send "HTTP/1.1 200 OK\r\n" ourself */
if (full_write(STDOUT_FILENO, HTTP_200, sizeof(HTTP_200)-1) != sizeof(HTTP_200)-1)
break;
+ if (verbose)
+ bb_error_msg("cgi response:%u", 200);
}
/* Used to add "Content-type: text/plain\r\n\r\n" here if CGI didn't,
* but it's wrong. Counter-example of valid CGI without Content-type:
- * echo -en "HTTP/1.1 302 Found\r\n"
+ * echo -en "Status: 302 Found\r\n"
* echo -en "Location: http://www.busybox.net\r\n"
* echo -en "\r\n"
*/
@@ -1571,6 +1604,7 @@ static NOINLINE void cgi_io_loop_and_exit(int fromCgi_rd, int toCgi_wr, int post
if (count <= 0)
send_EOF_and_exit(); /* EOF (or error) */
}
+ IF_FEATURE_HTTPD_CGI(G.cgi_output = 1;)
//FIXME: many (most?) servers translate bare "\n" to "\r\n", only in the headers, not body (the part after empty line)
if (full_write(STDOUT_FILENO, iobuf, count) != count)
break;
@@ -1598,23 +1632,19 @@ static void setenv1(const char *name, const char *value)
* Parameters:
* const char *url The requested URL (with leading /).
* const char *orig_uri The original URI before rewriting (if any)
- * int post_len Length of the POST body.
*/
static void send_cgi_and_exit(
const char *url,
const char *orig_uri,
- const char *request,
- int post_len) NORETURN;
+ const char *request) NORETURN;
static void send_cgi_and_exit(
const char *url,
const char *orig_uri,
- const char *request,
- int post_len)
+ const char *request)
{
struct fd_pair fromCgi; /* CGI -> httpd pipe */
struct fd_pair toCgi; /* httpd -> CGI pipe */
char *script, *last_slash;
- int pid;
/* Make a copy. NB: caller guarantees:
* url[0] == '/', url[1] != '/' */
@@ -1687,8 +1717,8 @@ static void send_cgi_and_exit(
#endif
}
}
- if (post_len)
- putenv(xasprintf("CONTENT_LENGTH=%u", post_len));
+ if (G.POST_len > 0)
+ putenv(xasprintf("CONTENT_LENGTH=%u", G.POST_len));
#if ENABLE_FEATURE_HTTPD_BASIC_AUTH
if (remoteuser) {
setenv1("REMOTE_USER", remoteuser);
@@ -1701,17 +1731,20 @@ static void send_cgi_and_exit(
xpiped_pair(fromCgi);
xpiped_pair(toCgi);
- pid = vfork();
- if (pid < 0) {
+ G.cgi_pid = vfork();
+ if (G.cgi_pid < 0) {
if (VERBOSE_1)
bb_simple_perror_msg("vfork");
log_and_exit();
}
- if (pid == 0) {
+ if (G.cgi_pid == 0) {
/* Child process */
char *argv[3];
+ /* -K SECS kills entire process group: set up one */
+ bb_setpgrp();
+
/* NB: close _first_, then move fds! */
close(toCgi.wr);
close(fromCgi.rd);
@@ -1776,7 +1809,10 @@ static void send_cgi_and_exit(
error_execing_cgi:
/* send to stdout
* (we are CGI here, our stdout is pumped to the net) */
- send_headers_and_exit(HTTP_NOT_FOUND);
+ //send_headers_and_exit(HTTP_NOT_FOUND);
+ //^^^ WRONG, this logs "closed", then the parent logs it again
+ send_headers(HTTP_NOT_FOUND);
+ _exit_FAILURE();
} /* end child */
/* Parent process */
@@ -1784,7 +1820,7 @@ static void send_cgi_and_exit(
/* Pump data */
close(fromCgi.wr);
close(toCgi.rd);
- cgi_io_loop_and_exit(fromCgi.rd, toCgi.wr, post_len);
+ cgi_io_loop_and_exit(fromCgi.rd, toCgi.wr);
}
#endif /* FEATURE_HTTPD_CGI */
@@ -1802,7 +1838,8 @@ static NOINLINE void send_file_and_exit(const char *url, int what)
int fd;
ssize_t count;
- if (content_gzip) {
+#if ENABLE_FEATURE_HTTPD_GZIP
+ if (accept_gzip) {
/* does <url>.gz exist? Then use it instead */
char *gzurl = xasprintf("%s.gz", url);
fd = open(gzurl, O_RDONLY);
@@ -1812,11 +1849,13 @@ static NOINLINE void send_file_and_exit(const char *url, int what)
fstat(fd, &sb);
file_size = sb.st_size;
last_mod = sb.st_mtime;
+ content_gzip = 1;
} else {
- IF_FEATURE_HTTPD_GZIP(content_gzip = 0;)
fd = open(url, O_RDONLY);
}
- } else {
+ } else
+#endif
+ {
fd = open(url, O_RDONLY);
/* file_size and last_mod are already populated */
}
@@ -2240,16 +2279,27 @@ static Htaccess_Proxy *find_proxy_entry(const char *url)
/*
* Handle timeouts
*/
-static void send_REQUEST_TIMEOUT_and_exit(int sig) NORETURN;
-static void send_REQUEST_TIMEOUT_and_exit(int sig UNUSED_PARAM)
+static void sigalrm_handler(int sig) NORETURN;
+static void sigalrm_handler(int sig UNUSED_PARAM)
{
- /* timed out reading headers */
- send_headers_and_exit(HTTP_REQUEST_TIMEOUT);
-//If we'd use alarm() for write timeouts too:
-// /* writing timed out: exit without writing anything */
-// if (VERBOSE_3)
-// bb_simple_error_msg("write timeout");
-// _exit_SUCCESS();
+ /* timed out reading headers, POSTDATA, or CGI runs too long */
+ int response = HTTP_REQUEST_TIMEOUT;
+#if ENABLE_FEATURE_HTTPD_CGI
+ if (G.cgi_pid > 0) {
+ if (kill(-G.cgi_pid, SIGTERM) == 0) {
+ bb_error_msg("kill cgi:%d", G.cgi_pid);
+ sleep1();
+ kill(-G.cgi_pid, SIGKILL);
+ }
+ /* Browsers were seen retrying if got HTTP_REQUEST_TIMEOUT,
+ * we don't want that for the case of stuck CGI. */
+ response = HTTP_INTERNAL_SERVER_ERROR;
+ }
+#endif
+ IF_FEATURE_HTTPD_CGI(if (!G.cgi_output))
+ send_headers_and_exit(response);
+ /* else: we already have some output, do not garble it with HTTP response */
+ log_and_exit();
}
static void prepare_write_timeout(void)
@@ -2286,7 +2336,6 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr)
static const char request_HEAD[] ALIGN1 = "HEAD";
#if ENABLE_FEATURE_HTTPD_CGI
static const char request_POST[] ALIGN1 = "POST";
- unsigned long POST_length;
enum CGI_type {
CGI_NONE = 0,
CGI_NORMAL,
@@ -2397,7 +2446,8 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr)
/* The above also allows http2 which starts with a fixed
* "PRI * HTTP/2.0" line
*/
- cgi_io_loop_and_exit(proxy_fd, proxy_fd, /*max POST length:*/ INT_MAX);
+ G.POST_len = INT_MAX; /* hack */
+ cgi_io_loop_and_exit(proxy_fd, proxy_fd);
}
#endif
/* We don't support http2 "*" URI, enforce "/URI" form */
@@ -2555,7 +2605,6 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr)
#if ENABLE_FEATURE_HTTPD_CGI
total_headers_len = 0;
- POST_length = 0;
#endif
/* Read until blank line */
@@ -2577,9 +2626,9 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr)
if (!tptr[0])
send_headers_and_exit(HTTP_BAD_REQUEST);
/* not using strtoul: it ignores leading minus! */
- POST_length = bb_strtou(tptr, NULL, 10);
- /* length is "ulong", but we need to pass it to int later */
- if (errno || POST_length > INT_MAX)
+ G.POST_len = (int)bb_strtou(tptr, NULL, 10);
+ /* we need to pass it to int later */
+ if (errno || G.POST_len < 0)
send_headers_and_exit(HTTP_BAD_REQUEST);
continue;
}
@@ -2630,7 +2679,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr)
// || s[-1] == ','
// || s[-1] == ':'
//) {
- content_gzip = 1;
+ accept_gzip = 1;
//}
}
continue;
@@ -2698,7 +2747,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr)
send_cgi_and_exit(
(cgi_type == CGI_INDEX) ? "/cgi-bin/index.cgi"
/*CGI_NORMAL or CGI_INTERPRETER*/ : urlcopy,
- urlcopy, prequest, POST_length
+ urlcopy, prequest
);
}
#endif
@@ -2785,7 +2834,7 @@ static void mini_httpd(int server_socket)
{
int countdown;
- signal(SIGALRM, send_REQUEST_TIMEOUT_and_exit);
+ signal(SIGALRM, sigalrm_handler);
xmove_fd(server_socket, 0);
/* NB: it's best to not use xfuncs in this loop before fork().
@@ -2843,7 +2892,7 @@ static void mini_httpd_nommu(int server_socket, int argc, char **argv)
argv_copy[1] = (char*)"-i";
memcpy(&argv_copy[2], &argv[1], argc * sizeof(argv[0]));
- /*signal(SIGALRM, send_REQUEST_TIMEOUT_and_exit);*/
+ /*signal(SIGALRM, sigalrm_handler);*/
/* ^^^ WRONG. mini_httpd_inetd() does this */
xmove_fd(server_socket, 0);
@@ -2895,7 +2944,7 @@ static void mini_httpd_inetd(void)
{
len_and_sockaddr fromAddr;
- signal(SIGALRM, send_REQUEST_TIMEOUT_and_exit);
+ signal(SIGALRM, sigalrm_handler);
memset(&fromAddr, 0, sizeof(fromAddr));
fromAddr.len = LSA_SIZEOF_SA;
@@ -2923,6 +2972,7 @@ enum {
IF_FEATURE_HTTPD_SETUID( u_opt_setuid ,)
p_opt_port ,
M_opt_maxconn ,
+ K_opt_killcgi ,
i_opt_inetd ,
f_opt_foreground,
v_opt_verbose ,
@@ -2974,7 +3024,7 @@ int httpd_main(int argc UNUSED_PARAM, char **argv)
IF_FEATURE_HTTPD_BASIC_AUTH("r:")
IF_FEATURE_HTTPD_AUTH_MD5("m:")
IF_FEATURE_HTTPD_SETUID("u:")
- "p:M:+ifv"
+ "p:M:+K:+ifv"
"\0"
/* -v counts, -i implies -f */
"vv:if",
@@ -2985,6 +3035,7 @@ int httpd_main(int argc UNUSED_PARAM, char **argv)
IF_FEATURE_HTTPD_SETUID(, &s_ugid)
, &bind_addr_or_port
, &G.conn_limit
+ , IF_FEATURE_HTTPD_CGI(&G.cgi_kill_timeout) IF_NOT_FEATURE_HTTPD_CGI(NULL)
, &verbose
);
if (opt & OPT_DECODE_URL) {