diff --git a/fuzzing/ttyd/build_fuzzers.sh b/fuzzing/ttyd/build_fuzzers.sh
@@ -1,170 +1,1028 @@
#!/bin/bash
-# Build script for TTYD authentication fuzzing harnesses
+# Build TTYD fuzzing harnesses
+#
+# What this script does:
+# 1. Installs build dependencies
+# 2. Clones ttyd source + runs cmake (for validation & compile_commands.json)
+# 3. Generates mock headers/stubs so ttyd's .c files compile without real lws/uv
+# 4. Compiles instrumented ttyd objects (http.c, protocol.c, utils.c)
+# 5. Builds AFL++ harnesses that #include the real ttyd source files
+# 6. Sets up AFL++ dictionaries (download from AFL++ repo, fall back to custom)
+# 7. Generates seed corpus
+# 8. Installs a crontab entry to back up crashes every 15 minutes
+#
+# Usage: ./build_fuzzers.sh [afl|libfuzzer]
+#
+# Note: gen-corpus.sh is merged into this script and is no longer separate.
set -e
-MODE="${1:-afl}" # afl or libfuzzer
-SANITIZER="address"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+MODE="${1:-afl}"
+SANITIZER="address,undefined"
+
+TTYD_SRC="$SCRIPT_DIR/ttyd-src"
+TTYD_OBJ="$SCRIPT_DIR/ttyd-obj"
+FUZZ_INCLUDE="$SCRIPT_DIR/fuzz-include"
+CRONTAB_MARKER="# ttyd-fuzz-crash-backup"
+BACKUP_SCRIPT="$SCRIPT_DIR/backup_crashes.sh"
echo "[*] Building TTYD fuzzing harnesses"
-echo "[*] Mode: $MODE"
+echo "[*] Mode: $MODE"
echo "[*] Sanitizer: $SANITIZER"
+echo "[*] Script: $SCRIPT_DIR"
echo
-# Install dependencies if missing (requires root)
-if [ "$(id -u)" -eq 0 ]; then
- DEPS_NEEDED=""
- dpkg -s libjson-c-dev &>/dev/null || DEPS_NEEDED="$DEPS_NEEDED libjson-c-dev"
- dpkg -s afl++ &>/dev/null || DEPS_NEEDED="$DEPS_NEEDED afl++"
- if [ -n "$DEPS_NEEDED" ]; then
- echo "[*] Installing missing dependencies:$DEPS_NEEDED"
- apt-get install -y $DEPS_NEEDED
+# ─── 1. dependency install ────────────────────────────────────────────────────
+
+install_deps() {
+ if [ "$(id -u)" -ne 0 ]; then
+ echo "[!] Warning: Not running as root. Skipping apt-get installs."
+ echo " If builds fail, run: sudo apt-get install afl++ libjson-c-dev"
+ echo " cmake git libwebsockets-dev libuv1-dev pkg-config"
+ return
+ fi
+
+ local DEPS="libjson-c-dev cmake git pkg-config"
+ if [ "$MODE" = "afl" ]; then
+ DEPS="$DEPS afl++"
+ else
+ DEPS="$DEPS clang"
+ fi
+
+ local MISSING=""
+ for pkg in $DEPS; do
+ dpkg -s "$pkg" &>/dev/null || MISSING="$MISSING $pkg"
+ done
+
+ if [ -n "$MISSING" ]; then
+ echo "[*] Installing:$MISSING"
+ apt-get install -y --no-install-recommends $MISSING
echo "[+] Dependencies installed"
- echo
fi
-fi
-# System setup for AFL++ (requires root)
-if [ "$MODE" == "afl" ] && [ "$(id -u)" -eq 0 ]; then
- echo "[*] Configuring system for AFL++..."
+ # System tuning for AFL++
+ if [ "$MODE" = "afl" ]; then
+ local CORE_PATTERN
+ CORE_PATTERN=$(cat /proc/sys/kernel/core_pattern 2>/dev/null || echo "")
+ if [[ "$CORE_PATTERN" == "|"* ]]; then
+ echo core > /proc/sys/kernel/core_pattern
+ echo "[+] core_pattern set to 'core'"
+ fi
+ for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
+ [ -f "$gov" ] && echo performance > "$gov" 2>/dev/null || true
+ done
+ fi
+}
+
+# ─── 2. clone + cmake ttyd ────────────────────────────────────────────────────
- # Fix core_pattern for crash detection
- CORE_PATTERN=$(cat /proc/sys/kernel/core_pattern)
- if [[ "$CORE_PATTERN" == "|"* ]]; then
- echo "[*] Setting core_pattern to 'core'..."
- echo core > /proc/sys/kernel/core_pattern
- echo "[+] core_pattern set"
+setup_ttyd_source() {
+ # Clone
+ if [ ! -d "$TTYD_SRC/.git" ]; then
+ echo "[*] Cloning ttyd source..."
+ git clone --depth=1 https://github.com/tsl0922/ttyd "$TTYD_SRC"
+ echo "[+] Cloned ttyd to $TTYD_SRC"
else
- echo "[+] core_pattern already OK: $CORE_PATTERN"
+ echo "[*] ttyd source already present — pulling latest..."
+ git -C "$TTYD_SRC" pull --ff-only 2>/dev/null || \
+ echo "[!] Pull skipped (uncommitted local changes or offline)"
fi
- # Set CPU governor to performance
- if [ -d /sys/devices/system/cpu/cpu0/cpufreq ]; then
- CUR_GOV=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || true)
- if [ "$CUR_GOV" != "performance" ]; then
- echo "[*] Setting CPU governor to performance (was: $CUR_GOV)..."
- for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
- echo performance > "$gov" 2>/dev/null || true
- done
- echo "[+] CPU governor set"
- else
- echo "[+] CPU governor already set to performance"
- fi
+ # cmake configure (validates deps, produces compile_commands.json for IDEs)
+ local BUILD_DIR="$TTYD_SRC/build"
+ mkdir -p "$BUILD_DIR"
+ echo "[*] Running cmake configure..."
+ (
+ cd "$BUILD_DIR"
+ # Use the real compiler here (not AFL++) — this is only for validation/headers
+ cmake -DCMAKE_BUILD_TYPE=Debug \
+ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
+ .. 2>&1 | tail -5 || true
+ )
+ 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 NULL; }
+bool process_running(pty_process *process) { return false; }
+void process_free(pty_process *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"
+
+ # ── compile instrumented ttyd objects ────────────────────────────────
+ mkdir -p "$TTYD_OBJ"
+ local JSON_CFLAGS
+ JSON_CFLAGS=$(pkg-config --cflags json-c 2>/dev/null || echo "")
+
+ local OBJ_CC OBJ_CFLAGS
+ if [ "$MODE" = "afl" ]; then
+ OBJ_CC="afl-clang-fast"
+ else
+ OBJ_CC="clang"
fi
+ OBJ_CFLAGS="-fsanitize=$SANITIZER -g -O1 -D_GNU_SOURCE -std=gnu99"
+ OBJ_CFLAGS="$OBJ_CFLAGS -I$FUZZ_INCLUDE -I$TTYD_SRC/src $JSON_CFLAGS"
+ # Suppress warnings from upstream code we don't control
+ OBJ_CFLAGS="$OBJ_CFLAGS -Wno-implicit-function-declaration -Wno-int-conversion"
echo
-elif [ "$MODE" == "afl" ] && [ "$(id -u)" -ne 0 ]; then
- echo "[!] Warning: Not running as root. System tuning skipped."
- echo " Run with sudo for AFL++ system setup (core_pattern, CPU governor)."
- echo
-fi
+ echo "[*] Compiling instrumented ttyd objects..."
+ # Only utils.c is pre-compiled as a shared object.
+ # http.c and protocol.c are compiled directly into each harness via
+ # #include — pre-compiling them here would cause multiple-definition
+ # linker errors.
+ for src in utils; do
+ echo " $src.c → ttyd-obj/$src.o"
+ $OBJ_CC $OBJ_CFLAGS -c "$TTYD_SRC/src/$src.c" -o "$TTYD_OBJ/$src.o"
+ done
+
+ # Mock objects
+ for src in fuzz_lws_mock mock_server_globals mock_pty_stubs; do
+ echo " $src.c → ttyd-obj/$src.o"
+ $OBJ_CC $OBJ_CFLAGS -c "$SCRIPT_DIR/$src.c" -o "$TTYD_OBJ/$src.o"
+ done
+
+ echo "[+] ttyd objects compiled to $TTYD_OBJ/"
+}
+
+# ─── 3. dictionaries ──────────────────────────────────────────────────────────
+
+setup_dictionaries() {
+ local DICT_DIR="$SCRIPT_DIR/dictionaries"
+ mkdir -p "$DICT_DIR"
+
+ # Try system AFL++ dictionaries first
+ for sys_dir in /usr/share/afl++/dictionaries /usr/local/share/afl++/dictionaries; do
+ if [ -d "$sys_dir" ]; then
+ [ -f "$sys_dir/http.dict" ] && [ ! -f "$DICT_DIR/http.dict" ] && \
+ cp "$sys_dir/http.dict" "$DICT_DIR/http.dict" && \
+ echo "[+] Copied http.dict from $sys_dir"
+ [ -f "$sys_dir/json.dict" ] && [ ! -f "$DICT_DIR/json.dict" ] && \
+ cp "$sys_dir/json.dict" "$DICT_DIR/json.dict" && \
+ echo "[+] Copied json.dict from $sys_dir"
+ break
+ fi
+ done
+
+ # Download from AFL++ GitHub if still missing (http.dict and json.dict
+ # ship under those exact names in the AFL++ stable branch)
+ local BASE="https://raw.githubusercontent.com/AFLplusplus/AFLplusplus/stable/dictionaries"
+ local DL=""
+ command -v wget &>/dev/null && DL="wget -q -O"
+ command -v curl &>/dev/null && [ -z "$DL" ] && DL="curl -fsSL -o"
+
+ for dict in http json; do
+ if [ ! -f "$DICT_DIR/$dict.dict" ] && [ -n "$DL" ]; then
+ echo "[*] Downloading $dict.dict from AFL++ repo..."
+ $DL "$DICT_DIR/$dict.dict" "$BASE/$dict.dict" && \
+ echo "[+] Downloaded $dict.dict" || \
+ rm -f "$DICT_DIR/$dict.dict"
+ 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
+ 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
+ echo "[+] Created dictionaries/json.dict (fallback)"
+ fi
+
+ echo "[+] Dictionaries: $(ls "$DICT_DIR")"
+}
+
+# ─── 4. corpus generation (formerly gen-corpus.sh) ───────────────────────────
+
+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"
+}
+
+# ─── 5. crash backup crontab ──────────────────────────────────────────────────
+
+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
+ chmod +x "$BACKUP_SCRIPT"
+ echo "[+] Created $BACKUP_SCRIPT"
+
+ # Install crontab entry (idempotent — remove stale entry first)
+ local TMPFILE
+ TMPFILE=$(mktemp)
+ crontab -l 2>/dev/null | grep -v "$CRONTAB_MARKER" | grep -v "backup_crashes.sh" > "$TMPFILE" || true
+ {
+ cat "$TMPFILE"
+ echo "$CRONTAB_MARKER"
+ echo "*/15 * * * * $BACKUP_SCRIPT >> $SCRIPT_DIR/crashes-backup/backup.log 2>&1"
+ } | crontab -
+ rm -f "$TMPFILE"
+ echo "[+] Crontab entry installed (every 15 min)"
+ crontab -l | grep -A1 "$CRONTAB_MARKER"
+}
+
+# ─── 6. select compiler ───────────────────────────────────────────────────────
case "$MODE" in
afl)
- echo "[*] Building with AFL++"
-
- # Check if AFL++ is installed
- if ! command -v afl-clang-fast &> /dev/null; then
- echo "[!] AFL++ not found. Please install AFL++:"
- echo " sudo apt-get install afl++"
- echo " Or: https://github.com/AFLplusplus/AFLplusplus"
+ if ! command -v afl-clang-fast &>/dev/null; then
+ echo "[!] afl-clang-fast not found. Install AFL++ first."
exit 1
fi
-
CC="afl-clang-fast"
CFLAGS="-fsanitize=$SANITIZER -g -O1"
SUFFIX=""
-
- echo "[+] Compiler: $CC"
;;
-
libfuzzer)
- echo "[*] Building with LibFuzzer"
-
- # Check if clang is installed
- if ! command -v clang &> /dev/null; then
- echo "[!] Clang not found. Please install:"
- echo " sudo apt-get install clang"
+ if ! command -v clang &>/dev/null; then
+ echo "[!] clang not found. Install clang first."
exit 1
fi
-
CC="clang"
CFLAGS="-fsanitize=fuzzer,$SANITIZER -g -O1"
SUFFIX="_lf"
-
- echo "[+] Compiler: $CC"
;;
-
*)
- echo "[!] Unknown mode: $MODE"
- echo "Usage: $0 [afl|libfuzzer]"
+ echo "[!] Unknown mode: $MODE (use 'afl' or 'libfuzzer')"
exit 1
;;
esac
-# Find json-c: try pkg-config first, then common paths
-JSON_C_CFLAGS=$(pkg-config --cflags json-c 2>/dev/null || true)
-JSON_C_LIBS=$(pkg-config --libs json-c 2>/dev/null || true)
+# ─── find json-c ─────────────────────────────────────────────────────────────
+
+JSON_C_CFLAGS=$(pkg-config --cflags json-c 2>/dev/null || echo "")
+JSON_C_LIBS=$(pkg-config --libs json-c 2>/dev/null || echo "-ljson-c")
if [ -z "$JSON_C_CFLAGS" ]; then
- for dir in /usr/include /usr/local/include; do
- if [ -f "$dir/json-c/json.h" ]; then
- JSON_C_CFLAGS="-I$dir"
- break
- fi
+ for d in /usr/include /usr/local/include; do
+ [ -f "$d/json-c/json.h" ] && JSON_C_CFLAGS="-I$d" && break
done
fi
-if [ -z "$JSON_C_LIBS" ]; then
- JSON_C_LIBS="-ljson-c"
-fi
if [ -z "$JSON_C_CFLAGS" ]; then
- echo "[!] json-c headers not found. Install with: sudo apt-get install libjson-c-dev"
+ echo "[!] json-c headers not found. Run: sudo apt-get install libjson-c-dev"
exit 1
fi
-# Build each harness
+# ─── run setup stages ────────────────────────────────────────────────────────
+
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "[1/7] Installing dependencies"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+install_deps
+
echo
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "[2/7] Setting up ttyd source + instrumented objects"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+setup_ttyd_source
+
+# ─── build harnesses ─────────────────────────────────────────────────────────
+
+echo
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "[3/7] Building harnesses (with real ttyd code)"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+
+# Common flags for all harnesses: our mock includes take priority over system
+COMMON_CFLAGS="$CFLAGS -I$FUZZ_INCLUDE -I$TTYD_SRC/src $JSON_C_CFLAGS"
+COMMON_CFLAGS="$COMMON_CFLAGS -Wno-implicit-function-declaration -Wno-int-conversion"
+
+# http.c and protocol.c are compiled into each harness via #include — do NOT
+# include their pre-compiled objects here or every symbol will be defined twice.
+MOCK_OBJS="$TTYD_OBJ/utils.o"
+MOCK_OBJS="$MOCK_OBJS $TTYD_OBJ/fuzz_lws_mock.o $TTYD_OBJ/mock_server_globals.o $TTYD_OBJ/mock_pty_stubs.o"
+
+# http.c decompresses gzip-encoded HTML using zlib
+EXTRA_LIBS="-lz"
+
echo "[*] Building fuzz_auth_header..."
-$CC $CFLAGS fuzz_auth_header.c -o fuzz_auth_header$SUFFIX
+$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 $CFLAGS $JSON_C_CFLAGS fuzz_websocket_auth.c $JSON_C_LIBS -o fuzz_websocket_auth$SUFFIX
+$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"
echo "[*] Building fuzz_http_parsing..."
-$CC $CFLAGS fuzz_http_parsing.c -o fuzz_http_parsing$SUFFIX
+$CC $COMMON_CFLAGS fuzz_http_parsing.c $MOCK_OBJS $JSON_C_LIBS -o fuzz_http_parsing$SUFFIX
echo "[+] Built: fuzz_http_parsing$SUFFIX"
-# Build CmpLog variants (AFL++ only)
-if [ "$MODE" == "afl" ]; then
+# CmpLog variants (AFL++ only)
+if [ "$MODE" = "afl" ]; then
echo
- echo "[*] Building CmpLog variants (for solving string comparisons)..."
-
- AFL_LLVM_CMPLOG=1 $CC $CFLAGS fuzz_auth_header.c -o fuzz_auth_header_cmplog
- echo "[+] Built: fuzz_auth_header_cmplog"
-
- AFL_LLVM_CMPLOG=1 $CC $CFLAGS $JSON_C_CFLAGS fuzz_websocket_auth.c $JSON_C_LIBS -o fuzz_websocket_auth_cmplog
- echo "[+] Built: fuzz_websocket_auth_cmplog"
-
- AFL_LLVM_CMPLOG=1 $CC $CFLAGS fuzz_http_parsing.c -o fuzz_http_parsing_cmplog
+ 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_http_parsing.c $MOCK_OBJS $JSON_C_LIBS \
+ -o fuzz_http_parsing_cmplog
echo "[+] Built: fuzz_http_parsing_cmplog"
fi
echo
-echo "[+] All harnesses built successfully!"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "[4/7] Setting up dictionaries"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+setup_dictionaries
-# Generate corpus
echo
-echo "[*] Generating seed corpus..."
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-bash "$SCRIPT_DIR/gen-corpus.sh"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "[5/7] Generating seed corpus"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+generate_corpus
echo
-echo "[+] Ready to fuzz! Run:"
-if [ "$MODE" == "afl" ]; then
- echo " ./run_fuzzers.sh # Launch all in tmux"
- echo " # Or individually:"
- echo " afl-fuzz -i corpus/auth_header -o output/auth_header ./fuzz_auth_header"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "[6/7] Installing crash-backup crontab"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+setup_crash_backup
+
+echo
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "[7/7] Done"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+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"
else
- echo " ./fuzz_auth_header_lf corpus/auth_header"
+ echo " ./fuzz_auth_header_lf corpus/auth_header/"
fi
+echo
+echo "Crashes backed up automatically every 15 min → crashes-backup/"
+echo "To stop backup: ./clean.sh"
diff --git a/fuzzing/ttyd/clean.sh b/fuzzing/ttyd/clean.sh
@@ -1,6 +1,11 @@
#!/bin/bash
# Clean up TTYD fuzzing artifacts
-# Usage: ./clean.sh [--keep-crashes] [--corpus-only]
+#
+# Usage: ./clean.sh [OPTIONS]
+# --keep-crashes Save crash files before cleaning output/
+# --corpus-only Only clean corpus/, leave everything else
+# --keep-source Do not remove ttyd-src/ (the git clone)
+# -h, --help
set -e
@@ -8,17 +13,20 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
KEEP_CRASHES=false
CORPUS_ONLY=false
+KEEP_SOURCE=false
for arg in "$@"; do
case "$arg" in
--keep-crashes) KEEP_CRASHES=true ;;
--corpus-only) CORPUS_ONLY=true ;;
+ --keep-source) KEEP_SOURCE=true ;;
-h|--help)
echo "Usage: $0 [OPTIONS]"
echo
echo "Options:"
- echo " --keep-crashes Save crash files before cleaning output"
+ echo " --keep-crashes Save crash files before cleaning output/"
echo " --corpus-only Only clean the corpus directory"
+ echo " --keep-source Keep ttyd-src/ git clone"
echo " -h, --help Show this help"
exit 0
;;
@@ -30,14 +38,30 @@ for arg in "$@"; do
esac
done
-# Stop any running fuzzers
+# ── stop running fuzzers ──────────────────────────────────────────────────────
if tmux has-session -t ttyd-fuzz 2>/dev/null; then
echo "[*] Stopping running fuzzer session..."
tmux kill-session -t ttyd-fuzz
echo "[+] Fuzzer session killed"
fi
-# Clean corpus
+# ── remove crontab entry ──────────────────────────────────────────────────────
+CRONTAB_MARKER="# ttyd-fuzz-crash-backup"
+CURRENT_CRON=$(crontab -l 2>/dev/null || true)
+if echo "$CURRENT_CRON" | grep -q "$CRONTAB_MARKER"; then
+ TMPFILE=$(mktemp)
+ echo "$CURRENT_CRON" \
+ | grep -v "$CRONTAB_MARKER" \
+ | grep -v "backup_crashes.sh" \
+ > "$TMPFILE" || true
+ crontab "$TMPFILE"
+ rm -f "$TMPFILE"
+ echo "[+] Removed crash-backup crontab entry"
+else
+ echo "[*] No crash-backup crontab entry found"
+fi
+
+# ── clean corpus ──────────────────────────────────────────────────────────────
if [ -d "$SCRIPT_DIR/corpus" ]; then
echo "[*] Removing corpus/"
rm -rf "$SCRIPT_DIR/corpus"
@@ -49,24 +73,19 @@ if $CORPUS_ONLY; then
exit 0
fi
-# Clean output (fuzzing results)
+# ── clean output (optionally save crashes first) ──────────────────────────────
if [ -d "$SCRIPT_DIR/output" ]; then
if $KEEP_CRASHES; then
- # Save crashes before cleaning
CRASH_DIR="$SCRIPT_DIR/saved_crashes_$(date +%Y%m%d_%H%M%S)"
HAS_CRASHES=false
for target_dir in "$SCRIPT_DIR"/output/*/; do
[ -d "$target_dir" ] || continue
- target_name=$(basename "$target_dir")
-
for node_dir in "$target_dir"*/; do
[ -d "$node_dir" ] || continue
crash_path="$node_dir/crashes"
-
if [ -d "$crash_path" ] && [ "$(ls -A "$crash_path" 2>/dev/null)" ]; then
- node_name=$(basename "$node_dir")
- dest="$CRASH_DIR/$target_name/$node_name"
+ dest="$CRASH_DIR/$(basename "$target_dir")/$(basename "$node_dir")"
mkdir -p "$dest"
cp -r "$crash_path"/* "$dest/"
HAS_CRASHES=true
@@ -87,7 +106,7 @@ if [ -d "$SCRIPT_DIR/output" ]; then
echo "[+] Output removed"
fi
-# Clean compiled binaries
+# ── remove compiled harness binaries ─────────────────────────────────────────
REMOVED=0
for binary in fuzz_auth_header fuzz_websocket_auth fuzz_http_parsing; do
for suffix in "" "_cmplog" "_lf"; do
@@ -98,11 +117,45 @@ for binary in fuzz_auth_header fuzz_websocket_auth fuzz_http_parsing; do
fi
done
done
+[ "$REMOVED" -gt 0 ] && echo "[+] Removed $REMOVED compiled binaries" \
+ || 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
-if [ "$REMOVED" -gt 0 ]; then
- echo "[+] Removed $REMOVED compiled binaries"
-else
- echo "[*] No binaries to remove"
+# ── remove instrumented objects and mock headers ──────────────────────────────
+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
+ echo "[*] Keeping ttyd-src/ (--keep-source)"
+ else
+ echo "[*] Removing ttyd-src/ (pass --keep-source to skip)"
+ rm -rf "$SCRIPT_DIR/ttyd-src"
+ echo "[+] ttyd-src/ removed"
+ fi
+fi
+
+# ── remove crash backups log (keep archives — user's decision) ───────────────
+if [ -f "$SCRIPT_DIR/crashes-backup/backup.log" ]; then
+ rm -f "$SCRIPT_DIR/crashes-backup/backup.log"
+ echo "[+] Removed crashes-backup/backup.log"
fi
echo "[+] Clean complete"
diff --git a/fuzzing/ttyd/fuzz_auth_header.c b/fuzzing/ttyd/fuzz_auth_header.c
@@ -1,18 +1,21 @@
/*
- * AFL++ / LibFuzzer Harness for TTYD Authentication Header Parsing
+ * AFL++ / LibFuzzer harness — TTYD HTTP Authentication Header
*
- * Target: HTTP Authorization header parsing in check_auth()
- * Goal: Find buffer overflows, format string bugs, logic errors
+ * Target: the real check_auth() function from ttyd's http.c
+ * Technique: #include the source file directly so static functions are
+ * accessible (standard OSS-Fuzz approach).
*
- * Compile with AFL++:
- * afl-clang-fast -fsanitize=address -g -O1 fuzz_auth_header.c -o fuzz_auth_header
+ * Input format: [mode:1][credential_len:2 BE][credential][auth_header_value]
+ * mode 0 — no auth required
+ * mode 1 — Basic auth (credential = expected base64 value)
+ * mode 2 — custom header auth (X-Auth-User)
*
- * Compile with libFuzzer:
- * clang -fsanitize=fuzzer,address -g -O1 fuzz_auth_header.c -o fuzz_auth_header
- *
- * Run:
- * afl-fuzz -i input/ -o output/ ./fuzz_auth_header
- * ./fuzz_auth_header # libFuzzer
+ * Build (handled by build_fuzzers.sh):
+ * afl-clang-fast -fsanitize=address,undefined -g -O1
+ * -I fuzz-include -I ttyd-src/src
+ * fuzz_auth_header.c
+ * ttyd-obj/{http,protocol,utils,fuzz_lws_mock,mock_server_globals,mock_pty_stubs}.o
+ * -ljson-c -o fuzz_auth_header
*/
#include <stdio.h>
@@ -20,186 +23,96 @@
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
-#include <unistd.h>
-
-// Simulated ttyd structures
-struct server_t {
- char *credential; // Base64 encoded username:password
- char *auth_header; // Custom auth header name
-} server_instance;
-
-struct server_t *server = &server_instance;
-
-// Simulated check_auth logic from protocol.c
-bool check_auth_fuzzing(const char *auth_header_value, const char *custom_header_value) {
- // Simulate auth_header mode
- if (server->auth_header != NULL) {
- if (custom_header_value != NULL && strlen(custom_header_value) > 0) {
- return true;
- }
- return false;
- }
-
- // Simulate credential mode
- if (server->credential != NULL) {
- if (auth_header_value == NULL) return false;
-
- char buf[256];
- size_t n = strlen(auth_header_value);
- if (n >= sizeof(buf)) n = sizeof(buf) - 1;
- memcpy(buf, auth_header_value, n);
- buf[n] = '\0';
-
- // Check for "Basic " prefix and compare
- if (n >= 7 && strstr(buf, "Basic ") && strcmp(buf + 6, server->credential) == 0) {
- return true;
- }
- return false;
- }
-
- return true; // No auth required
-}
-
-// Base64 encoding for credential generation
-static const char base64_chars[] =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-
-void base64_encode_simple(const char *input, char *output, size_t out_size) {
- size_t len = strlen(input);
- size_t i, j = 0;
-
- for (i = 0; i < len && j < out_size - 1; i += 3) {
- uint32_t octet_a = i < len ? (unsigned char)input[i] : 0;
- uint32_t octet_b = i + 1 < len ? (unsigned char)input[i + 1] : 0;
- uint32_t octet_c = i + 2 < len ? (unsigned char)input[i + 2] : 0;
- uint32_t triple = (octet_a << 16) + (octet_b << 8) + octet_c;
-
- if (j + 4 >= out_size) break;
- output[j++] = base64_chars[(triple >> 18) & 0x3F];
- output[j++] = base64_chars[(triple >> 12) & 0x3F];
- output[j++] = i + 1 < len ? base64_chars[(triple >> 6) & 0x3F] : '=';
- output[j++] = i + 2 < len ? base64_chars[triple & 0x3F] : '=';
- }
- output[j] = '\0';
-}
-
-// Fuzzing target
-int fuzz_target(const uint8_t *data, size_t size) {
- if (size < 10) return 0; // Need minimum data
- // Parse fuzzer input
- // Format: [mode:1][credential_len:2][credential][auth_header]
+/* Mock lws control state — must come before including the real ttyd source */
+#include "fuzz_lws_mock.h"
- uint8_t mode = data[0] % 3; // 0=no auth, 1=basic auth, 2=custom header
- uint16_t cred_len = (data[1] << 8) | data[2];
+/* Pull in the real ttyd structs (server.h included transitively via http.c) */
+/* fuzz-include/libwebsockets.h and fuzz-include/uv.h shadow the real ones */
+#include "ttyd-src/src/http.c" /* gives us static check_auth() */
- if (cred_len > size - 3) cred_len = size - 3;
- if (cred_len > 500) cred_len = 500; // Reasonable limit
+/* Provided by mock_server_globals.c */
+extern struct server _fuzz_server;
+extern void fuzz_server_reset(void);
- char credential_raw[512] = {0};
- char credential_b64[1024] = {0};
- char auth_header_input[1024] = {0};
- char custom_header_value[256] = {0};
+/* ── fuzz target ─────────────────────────────────────────────────────────── */
- // Extract credential
- if (cred_len > 0) {
- memcpy(credential_raw, data + 3, cred_len);
- credential_raw[cred_len] = '\0';
- base64_encode_simple(credential_raw, credential_b64, sizeof(credential_b64));
- }
-
- // Extract auth header value
- size_t remaining = size - 3 - cred_len;
- if (remaining > 0) {
- size_t copy_len = remaining < sizeof(auth_header_input) - 1 ?
- remaining : sizeof(auth_header_input) - 1;
- memcpy(auth_header_input, data + 3 + cred_len, copy_len);
- auth_header_input[copy_len] = '\0';
-
- // Also extract custom header value (first 255 bytes)
- size_t custom_len = remaining < sizeof(custom_header_value) - 1 ?
- remaining : sizeof(custom_header_value) - 1;
- memcpy(custom_header_value, data + 3 + cred_len, custom_len);
- custom_header_value[custom_len] = '\0';
+int fuzz_target(const uint8_t *data, size_t size) {
+ if (size < 3) return 0;
+
+ /* Parse structured input */
+ uint8_t mode = data[0] % 3;
+ uint16_t cred_len = (uint16_t)((data[1] << 8) | data[2]);
+ if ((size_t)cred_len > size - 3) cred_len = (uint16_t)(size - 3);
+
+ /* Reset fuzzer-controlled lws state and the server global */
+ fuzz_lws_reset();
+ fuzz_server_reset();
+
+ /* Set up the credential the server expects */
+ static char cred_buf[65536];
+ if (mode == 1 && cred_len > 0) {
+ /* Basic auth: credential bytes become the expected base64 value */
+ memcpy(cred_buf, data + 3, cred_len);
+ cred_buf[cred_len] = '\0';
+ _fuzz_server.credential = cred_buf;
+ _fuzz_server.auth_header = NULL;
+ } else if (mode == 2) {
+ /* Custom header auth */
+ _fuzz_server.credential = NULL;
+ _fuzz_server.auth_header = (char *)"X-Auth-User";
}
+ /* mode 0: no auth, both remain NULL */
- // Setup server state based on mode
- if (mode == 0) {
- // No authentication required
- server->credential = NULL;
- server->auth_header = NULL;
- } else if (mode == 1) {
- // Basic auth mode
- server->credential = credential_b64[0] ? strdup(credential_b64) : NULL;
- server->auth_header = NULL;
- } else {
- // Custom header mode
- server->credential = NULL;
- server->auth_header = strdup("X-Auth-User");
- }
+ /* Remaining bytes are the inbound Authorization header value that
+ * lws_hdr_copy() will return via the mock */
+ const uint8_t *auth_data = data + 3 + cred_len;
+ size_t auth_len = size - 3 - cred_len;
+ fuzz_lws_set_auth((const char *)auth_data, (int)auth_len);
- // Fuzz the authentication check
- bool result = check_auth_fuzzing(auth_header_input, custom_header_value);
+ /* For custom-header mode, same bytes are treated as the custom header */
+ if (mode == 2)
+ fuzz_lws_set_custom((const char *)auth_data, (int)auth_len);
- // Cleanup
- if (server->credential) free(server->credential);
- if (server->auth_header) free(server->auth_header);
- server->credential = NULL;
- server->auth_header = NULL;
+ /* Call the REAL check_auth() from http.c */
+ struct lws mock_wsi = {0};
+ struct pss_http pss = {0};
+ (void)check_auth(&mock_wsi, &pss);
return 0;
}
+/* ── AFL++ persistent mode ───────────────────────────────────────────────── */
#ifdef __AFL_FUZZ_TESTCASE_LEN
-// AFL++ persistent mode
__AFL_FUZZ_INIT();
-
int main(void) {
- #ifdef __AFL_HAVE_MANUAL_CONTROL
- __AFL_INIT();
- #endif
-
+#ifdef __AFL_HAVE_MANUAL_CONTROL
+ __AFL_INIT();
+#endif
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
-
while (__AFL_LOOP(10000)) {
- int len = __AFL_FUZZ_TESTCASE_LEN;
- fuzz_target(buf, len);
+ fuzz_target(buf, __AFL_FUZZ_TESTCASE_LEN);
}
-
return 0;
}
+
#else
-// LibFuzzer mode
+/* LibFuzzer */
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
return fuzz_target(data, size);
}
-// Standalone mode for testing
#ifndef LIBFUZZER
+/* Standalone file-based testing */
int main(int argc, char **argv) {
- if (argc != 2) {
- fprintf(stderr, "Usage: %s <input_file>\n", argv[0]);
- return 1;
- }
-
+ if (argc != 2) { fprintf(stderr, "Usage: %s <input_file>\n", argv[0]); return 1; }
FILE *f = fopen(argv[1], "rb");
- if (!f) {
- perror("fopen");
- return 1;
- }
-
- fseek(f, 0, SEEK_END);
- long size = ftell(f);
- fseek(f, 0, SEEK_SET);
-
- uint8_t *data = malloc(size);
- fread(data, 1, size, f);
- fclose(f);
-
- fuzz_target(data, size);
- free(data);
-
- printf("Test completed successfully\n");
+ if (!f) { perror("fopen"); return 1; }
+ fseek(f, 0, SEEK_END); long sz = ftell(f); rewind(f);
+ uint8_t *buf = malloc(sz);
+ fread(buf, 1, sz, f); fclose(f);
+ fuzz_target(buf, sz);
+ free(buf);
return 0;
}
#endif
diff --git a/fuzzing/ttyd/fuzz_websocket_auth.c b/fuzzing/ttyd/fuzz_websocket_auth.c
@@ -1,18 +1,19 @@
/*
- * AFL++ / LibFuzzer Harness for TTYD WebSocket Authentication
+ * AFL++ / LibFuzzer harness — TTYD WebSocket AuthToken + window-size JSON
*
- * Target: WebSocket AuthToken JSON parsing and validation
- * Goal: Find JSON parsing bugs, buffer overflows, logic errors
+ * Target: the real parse_window_size() and check_auth() from ttyd's protocol.c
+ * Technique: #include the source file so static functions are accessible.
*
- * Compile with AFL++:
- * afl-clang-fast -fsanitize=address -g -O1 fuzz_websocket_auth.c -ljson-c -o fuzz_websocket_auth
+ * 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)
*
- * Compile with libFuzzer:
- * clang -fsanitize=fuzzer,address -g -O1 fuzz_websocket_auth.c -ljson-c -o fuzz_websocket_auth
- *
- * Run:
- * afl-fuzz -i input/ -o output/ ./fuzz_websocket_auth
- * ./fuzz_websocket_auth # libFuzzer
+ * 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
*/
#include <stdio.h>
@@ -20,169 +21,102 @@
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
-#include <unistd.h>
#include <json-c/json.h>
-// Simulated ttyd structures
-struct server_t {
- char *credential;
-} server_instance;
-
-struct pss_tty_t {
- bool authenticated;
-} pss_instance;
-
-struct server_t *server = &server_instance;
-
-// Simulated parse_window_size and auth check from protocol.c
-struct json_object *parse_window_size_fuzzing(const char *buf, size_t len,
- uint16_t *columns, uint16_t *rows) {
- if (buf == NULL || len == 0) return NULL;
-
- // Ensure null termination
- char *safe_buf = malloc(len + 1);
- if (!safe_buf) return NULL;
- memcpy(safe_buf, buf, len);
- safe_buf[len] = '\0';
-
- struct json_tokener *tok = json_tokener_new();
- struct json_object *obj = json_tokener_parse_ex(tok, safe_buf, len);
- json_tokener_free(tok);
- free(safe_buf);
-
- if (obj == NULL) return NULL;
-
- // Extract columns and rows
- struct json_object *o = NULL;
- if (json_object_object_get_ex(obj, "columns", &o)) {
- *columns = (uint16_t)json_object_get_int(o);
- }
- if (json_object_object_get_ex(obj, "rows", &o)) {
- *rows = (uint16_t)json_object_get_int(o);
- }
-
- return obj;
-}
-
-bool check_websocket_auth_fuzzing(const char *buf, size_t len) {
- struct pss_tty_t *pss = &pss_instance;
- pss->authenticated = false;
+/* Mock lws control state — must come before the real ttyd source */
+#include "fuzz_lws_mock.h"
- uint16_t columns = 0, rows = 0;
- struct json_object *obj = parse_window_size_fuzzing(buf, len, &columns, &rows);
-
- if (obj == NULL) {
- return false;
- }
+/* Pull in real ttyd protocol code (parse_window_size, check_auth static fns) */
+#include "ttyd-src/src/protocol.c"
- if (server->credential != NULL) {
- struct json_object *o = NULL;
- if (json_object_object_get_ex(obj, "AuthToken", &o)) {
- const char *token = json_object_get_string(o);
- if (token != NULL && strcmp(token, server->credential) == 0) {
- pss->authenticated = true;
- }
- }
+/* Provided by mock_server_globals.c */
+extern struct server _fuzz_server;
+extern void fuzz_server_reset(void);
- if (!pss->authenticated) {
- json_object_put(obj);
- return false;
- }
- }
+/* ── fuzz target ─────────────────────────────────────────────────────────── */
- json_object_put(obj);
- return true;
-}
-
-// Fuzzing target
int fuzz_target(const uint8_t *data, size_t size) {
- if (size < 2) return 0;
-
- // Parse fuzzer input
- // Format: [has_credential:1][credential_len:1][credential][json_message]
+ if (size < 3) return 0;
- bool has_credential = (data[0] % 2) == 1;
- uint8_t cred_len = data[1];
+ 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);
- if (cred_len > size - 2) cred_len = size - 2;
- if (cred_len > 200) cred_len = 200;
+ fuzz_lws_reset();
+ fuzz_server_reset();
- char credential[256] = {0};
+ /* Configure server credential */
+ static char cred_buf[65536];
if (has_credential && cred_len > 0) {
- memcpy(credential, data + 2, cred_len);
- credential[cred_len] = '\0';
- server->credential = credential;
- } else {
- server->credential = NULL;
+ memcpy(cred_buf, data + 3, cred_len);
+ cred_buf[cred_len] = '\0';
+ _fuzz_server.credential = cred_buf;
}
-
- // Extract JSON message
- size_t json_start = 2 + (has_credential ? cred_len : 0);
- if (json_start >= size) {
- server->credential = NULL;
- return 0;
+ /* 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);
+ }
}
- size_t json_len = size - json_start;
- const char *json_data = (const char *)(data + json_start);
-
- // Fuzz the WebSocket auth check
- bool result = check_websocket_auth_fuzzing(json_data, json_len);
+ /* Also exercise the WS check_auth() static function */
+ struct lws mock_wsi = {0};
+ struct pss_tty pss = {0};
+ (void)check_auth(&mock_wsi, &pss);
- server->credential = NULL;
+ json_object_put(obj);
return 0;
}
+/* ── AFL++ persistent mode ───────────────────────────────────────────────── */
#ifdef __AFL_FUZZ_TESTCASE_LEN
-// AFL++ persistent mode
__AFL_FUZZ_INIT();
-
int main(void) {
- #ifdef __AFL_HAVE_MANUAL_CONTROL
- __AFL_INIT();
- #endif
-
+#ifdef __AFL_HAVE_MANUAL_CONTROL
+ __AFL_INIT();
+#endif
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
-
while (__AFL_LOOP(10000)) {
- int len = __AFL_FUZZ_TESTCASE_LEN;
- fuzz_target(buf, len);
+ fuzz_target(buf, __AFL_FUZZ_TESTCASE_LEN);
}
-
return 0;
}
+
#else
-// LibFuzzer mode
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
return fuzz_target(data, size);
}
-// Standalone mode for testing
#ifndef LIBFUZZER
int main(int argc, char **argv) {
- if (argc != 2) {
- fprintf(stderr, "Usage: %s <input_file>\n", argv[0]);
- return 1;
- }
-
+ if (argc != 2) { fprintf(stderr, "Usage: %s <input_file>\n", argv[0]); return 1; }
FILE *f = fopen(argv[1], "rb");
- if (!f) {
- perror("fopen");
- return 1;
- }
-
- fseek(f, 0, SEEK_END);
- long size = ftell(f);
- fseek(f, 0, SEEK_SET);
-
- uint8_t *data = malloc(size);
- fread(data, 1, size, f);
- fclose(f);
-
- fuzz_target(data, size);
- free(data);
-
- printf("Test completed successfully\n");
+ if (!f) { perror("fopen"); return 1; }
+ fseek(f, 0, SEEK_END); long sz = ftell(f); rewind(f);
+ uint8_t *buf = malloc(sz);
+ fread(buf, 1, sz, f); fclose(f);
+ fuzz_target(buf, sz);
+ free(buf);
return 0;
}
#endif
diff --git a/fuzzing/ttyd/gen-corpus.sh b/fuzzing/ttyd/gen-corpus.sh
@@ -1,346 +0,0 @@
-#!/bin/bash
-# Generate seed corpus for TTYD fuzzing harnesses
-# Creates structured inputs matching each harness's expected format
-
-set -e
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-
-mkdir -p "$SCRIPT_DIR/corpus/auth_header"
-mkdir -p "$SCRIPT_DIR/corpus/websocket_auth"
-mkdir -p "$SCRIPT_DIR/corpus/http_parsing"
-
-echo "[*] Generating corpus for fuzz_auth_header"
-echo " Format: [mode:1][credential_len:2][credential][auth_header_value]"
-
-DIR="$SCRIPT_DIR/corpus/auth_header"
-
-# Mode 0: no auth required
-# mode=0x00, cred_len=0x0000, payload
-printf '\x00\x00\x00anything-goes' > "$DIR/noauth_basic"
-printf '\x00\x00\x00' > "$DIR/noauth_empty"
-printf '\x00\x00\x00Basic YWRtaW46cGFzcw==' > "$DIR/noauth_with_basic"
-
-# Mode 1: basic auth (mode % 3 == 1)
-# mode=0x01, cred_len=0x000b (11), credential="admin:pass", auth_header
-printf '\x01\x00\x0badmin:pass\x00Basic YWRtaW46cGFzcw==' > "$DIR/basic_valid"
-printf '\x01\x00\x0badmin:pass\x00Basic INVALIDBASE64==' > "$DIR/basic_mismatch"
-printf '\x01\x00\x0badmin:pass\x00Basic ' > "$DIR/basic_empty_token"
-printf '\x01\x00\x0badmin:pass\x00' > "$DIR/basic_no_header"
-printf '\x01\x00\x0badmin:pass\x00Bearer token123' > "$DIR/basic_wrong_scheme"
-printf '\x01\x00\x0badmin:pass\x00basic YWRtaW46cGFzcw==' > "$DIR/basic_lowercase"
-printf '\x01\x00\x00\x00Basic YWRtaW46cGFzcw==' > "$DIR/basic_no_credential"
-
-# Mode 1: long credentials
-python3 -c "
-import sys
-cred = b'A' * 255 + b':' + b'B' * 255
-cred_len = len(cred)
-header = b'\x01' + cred_len.to_bytes(2, 'big') + cred + b'Basic ' + b'A' * 500
-sys.stdout.buffer.write(header)
-" > "$DIR/basic_long_cred"
-
-# Mode 1: special characters in credentials
-printf '\x01\x00\x13user:p@ss\x00w0rd!#$%%\x00Basic dXNlcjpwQHNzAHcwcmQhIyQl' > "$DIR/basic_special_chars"
-printf '\x01\x00\x0auser\x00:pass\x00Basic dXNlcgA6cGFzcw==' > "$DIR/basic_null_in_cred"
-
-# Mode 2: custom header auth (mode % 3 == 2)
-printf '\x02\x00\x00some-token-value' > "$DIR/custom_with_value"
-printf '\x02\x00\x00' > "$DIR/custom_empty"
-printf '\x02\x00\x00\x00' > "$DIR/custom_null_byte"
-
-# Edge cases
-printf '\x01\x00\x01\x00\x00' > "$DIR/edge_single_byte_cred"
-printf '\x01\xff\xff' > "$DIR/edge_huge_cred_len"
-
-# Format string attempts
-printf '\x01\x00\x10%%s%%s%%s%%s%%s%%s%%s%%s\x00Basic %%x%%x%%x%%x' > "$DIR/fmt_string"
-printf '\x01\x00\x04%%n%%n\x00%%n%%n%%n%%n' > "$DIR/fmt_write"
-
-# Buffer boundary sizes
-python3 -c "
-import sys
-# 254 byte auth header (near buf[256] boundary)
-cred = b'x:y'
-header = b'Basic ' + b'A' * 248
-sys.stdout.buffer.write(b'\x01\x00\x03' + cred + header)
-" > "$DIR/boundary_254"
-
-python3 -c "
-import sys
-# 256 byte auth header (exact buf[256] boundary)
-cred = b'x:y'
-header = b'Basic ' + b'A' * 250
-sys.stdout.buffer.write(b'\x01\x00\x03' + cred + header)
-" > "$DIR/boundary_256"
-
-python3 -c "
-import sys
-# 257 byte auth header (overflow buf[256])
-cred = b'x:y'
-header = b'Basic ' + b'A' * 251
-sys.stdout.buffer.write(b'\x01\x00\x03' + cred + header)
-" > "$DIR/boundary_257"
-
-# Mixed-case Basic prefix variations
-printf '\x01\x00\x0badmin:pass\x00BASIC YWRtaW46cGFzcw==' > "$DIR/basic_uppercase"
-printf '\x01\x00\x0badmin:pass\x00bAsIc YWRtaW46cGFzcw==' > "$DIR/basic_mixedcase"
-
-# Base64 padding edge cases
-printf '\x01\x00\x02a:\x00Basic YTo=' > "$DIR/basic_b64_pad1"
-printf '\x01\x00\x01a\x00Basic YQ==' > "$DIR/basic_b64_pad2"
-printf '\x01\x00\x03abc\x00Basic YWJj' > "$DIR/basic_b64_nopad"
-printf '\x01\x00\x05ad:mn\x00Basic =====' > "$DIR/basic_b64_allpad"
-printf '\x01\x00\x05ad:mn\x00Basic @#$!!' > "$DIR/basic_b64_invalid_chars"
-
-# Credential separator positions
-printf '\x01\x00\x01:\x00Basic Og==' > "$DIR/basic_colon_only"
-printf '\x01\x00\x07:::::::' > "$DIR/basic_all_colons"
-printf '\x01\x00\x0aadmin:a:b:c\x00Basic YWRtaW46YTpiOmM=' > "$DIR/basic_multi_colon"
-
-# Whitespace in auth header
-printf '\x01\x00\x0badmin:pass\x00Basic YWRtaW46cGFzcw==' > "$DIR/basic_double_space"
-printf '\x01\x00\x0badmin:pass\x00 Basic YWRtaW46cGFzcw==' > "$DIR/basic_leading_space"
-
-echo "[+] Generated $(ls "$DIR" | wc -l) seeds for auth_header"
-
-###############################################################################
-echo
-echo "[*] Generating corpus for fuzz_websocket_auth"
-echo " Format: [has_credential:1][credential_len:1][credential][json_message]"
-
-DIR="$SCRIPT_DIR/corpus/websocket_auth"
-
-# No credential (even byte = no cred)
-printf '\x00\x00{"columns":80,"rows":24}' > "$DIR/noauth_normal"
-printf '\x00\x00{"columns":200,"rows":50}' > "$DIR/noauth_large_term"
-printf '\x00\x00{}' > "$DIR/noauth_empty_json"
-printf '\x00\x00{"columns":80,"rows":24,"AuthToken":"ignored"}' > "$DIR/noauth_with_token"
-
-# With credential (odd byte = has cred)
-printf '\x01\x05admin{"columns":80,"rows":24,"AuthToken":"admin"}' > "$DIR/auth_valid"
-printf '\x01\x05admin{"columns":80,"rows":24,"AuthToken":"wrong"}' > "$DIR/auth_invalid"
-printf '\x01\x05admin{"columns":80,"rows":24}' > "$DIR/auth_missing_token"
-printf '\x01\x05admin{"AuthToken":"admin"}' > "$DIR/auth_no_dimensions"
-printf '\x01\x00{"columns":80,"rows":24}' > "$DIR/auth_empty_cred"
-
-# Malformed JSON
-printf '\x00\x00{invalid json}' > "$DIR/json_malformed"
-printf '\x00\x00{"columns":' > "$DIR/json_truncated"
-printf '\x00\x00' > "$DIR/json_empty"
-printf '\x00\x00\x00' > "$DIR/json_null"
-printf '\x01\x03foo[1,2,3]' > "$DIR/json_array"
-printf '\x01\x03foo"just a string"' > "$DIR/json_string"
-
-# Large/extreme values
-printf '\x00\x00{"columns":99999,"rows":99999}' > "$DIR/json_huge_values"
-printf '\x00\x00{"columns":-1,"rows":-1}' > "$DIR/json_negative"
-printf '\x00\x00{"columns":0,"rows":0}' > "$DIR/json_zero"
-printf '\x00\x00{"columns":65535,"rows":65535}' > "$DIR/json_uint16_max"
-printf '\x00\x00{"columns":65536,"rows":65536}' > "$DIR/json_uint16_overflow"
-
-# Deeply nested / large JSON
-python3 -c "
-import sys
-nested = '{\"a\":' * 50 + '1' + '}' * 50
-sys.stdout.buffer.write(b'\x00\x00' + nested.encode())
-" > "$DIR/json_deep_nested"
-
-python3 -c "
-import sys
-big = '{\"columns\":80,\"rows\":24,\"AuthToken\":\"' + 'A' * 5000 + '\"}'
-sys.stdout.buffer.write(b'\x01\x05admin' + big.encode())
-" > "$DIR/json_huge_token"
-
-# Unicode / special strings in JSON
-printf '\x01\x05admin{"columns":80,"rows":24,"AuthToken":"\x00\x00\x00"}' > "$DIR/json_null_bytes_token"
-printf '\x00\x00{"columns":80,"rows":24,"extra":"\\u0000\\u0000"}' > "$DIR/json_unicode_null"
-
-# Format strings in JSON
-printf '\x01\x05admin{"AuthToken":"%%s%%s%%s%%s%%n"}' > "$DIR/json_fmt_string"
-
-# AuthToken type confusion (non-string values)
-printf '\x01\x05admin{"columns":80,"rows":24,"AuthToken":true}' > "$DIR/auth_token_bool"
-printf '\x01\x05admin{"columns":80,"rows":24,"AuthToken":null}' > "$DIR/auth_token_null"
-printf '\x01\x05admin{"columns":80,"rows":24,"AuthToken":12345}' > "$DIR/auth_token_int"
-printf '\x01\x05admin{"columns":80,"rows":24,"AuthToken":["admin"]}' > "$DIR/auth_token_array"
-printf '\x01\x05admin{"columns":80,"rows":24,"AuthToken":{"nested":"admin"}}' > "$DIR/auth_token_object"
-
-# Duplicate keys
-printf '\x01\x05admin{"AuthToken":"wrong","AuthToken":"admin"}' > "$DIR/auth_token_dup"
-printf '\x00\x00{"columns":80,"columns":200,"rows":24}' > "$DIR/json_dup_columns"
-
-# Very long key names
-python3 -c "
-import sys
-key = 'A' * 1000
-msg = '{\"' + key + '\":1,\"columns\":80,\"rows\":24}'
-sys.stdout.buffer.write(b'\x00\x00' + msg.encode())
-" > "$DIR/json_long_key"
-
-# Whitespace variations in JSON
-printf '\x00\x00{ "columns" : 80 , "rows" : 24 }' > "$DIR/json_extra_whitespace"
-printf '\x00\x00{"columns":80,"rows":24}\x00garbage' > "$DIR/json_trailing_garbage"
-
-# Large credential
-python3 -c "
-import sys
-cred = b'A' * 200
-sys.stdout.buffer.write(b'\x01\xc8' + cred + b'{\"AuthToken\":\"' + cred + b'\"}')
-" > "$DIR/auth_long_cred"
-
-echo "[+] Generated $(ls "$DIR" | wc -l) seeds for websocket_auth"
-
-###############################################################################
-echo
-echo "[*] Generating corpus for fuzz_http_parsing"
-echo " Format: raw HTTP request"
-
-DIR="$SCRIPT_DIR/corpus/http_parsing"
-
-# Standard valid requests
-printf 'GET / HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/get_root"
-printf 'GET /token HTTP/1.1\r\nHost: localhost:7681\r\nAuthorization: Basic YWRtaW46cGFzcw==\r\n\r\n' > "$DIR/get_with_auth"
-printf 'GET /ws HTTP/1.1\r\nHost: localhost:7681\r\nOrigin: http://localhost:7681\r\n\r\n' > "$DIR/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' > "$DIR/get_full_headers"
-printf 'POST /token HTTP/1.1\r\nHost: localhost:7681\r\nContent-Length: 0\r\n\r\n' > "$DIR/post_token"
-
-# Path traversal attempts
-printf 'GET /../../../etc/passwd HTTP/1.1\r\nHost: localhost\r\n\r\n' > "$DIR/path_traversal_basic"
-printf 'GET /..%%2f..%%2f..%%2fetc%%2fpasswd HTTP/1.1\r\nHost: localhost\r\n\r\n' > "$DIR/path_traversal_encoded"
-printf 'GET /%%2e%%2e/%%2e%%2e/etc/passwd HTTP/1.1\r\nHost: localhost\r\n\r\n' > "$DIR/path_traversal_dot_encoded"
-printf 'GET //etc/passwd HTTP/1.1\r\nHost: localhost\r\n\r\n' > "$DIR/path_double_slash"
-printf 'GET /./././../etc/passwd HTTP/1.1\r\nHost: localhost\r\n\r\n' > "$DIR/path_traversal_mixed"
-printf 'GET /\x00/etc/passwd HTTP/1.1\r\nHost: localhost\r\n\r\n' > "$DIR/path_null_byte"
-
-# Long paths
-python3 -c "
-import sys
-path = '/' + 'A' * 127 # at MAX_PATH_LEN boundary
-sys.stdout.buffer.write(f'GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n'.encode())
-" > "$DIR/path_long_127"
-
-python3 -c "
-import sys
-path = '/' + 'A' * 200 # over MAX_PATH_LEN
-sys.stdout.buffer.write(f'GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n'.encode())
-" > "$DIR/path_long_200"
-
-# Auth header edge cases
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic \r\n\r\n' > "$DIR/auth_empty_b64"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: \r\n\r\n' > "$DIR/auth_empty"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Bearer token123\r\n\r\n' > "$DIR/auth_bearer"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic dXNlcjpwYXNz\r\n\r\n' > "$DIR/auth_valid_b64"
-
-# Long auth header
-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())
-" > "$DIR/auth_long_value"
-
-# Auth with colon for extract_basic_auth
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic user:password\r\n\r\n' > "$DIR/auth_with_colon"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic :nouser\r\n\r\n' > "$DIR/auth_empty_user"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic nopass:\r\n\r\n' > "$DIR/auth_empty_pass"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic a:b:c:d:e\r\n\r\n' > "$DIR/auth_multi_colon"
-
-# Origin/Host CSRF checks
-printf 'GET / HTTP/1.1\r\nHost: localhost:7681\r\nOrigin: http://evil.com\r\n\r\n' > "$DIR/csrf_mismatch"
-printf 'GET / HTTP/1.1\r\nHost: localhost:7681\r\nOrigin: http://localhost:7681\r\n\r\n' > "$DIR/csrf_match"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nOrigin: http://localhost:9999\r\n\r\n' > "$DIR/csrf_port_diff"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nOrigin: https://localhost\r\n\r\n' > "$DIR/csrf_scheme_diff"
-printf 'GET / HTTP/1.1\r\nHost: target.com\r\nOrigin: http://target.com.evil.com\r\n\r\n' > "$DIR/csrf_subdomain_trick"
-
-# Malformed HTTP
-printf 'GARBAGE\r\n\r\n' > "$DIR/malformed_no_space"
-printf 'GET\r\n\r\n' > "$DIR/malformed_no_path"
-printf '\r\n\r\n' > "$DIR/malformed_empty_line"
-printf 'GET / HTTP/1.1\r\nBadHeader\r\n\r\n' > "$DIR/malformed_no_colon"
-printf 'GET / HTTP/1.1\r\n: empty-name\r\n\r\n' > "$DIR/malformed_empty_header_name"
-printf 'GET / HTTP/1.1\r\nHost:\r\n\r\n' > "$DIR/malformed_empty_header_value"
-
-# No terminating \r\n\r\n
-printf 'GET / HTTP/1.1\r\nHost: localhost' > "$DIR/malformed_no_end"
-
-# Lots of headers
-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())
-" > "$DIR/many_headers"
-
-# Header injection / CRLF injection
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nX-Injected\r\nAuthorization: Basic AAAA\r\n\r\n' > "$DIR/header_injection"
-printf 'GET / HTTP/1.1\r\nHost: localhost%%0d%%0aInjected: true\r\n\r\n' > "$DIR/crlf_encoded"
-
-# Format strings in various positions
-printf 'GET /%%s%%s%%s%%n HTTP/1.1\r\nHost: %%x%%x%%x\r\nAuthorization: Basic %%n%%n\r\n\r\n' > "$DIR/fmt_everywhere"
-
-# More HTTP methods
-printf 'PUT /api HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/method_put"
-printf 'DELETE /api HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/method_delete"
-printf 'PATCH /api HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/method_patch"
-printf 'OPTIONS / HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/method_options"
-printf 'HEAD / HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/method_head"
-printf 'CONNECT localhost:443 HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/method_connect"
-printf 'TRACE / HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/method_trace"
-
-# HTTP version variations
-printf 'GET / HTTP/0.9\r\n\r\n' > "$DIR/http_09"
-printf 'GET / HTTP/2.0\r\nHost: localhost\r\n\r\n' > "$DIR/http_20"
-printf 'GET / HTTP/9.9\r\nHost: localhost\r\n\r\n' > "$DIR/http_99"
-
-# WebSocket upgrade (relevant to ttyd)
-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' > "$DIR/ws_upgrade"
-printf 'GET /ws HTTP/1.1\r\nHost: localhost:7681\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nOrigin: http://localhost:7681\r\nAuthorization: Basic YWRtaW46cGFzcw==\r\n\r\n' > "$DIR/ws_upgrade_auth"
-
-# Duplicate headers
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic YWRtaW46cGFzcw==\r\nAuthorization: Basic d3Jvbmc6d3Jvbmc=\r\n\r\n' > "$DIR/dup_auth_headers"
-printf 'GET / HTTP/1.1\r\nHost: evil.com\r\nHost: localhost\r\n\r\n' > "$DIR/dup_host_headers"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\nOrigin: http://evil.com\r\nOrigin: http://localhost\r\n\r\n' > "$DIR/dup_origin_headers"
-
-# Very long header names / values
-python3 -c "
-import sys
-name = 'X-' + 'A' * 300
-req = f'GET / HTTP/1.1\r\nHost: localhost\r\n{name}: value\r\n\r\n'
-sys.stdout.buffer.write(req.encode())
-" > "$DIR/long_header_name"
-
-python3 -c "
-import sys
-value = 'A' * 1000
-req = f'GET / HTTP/1.1\r\nHost: localhost\r\nX-Custom: {value}\r\n\r\n'
-sys.stdout.buffer.write(req.encode())
-" > "$DIR/long_header_value"
-
-# Request with body
-printf 'POST /token HTTP/1.1\r\nHost: localhost:7681\r\nContent-Length: 27\r\nContent-Type: application/json\r\n\r\n{"username":"admin","pass":"x"}' > "$DIR/post_with_body"
-
-# ttyd-specific paths
-printf 'GET /ws HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/path_ws"
-printf 'GET /token HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/path_token"
-printf 'GET /auth_token.js HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/path_auth_token_js"
-printf 'GET /favicon.ico HTTP/1.1\r\nHost: localhost:7681\r\n\r\n' > "$DIR/path_favicon"
-
-# Tab and space variations in request line
-printf 'GET\t/\tHTTP/1.1\r\nHost: localhost\r\n\r\n' > "$DIR/tab_separated"
-printf 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n' > "$DIR/double_space_request"
-
-# LF-only line endings (no CR)
-printf 'GET / HTTP/1.1\nHost: localhost\nAuthorization: Basic YWRtaW46cGFzcw==\n\n' > "$DIR/lf_only"
-
-echo "[+] Generated $(ls "$DIR" | wc -l) seeds for http_parsing"
-
-###############################################################################
-echo
-TOTAL=$(find "$SCRIPT_DIR/corpus" -type f | wc -l)
-echo "[+] Total corpus: $TOTAL seed files"
-echo "[+] Corpus directories:"
-echo " corpus/auth_header/ ($(ls "$SCRIPT_DIR/corpus/auth_header" | wc -l) files)"
-echo " corpus/websocket_auth/ ($(ls "$SCRIPT_DIR/corpus/websocket_auth" | wc -l) files)"
-echo " corpus/http_parsing/ ($(ls "$SCRIPT_DIR/corpus/http_parsing" | wc -l) files)"
| | | | |