Skip to content

Commit c3b3439

Browse files
sueun-devklauspost
andauthored
zstd: don't clear the registered dictionary when decoding past the window (#1177)
* zstd: don't clear the registered dictionary when decoding past the window executeSequences set hist.dict.content = nil when the frame history grew past the window. hist.dict is the same *dict registered via WithDecoderDicts/WithDecoderDictRaw, so this blanked the dictionary for every later decode on the decoder: after one streaming decode of a dictionary frame larger than the window, subsequent decodes of a valid dictionary frame failed with 'match offset (...) bigger than current history'. The write was dead for the current decode (the streaming path reads its own hist.decoders.dict copy) and only ever corrupted reuse. Drop the mutation and keep the window trim; the shared dictionary is now left intact, matching the sync decode path. Add TestDecoderDictReuseAfterLargeStream (fails before, passes after). * go fix * Move test into existing file and simplify the test somewhat. Verified that it still reproduces. --------- Co-authored-by: Klaus Post <klauspost@gmail.com>
1 parent 9874bc9 commit c3b3439

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

zstd/blockdec.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -674,10 +674,6 @@ func (b *blockDec) executeSequences(hist *history) error {
674674
hbytes := hist.b
675675
if len(hbytes) > hist.windowSize {
676676
hbytes = hbytes[len(hbytes)-hist.windowSize:]
677-
// We do not need history anymore.
678-
if hist.dict != nil {
679-
hist.dict.content = nil
680-
}
681677
}
682678
hist.decoders.windowSize = hist.windowSize
683679
hist.decoders.out = b.dst[:0]

zstd/dict_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,3 +1056,51 @@ func TestSharedDecoderTrainedDictRace(t *testing.T) {
10561056
}
10571057
wg.Wait()
10581058
}
1059+
1060+
// hideLen wraps a reader so the decoder cannot detect a *bytes.Reader and take
1061+
// the small-buffer sync path; this forces the concurrent streaming decode path.
1062+
type hideLen struct{ r io.Reader }
1063+
1064+
func (h hideLen) Read(p []byte) (int, error) { return h.r.Read(p) }
1065+
1066+
// TestDecoderDictReuseAfterLargeStream verifies that streaming-decoding a
1067+
// dictionary-compressed frame whose output exceeds the window does not corrupt
1068+
// the registered dictionary for later decodes on the same decoder.
1069+
func TestDecoderDictReuseAfterLargeStream(t *testing.T) {
1070+
rng := rand.New(rand.NewSource(1))
1071+
dictContent := make([]byte, 8<<10)
1072+
rng.Read(dictContent)
1073+
const dictID = 12345
1074+
const windowSize = 256 << 10
1075+
1076+
// The payload starts with the dictionary content (so the encoder emits a
1077+
// match into the dictionary) and is long enough that decoded history
1078+
// exceeds the window while compressed blocks are still being executed.
1079+
payload := bytes.Repeat(dictContent, 2*windowSize/len(dictContent))
1080+
1081+
enc, err := NewWriter(nil, WithEncoderDictRaw(dictID, dictContent), WithWindowSize(windowSize))
1082+
if err != nil {
1083+
t.Fatal(err)
1084+
}
1085+
frame := enc.EncodeAll(payload, nil)
1086+
enc.Close()
1087+
1088+
dec, err := NewReader(nil, WithDecoderDictRaw(dictID, dictContent))
1089+
if err != nil {
1090+
t.Fatal(err)
1091+
}
1092+
defer dec.Close()
1093+
1094+
for i := range 3 {
1095+
if err := dec.Reset(hideLen{bytes.NewReader(frame)}); err != nil {
1096+
t.Fatalf("reset %d: %v", i, err)
1097+
}
1098+
got, err := io.ReadAll(dec)
1099+
if err != nil {
1100+
t.Fatalf("decode %d of a valid dictionary frame failed: %v", i, err)
1101+
}
1102+
if !bytes.Equal(got, payload) {
1103+
t.Fatalf("decode %d: output mismatch: got %d bytes, want %d", i, len(got), len(payload))
1104+
}
1105+
}
1106+
}

0 commit comments

Comments
 (0)