From 3fb03d9f5aa63636f2f53a8fcbad847296f6ba49 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 30 Mar 2026 16:37:16 +0200 Subject: [PATCH] assert,util: fix stale nested cycle memo entries Temporary nested cycle-tracking entries could remain in the memory set after a successful comparison. If a later sibling comparison reused one of those objects, deepStrictEqual could incorrectly fail for equivalent structures. This cleans up the temporary nested entries after the nested comparison returns. Fixes: https://github.com/nodejs/node/issues/62422 --- lib/internal/util/comparisons.js | 4 ++++ test/parallel/test-assert-deep.js | 38 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index a8d5178242fdc8..cc6895ec935f9f 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -529,6 +529,10 @@ function handleCycles(val1, val2, mode, keys1, keys2, memos, iterationType) { memos.deep = true; const result = objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType); memos.deep = false; + if (memos.set !== undefined) { + memos.set.delete(memos.c); + memos.set.delete(memos.d); + } return result; } memos.set = new SafeSet(); diff --git a/test/parallel/test-assert-deep.js b/test/parallel/test-assert-deep.js index c1a42aa4de624d..6f48d790ed9abe 100644 --- a/test/parallel/test-assert-deep.js +++ b/test/parallel/test-assert-deep.js @@ -248,6 +248,14 @@ function assertOnlyDeepEqual(a, b, err) { ); } +function activateMemoizedCycleDetection() { + const circA = {}; + circA.self = circA; + const circB = {}; + circB.self = circB; + assert.deepStrictEqual(circA, circB); +} + test('es6 Maps and Sets', () => { assertDeepAndStrictEqual(new Set(), new Set()); assertDeepAndStrictEqual(new Map(), new Map()); @@ -597,6 +605,36 @@ test('GH-14441. Circular structures should be consistent', () => { } }); +test('deepStrictEqual handles shared expected array elements after cycle detection', () => { + const sharedExpected = { outer: { inner: 0 } }; + const actualValues = [{ outer: { inner: 0 } }, { outer: { inner: 0 } }]; + const expectedValues = [sharedExpected, sharedExpected]; + + activateMemoizedCycleDetection(); + + assertDeepAndStrictEqual(actualValues[0], expectedValues[0]); + assertDeepAndStrictEqual(actualValues[1], expectedValues[1]); + assertDeepAndStrictEqual(actualValues, expectedValues); +}); + +test('deepStrictEqual handles cross-root aliases after cycle detection', () => { + activateMemoizedCycleDetection(); + + const nestedExpected = {}; + nestedExpected.loop = nestedExpected; + nestedExpected.payload = { value: 1 }; + + const expected = {}; + expected.loop = nestedExpected; + expected.payload = { value: 1 }; + + const actual = {}; + actual.loop = expected; + actual.payload = { value: 1 }; + + assertDeepAndStrictEqual(actual, expected); +}); + // https://github.com/nodejs/node-v0.x-archive/pull/7178 test('Ensure reflexivity of deepEqual with `arguments` objects.', () => { const args = (function() { return arguments; })();