Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion lib/internal/assert/assertion_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
ObjectPrototypeHasOwnProperty,
SafeSet,
String,
StringPrototypeEndsWith,
StringPrototypeRepeat,
StringPrototypeSlice,
StringPrototypeSplit,
Expand Down Expand Up @@ -42,6 +43,11 @@ const kReadableOperator = {

const kMaxShortStringLength = 12;
const kMaxLongStringLength = 512;
// Maximum size for inspect output before truncation to prevent OOM.
// Objects with many converging paths can produce exponential growth in
// util.inspect output at high depths, leading to OOM during diff generation.
const kMaxInspectOutputLength = 2 * 1024 * 1024; // 2MB
const kTruncatedByteMarker = '\n... [truncated]';

const kMethodsWithCustomMessageDiff = new SafeSet()
.add('deepStrictEqual')
Expand Down Expand Up @@ -72,7 +78,7 @@ function copyError(source) {
function inspectValue(val) {
// The util.inspect default values could be changed. This makes sure the
// error messages contain the necessary information nevertheless.
return inspect(val, {
const result = inspect(val, {
compact: false,
customInspect: false,
depth: 1000,
Expand All @@ -85,6 +91,17 @@ function inspectValue(val) {
// Inspect getters as we also check them when comparing entries.
getters: true,
});

// Truncate if the output is too large to prevent OOM during diff generation.
// Objects with deeply nested structures can produce exponentially large
// inspect output that causes memory exhaustion when passed to the diff
// algorithm.
if (result.length > kMaxInspectOutputLength) {
return StringPrototypeSlice(result, 0, kMaxInspectOutputLength) +
kTruncatedByteMarker;
}

return result;
}

function getErrorMessage(operator, message) {
Expand Down Expand Up @@ -189,6 +206,12 @@ function createErrDiff(actual, expected, operator, customMessage, diffType = 'si
let message = '';
const inspectedActual = inspectValue(actual);
const inspectedExpected = inspectValue(expected);

// Check if either value was truncated due to size limits
if (StringPrototypeEndsWith(inspectedActual, kTruncatedByteMarker) ||
StringPrototypeEndsWith(inspectedExpected, kTruncatedByteMarker)) {
skipped = true;
}
const inspectedSplitActual = StringPrototypeSplit(inspectedActual, '\n');
const inspectedSplitExpected = StringPrototypeSplit(inspectedExpected, '\n');
const showSimpleDiff = isSimpleDiff(actual, inspectedSplitActual, expected, inspectedSplitExpected);
Expand Down
91 changes: 91 additions & 0 deletions test/parallel/test-assert-large-object-diff-oom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Flags: --max-old-space-size=512
'use strict';

// Regression test: assert.strictEqual should not OOM when comparing objects
// with many converging paths to shared objects. Such objects cause exponential
// growth in util.inspect output, which previously led to OOM during error
// message generation.

require('../common');
const assert = require('assert');

// Test: should throw AssertionError, not OOM
{
const { doc1, doc2 } = createTestObjects();

assert.throws(
() => assert.strictEqual(doc1, doc2),
(err) => {
assert.ok(err instanceof assert.AssertionError);
// Message should be bounded (fix truncates inspect output at 2MB)
assert.ok(err.message.length < 5 * 1024 * 1024);
return true;
}
);
}

// Creates objects where many paths converge on shared objects, causing
// exponential growth in util.inspect output at high depths.
function createTestObjects() {
const base = createBase();

const s1 = createSchema(base, 's1');
const s2 = createSchema(base, 's2');
base.schemas.s1 = s1;
base.schemas.s2 = s2;

const doc1 = createDoc(s1, base);
const doc2 = createDoc(s2, base);

// Populated refs create additional converging paths
for (let i = 0; i < 2; i++) {
const ps = createSchema(base, 'p' + i);
base.schemas['p' + i] = ps;
doc1.$__.pop['r' + i] = { value: createDoc(ps, base), opts: { base, schema: ps } };
}

// Cross-link creates more converging paths
doc1.$__.pop.r0.value.$__parent = doc2;

return { doc1, doc2 };
}

function createBase() {
const base = { types: {}, schemas: {} };
for (let i = 0; i < 4; i++) {
base.types['t' + i] = {
base,
caster: { base },
opts: { base, validators: [{ base }, { base }] }
};
}
return base;
}

function createSchema(base, name) {
const schema = { name, base, paths: {}, children: [] };
for (let i = 0; i < 6; i++) {
schema.paths['f' + i] = {
schema, base,
type: base.types['t' + (i % 4)],
caster: base.types['t' + (i % 4)].caster,
opts: { schema, base, validators: [{ schema, base }] }
};
}
for (let i = 0; i < 2; i++) {
const child = { name: name + '_c' + i, base, parent: schema, paths: {} };
for (let j = 0; j < 3; j++) {
child.paths['cf' + j] = { schema: child, base, type: base.types['t' + (j % 4)] };
}
schema.children.push(child);
}
return schema;
}

function createDoc(schema, base) {
const doc = { schema, base, $__: { scopes: {}, pop: {} } };
for (let i = 0; i < 6; i++) {
doc.$__.scopes['p' + i] = { schema, base, type: base.types['t' + (i % 4)] };
}
return doc;
}
Loading