From bc1275badc0c74b43a96a7f9b2a933cc978dfecb Mon Sep 17 00:00:00 2001 From: Jonathon Leight Date: Sun, 5 Jul 2026 19:16:35 -0400 Subject: [PATCH] Cap the wsbridge KISS reassembly buffer --- internal/wsbridge/wsbridge.go | 27 +++++++++++++++++++++- internal/wsbridge/wsbridge_test.go | 36 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/internal/wsbridge/wsbridge.go b/internal/wsbridge/wsbridge.go index 060ab7d..7d74ca7 100644 --- a/internal/wsbridge/wsbridge.go +++ b/internal/wsbridge/wsbridge.go @@ -13,12 +13,27 @@ import ( "bytes" "context" "encoding/json" + "errors" "sync" "github.com/coder/websocket" "github.com/meshcore-go/meshcore-go/hardware" ) +// maxKissBuffer bounds the inbound reassembly buffer. A single KISS frame carries +// at most one MeshCore packet — MAX_TRANS_UNIT is 255 bytes over the air (firmware +// src/MeshCore.h) and meshcore-go caps a KISS packet at KISS_MAX_PACKET_SIZE=255 — +// so even with worst-case KISS byte-stuffing (every byte escaped ×2) plus FEND +// framing a frame is ~520 bytes on the wire. This buffer only ever holds one +// not-yet-FEND-terminated frame, so 4 KiB is generous headroom; if this much +// accumulates with no frame delimiter the peer is sending garbage, so we drop it +// instead of letting it grow without bound. +const maxKissBuffer = 4096 + +// ErrKissBufferOverflow is reported to the error handler when the reassembly +// buffer is dropped for exceeding maxKissBuffer with no frame boundary. +var ErrKissBufferOverflow = errors.New("wsbridge: inbound KISS buffer overflow; discarding") + // Conn implements hardware.Transport over a WebSocket. type Conn struct { ws *websocket.Conn @@ -101,8 +116,18 @@ func (c *Conn) Feed(data []byte) { c.buf = append(c.buf, data...) last := bytes.LastIndexByte(c.buf, hardware.KISS_FEND) if last < 0 { + // No complete frame boundary yet. Cap the buffer so a peer that never sends + // a FEND can't grow it without bound; drop it and report the overflow. + var eh func(error) + if len(c.buf) > maxKissBuffer { + c.buf = nil + eh = c.errH + } c.mu.Unlock() - return // no complete frame boundary yet + if eh != nil { + eh(ErrKissBufferOverflow) + } + return } processable := c.buf[:last+1] c.buf = append([]byte(nil), c.buf[last+1:]...) // bytes after the last FEND diff --git a/internal/wsbridge/wsbridge_test.go b/internal/wsbridge/wsbridge_test.go index e71d80a..5fba809 100644 --- a/internal/wsbridge/wsbridge_test.go +++ b/internal/wsbridge/wsbridge_test.go @@ -1,7 +1,9 @@ package wsbridge import ( + "bytes" "context" + "errors" "testing" "github.com/meshcore-go/meshcore-go/hardware" @@ -42,6 +44,40 @@ func TestFeedReassemblesAcrossChunkBoundaries(t *testing.T) { assertCmds(t, got, wantCmds, "byte-by-byte") } +// TestFeedCapsBufferWithoutFrameBoundary verifies that a peer streaming bytes +// with no FEND delimiter can't grow the reassembly buffer without bound: past the +// cap the buffer is dropped and the overflow is reported, and framing recovers on +// the next valid frame. +func TestFeedCapsBufferWithoutFrameBoundary(t *testing.T) { + t.Parallel() + c := New(context.Background(), nil) + + var errs []error + c.SetErrorHandler(func(err error) { errs = append(errs, err) }) + var got []*hardware.KissFrame + c.SetFrameHandler(func(f *hardware.KissFrame) { got = append(got, f) }) + + // Stream well past the cap with no FEND (0xC0) byte anywhere. + junk := bytes.Repeat([]byte{0x00}, maxKissBuffer+1024) + c.Feed(junk) + + c.mu.Lock() + buffered := len(c.buf) + c.mu.Unlock() + if buffered > maxKissBuffer { + t.Fatalf("buffer = %d bytes, want it dropped to <= %d", buffered, maxKissBuffer) + } + if len(errs) != 1 || !errors.Is(errs[0], ErrKissBufferOverflow) { + t.Fatalf("errs = %v, want one ErrKissBufferOverflow", errs) + } + + // Framing still works after the drop: a valid frame decodes normally. + c.Feed(hardware.EncodeDataFrame([]byte{0xAA, 0xBB})) + if len(got) != 1 || got[0].Command != hardware.KISS_CMD_DATA { + t.Fatalf("got %d frames after overflow, want 1 data frame", len(got)) + } +} + func feedChunks(t *testing.T, chunks ...[]byte) []*hardware.KissFrame { t.Helper() c := New(context.Background(), nil) // Feed doesn't touch the websocket