Open
Description
https://golang.org/cl/84897 introduces a denial-of-service attack on json.Marshal
via a live-lock situation with sync.Pool
.
Consider this snippet:
type CustomMarshaler int
func (c CustomMarshaler) MarshalJSON() ([]byte, error) {
time.Sleep(500 * time.Millisecond) // simulate processing time
b := make([]byte, int(c))
b[0] = '"'
for i := 1; i < len(b)-1; i++ {
b[i] = 'a'
}
b[len(b)-1] = '"'
return b, nil
}
func main() {
processRequest := func(size int) {
json.Marshal(CustomMarshaler(size))
time.Sleep(1 * time.Millisecond) // simulate idle time
}
// Simulate a steady stream of infrequent large requests.
go func() {
for {
processRequest(1 << 28) // 256MiB
}
}()
// Simulate a storm of small requests.
for i := 0; i < 1000; i++ {
go func() {
for {
processRequest(1 << 10) // 1KiB
}
}()
}
// Continually run a GC and track the allocated bytes.
var stats runtime.MemStats
for i := 0; ; i++ {
runtime.ReadMemStats(&stats)
fmt.Printf("Cycle %d: %dB\n", i, stats.Alloc)
time.Sleep(time.Second)
runtime.GC()
}
}
This is a variation of #23199 (comment) of a situation suggested by @bcmills.
Essentially, we have a 1-to-1000 ratio of a routines that either use 1KiB or 256MiB, respectively. The occasional insertion of a 256MiB buffer into the sync.Pool
gets continually held by the 1KiB routines. On my machine, after 300 GC cycles, the above program occupies 6GiB of my heap, when I expect it to be 256MiB in the worst-case.
\cc @jnjackins @bradfitz