Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ All notable changes to ShellClaw are documented here. Format follows [Keep a Cha
## [Unreleased]

### Fixed
- Discord Gateway RX grows for the trailing NUL so two 64 KiB libwebsockets fragments cannot write one byte past the heap block (typical READY payloads).
- WebChat inbound WS `rx_buffer_size` is `WS_RX_BUFFER_SIZE` (`WS_TEXT_MAX` plus JSON envelope) so dashboard messages are not split across 256-byte RECEIVE callbacks and dropped.
- WebChat WebSocket sends now accept agent replies up to 32 KiB (`WS_TEXT_MAX`, matching `RESPONSE_BUF_SIZE`) instead of silently dropping payloads above 8 KiB. Dest buffers are `WS_TEXT_BUF_SIZE` so a max-length payload keeps its NUL; a too-large frame is logged instead of skipped with `<`.
- Memory injection is skipped when the system prompt already fills its 64 KiB buffer, instead of writing the full `Relevant memories` prefix past the allocation after clamping recall to 0.
- Session JSON that would exceed the 128 KiB cap is refused instead of truncated, so the next parse cannot wipe history. An oversized stored blob is left in place (distinct `SESSION_LOAD_TOO_LARGE`) rather than replaced by a later small turn.
Expand Down
29 changes: 10 additions & 19 deletions src/channels/discord.c
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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;
Expand Down Expand Up @@ -176,30 +177,14 @@ static uint64_t discord_now_ms(void)
static void discord_rx_reset(struct discord_ctx *dc)
{
dc->rx_len = 0;
dc->rx_skip = 0;
}

/** 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)
Expand Down Expand Up @@ -538,9 +523,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);
Comment thread
cursor[bot] marked this conversation as resolved.
break;
}
if (lws_is_final_fragment(wsi)) {
Expand Down
36 changes: 36 additions & 0 deletions src/channels/discord_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "channels/discord_helpers.h"
#include "channels/channel.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const char *discord_lifecycle_str(discord_lifecycle_t lc)
Expand Down Expand Up @@ -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;

if (!buf || !len || !cap || (!in && chunk_len != 0) || max_cap < 1)
Comment thread
cursor[bot] marked this conversation as resolved.
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) {
size_t ncap = *cap ? *cap * 2 : 4096;
char *p;

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;
}
12 changes: 11 additions & 1 deletion src/channels/discord_helpers.h
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -53,6 +53,16 @@ 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.
*
* Example: two 64 KiB LWS fragments must grow past 131072 so the NUL fits.
*/
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
Expand Down
5 changes: 4 additions & 1 deletion src/gateway/http.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ static const struct lws_protocols protocols[] = {
{
.name = "ws",
.callback = ws_callback,
.rx_buffer_size = 256,
.rx_buffer_size = WS_RX_BUFFER_SIZE,
},
{ .name = NULL },
};

_Static_assert(WS_RX_BUFFER_SIZE >= (size_t)WS_TEXT_MAX + 32,
"WebChat rx_buffer_size must fit JSON envelope around WS_TEXT_MAX");
Comment thread
cursor[bot] marked this conversation as resolved.

static const struct lws_http_mount mount_http = {
.mountpoint = "/",
.origin = "http",
Expand Down
3 changes: 3 additions & 0 deletions src/gateway/http_lws.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "core/version.h"
#include "gateway/auth.h"
#include "gateway/lws_compat.h"
#include "gateway/ws.h"
#include <libwebsockets.h>
#include <pthread.h>
#include <time.h>
Expand All @@ -24,6 +25,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 `{"type":"message","text":...}` at WS_TEXT_MAX. */
#define WS_RX_BUFFER_SIZE (WS_TEXT_MAX + 64)

enum http_method {
HTTP_GET = 1,
Expand Down
69 changes: 69 additions & 0 deletions tests/test_discord_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "channels/channel.h"
#include "cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ASSERT(c) do { if (!(c)) { fprintf(stderr, "FAIL: %s:%d %s\n", __FILE__, __LINE__, #c); return 1; } } while (0)
Expand Down Expand Up @@ -140,6 +141,72 @@ static int test_route_message_create_rejects_edge_cases(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;
size_t len;
size_t cap;
char a[4095];
char one;
char *big;
char *big2;

buf = NULL;
len = 0;
cap = 0;
one = 'Z';
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;
size_t len;
size_t cap;
char chunk[16];

buf = NULL;
len = 0;
cap = 0;
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());
Expand All @@ -150,6 +217,8 @@ int main(void)
RUN(test_lifecycle_str());
RUN(test_route_message_create_fixture());
RUN(test_route_message_create_rejects_edge_cases());
RUN(test_rx_append_grows_for_nul());
RUN(test_rx_append_rejects_over_max());
printf("test_discord_helpers: all tests passed\n");
return 0;
}
Loading