Module: Vivarium
- Defined in:
- lib/vivarium.rb,
lib/vivarium/box.rb,
lib/vivarium/cli.rb,
lib/vivarium/version.rb,
lib/vivarium/raw_store.rb,
lib/vivarium/api_server.rb,
lib/vivarium/correlator.rb,
lib/vivarium/otel_stream.rb,
lib/vivarium/http_decoder.rb,
lib/vivarium/daemon_client.rb,
lib/vivarium/otel_exporter.rb,
lib/vivarium/tree_renderer.rb,
lib/vivarium/display_filter.rb
Defined Under Namespace
Modules: CLI, OtelExporter, RawStore Classes: ApiServer, Box, Correlator, Daemon, DaemonClient, DisplayFilter, Error, EventLog, HttpDecoder, ObservationSession, OtelHttpExporter, OtelSpanStreamer, RawEvent, Registry, TreeRenderer
Constant Summary collapse
- PIN_DIR =
ENV.fetch("VIVARIUM_BPF_PIN_DIR", "/sys/fs/bpf/vivarium")
- CONFIG_ROOT_TARGETS_PIN =
File.join(PIN_DIR, "config_root_targets")
- CONFIG_SPAWNED_TARGETS_PIN =
File.join(PIN_DIR, "config_spawned_targets")
- CONFIG_TARGETS_PIN =
CONFIG_ROOT_TARGETS_PIN- EVENTS_PIN =
File.join(PIN_DIR, "events")
- SOCKET_PATH =
ENV.fetch("VIVARIUM_SOCKET_PATH", "/run/vivarium/vivariumd.sock")
- EVENT_NAME_SIZE =
16- EVENT_PAYLOAD_SIZE =
256- EVENT_TS_SIZE =
8- EVENT_COMM_SIZE =
16- PROC_EXEC_SLOT_SIZE =
64- PROC_EXEC_SLOT_COUNT =
4- EVENT_STRUCT_SIZE =
352- EVENT_TS_OFFSET =
0- EVENT_PID_OFFSET =
8- EVENT_TID_OFFSET =
12- EVENT_UID_OFFSET =
16- EVENT_GID_OFFSET =
20- EVENT_TRACE_HI_OFFSET =
24- EVENT_TRACE_LO_OFFSET =
32- EVENT_SPAN_OFFSET =
40- EVENT_PARENT_SPAN_OFFSET =
48- EVENT_COMM_OFFSET =
56- EVENT_NAME_OFFSET =
72- EVENT_PAYLOAD_OFFSET =
88- EVENT_DROPPED_OFFSET =
344- EVENTS_RINGBUF_PAGES =
256- SPAN_METHOD_SIZE =
128- SPAN_FILE_SIZE =
120- SPAN_LINENO_OFFSET =
248
SPAN_METHOD_SIZE + SPAN_FILE_SIZE
- SPAN_FILE_ARG_MAX =
SPAN_FILE_SIZE - 1
- SPAN_RAISE_SLOT_SIZE =
80- SPAN_RAISE_LINENO_OFFSET =
240
SPAN_RAISE_SLOT_SIZE * 3
- SPAN_RAISE_FILE_ARG_MAX =
SPAN_RAISE_SLOT_SIZE - 1
- SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET =
0- SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET =
4- SSL_WRITE_PAYLOAD_DATA_OFFSET =
8- SSL_WRITE_PAYLOAD_DATA_MAX =
EVENT_PAYLOAD_SIZE - SSL_WRITE_PAYLOAD_DATA_OFFSET
- LIBSSL_SEARCH_PATHS =
[ "/lib/x86_64-linux-gnu/libssl.so.3", "/lib/x86_64-linux-gnu/libssl.so.1.1", "/lib/aarch64-linux-gnu/libssl.so.3", "/lib/aarch64-linux-gnu/libssl.so.1.1", "/usr/lib/x86_64-linux-gnu/libssl.so.3", "/usr/lib/x86_64-linux-gnu/libssl.so.1.1", "/usr/lib/aarch64-linux-gnu/libssl.so.3", "/usr/lib/aarch64-linux-gnu/libssl.so.1.1", "/usr/lib64/libssl.so.3", "/usr/lib64/libssl.so.1.1", "/usr/lib/libssl.so.3", "/usr/lib/libssl.so.1.1" ].freeze
- LIBC_SEARCH_PATHS =
[ "/lib/x86_64-linux-gnu/libc.so.6", "/lib/aarch64-linux-gnu/libc.so.6", "/usr/lib/x86_64-linux-gnu/libc.so.6", "/usr/lib/aarch64-linux-gnu/libc.so.6", "/lib64/libc.so.6", "/usr/lib64/libc.so.6", "/lib/libc.so.6", ].freeze
- SPAN_ALLOWCLASSES =
[ Socket, BasicSocket, IPSocket, TCPSocket, UDPSocket, UNIXSocket, Signal, Process, Process::UID, Process::GID, Net::HTTP, ]
- SPAN_FILE_METHODS =
File/Dir are deliberately NOT in SPAN_ALLOWCLASSES: tracing every method is far too noisy and read/query methods (exist?, basename, read, stat, …) carry little security signal. Instead only the security-relevant methods below are turned into spans. Detection is done via tp.self (not tp.defined_class) so that e.g. File.open, whose method is owned by IO, is still matched. Kernel LSM events (path_open, file_chmod, file_rename, file_symlink, file_hardlink, file_getdents) already capture the underlying filesystem actions regardless of the Ruby method.
%i[ open new write binwrite delete unlink rename truncate chmod lchmod chown lchown symlink link readlink realpath realdirpath mkfifo mknod utime ].to_set.freeze
- SPAN_DIR_METHODS =
%i[ mkdir rmdir delete unlink chdir chroot glob ].to_set.freeze
- SPAN_ALLOWLIST =
[ "Kernel#system", "Kernel#require", "Kernel#require_relative", "Kernel#load", "Kernel#eval", "Object#instance_eval", "Object#instance_exec", "ENV#[]", "ENV#fetch", "ENV#key?", "ENV#[]=", "ENV#store", "ENV#delete", "ENV#clear", "ENV#replace", ].freeze
- ENV_PAYLOAD_OP_SIZE =
16- ENV_PAYLOAD_KEY_OFFSET =
ENV_PAYLOAD_OP_SIZE- ENV_PAYLOAD_KEY_SIZE =
EVENT_PAYLOAD_SIZE - ENV_PAYLOAD_KEY_OFFSET
- EVENT_SEVERITY_HIGH =
%w[ capable_check bprm_creds setid_change task_kill ptrace_check sb_mount kernel_read_file dlopen ].freeze
- DEFAULT_FILTER =
Default display filter applied by both ‘vivarium load` (CLI) and Vivarium::Box. path_open fires on every file open and is far too noisy to show in full, so it is restricted to opens under /etc and /proc (config/state that matters for security review). render_event_payload renders the path via String#inspect, so the matched target text looks like “/etc/passwd” (leading quote included).
{ include_events: %w[ proc_fork proc_exec span_start span_stop path_open sock_connect dns_req odd_socket ssl_write dlopen mmap_exec task_kill setid_change capable_check bprm_creds ], payload: { "path_open" => %r{\A"?/(?:home|root|etc|proc)(?:/|"|\z)} } }.freeze
- CAPABILITY_NAMES =
{ 0 => "CAP_CHOWN", 1 => "CAP_DAC_OVERRIDE", 2 => "CAP_DAC_READ_SEARCH", 3 => "CAP_FOWNER", 4 => "CAP_FSETID", 5 => "CAP_KILL", 6 => "CAP_SETGID", 7 => "CAP_SETUID", 8 => "CAP_SETPCAP", 9 => "CAP_LINUX_IMMUTABLE", 10 => "CAP_NET_BIND_SERVICE", 12 => "CAP_NET_ADMIN", 13 => "CAP_NET_RAW", 16 => "CAP_SYS_MODULE", 17 => "CAP_SYS_RAWIO", 18 => "CAP_SYS_CHROOT", 19 => "CAP_SYS_PTRACE", 21 => "CAP_SYS_ADMIN", 22 => "CAP_SYS_BOOT", 23 => "CAP_SYS_NICE", 24 => "CAP_SYS_RESOURCE", 25 => "CAP_SYS_TIME", 27 => "CAP_MKNOD", 29 => "CAP_AUDIT_WRITE", 37 => "CAP_AUDIT_READ", 38 => "CAP_PERFMON", 39 => "CAP_BPF", 40 => "CAP_CHECKPOINT_RESTORE" }.freeze
- SETID_FLAG_NAMES =
{ 0x01 => "LSM_SETID_ID", 0x02 => "LSM_SETID_RE", 0x04 => "LSM_SETID_RES", 0x08 => "LSM_SETID_FS" }.freeze
- U64_MASK =
0xFFFFFFFFFFFFFFFF- VERSION =
"0.6.0"
Class Attribute Summary collapse
Class Method Summary collapse
- .build_observe_tracepoint ⇒ Object
- .c_string(bytes) ⇒ Object
- .decode_bad_socket_payload(raw_payload) ⇒ Object
- .decode_bprm_creds_payload(raw_payload) ⇒ Object
- .decode_capable_check_payload(raw_payload) ⇒ Object
- .decode_dns_qname(raw_payload) ⇒ Object
- .decode_env_payload(raw_payload) ⇒ Object
- .decode_file_chmod_payload(raw_payload) ⇒ Object
- .decode_file_getdents_payload(raw_payload) ⇒ Object
- .decode_file_hardlink_payload(raw_payload) ⇒ Object
- .decode_file_rename_payload(raw_payload) ⇒ Object
- .decode_file_symlink_payload(raw_payload) ⇒ Object
- .decode_file_unlink_payload(raw_payload) ⇒ Object
- .decode_kernel_read_file_payload(raw_payload) ⇒ Object
- .decode_odd_socket_payload(raw_payload) ⇒ Object
- .decode_proc_exec_payload(raw_payload) ⇒ Object
- .decode_proc_fork_payload(raw_payload) ⇒ Object
- .decode_ptrace_check_payload(raw_payload) ⇒ Object
- .decode_sb_mount_payload(raw_payload) ⇒ Object
- .decode_setid_change_payload(raw_payload) ⇒ Object
- .decode_sock_connect_payload(raw_payload) ⇒ Object
- .decode_span_payload(raw_payload) ⇒ Object
- .decode_span_raise_payload(raw_payload) ⇒ Object
- .decode_ssl_write_payload(raw_payload) ⇒ Object
- .decode_task_kill_payload(raw_payload) ⇒ Object
- .event_severity(event_name) ⇒ Object
- .gettid ⇒ Object
- .locate_vivarium_usdt_so ⇒ Object
- .mix64(value) ⇒ Object
- .monotonic_ktime_ns ⇒ Object
- .observe(socket_path: self.socket_path, dest: $stdout, filter: nil, save_raw: nil, otel_out: nil, otel_endpoint: nil, &block) ⇒ Object
- .render_event_payload(event) ⇒ Object
- .run_daemon!(argv = ARGV) ⇒ Object
- .scoped_observe(socket_path: self.socket_path, dest:, filter: nil, save_raw: nil, otel_out: nil, otel_endpoint: nil) ⇒ Object
- .socket_const_name(prefix, value) ⇒ Object
- .strip_to_first_null(bytes) ⇒ Object
-
.synth_span_id(trace_hi, trace_lo, tid, start_ktime) ⇒ Object
Deterministic 64-bit span id for a method-call span, derived by folding the trace id, tid, and span-start ktime through splitmix64.
-
.synth_trace_id(seed_hi, seed_lo, tid, start_ktime) ⇒ Object
Deterministic 128-bit trace id (returned as [hi, lo], both non-zero) for a top-level method span in the streaming exporter, where each top span starts its own OTel trace.
- .tail_fit_string(value, max_bytes, marker: "...") ⇒ Object
- .top_observe(socket_path: self.socket_path, dest: $stdout, filter: nil, save_raw: nil, otel_out: nil, otel_endpoint: nil) ⇒ Object
Class Attribute Details
.bpf_pin_dir ⇒ Object
217 218 219 |
# File 'lib/vivarium.rb', line 217 def bpf_pin_dir @bpf_pin_dir || PIN_DIR end |
.socket_path ⇒ Object
221 222 223 |
# File 'lib/vivarium.rb', line 221 def socket_path @socket_path || SOCKET_PATH end |
Class Method Details
.build_observe_tracepoint ⇒ Object
2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 |
# File 'lib/vivarium.rb', line 2357 def self.build_observe_tracepoint allow_classes = SPAN_ALLOWCLASSES allowlist = SPAN_ALLOWLIST TracePoint.new(:call, :c_call, :return, :c_return, :raise) do |tp| if tp.event == :raise # FIXME: handle threaded events in the future next if tp.raised_exception.kind_of?(ThreadError) file_arg = tail_fit_string(tp.path, SPAN_RAISE_FILE_ARG_MAX) Vivarium::Usdt.raise( tp.raised_exception.class.to_s, tp.raised_exception..to_s, file: file_arg, lineno: tp.lineno ) next end signature = if tp.self.equal?(ENV) "ENV##{tp.method_id}" else "#{tp.defined_class}##{tp.method_id}" end recv = tp.self mid = tp.method_id file_dir_name = if (recv.is_a?(Class) ? recv <= File : recv.is_a?(File)) && SPAN_FILE_METHODS.include?(mid) "File" elsif (recv.is_a?(Class) ? recv <= Dir : recv.is_a?(Dir)) && SPAN_DIR_METHODS.include?(mid) "Dir" end is_target = !file_dir_name.nil? || \ allowlist.include?(signature) || \ allow_classes.any? { |klass| tp.defined_class == klass } || \ allow_classes.any? { |klass| tp.defined_class == klass.singleton_class } next unless is_target file_arg = tail_fit_string(tp.path, SPAN_FILE_ARG_MAX) span_class_name = tp.self.equal?(ENV) ? "ENV" : (file_dir_name || tp.defined_class.to_s) case tp.event when :call, :c_call Vivarium::Usdt.start(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno) when :return, :c_return Vivarium::Usdt.stop(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno) end end end |
.c_string(bytes) ⇒ Object
226 227 228 229 230 231 232 |
# File 'lib/vivarium.rb', line 226 def self.c_string(bytes) str = bytes.to_s.b nul = str.index("\x00") return str if nul.nil? str[0, nul] end |
.decode_bad_socket_payload(raw_payload) ⇒ Object
347 348 349 |
# File 'lib/vivarium.rb', line 347 def self.decode_bad_socket_payload(raw_payload) decode_odd_socket_payload(raw_payload) end |
.decode_bprm_creds_payload(raw_payload) ⇒ Object
476 477 478 479 480 481 482 483 |
# File 'lib/vivarium.rb', line 476 def self.decode_bprm_creds_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 2 has_file = bytes.getbyte(0).to_i path = c_string(bytes[1, EVENT_PAYLOAD_SIZE - 1]) "has_file=#{has_file} file=#{path.inspect}" end |
.decode_capable_check_payload(raw_payload) ⇒ Object
466 467 468 469 470 471 472 473 474 |
# File 'lib/vivarium.rb', line 466 def self.decode_capable_check_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 8 cap = bytes[0, 4].unpack1("L<") opts = bytes[4, 4].unpack1("L<") cap_name = CAPABILITY_NAMES.fetch(cap, "UNKNOWN") "cap=#{cap}(#{cap_name}) opts=0x#{opts.to_s(16)}" end |
.decode_dns_qname(raw_payload) ⇒ Object
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
# File 'lib/vivarium.rb', line 278 def self.decode_dns_qname(raw_payload) bytes = raw_payload.to_s.b.bytes labels = [] idx = 0 while idx < bytes.length length = bytes[idx] break if length.nil? || length.zero? break if length > 63 idx += 1 break if (idx + length) > bytes.length label = bytes[idx, length].pack("C*") labels << label idx += length end return "" if labels.empty? labels.join(".") end |
.decode_env_payload(raw_payload) ⇒ Object
522 523 524 525 526 527 528 529 530 531 532 533 534 |
# File 'lib/vivarium.rb', line 522 def self.decode_env_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < ENV_PAYLOAD_OP_SIZE op = c_string(bytes[0, ENV_PAYLOAD_OP_SIZE]) key = c_string(bytes[ENV_PAYLOAD_KEY_OFFSET, ENV_PAYLOAD_KEY_SIZE]) return "" if op.empty? return "op=#{op}" if key.empty? key = key.split("=", 2).first if op == "putenv" "op=#{op} key=#{key.inspect}" end |
.decode_file_chmod_payload(raw_payload) ⇒ Object
381 382 383 384 385 386 387 388 |
# File 'lib/vivarium.rb', line 381 def self.decode_file_chmod_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 2 mode = bytes[0, 2].unpack1("S<") path = c_string(bytes[2, 254]) "mode=#{format('0o%o', mode)} path=#{path.inspect}" end |
.decode_file_getdents_payload(raw_payload) ⇒ Object
390 391 392 393 394 395 396 397 |
# File 'lib/vivarium.rb', line 390 def self.decode_file_getdents_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 8 fd = bytes[0, 4].unpack1("L<") count = bytes[4, 4].unpack1("L<") "fd=#{fd} count=#{count}" end |
.decode_file_hardlink_payload(raw_payload) ⇒ Object
358 359 360 361 362 363 |
# File 'lib/vivarium.rb', line 358 def self.decode_file_hardlink_payload(raw_payload) bytes = raw_payload.to_s.b old_path = c_string(bytes[0, 128]) new_name = c_string(bytes[128, 128]) "old_path=#{old_path.inspect} new_name=#{new_name.inspect}" end |
.decode_file_rename_payload(raw_payload) ⇒ Object
365 366 367 368 369 370 |
# File 'lib/vivarium.rb', line 365 def self.decode_file_rename_payload(raw_payload) bytes = raw_payload.to_s.b old_name = c_string(bytes[0, 128]) new_name = c_string(bytes[128, 128]) "old_name=#{old_name.inspect} new_name=#{new_name.inspect}" end |
.decode_file_symlink_payload(raw_payload) ⇒ Object
351 352 353 354 355 356 |
# File 'lib/vivarium.rb', line 351 def self.decode_file_symlink_payload(raw_payload) bytes = raw_payload.to_s.b target = c_string(bytes[0, 128]) link_name = c_string(bytes[128, 128]) "target=#{target.inspect} link_name=#{link_name.inspect}" end |
.decode_file_unlink_payload(raw_payload) ⇒ Object
372 373 374 375 376 377 378 379 |
# File 'lib/vivarium.rb', line 372 def self.decode_file_unlink_payload(raw_payload) bytes = raw_payload.to_s.b filename = c_string(bytes[0, 128]) parent_dir = c_string(bytes[128, 128]) result = "filename=#{filename.inspect}" result += " parent_dir=#{parent_dir.inspect}" if !parent_dir.empty? result end |
.decode_kernel_read_file_payload(raw_payload) ⇒ Object
431 432 433 434 435 436 437 438 |
# File 'lib/vivarium.rb', line 431 def self.decode_kernel_read_file_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 8 id = bytes[0, 4].unpack1("L<") contents = bytes[4, 4].unpack1("L<") "id=#{id} contents=#{contents}" end |
.decode_odd_socket_payload(raw_payload) ⇒ Object
322 323 324 325 326 327 328 329 330 331 332 333 |
# File 'lib/vivarium.rb', line 322 def self.decode_odd_socket_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 6 family = bytes[0, 2].unpack1("S<") type = bytes[2, 2].unpack1("S<") protocol = bytes[4, 2].unpack1("S<") family_name = socket_const_name("AF_", family) type_name = socket_const_name("SOCK_", type) protocol_name = socket_const_name("IPPROTO_", protocol) "family=#{family}(#{family_name}) type=#{type}(#{type_name}) protocol=#{protocol}(#{protocol_name})" end |
.decode_proc_exec_payload(raw_payload) ⇒ Object
399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/vivarium.rb', line 399 def self.decode_proc_exec_payload(raw_payload) bytes = raw_payload.to_s.b slots = PROC_EXEC_SLOT_COUNT.times.map do |index| offset = index * PROC_EXEC_SLOT_SIZE c_string(bytes[offset, PROC_EXEC_SLOT_SIZE]) end slots.reject!(&:empty?) return "" if slots.empty? filename = slots.shift argv = slots "filename=#{filename.inspect} argv=[#{argv.map(&:inspect).join(', ')}]" end |
.decode_proc_fork_payload(raw_payload) ⇒ Object
485 486 487 488 489 490 491 492 |
# File 'lib/vivarium.rb', line 485 def self.decode_proc_fork_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 8 child_pid = bytes[0, 4].unpack1("L<") child_tid = bytes[4, 4].unpack1("L<") "child_pid=#{child_pid} child_tid=#{child_tid}" end |
.decode_ptrace_check_payload(raw_payload) ⇒ Object
413 414 415 416 417 418 419 |
# File 'lib/vivarium.rb', line 413 def self.decode_ptrace_check_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 4 mode = bytes[0, 4].unpack1("L<") "mode=0x#{mode.to_s(16)}" end |
.decode_sb_mount_payload(raw_payload) ⇒ Object
421 422 423 424 425 426 427 428 429 |
# File 'lib/vivarium.rb', line 421 def self.decode_sb_mount_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 248 flags = bytes[0, 8].unpack1("Q<") dev_name = c_string(bytes[8, 120]) fs_type = c_string(bytes[128, 120]) "flags=0x#{flags.to_s(16)} dev_name=#{dev_name.inspect} fs_type=#{fs_type.inspect}" end |
.decode_setid_change_payload(raw_payload) ⇒ Object
454 455 456 457 458 459 460 461 462 463 464 |
# File 'lib/vivarium.rb', line 454 def self.decode_setid_change_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 4 flags = bytes[0, 4].unpack1("L<") names = SETID_FLAG_NAMES.each_with_object([]) do |(bit, name), acc| acc << name if (flags & bit) != 0 end names << "UNKNOWN" if names.empty? "flags=0x#{flags.to_s(16)} kinds=[#{names.join(', ')}]" end |
.decode_sock_connect_payload(raw_payload) ⇒ Object
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/vivarium.rb', line 301 def self.decode_sock_connect_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 20 family = bytes[0, 2].unpack1("S<") port = bytes[2, 2].unpack1("n") addr = bytes[4, 16] case family when 2 # AF_INET ipv4 = addr[0, 4].bytes.join(".") "#{ipv4}:#{port} (#{socket_const_name("AF_", family)})" when 10 # AF_INET6 words = addr.unpack("n8") ipv6 = words.map { |w| format("%x", w) }.join(":") "[#{ipv6}]:#{port} (#{socket_const_name("AF_", family)})" else "family=#{family}(#{socket_const_name("AF_", family)}) port=#{port}" end end |
.decode_span_payload(raw_payload) ⇒ Object
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 |
# File 'lib/vivarium.rb', line 494 def self.decode_span_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 8 method_id = bytes[0, 8].unpack1("q<") result = format("method_id=0x%016X", method_id & 0xFFFF_FFFF_FFFF_FFFF) if bytes.bytesize >= 24 file_id = bytes[8, 8].unpack1("q<") lineno = bytes[16, 8].unpack1("q<") result += format(" file_id=0x%016X", file_id & 0xFFFF_FFFF_FFFF_FFFF) if file_id != -1 result += " lineno=#{lineno}" if lineno > 0 end result end |
.decode_span_raise_payload(raw_payload) ⇒ Object
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 |
# File 'lib/vivarium.rb', line 536 def self.decode_span_raise_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 8 error_id = bytes[0, 8].unpack1("q<") result = format("error_id=0x%016X", error_id & 0xFFFF_FFFF_FFFF_FFFF) if bytes.bytesize >= 16 = bytes[8, 8].unpack1("q<") result += format(" message_id=0x%016X", & 0xFFFF_FFFF_FFFF_FFFF) end if bytes.bytesize >= 24 file_id = bytes[16, 8].unpack1("q<") result += format(" file_id=0x%016X", file_id & 0xFFFF_FFFF_FFFF_FFFF) if file_id != -1 end if bytes.bytesize >= 32 lineno = bytes[24, 8].unpack1("q<") result += " lineno=#{lineno}" if lineno > 0 end result end |
.decode_ssl_write_payload(raw_payload) ⇒ Object
511 512 513 514 515 516 517 518 519 520 |
# File 'lib/vivarium.rb', line 511 def self.decode_ssl_write_payload(raw_payload) bytes = raw_payload.to_s.b return { data_len: 0, cap_len: 0, data: "".b } if bytes.bytesize < SSL_WRITE_PAYLOAD_DATA_OFFSET data_len = bytes[SSL_WRITE_PAYLOAD_DATA_LEN_OFFSET, 4].unpack1("L<") cap_len = bytes[SSL_WRITE_PAYLOAD_CAP_LEN_OFFSET, 4].unpack1("L<") cap_len = SSL_WRITE_PAYLOAD_DATA_MAX if cap_len > SSL_WRITE_PAYLOAD_DATA_MAX data = bytes[SSL_WRITE_PAYLOAD_DATA_OFFSET, cap_len] || "".b { data_len: data_len, cap_len: cap_len, data: data } end |
.decode_task_kill_payload(raw_payload) ⇒ Object
440 441 442 443 444 445 446 447 448 449 450 451 452 |
# File 'lib/vivarium.rb', line 440 def self.decode_task_kill_payload(raw_payload) bytes = raw_payload.to_s.b return "" if bytes.bytesize < 4 sig = bytes[0, 4].unpack1("l<") signame = begin Signal.signame(sig) rescue ArgumentError nil end signame ? "sig=#{sig} signame=#{signame}" : "sig=#{sig}" end |
.event_severity(event_name) ⇒ Object
244 245 246 |
# File 'lib/vivarium.rb', line 244 def self.event_severity(event_name) EVENT_SEVERITY_HIGH.include?(event_name.to_s) ? "high" : "medium" end |
.gettid ⇒ Object
2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 |
# File 'lib/vivarium.rb', line 2407 def self.gettid @gettid_func ||= begin libc = Fiddle.dlopen("libc.so.6") Fiddle::Function.new(libc["gettid"], [], Fiddle::TYPE_INT) rescue Fiddle::DLError libc = Fiddle.dlopen(nil) Fiddle::Function.new(libc["gettid"], [], Fiddle::TYPE_INT) end @gettid_func.call end |
.locate_vivarium_usdt_so ⇒ Object
2422 2423 2424 2425 2426 2427 2428 2429 |
# File 'lib/vivarium.rb', line 2422 def self.locate_vivarium_usdt_so so = $LOADED_FEATURES.find { |p| p =~ %r{vivarium_usdt/vivarium_usdt\.(so|bundle|dylib)\z} } raise Error, "vivarium_usdt native extension not found in $LOADED_FEATURES" unless so File.realpath(so) rescue LoadError => e raise Error, "failed to load vivarium_usdt: #{e.}" end |
.mix64(value) ⇒ Object
262 263 264 265 266 267 |
# File 'lib/vivarium.rb', line 262 def self.mix64(value) x = (value.to_i + 0x9E3779B97F4A7C15) & U64_MASK x = ((x ^ (x >> 30)) * 0xBF58476D1CE4E5B9) & U64_MASK x = ((x ^ (x >> 27)) * 0x94D049BB133111EB) & U64_MASK (x ^ (x >> 31)) & U64_MASK end |
.monotonic_ktime_ns ⇒ Object
2418 2419 2420 |
# File 'lib/vivarium.rb', line 2418 def self.monotonic_ktime_ns Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) end |
.observe(socket_path: self.socket_path, dest: $stdout, filter: nil, save_raw: nil, otel_out: nil, otel_endpoint: nil, &block) ⇒ Object
2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 |
# File 'lib/vivarium.rb', line 2288 def self.observe(socket_path: self.socket_path, dest: $stdout, filter: nil, save_raw: nil, otel_out: nil, otel_endpoint: nil, &block) if block_given? return scoped_observe(socket_path: socket_path, dest: dest, filter: filter, save_raw: save_raw, otel_out: otel_out, otel_endpoint: otel_endpoint, &block) end top_observe(socket_path: socket_path, dest: dest, filter: filter, save_raw: save_raw, otel_out: otel_out, otel_endpoint: otel_endpoint) end |
.render_event_payload(event) ⇒ Object
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 |
# File 'lib/vivarium.rb', line 561 def self.render_event_payload(event) case event.event_name when "dns_req" decoded = decode_dns_qname(event.payload) decoded.empty? ? event.payload.inspect : decoded when "sock_connect" decoded = decode_sock_connect_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "odd_socket" decoded = decode_odd_socket_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "proc_exec" decoded = decode_proc_exec_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "ptrace_check" decoded = decode_ptrace_check_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "sb_mount" decoded = decode_sb_mount_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "kernel_read_file" decoded = decode_kernel_read_file_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "task_kill" decoded = decode_task_kill_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "setid_change" decoded = decode_setid_change_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "capable_check" decoded = decode_capable_check_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "bprm_creds" decoded = decode_bprm_creds_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "proc_fork" decoded = decode_proc_fork_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "span_start", "span_stop" decoded = decode_span_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "span_raise" decoded = decode_span_raise_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "file_symlink" decoded = decode_file_symlink_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "file_hardlink" decoded = decode_file_hardlink_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "file_rename" decoded = decode_file_rename_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "file_unlink" decoded = decode_file_unlink_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "file_chmod" decoded = decode_file_chmod_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "file_getdents" decoded = decode_file_getdents_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "ssl_write" decoded = decode_ssl_write_payload(event.payload) "data_len=#{decoded[:data_len]} cap_len=#{decoded[:cap_len]}" when "env_caccess" decoded = decode_env_payload(event.payload) decoded.empty? ? event.payload.inspect : decoded when "dlopen", "mmap_exec" strip_to_first_null(event.payload).inspect else strip_to_first_null(event.payload).inspect end end |
.run_daemon!(argv = ARGV) ⇒ Object
2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 |
# File 'lib/vivarium.rb', line 2431 def self.run_daemon!(argv = ARGV) = { pin_dir: bpf_pin_dir, socket_path: socket_path, ssl_trace: true, libssl_path: nil, env_trace: true, dlopen_trace: true, libc_path: nil, usdt_so_paths: [] } OptionParser.new do |opts| opts. = "Usage: vivariumd [--pin-dir PATH] [--socket PATH] [--no-ssl-trace] [--libssl PATH] " \ "[--no-dlopen-trace] [--no-env-trace] [--libc PATH] [--usdt-so PATH ...]" opts.on("--usdt-so PATH", "USDT .so to attach (repeatable; " \ "overrides VIVARIUM_USDT_SO_PATH)") do |v| [:usdt_so_paths] << v end opts.on("--pin-dir PATH", "Pinned map directory") { |v| [:pin_dir] = v } opts.on("--socket PATH", "Unix domain socket path for the HTTP API") { |v| [:socket_path] = v } opts.on("--[no-]ssl-trace", "Attach OpenSSL SSL_write uprobe (default: enabled)") do |v| [:ssl_trace] = v end opts.on("--libssl PATH", "Path to libssl.so to attach SSL_write to") do |v| [:libssl_path] = v end opts.on("--[no-]dlopen-trace", "Attach libc dlopen uprobe (default: enabled)") do |v| [:dlopen_trace] = v end opts.on("--[no-]env-trace", "Attach libc getenv/setenv uprobes (default: enabled)") do |v| [:env_trace] = v end opts.on("--libc PATH", "Path to libc.so for dlopen uprobe") do |v| [:libc_path] = v end end.parse!(argv) Daemon.new( pin_dir: [:pin_dir], socket_path: [:socket_path], ssl_trace: [:ssl_trace], libssl_path: [:libssl_path], dlopen_trace: [:dlopen_trace], env_trace: [:env_trace], libc_path: [:libc_path], usdt_so_paths: [:usdt_so_paths] ).run end |
.scoped_observe(socket_path: self.socket_path, dest:, filter: nil, save_raw: nil, otel_out: nil, otel_endpoint: nil) ⇒ Object
2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 |
# File 'lib/vivarium.rb', line 2328 def self.scoped_observe(socket_path: self.socket_path, dest:, filter: nil, save_raw: nil, otel_out: nil, otel_endpoint: nil) client = DaemonClient.new(socket_path: socket_path) pid = Process.pid main_tid = gettid correlator = Correlator.new( socket_path: socket_path, observer_pid: pid, main_tid: main_tid, filter: filter, dest: dest, save_raw: save_raw, otel_out: otel_out, otel_endpoint: otel_endpoint ) correlator.start client.register(pid) tracer = build_observe_tracepoint tracer.enable yield ensure tracer&.disable client&.unregister(pid) correlator&.stop end |
.socket_const_name(prefix, value) ⇒ Object
335 336 337 338 339 340 341 342 343 344 345 |
# File 'lib/vivarium.rb', line 335 def self.socket_const_name(prefix, value) return "UNKNOWN" unless defined?(Socket) key = Socket.constants.find do |name| name.to_s.start_with?(prefix) && Socket.const_get(name) == value rescue NameError false end key ? key.to_s : "UNKNOWN" end |
.strip_to_first_null(bytes) ⇒ Object
636 637 638 639 640 641 |
# File 'lib/vivarium.rb', line 636 def self.strip_to_first_null(bytes) nul = bytes.index("\x00") return bytes if nul.nil? bytes[0, nul] end |
.synth_span_id(trace_hi, trace_lo, tid, start_ktime) ⇒ Object
Deterministic 64-bit span id for a method-call span, derived by folding the trace id, tid, and span-start ktime through splitmix64. Non-zero, unique within a trace, and stable across re-runs. Shared by the report –dump-otel view and the OTLP exporter so both assign identical method span ids.
254 255 256 257 258 259 260 |
# File 'lib/vivarium.rb', line 254 def self.synth_span_id(trace_hi, trace_lo, tid, start_ktime) seed = mix64(trace_hi) seed = mix64(seed ^ (trace_lo & U64_MASK)) seed = mix64(seed ^ (tid.to_i & U64_MASK)) seed = mix64(seed ^ (start_ktime.to_i & U64_MASK)) seed.zero? ? 1 : seed end |
.synth_trace_id(seed_hi, seed_lo, tid, start_ktime) ⇒ Object
Deterministic 128-bit trace id (returned as [hi, lo], both non-zero) for a top-level method span in the streaming exporter, where each top span starts its own OTel trace. Folds the BPF trace id, tid, and span-start ktime.
272 273 274 275 276 |
# File 'lib/vivarium.rb', line 272 def self.synth_trace_id(seed_hi, seed_lo, tid, start_ktime) hi = synth_span_id(seed_hi, seed_lo, tid, start_ktime) lo = synth_span_id(seed_lo ^ U64_MASK, seed_hi, tid, start_ktime) [hi, lo] end |
.tail_fit_string(value, max_bytes, marker: "...") ⇒ Object
234 235 236 237 238 239 240 241 242 |
# File 'lib/vivarium.rb', line 234 def self.tail_fit_string(value, max_bytes, marker: "...") str = value.to_s.b return str if str.bytesize <= max_bytes return str.byteslice(-max_bytes, max_bytes) || "" if max_bytes <= marker.bytesize tail_size = max_bytes - marker.bytesize tail = str.byteslice(-tail_size, tail_size) || "" "#{marker}#{tail}" end |
.top_observe(socket_path: self.socket_path, dest: $stdout, filter: nil, save_raw: nil, otel_out: nil, otel_endpoint: nil) ⇒ Object
2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 |
# File 'lib/vivarium.rb', line 2299 def self.top_observe(socket_path: self.socket_path, dest: $stdout, filter: nil, save_raw: nil, otel_out: nil, otel_endpoint: nil) client = DaemonClient.new(socket_path: socket_path) pid = Process.pid main_tid = gettid correlator = Correlator.new( socket_path: socket_path, observer_pid: pid, main_tid: main_tid, filter: filter, dest: dest, save_raw: save_raw, otel_out: otel_out, otel_endpoint: otel_endpoint ) correlator.start client.register(pid) tracer = build_observe_tracepoint tracer.enable session = ObservationSession.new( client: client, pid: pid, tracer: tracer, correlator: correlator ) at_exit { session.stop } session end |