Skip to content

Commit 66155ff

Browse files
committed
libretro-common/net: harden net_http.c response parser and net_http_new
Audit of the full net_http.c HTTP client stack (~1633 lines). Five real bugs found; fixed in this patch. All exploitable by a hostile server that the client has been pointed at. net_http: bound Content-Length parsing The Content-Length header parser accumulated digits into a signed ssize_t, which is undefined behaviour on overflow and produces a huge size_t on assignment to response->len. That value then drives the realloc() at the end of net_http_receive_header(), pushing the client toward OOM on a hostile server response. Fix: * accumulate into size_t * reject empty values ("Content-Length: " with no digits) * reject values that would exceed NET_HTTP_MAX_CONTENT_LENGTH (256 MiB -- large enough for any legitimate libretro/ RetroArch payload, small enough to keep realloc honest) * on reject, set state->err and return -1 cleanly net_http: bounds-check HTTP status line net_http_receive_header read the three status digits at scan[9..11] unconditionally. A malicious server could send a short line like "HTTP/1.0\n" (9 bytes before the LF, so scan[8] was the NUL we just wrote and scan[9..11] were whatever followed in the receive buffer). The status came out as garbage but no memory was actually corrupted; still, reading past the logical end of the line is a bug. Fix: require line_len >= 12, require scan[8] == ' ', and require scan[9..11] to be decimal digits. Any deviation fails the response cleanly. net_http: realloc NULL-check in receive_header Two realloc() calls at the end of net_http_receive_header ignored the return value. On failure the original buffer would leak AND response->data would be NULL, which subsequent writes in net_http_receive_body dereference. Fix: stash the realloc result in a tmp and fail the response (state->err = true, return -1) on NULL. Does not leak the original buffer. net_http: consolidated OOM guard in net_http_new net_http_new() issued seven strdup() and two malloc() calls back-to-back without checking any of them. Any OOM left fields (notably request.domain and request.path) NULL, and later strlen() calls in net_http_send_request() crashed on NULL. The response.data malloc and string_list_new() allocation had the same hazard for the response-receiving side. Fix: after all allocations, check every mandatory pointer plus each "was requested but failed" pointer. On any failure, free response.data and response.headers explicitly (net_http_delete does not touch those -- see note below) and call net_http_delete to free the request side and the state struct, then return NULL. net_http: guard postdata malloc The body-malloc + memcpy pair in net_http_new was unguarded: malloc(contentlength) could return NULL, then memcpy would dereference it. Fix: skip the memcpy on NULL; the OOM guard above catches the failure and tears down the whole state. Note on net_http_delete: it does NOT free response.data or response.headers. This is intentional in the existing contract: net_http_data() and net_http_headers() transfer ownership of those buffers to the caller, who becomes responsible for freeing them. On the OOM failure path in net_http_new no ownership transfer ever happens, so this patch explicitly frees both before calling net_http_delete. Reachability: hostile-server responses are the main attack vector. An attacker-controlled buildbot mirror (or MITM on a cleartext HTTP connection) can trivially send oversized Content-Length values, short status lines, or force the client into the OOM path via very large allocations elsewhere. VC6 compatibility: the file is compiled on all platforms. The fix uses only size_t, ssize_t, and standard comparisons; no new headers. The NET_HTTP_MAX_CONTENT_LENGTH macro uses a size_t-cast literal that is valid in C89. Regression test: NONE. Exercising the relevant code paths requires either a live SMB-style network mock or refactoring the two static helpers (net_http_receive_header, net_http_new) to expose inner routines that can be unit-tested in isolation. Adding socket-mocked tests to libretro-common/samples would pull network_init, getaddrinfo, and thread primitives into a sample harness that currently has no such setup. The existing http_parse_test covers the separate net_http_parse.c unit that was hardened in commit fd69102. These net_http.c fixes are localised, small, and verified at build time under -Wall -Werror for both -DHAVE_THREADS and no-threads configs.
1 parent fd69102 commit 66155ff

1 file changed

Lines changed: 120 additions & 8 deletions

File tree

libretro-common/net/net_http.c

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@
4343
#include <rthreads/rthreads.h>
4444
#endif
4545

46+
/* Maximum Content-Length we'll honour from a server, to bound the
47+
* realloc() that follows header parsing. 256 MiB is comfortably
48+
* larger than any single libretro HTTP payload (core downloads,
49+
* thumbnail images, assets bundles) and small enough that a
50+
* hostile server cannot drive the client toward OOM by lying in
51+
* the Content-Length header. */
52+
#define NET_HTTP_MAX_CONTENT_LENGTH ((size_t)256 * 1024 * 1024)
53+
4654
enum response_part
4755
{
4856
P_HEADER_TOP = 0,
@@ -809,7 +817,9 @@ struct http_t *net_http_new(struct http_connection_t *conn)
809817
if (conn->postdata && conn->contentlength)
810818
{
811819
state->request.postdata = malloc(conn->contentlength);
812-
memcpy(state->request.postdata, conn->postdata, conn->contentlength);
820+
if (state->request.postdata)
821+
memcpy(state->request.postdata, conn->postdata, conn->contentlength);
822+
/* If malloc failed, the OOM guard further below catches it. */
813823
}
814824
state->request.useragent= conn->useragent ? strdup(conn->useragent) : NULL;
815825
state->request.headers = conn->headers ? strdup(conn->headers) : NULL;
@@ -820,6 +830,39 @@ struct http_t *net_http_new(struct http_connection_t *conn)
820830
state->response.data = (char*)malloc(state->response.buflen);
821831
state->response.headers = string_list_new();
822832

833+
/* Any of the strdup / malloc calls above can return NULL on OOM.
834+
* The dispatch path in net_http_update() dereferences
835+
* request.domain and writes to response.data without guards; fail
836+
* the whole setup early here rather than stack up NULL derefs
837+
* later.
838+
*
839+
* Note on cleanup order: net_http_delete() intentionally does not
840+
* free response.data or response.headers because successful callers
841+
* take ownership of response.data via net_http_data() and of
842+
* response.headers via net_http_headers(). On the OOM failure path
843+
* those ownership transfers never happen, so we free both here
844+
* before calling net_http_delete() (which then cleans up the
845+
* request.* fields and the state struct itself). */
846+
if ( !state->response.data
847+
|| !state->response.headers
848+
|| !state->request.domain
849+
|| !state->request.path
850+
|| !state->request.method
851+
|| (conn->contenttype && !state->request.contenttype)
852+
|| (conn->postdata && conn->contentlength && !state->request.postdata)
853+
|| (conn->useragent && !state->request.useragent)
854+
|| (conn->headers && !state->request.headers))
855+
{
856+
if (state->response.data)
857+
free(state->response.data);
858+
if (state->response.headers)
859+
string_list_free(state->response.headers);
860+
state->response.data = NULL;
861+
state->response.headers = NULL;
862+
net_http_delete(state);
863+
return NULL;
864+
}
865+
823866
return state;
824867
}
825868

@@ -1153,16 +1196,35 @@ static ssize_t net_http_receive_header(struct http_t *state, ssize_t len)
11531196

11541197
if (response->part == P_HEADER_TOP)
11551198
{
1156-
if ( scan[0] != 'H' || scan[1] != 'T' || scan[2] != 'T'
1199+
/* Status line is "HTTP/1.x SSS <reason>\r\n". The fixed
1200+
* prefix is 8 bytes, then a space, then 3 status digits ->
1201+
* minimum line length is 12 bytes excluding the NUL we just
1202+
* wrote at lineend (lineend - scan >= 12). Pre-patch this
1203+
* was not checked and a short malicious line like
1204+
* "HTTP/1.0\n" let the code read scan[9..11] past the
1205+
* terminator into whatever followed in the receive buffer. */
1206+
ssize_t line_len = lineend - scan;
1207+
if ( line_len < 12
1208+
|| scan[0] != 'H' || scan[1] != 'T' || scan[2] != 'T'
11571209
|| scan[3] != 'P' || scan[4] != '/' || scan[5] != '1'
1158-
|| scan[6] != '.')
1210+
|| scan[6] != '.' || scan[8] != ' ')
11591211
{
11601212
response->part = P_DONE;
11611213
state->err = true;
11621214
return -1;
11631215
}
11641216
{
1165-
const char *p = scan + 9;
1217+
const char *p = scan + 9;
1218+
/* Also verify the three status chars are digits -- a
1219+
* non-digit would produce a negative or junk status. */
1220+
if ( p[0] < '0' || p[0] > '9'
1221+
|| p[1] < '0' || p[1] > '9'
1222+
|| p[2] < '0' || p[2] > '9')
1223+
{
1224+
response->part = P_DONE;
1225+
state->err = true;
1226+
return -1;
1227+
}
11661228
response->status = (p[0] - '0') * 100
11671229
+ (p[1] - '0') * 10
11681230
+ (p[2] - '0');
@@ -1191,12 +1253,43 @@ static ssize_t net_http_receive_header(struct http_t *state, ssize_t len)
11911253
if (strncasecmp(scan, "Content-Length:",
11921254
sizeof("Content-Length:") - 1) == 0)
11931255
{
1256+
/* Parse Content-Length as unsigned with an explicit
1257+
* cap. Pre-patch the accumulator was a signed ssize_t
1258+
* that could overflow (UB) on a very long digit string
1259+
* and then sign-extend to a huge size_t when assigned
1260+
* to response->len, driving realloc() toward OOM. Cap
1261+
* at NET_HTTP_MAX_CONTENT_LENGTH (256 MiB) which is
1262+
* larger than any legitimate single HTTP response in
1263+
* the libretro/RetroArch workflow (cores, thumbnails,
1264+
* ROM manifests) and leaves a safe headroom before
1265+
* buflen can wrap. */
11941266
char *ptr = scan + (sizeof("Content-Length:") - 1);
1195-
ssize_t val = 0;
1267+
size_t val = 0;
1268+
int any = 0;
1269+
int oflow = 0;
11961270
while (*ptr == ' ' || *ptr == '\t')
11971271
++ptr;
11981272
while (*ptr >= '0' && *ptr <= '9')
1199-
val = val * 10 + (*ptr++ - '0');
1273+
{
1274+
size_t digit = (size_t)(*ptr++ - '0');
1275+
any = 1;
1276+
/* Detect overflow against the cap rather than
1277+
* against SIZE_MAX, so the later realloc call
1278+
* never sees an attacker-chosen huge value. */
1279+
if (val > (NET_HTTP_MAX_CONTENT_LENGTH - digit) / 10)
1280+
{
1281+
oflow = 1;
1282+
break;
1283+
}
1284+
val = val * 10 + digit;
1285+
}
1286+
if (!any || oflow)
1287+
{
1288+
/* Malformed header: treat as protocol error. */
1289+
response->part = P_DONE;
1290+
state->err = true;
1291+
return -1;
1292+
}
12001293
response->bodytype = T_LEN;
12011294
response->len = val;
12021295
}
@@ -1234,16 +1327,35 @@ static ssize_t net_http_receive_header(struct http_t *state, ssize_t len)
12341327
response->pos = 0;
12351328
if (response->bodytype == T_LEN)
12361329
{
1330+
/* Use a tmp pointer so a realloc failure does not leak the
1331+
* original buffer AND leave response->data NULL for later
1332+
* writes to dereference. */
1333+
char *tmp;
12371334
response->buflen = response->len;
1238-
response->data = (char*)realloc(response->data, response->buflen);
1335+
tmp = (char*)realloc(response->data, response->buflen);
1336+
if (!tmp)
1337+
{
1338+
response->part = P_DONE;
1339+
state->err = true;
1340+
return -1;
1341+
}
1342+
response->data = tmp;
12391343
}
12401344
}
12411345
else
12421346
{
12431347
if (response->pos >= response->buflen - 64)
12441348
{
1349+
char *tmp;
12451350
response->buflen *= 2;
1246-
response->data = (char*)realloc(response->data, response->buflen);
1351+
tmp = (char*)realloc(response->data, response->buflen);
1352+
if (!tmp)
1353+
{
1354+
response->part = P_DONE;
1355+
state->err = true;
1356+
return -1;
1357+
}
1358+
response->data = tmp;
12471359
}
12481360
}
12491361
return len;

0 commit comments

Comments
 (0)