vivarium 0.3.2 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0e7e80cef690342f163fde6c05fed6859c3ef81aa794fe532c87b00fbe11cd5
4
- data.tar.gz: 4f2de93c9795fe93ecb0d8291f86a117ac0f595c8c12e2c7b41caabaae7c3273
3
+ metadata.gz: be07034ab56d73e0aaa4fe9e124d67888ae2006cc5f6cb632cf4b93b20855422
4
+ data.tar.gz: 5f95cdaa111a56415f4fed08da0c638a87f5c08f01e51db2b5b818c04c256bc4
5
5
  SHA512:
6
- metadata.gz: 9a116f2c50b978a368a05cca351411f9724686746820085b651038d060c8452a8546170cb6e6ac885ab8a721aa20bcf42c5746639d32cbfea0ba8a9452b9687e
7
- data.tar.gz: fb5b4b5307682fa84b77dcfaa2165a71a5e16deef8d7b7d21d6b3267e82b5a12ce1f73dee946193605e039a247d59613a9c2dffca15ef96ddb502a17c9ce742d
6
+ metadata.gz: 49e166adbaca6231263d10255779ce4ff16a98ba9905a4afc48da012382d21fe1635b4f3bfff3990125bcb9718f7e1e5f3c5168a3a7f39b7fd8981be312bc3a4
7
+ data.tar.gz: a85783ba20a6eb866bd8215f55a4edb6890bef1b0b3e3e7af6d11bfcc0fda77adbfa2e7ea465e201c424d6fc9f883bcb20d7846e3c051c294560151ac0078d30
data/README.md CHANGED
@@ -139,6 +139,22 @@ bundle exec ruby examples/privilege_event_demo.rb
139
139
 
140
140
  This demo attempts setuid/setgid changes, sensitive file access, and `sudo` exec to trigger privilege-related events such as `setid_change`, `capable_check`, and `bprm_creds`.
141
141
 
142
+ 8) Ruby internal ENV access demo client:
143
+
144
+ ```bash
145
+ bundle exec ruby examples/env_access_ruby_demo.rb
146
+ ```
147
+
148
+ This demo triggers Ruby-side ENV methods (`[]`, `fetch`, `key?`, `[]=`, `store`, `delete`, `clear`, `replace`) and is intended to produce SPAN events.
149
+
150
+ 9) External command ENV libc access demo client:
151
+
152
+ ```bash
153
+ bundle exec ruby examples/env_access_external_demo.rb
154
+ ```
155
+
156
+ This demo spawns an external process that directly calls libc `getenv`, `setenv`, `unsetenv`, `putenv`, and `clearenv`, intended to trigger `env_caccess` eBPF events.
157
+
142
158
  You can also start top-level observation without a block (it keeps observing until process exit):
143
159
 
144
160
  ```ruby
@@ -14,16 +14,15 @@ require "vivarium"
14
14
  require "vivarium/correlator"
15
15
  require "vivarium/tree_renderer"
16
16
  require "vivarium/display_filter"
17
- require "vivarium_usdt"
18
17
 
19
- t0 = 1_000_000_000 # base ktime_ns
20
- pid = Process.pid
21
- tid = Process.pid
22
- method_id = 0x0001_0001
18
+ t0 = 1_000_000_000 # base ktime_ns
19
+ pid = Process.pid
20
+ tid = Process.pid
23
21
 
24
- # span_start payload: method_id (8B) + file_id (8B) + lineno (8B)
25
- span_start_payload = [method_id, 0, 10].pack("q<q<q<")
26
- .ljust(Vivarium::EVENT_PAYLOAD_SIZE, "\x00")
22
+ # span_start payload: method_name (128B) + file_name (120B) + lineno (8B)
23
+ span_start_payload = "MyClass#my_method".ljust(Vivarium::SPAN_METHOD_SIZE, "\x00") +
24
+ "drop_demo.rb".ljust(Vivarium::SPAN_FILE_SIZE, "\x00") +
25
+ [10].pack("q<")
27
26
 
28
27
  events = [
29
28
  Vivarium::Correlator::RawEvent.new(
@@ -67,7 +66,6 @@ events = [
67
66
 
68
67
  Vivarium::TreeRenderer.new(
69
68
  events: events,
70
- method_table: { method_id => "MyClass#my_method" },
71
69
  observer_pid: pid,
72
70
  main_tid: tid,
73
71
  session_start_iso: "2026-06-02T00:00:00.000Z",
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rbconfig"
5
+ require "vivarium"
6
+
7
+ # Usage:
8
+ # 1) In another shell (root): sudo bundle exec vivariumd
9
+ # 2) Run this script: bundle exec ruby examples/env_access_external_demo.rb
10
+ #
11
+ # This demo launches an external Ruby process and forces direct libc calls to
12
+ # getenv/setenv/unsetenv/putenv/clearenv through Fiddle.
13
+ # These should appear as eBPF events with event_name=env_caccess.
14
+
15
+ FILTER = {
16
+ include_events: %w[env_caccess proc_fork proc_exec]
17
+ }.freeze
18
+
19
+ CHILD_CODE = <<~RUBY
20
+ require "fiddle"
21
+
22
+ libc = begin
23
+ Fiddle.dlopen("libc.so.6")
24
+ rescue Fiddle::DLError
25
+ Fiddle.dlopen(nil)
26
+ end
27
+
28
+ getenv = Fiddle::Function.new(libc["getenv"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
29
+ setenv = Fiddle::Function.new(libc["setenv"], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
30
+ unsetenv = Fiddle::Function.new(libc["unsetenv"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
31
+ putenv = Fiddle::Function.new(libc["putenv"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
32
+ clearenv = Fiddle::Function.new(libc["clearenv"], [], Fiddle::TYPE_INT)
33
+
34
+ key = "VIVARIUM_ENV_EXT_DEMO"
35
+ putenv_buf = "VIVARIUM_ENV_EXT_PUT=from_putenv"
36
+
37
+ getenv.call("HOME")
38
+ setenv.call(key, "from_setenv", 1)
39
+ getenv.call(key)
40
+ putenv.call(putenv_buf)
41
+ unsetenv.call(key)
42
+ clearenv.call
43
+ RUBY
44
+
45
+ Vivarium.observe(filter: FILTER) do
46
+ puts "[env-external-demo] spawning external child"
47
+ pid = Process.spawn(RbConfig.ruby, "-e", CHILD_CODE)
48
+ Process.wait(pid)
49
+ puts "[env-external-demo] child exit status=#{Process.last_status.exitstatus}"
50
+ end
51
+
52
+ puts "[env-external-demo] done"
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "vivarium"
5
+
6
+ # Usage:
7
+ # 1) In another shell (root): sudo bundle exec vivariumd
8
+ # 2) Run this script: bundle exec ruby examples/env_access_ruby_demo.rb
9
+ #
10
+ # This demo intentionally triggers Ruby-side ENV access methods so they are
11
+ # observed through TracePoint -> SPAN (USDT) path.
12
+
13
+ FILTER = {
14
+ include_events: %w[span_start span_stop env_caccess]
15
+ }.freeze
16
+
17
+ def safe_fetch(key)
18
+ ENV.fetch(key)
19
+ rescue KeyError
20
+ nil
21
+ end
22
+
23
+ def demo_env_reads
24
+ ENV["HOME"]
25
+ safe_fetch("PATH")
26
+ ENV.key?("SHELL")
27
+ end
28
+
29
+ def demo_env_writes
30
+ ENV["VIVARIUM_ENV_DEMO_A"] = "1"
31
+ ENV.store("VIVARIUM_ENV_DEMO_B", "2")
32
+ ENV.delete("VIVARIUM_ENV_DEMO_A")
33
+ ENV.replace(ENV.to_h.merge("VIVARIUM_ENV_DEMO_C" => "3"))
34
+ ENV.delete("VIVARIUM_ENV_DEMO_B")
35
+ ENV.delete("VIVARIUM_ENV_DEMO_C")
36
+ end
37
+
38
+ Vivarium.observe(filter: FILTER) do
39
+ original_env = ENV.to_h
40
+
41
+ puts "[env-ruby-demo] read methods"
42
+ demo_env_reads
43
+
44
+ puts "[env-ruby-demo] write methods"
45
+ demo_env_writes
46
+
47
+ puts "[env-ruby-demo] clear"
48
+ ENV.clear
49
+ ENV.replace(original_env)
50
+ end
51
+
52
+ puts "[env-ruby-demo] done"
@@ -23,17 +23,15 @@ module Vivarium
23
23
 
24
24
  POLL_TIMEOUT_MS = 200
25
25
 
26
- def initialize(pin_dir:, observer_pid:, main_tid:, method_id_queue:, filter: nil, dest: $stdout)
26
+ def initialize(pin_dir:, observer_pid:, main_tid:, filter: nil, dest: $stdout)
27
27
  @pin_dir = pin_dir
28
28
  @observer_pid = observer_pid
29
29
  @main_tid = main_tid
30
- @method_id_queue = method_id_queue
31
30
  @filter = filter
32
31
  @dest = dest
33
32
 
34
33
  @events = []
35
34
  @events_mutex = Mutex.new
36
- @method_table = {}
37
35
  @stop_flag = false
38
36
  @started = false
39
37
 
@@ -66,15 +64,12 @@ module Vivarium
66
64
  @session_stop_ktime = Vivarium.monotonic_ktime_ns
67
65
 
68
66
  3.times { safe_poll(50) }
69
- drain_method_id_queue
70
67
 
71
68
  events_snapshot = @events_mutex.synchronize { @events.dup }
72
- method_table_snapshot = @method_table.dup
73
69
  @stopped = true
74
70
 
75
71
  TreeRenderer.new(
76
72
  events: events_snapshot,
77
- method_table: method_table_snapshot,
78
73
  observer_pid: @observer_pid,
79
74
  main_tid: @main_tid,
80
75
  session_start_iso: @session_start_iso,
@@ -91,7 +86,6 @@ module Vivarium
91
86
  def run
92
87
  until @stop_flag
93
88
  safe_poll(POLL_TIMEOUT_MS)
94
- drain_method_id_queue
95
89
  end
96
90
  end
97
91
 
@@ -126,17 +120,5 @@ module Vivarium
126
120
  warn "[vivarium correlator] capture error: #{e.class}: #{e.message}"
127
121
  end
128
122
 
129
- def drain_method_id_queue
130
- loop do
131
- msg = begin
132
- @method_id_queue.pop(true)
133
- rescue ThreadError
134
- return
135
- end
136
-
137
- method_id, signature = msg
138
- @method_table[method_id] = signature
139
- end
140
- end
141
123
  end
142
124
  end
@@ -28,7 +28,7 @@ module Vivarium
28
28
  UNRESOLVED_METHOD_PREFIX = "<method_id="
29
29
 
30
30
  Span = Struct.new(
31
- :tid, :method_id, :file_id, :lineno, :start_ktime, :stop_ktime, :exit_kind,
31
+ :tid, :method_name, :file_name, :lineno, :start_ktime, :stop_ktime, :exit_kind,
32
32
  :events, :descendant_pids, :synthetic, :raised,
33
33
  keyword_init: true
34
34
  ) do
@@ -52,11 +52,10 @@ module Vivarium
52
52
  EventNode = Struct.new(:kind, :name, :target, :offset_ns, :child_proc, keyword_init: true)
53
53
  ProcNode = Struct.new(:pid, :comm, :parent_pid, :children, keyword_init: true)
54
54
 
55
- def initialize(events:, method_table:, observer_pid:, main_tid:,
55
+ def initialize(events:, observer_pid:, main_tid:,
56
56
  session_start_iso:, session_start_ktime:,
57
57
  session_stop_iso:, session_stop_ktime:, filter: nil, dest:)
58
58
  @events = events
59
- @method_table = method_table
60
59
  @observer_pid = observer_pid
61
60
  @main_tid = main_tid
62
61
  @session_start_iso = session_start_iso
@@ -68,7 +67,6 @@ module Vivarium
68
67
 
69
68
  @pid_comm = { observer_pid => "ruby" }
70
69
  @pid_parent = {}
71
- @unresolved_method_ids = []
72
70
  end
73
71
 
74
72
  def render
@@ -101,11 +99,11 @@ module Vivarium
101
99
  events.each do |ev|
102
100
  case ev.event_name
103
101
  when "span_start"
104
- mid, fid, lno = read_span_payload(ev.payload)
102
+ method_name, file_name, lno = read_span_payload(ev.payload)
105
103
  span = Span.new(
106
104
  tid: ev.tid,
107
- method_id: mid,
108
- file_id: fid,
105
+ method_name: method_name,
106
+ file_name: file_name,
109
107
  lineno: lno,
110
108
  start_ktime: ev.ktime_ns,
111
109
  stop_ktime: nil,
@@ -195,8 +193,8 @@ module Vivarium
195
193
  def synthetic_span(start_ktime, stop_ktime)
196
194
  Span.new(
197
195
  tid: @main_tid,
198
- method_id: nil,
199
- file_id: nil,
196
+ method_name: nil,
197
+ file_name: nil,
200
198
  lineno: nil,
201
199
  start_ktime: start_ktime,
202
200
  stop_ktime: stop_ktime,
@@ -262,9 +260,6 @@ module Vivarium
262
260
  end
263
261
 
264
262
  def print_warnings
265
- @unresolved_method_ids.uniq.each do |mid|
266
- @dest.puts format("# warning method_id=0x%016X unresolved at render time", mid & 0xFFFF_FFFF_FFFF_FFFF)
267
- end
268
263
  end
269
264
 
270
265
  def print_observer_proc(spans)
@@ -301,25 +296,17 @@ module Vivarium
301
296
 
302
297
  def span_file_info(span)
303
298
  return "" if span.synthetic
304
- return "" unless span.file_id && span.file_id != -1
305
-
306
- file_name = Vivarium::Usdt.get_file_name(span.file_id)
307
- return "" unless file_name
299
+ return "" if span.file_name.nil? || span.file_name.empty?
308
300
 
309
301
  lno = span.lineno && span.lineno > 0 ? ":#{span.lineno}" : ""
310
- " at=#{File.basename(file_name)}#{lno}"
302
+ " at=#{File.basename(span.file_name)}#{lno}"
311
303
  end
312
304
 
313
305
  def span_display_name(span)
314
306
  return SYNTHETIC_SPAN_NAME if span.synthetic
315
- return SYNTHETIC_SPAN_NAME if span.method_id.nil?
316
-
317
- name = @method_table[span.method_id]
318
- name ||= Vivarium::Usdt.get_method_name(span.method_id)
319
- return name if name
307
+ return SYNTHETIC_SPAN_NAME if span.method_name.nil? || span.method_name.empty?
320
308
 
321
- @unresolved_method_ids << span.method_id
322
- format("#{UNRESOLVED_METHOD_PREFIX}0x%016X>", span.method_id & 0xFFFF_FFFF_FFFF_FFFF)
309
+ span.method_name
323
310
  end
324
311
 
325
312
  def build_span_children(span)
@@ -548,28 +535,20 @@ module Vivarium
548
535
 
549
536
  def render_raise_target(ev)
550
537
  bytes = ev.payload.to_s.b
551
- return "-" if bytes.bytesize < 8
552
-
553
- error_id = bytes[0, 8].unpack1("q<")
554
- message_id = bytes.bytesize >= 16 ? bytes[8, 8].unpack1("q<") : -1
555
- file_id = bytes.bytesize >= 24 ? bytes[16, 8].unpack1("q<") : -1
556
- lineno = bytes.bytesize >= 32 ? bytes[24, 8].unpack1("q<") : -1
557
-
558
- error_name = Vivarium::Usdt.get_error_name(error_id) ||
559
- format("0x%016X", error_id & 0xFFFF_FFFF_FFFF_FFFF)
560
-
561
- parts = ["error=#{error_name}"]
562
-
563
- if message_id != -1
564
- msg = Vivarium::Usdt.get_message_name(message_id)
565
- parts << "message=#{msg.inspect}" if msg
566
- end
567
-
568
- if file_id != -1 && (file_name = Vivarium::Usdt.get_file_name(file_id))
569
- lno = lineno && lineno > 0 ? ":#{lineno}" : ""
538
+ return "-" if bytes.empty?
539
+
540
+ slot = Vivarium::SPAN_RAISE_SLOT_SIZE
541
+ error_name = Vivarium.c_string(bytes[0, slot])
542
+ message = Vivarium.c_string(bytes[slot, slot])
543
+ file_name = Vivarium.c_string(bytes[slot * 2, slot])
544
+ lineno = bytes.bytesize > Vivarium::SPAN_RAISE_LINENO_OFFSET ? bytes[Vivarium::SPAN_RAISE_LINENO_OFFSET, 8].unpack1("q<") : -1
545
+
546
+ parts = ["error=#{error_name.empty? ? '?' : error_name}"]
547
+ parts << "message=#{message.inspect}" unless message.empty?
548
+ unless file_name.empty?
549
+ lno = lineno > 0 ? ":#{lineno}" : ""
570
550
  parts << "at=#{File.basename(file_name)}#{lno}"
571
551
  end
572
-
573
552
  parts.join(" ")
574
553
  end
575
554
 
@@ -589,12 +568,12 @@ module Vivarium
589
568
 
590
569
  def read_span_payload(payload)
591
570
  bytes = payload.to_s.b
592
- return [0, -1, -1] if bytes.bytesize < 8
571
+ return [nil, nil, -1] if bytes.empty?
593
572
 
594
- method_id = bytes[0, 8].unpack1("q<")
595
- file_id = bytes.bytesize >= 16 ? bytes[8, 8].unpack1("q<") : -1
596
- lineno = bytes.bytesize >= 24 ? bytes[16, 8].unpack1("q<") : -1
597
- [method_id, file_id, lineno]
573
+ method_name = Vivarium.c_string(bytes[0, Vivarium::SPAN_METHOD_SIZE])
574
+ file_name = Vivarium.c_string(bytes[Vivarium::SPAN_METHOD_SIZE, Vivarium::SPAN_FILE_SIZE])
575
+ lineno = bytes.bytesize > Vivarium::SPAN_LINENO_OFFSET ? bytes[Vivarium::SPAN_LINENO_OFFSET, 8].unpack1("q<") : -1
576
+ [method_name, file_name, lineno]
598
577
  end
599
578
 
600
579
  def read_proc_fork_child_pid(payload)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vivarium
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/vivarium.rb CHANGED
@@ -33,6 +33,15 @@ module Vivarium
33
33
  EVENT_DROPPED_OFFSET = 288
34
34
  EVENTS_RINGBUF_PAGES = 256
35
35
 
36
+ SPAN_METHOD_SIZE = 128
37
+ SPAN_FILE_SIZE = 120
38
+ SPAN_LINENO_OFFSET = SPAN_METHOD_SIZE + SPAN_FILE_SIZE # 248
39
+ SPAN_FILE_ARG_MAX = SPAN_FILE_SIZE - 1
40
+
41
+ SPAN_RAISE_SLOT_SIZE = 80
42
+ SPAN_RAISE_LINENO_OFFSET = SPAN_RAISE_SLOT_SIZE * 3 # 240
43
+ SPAN_RAISE_FILE_ARG_MAX = SPAN_RAISE_SLOT_SIZE - 1
44
+
36
45
  SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET = 0
37
46
  SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET = 4
38
47
  SSL_WRITE_PAYLOAD_DATA_OFFSET = 8
@@ -86,7 +95,20 @@ module Vivarium
86
95
  "Kernel#eval",
87
96
  "Object#instance_eval",
88
97
  "Object#instance_exec",
98
+ "ENV#[]",
99
+ "ENV#fetch",
100
+ "ENV#key?",
101
+ "ENV#[]=",
102
+ "ENV#store",
103
+ "ENV#delete",
104
+ "ENV#clear",
105
+ "ENV#replace",
89
106
  ].freeze
107
+
108
+ ENV_PAYLOAD_OP_SIZE = 16
109
+ ENV_PAYLOAD_KEY_OFFSET = ENV_PAYLOAD_OP_SIZE
110
+ ENV_PAYLOAD_KEY_SIZE = EVENT_PAYLOAD_SIZE - ENV_PAYLOAD_KEY_OFFSET
111
+
90
112
  EVENT_SEVERITY_HIGH = %w[
91
113
  capable_check bprm_creds setid_change task_kill
92
114
  ptrace_check sb_mount kernel_read_file
@@ -149,6 +171,16 @@ module Vivarium
149
171
  str[0, nul]
150
172
  end
151
173
 
174
+ def self.tail_fit_string(value, max_bytes, marker: "...")
175
+ str = value.to_s.b
176
+ return str if str.bytesize <= max_bytes
177
+ return str.byteslice(-max_bytes, max_bytes) || "" if max_bytes <= marker.bytesize
178
+
179
+ tail_size = max_bytes - marker.bytesize
180
+ tail = str.byteslice(-tail_size, tail_size) || ""
181
+ "#{marker}#{tail}"
182
+ end
183
+
152
184
  def self.event_severity(event_name)
153
185
  EVENT_SEVERITY_HIGH.include?(event_name.to_s) ? "high" : "medium"
154
186
  end
@@ -388,6 +420,20 @@ module Vivarium
388
420
  { data_len: data_len, cap_len: cap_len, data: data }
389
421
  end
390
422
 
423
+ def self.decode_env_payload(raw_payload)
424
+ bytes = raw_payload.to_s.b
425
+ return "" if bytes.bytesize < ENV_PAYLOAD_OP_SIZE
426
+
427
+ op = c_string(bytes[0, ENV_PAYLOAD_OP_SIZE])
428
+ key = c_string(bytes[ENV_PAYLOAD_KEY_OFFSET, ENV_PAYLOAD_KEY_SIZE])
429
+
430
+ return "" if op.empty?
431
+ return "op=#{op}" if key.empty?
432
+
433
+ key = key.split("=", 2).first if op == "putenv"
434
+ "op=#{op} key=#{key.inspect}"
435
+ end
436
+
391
437
  def self.decode_span_raise_payload(raw_payload)
392
438
  bytes = raw_payload.to_s.b
393
439
  return "" if bytes.bytesize < 8
@@ -475,6 +521,9 @@ module Vivarium
475
521
  when "ssl_write"
476
522
  decoded = decode_ssl_write_payload(event.payload)
477
523
  "data_len=#{decoded[:data_len]} cap_len=#{decoded[:cap_len]}"
524
+ when "env_caccess"
525
+ decoded = decode_env_payload(event.payload)
526
+ decoded.empty? ? event.payload.inspect : decoded
478
527
  when "dlopen", "mmap_exec"
479
528
  strip_to_first_null(event.payload).inspect
480
529
  else
@@ -707,6 +756,26 @@ module Vivarium
707
756
  events.ringbuf_submit(ev, 0);
708
757
  }
709
758
 
759
+ static __always_inline void submit_env_event(u32 pid, const char *op, u32 op_len, const char *name_ptr)
760
+ {
761
+ struct event_t ev = {};
762
+ ev.pid = pid;
763
+ __builtin_memcpy(ev.event_name, "env_caccess", 12);
764
+
765
+ if (op && op_len > 0) {
766
+ if (op_len > #{ENV_PAYLOAD_OP_SIZE} - 1) {
767
+ op_len = #{ENV_PAYLOAD_OP_SIZE} - 1;
768
+ }
769
+ __builtin_memcpy(&ev.payload[0], op, op_len);
770
+ }
771
+
772
+ if (name_ptr) {
773
+ bpf_probe_read_user_str(&ev.payload[#{ENV_PAYLOAD_KEY_OFFSET}], #{ENV_PAYLOAD_KEY_SIZE}, name_ptr);
774
+ }
775
+
776
+ submit_event(&ev);
777
+ }
778
+
710
779
  static __always_inline int is_dns_destination(void *addr)
711
780
  {
712
781
  u16 family = 0;
@@ -1394,19 +1463,19 @@ module Vivarium
1394
1463
  return 0;
1395
1464
  }
1396
1465
 
1397
- u64 method_id = 0;
1398
- u64 file_id = 0;
1399
- u64 lineno = 0;
1400
- bpf_usdt_readarg(1, ctx, &method_id);
1401
- bpf_usdt_readarg(2, ctx, &file_id);
1466
+ u64 method_str_ptr = 0;
1467
+ u64 file_str_ptr = 0;
1468
+ s64 lineno = 0;
1469
+ bpf_usdt_readarg(1, ctx, &method_str_ptr);
1470
+ bpf_usdt_readarg(2, ctx, &file_str_ptr);
1402
1471
  bpf_usdt_readarg(3, ctx, &lineno);
1403
1472
 
1404
1473
  struct event_t ev = {};
1405
1474
  ev.pid = pid;
1406
1475
  __builtin_memcpy(ev.event_name, "span_start", 11);
1407
- __builtin_memcpy(&ev.payload[0], &method_id, sizeof(method_id));
1408
- __builtin_memcpy(&ev.payload[8], &file_id, sizeof(file_id));
1409
- __builtin_memcpy(&ev.payload[16], &lineno, sizeof(lineno));
1476
+ bpf_probe_read_user_str(&ev.payload[0], #{SPAN_METHOD_SIZE}, (void*)method_str_ptr);
1477
+ bpf_probe_read_user_str(&ev.payload[#{SPAN_METHOD_SIZE}], #{SPAN_FILE_SIZE}, (void*)file_str_ptr);
1478
+ __builtin_memcpy(&ev.payload[#{SPAN_LINENO_OFFSET}], &lineno, sizeof(lineno));
1410
1479
  submit_event(&ev);
1411
1480
  return 0;
1412
1481
  }
@@ -1421,19 +1490,19 @@ module Vivarium
1421
1490
  return 0;
1422
1491
  }
1423
1492
 
1424
- u64 method_id = 0;
1425
- u64 file_id = 0;
1426
- u64 lineno = 0;
1427
- bpf_usdt_readarg(1, ctx, &method_id);
1428
- bpf_usdt_readarg(2, ctx, &file_id);
1493
+ u64 method_str_ptr = 0;
1494
+ u64 file_str_ptr = 0;
1495
+ s64 lineno = 0;
1496
+ bpf_usdt_readarg(1, ctx, &method_str_ptr);
1497
+ bpf_usdt_readarg(2, ctx, &file_str_ptr);
1429
1498
  bpf_usdt_readarg(3, ctx, &lineno);
1430
1499
 
1431
1500
  struct event_t ev = {};
1432
1501
  ev.pid = pid;
1433
1502
  __builtin_memcpy(ev.event_name, "span_stop", 10);
1434
- __builtin_memcpy(&ev.payload[0], &method_id, sizeof(method_id));
1435
- __builtin_memcpy(&ev.payload[8], &file_id, sizeof(file_id));
1436
- __builtin_memcpy(&ev.payload[16], &lineno, sizeof(lineno));
1503
+ bpf_probe_read_user_str(&ev.payload[0], #{SPAN_METHOD_SIZE}, (void*)method_str_ptr);
1504
+ bpf_probe_read_user_str(&ev.payload[#{SPAN_METHOD_SIZE}], #{SPAN_FILE_SIZE}, (void*)file_str_ptr);
1505
+ __builtin_memcpy(&ev.payload[#{SPAN_LINENO_OFFSET}], &lineno, sizeof(lineno));
1437
1506
  submit_event(&ev);
1438
1507
  return 0;
1439
1508
  }
@@ -1500,6 +1569,80 @@ module Vivarium
1500
1569
  return 0;
1501
1570
  }
1502
1571
 
1572
+ int on_getenv(struct pt_regs *ctx)
1573
+ {
1574
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1575
+ u32 pid = pid_tgid >> 32;
1576
+ u32 tid = (u32)pid_tgid;
1577
+ const char *name = (const char *)PT_REGS_PARM1(ctx);
1578
+
1579
+ if (!target_enabled(pid, tid) || !name) {
1580
+ return 0;
1581
+ }
1582
+
1583
+ submit_env_event(pid, "getenv", 6, name);
1584
+ return 0;
1585
+ }
1586
+
1587
+ int on_setenv(struct pt_regs *ctx)
1588
+ {
1589
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1590
+ u32 pid = pid_tgid >> 32;
1591
+ u32 tid = (u32)pid_tgid;
1592
+ const char *name = (const char *)PT_REGS_PARM1(ctx);
1593
+
1594
+ if (!target_enabled(pid, tid) || !name) {
1595
+ return 0;
1596
+ }
1597
+
1598
+ submit_env_event(pid, "setenv", 6, name);
1599
+ return 0;
1600
+ }
1601
+
1602
+ int on_unsetenv(struct pt_regs *ctx)
1603
+ {
1604
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1605
+ u32 pid = pid_tgid >> 32;
1606
+ u32 tid = (u32)pid_tgid;
1607
+ const char *name = (const char *)PT_REGS_PARM1(ctx);
1608
+
1609
+ if (!target_enabled(pid, tid) || !name) {
1610
+ return 0;
1611
+ }
1612
+
1613
+ submit_env_event(pid, "unsetenv", 8, name);
1614
+ return 0;
1615
+ }
1616
+
1617
+ int on_putenv(struct pt_regs *ctx)
1618
+ {
1619
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1620
+ u32 pid = pid_tgid >> 32;
1621
+ u32 tid = (u32)pid_tgid;
1622
+ const char *string = (const char *)PT_REGS_PARM1(ctx);
1623
+
1624
+ if (!target_enabled(pid, tid) || !string) {
1625
+ return 0;
1626
+ }
1627
+
1628
+ submit_env_event(pid, "putenv", 6, string);
1629
+ return 0;
1630
+ }
1631
+
1632
+ int on_clearenv(struct pt_regs *ctx)
1633
+ {
1634
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1635
+ u32 pid = pid_tgid >> 32;
1636
+ u32 tid = (u32)pid_tgid;
1637
+
1638
+ if (!target_enabled(pid, tid)) {
1639
+ return 0;
1640
+ }
1641
+
1642
+ submit_env_event(pid, "clearenv", 8, 0);
1643
+ return 0;
1644
+ }
1645
+
1503
1646
  int on_span_raise(struct pt_regs *ctx)
1504
1647
  {
1505
1648
  u64 pid_tgid = bpf_get_current_pid_tgid();
@@ -1510,33 +1653,34 @@ module Vivarium
1510
1653
  return 0;
1511
1654
  }
1512
1655
 
1513
- u64 error_id = 0;
1514
- u64 message_id = 0;
1515
- u64 file_id = 0;
1516
- u64 lineno = 0;
1517
- bpf_usdt_readarg(1, ctx, &error_id);
1518
- bpf_usdt_readarg(2, ctx, &message_id);
1519
- bpf_usdt_readarg(3, ctx, &file_id);
1656
+ u64 error_str_ptr = 0;
1657
+ u64 message_str_ptr = 0;
1658
+ u64 file_str_ptr = 0;
1659
+ s64 lineno = 0;
1660
+ bpf_usdt_readarg(1, ctx, &error_str_ptr);
1661
+ bpf_usdt_readarg(2, ctx, &message_str_ptr);
1662
+ bpf_usdt_readarg(3, ctx, &file_str_ptr);
1520
1663
  bpf_usdt_readarg(4, ctx, &lineno);
1521
1664
 
1522
1665
  struct event_t ev = {};
1523
1666
  ev.pid = pid;
1524
1667
  __builtin_memcpy(ev.event_name, "span_raise", 11);
1525
- __builtin_memcpy(&ev.payload[0], &error_id, sizeof(error_id));
1526
- __builtin_memcpy(&ev.payload[8], &message_id, sizeof(message_id));
1527
- __builtin_memcpy(&ev.payload[16], &file_id, sizeof(file_id));
1528
- __builtin_memcpy(&ev.payload[24], &lineno, sizeof(lineno));
1668
+ bpf_probe_read_user_str(&ev.payload[0], #{SPAN_RAISE_SLOT_SIZE}, (void*)error_str_ptr);
1669
+ bpf_probe_read_user_str(&ev.payload[#{SPAN_RAISE_SLOT_SIZE}], #{SPAN_RAISE_SLOT_SIZE}, (void*)message_str_ptr);
1670
+ bpf_probe_read_user_str(&ev.payload[#{SPAN_RAISE_SLOT_SIZE * 2}], #{SPAN_RAISE_SLOT_SIZE}, (void*)file_str_ptr);
1671
+ __builtin_memcpy(&ev.payload[#{SPAN_RAISE_LINENO_OFFSET}], &lineno, sizeof(lineno));
1529
1672
  submit_event(&ev);
1530
1673
  return 0;
1531
1674
  }
1532
1675
  CLANG
1533
1676
 
1534
1677
  def initialize(pin_dir: Vivarium.bpf_pin_dir, ssl_trace: true, libssl_path: nil,
1535
- dlopen_trace: true, libc_path: nil)
1678
+ dlopen_trace: true, env_trace: true, libc_path: nil)
1536
1679
  @pin_dir = pin_dir
1537
1680
  @ssl_trace = ssl_trace
1538
1681
  @libssl_path = libssl_path
1539
1682
  @dlopen_trace = dlopen_trace
1683
+ @env_trace = env_trace
1540
1684
  @libc_path = libc_path
1541
1685
  end
1542
1686
 
@@ -1561,6 +1705,7 @@ module Vivarium
1561
1705
 
1562
1706
  attach_ssl_write_uprobe(bpf) if @ssl_trace
1563
1707
  attach_dlopen_uprobe(bpf) if @dlopen_trace
1708
+ attach_env_uprobes(bpf) if @env_trace
1564
1709
 
1565
1710
  config_root_targets = bpf["config_root_targets"]
1566
1711
  config_spawned_targets = bpf["config_spawned_targets"]
@@ -1633,6 +1778,30 @@ module Vivarium
1633
1778
  warn "[vivariumd] dlopen uprobe attach failed: #{e.class}: #{e.message}"
1634
1779
  end
1635
1780
 
1781
+ def attach_env_uprobes(bpf)
1782
+ path = resolve_libc_path
1783
+ unless path
1784
+ warn "[vivariumd] libc not found; ENV uprobes disabled " \
1785
+ "(set --libc PATH or VIVARIUM_LIBC_PATH to override)"
1786
+ return
1787
+ end
1788
+
1789
+ {
1790
+ "getenv" => "on_getenv",
1791
+ "setenv" => "on_setenv",
1792
+ "unsetenv" => "on_unsetenv",
1793
+ "putenv" => "on_putenv",
1794
+ "clearenv" => "on_clearenv"
1795
+ }.each do |sym, fn_name|
1796
+ begin
1797
+ bpf.attach_uprobe(name: path, sym: sym, fn_name: fn_name)
1798
+ puts "[vivariumd] #{sym} uprobe attached via #{path}"
1799
+ rescue StandardError => e
1800
+ warn "[vivariumd] #{sym} uprobe attach failed: #{e.class}: #{e.message}"
1801
+ end
1802
+ end
1803
+ end
1804
+
1636
1805
  def resolve_libc_path
1637
1806
  if @libc_path
1638
1807
  return @libc_path if File.exist?(@libc_path)
@@ -1806,20 +1975,18 @@ module Vivarium
1806
1975
  pid = Process.pid
1807
1976
  store.register_pid(pid)
1808
1977
 
1809
- method_id_queue = Thread::Queue.new
1810
1978
  main_tid = gettid
1811
1979
 
1812
1980
  correlator = Correlator.new(
1813
1981
  pin_dir: pin_dir,
1814
1982
  observer_pid: pid,
1815
1983
  main_tid: main_tid,
1816
- method_id_queue: method_id_queue,
1817
1984
  filter: filter,
1818
1985
  dest: dest
1819
1986
  )
1820
1987
  correlator.start
1821
1988
 
1822
- tracer = build_observe_tracepoint(method_id_queue)
1989
+ tracer = build_observe_tracepoint
1823
1990
  tracer.enable
1824
1991
 
1825
1992
  session = ObservationSession.new(
@@ -1836,20 +2003,18 @@ module Vivarium
1836
2003
  pid = Process.pid
1837
2004
  store.register_pid(pid)
1838
2005
 
1839
- method_id_queue = Thread::Queue.new
1840
2006
  main_tid = gettid
1841
2007
 
1842
2008
  correlator = Correlator.new(
1843
2009
  pin_dir: pin_dir,
1844
2010
  observer_pid: pid,
1845
2011
  main_tid: main_tid,
1846
- method_id_queue: method_id_queue,
1847
2012
  filter: filter,
1848
2013
  dest: dest
1849
2014
  )
1850
2015
  correlator.start
1851
2016
 
1852
- tracer = build_observe_tracepoint(method_id_queue)
2017
+ tracer = build_observe_tracepoint
1853
2018
  tracer.enable
1854
2019
 
1855
2020
  yield
@@ -1859,37 +2024,41 @@ module Vivarium
1859
2024
  correlator&.stop
1860
2025
  end
1861
2026
 
1862
- def self.build_observe_tracepoint(method_id_queue)
2027
+ def self.build_observe_tracepoint
1863
2028
  allow_classes = SPAN_ALLOWCLASSES
1864
2029
  allowlist = SPAN_ALLOWLIST
1865
2030
  TracePoint.new(:call, :c_call, :return, :c_return, :raise) do |tp|
1866
2031
  if tp.event == :raise
1867
2032
  # FIXME: handle threaded events in the future
1868
- if tp.raised_exception.kind_of?(ThreadError)
1869
- next
1870
- end
2033
+ next if tp.raised_exception.kind_of?(ThreadError)
1871
2034
 
2035
+ file_arg = tail_fit_string(tp.path, SPAN_RAISE_FILE_ARG_MAX)
1872
2036
  Vivarium::Usdt.raise(
1873
2037
  tp.raised_exception.class.to_s,
1874
2038
  tp.raised_exception.message.to_s,
1875
- file: tp.path,
2039
+ file: file_arg,
1876
2040
  lineno: tp.lineno
1877
2041
  )
1878
2042
  next
1879
2043
  end
1880
2044
 
1881
- signature = "#{tp.defined_class}##{tp.method_id}"
2045
+ signature = if tp.self.equal?(ENV)
2046
+ "ENV##{tp.method_id}"
2047
+ else
2048
+ "#{tp.defined_class}##{tp.method_id}"
2049
+ end
1882
2050
  is_target = allowlist.include?(signature) || \
1883
2051
  allow_classes.any? { |klass| tp.defined_class == klass } || \
1884
2052
  allow_classes.any? { |klass| tp.defined_class == klass.singleton_class }
1885
2053
  next unless is_target
1886
2054
 
2055
+ file_arg = tail_fit_string(tp.path, SPAN_FILE_ARG_MAX)
2056
+ span_class_name = tp.self.equal?(ENV) ? "ENV" : tp.defined_class.to_s
1887
2057
  case tp.event
1888
2058
  when :call, :c_call
1889
- method_id = Vivarium::Usdt.start(tp.defined_class.to_s, tp.method_id.to_s, file: tp.path, lineno: tp.lineno)
1890
- method_id_queue << [method_id, signature]
2059
+ Vivarium::Usdt.start(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
1891
2060
  when :return, :c_return
1892
- Vivarium::Usdt.stop(tp.defined_class.to_s, tp.method_id.to_s, file: tp.path, lineno: tp.lineno)
2061
+ Vivarium::Usdt.stop(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
1893
2062
  end
1894
2063
  end
1895
2064
  end
@@ -1921,10 +2090,11 @@ module Vivarium
1921
2090
 
1922
2091
  def self.run_daemon!(argv = ARGV)
1923
2092
  options = { pin_dir: bpf_pin_dir, ssl_trace: true, libssl_path: nil,
2093
+ env_trace: true,
1924
2094
  dlopen_trace: true, libc_path: nil }
1925
2095
  OptionParser.new do |opts|
1926
2096
  opts.banner = "Usage: vivariumd [--pin-dir PATH] [--no-ssl-trace] [--libssl PATH] " \
1927
- "[--no-dlopen-trace] [--libc PATH]"
2097
+ "[--no-dlopen-trace] [--no-env-trace] [--libc PATH]"
1928
2098
  opts.on("--pin-dir PATH", "Pinned map directory") { |v| options[:pin_dir] = v }
1929
2099
  opts.on("--[no-]ssl-trace", "Attach OpenSSL SSL_write uprobe (default: enabled)") do |v|
1930
2100
  options[:ssl_trace] = v
@@ -1935,6 +2105,9 @@ module Vivarium
1935
2105
  opts.on("--[no-]dlopen-trace", "Attach libc dlopen uprobe (default: enabled)") do |v|
1936
2106
  options[:dlopen_trace] = v
1937
2107
  end
2108
+ opts.on("--[no-]env-trace", "Attach libc getenv/setenv uprobes (default: enabled)") do |v|
2109
+ options[:env_trace] = v
2110
+ end
1938
2111
  opts.on("--libc PATH", "Path to libc.so for dlopen uprobe") do |v|
1939
2112
  options[:libc_path] = v
1940
2113
  end
@@ -1945,6 +2118,7 @@ module Vivarium
1945
2118
  ssl_trace: options[:ssl_trace],
1946
2119
  libssl_path: options[:libssl_path],
1947
2120
  dlopen_trace: options[:dlopen_trace],
2121
+ env_trace: options[:env_trace],
1948
2122
  libc_path: options[:libc_path]
1949
2123
  ).run
1950
2124
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vivarium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.3.0
32
+ version: 0.4.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.3.0
39
+ version: 0.4.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: ostruct
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +66,8 @@ files:
66
66
  - Rakefile
67
67
  - examples/dlopen_demo.rb
68
68
  - examples/drop_demo.rb
69
+ - examples/env_access_external_demo.rb
70
+ - examples/env_access_ruby_demo.rb
69
71
  - examples/execve_demo.rb
70
72
  - examples/file_operation_demo.rb
71
73
  - examples/network_client_demo.rb