Skip to content

Commit 98be430

Browse files
Ic3b3rgrichardlau
authored andcommitted
zlib: validate flush kind for brotli streams
BrotliCompress/Decompress.flush(kind) forwarded any value to the native layer, causing a 100% CPU hang for kinds outside the brotli operation range (e.g. Z_FINISH). Validate against [0, 3] and throw ERR_OUT_OF_RANGE on invalid input. Fixes: #63701 Signed-off-by: Ic3b3rg <s.ceccarini94@gmail.com> PR-URL: #63746 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent d5f01bb commit 98be430

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

lib/zlib.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) {
276276
this._defaultFlushFlag = flush;
277277
this._finishFlushFlag = finishFlush;
278278
this._defaultFullFlushFlag = fullFlush;
279+
this._flushBoundIdx = flushBoundIdx;
279280
this._info = opts?.info;
280281
this._maxOutputLength = maxOutputLength;
281282

@@ -357,6 +358,18 @@ ZlibBase.prototype.flush = function(kind, callback) {
357358
kind = this._defaultFullFlushFlag;
358359
}
359360

361+
// Reject kinds outside the brotli operation range. Otherwise the fake-chunk
362+
// path forwards an unsupported flush flag to the native encoder/decoder,
363+
// which spins at 100% CPU without making progress (see #63701).
364+
if (this._flushBoundIdx === FLUSH_BOUND_IDX_BROTLI) {
365+
const min = FLUSH_BOUND[FLUSH_BOUND_IDX_BROTLI][0];
366+
const max = FLUSH_BOUND[FLUSH_BOUND_IDX_BROTLI][1];
367+
if (typeof kind !== 'number' || NumberIsNaN(kind) ||
368+
kind < min || kind > max) {
369+
throw new ERR_OUT_OF_RANGE('kind', `>= ${min} and <= ${max}`, kind);
370+
}
371+
}
372+
360373
if (this.writableFinished) {
361374
if (callback)
362375
process.nextTick(callback);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use strict';
2+
// Regression test for https://github.com/nodejs/node/issues/63701
3+
// BrotliCompress/BrotliDecompress.flush(kind) used to spin at 100% CPU when
4+
// kind was outside the brotli operation range (0..3) — e.g. Z_FINISH (4) or
5+
// Z_BLOCK (5). It must now throw ERR_OUT_OF_RANGE before reaching the native
6+
// layer.
7+
8+
require('../common');
9+
const assert = require('assert');
10+
const zlib = require('zlib');
11+
12+
const {
13+
BROTLI_OPERATION_PROCESS,
14+
BROTLI_OPERATION_FLUSH,
15+
BROTLI_OPERATION_FINISH,
16+
BROTLI_OPERATION_EMIT_METADATA,
17+
Z_BLOCK,
18+
Z_FINISH,
19+
} = zlib.constants;
20+
21+
// Brotli operations 0..3 are valid and must not throw synchronously from
22+
// flush(). Some operations (e.g. FINISH on a decoder with no input) emit a
23+
// Z_BUF_ERROR asynchronously — that's expected and unrelated to the input
24+
// validation under test, so we attach a noop error handler.
25+
for (const validKind of [
26+
BROTLI_OPERATION_PROCESS,
27+
BROTLI_OPERATION_FLUSH,
28+
BROTLI_OPERATION_FINISH,
29+
BROTLI_OPERATION_EMIT_METADATA,
30+
]) {
31+
const c = zlib.createBrotliCompress();
32+
c.on('error', () => {});
33+
c.flush(validKind);
34+
35+
const d = zlib.createBrotliDecompress();
36+
d.on('error', () => {});
37+
d.flush(validKind);
38+
}
39+
40+
// Values outside [0, 3] must throw ERR_OUT_OF_RANGE for both compress and
41+
// decompress streams. Z_FINISH (4) and Z_BLOCK (5) previously hung at 100% CPU.
42+
const outOfRange = [-1, Z_FINISH, Z_BLOCK, 6, 7, 100];
43+
44+
for (const factory of [zlib.createBrotliCompress, zlib.createBrotliDecompress]) {
45+
for (const kind of outOfRange) {
46+
assert.throws(
47+
() => factory().flush(kind),
48+
{ code: 'ERR_OUT_OF_RANGE', name: 'RangeError' },
49+
);
50+
}
51+
}
52+
53+
// Non-number kinds must also throw, instead of silently triggering the
54+
// fake-chunk path with an undefined buffer.
55+
for (const factory of [zlib.createBrotliCompress, zlib.createBrotliDecompress]) {
56+
for (const kind of ['foobar', null, {}, NaN]) {
57+
assert.throws(
58+
() => factory().flush(kind),
59+
{ code: 'ERR_OUT_OF_RANGE' },
60+
);
61+
}
62+
}
63+
64+
// flush() with no arguments (or with only a callback) must still work —
65+
// it uses the stream's _defaultFullFlushFlag, which is a valid brotli op.
66+
zlib.createBrotliCompress().flush();
67+
zlib.createBrotliCompress().flush(() => {});
68+
zlib.createBrotliDecompress().flush();
69+
zlib.createBrotliDecompress().flush(() => {});

0 commit comments

Comments
 (0)