java-topology/whitepaper/outreach/rubocop.md

4.4 KiB
Raw Blame History

RuboCop — CWE-407 Disclosure Brief

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

Finding

Two O(n²) defects in RuboCop's IgnoredNode mixin and RedundantSelf cop. Both patched. Patches ready for upstream review. RuboCop is the dominant Ruby static analysis tool — used in virtually every Ruby/Rails CI pipeline.

The Defects

rubocop-0001 (PATCHED — MEDIUM): lib/rubocop/cop/mixin/ignored_node.rb:32

# IgnoredNode mixin — included in every cop via Cop::Base:
def initialize(*)
    super
    @ignored_nodes = []   # plain Array
end

def ignore_node(node)
    @ignored_nodes << node
end

def part_of_ignored_node?(node)
    @ignored_nodes.any? { |ignored| ignored.equal?(node) ||
                                    node.each_ancestor.include?(ignored) }
    # Array.any? — O(R) scan for R ignored nodes
end

@ignored_nodes is a plain Ruby Array. @ignored_nodes.any? scans linearly for every on_str and on_sym node encountered. In a Ruby file with many string literals and active cops that use IgnoredNode, this fires O(R × S) times where R = ignored nodes, S = string/symbol nodes.

rubocop-0002 (PATCHED — LOW): lib/rubocop/cop/style/redundant_self.rb:62

# In RedundantSelf cop:
def initialize(*)
    super
    @allowed_send_nodes = []   # plain Array
end

def allow_send_node?(node)
    @allowed_send_nodes.include?(node.method_name)  # O(A) per on_send call
end

@allowed_send_nodes is a plain Array. include? is O(A) per on_send callback. Called for every method call in the file. O(A × M) where A = allowed nodes, M = method calls.

Complexity Proof

rubocop-0001: For R ignored nodes and S string/symbol nodes per file:

  • Per node: O(R) Array.any? scan
  • Total per file: O(R × S)

In large Ruby files with many string literals (template files, configuration DSLs, i18n files): R grows as cops accumulate ignored regions; S grows with file size. 100× op reduction with Set.new.compare_by_identity.

rubocop-0002: For A allowed nodes and M method calls:

  • Per call: O(A) Array.include? scan
  • Total per file: O(A × M)

Fix: Set.new for both.

Impact

RuboCop is used in virtually every Ruby and Rails CI pipeline. It runs on every file on every push for code style enforcement. Large Rails applications with thousands of files, run through RuboCop on every CI commit, pay this overhead on every file analysis.

rubocop-0001 is particularly significant: IgnoredNode is included in Cop::Base, which is the base class for every RuboCop cop. Every cop that uses ignore_node (multiple built-in cops do) creates an @ignored_nodes Array. For files with many string literals (common in Rails view helpers, i18n YAML loaders, RSpec test files), this fires repeatedly per node per active cop.

The IgnoredNode mixin is performance-sensitive — it fires for every node of certain types in every analyzed file.

The Fix

rubocop-0001: Replace Array with Set.new.compare_by_identity:

# Before
@ignored_nodes = []

# After
# CWE-407 fix: Set with identity comparison for O(1) include? instead of O(R) Array scan.
require 'set'
@ignored_nodes = Set.new.compare_by_identity

compare_by_identity uses object identity (equal?) for set membership — matching the existing ignored.equal?(node) semantics exactly.

rubocop-0002: Replace Array with Set.new:

# Before
@allowed_send_nodes = []

# After
# CWE-407 fix: Set for O(1) include? instead of O(A) Array scan.
@allowed_send_nodes = Set.new

Patch

Fix available: defects/rubocop/patch/rubocop-0001-0002-ignored-nodes-set.patch

Two-location patch across cop/mixin/ignored_node.rb and cop/style/redundant_self.rb.

Unit test: rubocop-0001 O(R×S) → O(S) growth confirmed. Significant speedup on large files with many string literals. rubocop-0002: O(A×M) → O(M).

What We Ask

A patch is ready for review.

  1. Confirm receipt and assign a GitHub issue reference (rubocop/rubocop).
  2. Assess severity — rubocop-0001 fires for every string/symbol node in every file; the IgnoredNode mixin affects all cops that track ignored regions.
  3. Coordinate a disclosure date — we are targeting 90 days from first contact.
  4. We will credit the RuboCop team in the public disclosure. Preferred acknowledgment format welcome.

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