From e98479be3f140d42b808402e359f55684d908b97 Mon Sep 17 00:00:00 2001 From: anthony Date: Mon, 4 Nov 2024 16:35:55 -0500 Subject: [PATCH] http server fuzzer --- src/ballet/http/fuzz_httpserver.c | 654 ++++++++++++++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 src/ballet/http/fuzz_httpserver.c diff --git a/src/ballet/http/fuzz_httpserver.c b/src/ballet/http/fuzz_httpserver.c new file mode 100644 index 0000000000..5cf6bcca81 --- /dev/null +++ b/src/ballet/http/fuzz_httpserver.c @@ -0,0 +1,654 @@ +#if !FD_HAS_HOSTED +#error "This target requires FD_HAS_HOSTED" +#endif + +#include +#include +#include +#include +#include +#include +#include + #include + +#include "../../util/fd_util.h" +#include "../../util/sanitize/fd_fuzz.h" +#include "fd_http_server_private.h" +#include "fd_http_server.h" + +#define PORT 8080 + +#define FD_HTTP_SERVER_GUI_MAX_CONNS 4 +#define FD_HTTP_SERVER_GUI_MAX_REQUEST_LEN 2048 +#define FD_HTTP_SERVER_GUI_MAX_WS_CONNS 4 +#define FD_HTTP_SERVER_GUI_MAX_WS_RECV_FRAME_LEN 2048 +#define FD_HTTP_SERVER_GUI_MAX_WS_SEND_FRAME_CNT 8192 +#define FD_HTTP_SERVER_GUI_OUTGOING_BUFFER_SZ (1UL<<28UL) /* 256MiB reserved for buffering GUI websockets */ + +const fd_http_server_params_t PARAMS = { + .max_connection_cnt = FD_HTTP_SERVER_GUI_MAX_CONNS, + .max_ws_connection_cnt = FD_HTTP_SERVER_GUI_MAX_WS_CONNS, + .max_request_len = FD_HTTP_SERVER_GUI_MAX_REQUEST_LEN, + .max_ws_recv_frame_len = FD_HTTP_SERVER_GUI_MAX_WS_RECV_FRAME_LEN, + .max_ws_send_frame_cnt = FD_HTTP_SERVER_GUI_MAX_WS_SEND_FRAME_CNT, + .outgoing_buffer_sz = FD_HTTP_SERVER_GUI_OUTGOING_BUFFER_SZ, +}; + +/* + Wraps libfuzzer input and regress to rand() when everything is consumed. + Assume that srand() was correctly done prior. + */ +struct Unstructured { + uchar const *data; + ulong size; + ulong used; +}; + +uchar rand_uchar(struct Unstructured *u) __attribute__((no_sanitize("alignment"))) { + if (sizeof(uchar) + u->used < u->size) { + uchar v = *(uchar *)(u->data + u->used); + u->used += sizeof(uchar); + return v; + } + return (uchar) rand(); +} + +uint rand_uint(struct Unstructured *u) __attribute__((no_sanitize("alignment"))) { + if (sizeof(uint) + u->used < u->size) { + uint v = *(uint *)(u->data + u->used); + u->used += sizeof(uint); + return v; + } + return (uint) rand(); +} + +ulong rand_ulong(struct Unstructured *u) __attribute__((no_sanitize("alignment"))) { + if (sizeof(ulong) + u->used < u->size) { + ulong v = *(ulong *)(u->data + u->used); + u->used += sizeof(ulong); + return v; + } + return ((ulong)rand()) << 32 | ((ulong)rand()); +} + +void rand_bytes(struct Unstructured *u, size_t len, uchar *p) __attribute__((no_sanitize("alignment"))) { + if (len + u->used < u->size) { + memcpy(p, u->data + u->used, len); + u->used += len; + } else { + for (ulong i = 0; i < len; ++i) { + p[i] = (uchar) rand(); + } + } +} + +static fd_http_server_t *http_server = NULL; +static int clients_fd[FD_HTTP_SERVER_GUI_MAX_CONNS * 2] = {-1}; +static char clients_ws_fd[FD_HTTP_SERVER_GUI_MAX_CONNS * 2] = {0}; +static uint clients_fd_cnt = 0; + +void reset_clients_fd(void) { + clients_fd_cnt = 0; + for (ulong i = 0; i < FD_HTTP_SERVER_GUI_MAX_CONNS * 2; ++i) { + clients_fd[i] = -1; + clients_ws_fd[i] = 0; + } +} + +int +LLVMFuzzerInitialize( int * argc, + char *** argv ) { + /* Set up shell without signal handlers */ + putenv( "FD_LOG_BACKTRACE=0" ); + fd_boot( argc, argv ); + atexit( fd_halt ); + + /* Disable parsing error logging */ + fd_log_level_stderr_set(4); + + reset_clients_fd(); + + return 0; +} + +void open_callback( ulong conn_id, int sockfd, void * ctx ) { + (void)conn_id; + (void)sockfd; + (void)ctx; +} + +void close_callback( ulong conn_id, int reason, void * ctx ) { + (void)conn_id; + (void)reason; + (void)ctx; +} + +typedef struct { + uint32_t state; // Internal state of the generator +} Xorshift; + +void xorshift_init(Xorshift* x, uint32_t seed) { + x->state = seed ? seed : 1; // Avoid zero state +} + +uint32_t xorshift_next(Xorshift* x) { + uint32_t s = x->state; + s ^= s << 13; // Shift left and xor + s ^= s >> 17; // Shift right and xor + s ^= s << 5; // Shift left and xor + x->state = s; // Update state + return s; // Return the random number +} + +static Xorshift poll_rng; + +// in callbacks try to execute some actions too (maybe not all) +fd_http_server_response_t request_callback( fd_http_server_request_t const * request ) { + (void)request; + + fd_http_server_response_t resp; + memset(&resp, 0, sizeof(fd_http_server_response_t)); + + switch(xorshift_next(&poll_rng) % 7) { + case 0: + { + resp.status = 200; + resp.upgrade_websocket = xorshift_next(&poll_rng) % 2; + } + break; + case 1: + { + resp.status = 204; + } + break; + case 2: + { + resp.status = 400; + } + break; + case 3: + { + resp.status = 404; + } + break; + case 4: + { + resp.status = 405; + } + break; + case 5: + { + resp.status = 500; + } + break; + default: + { + resp.status = xorshift_next(&poll_rng); + } + break; + } + + if (xorshift_next(&poll_rng) % 2 == 0) { + resp.content_type = "Any content_type"; + } + + if (xorshift_next(&poll_rng) % 2 == 0) { + resp.cache_control = "Any cache_control"; + } + + if (xorshift_next(&poll_rng) % 2 == 0) { + resp.content_encoding = "Any content_encoding"; + } + + if (xorshift_next(&poll_rng) % 2 == 0) { + resp.access_control_allow_origin = "Any access_control_allow_origin"; + } + + if (xorshift_next(&poll_rng) % 2 == 0) { + resp.access_control_allow_methods = "Any access_control_allow_methods"; + } + + if (xorshift_next(&poll_rng) % 2 == 0) { + resp.access_control_allow_headers = "Any access_control_allow_headers"; + } + + if (xorshift_next(&poll_rng) % 2 == 0) { + resp.access_control_max_age = ((ulong)(&poll_rng) << 32) | (ulong)xorshift_next(&poll_rng); + } + + if (xorshift_next(&poll_rng) % 2 == 0) { + resp.static_body = (const uchar *) "resp_body"; + resp.static_body_len = 9; + } + + // 1/100 + if (request->headers.upgrade_websocket && (xorshift_next(&poll_rng) % 100) > 0) { + resp.status = 200; + resp.upgrade_websocket = 1; + } + + return resp; +} + +void ws_open_callback( ulong ws_conn_id, void * ctx ) { + (void) ws_conn_id; + (void) ctx; +} + +void ws_close_callback( ulong ws_conn_id, int reason, void * ctx ) { + (void) ws_conn_id; + (void) reason; + (void) ctx; +} + +void ws_message_callback( ulong ws_conn_id, uchar const * data, ulong data_len, void * ctx ) { + (void) ws_conn_id; + (void) data; + (void) data_len; + (void) ctx; +} + +void close_reset_clients_fd(fd_http_server_t * http) { + for (ulong i = 0; i < clients_fd_cnt; ++i) { + if (clients_fd[i] != -1) { + close(clients_fd[i]); + clients_fd[i] = -1; + clients_ws_fd[i] = 0; + } + } + clients_fd_cnt = 0; + + for (ulong conn_idx = 0; conn_idx < (PARAMS.max_connection_cnt + PARAMS.max_ws_connection_cnt); ++conn_idx) { + if (http->pollfds[ conn_idx ].fd != -1) { + close(http->pollfds[ conn_idx ].fd); + } + } +} + +int *reserve_client_fd(void) { + if (clients_fd_cnt >= (FD_HTTP_SERVER_GUI_MAX_CONNS * 2)) { + return NULL; + } + return &clients_fd[clients_fd_cnt++]; +} + +int build_http_header(struct Unstructured *u, char *buf, int max_len, int *use_web_socket) __attribute__((no_sanitize("alignment"))) { + if (max_len <= 0) return 0; + + int used = 0; + + switch (rand_uchar(u) % 5) { + // Content-type + case 0: + { + const char *CONTENT_TYPES[] = {"text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded", "multipart/form-data", "application/octet-stream", "image/png", "image/jpeg", "audio/mpeg", "video/mp4", "application/pdf"}; + const char *CHARSET = "; charset=UTF-8"; + const char *content_type = CONTENT_TYPES[rand_uchar(u) % 12]; + if (rand_uchar(u) % 2 == 0) { + used = snprintf(buf, max_len, "Content-Type: %s\r\n", content_type); + } else { + used = snprintf(buf, max_len, "Content-Type: %s%s\r\n", content_type, CHARSET); + } + } + break; + // Accept-encoding + case 1: + { + const char *ACCEPT_ENCODINGS[] = {"gzip", "compress", "deflate", "br", "identity", "*"}; + char accept_encoding[64]; + memset(accept_encoding, 0, 64); + char *cur_encoding_pos = &accept_encoding[0]; + int rem = 64; + + for (int i = 0; i < (1 + (rand_uchar(u) % 6)); ++i) { + int size = snprintf(cur_encoding_pos, rem, "%s, ", ACCEPT_ENCODINGS[rand_uchar(u) % 6]); + cur_encoding_pos += size; + rem -= size; + } + + accept_encoding[strlen(accept_encoding)-2] = 0; // remove ", " + + used = snprintf(buf, max_len, "Accept-Encoding: %s\r\n", accept_encoding); + } + break; + // websocket + case 2: + { + used = snprintf(buf, max_len, "Upgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n"); + *use_web_socket = 1; + } + break; + } + + if (used >= max_len) { + buf[0] = 0; + used = 0; + } + + return used; +} + +void build_http_req(struct Unstructured *u, uchar *buf, int *len, int *use_websocket) __attribute__((no_sanitize("alignment"))) { + int max_size = *len; + int size = 0; + memset((char *)buf, 0, (size_t)*len); + + const char *METHODS[] = {"GET", "POST", "OPTIONS"}; + const char *method = METHODS[rand_uchar(u) % 3]; + int is_post = strlen(method) == 4 && strcmp(method, "POST") == 0 ? 1 : 0; + const char *uri = "/home"; + + const char *version = "HTTP/1.1"; + + char headers[256]; + memset(headers, 0, 256); + uint n_headers = 0; + char *cur_header_pos = &headers[0]; + int rem = 256; + if (is_post) { + int used = snprintf(cur_header_pos, rem, "Content-Length: 4\r\n"); + if (used >= rem) return; + cur_header_pos += used; + rem -= used; + n_headers++; + } + + while (n_headers < (rand_uint(u) % 32)) { + int used = build_http_header(u, cur_header_pos, rem, use_websocket); + cur_header_pos += used; + rem -= used; + n_headers++; + } + + // max_size includes the NULL byte + size = (uchar) snprintf((char *)buf, max_size, "%s %s %s\r\n%s\r\n", method, uri, version, headers); + if (size >= max_size) return; + + if (is_post) { + int _size = snprintf((char *)buf + size, max_size-size, "body"); + if (_size <= max_size-size) return; + size += _size; + } + + *len = size; // excludes the NULL byte +} + +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-------+-+-------------+-------------------------------+ +// |F|R|R|R| opcode|M| Payload len | Extended payload length | +// |I|S|S|S| (4) |A| (7) | (16/64) | +// |N|V|V|V| |S| | (if payload len==126/127) | +// | |1|2|3| |K| | | +// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + +// | Extended payload length continued, if payload len == 127 | +// + - - - - - - - - - - - - - - - +-------------------------------+ +// | |Masking-key, if MASK set to 1 | +// +-------------------------------+-------------------------------+ +// | Masking-key (continued) | Payload Data | +// +-------------------------------- - - - - - - - - - - - - - - - + +// : Payload Data continued ... : +// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// | Payload Data continued ... | +// +---------------------------------------------------------------+ + + +// at least 128 bytes given +void build_ws_req(struct Unstructured *u, uchar *buf, int *len) __attribute__((no_sanitize("alignment"))) { + uchar *cur_pos = buf; + + // offset 0 + const uchar OPCODES[] = {0x0, 0x1, 0x2, 0x8, 0x9, 0xA}; + uchar opcode = OPCODES[rand_uchar(u) % 6] & 0x0F; + *cur_pos = opcode; + if (rand_uchar(u) % 2 == 0) { + *cur_pos |= (1 << 7); // is_fin_set + } + + ++cur_pos; // offset 1 + + uint payload_len = (uchar) rand_uint(u); + if (opcode == 0x8 || opcode == 0x9 || opcode == 0xA || *len < 140) { + payload_len %= 126; + } else { + payload_len %= 256; + } + + if (payload_len < 126) { + *cur_pos = (uchar) payload_len; + } else if (rand_uchar(u) % 2 == 0) { + *cur_pos = 126; + } else { + *cur_pos = 127; + } + + int payload_len_choice = *cur_pos; + + *cur_pos |= (1 << 7); // is_mask_set + + ++cur_pos; + if (payload_len_choice == 126) { + *(ushort *)cur_pos = (ushort) payload_len; + cur_pos += sizeof(ushort); + } else if (payload_len_choice == 127) { + *(ulong *)cur_pos = (ulong) payload_len; + cur_pos += sizeof(ulong); + } + + // offset: mask + *(ulong *)cur_pos = 0; // no mask + cur_pos += sizeof(ulong); + + // payload + for (uint i = 0; i < payload_len; ++i) { + cur_pos[i] = rand_uchar(u); + } + + *len = (int) (cur_pos - buf); +} + +int +fd_http_server_ws_send( fd_http_server_t * http, + ulong ws_conn_id ); + +int +fd_http_server_ws_broadcast( fd_http_server_t * http ); + +void +fd_http_server_evict_until( fd_http_server_t * http, + ulong off ); + +void random_api_call(Xorshift *u) { + switch(xorshift_next(u) % 3) { + case 0: + { + ulong pos = xorshift_next(u) % (FD_HTTP_SERVER_GUI_MAX_WS_CONNS); + if (http_server->pollfds[ pos + http_server->max_conns ].fd != -1) + fd_http_server_ws_send(http_server, pos); + } + break; + case 1: + { + fd_http_server_ws_broadcast(http_server); + } + break; + case 2: + { + fd_http_server_evict_until(http_server, xorshift_next(u)); + } + break; + } +} + +static ulong stem_iters = 0; +static int stop = 0; +void* stem_thread(void* arg) { + stem_iters = 0; + Xorshift *stem_prng = (Xorshift *)arg; + (void) stem_prng; + + while (1) { + for (uint i = 0; i < xorshift_next(stem_prng) % 3; ++i) { + random_api_call(stem_prng); + } + + fd_http_server_poll(http_server); + + for (uint i = 0; i < xorshift_next(stem_prng) % 3; ++i) { + random_api_call(stem_prng); + } + + ++stem_iters; + + if (stop) break; + sched_yield(); + } + return NULL; +} + +enum Action { + HttpOpen = 0, + Close, + Send, + ActionEnd, +}; + +void do_action(struct Unstructured *u) { + switch(rand_uchar(u) % ActionEnd) { + case HttpOpen: + { + int *client_fd = reserve_client_fd(); + if (!client_fd) return; + + struct sockaddr_in server_addr; + *client_fd = socket(AF_INET, SOCK_STREAM, 0); + + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(PORT); + + if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) != 1) { + close(*client_fd); + *client_fd = -1; + clients_fd_cnt--; + return; + } + + if (connect(*client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { + close(*client_fd); + *client_fd = -1; + clients_fd_cnt--; + } + } + break; + case Close: + { + if (clients_fd_cnt > 0) { + uchar pos = rand_uchar(u) % clients_fd_cnt; + if (clients_fd[pos] != -1) { + close(clients_fd[pos]); + clients_fd[pos] = -1; + clients_ws_fd[pos] = 0; + } + } + } + break; + case Send: + { + if (clients_fd_cnt > 0) { + int len = 1024; + uchar buf[1024]; + int use_websocket = 0; + uchar pos = rand_uchar(u) % clients_fd_cnt; + + if (clients_fd[pos] != -1 && clients_ws_fd[pos] == 0) { + build_http_req(u, buf, &len, &use_websocket); + if (rand_uchar(u) % 5 == 0) { + LLVMFuzzerMutate(buf, (ulong)len, (ulong)len); + } + send(clients_fd[pos], buf, (size_t)len, MSG_NOSIGNAL); + if (use_websocket) { + clients_ws_fd[pos] = 1; + } + } + + else if (clients_fd[pos] != -1 && clients_ws_fd[pos] == 1) { + build_ws_req(u, buf, &len); + + // add up to 2 messages + for (ulong i = 0; i < rand_uchar(u) % 3 && len < 1024; ++i) { + int len2 = 1024 - len; + build_ws_req(u, buf + len, &len2); + len += len2; + } + + if (rand_uchar(u) % 5 == 0) { + LLVMFuzzerMutate(buf, (ulong)len, (ulong)len); + } + + send(clients_fd[pos], buf, (size_t)len, MSG_NOSIGNAL); + } + } + } + break; + } +} + +int +LLVMFuzzerTestOneInput( uchar const * data, + ulong size ) { + + if (size >= sizeof(int)) { + const fd_valloc_t valloc = fd_libc_alloc_virtual(); + struct Unstructured u = { + .data = data, + .size = size, + .used = 0 + }; + pthread_t thread; + + srand(rand_uint(&u)); + + // init + void *shmem = fd_valloc_malloc(valloc, fd_http_server_align(), fd_http_server_footprint( PARAMS )); + + fd_http_server_callbacks_t gui_callbacks = { + .open = open_callback, + .close = close_callback, + .request = request_callback, + .ws_open = ws_open_callback, + .ws_close = ws_close_callback, + .ws_message = ws_message_callback, + }; + + http_server = fd_http_server_join( fd_http_server_new( shmem, PARAMS, gui_callbacks, NULL ) ); + fd_http_server_listen( http_server, PORT ); + + xorshift_init(&poll_rng, rand_uint(&u)); + + Xorshift stem_prng; + xorshift_init(&stem_prng, rand_uint(&u)); + stop = 0; + pthread_create(&thread, NULL, stem_thread, &stem_prng); + + uchar n_actions = rand_uchar(&u) % 32; + for (uchar i = 0; i < n_actions; ++i) { + do_action(&u); + + // sink to make sure last action has been executed by the server + ulong iters = stem_iters; + do { sched_yield(); } while (stem_iters < iters + 1); + } + + stop = 1; + pthread_join(thread, NULL); + + // cleanup + close_reset_clients_fd(http_server); + close(fd_http_server_fd(http_server)); + fd_http_server_delete(fd_http_server_leave(http_server)); + fd_valloc_free(valloc, shmem); + } + + return 0; +}