java-topology/tools/tickets/defects/solargraph-0001.md
russell@unturf.com db29a08762 undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
2026-03-26 19:48:18 -04:00

3 KiB

id repo severity status created
solargraph-0001 Solargraph MEDIUM OPEN 2026-03-26

Defect

File: lib/solargraph/source/chain.rb:38,225,252 Pattern: @@inference_stack = [] — global class Array used as recursion guard during type inference; linear scan per pin Complexity: O(depth²) in the worst case per infer_from_definitions call chain Language: Ruby

Description

Chain#infer_from_definitions is the central type-inference method in Solargraph. It is called for every symbol the LSP server resolves. It uses a class-level global Array as a recursion guard:

# line 38 — class-level global, shared across ALL inference calls
@@inference_stack = []

# line 225 — called once per pin in `pins.each`
next if @@inference_stack.include?(pin)        # O(depth) linear scan
@@inference_stack.push pin

# line 252 — called once per unresolved pin
if @@inference_stack.include?(pin.identity)    # O(depth) linear scan
  next
end
@@inference_stack.push(pin.identity)

Two separate objects are pushed onto the same array: pin objects (line 227) and pin.identity strings (line 256). The mixed content means include? on line 225 will scan past identity strings looking for pin objects and vice versa, adding unnecessary comparisons.

@@inference_depth caps recursion at 10 (line 244), which bounds maximum stack depth to ~20 entries in normal operation. The O(depth²) overhead is therefore bounded in practice, but:

  1. The class-level @@ variable is shared across threads — Solargraph uses threads for parallel workspace indexing. Multiple threads modifying @@inference_stack concurrently is a data race.
  2. Mixed object types (Pin objects + String identities) in the same array means every include? call unnecessarily compares against the wrong type half the time.

Fix

Split into two separate Sets (or use thread-local variables) to eliminate the mixed-type problem and reduce lookup to O(1):

# Replace class-level Array with two thread-local Sets
def inference_pin_stack
  Thread.current[:solargraph_inference_pin_stack] ||= Set.new.compare_by_identity
end

def inference_identity_stack
  Thread.current[:solargraph_inference_identity_stack] ||= Set.new
end

# line 225
next if inference_pin_stack.include?(pin)
inference_pin_stack.add(pin)
# ... typify ...
inference_pin_stack.delete(pin)

# line 252
next if inference_identity_stack.include?(pin.identity)
inference_identity_stack.add(pin.identity)
# ... probe ...
inference_identity_stack.delete(pin.identity)

Using thread-local storage also eliminates the data race.

Speedup estimate

Low (depth is bounded at ~20) but fixes a thread-safety defect. In a multi-threaded workspace index with many concurrent resolutions the thread-local fix prevents incorrect inference results from concurrent stack corruption.

Work required

  • Patch in defects/solargraph/patch/
  • Unit test — asserts correct behaviour under concurrent inference
  • Integration test
  • Benchmark
  • White paper section