audition 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1770 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Audition
6
+ module Static
7
+ # Call sites into bundled gems whose compiled extensions do
8
+ # not declare Ractor safety (rb_ext_ractor_safe), so calling
9
+ # into them from a non-main Ractor raises Ractor::UnsafeError.
10
+ # The extension's code is outside the scanned tree; the call
11
+ # site is the only place a static pass can flag.
12
+ #
13
+ # Rules are derived from the target's own bundle, never from a
14
+ # gem list: a pinned gem whose installed extension lacks the
15
+ # declaration—or that pins a platform-specific build Audition
16
+ # cannot inspect (the running Ruby's own copy of the extension
17
+ # settles it when one is shipped)—gets its call sites
18
+ # flagged, anchored to the
19
+ # gem's own namespace (read from its entry file's module
20
+ # nesting, or the require-path convention when the gem is not
21
+ # installed). Matched calls taint: the value handed back is
22
+ # presumed to still live in the extension, so calls chained
23
+ # onto it, or on the local or ivar it is assigned to, are
24
+ # flagged and keep the taint moving; a predicate ends the
25
+ # chain, and freeze, tap, itself, and class pass it along
26
+ # silently. Sorbet
27
+ # annotations extend the reach—a sig param, T.let, or T.cast
28
+ # typed with a constant under a flagged gem's namespace taints
29
+ # the annotated variable the same way, and a plain type clears
30
+ # the guess. A per-class pre-pass makes the body order-free:
31
+ # a method whose return expression or sig return type is
32
+ # rooted in a flagged namespace hands the taint to callers on
33
+ # self, self.class, or the class's own constant, and an ivar
34
+ # assigned such a value anywhere in the body—directly or
35
+ # through its attr writer—is tainted throughout. A pass over
36
+ # the whole tree first promotes classes that hold extension
37
+ # values in their instances—or subclass one that does—to
38
+ # rules of their own, so constructing or receiving one in
39
+ # another file carries the taint across files.
40
+ class GemCalls
41
+ UNSAFE_WHY =
42
+ "%{gem}'s compiled extension does not declare Ractor " \
43
+ "safety (rb_ext_ractor_safe), so its methods raise " \
44
+ "Ractor::UnsafeError (\"ractor unsafe method called from " \
45
+ "not main ractor\") from any non-main Ractor."
46
+
47
+ UNVERIFIED_WHY =
48
+ "%{gem} pins a compiled extension Audition could not " \
49
+ "inspect (the gem is not installed here). An extension " \
50
+ "that does not declare Ractor safety " \
51
+ "(rb_ext_ractor_safe) raises Ractor::UnsafeError from " \
52
+ "any non-main Ractor; install the bundle to verify."
53
+
54
+ MESSAGE =
55
+ "%{receiver}.%{method} calls into %{gem}, whose compiled " \
56
+ "extension does not declare Ractor safety"
57
+
58
+ UNVERIFIED_MESSAGE =
59
+ "%{receiver}.%{method} calls into %{gem}, which pins a " \
60
+ "compiled extension Audition could not inspect"
61
+
62
+ DERIVED_MESSAGE =
63
+ "%{method} called on a value handed out by %{gem}'s " \
64
+ "native extension"
65
+
66
+ FIX =
67
+ "Keep calls into %{gem} on the main Ractor and share " \
68
+ "only extracted plain data (frozen strings, numbers) " \
69
+ "between Ractors, or get the extension to declare " \
70
+ "rb_ext_ractor_safe(true)."
71
+
72
+ # Object/Kernel identity methods that never enter the
73
+ # extension, even on a gem object.
74
+ CORE_METHODS = Ractor.make_shareable(
75
+ Set.new(
76
+ %i[nil? is_a? kind_of? instance_of? respond_to? frozen?
77
+ equal? class object_id itself hash tap then freeze]
78
+ )
79
+ )
80
+
81
+ # Core methods that hand back the receiver—or, for class,
82
+ # the extension's own class object: no finding, but a
83
+ # tainted receiver's taint passes through.
84
+ CHAIN_METHODS = Ractor.make_shareable(
85
+ Set.new(%i[class itself tap freeze])
86
+ )
87
+
88
+ CHECK = "native-gem-calls"
89
+
90
+ EMPTY_NESTING = Ractor.make_shareable([])
91
+
92
+ # Predicates whose constant argument proves the receiver's
93
+ # class when they gate a branch.
94
+ TYPE_CHECKS = Ractor.make_shareable(
95
+ Set.new(%i[is_a? kind_of? instance_of?])
96
+ )
97
+
98
+ EVAL_REOPENINGS = Ractor.make_shareable(
99
+ Set.new(%i[class_eval module_eval])
100
+ )
101
+
102
+ # Methods that hand the receiver itself to their block.
103
+ YIELD_SELF = Ractor.make_shareable(
104
+ Set.new(%i[then yield_self])
105
+ )
106
+
107
+ EMPTY_SET = Ractor.make_shareable(Set.new)
108
+
109
+ # What a stub has to show before its gem counts as compiled:
110
+ # one source location holding this share of the gem's located
111
+ # methods, across at least this many classes.
112
+ STUB_SHARE = 0.6
113
+ STUB_OWNERS = 2
114
+ STUB_SOURCE = %r{# source://(\S+)}
115
+ STUB_DEF = /\A\s*def [\w\[\]<>=!+\-*\/%~^&|?]/
116
+ STUB_SCOPE = /\A\s*(?:class|module)\s+([A-Za-z0-9_:]+)/
117
+
118
+ # Everything an object answers before any extension gets
119
+ # involved: a bare call to anything else inside a reopened
120
+ # bound class lands in the extension.
121
+ RUBY_METHODS = Ractor.make_shareable(
122
+ Set.new(
123
+ [Object, Kernel, Module, Class].flat_map do |mod|
124
+ mod.instance_methods + mod.private_instance_methods
125
+ end
126
+ )
127
+ )
128
+
129
+ # Core iterators hand elements to their block; these
130
+ # positions carry the memo or index instead.
131
+ BLOCK_MEMO_POSITIONS = Ractor.make_shareable(
132
+ {each_with_object: 1, with_object: 1, each_with_index: 1,
133
+ with_index: 1, inject: 0, reduce: 0}
134
+ )
135
+
136
+ # One flagged gem: verified means its installed extension
137
+ # binary was read and lacks the declaration; unverified
138
+ # means the lockfile proves compiled code exists but no
139
+ # binary was available to read. A bound rule's namespace IS
140
+ # the extension's own class or module—the gem namespace,
141
+ # a constant assigned an extension-rooted value, or a
142
+ # subclass of either—so reopening it defines methods on
143
+ # extension instances; an unbound rule marks app code that
144
+ # merely holds extension values.
145
+ Rule = Data.define(:gem, :namespace, :verified, :bound) do
146
+ def initialize(gem:, namespace:, verified:, bound: false)
147
+ super
148
+ end
149
+ end
150
+
151
+ # @param root [String] target root, where Gemfile.lock lives
152
+ # @param stubs [Array<String>] the target's .rbi stubs
153
+ # @param rules [Array<Rule>, nil] override bundle resolution
154
+ def initialize(root:, stubs: [], rules: nil)
155
+ @stubs = stubs
156
+ @rules = rules || resolve(root)
157
+ @nesting = EMPTY_NESTING
158
+ @self_rule = nil
159
+ @self_defs = EMPTY_SET
160
+ @param_seeds = {}
161
+ @return_taints = {}
162
+ reset_seed_ledger
163
+ index_rules
164
+ end
165
+
166
+ # @param paths [Array<String>] files to scan
167
+ # @param progress [Progress] narrates the
168
+ # three passes this phase makes over the tree
169
+ # @return [Array<Finding>]
170
+ def analyze_paths(paths, progress: Progress::SILENT)
171
+ return [] if @rules.empty?
172
+
173
+ progress.stage("subclasses", total: paths.size)
174
+ @rules += derived_class_rules(paths, progress)
175
+ index_rules
176
+ @param_seeds = {}
177
+ reset_seed_ledger
178
+ findings = {}
179
+ progress.stage("scanning", total: paths.size)
180
+ paths.each do |path|
181
+ progress.tick
182
+ source = read_source(path)
183
+ findings[path] = analyze_file(path, source) if source
184
+ end
185
+ settle_seeds(findings, progress)
186
+ findings.values.flatten(1)
187
+ end
188
+
189
+ private
190
+
191
+ # A call in one file seeds a parameter of a method defined
192
+ # in another, so a file walked before the call was seen has
193
+ # to walk again. Rounds are capped: each one costs a walk of
194
+ # every file the round before it taught something new.
195
+ SEED_ROUNDS = 3
196
+
197
+ def reset_seed_ledger
198
+ @seed_clock = 0
199
+ @seed_at = {}
200
+ @def_keys = {}
201
+ @walked_at = {}
202
+ end
203
+
204
+ def settle_seeds(findings, progress = Progress::SILENT)
205
+ SEED_ROUNDS.times do |round|
206
+ stale = findings.keys.select { |path| stale?(path) }
207
+ break if stale.empty?
208
+
209
+ progress.stage("settling #{round + 1}", total: stale.size)
210
+ stale.each do |path|
211
+ progress.tick
212
+ source = read_source(path)
213
+ findings[path] = analyze_file(path, source) if source
214
+ end
215
+ end
216
+ end
217
+
218
+ def stale?(path)
219
+ walked = @walked_at[path]
220
+ return false unless walked
221
+
222
+ @def_keys[path].any? do |key|
223
+ (@seed_at[key] || -1) > walked
224
+ end
225
+ end
226
+
227
+ def read_source(path)
228
+ File.read(path)
229
+ rescue SystemCallError
230
+ nil
231
+ end
232
+
233
+ def resolve(root)
234
+ lockfile = File.join(root, "Gemfile.lock")
235
+ return [] unless File.file?(lockfile)
236
+
237
+ pinned(lockfile).flat_map do |name, version, platform|
238
+ rules_for(name, version, platform)
239
+ end
240
+ end
241
+
242
+ # Lockfile rows collapse platform variants into one gem with
243
+ # a platform mark: a platform suffix on any variant proves
244
+ # the gem ships compiled code.
245
+ def pinned(lockfile)
246
+ rows = {}
247
+ File.foreach(lockfile) do |line|
248
+ match = line.match(/\A ([A-Za-z0-9_-]+) \(([^)\s]+)\)/)
249
+ next unless match
250
+
251
+ version, platform =
252
+ match[2].match(/\A([0-9][\w.]*?)(?:-(.+))?\z/)&.captures
253
+ next unless version
254
+
255
+ row = rows[match[1]] ||= [match[1], version, nil]
256
+ row[2] ||= platform
257
+ end
258
+ rows.values
259
+ end
260
+
261
+ # An installed copy is the best evidence. Without one, the
262
+ # generated type stubs in the target's own tree name the
263
+ # classes whose methods are not Ruby-defined, and a
264
+ # platform pin proves compiled code ships under the gem's
265
+ # own name: both hold, so both are flagged.
266
+ def rules_for(name, version, platform)
267
+ spec = installed_spec(name, version)
268
+ return Array(installed_rule(name, spec)) if spec
269
+
270
+ namespaces = stub_namespaces(name, version)
271
+ namespaces += [convention_namespace(name)] if platform
272
+ namespaces.uniq.filter_map { |ns| unverified_rule(name, ns) }
273
+ end
274
+
275
+ def installed_rule(name, spec)
276
+ compiled = Target.compiled_for(spec)
277
+ if compiled.any?
278
+ return nil if compiled.all? { |path| declares_safety?(path) }
279
+
280
+ Rule.new(gem: name, namespace: namespace_for(name, spec),
281
+ verified: true, bound: true)
282
+ elsif spec.extensions.any?
283
+ unverified_rule(name, namespace_for(name, spec))
284
+ end
285
+ end
286
+
287
+ # A pin with no readable binary—a platform build the
288
+ # bundle did not install, or a default gem whose extension
289
+ # lives outside its require paths—can still be judged when
290
+ # the running Ruby ships the same extension: Ruby's own
291
+ # binary is the evidence.
292
+ def unverified_rule(name, namespace)
293
+ shipped = ruby_shipped_extension(name)
294
+ unless shipped
295
+ return Rule.new(gem: name, namespace: namespace,
296
+ verified: false, bound: true)
297
+ end
298
+ return nil if declares_safety?(shipped)
299
+
300
+ Rule.new(gem: name, namespace: namespace, verified: true,
301
+ bound: true)
302
+ end
303
+
304
+ def ruby_shipped_extension(name)
305
+ base = File.join(RbConfig::CONFIG["archdir"],
306
+ name.tr("-", "/"))
307
+ Dir["#{base}.{so,bundle}"].first
308
+ end
309
+
310
+ # Bundler hides gems outside the current bundle from
311
+ # find_by_name, so fall back to reading gemspecs straight
312
+ # from the installed specification directories.
313
+ def installed_spec(name, version)
314
+ Gem::Specification.find_by_name(name, version)
315
+ rescue Gem::LoadError
316
+ Gem.path.each do |base|
317
+ pattern = File.join(base, "specifications",
318
+ "#{name}-#{version}{,-*}.gemspec")
319
+ Dir[pattern].each do |path|
320
+ spec = Gem::Specification.load(path)
321
+ return spec if spec&.name == name
322
+ end
323
+ end
324
+ nil
325
+ end
326
+
327
+ def declares_safety?(path)
328
+ File.binread(path).include?(NativeExtensions::SYMBOL)
329
+ rescue SystemCallError
330
+ false
331
+ end
332
+
333
+ # A generated type stub is the target's own record of what a
334
+ # gem's methods look like. The generator writes each method's
335
+ # Ruby source location, and methods a compiled extension
336
+ # defines all land on the one line that loads it: a single
337
+ # location holding most of a gem's methods, spread over
338
+ # several classes, is that shape. A metaprogramming block
339
+ # defines its methods on the class it sits in, so one owner
340
+ # is not evidence. The owners of that cluster are the
341
+ # extension's own classes, which name it better than any
342
+ # convention can.
343
+ # Generators name a gem's stub for the gem and version it
344
+ # documents, which identifies it wherever the generator was
345
+ # told to write.
346
+ def stub_namespaces(name, version)
347
+ wanted = "#{name}@#{version}.rbi"
348
+ path = @stubs.find { |s| File.basename(s) == wanted }
349
+ return [] unless path
350
+
351
+ clusters = stub_clusters(path)
352
+ located = clusters.sum { |_, owners| owners.values.sum }
353
+ return [] if located.zero?
354
+
355
+ owners = clusters.each_value.max_by { |o| o.values.sum }
356
+ return [] if owners.size < STUB_OWNERS ||
357
+ owners.values.sum < located * STUB_SHARE
358
+
359
+ owners.keys
360
+ end
361
+
362
+ def stub_clusters(path)
363
+ clusters = Hash.new { |h, k| h[k] = Hash.new(0) }
364
+ scope = []
365
+ indents = []
366
+ source = nil
367
+ File.foreach(path) do |raw|
368
+ line = raw.rstrip
369
+ next if line.empty?
370
+
371
+ indent = line[/\A */].size
372
+ while indents.any? && indent <= indents.last
373
+ indents.pop
374
+ scope.pop
375
+ end
376
+ if (match = line.match(STUB_SCOPE))
377
+ scope.push(stub_scope_name(scope, match[1]))
378
+ indents.push(indent)
379
+ source = nil
380
+ elsif (match = line.match(STUB_SOURCE))
381
+ source = match[1]
382
+ else
383
+ if source && scope.last && line.match?(STUB_DEF)
384
+ clusters[source][scope.last] += 1
385
+ end
386
+ source = nil
387
+ end
388
+ end
389
+ clusters
390
+ rescue SystemCallError
391
+ {}
392
+ end
393
+
394
+ # Stubs write top-level definitions under their full path and
395
+ # nested ones relative to the enclosing scope.
396
+ def stub_scope_name(scope, written)
397
+ scope.empty? ? written : "#{scope.last}::#{written}"
398
+ end
399
+
400
+ # The entry file's module nesting names the gem's namespace
401
+ # more reliably than name conventions do.
402
+ def namespace_for(name, spec)
403
+ entry = spec.full_require_paths
404
+ .map { |rp| File.join(rp, "#{name.tr("-", "/")}.rb") }
405
+ .find { |path| File.file?(path) }
406
+ (entry && nesting_namespace(entry)) ||
407
+ convention_namespace(name)
408
+ end
409
+
410
+ def nesting_namespace(path)
411
+ result = Prism.parse(File.read(path))
412
+ return nil unless result.success?
413
+
414
+ names = []
415
+ body = result.value.statements.body
416
+ while (mod = sole_module(body))
417
+ names << mod.constant_path.location.slice
418
+ .delete_prefix("::")
419
+ body =
420
+ mod.body.is_a?(Prism::StatementsNode) ? mod.body.body : []
421
+ end
422
+ names.join("::") unless names.empty?
423
+ rescue SystemCallError
424
+ nil
425
+ end
426
+
427
+ def sole_module(statements)
428
+ mods = statements.select do |node|
429
+ node.is_a?(Prism::ModuleNode) || node.is_a?(Prism::ClassNode)
430
+ end
431
+ mods.first if mods.size == 1
432
+ end
433
+
434
+ def convention_namespace(name)
435
+ name.split("-").map do |seg|
436
+ seg.split("_").map(&:capitalize).join
437
+ end.join("::")
438
+ end
439
+
440
+ # Each pass may learn argument seeds and computed return
441
+ # taints that change what an earlier line would flag, so
442
+ # the file re-walks until a pass learns nothing new and
443
+ # only that pass's findings stand.
444
+ def analyze_file(path, source)
445
+ file = SourceFile.new(source: source, path: path)
446
+ return [] unless file.valid_syntax?
447
+
448
+ @path = path
449
+
450
+ @return_taints = {}
451
+ @def_keys[path] = Set.new
452
+ scan = nil
453
+ loop do
454
+ before = knowledge_size
455
+ @registry = nil
456
+ @singleton = false
457
+ @nesting = EMPTY_NESTING
458
+ @self_rule = nil
459
+ @self_defs = EMPTY_SET
460
+ scan = Scan.new(path: path, findings: [], producers: {})
461
+ walk(file.root, {}, {}, scan)
462
+ break if knowledge_size == before
463
+ end
464
+ @walked_at[path] = @seed_clock
465
+ scan.findings
466
+ end
467
+
468
+ # Seeds and return taints only grow, so the loop stops at
469
+ # the first pass that learns nothing.
470
+ def knowledge_size
471
+ @param_seeds.sum { |_, seeds| seeds.size } +
472
+ @return_taints.size
473
+ end
474
+
475
+ Scan = Data.define(:path, :findings, :producers)
476
+
477
+ # Ordered walk carrying the taint state: tainted maps a local
478
+ # name to the rule whose call produced its value, ivars does
479
+ # the same for instance variables; producers remembers which
480
+ # call nodes are rooted in a flagged namespace so a chained
481
+ # call or an assignment can pick the taint up. Defs open
482
+ # fresh local scopes (seeded from the preceding sig); classes
483
+ # and modules open fresh ivar scopes; blocks close over the
484
+ # enclosing.
485
+ def walk(node, tainted, ivars, scan)
486
+ case node
487
+ when Prism::ClassNode, Prism::ModuleNode
488
+ registry, singleton, nesting = @registry, @singleton, @nesting
489
+ self_rule, self_defs = @self_rule, @self_defs
490
+ bound = source_rule(node.constant_path.location.slice)
491
+ @self_rule = bound&.bound ? bound : nil
492
+ @self_defs = @self_rule ? body_defs(node.body) : EMPTY_SET
493
+ @nesting = nested_scope(node)
494
+ @registry, @singleton = build_registry(node), false
495
+ each_child(node) do |child|
496
+ walk(child, {}, @registry.ivars[:instance].dup, scan)
497
+ end
498
+ @registry, @singleton, @nesting = registry, singleton, nesting
499
+ @self_rule, @self_defs = self_rule, self_defs
500
+ when Prism::SingletonClassNode
501
+ if @registry && node.expression.is_a?(Prism::SelfNode)
502
+ singleton = @singleton
503
+ @singleton = true
504
+ each_child(node) do |child|
505
+ walk(child, {}, @registry.ivars[:singleton].dup, scan)
506
+ end
507
+ @singleton = singleton
508
+ else
509
+ registry = @registry
510
+ @registry = nil
511
+ each_child(node) { |child| walk(child, {}, {}, scan) }
512
+ @registry = registry
513
+ end
514
+ when Prism::DefNode
515
+ walk_def(node, nil, ivars, scan)
516
+ when Prism::StatementsNode
517
+ sig = nil
518
+ node.body.each do |child|
519
+ if child.is_a?(Prism::DefNode)
520
+ walk_def(child, sig, ivars, scan)
521
+ else
522
+ walk(child, tainted, ivars, scan)
523
+ guard_taint(child, tainted, ivars)
524
+ end
525
+ sig = sig_node?(child) ? child : nil
526
+ end
527
+ when Prism::CaseNode
528
+ walk_case(node, tainted, ivars, scan)
529
+ when Prism::IfNode
530
+ walk_if(node, tainted, ivars, scan)
531
+ when Prism::CallNode
532
+ if (rule = reopening_rule(node))
533
+ walk_reopening(node, rule, tainted, ivars, scan)
534
+ else
535
+ walk_call(node, tainted, ivars, scan)
536
+ end
537
+ when Prism::LocalVariableWriteNode,
538
+ Prism::LocalVariableOrWriteNode,
539
+ Prism::LocalVariableAndWriteNode,
540
+ Prism::LocalVariableOperatorWriteNode
541
+ walk(node.value, tainted, ivars, scan)
542
+ rule = value_rule(node.value, tainted, ivars, scan)
543
+ rule ? tainted[node.name] = rule : tainted.delete(node.name)
544
+ when Prism::InstanceVariableWriteNode,
545
+ Prism::InstanceVariableOrWriteNode,
546
+ Prism::InstanceVariableAndWriteNode,
547
+ Prism::InstanceVariableOperatorWriteNode
548
+ walk(node.value, tainted, ivars, scan)
549
+ rule = value_rule(node.value, tainted, ivars, scan)
550
+ rule ? ivars[node.name] = rule : ivars.delete(node.name)
551
+ when Prism::MultiWriteNode
552
+ each_child(node) { |child| walk(child, tainted, ivars, scan) }
553
+ multi_targets(node).each do |target|
554
+ case target
555
+ when Prism::LocalVariableTargetNode
556
+ tainted.delete(target.name)
557
+ when Prism::InstanceVariableTargetNode
558
+ ivars.delete(target.name)
559
+ end
560
+ end
561
+ else
562
+ each_child(node) { |child| walk(child, tainted, ivars, scan) }
563
+ end
564
+ end
565
+
566
+ # Branches run on their own copy: a value one branch
567
+ # assigns may still be live after the branch, and one
568
+ # every branch replaces is not.
569
+ def walk_if(node, tainted, ivars, scan)
570
+ walk(node.predicate, tainted, ivars, scan)
571
+ key, rule = type_check(node.predicate)
572
+ branches = []
573
+ if node.statements
574
+ copies = [tainted.dup, ivars.dup]
575
+ with_taint(key, rule, *copies) do
576
+ walk(node.statements, *copies, scan)
577
+ end
578
+ branches << copies
579
+ end
580
+ if node.subsequent
581
+ copies = [tainted.dup, ivars.dup]
582
+ walk(node.subsequent, *copies, scan)
583
+ branches << copies
584
+ end
585
+ merge_branches(tainted, ivars, branches,
586
+ node.statements && node.subsequent)
587
+ end
588
+
589
+ def merge_branches(tainted, ivars, branches, total)
590
+ branches.each do |branch_tainted, branch_ivars|
591
+ branch_tainted.each { |k, v| tainted[k] ||= v }
592
+ branch_ivars.each { |k, v| ivars[k] ||= v }
593
+ end
594
+ return unless total
595
+
596
+ tainted.delete_if do |k, _|
597
+ branches.none? { |t, _| t.key?(k) }
598
+ end
599
+ ivars.delete_if do |k, _|
600
+ branches.none? { |_, i| i.key?(k) }
601
+ end
602
+ end
603
+
604
+ def walk_def(node, sig, ivars, scan)
605
+ singleton_def = node.receiver.is_a?(Prism::SelfNode)
606
+ side = (@singleton || singleton_def) ? :singleton : :instance
607
+ tainted = param_taints(node, sig, side)
608
+ if singleton_def && !@singleton
609
+ singleton = @singleton
610
+ @singleton = true
611
+ ivars = @registry ? @registry.ivars[:singleton].dup : {}
612
+ each_child(node) { |child| walk(child, tainted, ivars, scan) }
613
+ @singleton = singleton
614
+ else
615
+ each_child(node) { |child| walk(child, tainted, ivars, scan) }
616
+ end
617
+ note_return_taint(node, side, scan)
618
+ end
619
+
620
+ # Sig taints seed the def's scope, then call sites already
621
+ # walked add argument taints for params the sig leaves
622
+ # untyped or erased; a plain constant type keeps the param
623
+ # clean.
624
+ def param_taints(node, sig, side)
625
+ taints = sig ? sig_taints(sig) : {}
626
+ seeds = seeds_for(side, node.name)
627
+ return taints unless seeds
628
+
629
+ plain = sig ? plain_params(sig) : EMPTY_SET
630
+ seeds.each do |key, rule|
631
+ name = seed_param_name(node.parameters, key)
632
+ next if name.nil? || taints.key?(name) ||
633
+ plain.include?(name)
634
+
635
+ taints[name] = rule
636
+ end
637
+ taints
638
+ end
639
+
640
+ # A call through the module's own name reaches an instance
641
+ # method the module extended itself with.
642
+ def seeds_for(side, name)
643
+ key = [seed_scope, side, name]
644
+ note_def_key(key)
645
+ seeds = @param_seeds[key]
646
+ return seeds if seeds || side != :instance ||
647
+ !@registry&.self_extended
648
+
649
+ fallback = [seed_scope, :singleton, name]
650
+ note_def_key(fallback)
651
+ @param_seeds[fallback]
652
+ end
653
+
654
+ # What this file's definitions would consume, so a seed
655
+ # learned later can call the file back.
656
+ def note_def_key(key)
657
+ @def_keys[@path]&.add(key)
658
+ end
659
+
660
+ def seed_param_name(params, key)
661
+ return nil unless params
662
+
663
+ if key.is_a?(Integer)
664
+ requireds = params.requireds
665
+ param = if key < requireds.size
666
+ requireds[key]
667
+ else
668
+ params.optionals[key - requireds.size]
669
+ end
670
+ case param
671
+ when Prism::RequiredParameterNode,
672
+ Prism::OptionalParameterNode
673
+ param.name
674
+ end
675
+ else
676
+ params.keywords.find do |kw|
677
+ kw.respond_to?(:name) && kw.name == key
678
+ end&.name
679
+ end
680
+ end
681
+
682
+ # A def whose final expression produces taint hands it to
683
+ # callers even when its sig says nothing; predicates stay
684
+ # plain. Learned during one pass, applied on the next.
685
+ def note_return_taint(node, side, scan)
686
+ return if node.name.end_with?("?")
687
+
688
+ expr = def_return_expr(node)
689
+ rule = expr && scan.producers[expr.object_id]
690
+ @return_taints[[seed_scope, side, node.name]] = rule if rule
691
+ end
692
+
693
+ def seed_scope
694
+ @registry&.class_name
695
+ end
696
+
697
+ # A bare or self call names a method in the scope being
698
+ # walked. A constant receiver names the scope itself, by its
699
+ # last segment: a call site and a definition rarely spell
700
+ # the path the same way.
701
+ def seed_key(node)
702
+ case node.receiver
703
+ when nil, Prism::SelfNode
704
+ [seed_scope, @singleton ? :singleton : :instance,
705
+ node.name]
706
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
707
+ name = node.receiver.location.slice
708
+ .delete_prefix("::").split("::").last
709
+ [name, :singleton, node.name]
710
+ end
711
+ end
712
+
713
+ # The block walks after the call resolves: a flagged call's
714
+ # block iterates values living in the same extension, so its
715
+ # element parameters carry the taint.
716
+ def walk_call(node, tainted, ivars, scan)
717
+ block = node.block
718
+ each_child(node) do |child|
719
+ walk(child, tainted, ivars, scan) unless child.equal?(block)
720
+ end
721
+ rule = visit_call(node, tainted, ivars, scan) ||
722
+ yielded_self_rule(node, tainted, ivars, scan)
723
+ note_argument_taints(node, tainted, ivars, scan)
724
+ return unless block
725
+
726
+ if rule && block.is_a?(Prism::BlockNode)
727
+ walk_block(node, rule, tainted, ivars, scan)
728
+ else
729
+ walk(block, tainted, ivars, scan)
730
+ end
731
+ end
732
+
733
+ # A tainted argument seeds the named method's parameter for
734
+ # the passes that follow.
735
+ def note_argument_taints(node, tainted, ivars, scan)
736
+ key = seed_key(node)
737
+ return unless key
738
+
739
+ args = node.arguments&.arguments
740
+ return unless args
741
+
742
+ args.each_with_index do |arg, i|
743
+ if arg.is_a?(Prism::KeywordHashNode)
744
+ arg.elements.each do |assoc|
745
+ next unless assoc.is_a?(Prism::AssocNode) &&
746
+ assoc.key.is_a?(Prism::SymbolNode)
747
+
748
+ note_seed(key, assoc.key.unescaped.to_sym,
749
+ value_rule(assoc.value, tainted, ivars, scan))
750
+ end
751
+ else
752
+ note_seed(key, i,
753
+ value_rule(arg, tainted, ivars, scan))
754
+ end
755
+ end
756
+ end
757
+
758
+ def note_seed(key, param, rule)
759
+ return unless rule
760
+
761
+ seeds = (@param_seeds[key] ||= {})
762
+ fresh = !seeds.key?(param)
763
+ seeds[param] = rule
764
+ @seed_at[key] = (@seed_clock += 1) if fresh
765
+ end
766
+
767
+ # then and yield_self pass the receiver straight in, so
768
+ # the block parameter stands for the receiver.
769
+ def yielded_self_rule(node, tainted, ivars, scan)
770
+ return nil unless YIELD_SELF.include?(node.name)
771
+
772
+ taint_source(node, tainted, ivars, scan) ||
773
+ receiver_rule(node.receiver)
774
+ end
775
+
776
+ # What an expression hands over: a variable read carries
777
+ # whatever the variable holds, self carries the body's own
778
+ # binding, anything else carries what the walk recorded.
779
+ def value_rule(node, tainted, ivars, scan)
780
+ case node
781
+ when Prism::LocalVariableReadNode then tainted[node.name]
782
+ when Prism::InstanceVariableReadNode then ivars[node.name]
783
+ when Prism::SelfNode then @singleton ? nil : @self_rule
784
+ else scan.producers[node.object_id]
785
+ end
786
+ end
787
+
788
+ def walk_block(node, rule, tainted, ivars, scan)
789
+ names = element_params(node)
790
+ saved = names.map { |n| [n, tainted.key?(n), tainted[n]] }
791
+ names.each { |n| tainted[n] = rule }
792
+ each_child(node.block) do |child|
793
+ walk(child, tainted, ivars, scan)
794
+ end
795
+ saved.each do |name, had, prev|
796
+ had ? tainted[name] = prev : tainted.delete(name)
797
+ end
798
+ end
799
+
800
+ def element_params(node)
801
+ params = node.block.parameters
802
+ skip = BLOCK_MEMO_POSITIONS[node.name]
803
+ case params
804
+ when Prism::NumberedParametersNode
805
+ (1..params.maximum).filter_map do |i|
806
+ :"_#{i}" unless skip == i - 1
807
+ end
808
+ when Prism::BlockParametersNode
809
+ names = []
810
+ (params.parameters&.requireds || [])
811
+ .each_with_index do |param, i|
812
+ collect_param_names(param, names) unless skip == i
813
+ end
814
+ names
815
+ else
816
+ []
817
+ end
818
+ end
819
+
820
+ def collect_param_names(param, names)
821
+ case param
822
+ when Prism::RequiredParameterNode,
823
+ Prism::LocalVariableTargetNode
824
+ names << param.name
825
+ when Prism::MultiTargetNode
826
+ [*param.lefts, param.rest, *param.rights].compact
827
+ .each { |part| collect_param_names(part, names) }
828
+ end
829
+ end
830
+
831
+ def each_child(node, &block)
832
+ node.child_nodes.compact.each(&block)
833
+ end
834
+
835
+ def nested_scope(node)
836
+ slice = node.constant_path.location.slice
837
+ segments = slice.delete_prefix("::").split("::")
838
+ slice.start_with?("::") ? segments : [*@nesting, *segments]
839
+ end
840
+
841
+ def multi_targets(node)
842
+ [*node.lefts, node.rest, *node.rights].compact
843
+ end
844
+
845
+ # A when clause matching the subject against a flagged
846
+ # constant proves its type inside the branch; if every
847
+ # branch proves the same rule, the else clause sees that
848
+ # value too, and a branch handing the value back makes the
849
+ # whole case expression a producer.
850
+ def walk_case(node, tainted, ivars, scan)
851
+ walk(node.predicate, tainted, ivars, scan) if node.predicate
852
+ key = node.predicate && taint_key(node.predicate)
853
+ rules = node.conditions.map { |c| key ? when_rule(c) : nil }
854
+ produced = nil
855
+ node.conditions.each_with_index do |clause, i|
856
+ clause.conditions.each do |cond|
857
+ walk(cond, tainted, ivars, scan) unless constant_type?(cond)
858
+ end
859
+ next unless clause.statements
860
+
861
+ with_taint(key, rules[i], tainted, ivars) do
862
+ walk(clause.statements, tainted, ivars, scan)
863
+ produced ||= branch_rule(clause.statements, key, rules[i], scan)
864
+ end
865
+ end
866
+ if node.else_clause
867
+ shared = (rules.uniq.size == 1) ? rules.first : nil
868
+ with_taint(key, shared, tainted, ivars) do
869
+ walk(node.else_clause, tainted, ivars, scan)
870
+ produced ||=
871
+ branch_rule(node.else_clause.statements, key, shared, scan)
872
+ end
873
+ end
874
+ scan.producers[node.object_id] = produced if produced
875
+ end
876
+
877
+ # One rule only when every condition in the clause is a
878
+ # constant resolving to it: mixed or non-constant
879
+ # conditions are not a class match.
880
+ def when_rule(clause)
881
+ rules = clause.conditions.map do |cond|
882
+ constant_type?(cond) ? source_rule(cond.location.slice) : nil
883
+ end
884
+ (rules.uniq.size == 1) ? rules.first : nil
885
+ end
886
+
887
+ # The value a branch hands back, seen through trailing
888
+ # modifier conditionals.
889
+ def branch_rule(statements, key, rule, scan)
890
+ tail = statements&.body&.last
891
+ while tail.is_a?(Prism::IfNode) || tail.is_a?(Prism::UnlessNode)
892
+ tail = tail.statements&.body&.last
893
+ end
894
+ return nil unless tail
895
+
896
+ scan.producers[tail.object_id] ||
897
+ (rule if key && taint_key(tail) == key)
898
+ end
899
+
900
+ # Reopening a bound name through class_eval defines methods
901
+ # on the extension's own class; the eval call itself never
902
+ # enters the extension.
903
+ def reopening_rule(node)
904
+ return nil unless EVAL_REOPENINGS.include?(node.name) &&
905
+ node.block.is_a?(Prism::BlockNode)
906
+
907
+ rule = receiver_rule(node.receiver)
908
+ rule&.bound ? rule : nil
909
+ end
910
+
911
+ def walk_reopening(node, rule, tainted, ivars, scan)
912
+ self_rule, self_defs = @self_rule, @self_defs
913
+ @self_rule = rule
914
+ @self_defs = body_defs(node.block.body)
915
+ each_child(node) { |child| walk(child, tainted, ivars, scan) }
916
+ @self_rule, @self_defs = self_rule, self_defs
917
+ end
918
+
919
+ # Names the reopened body itself defines: self-calls on
920
+ # these stay ordinary Ruby.
921
+ def body_defs(body)
922
+ defs = Set.new
923
+ statements =
924
+ body.is_a?(Prism::StatementsNode) ? body.body : []
925
+ statements.each do |stmt|
926
+ case stmt
927
+ when Prism::DefNode
928
+ defs << stmt.name
929
+ when Prism::CallNode
930
+ roles = ATTR_ROLES[stmt.name]
931
+ next unless roles && stmt.receiver.nil?
932
+
933
+ args = stmt.arguments&.arguments || []
934
+ args.grep(Prism::SymbolNode).each do |sym|
935
+ defs << sym.unescaped.to_sym
936
+ if roles.include?(:writer)
937
+ defs << :"#{sym.unescaped}="
938
+ end
939
+ end
940
+ end
941
+ end
942
+ defs
943
+ end
944
+
945
+ # The store slot a subject narrows to: a local or an
946
+ # instance variable; anything else cannot hold taint.
947
+ def taint_key(node)
948
+ case node
949
+ when Prism::LocalVariableReadNode then [:local, node.name]
950
+ when Prism::InstanceVariableReadNode then [:ivar, node.name]
951
+ end
952
+ end
953
+
954
+ def with_taint(key, rule, tainted, ivars)
955
+ return yield unless key && rule
956
+
957
+ store = (key[0] == :local) ? tainted : ivars
958
+ name = key[1]
959
+ had, prev = store.key?(name), store[name]
960
+ store[name] = rule
961
+ yield
962
+ had ? store[name] = prev : store.delete(name)
963
+ end
964
+
965
+ # A guard that bails unless the variable is one of a
966
+ # flagged gem's classes proves its type for whatever
967
+ # follows in the surrounding sequence.
968
+ def guard_taint(child, tainted, ivars)
969
+ return unless child.is_a?(Prism::UnlessNode) &&
970
+ terminates?(child.statements)
971
+
972
+ key, rule = type_check(child.predicate)
973
+ return unless key && rule
974
+
975
+ store = (key[0] == :local) ? tainted : ivars
976
+ store[key[1]] = rule
977
+ end
978
+
979
+ def terminates?(statements)
980
+ last = statements&.body&.last
981
+ last.is_a?(Prism::ReturnNode) || last.is_a?(Prism::NextNode) ||
982
+ last.is_a?(Prism::BreakNode) ||
983
+ (last.is_a?(Prism::CallNode) && last.name == :raise)
984
+ end
985
+
986
+ # x.is_a?(Some::Const) as a type proof for x, through the
987
+ # left side of a && chain.
988
+ def type_check(predicate)
989
+ node = predicate
990
+ node = node.left while node.is_a?(Prism::AndNode)
991
+ return unless node.is_a?(Prism::CallNode) &&
992
+ TYPE_CHECKS.include?(node.name) && node.receiver
993
+
994
+ key = taint_key(node.receiver)
995
+ return unless key
996
+
997
+ args = node.arguments&.arguments
998
+ return unless args&.size == 1 && constant_type?(args.first)
999
+
1000
+ rule = source_rule(args.first.location.slice)
1001
+ [key, rule] if rule
1002
+ end
1003
+
1004
+ # Classes whose instances hold extension values—an ivar or
1005
+ # method rooted in a flagged namespace—become rules under
1006
+ # their own qualified name: constructing one, or receiving
1007
+ # one through a sig or annotation, taints in any file. A
1008
+ # subclass of a promoted or flagged class is promoted too,
1009
+ # and so is a constant assigned a value rooted in a flagged
1010
+ # namespace, the way protobuf-style generated Ruby binds
1011
+ # extension classes to its own names.
1012
+ def derived_class_rules(paths, progress = Progress::SILENT)
1013
+ found = {}
1014
+ mixins = []
1015
+ @nesting = EMPTY_NESTING
1016
+ paths.each do |path|
1017
+ progress.tick
1018
+ source = read_source(path)
1019
+ next unless source
1020
+
1021
+ result = Prism.parse(source)
1022
+ next unless result.success?
1023
+
1024
+ collect_classes(result.value, [], found)
1025
+ collect_mixins(result.value, [], mixins)
1026
+ end
1027
+ settle_subclasses(found)
1028
+ settle_mixins(found, mixins)
1029
+ found.values.filter_map { |info| info[:rule] }
1030
+ end
1031
+
1032
+ # A mixin's instance methods run with the host as self:
1033
+ # include and prepend bind self to a host instance, extend
1034
+ # to the host itself. Either way, a module mixed into a
1035
+ # name bound to an extension value hands that value out
1036
+ # through self.
1037
+ MIXINS = Ractor.make_shareable(
1038
+ Set.new(%i[include prepend extend])
1039
+ )
1040
+
1041
+ def collect_mixins(node, nesting, out)
1042
+ case node
1043
+ when Prism::ClassNode, Prism::ModuleNode
1044
+ path = node.constant_path.location.slice
1045
+ .delete_prefix("::")
1046
+ return each_child(node) do |child|
1047
+ collect_mixins(child, [*nesting, path], out)
1048
+ end
1049
+ when Prism::CallNode
1050
+ note_mixin(node, nesting, out)
1051
+ end
1052
+ each_child(node) { |child| collect_mixins(child, nesting, out) }
1053
+ end
1054
+
1055
+ # A bare call mixes into the body it sits in; a receiver
1056
+ # names the host itself.
1057
+ def note_mixin(node, nesting, out)
1058
+ return unless MIXINS.include?(node.name)
1059
+
1060
+ host = case node.receiver
1061
+ when nil then nesting.last
1062
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
1063
+ node.receiver.location.slice.delete_prefix("::")
1064
+ end
1065
+ return unless host
1066
+
1067
+ Array(node.arguments&.arguments).each do |arg|
1068
+ next unless constant_type?(arg)
1069
+
1070
+ out << [host, arg.location.slice.delete_prefix("::"),
1071
+ nesting]
1072
+ end
1073
+ end
1074
+
1075
+ def settle_mixins(found, mixins)
1076
+ loop do
1077
+ changed = false
1078
+ mixins.each do |host, mixin, nesting|
1079
+ info = found[mixin] ||= {rule: nil, superclass: nil,
1080
+ nesting: nesting}
1081
+ next if info[:rule]
1082
+
1083
+ rule = resolve_name(host, nesting, found)
1084
+ next unless rule&.bound
1085
+
1086
+ info[:rule] = derived_rule(rule, mixin, bound: true)
1087
+ changed = true
1088
+ end
1089
+ break unless changed
1090
+ end
1091
+ end
1092
+
1093
+ # A reference resolves the way Ruby would: against each
1094
+ # level of the enclosing nesting, then as written.
1095
+ def resolve_name(name, nesting, found)
1096
+ nesting.size.downto(0) do |depth|
1097
+ candidate = [*nesting.first(depth), name].join("::")
1098
+ rule = namespace_rule(candidate) ||
1099
+ found.dig(candidate, :rule)
1100
+ return rule if rule
1101
+ end
1102
+ nil
1103
+ end
1104
+
1105
+ def collect_classes(node, nesting, found)
1106
+ case node
1107
+ when Prism::ClassNode, Prism::ModuleNode
1108
+ path = node.constant_path.location.slice
1109
+ .delete_prefix("::")
1110
+ note_class(node, nesting, path, found) if
1111
+ node.is_a?(Prism::ClassNode)
1112
+ return each_child(node) do |child|
1113
+ collect_classes(child, [*nesting, path], found)
1114
+ end
1115
+ when Prism::ConstantWriteNode, Prism::ConstantOrWriteNode,
1116
+ Prism::ConstantPathWriteNode,
1117
+ Prism::ConstantPathOrWriteNode
1118
+ note_constant(node, nesting, found)
1119
+ end
1120
+ each_child(node) { |child| collect_classes(child, nesting, found) }
1121
+ end
1122
+
1123
+ # A constant bound to an extension-rooted value is the value
1124
+ # under a new name; the name inherits the rule.
1125
+ def note_constant(node, nesting, found)
1126
+ path = if node.respond_to?(:target)
1127
+ node.target.location.slice.delete_prefix("::")
1128
+ else
1129
+ node.name.to_s
1130
+ end
1131
+ fqn = [*nesting, path].join("::")
1132
+ return if namespace_rule(fqn) || found.dig(fqn, :rule)
1133
+
1134
+ @nesting = nesting
1135
+ taint = prepass_rule(node.value, :instance, EMPTY_REGISTRY)
1136
+ return unless taint
1137
+
1138
+ found[fqn] = {rule: derived_rule(taint, fqn, bound: true),
1139
+ superclass: nil, nesting: nesting}
1140
+ end
1141
+
1142
+ def note_class(node, nesting, path, found)
1143
+ fqn = [*nesting, path].join("::")
1144
+ return if namespace_rule(fqn)
1145
+
1146
+ @nesting = [*nesting, path]
1147
+ registry = build_registry(node)
1148
+ taint = registry.ivars[:instance].values.first ||
1149
+ registry.methods[:instance].values.first
1150
+ superclass = node.superclass
1151
+ info = found[fqn] ||= {rule: nil, superclass: nil,
1152
+ nesting: nesting}
1153
+ info[:rule] ||= taint && derived_rule(taint, fqn)
1154
+ if info[:superclass].nil? && constant_type?(superclass)
1155
+ info[:superclass] =
1156
+ superclass.location.slice.delete_prefix("::")
1157
+ end
1158
+ end
1159
+
1160
+ def derived_rule(taint, fqn, bound: false)
1161
+ Rule.new(gem: taint.gem, namespace: fqn,
1162
+ verified: taint.verified, bound: bound)
1163
+ end
1164
+
1165
+ # Promotion follows inheritance to a fixpoint: the superclass
1166
+ # reference resolves the way Ruby would, against each level
1167
+ # of the subclass's own nesting, then as written.
1168
+ def settle_subclasses(found)
1169
+ loop do
1170
+ changed = false
1171
+ found.each do |fqn, info|
1172
+ next if info[:rule] || info[:superclass].nil?
1173
+
1174
+ parent = superclass_rule(info, found)
1175
+ next unless parent
1176
+
1177
+ info[:rule] = derived_rule(parent, fqn,
1178
+ bound: parent.bound)
1179
+ changed = true
1180
+ end
1181
+ break unless changed
1182
+ end
1183
+ end
1184
+
1185
+ def superclass_rule(info, found)
1186
+ resolve_name(info[:superclass], info[:nesting], found)
1187
+ end
1188
+
1189
+ # Pre-pass results for one class or module body: methods
1190
+ # whose return value is rooted in a flagged namespace and
1191
+ # ivars assigned such a value, split by definition side.
1192
+ Registry = Data.define(:class_name, :methods, :ivars,
1193
+ :self_extended)
1194
+
1195
+ EMPTY_REGISTRY = Ractor.make_shareable(
1196
+ Registry.new(class_name: "",
1197
+ methods: {instance: {}, singleton: {}},
1198
+ ivars: {instance: {}, singleton: {}},
1199
+ self_extended: false)
1200
+ )
1201
+
1202
+ Entry = Data.define(:side, :name, :return_expr, :ivar_writes,
1203
+ :writer_calls)
1204
+
1205
+ ATTR_ROLES = Ractor.make_shareable(
1206
+ {attr_accessor: %i[reader writer], attr_reader: %i[reader],
1207
+ attr_writer: %i[writer]}
1208
+ )
1209
+
1210
+ WRITER_NAME = /\A[a-z_]\w*=\z/
1211
+
1212
+ # Collects the body's defs, attr declarations, and ivar
1213
+ # writes, then settles taint to a fixpoint, so a factory
1214
+ # defined below its callers still taints them.
1215
+ def build_registry(node)
1216
+ registry = Registry.new(
1217
+ class_name: node.constant_path.location.slice
1218
+ .delete_prefix("::"),
1219
+ methods: {instance: {}, singleton: {}},
1220
+ ivars: {instance: {}, singleton: {}},
1221
+ self_extended: self_extended?(node.body)
1222
+ )
1223
+ entries = []
1224
+ attrs = {instance: {reader: [], writer: []},
1225
+ singleton: {reader: [], writer: []}}
1226
+ body = node.body
1227
+ if body.is_a?(Prism::StatementsNode)
1228
+ collect_entries(body.body, :instance, entries, attrs,
1229
+ registry)
1230
+ end
1231
+ settle(registry, entries, attrs)
1232
+ registry
1233
+ end
1234
+
1235
+ # extend self and a bare module_function make the body's
1236
+ # instance methods singleton methods too, so a call through
1237
+ # the module's own name reaches them.
1238
+ def self_extended?(body)
1239
+ return false unless body.is_a?(Prism::StatementsNode)
1240
+
1241
+ body.body.any? do |stmt|
1242
+ next false unless stmt.is_a?(Prism::CallNode) &&
1243
+ stmt.receiver.nil?
1244
+
1245
+ case stmt.name
1246
+ when :extend
1247
+ Array(stmt.arguments&.arguments)
1248
+ .any?(Prism::SelfNode)
1249
+ when :module_function then stmt.arguments.nil?
1250
+ end
1251
+ end
1252
+ end
1253
+
1254
+ def collect_entries(statements, side, entries, attrs, registry)
1255
+ sig = nil
1256
+ statements.each do |stmt|
1257
+ case stmt
1258
+ when Prism::DefNode
1259
+ def_side =
1260
+ stmt.receiver.is_a?(Prism::SelfNode) ? :singleton : side
1261
+ entries << def_entry(stmt, def_side)
1262
+ note_sig_return(sig, stmt, def_side, registry)
1263
+ when Prism::SingletonClassNode
1264
+ if stmt.expression.is_a?(Prism::SelfNode) &&
1265
+ stmt.body.is_a?(Prism::StatementsNode)
1266
+ collect_entries(stmt.body.body, :singleton, entries,
1267
+ attrs, registry)
1268
+ end
1269
+ when Prism::CallNode
1270
+ collect_attr(stmt, side, attrs)
1271
+ when Prism::InstanceVariableWriteNode,
1272
+ Prism::InstanceVariableOrWriteNode
1273
+ entries << Entry.new(side: :singleton, name: nil,
1274
+ return_expr: nil,
1275
+ ivar_writes: [[stmt.name, stmt.value]],
1276
+ writer_calls: [])
1277
+ end
1278
+ sig = sig_node?(stmt) ? stmt : nil
1279
+ end
1280
+ end
1281
+
1282
+ # A sig return type under a flagged namespace marks the
1283
+ # method as handing out extension values, body regardless.
1284
+ def note_sig_return(sig, def_node, side, registry)
1285
+ return unless sig
1286
+
1287
+ returns = sig_chain_call(sig, :returns)
1288
+ args = returns&.arguments&.arguments
1289
+ rule = args && args.size == 1 && type_rule(args[0])
1290
+ registry.methods[side][def_node.name] = rule if rule
1291
+ end
1292
+
1293
+ def collect_attr(call, side, attrs)
1294
+ roles = ATTR_ROLES[call.name]
1295
+ return unless roles && call.receiver.nil?
1296
+
1297
+ args = call.arguments&.arguments || []
1298
+ args.grep(Prism::SymbolNode).each do |sym|
1299
+ name = sym.unescaped.to_sym
1300
+ roles.each { |role| attrs[side][role] << name }
1301
+ end
1302
+ end
1303
+
1304
+ def def_entry(node, side)
1305
+ ivar_writes = []
1306
+ writer_calls = []
1307
+ scan_def_body(node.body, ivar_writes, writer_calls)
1308
+ Entry.new(side: side, name: node.name,
1309
+ return_expr: def_return_expr(node),
1310
+ ivar_writes: ivar_writes, writer_calls: writer_calls)
1311
+ end
1312
+
1313
+ def scan_def_body(node, ivar_writes, writer_calls)
1314
+ return if node.nil? || node.is_a?(Prism::ClassNode) ||
1315
+ node.is_a?(Prism::ModuleNode) ||
1316
+ node.is_a?(Prism::SingletonClassNode) ||
1317
+ node.is_a?(Prism::DefNode)
1318
+
1319
+ case node
1320
+ when Prism::InstanceVariableWriteNode,
1321
+ Prism::InstanceVariableOrWriteNode
1322
+ ivar_writes << [node.name, node.value]
1323
+ when Prism::CallNode
1324
+ args = node.arguments&.arguments
1325
+ if node.receiver && args && args.size == 1 &&
1326
+ node.name.match?(WRITER_NAME)
1327
+ writer_calls << [node.name.to_s.chomp("=").to_sym,
1328
+ args.first]
1329
+ end
1330
+ end
1331
+ node.child_nodes.compact.each do |child|
1332
+ scan_def_body(child, ivar_writes, writer_calls)
1333
+ end
1334
+ end
1335
+
1336
+ def def_return_expr(node)
1337
+ body = node.body
1338
+ body = body.statements if body.is_a?(Prism::BeginNode)
1339
+ body.body.last if body.is_a?(Prism::StatementsNode)
1340
+ end
1341
+
1342
+ def settle(registry, entries, attrs)
1343
+ loop do
1344
+ changed = false
1345
+ entries.each do |entry|
1346
+ changed = true if settle_entry(entry, registry, attrs)
1347
+ end
1348
+ changed = true if settle_readers(registry, attrs)
1349
+ break unless changed
1350
+ end
1351
+ end
1352
+
1353
+ # An attr reader over a tainted ivar hands the taint out
1354
+ # like a method returning it would.
1355
+ def settle_readers(registry, attrs)
1356
+ changed = false
1357
+ %i[instance singleton].each do |side|
1358
+ attrs[side][:reader].each do |name|
1359
+ next if registry.methods[side][name]
1360
+
1361
+ rule = registry.ivars[side][:"@#{name}"]
1362
+ if rule
1363
+ registry.methods[side][name] = rule
1364
+ changed = true
1365
+ end
1366
+ end
1367
+ end
1368
+ changed
1369
+ end
1370
+
1371
+ def settle_entry(entry, registry, attrs)
1372
+ changed = false
1373
+ if entry.name && !registry.methods[entry.side][entry.name] &&
1374
+ (rule = return_rule(entry.return_expr, entry.side, registry))
1375
+ registry.methods[entry.side][entry.name] = rule
1376
+ changed = true
1377
+ end
1378
+ entry.ivar_writes.each do |ivar, value|
1379
+ next if registry.ivars[entry.side][ivar]
1380
+
1381
+ rule = prepass_rule(value, entry.side, registry)
1382
+ if rule
1383
+ registry.ivars[entry.side][ivar] = rule
1384
+ changed = true
1385
+ end
1386
+ end
1387
+ entry.writer_calls.each do |name, value|
1388
+ side = writer_side(name, entry.side, attrs)
1389
+ next unless side
1390
+ next if registry.ivars[side][:"@#{name}"]
1391
+
1392
+ rule = prepass_rule(value, entry.side, registry)
1393
+ if rule
1394
+ registry.ivars[side][:"@#{name}"] = rule
1395
+ changed = true
1396
+ end
1397
+ end
1398
+ changed
1399
+ end
1400
+
1401
+ # A writer call taints the ivar behind the attr on the side
1402
+ # that declares it, wherever the call sits.
1403
+ def writer_side(name, caller_side, attrs)
1404
+ %i[instance singleton]
1405
+ .sort_by { |side| (side == caller_side) ? 0 : 1 }
1406
+ .find { |side| attrs[side][:writer].include?(name) }
1407
+ end
1408
+
1409
+ def return_rule(expr, side, registry)
1410
+ case expr
1411
+ when Prism::InstanceVariableWriteNode,
1412
+ Prism::InstanceVariableOrWriteNode
1413
+ prepass_rule(expr.value, side, registry)
1414
+ else
1415
+ prepass_rule(expr, side, registry)
1416
+ end
1417
+ end
1418
+
1419
+ # The pre-pass mirror of visit_call's taint sources, over
1420
+ # syntax alone: a chain is tainted when its root is a
1421
+ # flagged constant, a tainted ivar, or a call to a method
1422
+ # the registry already holds.
1423
+ def prepass_rule(node, side, registry)
1424
+ case node
1425
+ when Prism::InstanceVariableReadNode
1426
+ registry.ivars[side][node.name]
1427
+ when Prism::CallNode
1428
+ if CORE_METHODS.include?(node.name)
1429
+ return nil unless CHAIN_METHODS.include?(node.name)
1430
+
1431
+ return prepass_rule(node.receiver, side, registry)
1432
+ end
1433
+ return nil if node.name.end_with?("?")
1434
+
1435
+ if t_call?(node, :let) || t_call?(node, :cast)
1436
+ args = node.arguments&.arguments
1437
+ return (args && args.size >= 2) ? type_rule(args[1]) : nil
1438
+ end
1439
+ prepass_receiver_rule(node, side, registry)
1440
+ end
1441
+ end
1442
+
1443
+ def prepass_receiver_rule(node, side, registry)
1444
+ case (receiver = node.receiver)
1445
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
1446
+ receiver_rule(receiver) ||
1447
+ (registry.methods[:singleton][node.name] if
1448
+ names_class?(receiver, registry.class_name))
1449
+ when Prism::CallNode
1450
+ if receiver.name == :class &&
1451
+ receiver.receiver.is_a?(Prism::SelfNode)
1452
+ registry.methods[:singleton][node.name]
1453
+ else
1454
+ prepass_rule(receiver, side, registry)
1455
+ end
1456
+ when nil, Prism::SelfNode
1457
+ registry.methods[side][node.name]
1458
+ when Prism::InstanceVariableReadNode
1459
+ registry.ivars[side][receiver.name]
1460
+ end
1461
+ end
1462
+
1463
+ # The class's own name in receiver position reaches its
1464
+ # singleton: the full path, or the trailing segments an
1465
+ # inner reference would use.
1466
+ def names_class?(receiver, class_name)
1467
+ path = receiver.location.slice.delete_prefix("::")
1468
+ path == class_name || class_name.end_with?("::#{path}") ||
1469
+ path == class_name.split("::").last
1470
+ end
1471
+
1472
+ # Returns the matched rule so the caller can taint the
1473
+ # call's block even when the return value stays clean.
1474
+ def visit_call(node, tainted, ivars, scan)
1475
+ if CORE_METHODS.include?(node.name)
1476
+ return chain_through(node, tainted, ivars, scan)
1477
+ end
1478
+
1479
+ if (rule = receiver_rule(node.receiver))
1480
+ scan.findings << finding_for(rule, node, scan.path)
1481
+ scan.producers[node.object_id] = rule
1482
+ elsif (rule = annotation_rule(node, scan))
1483
+ scan.producers[node.object_id] = rule
1484
+ elsif (rule = taint_source(node, tainted, ivars, scan))
1485
+ scan.findings << derived_finding(rule, node, scan.path)
1486
+ # A predicate returns a plain boolean and ends the
1487
+ # chain; anything else is presumed to still live in the
1488
+ # extension.
1489
+ unless node.name.end_with?("?")
1490
+ scan.producers[node.object_id] = rule
1491
+ end
1492
+ rule
1493
+ elsif (rule = registry_rule(node))
1494
+ scan.producers[node.object_id] = rule
1495
+ elsif (rule = self_call_rule(node))
1496
+ scan.findings << derived_finding(rule, node, scan.path)
1497
+ unless node.name.end_with?("?")
1498
+ scan.producers[node.object_id] = rule
1499
+ end
1500
+ rule
1501
+ end
1502
+ end
1503
+
1504
+ # Inside a reopened bound class, self is the extension's
1505
+ # own object: a self-call the body does not define lands in
1506
+ # the extension, and so does a bare argumentless call that
1507
+ # is not plain Ruby.
1508
+ def self_call_rule(node)
1509
+ return nil unless @self_rule
1510
+
1511
+ if node.receiver.is_a?(Prism::SelfNode)
1512
+ return nil if @self_defs.include?(node.name)
1513
+
1514
+ @self_rule
1515
+ elsif node.receiver.nil? && node.arguments.nil? && !node.block
1516
+ return nil if @self_defs.include?(node.name) ||
1517
+ RUBY_METHODS.include?(node.name)
1518
+
1519
+ @self_rule
1520
+ end
1521
+ end
1522
+
1523
+ def chain_through(node, tainted, ivars, scan)
1524
+ return unless CHAIN_METHODS.include?(node.name)
1525
+
1526
+ rule = taint_source(node, tainted, ivars, scan) ||
1527
+ receiver_rule(node.receiver)
1528
+ scan.producers[node.object_id] = rule if rule
1529
+ end
1530
+
1531
+ # Calls to methods the pre-pass proved to hand out extension
1532
+ # values produce taint but no finding of their own: the
1533
+ # finding lands where the value is used.
1534
+ def registry_rule(node)
1535
+ return nil unless @registry
1536
+
1537
+ case (receiver = node.receiver)
1538
+ when nil, Prism::SelfNode
1539
+ method_rule(@singleton ? :singleton : :instance, node.name)
1540
+ when Prism::CallNode
1541
+ if receiver.name == :class &&
1542
+ receiver.receiver.is_a?(Prism::SelfNode)
1543
+ method_rule(:singleton, node.name)
1544
+ end
1545
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
1546
+ if names_class?(receiver, @registry.class_name)
1547
+ method_rule(:singleton, node.name)
1548
+ end
1549
+ end
1550
+ end
1551
+
1552
+ def method_rule(side, name)
1553
+ @registry.methods[side][name] ||
1554
+ @return_taints[[seed_scope, side, name]]
1555
+ end
1556
+
1557
+ def taint_source(node, tainted, ivars, scan)
1558
+ case (receiver = node.receiver)
1559
+ when Prism::LocalVariableReadNode
1560
+ tainted[receiver.name]
1561
+ when Prism::InstanceVariableReadNode
1562
+ ivars[receiver.name]
1563
+ when Prism::CallNode
1564
+ scan.producers[receiver.object_id]
1565
+ end
1566
+ end
1567
+
1568
+ # T.let and T.cast assert the value's type: a type under a
1569
+ # flagged namespace taints, any other constant type clears
1570
+ # whatever the value expression suggested.
1571
+ def annotation_rule(node, scan)
1572
+ return nil unless t_call?(node, :let) || t_call?(node, :cast)
1573
+
1574
+ args = node.arguments&.arguments
1575
+ return nil unless args && args.size >= 2
1576
+
1577
+ rule = type_rule(args[1])
1578
+ return rule if rule
1579
+
1580
+ constant_type?(args[1]) ? nil : scan.producers[args[0].object_id]
1581
+ end
1582
+
1583
+ def constant_type?(node)
1584
+ node.is_a?(Prism::ConstantReadNode) ||
1585
+ node.is_a?(Prism::ConstantPathNode)
1586
+ end
1587
+
1588
+ def sig_node?(node)
1589
+ node.is_a?(Prism::CallNode) && node.name == :sig && node.block
1590
+ end
1591
+
1592
+ # Params typed with a constant under a flagged namespace seed
1593
+ # the def's taint scope.
1594
+ def sig_taints(sig)
1595
+ params = sig_chain_call(sig, :params)
1596
+ args = params&.arguments&.arguments
1597
+ return {} unless args
1598
+
1599
+ taints = {}
1600
+ args.grep(Prism::KeywordHashNode).each do |kw|
1601
+ kw.elements.each do |assoc|
1602
+ next unless assoc.is_a?(Prism::AssocNode) &&
1603
+ assoc.key.is_a?(Prism::SymbolNode)
1604
+
1605
+ rule = type_rule(assoc.value)
1606
+ taints[assoc.key.unescaped.to_sym] = rule if rule
1607
+ end
1608
+ end
1609
+ taints
1610
+ end
1611
+
1612
+ # Param names the sig types with a constant outside every
1613
+ # flagged namespace: proven plain, immune to seeding.
1614
+ def plain_params(sig)
1615
+ params = sig_chain_call(sig, :params)
1616
+ args = params&.arguments&.arguments
1617
+ return EMPTY_SET unless args
1618
+
1619
+ plain = Set.new
1620
+ args.grep(Prism::KeywordHashNode).each do |kw|
1621
+ kw.elements.each do |assoc|
1622
+ next unless assoc.is_a?(Prism::AssocNode) &&
1623
+ assoc.key.is_a?(Prism::SymbolNode)
1624
+ next unless plain_type?(assoc.value)
1625
+
1626
+ plain << assoc.key.unescaped.to_sym
1627
+ end
1628
+ end
1629
+ plain
1630
+ end
1631
+
1632
+ # A bare unflagged constant, T.nilable of one, or a T::
1633
+ # container holding only such constants; T.untyped inside
1634
+ # a container keeps the erasure.
1635
+ def plain_type?(node)
1636
+ case node
1637
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
1638
+ source_rule(node.location.slice).nil?
1639
+ when Prism::CallNode
1640
+ args = node.arguments&.arguments
1641
+ if t_call?(node, :nilable)
1642
+ args&.size == 1 && plain_type?(args.first)
1643
+ elsif t_container?(node)
1644
+ !args.nil? && args.all? { |arg| plain_type?(arg) }
1645
+ else
1646
+ false
1647
+ end
1648
+ else
1649
+ false
1650
+ end
1651
+ end
1652
+
1653
+ def t_container?(node)
1654
+ return false unless node.name == :[] &&
1655
+ constant_type?(node.receiver)
1656
+
1657
+ node.receiver.location.slice.delete_prefix("::")
1658
+ .start_with?("T::")
1659
+ end
1660
+
1661
+ def sig_chain_call(sig, name)
1662
+ body = sig.block.body
1663
+ node = body.is_a?(Prism::StatementsNode) ? body.body.first : body
1664
+ while node.is_a?(Prism::CallNode)
1665
+ return node if node.name == name
1666
+
1667
+ node = node.receiver
1668
+ end
1669
+ nil
1670
+ end
1671
+
1672
+ def type_rule(node)
1673
+ case node
1674
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
1675
+ source_rule(node.location.slice)
1676
+ when Prism::CallNode
1677
+ args = node.arguments&.arguments
1678
+ if t_call?(node, :nilable) && args&.size == 1
1679
+ type_rule(args.first)
1680
+ end
1681
+ end
1682
+ end
1683
+
1684
+ def t_call?(node, name)
1685
+ node.name == name &&
1686
+ node.receiver.is_a?(Prism::ConstantReadNode) &&
1687
+ node.receiver.name == :T
1688
+ end
1689
+
1690
+ def index_rules
1691
+ @rule_index = @rules.to_h { |rule| [rule.namespace, rule] }
1692
+ end
1693
+
1694
+ # Cumulative :: prefixes of the path against the namespace
1695
+ # index, shortest first, tried under each level of the given
1696
+ # nesting before the path as written—flat cost however
1697
+ # many classes the tree promotes to rules.
1698
+ def namespace_rule(path, nesting = EMPTY_NESTING)
1699
+ segments = path.split("::")
1700
+ nesting.size.downto(1) do |depth|
1701
+ rule = prefix_rule(nesting.first(depth), segments)
1702
+ return rule if rule
1703
+ end
1704
+ prefix_rule(EMPTY_NESTING, segments)
1705
+ end
1706
+
1707
+ def prefix_rule(base, segments)
1708
+ prefix = base.empty? ? nil : base.join("::")
1709
+ segments.each do |seg|
1710
+ prefix = prefix ? "#{prefix}::#{seg}" : seg
1711
+ rule = @rule_index[prefix]
1712
+ return rule if rule
1713
+ end
1714
+ nil
1715
+ end
1716
+
1717
+ # An anchored path resolves from the root; anything else the
1718
+ # way Ruby would look it up, innermost scope first.
1719
+ def source_rule(slice)
1720
+ if slice.start_with?("::")
1721
+ namespace_rule(slice.delete_prefix("::"))
1722
+ else
1723
+ namespace_rule(slice, @nesting)
1724
+ end
1725
+ end
1726
+
1727
+ def receiver_rule(receiver)
1728
+ return nil unless constant_type?(receiver)
1729
+
1730
+ source_rule(receiver.location.slice)
1731
+ end
1732
+
1733
+ def finding_for(rule, node, path)
1734
+ interp = {
1735
+ method: node.name, gem: rule.gem,
1736
+ receiver: node.receiver.location.slice.delete_prefix("::")
1737
+ }
1738
+ message = rule.verified ? MESSAGE : UNVERIFIED_MESSAGE
1739
+ # In a chained call the node spans its whole receiver;
1740
+ # the method-name line is where the reader looks.
1741
+ location = node.message_loc || node.location
1742
+ Finding.new(
1743
+ check: CHECK,
1744
+ severity: :warning,
1745
+ message: format(message, interp),
1746
+ why: format(rule.verified ? UNSAFE_WHY : UNVERIFIED_WHY, interp),
1747
+ fix: format(FIX, interp),
1748
+ path: path,
1749
+ line: location.start_line,
1750
+ source: node.location.slice.lines.first&.strip
1751
+ )
1752
+ end
1753
+
1754
+ def derived_finding(rule, node, path)
1755
+ interp = {method: node.name, gem: rule.gem}
1756
+ location = node.message_loc || node.location
1757
+ Finding.new(
1758
+ check: CHECK,
1759
+ severity: :warning,
1760
+ message: format(DERIVED_MESSAGE, interp),
1761
+ why: format(rule.verified ? UNSAFE_WHY : UNVERIFIED_WHY, interp),
1762
+ fix: format(FIX, interp),
1763
+ path: path,
1764
+ line: location.start_line,
1765
+ source: node.location.slice.lines.first&.strip
1766
+ )
1767
+ end
1768
+ end
1769
+ end
1770
+ end