fiber_audit 0.2.0 → 0.3.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.
@@ -9,17 +9,90 @@ require_relative '../../operation_vocabulary'
9
9
  module FiberAudit
10
10
  module Static
11
11
  module Rules
12
+ # FA1001: Subprocess lifecycle operations that may interfere with
13
+ # the fiber scheduler. Detects subprocess creation, replacement,
14
+ # waiting, detachment, and stream lifecycle operations.
15
+ #
16
+ # Operations are classified into semantic categories:
17
+ # - creation: spawning a new process (info)
18
+ # - replacement: replacing the current process via exec (info)
19
+ # - waiting: blocking waits for subprocess completion (medium)
20
+ # - detach: detaching a subprocess without waiting (info)
21
+ # - stream: subprocess pipe/stream lifecycle via IO.popen (medium)
22
+ #
23
+ # Severity: info for creation/replacement/detach, medium for waits/stream.
24
+ # These are scheduler-cooperation requirements, so execution context does
25
+ # not by itself escalate them to critical.
12
26
  class BlockingSubprocess < Base
13
27
  id 'FA1001'
14
- severity :high
28
+ severity :medium
15
29
  default_confidence :high
16
- description 'Blocking subprocess call in the fiber scheduler path'
30
+ description 'Subprocess lifecycle operations may interfere with the fiber scheduler'
17
31
 
18
32
  TARGETS = OperationVocabulary::FA1001_TARGETS
19
33
  BARE_KERNEL_METHODS = OperationVocabulary::FA1001_KERNEL_METHODS
20
34
 
21
- MESSAGE = 'Subprocess operation may block the thread running the fiber scheduler.'
22
- REMEDIATION = 'Move long-running subprocess work outside the request path, or verify scheduler behaviour under load.'
35
+ # Per-operation semantic category
36
+ OPERATION_CATEGORY = {
37
+ # Creation (info) - spawning a new process
38
+ 'Kernel.spawn' => :creation,
39
+ 'Process.spawn' => :creation,
40
+ # Replacement (info) - replacing current process via exec
41
+ 'Kernel.exec' => :replacement,
42
+ 'Process.exec' => :replacement,
43
+ # Waiting (medium) - blocking waits for subprocess completion
44
+ 'Kernel.system' => :waiting,
45
+ 'Process.wait' => :waiting,
46
+ 'Process.wait2' => :waiting,
47
+ 'Process.waitpid' => :waiting,
48
+ 'Process.waitpid2' => :waiting,
49
+ 'Process.waitall' => :waiting,
50
+ 'Process::Status.wait' => :waiting,
51
+ 'Open3.capture2' => :waiting,
52
+ 'Open3.capture2e' => :waiting,
53
+ 'Open3.capture3' => :waiting,
54
+ 'Open3.pipeline' => :waiting,
55
+ # Detach (info) - detaching subprocess without waiting
56
+ 'Process.detach' => :detach,
57
+ # Stream (medium) - subprocess pipe/stream lifecycle
58
+ 'IO.popen' => :stream
59
+ }.freeze
60
+
61
+ # Per-category metadata
62
+ CATEGORY_METADATA = {
63
+ creation: {
64
+ severity: :info,
65
+ title: 'Subprocess creation',
66
+ message: 'Spawning a subprocess may leave background processes that outlive the fiber scheduler session.',
67
+ remediation: 'Track spawned processes or move subprocess creation outside the fiber-scheduled path.'
68
+ },
69
+ replacement: {
70
+ severity: :info,
71
+ title: 'Process replacement',
72
+ message: 'Process replacement via exec replaces the current process image, terminating the fiber scheduler.',
73
+ remediation: 'Avoid exec in fiber-scheduled code; prefer subprocess creation with explicit lifecycle management.'
74
+ },
75
+ waiting: {
76
+ severity: :medium,
77
+ title: 'Subprocess wait',
78
+ message: 'Subprocess waiting requires scheduler process-wait cooperation in a non-blocking Fiber.',
79
+ remediation: 'Verify scheduler process_wait support and runtime progress, ' \
80
+ 'or move waits outside the fiber-scheduled path.'
81
+ },
82
+ detach: {
83
+ severity: :info,
84
+ title: 'Subprocess detach',
85
+ message: 'Detaching a subprocess may leave it unmanaged by the fiber scheduler.',
86
+ remediation: 'Avoid detaching subprocesses in fiber-scheduled code; use explicit lifecycle management.'
87
+ },
88
+ stream: {
89
+ severity: :medium,
90
+ title: 'Subprocess pipe stream',
91
+ message: 'Subprocess pipe I/O requires scheduler cooperation while the stream is open.',
92
+ remediation: 'Verify scheduler-aware I/O and runtime progress on the pipe, ' \
93
+ 'or move pipe operations outside the fiber-scheduled path.'
94
+ }
95
+ }.freeze
23
96
 
24
97
  def analyze(call_sites:)
25
98
  call_sites.filter_map do |site|
@@ -60,11 +133,14 @@ module FiberAudit
60
133
  def build_finding(site, match)
61
134
  operation = "#{match[:constant]}.#{match[:method]}"
62
135
  context = site.execution_context || :unknown
63
- sev = severity_for(self.class.severity, context)
136
+ category = OPERATION_CATEGORY.fetch(operation, :waiting)
137
+ metadata = CATEGORY_METADATA.fetch(category)
138
+ base_severity = metadata[:severity]
139
+ sev = advisory_severity(base_severity)
64
140
 
65
141
  Finding.new(
66
142
  rule_id: self.class.id,
67
- title: 'Blocking subprocess call',
143
+ title: metadata[:title],
68
144
  category: :subprocess,
69
145
  severity: sev,
70
146
  confidence: match[:confidence],
@@ -72,15 +148,19 @@ module FiberAudit
72
148
  symbol: site.enclosing_symbol,
73
149
  operation: operation,
74
150
  execution_context: context,
75
- message: MESSAGE,
151
+ message: metadata[:message],
76
152
  evidence: [
77
153
  Evidence.new(
78
154
  source: :static,
79
- message: "Matched #{operation}",
80
- details: { receiver: match[:constant], method: match[:method] }
155
+ message: "Matched #{operation} (#{category})",
156
+ details: {
157
+ receiver: match[:constant],
158
+ method: match[:method],
159
+ semantic: category
160
+ }
81
161
  )
82
162
  ],
83
- remediation: REMEDIATION
163
+ remediation: metadata[:remediation]
84
164
  )
85
165
  end
86
166
  end
@@ -9,23 +9,22 @@ require_relative '../../operation_vocabulary'
9
9
  module FiberAudit
10
10
  module Static
11
11
  module Rules
12
- # FA1006 – Detects direct socket creation that may bypass
13
- # scheduler-aware networking and block the scheduler thread.
12
+ # FA1006 – Detects direct socket construction whose DNS, connection, and
13
+ # later I/O require scheduler cooperation. Advisory rule with :low default.
14
14
  class DirectSocket < Base
15
15
  id 'FA1006'
16
- severity :medium
16
+ severity :low
17
17
  default_confidence :high
18
- description 'Detects direct socket creation that may bypass scheduler-aware networking.'
18
+ description 'Direct socket paths require scheduler-aware DNS and I/O cooperation.'
19
19
 
20
20
  TITLE = 'Direct socket creation'
21
21
  CATEGORY = :network
22
22
 
23
23
  EXACT = OperationVocabulary::FA1006_EXACT
24
24
 
25
- MESSAGE = 'Direct socket use may bypass scheduler-aware networking ' \
26
- 'and block the scheduler thread.'
27
- REMEDIATION = 'Use scheduler-aware networking APIs or verify the ' \
28
- 'socket operations cooperate with the active Fiber scheduler.'
25
+ MESSAGE = 'This socket path may require scheduler cooperation for DNS, connection, or subsequent I/O.'
26
+ REMEDIATION = 'Distinguish allocation from DNS/connect/I/O, then verify ' \
27
+ 'the active scheduler hooks and runtime progress.'
29
28
 
30
29
  class << self
31
30
  def title = TITLE
@@ -88,7 +87,7 @@ module FiberAudit
88
87
  rule_id: self.class.id,
89
88
  title: self.class.title,
90
89
  category: self.class.category,
91
- severity: severity_for(:medium, ctx),
90
+ severity: advisory_severity(:low),
92
91
  confidence: site.confidence,
93
92
  location: site.location,
94
93
  symbol: site.enclosing_symbol,
@@ -9,19 +9,19 @@ require_relative '../../operation_vocabulary'
9
9
  module FiberAudit
10
10
  module Static
11
11
  module Rules
12
- # FA1005: Detects explicit IO.select calls that may bypass
13
- # scheduler-aware I/O and block the scheduler thread.
12
+ # FA1005: Detects explicit IO.select scheduler-capability requirements.
13
+ # Advisory rule with :medium default.
14
14
  class IOSelect < Base
15
15
  id 'FA1005'
16
16
  severity :medium
17
17
  default_confidence :high
18
- description 'Explicit IO.select call that may bypass scheduler-aware I/O'
18
+ description 'Explicit IO.select requires scheduler io_select cooperation'
19
19
 
20
20
  TITLE = 'Explicit IO.select call'
21
21
  CATEGORY = :blocking_io
22
22
 
23
- MESSAGE = 'IO.select may bypass scheduler-aware I/O and block the thread running the fiber scheduler.'
24
- REMEDIATION = 'Use scheduler-aware I/O APIs or allow the active Fiber scheduler to manage readiness.'
23
+ MESSAGE = 'IO.select requires scheduler io_select support when used from a non-blocking Fiber.'
24
+ REMEDIATION = 'Verify the selected scheduler implements io_select and confirm runtime progress under load.'
25
25
 
26
26
  TARGETS = OperationVocabulary::FA1005_TARGETS
27
27
 
@@ -79,7 +79,7 @@ module FiberAudit
79
79
  rule_id: self.class.id,
80
80
  title: TITLE,
81
81
  category: CATEGORY,
82
- severity: severity_for(:medium, context),
82
+ severity: advisory_severity(:medium),
83
83
  confidence: confidence,
84
84
  location: site.location,
85
85
  symbol: site.enclosing_symbol,
@@ -9,19 +9,22 @@ require_relative '../../operation_vocabulary'
9
9
  module FiberAudit
10
10
  module Static
11
11
  module Rules
12
- # FA1007: Detects blocking HTTP calls in request-like contexts.
12
+ # FA1007: Detects HTTP scheduler-cooperation requirements in request-like contexts.
13
13
  # Targets Net::HTTP.{get,get_response,start,request}, URI.open, OpenURI.open_uri.
14
14
  # Excludes Net::HTTP.get_print. Emits only for request/middleware/websocket/callback.
15
+ #
16
+ # Advisory rule with :medium default, no context ceiling.
15
17
  class NetHTTPInRequest < Base
16
18
  id 'FA1007'
17
- severity :high
19
+ severity :medium
18
20
  default_confidence :high
19
- description 'Blocking HTTP call in request path'
21
+ description 'HTTP calls require scheduler-aware DNS, socket, and TLS cooperation in request contexts'
20
22
 
21
- TITLE = 'Blocking HTTP call in request path'
23
+ TITLE = 'HTTP scheduler-cooperation requirement'
22
24
  CATEGORY = :network
23
- MESSAGE = 'Synchronous HTTP activity in a request-like context may block the thread running the fiber scheduler.'
24
- REMEDIATION = 'Use a scheduler-aware HTTP client, or move outbound HTTP work outside the request path.'
25
+ MESSAGE = 'HTTP activity requires scheduler-aware DNS and I/O cooperation in a non-blocking Fiber.'
26
+ REMEDIATION = 'Verify the selected HTTP/TLS/DNS stack and active scheduler ' \
27
+ 'under load; investigate correlated stalls.'
25
28
 
26
29
  NET_HTTP_METHODS = OperationVocabulary::FA1007_NET_HTTP_METHODS
27
30
  URI_METHODS = OperationVocabulary::FA1007_URI_METHODS
@@ -82,7 +85,7 @@ module FiberAudit
82
85
  def build_finding(site, match)
83
86
  context = site.execution_context
84
87
  sev, conf = if match[:type] == :net_http
85
- [severity_for(:high, context), site.confidence]
88
+ [advisory_severity(:medium), site.confidence]
86
89
  else
87
90
  %i[medium low]
88
91
  end
@@ -101,7 +104,7 @@ module FiberAudit
101
104
  evidence: [
102
105
  FiberAudit::Evidence.new(
103
106
  source: 'static_analysis',
104
- message: "Blocking HTTP call detected: #{match[:operation]}",
107
+ message: "HTTP scheduler-cooperation point: #{match[:operation]}",
105
108
  details: { receiver: site.receiver_constant, method: site.method_name, context: context }
106
109
  )
107
110
  ],
@@ -9,20 +9,24 @@ require_relative '../../operation_vocabulary'
9
9
  module FiberAudit
10
10
  module Static
11
11
  module Rules
12
+ # FA1003: Thread synchronization primitives that may interfere with
13
+ # fiber scheduler cooperation. Advisory rule with :low default.
14
+ #
15
+ # Detects Mutex, ConditionVariable, Monitor, and MonitorMixin
16
+ # synchronization operations. try_lock is treated as :info since
17
+ # it's non-blocking but indicates thread-oriented synchronization.
12
18
  class Synchronization < Base
13
19
  id 'FA1003'
14
- severity :medium
20
+ severity :low
15
21
  default_confidence :high
16
- description 'Thread synchronization primitives that may block the fiber scheduler thread'
22
+ description 'Thread synchronization may interfere with fiber scheduler cooperation'
17
23
 
18
24
  RULE_TITLE = 'Thread synchronization'
19
25
  RULE_CATEGORY = :synchronization
20
26
  TARGETS = OperationVocabulary::FA1003_TARGETS
21
- TRY_LOCK_MSG = 'Mutex.try_lock is non-blocking but may indicate ' \
22
- 'thread-oriented synchronization in fiber-scheduled code.'
23
- NORMAL_MSG = 'Synchronization operation may block the thread running the fiber scheduler.'
24
- REMEDIATION = 'Use scheduler-aware synchronization primitives, or verify ' \
25
- 'contention and scheduler behaviour under load.'
27
+ TRY_LOCK_MSG = 'Mutex#try_lock is non-blocking and identifies a thread-oriented coordination point.'
28
+ NORMAL_MSG = 'Synchronization contention requires scheduler block/unblock cooperation in a non-blocking Fiber.'
29
+ REMEDIATION = 'Verify scheduler coordination and contention behavior under load; investigate correlated stalls.'
26
30
 
27
31
  def analyze(call_sites:)
28
32
  explicit_monitor_mixins = explicit_monitor_mixin_classes(call_sites)
@@ -98,7 +102,8 @@ module FiberAudit
98
102
  def build_finding(site, target, method)
99
103
  try_lock = target == 'Mutex' && method == :try_lock
100
104
  operation = "#{target}##{method}"
101
- severity = try_lock ? :info : severity_for(:medium, site.execution_context)
105
+ # try_lock has fixed :info, no config override
106
+ severity = try_lock ? :info : advisory_severity(:low)
102
107
  confidence = try_lock ? :high : site.confidence
103
108
  message = try_lock ? TRY_LOCK_MSG : NORMAL_MSG
104
109
  evidence = Evidence.new(source: operation, message: message,
@@ -9,27 +9,32 @@ require_relative '../../operation_vocabulary'
9
9
  module FiberAudit
10
10
  module Static
11
11
  module Rules
12
- # Detect thread-variable and Thread.current index state.
12
+ # FA1004: Detect thread-variable state access.
13
+ #
14
+ # Detects only thread_variable_get/set operations, not Thread.current[]
15
+ # index operations. Thread variables are shared across all fibers on the
16
+ # same thread and may leak request-local data.
13
17
  class ThreadCurrentState < Base
14
18
  id 'FA1004'
15
19
  severity :high
16
20
  confidence :high
17
- description 'Thread-local state in fiber code may be shared across fibers and leak request-local data'
21
+ description 'Thread thread variables in fiber code may be shared across fibers and leak request-local data'
18
22
 
19
- TITLE = 'Thread-local state in fiber code'
23
+ TITLE = 'Thread thread variables in fiber code'
20
24
  CATEGORY = :thread_local
21
- MESSAGE = 'Thread-local state may be shared across fibers and leak request-local data.'
22
- REMEDIATION = 'Use fiber-local or framework-provided request-local state instead of Thread thread variables.'
25
+ MESSAGE = 'Thread thread variables are shared across all fibers on the same thread ' \
26
+ 'and may leak request-local data between concurrent requests.'
27
+ REMEDIATION = 'Prefer fiber-local storage (Fiber[:key]) or framework-provided ' \
28
+ 'request-local state over thread_variable_get/set.'
23
29
 
24
30
  THREAD_VARIABLE_METHODS = OperationVocabulary::FA1004_THREAD_VARIABLE_METHODS
25
- INDEX_METHODS = OperationVocabulary::FA1004_INDEX_METHODS
26
31
 
27
32
  def analyze(call_sites:)
28
33
  findings = []
29
34
  call_sites.each do |site|
30
35
  next if skip?(site)
31
36
 
32
- finding = match_thread_variable(site) || match_index_op(site)
37
+ finding = match_thread_variable(site)
33
38
  findings << finding if finding
34
39
  end
35
40
  findings
@@ -74,19 +79,8 @@ module FiberAudit
74
79
  build_finding(site, :high, :high, operation(site))
75
80
  end
76
81
 
77
- def match_index_op(site)
78
- return unless INDEX_METHODS.include?(site.method_name)
79
- return unless site.receiver_source == 'Thread.current'
80
-
81
- build_finding(site, :medium, :high, operation(site))
82
- end
83
-
84
82
  def operation(site)
85
- if INDEX_METHODS.include?(site.method_name)
86
- "Thread.current.#{site.method_name}"
87
- else
88
- "Thread.#{site.method_name}"
89
- end
83
+ "Thread.#{site.method_name}"
90
84
  end
91
85
 
92
86
  def build_finding(site, default_sev, conf, operation)
@@ -104,7 +98,11 @@ module FiberAudit
104
98
  operation: operation,
105
99
  execution_context: context,
106
100
  message: MESSAGE,
107
- evidence: [Evidence.new(source: site.receiver_source, message: "Thread-local access via #{site.method_name}")],
101
+ evidence: [Evidence.new(
102
+ source: site.receiver_source,
103
+ message: "Thread thread variable access via #{site.method_name}",
104
+ details: { operation: operation, receiver: site.receiver_source }
105
+ )],
108
106
  remediation: REMEDIATION
109
107
  )
110
108
  end
@@ -9,7 +9,7 @@ require_relative '../../operation_vocabulary'
9
9
  module FiberAudit
10
10
  module Static
11
11
  module Rules
12
- # FA1002: Thread#join / Thread#value may block the fiber-scheduler thread.
12
+ # FA1002: Thread#join / Thread#value are scheduler coordination points.
13
13
  #
14
14
  # Matches join/value on Thread instances whose receiver_constant is
15
15
  # 'Thread' but whose receiver_source is not the bare literal 'Thread'.
@@ -20,17 +20,19 @@ module FiberAudit
20
20
  # and workspace-shadowed Thread (checked via workspace or
21
21
  # workspace.semantic_index resolve_constant seam). Adapter errors from
22
22
  # those seams are swallowed — they never raise.
23
+ #
24
+ # Advisory rule: uses advisory_severity (no context ceiling), default :low.
23
25
  class ThreadJoin < Base
24
26
  id 'FA1002'
25
- severity :high
27
+ severity :low
26
28
  confidence :high
27
- description 'Waiting for a thread may block the thread running the fiber scheduler.'
29
+ description 'Thread waits require scheduler block/unblock cooperation'
28
30
 
29
31
  TITLE = 'Thread wait'
30
32
  CATEGORY = :synchronization
31
- MESSAGE = 'Waiting for a thread may block the thread running the fiber scheduler.'
32
- REMEDIATION = 'Replace thread waits with scheduler-aware coordination, ' \
33
- 'or move the work outside the fiber-scheduled path.'
33
+ MESSAGE = 'Thread#join/value requires scheduler block/unblock cooperation in a non-blocking Fiber.'
34
+ REMEDIATION = 'Verify scheduler coordination and runtime progress, ' \
35
+ 'or move the wait outside the fiber-scheduled path.'
34
36
  TARGET_METHODS = OperationVocabulary::FA1002_METHODS
35
37
  CANONICAL_OPS = OperationVocabulary::FA1002_OPERATIONS
36
38
  DIRECT_CLASS_SOURCE = 'Thread'
@@ -73,7 +75,7 @@ module FiberAudit
73
75
  def build_finding(site)
74
76
  loc = site.location
75
77
  op = "Thread.#{site.method_name}"
76
- sev = severity_for(self.class.severity, site.execution_context)
78
+ sev = advisory_severity(self.class.severity)
77
79
  conf = site.receiver_source == 'Thread.current' ? :high : site.confidence
78
80
 
79
81
  Finding.new(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FiberAudit
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fiber_audit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - FiberAudit Contributors
@@ -95,6 +95,7 @@ files:
95
95
  - lib/fiber_audit/runtime/redactor.rb
96
96
  - lib/fiber_audit/runtime/sampler.rb
97
97
  - lib/fiber_audit/runtime/scheduler_observer.rb
98
+ - lib/fiber_audit/runtime/scheduler_snapshot.rb
98
99
  - lib/fiber_audit/runtime/session.rb
99
100
  - lib/fiber_audit/runtime/supervisor.rb
100
101
  - lib/fiber_audit/runtime/validation.rb