From 5c172f2e1c214660a8629d05ae2b557223b4e0b0 Mon Sep 17 00:00:00 2001 From: Zac Spitzer Date: Sat, 9 May 2026 18:02:20 +0200 Subject: [PATCH 1/2] test(LDEV-6309): failing spec for self-referential struct inspection --- test/cfml/VariablesTest.cfc | 41 +++++++++++++++++++ .../cfml/artifacts/circular-struct-target.cfm | 26 ++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 test/cfml/artifacts/circular-struct-target.cfm diff --git a/test/cfml/VariablesTest.cfc b/test/cfml/VariablesTest.cfc index 66d930d..d9a3b0c 100644 --- a/test/cfml/VariablesTest.cfc +++ b/test/cfml/VariablesTest.cfc @@ -215,6 +215,47 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="dap" { cleanupThread( threadId ); } ); + it( "inspects a self-referential struct without StackOverflowError (LDEV-6309)", function() { + var circularTarget = getArtifactPath( "circular-struct-target.cfm" ); + var circularDebugLine = 18; // var debugLine = "inspect here"; + + dap.setBreakpoints( circularTarget, [ circularDebugLine ] ); + triggerArtifact( "circular-struct-target.cfm" ); + + var stopped = dap.waitForEvent( "stopped", 2000 ); + var threadId = stopped.body.threadId; + + var frame = getTopFrame( threadId ); + var localScope = getScopeByName( frame.id, "Local" ); + + var varsResponse = dap.getVariables( localScope.variablesReference ); + expect( varsResponse.success ).toBeTrue( "Variables request should not crash on self-referential struct" ); + + var varMap = {}; + for ( var v in varsResponse.body.variables ) { + varMap[ v.name ] = v; + } + + expect( varMap ).toHaveKey( "circular" ); + expect( varMap.circular.variablesReference ).toBeGT( 0, "circular should be expandable" ); + + var circularChildren = dap.getVariables( varMap.circular.variablesReference ); + expect( circularChildren.success ).toBeTrue( "Expanding the cycle should not crash" ); + + var childMap = {}; + for ( var v in circularChildren.body.variables ) { + childMap[ v.name ] = v; + } + expect( childMap ).toHaveKey( "self" ); + expect( childMap.self.variablesReference ).toBe( varMap.circular.variablesReference, "self should reuse the parent's variablesReference" ); + + expect( varMap ).toHaveKey( "indirectA" ); + var indirectAChildren = dap.getVariables( varMap.indirectA.variablesReference ); + expect( indirectAChildren.success ).toBeTrue( "Expanding indirect cycle (a->b->a) should not crash" ); + + cleanupThread( threadId ); + } ); + } ); } } diff --git a/test/cfml/artifacts/circular-struct-target.cfm b/test/cfml/artifacts/circular-struct-target.cfm new file mode 100644 index 0000000..f786063 --- /dev/null +++ b/test/cfml/artifacts/circular-struct-target.cfm @@ -0,0 +1,26 @@ + +/** + * Target file for self-referential struct inspection (LDEV-6309). + * + * Repros the StackOverflowError when the debugger registers a struct + * containing itself in ValTracker.wrapperByObj. + */ + +function testCircular() { + var circular = {}; + circular.self = circular; + + var indirectA = {}; + var indirectB = {}; + indirectA.b = indirectB; + indirectB.a = indirectA; + + var debugLine = "inspect here"; + + return circular; +} + +testCircular(); + +writeOutput( "Done" ); + From e6b6edf82ac5cd1fd00fe4c30b9bb4640a22e9c0 Mon Sep 17 00:00:00 2001 From: Zac Spitzer Date: Sat, 9 May 2026 18:14:14 +0200 Subject: [PATCH 2/2] fix(LDEV-6309): use identity-keyed weak map for ValTracker.wrapperByObj --- .../extension/debugger/coreinject/ValTracker.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/source/java/src/org/lucee/extension/debugger/coreinject/ValTracker.java b/source/java/src/org/lucee/extension/debugger/coreinject/ValTracker.java index aea8fd1..acb0069 100644 --- a/source/java/src/org/lucee/extension/debugger/coreinject/ValTracker.java +++ b/source/java/src/org/lucee/extension/debugger/coreinject/ValTracker.java @@ -2,23 +2,24 @@ import java.lang.ref.Cleaner; import java.lang.ref.WeakReference; -import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; +import org.lucee.extension.debugger.util.ConcurrentWeakKeyMap; + public class ValTracker { private final Cleaner cleaner; /** - * Really we want a ConcurrentWeakHashMap - we could use Guava mapMaker with weakKeys. - * Instead we opt to use a sync'd map, because we expect that the number of threads - * touching the map can be more than 1, but will typically be exactly 1 (the DAP session issuing 'show variables' requests) + * Identity-keyed weak map: entries are removed when the referent is GC'd, and + * lookups use System.identityHashCode rather than the referent's hashCode(). + * This avoids StackOverflowError when a struct references itself (LDEV-6309) — + * StructImpl.hashCode() walks all values and would recurse forever on a cycle. */ - private final Map wrapperByObj = Collections.synchronizedMap(new WeakHashMap<>()); + private final Map wrapperByObj = new ConcurrentWeakKeyMap<>(); private final Map wrapperByID = new ConcurrentHashMap<>(); /**