vivarium 0.4.0 → 0.4.2

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: 6549be32807adc904ffe18fbefc055600cf14c85c68b106a3abfd7dce354e8b1
4
- data.tar.gz: 1f9c961424d4712b2c43ad3df8d81ee679ea2190acaa8ea24e78a4f88b92f452
3
+ metadata.gz: 0a0f8b6dfc29af71d39ff5fe1ebceca9040cf62a2c6147dba5c2a074b392cdc1
4
+ data.tar.gz: c9f10129e5b42fd51653dcda0a02ed0e7be8b4c2ad7daef4f48a9fa5c03dcf41
5
5
  SHA512:
6
- metadata.gz: ebe88587a6328a37703899da2f0c8275f9321d7ea34e1af9806dccfaf929a6ee45bce68378356aeb2171708936b74ebb33545ada8a7eace2774aa60783b31b24
7
- data.tar.gz: f707b190642345628ddf613038942a87cd95585877b45a264e13ffb1c243da0b7576712d6b5e5eb65e336dab06ff95a01f329b383bdb0007aec879629c30d2eb
6
+ metadata.gz: 9530187d826e63976bd7e1c86872345f5cad281608b2ee06f0dbe20e6211455d8d050a18013822f6baecf4c0af18e9e01b58fec7fe13174d9631181df8660569
7
+ data.tar.gz: c3ee802cf168d5ca917c6d9de5cce25c9441ba46a35995bc5e5f78ce6c78055c1a4d8141bff7dd4e7763205d65c1a41344a789185dbea8fc65f4589ef8e90586
data/README.md CHANGED
@@ -139,6 +139,30 @@ 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
+
158
+ 10) Box context demo:
159
+
160
+ ```bash
161
+ bundle exec ruby examples/box_demo.rb
162
+ ```
163
+
164
+ This demo shows how to use `Vivarium::Box` to isolate Ruby code evaluation and trace method calls within that isolated context.
165
+
142
166
  You can also start top-level observation without a block (it keeps observing until process exit):
143
167
 
144
168
  ```ruby
@@ -0,0 +1,59 @@
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/box_demo.rb
9
+ #
10
+ # This demo demonstrates Vivarium::Box usage for automatically tracing
11
+ # method calls within an isolated eval context.
12
+
13
+ FILTER = {
14
+ include_events: %w[span_start span_stop]
15
+ }.freeze
16
+
17
+ # Create a box and define a class within it
18
+ box = Vivarium::Box.new
19
+
20
+ box.eval(<<~RUBY)
21
+ require "net/http"
22
+ class Calculator
23
+ def add(a, b)
24
+ a + b
25
+ end
26
+
27
+ def multiply(a, b)
28
+ a * b
29
+ end
30
+ end
31
+
32
+ class Greeter
33
+ def greet(name)
34
+ system "echo Hello \#{name}!"
35
+ end
36
+ end
37
+
38
+ system "ping -c 1 example.com"
39
+ RUBY
40
+
41
+ box.done_load!
42
+
43
+ # Enable observation - all Box method calls will be automatically traced
44
+ puts "[box-demo] calling box methods with automatic tracing"
45
+
46
+ # Access classes defined in the box through const_missing
47
+ calc = box::Calculator.new
48
+ result1 = calc.add(10, 20)
49
+ puts "[box-demo] calc.add(10, 20) = #{result1}"
50
+
51
+ result2 = calc.multiply(3, 4)
52
+ puts "[box-demo] calc.multiply(3, 4) = #{result2}"
53
+
54
+ greeter = box::Greeter.new
55
+ greeting = greeter.greet("World")
56
+ puts "[box-demo] greeter.greet('World') = #{greeting}"
57
+
58
+ puts "[box-demo] done"
59
+
@@ -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"
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Vivarium
6
+ # Box provides an isolated execution context where method calls are automatically traced
7
+ # through Vivarium's observation system.
8
+ #
9
+ # Usage:
10
+ # box = Vivarium::Box.new
11
+ # box.eval('class MyClass; def foo; "result"; end; end')
12
+ # result = box::MyClass.new.foo # automatically traced if Vivarium.observe is active
13
+ #
14
+ class Box < Module
15
+ DEFAULT_FILTER = {
16
+ include_events: %w[
17
+ proc_fork proc_exec span_start span_stop
18
+ sock_connect dns_req odd_socket
19
+ ssl_write
20
+ dlopen mmap_exec
21
+ task_kill
22
+ setid_change capable_check bprm_creds
23
+ ]
24
+ }
25
+
26
+ def initialize(pin_dir: Vivarium.bpf_pin_dir, dest: $stdout, filter: DEFAULT_FILTER)
27
+ super()
28
+ @inner_box = Ruby::Box.new
29
+ @pin_dir = pin_dir
30
+ @dest = dest
31
+ @filter = filter
32
+ @session = nil
33
+
34
+ @tracing_level = [0]
35
+ # Set up TracePoint to automatically trace method calls within this box
36
+ @tracer = TracePoint.new(:call, :return) do |tp|
37
+ handle_trace_event(tp, @tracing_level, @inner_box)
38
+ end
39
+ end
40
+ attr_reader :inner_box, :tracer
41
+
42
+ # Evaluate code within the box context
43
+ def eval(code)
44
+ result = nil
45
+ Vivarium.observe(filter: @filter) do
46
+ result = @inner_box.eval(code)
47
+ end
48
+ result
49
+ end
50
+
51
+ # Require a file within the box context
52
+ # Automatically traced if Vivarium.observe is active
53
+ def require(path)
54
+ result = nil
55
+ Vivarium.observe(filter: @filter) do
56
+ result = @inner_box.require(path)
57
+ end
58
+ result
59
+ end
60
+
61
+ # Require a file relative to the current file within the box context
62
+ # Automatically traced if Vivarium.observe is active
63
+ def require_relative(path)
64
+ result = nil
65
+ Vivarium.observe(filter: @filter) do
66
+ result = @inner_box.require_relative(path)
67
+ end
68
+ result
69
+ end
70
+
71
+ # Load a file within the box context (executed every time, unlike require)
72
+ # Automatically traced if Vivarium.observe is active
73
+ def load(path, wrap = false)
74
+ result = nil
75
+ Vivarium.observe(filter: @filter) do
76
+ result = @inner_box.load(path, wrap)
77
+ end
78
+ result
79
+ end
80
+
81
+ # Intercept constant access to resolve from the box's evaluated context
82
+ def const_missing(name)
83
+ @inner_box.const_get(name)
84
+ rescue NameError => e
85
+ raise NameError, "#{name} not found in box: #{e.message}"
86
+ end
87
+
88
+ def done_load!
89
+ @tracer.enable
90
+ end
91
+
92
+ private
93
+
94
+ def handle_trace_event(tp, tracing_level, target_box)
95
+ begin
96
+ if should_trace_call?(tp, target_box)
97
+ case tp.event
98
+ when :call
99
+ if tracing_level[0].zero?
100
+ start_vivarium_observation
101
+ end
102
+ tracing_level[0] += 1
103
+ file_arg = Vivarium.tail_fit_string(tp.path, Vivarium::SPAN_FILE_ARG_MAX)
104
+ root = Ruby::Box.root
105
+ root::Vivarium::Usdt.start("#{tp.defined_class}", tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
106
+ when :return
107
+ tracing_level[0] -= 1
108
+ file_arg = Vivarium.tail_fit_string(tp.path, Vivarium::SPAN_FILE_ARG_MAX)
109
+ root = Ruby::Box.root
110
+ root::Vivarium::Usdt.stop("#{tp.defined_class}", tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
111
+ if tracing_level[0].zero?
112
+ stop_vivarium_observation
113
+ end
114
+ end
115
+ end
116
+ rescue StandardError
117
+ # Silently ignore tracing errors to avoid breaking user code
118
+ end
119
+ end
120
+
121
+ def should_trace_call?(tp, target_box)
122
+ tp.binding.eval("Ruby::Box.current") == target_box
123
+ end
124
+
125
+ def start_vivarium_observation
126
+ puts "[debug] Starting Vivarium observation for Box method calls"
127
+ @session = Vivarium.top_observe(pin_dir: @pin_dir, dest: @dest, filter: @filter)
128
+ end
129
+
130
+ def stop_vivarium_observation
131
+ puts "[debug] Stopping Vivarium observation for Box method calls"
132
+ @session&.stop
133
+ @session = nil
134
+ end
135
+ end
136
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vivarium
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.2"
5
5
  end
data/lib/vivarium.rb CHANGED
@@ -7,6 +7,12 @@ require "optparse"
7
7
  require "pathname"
8
8
  require "rbbcc"
9
9
  require "socket"
10
+ if defined?(Ruby) && defined?(Ruby::Box) && Ruby::Box.enabled?
11
+ Ruby::Box.root.require "vivarium_usdt"
12
+ else
13
+ require "vivarium_usdt"
14
+ end
15
+
10
16
  require_relative "vivarium/version"
11
17
  require_relative "vivarium/cli"
12
18
 
@@ -95,7 +101,20 @@ module Vivarium
95
101
  "Kernel#eval",
96
102
  "Object#instance_eval",
97
103
  "Object#instance_exec",
104
+ "ENV#[]",
105
+ "ENV#fetch",
106
+ "ENV#key?",
107
+ "ENV#[]=",
108
+ "ENV#store",
109
+ "ENV#delete",
110
+ "ENV#clear",
111
+ "ENV#replace",
98
112
  ].freeze
113
+
114
+ ENV_PAYLOAD_OP_SIZE = 16
115
+ ENV_PAYLOAD_KEY_OFFSET = ENV_PAYLOAD_OP_SIZE
116
+ ENV_PAYLOAD_KEY_SIZE = EVENT_PAYLOAD_SIZE - ENV_PAYLOAD_KEY_OFFSET
117
+
99
118
  EVENT_SEVERITY_HIGH = %w[
100
119
  capable_check bprm_creds setid_change task_kill
101
120
  ptrace_check sb_mount kernel_read_file
@@ -407,6 +426,20 @@ module Vivarium
407
426
  { data_len: data_len, cap_len: cap_len, data: data }
408
427
  end
409
428
 
429
+ def self.decode_env_payload(raw_payload)
430
+ bytes = raw_payload.to_s.b
431
+ return "" if bytes.bytesize < ENV_PAYLOAD_OP_SIZE
432
+
433
+ op = c_string(bytes[0, ENV_PAYLOAD_OP_SIZE])
434
+ key = c_string(bytes[ENV_PAYLOAD_KEY_OFFSET, ENV_PAYLOAD_KEY_SIZE])
435
+
436
+ return "" if op.empty?
437
+ return "op=#{op}" if key.empty?
438
+
439
+ key = key.split("=", 2).first if op == "putenv"
440
+ "op=#{op} key=#{key.inspect}"
441
+ end
442
+
410
443
  def self.decode_span_raise_payload(raw_payload)
411
444
  bytes = raw_payload.to_s.b
412
445
  return "" if bytes.bytesize < 8
@@ -494,6 +527,9 @@ module Vivarium
494
527
  when "ssl_write"
495
528
  decoded = decode_ssl_write_payload(event.payload)
496
529
  "data_len=#{decoded[:data_len]} cap_len=#{decoded[:cap_len]}"
530
+ when "env_caccess"
531
+ decoded = decode_env_payload(event.payload)
532
+ decoded.empty? ? event.payload.inspect : decoded
497
533
  when "dlopen", "mmap_exec"
498
534
  strip_to_first_null(event.payload).inspect
499
535
  else
@@ -726,6 +762,26 @@ module Vivarium
726
762
  events.ringbuf_submit(ev, 0);
727
763
  }
728
764
 
765
+ static __always_inline void submit_env_event(u32 pid, const char *op, u32 op_len, const char *name_ptr)
766
+ {
767
+ struct event_t ev = {};
768
+ ev.pid = pid;
769
+ __builtin_memcpy(ev.event_name, "env_caccess", 12);
770
+
771
+ if (op && op_len > 0) {
772
+ if (op_len > #{ENV_PAYLOAD_OP_SIZE} - 1) {
773
+ op_len = #{ENV_PAYLOAD_OP_SIZE} - 1;
774
+ }
775
+ __builtin_memcpy(&ev.payload[0], op, op_len);
776
+ }
777
+
778
+ if (name_ptr) {
779
+ bpf_probe_read_user_str(&ev.payload[#{ENV_PAYLOAD_KEY_OFFSET}], #{ENV_PAYLOAD_KEY_SIZE}, name_ptr);
780
+ }
781
+
782
+ submit_event(&ev);
783
+ }
784
+
729
785
  static __always_inline int is_dns_destination(void *addr)
730
786
  {
731
787
  u16 family = 0;
@@ -1519,6 +1575,80 @@ module Vivarium
1519
1575
  return 0;
1520
1576
  }
1521
1577
 
1578
+ int on_getenv(struct pt_regs *ctx)
1579
+ {
1580
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1581
+ u32 pid = pid_tgid >> 32;
1582
+ u32 tid = (u32)pid_tgid;
1583
+ const char *name = (const char *)PT_REGS_PARM1(ctx);
1584
+
1585
+ if (!target_enabled(pid, tid) || !name) {
1586
+ return 0;
1587
+ }
1588
+
1589
+ submit_env_event(pid, "getenv", 6, name);
1590
+ return 0;
1591
+ }
1592
+
1593
+ int on_setenv(struct pt_regs *ctx)
1594
+ {
1595
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1596
+ u32 pid = pid_tgid >> 32;
1597
+ u32 tid = (u32)pid_tgid;
1598
+ const char *name = (const char *)PT_REGS_PARM1(ctx);
1599
+
1600
+ if (!target_enabled(pid, tid) || !name) {
1601
+ return 0;
1602
+ }
1603
+
1604
+ submit_env_event(pid, "setenv", 6, name);
1605
+ return 0;
1606
+ }
1607
+
1608
+ int on_unsetenv(struct pt_regs *ctx)
1609
+ {
1610
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1611
+ u32 pid = pid_tgid >> 32;
1612
+ u32 tid = (u32)pid_tgid;
1613
+ const char *name = (const char *)PT_REGS_PARM1(ctx);
1614
+
1615
+ if (!target_enabled(pid, tid) || !name) {
1616
+ return 0;
1617
+ }
1618
+
1619
+ submit_env_event(pid, "unsetenv", 8, name);
1620
+ return 0;
1621
+ }
1622
+
1623
+ int on_putenv(struct pt_regs *ctx)
1624
+ {
1625
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1626
+ u32 pid = pid_tgid >> 32;
1627
+ u32 tid = (u32)pid_tgid;
1628
+ const char *string = (const char *)PT_REGS_PARM1(ctx);
1629
+
1630
+ if (!target_enabled(pid, tid) || !string) {
1631
+ return 0;
1632
+ }
1633
+
1634
+ submit_env_event(pid, "putenv", 6, string);
1635
+ return 0;
1636
+ }
1637
+
1638
+ int on_clearenv(struct pt_regs *ctx)
1639
+ {
1640
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1641
+ u32 pid = pid_tgid >> 32;
1642
+ u32 tid = (u32)pid_tgid;
1643
+
1644
+ if (!target_enabled(pid, tid)) {
1645
+ return 0;
1646
+ }
1647
+
1648
+ submit_env_event(pid, "clearenv", 8, 0);
1649
+ return 0;
1650
+ }
1651
+
1522
1652
  int on_span_raise(struct pt_regs *ctx)
1523
1653
  {
1524
1654
  u64 pid_tgid = bpf_get_current_pid_tgid();
@@ -1551,11 +1681,12 @@ module Vivarium
1551
1681
  CLANG
1552
1682
 
1553
1683
  def initialize(pin_dir: Vivarium.bpf_pin_dir, ssl_trace: true, libssl_path: nil,
1554
- dlopen_trace: true, libc_path: nil)
1684
+ dlopen_trace: true, env_trace: true, libc_path: nil)
1555
1685
  @pin_dir = pin_dir
1556
1686
  @ssl_trace = ssl_trace
1557
1687
  @libssl_path = libssl_path
1558
1688
  @dlopen_trace = dlopen_trace
1689
+ @env_trace = env_trace
1559
1690
  @libc_path = libc_path
1560
1691
  end
1561
1692
 
@@ -1569,7 +1700,6 @@ module Vivarium
1569
1700
  .gsub("__VIVARIUM_F_PATH_OFFSET__", f_path_offset.to_s)
1570
1701
  .gsub("__VIVARIUM_DENTRY_D_NAME_OFFSET__", d_name_offset.to_s)
1571
1702
 
1572
- require "vivarium_usdt"
1573
1703
  usdt_so_path = ENV.fetch("VIVARIUM_USDT_SO_PATH") { Vivarium.locate_vivarium_usdt_so }
1574
1704
  usdt = RbBCC::USDT.new(path: usdt_so_path)
1575
1705
  usdt.enable_probe(probe: "start_probe", fn_name: "on_span_start")
@@ -1580,6 +1710,7 @@ module Vivarium
1580
1710
 
1581
1711
  attach_ssl_write_uprobe(bpf) if @ssl_trace
1582
1712
  attach_dlopen_uprobe(bpf) if @dlopen_trace
1713
+ attach_env_uprobes(bpf) if @env_trace
1583
1714
 
1584
1715
  config_root_targets = bpf["config_root_targets"]
1585
1716
  config_spawned_targets = bpf["config_spawned_targets"]
@@ -1652,6 +1783,30 @@ module Vivarium
1652
1783
  warn "[vivariumd] dlopen uprobe attach failed: #{e.class}: #{e.message}"
1653
1784
  end
1654
1785
 
1786
+ def attach_env_uprobes(bpf)
1787
+ path = resolve_libc_path
1788
+ unless path
1789
+ warn "[vivariumd] libc not found; ENV uprobes disabled " \
1790
+ "(set --libc PATH or VIVARIUM_LIBC_PATH to override)"
1791
+ return
1792
+ end
1793
+
1794
+ {
1795
+ "getenv" => "on_getenv",
1796
+ "setenv" => "on_setenv",
1797
+ "unsetenv" => "on_unsetenv",
1798
+ "putenv" => "on_putenv",
1799
+ "clearenv" => "on_clearenv"
1800
+ }.each do |sym, fn_name|
1801
+ begin
1802
+ bpf.attach_uprobe(name: path, sym: sym, fn_name: fn_name)
1803
+ puts "[vivariumd] #{sym} uprobe attached via #{path}"
1804
+ rescue StandardError => e
1805
+ warn "[vivariumd] #{sym} uprobe attach failed: #{e.class}: #{e.message}"
1806
+ end
1807
+ end
1808
+ end
1809
+
1655
1810
  def resolve_libc_path
1656
1811
  if @libc_path
1657
1812
  return @libc_path if File.exist?(@libc_path)
@@ -1819,8 +1974,6 @@ module Vivarium
1819
1974
  end
1820
1975
 
1821
1976
  def self.top_observe(pin_dir: bpf_pin_dir, dest: $stdout, filter: nil)
1822
- require "vivarium_usdt"
1823
-
1824
1977
  store = MapStore.new(pin_dir: pin_dir)
1825
1978
  pid = Process.pid
1826
1979
  store.register_pid(pid)
@@ -1847,8 +2000,6 @@ module Vivarium
1847
2000
  end
1848
2001
 
1849
2002
  def self.scoped_observe(pin_dir:, dest:, filter: nil)
1850
- require "vivarium_usdt"
1851
-
1852
2003
  store = MapStore.new(pin_dir: pin_dir)
1853
2004
  pid = Process.pid
1854
2005
  store.register_pid(pid)
@@ -1892,18 +2043,23 @@ module Vivarium
1892
2043
  next
1893
2044
  end
1894
2045
 
1895
- signature = "#{tp.defined_class}##{tp.method_id}"
2046
+ signature = if tp.self.equal?(ENV)
2047
+ "ENV##{tp.method_id}"
2048
+ else
2049
+ "#{tp.defined_class}##{tp.method_id}"
2050
+ end
1896
2051
  is_target = allowlist.include?(signature) || \
1897
2052
  allow_classes.any? { |klass| tp.defined_class == klass } || \
1898
2053
  allow_classes.any? { |klass| tp.defined_class == klass.singleton_class }
1899
2054
  next unless is_target
1900
2055
 
1901
2056
  file_arg = tail_fit_string(tp.path, SPAN_FILE_ARG_MAX)
2057
+ span_class_name = tp.self.equal?(ENV) ? "ENV" : tp.defined_class.to_s
1902
2058
  case tp.event
1903
2059
  when :call, :c_call
1904
- Vivarium::Usdt.start(tp.defined_class.to_s, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
2060
+ Vivarium::Usdt.start(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
1905
2061
  when :return, :c_return
1906
- Vivarium::Usdt.stop(tp.defined_class.to_s, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
2062
+ Vivarium::Usdt.stop(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
1907
2063
  end
1908
2064
  end
1909
2065
  end
@@ -1924,7 +2080,6 @@ module Vivarium
1924
2080
  end
1925
2081
 
1926
2082
  def self.locate_vivarium_usdt_so
1927
- require "vivarium_usdt/vivarium_usdt"
1928
2083
  so = $LOADED_FEATURES.find { |p| p =~ %r{vivarium_usdt/vivarium_usdt\.(so|bundle|dylib)\z} }
1929
2084
  raise Error, "vivarium_usdt native extension not found in $LOADED_FEATURES" unless so
1930
2085
 
@@ -1935,10 +2090,11 @@ module Vivarium
1935
2090
 
1936
2091
  def self.run_daemon!(argv = ARGV)
1937
2092
  options = { pin_dir: bpf_pin_dir, ssl_trace: true, libssl_path: nil,
2093
+ env_trace: true,
1938
2094
  dlopen_trace: true, libc_path: nil }
1939
2095
  OptionParser.new do |opts|
1940
2096
  opts.banner = "Usage: vivariumd [--pin-dir PATH] [--no-ssl-trace] [--libssl PATH] " \
1941
- "[--no-dlopen-trace] [--libc PATH]"
2097
+ "[--no-dlopen-trace] [--no-env-trace] [--libc PATH]"
1942
2098
  opts.on("--pin-dir PATH", "Pinned map directory") { |v| options[:pin_dir] = v }
1943
2099
  opts.on("--[no-]ssl-trace", "Attach OpenSSL SSL_write uprobe (default: enabled)") do |v|
1944
2100
  options[:ssl_trace] = v
@@ -1949,6 +2105,9 @@ module Vivarium
1949
2105
  opts.on("--[no-]dlopen-trace", "Attach libc dlopen uprobe (default: enabled)") do |v|
1950
2106
  options[:dlopen_trace] = v
1951
2107
  end
2108
+ opts.on("--[no-]env-trace", "Attach libc getenv/setenv uprobes (default: enabled)") do |v|
2109
+ options[:env_trace] = v
2110
+ end
1952
2111
  opts.on("--libc PATH", "Path to libc.so for dlopen uprobe") do |v|
1953
2112
  options[:libc_path] = v
1954
2113
  end
@@ -1959,6 +2118,7 @@ module Vivarium
1959
2118
  ssl_trace: options[:ssl_trace],
1960
2119
  libssl_path: options[:libssl_path],
1961
2120
  dlopen_trace: options[:dlopen_trace],
2121
+ env_trace: options[:env_trace],
1962
2122
  libc_path: options[:libc_path]
1963
2123
  ).run
1964
2124
  end
@@ -1967,3 +2127,6 @@ end
1967
2127
  require_relative "vivarium/correlator"
1968
2128
  require_relative "vivarium/display_filter"
1969
2129
  require_relative "vivarium/tree_renderer"
2130
+ if defined?(Ruby) && defined?(Ruby::Box) && Ruby::Box.enabled?
2131
+ require_relative "vivarium/box"
2132
+ 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.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo
@@ -64,8 +64,11 @@ files:
64
64
  - CONTEXT.md
65
65
  - README.md
66
66
  - Rakefile
67
+ - examples/box_demo.rb
67
68
  - examples/dlopen_demo.rb
68
69
  - examples/drop_demo.rb
70
+ - examples/env_access_external_demo.rb
71
+ - examples/env_access_ruby_demo.rb
69
72
  - examples/execve_demo.rb
70
73
  - examples/file_operation_demo.rb
71
74
  - examples/network_client_demo.rb
@@ -78,6 +81,7 @@ files:
78
81
  - exe/vivariumd
79
82
  - image.png
80
83
  - lib/vivarium.rb
84
+ - lib/vivarium/box.rb
81
85
  - lib/vivarium/cli.rb
82
86
  - lib/vivarium/correlator.rb
83
87
  - lib/vivarium/display_filter.rb