Class: Audition::Static::GemCalls

Inherits:
Object
  • Object
show all
Defined in:
lib/audition/static/gem_calls.rb

Overview

Call sites into bundled gems whose compiled extensions do not declare Ractor safety (rb_ext_ractor_safe), so calling into them from a non-main Ractor raises Ractor::UnsafeError. The extension's code is outside the scanned tree; the call site is the only place a static pass can flag.

Rules are derived from the target's own bundle, never from a gem list: a pinned gem whose installed extension lacks the declaration—or that pins a platform-specific build Audition cannot inspect (the running Ruby's own copy of the extension settles it when one is shipped)—gets its call sites flagged, anchored to the gem's own namespace (read from its entry file's module nesting, or the require-path convention when the gem is not installed). Matched calls taint: the value handed back is presumed to still live in the extension, so calls chained onto it, or on the local or ivar it is assigned to, are flagged and keep the taint moving; a predicate ends the chain, and freeze, tap, itself, and class pass it along silently. Sorbet annotations extend the reach—a sig param, T.let, or T.cast typed with a constant under a flagged gem's namespace taints the annotated variable the same way, and a plain type clears the guess. A per-class pre-pass makes the body order-free: a method whose return expression or sig return type is rooted in a flagged namespace hands the taint to callers on self, self.class, or the class's own constant, and an ivar assigned such a value anywhere in the body—directly or through its attr writer—is tainted throughout. A pass over the whole tree first promotes classes that hold extension values in their instances—or subclass one that does—to rules of their own, so constructing or receiving one in another file carries the taint across files.

Defined Under Namespace

Classes: Entry, Registry, Rule, Scan

Constant Summary collapse

UNSAFE_WHY =
"%{gem}'s compiled extension does not declare Ractor " \
"safety (rb_ext_ractor_safe), so its methods raise " \
"Ractor::UnsafeError (\"ractor unsafe method called from " \
"not main ractor\") from any non-main Ractor."
UNVERIFIED_WHY =
"%{gem} pins a compiled extension Audition could not " \
"inspect (the gem is not installed here). An extension " \
"that does not declare Ractor safety " \
"(rb_ext_ractor_safe) raises Ractor::UnsafeError from " \
"any non-main Ractor; install the bundle to verify."
MESSAGE =
"%{receiver}.%{method} calls into %{gem}, whose compiled " \
"extension does not declare Ractor safety"
UNVERIFIED_MESSAGE =
"%{receiver}.%{method} calls into %{gem}, which pins a " \
"compiled extension Audition could not inspect"
DERIVED_MESSAGE =
"%{method} called on a value handed out by %{gem}'s " \
"native extension"
FIX =
"Keep calls into %{gem} on the main Ractor and share " \
"only extracted plain data (frozen strings, numbers) " \
"between Ractors, or get the extension to declare " \
"rb_ext_ractor_safe(true)."
CORE_METHODS =

Object/Kernel identity methods that never enter the extension, even on a gem object.

Ractor.make_shareable(
  Set.new(
    %i[nil? is_a? kind_of? instance_of? respond_to? frozen?
      equal? class object_id itself hash tap then freeze]
  )
)
CHAIN_METHODS =

Core methods that hand back the receiver—or, for class, the extension's own class object: no finding, but a tainted receiver's taint passes through.

Ractor.make_shareable(
  Set.new(%i[class itself tap freeze])
)
CHECK =
"native-gem-calls"
EMPTY_NESTING =
Ractor.make_shareable([])
TYPE_CHECKS =

Predicates whose constant argument proves the receiver's class when they gate a branch.

Ractor.make_shareable(
  Set.new(%i[is_a? kind_of? instance_of?])
)
EVAL_REOPENINGS =
Ractor.make_shareable(
  Set.new(%i[class_eval module_eval])
)
YIELD_SELF =

Methods that hand the receiver itself to their block.

Ractor.make_shareable(
  Set.new(%i[then yield_self])
)
EMPTY_SET =
Ractor.make_shareable(Set.new)
STUB_SHARE =

What a stub has to show before its gem counts as compiled: one source location holding this share of the gem's located methods, across at least this many classes.

0.6
STUB_OWNERS =
2
STUB_SOURCE =
%r{# source://(\S+)}
STUB_DEF =
/\A\s*def [\w\[\]<>=!+\-*\/%~^&|?]/
STUB_SCOPE =
/\A\s*(?:class|module)\s+([A-Za-z0-9_:]+)/
RUBY_METHODS =

Everything an object answers before any extension gets involved: a bare call to anything else inside a reopened bound class lands in the extension.

Ractor.make_shareable(
  Set.new(
    [Object, Kernel, Module, Class].flat_map do |mod|
      mod.instance_methods + mod.private_instance_methods
    end
  )
)
BLOCK_MEMO_POSITIONS =

Core iterators hand elements to their block; these positions carry the memo or index instead.

Ractor.make_shareable(
  {each_with_object: 1, with_object: 1, each_with_index: 1,
   with_index: 1, inject: 0, reduce: 0}
)

Instance Method Summary collapse

Constructor Details

#initialize(root:, stubs: [], rules: nil) ⇒ GemCalls

Returns a new instance of GemCalls.

Parameters:

  • root (String) —

    target root, where Gemfile.lock lives

  • stubs (Array<String>) (defaults to: []) —

    the target's .rbi stubs

  • rules (Array<Rule>, nil) (defaults to: nil) —

    override bundle resolution



154
155
156
157
158
159
160
161
162
163
164
# File 'lib/audition/static/gem_calls.rb', line 154

def initialize(root:, stubs: [], rules: nil)
  @stubs = stubs
  @rules = rules || resolve(root)
  @nesting = EMPTY_NESTING
  @self_rule = nil
  @self_defs = EMPTY_SET
  @param_seeds = {}
  @return_taints = {}
  reset_seed_ledger
  index_rules
end

Instance Method Details

#analyze_paths(paths, progress: Progress::SILENT) ⇒ Array<Finding>

Parameters:

  • paths (Array<String>) —

    files to scan

  • progress (Progress) (defaults to: Progress::SILENT) —

    narrates the three passes this phase makes over the tree

Returns:



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/audition/static/gem_calls.rb', line 170

def analyze_paths(paths, progress: Progress::SILENT)
  return [] if @rules.empty?

  progress.stage("subclasses", total: paths.size)
  @rules += derived_class_rules(paths, progress)
  index_rules
  @param_seeds = {}
  reset_seed_ledger
  findings = {}
  progress.stage("scanning", total: paths.size)
  paths.each do |path|
    progress.tick
    source = read_source(path)
    findings[path] = analyze_file(path, source) if source
  end
  settle_seeds(findings, progress)
  findings.values.flatten(1)
end