Skip to content

Commit 224dd20

Browse files
committed
Fix zero maxBuffer allowing unbounded synchronous output
`maxBuffer: 0` disables the limit of `child_process.spawnSync()`, so the subprocess' output was never limited. A zero value must reject the first byte, like it does with asynchronous methods.
1 parent ed80079 commit 224dd20

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

‎lib/io/max-buffer.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ const getMaxBufferInfo = (error, maxBuffer) => {
6363
// The native `maxBuffer` option passed to `spawnSync()` does not allow differentiating values per file descriptor, so we use `stdout`'s.
6464
// Since each other file descriptor's own limit is capped by `stdout`'s, this is also the highest one.
6565
// Node.js applies it to the total output of all piped file descriptors, not to each of them. When it is hit, the subprocess is killed and a `ENOBUFS` error is thrown.
66-
export const getMaxBufferSync = ([, stdoutMaxBuffer]) => stdoutMaxBuffer;
66+
// Node.js disables its limit at zero, so use the smallest positive value to reject the first output byte.
67+
export const getMaxBufferSync = ([, stdoutMaxBuffer]) => stdoutMaxBuffer === 0 ? Number.MIN_VALUE : stdoutMaxBuffer;
6768

6869
// File descriptors with a limit lower than `spawnSync()`'s native `maxBuffer` are enforced from the output, after the subprocess exits.
6970
// Since no native error was thrown, we create the `ENOBUFS` error ourselves.

‎test/io/max-buffer.js‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,65 @@ test('maxBuffer.fd10 is invalid, sync', () => testInvalidFd('fd10', execaSync));
369369
// These tests need the subprocess to have written its output before the timeout kills it, which is only guaranteed if the timeout leaves room for the subprocess to start. On a busy machine, starting Node.js can take over a second.
370370
const SYNC_TIMEOUT = 1e4;
371371

372+
test('maxBuffer 0 terminates on the first output byte before timeout, sync', () => {
373+
const error = assertThrows(() => {
374+
execaSync('max-buffer-forever.js', ['1', '1'], {timeout: SYNC_TIMEOUT, maxBuffer: 0});
375+
}, maxBufferCodeSync);
376+
assert.equal(error.timedOut, false);
377+
assert.equal(error.isMaxBuffer, true);
378+
assert.equal(error.stdout, '');
379+
assertErrorMessage(error.shortMessage, {execaMethod: execaSync, length: 0});
380+
});
381+
382+
const testZeroMaxBufferSync = (fdNumber, maxBuffer, encoding = 'utf8') => {
383+
const error = execaSync('max-buffer-forever.js', [`${fdNumber}`, '1'], {
384+
...fullStdio,
385+
timeout: SYNC_TIMEOUT,
386+
maxBuffer,
387+
encoding,
388+
reject: false,
389+
});
390+
assert.equal(error.code, 'ENOBUFS');
391+
assert.equal(error.failed, true);
392+
assert.equal(error.timedOut, false);
393+
assert.equal(error.isMaxBuffer, true);
394+
assert.deepEqual(error.stdio[fdNumber], encoding === 'buffer' ? new Uint8Array() : '');
395+
assertErrorMessage(error.shortMessage, {execaMethod: execaSync, fdNumber, length: 0});
396+
};
397+
398+
test('maxBuffer 0 terminates on the first stderr byte before timeout, sync', () => testZeroMaxBufferSync(2, 0));
399+
test('maxBuffer 0 terminates on the first fd3 byte before timeout with binary output, sync', () => testZeroMaxBufferSync(3, 0, 'buffer'));
400+
test('maxBuffer 0 terminates on the first output byte with stdout-specific limit, sync', () => testZeroMaxBufferSync(1, {stdout: 0}));
401+
test('maxBuffer 0 terminates on the first fd3 byte with fd1-specific limit, sync', () => testZeroMaxBufferSync(3, {fd1: 0}));
402+
403+
const testZeroMaxBufferEmptySync = (maxBuffer, encoding = 'utf8') => {
404+
const {failed, isMaxBuffer, stdout} = execaSync('empty.js', {maxBuffer, encoding});
405+
assert.equal(failed, false);
406+
assert.equal(isMaxBuffer, false);
407+
assert.deepEqual(stdout, encoding === 'buffer' ? new Uint8Array() : '');
408+
};
409+
410+
test('maxBuffer 0 allows empty text output, sync', () => testZeroMaxBufferEmptySync(0));
411+
test('maxBuffer 0 allows empty binary output, sync', () => testZeroMaxBufferEmptySync(0, 'buffer'));
412+
test('maxBuffer 0 allows empty output with stdout-specific limit, sync', () => testZeroMaxBufferEmptySync({stdout: 0}));
413+
414+
test('maxBuffer small positive limit allows exactly one byte, sync', () => {
415+
const {failed, isMaxBuffer, stdout} = execaSync('max-buffer.js', ['1', '1'], {maxBuffer: 1});
416+
assert.equal(failed, false);
417+
assert.equal(isMaxBuffer, false);
418+
assert.equal(stdout, '.');
419+
});
420+
421+
test('maxBuffer small positive fractional limit rejects the first byte, sync', () => {
422+
const error = assertThrows(() => {
423+
execaSync('max-buffer-forever.js', ['1', '1'], {timeout: SYNC_TIMEOUT, maxBuffer: 0.5});
424+
}, maxBufferCodeSync);
425+
assert.equal(error.timedOut, false);
426+
assert.equal(error.isMaxBuffer, true);
427+
assert.equal(error.stdout, '');
428+
assertErrorMessage(error.shortMessage, {execaMethod: execaSync, length: 0.5});
429+
});
430+
372431
// A file descriptor limit lower than `maxBuffer.stdout` does not stop the subprocess with synchronous methods, so the subprocess can still time out afterwards
373432
const testMaxBufferTimeoutSync = (fdNumber, fdName) => {
374433
const length = 3;

0 commit comments

Comments
 (0)