zlib: reject reset after gzip/deflate emitted incomplete output · nodejs/node@54e9330 · GitHub
Skip to content

Commit 54e9330

Browse files
xia-chaoaduh95
authored andcommitted
zlib: reject reset after gzip/deflate emitted incomplete output
deflateReset starts a new member. Bytes already written out cannot be taken back, so gunzip/inflate see a truncated member followed by a new header. Refuse reset in that case. Raw deflate has no wrapper header and is unchanged. Signed-off-by: Xia Chao <shapirolutts@gmail.com> PR-URL: #66179 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com>
1 parent 480ca7c commit 54e9330

3 files changed

Lines changed: 202 additions & 1 deletion

File tree

‎doc/api/zlib.md‎

Lines changed: 9 additions & 0 deletions

‎src/node_zlib.cc‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,15 @@ class ZlibContext final : public MemoryRetainer {
229229
unsigned int gzip_id_bytes_read_ = 0;
230230
std::vector<unsigned char> dictionary_;
231231

232+
// gzip and zlib-wrapped deflate emit a header on the first deflate() call.
233+
// Resetting after those bytes have left the compressor starts a new member
234+
// while the fragment remains, so gunzip/inflate fail with Z_DATA_ERROR.
235+
// Raw deflate has no header; Z_FULL_FLUSH + reset still concatenates.
236+
// A member is complete once deflate() has been called with Z_FINISH and
237+
// returned Z_STREAM_END.
238+
bool stream_complete_ = true;
239+
bool output_emitted_ = false;
240+
232241
z_stream strm_;
233242
};
234243

@@ -1096,9 +1105,20 @@ void ZlibContext::DoThreadPoolWork() {
10961105
switch (mode_) {
10971106
case DEFLATE:
10981107
case GZIP:
1099-
case DEFLATERAW:
1108+
case DEFLATERAW: {
1109+
const unsigned out_before = strm_.avail_out;
11001110
err_ = deflate(&strm_, flush_);
1111+
if (out_before > strm_.avail_out) {
1112+
output_emitted_ = true;
1113+
}
1114+
if (err_ == Z_STREAM_END) {
1115+
stream_complete_ = true;
1116+
output_emitted_ = false;
1117+
} else if (err_ == Z_OK || err_ == Z_BUF_ERROR) {
1118+
stream_complete_ = false;
1119+
}
11011120
break;
1121+
}
11021122
case UNZIP:
11031123
if (strm_.avail_in > 0) {
11041124
next_expected_header_byte = strm_.next_in;
@@ -1241,6 +1261,20 @@ CompressionError ZlibContext::GetErrorInfo() const {
12411261

12421262

12431263
CompressionError ZlibContext::ResetStream() {
1264+
// deflateReset() is deflateEnd + deflateInit: a new stream. gzip emits a
1265+
// wrapper header on the first write; those bytes cannot be taken back, so
1266+
// refuse reset once an incomplete gzip member has emitted output.
1267+
// zlib-wrapped deflate still allows reset after flush: callers discard the
1268+
// first member (test-zlib-dictionary.js). Raw deflate has no wrapper header,
1269+
// so flush+reset still concatenates.
1270+
if (mode_ == GZIP && !stream_complete_ && output_emitted_) {
1271+
return CompressionError(
1272+
"Cannot reset a gzip stream with an incomplete member; end the "
1273+
"stream or start a new gzip compressor",
1274+
"ERR_ZLIB_INCOMPLETE_FRAME",
1275+
Z_STREAM_ERROR);
1276+
}
1277+
12441278
bool first_init_call = InitZlib();
12451279
if (first_init_call && err_ != Z_OK) {
12461280
return ErrorForMessage("Failed to init stream before reset");
@@ -1266,6 +1300,8 @@ CompressionError ZlibContext::ResetStream() {
12661300
if (err_ != Z_OK)
12671301
return ErrorForMessage("Failed to reset stream");
12681302

1303+
stream_complete_ = true;
1304+
output_emitted_ = false;
12691305
return SetDictionary();
12701306
}
12711307

@@ -1310,6 +1346,8 @@ void ZlibContext::Init(int level,
13101346
flush_ = Z_NO_FLUSH;
13111347

13121348
err_ = Z_OK;
1349+
stream_complete_ = true;
1350+
output_emitted_ = false;
13131351

13141352
if (mode_ == GZIP || mode_ == GUNZIP) {
13151353
window_bits_ += 16;
Lines changed: 154 additions & 0 deletions

0 commit comments

Comments
 (0)