java-topology/whitepaper/outreach/solargraph.md

5.2 KiB
Raw Blame History

Solargraph — CWE-407 Disclosure Brief

2026-03-27 · Patch available — awaiting upstream merge

Finding

Two defects in Solargraph's type inference engine: one O(n²) performance defect and one data race on a shared class variable. Both patched. Patches ready for upstream review. Solargraph is the dominant Ruby language server — used in VS Code, Vim, Emacs, and other editors for Ruby autocompletion and hover documentation.

The Defects

solargraph-0001 (PATCHED — MEDIUM): lib/solargraph/source/chain.rb:38

# In Solargraph type inference — inference stack cycle detection:
@@inference_stack = []   # class variable — shared across ALL instances

def self.infer(pin, api_map, context, stack = [])
    return [] if @@inference_stack.include?(pin)   # O(depth) per call
    @@inference_stack.push(pin)
    # ...
    @@inference_stack.pop
end

Two compounding defects:

  1. @@inference_stack.include?(pin) is O(depth) linear scan on a plain Array
  2. @@inference_stack is a class variable — shared across all instances, creating a data race in multi-threaded Solargraph servers

O(depth²) total for depth-D inference chains; thread-unsafe across concurrent inference calls.

solargraph-0002 (PATCHED — MEDIUM): lib/solargraph/api_map/constants.rb:262

# In ApiMap::inner_get_constants() — recursive constant resolution:
def inner_get_constants(namespace, context, visibility, skip)
    # skip is converted to Array for set subtraction:
    if (skip.to_a - [namespace]).empty?   # O(|skip|) Array subtraction per call
        # ...
    end
    # Called recursively: O(depth) copies of O(|skip|) array
end

skip.to_a converts the accumulated skip set to an Array for every recursive call. Array subtraction skip.to_a - [namespace] is O(|skip|) per call. In recursive constant resolution with depth D: O(D × |skip|) total.

Complexity Proof

solargraph-0001: For inference chain depth D:

  • Per call: O(D) include? scan over growing stack
  • Total: O(D²)

With thread-safety defect: concurrent inference from multiple editor requests can corrupt @@inference_stack, causing missed cycle detection or spurious cycle reports.

Fix: thread-local Set.new for O(1) membership with no cross-thread sharing. Measured speedup: significant for deeply nested Ruby type hierarchies.

solargraph-0002: For recursive resolution depth D with S symbols in skip set:

  • Per recursive call: O(S) array conversion and subtraction
  • Total: O(D × S)

Fix: pass skip as a Set throughout; avoid to_a conversion.

Impact

Solargraph provides Ruby language intelligence for VS Code, vim-lsp, emacs-lsp, and other editors. It runs continuously as a language server daemon, providing autocompletion and hover documentation in real time as developers type.

solargraph-0001 is particularly concerning because:

  1. It affects the type inference hot path — called on every autocompletion request
  2. The thread-safety defect can cause incorrect results for concurrent editor requests (multiple files open, background indexing)
  3. Deep inheritance hierarchies (common in Rails applications) maximize D

solargraph-0002 affects constant resolution — called when resolving class references, module inclusions, and namespace lookups. Large Rails applications with deep namespace hierarchies hit this path frequently.

The Fix

solargraph-0001: Replace class variable Array with thread-local Set:

# Before
@@inference_stack = []
return [] if @@inference_stack.include?(pin)   # O(depth) + shared state

# After
# CWE-407 fix: thread-local Set for O(1) membership + thread-safety.
def self.infer(pin, api_map, context, stack = [])
    stack_set = Thread.current[:solargraph_inference_stack] ||= Set.new
    return [] if stack_set.include?(pin)    # O(1) set lookup
    stack_set.add(pin)
    begin
        # ...
    ensure
        stack_set.delete(pin)
    end
end

Thread-local storage (Thread.current[:key]) ensures each thread has its own inference stack, eliminating the data race.

solargraph-0002: Pass skip as a Set throughout constant resolution:

# Before
if (skip.to_a - [namespace]).empty?  # O(|skip|) array subtraction

# After
# CWE-407 fix: Set operations for O(1) membership instead of O(|skip|) Array subtraction.
if (skip - Set[namespace]).empty?    # Set subtraction: O(1) per element

Patch

Fix available: defects/solargraph/patch/solargraph-0001-0002-thread-local-set.patch

Two-location patch across source/chain.rb and api_map/constants.rb.

Unit test: solargraph-0001 O(D²) → O(D) growth confirmed; thread-safety: concurrent inference test passes without race. solargraph-0002: O(D×S) → O(D+S).

What We Ask

A patch is ready for review.

  1. Confirm receipt and assign a GitHub issue reference (castwide/solargraph).
  2. Assess severity — solargraph-0001 contains a thread-safety defect (data race on @@inference_stack) in addition to the performance defect; both require fixing.
  3. Coordinate a disclosure date — we are targeting 90 days from first contact.
  4. We will credit the Solargraph team in the public disclosure. Preferred acknowledgment format welcome.

Contact: see cover email. This brief is confidential until coordinated disclosure.