Cap the wsbridge KISS reassembly buffer

This commit is contained in:
Jonathon Leight
2026-07-05 19:16:35 -04:00
parent 0e106068e4
commit bc1275badc
2 changed files with 62 additions and 1 deletions
+26 -1
View File
@@ -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
+36
View File
@@ -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