fix(ringbuffer): fix off-by-one in Discard() causing index out of range panic

RingBuffer.Discard() used `end <= cap` to decide whether the discard
range is contiguous. When head + n equals exactly len(elements), this
incorrectly set head to len(elements) instead of wrapping it to 0.

A subsequent Peek() or Push() would then access elements[len(elements)],
triggering a runtime panic: "index out of range [64] with length 64".

The crash path in production:
  KCP.Input -> parse_una -> snd_buf.Discard(count) -> shrink_buf ->
  snd_buf.Peek() -> panic

Fix: change the boundary condition from `end <= cap` to `end < cap`,
so the exact-boundary case falls through to the wrap-around branch
which correctly computes head = end - cap = 0.

Add TestRingBufferDiscardBoundary to cover this edge case.
This commit is contained in:
xtaci
2026-02-10 14:29:29 +08:00
parent 68987c794b
commit 6d3de74039
2 changed files with 33 additions and 1 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ func (r *RingBuffer[T]) Discard(n int) int {
}
cap := len(r.elements)
end := r.head + n
if end <= cap {
if end < cap {
// no wrap: clear contiguous range
clear(r.elements[r.head:end])
r.head = end
+32
View File
@@ -236,6 +236,38 @@ func TestRingForEach(t *testing.T) {
}
}
// TestRingBufferDiscardBoundary tests discarding elements that land exactly
// at the end of the backing array. This previously caused head to equal
// len(elements), leading to an index-out-of-range panic in Peek().
func TestRingBufferDiscardBoundary(t *testing.T) {
// Create a ring buffer with backing array length 64 (MaxLen = 63).
r := NewRingBuffer[int](64)
// Fill to capacity (63 elements), head=0, tail=63.
for i := range 63 {
r.Push(i)
}
// Pop 1 element so head=1, push 1 so tail wraps to 0. Still 63 elements.
r.Pop()
r.Push(63)
// Now head=1, tail=0, len=63.
// Discard exactly 63 elements. head + 63 = 64 = len(elements).
// This must wrap head to 0, not leave it at 64.
r.Discard(63)
if r.Len() != 0 {
t.Fatalf("expected empty ring, got Len()=%d", r.Len())
}
// Push a new element and Peek — this panicked before the fix.
r.Push(99)
if v, ok := r.Peek(); !ok || *v != 99 {
t.Fatalf("expected Peek()=99, got %v (ok=%v)", v, ok)
}
}
func TestRingForEachReverse(t *testing.T) {
r := NewRingBuffer[int](10)
for i := range 10 {