valyala/fasthttp

BUG: content-length smaller than prefetched body size will lead requestStream Read() PANIC

DrakeOu opened this issue · 1 comments

Steps to Reproduce:

  1. Enable StreamRequestBody to use StreamBody() for reading the body stream data.
  2. Set MaxRequestBodySize to the lowest possible value (1).
  3. Send a PUT HTTP request with a Content-Length smaller than the actual body size being transported, but ensure the Content-Length is smaller than 1024.
  4. Implement a reader to read out the stream data until io.EOF. This will cause a panic.

Demo Code:

package main

import (
	"fmt"
	"github.com/valyala/fasthttp"
)

func main() {
	// Server request handler
	requestHandler := func(ctx *fasthttp.RequestCtx) {
		stream := ctx.RequestBodyStream()

		buf := make([]byte, 10)
		for {
			n, err := stream.Read(buf)
			if err != nil {
				break
			}
			fmt.Printf("Read %d bytes: %s\n", n, buf[:n])
		}
	}

	// Start the server
	server := fasthttp.Server{
		Handler:            requestHandler,
		MaxRequestBodySize: 1,
		StreamRequestBody:  true,
	}
	if err := server.ListenAndServe("localhost:20020"); err != nil {
		fmt.Printf("Error in ListenAndServe: %s", err)
	}
}

Related Code in requestStream Class:

func (rs *requestStream) Read(p []byte) (int, error) {
	var (
		n   int
		err error
	)
	if rs.header.ContentLength() == -1 {
		// ... chunk logic
	}
	if rs.totalBytesRead == rs.header.ContentLength() {
		return 0, io.EOF
	}
	prefetchedSize := int(rs.prefetchedBytes.Size())
	if prefetchedSize > rs.totalBytesRead {
		left := prefetchedSize - rs.totalBytesRead
		if len(p) > left {
			p = p[:left]
		}
		n, err := rs.prefetchedBytes.Read(p)
		rs.totalBytesRead += n
		if n == rs.header.ContentLength() {
			return n, io.EOF
		}
		return n, err
	}
	left := rs.header.ContentLength() - rs.totalBytesRead
	if len(p) > left {
		p = p[:left]
	}
	n, err = rs.reader.Read(p)
	rs.totalBytesRead += n
	if err != nil {
		return n, err
	}

	if rs.totalBytesRead == rs.header.ContentLength() {
		err = io.EOF
	}
	return n, err
}

Issue Description:

Given an HTTP request with Content-Length set to 10 and a body size greater than 10 (e.g., 12), the problem arises with the prefetched buffer. The prefetched buffer is a fixed size of 1024 bytes and attempts to fill the buffer, ignoring the real Content-Length value, resulting in a prefetched buffer size of 12.

SO The first read returns n as 12 and err as nil, causing an inconsistency between the size and Content-Length. The second read results in a left value of -2, leading to a slice operation panic:

if len(p) > left {
	p = p[:left]
}

The core problem is the prefetched buffer logic and subsequent logic not properly handling this inconsistency, resulting in a negative size for the second read. Maybe some additional logic to properly handling should return io.EOF when the body size exceeds Content-Length.

The magic number 1024 is defined here (fasthttp/http.go:2300):

func readBodyIdentity(r *bufio.Reader, maxBodySize int, dst []byte) ([]byte, error) {
	dst = dst[:cap(dst)]
	if len(dst) == 0 {
		dst = make([]byte, 1024)
	}
...
}

This issue might be related to #1469.

Another way to reproduce is to call requestCtx.Body() when StreamBody enabled and Content-Length smaller than bodySize, reason is the same.