Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object, WeakTaggedObject> wrapperByObj = Collections.synchronizedMap(new WeakHashMap<>());
private final Map<Object, WeakTaggedObject> wrapperByObj = new ConcurrentWeakKeyMap<>();
private final Map<Long, WeakTaggedObject> wrapperByID = new ConcurrentHashMap<>();

/**
Expand Down
41 changes: 41 additions & 0 deletions test/cfml/VariablesTest.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
} );

} );
}
}
26 changes: 26 additions & 0 deletions test/cfml/artifacts/circular-struct-target.cfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<cfscript>
/**
* 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" );
</cfscript>