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.
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. fcntlandsendmsgmust remain allowed because Ruby uses them to wrap and transfer the listener descriptor. Policies that notify or deny them are rejected.
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.