java-topology/whitepaper/outreach/solargraph.md

125 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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