On 4 April 2017, Node.js versions v4.8.2 and v6.10.2 were
released. These versions bumped the vendored zlib library from
v1.2.8 to v1.2.11 in response to what it describes as low-severity
CVEs. In zlib v1.2.9, a change was made that causes an error to be
raised when a raw deflate stream is initialised with windowBits set
to 8.
In zlib v1.2.9, 8 become an invalid value for this parameter, and Node's zlib
module will crash if you call this:
```
zlib.createDeflateRaw({windowBits: 8})
```
On some versions this crashes Node and you cannot recover from it, while on some
versions it throws an exception. The permessage-deflate library up to
version v0.1.5 does make such a call with no try/catch
This commit reverts to the original behavior of zlib by gracefully changed
windowBits: 8 to windowBits: 9 for raw deflate streams.
PR-URL: https://github.com/nodejs-private/node-private/pull/95
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Evan Lucas <evanlucas@me.com>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
229 lines
6.6 KiB
JavaScript
229 lines
6.6 KiB
JavaScript
'use strict';
|
||
var common = require('../common');
|
||
var assert = require('assert');
|
||
var zlib = require('zlib');
|
||
var path = require('path');
|
||
|
||
var zlibPairs =
|
||
[[zlib.Deflate, zlib.Inflate],
|
||
[zlib.Gzip, zlib.Gunzip],
|
||
[zlib.Deflate, zlib.Unzip],
|
||
[zlib.Gzip, zlib.Unzip],
|
||
[zlib.DeflateRaw, zlib.InflateRaw]];
|
||
|
||
// how fast to trickle through the slowstream
|
||
var trickle = [128, 1024, 1024 * 1024];
|
||
|
||
// tunable options for zlib classes.
|
||
|
||
// several different chunk sizes
|
||
var chunkSize = [128, 1024, 1024 * 16, 1024 * 1024];
|
||
|
||
// this is every possible value.
|
||
var level = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||
var windowBits = [8, 9, 10, 11, 12, 13, 14, 15];
|
||
var memLevel = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||
var strategy = [0, 1, 2, 3, 4];
|
||
|
||
// it's nice in theory to test every combination, but it
|
||
// takes WAY too long. Maybe a pummel test could do this?
|
||
if (!process.env.PUMMEL) {
|
||
trickle = [1024];
|
||
chunkSize = [1024 * 16];
|
||
level = [6];
|
||
memLevel = [8];
|
||
windowBits = [15];
|
||
strategy = [0];
|
||
}
|
||
|
||
var fs = require('fs');
|
||
|
||
var testFiles = ['person.jpg', 'elipses.txt', 'empty.txt'];
|
||
|
||
if (process.env.FAST) {
|
||
zlibPairs = [[zlib.Gzip, zlib.Unzip]];
|
||
testFiles = ['person.jpg'];
|
||
}
|
||
|
||
var tests = {};
|
||
testFiles.forEach(function(file) {
|
||
tests[file] = fs.readFileSync(path.resolve(common.fixturesDir, file));
|
||
});
|
||
|
||
var util = require('util');
|
||
var stream = require('stream');
|
||
|
||
|
||
// stream that saves everything
|
||
function BufferStream() {
|
||
this.chunks = [];
|
||
this.length = 0;
|
||
this.writable = true;
|
||
this.readable = true;
|
||
}
|
||
|
||
util.inherits(BufferStream, stream.Stream);
|
||
|
||
BufferStream.prototype.write = function(c) {
|
||
this.chunks.push(c);
|
||
this.length += c.length;
|
||
return true;
|
||
};
|
||
|
||
BufferStream.prototype.end = function(c) {
|
||
if (c) this.write(c);
|
||
// flatten
|
||
var buf = new Buffer(this.length);
|
||
var i = 0;
|
||
this.chunks.forEach(function(c) {
|
||
c.copy(buf, i);
|
||
i += c.length;
|
||
});
|
||
this.emit('data', buf);
|
||
this.emit('end');
|
||
return true;
|
||
};
|
||
|
||
|
||
function SlowStream(trickle) {
|
||
this.trickle = trickle;
|
||
this.offset = 0;
|
||
this.readable = this.writable = true;
|
||
}
|
||
|
||
util.inherits(SlowStream, stream.Stream);
|
||
|
||
SlowStream.prototype.write = function() {
|
||
throw new Error('not implemented, just call ss.end(chunk)');
|
||
};
|
||
|
||
SlowStream.prototype.pause = function() {
|
||
this.paused = true;
|
||
this.emit('pause');
|
||
};
|
||
|
||
SlowStream.prototype.resume = function() {
|
||
const emit = () => {
|
||
if (this.paused) return;
|
||
if (this.offset >= this.length) {
|
||
this.ended = true;
|
||
return this.emit('end');
|
||
}
|
||
var end = Math.min(this.offset + this.trickle, this.length);
|
||
var c = this.chunk.slice(this.offset, end);
|
||
this.offset += c.length;
|
||
this.emit('data', c);
|
||
process.nextTick(emit);
|
||
};
|
||
|
||
if (this.ended) return;
|
||
this.emit('resume');
|
||
if (!this.chunk) return;
|
||
this.paused = false;
|
||
emit();
|
||
};
|
||
|
||
SlowStream.prototype.end = function(chunk) {
|
||
// walk over the chunk in blocks.
|
||
this.chunk = chunk;
|
||
this.length = chunk.length;
|
||
this.resume();
|
||
return this.ended;
|
||
};
|
||
|
||
// windowBits: 8 shouldn't throw
|
||
assert.doesNotThrow(() => {
|
||
zlib.createDeflateRaw({ windowBits: 8 });
|
||
}, 'windowsBits set to 8 should follow legacy zlib behavior');
|
||
|
||
{
|
||
const node = fs.createReadStream(process.execPath);
|
||
const raw = [];
|
||
const reinflated = [];
|
||
node.on('data', (chunk) => raw.push(chunk));
|
||
|
||
// Usually, the inflate windowBits parameter needs to be at least the
|
||
// value of the matching deflate’s windowBits. However, inflate raw with
|
||
// windowBits = 8 should be able to handle compressed data from a source
|
||
// that does not know about the silent 8-to-9 upgrade of windowBits
|
||
// that older versions of zlib/Node perform.
|
||
node.pipe(zlib.createDeflateRaw({ windowBits: 9 }))
|
||
.pipe(zlib.createInflateRaw({ windowBits: 8 }))
|
||
.on('data', (chunk) => reinflated.push(chunk))
|
||
.on('end', common.mustCall(
|
||
() => assert(Buffer.concat(raw).equals(Buffer.concat(reinflated)))));
|
||
}
|
||
|
||
// for each of the files, make sure that compressing and
|
||
// decompressing results in the same data, for every combination
|
||
// of the options set above.
|
||
var failures = 0;
|
||
var total = 0;
|
||
var done = 0;
|
||
|
||
Object.keys(tests).forEach(function(file) {
|
||
var test = tests[file];
|
||
chunkSize.forEach(function(chunkSize) {
|
||
trickle.forEach(function(trickle) {
|
||
windowBits.forEach(function(windowBits) {
|
||
level.forEach(function(level) {
|
||
memLevel.forEach(function(memLevel) {
|
||
strategy.forEach(function(strategy) {
|
||
zlibPairs.forEach(function(pair) {
|
||
var Def = pair[0];
|
||
var Inf = pair[1];
|
||
var opts = { level: level,
|
||
windowBits: windowBits,
|
||
memLevel: memLevel,
|
||
strategy: strategy };
|
||
|
||
total++;
|
||
|
||
var def = new Def(opts);
|
||
var inf = new Inf(opts);
|
||
var ss = new SlowStream(trickle);
|
||
var buf = new BufferStream();
|
||
|
||
// verify that the same exact buffer comes out the other end.
|
||
buf.on('data', function(c) {
|
||
var msg = file + ' ' +
|
||
chunkSize + ' ' +
|
||
JSON.stringify(opts) + ' ' +
|
||
Def.name + ' -> ' + Inf.name;
|
||
var ok = true;
|
||
var testNum = ++done;
|
||
for (var i = 0; i < Math.max(c.length, test.length); i++) {
|
||
if (c[i] !== test[i]) {
|
||
ok = false;
|
||
failures++;
|
||
break;
|
||
}
|
||
}
|
||
if (ok) {
|
||
console.log('ok ' + (testNum) + ' ' + msg);
|
||
} else {
|
||
console.log('not ok ' + (testNum) + ' ' + msg);
|
||
console.log(' ...');
|
||
console.log(' testfile: ' + file);
|
||
console.log(' type: ' + Def.name + ' -> ' + Inf.name);
|
||
console.log(' position: ' + i);
|
||
console.log(' options: ' + JSON.stringify(opts));
|
||
console.log(' expect: ' + test[i]);
|
||
console.log(' actual: ' + c[i]);
|
||
console.log(' chunkSize: ' + chunkSize);
|
||
console.log(' ---');
|
||
}
|
||
});
|
||
|
||
// the magic happens here.
|
||
ss.pipe(def).pipe(inf).pipe(buf);
|
||
ss.end(test);
|
||
});
|
||
}); }); }); }); }); }); // sad stallman is sad.
|
||
});
|
||
|
||
process.on('exit', function(code) {
|
||
console.log('1..' + done);
|
||
assert.equal(done, total, (total - done) + ' tests left unfinished');
|
||
assert.ok(!failures, 'some test failures');
|
||
});
|