Capacity of the object is too big
| Vulnerability potential | None |
| DDoS potential | Low |
Overbuffered object
Impact
Allocating a slice, map, or buffer with a capacity far larger than what is
actually used wastes memory for the lifetime of the object. A single
over-allocation is harmless, but the pattern multiplied across many objects, or
inside a hot loop, inflates the working set, increases garbage-collector
pressure, and hurts cache locality. A subtler effect is retention: a small
slice that re-slices a much larger backing array (big[:1]) keeps the entire
backing array alive, so a tiny visible length pins megabytes that the GC cannot
reclaim. The program is correct but uses far more memory than it needs.
Vulnerability potential
There is no memory-safety or information-disclosure angle here — over-allocation
does not corrupt state or cross a trust boundary. The only security-adjacent
concern is denial of service: if the capacity is derived from
attacker-controlled input (a length prefix, a Content-Length, a count field),
an attacker can request a huge make([]T, 0, n) and force the process to
reserve large amounts of memory per request, exhausting RAM. That is a
pre-allocation/untrusted-size problem; bounded, validated sizes remove it.
Technical details
In Go, make([]T, len, cap) reserves cap elements immediately. Capacity that
greatly exceeds the eventual length is pure overhead. Two common sources:
Over-eager pre-sizing
Guessing a large capacity “to be safe” when the typical fill is small. append
grows slices automatically and amortizes reallocation, so over-reserving rarely
pays off unless the final size is known and large.
Backing-array retention via re-slicing
small := big[:n] shares big’s backing array; cap(small) is still the full
length of big, so the whole array stays reachable as long as small lives.
Returning such a sub-slice from a function that read a large buffer is a classic
hidden leak. The fix is to copy the needed part into a right-sized slice
(append([]T(nil), big[:n]...)).
Untrusted sizes
make([]byte, 0, n) with n from the network lets a client dictate allocation
size; always cap n against a sane limit before allocating.
Catching the issue
Profiling
pprof heap profiles (go tool pprof) and runtime.ReadMemStats reveal
objects whose capacity dwarfs their length and backing arrays kept alive longer
than expected. Benchmark with -benchmem to see allocations per operation.
Linters and review
golangci-lint (prealloc for the opposite case, gocritic) and code review
catch obviously oversized make calls and re-slicing that retains large
backings. As a rule, validate any externally supplied size before using it as a
capacity, and copy out small results from large temporary buffers.
How to reproduce
Run with memory stats; the returned 4-byte slice pins the entire 10 MB backing array because re-slicing preserves capacity.
package main
import (
"fmt"
"runtime"
)
func firstFour() []byte {
big := make([]byte, 10<<20) // 10 MB scratch buffer
// ... fill big ...
return big[:4] // BUG: keeps all 10 MB alive via the shared backing array
}
func main() {
s := firstFour()
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("len=%d cap=%d heap=%d KB\n", len(s), cap(s), m.HeapAlloc/1024)
// Fix: out := append([]byte(nil), big[:4]...); return out
}