5.2 KiB
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:
@@inference_stack.include?(pin)is O(depth) linear scan on a plain Array@@inference_stackis 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:
- It affects the type inference hot path — called on every autocompletion request
- The thread-safety defect can cause incorrect results for concurrent editor requests (multiple files open, background indexing)
- 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.
- Confirm receipt and assign a GitHub issue reference (castwide/solargraph).
- Assess severity — solargraph-0001 contains a thread-safety defect (data race on
@@inference_stack) in addition to the performance defect; both require fixing. - Coordinate a disclosure date — we are targeting 90 days from first contact.
- 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.