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
-
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, 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.
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.stopBy 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 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.
-
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.
