From 017be04f129123338771715461cae8f501c35691 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 11:15:00 +0000 Subject: [PATCH 1/2] fix(discord): grow gateway RX for trailing NUL to avoid heap OOB discord_rx_append grew only when cap < rx_len+len, then wrote a NUL at rx_len+len. Two 64KiB LWS fragments filled a doubled buffer exactly, so the terminator was one byte past the heap block (typical READY payloads). Co-authored-by: esadrianno --- src/channels/discord.c | 28 +++++---------- src/channels/discord_helpers.c | 36 ++++++++++++++++++++ src/channels/discord_helpers.h | 9 ++++- tests/test_discord_helpers.c | 62 ++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 20 deletions(-) diff --git a/src/channels/discord.c b/src/channels/discord.c index d64b510..195e303 100644 --- a/src/channels/discord.c +++ b/src/channels/discord.c @@ -142,6 +142,7 @@ struct discord_ctx { char *rx_buf; size_t rx_len; size_t rx_cap; + int rx_skip; int heartbeat_interval_ms; uint64_t last_seq; char *session_id; @@ -180,25 +181,8 @@ static void discord_rx_reset(struct discord_ctx *dc) /** Appends one RX fragment; returns 0 ok, -1 overflow. */ static int discord_rx_append(struct discord_ctx *dc, const void *in, size_t len) { - if (len > RX_MAX || dc->rx_len > RX_MAX - len) - return -1; - if (dc->rx_cap < dc->rx_len + len) { - size_t need = dc->rx_len + len + 1; - size_t ncap = dc->rx_cap ? dc->rx_cap * 2 : 4096; - while (ncap < need && ncap < RX_MAX) - ncap *= 2; - if (need > RX_MAX) - return -1; - char *p = realloc(dc->rx_buf, ncap); - if (!p) - return -1; - dc->rx_buf = p; - dc->rx_cap = ncap; - } - memcpy(dc->rx_buf + dc->rx_len, in, len); - dc->rx_len += len; - dc->rx_buf[dc->rx_len] = '\0'; - return 0; + return discord_helpers_rx_append(&dc->rx_buf, &dc->rx_len, &dc->rx_cap, in, len, + RX_MAX); } static int discord_send_json(struct lws *wsi, const char *json) @@ -558,9 +542,15 @@ static int callback_discord(struct lws *wsi, enum lws_callback_reasons reason, } break; case LWS_CALLBACK_CLIENT_RECEIVE: + if (dc->rx_skip) { + if (lws_is_final_fragment(wsi)) + dc->rx_skip = 0; + break; + } if (discord_rx_append(dc, in, len) != 0) { fprintf(stderr, "shellclaw: discord: gateway payload too large\n"); discord_rx_reset(dc); + dc->rx_skip = !lws_is_final_fragment(wsi); break; } if (lws_is_final_fragment(wsi)) { diff --git a/src/channels/discord_helpers.c b/src/channels/discord_helpers.c index 1e6e00b..f4258dc 100644 --- a/src/channels/discord_helpers.c +++ b/src/channels/discord_helpers.c @@ -6,6 +6,7 @@ #include "channels/discord_helpers.h" #include "channels/channel.h" #include +#include #include const char *discord_lifecycle_str(discord_lifecycle_t lc) @@ -139,3 +140,38 @@ int discord_helpers_send_backoff_ms(int attempt, double retry_after_sec, int jit sleep_ms = cap_ms; return sleep_ms; } + +int discord_helpers_rx_append(char **buf, size_t *len, size_t *cap, const void *in, + size_t chunk_len, size_t max_cap) +{ + size_t need; + size_t ncap; + char *p; + + if (!buf || !len || !cap || (!in && chunk_len != 0) || max_cap < 1) + return -1; + if (chunk_len > max_cap || *len > max_cap - chunk_len) + return -1; + need = *len + chunk_len + 1; + if (need > max_cap) + return -1; + if (*cap < need) { + ncap = *cap ? *cap * 2 : 4096; + while (ncap < need) + ncap *= 2; + if (ncap > max_cap) + ncap = max_cap; + if (ncap < need) + return -1; + p = realloc(*buf, ncap); + if (!p) + return -1; + *buf = p; + *cap = ncap; + } + if (chunk_len > 0) + memcpy(*buf + *len, in, chunk_len); + *len += chunk_len; + (*buf)[*len] = '\0'; + return 0; +} diff --git a/src/channels/discord_helpers.h b/src/channels/discord_helpers.h index 7632741..7359f45 100644 --- a/src/channels/discord_helpers.h +++ b/src/channels/discord_helpers.h @@ -1,7 +1,7 @@ /** * @file discord_helpers.h * @brief Pure helpers shared by Discord channel and unit tests (allowlist, session id, mentions, - * REST 429 backoff calculation). + * REST 429 backoff calculation, Gateway RX append). */ #ifndef SHELLCLAW_DISCORD_HELPERS_H @@ -53,6 +53,13 @@ int discord_helpers_route_message_create(const cJSON *payload, const char *const int allowed_count, const char *bot_user_id, char *session_out, size_t session_outsz); +/** + * Append one Gateway RX fragment, growing so payload plus a trailing NUL always fit. + * @return 0 on success, -1 if the total would exceed @p max_cap or allocation failed. + */ +int discord_helpers_rx_append(char **buf, size_t *len, size_t *cap, const void *in, + size_t chunk_len, size_t max_cap); + #ifdef __cplusplus } #endif diff --git a/tests/test_discord_helpers.c b/tests/test_discord_helpers.c index 954bfdb..02e1cb6 100644 --- a/tests/test_discord_helpers.c +++ b/tests/test_discord_helpers.c @@ -7,6 +7,7 @@ #include "channels/channel.h" #include "cJSON.h" #include +#include #include #define ASSERT(c) do { if (!(c)) { fprintf(stderr, "FAIL: %s:%d %s\n", __FILE__, __LINE__, #c); return 1; } } while (0) @@ -99,6 +100,65 @@ static int test_route_message_create_fixture(void) return 0; } +/** Exact payload fill of a power-of-two cap must still reserve one byte for NUL. */ +static int test_rx_append_grows_for_nul(void) +{ + char *buf = NULL; + size_t len = 0; + size_t cap = 0; + char a[4095]; + char one = 'Z'; + char *big; + char *big2; + + memset(a, 'A', sizeof(a)); + ASSERT(discord_helpers_rx_append(&buf, &len, &cap, a, sizeof(a), 512 * 1024) == 0); + ASSERT(len == sizeof(a)); + ASSERT(cap >= len + 1); + ASSERT(buf[len] == '\0'); + ASSERT(discord_helpers_rx_append(&buf, &len, &cap, &one, 1, 512 * 1024) == 0); + ASSERT(len == 4096); + ASSERT(cap >= 4097); + ASSERT(buf[4096] == '\0'); + ASSERT(buf[4095] == 'Z'); + + free(buf); + buf = NULL; + len = 0; + cap = 0; + big = malloc(65536); + big2 = malloc(65536); + ASSERT(big && big2); + memset(big, 'B', 65536); + memset(big2, 'C', 65536); + ASSERT(discord_helpers_rx_append(&buf, &len, &cap, big, 65536, 512 * 1024) == 0); + ASSERT(len == 65536); + ASSERT(cap == 131072); + ASSERT(discord_helpers_rx_append(&buf, &len, &cap, big2, 65536, 512 * 1024) == 0); + ASSERT(len == 131072); + ASSERT(cap >= 131073); + ASSERT(buf[131072] == '\0'); + ASSERT(buf[0] == 'B' && buf[65535] == 'B'); + ASSERT(buf[65536] == 'C' && buf[131071] == 'C'); + free(big); + free(big2); + free(buf); + return 0; +} + +static int test_rx_append_rejects_over_max(void) +{ + char *buf = NULL; + size_t len = 0; + size_t cap = 0; + char chunk[16]; + + memset(chunk, 'x', sizeof(chunk)); + ASSERT(discord_helpers_rx_append(&buf, &len, &cap, chunk, sizeof(chunk), 8) != 0); + ASSERT(buf == NULL); + return 0; +} + int main(void) { RUN(test_allow_entry_equals()); @@ -108,6 +168,8 @@ int main(void) RUN(test_backoff_math()); RUN(test_lifecycle_str()); RUN(test_route_message_create_fixture()); + RUN(test_rx_append_grows_for_nul()); + RUN(test_rx_append_rejects_over_max()); printf("test_discord_helpers: all tests passed\n"); return 0; } From 6fa01ac653dc2f663f931b82a9394e5c360151da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 11:15:03 +0000 Subject: [PATCH 2/2] fix(gateway): raise WebChat WS rx_buffer_size so inbound JSON is not split LWS delivers at most rx_buffer_size bytes per RECEIVE. The ws protocol used 256 with no fragment reassembly, so dashboard messages over ~220 characters were parsed as incomplete JSON and dropped. Size the buffer to fit a full WebChat frame (MSG_MAX plus JSON envelope). Distinct from outbound #62. Co-authored-by: esadrianno --- src/gateway/http.c | 2 +- src/gateway/http_lws.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gateway/http.c b/src/gateway/http.c index 7bcd4bf..5e06c04 100644 --- a/src/gateway/http.c +++ b/src/gateway/http.c @@ -37,7 +37,7 @@ static const struct lws_protocols protocols[] = { { .name = "ws", .callback = ws_callback, - .rx_buffer_size = 256, + .rx_buffer_size = WS_RX_BUFFER_SIZE, }, { .name = NULL }, }; diff --git a/src/gateway/http_lws.h b/src/gateway/http_lws.h index d7c4ea0..2f13c99 100644 --- a/src/gateway/http_lws.h +++ b/src/gateway/http_lws.h @@ -22,6 +22,8 @@ extern "C" { #define CONFIG_BODY_MAX 65536 #define BODY_BUF_SIZE CONFIG_BODY_MAX #define ASAP_BODY_MAX (1024 * 1024) +/** Per-callback WebChat RX. Must fit a full `{"type":"message","text":...}` frame (MSG_MAX). */ +#define WS_RX_BUFFER_SIZE 32768 enum http_method { HTTP_GET = 1,