fiber_audit 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.fiber-audit.example.yml +19 -0
- data/ARCHITECTURE.md +726 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE +201 -0
- data/README.md +66 -8
- data/lib/fiber_audit/cli.rb +87 -2
- data/lib/fiber_audit/configuration.rb +106 -6
- data/lib/fiber_audit/errors.rb +2 -0
- data/lib/fiber_audit/operation_vocabulary.rb +42 -0
- data/lib/fiber_audit/reporters/text.rb +1 -1
- data/lib/fiber_audit/runtime/active_operations.rb +146 -0
- data/lib/fiber_audit/runtime/boot.rb +83 -0
- data/lib/fiber_audit/runtime/clock.rb +35 -0
- data/lib/fiber_audit/runtime/environment.rb +289 -0
- data/lib/fiber_audit/runtime/event.rb +86 -0
- data/lib/fiber_audit/runtime/execution_context.rb +89 -0
- data/lib/fiber_audit/runtime/heartbeat.rb +113 -0
- data/lib/fiber_audit/runtime/jsonl/schema.rb +312 -0
- data/lib/fiber_audit/runtime/jsonl/writer.rb +122 -0
- data/lib/fiber_audit/runtime/lifecycle.rb +343 -0
- data/lib/fiber_audit/runtime/limits.rb +102 -0
- data/lib/fiber_audit/runtime/location.rb +44 -0
- data/lib/fiber_audit/runtime/policy.rb +121 -0
- data/lib/fiber_audit/runtime/probes/base.rb +333 -0
- data/lib/fiber_audit/runtime/probes/http.rb +80 -0
- data/lib/fiber_audit/runtime/probes/io_select.rb +50 -0
- data/lib/fiber_audit/runtime/probes/registry.rb +156 -0
- data/lib/fiber_audit/runtime/probes/socket.rb +76 -0
- data/lib/fiber_audit/runtime/probes/subprocess.rb +83 -0
- data/lib/fiber_audit/runtime/probes/synchronization.rb +58 -0
- data/lib/fiber_audit/runtime/probes/thread_state.rb +39 -0
- data/lib/fiber_audit/runtime/probes/thread_wait.rb +25 -0
- data/lib/fiber_audit/runtime/rails_integration.rb +279 -0
- data/lib/fiber_audit/runtime/recorder.rb +333 -0
- data/lib/fiber_audit/runtime/redactor.rb +102 -0
- data/lib/fiber_audit/runtime/sampler.rb +26 -0
- data/lib/fiber_audit/runtime/scheduler_observer.rb +128 -0
- data/lib/fiber_audit/runtime/session.rb +112 -0
- data/lib/fiber_audit/runtime/supervisor.rb +114 -0
- data/lib/fiber_audit/runtime/validation.rb +68 -0
- data/lib/fiber_audit/runtime/watchdog.rb +479 -0
- data/lib/fiber_audit/runtime/watchdog_policy.rb +64 -0
- data/lib/fiber_audit/runtime.rb +37 -0
- data/lib/fiber_audit/static/rules/blocking_subprocess.rb +3 -8
- data/lib/fiber_audit/static/rules/direct_socket.rb +2 -3
- data/lib/fiber_audit/static/rules/io_select.rb +2 -4
- data/lib/fiber_audit/static/rules/net_http_in_request.rb +3 -5
- data/lib/fiber_audit/static/rules/synchronization.rb +2 -6
- data/lib/fiber_audit/static/rules/thread_current_state.rb +3 -2
- data/lib/fiber_audit/static/rules/thread_join.rb +3 -2
- data/lib/fiber_audit/version.rb +1 -1
- data/lib/fiber_audit.rb +1 -0
- metadata +38 -2
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'http'
|
|
5
|
+
require_relative 'io_select'
|
|
6
|
+
require_relative 'socket'
|
|
7
|
+
require_relative 'subprocess'
|
|
8
|
+
require_relative 'synchronization'
|
|
9
|
+
require_relative 'thread_state'
|
|
10
|
+
require_relative 'thread_wait'
|
|
11
|
+
|
|
12
|
+
module FiberAudit
|
|
13
|
+
module Runtime
|
|
14
|
+
module Probes
|
|
15
|
+
# Process-local owner of idempotent probe installation and dispatch.
|
|
16
|
+
class Registry
|
|
17
|
+
PROBE_INSTALLERS = [
|
|
18
|
+
Subprocess,
|
|
19
|
+
ThreadWait,
|
|
20
|
+
Synchronization,
|
|
21
|
+
ThreadState,
|
|
22
|
+
IOSelect,
|
|
23
|
+
Socket,
|
|
24
|
+
HTTP
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
module RequireInstanceHook
|
|
28
|
+
def require(...)
|
|
29
|
+
result = super
|
|
30
|
+
Registry.rescan_current!
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private :require
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module RequireSingletonHook
|
|
38
|
+
def require(...)
|
|
39
|
+
result = super
|
|
40
|
+
Registry.rescan_current!
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
def activate(base:)
|
|
47
|
+
raise RuntimeContractError, 'base must be a Runtime::Probes::Base' unless base.is_a?(Base)
|
|
48
|
+
|
|
49
|
+
registry = new(base: base)
|
|
50
|
+
@current = registry
|
|
51
|
+
registry.install!
|
|
52
|
+
registry
|
|
53
|
+
rescue StandardError
|
|
54
|
+
registry&.deactivate
|
|
55
|
+
@current = nil if @current.equal?(registry)
|
|
56
|
+
raise
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def observe(operation:, measurements: {}, emit_start: false, measurement_builder: nil, &application)
|
|
60
|
+
raise ArgumentError, 'probe observation requires a block' unless application
|
|
61
|
+
|
|
62
|
+
registry = current_for_observation
|
|
63
|
+
return application.call unless registry
|
|
64
|
+
|
|
65
|
+
registry.base.observe(
|
|
66
|
+
operation: operation,
|
|
67
|
+
measurements: measurements,
|
|
68
|
+
emit_start: emit_start,
|
|
69
|
+
measurement_builder: measurement_builder,
|
|
70
|
+
&application
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def rescan_current!
|
|
75
|
+
current_for_observation&.scan!
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def deactivate(registry)
|
|
79
|
+
@current = nil if @current.equal?(registry)
|
|
80
|
+
registry&.deactivate
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def current
|
|
84
|
+
current_for_observation
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def current_for_observation
|
|
90
|
+
registry = @current
|
|
91
|
+
return unless registry
|
|
92
|
+
return registry if registry.active_for_current_process?
|
|
93
|
+
return unless registry.stale_process?
|
|
94
|
+
|
|
95
|
+
FiberAudit::Runtime::Boot.current if defined?(FiberAudit::Runtime::Boot)
|
|
96
|
+
registry = @current
|
|
97
|
+
registry if registry&.active_for_current_process?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
attr_reader :base, :owner_pid
|
|
102
|
+
|
|
103
|
+
def initialize(base:)
|
|
104
|
+
@base = base
|
|
105
|
+
@owner_pid = Process.pid
|
|
106
|
+
@mutex = Mutex.new
|
|
107
|
+
@active = true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def install!
|
|
111
|
+
scan!
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def scan!
|
|
116
|
+
return self unless active_for_current_process?
|
|
117
|
+
|
|
118
|
+
base.with_guard do
|
|
119
|
+
@mutex.synchronize do
|
|
120
|
+
prepend_once(Kernel, RequireInstanceHook)
|
|
121
|
+
prepend_once(Kernel.singleton_class, RequireSingletonHook)
|
|
122
|
+
PROBE_INSTALLERS.each { |installer| installer.install!(self) }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
self
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
base.instrumentation_failure(e)
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def prepend_once(target, hook)
|
|
132
|
+
unless target.is_a?(Module) && hook.is_a?(Module)
|
|
133
|
+
raise RuntimeContractError, 'probe target and hook must be Modules'
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
target.prepend(hook) unless target.ancestors.include?(hook)
|
|
137
|
+
target
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def deactivate
|
|
141
|
+
@active = false
|
|
142
|
+
base.deactivate
|
|
143
|
+
self
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def active_for_current_process?
|
|
147
|
+
@active && owner_pid == Process.pid
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def stale_process?
|
|
151
|
+
owner_pid != Process.pid
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../operation_vocabulary'
|
|
4
|
+
|
|
5
|
+
module FiberAudit
|
|
6
|
+
module Runtime
|
|
7
|
+
module Probes
|
|
8
|
+
module Socket
|
|
9
|
+
MODULE_NAME = Module.instance_method(:name)
|
|
10
|
+
MODULE_COMPARE = Module.instance_method(:<=)
|
|
11
|
+
CONST_DEFINED = Module.instance_method(:const_defined?)
|
|
12
|
+
CONST_GET = Module.instance_method(:const_get)
|
|
13
|
+
|
|
14
|
+
module ConstructorHook
|
|
15
|
+
def new(...)
|
|
16
|
+
operation = Socket.operation_for(self)
|
|
17
|
+
return super unless operation
|
|
18
|
+
|
|
19
|
+
Registry.observe(operation: operation) { super }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def install!(registry)
|
|
26
|
+
return unless defined?(::BasicSocket)
|
|
27
|
+
|
|
28
|
+
registry.prepend_once(::BasicSocket.singleton_class, ConstructorHook)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def operation_for(klass)
|
|
32
|
+
name = MODULE_NAME.bind_call(klass)
|
|
33
|
+
return unless safe_constant_name?(name)
|
|
34
|
+
return unless exact_socket?(klass, name) || ip_socket_subclass?(klass)
|
|
35
|
+
return unless resolve_constant(name).equal?(klass)
|
|
36
|
+
|
|
37
|
+
"#{name}.new"
|
|
38
|
+
rescue StandardError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def exact_socket?(klass, name)
|
|
43
|
+
return false unless OperationVocabulary::FA1006_EXACT.include?(name)
|
|
44
|
+
|
|
45
|
+
resolve_constant(name).equal?(klass)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ip_socket_subclass?(klass)
|
|
49
|
+
return false unless defined?(::IPSocket)
|
|
50
|
+
|
|
51
|
+
MODULE_COMPARE.bind_call(klass, ::IPSocket) == true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def safe_constant_name?(name)
|
|
55
|
+
name.is_a?(String) && name.bytesize <= 240 &&
|
|
56
|
+
name.match?(/\A[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*\z/)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_constant(name)
|
|
60
|
+
namespace = Object
|
|
61
|
+
name.split('::').each do |part|
|
|
62
|
+
unless CONST_DEFINED.bind_call(namespace, part, false)
|
|
63
|
+
namespace = nil
|
|
64
|
+
break
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
namespace = CONST_GET.bind_call(namespace, part, false)
|
|
68
|
+
end
|
|
69
|
+
namespace
|
|
70
|
+
rescue NameError, TypeError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
module Runtime
|
|
5
|
+
module Probes
|
|
6
|
+
module Subprocess
|
|
7
|
+
module KernelInstanceHook
|
|
8
|
+
def system(...)
|
|
9
|
+
Registry.observe(operation: 'Kernel.system') { super }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def exec(...)
|
|
13
|
+
Registry.observe(operation: 'Kernel.exec', emit_start: true) { super }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def spawn(...)
|
|
17
|
+
Registry.observe(operation: 'Kernel.spawn') { super }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private :system, :exec, :spawn
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module KernelSingletonHook
|
|
24
|
+
def system(...)
|
|
25
|
+
Registry.observe(operation: 'Kernel.system') { super }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def exec(...)
|
|
29
|
+
Registry.observe(operation: 'Kernel.exec', emit_start: true) { super }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def spawn(...)
|
|
33
|
+
Registry.observe(operation: 'Kernel.spawn') { super }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module IOHook
|
|
38
|
+
def popen(...)
|
|
39
|
+
Registry.observe(operation: 'IO.popen') { super }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module ProcessHook
|
|
44
|
+
def waitall(...)
|
|
45
|
+
Registry.observe(operation: 'Process.waitall') { super }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def detach(...)
|
|
49
|
+
Registry.observe(operation: 'Process.detach') { super }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module Open3Hook
|
|
54
|
+
def capture2(...)
|
|
55
|
+
Registry.observe(operation: 'Open3.capture2') { super }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def capture2e(...)
|
|
59
|
+
Registry.observe(operation: 'Open3.capture2e') { super }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def capture3(...)
|
|
63
|
+
Registry.observe(operation: 'Open3.capture3') { super }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def pipeline(...)
|
|
67
|
+
Registry.observe(operation: 'Open3.pipeline') { super }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
module_function
|
|
72
|
+
|
|
73
|
+
def install!(registry)
|
|
74
|
+
registry.prepend_once(Kernel, KernelInstanceHook)
|
|
75
|
+
registry.prepend_once(Kernel.singleton_class, KernelSingletonHook)
|
|
76
|
+
registry.prepend_once(IO.singleton_class, IOHook)
|
|
77
|
+
registry.prepend_once(Process.singleton_class, ProcessHook)
|
|
78
|
+
registry.prepend_once(Open3.singleton_class, Open3Hook) if defined?(Open3)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
module Runtime
|
|
5
|
+
module Probes
|
|
6
|
+
module Synchronization
|
|
7
|
+
TRY_LOCK_MEASUREMENTS = lambda do |result|
|
|
8
|
+
acquired = result == true
|
|
9
|
+
{ acquired: acquired, contention_observed: !acquired }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module MutexHook
|
|
13
|
+
def lock(...)
|
|
14
|
+
Registry.observe(operation: 'Mutex#lock') { super }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def synchronize(...)
|
|
18
|
+
Registry.observe(operation: 'Mutex#synchronize') { super }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def try_lock(...)
|
|
22
|
+
Registry.observe(
|
|
23
|
+
operation: 'Mutex#try_lock',
|
|
24
|
+
measurement_builder: TRY_LOCK_MEASUREMENTS
|
|
25
|
+
) { super }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module ConditionVariableHook
|
|
30
|
+
def wait(...)
|
|
31
|
+
Registry.observe(operation: 'ConditionVariable#wait') { super }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module MonitorHook
|
|
36
|
+
def synchronize(...)
|
|
37
|
+
Registry.observe(operation: 'Monitor#synchronize') { super }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module MonitorMixinHook
|
|
42
|
+
def synchronize(...)
|
|
43
|
+
Registry.observe(operation: 'MonitorMixin#synchronize') { super }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
def install!(registry)
|
|
50
|
+
registry.prepend_once(Mutex, MutexHook)
|
|
51
|
+
registry.prepend_once(ConditionVariable, ConditionVariableHook) if defined?(ConditionVariable)
|
|
52
|
+
registry.prepend_once(Monitor, MonitorHook) if defined?(Monitor)
|
|
53
|
+
registry.prepend_once(MonitorMixin, MonitorMixinHook) if defined?(MonitorMixin)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
module Runtime
|
|
5
|
+
module Probes
|
|
6
|
+
module ThreadState
|
|
7
|
+
module Hook
|
|
8
|
+
def thread_variable_get(...)
|
|
9
|
+
Registry.observe(operation: 'Thread.thread_variable_get') { super }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def thread_variable_set(...)
|
|
13
|
+
Registry.observe(operation: 'Thread.thread_variable_set') { super }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def [](...)
|
|
17
|
+
return super unless equal?(Thread.current)
|
|
18
|
+
|
|
19
|
+
Registry.observe(operation: 'Thread.current.[]') { super }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def []=(...)
|
|
23
|
+
if equal?(Thread.current)
|
|
24
|
+
Registry.observe(operation: 'Thread.current.[]=') { super }
|
|
25
|
+
else
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def install!(registry)
|
|
34
|
+
registry.prepend_once(Thread, Hook)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
module Runtime
|
|
5
|
+
module Probes
|
|
6
|
+
module ThreadWait
|
|
7
|
+
module Hook
|
|
8
|
+
def join(...)
|
|
9
|
+
Registry.observe(operation: 'Thread.join') { super }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def value(...)
|
|
13
|
+
Registry.observe(operation: 'Thread.value') { super }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def install!(registry)
|
|
20
|
+
registry.prepend_once(Thread, Hook)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|