Skip to content

Commit

Permalink
util: limit inspection output size to 128 MB
Browse files Browse the repository at this point in the history
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: nodejs#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>
  • Loading branch information
BridgeAR committed Sep 13, 2018
1 parent 1cee085 commit eb61127
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 28 deletions.
15 changes: 10 additions & 5 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
69 changes: 46 additions & 23 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 > 3 && arguments[3] !== undefined) {
ctx.colors = arguments[3];
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];
}
}
}
// 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;
Expand Down Expand Up @@ -623,14 +626,19 @@ 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);
}
if (value === null) {
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) {
Expand All @@ -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)) {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions test/parallel/test-util-inspect-long-running.js
Original file line number Diff line number Diff line change
@@ -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 });

0 comments on commit eb61127

Please sign in to comment.