Vivarium
RubyGems: https://rubygems.org/gems/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 hook on
file_open - BPF LSM hooks on
inode_symlink,inode_link,inode_rename,path_chmod - BPF tracepoint on
sys_enter_getdents64 - BPF tracepoint on
sys_enter_execve(captures executable path and first few argv entries asproc_exec) - 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) - Shared pinned maps on bpffs
config_root_targets(root PID -> 0/1)config_spawned_targets(spawned TID -> 0/1)event_invoked(array length 1024 withevent_trecords)event_write_pos(cursor for appending intoevent_invoked)
- Ruby API
Vivarium.observe do ... end- Registers current PID to
config_root_targets - eBPF tracks spawned descendants into
config_spawned_targetsviasched_process_fork - On each
:return/:c_return, drainsevent_invoked - Prints stack trace + events
- Clears event slots and cursor
- Unregisters PID on block exit
- Registers current PID to
event_t currently:
struct event_t {
u64 ktime_ns;
u32 pid;
char event_name[16];
char payload[256];
};
Requirements
- Linux kernel/environment supporting BPF LSM
libbccinstalledbpftoolinstalled (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 install
Usage
1) Start daemon (root):
sudo bundle exec vivariumd
2) Observe in Ruby process:
require "vivarium"
Vivarium.observe do
File.read("/etc/passwd")
end
3) Network monitoring demo client:
bundle exec ruby examples/network_client_demo.rb
This demo intentionally triggers sock_connect, dns_req, and odd_socket events.
4) File operation demo client (only touches /tmp):
bundle exec ruby examples/file_operation_demo.rb
This demo intentionally triggers path_open, file_symlink, file_hardlink, file_rename, file_chmod, and file_getdents events under /tmp.
5) Execve demo client:
bundle exec ruby examples/execve_demo.rb
This demo intentionally triggers proc_exec with several argument patterns using direct execve-style process launches.
6) Signal demo client:
bundle exec ruby examples/signal_kill_demo.rb
This demo forks a child process and sends TERM with Process.kill, which is useful for triggering task_kill.
7) Privilege-related event demo client:
bundle exec ruby examples/privilege_event_demo.rb
This 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.
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.stop
By default, Vivarium excludes its own internal frames from stack output. Set VIVARIUM_FILTER_INTERNAL_FRAMES=0 to disable this filter.
You can override pin directory via VIVARIUM_BPF_PIN_DIR on both sides:
VIVARIUM_BPF_PIN_DIR=/sys/fs/bpf/vivarium bundle exec vivariumd
Use 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 test
Daemon entrypoint:
bundle exec vivariumd --pin-dir /sys/fs/bpf/vivarium
Notes
- Thread/Ractor-awareness is not yet implemented.
event_invokeduses fixed 1024 slots and wraps around when full.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.- 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. - In
humanformat output,highseverity events are rendered in red. capable_checkis intentionally filtered to high-risk capabilities to reduce noise from extremely frequentcapablehook calls.- Current output format is textual and intended for iteration.
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. vivariumdalso printsbpf_trace_printklines (vivarium: pid=... path=...) to its own logs.
Contributing
Issues and pull requests are welcome.