Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/zlib.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) {
this._defaultFlushFlag = flush;
this._finishFlushFlag = finishFlush;
this._defaultFullFlushFlag = fullFlush;
this._flushBoundIdx = flushBoundIdx;
this._info = opts?.info;
this._maxOutputLength = maxOutputLength;

Expand Down Expand Up @@ -349,6 +350,18 @@ ZlibBase.prototype.flush = function(kind, callback) {
kind = this._defaultFullFlushFlag;
}

// Reject kinds outside the brotli operation range. Otherwise the fake-chunk
// path forwards an unsupported flush flag to the native encoder/decoder,
// which spins at 100% CPU without making progress (see #63701).
if (this._flushBoundIdx === FLUSH_BOUND_IDX_BROTLI) {
const min = FLUSH_BOUND[FLUSH_BOUND_IDX_BROTLI][0];
const max = FLUSH_BOUND[FLUSH_BOUND_IDX_BROTLI][1];
if (typeof kind !== 'number' || NumberIsNaN(kind) ||
kind < min || kind > max) {
throw new ERR_OUT_OF_RANGE('kind', `>= ${min} and <= ${max}`, kind);
}
}
Comment thread
Ic3b3rg marked this conversation as resolved.
Outdated

if (this.writableFinished) {
if (callback)
process.nextTick(callback);
Expand Down
69 changes: 69 additions & 0 deletions test/parallel/test-zlib-brotli-flush-invalid-kind.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict';
// Regression test for https://github.com/nodejs/node/issues/63701
// BrotliCompress/BrotliDecompress.flush(kind) used to spin at 100% CPU when
// kind was outside the brotli operation range (0..3) — e.g. Z_FINISH (4) or
// Z_BLOCK (5). It must now throw ERR_OUT_OF_RANGE before reaching the native
// layer.

require('../common');
const assert = require('assert');
const zlib = require('zlib');

const {
BROTLI_OPERATION_PROCESS,
BROTLI_OPERATION_FLUSH,
BROTLI_OPERATION_FINISH,
BROTLI_OPERATION_EMIT_METADATA,
Z_BLOCK,
Z_FINISH,
} = zlib.constants;

// Brotli operations 0..3 are valid and must not throw synchronously from
// flush(). Some operations (e.g. FINISH on a decoder with no input) emit a
// Z_BUF_ERROR asynchronously — that's expected and unrelated to the input
// validation under test, so we attach a noop error handler.
for (const validKind of [
BROTLI_OPERATION_PROCESS,
BROTLI_OPERATION_FLUSH,
BROTLI_OPERATION_FINISH,
BROTLI_OPERATION_EMIT_METADATA,
]) {
const c = zlib.createBrotliCompress();
c.on('error', () => {});
c.flush(validKind);

const d = zlib.createBrotliDecompress();
d.on('error', () => {});
d.flush(validKind);
}

// Values outside [0, 3] must throw ERR_OUT_OF_RANGE for both compress and
// decompress streams. Z_FINISH (4) and Z_BLOCK (5) previously hung at 100% CPU.
const outOfRange = [-1, Z_FINISH, Z_BLOCK, 6, 7, 100];

for (const factory of [zlib.createBrotliCompress, zlib.createBrotliDecompress]) {
for (const kind of outOfRange) {
assert.throws(
() => factory().flush(kind),
{ code: 'ERR_OUT_OF_RANGE', name: 'RangeError' },
);
}
}

// Non-number kinds must also throw, instead of silently triggering the
// fake-chunk path with an undefined buffer.
for (const factory of [zlib.createBrotliCompress, zlib.createBrotliDecompress]) {
for (const kind of ['foobar', null, {}, NaN]) {
assert.throws(
() => factory().flush(kind),
{ code: 'ERR_OUT_OF_RANGE' },
);
}
}

// flush() with no arguments (or with only a callback) must still work —
// it uses the stream's _defaultFullFlushFlag, which is a valid brotli op.
zlib.createBrotliCompress().flush();
zlib.createBrotliCompress().flush(() => {});
zlib.createBrotliDecompress().flush();
zlib.createBrotliDecompress().flush(() => {});
Loading