Skip to content

Commit eb61127

Browse files
committed
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: #22756 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: John-David Dalton <[email protected]>
1 parent 1cee085 commit eb61127

File tree

3 files changed

+76
-28
lines changed

3 files changed

+76
-28
lines changed

doc/api/util.md

+10-5
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,10 @@ stream.write('With ES6');
360360
<!-- YAML
361361
added: v0.3.0
362362
changes:
363+
- version: REPLACEME
364+
pr-url: https://github.com/nodejs/node/pull/22756
365+
description: The inspection output is now limited to about 128 MB. Data
366+
above that size will not be fully inspected.
363367
- version: v10.6.0
364368
pr-url: https://github.com/nodejs/node/pull/20725
365369
description: Inspecting linked lists and similar objects is now possible
@@ -408,11 +412,11 @@ changes:
408412
TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
409413
`maxEntries`.
410414
-->
411-
* `maxArrayLength` {number} Specifies the maximum number of `Array`,
415+
* `maxArrayLength` {integer} Specifies the maximum number of `Array`,
412416
[`TypedArray`][], [`WeakMap`][] and [`WeakSet`][] elements to include when
413417
formatting. Set to `null` or `Infinity` to show all elements. Set to `0` or
414418
negative to show no elements. **Default:** `100`.
415-
* `breakLength` {number} The length at which an object's keys are split
419+
* `breakLength` {integer} The length at which an object's keys are split
416420
across multiple lines. Set to `Infinity` to format an object as a single
417421
line. **Default:** `60` for legacy compatibility.
418422
* `compact` {boolean} Setting this to `false` changes the default indentation
@@ -532,9 +536,10 @@ console.log(inspect(weakSet, { showHidden: true }));
532536
```
533537

534538
Please note that `util.inspect()` is a synchronous method that is mainly
535-
intended as a debugging tool. Some input values can have a significant
536-
performance overhead that can block the event loop. Use this function
537-
with care and never in a hot code path.
539+
intended as a debugging tool. Its maximum output length is limited to
540+
approximately 128 MB and input values that result in output bigger than that
541+
will not be inspected fully. Such values can have a significant performance
542+
overhead that can block the event loop for a significant amount of time.
538543

539544
### Customizing `util.inspect` colors
540545

lib/util.js

+46-23
Original file line numberDiff line numberDiff line change
@@ -406,24 +406,27 @@ function inspect(value, opts) {
406406
maxArrayLength: inspectDefaultOptions.maxArrayLength,
407407
breakLength: inspectDefaultOptions.breakLength,
408408
indentationLvl: 0,
409-
compact: inspectDefaultOptions.compact
409+
compact: inspectDefaultOptions.compact,
410+
budget: {}
410411
};
411-
// Legacy...
412-
if (arguments.length > 2) {
413-
if (arguments[2] !== undefined) {
414-
ctx.depth = arguments[2];
415-
}
416-
if (arguments.length > 3 && arguments[3] !== undefined) {
417-
ctx.colors = arguments[3];
412+
if (arguments.length > 1) {
413+
// Legacy...
414+
if (arguments.length > 2) {
415+
if (arguments[2] !== undefined) {
416+
ctx.depth = arguments[2];
417+
}
418+
if (arguments.length > 3 && arguments[3] !== undefined) {
419+
ctx.colors = arguments[3];
420+
}
418421
}
419-
}
420-
// Set user-specified options
421-
if (typeof opts === 'boolean') {
422-
ctx.showHidden = opts;
423-
} else if (opts) {
424-
const optKeys = Object.keys(opts);
425-
for (var i = 0; i < optKeys.length; i++) {
426-
ctx[optKeys[i]] = opts[optKeys[i]];
422+
// Set user-specified options
423+
if (typeof opts === 'boolean') {
424+
ctx.showHidden = opts;
425+
} else if (opts) {
426+
const optKeys = Object.keys(opts);
427+
for (var i = 0; i < optKeys.length; i++) {
428+
ctx[optKeys[i]] = opts[optKeys[i]];
429+
}
427430
}
428431
}
429432
if (ctx.colors) ctx.stylize = stylizeWithColor;
@@ -623,14 +626,19 @@ function noPrototypeIterator(ctx, value, recurseTimes) {
623626
// corrected by setting `ctx.indentationLvL += diff` and then to decrease the
624627
// value afterwards again.
625628
function formatValue(ctx, value, recurseTimes) {
626-
// Primitive types cannot have properties
629+
// Primitive types cannot have properties.
627630
if (typeof value !== 'object' && typeof value !== 'function') {
628631
return formatPrimitive(ctx.stylize, value, ctx);
629632
}
630633
if (value === null) {
631634
return ctx.stylize('null', 'null');
632635
}
633636

637+
if (ctx.stop !== undefined) {
638+
const name = getConstructorName(value) || value[Symbol.toStringTag];
639+
return ctx.stylize(`[${name || 'Object'}]`, 'special');
640+
}
641+
634642
if (ctx.showProxy) {
635643
const proxy = getProxyDetails(value);
636644
if (proxy !== undefined) {
@@ -639,11 +647,11 @@ function formatValue(ctx, value, recurseTimes) {
639647
}
640648

641649
// Provide a hook for user-specified inspect functions.
642-
// Check that value is an object with an inspect function on it
650+
// Check that value is an object with an inspect function on it.
643651
if (ctx.customInspect) {
644652
const maybeCustom = value[customInspectSymbol];
645653
if (typeof maybeCustom === 'function' &&
646-
// Filter out the util module, its inspect function is special
654+
// Filter out the util module, its inspect function is special.
647655
maybeCustom !== exports.inspect &&
648656
// Also filter out any prototype objects using the circular check.
649657
!(value.constructor && value.constructor.prototype === value)) {
@@ -685,7 +693,7 @@ function formatRaw(ctx, value, recurseTimes) {
685693

686694
let extrasType = kObjectType;
687695

688-
// Iterators and the rest are split to reduce checks
696+
// Iterators and the rest are split to reduce checks.
689697
if (value[Symbol.iterator]) {
690698
noIterator = false;
691699
if (Array.isArray(value)) {
@@ -766,7 +774,7 @@ function formatRaw(ctx, value, recurseTimes) {
766774
}
767775
base = dateToISOString(value);
768776
} else if (isError(value)) {
769-
// Make error with message first say the error
777+
// Make error with message first say the error.
770778
base = formatError(value);
771779
// Wrap the error in brackets in case it has no stack trace.
772780
const stackStart = base.indexOf('\n at');
@@ -885,7 +893,21 @@ function formatRaw(ctx, value, recurseTimes) {
885893
}
886894
ctx.seen.pop();
887895

888-
return reduceToSingleString(ctx, output, base, braces);
896+
const res = reduceToSingleString(ctx, output, base, braces);
897+
const budget = ctx.budget[ctx.indentationLvl] || 0;
898+
const newLength = budget + res.length;
899+
ctx.budget[ctx.indentationLvl] = newLength;
900+
// If any indentationLvl exceeds this limit, limit further inspecting to the
901+
// minimum. Otherwise the recursive algorithm might continue inspecting the
902+
// object even though the maximum string size (~2 ** 28 on 32 bit systems and
903+
// ~2 ** 30 on 64 bit systems) exceeded. The actual output is not limited at
904+
// exactly 2 ** 27 but a bit higher. This depends on the object shape.
905+
// This limit also makes sure that huge objects don't block the event loop
906+
// significantly.
907+
if (newLength > 2 ** 27) {
908+
ctx.stop = true;
909+
}
910+
return res;
889911
}
890912

891913
function handleMaxCallStackSize(ctx, err, constructor, tag) {
@@ -1057,8 +1079,9 @@ function formatTypedArray(ctx, value, recurseTimes) {
10571079
formatBigInt;
10581080
for (var i = 0; i < maxLength; ++i)
10591081
output[i] = elementFormatter(ctx.stylize, value[i]);
1060-
if (remaining > 0)
1082+
if (remaining > 0) {
10611083
output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
1084+
}
10621085
if (ctx.showHidden) {
10631086
// .buffer goes last, it's not a primitive like the others.
10641087
ctx.indentationLvl += 2;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
// Test that huge objects don't crash due to exceeding the maximum heap size.
6+
7+
const util = require('util');
8+
9+
// Create a difficult to stringify object. Without the artificial limitation
10+
// this would crash or throw an maximum string size error.
11+
let last = {};
12+
const obj = last;
13+
14+
for (let i = 0; i < 1000; i++) {
15+
last.next = { circular: obj, last, obj: { a: 1, b: 2, c: true } };
16+
last = last.next;
17+
obj[i] = last;
18+
}
19+
20+
util.inspect(obj, { depth: Infinity });

0 commit comments

Comments
 (0)