Your Cart
Loading

allocguard - find Go parsers that allocate before they validate

On Sale
$19.00
$19.00
Added to cart

Some Go parsers size a slice from a separator count taken straight off the wire, before any element has been validated:




out := make([]net.IP, 0, strings.Count(b, ",")+1)


for len(b) > 0 {


    e, b, _ = strings.Cut(b, ",")


    if net.ParseIP(e) == nil { return errors.New("bad ip") }  // rejected on element #1


}




The count is attacker-controlled and is taken before any element is checked, so a value of nothing but separators reserves memory per separator and then throws it away on the first one. allocguard finds these.




It found one I had missed by hand. In buger/jsonparser, path_compiler.go:40 sizes its slice from two strings.Count calls on the path and then rejects a leading "." immediately. Measured on an Apple M3 Pro, go1.22.5:




before  127667 ns/op  3203087 B/op


after    11761 ns/op     9472 B/op




3.2 MB reserved and discarded for input rejected at byte one.




Two checks, and one of them is off by default. (1) Pre-allocation from unvalidated input, as above — this is the check worth paying for, and it is the default. (2) Unbounded recursion over untrusted input, behind -recursion, because its measured precision on real code is poor: a sweep of 11 OSS-Fuzz Go projects produced 55 recursion findings and zero true positives. Proving a recursion unbounded needs the depth to be attacker-controlled with no limit, and syntax alone cannot see that. It ships as a lead generator, not a verdict, and the default output stays clean without it.




It stays quiet. make([]T, 0, n) is almost always fine, so a grep is useless. A site is reported only when the capacity is counted from a value the function did not create and the fill loop can return early — the condition that makes the space wasted rather than used. Across 37 large repositories (hugo, go-git, prometheus, tailscale, caddy, etcd, containerd, fasthttp, gitea, quic-go and more) default mode stays silent on all but a handful.




The suggested fix changes no behaviour. Capacity is only a hint to append, which still grows as needed, so clamping it cannot change which inputs are accepted or what a successful parse returns.




Single binary, no dependencies beyond the Go standard library, exits 1 on findings so it drops into CI unchanged. Ships with full source (MIT) and a 14-test suite.




What it does not do — stated plainly. Syntactic analysis only, no type information: it matches the strings.Count / bytes.Count idiom and will miss capacity derived another way. It cannot see who produces the counted value: scanning go-gitea/gitea it reported a site at high severity that turned out to be safe, because those bytes come from git ls-tree and git escapes newlines in filenames, so malformed input never arrives. It tells you the capacity is untrusted; whether that is exploitable depends on callers and on any upstream size cap. Verify before filing anything upstream.




Requires Go 1.21 or newer. Build with: go build -o allocguard . then run: ./allocguard ./...

You will get a ZIP (24KB) file