Vivarium
Vivarium is an observation and sandbox helper for Ruby.
It combines:
- eBPF LSM monitoring via RbBCC (
vivariumd) - Ruby-side method boundary observation via
TracePoint(Vivarium.observe)
The goal is to visualize which Ruby method context triggered low-level events.
Current Scope
Implemented in this repository:
BPF LSM hooks on inode_symlink, inode_link, inode_rename, inode_unlink (filename and parent directory name are captured as reference information only), path_chmod
- BPF LSM hooks for suspicious behavior checks:
-
ptrace_access_check(emitsptrace_check) -
sb_mount(emitssb_mount) -
kernel_read_file(emitskernel_read_file) -
task_kill(emitstask_kill) -
task_fix_setuid(emitssetid_change) -
capablefor high-risk capabilities only (emitscapable_check) -
bprm_creds_from_file(emitsbprm_creds)
-
- BPF LSM hook on
socket_create(flags unusual socket creation asodd_socket) - BPF LSM hook on
socket_connect(captures destination family/address/port assock_connect) - BPF tracepoints on
sys_enter_sendmsg,sys_enter_sendto,sys_enter_sendmmsg(capture UDP/53 DNS QNAME raw bytes asdns_req) - USDT probes for method span boundaries and exceptions (
span_start,span_stop,span_raise) - OpenSSL
SSL_writeuprobe event (ssl_write) - Shared pinned maps on bpffs
-
config_root_targets(root PID -> 0/1) -
config_spawned_targets(spawned TID -> 0/1) -
events(BPF_RINGBUF_OUTPUT, shared by system events and span events)
-
- Ruby API
Vivarium.observe do ... end- Registers current PID to
config_root_targets - eBPF tracks spawned descendants into
config_spawned_targetsviasched_process_fork -
TracePointemits span probes on allowlisted call/return and emitsspan_raiseon Ruby:raise - Correlator thread consumes ringbuf events and joins them to spans by
tid/time window - Renders a process tree once at session end
- Unregisters PID on block exit
- Registers current PID to
event_t currently:
struct event_t {
u64 ktime_ns;
u32 pid;
u32 tid;
char event_name[16];
char payload[256];
};Requirements
- Linux kernel/environment supporting BPF LSM
-
libbccinstalled -
bpftoolinstalled (used to resolvestruct file::f_pathandstruct dentry::d_nameoffsets from BTF) - root privileges for
vivariumd - bpffs mounted (typically
/sys/fs/bpf)
Installation
Add to Gemfile:
gem "vivarium"Then:
bundle installUsage
- Start daemon (root):
sudo bundle exec vivariumd- Observe in Ruby process:
require "vivarium"
Vivarium.observe do
File.read("/etc/passwd")
end- Network monitoring demo client:
bundle exec ruby examples/network_client_demo.rbThis demo intentionally triggers sock_connect, dns_req, and odd_socket events.
- File operation demo client (only touches
/tmp):
bundle exec ruby examples/file_operation_demo.rbThis demo intentionally triggers path_open, file_symlink, file_hardlink, file_rename, file_chmod, file_unlink, and file_getdents events under /tmp.
- Execve demo client:
bundle exec ruby examples/execve_demo.rbThis demo intentionally triggers proc_exec with several argument patterns using direct execve-style process launches.
- Signal demo client:
bundle exec ruby examples/signal_kill_demo.rbThis demo forks a child process and sends TERM with Process.kill, which is useful for triggering task_kill.
- Privilege-related event demo client:
bundle exec ruby examples/privilege_event_demo.rbThis demo attempts setuid/setgid changes, sensitive file access, and sudo exec to trigger privilege-related events such as setid_change, capable_check, and bprm_creds.
- Ruby internal ENV access demo client:
bundle exec ruby examples/env_access_ruby_demo.rbThis demo triggers Ruby-side ENV methods ([], fetch, key?, []=, store, delete, clear, replace) and is intended to produce SPAN events.
- External command ENV libc access demo client:
bundle exec ruby examples/env_access_external_demo.rbThis demo spawns an external process that directly calls libc getenv, setenv, unsetenv, putenv, and clearenv, intended to trigger env_caccess eBPF events.
- Box context demo:
bundle exec ruby examples/box_demo.rbThis demo shows how to use Vivarium::Box to isolate Ruby code evaluation and trace method calls within that isolated context.
You can also start top-level observation without a block (it keeps observing until process exit):
require "vivarium"
observer = Vivarium.top_observe
# or: Vivarium.observe
# do anything ...
observer.stopVivarium.observe / Vivarium.top_observe produce one process-tree report at session end (block exit, observer.stop, or process exit).
You can override pin directory via VIVARIUM_BPF_PIN_DIR on both sides:
VIVARIUM_BPF_PIN_DIR=/sys/fs/bpf/vivarium bundle exec vivariumdUse Vivarium.bpf_pin_dir = "/sys/fs/bpf/..." in Ruby code to set it programmatically.
require "vivarium"
Vivarium.bpf_pin_dir = "/sys/fs/bpf/vivarium"Development
Run tests:
bundle exec rake testDaemon entrypoint:
bundle exec vivariumd --pin-dir /sys/fs/bpf/vivariumNotes
- Thread/Ractor-awareness is not yet implemented.
- Current transport is ring buffer (
events) pinned under bpffs. - Ring buffer is single-consumer by nature; v1 supports a single observer per host.
-
payloadis 256 bytes inevent_t; some event types intentionally use smaller structured slices inside that buffer. -
proc_execcurrently stores the executable path plus up to 3 argv entries in 4 fixed 64-byte slots to keep the BPF verifier happy. -
span_raiseis emitted on Ruby:raiseand rendered asEXCPlines within the enclosing span. - Events that do not match any real span are grouped into synthetic
<no-span>spans. - Each event is tagged with severity metadata:
highforsetid_change,capable_check,bprm_creds,task_kill,ptrace_check,sb_mount, andkernel_read_file; others aremediumby default. -
capable_checkis intentionally filtered to high-risk capabilities to reduce noise from extremely frequentcapablehook calls. - Output format is textual process tree with a session header and per-span relative timing.
-
vivariumdresolvesstruct file::f_pathoffset from/sys/kernel/btf/vmlinuxat startup. -
vivariumdalso resolvesstruct dentry::d_nameoffset from/sys/kernel/btf/vmlinuxat startup. - You can override offsets manually with
VIVARIUM_FILE_F_PATH_OFFSETandVIVARIUM_DENTRY_D_NAME_OFFSETif auto-detection fails.
Contributing
Issues and pull requests are welcome.
