seccomp-notify
Pure Ruby bindings for Linux seccomp user notifications
Features · Installation · Quick Start · API Docs · Responses · Security · Development
seccomp-notify provides the plumbing for Linux
SECCOMP_RET_USER_NOTIF.
It builds cBPF filters, transfers listener file descriptors, supervises syscall notifications,
reads target memory, and injects file descriptors without a native extension.
[!WARNING] This gem is a mechanism, not a security policy or a complete sandbox. Never inspect a pointer argument and then use
continue!as a security decision: another target thread can change that memory between inspection and syscall execution (TOCTOU). Reject or emulate the syscall instead.
Features
- Pure Ruby cBPF filter generation and seccomp installation
- Parent supervisor with a filtered child through
Seccomp::Notify.spawn - Existing-process supervision through
Seccomp::Notify.supervise_self - Syscall handlers with errno, emulation, continuation, and process termination responses
- Target memory helpers for strings, bytes, and socket addresses
- File descriptor injection with atomic and fallback ADDFD paths
- Runtime detection for optional kernel features
- Handler timeouts, concurrent workers, and supervisor health checks
- Linux x86_64 and aarch64 support
Installation
Add the gem to your Gemfile:
gem "seccomp-notify"
Then install:
bundle install
Requirements
- Linux on x86_64 or aarch64
- Ruby 3.1 or newer
- Linux 5.0 or newer; ADDFD requires Linux 5.9 or newer
CONFIG_SECCOMP_FILTER=y
Container runtimes may block seccomp(2) with their own profile. For Docker development, use an
isolated test container with seccomp disabled:
docker run --rm --security-opt seccomp=unconfined -v "$PWD:/app" -w /app ruby:3.4 bundle exec rake
Quick Start
Keep the unfiltered supervisor in the parent and install the filter in a child:
require "seccomp/notify"
policy = Seccomp::Notify::Policy.new do
notify :connect, :sendto
end
supervisor = Seccomp::Notify.spawn(policy) do
exec("curl", "https://example.com")
end
supervisor.on(:connect) { |request| request.error!(Errno::ENETUNREACH) }
supervisor.on(:sendto) { |request| request.error!(Errno::ENETUNREACH) }
status = supervisor.run
request.pid is an alias for request.tid: the kernel reports the thread ID that issued the
syscall, not the process ID.
Supervise the current process
supervise_self(policy, supervisor: :fork) accepts a block that configures the child supervisor.
The safer :spawn form starts a clean Ruby VM and supports the default pass-through handler only.
Both forms detach the supervisor child, so broad Process.wait calls in the target can still
observe related lifecycle effects. The default pass-through handler requires continue! and Linux
5.5 or newer; handler-based supervision works on Linux 5.0 or newer.
Responses
| Method | Behavior | Requirement |
|---|---|---|
allow!(value = 0) |
Emulate a successful return value without running the syscall | Linux 5.0+ |
error!(Errno::EPERM) |
Return an errno | Linux 5.0+ |
continue! |
Run the original syscall | Linux 5.5+ |
add_fd!(io) |
Inject a file descriptor and return its number | Linux 5.9+ |
kill! |
Terminate the issuing process | — |
Use Seccomp::Notify.features to inspect runtime support. Set
SECCOMP_NOTIFY_DISABLE_FEATURES=addfd,continue to force feature fallbacks in tests.
Targets started by this gem can call Seccomp::Notify.supervisor_alive? to poll the health pipe.
If the target calls exec, the pipe descriptor is inherited and published as
SECCOMP_NOTIFY_HEALTH_FD.
How It Works
- The parent creates a Unix socket pair and forks the target.
- The target installs a cBPF filter and sends its listener descriptor to the parent with
SCM_RIGHTS. - The parent receives syscall notifications and dispatches them to registered handlers.
- Each handler returns an errno, emulated value, continuation, or injected descriptor.
Security
- Pointer data may change after inspection. Never use it to justify
continue!. - vDSO calls, existing file descriptors, and indirect shared-memory effects are outside seccomp's control.
- I/O submitted through an existing
io_uringdoes not pass through the filter;io_uring_setupis denied by default. - If the supervisor dies, blocked notification syscalls return
ENOSYS. fcntlmust remain allowed while Ruby wraps the listener descriptor, so policies that notify or deny it are rejected.sendmsgmay be filtered; only listener transfer on its reserved descriptor is allowed, and that descriptor remains open acrossexecto prevent fd-number reuse.
Examples
examples/deny_network.rbrejects outbound network syscalls.examples/emulate_open.rbreplaces/dev/urandomwith an injected file descriptor.examples/strace_lite.rblogs selected syscall notifications.
Development
bundle install
bundle exec rake
Linux runs include real seccomp integration tests; other systems run portable filter and layout
tests only. spike/notify_min.c is the reference C round trip, and
tools/gen_syscall_table.rb regenerates architecture tables from Linux kernel syscall tables.
Contributing
Bug reports and pull requests are welcome at https://github.com/ydah/seccomp-notify.
Releases before 1.0 may change the API.
License
Released under the MIT License.