-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Open
Labels
NeedsFixThe path to resolution is known, but the work has not been done.The path to resolution is known, but the work has not been done.Performance
Milestone
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
Metadata
Metadata
Assignees
Labels
NeedsFixThe path to resolution is known, but the work has not been done.The path to resolution is known, but the work has not been done.Performance