Class: Audition::Static::GemCalls
- Inherits:
-
Object
- Object
- Audition::Static::GemCalls
- 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
- #analyze_paths(paths, progress: Progress::SILENT) ⇒ Array<Finding>
-
#initialize(root:, stubs: [], rules: nil) ⇒ GemCalls
constructor
A new instance of GemCalls.
Constructor Details
#initialize(root:, stubs: [], rules: nil) ⇒ GemCalls
Returns a new instance of GemCalls.
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>
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 |