diff --git a/fuzzing/ttyd/build_fuzzers.sh b/fuzzing/ttyd/build_fuzzers.sh
@@ -316,6 +316,18 @@ echo "[*] Building fuzz_http_parsing..."
$CC $COMMON_CFLAGS fuzz_http_parsing.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS -o fuzz_http_parsing$SUFFIX
echo "[+] Built: fuzz_http_parsing$SUFFIX"
+echo "[*] Building fuzz_ws_fragments..."
+$CC $COMMON_CFLAGS fuzz_ws_fragments.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS -o fuzz_ws_fragments$SUFFIX
+echo "[+] Built: fuzz_ws_fragments$SUFFIX"
+
+echo "[*] Building fuzz_prefs_message..."
+$CC $COMMON_CFLAGS fuzz_prefs_message.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS -o fuzz_prefs_message$SUFFIX
+echo "[+] Built: fuzz_prefs_message$SUFFIX"
+
+echo "[*] Building fuzz_json_recv..."
+$CC $COMMON_CFLAGS fuzz_json_recv.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS -o fuzz_json_recv$SUFFIX
+echo "[+] Built: fuzz_json_recv$SUFFIX"
+
# CmpLog variants (AFL++ only)
if [ "$MODE" = "afl" ]; then
echo
@@ -326,6 +338,15 @@ if [ "$MODE" = "afl" ]; then
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"
+ AFL_LLVM_CMPLOG=1 $CC $COMMON_CFLAGS fuzz_ws_fragments.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS \
+ -o fuzz_ws_fragments_cmplog
+ echo "[+] Built: fuzz_ws_fragments_cmplog"
+ AFL_LLVM_CMPLOG=1 $CC $COMMON_CFLAGS fuzz_prefs_message.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS \
+ -o fuzz_prefs_message_cmplog
+ echo "[+] Built: fuzz_prefs_message_cmplog"
+ AFL_LLVM_CMPLOG=1 $CC $COMMON_CFLAGS fuzz_json_recv.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS \
+ -o fuzz_json_recv_cmplog
+ echo "[+] Built: fuzz_json_recv_cmplog"
echo
echo "[*] Building HTTP custom mutator..."
diff --git a/fuzzing/ttyd/fuzz_json_recv.c b/fuzzing/ttyd/fuzz_json_recv.c
@@ -0,0 +1,152 @@
+/*
+ * AFL++ / LibFuzzer harness — RECEIVE command state machine
+ *
+ * Target: callback_tty RECEIVE handler, protocol.c:307-354
+ *
+ * The existing fuzz_websocket_auth harness sends exactly one RECEIVE call.
+ * This harness sends up to 4 sequential RECEIVE calls, exercising:
+ *
+ * Phase 1 (pss->process == NULL):
+ * JSON_DATA ('{') → parse_window_size() + auth token check + spawn_process()
+ * RESIZE_TERMINAL ('1') → parse_window_size(buf+1, len-1) — no process, no-op
+ *
+ * Phase 2 (pss->process != NULL, after JSON_DATA):
+ * RESIZE_TERMINAL → parse_window_size(buf+1, len-1) + pty_resize()
+ * INPUT ('0') → pty_write() (mock returns -1 → callback returns -1)
+ * PAUSE ('2') / RESUME ('3') → pty_pause/pty_resume (mock no-ops)
+ *
+ * Key targets:
+ * - parse_window_size() with arbitrary JSON (json-c attack surface)
+ * - Integer truncation: (uint16_t)json_object_get_int() with extreme values
+ * - State machine transitions across multiple messages
+ * - Buffer management: pss->buffer malloc/free per RECEIVE, no carry-over
+ *
+ * Input format:
+ * [num_msgs:1] 1–4 messages (bottom 2 bits + 1)
+ * per message:
+ * [cmd:1] command byte ('0', '1', '2', '3', '{', or other)
+ * [len:2 BE] payload length (0 is valid but produces len=1 buffer)
+ * [payload...] raw bytes
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <json-c/json.h>
+
+#include "fuzz_lws_mock.h"
+#include "ttyd-src/src/protocol.c"
+
+extern struct server _fuzz_server;
+extern void fuzz_server_reset(void);
+
+#define MAX_MSGS 4
+
+int fuzz_target(const uint8_t *data, size_t size) {
+ if (size < 4) return 0;
+
+ int num_msgs = (data[0] & 0x03) + 1;
+ size_t off = 1;
+
+ struct {
+ uint8_t cmd;
+ const uint8_t *payload;
+ size_t len;
+ } msgs[MAX_MSGS];
+ int parsed = 0;
+
+ for (int i = 0; i < num_msgs && off + 3 <= size; i++) {
+ uint8_t cmd = data[off++];
+ uint16_t plen = (uint16_t)((data[off] << 8) | data[off + 1]);
+ off += 2;
+ if (plen > size - off) plen = (uint16_t)(size - off);
+ msgs[parsed].cmd = cmd;
+ msgs[parsed].payload = data + off;
+ msgs[parsed].len = plen;
+ off += plen;
+ parsed++;
+ }
+
+ if (parsed == 0) return 0;
+
+ fuzz_lws_reset();
+ fuzz_server_reset();
+ _fuzz_server.writable = true;
+ _fuzz_server.command = "bash";
+ _fuzz_server.prefs_json = "{}";
+ strncpy(_fuzz_server.terminal_type, "xterm-256color",
+ sizeof(_fuzz_server.terminal_type) - 1);
+ fuzz_lws_set_uri_path("/ws", 3);
+
+ struct lws mock_wsi = {0};
+ struct pss_tty pss;
+ memset(&pss, 0, sizeof(pss));
+
+ if (callback_tty(&mock_wsi, LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION,
+ &pss, NULL, 0) != 0)
+ goto cleanup;
+
+ if (callback_tty(&mock_wsi, LWS_CALLBACK_ESTABLISHED,
+ &pss, NULL, 0) != 0)
+ goto cleanup;
+
+ for (int i = 0; i < 3; i++)
+ callback_tty(&mock_wsi, LWS_CALLBACK_SERVER_WRITEABLE, &pss, NULL, 0);
+
+ for (int i = 0; i < parsed; i++) {
+ /* Full RECEIVE buffer: [cmd_byte][payload...] */
+ size_t buf_len = 1 + msgs[i].len;
+ char *buf = malloc(buf_len);
+ if (!buf) break;
+ buf[0] = (char)msgs[i].cmd;
+ if (msgs[i].len > 0)
+ memcpy(buf + 1, msgs[i].payload, msgs[i].len);
+
+ fuzz_lws_set_fragment(1, 0);
+ int ret = callback_tty(&mock_wsi, LWS_CALLBACK_RECEIVE,
+ &pss, buf, buf_len);
+ free(buf);
+
+ if (ret != 0) break;
+ }
+
+cleanup:
+ fuzz_lws_set_fragment(1, 0);
+ callback_tty(&mock_wsi, LWS_CALLBACK_CLOSED, &pss, NULL, 0);
+ return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+int main(void) {
+#ifdef __AFL_HAVE_MANUAL_CONTROL
+ __AFL_INIT();
+#endif
+ unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
+ while (__AFL_LOOP(10000)) {
+ fuzz_target(buf, __AFL_FUZZ_TESTCASE_LEN);
+ }
+ return 0;
+}
+
+#else
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ return fuzz_target(data, size);
+}
+
+#ifndef LIBFUZZER
+int main(int argc, char **argv) {
+ 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 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
+#endif
diff --git a/fuzzing/ttyd/fuzz_lws_mock.c b/fuzzing/ttyd/fuzz_lws_mock.c
@@ -25,6 +25,9 @@ char _fuzz_uri_args[FUZZ_MAX_URI_ARGS][256];
int _fuzz_uri_args_len[FUZZ_MAX_URI_ARGS];
int _fuzz_uri_args_count;
+int _fuzz_is_final_fragment = 1;
+size_t _fuzz_remaining_payload = 0;
+
void fuzz_lws_reset(void) {
memset(_fuzz_auth_header, 0, sizeof(_fuzz_auth_header));
_fuzz_auth_header_len = 0;
@@ -39,6 +42,8 @@ void fuzz_lws_reset(void) {
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;
+ _fuzz_is_final_fragment = 1;
+ _fuzz_remaining_payload = 0;
}
/* ── header accessors ────────────────────────────────────────────────── */
@@ -163,8 +168,8 @@ 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_is_final_fragment(struct lws *wsi) { return _fuzz_is_final_fragment; }
+size_t lws_remaining_packet_payload(struct lws *wsi) { return _fuzz_remaining_payload; }
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;
diff --git a/fuzzing/ttyd/fuzz_prefs_message.c b/fuzzing/ttyd/fuzz_prefs_message.c
@@ -0,0 +1,129 @@
+/*
+ * AFL++ / LibFuzzer harness — send_initial_message() stack overflow
+ *
+ * Target: protocol.c:26 (SET_WINDOW_TITLE) and protocol.c:29 (SET_PREFERENCES)
+ *
+ * unsigned char message[LWS_PRE + 1 + 4096]; // 4129 bytes
+ * unsigned char *p = &message[LWS_PRE]; // 4097 bytes of headroom
+ *
+ * SET_WINDOW_TITLE:
+ * n = sprintf((char *)p, "%c%s (%s)", cmd, server->command, hostname);
+ * Overflows when server->command > ~3965 bytes (hostname takes up to 127).
+ *
+ * SET_PREFERENCES:
+ * n = sprintf((char *)p, "%c%s", cmd, server->prefs_json);
+ * Overflows when server->prefs_json > 4095 bytes.
+ *
+ * In real ttyd, both fields are admin-controlled (CLI flags --pref, command
+ * argv). This harness makes them fuzzer-controlled to find the overflow and
+ * any downstream UB (wrong return value → unexpected -1 from send_initial_message
+ * → lws_close_reason path, etc.).
+ *
+ * Input format:
+ * [prefs_len:2 BE][prefs_json bytes][cmd_len:2 BE][command bytes]
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <json-c/json.h>
+
+#include "fuzz_lws_mock.h"
+#include "ttyd-src/src/protocol.c"
+
+extern struct server _fuzz_server;
+extern void fuzz_server_reset(void);
+
+static char prefs_buf[65536];
+static char cmd_buf[65536];
+
+int fuzz_target(const uint8_t *data, size_t size) {
+ if (size < 4) return 0;
+
+ /* Parse prefs_json */
+ uint16_t prefs_len = (uint16_t)((data[0] << 8) | data[1]);
+ size_t off = 2;
+ if (prefs_len > size - off) prefs_len = (uint16_t)(size - off);
+ if (prefs_len > sizeof(prefs_buf)-1) prefs_len = sizeof(prefs_buf) - 1;
+ memcpy(prefs_buf, data + off, prefs_len);
+ prefs_buf[prefs_len] = '\0';
+ off += prefs_len;
+
+ /* Parse command */
+ if (off + 2 > size) return 0;
+ uint16_t cmd_len = (uint16_t)((data[off] << 8) | data[off + 1]);
+ off += 2;
+ if (cmd_len > size - off) cmd_len = (uint16_t)(size - off);
+ if (cmd_len > sizeof(cmd_buf) - 1) cmd_len = sizeof(cmd_buf) - 1;
+ memcpy(cmd_buf, data + off, cmd_len);
+ cmd_buf[cmd_len] = '\0';
+
+ fuzz_lws_reset();
+ fuzz_server_reset();
+ _fuzz_server.prefs_json = prefs_buf;
+ _fuzz_server.command = cmd_buf;
+ strncpy(_fuzz_server.terminal_type, "xterm-256color",
+ sizeof(_fuzz_server.terminal_type) - 1);
+ fuzz_lws_set_uri_path("/ws", 3);
+
+ struct lws mock_wsi = {0};
+ struct pss_tty pss;
+ memset(&pss, 0, sizeof(pss));
+
+ /* FILTER: no auth, no origin — always passes */
+ if (callback_tty(&mock_wsi, LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION,
+ &pss, NULL, 0) != 0)
+ goto cleanup;
+
+ if (callback_tty(&mock_wsi, LWS_CALLBACK_ESTABLISHED,
+ &pss, NULL, 0) != 0)
+ goto cleanup;
+
+ /*
+ * Drive through:
+ * WRITEABLE call 1 → SET_WINDOW_TITLE (sprintf with server->command)
+ * WRITEABLE call 2 → SET_PREFERENCES (sprintf with server->prefs_json)
+ * WRITEABLE call 3 → sets initialized=true, calls pty_resume(NULL)
+ */
+ for (int i = 0; i < 3; i++)
+ callback_tty(&mock_wsi, LWS_CALLBACK_SERVER_WRITEABLE, &pss, NULL, 0);
+
+cleanup:
+ callback_tty(&mock_wsi, LWS_CALLBACK_CLOSED, &pss, NULL, 0);
+ return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+int main(void) {
+#ifdef __AFL_HAVE_MANUAL_CONTROL
+ __AFL_INIT();
+#endif
+ unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
+ while (__AFL_LOOP(10000)) {
+ fuzz_target(buf, __AFL_FUZZ_TESTCASE_LEN);
+ }
+ return 0;
+}
+
+#else
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ return fuzz_target(data, size);
+}
+
+#ifndef LIBFUZZER
+int main(int argc, char **argv) {
+ 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 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
+#endif
diff --git a/fuzzing/ttyd/fuzz_ws_fragments.c b/fuzzing/ttyd/fuzz_ws_fragments.c
@@ -0,0 +1,142 @@
+/*
+ * AFL++ / LibFuzzer harness — WebSocket fragmented message accumulation
+ *
+ * Target: callback_tty RECEIVE path, protocol.c:284-293
+ *
+ * if (pss->buffer == NULL) {
+ * pss->buffer = xmalloc(len);
+ * pss->len = len;
+ * memcpy(pss->buffer, in, len);
+ * } else {
+ * pss->buffer = xrealloc(pss->buffer, pss->len + len); <-- integer overflow
+ * memcpy(pss->buffer + pss->len, in, len);
+ * pss->len += len;
+ * }
+ *
+ * The existing fuzz_websocket_auth harness never reaches the xrealloc branch:
+ * lws_is_final_fragment() and lws_remaining_packet_payload() are hardcoded to
+ * 1 and 0 respectively, so the early-return at protocol.c:303 is never taken.
+ *
+ * This harness makes both values fuzzer-controlled so AFL++ can explore:
+ * - Multi-fragment accumulation across N RECEIVE calls
+ * - Integer overflow: pss->len + len → xrealloc(buf, 0) → heap overflow
+ * - Command dispatch on the fully-assembled multi-fragment message
+ *
+ * Input format:
+ * [num_frags:1] number of fragments, 1–8 (bottom 3 bits)
+ * [cmd:1] WebSocket command byte prepended to the first fragment
+ * [payload...] raw bytes split evenly across fragments
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <json-c/json.h>
+
+#include "fuzz_lws_mock.h"
+#include "ttyd-src/src/protocol.c"
+
+extern struct server _fuzz_server;
+extern void fuzz_server_reset(void);
+
+int fuzz_target(const uint8_t *data, size_t size) {
+ /* need: num_frags(1) + cmd(1) + at least 1 payload byte */
+ if (size < 3) return 0;
+
+ int num_frags = (data[0] & 0x07) + 1; /* 1–8 */
+ uint8_t cmd = data[1];
+ const uint8_t *payload = data + 2;
+ size_t payload_size = size - 2;
+
+ /* Build the full virtual message: [cmd][payload...] */
+ size_t total = payload_size + 1;
+ char *msg = malloc(total);
+ if (!msg) return 0;
+ msg[0] = (char)cmd;
+ memcpy(msg + 1, payload, payload_size);
+
+ fuzz_lws_reset();
+ fuzz_server_reset();
+ _fuzz_server.writable = true;
+ _fuzz_server.command = "bash";
+ _fuzz_server.prefs_json = "{}";
+ strncpy(_fuzz_server.terminal_type, "xterm-256color",
+ sizeof(_fuzz_server.terminal_type) - 1);
+ fuzz_lws_set_uri_path("/ws", 3);
+
+ struct lws mock_wsi = {0};
+ struct pss_tty pss;
+ memset(&pss, 0, sizeof(pss));
+
+ if (callback_tty(&mock_wsi, LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION,
+ &pss, NULL, 0) != 0)
+ goto cleanup;
+
+ if (callback_tty(&mock_wsi, LWS_CALLBACK_ESTABLISHED,
+ &pss, NULL, 0) != 0)
+ goto cleanup;
+
+ /* Burn through the two initial messages + initialization sentinel */
+ for (int i = 0; i < 3; i++)
+ callback_tty(&mock_wsi, LWS_CALLBACK_SERVER_WRITEABLE, &pss, NULL, 0);
+
+ /* Split msg into num_frags fragments and send each as a RECEIVE call */
+ size_t frag_size = total / (size_t)num_frags;
+ if (frag_size == 0) frag_size = 1;
+
+ size_t off = 0;
+ for (int i = 0; i < num_frags && off < total; i++) {
+ size_t this_len = (i == num_frags - 1) ? (total - off) : frag_size;
+ if (off + this_len > total) this_len = total - off;
+
+ int is_final = (i == num_frags - 1) ? 1 : 0;
+ size_t remaining = is_final ? 0 : (total - off - this_len);
+
+ fuzz_lws_set_fragment(is_final, remaining);
+ int ret = callback_tty(&mock_wsi, LWS_CALLBACK_RECEIVE,
+ &pss, msg + off, this_len);
+ if (ret != 0) break;
+ off += this_len;
+ }
+
+cleanup:
+ free(msg);
+ fuzz_lws_set_fragment(1, 0);
+ callback_tty(&mock_wsi, LWS_CALLBACK_CLOSED, &pss, NULL, 0);
+ return 0;
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+int main(void) {
+#ifdef __AFL_HAVE_MANUAL_CONTROL
+ __AFL_INIT();
+#endif
+ unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
+ while (__AFL_LOOP(10000)) {
+ fuzz_target(buf, __AFL_FUZZ_TESTCASE_LEN);
+ }
+ return 0;
+}
+
+#else
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ return fuzz_target(data, size);
+}
+
+#ifndef LIBFUZZER
+int main(int argc, char **argv) {
+ 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 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
+#endif
diff --git a/fuzzing/ttyd/gen_corpus.py b/fuzzing/ttyd/gen_corpus.py
@@ -204,11 +204,132 @@ http_seeds = {
}
+# ── ws_fragments seeds ────────────────────────────────────────────────────────
+# Format: [num_frags:1][cmd:1][payload...]
+
+def frags_seed(num_frags, cmd, payload):
+ return bytes([num_frags & 0x07, cmd]) + payload
+
+CMD_JSON = ord('{')
+CMD_INPUT = ord('0')
+CMD_RESIZE= ord('1')
+CMD_PAUSE = ord('2')
+
+ws_frags_seeds = {
+ "single_json": frags_seed(1, CMD_JSON, b'{"columns":80,"rows":24}'),
+ "two_json": frags_seed(2, CMD_JSON, b'{"columns":80,"rows":24}'),
+ "four_json": frags_seed(4, CMD_JSON, b'{"columns":200,"rows":50}abcdef'),
+ "single_resize": frags_seed(1, CMD_RESIZE,b'{"columns":120,"rows":40}'),
+ "three_resize": frags_seed(3, CMD_RESIZE,b'{"columns":120,"rows":40}padding'),
+ "single_input": frags_seed(1, CMD_INPUT, b'hello world'),
+ "two_input": frags_seed(2, CMD_INPUT, b'hello world long payload here'),
+ "eight_input": frags_seed(8, CMD_INPUT, b'A' * 64),
+ "single_pause": frags_seed(1, CMD_PAUSE, b''),
+ "large_single": frags_seed(1, CMD_INPUT, b'X' * 512),
+ "malformed_json": frags_seed(2, CMD_JSON, b'{invalid'),
+ "empty_payload": frags_seed(1, CMD_JSON, b'{}'),
+}
+
+# ── prefs_message seeds ───────────────────────────────────────────────────────
+# Format: [prefs_len:2 BE][prefs_json][cmd_len:2 BE][command]
+
+def prefs_seed(prefs, command):
+ return (struct.pack(">H", len(prefs)) + prefs +
+ struct.pack(">H", len(command)) + command)
+
+prefs_seeds = {
+ "normal": prefs_seed(b'{}', b'bash'),
+ "prefs_medium": prefs_seed(b'{"theme":"light"}', b'bash'),
+ "prefs_boundary": prefs_seed(b'{"x":"' + b'A'*4080 + b'"}', b'bash'),
+ "prefs_overflow": prefs_seed(b'A' * 4097, b'bash'),
+ "prefs_overflow2": prefs_seed(b'A' * 8192, b'bash'),
+ "cmd_medium": prefs_seed(b'{}', b'/bin/bash'),
+ "cmd_boundary": prefs_seed(b'{}', b'/bin/' + b'A'*3965),
+ "cmd_overflow": prefs_seed(b'{}', b'A' * 4000),
+ "both_large": prefs_seed(b'A' * 3000, b'B' * 3000),
+ "empty_prefs": prefs_seed(b'', b'bash'),
+ "empty_cmd": prefs_seed(b'{}', b''),
+ "fmt_string": prefs_seed(b'%s%s%n%n', b'%s%n'),
+}
+
+# ── json_recv seeds ───────────────────────────────────────────────────────────
+# Format: [num_msgs:1][cmd:1][len:2 BE][payload...] × N
+
+def recv_msg(cmd, payload):
+ return bytes([cmd]) + struct.pack(">H", len(payload)) + payload
+
+def recv_seed(*messages):
+ n = min(len(messages), 4)
+ return bytes([n & 0x03]) + b''.join(messages[:n])
+
+JSON_DATA = ord('{')
+RESIZE = ord('1')
+INPUT_CMD = ord('0')
+PAUSE_CMD = ord('2')
+RESUME_CMD= ord('3')
+
+json_recv_seeds = {
+ # Single JSON_DATA
+ "json_only": recv_seed(recv_msg(JSON_DATA, b'{"columns":80,"rows":24}')),
+ "json_empty": recv_seed(recv_msg(JSON_DATA, b'{}')),
+ "json_malformed": recv_seed(recv_msg(JSON_DATA, b'{invalid}')),
+ "json_huge_cols": recv_seed(recv_msg(JSON_DATA, b'{"columns":65535,"rows":65535}')),
+ "json_neg": recv_seed(recv_msg(JSON_DATA, b'{"columns":-1,"rows":-1}')),
+ "json_overflow16": recv_seed(recv_msg(JSON_DATA, b'{"columns":65536,"rows":65536}')),
+ "json_deep": recv_seed(recv_msg(JSON_DATA, b'{"a":' * 30 + b'1' + b'}' * 30)),
+ "json_long_str": recv_seed(recv_msg(JSON_DATA, b'{"columns":80,"rows":24,"x":"' + b'A'*500 + b'"}')),
+
+ # JSON_DATA then RESIZE
+ "json_then_resize": recv_seed(
+ recv_msg(JSON_DATA, b'{"columns":80,"rows":24}'),
+ recv_msg(RESIZE, b'{"columns":120,"rows":40}'),
+ ),
+ "json_then_resize_bad": recv_seed(
+ recv_msg(JSON_DATA, b'{"columns":80,"rows":24}'),
+ recv_msg(RESIZE, b'{bad json'),
+ ),
+
+ # JSON_DATA then INPUT
+ "json_then_input": recv_seed(
+ recv_msg(JSON_DATA, b'{"columns":80,"rows":24}'),
+ recv_msg(INPUT_CMD, b'ls -la\n'),
+ ),
+
+ # JSON_DATA then PAUSE/RESUME
+ "json_pause_resume":recv_seed(
+ recv_msg(JSON_DATA, b'{"columns":80,"rows":24}'),
+ recv_msg(PAUSE_CMD, b''),
+ recv_msg(RESUME_CMD, b''),
+ ),
+
+ # Four messages
+ "full_sequence": recv_seed(
+ recv_msg(JSON_DATA, b'{"columns":80,"rows":24}'),
+ recv_msg(RESIZE, b'{"columns":160,"rows":50}'),
+ recv_msg(INPUT_CMD, b'echo hello\n'),
+ recv_msg(PAUSE_CMD, b''),
+ ),
+
+ # RESIZE before JSON_DATA (pss->process == NULL → no-op in RESIZE)
+ "resize_first": recv_seed(recv_msg(RESIZE, b'{"columns":80,"rows":24}')),
+
+ # Unknown command
+ "unknown_cmd": recv_seed(recv_msg(ord('Z'), b'some data')),
+ "unknown_two": recv_seed(
+ recv_msg(JSON_DATA, b'{"columns":80,"rows":24}'),
+ recv_msg(ord('X'), b'garbage'),
+ ),
+}
+
+
# ── main ─────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
- write_seeds("websocket_auth", ws_seeds)
- write_seeds("http_parsing", http_seeds)
+ write_seeds("websocket_auth", ws_seeds)
+ write_seeds("http_parsing", http_seeds)
+ write_seeds("ws_fragments", ws_frags_seeds)
+ write_seeds("prefs_message", prefs_seeds)
+ write_seeds("json_recv", json_recv_seeds)
- total = len(ws_seeds) + len(http_seeds)
+ total = len(ws_seeds) + len(http_seeds) + len(ws_frags_seeds) + len(prefs_seeds) + len(json_recv_seeds)
print(f"[+] Total corpus: {total} files")
| | | | | |