vivarium 0.2.0 → 0.3.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.
data/lib/vivarium.rb CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  require "fiddle"
4
4
  require "fileutils"
5
+ require "net/http"
5
6
  require "optparse"
6
7
  require "pathname"
7
8
  require "rbbcc"
8
9
  require "socket"
9
10
  require_relative "vivarium/version"
10
- require_relative "vivarium/logger"
11
+ require_relative "vivarium/cli"
11
12
 
12
13
  module Vivarium
13
14
  class Error < StandardError; end
@@ -16,8 +17,7 @@ module Vivarium
16
17
  CONFIG_ROOT_TARGETS_PIN = File.join(PIN_DIR, "config_root_targets")
17
18
  CONFIG_SPAWNED_TARGETS_PIN = File.join(PIN_DIR, "config_spawned_targets")
18
19
  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")
20
+ EVENTS_PIN = File.join(PIN_DIR, "events")
21
21
 
22
22
  EVENT_NAME_SIZE = 16
23
23
  EVENT_PAYLOAD_SIZE = 256
@@ -27,9 +27,55 @@ module Vivarium
27
27
  EVENT_STRUCT_SIZE = 288
28
28
  EVENT_TS_OFFSET = 0
29
29
  EVENT_PID_OFFSET = 8
30
- EVENT_NAME_OFFSET = 12
31
- EVENT_PAYLOAD_OFFSET = 28
32
- EVENT_CAPACITY = 1024
30
+ EVENT_TID_OFFSET = 12
31
+ EVENT_NAME_OFFSET = 16
32
+ EVENT_PAYLOAD_OFFSET = 32
33
+ EVENTS_RINGBUF_PAGES = 256
34
+
35
+ SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET = 0
36
+ SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET = 4
37
+ SSL_WRITE_PAYLOAD_DATA_OFFSET = 8
38
+ SSL_WRITE_PAYLOAD_DATA_MAX = EVENT_PAYLOAD_SIZE - SSL_WRITE_PAYLOAD_DATA_OFFSET
39
+
40
+ LIBSSL_SEARCH_PATHS = [
41
+ "/lib/x86_64-linux-gnu/libssl.so.3",
42
+ "/lib/x86_64-linux-gnu/libssl.so.1.1",
43
+ "/lib/aarch64-linux-gnu/libssl.so.3",
44
+ "/lib/aarch64-linux-gnu/libssl.so.1.1",
45
+ "/usr/lib/x86_64-linux-gnu/libssl.so.3",
46
+ "/usr/lib/x86_64-linux-gnu/libssl.so.1.1",
47
+ "/usr/lib/aarch64-linux-gnu/libssl.so.3",
48
+ "/usr/lib/aarch64-linux-gnu/libssl.so.1.1",
49
+ "/usr/lib64/libssl.so.3",
50
+ "/usr/lib64/libssl.so.1.1",
51
+ "/usr/lib/libssl.so.3",
52
+ "/usr/lib/libssl.so.1.1"
53
+ ].freeze
54
+
55
+ SPAN_ALLOWCLASSES = [
56
+ Socket,
57
+ BasicSocket,
58
+ IPSocket,
59
+ TCPSocket,
60
+ UDPSocket,
61
+ UNIXSocket,
62
+ File,
63
+ Dir,
64
+ Signal,
65
+ Process,
66
+ Process::UID,
67
+ Process::GID,
68
+ Net::HTTP,
69
+ ]
70
+ SPAN_ALLOWLIST = [
71
+ "Kernel#system",
72
+ "Kernel#require",
73
+ "Kernel#require_relative",
74
+ "Kernel#load",
75
+ "Kernel#eval",
76
+ "Object#instance_eval",
77
+ "Object#instance_exec",
78
+ ].freeze
33
79
  EVENT_SEVERITY_HIGH = %w[
34
80
  capable_check bprm_creds setid_change task_kill
35
81
  ptrace_check sb_mount kernel_read_file
@@ -83,49 +129,12 @@ module Vivarium
83
129
  end
84
130
  end
85
131
 
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
132
  def self.c_string(bytes)
128
- Event.c_string(bytes)
133
+ str = bytes.to_s.b
134
+ nul = str.index("\x00")
135
+ return str if nul.nil?
136
+
137
+ str[0, nul]
129
138
  end
130
139
 
131
140
  def self.event_severity(event_name)
@@ -330,6 +339,68 @@ module Vivarium
330
339
  "has_file=#{has_file} file=#{path.inspect}"
331
340
  end
332
341
 
342
+ def self.decode_proc_fork_payload(raw_payload)
343
+ bytes = raw_payload.to_s.b
344
+ return "" if bytes.bytesize < 8
345
+
346
+ child_pid = bytes[0, 4].unpack1("L<")
347
+ child_tid = bytes[4, 4].unpack1("L<")
348
+ "child_pid=#{child_pid} child_tid=#{child_tid}"
349
+ end
350
+
351
+ def self.decode_span_payload(raw_payload)
352
+ bytes = raw_payload.to_s.b
353
+ return "" if bytes.bytesize < 8
354
+
355
+ method_id = bytes[0, 8].unpack1("q<")
356
+ result = format("method_id=0x%016X", method_id & 0xFFFF_FFFF_FFFF_FFFF)
357
+
358
+ if bytes.bytesize >= 24
359
+ file_id = bytes[8, 8].unpack1("q<")
360
+ lineno = bytes[16, 8].unpack1("q<")
361
+ result += format(" file_id=0x%016X", file_id & 0xFFFF_FFFF_FFFF_FFFF) if file_id != -1
362
+ result += " lineno=#{lineno}" if lineno > 0
363
+ end
364
+
365
+ result
366
+ end
367
+
368
+ def self.decode_ssl_write_payload(raw_payload)
369
+ bytes = raw_payload.to_s.b
370
+ return { data_len: 0, cap_len: 0, data: "".b } if bytes.bytesize < SSL_WRITE_PAYLOAD_DATA_OFFSET
371
+
372
+ data_len = bytes[SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET, 4].unpack1("L<")
373
+ cap_len = bytes[SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET, 4].unpack1("L<")
374
+ cap_len = SSL_WRITE_PAYLOAD_DATA_MAX if cap_len > SSL_WRITE_PAYLOAD_DATA_MAX
375
+ data = bytes[SSL_WRITE_PAYLOAD_DATA_OFFSET, cap_len] || "".b
376
+ { data_len: data_len, cap_len: cap_len, data: data }
377
+ end
378
+
379
+ def self.decode_span_raise_payload(raw_payload)
380
+ bytes = raw_payload.to_s.b
381
+ return "" if bytes.bytesize < 8
382
+
383
+ error_id = bytes[0, 8].unpack1("q<")
384
+ result = format("error_id=0x%016X", error_id & 0xFFFF_FFFF_FFFF_FFFF)
385
+
386
+ if bytes.bytesize >= 16
387
+ message_id = bytes[8, 8].unpack1("q<")
388
+ result += format(" message_id=0x%016X", message_id & 0xFFFF_FFFF_FFFF_FFFF)
389
+ end
390
+
391
+ if bytes.bytesize >= 24
392
+ file_id = bytes[16, 8].unpack1("q<")
393
+ result += format(" file_id=0x%016X", file_id & 0xFFFF_FFFF_FFFF_FFFF) if file_id != -1
394
+ end
395
+
396
+ if bytes.bytesize >= 32
397
+ lineno = bytes[24, 8].unpack1("q<")
398
+ result += " lineno=#{lineno}" if lineno > 0
399
+ end
400
+
401
+ result
402
+ end
403
+
333
404
  def self.render_event_payload(event)
334
405
  case event.event_name
335
406
  when "dns_req"
@@ -365,6 +436,15 @@ module Vivarium
365
436
  when "bprm_creds"
366
437
  decoded = decode_bprm_creds_payload(event.payload)
367
438
  decoded.empty? ? event.payload.inspect : decoded
439
+ when "proc_fork"
440
+ decoded = decode_proc_fork_payload(event.payload)
441
+ decoded.empty? ? event.payload.inspect : decoded
442
+ when "span_start", "span_stop"
443
+ decoded = decode_span_payload(event.payload)
444
+ decoded.empty? ? event.payload.inspect : decoded
445
+ when "span_raise"
446
+ decoded = decode_span_raise_payload(event.payload)
447
+ decoded.empty? ? event.payload.inspect : decoded
368
448
  when "file_symlink"
369
449
  decoded = decode_file_symlink_payload(event.payload)
370
450
  decoded.empty? ? event.payload.inspect : decoded
@@ -380,11 +460,21 @@ module Vivarium
380
460
  when "file_getdents"
381
461
  decoded = decode_file_getdents_payload(event.payload)
382
462
  decoded.empty? ? event.payload.inspect : decoded
463
+ when "ssl_write"
464
+ decoded = decode_ssl_write_payload(event.payload)
465
+ "data_len=#{decoded[:data_len]} cap_len=#{decoded[:cap_len]}"
383
466
  else
384
- event.payload.inspect
467
+ strip_to_first_null(event.payload).inspect
385
468
  end
386
469
  end
387
470
 
471
+ def self.strip_to_first_null(bytes)
472
+ nul = bytes.index("\x00")
473
+ return bytes if nul.nil?
474
+
475
+ bytes[0, nul]
476
+ end
477
+
388
478
  class MapStore
389
479
  def initialize(pin_dir: Vivarium.bpf_pin_dir)
390
480
  @pin_dir = pin_dir
@@ -402,20 +492,6 @@ module Vivarium
402
492
  keysize: 4,
403
493
  leafsize: 1
404
494
  )
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
495
  rescue StandardError => e
420
496
  raise Error, "failed to open pinned maps under #{@pin_dir}: #{e.class}: #{e.message}"
421
497
  end
@@ -430,31 +506,6 @@ module Vivarium
430
506
  rescue KeyError
431
507
  nil
432
508
  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
509
  end
459
510
 
460
511
  class Daemon
@@ -565,6 +616,7 @@ module Vivarium
565
616
  struct event_t {
566
617
  u64 ktime_ns;
567
618
  u32 pid;
619
+ u32 tid;
568
620
  char event_name[16];
569
621
  char payload[#{EVENT_PAYLOAD_SIZE}];
570
622
  };
@@ -572,8 +624,7 @@ module Vivarium
572
624
  BPF_HASH(config_root_targets, u32, u8, 1024);
573
625
  BPF_HASH(config_spawned_targets, u32, u8, 8192);
574
626
  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);
627
+ BPF_RINGBUF_OUTPUT(events, #{EVENTS_RINGBUF_PAGES});
577
628
 
578
629
  static __always_inline int target_enabled(u32 pid, u32 tid)
579
630
  {
@@ -613,19 +664,18 @@ module Vivarium
613
664
  }
614
665
  }
615
666
 
616
- static __always_inline void submit_event(struct event_t *ev)
667
+ static __always_inline void submit_event(struct event_t *src)
617
668
  {
618
- u32 zero = 0;
619
- u32 *write_pos = event_write_pos.lookup(&zero);
620
- if (!write_pos) {
669
+ struct event_t *ev = events.ringbuf_reserve(sizeof(struct event_t));
670
+ if (!ev) {
621
671
  return;
622
672
  }
623
673
 
674
+ __builtin_memcpy(ev, src, sizeof(*ev));
624
675
  ev->ktime_ns = bpf_ktime_get_ns();
676
+ ev->tid = (u32)bpf_get_current_pid_tgid();
625
677
 
626
- u32 idx = *write_pos % #{EVENT_CAPACITY};
627
- __sync_fetch_and_add(write_pos, 1);
628
- event_invoked.update(&idx, ev);
678
+ events.ringbuf_submit(ev, 0);
629
679
  }
630
680
 
631
681
  static __always_inline int is_dns_destination(void *addr)
@@ -696,16 +746,28 @@ module Vivarium
696
746
  u32 parent = args->parent_pid;
697
747
  u32 child = args->child_pid;
698
748
  u8 one = 1;
749
+ int is_target = 0;
699
750
 
700
751
  u8 *enabled_root = config_root_targets.lookup(&parent);
701
752
  if (enabled_root && *enabled_root == 1) {
753
+ is_target = 1;
702
754
  config_spawned_targets.update(&child, &one);
703
- return 0;
755
+ } else {
756
+ u8 *enabled_spawned = config_spawned_targets.lookup(&parent);
757
+ if (enabled_spawned && *enabled_spawned == 1) {
758
+ is_target = 1;
759
+ config_spawned_targets.update(&child, &one);
760
+ }
704
761
  }
705
762
 
706
- u8 *enabled_spawned = config_spawned_targets.lookup(&parent);
707
- if (enabled_spawned && *enabled_spawned == 1) {
708
- config_spawned_targets.update(&child, &one);
763
+ if (is_target) {
764
+ u64 pid_tgid = bpf_get_current_pid_tgid();
765
+ struct event_t ev = {};
766
+ ev.pid = pid_tgid >> 32;
767
+ __builtin_memcpy(ev.event_name, "proc_fork", 10);
768
+ __builtin_memcpy(&ev.payload[0], &child, sizeof(child));
769
+ __builtin_memcpy(&ev.payload[4], &child, sizeof(child));
770
+ submit_event(&ev);
709
771
  }
710
772
 
711
773
  return 0;
@@ -724,7 +786,6 @@ module Vivarium
724
786
  u64 pid_tgid = bpf_get_current_pid_tgid();
725
787
  u32 pid = pid_tgid >> 32;
726
788
  u32 tid = (u32)pid_tgid;
727
- bpf_trace_printk("vivarium: invoked pid=%d\\n", pid);
728
789
  if (!target_enabled(pid, tid)) {
729
790
  return 0;
730
791
  }
@@ -738,11 +799,9 @@ module Vivarium
738
799
  if (path_ret < 0) {
739
800
  if (ev.payload[0] == 0) {
740
801
  __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
802
  }
743
803
  }
744
804
 
745
- bpf_trace_printk("vivarium: pid=%d path=%s\\n", pid, ev.payload);
746
805
  submit_event(&ev);
747
806
 
748
807
  return 0;
@@ -1262,10 +1321,132 @@ module Vivarium
1262
1321
  submit_event(&ev);
1263
1322
  return 0;
1264
1323
  }
1324
+
1325
+ int on_span_start(struct pt_regs *ctx)
1326
+ {
1327
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1328
+ u32 pid = pid_tgid >> 32;
1329
+ u32 tid = (u32)pid_tgid;
1330
+
1331
+ if (!target_enabled(pid, tid)) {
1332
+ return 0;
1333
+ }
1334
+
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);
1340
+ bpf_usdt_readarg(3, ctx, &lineno);
1341
+
1342
+ struct event_t ev = {};
1343
+ ev.pid = pid;
1344
+ __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));
1348
+ submit_event(&ev);
1349
+ return 0;
1350
+ }
1351
+
1352
+ int on_span_stop(struct pt_regs *ctx)
1353
+ {
1354
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1355
+ u32 pid = pid_tgid >> 32;
1356
+ u32 tid = (u32)pid_tgid;
1357
+
1358
+ if (!target_enabled(pid, tid)) {
1359
+ return 0;
1360
+ }
1361
+
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);
1367
+ bpf_usdt_readarg(3, ctx, &lineno);
1368
+
1369
+ struct event_t ev = {};
1370
+ ev.pid = pid;
1371
+ __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));
1375
+ submit_event(&ev);
1376
+ return 0;
1377
+ }
1378
+
1379
+ int on_ssl_write(struct pt_regs *ctx)
1380
+ {
1381
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1382
+ u32 pid = pid_tgid >> 32;
1383
+ u32 tid = (u32)pid_tgid;
1384
+
1385
+ if (!target_enabled(pid, tid)) {
1386
+ return 0;
1387
+ }
1388
+
1389
+ const char *buf = (const char *)PT_REGS_PARM2(ctx);
1390
+ int num = (int)PT_REGS_PARM3(ctx);
1391
+ if (!buf || num <= 0) {
1392
+ return 0;
1393
+ }
1394
+
1395
+ struct event_t ev = {};
1396
+ ev.pid = pid;
1397
+ __builtin_memcpy(ev.event_name, "ssl_write", 10);
1398
+
1399
+ u32 data_len = (u32)num;
1400
+ u32 cap = data_len;
1401
+ if (cap > #{SSL_WRITE_PAYLOAD_DATA_MAX}) {
1402
+ cap = #{SSL_WRITE_PAYLOAD_DATA_MAX};
1403
+ }
1404
+ __builtin_memcpy(&ev.payload[#{SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET}], &data_len, sizeof(data_len));
1405
+ __builtin_memcpy(&ev.payload[#{SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET}], &cap, sizeof(cap));
1406
+ if (bpf_probe_read_user(&ev.payload[#{SSL_WRITE_PAYLOAD_DATA_OFFSET}], cap, buf) < 0) {
1407
+ u32 zero = 0;
1408
+ __builtin_memcpy(&ev.payload[#{SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET}], &zero, sizeof(zero));
1409
+ }
1410
+
1411
+ submit_event(&ev);
1412
+ return 0;
1413
+ }
1414
+
1415
+ int on_span_raise(struct pt_regs *ctx)
1416
+ {
1417
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1418
+ u32 pid = pid_tgid >> 32;
1419
+ u32 tid = (u32)pid_tgid;
1420
+
1421
+ if (!target_enabled(pid, tid)) {
1422
+ return 0;
1423
+ }
1424
+
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);
1432
+ bpf_usdt_readarg(4, ctx, &lineno);
1433
+
1434
+ struct event_t ev = {};
1435
+ ev.pid = pid;
1436
+ __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));
1441
+ submit_event(&ev);
1442
+ return 0;
1443
+ }
1265
1444
  CLANG
1266
1445
 
1267
- def initialize(pin_dir: Vivarium.bpf_pin_dir)
1446
+ def initialize(pin_dir: Vivarium.bpf_pin_dir, ssl_trace: true, libssl_path: nil)
1268
1447
  @pin_dir = pin_dir
1448
+ @ssl_trace = ssl_trace
1449
+ @libssl_path = libssl_path
1269
1450
  end
1270
1451
 
1271
1452
  def run
@@ -1278,42 +1459,73 @@ module Vivarium
1278
1459
  .gsub("__VIVARIUM_F_PATH_OFFSET__", f_path_offset.to_s)
1279
1460
  .gsub("__VIVARIUM_DENTRY_D_NAME_OFFSET__", d_name_offset.to_s)
1280
1461
 
1281
- bpf = RbBCC::BCC.new(text: program)
1282
- kprint_thread = start_kprint_logger(bpf)
1462
+ usdt_so_path = ENV.fetch("VIVARIUM_USDT_SO_PATH") { Vivarium.locate_vivarium_usdt_so }
1463
+ usdt = RbBCC::USDT.new(path: usdt_so_path)
1464
+ usdt.enable_probe(probe: "start_probe", fn_name: "on_span_start")
1465
+ usdt.enable_probe(probe: "stop_probe", fn_name: "on_span_stop")
1466
+ usdt.enable_probe(probe: "raise_probe", fn_name: "on_span_raise")
1467
+
1468
+ bpf = RbBCC::BCC.new(text: program, usdt_contexts: [usdt])
1469
+
1470
+ attach_ssl_write_uprobe(bpf) if @ssl_trace
1283
1471
 
1284
1472
  config_root_targets = bpf["config_root_targets"]
1285
1473
  config_spawned_targets = bpf["config_spawned_targets"]
1286
- event_invoked = bpf["event_invoked"]
1287
- event_write_pos = bpf["event_write_pos"]
1474
+ events_ringbuf = bpf["events"]
1288
1475
 
1289
- clear_event_slots(event_invoked)
1290
- event_write_pos[0] = 0
1291
1476
  config_spawned_targets.clear
1292
1477
 
1293
1478
  pin_map(config_root_targets, File.join(@pin_dir, "config_root_targets"))
1294
1479
  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"))
1480
+ pin_map(events_ringbuf, File.join(@pin_dir, "events"))
1297
1481
 
1298
1482
  puts "[vivariumd] started"
1299
1483
  puts "[vivariumd] pinned maps in #{@pin_dir}"
1300
1484
  puts "[vivariumd] watching LSM file_open (f_path offset=#{f_path_offset})"
1301
- puts "[vivariumd] kprint logger enabled"
1485
+ puts "[vivariumd] USDT attached via #{usdt_so_path}"
1302
1486
 
1303
1487
  loop do
1304
1488
  sleep 1
1305
1489
  end
1306
1490
  rescue Interrupt
1307
1491
  puts "\n[vivariumd] stopping"
1308
- ensure
1309
- if kprint_thread
1310
- kprint_thread.kill
1311
- kprint_thread.join(0.2)
1312
- end
1313
1492
  end
1314
1493
 
1315
1494
  private
1316
1495
 
1496
+ def attach_ssl_write_uprobe(bpf)
1497
+ path = resolve_libssl_path
1498
+ unless path
1499
+ warn "[vivariumd] libssl not found; SSL_write uprobe disabled " \
1500
+ "(set --libssl PATH or VIVARIUM_LIBSSL_PATH to override)"
1501
+ return
1502
+ end
1503
+
1504
+ bpf.attach_uprobe(name: path, sym: "SSL_write", fn_name: "on_ssl_write")
1505
+ puts "[vivariumd] SSL_write uprobe attached via #{path}"
1506
+ rescue StandardError => e
1507
+ warn "[vivariumd] SSL_write uprobe attach failed: #{e.class}: #{e.message}"
1508
+ end
1509
+
1510
+ def resolve_libssl_path
1511
+ if @libssl_path
1512
+ return @libssl_path if File.exist?(@libssl_path)
1513
+
1514
+ warn "[vivariumd] --libssl path does not exist: #{@libssl_path}"
1515
+ return nil
1516
+ end
1517
+
1518
+ env_path = ENV["VIVARIUM_LIBSSL_PATH"]
1519
+ if env_path && !env_path.empty?
1520
+ return env_path if File.exist?(env_path)
1521
+
1522
+ warn "[vivariumd] VIVARIUM_LIBSSL_PATH does not exist: #{env_path}"
1523
+ return nil
1524
+ end
1525
+
1526
+ LIBSSL_SEARCH_PATHS.find { |p| File.exist?(p) }
1527
+ end
1528
+
1317
1529
  def ensure_root!
1318
1530
  return if Process.uid.zero?
1319
1531
 
@@ -1325,34 +1537,6 @@ module Vivarium
1325
1537
  RbBCC::BCC.pin!(table.map_fd, path)
1326
1538
  end
1327
1539
 
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
1540
  def detect_f_path_offset
1357
1541
  env_offset = ENV["VIVARIUM_FILE_F_PATH_OFFSET"]
1358
1542
  return Integer(env_offset, 10) if env_offset
@@ -1465,84 +1649,171 @@ module Vivarium
1465
1649
  end
1466
1650
 
1467
1651
  class ObservationSession
1468
- def initialize(store:, pid:, tracer:)
1652
+ def initialize(store:, pid:, tracer:, correlator:)
1469
1653
  @store = store
1470
1654
  @pid = pid
1471
1655
  @tracer = tracer
1656
+ @correlator = correlator
1472
1657
  @stopped = false
1473
1658
  end
1474
1659
 
1475
1660
  def stop
1476
1661
  return if @stopped
1477
1662
 
1663
+ @stopped = true
1478
1664
  @tracer.disable
1479
1665
  @store.unregister_pid(@pid)
1480
- @stopped = true
1666
+ @correlator.stop
1481
1667
  end
1482
1668
  end
1483
1669
 
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?
1670
+ def self.observe(pin_dir: bpf_pin_dir, dest: $stdout, filter: nil, &block)
1671
+ return scoped_observe(pin_dir: pin_dir, dest: dest, filter: filter, &block) if block_given?
1486
1672
 
1487
- top_observe(pin_dir: pin_dir, logger: logger, dest: dest, format: format)
1673
+ top_observe(pin_dir: pin_dir, dest: dest, filter: filter)
1488
1674
  end
1489
1675
 
1490
- def self.top_observe(pin_dir: bpf_pin_dir, logger: nil, dest: $stdout, format: :human)
1491
- logger ||= Logger.new(dest: dest, format: format)
1676
+ def self.top_observe(pin_dir: bpf_pin_dir, dest: $stdout, filter: nil)
1677
+ require "vivarium_usdt"
1678
+
1492
1679
  store = MapStore.new(pin_dir: pin_dir)
1493
1680
  pid = Process.pid
1494
1681
  store.register_pid(pid)
1495
- logger.info("top-level observing with pid=#{pid}")
1496
1682
 
1497
- tracer = build_observe_tracepoint(store, logger)
1683
+ method_id_queue = Thread::Queue.new
1684
+ main_tid = gettid
1685
+
1686
+ correlator = Correlator.new(
1687
+ pin_dir: pin_dir,
1688
+ observer_pid: pid,
1689
+ main_tid: main_tid,
1690
+ method_id_queue: method_id_queue,
1691
+ filter: filter,
1692
+ dest: dest
1693
+ )
1694
+ correlator.start
1695
+
1696
+ tracer = build_observe_tracepoint(method_id_queue)
1498
1697
  tracer.enable
1499
1698
 
1500
- session = ObservationSession.new(store: store, pid: pid, tracer: tracer)
1699
+ session = ObservationSession.new(
1700
+ store: store, pid: pid, tracer: tracer, correlator: correlator
1701
+ )
1501
1702
  at_exit { session.stop }
1502
1703
  session
1503
1704
  end
1504
1705
 
1505
- def self.scoped_observe(pin_dir:, logger:, dest:, format:)
1506
- logger ||= Logger.new(dest: dest, format: format)
1706
+ def self.scoped_observe(pin_dir:, dest:, filter: nil)
1707
+ require "vivarium_usdt"
1708
+
1507
1709
  store = MapStore.new(pin_dir: pin_dir)
1508
1710
  pid = Process.pid
1509
1711
  store.register_pid(pid)
1510
- logger.info("scoped observing with pid=#{pid}")
1511
1712
 
1512
- tracer = build_observe_tracepoint(store, logger)
1713
+ method_id_queue = Thread::Queue.new
1714
+ main_tid = gettid
1715
+
1716
+ correlator = Correlator.new(
1717
+ pin_dir: pin_dir,
1718
+ observer_pid: pid,
1719
+ main_tid: main_tid,
1720
+ method_id_queue: method_id_queue,
1721
+ filter: filter,
1722
+ dest: dest
1723
+ )
1724
+ correlator.start
1725
+
1726
+ tracer = build_observe_tracepoint(method_id_queue)
1513
1727
  tracer.enable
1514
1728
 
1515
1729
  yield
1516
1730
  ensure
1517
1731
  tracer&.disable
1518
1732
  store&.unregister_pid(pid)
1733
+ correlator&.stop
1519
1734
  end
1520
1735
 
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?
1736
+ def self.build_observe_tracepoint(method_id_queue)
1737
+ allow_classes = SPAN_ALLOWCLASSES
1738
+ allowlist = SPAN_ALLOWLIST
1739
+ TracePoint.new(:call, :c_call, :return, :c_return, :raise) do |tp|
1740
+ if tp.event == :raise
1741
+ # FIXME: handle threaded events in the future
1742
+ if tp.raised_exception.kind_of?(ThreadError)
1743
+ next
1744
+ end
1525
1745
 
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)
1746
+ Vivarium::Usdt.raise(
1747
+ tp.raised_exception.class.to_s,
1748
+ tp.raised_exception.message.to_s,
1749
+ file: tp.path,
1750
+ lineno: tp.lineno
1751
+ )
1752
+ next
1753
+ end
1754
+
1755
+ signature = "#{tp.defined_class}##{tp.method_id}"
1756
+ is_target = allowlist.include?(signature) || \
1757
+ allow_classes.any? { |klass| tp.defined_class == klass } || \
1758
+ allow_classes.any? { |klass| tp.defined_class == klass.singleton_class }
1759
+ next unless is_target
1760
+
1761
+ case tp.event
1762
+ 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]
1765
+ when :return, :c_return
1766
+ Vivarium::Usdt.stop(tp.defined_class.to_s, tp.method_id.to_s, file: tp.path, lineno: tp.lineno)
1767
+ end
1529
1768
  end
1530
1769
  end
1531
1770
 
1532
- def self.filter_internal_frames?
1533
- value = ENV["VIVARIUM_FILTER_INTERNAL_FRAMES"]
1534
- return true if value.nil?
1771
+ def self.gettid
1772
+ @gettid_func ||= begin
1773
+ libc = Fiddle.dlopen("libc.so.6")
1774
+ Fiddle::Function.new(libc["gettid"], [], Fiddle::TYPE_INT)
1775
+ rescue Fiddle::DLError
1776
+ libc = Fiddle.dlopen(nil)
1777
+ Fiddle::Function.new(libc["gettid"], [], Fiddle::TYPE_INT)
1778
+ end
1779
+ @gettid_func.call
1780
+ end
1535
1781
 
1536
- !%w[0 false off no].include?(value.strip.downcase)
1782
+ def self.monotonic_ktime_ns
1783
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
1784
+ end
1785
+
1786
+ def self.locate_vivarium_usdt_so
1787
+ require "vivarium_usdt/vivarium_usdt"
1788
+ so = $LOADED_FEATURES.find { |p| p =~ %r{vivarium_usdt/vivarium_usdt\.(so|bundle|dylib)\z} }
1789
+ raise Error, "vivarium_usdt native extension not found in $LOADED_FEATURES" unless so
1790
+
1791
+ File.realpath(so)
1792
+ rescue LoadError => e
1793
+ raise Error, "failed to load vivarium_usdt: #{e.message}"
1537
1794
  end
1538
1795
 
1539
1796
  def self.run_daemon!(argv = ARGV)
1540
- options = { pin_dir: bpf_pin_dir }
1797
+ options = { pin_dir: bpf_pin_dir, ssl_trace: true, libssl_path: nil }
1541
1798
  OptionParser.new do |opts|
1542
- opts.banner = "Usage: vivariumd [--pin-dir PATH]"
1799
+ opts.banner = "Usage: vivariumd [--pin-dir PATH] [--no-ssl-trace] [--libssl PATH]"
1543
1800
  opts.on("--pin-dir PATH", "Pinned map directory") { |v| options[:pin_dir] = v }
1801
+ opts.on("--[no-]ssl-trace", "Attach OpenSSL SSL_write uprobe (default: enabled)") do |v|
1802
+ options[:ssl_trace] = v
1803
+ end
1804
+ opts.on("--libssl PATH", "Path to libssl.so to attach SSL_write to") do |v|
1805
+ options[:libssl_path] = v
1806
+ end
1544
1807
  end.parse!(argv)
1545
1808
 
1546
- Daemon.new(pin_dir: options[:pin_dir]).run
1809
+ Daemon.new(
1810
+ pin_dir: options[:pin_dir],
1811
+ ssl_trace: options[:ssl_trace],
1812
+ libssl_path: options[:libssl_path]
1813
+ ).run
1547
1814
  end
1548
1815
  end
1816
+
1817
+ require_relative "vivarium/correlator"
1818
+ require_relative "vivarium/display_filter"
1819
+ require_relative "vivarium/tree_renderer"