brittle
Deterministic syscall fault injection for Ruby programs using Linux seccomp user notifications.
Features · Installation · Quick Start · Usage · How It Works · Development
Brittle makes real syscalls fail at deterministic points so Ruby error paths can be exercised
without replacing application code with mocks. It is built on
seccomp-notify.
Warning
Brittle deliberately breaks its target process. Use it only in isolated development and test environments, never in production.
Features
- Inject an errno or synthetic return value into selected syscalls
- Arm and disarm around only the operation under test
- Select exact occurrences, every nth call, or a seeded probability
- Scope calls by file descriptor path, opened path, or destination port
- Sweep occurrence ranges to find failure boundaries
- Record output, artifacts, fd counts, and verdicts in reproducible JSON journals
Requirements
- Linux 5.5 or newer
- x86_64 or aarch64
- Ruby 3.2 or newer
- A container or host that permits seccomp user notifications
Linux 5.0 can return synthetic errno values, but Brittle also needs continue! to pass unmatched
syscalls through, which requires Linux 5.5.
Installation
Add Brittle to your Gemfile:
gem "brittle"Then install it with Bundler:
bundle installOr install it directly:
gem install brittleQuick Start
Clone the repository and build the isolated Linux environment:
git clone https://github.com/ydah/brittle.git
cd brittle
docker build -f Dockerfile.dev -t brittle-dev .
bin/dev bundle exec exe/brittle doctorInject ENOSPC into the first armed write from the included file harness:
bin/dev bundle exec exe/brittle run \
harness/file_write.rb --inject write --errno ENOSPC --at 1The target reports Errno::ENOSPC, Brittle classifies it as expected, and a JSON journal is
written under results/.
Writing a Harness
Harnesses call Brittle.arm! immediately before the operation under test and always call
Brittle.disarm! afterward. Startup, requires, and result output therefore pass through without
fault injection.
require "brittle/marker"
result = nil
Brittle.arm!
begin
File.write(ENV.fetch("BRITTLE_SANDBOX") + "/out.txt", "hello")
result = "OK:out.txt:5"
rescue => error
result = "ERR:#{error.class}:#{error.message}"
ensure
Brittle.disarm!
end
puts resultResult lines use OK: or ERR:. A harness with a target-specific oracle may instead emit
EXPECTED:, CORRUPT:, or SWALLOWED:.
Usage
Run one deterministic injection:
brittle run harness/file_write.rb --inject write --errno ENOSPC --at 1Use sweep to test every matching occurrence in a range:
brittle sweep harness/logger.rb \
--syscall write --errno ENOSPC --fd-path /brittle- --at 1..20Selectors and scopes can be combined as needed:
# Multiple exact occurrences
brittle run harness/file_write.rb --inject write --errno EDQUOT --at 3,7,11
# Every fifth matching call
brittle run harness/file_write.rb --inject write --errno ENOSPC --every 5
# Seeded probability
brittle run harness/file_write.rb \
--inject write --errno ENOSPC --probability 0.1 --seed 42
# Path and destination scopes
brittle run harness/file_write.rb --inject openat --errno EMFILE --path /brittle-
brittle run harness/net_http.rb --inject connect --errno ECONNREFUSED --port 8080
# Synthetic successful return without executing the syscall
brittle run harness/syswrite_loop.rb --inject write --return-value 10 --at 1Journals
Every run writes a JSON journal under results/. Journals include the scenario, environment,
injection events, stdout/stderr, artifact sizes and SHA-256 hashes, fd counts, and verdict. They can
be summarized or converted back into a command:
brittle report results/
brittle reproduce results/file_write-write-ENOSPC-1-....jsonVerdicts are expected, crash, swallowed, corrupt, leak, or hang.
How It Works
- Brittle starts the harness as a filtered child process and keeps the supervisor unfiltered.
-
Brittle.arm!andBrittle.disarm!issue marker calls that delimit the operation under test. - The supervisor applies scope and occurrence rules only while the harness is armed.
- A matching call receives the configured errno or return value; every other call continues.
- Brittle records the injection and resulting process, descriptor, and artifact state in a journal.
Scenario DSL
The same matching rules are available to Ruby callers:
scenario = Brittle.scenario do
inject :write, errno: Errno::ENOSPC, at: 3, when_fd_path: /brittle-/
inject :connect, errno: Errno::ECONNREFUSED, port: 8080, every: 1
endReality checks
Fault injection identifies candidates; it does not prove that a result is an upstream bug. Re-run
every finding under the real failure condition before reporting it. The checks under verify/
demonstrate this workflow.
-
allow!(n)reports success without performing the syscall. Forwrite, the reported prefix is not written, so this is not a faithful partial-write emulator. Such journals sayrealistic: false. - An injected
EINTRhas no accompanying signal. Brittle marks that combination as unrealistic. - Pointer-based path and port scopes are for test targeting, not security decisions; target memory may change before a continued syscall executes.
See NOTES.md for measurements and the first real-condition finding.
Further Reading
Development
bundle install
bundle exec rake
# Linux integration tests
docker build -f Dockerfile.dev -t brittle-dev .
bin/dev bundle exec rake
# Short catalog check; omit AT_RANGE for the full 1..20 sweep
AT_RANGE=1..2 bin/dev script/sweep_catalogContributing
Bug reports and pull requests are welcome at https://github.com/ydah/brittle.
License
Brittle is available under the MIT License.