Straycall
Report and block unintended syscalls in Ruby builds and tests
Website · Features · Installation · Quick Start · Configuration · Integrations · How It Works · Limitations
Straycall runs an unfiltered supervisor over Linux seccomp user notifications to report and block unintended network, filesystem, and process access. Each report includes the responsible Ruby backtrace when available.
[!WARNING] Straycall detects accidents; it is not a security boundary for hostile code. Pointer-based policy checks, symbolic links, existing file descriptors, and kernel interfaces outside seccomp's reach leave intentional escape routes.
On unsupported platforms, non-MRI Ruby, or with STRAYCALL=0, Straycall is a no-op and the wrapped command runs normally.
Features
- Network policies for connections, Unix sockets, binds, and listeners
- Filesystem policies for writes and sensitive reads
- Process policies for allowed and denied executables
- Ruby backtraces with native-library fallback
fail,warn,record, and interactivepromptmodes- CLI, YAML, and Ruby configuration
- RSpec and Minitest integrations
- Console and versioned JSON reports
- Automatic no-op behavior outside supported Linux environments
Installation
Add Straycall to your Gemfile:
gem "straycall"
Then install:
bundle install
Requirements
- Linux x86_64 or aarch64
- Linux 5.5+ with seccomp user notifications and
CONTINUE - MRI Ruby 3.1+
seccomp-notify0.2.x
Container runtimes may block seccomp(2) in their own profile. Use an isolated container with --security-opt seccomp=unconfined when needed.
Quick Start
The CLI keeps the supervisor outside the filtered command, so it does not interfere with the command's Process.wait calls.
Run a test suite with a YAML policy:
straycall --config .straycall.yml -- bundle exec rspec
Write a JSON report:
straycall --report-path tmp/straycall.json -- ruby script.rb
Record observed access as a reviewable policy:
straycall --record -- bundle exec rspec
Prompt while installing dependencies:
straycall --preset=bundle-install -- bundle install
Violation Modes
| Mode | Behavior |
|---|---|
fail |
Deny the syscall with EPERM and exit nonzero |
warn |
Allow the syscall and report it |
record |
Allow the syscall and write a reviewable policy |
prompt |
Ask through /dev/tty; deny when no TTY is available |
The default mode is fail.
Record mode writes .straycall.recorded.yml and refuses to replace an existing file. Review it before moving it to .straycall.yml; pass --force only when replacing a previous recording intentionally.
Configuration
Create .straycall.yml in your project root:
network:
default: deny
allow:
- host: 127.0.0.1
ports: [5432, 6379]
- unix: /var/run/postgresql/.s.PGSQL.5432
- domain: 17 # platform socket-family number
allow_unbound_listen: false
write:
default: deny
allow: [tmp, log, coverage, /tmp]
deny_read: [/etc/shadow, ~/.ssh, ~/.aws, ~/.gem/credentials]
exec:
default: deny
allow_under: [/usr/bin]
on_violation: fail
report_backtrace: true
report_path: tmp/straycall.json
| Option | Default | Description |
|---|---|---|
network.default |
deny |
Allow or deny network endpoints not listed in network.allow |
network.allow_unbound_listen |
false |
Permit listeners whose bound endpoint cannot be recovered |
write.default |
allow |
Allow or deny writes outside write.allow |
exec.default |
allow |
Allow or deny executables outside exec.allow |
on_violation |
fail |
Select fail, warn, record, or prompt |
report_backtrace |
true |
Include Ruby backtraces in reports |
report_path |
unset | Write a versioned JSON report to this path |
Hosts are resolved once while loading the configuration. Filesystem paths are expanded; executable paths are resolved through symbolic links when possible.
Ruby Configuration
require "straycall"
Straycall.configure do |config|
config.deny_network!
config.allow_loopback ports: [5432, 6379]
config.allow_host "registry.internal.example", ports: [443]
config.allow_unix "/var/run/postgresql/.s.PGSQL.5432"
config.allow_socket_domain Socket::AF_PACKET
config.allow_unbound_listen!
config.allow_write_under "tmp", "log", "coverage", Dir.tmpdir
config.deny_write_elsewhere!
config.deny_read "/etc/shadow", "~/.ssh"
config.allow_exec "/usr/bin/git", "/usr/bin/make"
config.allow_exec_under "/opt/project/bin"
config.deny_exec_elsewhere!
# config.allow_io_uring! # disables complete syscall coverage
config.report_path = "tmp/straycall.json"
end
Integrations
RSpec and Minitest
Load the integration at the top of the test helper:
# RSpec
require "straycall/rspec"
# Minitest
require "straycall/minitest"
In-process mode starts a clean detached supervisor with Process.spawn, associates violations with the current test, and checks the supervisor health pipe. When it runs below the straycall CLI, it reuses the CLI supervisor instead of installing a second seccomp listener. Prefer the CLI if tests use broad Process.wait calls or fork heavily; filters are inherited and cannot be removed.
Reports
Console reports include the syscall, target, current test, Ruby backtrace, native-library fallback, and an allow-list suggestion. Set report_path to write a versioned JSON document:
{
"version": 1,
"violations": [
{
"syscall": "connect",
"tid": 123,
"target": {
"type": "inet",
"host": "192.0.2.1",
"port": 443
},
"action": "denied"
}
]
}
How It Works
- Straycall starts an unfiltered supervisor before the target command.
- The target installs a seccomp filter for the configured syscall classes.
- Linux pauses matching syscalls and sends user notifications to the supervisor.
- The supervisor reads the target, evaluates the policy, and allows or denies the call.
- Straycall joins violations with the current test and Ruby backtrace, then writes the configured reports.
Known Limitations
- Straycall is not a security boundary. Allowing a pointer-based syscall with
continue!is subject to TOCTOU. - vDSO calls such as normal clock reads cannot be intercepted.
- File descriptors opened before filter installation are outside the policy.
io_uring_setupis denied so io_uring cannot bypass monitored syscalls.allow_io_uring!opts out, but network and filesystem coverage is then incomplete.- Ruby backtraces require MRI 3.1+; native calls fall back to
/proc/<tid>/maps. - Filesystem checks use the unresolved path string. A symlink can point outside an allowed root.
- If the supervisor dies, the kernel returns
ENOSYS; integrations detect the health-pipe closure as soon as they regain control. - A connected
sendto,sendmsg, orsendmmsgwithout an explicit destination relies on the earlier monitoredconnect; Straycall cannot recover a peer address from the target file descriptor. - Listener tracking observes an allowed
bindbefore the kernel returns. It prevents accidental auto-bind listeners, but it is not a hostile-code boundary. - In-process integrations only filter the thread that installs seccomp. Load them before starting application threads; Straycall warns when threads already exist.
ptraceis denied inside supervised processes, so attach profilers and debuggers from outside the target.
Development
bundle install
bundle exec rake
gem build straycall.gemspec
Real seccomp integration examples skip automatically outside supported Linux environments.
On Linux, ruby benchmark/require_overhead.rb checks that read-only openat overhead stays below 10% across 100,000 generated require files. Set STRAYCALL_BENCHMARK_FILES to scale the workload.
Contributing
Bug reports and pull requests are welcome at https://github.com/ydah/straycall.
License
Released under the MIT License.