diff --git a/fuzzing/ttyd/build_fuzzers.sh b/fuzzing/ttyd/build_fuzzers.sh
@@ -102,500 +102,19 @@ setup_ttyd_source() {
)
echo "[+] cmake done (compile_commands.json at $BUILD_DIR/compile_commands.json)"
- # ── generate mock headers ────────────────────────────────────────────────
- mkdir -p "$FUZZ_INCLUDE"
-
- # fuzz-include/libwebsockets.h — replaces the real lws header when we
- # compile ttyd source files. Covers all types/constants/functions used
- # by http.c and protocol.c. Implementations live in fuzz_lws_mock.c.
- cat > "$FUZZ_INCLUDE/libwebsockets.h" <<'EOF'
-#pragma once
-/* Mock libwebsockets header for AFL++ fuzzing of ttyd.
- * Provides all types and function declarations used by http.c / protocol.c.
- * Runtime implementations are in fuzz_lws_mock.c. */
-#include <stddef.h>
-#include <stdint.h>
-#include <stdbool.h>
-#include <stdio.h>
-
-/* ── opaque types ──────────────────────────────────────────────────────── */
-struct lws { int _fd; };
-struct lws_context { int _dummy; };
-struct lws_vhost { int _dummy; };
-
-typedef struct lws lws_t;
-typedef struct lws_context lws_context;
-
-/* ── callback reasons ──────────────────────────────────────────────────── */
-typedef enum lws_callback_reasons {
- LWS_CALLBACK_HTTP = 0,
- LWS_CALLBACK_HTTP_WRITEABLE,
- LWS_CALLBACK_HTTP_FILE_COMPLETION,
- LWS_CALLBACK_HTTP_BIND_PROTOCOL,
- LWS_CALLBACK_FILTER_HTTP_CONNECTION,
- LWS_CALLBACK_CLOSED_HTTP,
- LWS_CALLBACK_ADD_HEADERS,
- LWS_CALLBACK_CHECK_ACCESS_RIGHTS,
- LWS_CALLBACK_PROCESS_HTML,
- LWS_CALLBACK_ESTABLISHED = 100,
- LWS_CALLBACK_CLOSED,
- LWS_CALLBACK_RECEIVE,
- LWS_CALLBACK_SERVER_WRITEABLE,
- LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION,
- LWS_CALLBACK_PROTOCOL_INIT,
- LWS_CALLBACK_PROTOCOL_DESTROY,
- LWS_CALLBACK_WS_PEER_INITIATED_CLOSE,
- LWS_CALLBACK_SESSION_INFO,
- _LWS_CALLBACK_DUMMY = 9999
-} lws_callback_reasons;
-
-/* ── write protocols ───────────────────────────────────────────────────── */
-typedef enum lws_write_protocol {
- LWS_WRITE_TEXT = 0,
- LWS_WRITE_BINARY,
- LWS_WRITE_CONTINUATION,
- LWS_WRITE_HTTP,
- LWS_WRITE_PING,
- LWS_WRITE_PONG,
- LWS_WRITE_CLOSE,
- LWS_WRITE_HTTP_HEADERS,
- LWS_WRITE_HTTP_FINAL_HEADERS,
- _LWS_WRITE_DUMMY = 9999
-} lws_write_protocol;
-
-/* ── token indexes ─────────────────────────────────────────────────────── */
-typedef enum lws_token_indexes {
- WSI_TOKEN_GET_URI = 0,
- WSI_TOKEN_POST_URI,
- WSI_TOKEN_OPTIONS_URI,
- WSI_TOKEN_HOST,
- WSI_TOKEN_CONNECTION,
- WSI_TOKEN_UPGRADE,
- WSI_TOKEN_ORIGIN,
- WSI_TOKEN_HTTP,
- WSI_TOKEN_HTTP_ACCEPT,
- WSI_TOKEN_HTTP_AUTHORIZATION,
- WSI_TOKEN_HTTP_COOKIE,
- WSI_TOKEN_HTTP_CONTENT_LENGTH,
- WSI_TOKEN_HTTP_CONTENT_TYPE,
- WSI_TOKEN_HTTP_RANGE,
- WSI_TOKEN_HTTP_REFERER,
- WSI_TOKEN_HTTP_COLON_AUTHORITY,
- WSI_TOKEN_HTTP_COLON_METHOD,
- WSI_TOKEN_HTTP_COLON_PATH,
- WSI_TOKEN_HTTP_COLON_SCHEME,
- WSI_TOKEN_HTTP_ACCEPT_ENCODING,
- WSI_TOKEN_HTTP_ACCEPT_LANGUAGE,
- WSI_TOKEN_HTTP_PRAGMA,
- WSI_TOKEN_HTTP_CACHE_CONTROL,
- WSI_TOKEN_HTTP_IF_MODIFIED_SINCE,
- WSI_TOKEN_HTTP_IF_NONE_MATCH,
- WSI_TOKEN_HTTP_TRANSFER_ENCODING,
- WSI_TOKEN_HTTP_WWW_AUTHENTICATE,
- WSI_TOKEN_HTTP_PROXY_AUTHENTICATE,
- WSI_TOKEN_HTTP_PROXY_AUTHORIZATION,
- WSI_TOKEN_HTTP_X_FORWARDED_FOR,
- WSI_TOKEN_HTTP_URI_ARGS,
- WSI_TOKEN_COLON_STATUS,
- WSI_TOKEN_HTTP_ACCEPT_RANGES,
- WSI_TOKEN_HTTP_LOCATION,
- WSI_TOKEN_HTTP_CONTENT_RANGE,
- WSI_TOKEN_HTTP_SET_COOKIE,
- WSI_TOKEN_HTTP_CONTENT_DISPOSITION,
- WSI_TOKEN_HTTP_CONTENT_ENCODING,
- WSI_TOKEN_HTTP_CONTENT_LANGUAGE,
- WSI_TOKEN_HTTP_CONTENT_LOCATION,
- ARRAY_SIZE_WSI_TOKENS,
-} lws_token_indexes;
-
-/* ── HTTP status codes ─────────────────────────────────────────────────── */
-#define HTTP_STATUS_OK 200
-#define HTTP_STATUS_NO_CONTENT 204
-#define HTTP_STATUS_PARTIAL_CONTENT 206
-#define HTTP_STATUS_MOVED_PERMANENTLY 301
-#define HTTP_STATUS_FOUND 302
-#define HTTP_STATUS_NOT_MODIFIED 304
-#define HTTP_STATUS_BAD_REQUEST 400
-#define HTTP_STATUS_UNAUTHORIZED 401
-#define HTTP_STATUS_FORBIDDEN 403
-#define HTTP_STATUS_NOT_FOUND 404
-#define HTTP_STATUS_PROXY_AUTH_REQUIRED 407
-#define HTTP_STATUS_INTERNAL_SERVER_ERROR 500
-#define HTTP_STATUS_NOT_IMPLEMENTED 501
-
-/* ── misc types ────────────────────────────────────────────────────────── */
-typedef uint64_t lws_filepos_t;
-
-/* ── misc constants ────────────────────────────────────────────────────── */
-#define LWS_PRE 32
-#define LWS_CLOSE_STATUS_NOSTATUS 0
-#define LWS_CLOSE_STATUS_NORMAL 1000
-#define LWS_CLOSE_STATUS_POLICY_VIOLATION 1008
-#define LWS_CLOSE_STATUS_UNEXPECTED_CONDITION 1011
-
-typedef enum pending_timeout {
- NO_PENDING_TIMEOUT = 0,
- PENDING_TIMEOUT_AWAITING_PROXY_RESPONSE,
- _PENDING_TIMEOUT_DUMMY = 100
-} pending_timeout;
-
-/* ── protocol registration (used in server.c, not in fuzzing target) ─── */
-typedef int (*lws_callback_function)(struct lws *wsi,
- enum lws_callback_reasons reason,
- void *user, void *in, size_t len);
-struct lws_protocols {
- const char *name;
- lws_callback_function callback;
- size_t per_session_data_size;
- size_t rx_buffer_size;
- unsigned int id;
- void *user;
- size_t tx_packet_size;
-};
-
-struct lws_http_mount { int _dummy; };
-struct lws_context_creation_info { int _dummy; };
-
-/* ── logging macros ────────────────────────────────────────────────────── */
-#define lwsl_err(fmt, ...) fprintf(stderr, "ERR: " fmt, ##__VA_ARGS__)
-#define lwsl_warn(fmt, ...) fprintf(stderr, "WRN: " fmt, ##__VA_ARGS__)
-#define lwsl_notice(fmt, ...) do {} while (0)
-#define lwsl_info(fmt, ...) do {} while (0)
-#define lwsl_debug(fmt, ...) do {} while (0)
-#define lwsl_user(fmt, ...) do {} while (0)
-
-/* ── function declarations (implementations in fuzz_lws_mock.c) ────────── */
-int lws_hdr_copy(struct lws *wsi, char *dest, int len,
- enum lws_token_indexes h);
-int lws_hdr_custom_length(struct lws *wsi, const char *name, int nlen);
-int lws_hdr_custom_copy(struct lws *wsi, char *dst, int len,
- const char *name, int nlen);
-int lws_hdr_copy_fragment(struct lws *wsi, char *dst, int len,
- enum lws_token_indexes h, int frag_idx);
-int lws_return_http_status(struct lws *wsi, unsigned int code,
- const char *html_body);
-int lws_add_http_header_status(struct lws *wsi, unsigned int code,
- unsigned char **p, unsigned char *end);
-int lws_add_http_header_content_length(struct lws *wsi,
- lws_filepos_t content_length,
- unsigned char **p,
- unsigned char *end);
-int lws_add_http_header_by_token(struct lws *wsi, enum lws_token_indexes h,
- const unsigned char *value, int length,
- unsigned char **p, unsigned char *end);
-int lws_add_http_header_by_name(struct lws *wsi, const unsigned char *name,
- const unsigned char *value, int length,
- unsigned char **p, unsigned char *end);
-int lws_finalize_http_header(struct lws *wsi, unsigned char **p,
- unsigned char *end);
-int lws_finalize_write_http_header(struct lws *wsi, unsigned char *start,
- unsigned char **p, unsigned char *end);
-int lws_write(struct lws *wsi, unsigned char *buf, size_t len,
- enum lws_write_protocol protocol);
-int lws_callback_on_writable(struct lws *wsi);
-void lws_set_timeout(struct lws *wsi, enum pending_timeout reason, int secs);
-void lws_close_reason(struct lws *wsi, int status,
- unsigned char *buf, size_t len);
-struct lws_context *lws_get_context(const struct lws *wsi);
-int lws_parse_uri(char *p, const char **prot, const char **host,
- int *port, const char **path);
-int lws_is_final_fragment(struct lws *wsi);
-size_t lws_remaining_packet_payload(struct lws *wsi);
-int lws_get_peer_simple(struct lws *wsi, char *name, size_t namelen);
-int lws_serve_http_file(struct lws *wsi, const char *file,
- const char *content_type,
- const char *other_headers, int other_headers_len);
-int lws_http_transaction_completed(struct lws *wsi);
-struct lws_context *lws_create_context(struct lws_context_creation_info *info);
-void lws_context_destroy(struct lws_context *context);
-int lws_service(struct lws_context *context, int timeout_ms);
-void lws_cancel_service(struct lws_context *context);
-void *lws_wsi_user(struct lws *wsi);
-struct lws *lws_get_network_wsi(struct lws *wsi);
-int lws_get_peer_write_allowance(struct lws *wsi);
-int lws_write_http(struct lws *wsi, void *buf, size_t len);
-int lws_send_pipe_choked(struct lws *wsi);
-EOF
- echo "[+] Generated fuzz-include/libwebsockets.h"
-
- # fuzz-include/uv.h — minimal libuv stubs (server.h and pty.h use uv types)
- cat > "$FUZZ_INCLUDE/uv.h" <<'EOF'
-#pragma once
-/* Minimal libuv stub for fuzzing ttyd — only types, no event loop. */
-#include <stddef.h>
-#include <stdint.h>
-#include <sys/types.h>
-
-typedef struct uv_loop_s uv_loop_t;
-typedef struct uv_handle_s uv_handle_t;
-typedef struct uv_async_s uv_async_t;
-typedef struct uv_pipe_s uv_pipe_t;
-typedef struct uv_timer_s uv_timer_t;
-typedef struct uv_thread_s uv_thread_t;
-typedef struct uv_mutex_s uv_mutex_t;
-typedef struct uv_process_s uv_process_t;
-typedef struct uv_signal_s uv_signal_t;
-
-struct uv_loop_s { void *data; };
-struct uv_handle_s { void *data; uv_loop_t *loop; };
-struct uv_async_s { void *data; uv_loop_t *loop; };
-struct uv_pipe_s { void *data; uv_loop_t *loop; };
-struct uv_timer_s { void *data; uv_loop_t *loop; };
-struct uv_thread_s { unsigned long _tid; };
-struct uv_mutex_s { int _m; };
-struct uv_process_s{ void *data; };
-struct uv_signal_s { void *data; };
-
-typedef void (*uv_close_cb)(uv_handle_t *handle);
-typedef void (*uv_async_cb)(uv_async_t *handle);
-typedef void (*uv_alloc_cb)(uv_handle_t *handle, size_t suggested_size, void *buf);
-typedef void (*uv_read_cb)(uv_pipe_t *stream, ssize_t nread, const void *buf);
-typedef void (*uv_exit_cb)(uv_process_t *process, int64_t exit_status, int term_signal);
-typedef void (*uv_walk_cb)(uv_handle_t *handle, void *arg);
-typedef void (*uv_thread_cb)(void *arg);
-typedef void (*uv_timer_cb)(uv_timer_t *handle);
-
-#define UV_RUN_DEFAULT 0
-#define UV_RUN_ONCE 1
-#define UV_RUN_NOWAIT 2
-
-static inline int uv_loop_init(uv_loop_t *l) { return 0; }
-static inline int uv_run(uv_loop_t *l, int mode) { return 0; }
-static inline void uv_stop(uv_loop_t *l) {}
-static inline int uv_loop_close(uv_loop_t *l) { return 0; }
-static inline void uv_close(uv_handle_t *h, uv_close_cb cb) {}
-static inline int uv_async_init(uv_loop_t *l, uv_async_t *a, uv_async_cb cb) { a->loop = l; return 0; }
-static inline int uv_async_send(uv_async_t *a) { return 0; }
-static inline int uv_timer_init(uv_loop_t *l, uv_timer_t *t){ return 0; }
-static inline int uv_timer_start(uv_timer_t *t, uv_timer_cb cb, uint64_t timeout, uint64_t repeat) { return 0; }
-static inline int uv_timer_stop(uv_timer_t *t) { return 0; }
-static inline int uv_mutex_init(uv_mutex_t *m) { return 0; }
-static inline void uv_mutex_lock(uv_mutex_t *m) {}
-static inline void uv_mutex_unlock(uv_mutex_t *m) {}
-static inline void uv_mutex_destroy(uv_mutex_t *m) {}
-static inline int uv_thread_create(uv_thread_t *t, uv_thread_cb cb, void *arg) { return 0; }
-static inline int uv_thread_join(uv_thread_t *t) { return 0; }
-static inline uv_loop_t *uv_default_loop(void) { return NULL; }
-static inline int uv_walk(uv_loop_t *l, uv_walk_cb cb, void *arg) { return 0; }
-static inline const char *uv_err_name(int err) { return "UNKNOWN"; }
-static inline const char *uv_strerror(int err) { return "unknown error"; }
-EOF
- echo "[+] Generated fuzz-include/uv.h"
-
- # fuzz_lws_mock.h — declares fuzzer-controlled lws state, included by harnesses
- cat > "$SCRIPT_DIR/fuzz_lws_mock.h" <<'EOF'
-#pragma once
-#include <stddef.h>
-#include <stdint.h>
-#include <string.h>
-
-/* Fuzzer-controlled header values. Set these before calling any ttyd
- * function that reads HTTP headers; the mock lws_hdr_* functions return
- * whatever is stored here. */
-extern char _fuzz_auth_header[4096];
-extern int _fuzz_auth_header_len;
-extern char _fuzz_custom_header[4096];
-extern int _fuzz_custom_header_len;
-
-/* Reset all mock state — call at the start of every fuzz iteration. */
-void fuzz_lws_reset(void);
-
-/* Convenience setters. */
-static inline void fuzz_lws_set_auth(const char *v, int len) {
- int n = len < 4095 ? len : 4095;
- memcpy(_fuzz_auth_header, v, n);
- _fuzz_auth_header[n] = '\0';
- _fuzz_auth_header_len = n;
-}
-static inline void fuzz_lws_set_custom(const char *v, int len) {
- int n = len < 4095 ? len : 4095;
- memcpy(_fuzz_custom_header, v, n);
- _fuzz_custom_header[n] = '\0';
- _fuzz_custom_header_len = n;
-}
-EOF
- echo "[+] Generated fuzz_lws_mock.h"
-
- # fuzz_lws_mock.c — runtime implementations of the mocked lws functions
- cat > "$SCRIPT_DIR/fuzz_lws_mock.c" <<'EOF'
-/* Mock implementations of libwebsockets runtime functions for fuzzing ttyd.
- * lws_hdr_copy / lws_hdr_custom_* return fuzzer-controlled data so AFL++
- * can drive the auth logic. All other lws functions are safe no-ops. */
-
-#include "fuzz-include/libwebsockets.h"
-#include "fuzz_lws_mock.h"
-#include <string.h>
-#include <stdio.h>
-#include <stdint.h>
-
-/* ── fuzzer-controlled state ─────────────────────────────────────────── */
-char _fuzz_auth_header[4096];
-int _fuzz_auth_header_len;
-char _fuzz_custom_header[4096];
-int _fuzz_custom_header_len;
-
-void fuzz_lws_reset(void) {
- memset(_fuzz_auth_header, 0, sizeof(_fuzz_auth_header));
- _fuzz_auth_header_len = 0;
- memset(_fuzz_custom_header, 0, sizeof(_fuzz_custom_header));
- _fuzz_custom_header_len = 0;
-}
-
-/* ── header accessors ────────────────────────────────────────────────── */
-int lws_hdr_copy(struct lws *wsi, char *dest, int len,
- enum lws_token_indexes h) {
- const char *src = NULL;
- int srclen = 0;
- if (h == WSI_TOKEN_HTTP_AUTHORIZATION) {
- src = _fuzz_auth_header;
- srclen = _fuzz_auth_header_len;
- }
- if (!src || !dest || len <= 0) { if (dest && len > 0) dest[0] = '\0'; return 0; }
- int n = srclen < len - 1 ? srclen : len - 1;
- memcpy(dest, src, n);
- dest[n] = '\0';
- return n;
-}
-
-int lws_hdr_custom_length(struct lws *wsi, const char *name, int nlen) {
- return _fuzz_custom_header_len;
-}
-
-int lws_hdr_custom_copy(struct lws *wsi, char *dst, int len,
- const char *name, int nlen) {
- if (!dst || len <= 0) return 0;
- int n = _fuzz_custom_header_len < len - 1 ? _fuzz_custom_header_len : len - 1;
- memcpy(dst, _fuzz_custom_header, n);
- dst[n] = '\0';
- return n;
-}
-
-int lws_hdr_copy_fragment(struct lws *wsi, char *dst, int len,
- enum lws_token_indexes h, int frag_idx) {
- if (dst && len > 0) dst[0] = '\0';
- return 0;
-}
-
-/* ── URI parsing ─────────────────────────────────────────────────────── */
-int lws_parse_uri(char *p, const char **prot, const char **host,
- int *port, const char **path) {
- if (prot) *prot = "";
- if (host) *host = "";
- if (port) *port = 0;
- if (path) *path = p ? p : "";
- return 0;
-}
-
-/* ── HTTP response helpers — all no-ops for fuzzing ─────────────────── */
-int lws_return_http_status(struct lws *wsi, unsigned int code,
- const char *html_body) { return 0; }
-int lws_add_http_header_status(struct lws *wsi, unsigned int code,
- unsigned char **p, unsigned char *end) { return 0; }
-int lws_add_http_header_content_length(struct lws *wsi, lws_filepos_t content_length,
- unsigned char **p, unsigned char *end) { return 0; }
-int lws_add_http_header_by_token(struct lws *wsi, enum lws_token_indexes h,
- const unsigned char *v, int len,
- unsigned char **p, unsigned char *end) { return 0; }
-int lws_add_http_header_by_name(struct lws *wsi, const unsigned char *n,
- const unsigned char *v, int len,
- unsigned char **p, unsigned char *end) { return 0; }
-int lws_finalize_http_header(struct lws *wsi, unsigned char **p,
- unsigned char *end) { return 0; }
-int lws_finalize_write_http_header(struct lws *wsi, unsigned char *start,
- unsigned char **p, unsigned char *end) { return 0; }
-int lws_write(struct lws *wsi, unsigned char *buf, size_t len,
- enum lws_write_protocol protocol) { return (int)len; }
-int lws_callback_on_writable(struct lws *wsi) { return 0; }
-void lws_set_timeout(struct lws *wsi, enum pending_timeout reason, int secs) {}
-void lws_close_reason(struct lws *wsi, int status,
- unsigned char *buf, size_t len) {}
-struct lws_context *lws_get_context(const struct lws *wsi) { return NULL; }
-int lws_is_final_fragment(struct lws *wsi) { return 1; }
-size_t lws_remaining_packet_payload(struct lws *wsi) { return 0; }
-int lws_get_peer_simple(struct lws *wsi, char *name, size_t namelen) {
- if (name && namelen > 0) { strncpy(name, "127.0.0.1", namelen - 1); name[namelen-1] = '\0'; }
- return 0;
-}
-int lws_serve_http_file(struct lws *wsi, const char *file,
- const char *content_type, const char *other_headers,
- int other_headers_len) { return 0; }
-int lws_http_transaction_completed(struct lws *wsi) { return 0; }
-struct lws_context *lws_create_context(struct lws_context_creation_info *i) { return NULL; }
-void lws_context_destroy(struct lws_context *ctx) {}
-int lws_service(struct lws_context *ctx, int timeout_ms) { return 0; }
-void lws_cancel_service(struct lws_context *ctx) {}
-void *lws_wsi_user(struct lws *wsi) { return NULL; }
-struct lws *lws_get_network_wsi(struct lws *wsi) { return wsi; }
-int lws_get_peer_write_allowance(struct lws *wsi) { return -1; }
-int lws_write_http(struct lws *wsi, void *buf, size_t len) { return (int)len; }
-int lws_send_pipe_choked(struct lws *wsi) { return 0; }
-EOF
- echo "[+] Generated fuzz_lws_mock.c"
-
- # mock_server_globals.c — provides the global `struct server *server`
- # expected by http.c and protocol.c
- cat > "$SCRIPT_DIR/mock_server_globals.c" <<'EOF'
-/* Provides globals that ttyd's http.c and protocol.c reference.
- * Normally defined in main.c; here we provide stub definitions.
- * Fuzzing harnesses call fuzz_server_reset() before each iteration. */
-
-#include "fuzz-include/libwebsockets.h"
-#include "fuzz-include/uv.h"
-#include "ttyd-src/src/server.h"
-#include <string.h>
-
-/* Globals declared extern in server.h */
-struct server *server = NULL;
-struct lws_context *context = NULL;
-volatile bool force_exit = false;
-struct endpoints endpoints = {"/ws", "/", "/token", "/"};
-
-/* Fuzzer-private server instance */
-struct server _fuzz_server;
-
-void fuzz_server_reset(void) {
- memset(&_fuzz_server, 0, sizeof(_fuzz_server));
- _fuzz_server.writable = true;
- _fuzz_server.check_origin = false;
- _fuzz_server.max_clients = 0;
- server = &_fuzz_server;
- context = NULL;
- force_exit = false;
-}
-EOF
- echo "[+] Generated mock_server_globals.c"
-
- # mock_pty_stubs.c — stubs for PTY functions called by protocol.c
- # Prevents actual process spawning during fuzzing
- cat > "$SCRIPT_DIR/mock_pty_stubs.c" <<'EOF'
-/* Stub implementations of PTY functions used by ttyd's protocol.c.
- * All functions are no-ops so no real processes are ever launched during
- * fuzzing. Signatures match pty.h exactly. */
-
-#include "fuzz-include/libwebsockets.h"
-#include "fuzz-include/uv.h"
-#include "ttyd-src/src/server.h"
-#include "ttyd-src/src/pty.h"
-#include <stdlib.h>
-#include <string.h>
-
-pty_buf_t *pty_buf_init(char *base, size_t len) { return NULL; }
-void pty_buf_free(pty_buf_t *buf) {}
-pty_process *process_init(void *ctx, uv_loop_t *loop,
- char *argv[], char *envp[]) { return calloc(1, sizeof(pty_process)); }
-bool process_running(pty_process *process) { return false; }
-void process_free(pty_process *process) { free(process); }
-int pty_spawn(pty_process *process,
- pty_read_cb read_cb,
- pty_exit_cb exit_cb) { return -1; }
-void pty_pause(pty_process *process) {}
-void pty_resume(pty_process *process) {}
-int pty_write(pty_process *process,
- pty_buf_t *buf) { return -1; }
-bool pty_resize(pty_process *process) { return false; }
-bool pty_kill(pty_process *process, int sig) { return false; }
-EOF
- echo "[+] Generated mock_pty_stubs.c"
+ # ── verify mock headers/sources exist ────────────────────────────────
+ # These files are checked into the repo alongside the harnesses.
+ # (Previously they were generated inline by this script.)
+ for f in "$FUZZ_INCLUDE/libwebsockets.h" "$FUZZ_INCLUDE/uv.h" \
+ "$SCRIPT_DIR/fuzz_lws_mock.h" "$SCRIPT_DIR/fuzz_lws_mock.c" \
+ "$SCRIPT_DIR/mock_server_globals.c" "$SCRIPT_DIR/mock_pty_stubs.c"; do
+ if [ ! -f "$f" ]; then
+ echo "[!] Missing required file: $f"
+ echo " These files should be checked in alongside the harnesses."
+ exit 1
+ fi
+ done
+ echo "[+] Mock headers and stubs verified"
# ── compile instrumented ttyd objects ────────────────────────────────
mkdir -p "$TTYD_OBJ"
@@ -668,177 +187,25 @@ setup_dictionaries() {
fi
done
- # Minimal fallback if still missing
- if [ ! -f "$DICT_DIR/http.dict" ]; then
- cat > "$DICT_DIR/http.dict" <<'DICT'
-"Basic "
-"Bearer "
-"Authorization: Basic "
-"Origin: "
-"Host: "
-"GET "
-"POST "
-"HTTP/1.1"
-"\r\n\r\n"
-"X-Auth-User"
-"Upgrade: websocket"
-"Sec-WebSocket-Version: 13"
-DICT
+ # Minimal fallback if still missing (from checked-in fallback files)
+ if [ ! -f "$DICT_DIR/http.dict" ] && [ -f "$SCRIPT_DIR/dictionaries/http_fallback.dict" ]; then
+ cp "$SCRIPT_DIR/dictionaries/http_fallback.dict" "$DICT_DIR/http.dict"
echo "[+] Created dictionaries/http.dict (fallback)"
fi
- if [ ! -f "$DICT_DIR/json.dict" ]; then
- cat > "$DICT_DIR/json.dict" <<'DICT'
-"{"
-"}"
-"null"
-"true"
-"false"
-"\"columns\":"
-"\"rows\":"
-"\"AuthToken\":"
-DICT
+ if [ ! -f "$DICT_DIR/json.dict" ] && [ -f "$SCRIPT_DIR/dictionaries/json_fallback.dict" ]; then
+ cp "$SCRIPT_DIR/dictionaries/json_fallback.dict" "$DICT_DIR/json.dict"
echo "[+] Created dictionaries/json.dict (fallback)"
fi
echo "[+] Dictionaries: $(ls "$DICT_DIR")"
}
-# ─── 4. corpus generation (formerly gen-corpus.sh) ───────────────────────────
+# ─── 4. corpus generation ─────────────────────────────────────────────────────
generate_corpus() {
- mkdir -p "$SCRIPT_DIR/corpus/auth_header"
- mkdir -p "$SCRIPT_DIR/corpus/websocket_auth"
- mkdir -p "$SCRIPT_DIR/corpus/http_parsing"
-
- # ── auth_header ──────────────────────────────────────────────────────────
- # Format: [mode:1][credential_len:2 BE][credential][auth_header_value]
- echo "[*] Generating corpus/auth_header/"
- local D="$SCRIPT_DIR/corpus/auth_header"
-
- printf '\x00\x00\x00anything-goes' > "$D/noauth_basic"
- printf '\x00\x00\x00' > "$D/noauth_empty"
- printf '\x00\x00\x00Basic YWRtaW46cGFzcw==' > "$D/noauth_with_basic"
- printf '\x01\x00\x0badmin:pass\x00Basic YWRtaW46cGFzcw==' > "$D/basic_valid"
- printf '\x01\x00\x0badmin:pass\x00Basic INVALIDBASE64==' > "$D/basic_mismatch"
- printf '\x01\x00\x0badmin:pass\x00Basic ' > "$D/basic_empty_token"
- printf '\x01\x00\x0badmin:pass\x00' > "$D/basic_no_header"
- printf '\x01\x00\x0badmin:pass\x00Bearer token123' > "$D/basic_wrong_scheme"
- printf '\x01\x00\x0badmin:pass\x00basic YWRtaW46cGFzcw==' > "$D/basic_lowercase"
- printf '\x01\x00\x00\x00Basic YWRtaW46cGFzcw==' > "$D/basic_no_credential"
- printf '\x02\x00\x00some-token-value' > "$D/custom_with_value"
- printf '\x02\x00\x00' > "$D/custom_empty"
- printf '\x01\x00\x0badmin:pass\x00BASIC YWRtaW46cGFzcw==' > "$D/basic_uppercase"
- printf '\x01\x00\x0badmin:pass\x00bAsIc YWRtaW46cGFzcw==' > "$D/basic_mixedcase"
- printf '\x01\x00\x02a:\x00Basic YTo=' > "$D/basic_b64_pad1"
- printf '\x01\x00\x01a\x00Basic YQ==' > "$D/basic_b64_pad2"
- printf '\x01\x00\x01:\x00Basic Og==' > "$D/basic_colon_only"
- printf '\x01\x00\x0badmin:pass\x00Basic YWRtaW46cGFzcw==' > "$D/basic_double_space"
- printf '\x01\x00\x0badmin:pass\x00 Basic YWRtaW46cGFzcw==' > "$D/basic_leading_space"
- python3 -c "
-import sys
-cred = b'A' * 255 + b':' + b'B' * 255
-hdr = b'\x01' + len(cred).to_bytes(2,'big') + cred + b'Basic ' + b'A'*500
-sys.stdout.buffer.write(hdr)
-" > "$D/basic_long_cred"
- python3 -c "
-import sys
-cred = b'x:y'
-hdr = b'Basic ' + b'A' * 248
-sys.stdout.buffer.write(b'\x01' + len(cred).to_bytes(2,'big') + cred + hdr)
-" > "$D/boundary_254"
- python3 -c "
-import sys
-cred = b'x:y'
-hdr = b'Basic ' + b'A' * 250
-sys.stdout.buffer.write(b'\x01' + len(cred).to_bytes(2,'big') + cred + hdr)
-" > "$D/boundary_256"
- python3 -c "
-import sys
-cred = b'x:y'
-hdr = b'Basic ' + b'A' * 251
-sys.stdout.buffer.write(b'\x01' + len(cred).to_bytes(2,'big') + cred + hdr)
-" > "$D/boundary_257"
- echo "[+] Generated $(ls "$D" | wc -l) seeds for auth_header"
-
- # ── websocket_auth ───────────────────────────────────────────────────────
- # Format: [has_credential:1][credential_len:2 BE][credential][json_message]
- echo "[*] Generating corpus/websocket_auth/"
- local W="$SCRIPT_DIR/corpus/websocket_auth"
-
- printf '\x00\x00\x00{"columns":80,"rows":24}' > "$W/noauth_normal"
- printf '\x00\x00\x00{"columns":200,"rows":50}' > "$W/noauth_large_term"
- printf '\x00\x00\x00{}' > "$W/noauth_empty_json"
- printf '\x00\x00\x00{"columns":80,"rows":24,"AuthToken":"ignored"}' > "$W/noauth_with_token"
- printf '\x01\x00\x05admin{"columns":80,"rows":24,"AuthToken":"admin"}' > "$W/auth_valid"
- printf '\x01\x00\x05admin{"columns":80,"rows":24,"AuthToken":"wrong"}' > "$W/auth_invalid"
- printf '\x01\x00\x05admin{"columns":80,"rows":24}' > "$W/auth_missing_token"
- printf '\x01\x00\x05admin{"AuthToken":"admin"}' > "$W/auth_no_dimensions"
- printf '\x01\x00\x00{"columns":80,"rows":24}' > "$W/auth_empty_cred"
- printf '\x00\x00\x00{invalid json}' > "$W/json_malformed"
- printf '\x00\x00\x00{"columns":' > "$W/json_truncated"
- printf '\x00\x00\x00' > "$W/json_empty"
- printf '\x01\x00\x03foo[1,2,3]' > "$W/json_array"
- printf '\x00\x00\x00{"columns":99999,"rows":99999}' > "$W/json_huge_values"
- printf '\x00\x00\x00{"columns":-1,"rows":-1}' > "$W/json_negative"
- printf '\x00\x00\x00{"columns":65535,"rows":65535}' > "$W/json_uint16_max"
- printf '\x00\x00\x00{"columns":65536,"rows":65536}' > "$W/json_uint16_overflow"
- printf '\x00\x00\x00{ "columns" : 80 , "rows" : 24 }' > "$W/json_extra_whitespace"
- python3 -c "import sys; sys.stdout.buffer.write(b'\x00\x00\x00' + (b'{\"a\":' * 50 + b'1' + b'}' * 50))" > "$W/json_deep_nested"
- python3 -c "
-import sys
-cred = b'admin'; hdr = b'\x01' + len(cred).to_bytes(2,'big') + cred
-sys.stdout.buffer.write(hdr + b'{\"columns\":80,\"rows\":24,\"AuthToken\":\"' + b'A'*5000 + b'\"}')
-" > "$W/json_huge_token"
- python3 -c "import sys; cred=b'admin'; h=b'\x01'+len(cred).to_bytes(2,'big')+cred; sys.stdout.buffer.write(h+b'{\"columns\":80,\"rows\":24,\"AuthToken\":true}')" > "$W/auth_token_bool"
- python3 -c "import sys; cred=b'admin'; h=b'\x01'+len(cred).to_bytes(2,'big')+cred; sys.stdout.buffer.write(h+b'{\"columns\":80,\"rows\":24,\"AuthToken\":null}')" > "$W/auth_token_null"
- python3 -c "import sys; cred=b'admin'; h=b'\x01'+len(cred).to_bytes(2,'big')+cred; sys.stdout.buffer.write(h+b'{\"columns\":80,\"rows\":24,\"AuthToken\":12345}')" > "$W/auth_token_int"
- python3 -c "import sys; cred=b'admin'; h=b'\x01'+len(cred).to_bytes(2,'big')+cred; sys.stdout.buffer.write(h+b'{\"AuthToken\":\"wrong\",\"AuthToken\":\"admin\"}')" > "$W/auth_token_dup"
- python3 -c "
-import sys
-cred = b'A'*200
-sys.stdout.buffer.write(b'\x01' + len(cred).to_bytes(2,'big') + cred + b'{\"AuthToken\":\"' + cred + b'\"}')
-" > "$W/auth_long_cred"
- echo "[+] Generated $(ls "$W" | wc -l) seeds for websocket_auth"
-
- # ── http_parsing ─────────────────────────────────────────────────────────
- echo "[*] Generating corpus/http_parsing/"
- local H="$SCRIPT_DIR/corpus/http_parsing"
-
- printf 'GET / HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$H/get_root"
- printf 'GET /token HTTP/1.1\r\nHost: localhost:7681\r\nAuthorization: Basic YWRtaW46cGFzcw==\r\n\r\n' > "$H/get_with_auth"
- printf 'GET /ws HTTP/1.1\r\nHost: localhost:7681\r\nOrigin: http://localhost:7681\r\n\r\n' > "$H/get_with_origin"
- printf 'GET / HTTP/1.1\r\nHost: localhost:7681\r\nAuthorization: Basic YWRtaW46cGFzcw==\r\nOrigin: http://localhost:7681\r\n\r\n' > "$H/get_full_headers"
- printf 'GET /../../../etc/passwd HTTP/1.1\r\nHost: localhost\r\n\r\n' > "$H/path_traversal"
- printf 'GET //etc/passwd HTTP/1.1\r\nHost: localhost\r\n\r\n' > "$H/path_double_slash"
- printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic \r\n\r\n' > "$H/auth_empty_b64"
- printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Bearer token123\r\n\r\n' > "$H/auth_bearer"
- printf 'GET / HTTP/1.1\r\nHost: localhost:7681\r\nOrigin: http://evil.com\r\n\r\n' > "$H/csrf_mismatch"
- printf 'GET / HTTP/1.1\r\nHost: target.com\r\nOrigin: http://target.com.evil.com\r\n\r\n' > "$H/csrf_subdomain_trick"
- printf 'GARBAGE\r\n\r\n' > "$H/malformed_no_space"
- printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic user:password\r\n\r\n' > "$H/auth_with_colon"
- printf 'GET /ws HTTP/1.1\r\nHost: localhost:7681\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n' > "$H/ws_upgrade"
- printf 'GET /%%s%%s%%s%%n HTTP/1.1\r\nHost: %%x%%x%%x\r\nAuthorization: Basic %%n%%n\r\n\r\n' > "$H/fmt_everywhere"
- python3 -c "
-import sys
-path = '/' + 'A' * 200
-sys.stdout.buffer.write(f'GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n'.encode())
-" > "$H/path_long_200"
- python3 -c "
-import sys
-auth = 'Basic ' + 'A' * 300
-sys.stdout.buffer.write(f'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: {auth}\r\n\r\n'.encode())
-" > "$H/auth_long_value"
- python3 -c "
-import sys
-req = 'GET / HTTP/1.1\r\n'
-for i in range(100): req += f'X-Header-{i}: value-{i}\r\n'
-req += '\r\n'
-sys.stdout.buffer.write(req.encode())
-" > "$H/many_headers"
- echo "[+] Generated $(ls "$H" | wc -l) seeds for http_parsing"
-
- echo "[+] Total corpus: $(find "$SCRIPT_DIR/corpus" -type f | wc -l) files"
+ echo "[*] Running gen_corpus.py..."
+ python3 "$SCRIPT_DIR/gen_corpus.py" "$SCRIPT_DIR"
}
# ─── 5. crash backup crontab ──────────────────────────────────────────────────
@@ -846,37 +213,11 @@ sys.stdout.buffer.write(req.encode())
setup_crash_backup() {
local BACKUP_DIR="$SCRIPT_DIR/crashes-backup"
- # Write the backup script
- cat > "$BACKUP_SCRIPT" <<BKUP
-#!/bin/bash
-# Auto-generated by build_fuzzers.sh — backs up AFL++ crashes to a dated archive
-# $CRONTAB_MARKER
-SCRIPT_DIR="$SCRIPT_DIR"
-BACKUP_DIR="$BACKUP_DIR"
-OUTPUT_DIR="\$SCRIPT_DIR/output"
-
-mkdir -p "\$BACKUP_DIR"
-
-HAS_CRASHES=false
-for crash_dir in "\$OUTPUT_DIR"/*/main/crashes "\$OUTPUT_DIR"/*/cmplog/crashes; do
- [ -d "\$crash_dir" ] || continue
- count=\$(find "\$crash_dir" -maxdepth 1 -name 'id:*' 2>/dev/null | wc -l)
- [ "\$count" -gt 0 ] || continue
- HAS_CRASHES=true
- break
-done
-
-\$HAS_CRASHES || exit 0
-
-STAMP=\$(date +%Y%m%d_%H%M%S)
-ARCHIVE="\$BACKUP_DIR/crashes_\${STAMP}.tar.gz"
-tar -czf "\$ARCHIVE" \
- --exclude='README.txt' \
- \$(find "\$OUTPUT_DIR" -type d -name 'crashes' 2>/dev/null | tr '\n' ' ') \
- 2>/dev/null || true
-
-echo "[\$(date)] Backed up crashes → \$ARCHIVE" >> "\$BACKUP_DIR/backup.log"
-BKUP
+ # Write the backup script from template
+ sed -e "s|%%CRONTAB_MARKER%%|$CRONTAB_MARKER|g" \
+ -e "s|%%SCRIPT_DIR%%|$SCRIPT_DIR|g" \
+ -e "s|%%BACKUP_DIR%%|$BACKUP_DIR|g" \
+ "$SCRIPT_DIR/backup_crashes.sh.in" > "$BACKUP_SCRIPT"
chmod +x "$BACKUP_SCRIPT"
echo "[+] Created $BACKUP_SCRIPT"
@@ -967,10 +308,6 @@ MOCK_OBJS="$MOCK_OBJS $TTYD_OBJ/fuzz_lws_mock.o $TTYD_OBJ/mock_server_globals.o
# http.c decompresses gzip-encoded HTML using zlib
EXTRA_LIBS="-lz"
-echo "[*] Building fuzz_auth_header..."
-$CC $COMMON_CFLAGS fuzz_auth_header.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS -o fuzz_auth_header$SUFFIX
-echo "[+] Built: fuzz_auth_header$SUFFIX"
-
echo "[*] Building fuzz_websocket_auth..."
$CC $COMMON_CFLAGS fuzz_websocket_auth.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS -o fuzz_websocket_auth$SUFFIX
echo "[+] Built: fuzz_websocket_auth$SUFFIX"
@@ -983,14 +320,17 @@ echo "[+] Built: fuzz_http_parsing$SUFFIX"
if [ "$MODE" = "afl" ]; then
echo
echo "[*] Building CmpLog variants..."
- for target in fuzz_auth_header fuzz_websocket_auth; do
- AFL_LLVM_CMPLOG=1 $CC $COMMON_CFLAGS ${target}.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS \
- -o ${target}_cmplog
- echo "[+] Built: ${target}_cmplog"
- done
+ AFL_LLVM_CMPLOG=1 $CC $COMMON_CFLAGS fuzz_websocket_auth.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS \
+ -o fuzz_websocket_auth_cmplog
+ echo "[+] Built: fuzz_websocket_auth_cmplog"
AFL_LLVM_CMPLOG=1 $CC $COMMON_CFLAGS fuzz_http_parsing.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS \
-o fuzz_http_parsing_cmplog
echo "[+] Built: fuzz_http_parsing_cmplog"
+
+ echo
+ echo "[*] Building HTTP custom mutator..."
+ cc -shared -fPIC -O2 http_mutator.c -o http_mutator.so
+ echo "[+] Built: http_mutator.so"
fi
echo
@@ -1019,7 +359,7 @@ echo
echo "Ready to fuzz:"
if [ "$MODE" = "afl" ]; then
echo " ./run-fuzzers.sh # launch all targets in tmux"
- echo " afl-fuzz -i corpus/auth_header -o output/auth_header -x dictionaries/http.dict ./fuzz_auth_header"
+ echo " afl-fuzz -i corpus/websocket_auth -o output/websocket_auth -x dictionaries/json.dict ./fuzz_websocket_auth"
else
echo " ./fuzz_auth_header_lf corpus/auth_header/"
fi
diff --git a/fuzzing/ttyd/clean.sh b/fuzzing/ttyd/clean.sh
@@ -45,6 +45,15 @@ if tmux has-session -t ttyd-fuzz 2>/dev/null; then
echo "[+] Fuzzer session killed"
fi
+# Kill headless background fuzzer processes
+if [ -f "$SCRIPT_DIR/.bg_fuzz_pids" ]; then
+ while read -r pid; do
+ kill "$pid" 2>/dev/null || true
+ done < "$SCRIPT_DIR/.bg_fuzz_pids"
+ rm -f "$SCRIPT_DIR/.bg_fuzz_pids"
+ echo "[+] Killed headless fuzzer processes"
+fi
+
# ── remove crontab entry ──────────────────────────────────────────────────────
CRONTAB_MARKER="# ttyd-fuzz-crash-backup"
CURRENT_CRON=$(crontab -l 2>/dev/null || true)
@@ -108,7 +117,7 @@ fi
# ── remove compiled harness binaries ─────────────────────────────────────────
REMOVED=0
-for binary in fuzz_auth_header fuzz_websocket_auth fuzz_http_parsing; do
+for binary in fuzz_websocket_auth fuzz_http_parsing; do
for suffix in "" "_cmplog" "_lf"; do
target="$SCRIPT_DIR/${binary}${suffix}"
if [ -f "$target" ]; then
@@ -117,30 +126,28 @@ for binary in fuzz_auth_header fuzz_websocket_auth fuzz_http_parsing; do
fi
done
done
-[ "$REMOVED" -gt 0 ] && echo "[+] Removed $REMOVED compiled binaries" \
+if [ -f "$SCRIPT_DIR/http_mutator.so" ]; then
+ rm -f "$SCRIPT_DIR/http_mutator.so"
+ REMOVED=$((REMOVED + 1))
+fi
+[ "$REMOVED" -gt 0 ] && echo "[+] Removed $REMOVED compiled binaries/libraries" \
|| echo "[*] No binaries to remove"
-# ── remove generated mock files ───────────────────────────────────────────────
-for f in fuzz_lws_mock.c fuzz_lws_mock.h mock_server_globals.c mock_pty_stubs.c backup_crashes.sh; do
- if [ -f "$SCRIPT_DIR/$f" ]; then
- rm -f "$SCRIPT_DIR/$f"
- echo "[+] Removed $f"
- fi
-done
+# ── remove build artifacts (NOT tracked source files) ─────────────────────────
+# fuzz_lws_mock.{c,h}, mock_server_globals.c, mock_pty_stubs.c, and
+# fuzz-include/{libwebsockets,uv}.h are tracked source — keep them.
+if [ -f "$SCRIPT_DIR/backup_crashes.sh" ]; then
+ rm -f "$SCRIPT_DIR/backup_crashes.sh"
+ echo "[+] Removed backup_crashes.sh"
+fi
-# ── remove instrumented objects and mock headers ──────────────────────────────
+# ── remove instrumented objects ───────────────────────────────────────────────
if [ -d "$SCRIPT_DIR/ttyd-obj" ]; then
echo "[*] Removing ttyd-obj/"
rm -rf "$SCRIPT_DIR/ttyd-obj"
echo "[+] ttyd-obj/ removed"
fi
-if [ -d "$SCRIPT_DIR/fuzz-include" ]; then
- echo "[*] Removing fuzz-include/"
- rm -rf "$SCRIPT_DIR/fuzz-include"
- echo "[+] fuzz-include/ removed"
-fi
-
# ── optionally remove the ttyd source clone ───────────────────────────────────
if [ -d "$SCRIPT_DIR/ttyd-src" ]; then
if $KEEP_SOURCE; then
diff --git a/fuzzing/ttyd/dictionaries/http.dict b/fuzzing/ttyd/dictionaries/http.dict
@@ -0,0 +1,119 @@
+# Sources: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
+
+# misc
+"HTTP/1.1"
+
+# verbs
+"CONNECT"
+"DELETE"
+"GET"
+"HEAD"
+"OPTIONS"
+"PATCH"
+"POST"
+"PUT"
+"TRACE"
+
+
+# Fields
+"A-IM"
+"Accept"
+"Accept-Charset"
+"Accept-Datetime"
+"Accept-Encoding"
+"Accept-Language"
+"Accept-Patch"
+"Accept-Ranges"
+"Access-Control-Allow-Credentials"
+"Access-Control-Allow-Headers"
+"Access-Control-Allow-Methods"
+"Access-Control-Allow-Origin"
+"Access-Control-Expose-Headers"
+"Access-Control-Max-Age"
+"Access-Control-Request-Headers"
+"Access-Control-Request-Method"
+"Age"
+"Allow"
+"Alt-Svc"
+"Authorization"
+"Cache-Control"
+"Connection"
+"Connection:"
+"Content-Disposition"
+"Content-Encoding"
+"Content-Language"
+"Content-Length"
+"Content-Location"
+"Content-MD5"
+"Content-Range"
+"Content-Security-Policy"
+"Content-Type"
+"Cookie"
+"DNT"
+"Date"
+"Delta-Base"
+"ETag"
+"Expect"
+"Expires"
+"Forwarded"
+"From"
+"Front-End-Https"
+"HTTP2-Settings"
+"Host"
+"IM"
+"If-Match"
+"If-Modified-Since"
+"If-None-Match"
+"If-Range"
+"If-Unmodified-Since"
+"Last-Modified"
+"Link"
+"Location"
+"Max-Forwards"
+"Origin"
+"P3P"
+"Pragma"
+"Proxy-Authenticate"
+"Proxy-Authorization"
+"Proxy-Connection"
+"Public-Key-Pins"
+"Range"
+"Referer"
+"Refresh"
+"Retry-After"
+"Save-Data"
+"Server"
+"Set-Cookie"
+"Status"
+"Strict-Transport-Security"
+"TE"
+"Timing-Allow-Origin"
+"Tk"
+"Trailer"
+"Transfer-Encoding"
+"Upgrade"
+"Upgrade-Insecure-Requests"
+"User-Agent"
+"Vary"
+"Via"
+"WWW-Authenticate"
+"Warning"
+"X-ATT-DeviceId"
+"X-Content-Duration"
+"X-Content-Security-Policy"
+"X-Content-Type-Options"
+"X-Correlation-ID"
+"X-Csrf-Token"
+"X-Forwarded-For"
+"X-Forwarded-Host"
+"X-Forwarded-Proto"
+"X-Frame-Options"
+"X-Http-Method-Override"
+"X-Powered-By"
+"X-Request-ID"
+"X-Requested-With"
+"X-UA-Compatible"
+"X-UIDH"
+"X-Wap-Profile"
+"X-WebKit-CSP"
+"X-XSS-Protection"
diff --git a/fuzzing/ttyd/dictionaries/http_fallback.dict b/fuzzing/ttyd/dictionaries/http_fallback.dict
@@ -0,0 +1,12 @@
+"Basic "
+"Bearer "
+"Authorization: Basic "
+"Origin: "
+"Host: "
+"GET "
+"POST "
+"HTTP/1.1"
+"\r\n\r\n"
+"X-Auth-User"
+"Upgrade: websocket"
+"Sec-WebSocket-Version: 13"
diff --git a/fuzzing/ttyd/dictionaries/json.dict b/fuzzing/ttyd/dictionaries/json.dict
@@ -0,0 +1,61 @@
+#
+# AFL dictionary for JSON
+# -----------------------
+#
+# Just the very basics.
+#
+# Inspired by a dictionary by Jakub Wilk <[email protected]>
+#
+
+"0"
+",0"
+":0"
+"0:"
+"-1.2e+3"
+
+"true"
+"false"
+"null"
+
+"\"\""
+",\"\""
+":\"\""
+"\"\":"
+
+"{}"
+",{}"
+":{}"
+"{\"\":0}"
+"{{}}"
+
+"[]"
+",[]"
+":[]"
+"[0]"
+"[[]]"
+
+"''"
+"\\"
+"\\b"
+"\\f"
+"\\n"
+"\\r"
+"\\t"
+"\\u0000"
+"\\x00"
+"\\0"
+"\\uD800\\uDC00"
+"\\uDBFF\\uDFFF"
+
+"\"\":0"
+"//"
+"/**/"
+
+"$ref"
+"type"
+"coordinates"
+"@context"
+"@id"
+
+","
+":"
diff --git a/fuzzing/ttyd/dictionaries/json_fallback.dict b/fuzzing/ttyd/dictionaries/json_fallback.dict
@@ -0,0 +1,8 @@
+"{"
+"}"
+"null"
+"true"
+"false"
+"\"columns\":"
+"\"rows\":"
+"\"AuthToken\":"
diff --git a/fuzzing/ttyd/fuzz-include/libwebsockets.h b/fuzzing/ttyd/fuzz-include/libwebsockets.h
@@ -0,0 +1,206 @@
+#pragma once
+/* Mock libwebsockets header for AFL++ fuzzing of ttyd.
+ * Provides all types and function declarations used by http.c / protocol.c.
+ * Runtime implementations are in fuzz_lws_mock.c. */
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+/* ── opaque types ──────────────────────────────────────────────────────── */
+struct lws { int _fd; };
+struct lws_context { int _dummy; };
+struct lws_vhost { int _dummy; };
+
+typedef struct lws lws_t;
+typedef struct lws_context lws_context;
+
+/* ── callback reasons ──────────────────────────────────────────────────── */
+typedef enum lws_callback_reasons {
+ LWS_CALLBACK_HTTP = 0,
+ LWS_CALLBACK_HTTP_WRITEABLE,
+ LWS_CALLBACK_HTTP_FILE_COMPLETION,
+ LWS_CALLBACK_HTTP_BIND_PROTOCOL,
+ LWS_CALLBACK_FILTER_HTTP_CONNECTION,
+ LWS_CALLBACK_CLOSED_HTTP,
+ LWS_CALLBACK_ADD_HEADERS,
+ LWS_CALLBACK_CHECK_ACCESS_RIGHTS,
+ LWS_CALLBACK_PROCESS_HTML,
+ LWS_CALLBACK_ESTABLISHED = 100,
+ LWS_CALLBACK_CLOSED,
+ LWS_CALLBACK_RECEIVE,
+ LWS_CALLBACK_SERVER_WRITEABLE,
+ LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION,
+ LWS_CALLBACK_PROTOCOL_INIT,
+ LWS_CALLBACK_PROTOCOL_DESTROY,
+ LWS_CALLBACK_WS_PEER_INITIATED_CLOSE,
+ LWS_CALLBACK_SESSION_INFO,
+ _LWS_CALLBACK_DUMMY = 9999
+} lws_callback_reasons;
+
+/* ── write protocols ───────────────────────────────────────────────────── */
+typedef enum lws_write_protocol {
+ LWS_WRITE_TEXT = 0,
+ LWS_WRITE_BINARY,
+ LWS_WRITE_CONTINUATION,
+ LWS_WRITE_HTTP,
+ LWS_WRITE_PING,
+ LWS_WRITE_PONG,
+ LWS_WRITE_CLOSE,
+ LWS_WRITE_HTTP_HEADERS,
+ LWS_WRITE_HTTP_FINAL_HEADERS,
+ _LWS_WRITE_DUMMY = 9999
+} lws_write_protocol;
+
+/* ── token indexes ─────────────────────────────────────────────────────── */
+typedef enum lws_token_indexes {
+ WSI_TOKEN_GET_URI = 0,
+ WSI_TOKEN_POST_URI,
+ WSI_TOKEN_OPTIONS_URI,
+ WSI_TOKEN_HOST,
+ WSI_TOKEN_CONNECTION,
+ WSI_TOKEN_UPGRADE,
+ WSI_TOKEN_ORIGIN,
+ WSI_TOKEN_HTTP,
+ WSI_TOKEN_HTTP_ACCEPT,
+ WSI_TOKEN_HTTP_AUTHORIZATION,
+ WSI_TOKEN_HTTP_COOKIE,
+ WSI_TOKEN_HTTP_CONTENT_LENGTH,
+ WSI_TOKEN_HTTP_CONTENT_TYPE,
+ WSI_TOKEN_HTTP_RANGE,
+ WSI_TOKEN_HTTP_REFERER,
+ WSI_TOKEN_HTTP_COLON_AUTHORITY,
+ WSI_TOKEN_HTTP_COLON_METHOD,
+ WSI_TOKEN_HTTP_COLON_PATH,
+ WSI_TOKEN_HTTP_COLON_SCHEME,
+ WSI_TOKEN_HTTP_ACCEPT_ENCODING,
+ WSI_TOKEN_HTTP_ACCEPT_LANGUAGE,
+ WSI_TOKEN_HTTP_PRAGMA,
+ WSI_TOKEN_HTTP_CACHE_CONTROL,
+ WSI_TOKEN_HTTP_IF_MODIFIED_SINCE,
+ WSI_TOKEN_HTTP_IF_NONE_MATCH,
+ WSI_TOKEN_HTTP_TRANSFER_ENCODING,
+ WSI_TOKEN_HTTP_WWW_AUTHENTICATE,
+ WSI_TOKEN_HTTP_PROXY_AUTHENTICATE,
+ WSI_TOKEN_HTTP_PROXY_AUTHORIZATION,
+ WSI_TOKEN_HTTP_X_FORWARDED_FOR,
+ WSI_TOKEN_HTTP_URI_ARGS,
+ WSI_TOKEN_COLON_STATUS,
+ WSI_TOKEN_HTTP_ACCEPT_RANGES,
+ WSI_TOKEN_HTTP_LOCATION,
+ WSI_TOKEN_HTTP_CONTENT_RANGE,
+ WSI_TOKEN_HTTP_SET_COOKIE,
+ WSI_TOKEN_HTTP_CONTENT_DISPOSITION,
+ WSI_TOKEN_HTTP_CONTENT_ENCODING,
+ WSI_TOKEN_HTTP_CONTENT_LANGUAGE,
+ WSI_TOKEN_HTTP_CONTENT_LOCATION,
+ ARRAY_SIZE_WSI_TOKENS,
+} lws_token_indexes;
+
+/* ── HTTP status codes ─────────────────────────────────────────────────── */
+#define HTTP_STATUS_OK 200
+#define HTTP_STATUS_NO_CONTENT 204
+#define HTTP_STATUS_PARTIAL_CONTENT 206
+#define HTTP_STATUS_MOVED_PERMANENTLY 301
+#define HTTP_STATUS_FOUND 302
+#define HTTP_STATUS_NOT_MODIFIED 304
+#define HTTP_STATUS_BAD_REQUEST 400
+#define HTTP_STATUS_UNAUTHORIZED 401
+#define HTTP_STATUS_FORBIDDEN 403
+#define HTTP_STATUS_NOT_FOUND 404
+#define HTTP_STATUS_PROXY_AUTH_REQUIRED 407
+#define HTTP_STATUS_INTERNAL_SERVER_ERROR 500
+#define HTTP_STATUS_NOT_IMPLEMENTED 501
+
+/* ── misc types ────────────────────────────────────────────────────────── */
+typedef uint64_t lws_filepos_t;
+
+/* ── misc constants ────────────────────────────────────────────────────── */
+#define LWS_PRE 32
+#define LWS_CLOSE_STATUS_NOSTATUS 0
+#define LWS_CLOSE_STATUS_NORMAL 1000
+#define LWS_CLOSE_STATUS_POLICY_VIOLATION 1008
+#define LWS_CLOSE_STATUS_UNEXPECTED_CONDITION 1011
+
+typedef enum pending_timeout {
+ NO_PENDING_TIMEOUT = 0,
+ PENDING_TIMEOUT_AWAITING_PROXY_RESPONSE,
+ _PENDING_TIMEOUT_DUMMY = 100
+} pending_timeout;
+
+/* ── protocol registration (used in server.c, not in fuzzing target) ─── */
+typedef int (*lws_callback_function)(struct lws *wsi,
+ enum lws_callback_reasons reason,
+ void *user, void *in, size_t len);
+struct lws_protocols {
+ const char *name;
+ lws_callback_function callback;
+ size_t per_session_data_size;
+ size_t rx_buffer_size;
+ unsigned int id;
+ void *user;
+ size_t tx_packet_size;
+};
+
+struct lws_http_mount { int _dummy; };
+struct lws_context_creation_info { int _dummy; };
+
+/* ── logging macros ────────────────────────────────────────────────────── */
+#define lwsl_err(fmt, ...) fprintf(stderr, "ERR: " fmt, ##__VA_ARGS__)
+#define lwsl_warn(fmt, ...) fprintf(stderr, "WRN: " fmt, ##__VA_ARGS__)
+#define lwsl_notice(fmt, ...) do {} while (0)
+#define lwsl_info(fmt, ...) do {} while (0)
+#define lwsl_debug(fmt, ...) do {} while (0)
+#define lwsl_user(fmt, ...) do {} while (0)
+
+/* ── function declarations (implementations in fuzz_lws_mock.c) ────────── */
+int lws_hdr_copy(struct lws *wsi, char *dest, int len,
+ enum lws_token_indexes h);
+int lws_hdr_custom_length(struct lws *wsi, const char *name, int nlen);
+int lws_hdr_custom_copy(struct lws *wsi, char *dst, int len,
+ const char *name, int nlen);
+int lws_hdr_copy_fragment(struct lws *wsi, char *dst, int len,
+ enum lws_token_indexes h, int frag_idx);
+int lws_return_http_status(struct lws *wsi, unsigned int code,
+ const char *html_body);
+int lws_add_http_header_status(struct lws *wsi, unsigned int code,
+ unsigned char **p, unsigned char *end);
+int lws_add_http_header_content_length(struct lws *wsi,
+ lws_filepos_t content_length,
+ unsigned char **p,
+ unsigned char *end);
+int lws_add_http_header_by_token(struct lws *wsi, enum lws_token_indexes h,
+ const unsigned char *value, int length,
+ unsigned char **p, unsigned char *end);
+int lws_add_http_header_by_name(struct lws *wsi, const unsigned char *name,
+ const unsigned char *value, int length,
+ unsigned char **p, unsigned char *end);
+int lws_finalize_http_header(struct lws *wsi, unsigned char **p,
+ unsigned char *end);
+int lws_finalize_write_http_header(struct lws *wsi, unsigned char *start,
+ unsigned char **p, unsigned char *end);
+int lws_write(struct lws *wsi, unsigned char *buf, size_t len,
+ enum lws_write_protocol protocol);
+int lws_callback_on_writable(struct lws *wsi);
+void lws_set_timeout(struct lws *wsi, enum pending_timeout reason, int secs);
+void lws_close_reason(struct lws *wsi, int status,
+ unsigned char *buf, size_t len);
+struct lws_context *lws_get_context(const struct lws *wsi);
+int lws_parse_uri(char *p, const char **prot, const char **host,
+ int *port, const char **path);
+int lws_is_final_fragment(struct lws *wsi);
+size_t lws_remaining_packet_payload(struct lws *wsi);
+int lws_get_peer_simple(struct lws *wsi, char *name, size_t namelen);
+int lws_serve_http_file(struct lws *wsi, const char *file,
+ const char *content_type,
+ const char *other_headers, int other_headers_len);
+int lws_http_transaction_completed(struct lws *wsi);
+struct lws_context *lws_create_context(struct lws_context_creation_info *info);
+void lws_context_destroy(struct lws_context *context);
+int lws_service(struct lws_context *context, int timeout_ms);
+void lws_cancel_service(struct lws_context *context);
+void *lws_wsi_user(struct lws *wsi);
+struct lws *lws_get_network_wsi(struct lws *wsi);
+int lws_get_peer_write_allowance(struct lws *wsi);
+int lws_write_http(struct lws *wsi, void *buf, size_t len);
+int lws_send_pipe_choked(struct lws *wsi);
diff --git a/fuzzing/ttyd/fuzz-include/uv.h b/fuzzing/ttyd/fuzz-include/uv.h
@@ -0,0 +1,59 @@
+#pragma once
+/* Minimal libuv stub for fuzzing ttyd — only types, no event loop. */
+#include <stddef.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+typedef struct uv_loop_s uv_loop_t;
+typedef struct uv_handle_s uv_handle_t;
+typedef struct uv_async_s uv_async_t;
+typedef struct uv_pipe_s uv_pipe_t;
+typedef struct uv_timer_s uv_timer_t;
+typedef struct uv_thread_s uv_thread_t;
+typedef struct uv_mutex_s uv_mutex_t;
+typedef struct uv_process_s uv_process_t;
+typedef struct uv_signal_s uv_signal_t;
+
+struct uv_loop_s { void *data; };
+struct uv_handle_s { void *data; uv_loop_t *loop; };
+struct uv_async_s { void *data; uv_loop_t *loop; };
+struct uv_pipe_s { void *data; uv_loop_t *loop; };
+struct uv_timer_s { void *data; uv_loop_t *loop; };
+struct uv_thread_s { unsigned long _tid; };
+struct uv_mutex_s { int _m; };
+struct uv_process_s{ void *data; };
+struct uv_signal_s { void *data; };
+
+typedef void (*uv_close_cb)(uv_handle_t *handle);
+typedef void (*uv_async_cb)(uv_async_t *handle);
+typedef void (*uv_alloc_cb)(uv_handle_t *handle, size_t suggested_size, void *buf);
+typedef void (*uv_read_cb)(uv_pipe_t *stream, ssize_t nread, const void *buf);
+typedef void (*uv_exit_cb)(uv_process_t *process, int64_t exit_status, int term_signal);
+typedef void (*uv_walk_cb)(uv_handle_t *handle, void *arg);
+typedef void (*uv_thread_cb)(void *arg);
+typedef void (*uv_timer_cb)(uv_timer_t *handle);
+
+#define UV_RUN_DEFAULT 0
+#define UV_RUN_ONCE 1
+#define UV_RUN_NOWAIT 2
+
+static inline int uv_loop_init(uv_loop_t *l) { return 0; }
+static inline int uv_run(uv_loop_t *l, int mode) { return 0; }
+static inline void uv_stop(uv_loop_t *l) {}
+static inline int uv_loop_close(uv_loop_t *l) { return 0; }
+static inline void uv_close(uv_handle_t *h, uv_close_cb cb) {}
+static inline int uv_async_init(uv_loop_t *l, uv_async_t *a, uv_async_cb cb) { a->loop = l; return 0; }
+static inline int uv_async_send(uv_async_t *a) { return 0; }
+static inline int uv_timer_init(uv_loop_t *l, uv_timer_t *t){ return 0; }
+static inline int uv_timer_start(uv_timer_t *t, uv_timer_cb cb, uint64_t timeout, uint64_t repeat) { return 0; }
+static inline int uv_timer_stop(uv_timer_t *t) { return 0; }
+static inline int uv_mutex_init(uv_mutex_t *m) { return 0; }
+static inline void uv_mutex_lock(uv_mutex_t *m) {}
+static inline void uv_mutex_unlock(uv_mutex_t *m) {}
+static inline void uv_mutex_destroy(uv_mutex_t *m) {}
+static inline int uv_thread_create(uv_thread_t *t, uv_thread_cb cb, void *arg) { return 0; }
+static inline int uv_thread_join(uv_thread_t *t) { return 0; }
+static inline uv_loop_t *uv_default_loop(void) { return NULL; }
+static inline int uv_walk(uv_loop_t *l, uv_walk_cb cb, void *arg) { return 0; }
+static inline const char *uv_err_name(int err) { return "UNKNOWN"; }
+static inline const char *uv_strerror(int err) { return "unknown error"; }
diff --git a/fuzzing/ttyd/fuzz_lws_mock.c b/fuzzing/ttyd/fuzz_lws_mock.c
@@ -0,0 +1,184 @@
+/* Mock implementations of libwebsockets runtime functions for fuzzing ttyd.
+ * lws_hdr_copy / lws_hdr_custom_* return fuzzer-controlled data so AFL++
+ * can drive the auth logic. All other lws functions are safe no-ops. */
+
+#include "fuzz-include/libwebsockets.h"
+#include "fuzz_lws_mock.h"
+#include <string.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+/* ── fuzzer-controlled state ─────────────────────────────────────────── */
+char _fuzz_auth_header[4096];
+int _fuzz_auth_header_len;
+char _fuzz_custom_header[4096];
+int _fuzz_custom_header_len;
+char _fuzz_uri_path[4096];
+int _fuzz_uri_path_len;
+char _fuzz_origin[4096];
+int _fuzz_origin_len;
+char _fuzz_host[4096];
+int _fuzz_host_len;
+
+char _fuzz_uri_args[FUZZ_MAX_URI_ARGS][256];
+int _fuzz_uri_args_len[FUZZ_MAX_URI_ARGS];
+int _fuzz_uri_args_count;
+
+void fuzz_lws_reset(void) {
+ memset(_fuzz_auth_header, 0, sizeof(_fuzz_auth_header));
+ _fuzz_auth_header_len = 0;
+ memset(_fuzz_custom_header, 0, sizeof(_fuzz_custom_header));
+ _fuzz_custom_header_len = 0;
+ memset(_fuzz_uri_path, 0, sizeof(_fuzz_uri_path));
+ _fuzz_uri_path_len = 0;
+ memset(_fuzz_origin, 0, sizeof(_fuzz_origin));
+ _fuzz_origin_len = 0;
+ memset(_fuzz_host, 0, sizeof(_fuzz_host));
+ _fuzz_host_len = 0;
+ memset(_fuzz_uri_args, 0, sizeof(_fuzz_uri_args));
+ memset(_fuzz_uri_args_len, 0, sizeof(_fuzz_uri_args_len));
+ _fuzz_uri_args_count = 0;
+}
+
+/* ── header accessors ────────────────────────────────────────────────── */
+
+/* Helper: copy from a source buffer into dest, return bytes copied */
+static int copy_hdr(char *dest, int len, const char *src, int srclen) {
+ if (!dest || len <= 0) return 0;
+ if (!src || srclen <= 0) { dest[0] = '\0'; return 0; }
+ int n = srclen < len - 1 ? srclen : len - 1;
+ memcpy(dest, src, n);
+ dest[n] = '\0';
+ return n;
+}
+
+int lws_hdr_copy(struct lws *wsi, char *dest, int len,
+ enum lws_token_indexes h) {
+ switch (h) {
+ case WSI_TOKEN_HTTP_AUTHORIZATION:
+ return copy_hdr(dest, len, _fuzz_auth_header, _fuzz_auth_header_len);
+ case WSI_TOKEN_GET_URI:
+ case WSI_TOKEN_HTTP_COLON_PATH:
+ return copy_hdr(dest, len, _fuzz_uri_path, _fuzz_uri_path_len);
+ case WSI_TOKEN_ORIGIN:
+ return copy_hdr(dest, len, _fuzz_origin, _fuzz_origin_len);
+ case WSI_TOKEN_HOST:
+ return copy_hdr(dest, len, _fuzz_host, _fuzz_host_len);
+ default:
+ if (dest && len > 0) dest[0] = '\0';
+ return 0;
+ }
+}
+
+int lws_hdr_custom_length(struct lws *wsi, const char *name, int nlen) {
+ return _fuzz_custom_header_len;
+}
+
+int lws_hdr_custom_copy(struct lws *wsi, char *dst, int len,
+ const char *name, int nlen) {
+ return copy_hdr(dst, len, _fuzz_custom_header, _fuzz_custom_header_len);
+}
+
+int lws_hdr_copy_fragment(struct lws *wsi, char *dst, int len,
+ enum lws_token_indexes h, int frag_idx) {
+ if (h == WSI_TOKEN_HTTP_URI_ARGS &&
+ frag_idx < _fuzz_uri_args_count && frag_idx < FUZZ_MAX_URI_ARGS) {
+ return copy_hdr(dst, len,
+ _fuzz_uri_args[frag_idx],
+ _fuzz_uri_args_len[frag_idx]);
+ }
+ if (dst && len > 0) dst[0] = '\0';
+ return 0;
+}
+
+/* ── URI parsing ─────────────────────────────────────────────────────── */
+/* Simple parser that extracts protocol, host, port, path from a URI.
+ * Modifies the input string in-place (like the real lws_parse_uri). */
+int lws_parse_uri(char *p, const char **prot, const char **host,
+ int *port, const char **path) {
+ if (!p || !*p) {
+ if (prot) *prot = "";
+ if (host) *host = "";
+ if (port) *port = 0;
+ if (path) *path = "";
+ return -1;
+ }
+
+ /* Find protocol: "http://" or "https://" etc. */
+ char *sep = strstr(p, "://");
+ if (sep) {
+ *sep = '\0';
+ if (prot) *prot = p;
+ p = sep + 3;
+ } else {
+ if (prot) *prot = "";
+ }
+
+ /* host[:port][/path] */
+ if (host) *host = p;
+
+ /* Find path separator */
+ char *slash = strchr(p, '/');
+ if (slash) {
+ if (path) *path = slash + 1;
+ *slash = '\0';
+ } else {
+ if (path) *path = "";
+ }
+
+ /* Find port in host part */
+ char *colon = strchr(p, ':');
+ if (colon) {
+ *colon = '\0';
+ if (port) *port = atoi(colon + 1);
+ } else {
+ if (port) *port = 0;
+ }
+
+ return 0;
+}
+
+/* ── HTTP response helpers — all no-ops for fuzzing ─────────────────── */
+int lws_return_http_status(struct lws *wsi, unsigned int code,
+ const char *html_body) { return 0; }
+int lws_add_http_header_status(struct lws *wsi, unsigned int code,
+ unsigned char **p, unsigned char *end) { return 0; }
+int lws_add_http_header_content_length(struct lws *wsi, lws_filepos_t content_length,
+ unsigned char **p, unsigned char *end) { return 0; }
+int lws_add_http_header_by_token(struct lws *wsi, enum lws_token_indexes h,
+ const unsigned char *v, int len,
+ unsigned char **p, unsigned char *end) { return 0; }
+int lws_add_http_header_by_name(struct lws *wsi, const unsigned char *n,
+ const unsigned char *v, int len,
+ unsigned char **p, unsigned char *end) { return 0; }
+int lws_finalize_http_header(struct lws *wsi, unsigned char **p,
+ unsigned char *end) { return 0; }
+int lws_finalize_write_http_header(struct lws *wsi, unsigned char *start,
+ unsigned char **p, unsigned char *end) { return 0; }
+int lws_write(struct lws *wsi, unsigned char *buf, size_t len,
+ enum lws_write_protocol protocol) { return (int)len; }
+int lws_callback_on_writable(struct lws *wsi) { return 0; }
+void lws_set_timeout(struct lws *wsi, enum pending_timeout reason, int secs) {}
+void lws_close_reason(struct lws *wsi, int status,
+ unsigned char *buf, size_t len) {}
+struct lws_context *lws_get_context(const struct lws *wsi) { return NULL; }
+int lws_is_final_fragment(struct lws *wsi) { return 1; }
+size_t lws_remaining_packet_payload(struct lws *wsi) { return 0; }
+int lws_get_peer_simple(struct lws *wsi, char *name, size_t namelen) {
+ if (name && namelen > 0) { strncpy(name, "127.0.0.1", namelen - 1); name[namelen-1] = '\0'; }
+ return 0;
+}
+int lws_serve_http_file(struct lws *wsi, const char *file,
+ const char *content_type, const char *other_headers,
+ int other_headers_len) { return 0; }
+int lws_http_transaction_completed(struct lws *wsi) { return 0; }
+struct lws_context *lws_create_context(struct lws_context_creation_info *i) { return NULL; }
+void lws_context_destroy(struct lws_context *ctx) {}
+int lws_service(struct lws_context *ctx, int timeout_ms) { return 0; }
+void lws_cancel_service(struct lws_context *ctx) {}
+void *lws_wsi_user(struct lws *wsi) { return NULL; }
+struct lws *lws_get_network_wsi(struct lws *wsi) { return wsi; }
+int lws_get_peer_write_allowance(struct lws *wsi) { return -1; }
+int lws_write_http(struct lws *wsi, void *buf, size_t len) { return (int)len; }
+int lws_send_pipe_choked(struct lws *wsi) { return 0; }
diff --git a/fuzzing/ttyd/fuzz_lws_mock.h b/fuzzing/ttyd/fuzz_lws_mock.h
@@ -0,0 +1,61 @@
+#pragma once
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+/* Fuzzer-controlled header values. Set these before calling any ttyd
+ * function that reads HTTP headers; the mock lws_hdr_* functions return
+ * whatever is stored here. */
+extern char _fuzz_auth_header[4096];
+extern int _fuzz_auth_header_len;
+extern char _fuzz_custom_header[4096];
+extern int _fuzz_custom_header_len;
+
+/* Additional header mocks for websocket/protocol fuzzing */
+extern char _fuzz_uri_path[4096];
+extern int _fuzz_uri_path_len;
+extern char _fuzz_origin[4096];
+extern int _fuzz_origin_len;
+extern char _fuzz_host[4096];
+extern int _fuzz_host_len;
+
+/* URI arg fragments for lws_hdr_copy_fragment(WSI_TOKEN_HTTP_URI_ARGS) */
+#define FUZZ_MAX_URI_ARGS 8
+extern char _fuzz_uri_args[FUZZ_MAX_URI_ARGS][256];
+extern int _fuzz_uri_args_len[FUZZ_MAX_URI_ARGS];
+extern int _fuzz_uri_args_count;
+
+/* Reset all mock state — call at the start of every fuzz iteration. */
+void fuzz_lws_reset(void);
+
+/* Convenience setters. */
+static inline void fuzz_lws_set_auth(const char *v, int len) {
+ int n = len < 4095 ? len : 4095;
+ memcpy(_fuzz_auth_header, v, n);
+ _fuzz_auth_header[n] = '\0';
+ _fuzz_auth_header_len = n;
+}
+static inline void fuzz_lws_set_custom(const char *v, int len) {
+ int n = len < 4095 ? len : 4095;
+ memcpy(_fuzz_custom_header, v, n);
+ _fuzz_custom_header[n] = '\0';
+ _fuzz_custom_header_len = n;
+}
+static inline void fuzz_lws_set_uri_path(const char *v, int len) {
+ int n = len < 4095 ? len : 4095;
+ memcpy(_fuzz_uri_path, v, n);
+ _fuzz_uri_path[n] = '\0';
+ _fuzz_uri_path_len = n;
+}
+static inline void fuzz_lws_set_origin(const char *v, int len) {
+ int n = len < 4095 ? len : 4095;
+ memcpy(_fuzz_origin, v, n);
+ _fuzz_origin[n] = '\0';
+ _fuzz_origin_len = n;
+}
+static inline void fuzz_lws_set_host(const char *v, int len) {
+ int n = len < 4095 ? len : 4095;
+ memcpy(_fuzz_host, v, n);
+ _fuzz_host[n] = '\0';
+ _fuzz_host_len = n;
+}
diff --git a/fuzzing/ttyd/fuzz_websocket_auth.c b/fuzzing/ttyd/fuzz_websocket_auth.c
@@ -1,19 +1,21 @@
/*
- * AFL++ / LibFuzzer harness — TTYD WebSocket AuthToken + window-size JSON
+ * AFL++ / LibFuzzer harness — TTYD WebSocket protocol (callback_tty)
*
- * Target: the real parse_window_size() and check_auth() from ttyd's protocol.c
- * Technique: #include the source file so static functions are accessible.
+ * Exercises the full websocket lifecycle: connection filtering, auth,
+ * origin checking, message receive (INPUT, RESIZE, JSON_DATA, etc.),
+ * writable callbacks, and connection close/cleanup.
*
- * Input format: [has_credential:1][credential_len:2 BE][credential][json_message]
- * has_credential odd → server->credential is set to the credential bytes
- * has_credential even → no credential (open server)
+ * Input format:
+ * [flags:1][cred_len:2 BE][credential]
+ * [path_len:1][path][origin_len:1][origin][host_len:1][host]
+ * [ws_message...]
*
- * Build (handled by build_fuzzers.sh):
- * afl-clang-fast -fsanitize=address,undefined -g -O1
- * -I fuzz-include -I ttyd-src/src
- * fuzz_websocket_auth.c
- * ttyd-obj/{http,protocol,utils,fuzz_lws_mock,mock_server_globals,mock_pty_stubs}.o
- * -ljson-c -o fuzz_websocket_auth
+ * Flags byte:
+ * bit 0: has credential (Basic auth)
+ * bit 1: has auth_header (custom header proxy auth)
+ * bit 2: check_origin enabled
+ * bit 3: url_arg enabled
+ * bit 4: writable
*/
#include <stdio.h>
@@ -26,69 +28,166 @@
/* Mock lws control state — must come before the real ttyd source */
#include "fuzz_lws_mock.h"
-/* Pull in real ttyd protocol code (parse_window_size, check_auth static fns) */
+/* Pull in real ttyd protocol code (callback_tty and all static helpers) */
#include "ttyd-src/src/protocol.c"
/* Provided by mock_server_globals.c */
extern struct server _fuzz_server;
extern void fuzz_server_reset(void);
-/* ── fuzz target ─────────────────────────────────────────────────────────── */
+/* ── input parser ──────────────────────────────────────────────────── */
+
+/* Read a 1-byte length-prefixed field from data. Returns bytes consumed. */
+static size_t read_field_1(const uint8_t *data, size_t avail,
+ char *out, int *out_len, int max) {
+ if (avail < 1) { *out_len = 0; return 0; }
+ int flen = data[0];
+ if (flen > (int)(avail - 1)) flen = (int)(avail - 1);
+ if (flen > max) flen = max;
+ memcpy(out, data + 1, flen);
+ out[flen] = '\0';
+ *out_len = flen;
+ return 1 + flen;
+}
-int fuzz_target(const uint8_t *data, size_t size) {
- if (size < 3) return 0;
+/* ── fuzz target ───────────────────────────────────────────────────── */
- bool has_credential = (data[0] & 1) == 1;
- uint16_t cred_len = (uint16_t)((data[1] << 8) | data[2]);
- if ((size_t)cred_len > size - 3) cred_len = (uint16_t)(size - 3);
+int fuzz_target(const uint8_t *data, size_t size) {
+ /* Minimum: flags(1) + cred_len(2) + path_len(1) + origin_len(1) + host_len(1) */
+ if (size < 6) return 0;
+
+ /* Parse flags */
+ uint8_t flags = data[0];
+ bool has_credential = (flags & 0x01) != 0;
+ bool has_auth_header = (flags & 0x02) != 0;
+ bool check_origin = (flags & 0x04) != 0;
+ bool url_arg = (flags & 0x08) != 0;
+ bool writable = (flags & 0x10) != 0;
+
+ /* Parse credential */
+ uint16_t cred_len = (uint16_t)((data[1] << 8) | data[2]);
+ size_t off = 3;
+ if (cred_len > size - off) cred_len = (uint16_t)(size - off);
+ static char cred_buf[65536];
+ memcpy(cred_buf, data + off, cred_len);
+ cred_buf[cred_len] = '\0';
+ off += cred_len;
+
+ /* Parse length-prefixed fields: path, origin, host */
+ char path_buf[4096], origin_buf[4096], host_buf[4096];
+ int path_len = 0, origin_len = 0, host_len = 0;
+ size_t n;
+
+ n = read_field_1(data + off, size - off, path_buf, &path_len, 4095);
+ off += n;
+ n = read_field_1(data + off, size - off, origin_buf, &origin_len, 4095);
+ off += n;
+ n = read_field_1(data + off, size - off, host_buf, &host_len, 4095);
+ off += n;
+
+ /* Remaining bytes = websocket message payload */
+ const uint8_t *ws_msg = (off < size) ? data + off : NULL;
+ size_t ws_len = (off < size) ? size - off : 0;
+
+ /* ── Reset mock state ─────────────────────────────────────────── */
fuzz_lws_reset();
fuzz_server_reset();
- /* Configure server credential */
- static char cred_buf[65536];
+ /* Configure server */
+ _fuzz_server.writable = writable;
+ _fuzz_server.check_origin = check_origin;
+ _fuzz_server.url_arg = url_arg;
+ _fuzz_server.command = "bash";
+ _fuzz_server.prefs_json = "{}";
+ _fuzz_server.terminal_type[0] = '\0';
+ strncpy(_fuzz_server.terminal_type, "xterm-256color",
+ sizeof(_fuzz_server.terminal_type) - 1);
+
if (has_credential && cred_len > 0) {
- memcpy(cred_buf, data + 3, cred_len);
- cred_buf[cred_len] = '\0';
_fuzz_server.credential = cred_buf;
}
- /* lws auth header: supply the same credential bytes so the WS
- * check_auth() path (which uses lws_hdr_copy) can also be explored */
- fuzz_lws_set_auth((const char *)cred_buf, has_credential ? cred_len : 0);
-
- /* JSON message starts after the credential */
- size_t json_off = 3 + cred_len;
- if (json_off >= size) return 0;
-
- const char *json_data = (const char *)(data + json_off);
- size_t json_len = size - json_off;
-
- /* Exercise the real parse_window_size() from protocol.c */
- uint16_t cols = 0, rows = 0;
- struct json_object *obj = parse_window_size(json_data, (int)json_len,
- &cols, &rows);
- if (!obj) return 0;
-
- /* Exercise AuthToken validation when credential is configured */
- if (_fuzz_server.credential) {
- struct json_object *tok_obj = NULL;
- if (json_object_object_get_ex(obj, "AuthToken", &tok_obj)) {
- const char *token = json_object_get_string(tok_obj);
- if (token)
- (void)strcmp(token, _fuzz_server.credential);
- }
+ if (has_auth_header) {
+ _fuzz_server.auth_header = "X-Auth-User";
+ /* Put credential in custom header mock too */
+ fuzz_lws_set_custom(cred_buf, cred_len);
}
- /* Also exercise the WS check_auth() static function */
+ /* Set up fuzzer-controlled headers */
+ if (has_credential) {
+ fuzz_lws_set_auth(cred_buf, cred_len);
+ }
+ fuzz_lws_set_uri_path(path_buf, path_len);
+ fuzz_lws_set_origin(origin_buf, origin_len);
+ fuzz_lws_set_host(host_buf, host_len);
+
+ /* Set up URI args from the ws_msg if url_arg is enabled.
+ * We'll stuff "arg=<first 50 bytes of msg>" as a URI arg fragment
+ * to exercise the arg-parsing path in ESTABLISHED. */
+ if (url_arg && ws_len > 0) {
+ int arg_len = ws_len > 50 ? 50 : (int)ws_len;
+ char arg_buf[256];
+ memcpy(arg_buf, "arg=", 4);
+ memcpy(arg_buf + 4, ws_msg, arg_len);
+ arg_buf[4 + arg_len] = '\0';
+ memcpy(_fuzz_uri_args[0], arg_buf, 4 + arg_len + 1);
+ _fuzz_uri_args_len[0] = 4 + arg_len;
+ _fuzz_uri_args_count = 1;
+ }
+
+ /* ── Simulate WebSocket lifecycle ─────────────────────────────── */
struct lws mock_wsi = {0};
- struct pss_tty pss = {0};
- (void)check_auth(&mock_wsi, &pss);
+ struct pss_tty pss;
+ memset(&pss, 0, sizeof(pss));
+
+ /* Step 1: FILTER_PROTOCOL_CONNECTION (auth + path + origin check) */
+ int ret = callback_tty(&mock_wsi, LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION,
+ &pss, NULL, 0);
+ if (ret != 0) goto cleanup;
+
+ /* Step 2: ESTABLISHED (client setup, URL arg parsing) */
+ ret = callback_tty(&mock_wsi, LWS_CALLBACK_ESTABLISHED,
+ &pss, NULL, 0);
+ if (ret != 0) goto cleanup;
+
+ /* Step 3: SERVER_WRITEABLE (send initial messages) */
+ callback_tty(&mock_wsi, LWS_CALLBACK_SERVER_WRITEABLE, &pss, NULL, 0);
+
+ /* Step 4: RECEIVE with the fuzz payload */
+ if (ws_msg && ws_len > 0) {
+ /* Make a mutable copy — callback_tty may modify the buffer via
+ * xrealloc on pss->buffer, but the initial 'in' pointer is just read */
+ char *msg_copy = malloc(ws_len);
+ if (msg_copy) {
+ memcpy(msg_copy, ws_msg, ws_len);
+ ret = callback_tty(&mock_wsi, LWS_CALLBACK_RECEIVE,
+ &pss, msg_copy, ws_len);
+ free(msg_copy);
+
+ /* If RECEIVE succeeded and the command was JSON_DATA,
+ * try another WRITEABLE to exercise the post-auth path */
+ if (ret == 0) {
+ callback_tty(&mock_wsi, LWS_CALLBACK_SERVER_WRITEABLE,
+ &pss, NULL, 0);
+ }
+ }
+ }
+
+cleanup:
+ /* Step 5: CLOSED (cleanup buffers, args, etc.)
+ * Do NOT override pss.wsi here. ttyd only sets pss->wsi after a
+ * successful pty_spawn + LIST_INSERT_HEAD. If FILTER rejected the
+ * connection, or ESTABLISHED failed (spawn always returns -1 in the
+ * mock), pss->wsi is NULL and ttyd's CLOSED handler skips LIST_REMOVE —
+ * which is correct, since pss was never inserted. Forcing pss->wsi
+ * non-NULL here would make LIST_REMOVE deref a NULL le_prev and crash,
+ * producing harness artifacts rather than real bugs. */
+ callback_tty(&mock_wsi, LWS_CALLBACK_CLOSED, &pss, NULL, 0);
- json_object_put(obj);
return 0;
}
-/* ── AFL++ persistent mode ───────────────────────────────────────────────── */
+/* ── AFL++ persistent mode ───────────────────────────────────────────── */
#ifdef __AFL_FUZZ_TESTCASE_LEN
__AFL_FUZZ_INIT();
int main(void) {
diff --git a/fuzzing/ttyd/gen_corpus.py b/fuzzing/ttyd/gen_corpus.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env python3
+"""Generate seed corpus for TTYD fuzzing harnesses.
+
+Creates structured binary seeds in corpus/{websocket_auth,http_parsing}/.
+Run from the script directory or pass the output base dir as argv[1].
+
+Formats:
+ websocket_auth: [flags:1][cred_len:2 BE][cred][path_len:1][path]
+ [origin_len:1][origin][host_len:1][host][ws_message]
+ flags: bit0=has_cred, bit1=auth_header, bit2=check_origin,
+ bit3=url_arg, bit4=writable
+
+ http_parsing: [auth_mode:1][cred_len:2 BE][cred][path_len:2 BE][path][auth_header_value]
+"""
+
+import os
+import sys
+import struct
+
+base_dir = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(__file__))
+
+
+def write_seeds(subdir, seeds):
+ d = os.path.join(base_dir, "corpus", subdir)
+ os.makedirs(d, exist_ok=True)
+ for name, data in seeds.items():
+ with open(os.path.join(d, name), "wb") as f:
+ f.write(data)
+ print(f"[+] Generated {len(seeds)} seeds for {subdir}")
+
+
+# ── helpers ──────────────────────────────────────────────────────────────────
+
+def ws_seed(flags, cred, path, origin, host, ws_msg):
+ """websocket_auth format:
+ [flags:1][cred_len:2 BE][cred][path_len:1][path]
+ [origin_len:1][origin][host_len:1][host][ws_message]"""
+ return (struct.pack(">B", flags)
+ + struct.pack(">H", len(cred)) + cred
+ + struct.pack(">B", len(path)) + path
+ + struct.pack(">B", len(origin)) + origin
+ + struct.pack(">B", len(host)) + host
+ + ws_msg)
+
+
+def http_seed(mode, cred, path, auth=b""):
+ """http_parsing format: [mode:1][cred_len:2 BE][cred][path_len:2 BE][path][auth]"""
+ return (struct.pack(">B", mode)
+ + struct.pack(">H", len(cred)) + cred
+ + struct.pack(">H", len(path)) + path
+ + auth)
+
+
+# ── websocket_auth seeds ─────────────────────────────────────────────────────
+# Flags: 0x01=has_cred, 0x02=auth_header, 0x04=check_origin, 0x08=url_arg, 0x10=writable
+
+WS = b"/ws"
+ORIGIN = b"http://localhost:7681"
+HOST = b"localhost:7681"
+JSON_AUTH = b'{"columns":80,"rows":24,"AuthToken":"admin"}'
+JSON_DIMS = b'{"columns":80,"rows":24}'
+
+ws_seeds = {
+ # ── No auth, basic JSON_DATA command ──
+ "noauth_json": ws_seed(0x10, b"", WS, b"", b"", JSON_DIMS),
+ "noauth_json_large": ws_seed(0x10, b"", WS, b"", b"", b'{"columns":200,"rows":50}'),
+ "noauth_json_empty": ws_seed(0x10, b"", WS, b"", b"", b"{}"),
+ "noauth_json_malformed":ws_seed(0x10, b"", WS, b"", b"", b"{invalid json}"),
+
+ # ── With credential — JSON_DATA auth flow ──
+ "auth_valid": ws_seed(0x11, b"admin", WS, b"", b"", JSON_AUTH),
+ "auth_invalid": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'{"columns":80,"rows":24,"AuthToken":"wrong"}'),
+ "auth_missing_token": ws_seed(0x11, b"admin", WS, b"", b"", JSON_DIMS),
+ "auth_no_dims": ws_seed(0x11, b"admin", WS, b"", b"", b'{"AuthToken":"admin"}'),
+ "auth_empty_cred": ws_seed(0x01, b"", WS, b"", b"", JSON_DIMS),
+ "auth_token_null": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'{"columns":80,"rows":24,"AuthToken":null}'),
+ "auth_token_bool": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'{"columns":80,"rows":24,"AuthToken":true}'),
+ "auth_long_cred": ws_seed(0x11, b"A" * 200, WS, b"", b"",
+ b'{"AuthToken":"' + b"A" * 200 + b'"}'),
+
+ # ── Custom auth header (bit 1) ──
+ "custom_auth": ws_seed(0x12, b"someuser", WS, b"", b"", JSON_DIMS),
+ "custom_auth_empty": ws_seed(0x02, b"", WS, b"", b"", JSON_DIMS),
+
+ # ── INPUT command ('0' prefix) ──
+ "input_hello": ws_seed(0x10, b"", WS, b"", b"", b"0hello world"),
+ "input_ctrl_c": ws_seed(0x10, b"", WS, b"", b"", b"0\x03"),
+ "input_long": ws_seed(0x10, b"", WS, b"", b"", b"0" + b"A" * 4096),
+ "input_not_writable": ws_seed(0x00, b"", WS, b"", b"", b"0hello"),
+
+ # ── RESIZE command ('1' prefix) ──
+ "resize_normal": ws_seed(0x10, b"", WS, b"", b"",
+ b'1{"columns":120,"rows":40}'),
+ "resize_huge": ws_seed(0x10, b"", WS, b"", b"",
+ b'1{"columns":65535,"rows":65535}'),
+ "resize_negative": ws_seed(0x10, b"", WS, b"", b"",
+ b'1{"columns":-1,"rows":-1}'),
+
+ # ── PAUSE/RESUME commands ──
+ "pause": ws_seed(0x10, b"", WS, b"", b"", b"2"),
+ "resume": ws_seed(0x10, b"", WS, b"", b"", b"3"),
+
+ # ── Unknown command ──
+ "unknown_cmd": ws_seed(0x10, b"", WS, b"", b"", b"Xunknown"),
+
+ # ── Origin checking (bit 2) ──
+ "origin_match": ws_seed(0x14, b"", WS, ORIGIN, HOST, JSON_DIMS),
+ "origin_mismatch": ws_seed(0x14, b"", WS, b"http://evil.com", HOST, JSON_DIMS),
+ "origin_no_host": ws_seed(0x14, b"", WS, ORIGIN, b"", JSON_DIMS),
+ "origin_no_origin": ws_seed(0x14, b"", WS, b"", HOST, JSON_DIMS),
+ "origin_long": ws_seed(0x14, b"", WS,
+ b"http://" + b"A" * 200 + b":9999",
+ b"A" * 200 + b":9999", JSON_DIMS),
+ "origin_port_80": ws_seed(0x14, b"", WS,
+ b"http://localhost:80", b"localhost", JSON_DIMS),
+ "origin_port_443": ws_seed(0x14, b"", WS,
+ b"https://localhost:443", b"localhost", JSON_DIMS),
+
+ # ── URL args (bit 3) ──
+ "url_arg_enabled": ws_seed(0x18, b"", WS, b"", b"", b"0echo test"),
+ "url_arg_long": ws_seed(0x18, b"", WS, b"", b"", b"0" + b"X" * 250),
+
+ # ── Path variations ──
+ "path_wrong": ws_seed(0x10, b"", b"/wrong", b"", b"", JSON_DIMS),
+ "path_empty": ws_seed(0x10, b"", b"", b"", b"", JSON_DIMS),
+ "path_traversal": ws_seed(0x10, b"", b"/../ws", b"", b"", JSON_DIMS),
+ "path_long": ws_seed(0x10, b"", b"/" + b"A" * 200, b"", b"", JSON_DIMS),
+ "path_null": ws_seed(0x10, b"", b"/ws\x00extra", b"", b"", JSON_DIMS),
+
+ # ── Auth + origin combined ──
+ "auth_origin_valid": ws_seed(0x15, b"admin", WS, ORIGIN, HOST, JSON_AUTH),
+ "auth_origin_invalid": ws_seed(0x15, b"admin", WS, b"http://evil.com", HOST, JSON_AUTH),
+
+ # ── Empty / minimal ──
+ "empty_msg": ws_seed(0x10, b"", WS, b"", b"", b""),
+ "minimal": ws_seed(0x00, b"", b"", b"", b"", b""),
+
+ # ── JSON edge cases ──
+ "json_deep_nested": ws_seed(0x10, b"", WS, b"", b"",
+ b'{"a":' * 50 + b"1" + b"}" * 50),
+ "json_huge_token": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'{"columns":80,"rows":24,"AuthToken":"' + b"A" * 5000 + b'"}'),
+ "json_truncated": ws_seed(0x10, b"", WS, b"", b"", b'{"columns":'),
+ "json_extra_ws": ws_seed(0x10, b"", WS, b"", b"",
+ b'{ "columns" : 80 , "rows" : 24 }'),
+ "json_uint16_overflow": ws_seed(0x10, b"", WS, b"", b"",
+ b'{"columns":65536,"rows":65536}'),
+
+ # ── Authenticated commands (need JSON_DATA first to auth) ──
+ # Note: without auth, non-JSON_DATA commands with credential set get rejected
+ "unauth_input": ws_seed(0x11, b"admin", WS, b"", b"", b"0hello"),
+ "unauth_resize": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'1{"columns":80,"rows":24}'),
+}
+
+
+# ── http_parsing seeds ───────────────────────────────────────────────────────
+
+B64_CRED = b"YWRtaW46cGFzcw=="
+
+http_seeds = {
+ # No auth, various paths
+ "noauth_root": http_seed(0, b"", b"/"),
+ "noauth_token": http_seed(0, b"", b"/token"),
+ "noauth_ws": http_seed(0, b"", b"/ws"),
+ "noauth_parent": http_seed(0, b"", b"/base-path"),
+ "noauth_404": http_seed(0, b"", b"/nonexistent"),
+ "noauth_empty_path": http_seed(0, b"", b""),
+ # Path traversal
+ "path_traversal": http_seed(0, b"", b"/../../../etc/passwd"),
+ "path_double_slash": http_seed(0, b"", b"//etc/passwd"),
+ "path_dotdot": http_seed(0, b"", b"/./../../etc/shadow"),
+ "path_null": http_seed(0, b"", b"/\x00hidden"),
+ "path_encoded": http_seed(0, b"", b"/%2e%2e/%2e%2e/etc/passwd"),
+ "path_fmt": http_seed(0, b"", b"/%s%s%s%n"),
+ "path_crlf": http_seed(0, b"", b"/\r\nInjected: header"),
+ "path_long": http_seed(0, b"", b"/" + b"A" * 200),
+ # Basic auth — valid
+ "basic_valid": http_seed(1, B64_CRED, b"/", b"Basic " + B64_CRED),
+ "basic_token_path": http_seed(1, B64_CRED, b"/token", b"Basic " + B64_CRED),
+ # Basic auth — invalid
+ "basic_mismatch": http_seed(1, B64_CRED, b"/", b"Basic INVALIDBASE64=="),
+ "basic_empty": http_seed(1, B64_CRED, b"/", b"Basic "),
+ "basic_no_header": http_seed(1, B64_CRED, b"/", b""),
+ "basic_wrong_scheme": http_seed(1, B64_CRED, b"/", b"Bearer token123"),
+ "basic_lowercase": http_seed(1, B64_CRED, b"/", b"basic " + B64_CRED),
+ "basic_uppercase": http_seed(1, B64_CRED, b"/", b"BASIC " + B64_CRED),
+ "basic_dbl_space": http_seed(1, B64_CRED, b"/", b"Basic " + B64_CRED),
+ "basic_leading_sp": http_seed(1, B64_CRED, b"/", b" Basic " + B64_CRED),
+ "basic_no_cred": http_seed(1, b"", b"/", b"Basic " + B64_CRED),
+ # Boundary (256-byte buf in check_auth)
+ "basic_long_auth": http_seed(1, b"x:y", b"/", b"Basic " + b"A" * 300),
+ "basic_boundary_254": http_seed(1, b"x:y", b"/", b"Basic " + b"A" * 248),
+ "basic_boundary_256": http_seed(1, b"x:y", b"/", b"Basic " + b"A" * 250),
+ # Custom header auth
+ "custom_with_value": http_seed(2, b"", b"/", b"some-token-value"),
+ "custom_empty": http_seed(2, b"", b"/"),
+ "custom_token_path": http_seed(2, b"", b"/token", b"admin"),
+ # Format string in auth
+ "auth_fmt": http_seed(1, b"cred", b"/", b"Basic %s%s%s%n%n"),
+}
+
+
+# ── main ─────────────────────────────────────────────────────────────────────
+
+if __name__ == "__main__":
+ write_seeds("websocket_auth", ws_seeds)
+ write_seeds("http_parsing", http_seeds)
+
+ total = len(ws_seeds) + len(http_seeds)
+ print(f"[+] Total corpus: {total} files")
diff --git a/fuzzing/ttyd/http_mutator.c b/fuzzing/ttyd/http_mutator.c
@@ -0,0 +1,316 @@
+/*
+ * AFL++ Custom Mutator for TTYD HTTP fuzzing
+ *
+ * Understands the structured input format used by fuzz_http_parsing:
+ * [auth_mode:1][cred_len:2 BE][credential][path_len:2 BE][path][auth_header_value]
+ *
+ * Instead of random byte flips (which mostly break the format envelope),
+ * this mutator makes semantically meaningful changes:
+ * - Swaps URL paths to known ttyd endpoints or traversal payloads
+ * - Mutates auth header values (scheme, base64, truncation)
+ * - Toggles auth modes
+ * - Varies credential content and length
+ *
+ * Build:
+ * afl-clang-fast -shared -fPIC -O2 -o http_mutator.so http_mutator.c
+ *
+ * Use:
+ * AFL_CUSTOM_MUTATOR_LIBRARY=./http_mutator.so afl-fuzz ...
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+/* AFL++ types */
+typedef unsigned char u8;
+typedef uint32_t u32;
+typedef struct afl_state afl_state_t;
+
+/* ── mutator state ──────────────────────────────────────────────────────── */
+
+typedef struct {
+ u8 *out_buf;
+ size_t out_buf_size;
+ u32 seed;
+} http_mutator_t;
+
+/* ── interesting values ─────────────────────────────────────────────────── */
+
+static const char *interesting_paths[] = {
+ "/",
+ "/ws",
+ "/token",
+ "/base-path",
+ "/base-path/",
+ "/../../../etc/passwd",
+ "//etc/passwd",
+ "/./../.././etc/shadow",
+ "/%00/token",
+ "/token%00.html",
+ "/ws/../token",
+ "/%2e%2e/%2e%2e/etc/passwd",
+ "/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ "",
+ "/\r\nInjected: header",
+ "/\x00hidden",
+ "/%s%s%s%s%n",
+ "/token?arg=;id",
+ "/ws?arg=test&arg=another",
+ "///",
+};
+#define N_PATHS (sizeof(interesting_paths) / sizeof(interesting_paths[0]))
+
+static const char *interesting_auth[] = {
+ "Basic YWRtaW46cGFzcw==",
+ "Basic ",
+ "Basic AAAA",
+ "Basic ====",
+ "Basic YWRtaW46",
+ "Bearer token123",
+ "Digest username=\"admin\"",
+ "",
+ "Basic \x00hidden",
+ "BASIC YWRtaW46cGFzcw==",
+ "basic YWRtaW46cGFzcw==",
+ "Basic YWRtaW46cGFzcw==",
+ " Basic YWRtaW46cGFzcw==",
+ "Basic\tYWRtaW46cGFzcw==",
+ "Basic AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ "Basic %s%s%s%n%n",
+ "X-Auth-User: admin",
+};
+#define N_AUTH (sizeof(interesting_auth) / sizeof(interesting_auth[0]))
+
+static const char *interesting_creds[] = {
+ "admin:pass",
+ "YWRtaW46cGFzcw==",
+ "",
+ "AAAA",
+ "a]very:long:credential:with:many:colons",
+ "admin",
+ ":nouser",
+ "user:",
+ ":",
+};
+#define N_CREDS (sizeof(interesting_creds) / sizeof(interesting_creds[0]))
+
+/* ── helpers ────────────────────────────────────────────────────────────── */
+
+static u32 rand_next(http_mutator_t *m) {
+ m->seed = m->seed * 1103515245 + 12345;
+ return (m->seed >> 16) & 0x7fff;
+}
+
+static size_t build_input(u8 *out, size_t max_size,
+ u8 auth_mode, const char *cred,
+ const char *path, const char *auth_hdr) {
+ size_t cred_len = cred ? strlen(cred) : 0;
+ size_t path_len = path ? strlen(path) : 0;
+ size_t auth_len = auth_hdr ? strlen(auth_hdr) : 0;
+
+ /* Cap lengths to prevent overflow */
+ if (cred_len > 0xFFFF) cred_len = 0xFFFF;
+ if (path_len > 0xFFFF) path_len = 0xFFFF;
+
+ size_t total = 1 + 2 + cred_len + 2 + path_len + auth_len;
+ if (total > max_size) {
+ /* Trim auth_hdr to fit */
+ if (1 + 2 + cred_len + 2 + path_len >= max_size) return 0;
+ auth_len = max_size - 1 - 2 - cred_len - 2 - path_len;
+ total = max_size;
+ }
+
+ size_t off = 0;
+ out[off++] = auth_mode;
+ out[off++] = (u8)(cred_len >> 8);
+ out[off++] = (u8)(cred_len & 0xFF);
+ if (cred_len > 0) { memcpy(out + off, cred, cred_len); off += cred_len; }
+ out[off++] = (u8)(path_len >> 8);
+ out[off++] = (u8)(path_len & 0xFF);
+ if (path_len > 0) { memcpy(out + off, path, path_len); off += path_len; }
+ if (auth_len > 0) { memcpy(out + off, auth_hdr, auth_len); off += auth_len; }
+
+ return off;
+}
+
+/* Try to parse the structured format. Returns 0 on failure. */
+static int parse_input(const u8 *buf, size_t buf_size,
+ u8 *auth_mode, const u8 **cred, size_t *cred_len,
+ const u8 **path, size_t *path_len,
+ const u8 **auth_hdr, size_t *auth_hdr_len) {
+ if (buf_size < 5) return 0;
+
+ *auth_mode = buf[0];
+ *cred_len = (size_t)((buf[1] << 8) | buf[2]);
+ size_t off = 3;
+
+ if (*cred_len > buf_size - off - 2) return 0;
+ *cred = buf + off;
+ off += *cred_len;
+
+ if (off + 2 > buf_size) return 0;
+ *path_len = (size_t)((buf[off] << 8) | buf[off + 1]);
+ off += 2;
+
+ if (*path_len > buf_size - off) return 0;
+ *path = buf + off;
+ off += *path_len;
+
+ *auth_hdr = buf + off;
+ *auth_hdr_len = buf_size - off;
+
+ return 1;
+}
+
+/* ── AFL++ custom mutator API ───────────────────────────────────────────── */
+
+void *afl_custom_init(afl_state_t *afl, unsigned int seed) {
+ http_mutator_t *m = calloc(1, sizeof(http_mutator_t));
+ if (!m) return NULL;
+ m->out_buf_size = 1 << 16;
+ m->out_buf = malloc(m->out_buf_size);
+ if (!m->out_buf) { free(m); return NULL; }
+ m->seed = seed;
+ return m;
+}
+
+size_t afl_custom_fuzz(void *data, u8 *buf, size_t buf_size,
+ u8 **out_buf, u8 *add_buf, size_t add_buf_size,
+ size_t max_size) {
+ http_mutator_t *m = (http_mutator_t *)data;
+
+ if (max_size > m->out_buf_size) {
+ m->out_buf = realloc(m->out_buf, max_size);
+ if (!m->out_buf) { m->out_buf_size = 0; *out_buf = buf; return buf_size; }
+ m->out_buf_size = max_size;
+ }
+
+ /* Parse the existing input */
+ u8 auth_mode;
+ const u8 *cred, *path, *auth_hdr;
+ size_t cred_len, path_len, auth_hdr_len;
+
+ int parsed = parse_input(buf, buf_size, &auth_mode, &cred, &cred_len,
+ &path, &path_len, &auth_hdr, &auth_hdr_len);
+
+ /* Copy current fields into mutable buffers */
+ static char m_cred[4096], m_path[4096], m_auth[4096];
+ u8 m_mode;
+
+ if (parsed) {
+ m_mode = auth_mode % 3;
+ size_t n;
+ n = cred_len < sizeof(m_cred) - 1 ? cred_len : sizeof(m_cred) - 1;
+ memcpy(m_cred, cred, n); m_cred[n] = '\0';
+ n = path_len < sizeof(m_path) - 1 ? path_len : sizeof(m_path) - 1;
+ memcpy(m_path, path, n); m_path[n] = '\0';
+ n = auth_hdr_len < sizeof(m_auth) - 1 ? auth_hdr_len : sizeof(m_auth) - 1;
+ memcpy(m_auth, auth_hdr, n); m_auth[n] = '\0';
+ } else {
+ /* Can't parse — generate a fresh valid input */
+ m_mode = rand_next(m) % 3;
+ strcpy(m_path, interesting_paths[rand_next(m) % N_PATHS]);
+ strcpy(m_auth, interesting_auth[rand_next(m) % N_AUTH]);
+ strcpy(m_cred, interesting_creds[rand_next(m) % N_CREDS]);
+ }
+
+ /* Pick a mutation strategy */
+ u32 strategy = rand_next(m) % 8;
+
+ switch (strategy) {
+ case 0:
+ /* Replace path with an interesting one */
+ strcpy(m_path, interesting_paths[rand_next(m) % N_PATHS]);
+ break;
+
+ case 1:
+ /* Replace auth header with an interesting one */
+ strcpy(m_auth, interesting_auth[rand_next(m) % N_AUTH]);
+ break;
+
+ case 2:
+ /* Toggle auth mode */
+ m_mode = rand_next(m) % 3;
+ if (m_mode == 1 && m_cred[0] == '\0')
+ strcpy(m_cred, interesting_creds[rand_next(m) % N_CREDS]);
+ break;
+
+ case 3:
+ /* Replace credential */
+ strcpy(m_cred, interesting_creds[rand_next(m) % N_CREDS]);
+ break;
+
+ case 4: {
+ /* Byte-level mutation on the path */
+ size_t plen = strlen(m_path);
+ if (plen > 0) {
+ size_t pos = rand_next(m) % plen;
+ m_path[pos] ^= (1 << (rand_next(m) % 8));
+ }
+ break;
+ }
+
+ case 5: {
+ /* Byte-level mutation on the auth header */
+ size_t alen = strlen(m_auth);
+ if (alen > 0) {
+ size_t pos = rand_next(m) % alen;
+ m_auth[pos] ^= (1 << (rand_next(m) % 8));
+ }
+ break;
+ }
+
+ case 6: {
+ /* Splice path from add_buf if available */
+ if (add_buf && add_buf_size >= 5) {
+ u8 am2; const u8 *c2, *p2, *a2;
+ size_t cl2, pl2, al2;
+ if (parse_input(add_buf, add_buf_size, &am2, &c2, &cl2,
+ &p2, &pl2, &a2, &al2) && pl2 > 0) {
+ size_t n = pl2 < sizeof(m_path) - 1 ? pl2 : sizeof(m_path) - 1;
+ memcpy(m_path, p2, n);
+ m_path[n] = '\0';
+ }
+ }
+ break;
+ }
+
+ case 7: {
+ /* Mutate everything at once — max chaos while keeping structure */
+ m_mode = rand_next(m) % 3;
+ strcpy(m_path, interesting_paths[rand_next(m) % N_PATHS]);
+ strcpy(m_auth, interesting_auth[rand_next(m) % N_AUTH]);
+ if (m_mode == 1)
+ strcpy(m_cred, interesting_creds[rand_next(m) % N_CREDS]);
+ break;
+ }
+ }
+
+ /* Rebuild the structured output */
+ size_t out_len = build_input(m->out_buf, max_size,
+ m_mode, m_cred, m_path, m_auth);
+ if (out_len == 0) {
+ /* Fallback: pass through original */
+ *out_buf = buf;
+ return buf_size;
+ }
+
+ *out_buf = m->out_buf;
+ return out_len;
+}
+
+void afl_custom_deinit(void *data) {
+ http_mutator_t *m = (http_mutator_t *)data;
+ if (m) {
+ free(m->out_buf);
+ free(m);
+ }
+}
diff --git a/fuzzing/ttyd/mock_pty_stubs.c b/fuzzing/ttyd/mock_pty_stubs.c
@@ -0,0 +1,26 @@
+/* Stub implementations of PTY functions used by ttyd's protocol.c.
+ * All functions are no-ops so no real processes are ever launched during
+ * fuzzing. Signatures match pty.h exactly. */
+
+#include "fuzz-include/libwebsockets.h"
+#include "fuzz-include/uv.h"
+#include "ttyd-src/src/server.h"
+#include "ttyd-src/src/pty.h"
+#include <stdlib.h>
+#include <string.h>
+
+pty_buf_t *pty_buf_init(char *base, size_t len) { return NULL; }
+void pty_buf_free(pty_buf_t *buf) {}
+pty_process *process_init(void *ctx, uv_loop_t *loop,
+ char *argv[], char *envp[]) { pty_process *p = calloc(1, sizeof(pty_process)); if (p) p->ctx = ctx; return p; }
+bool process_running(pty_process *process) { return false; }
+void process_free(pty_process *process) { free(process); }
+int pty_spawn(pty_process *process,
+ pty_read_cb read_cb,
+ pty_exit_cb exit_cb) { return 0; }
+void pty_pause(pty_process *process) {}
+void pty_resume(pty_process *process) {}
+int pty_write(pty_process *process,
+ pty_buf_t *buf) { return -1; }
+bool pty_resize(pty_process *process) { return false; }
+bool pty_kill(pty_process *process, int sig) { return false; }
diff --git a/fuzzing/ttyd/mock_server_globals.c b/fuzzing/ttyd/mock_server_globals.c
@@ -0,0 +1,27 @@
+/* Provides globals that ttyd's http.c and protocol.c reference.
+ * Normally defined in main.c; here we provide stub definitions.
+ * Fuzzing harnesses call fuzz_server_reset() before each iteration. */
+
+#include "fuzz-include/libwebsockets.h"
+#include "fuzz-include/uv.h"
+#include "ttyd-src/src/server.h"
+#include <string.h>
+
+/* Globals declared extern in server.h */
+struct server *server = NULL;
+struct lws_context *context = NULL;
+volatile bool force_exit = false;
+struct endpoints endpoints = {"/ws", "/", "/token", "/"};
+
+/* Fuzzer-private server instance */
+struct server _fuzz_server;
+
+void fuzz_server_reset(void) {
+ memset(&_fuzz_server, 0, sizeof(_fuzz_server));
+ _fuzz_server.writable = true;
+ _fuzz_server.check_origin = false;
+ _fuzz_server.max_clients = 0;
+ server = &_fuzz_server;
+ context = NULL;
+ force_exit = false;
+}
diff --git a/fuzzing/ttyd/run-fuzzers.sh b/fuzzing/ttyd/run-fuzzers.sh
@@ -1,7 +1,7 @@
#!/bin/bash
-# Run all TTYD fuzzers in tmux sessions
-# Supports: dictionaries, CmpLog, parallel fuzzing
-# Safe to detach from SSH - sessions persist in background
+# Run all TTYD fuzzers in tmux
+# Two visible panes (one main per target), cmplog + secondaries run headless
+# Safe to detach from SSH - everything persists in background
set -e
@@ -20,7 +20,7 @@ if ! command -v afl-fuzz &> /dev/null; then
fi
# Check that harnesses are built
-for harness in fuzz_auth_header fuzz_websocket_auth fuzz_http_parsing; do
+for harness in fuzz_websocket_auth fuzz_http_parsing; do
if [ ! -x "$SCRIPT_DIR/$harness" ]; then
echo "[!] $harness not found. Run ./build_fuzzers.sh first."
exit 1
@@ -28,7 +28,7 @@ for harness in fuzz_auth_header fuzz_websocket_auth fuzz_http_parsing; do
done
# Generate corpus if missing
-for dir in auth_header websocket_auth http_parsing; do
+for dir in websocket_auth http_parsing; do
if [ ! -d "$SCRIPT_DIR/corpus/$dir" ] || [ -z "$(ls -A "$SCRIPT_DIR/corpus/$dir" 2>/dev/null)" ]; then
echo "[*] Corpus missing. Run ./build_fuzzers.sh first to generate it."
exit 1
@@ -45,125 +45,108 @@ for candidate in /usr/share/afl++/dictionaries /usr/local/share/afl++/dictionari
done
if [ -z "$DICT_DIR" ]; then
- echo "[!] Warning: No AFL++ dictionaries directory found. Running without dictionaries."
- echo " Expected at: /usr/share/afl++/dictionaries/"
+ echo "[!] Warning: No AFL++ dictionaries directory found."
fi
# Calculate core allocation
TOTAL_CORES=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
-CORES_PER_TARGET=$((TOTAL_CORES / 3))
+CORES_PER_TARGET=$((TOTAL_CORES / 2))
[ "$CORES_PER_TARGET" -lt 1 ] && CORES_PER_TARGET=1
echo "[*] System: $TOTAL_CORES cores, $CORES_PER_TARGET per target"
# Create output directories
-mkdir -p "$SCRIPT_DIR/output/auth_header" "$SCRIPT_DIR/output/websocket_auth" "$SCRIPT_DIR/output/http_parsing"
+mkdir -p "$SCRIPT_DIR/output/websocket_auth" "$SCRIPT_DIR/output/http_parsing"
# Kill existing session if running
tmux kill-session -t "$SESSION" 2>/dev/null || true
-echo "[*] Starting fuzzing sessions in tmux session: $SESSION"
+echo "[*] Starting fuzzing in tmux session: $SESSION"
echo
-# Build afl-fuzz command for a given target
-# Usage: build_afl_cmd <harness_name> <corpus_dir> <output_dir> <dict_file> <node_type> [extra_flags]
+# Build afl-fuzz command
build_afl_cmd() {
- local harness=$1
- local corpus=$2
- local output=$3
- local dict=$4
- local node_flag=$5
+ local harness=$1 corpus=$2 output=$3 dict=$4 node_flag=$5
shift 5
- local extra_flags="$*"
-
+ local extra="$*"
local cmd="afl-fuzz -i $corpus -o $output $node_flag"
-
- # Add dictionary if available
- if [ -n "$DICT_DIR" ] && [ -f "$DICT_DIR/$dict" ]; then
- cmd="$cmd -x $DICT_DIR/$dict"
- fi
-
- # Add any extra flags (e.g., -c for CmpLog)
- if [ -n "$extra_flags" ]; then
- cmd="$cmd $extra_flags"
- fi
-
+ [ -n "$DICT_DIR" ] && [ -f "$DICT_DIR/$dict" ] && cmd="$cmd -x $DICT_DIR/$dict"
+ [ -n "$extra" ] && cmd="$cmd $extra"
cmd="$cmd -- ./$harness"
echo "$cmd"
}
-# Launch a fuzzing campaign for one target
-# Usage: launch_target <window_name> <harness_name> <corpus_dir_name> <dict_file>
-launch_target() {
- local window_name=$1
- local harness=$2
- local corpus_dir=$3
- local dict=$4
- local output_dir="output/$corpus_dir"
- local corpus_path="corpus/$corpus_dir"
- local has_cmplog=false
-
- if [ -x "$SCRIPT_DIR/${harness}_cmplog" ]; then
- has_cmplog=true
- fi
+# Custom mutator for http_parsing
+HTTP_ENV=""
+if [ -f "$SCRIPT_DIR/http_mutator.so" ]; then
+ HTTP_ENV="AFL_CUSTOM_MUTATOR_LIBRARY=$SCRIPT_DIR/http_mutator.so"
+ echo "[+] Using custom HTTP mutator for http_parsing"
+fi
- # Master node
- local master_cmd
- master_cmd=$(build_afl_cmd "$harness" "$corpus_path" "$output_dir" "$dict" "-M main")
-
- if [ "$window_name" = "auth_header" ]; then
- # First window - create the session
- tmux new-session -d -s "$SESSION" -n "$window_name" \
- "cd '$SCRIPT_DIR' && $master_cmd; read"
- else
- tmux new-window -t "$SESSION" -n "$window_name" \
- "cd '$SCRIPT_DIR' && $master_cmd; read"
- fi
+# ── Launch headless background nodes ──────────────────────────────────────────
+# These run without a UI pane — they sync via the shared output dir
+BG_PIDS=()
+
+launch_bg() {
+ local env_prefix="$1"
+ local cmd="$2"
+ local logfile="$3"
+ (cd "$SCRIPT_DIR" && AFL_NO_UI=1 $env_prefix $cmd > "$logfile" 2>&1) &
+ BG_PIDS+=($!)
+}
+
+bg_count=0
- local node_count=1
+for target_info in "fuzz_websocket_auth|websocket_auth|json.dict|" "fuzz_http_parsing|http_parsing|http.dict|$HTTP_ENV"; do
+ IFS='|' read -r harness corpus_dir dict env_prefix <<< "$target_info"
+ output="output/$corpus_dir"
+ corpus="corpus/$corpus_dir"
+ logdir="$SCRIPT_DIR/output/$corpus_dir"
- # CmpLog node (if cmplog binary exists)
- if $has_cmplog && [ "$CORES_PER_TARGET" -ge 2 ]; then
- local cmplog_cmd
- cmplog_cmd=$(build_afl_cmd "$harness" "$corpus_path" "$output_dir" "$dict" "-S cmplog" "-c ./${harness}_cmplog")
- tmux split-window -h -t "$SESSION:$window_name" \
- "cd '$SCRIPT_DIR' && $cmplog_cmd; read"
- node_count=$((node_count + 1))
+ # CmpLog secondary (if binary exists and we have cores)
+ if [ -x "$SCRIPT_DIR/${harness}_cmplog" ] && [ "$CORES_PER_TARGET" -ge 2 ]; then
+ cmd=$(build_afl_cmd "$harness" "$corpus" "$output" "$dict" "-S cmplog" "-c ./${harness}_cmplog")
+ launch_bg "$env_prefix" "$cmd" "$logdir/cmplog.log"
+ bg_count=$((bg_count + 1))
fi
- # Additional secondary nodes if cores permit
- local remaining=$((CORES_PER_TARGET - node_count))
+ # Extra secondaries
+ used=2 # main + cmplog
+ remaining=$((CORES_PER_TARGET - used))
for i in $(seq 1 $remaining); do
- local slave_cmd
- slave_cmd=$(build_afl_cmd "$harness" "$corpus_path" "$output_dir" "$dict" "-S slave_$i")
- tmux split-window -v -t "$SESSION:$window_name" \
- "cd '$SCRIPT_DIR' && $slave_cmd; read"
+ cmd=$(build_afl_cmd "$harness" "$corpus" "$output" "$dict" "-S sec_$i")
+ launch_bg "$env_prefix" "$cmd" "$logdir/sec_$i.log"
+ bg_count=$((bg_count + 1))
done
+done
- echo "[+] $window_name: master + $([ $has_cmplog = true ] && [ $CORES_PER_TARGET -ge 2 ] && echo "cmplog + " || true)$((CORES_PER_TARGET - node_count)) secondary nodes"
-}
+echo "[*] Launched $bg_count headless secondary nodes"
-# Launch the three campaigns
-launch_target "auth_header" "fuzz_auth_header" "auth_header" "http.dict"
-launch_target "websocket_auth" "fuzz_websocket_auth" "websocket_auth" "json.dict"
-launch_target "http_parsing" "fuzz_http_parsing" "http_parsing" "http.dict"
+# ── Create tmux: 2 panes (main nodes with UI) ────────────────────────────────
+ws_main=$(build_afl_cmd "fuzz_websocket_auth" "corpus/websocket_auth" "output/websocket_auth" "json.dict" "-M main")
+http_main=$(build_afl_cmd "fuzz_http_parsing" "corpus/http_parsing" "output/http_parsing" "http.dict" "-M main")
-# Add a status window
+tmux new-session -d -s "$SESSION" -n "fuzzers" \
+ "cd '$SCRIPT_DIR' && $ws_main; echo '[!] Fuzzer exited'; read"
+tmux split-window -h -t "$SESSION:fuzzers" \
+ "cd '$SCRIPT_DIR' && $HTTP_ENV $http_main; echo '[!] Fuzzer exited'; read"
+
+# ── Status window ─────────────────────────────────────────────────────────────
tmux new-window -t "$SESSION" -n "status" \
- "cd '$SCRIPT_DIR' && watch -n 5 'afl-whatsup -s output/ 2>/dev/null || echo \"=== Auth Header ===\"; cat output/auth_header/main/fuzzer_stats 2>/dev/null | grep -E \"(execs_done|execs_per_sec|saved_crashes|saved_hangs|last_find)\"; echo; echo \"=== WebSocket Auth ===\"; cat output/websocket_auth/main/fuzzer_stats 2>/dev/null | grep -E \"(execs_done|execs_per_sec|saved_crashes|saved_hangs|last_find)\"; echo; echo \"=== HTTP Parsing ===\"; cat output/http_parsing/main/fuzzer_stats 2>/dev/null | grep -E \"(execs_done|execs_per_sec|saved_crashes|saved_hangs|last_find)\"'"
+ "cd '$SCRIPT_DIR' && watch -n 10 'echo \"=== WebSocket Auth ===\"; echo; afl-whatsup -s output/websocket_auth 2>&1; echo; echo \"=== HTTP Parsing ===\"; echo; afl-whatsup -s output/http_parsing 2>&1'"
+
+# Select fuzzers window
+tmux select-window -t "$SESSION:fuzzers"
-# Select first window
-tmux select-window -t "$SESSION:0"
+# Save background PIDs so clean.sh can kill them
+printf '%s\n' "${BG_PIDS[@]}" > "$SCRIPT_DIR/.bg_fuzz_pids"
echo
echo "[+] All fuzzers started in tmux session: $SESSION"
+echo " 2 main nodes (visible) + $bg_count headless secondaries"
echo
echo "Commands:"
echo " tmux attach -t $SESSION # Attach to session"
-echo " tmux select-window -t $SESSION:0 # auth_header"
-echo " tmux select-window -t $SESSION:1 # websocket_auth"
-echo " tmux select-window -t $SESSION:2 # http_parsing"
-echo " tmux select-window -t $SESSION:3 # status overview"
echo " Ctrl-b d # Detach (fuzzers keep running)"
echo " tmux kill-session -t $SESSION # Stop all fuzzers"
echo
| | | | | | | | | | | | | | | |