boundedread

package module
v0.1.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: MIT Imports: 11 Imported by: 0

README

Documentation

Overview

Package boundedread provides a go/analysis analyzer for the resiliency rule that a read of a stream whose size this code does not control must carry an explicit bound: io.ReadAll over a caller-supplied reader or an HTTP body allocates whatever the peer sends, which is a denial of service the program asked for. The conforming forms are io.ReadAll(io.LimitReader(r, max)), io.CopyN, and http.MaxBytesReader.

The sinks

Four shapes pull an entire stream into memory and are reported: io.ReadAll (and the ioutil alias), io.Copy into a *bytes.Buffer or *strings.Builder — a ReadAll spelled differently — io.CopyBuffer into the same, where the caller-supplied buffer changes how the bytes travel and not how many arrive, and (*bytes.Buffer).ReadFrom, which is that same copy written as a method. Copying to a file, a socket, or a ResponseWriter streams and allocates nothing unbounded, so it is not a sink.

The sources, and why the set is small

A finding needs a source whose size is provably outside this code's reach. Exactly two shapes qualify, and -sources selects which of them are claimed:

  • An HTTP message BODY — the Body field of an http.Request or an http.Response. The peer chooses that length, never this program, and no caller can bound it on this code's behalf. This class needs no judgment and is the DEFAULT (-sources=http).
  • A PARAMETER whose static type is a stream interface declared in io or net — io.Reader, io.ReadCloser, net.Conn, and so on. This is the same "untrusted input" that yze/fuzzreq classifies, seen from the reading side: the value arrives from a caller, its dynamic type is invisible here, and no interface can advertise a length. Such a read is unbounded by the letter, but whether the BOUND belongs here or at the caller is a design question a reviewer answers, so the class is opt-in (-sources=all). See sources.go.

Everything else is deliberately SILENT, because the alternative is a rule that fires on ordinary correct code:

  • A LOCAL variable is silent, whatever its type. `lr := io.LimitReader(r, max); io.ReadAll(lr)` is the conforming form and holds an io.Reader; a rule that judged the interface alone would report the very fix it asks for. Judging a local means tracking what was assigned to it, which is dataflow this analyzer does not do.

  • A CONCRETE type is silent — *bytes.Reader, *strings.Reader, an embed.FS entry, *io.LimitedReader, and *os.File alike. The first four are bounded by construction; a file the program itself named is the judgment call the rule declines to make, since reading a config file the operator chose is not a denial of service. The known cost of that line is a concrete socket — *net.TCPConn and its siblings are unbounded and go unreported, because a socket is nearly always held as net.Conn.

  • A REBOUND source is silent WHERE THE REBINDING GOVERNS IT: an assignment that replaces the very value being read, written in the function that reads it (`r = io.LimitReader(r, max)`, `req.Body = http.MaxBytesReader(w, req.Body, max)`), means what is read is no longer what arrived, so nothing is claimed about it. BOTH halves of that are load-bearing, and each was a hole on its own. net/http declares ONE Body field object for the whole program, so an exemption keyed on the field speaks for every request in the package — a single inert `req.Body = req.Body` disables the rule for all of them. And two handler closures registered beside one another share an enclosing function, so an exemption keyed on the function speaks for a request it never saw — `mux.HandleFunc("/small", func(w, r){ r.Body = cap(r.Body); read(r.Body) })` would silence the unbounded `/upload` handler registered on the line below it. The evidence is about a VALUE, and it reaches exactly as far as the value can be followed without dataflow: the same carrier, in the same function.

    The KNOWN COST is a bound applied in one function and the read in another — a ServeHTTP that caps and a helper that reads, and the http.MaxBytesReader middleware idiom. Both are reported, because proving the capped request is the one that reached the reader is interprocedural. A helper can be handed the bounded body instead of the request; the middleware form cannot, since http.Handler's signature admits nothing but the request, and for it the only answer is a bound at the read. The alternative is not a wider scope but a different kind of analysis: the middleware hands its request to next.ServeHTTP, and following it there is interprocedural dataflow this rule does not do. Widening the scope instead — to the enclosing function, or to the package — buys the middleware nothing it could not get from dataflow and costs every unbounded handler standing beside a capped one, which is what Routes in the b fixture shows.

  • TEST files are out of scope: a test reads the fixture it wrote itself.

bufio.Scanner is deliberately absent

A scanner is commonly named as an unbounded read, and the premise does not hold: bufio.Scanner caps a token at bufio.MaxScanTokenSize (64 KiB) and returns bufio.ErrTooLong past it, so the DEFAULT scanner is already bounded and reporting it would be a false positive. Only an explicit Buffer(buf, huge) removes the cap — and deciding which cap is "huge", and tying a Buffer call back to the source its scanner was built from, is dataflow and judgment rather than a provable fact. The rule stays silent rather than guess.

Index

Constants

This section is empty.

Variables

View Source
var Analyzer = newAnalyzer()

Analyzer reports unbounded reads of streams whose size is beyond the code's sight.

View Source
var Registration = goyze.Registration{
	Precision:  goyze.PrecisionExact,
	Name:       "boundedread",
	Categories: []goyze.Category{"patterns"},
	URL:        "https://docs.gomatic.dev/yze/boundedread",
	Analyzer:   Analyzer,
}

Registration declares this analyzer to the yze framework.

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL