vivarium 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/vivarium.rb CHANGED
@@ -7,7 +7,7 @@ require "pathname"
7
7
  require "rbbcc"
8
8
  require "socket"
9
9
  require_relative "vivarium/version"
10
- require_relative "vivarium/logger"
10
+ require_relative "vivarium/cli"
11
11
 
12
12
  module Vivarium
13
13
  class Error < StandardError; end
@@ -16,8 +16,7 @@ module Vivarium
16
16
  CONFIG_ROOT_TARGETS_PIN = File.join(PIN_DIR, "config_root_targets")
17
17
  CONFIG_SPAWNED_TARGETS_PIN = File.join(PIN_DIR, "config_spawned_targets")
18
18
  CONFIG_TARGETS_PIN = CONFIG_ROOT_TARGETS_PIN
19
- EVENT_INVOKED_PIN = File.join(PIN_DIR, "event_invoked")
20
- EVENT_WRITE_POS_PIN = File.join(PIN_DIR, "event_write_pos")
19
+ EVENTS_PIN = File.join(PIN_DIR, "events")
21
20
 
22
21
  EVENT_NAME_SIZE = 16
23
22
  EVENT_PAYLOAD_SIZE = 256
@@ -27,9 +26,33 @@ module Vivarium
27
26
  EVENT_STRUCT_SIZE = 288
28
27
  EVENT_TS_OFFSET = 0
29
28
  EVENT_PID_OFFSET = 8
30
- EVENT_NAME_OFFSET = 12
31
- EVENT_PAYLOAD_OFFSET = 28
32
- EVENT_CAPACITY = 1024
29
+ EVENT_TID_OFFSET = 12
30
+ EVENT_NAME_OFFSET = 16
31
+ EVENT_PAYLOAD_OFFSET = 32
32
+ EVENTS_RINGBUF_PAGES = 256
33
+ SPAN_ALLOWCLASSES = [
34
+ Socket,
35
+ BasicSocket,
36
+ IPSocket,
37
+ TCPSocket,
38
+ UDPSocket,
39
+ UNIXSocket,
40
+ File,
41
+ Dir,
42
+ Signal,
43
+ Process,
44
+ Process::UID,
45
+ Process::GID,
46
+ ]
47
+ SPAN_ALLOWLIST = [
48
+ "Kernel#system",
49
+ "Kernel#require",
50
+ "Kernel#require_relative",
51
+ "Kernel#load",
52
+ "Kernel#eval",
53
+ "Object#instance_eval",
54
+ "Object#instance_exec",
55
+ ].freeze
33
56
  EVENT_SEVERITY_HIGH = %w[
34
57
  capable_check bprm_creds setid_change task_kill
35
58
  ptrace_check sb_mount kernel_read_file
@@ -83,49 +106,12 @@ module Vivarium
83
106
  end
84
107
  end
85
108
 
86
- Event = Struct.new(:ktime_ns, :pid, :event_name, :payload, keyword_init: true) do
87
- def empty?
88
- ktime_ns.to_i.zero? && pid.to_i.zero? && event_name.to_s.empty? && payload.to_s.empty?
89
- end
90
-
91
- def severity
92
- Vivarium.event_severity(event_name)
93
- end
94
-
95
- def self.from_binary(raw)
96
- bytes = raw.to_s.b
97
- bytes = bytes.ljust(EVENT_STRUCT_SIZE, "\x00")
98
-
99
- ktime_ns = bytes[EVENT_TS_OFFSET, EVENT_TS_SIZE].unpack1("Q<")
100
- pid = bytes[EVENT_PID_OFFSET, 4].unpack1("L<")
101
- event_name = c_string(bytes[EVENT_NAME_OFFSET, EVENT_NAME_SIZE])
102
- raw_payload = bytes[EVENT_PAYLOAD_OFFSET, EVENT_PAYLOAD_SIZE]
103
- raw_payload_events = %w[
104
- dns_req sock_connect odd_socket proc_exec
105
- file_symlink file_hardlink file_rename file_chmod file_getdents
106
- ptrace_check sb_mount kernel_read_file task_kill
107
- setid_change capable_check bprm_creds
108
- ]
109
- payload = if raw_payload_events.include?(event_name)
110
- raw_payload
111
- else
112
- c_string(raw_payload)
113
- end
114
-
115
- new(ktime_ns: ktime_ns, pid: pid, event_name: event_name, payload: payload)
116
- end
117
-
118
- def self.c_string(bytes)
119
- str = bytes.to_s.b
120
- nul = str.index("\x00")
121
- return str if nul.nil?
122
-
123
- str[0, nul]
124
- end
125
- end
126
-
127
109
  def self.c_string(bytes)
128
- Event.c_string(bytes)
110
+ str = bytes.to_s.b
111
+ nul = str.index("\x00")
112
+ return str if nul.nil?
113
+
114
+ str[0, nul]
129
115
  end
130
116
 
131
117
  def self.event_severity(event_name)
@@ -330,6 +316,57 @@ module Vivarium
330
316
  "has_file=#{has_file} file=#{path.inspect}"
331
317
  end
332
318
 
319
+ def self.decode_proc_fork_payload(raw_payload)
320
+ bytes = raw_payload.to_s.b
321
+ return "" if bytes.bytesize < 8
322
+
323
+ child_pid = bytes[0, 4].unpack1("L<")
324
+ child_tid = bytes[4, 4].unpack1("L<")
325
+ "child_pid=#{child_pid} child_tid=#{child_tid}"
326
+ end
327
+
328
+ def self.decode_span_payload(raw_payload)
329
+ bytes = raw_payload.to_s.b
330
+ return "" if bytes.bytesize < 8
331
+
332
+ method_id = bytes[0, 8].unpack1("q<")
333
+ result = format("method_id=0x%016X", method_id & 0xFFFF_FFFF_FFFF_FFFF)
334
+
335
+ if bytes.bytesize >= 24
336
+ file_id = bytes[8, 8].unpack1("q<")
337
+ lineno = bytes[16, 8].unpack1("q<")
338
+ result += format(" file_id=0x%016X", file_id & 0xFFFF_FFFF_FFFF_FFFF) if file_id != -1
339
+ result += " lineno=#{lineno}" if lineno > 0
340
+ end
341
+
342
+ result
343
+ end
344
+
345
+ def self.decode_span_raise_payload(raw_payload)
346
+ bytes = raw_payload.to_s.b
347
+ return "" if bytes.bytesize < 8
348
+
349
+ error_id = bytes[0, 8].unpack1("q<")
350
+ result = format("error_id=0x%016X", error_id & 0xFFFF_FFFF_FFFF_FFFF)
351
+
352
+ if bytes.bytesize >= 16
353
+ message_id = bytes[8, 8].unpack1("q<")
354
+ result += format(" message_id=0x%016X", message_id & 0xFFFF_FFFF_FFFF_FFFF)
355
+ end
356
+
357
+ if bytes.bytesize >= 24
358
+ file_id = bytes[16, 8].unpack1("q<")
359
+ result += format(" file_id=0x%016X", file_id & 0xFFFF_FFFF_FFFF_FFFF) if file_id != -1
360
+ end
361
+
362
+ if bytes.bytesize >= 32
363
+ lineno = bytes[24, 8].unpack1("q<")
364
+ result += " lineno=#{lineno}" if lineno > 0
365
+ end
366
+
367
+ result
368
+ end
369
+
333
370
  def self.render_event_payload(event)
334
371
  case event.event_name
335
372
  when "dns_req"
@@ -365,6 +402,15 @@ module Vivarium
365
402
  when "bprm_creds"
366
403
  decoded = decode_bprm_creds_payload(event.payload)
367
404
  decoded.empty? ? event.payload.inspect : decoded
405
+ when "proc_fork"
406
+ decoded = decode_proc_fork_payload(event.payload)
407
+ decoded.empty? ? event.payload.inspect : decoded
408
+ when "span_start", "span_stop"
409
+ decoded = decode_span_payload(event.payload)
410
+ decoded.empty? ? event.payload.inspect : decoded
411
+ when "span_raise"
412
+ decoded = decode_span_raise_payload(event.payload)
413
+ decoded.empty? ? event.payload.inspect : decoded
368
414
  when "file_symlink"
369
415
  decoded = decode_file_symlink_payload(event.payload)
370
416
  decoded.empty? ? event.payload.inspect : decoded
@@ -381,10 +427,17 @@ module Vivarium
381
427
  decoded = decode_file_getdents_payload(event.payload)
382
428
  decoded.empty? ? event.payload.inspect : decoded
383
429
  else
384
- event.payload.inspect
430
+ strip_to_first_null(event.payload).inspect
385
431
  end
386
432
  end
387
433
 
434
+ def self.strip_to_first_null(bytes)
435
+ nul = bytes.index("\x00")
436
+ return bytes if nul.nil?
437
+
438
+ bytes[0, nul]
439
+ end
440
+
388
441
  class MapStore
389
442
  def initialize(pin_dir: Vivarium.bpf_pin_dir)
390
443
  @pin_dir = pin_dir
@@ -402,20 +455,6 @@ module Vivarium
402
455
  keysize: 4,
403
456
  leafsize: 1
404
457
  )
405
- @event_invoked = RbBCC::ArrayTable.from_pin(
406
- File.join(@pin_dir, "event_invoked"),
407
- "unsigned int",
408
- "char[#{EVENT_STRUCT_SIZE}]",
409
- keysize: 4,
410
- leafsize: EVENT_STRUCT_SIZE
411
- )
412
- @event_write_pos = RbBCC::ArrayTable.from_pin(
413
- File.join(@pin_dir, "event_write_pos"),
414
- "unsigned int",
415
- "unsigned int",
416
- keysize: 4,
417
- leafsize: 4
418
- )
419
458
  rescue StandardError => e
420
459
  raise Error, "failed to open pinned maps under #{@pin_dir}: #{e.class}: #{e.message}"
421
460
  end
@@ -430,31 +469,6 @@ module Vivarium
430
469
  rescue KeyError
431
470
  nil
432
471
  end
433
-
434
- def drain_events
435
- events = []
436
- EVENT_CAPACITY.times do |idx|
437
- ptr = @event_invoked[idx]
438
- next unless ptr
439
-
440
- event = Event.from_binary(ptr[0, EVENT_STRUCT_SIZE])
441
- next if event.empty?
442
-
443
- events << event
444
- @event_invoked[idx] = zeroed_event_ptr
445
- end
446
-
447
- @event_write_pos[0] = 0
448
- events
449
- end
450
-
451
- private
452
-
453
- def zeroed_event_ptr
454
- ptr = Fiddle::Pointer.malloc(EVENT_STRUCT_SIZE)
455
- ptr[0, EVENT_STRUCT_SIZE] = "\x00" * EVENT_STRUCT_SIZE
456
- ptr
457
- end
458
472
  end
459
473
 
460
474
  class Daemon
@@ -565,6 +579,7 @@ module Vivarium
565
579
  struct event_t {
566
580
  u64 ktime_ns;
567
581
  u32 pid;
582
+ u32 tid;
568
583
  char event_name[16];
569
584
  char payload[#{EVENT_PAYLOAD_SIZE}];
570
585
  };
@@ -572,8 +587,7 @@ module Vivarium
572
587
  BPF_HASH(config_root_targets, u32, u8, 1024);
573
588
  BPF_HASH(config_spawned_targets, u32, u8, 8192);
574
589
  BPF_HASH(dns_connected_tids, u32, u8, 8192);
575
- BPF_ARRAY(event_invoked, struct event_t, #{EVENT_CAPACITY});
576
- BPF_ARRAY(event_write_pos, u32, 1);
590
+ BPF_RINGBUF_OUTPUT(events, #{EVENTS_RINGBUF_PAGES});
577
591
 
578
592
  static __always_inline int target_enabled(u32 pid, u32 tid)
579
593
  {
@@ -613,19 +627,18 @@ module Vivarium
613
627
  }
614
628
  }
615
629
 
616
- static __always_inline void submit_event(struct event_t *ev)
630
+ static __always_inline void submit_event(struct event_t *src)
617
631
  {
618
- u32 zero = 0;
619
- u32 *write_pos = event_write_pos.lookup(&zero);
620
- if (!write_pos) {
632
+ struct event_t *ev = events.ringbuf_reserve(sizeof(struct event_t));
633
+ if (!ev) {
621
634
  return;
622
635
  }
623
636
 
637
+ __builtin_memcpy(ev, src, sizeof(*ev));
624
638
  ev->ktime_ns = bpf_ktime_get_ns();
639
+ ev->tid = (u32)bpf_get_current_pid_tgid();
625
640
 
626
- u32 idx = *write_pos % #{EVENT_CAPACITY};
627
- __sync_fetch_and_add(write_pos, 1);
628
- event_invoked.update(&idx, ev);
641
+ events.ringbuf_submit(ev, 0);
629
642
  }
630
643
 
631
644
  static __always_inline int is_dns_destination(void *addr)
@@ -696,16 +709,28 @@ module Vivarium
696
709
  u32 parent = args->parent_pid;
697
710
  u32 child = args->child_pid;
698
711
  u8 one = 1;
712
+ int is_target = 0;
699
713
 
700
714
  u8 *enabled_root = config_root_targets.lookup(&parent);
701
715
  if (enabled_root && *enabled_root == 1) {
716
+ is_target = 1;
702
717
  config_spawned_targets.update(&child, &one);
703
- return 0;
718
+ } else {
719
+ u8 *enabled_spawned = config_spawned_targets.lookup(&parent);
720
+ if (enabled_spawned && *enabled_spawned == 1) {
721
+ is_target = 1;
722
+ config_spawned_targets.update(&child, &one);
723
+ }
704
724
  }
705
725
 
706
- u8 *enabled_spawned = config_spawned_targets.lookup(&parent);
707
- if (enabled_spawned && *enabled_spawned == 1) {
708
- config_spawned_targets.update(&child, &one);
726
+ if (is_target) {
727
+ u64 pid_tgid = bpf_get_current_pid_tgid();
728
+ struct event_t ev = {};
729
+ ev.pid = pid_tgid >> 32;
730
+ __builtin_memcpy(ev.event_name, "proc_fork", 10);
731
+ __builtin_memcpy(&ev.payload[0], &child, sizeof(child));
732
+ __builtin_memcpy(&ev.payload[4], &child, sizeof(child));
733
+ submit_event(&ev);
709
734
  }
710
735
 
711
736
  return 0;
@@ -724,7 +749,6 @@ module Vivarium
724
749
  u64 pid_tgid = bpf_get_current_pid_tgid();
725
750
  u32 pid = pid_tgid >> 32;
726
751
  u32 tid = (u32)pid_tgid;
727
- bpf_trace_printk("vivarium: invoked pid=%d\\n", pid);
728
752
  if (!target_enabled(pid, tid)) {
729
753
  return 0;
730
754
  }
@@ -738,11 +762,9 @@ module Vivarium
738
762
  if (path_ret < 0) {
739
763
  if (ev.payload[0] == 0) {
740
764
  __builtin_memcpy(ev.payload, "<path_error>", 13);
741
- bpf_trace_printk("vivarium: failed to obtain full path. pid=%d path=%s\\n", pid, ev.payload);
742
765
  }
743
766
  }
744
767
 
745
- bpf_trace_printk("vivarium: pid=%d path=%s\\n", pid, ev.payload);
746
768
  submit_event(&ev);
747
769
 
748
770
  return 0;
@@ -1262,6 +1284,90 @@ module Vivarium
1262
1284
  submit_event(&ev);
1263
1285
  return 0;
1264
1286
  }
1287
+
1288
+ int on_span_start(struct pt_regs *ctx)
1289
+ {
1290
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1291
+ u32 pid = pid_tgid >> 32;
1292
+ u32 tid = (u32)pid_tgid;
1293
+
1294
+ if (!target_enabled(pid, tid)) {
1295
+ return 0;
1296
+ }
1297
+
1298
+ u64 method_id = 0;
1299
+ u64 file_id = 0;
1300
+ u64 lineno = 0;
1301
+ bpf_usdt_readarg(1, ctx, &method_id);
1302
+ bpf_usdt_readarg(2, ctx, &file_id);
1303
+ bpf_usdt_readarg(3, ctx, &lineno);
1304
+
1305
+ struct event_t ev = {};
1306
+ ev.pid = pid;
1307
+ __builtin_memcpy(ev.event_name, "span_start", 11);
1308
+ __builtin_memcpy(&ev.payload[0], &method_id, sizeof(method_id));
1309
+ __builtin_memcpy(&ev.payload[8], &file_id, sizeof(file_id));
1310
+ __builtin_memcpy(&ev.payload[16], &lineno, sizeof(lineno));
1311
+ submit_event(&ev);
1312
+ return 0;
1313
+ }
1314
+
1315
+ int on_span_stop(struct pt_regs *ctx)
1316
+ {
1317
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1318
+ u32 pid = pid_tgid >> 32;
1319
+ u32 tid = (u32)pid_tgid;
1320
+
1321
+ if (!target_enabled(pid, tid)) {
1322
+ return 0;
1323
+ }
1324
+
1325
+ u64 method_id = 0;
1326
+ u64 file_id = 0;
1327
+ u64 lineno = 0;
1328
+ bpf_usdt_readarg(1, ctx, &method_id);
1329
+ bpf_usdt_readarg(2, ctx, &file_id);
1330
+ bpf_usdt_readarg(3, ctx, &lineno);
1331
+
1332
+ struct event_t ev = {};
1333
+ ev.pid = pid;
1334
+ __builtin_memcpy(ev.event_name, "span_stop", 10);
1335
+ __builtin_memcpy(&ev.payload[0], &method_id, sizeof(method_id));
1336
+ __builtin_memcpy(&ev.payload[8], &file_id, sizeof(file_id));
1337
+ __builtin_memcpy(&ev.payload[16], &lineno, sizeof(lineno));
1338
+ submit_event(&ev);
1339
+ return 0;
1340
+ }
1341
+
1342
+ int on_span_raise(struct pt_regs *ctx)
1343
+ {
1344
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1345
+ u32 pid = pid_tgid >> 32;
1346
+ u32 tid = (u32)pid_tgid;
1347
+
1348
+ if (!target_enabled(pid, tid)) {
1349
+ return 0;
1350
+ }
1351
+
1352
+ u64 error_id = 0;
1353
+ u64 message_id = 0;
1354
+ u64 file_id = 0;
1355
+ u64 lineno = 0;
1356
+ bpf_usdt_readarg(1, ctx, &error_id);
1357
+ bpf_usdt_readarg(2, ctx, &message_id);
1358
+ bpf_usdt_readarg(3, ctx, &file_id);
1359
+ bpf_usdt_readarg(4, ctx, &lineno);
1360
+
1361
+ struct event_t ev = {};
1362
+ ev.pid = pid;
1363
+ __builtin_memcpy(ev.event_name, "span_raise", 11);
1364
+ __builtin_memcpy(&ev.payload[0], &error_id, sizeof(error_id));
1365
+ __builtin_memcpy(&ev.payload[8], &message_id, sizeof(message_id));
1366
+ __builtin_memcpy(&ev.payload[16], &file_id, sizeof(file_id));
1367
+ __builtin_memcpy(&ev.payload[24], &lineno, sizeof(lineno));
1368
+ submit_event(&ev);
1369
+ return 0;
1370
+ }
1265
1371
  CLANG
1266
1372
 
1267
1373
  def initialize(pin_dir: Vivarium.bpf_pin_dir)
@@ -1278,38 +1384,34 @@ module Vivarium
1278
1384
  .gsub("__VIVARIUM_F_PATH_OFFSET__", f_path_offset.to_s)
1279
1385
  .gsub("__VIVARIUM_DENTRY_D_NAME_OFFSET__", d_name_offset.to_s)
1280
1386
 
1281
- bpf = RbBCC::BCC.new(text: program)
1282
- kprint_thread = start_kprint_logger(bpf)
1387
+ usdt_so_path = ENV.fetch("VIVARIUM_USDT_SO_PATH") { Vivarium.locate_vivarium_usdt_so }
1388
+ usdt = RbBCC::USDT.new(path: usdt_so_path)
1389
+ usdt.enable_probe(probe: "start_probe", fn_name: "on_span_start")
1390
+ usdt.enable_probe(probe: "stop_probe", fn_name: "on_span_stop")
1391
+ usdt.enable_probe(probe: "raise_probe", fn_name: "on_span_raise")
1392
+
1393
+ bpf = RbBCC::BCC.new(text: program, usdt_contexts: [usdt])
1283
1394
 
1284
1395
  config_root_targets = bpf["config_root_targets"]
1285
1396
  config_spawned_targets = bpf["config_spawned_targets"]
1286
- event_invoked = bpf["event_invoked"]
1287
- event_write_pos = bpf["event_write_pos"]
1397
+ events_ringbuf = bpf["events"]
1288
1398
 
1289
- clear_event_slots(event_invoked)
1290
- event_write_pos[0] = 0
1291
1399
  config_spawned_targets.clear
1292
1400
 
1293
1401
  pin_map(config_root_targets, File.join(@pin_dir, "config_root_targets"))
1294
1402
  pin_map(config_spawned_targets, File.join(@pin_dir, "config_spawned_targets"))
1295
- pin_map(event_invoked, File.join(@pin_dir, "event_invoked"))
1296
- pin_map(event_write_pos, File.join(@pin_dir, "event_write_pos"))
1403
+ pin_map(events_ringbuf, File.join(@pin_dir, "events"))
1297
1404
 
1298
1405
  puts "[vivariumd] started"
1299
1406
  puts "[vivariumd] pinned maps in #{@pin_dir}"
1300
1407
  puts "[vivariumd] watching LSM file_open (f_path offset=#{f_path_offset})"
1301
- puts "[vivariumd] kprint logger enabled"
1408
+ puts "[vivariumd] USDT attached via #{usdt_so_path}"
1302
1409
 
1303
1410
  loop do
1304
1411
  sleep 1
1305
1412
  end
1306
1413
  rescue Interrupt
1307
1414
  puts "\n[vivariumd] stopping"
1308
- ensure
1309
- if kprint_thread
1310
- kprint_thread.kill
1311
- kprint_thread.join(0.2)
1312
- end
1313
1415
  end
1314
1416
 
1315
1417
  private
@@ -1325,34 +1427,6 @@ module Vivarium
1325
1427
  RbBCC::BCC.pin!(table.map_fd, path)
1326
1428
  end
1327
1429
 
1328
- def clear_event_slots(table)
1329
- ptr = Fiddle::Pointer.malloc(EVENT_STRUCT_SIZE)
1330
- ptr[0, EVENT_STRUCT_SIZE] = "\x00" * EVENT_STRUCT_SIZE
1331
- EVENT_CAPACITY.times do |idx|
1332
- table[idx] = ptr
1333
- end
1334
- end
1335
-
1336
- def start_kprint_logger(bpf)
1337
- Thread.new do
1338
- begin
1339
- bpf.trace_fields do |_task, pid, _cpu, _flags, ts, msg|
1340
- line = msg.to_s.strip
1341
- next unless line.start_with?("vivarium:")
1342
-
1343
- puts "[vivariumd:kprint #{ts} pid=#{pid}] #{line}"
1344
- end
1345
- rescue IOError, Errno::EINTR
1346
- nil
1347
- rescue StandardError => e
1348
- warn "[vivariumd] kprint stream stopped: #{e.class}: #{e.message}"
1349
- end
1350
- end
1351
- rescue StandardError => e
1352
- warn "[vivariumd] failed to start kprint logger: #{e.class}: #{e.message}"
1353
- nil
1354
- end
1355
-
1356
1430
  def detect_f_path_offset
1357
1431
  env_offset = ENV["VIVARIUM_FILE_F_PATH_OFFSET"]
1358
1432
  return Integer(env_offset, 10) if env_offset
@@ -1465,75 +1539,141 @@ module Vivarium
1465
1539
  end
1466
1540
 
1467
1541
  class ObservationSession
1468
- def initialize(store:, pid:, tracer:)
1542
+ def initialize(store:, pid:, tracer:, correlator:)
1469
1543
  @store = store
1470
1544
  @pid = pid
1471
1545
  @tracer = tracer
1546
+ @correlator = correlator
1472
1547
  @stopped = false
1473
1548
  end
1474
1549
 
1475
1550
  def stop
1476
1551
  return if @stopped
1477
1552
 
1553
+ @stopped = true
1478
1554
  @tracer.disable
1479
1555
  @store.unregister_pid(@pid)
1480
- @stopped = true
1556
+ @correlator.stop
1481
1557
  end
1482
1558
  end
1483
1559
 
1484
- def self.observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human)
1485
- return scoped_observe(pin_dir: pin_dir, logger: logger, dest: dest, format: format) { yield } if block_given?
1560
+ def self.observe(pin_dir: bpf_pin_dir, dest: $stdout, &block)
1561
+ return scoped_observe(pin_dir: pin_dir, dest: dest, &block) if block_given?
1486
1562
 
1487
- top_observe(pin_dir: pin_dir, logger: logger, dest: dest, format: format)
1563
+ top_observe(pin_dir: pin_dir, dest: dest)
1488
1564
  end
1489
1565
 
1490
- def self.top_observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human)
1491
- logger ||= Logger.new(dest: dest, format: format)
1566
+ def self.top_observe(pin_dir: bpf_pin_dir, dest: $stdout)
1567
+ require "vivarium_usdt"
1568
+
1492
1569
  store = MapStore.new(pin_dir: pin_dir)
1493
1570
  pid = Process.pid
1494
1571
  store.register_pid(pid)
1495
- logger.info("top-level observing with pid=#{pid}")
1496
1572
 
1497
- tracer = build_observe_tracepoint(store, logger)
1573
+ method_id_queue = Thread::Queue.new
1574
+ main_tid = gettid
1575
+
1576
+ correlator = Correlator.new(
1577
+ pin_dir: pin_dir,
1578
+ observer_pid: pid,
1579
+ main_tid: main_tid,
1580
+ method_id_queue: method_id_queue,
1581
+ dest: dest
1582
+ )
1583
+ correlator.start
1584
+
1585
+ tracer = build_observe_tracepoint(method_id_queue)
1498
1586
  tracer.enable
1499
1587
 
1500
- session = ObservationSession.new(store: store, pid: pid, tracer: tracer)
1588
+ session = ObservationSession.new(
1589
+ store: store, pid: pid, tracer: tracer, correlator: correlator
1590
+ )
1501
1591
  at_exit { session.stop }
1502
1592
  session
1503
1593
  end
1504
1594
 
1505
- def self.scoped_observe(pin_dir:, logger:, dest:, format:)
1506
- logger ||= Logger.new(dest: dest, format: format)
1595
+ def self.scoped_observe(pin_dir:, dest:)
1596
+ require "vivarium_usdt"
1597
+
1507
1598
  store = MapStore.new(pin_dir: pin_dir)
1508
1599
  pid = Process.pid
1509
1600
  store.register_pid(pid)
1510
- logger.info("scoped observing with pid=#{pid}")
1511
1601
 
1512
- tracer = build_observe_tracepoint(store, logger)
1602
+ method_id_queue = Thread::Queue.new
1603
+ main_tid = gettid
1604
+
1605
+ correlator = Correlator.new(
1606
+ pin_dir: pin_dir,
1607
+ observer_pid: pid,
1608
+ main_tid: main_tid,
1609
+ method_id_queue: method_id_queue,
1610
+ dest: dest
1611
+ )
1612
+ correlator.start
1613
+
1614
+ tracer = build_observe_tracepoint(method_id_queue)
1513
1615
  tracer.enable
1514
1616
 
1515
1617
  yield
1516
1618
  ensure
1517
1619
  tracer&.disable
1518
1620
  store&.unregister_pid(pid)
1621
+ correlator&.stop
1519
1622
  end
1520
1623
 
1521
- def self.build_observe_tracepoint(store, logger)
1522
- TracePoint.new(:return, :c_return) do |tp|
1523
- events = store.drain_events
1524
- next if events.empty?
1624
+ def self.build_observe_tracepoint(method_id_queue)
1625
+ allow_classes = SPAN_ALLOWCLASSES
1626
+ allowlist = SPAN_ALLOWLIST
1627
+ TracePoint.new(:call, :c_call, :return, :c_return, :raise) do |tp|
1628
+ if tp.event == :raise
1629
+ Vivarium::Usdt.raise(
1630
+ tp.raised_exception.class.to_s,
1631
+ tp.raised_exception.message.to_s,
1632
+ file: tp.path,
1633
+ lineno: tp.lineno
1634
+ )
1635
+ next
1636
+ end
1637
+
1638
+ signature = "#{tp.defined_class}##{tp.method_id}"
1639
+ is_target = allowlist.include?(signature) || \
1640
+ allow_classes.any? { |klass| tp.defined_class == klass } || \
1641
+ allow_classes.any? { |klass| tp.defined_class == klass.singleton_class }
1642
+ next unless is_target
1643
+
1644
+ case tp.event
1645
+ when :call, :c_call
1646
+ method_id = Vivarium::Usdt.start(tp.defined_class.to_s, tp.method_id.to_s, file: tp.path, lineno: tp.lineno)
1647
+ method_id_queue << [method_id, signature]
1648
+ when :return, :c_return
1649
+ Vivarium::Usdt.stop(tp.defined_class.to_s, tp.method_id.to_s, file: tp.path, lineno: tp.lineno)
1650
+ end
1651
+ end
1652
+ end
1525
1653
 
1526
- stack = caller_locations(2, 16)
1527
- stack = stack.reject { |loc| loc.path.to_s.include?("vivarium") } if filter_internal_frames?
1528
- logger.log(events, tp, stack)
1654
+ def self.gettid
1655
+ @gettid_func ||= begin
1656
+ libc = Fiddle.dlopen("libc.so.6")
1657
+ Fiddle::Function.new(libc["gettid"], [], Fiddle::TYPE_INT)
1658
+ rescue Fiddle::DLError
1659
+ libc = Fiddle.dlopen(nil)
1660
+ Fiddle::Function.new(libc["gettid"], [], Fiddle::TYPE_INT)
1529
1661
  end
1662
+ @gettid_func.call
1663
+ end
1664
+
1665
+ def self.monotonic_ktime_ns
1666
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
1530
1667
  end
1531
1668
 
1532
- def self.filter_internal_frames?
1533
- value = ENV["VIVARIUM_FILTER_INTERNAL_FRAMES"]
1534
- return true if value.nil?
1669
+ def self.locate_vivarium_usdt_so
1670
+ require "vivarium_usdt/vivarium_usdt"
1671
+ so = $LOADED_FEATURES.find { |p| p =~ %r{vivarium_usdt/vivarium_usdt\.(so|bundle|dylib)\z} }
1672
+ raise Error, "vivarium_usdt native extension not found in $LOADED_FEATURES" unless so
1535
1673
 
1536
- !%w[0 false off no].include?(value.strip.downcase)
1674
+ File.realpath(so)
1675
+ rescue LoadError => e
1676
+ raise Error, "failed to load vivarium_usdt: #{e.message}"
1537
1677
  end
1538
1678
 
1539
1679
  def self.run_daemon!(argv = ARGV)
@@ -1546,3 +1686,6 @@ module Vivarium
1546
1686
  Daemon.new(pin_dir: options[:pin_dir]).run
1547
1687
  end
1548
1688
  end
1689
+
1690
+ require_relative "vivarium/correlator"
1691
+ require_relative "vivarium/tree_renderer"
data/logo-simple.png ADDED
Binary file