vivarium 0.3.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 150b5b98d7555e1954b0002ca2224b1e445b799fa5d163ab4bbcbc8cac42ea74
4
- data.tar.gz: 5984b875d3fab3600c86f9e1d961a0c1ab84945f1c1ea76e78e72248ec2fd9f7
3
+ metadata.gz: 6549be32807adc904ffe18fbefc055600cf14c85c68b106a3abfd7dce354e8b1
4
+ data.tar.gz: 1f9c961424d4712b2c43ad3df8d81ee679ea2190acaa8ea24e78a4f88b92f452
5
5
  SHA512:
6
- metadata.gz: 17847e5f3852628e8bc58f4a31fc4e934b0b06d35d7961ac811d27d34ffcb4a8f17fd9e9c7f61253b9e7d37db09ee9d06354be637f8953c4bd1827baf8d3e0e7
7
- data.tar.gz: 1980ac521ccd83eadb68bbbb5b6cb6a6053f3737c2b8c7aaeded5b9ace3563b34666da472bfe133204caf5889a4e77febea2c9069900e4f040a728a6f609b75a
6
+ metadata.gz: ebe88587a6328a37703899da2f0c8275f9321d7ea34e1af9806dccfaf929a6ee45bce68378356aeb2171708936b74ebb33545ada8a7eace2774aa60783b31b24
7
+ data.tar.gz: f707b190642345628ddf613038942a87cd95585877b45a264e13ffb1c243da0b7576712d6b5e5eb65e336dab06ff95a01f329b383bdb0007aec879629c30d2eb
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fiddle"
5
+ require "vivarium"
6
+
7
+ FILTER = {
8
+ include_events: %w[dlopen mmap_exec]
9
+ }.freeze
10
+
11
+ # Usage:
12
+ # 1) In another shell (root): sudo bundle exec vivariumd
13
+ # (dlopen uprobe is attached automatically when libc is found)
14
+ # 2) Run this script: bundle exec ruby examples/dlopen_demo.rb
15
+ #
16
+ # Expected output: "DL dlopen" and "DL mmap_exec" events for each
17
+ # library loaded via Fiddle.dlopen.
18
+ #
19
+ # You can disable the dlopen uprobe with `sudo vivariumd --no-dlopen-trace`
20
+ # or point at a specific libc with `sudo vivariumd --libc /lib/x86_64-linux-gnu/libc.so.6`.
21
+
22
+ Vivarium.observe(filter: FILTER) do
23
+ # libm: math functions — almost universally available
24
+ begin
25
+ libm = Fiddle.dlopen("libm.so.6")
26
+ sin_fn = Fiddle::Function.new(libm["sin"], [Fiddle::TYPE_DOUBLE], Fiddle::TYPE_DOUBLE)
27
+ puts "[dlopen_demo] sin(PI/4) = #{sin_fn.call(Math::PI / 4).round(6)}"
28
+ libm.close
29
+ rescue Fiddle::DLError => e
30
+ warn "[dlopen_demo] libm: #{e.message}"
31
+ end
32
+
33
+ # libsqlite3: a common library that may not be loaded at startup
34
+ begin
35
+ libsqlite3 = Fiddle.dlopen("libsqlite3.so.0")
36
+ puts "[dlopen_demo] libsqlite3 loaded: version = #{Fiddle::Function.new(libsqlite3["sqlite3_libversion"], [], Fiddle::TYPE_VOIDP).call}"
37
+ libsqlite3.close
38
+ rescue Fiddle::DLError => e
39
+ warn "[dlopen_demo] libsqlite3: #{e.message}"
40
+ end
41
+
42
+ # Spawn a child process that also calls dlopen — its events should
43
+ # appear under a PROC node in the tree (descendant PID tracking).
44
+ # Unbundle so Bundler does not spawn anything.
45
+ Bundler.with_unbundled_env do
46
+ system("ruby -e 'require \"fiddle\"; Fiddle.dlopen(\"libm.so.6\").close'")
47
+ end
48
+ end
49
+
50
+ puts "[dlopen_demo] done"
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Standalone demo: shows what DROP warning nodes look like in the TreeRenderer
5
+ # output. Does NOT require BPF or vivariumd — constructs RawEvent objects
6
+ # directly and feeds them to TreeRenderer.
7
+ #
8
+ # Usage:
9
+ # ruby examples/drop_demo.rb
10
+
11
+ $LOAD_PATH.unshift File.join(__dir__, "..", "lib")
12
+
13
+ require "vivarium"
14
+ require "vivarium/correlator"
15
+ require "vivarium/tree_renderer"
16
+ require "vivarium/display_filter"
17
+
18
+ t0 = 1_000_000_000 # base ktime_ns
19
+ pid = Process.pid
20
+ tid = Process.pid
21
+
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<")
26
+
27
+ events = [
28
+ Vivarium::Correlator::RawEvent.new(
29
+ ktime_ns: t0,
30
+ pid: pid, tid: tid,
31
+ event_name: "span_start",
32
+ payload: span_start_payload,
33
+ dropped_since_last: 0
34
+ ),
35
+ # This event carries drop info: 5 events were lost before it arrived
36
+ Vivarium::Correlator::RawEvent.new(
37
+ ktime_ns: t0 + 10_000_000,
38
+ pid: pid, tid: tid,
39
+ event_name: "path_open",
40
+ payload: "/etc/passwd\x00".b.ljust(Vivarium::EVENT_PAYLOAD_SIZE, "\x00"),
41
+ dropped_since_last: 5
42
+ ),
43
+ Vivarium::Correlator::RawEvent.new(
44
+ ktime_ns: t0 + 20_000_000,
45
+ pid: pid, tid: tid,
46
+ event_name: "dns_req",
47
+ payload: "\x06google\x03com\x00".b.ljust(Vivarium::EVENT_PAYLOAD_SIZE, "\x00"),
48
+ dropped_since_last: 0
49
+ ),
50
+ # Another burst: 12 events dropped just before this sock_connect
51
+ Vivarium::Correlator::RawEvent.new(
52
+ ktime_ns: t0 + 25_000_000,
53
+ pid: pid, tid: tid,
54
+ event_name: "sock_connect",
55
+ payload: [2, 443, 0x7f000001, 0].pack("S<nNN").ljust(Vivarium::EVENT_PAYLOAD_SIZE, "\x00"),
56
+ dropped_since_last: 12
57
+ ),
58
+ Vivarium::Correlator::RawEvent.new(
59
+ ktime_ns: t0 + 30_000_000,
60
+ pid: pid, tid: tid,
61
+ event_name: "span_stop",
62
+ payload: "\x00" * Vivarium::EVENT_PAYLOAD_SIZE,
63
+ dropped_since_last: 0
64
+ ),
65
+ ]
66
+
67
+ Vivarium::TreeRenderer.new(
68
+ events: events,
69
+ observer_pid: pid,
70
+ main_tid: tid,
71
+ session_start_iso: "2026-06-02T00:00:00.000Z",
72
+ session_start_ktime: t0,
73
+ session_stop_iso: "2026-06-02T00:00:00.030Z",
74
+ session_stop_ktime: t0 + 30_000_000,
75
+ dest: $stdout
76
+ ).render
@@ -6,7 +6,7 @@ require "time"
6
6
  module Vivarium
7
7
  class Correlator
8
8
  RawEvent = Struct.new(
9
- :ktime_ns, :pid, :tid, :event_name, :payload,
9
+ :ktime_ns, :pid, :tid, :event_name, :payload, :dropped_since_last,
10
10
  keyword_init: true
11
11
  )
12
12
 
@@ -17,22 +17,21 @@ module Vivarium
17
17
  u32 tid;
18
18
  char event_name[16];
19
19
  char payload[256];
20
+ u64 dropped_since_last;
20
21
  };
21
22
  C
22
23
 
23
24
  POLL_TIMEOUT_MS = 200
24
25
 
25
- 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)
26
27
  @pin_dir = pin_dir
27
28
  @observer_pid = observer_pid
28
29
  @main_tid = main_tid
29
- @method_id_queue = method_id_queue
30
30
  @filter = filter
31
31
  @dest = dest
32
32
 
33
33
  @events = []
34
34
  @events_mutex = Mutex.new
35
- @method_table = {}
36
35
  @stop_flag = false
37
36
  @started = false
38
37
 
@@ -65,15 +64,12 @@ module Vivarium
65
64
  @session_stop_ktime = Vivarium.monotonic_ktime_ns
66
65
 
67
66
  3.times { safe_poll(50) }
68
- drain_method_id_queue
69
67
 
70
68
  events_snapshot = @events_mutex.synchronize { @events.dup }
71
- method_table_snapshot = @method_table.dup
72
69
  @stopped = true
73
70
 
74
71
  TreeRenderer.new(
75
72
  events: events_snapshot,
76
- method_table: method_table_snapshot,
77
73
  observer_pid: @observer_pid,
78
74
  main_tid: @main_tid,
79
75
  session_start_iso: @session_start_iso,
@@ -90,7 +86,6 @@ module Vivarium
90
86
  def run
91
87
  until @stop_flag
92
88
  safe_poll(POLL_TIMEOUT_MS)
93
- drain_method_id_queue
94
89
  end
95
90
  end
96
91
 
@@ -104,11 +99,12 @@ module Vivarium
104
99
  bytes = data[0, size].to_s.b
105
100
  bytes = bytes.ljust(Vivarium::EVENT_STRUCT_SIZE, "\x00") if bytes.bytesize < Vivarium::EVENT_STRUCT_SIZE
106
101
 
107
- ktime_ns = bytes[Vivarium::EVENT_TS_OFFSET, Vivarium::EVENT_TS_SIZE].unpack1("Q<")
108
- pid = bytes[Vivarium::EVENT_PID_OFFSET, 4].unpack1("L<")
109
- tid = bytes[Vivarium::EVENT_TID_OFFSET, 4].unpack1("L<")
110
- event_name = Vivarium.c_string(bytes[Vivarium::EVENT_NAME_OFFSET, Vivarium::EVENT_NAME_SIZE])
111
- payload = bytes[Vivarium::EVENT_PAYLOAD_OFFSET, Vivarium::EVENT_PAYLOAD_SIZE].to_s.b
102
+ ktime_ns = bytes[Vivarium::EVENT_TS_OFFSET, Vivarium::EVENT_TS_SIZE].unpack1("Q<")
103
+ pid = bytes[Vivarium::EVENT_PID_OFFSET, 4].unpack1("L<")
104
+ tid = bytes[Vivarium::EVENT_TID_OFFSET, 4].unpack1("L<")
105
+ event_name = Vivarium.c_string(bytes[Vivarium::EVENT_NAME_OFFSET, Vivarium::EVENT_NAME_SIZE])
106
+ payload = bytes[Vivarium::EVENT_PAYLOAD_OFFSET, Vivarium::EVENT_PAYLOAD_SIZE].to_s.b
107
+ dropped_since_last = bytes[Vivarium::EVENT_DROPPED_OFFSET, 8].unpack1("Q<")
112
108
 
113
109
  @events_mutex.synchronize do
114
110
  @events << RawEvent.new(
@@ -116,24 +112,13 @@ module Vivarium
116
112
  pid: pid,
117
113
  tid: tid,
118
114
  event_name: event_name,
119
- payload: payload
115
+ payload: payload,
116
+ dropped_since_last: dropped_since_last
120
117
  )
121
118
  end
122
119
  rescue StandardError => e
123
120
  warn "[vivarium correlator] capture error: #{e.class}: #{e.message}"
124
121
  end
125
122
 
126
- def drain_method_id_queue
127
- loop do
128
- msg = begin
129
- @method_id_queue.pop(true)
130
- rescue ThreadError
131
- return
132
- end
133
-
134
- method_id, signature = msg
135
- @method_table[method_id] = signature
136
- end
137
- end
138
123
  end
139
124
  end
@@ -22,12 +22,13 @@ module Vivarium
22
22
  ].to_set.freeze
23
23
 
24
24
  UPROBE_EVENT_NAMES = %w[ssl_write].to_set.freeze
25
+ DL_EVENT_NAMES = %w[dlopen mmap_exec].to_set.freeze
25
26
 
26
27
  SYNTHETIC_SPAN_NAME = "<no-span>"
27
28
  UNRESOLVED_METHOD_PREFIX = "<method_id="
28
29
 
29
30
  Span = Struct.new(
30
- :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,
31
32
  :events, :descendant_pids, :synthetic, :raised,
32
33
  keyword_init: true
33
34
  ) do
@@ -51,11 +52,10 @@ module Vivarium
51
52
  EventNode = Struct.new(:kind, :name, :target, :offset_ns, :child_proc, keyword_init: true)
52
53
  ProcNode = Struct.new(:pid, :comm, :parent_pid, :children, keyword_init: true)
53
54
 
54
- def initialize(events:, method_table:, observer_pid:, main_tid:,
55
+ def initialize(events:, observer_pid:, main_tid:,
55
56
  session_start_iso:, session_start_ktime:,
56
57
  session_stop_iso:, session_stop_ktime:, filter: nil, dest:)
57
58
  @events = events
58
- @method_table = method_table
59
59
  @observer_pid = observer_pid
60
60
  @main_tid = main_tid
61
61
  @session_start_iso = session_start_iso
@@ -67,7 +67,6 @@ module Vivarium
67
67
 
68
68
  @pid_comm = { observer_pid => "ruby" }
69
69
  @pid_parent = {}
70
- @unresolved_method_ids = []
71
70
  end
72
71
 
73
72
  def render
@@ -100,11 +99,11 @@ module Vivarium
100
99
  events.each do |ev|
101
100
  case ev.event_name
102
101
  when "span_start"
103
- mid, fid, lno = read_span_payload(ev.payload)
102
+ method_name, file_name, lno = read_span_payload(ev.payload)
104
103
  span = Span.new(
105
104
  tid: ev.tid,
106
- method_id: mid,
107
- file_id: fid,
105
+ method_name: method_name,
106
+ file_name: file_name,
108
107
  lineno: lno,
109
108
  start_ktime: ev.ktime_ns,
110
109
  stop_ktime: nil,
@@ -194,8 +193,8 @@ module Vivarium
194
193
  def synthetic_span(start_ktime, stop_ktime)
195
194
  Span.new(
196
195
  tid: @main_tid,
197
- method_id: nil,
198
- file_id: nil,
196
+ method_name: nil,
197
+ file_name: nil,
199
198
  lineno: nil,
200
199
  start_ktime: start_ktime,
201
200
  stop_ktime: stop_ktime,
@@ -261,9 +260,6 @@ module Vivarium
261
260
  end
262
261
 
263
262
  def print_warnings
264
- @unresolved_method_ids.uniq.each do |mid|
265
- @dest.puts format("# warning method_id=0x%016X unresolved at render time", mid & 0xFFFF_FFFF_FFFF_FFFF)
266
- end
267
263
  end
268
264
 
269
265
  def print_observer_proc(spans)
@@ -300,30 +296,23 @@ module Vivarium
300
296
 
301
297
  def span_file_info(span)
302
298
  return "" if span.synthetic
303
- return "" unless span.file_id && span.file_id != -1
304
-
305
- file_name = Vivarium::Usdt.get_file_name(span.file_id)
306
- return "" unless file_name
299
+ return "" if span.file_name.nil? || span.file_name.empty?
307
300
 
308
301
  lno = span.lineno && span.lineno > 0 ? ":#{span.lineno}" : ""
309
- " at=#{File.basename(file_name)}#{lno}"
302
+ " at=#{File.basename(span.file_name)}#{lno}"
310
303
  end
311
304
 
312
305
  def span_display_name(span)
313
306
  return SYNTHETIC_SPAN_NAME if span.synthetic
314
- return SYNTHETIC_SPAN_NAME if span.method_id.nil?
315
-
316
- name = @method_table[span.method_id]
317
- name ||= Vivarium::Usdt.get_method_name(span.method_id)
318
- return name if name
307
+ return SYNTHETIC_SPAN_NAME if span.method_name.nil? || span.method_name.empty?
319
308
 
320
- @unresolved_method_ids << span.method_id
321
- format("#{UNRESOLVED_METHOD_PREFIX}0x%016X>", span.method_id & 0xFFFF_FFFF_FFFF_FFFF)
309
+ span.method_name
322
310
  end
323
311
 
324
312
  def build_span_children(span)
325
313
  proc_node_by_pid = {}
326
314
  root_children = []
315
+ prev_event_ktime = span.start_ktime
327
316
 
328
317
  span.events.each do |ev|
329
318
  target_text = nil
@@ -353,6 +342,7 @@ module Vivarium
353
342
  )
354
343
 
355
344
  parent_container = container_for_pid(ev.pid, span, proc_node_by_pid, root_children)
345
+ maybe_inject_drop_node(parent_container, ev, span, prev_event_ktime)
356
346
  append_event(parent_container, ev_node)
357
347
  else
358
348
  ev_node = EventNode.new(
@@ -363,12 +353,11 @@ module Vivarium
363
353
  child_proc: nil
364
354
  )
365
355
 
366
- if ev.pid == @observer_pid && ev.tid == span.tid
367
- append_event(root_children, ev_node)
356
+ container = if ev.pid == @observer_pid && ev.tid == span.tid
357
+ root_children
368
358
  elsif (node = proc_node_by_pid[ev.pid])
369
- append_event(node.children, ev_node)
359
+ node.children
370
360
  else
371
- # event from a descendant pid we haven't materialized — synthesize a stub PROC node
372
361
  stub = ProcNode.new(
373
362
  pid: ev.pid,
374
363
  comm: @pid_comm[ev.pid] || "?",
@@ -376,14 +365,19 @@ module Vivarium
376
365
  children: []
377
366
  )
378
367
  proc_node_by_pid[ev.pid] = stub
379
- append_event(stub.children, ev_node)
380
368
  root_children << stub
369
+ stub.children
381
370
  end
382
371
 
372
+ maybe_inject_drop_node(container, ev, span, prev_event_ktime)
373
+ append_event(container, ev_node)
374
+
383
375
  if ev.event_name == EXEC_EVENT_NAME && (node = proc_node_by_pid[ev.pid])
384
376
  node.comm = @pid_comm[ev.pid] || node.comm
385
377
  end
386
378
  end
379
+
380
+ prev_event_ktime = ev.ktime_ns
387
381
  end
388
382
 
389
383
  # Interleave child spans by start time among the event/proc nodes
@@ -429,6 +423,23 @@ module Vivarium
429
423
  container << ev_node
430
424
  end
431
425
 
426
+ def maybe_inject_drop_node(container, ev, span, prev_event_ktime = nil)
427
+ n = ev.dropped_since_last.to_i
428
+ return if n.zero?
429
+
430
+ # Show the start of the drop window (= time of last good event).
431
+ # The end is implicitly ev.ktime_ns (shown on the following event line).
432
+ drop_start_ns = prev_event_ktime ? (prev_event_ktime - span.start_ktime) : nil
433
+
434
+ container << EventNode.new(
435
+ kind: "DROP",
436
+ name: "dropped_events",
437
+ target: "#{n} event(s) lost (ringbuf overflow)",
438
+ offset_ns: drop_start_ns,
439
+ child_proc: nil
440
+ )
441
+ end
442
+
432
443
  def print_nodes(nodes, prefix)
433
444
  visible_nodes = nodes.select do |node|
434
445
  !node.is_a?(Span) || span_visible?(node)
@@ -491,6 +502,7 @@ module Vivarium
491
502
  return "EXCP" if ev.event_name == "span_raise"
492
503
  return "USDT" if SPAN_EVENT_NAMES.include?(ev.event_name)
493
504
  return "SSL" if ev.event_name == SSL_WRITE_EVENT_NAME
505
+ return "DL" if DL_EVENT_NAMES.include?(ev.event_name)
494
506
  return "LSM" if LSM_EVENT_NAMES.include?(ev.event_name)
495
507
  return "TP" if TP_EVENT_NAMES.include?(ev.event_name)
496
508
 
@@ -523,28 +535,20 @@ module Vivarium
523
535
 
524
536
  def render_raise_target(ev)
525
537
  bytes = ev.payload.to_s.b
526
- return "-" if bytes.bytesize < 8
527
-
528
- error_id = bytes[0, 8].unpack1("q<")
529
- message_id = bytes.bytesize >= 16 ? bytes[8, 8].unpack1("q<") : -1
530
- file_id = bytes.bytesize >= 24 ? bytes[16, 8].unpack1("q<") : -1
531
- lineno = bytes.bytesize >= 32 ? bytes[24, 8].unpack1("q<") : -1
532
-
533
- error_name = Vivarium::Usdt.get_error_name(error_id) ||
534
- format("0x%016X", error_id & 0xFFFF_FFFF_FFFF_FFFF)
535
-
536
- parts = ["error=#{error_name}"]
537
-
538
- if message_id != -1
539
- msg = Vivarium::Usdt.get_message_name(message_id)
540
- parts << "message=#{msg.inspect}" if msg
541
- end
542
-
543
- if file_id != -1 && (file_name = Vivarium::Usdt.get_file_name(file_id))
544
- 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}" : ""
545
550
  parts << "at=#{File.basename(file_name)}#{lno}"
546
551
  end
547
-
548
552
  parts.join(" ")
549
553
  end
550
554
 
@@ -564,12 +568,12 @@ module Vivarium
564
568
 
565
569
  def read_span_payload(payload)
566
570
  bytes = payload.to_s.b
567
- return [0, -1, -1] if bytes.bytesize < 8
571
+ return [nil, nil, -1] if bytes.empty?
568
572
 
569
- method_id = bytes[0, 8].unpack1("q<")
570
- file_id = bytes.bytesize >= 16 ? bytes[8, 8].unpack1("q<") : -1
571
- lineno = bytes.bytesize >= 24 ? bytes[16, 8].unpack1("q<") : -1
572
- [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]
573
577
  end
574
578
 
575
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.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/vivarium.rb CHANGED
@@ -24,14 +24,24 @@ module Vivarium
24
24
  EVENT_TS_SIZE = 8
25
25
  PROC_EXEC_SLOT_SIZE = 64
26
26
  PROC_EXEC_SLOT_COUNT = 4
27
- EVENT_STRUCT_SIZE = 288
27
+ EVENT_STRUCT_SIZE = 296
28
28
  EVENT_TS_OFFSET = 0
29
29
  EVENT_PID_OFFSET = 8
30
30
  EVENT_TID_OFFSET = 12
31
31
  EVENT_NAME_OFFSET = 16
32
32
  EVENT_PAYLOAD_OFFSET = 32
33
+ EVENT_DROPPED_OFFSET = 288
33
34
  EVENTS_RINGBUF_PAGES = 256
34
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
+
35
45
  SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET = 0
36
46
  SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET = 4
37
47
  SSL_WRITE_PAYLOAD_DATA_OFFSET = 8
@@ -52,6 +62,16 @@ module Vivarium
52
62
  "/usr/lib/libssl.so.1.1"
53
63
  ].freeze
54
64
 
65
+ LIBC_SEARCH_PATHS = [
66
+ "/lib/x86_64-linux-gnu/libc.so.6",
67
+ "/lib/aarch64-linux-gnu/libc.so.6",
68
+ "/usr/lib/x86_64-linux-gnu/libc.so.6",
69
+ "/usr/lib/aarch64-linux-gnu/libc.so.6",
70
+ "/lib64/libc.so.6",
71
+ "/usr/lib64/libc.so.6",
72
+ "/lib/libc.so.6",
73
+ ].freeze
74
+
55
75
  SPAN_ALLOWCLASSES = [
56
76
  Socket,
57
77
  BasicSocket,
@@ -79,6 +99,7 @@ module Vivarium
79
99
  EVENT_SEVERITY_HIGH = %w[
80
100
  capable_check bprm_creds setid_change task_kill
81
101
  ptrace_check sb_mount kernel_read_file
102
+ dlopen
82
103
  ].freeze
83
104
 
84
105
  CAPABILITY_NAMES = {
@@ -137,6 +158,16 @@ module Vivarium
137
158
  str[0, nul]
138
159
  end
139
160
 
161
+ def self.tail_fit_string(value, max_bytes, marker: "...")
162
+ str = value.to_s.b
163
+ return str if str.bytesize <= max_bytes
164
+ return str.byteslice(-max_bytes, max_bytes) || "" if max_bytes <= marker.bytesize
165
+
166
+ tail_size = max_bytes - marker.bytesize
167
+ tail = str.byteslice(-tail_size, tail_size) || ""
168
+ "#{marker}#{tail}"
169
+ end
170
+
140
171
  def self.event_severity(event_name)
141
172
  EVENT_SEVERITY_HIGH.include?(event_name.to_s) ? "high" : "medium"
142
173
  end
@@ -463,6 +494,8 @@ module Vivarium
463
494
  when "ssl_write"
464
495
  decoded = decode_ssl_write_payload(event.payload)
465
496
  "data_len=#{decoded[:data_len]} cap_len=#{decoded[:cap_len]}"
497
+ when "dlopen", "mmap_exec"
498
+ strip_to_first_null(event.payload).inspect
466
499
  else
467
500
  strip_to_first_null(event.payload).inspect
468
501
  end
@@ -619,12 +652,14 @@ module Vivarium
619
652
  u32 tid;
620
653
  char event_name[16];
621
654
  char payload[#{EVENT_PAYLOAD_SIZE}];
655
+ u64 dropped_since_last;
622
656
  };
623
657
 
624
658
  BPF_HASH(config_root_targets, u32, u8, 1024);
625
659
  BPF_HASH(config_spawned_targets, u32, u8, 8192);
626
660
  BPF_HASH(dns_connected_tids, u32, u8, 8192);
627
661
  BPF_RINGBUF_OUTPUT(events, #{EVENTS_RINGBUF_PAGES});
662
+ BPF_ARRAY(drop_counter, u64, 1);
628
663
 
629
664
  static __always_inline int target_enabled(u32 pid, u32 tid)
630
665
  {
@@ -666,14 +701,27 @@ module Vivarium
666
701
 
667
702
  static __always_inline void submit_event(struct event_t *src)
668
703
  {
704
+ u32 key = 0;
705
+ u64 *cnt;
706
+
669
707
  struct event_t *ev = events.ringbuf_reserve(sizeof(struct event_t));
670
708
  if (!ev) {
709
+ cnt = drop_counter.lookup(&key);
710
+ if (cnt) {
711
+ __sync_fetch_and_add(cnt, 1);
712
+ }
671
713
  return;
672
714
  }
673
715
 
674
716
  __builtin_memcpy(ev, src, sizeof(*ev));
675
717
  ev->ktime_ns = bpf_ktime_get_ns();
676
718
  ev->tid = (u32)bpf_get_current_pid_tgid();
719
+ ev->dropped_since_last = 0;
720
+
721
+ cnt = drop_counter.lookup(&key);
722
+ if (cnt && *cnt > 0) {
723
+ ev->dropped_since_last = __sync_lock_test_and_set(cnt, 0);
724
+ }
677
725
 
678
726
  events.ringbuf_submit(ev, 0);
679
727
  }
@@ -807,6 +855,39 @@ module Vivarium
807
855
  return 0;
808
856
  }
809
857
 
858
+ LSM_PROBE(mmap_file, struct file *file, unsigned long reqprot,
859
+ unsigned long prot, unsigned long flags)
860
+ {
861
+ if (!file) {
862
+ return 0;
863
+ }
864
+ if (!((prot | reqprot) & 0x04)) { /* PROT_EXEC */
865
+ return 0;
866
+ }
867
+
868
+ u64 pid_tgid = bpf_get_current_pid_tgid();
869
+ u32 pid = pid_tgid >> 32;
870
+ u32 tid = (u32)pid_tgid;
871
+ if (!target_enabled(pid, tid)) {
872
+ return 0;
873
+ }
874
+
875
+ struct event_t ev = {};
876
+ int path_ret;
877
+ ev.pid = pid;
878
+ __builtin_memcpy(ev.event_name, "mmap_exec", 10);
879
+
880
+ path_ret = bpf_d_path(&file->f_path, ev.payload, sizeof(ev.payload));
881
+ if (path_ret < 0) {
882
+ if (ev.payload[0] == 0) {
883
+ __builtin_memcpy(ev.payload, "<path_error>", 13);
884
+ }
885
+ }
886
+
887
+ submit_event(&ev);
888
+ return 0;
889
+ }
890
+
810
891
  LSM_PROBE(socket_create, int family, int type, int protocol, int kern)
811
892
  {
812
893
  u64 pid_tgid = bpf_get_current_pid_tgid();
@@ -1332,19 +1413,19 @@ module Vivarium
1332
1413
  return 0;
1333
1414
  }
1334
1415
 
1335
- u64 method_id = 0;
1336
- u64 file_id = 0;
1337
- u64 lineno = 0;
1338
- bpf_usdt_readarg(1, ctx, &method_id);
1339
- bpf_usdt_readarg(2, ctx, &file_id);
1416
+ u64 method_str_ptr = 0;
1417
+ u64 file_str_ptr = 0;
1418
+ s64 lineno = 0;
1419
+ bpf_usdt_readarg(1, ctx, &method_str_ptr);
1420
+ bpf_usdt_readarg(2, ctx, &file_str_ptr);
1340
1421
  bpf_usdt_readarg(3, ctx, &lineno);
1341
1422
 
1342
1423
  struct event_t ev = {};
1343
1424
  ev.pid = pid;
1344
1425
  __builtin_memcpy(ev.event_name, "span_start", 11);
1345
- __builtin_memcpy(&ev.payload[0], &method_id, sizeof(method_id));
1346
- __builtin_memcpy(&ev.payload[8], &file_id, sizeof(file_id));
1347
- __builtin_memcpy(&ev.payload[16], &lineno, sizeof(lineno));
1426
+ bpf_probe_read_user_str(&ev.payload[0], #{SPAN_METHOD_SIZE}, (void*)method_str_ptr);
1427
+ bpf_probe_read_user_str(&ev.payload[#{SPAN_METHOD_SIZE}], #{SPAN_FILE_SIZE}, (void*)file_str_ptr);
1428
+ __builtin_memcpy(&ev.payload[#{SPAN_LINENO_OFFSET}], &lineno, sizeof(lineno));
1348
1429
  submit_event(&ev);
1349
1430
  return 0;
1350
1431
  }
@@ -1359,19 +1440,19 @@ module Vivarium
1359
1440
  return 0;
1360
1441
  }
1361
1442
 
1362
- u64 method_id = 0;
1363
- u64 file_id = 0;
1364
- u64 lineno = 0;
1365
- bpf_usdt_readarg(1, ctx, &method_id);
1366
- bpf_usdt_readarg(2, ctx, &file_id);
1443
+ u64 method_str_ptr = 0;
1444
+ u64 file_str_ptr = 0;
1445
+ s64 lineno = 0;
1446
+ bpf_usdt_readarg(1, ctx, &method_str_ptr);
1447
+ bpf_usdt_readarg(2, ctx, &file_str_ptr);
1367
1448
  bpf_usdt_readarg(3, ctx, &lineno);
1368
1449
 
1369
1450
  struct event_t ev = {};
1370
1451
  ev.pid = pid;
1371
1452
  __builtin_memcpy(ev.event_name, "span_stop", 10);
1372
- __builtin_memcpy(&ev.payload[0], &method_id, sizeof(method_id));
1373
- __builtin_memcpy(&ev.payload[8], &file_id, sizeof(file_id));
1374
- __builtin_memcpy(&ev.payload[16], &lineno, sizeof(lineno));
1453
+ bpf_probe_read_user_str(&ev.payload[0], #{SPAN_METHOD_SIZE}, (void*)method_str_ptr);
1454
+ bpf_probe_read_user_str(&ev.payload[#{SPAN_METHOD_SIZE}], #{SPAN_FILE_SIZE}, (void*)file_str_ptr);
1455
+ __builtin_memcpy(&ev.payload[#{SPAN_LINENO_OFFSET}], &lineno, sizeof(lineno));
1375
1456
  submit_event(&ev);
1376
1457
  return 0;
1377
1458
  }
@@ -1412,6 +1493,32 @@ module Vivarium
1412
1493
  return 0;
1413
1494
  }
1414
1495
 
1496
+ int on_dlopen(struct pt_regs *ctx)
1497
+ {
1498
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1499
+ u32 pid = pid_tgid >> 32;
1500
+ u32 tid = (u32)pid_tgid;
1501
+ if (!target_enabled(pid, tid)) {
1502
+ return 0;
1503
+ }
1504
+
1505
+ const char *filename = (const char *)PT_REGS_PARM1(ctx);
1506
+ if (!filename) {
1507
+ return 0;
1508
+ }
1509
+
1510
+ struct event_t ev = {};
1511
+ ev.pid = pid;
1512
+ __builtin_memcpy(ev.event_name, "dlopen", 7);
1513
+
1514
+ if (bpf_probe_read_user_str(ev.payload, sizeof(ev.payload), filename) < 0) {
1515
+ __builtin_memcpy(ev.payload, "<path_error>", 13);
1516
+ }
1517
+
1518
+ submit_event(&ev);
1519
+ return 0;
1520
+ }
1521
+
1415
1522
  int on_span_raise(struct pt_regs *ctx)
1416
1523
  {
1417
1524
  u64 pid_tgid = bpf_get_current_pid_tgid();
@@ -1422,31 +1529,34 @@ module Vivarium
1422
1529
  return 0;
1423
1530
  }
1424
1531
 
1425
- u64 error_id = 0;
1426
- u64 message_id = 0;
1427
- u64 file_id = 0;
1428
- u64 lineno = 0;
1429
- bpf_usdt_readarg(1, ctx, &error_id);
1430
- bpf_usdt_readarg(2, ctx, &message_id);
1431
- bpf_usdt_readarg(3, ctx, &file_id);
1532
+ u64 error_str_ptr = 0;
1533
+ u64 message_str_ptr = 0;
1534
+ u64 file_str_ptr = 0;
1535
+ s64 lineno = 0;
1536
+ bpf_usdt_readarg(1, ctx, &error_str_ptr);
1537
+ bpf_usdt_readarg(2, ctx, &message_str_ptr);
1538
+ bpf_usdt_readarg(3, ctx, &file_str_ptr);
1432
1539
  bpf_usdt_readarg(4, ctx, &lineno);
1433
1540
 
1434
1541
  struct event_t ev = {};
1435
1542
  ev.pid = pid;
1436
1543
  __builtin_memcpy(ev.event_name, "span_raise", 11);
1437
- __builtin_memcpy(&ev.payload[0], &error_id, sizeof(error_id));
1438
- __builtin_memcpy(&ev.payload[8], &message_id, sizeof(message_id));
1439
- __builtin_memcpy(&ev.payload[16], &file_id, sizeof(file_id));
1440
- __builtin_memcpy(&ev.payload[24], &lineno, sizeof(lineno));
1544
+ bpf_probe_read_user_str(&ev.payload[0], #{SPAN_RAISE_SLOT_SIZE}, (void*)error_str_ptr);
1545
+ bpf_probe_read_user_str(&ev.payload[#{SPAN_RAISE_SLOT_SIZE}], #{SPAN_RAISE_SLOT_SIZE}, (void*)message_str_ptr);
1546
+ bpf_probe_read_user_str(&ev.payload[#{SPAN_RAISE_SLOT_SIZE * 2}], #{SPAN_RAISE_SLOT_SIZE}, (void*)file_str_ptr);
1547
+ __builtin_memcpy(&ev.payload[#{SPAN_RAISE_LINENO_OFFSET}], &lineno, sizeof(lineno));
1441
1548
  submit_event(&ev);
1442
1549
  return 0;
1443
1550
  }
1444
1551
  CLANG
1445
1552
 
1446
- def initialize(pin_dir: Vivarium.bpf_pin_dir, ssl_trace: true, libssl_path: nil)
1447
- @pin_dir = pin_dir
1448
- @ssl_trace = ssl_trace
1449
- @libssl_path = libssl_path
1553
+ def initialize(pin_dir: Vivarium.bpf_pin_dir, ssl_trace: true, libssl_path: nil,
1554
+ dlopen_trace: true, libc_path: nil)
1555
+ @pin_dir = pin_dir
1556
+ @ssl_trace = ssl_trace
1557
+ @libssl_path = libssl_path
1558
+ @dlopen_trace = dlopen_trace
1559
+ @libc_path = libc_path
1450
1560
  end
1451
1561
 
1452
1562
  def run
@@ -1459,6 +1569,7 @@ module Vivarium
1459
1569
  .gsub("__VIVARIUM_F_PATH_OFFSET__", f_path_offset.to_s)
1460
1570
  .gsub("__VIVARIUM_DENTRY_D_NAME_OFFSET__", d_name_offset.to_s)
1461
1571
 
1572
+ require "vivarium_usdt"
1462
1573
  usdt_so_path = ENV.fetch("VIVARIUM_USDT_SO_PATH") { Vivarium.locate_vivarium_usdt_so }
1463
1574
  usdt = RbBCC::USDT.new(path: usdt_so_path)
1464
1575
  usdt.enable_probe(probe: "start_probe", fn_name: "on_span_start")
@@ -1468,6 +1579,7 @@ module Vivarium
1468
1579
  bpf = RbBCC::BCC.new(text: program, usdt_contexts: [usdt])
1469
1580
 
1470
1581
  attach_ssl_write_uprobe(bpf) if @ssl_trace
1582
+ attach_dlopen_uprobe(bpf) if @dlopen_trace
1471
1583
 
1472
1584
  config_root_targets = bpf["config_root_targets"]
1473
1585
  config_spawned_targets = bpf["config_spawned_targets"]
@@ -1526,6 +1638,39 @@ module Vivarium
1526
1638
  LIBSSL_SEARCH_PATHS.find { |p| File.exist?(p) }
1527
1639
  end
1528
1640
 
1641
+ def attach_dlopen_uprobe(bpf)
1642
+ path = resolve_libc_path
1643
+ unless path
1644
+ warn "[vivariumd] libc not found; dlopen uprobe disabled " \
1645
+ "(set --libc PATH or VIVARIUM_LIBC_PATH to override)"
1646
+ return
1647
+ end
1648
+
1649
+ bpf.attach_uprobe(name: path, sym: "dlopen", fn_name: "on_dlopen")
1650
+ puts "[vivariumd] dlopen uprobe attached via #{path}"
1651
+ rescue StandardError => e
1652
+ warn "[vivariumd] dlopen uprobe attach failed: #{e.class}: #{e.message}"
1653
+ end
1654
+
1655
+ def resolve_libc_path
1656
+ if @libc_path
1657
+ return @libc_path if File.exist?(@libc_path)
1658
+
1659
+ warn "[vivariumd] --libc path does not exist: #{@libc_path}"
1660
+ return nil
1661
+ end
1662
+
1663
+ env_path = ENV["VIVARIUM_LIBC_PATH"]
1664
+ if env_path && !env_path.empty?
1665
+ return env_path if File.exist?(env_path)
1666
+
1667
+ warn "[vivariumd] VIVARIUM_LIBC_PATH does not exist: #{env_path}"
1668
+ return nil
1669
+ end
1670
+
1671
+ LIBC_SEARCH_PATHS.find { |p| File.exist?(p) }
1672
+ end
1673
+
1529
1674
  def ensure_root!
1530
1675
  return if Process.uid.zero?
1531
1676
 
@@ -1680,20 +1825,18 @@ module Vivarium
1680
1825
  pid = Process.pid
1681
1826
  store.register_pid(pid)
1682
1827
 
1683
- method_id_queue = Thread::Queue.new
1684
1828
  main_tid = gettid
1685
1829
 
1686
1830
  correlator = Correlator.new(
1687
1831
  pin_dir: pin_dir,
1688
1832
  observer_pid: pid,
1689
1833
  main_tid: main_tid,
1690
- method_id_queue: method_id_queue,
1691
1834
  filter: filter,
1692
1835
  dest: dest
1693
1836
  )
1694
1837
  correlator.start
1695
1838
 
1696
- tracer = build_observe_tracepoint(method_id_queue)
1839
+ tracer = build_observe_tracepoint
1697
1840
  tracer.enable
1698
1841
 
1699
1842
  session = ObservationSession.new(
@@ -1710,20 +1853,18 @@ module Vivarium
1710
1853
  pid = Process.pid
1711
1854
  store.register_pid(pid)
1712
1855
 
1713
- method_id_queue = Thread::Queue.new
1714
1856
  main_tid = gettid
1715
1857
 
1716
1858
  correlator = Correlator.new(
1717
1859
  pin_dir: pin_dir,
1718
1860
  observer_pid: pid,
1719
1861
  main_tid: main_tid,
1720
- method_id_queue: method_id_queue,
1721
1862
  filter: filter,
1722
1863
  dest: dest
1723
1864
  )
1724
1865
  correlator.start
1725
1866
 
1726
- tracer = build_observe_tracepoint(method_id_queue)
1867
+ tracer = build_observe_tracepoint
1727
1868
  tracer.enable
1728
1869
 
1729
1870
  yield
@@ -1733,20 +1874,19 @@ module Vivarium
1733
1874
  correlator&.stop
1734
1875
  end
1735
1876
 
1736
- def self.build_observe_tracepoint(method_id_queue)
1877
+ def self.build_observe_tracepoint
1737
1878
  allow_classes = SPAN_ALLOWCLASSES
1738
1879
  allowlist = SPAN_ALLOWLIST
1739
1880
  TracePoint.new(:call, :c_call, :return, :c_return, :raise) do |tp|
1740
1881
  if tp.event == :raise
1741
1882
  # FIXME: handle threaded events in the future
1742
- if tp.raised_exception.kind_of?(ThreadError)
1743
- next
1744
- end
1883
+ next if tp.raised_exception.kind_of?(ThreadError)
1745
1884
 
1885
+ file_arg = tail_fit_string(tp.path, SPAN_RAISE_FILE_ARG_MAX)
1746
1886
  Vivarium::Usdt.raise(
1747
1887
  tp.raised_exception.class.to_s,
1748
1888
  tp.raised_exception.message.to_s,
1749
- file: tp.path,
1889
+ file: file_arg,
1750
1890
  lineno: tp.lineno
1751
1891
  )
1752
1892
  next
@@ -1758,12 +1898,12 @@ module Vivarium
1758
1898
  allow_classes.any? { |klass| tp.defined_class == klass.singleton_class }
1759
1899
  next unless is_target
1760
1900
 
1901
+ file_arg = tail_fit_string(tp.path, SPAN_FILE_ARG_MAX)
1761
1902
  case tp.event
1762
1903
  when :call, :c_call
1763
- method_id = Vivarium::Usdt.start(tp.defined_class.to_s, tp.method_id.to_s, file: tp.path, lineno: tp.lineno)
1764
- method_id_queue << [method_id, signature]
1904
+ Vivarium::Usdt.start(tp.defined_class.to_s, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
1765
1905
  when :return, :c_return
1766
- Vivarium::Usdt.stop(tp.defined_class.to_s, tp.method_id.to_s, file: tp.path, lineno: tp.lineno)
1906
+ Vivarium::Usdt.stop(tp.defined_class.to_s, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
1767
1907
  end
1768
1908
  end
1769
1909
  end
@@ -1794,9 +1934,11 @@ module Vivarium
1794
1934
  end
1795
1935
 
1796
1936
  def self.run_daemon!(argv = ARGV)
1797
- options = { pin_dir: bpf_pin_dir, ssl_trace: true, libssl_path: nil }
1937
+ options = { pin_dir: bpf_pin_dir, ssl_trace: true, libssl_path: nil,
1938
+ dlopen_trace: true, libc_path: nil }
1798
1939
  OptionParser.new do |opts|
1799
- opts.banner = "Usage: vivariumd [--pin-dir PATH] [--no-ssl-trace] [--libssl PATH]"
1940
+ opts.banner = "Usage: vivariumd [--pin-dir PATH] [--no-ssl-trace] [--libssl PATH] " \
1941
+ "[--no-dlopen-trace] [--libc PATH]"
1800
1942
  opts.on("--pin-dir PATH", "Pinned map directory") { |v| options[:pin_dir] = v }
1801
1943
  opts.on("--[no-]ssl-trace", "Attach OpenSSL SSL_write uprobe (default: enabled)") do |v|
1802
1944
  options[:ssl_trace] = v
@@ -1804,12 +1946,20 @@ module Vivarium
1804
1946
  opts.on("--libssl PATH", "Path to libssl.so to attach SSL_write to") do |v|
1805
1947
  options[:libssl_path] = v
1806
1948
  end
1949
+ opts.on("--[no-]dlopen-trace", "Attach libc dlopen uprobe (default: enabled)") do |v|
1950
+ options[:dlopen_trace] = v
1951
+ end
1952
+ opts.on("--libc PATH", "Path to libc.so for dlopen uprobe") do |v|
1953
+ options[:libc_path] = v
1954
+ end
1807
1955
  end.parse!(argv)
1808
1956
 
1809
1957
  Daemon.new(
1810
1958
  pin_dir: options[:pin_dir],
1811
1959
  ssl_trace: options[:ssl_trace],
1812
- libssl_path: options[:libssl_path]
1960
+ libssl_path: options[:libssl_path],
1961
+ dlopen_trace: options[:dlopen_trace],
1962
+ libc_path: options[:libc_path]
1813
1963
  ).run
1814
1964
  end
1815
1965
  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.1
4
+ version: 0.4.0
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
@@ -64,6 +64,8 @@ files:
64
64
  - CONTEXT.md
65
65
  - README.md
66
66
  - Rakefile
67
+ - examples/dlopen_demo.rb
68
+ - examples/drop_demo.rb
67
69
  - examples/execve_demo.rb
68
70
  - examples/file_operation_demo.rb
69
71
  - examples/network_client_demo.rb