seccomp-notify

Pure Ruby bindings for Linux seccomp user notifications

Gem Version Downloads Ruby Version License

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

  1. The parent creates a Unix socket pair and forks the target.
  2. The target installs a cBPF filter and sends its listener descriptor to the parent with SCM_RIGHTS.
  3. The parent receives syscall notifications and dispatches them to registered handlers.
  4. 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_uring does not pass through the filter; io_uring_setup is denied by default.
  • If the supervisor dies, blocked notification syscalls return ENOSYS.
  • fcntl must remain allowed while Ruby wraps the listener descriptor, so policies that notify or deny it are rejected. sendmsg may be filtered; only listener transfer on its reserved descriptor is allowed, and that descriptor remains open across exec to prevent fd-number reuse.

Examples

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.