From c7cf12703d26d5d7d59a3a0a4e2653829c467384 Mon Sep 17 00:00:00 2001 From: "Benjamin M. Schwartz" Date: Tue, 5 Aug 2025 02:05:52 -0400 Subject: [PATCH] quicvarint: tolerate empty reads of the underlying io.Reader (#5275) * Tolerate empty reads in `quicvarint.Read` Currently, `quicvarint.Read` misinterprets an empty read (`n==0`) as a single zero byte. While empty reads are "discouraged" in the `io.Reader` interface, they do sometimes happen, especially when using `io.Pipe()`. This change tolerates empty reads, adds a corresponding unit test, and also includes some additional test coverage related to empty capsules * minor test refactor --------- Co-authored-by: Marten Seemann --- http3/capsule_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++- quicvarint/io.go | 7 +++++- quicvarint/io_test.go | 16 +++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/http3/capsule_test.go b/http3/capsule_test.go index 344081909..cd4435070 100644 --- a/http3/capsule_test.go +++ b/http3/capsule_test.go @@ -26,8 +26,38 @@ func TestCapsuleParsing(t *testing.T) { data, err := io.ReadAll(r) // reads until EOF require.NoError(t, err) require.Equal(t, []byte("bar"), data) +} - // test EOF vs ErrUnexpectedEOF +func TestEmptyCapsuleParsing(t *testing.T) { + b := quicvarint.Append(nil, 1337) + b = quicvarint.Append(b, 0) + // Capsule content is empty. + + ct, r, err := ParseCapsule(bytes.NewReader(b)) + require.NoError(t, err) + require.Equal(t, CapsuleType(1337), ct) + data, err := io.ReadAll(r) // reads until EOF + require.NoError(t, err) + require.Equal(t, []byte{}, data) +} + +// test EOF vs ErrUnexpectedEOF +func TestCapsuleTruncation(t *testing.T) { + t.Run("with content", func(t *testing.T) { + b := quicvarint.Append(nil, 1337) + b = quicvarint.Append(b, 6) + b = append(b, []byte("foobar")...) + testCapsuleTruncation(t, b) + }) + + t.Run("empty content", func(t *testing.T) { + b := quicvarint.Append(nil, 1337) + b = quicvarint.Append(b, 0) + testCapsuleTruncation(t, b) + }) +} + +func testCapsuleTruncation(t *testing.T, b []byte) { for i := range b { ct, r, err := ParseCapsule(bytes.NewReader(b[:i])) if err != nil { @@ -55,3 +85,23 @@ func TestCapsuleWriting(t *testing.T) { require.NoError(t, err) require.Equal(t, "foobar", string(val)) } + +func TestCapsuleWriteEmpty(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, WriteCapsule(&buf, 1337, []byte{})) + require.NoError(t, WriteCapsule(&buf, 1337, []byte{})) + + ct, r, err := ParseCapsule(&buf) + require.NoError(t, err) + require.Equal(t, CapsuleType(1337), ct) + val, err := io.ReadAll(r) + require.NoError(t, err) + require.Empty(t, val) + + ct, r, err = ParseCapsule(&buf) + require.NoError(t, err) + require.Equal(t, CapsuleType(1337), ct) + val, err = io.ReadAll(r) + require.NoError(t, err) + require.Empty(t, val) +} diff --git a/quicvarint/io.go b/quicvarint/io.go index 27def4092..5c3453645 100644 --- a/quicvarint/io.go +++ b/quicvarint/io.go @@ -31,7 +31,12 @@ func NewReader(r io.Reader) Reader { func (r *byteReader) ReadByte() (byte, error) { var b [1]byte - n, err := r.Read(b[:]) + var n int + var err error + for n == 0 && err == nil { + n, err = r.Read(b[:]) + } + if n == 1 && err == io.EOF { err = nil } diff --git a/quicvarint/io_test.go b/quicvarint/io_test.go index 84a7cf507..04ce01b2e 100644 --- a/quicvarint/io_test.go +++ b/quicvarint/io_test.go @@ -81,6 +81,22 @@ func TestReaderHandlesEOF(t *testing.T) { require.EqualValues(t, 1337, n2) } +// Regression test: empty reads were being converted to successful +// reads of a zero value. +func TestReaderHandlesEmptyRead(t *testing.T) { + r, w := io.Pipe() + + go func() { + // io.Pipe turns empty writes into empty reads. + w.Write(nil) + w.Close() + }() + + br := NewReader(r) + _, err := Read(br) + require.ErrorIs(t, err, io.EOF) +} + func TestWriterPassesThroughUnchanged(t *testing.T) { b := &bytes.Buffer{} w := NewWriter(b)