Behavioral syscall profiler and seccomp policy generator for Linux process confinement.
The name SYSCAGE is a portmanteau of Syscall and Cage β a cage for system calls. It encapsulates the tool's purpose: confining a process to only the system calls it needs, blocking everything else.
Where LinSpec checks if protections are active and K-Scanner detects violations, SYSCAGE shifts from detection to active restriction. It learns what a process needs at runtime, then builds a seccomp-bpf filter that enforces that behavior.
SYSCAGE is a three-phase tool that observes, generates, and enforces syscall-level policies on Linux processes.
Core Pipeline:
- Learn β
syscage learnβ Running PID or command β.tracefile - Generate β
syscage genβ.tracefile β.syscageprofile - Enforce β
syscage enforce/syscage watchβ.syscageprofile β seccomp-bpf filter viaprctl(2)
- Behavioral syscall profiling β traces a process and records every syscall it makes
- Automatic seccomp generation β converts traced behavior into a ready-to-use seccomp-bpf filter
- Ptrace-based tracing (zero deps) β works on any Linux system without external libraries
- eBPF tracing (optional) β lower overhead when libbpf is available (
--ebpf) - Critical syscall injection β automatically includes essential syscalls (read, write, exit, etc.)
- Multiple enforcement modes β attach to running process (ptrace injection), spawn new process, or watch with violation monitoring
- JSON export β machine-readable profile output for toolchain integration (
--json) - Trace merging β combine multiple trace files before profile generation
- C header export β generate embeddable seccomp filter code (
--header) - Subcommand CLI β follows the
git/dockerpattern (learn,gen,enforce,watch) - Text-based profiles β human-readable, diffable, and version-controllable
- Pure C11 β minimal dependencies (system headers + libc)
ββ SYSCAGE βββββββββββββββββββ
β Kernel Policy Fence β
β Syscall Profiling/Seccomp β
ββββββββββββββββββββββββββββββ
[INF] 14:30:01 Tracing PID 1234 for 30 seconds...
[INF] 14:30:31 Traced 14203 syscalls in 30.0s
[INF] 14:30:31 Trace complete: 14203 syscalls observed, 21 unique.
[INF] 14:30:31 Trace saved to nginx.trace
[INF] 14:30:32 Profile saved to nginx.trace.syscage
Profile Summary
ββββββββββββββββββββββββββββββ
Rules: 37
Observations: 14203
Source PID: 1234
Duration: 30.0s
Default: KILL
NR SYSCALL
βββββββββββββββββ
0 read
1 write
2 open
3 close
4 stat
5 fstat
9 mmap
10 mprotect
11 munmap
12 brk
... and 27 more
[INF] 14:30:33 Spawned PID 1298 under seccomp profile.
[INF] 14:30:33 Watching PID 1298 under seccomp profile.
SYSCAGE reads syscall data from the kernel through two backends:
- Ptrace (default): Uses
PTRACE_SYSCALLto intercept every syscall entry, readsorig_raxfromstruct user_regs_struct, and records the syscall number. - eBPF (optional): Uses
raw_tracepoint/sys_enterfor lower-overhead syscall capture.
The pipeline flow:
PID or command
β
βΌ
ββββββββββββ ββββββββββββββ ββββββββββββββββ
β TRACER βββ>β PROFILER βββ>β ENFORCER β
β ptrace/ β β generates β β applies via β
β eBPF β β .syscage β β prctl + β
β β β profile β β seccomp(2) β
ββββββββββββ ββββββββββββββ ββββββββββββββββ
β β β
βΌ βΌ βΌ
.trace .syscage process with
(raw data) (text rules) seccomp filter
- All syscalls observed above the frequency threshold are added to the allowlist
- Critical syscalls (read, write, exit, getpid, etc.) are automatically injected
- The profile is sorted by syscall number for readability
- The default action for unknown syscalls is KILL
The enforcer builds a seccomp-bpf filter (the same BPF used by tcpdump) and applies it via:
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) // prevent filter bypass
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) // apply filter
git clone https://github.com/jeffersoncesarantunes/SYSCAGE.git
cd SYSCAGE && make
sudo ./bin/syscage learn -d 5 -o ls.trace -- /bin/ls -la /tmp
sudo ./bin/syscage gen ls.trace
sudo ./bin/syscage watch -p ls.trace.syscage -- /bin/ls /tmpThree commands and you've confined ls to only the syscalls it used during profiling. If it runs the same as without SYSCAGE, the profile is complete.
make # Compile
make clean && make # Clean rebuild from scratchTrace every syscall a process makes. The recommended way (works on any Linux) is to launch a command under trace. You can also attach to a running process if your kernel allows it.
# Recommended: launch and trace a command (works everywhere)
sudo ./bin/syscage learn -d 5 -o ls.trace -- /bin/ls -la /tmp
# Optional: trace an already running process (by PID or name)
sudo ./bin/syscage learn -d 15 -o nginx.trace 1234
sudo ./bin/syscage learn -d 15 -o nginx.trace $(pidof nginx)What each part means:
-d 5β trace for 5 seconds-o ls.traceβ save results to filels.trace-- /bin/ls -la /tmpβ the--separates SYSCAGE options from the command to launch1234β PID of a running process to trace$(pidof nginx)β resolves "nginx" to its PID automaticallyNote: Attaching to a running process requires
ptrace_scope = 0. Most Linux distros (including Arch) default to1, which only allows tracing child processes. This is why attaching by PID may fail with "Operation not permitted" β launch the command under trace instead, which always works.
Convert the raw trace into a human-readable policy file:
sudo ./bin/syscage gen ls.traceThis creates ls.trace.syscage β a text file listing every allowed syscall.
You can also generate a C header for embedding into other programs:
sudo ./bin/syscage gen --header ls.traceApply the policy to a process. Use enforce to spawn a command silently, or watch to see its output and exit status:
# watch shows the command's output and waits for it to finish (recommended)
sudo ./bin/syscage watch -p ls.trace.syscage -- /bin/ls /tmp
# enforce spawns and returns immediately (for scripts/production)
sudo ./bin/syscage enforce -p ls.trace.syscage -e -- /bin/ls /tmpWhat each part means:
-p ls.trace.syscageβ which profile to apply-eβ spawn a new process (instead of attaching to a running one)-- /bin/ls /tmpβ command to confine under the profilewatchβ same as enforce, but shows output and logs if a blocked syscall is attempted
SYSCAGE is designed for safe profiling and enforcement:
- Read-only during learning: The ptracer never modifies the target's memory or execution
- No kernel modification: All profiling uses standard ptrace/seccomp APIs
- Self-contained profiles: Profiles are plain text β no binary state
- Graceful fallback: If ptrace fails, no changes are made to the system
- Seccomp is process-scoped: A restricted process cannot affect other processes
- NO_NEW_PRIVS enforced: Prevents filter bypass via
setuidbinaries
βββ bin/
β βββ syscage
βββ build/
βββ docs/
β βββ ARCHITECTURE.md
β βββ BPF_REFERENCE.md
β βββ OPERATION_MODEL.md
β βββ THREAT_MODEL.md
βββ examples/
β βββ profiles/
βββ include/
β βββ enforcer.h
β βββ profiler.h
β βββ syscage.h
β βββ tracer.h
βββ src/
β βββ common.c
β βββ enforcer.c
β βββ main.c
β βββ profiler.c
β βββ tracer.c
βββ tests/
β βββ test_profiler.c
βββ .clang-format
βββ .gitignore
βββ LICENSE
βββ Makefile
βββ README.md
- Language: C (C11)
- Kernel Interface: ptrace(2), prctl(2), seccomp(2)
- Optional Backend: eBPF (
raw_tracepoint/sys_enter) - Filter Format: seccomp-bpf (
struct sock_fprog/struct sock_filter) - Build Tool: GNU Make
- Target Platforms: Linux Kernel 5.x, 6.x (x86_64)
- Ptrace-based syscall tracer (no deps)
- Trace file I/O (save/load .trace)
- Profile generation from trace data
- Text profile format (.syscage)
- C header export for embedding
- Seccomp filter building
- Process spawn under filter (
enforce -e) - Watch mode with violation monitoring
- Profile merging (combine multiple traces)
- JSON profile export
- Running process attach (ptrace seccomp injection)
- Systemd integration (generator mode)
- Container-aware profiling (Docker/k8s)
- eBPF backend (libbpf)
- Remote profiling over SSH