child_process: use non-infinite maxBuffer defaults

Set the default maxBuffer size to 204,800 bytes for execSync,
execFileSync, and spawnSync.

APIs that return the child output as a string should have non-infinite
defaults for maxBuffer sizes to avoid out-of-memory error conditions. A
non-infinite default used to be the documented behaviour for all
relevant APIs, but the implemented behaviour for execSync, execFileSync
and spawnSync was to have no maxBuffer limits.

PR-URL: https://github.com/nodejs/node/pull/23027
Refs: https://github.com/nodejs/node/pull/22894
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
kohta ito 2018-09-23 03:11:21 +09:00 committed by Sam Roberts
parent 1656cd2edb
commit eb8a51a35c
9 changed files with 138 additions and 16 deletions

View File

@ -721,7 +721,7 @@ changes:
process will be killed. **Default:** `'SIGTERM'`. process will be killed. **Default:** `'SIGTERM'`.
* `maxBuffer` {number} Largest amount of data in bytes allowed on stdout or * `maxBuffer` {number} Largest amount of data in bytes allowed on stdout or
stderr. If exceeded, the child process is terminated. See caveat at stderr. If exceeded, the child process is terminated. See caveat at
[`maxBuffer` and Unicode][]. **Default:** `Infinity`. [`maxBuffer` and Unicode][]. **Default:** `200 * 1024`.
* `encoding` {string} The encoding used for all stdio inputs and outputs. * `encoding` {string} The encoding used for all stdio inputs and outputs.
**Default:** `'buffer'`. **Default:** `'buffer'`.
* `windowsHide` {boolean} Hide the subprocess console window that would * `windowsHide` {boolean} Hide the subprocess console window that would
@ -788,7 +788,7 @@ changes:
* `maxBuffer` {number} Largest amount of data in bytes allowed on stdout or * `maxBuffer` {number} Largest amount of data in bytes allowed on stdout or
stderr. If exceeded, the child process is terminated and any output is stderr. If exceeded, the child process is terminated and any output is
truncated. See caveat at [`maxBuffer` and Unicode][]. truncated. See caveat at [`maxBuffer` and Unicode][].
**Default:** `Infinity`. **Default:** `200 * 1024`.
* `encoding` {string} The encoding used for all stdio inputs and outputs. * `encoding` {string} The encoding used for all stdio inputs and outputs.
**Default:** `'buffer'`. **Default:** `'buffer'`.
* `windowsHide` {boolean} Hide the subprocess console window that would * `windowsHide` {boolean} Hide the subprocess console window that would
@ -852,7 +852,7 @@ changes:
* `maxBuffer` {number} Largest amount of data in bytes allowed on stdout or * `maxBuffer` {number} Largest amount of data in bytes allowed on stdout or
stderr. If exceeded, the child process is terminated and any output is stderr. If exceeded, the child process is terminated and any output is
truncated. See caveat at [`maxBuffer` and Unicode][]. truncated. See caveat at [`maxBuffer` and Unicode][].
**Default:** `Infinity`. **Default:** `200 * 1024`.
* `encoding` {string} The encoding used for all stdio inputs and outputs. * `encoding` {string} The encoding used for all stdio inputs and outputs.
**Default:** `'buffer'`. **Default:** `'buffer'`.
* `shell` {boolean|string} If `true`, runs `command` inside of a shell. Uses * `shell` {boolean|string} If `true`, runs `command` inside of a shell. Uses

View File

@ -46,6 +46,8 @@ const {
ChildProcess ChildProcess
} = child_process; } = child_process;
const MAX_BUFFER = 200 * 1024;
exports.ChildProcess = ChildProcess; exports.ChildProcess = ChildProcess;
function stdioStringToArray(option) { function stdioStringToArray(option) {
@ -206,7 +208,7 @@ exports.execFile = function execFile(file /* , args, options, callback */) {
options = { options = {
encoding: 'utf8', encoding: 'utf8',
timeout: 0, timeout: 0,
maxBuffer: 200 * 1024, maxBuffer: MAX_BUFFER,
killSignal: 'SIGTERM', killSignal: 'SIGTERM',
cwd: null, cwd: null,
env: null, env: null,
@ -560,7 +562,11 @@ var spawn = exports.spawn = function spawn(file, args, options) {
function spawnSync(file, args, options) { function spawnSync(file, args, options) {
const opts = normalizeSpawnArguments(file, args, options); const opts = normalizeSpawnArguments(file, args, options);
options = opts.options; const defaults = {
maxBuffer: MAX_BUFFER,
...opts.options
};
options = opts.options = Object.assign(defaults, opts.options);
debug('spawnSync', opts.args, options); debug('spawnSync', opts.args, options);

View File

@ -16,7 +16,8 @@ const { spawnSync } = require('child_process');
const ret = spawnSync( const ret = spawnSync(
process.execPath, process.execPath,
['--stack_size=150', __filename, 'async'] ['--stack_size=150', __filename, 'async'],
{ maxBuffer: Infinity }
); );
assert.strictEqual(ret.status, 0, assert.strictEqual(ret.status, 0,
`EXIT CODE: ${ret.status}, STDERR:\n${ret.stderr}`); `EXIT CODE: ${ret.status}, STDERR:\n${ret.stderr}`);

View File

@ -56,6 +56,30 @@ function runChecks(err, stdio, streamName, expected) {
); );
} }
// default value
{
const cmd = `"${process.execPath}" -e "console.log('a'.repeat(200 * 1024))"`;
cp.exec(
cmd,
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stdout', 'a'.repeat(200 * 1024));
})
);
}
// default value
{
const cmd =
`"${process.execPath}" -e "console.log('a'.repeat(200 * 1024 - 1))"`;
cp.exec(cmd, common.mustCall((err, stdout, stderr) => {
assert.ifError(err);
assert.strictEqual(stdout.trim(), 'a'.repeat(200 * 1024 - 1));
assert.strictEqual(stderr, '');
}));
}
const unicode = '中文测试'; // length = 4, byte length = 12 const unicode = '中文测试'; // length = 4, byte length = 12
{ {

View File

@ -0,0 +1,50 @@
'use strict';
require('../common');
// This test checks that the maxBuffer option for child_process.spawnFileSync()
// works as expected.
const assert = require('assert');
const { execFileSync } = require('child_process');
const msgOut = 'this is stdout';
const msgOutBuf = Buffer.from(`${msgOut}\n`);
const args = [
'-e',
`console.log("${msgOut}");`
];
// Verify that an error is returned if maxBuffer is surpassed.
{
assert.throws(() => {
execFileSync(process.execPath, args, { maxBuffer: 1 });
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.errno, 'ENOBUFS');
// We can have buffers larger than maxBuffer because underneath we alloc 64k
// that matches our read sizes.
assert.deepStrictEqual(e.stdout, msgOutBuf);
return true;
});
}
// Verify that a maxBuffer size of Infinity works.
{
const ret = execFileSync(process.execPath, args, { maxBuffer: Infinity });
assert.deepStrictEqual(ret, msgOutBuf);
}
// Default maxBuffer size is 200 * 1024.
{
assert.throws(() => {
execFileSync(
process.execPath,
['-e', "console.log('a'.repeat(200 * 1024))"]
);
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.errno, 'ENOBUFS');
return true;
});
}

View File

@ -34,13 +34,19 @@ const args = [
assert.deepStrictEqual(ret, msgOutBuf); assert.deepStrictEqual(ret, msgOutBuf);
} }
// maxBuffer size is Infinity at default. // maxBuffer size is 200 * 1024 at default.
{ {
const ret = execFileSync( assert.throws(
process.execPath, () => {
['-e', "console.log('a'.repeat(200 * 1024))"], execFileSync(
{ encoding: 'utf-8' } process.execPath,
['-e', "console.log('a'.repeat(200 * 1024))"],
{ encoding: 'utf-8' }
);
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.errno, 'ENOBUFS');
return true;
}
); );
assert.ifError(ret.error);
} }

View File

@ -21,6 +21,8 @@ const args = [
}, (e) => { }, (e) => {
assert.ok(e, 'maxBuffer should error'); assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.errno, 'ENOBUFS'); assert.strictEqual(e.errno, 'ENOBUFS');
// We can have buffers larger than maxBuffer because underneath we alloc 64k
// that matches our read sizes.
assert.deepStrictEqual(e.stdout, msgOutBuf); assert.deepStrictEqual(e.stdout, msgOutBuf);
return true; return true;
}); });
@ -35,3 +37,23 @@ const args = [
assert.deepStrictEqual(ret, msgOutBuf); assert.deepStrictEqual(ret, msgOutBuf);
} }
// Default maxBuffer size is 200 * 1024.
{
assert.throws(() => {
execSync(`"${process.execPath}" -e "console.log('a'.repeat(200 * 1024))"`);
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.errno, 'ENOBUFS');
return true;
});
}
// Default maxBuffer size is 200 * 1024.
{
const ret = execSync(
`"${process.execPath}" -e "console.log('a'.repeat(200 * 1024 - 1))"`
);
assert.deepStrictEqual(ret.toString().trim(), 'a'.repeat(200 * 1024 - 1));
}

View File

@ -33,10 +33,23 @@ const args = [
assert.deepStrictEqual(ret.stdout, msgOutBuf); assert.deepStrictEqual(ret.stdout, msgOutBuf);
} }
// maxBuffer size is Infinity at default. // Default maxBuffer size is 200 * 1024.
{ {
const args = ['-e', "console.log('a'.repeat(200 * 1024))"]; const args = ['-e', "console.log('a'.repeat(200 * 1024))"];
const ret = spawnSync(process.execPath, args, { encoding: 'utf-8' }); const ret = spawnSync(process.execPath, args);
assert.ok(ret.error, 'maxBuffer should error');
assert.strictEqual(ret.error.errno, 'ENOBUFS');
}
// Default maxBuffer size is 200 * 1024.
{
const args = ['-e', "console.log('a'.repeat(200 * 1024 - 1))"];
const ret = spawnSync(process.execPath, args);
assert.ifError(ret.error); assert.ifError(ret.error);
assert.deepStrictEqual(
ret.stdout.toString().trim(),
'a'.repeat(200 * 1024 - 1)
);
} }

View File

@ -26,7 +26,7 @@ assert(logfile);
const { stdout } = spawnSync( const { stdout } = spawnSync(
process.execPath, process.execPath,
[ '--prof-process', '--preprocess', logfile ], [ '--prof-process', '--preprocess', logfile ],
{ cwd: tmpdir.path, encoding: 'utf8' }); { cwd: tmpdir.path, encoding: 'utf8', maxBuffer: Infinity });
// Make sure that the result is valid JSON. // Make sure that the result is valid JSON.
JSON.parse(stdout); JSON.parse(stdout);