From 102946aa3ba6d0dc73cb3508d1856e753e02c639 Mon Sep 17 00:00:00 2001 From: Pratik Dhanave Date: Sun, 2 Aug 2026 09:33:08 +0530 Subject: [PATCH] mcp: prevent MemoryEventStore.After panic on index past the end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After guarded only the lower bound (dl.first > start → ErrEventsPurged), so an index greater than the highest stored event made the final slices.Clone(dl.data[start-dl.first:]) panic with slice-bounds-out-of-range. The boundary start == dl.first+len(dl.data) (resume from the last event seen) is a valid empty replay, so only strictly-greater indices faulted. This is reachable from untrusted input: parseEventID accepts any non-negative index from the client-supplied Last-Event-ID header, and serveGET passes it to eventStore.After. Return an empty replay for an index at or beyond the latest stored event — there is nothing after it — matching the already-valid index == last case. Add regression coverage for indices past the end. Fixes #1131 --- mcp/event.go | 9 +++++++++ mcp/event_test.go | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/mcp/event.go b/mcp/event.go index bbb3e6c8..1d86b703 100644 --- a/mcp/event.go +++ b/mcp/event.go @@ -342,6 +342,15 @@ func (s *MemoryEventStore) After(_ context.Context, sessionID, streamID string, return nil, fmt.Errorf("MemoryEventStore.After: index %d, stream ID %v, session %q: %w", index, streamID, sessionID, ErrEventsPurged) } + // An index at or beyond the latest stored event has nothing after it, so + // there is nothing to replay. The boundary start == dl.first+len(dl.data) + // (resume from the last event seen) already yields an empty slice; guard + // the strictly-greater case too, otherwise dl.data[start-dl.first:] + // panics with a slice-bounds-out-of-range on an index past the end (for + // example a client-supplied Last-Event-ID beyond any event ever sent). + if start-dl.first > len(dl.data) { + return nil, nil + } return slices.Clone(dl.data[start-dl.first:]), nil } diff --git a/mcp/event_test.go b/mcp/event_test.go index 91ddb9d7..9a902dab 100644 --- a/mcp/event_test.go +++ b/mcp/event_test.go @@ -299,7 +299,13 @@ func TestMemoryEventStoreAfter(t *testing.T) { {"S1", "1", 0, []string{"d2", "d3"}, ""}, {"S1", "1", 1, []string{"d3"}, ""}, {"S1", "1", 2, nil, ""}, + // An index past the latest stored event (highest here is 2) must yield an + // empty replay, not panic with a slice-bounds-out-of-range. This is + // reachable from a client-supplied Last-Event-ID beyond any event sent. + {"S1", "1", 3, nil, ""}, + {"S1", "1", 100, nil, ""}, {"S1", "2", 0, nil, ""}, + {"S1", "2", 5, nil, ""}, // past the end of stream "2" (highest is 0) {"S1", "3", 0, nil, "unknown stream ID"}, {"S2", "0", 0, nil, "unknown session ID"}, } {