1
0
mirror of https://github.com/golang/go synced 2024-09-24 05:20:13 -06:00

compress/flate: fix deflate Reset consistency

Modify the overflow detection logic to shuffle the contents
of the table to a lower offset to avoid leaking the effects
of a previous use of compress.Writer past Reset calls.

Fixes #34121

Change-Id: I9963eadfa5482881e7b7adbad4c2cae146b669ab
GitHub-Last-Rev: 8b35798cdd
GitHub-Pull-Request: golang/go#34128
Reviewed-on: https://go-review.googlesource.com/c/go/+/193605
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
Run-TryBot: Joe Tsai <thebrokentoaster@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
Klaus Post 2020-05-07 12:50:00 +00:00 committed by Russ Cox
parent c5d7f2f1cb
commit 8d4330742c
2 changed files with 97 additions and 15 deletions

View File

@ -4,6 +4,8 @@
package flate
import "math"
// This encoding algorithm, which prioritizes speed over output size, is
// based on Snappy's LZ77-style encoder: github.com/golang/snappy
@ -12,6 +14,13 @@ const (
tableSize = 1 << tableBits // Size of the table.
tableMask = tableSize - 1 // Mask for table indices. Redundant, but can eliminate bounds checks.
tableShift = 32 - tableBits // Right-shift to get the tableBits most significant bits of a uint32.
// Reset the buffer offset when reaching this.
// Offsets are stored between blocks as int32 values.
// Since the offset we are checking against is at the beginning
// of the buffer, we need to subtract the current and input
// buffer to not risk overflowing the int32.
bufferReset = math.MaxInt32 - maxStoreBlockSize*2
)
func load32(b []byte, i int32) uint32 {
@ -59,8 +68,8 @@ func newDeflateFast() *deflateFast {
// to dst and returns the result.
func (e *deflateFast) encode(dst []token, src []byte) []token {
// Ensure that e.cur doesn't wrap.
if e.cur > 1<<30 {
e.resetAll()
if e.cur >= bufferReset {
e.shiftOffsets()
}
// This check isn't in the Snappy implementation, but there, the caller
@ -264,22 +273,32 @@ func (e *deflateFast) reset() {
e.cur += maxMatchOffset
// Protect against e.cur wraparound.
if e.cur > 1<<30 {
e.resetAll()
if e.cur >= bufferReset {
e.shiftOffsets()
}
}
// resetAll resets the deflateFast struct and is only called in rare
// situations to prevent integer overflow. It manually resets each field
// to avoid causing large stack growth.
// shiftOffsets will shift down all match offset.
// This is only called in rare situations to prevent integer overflow.
//
// See https://golang.org/issue/18636.
func (e *deflateFast) resetAll() {
// This is equivalent to:
// *e = deflateFast{cur: maxStoreBlockSize, prev: e.prev[:0]}
e.cur = maxStoreBlockSize
e.prev = e.prev[:0]
for i := range e.table {
e.table[i] = tableEntry{}
// See https://golang.org/issue/18636 and https://github.com/golang/go/issues/34121.
func (e *deflateFast) shiftOffsets() {
if len(e.prev) == 0 {
// We have no history; just clear the table.
for i := range e.table[:] {
e.table[i] = tableEntry{}
}
e.cur = maxMatchOffset
return
}
// Shift down everything in the table that isn't already too far away.
for i := range e.table[:] {
v := e.table[i].offset - e.cur + maxMatchOffset
if v < 0 {
v = 0
}
e.table[i].offset = v
}
e.cur = maxMatchOffset
}

View File

@ -173,3 +173,66 @@ func testDeterministic(i int, t *testing.T) {
t.Errorf("level %d did not produce deterministic result, result mismatch, len(a) = %d, len(b) = %d", i, len(b1b), len(b2b))
}
}
// TestDeflateFast_Reset will test that encoding is consistent
// across a warparound of the table offset.
// See https://github.com/golang/go/issues/34121
func TestDeflateFast_Reset(t *testing.T) {
buf := new(bytes.Buffer)
n := 65536
for i := 0; i < n; i++ {
fmt.Fprintf(buf, "asdfasdfasdfasdf%d%dfghfgujyut%dyutyu\n", i, i, i)
}
// This is specific to level 1.
const level = 1
in := buf.Bytes()
offset := 1
if testing.Short() {
offset = 256
}
// We do an encode with a clean buffer to compare.
var want bytes.Buffer
w, err := NewWriter(&want, level)
if err != nil {
t.Fatalf("NewWriter: level %d: %v", level, err)
}
// Output written 3 times.
w.Write(in)
w.Write(in)
w.Write(in)
w.Close()
for ; offset <= 256; offset *= 2 {
w, err := NewWriter(ioutil.Discard, level)
if err != nil {
t.Fatalf("NewWriter: level %d: %v", level, err)
}
// Reset until we are right before the wraparound.
// Each reset adds maxMatchOffset to the offset.
for i := 0; i < (bufferReset-len(in)-offset-maxMatchOffset)/maxMatchOffset; i++ {
// skip ahead to where we are close to wrap around...
w.d.reset(nil)
}
var got bytes.Buffer
w.Reset(&got)
// Write 3 times, close.
for i := 0; i < 3; i++ {
_, err = w.Write(in)
if err != nil {
t.Fatal(err)
}
}
err = w.Close()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got.Bytes(), want.Bytes()) {
t.Fatalf("output did not match at wraparound, len(want) = %d, len(got) = %d", want.Len(), got.Len())
}
}
}