util: limit inspection output size to 128 MB

The maximum hard limit that `util.inspect()` could theoretically handle
is the maximum string size. That is ~2 ** 28 on 32 bit systems and
~2 ** 30 on 64 bit systems.

Due to the recursive algorithm a complex object could easily exceed
that limit without throwing an error right away and therefore
crashing the application by exceeding the heap limit.

`util.inspect()` is fast enough to compute 128 MB of data below one
second on an Intel(R) Core(TM) i7-5600U CPU. This hard limit allows
to inspect arbitrary big objects from now on without crashing the
application or blocking the event loop significantly.

PR-URL: https://github.com/nodejs/node/pull/22756
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
This commit is contained in:
Ruben Bridgewater 2018-05-16 18:10:08 +02:00
parent 1cee085367
commit eb61127c48
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
3 changed files with 76 additions and 28 deletions

View File

@ -360,6 +360,10 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/22756
description: The inspection output is now limited to about 128 MB. Data
above that size will not be fully inspected.
- version: v10.6.0
pr-url: https://github.com/nodejs/node/pull/20725
description: Inspecting linked lists and similar objects is now possible
@ -408,11 +412,11 @@ changes:
TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
`maxEntries`.
-->
* `maxArrayLength` {number} Specifies the maximum number of `Array`,
* `maxArrayLength` {integer} Specifies the maximum number of `Array`,
[`TypedArray`][], [`WeakMap`][] and [`WeakSet`][] elements to include when
formatting. Set to `null` or `Infinity` to show all elements. Set to `0` or
negative to show no elements. **Default:** `100`.
* `breakLength` {number} The length at which an object's keys are split
* `breakLength` {integer} The length at which an object's keys are split
across multiple lines. Set to `Infinity` to format an object as a single
line. **Default:** `60` for legacy compatibility.
* `compact` {boolean} Setting this to `false` changes the default indentation
@ -532,9 +536,10 @@ console.log(inspect(weakSet, { showHidden: true }));
```
Please note that `util.inspect()` is a synchronous method that is mainly
intended as a debugging tool. Some input values can have a significant
performance overhead that can block the event loop. Use this function
with care and never in a hot code path.
intended as a debugging tool. Its maximum output length is limited to
approximately 128 MB and input values that result in output bigger than that
will not be inspected fully. Such values can have a significant performance
overhead that can block the event loop for a significant amount of time.
### Customizing `util.inspect` colors

View File

@ -406,24 +406,27 @@ function inspect(value, opts) {
maxArrayLength: inspectDefaultOptions.maxArrayLength,
breakLength: inspectDefaultOptions.breakLength,
indentationLvl: 0,
compact: inspectDefaultOptions.compact
compact: inspectDefaultOptions.compact,
budget: {}
};
// Legacy...
if (arguments.length > 2) {
if (arguments[2] !== undefined) {
ctx.depth = arguments[2];
if (arguments.length > 1) {
// Legacy...
if (arguments.length > 2) {
if (arguments[2] !== undefined) {
ctx.depth = arguments[2];
}
if (arguments.length > 3 && arguments[3] !== undefined) {
ctx.colors = arguments[3];
}
}
if (arguments.length > 3 && arguments[3] !== undefined) {
ctx.colors = arguments[3];
}
}
// Set user-specified options
if (typeof opts === 'boolean') {
ctx.showHidden = opts;
} else if (opts) {
const optKeys = Object.keys(opts);
for (var i = 0; i < optKeys.length; i++) {
ctx[optKeys[i]] = opts[optKeys[i]];
// Set user-specified options
if (typeof opts === 'boolean') {
ctx.showHidden = opts;
} else if (opts) {
const optKeys = Object.keys(opts);
for (var i = 0; i < optKeys.length; i++) {
ctx[optKeys[i]] = opts[optKeys[i]];
}
}
}
if (ctx.colors) ctx.stylize = stylizeWithColor;
@ -623,7 +626,7 @@ function noPrototypeIterator(ctx, value, recurseTimes) {
// corrected by setting `ctx.indentationLvL += diff` and then to decrease the
// value afterwards again.
function formatValue(ctx, value, recurseTimes) {
// Primitive types cannot have properties
// Primitive types cannot have properties.
if (typeof value !== 'object' && typeof value !== 'function') {
return formatPrimitive(ctx.stylize, value, ctx);
}
@ -631,6 +634,11 @@ function formatValue(ctx, value, recurseTimes) {
return ctx.stylize('null', 'null');
}
if (ctx.stop !== undefined) {
const name = getConstructorName(value) || value[Symbol.toStringTag];
return ctx.stylize(`[${name || 'Object'}]`, 'special');
}
if (ctx.showProxy) {
const proxy = getProxyDetails(value);
if (proxy !== undefined) {
@ -639,11 +647,11 @@ function formatValue(ctx, value, recurseTimes) {
}
// Provide a hook for user-specified inspect functions.
// Check that value is an object with an inspect function on it
// Check that value is an object with an inspect function on it.
if (ctx.customInspect) {
const maybeCustom = value[customInspectSymbol];
if (typeof maybeCustom === 'function' &&
// Filter out the util module, its inspect function is special
// Filter out the util module, its inspect function is special.
maybeCustom !== exports.inspect &&
// Also filter out any prototype objects using the circular check.
!(value.constructor && value.constructor.prototype === value)) {
@ -685,7 +693,7 @@ function formatRaw(ctx, value, recurseTimes) {
let extrasType = kObjectType;
// Iterators and the rest are split to reduce checks
// Iterators and the rest are split to reduce checks.
if (value[Symbol.iterator]) {
noIterator = false;
if (Array.isArray(value)) {
@ -766,7 +774,7 @@ function formatRaw(ctx, value, recurseTimes) {
}
base = dateToISOString(value);
} else if (isError(value)) {
// Make error with message first say the error
// Make error with message first say the error.
base = formatError(value);
// Wrap the error in brackets in case it has no stack trace.
const stackStart = base.indexOf('\n at');
@ -885,7 +893,21 @@ function formatRaw(ctx, value, recurseTimes) {
}
ctx.seen.pop();
return reduceToSingleString(ctx, output, base, braces);
const res = reduceToSingleString(ctx, output, base, braces);
const budget = ctx.budget[ctx.indentationLvl] || 0;
const newLength = budget + res.length;
ctx.budget[ctx.indentationLvl] = newLength;
// If any indentationLvl exceeds this limit, limit further inspecting to the
// minimum. Otherwise the recursive algorithm might continue inspecting the
// object even though the maximum string size (~2 ** 28 on 32 bit systems and
// ~2 ** 30 on 64 bit systems) exceeded. The actual output is not limited at
// exactly 2 ** 27 but a bit higher. This depends on the object shape.
// This limit also makes sure that huge objects don't block the event loop
// significantly.
if (newLength > 2 ** 27) {
ctx.stop = true;
}
return res;
}
function handleMaxCallStackSize(ctx, err, constructor, tag) {
@ -1057,8 +1079,9 @@ function formatTypedArray(ctx, value, recurseTimes) {
formatBigInt;
for (var i = 0; i < maxLength; ++i)
output[i] = elementFormatter(ctx.stylize, value[i]);
if (remaining > 0)
if (remaining > 0) {
output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
}
if (ctx.showHidden) {
// .buffer goes last, it's not a primitive like the others.
ctx.indentationLvl += 2;

View File

@ -0,0 +1,20 @@
'use strict';
require('../common');
// Test that huge objects don't crash due to exceeding the maximum heap size.
const util = require('util');
// Create a difficult to stringify object. Without the artificial limitation
// this would crash or throw an maximum string size error.
let last = {};
const obj = last;
for (let i = 0; i < 1000; i++) {
last.next = { circular: obj, last, obj: { a: 1, b: 2, c: true } };
last = last.next;
obj[i] = last;
}
util.inspect(obj, { depth: Infinity });