# 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`

```ruby
# 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`

```ruby
# 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:

```ruby
# 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:

```ruby
# 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.
