cloudflare/sandbox puts a seccomp filter on any Linux binary with two environment variables
Simple Linux seccomp rules without writing any code
At a glance
- What is it?
- sandbox is a small C library and utility that enables seccomp system call filtering for arbitrary programs, including proprietary ones, by setting SECCOMP_SYSCALL_ALLOW or SECCOMP_SYSCALL_DENY and loading libsandbox.so through LD_PRELOAD. It suits operators who want to confine a third party binary without touching its source, and it needs libseccomp built from source.
- Who is it for?
- Adopt sandbox if you need to confine a binary you did not write, including a proprietary one, and can accept building libseccomp from upstream source and deploying a preloaded shared library alongside it. Do not adopt it as a container or namespace isolation layer, because it filters system calls rather than isolating resources, and expect to iterate on the allow list before you enforce it.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem: restricting a binary you cannot recompile
sandbox is described as a package that provides a simple way to enable basic seccomp system call filtering in any application, even a proprietary one, via environment variables. The README states the design goal plainly: it is very similar to the SystemCallFilter= functionality in systemd, but with some advantages.
The problem it addresses is concrete. System call filtering is a good way to reduce what an untrusted or semi-trusted program can do to a machine, but the usual mechanisms all assume some level of control over the target. systemd's filter applies to units it starts, container runtimes apply it to whole namespaces, and writing the filter yourself means writing code. sandbox takes the position that none of that is necessary if the program is dynamically linked and you can set an environment variable before launching it.
The two systemd limitations the README calls out are specific. systemd implicitly whitelists the execve, exit, exit_group, getrlimit, rt_sigreturn and sigreturn system calls, along with the calls for querying time and sleeping, which means an allow list under systemd is never quite the list you wrote. And sandbox says it can provide tighter filtering for dynamically linked binaries, which is the case where the preload approach has something to add.
One naming point first, because it will save confusion. The README opens by pointing readers who want the container based sandboxing environment for Cloudflare Workers to the separate cloudflare/sandbox-sdk project. This repository is not that. It is the low level seccomp layer.
The repository is small and readable: preload.c, sandbox.c, sandbox.h and sandboxify.c at the root, a Makefile, a libsandbox.version linker script, and a LICENSE. The primary language is C and the licence is BSD-3-Clause.
The two environment variables that define the filter
The whole policy surface is two environment variables, and the semantics are a simple pair of mutually exclusive rules.
If SECCOMP_SYSCALL_ALLOW is defined, all system calls listed in the list will be allowed, and if the process attempts to call any system call not from the list, it will be killed by the operating system. The README's example is SECCOMP_SYSCALL_ALLOW="open:write".
Otherwise, if SECCOMP_SYSCALL_DENY is defined, all system calls listed in the list attempted to be used by the process will cause the process to be killed, and all other system calls are allowed. The example here is SECCOMP_SYSCALL_DENY="execve:mprotect".
The allow list is the safer default for a program you do not fully understand, because the deny list only protects you from the calls you thought of, while the allow list protects you from the calls you did not. The deny list is the right tool when you know the binary does one thing and you want to stop it doing another, such as preventing execve and mprotect.
Both variables take a colon separated list of system call names, and the names are the ordinary Linux syscall names, the kind you would find in a syscall table.
There is a third variable, and it is the one that makes this usable in practice. SECCOMP_DEFAULT_ACTION=log configures the sandbox to log filter violations instead of immediately killing the process. The README frames it correctly: when sandboxing new applications it is usually not very clear what system calls it uses to define the proper seccomp filter, so on initial stages it is possible to configure the sandbox to log filter violations instead of killing the process.
Building it: libseccomp from source, then make
The build expects a recent version of libseccomp as a static library, and the README recommends building it from upstream source rather than relying on the version from a particular distribution.
The Makefile looks for a valid libseccomp tree and expects the build in the libseccomp directory, so the README gives the three commands that satisfy the dependency:
user@dev:~/sandbox$ curl -L -O https://github.com/seccomp/libseccomp/releases/download/v2.4.3/libseccomp-2.4.3.tar.gz
user@dev:~/sandbox$ tar xf libseccomp-2.4.3.tar.gz && mv libseccomp-2.4.3 libseccomp
user@dev:~/sandbox$ cd libseccomp && ./configure --enable-shared=no && makeThen make from the project directory produces both artefacts:
user@dev:~/sandbox$ make
cc -c -fPIC -Ilibseccomp/include -o sandbox.o sandbox.c
cc -c -fPIC -Ilibseccomp/include -o preload.o preload.c
cc -shared -Wl,--version-script=libsandbox.version -o libsandbox.so sandbox.o preload.o libseccomp/src/.libs/libseccomp.a
cc -c -fPIC -Ilibseccomp/include -o sandboxify.o sandboxify.c
cc -o sandboxify sandboxify.o sandbox.o libseccomp/src/.libs/libseccomp.aTwo outputs come out of that. libsandbox.so is the dynamically linked library used for dynamically linked executables, and sandboxify is the command line utility for statically linked ones.
The Makefile shows one detail that matters more than it looks. The shared library is linked with a custom version script, libsandbox.version, and the comment in the Makefile explains why: the script hides the symbols from the included static libseccomp to avoid a potential clash with the libseccomp the sandboxed process might be using. That is a real hazard, since a statically linked libseccomp linked into a shared object that gets preloaded into a process which itself uses libseccomp would produce duplicate symbols.
The static libseccomp.a is linked into both outputs, which is why the shared library is built with --enable-shared=no for libseccomp in the first place.
Injecting into a dynamically linked program with LD_PRELOAD
For dynamically linked executables the sandboxing code is injected using the LD_PRELOAD dynamic linker option. The README's example runs the same program with and without the filter.
Without sandboxing it prints Hello, world!. With it:
$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libsandbox.so SECCOMP_SYSCALL_ALLOW="fstat:write:exit_group" ./helloworld
adding fstat to the process seccomp filter
adding write to the process seccomp filter
adding exit_group to the process seccomp filter
Hello, world!The three lines of output are the filter being assembled, one line per allowed call, which makes the policy visible at run time rather than buried in a config file.
Now remove write from the list and the program is killed by the kernel instead:
$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libsandbox.so SECCOMP_SYSCALL_ALLOW="fstat:exit_group" ./helloworld
adding fstat to the process seccomp filter
adding exit_group to the process seccomp filter
Bad system callAnd the kernel generates an audit event, with type=1326, the signal number 31, and the offending syscall number in the syscall field:
[Mon Apr 6 11:27:30 2020] audit: type=1326 audit(1586168850.755:5): auid=1000 uid=1000 gid=1000 ses=2 subj=kernel pid=3148 comm="helloworld" exe="/home/ignat/git/sandbox/helloworld" sig=31 arch=c000003e syscall=1 compat=0 ip=0x7f6c3650e504 code=0x80000000That audit line is how you learn what to put in the allow list. The syscall number identifies the call, and the code field distinguishes the seccomp kill from other audit events.
The mechanism is worth stating explicitly, because it is the security property of the whole thing. The filter is installed by the kernel on the process, so it applies to the code that actually runs, including anything the process execs later, rather than to the calls made by a wrapper that happens to be observing it.
Making the filter permanent with patchelf
Setting LD_PRELOAD for every invocation is workable but easy to forget, and the README documents a way to make it permanent without recompiling the target.
The tool is patchelf, and the command adds libsandbox.so as a runtime dependency:
$ patchelf --add-needed /usr/lib/x86_64-linux-gnu/libsandbox.so ./helloworld
$ ldd ./helloworld
linux-vdso.so.1 (0x00007fffb74cf000)
/usr/lib/x86_64-linux-gnu/libsandbox.so (0x00007f3706499000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f37062cc000)
/lib64/ld-linux-x86-64.so.2 (0x00007f37064ee000)
$ SECCOMP_SYSCALL_ALLOW="fstat:write:exit_group" ./helloworld
adding fstat to the process seccomp filter
adding write to the process seccomp filter
adding exit_group to the process seccomp filter
Hello, world!The ldd output before and after is the diagnostic: before, the binary depends on the vDSO, libc and the dynamic loader, and after, libsandbox.so appears in the list. Because the library is now a recorded NEEDED entry, the program loads it on every run whether or not LD_PRELOAD is set, and the environment variable alone selects the policy.
That is the shape of a deployable setup. A wrapper script or a unit file exports the variable, the binary has a permanent dependency on the filter library, and nothing about the target program has to change. The trade is that you have modified the binary on disk, so the original has to be kept somewhere for comparison and for rebuilds.
Note that the environment variable is still required. The library is present, but with no SECCOMP_SYSCALL_ALLOW or SECCOMP_SYSCALL_DENY defined there is no filter to install, so a run without the variable is an unsandboxed run rather than a failed one.
Statically linked binaries, sandboxify and the ptrace requirement
LD_PRELOAD does nothing for a statically linked executable, because there is no dynamic loader to intercept. For those, the README describes a different route: the sandboxing code is injected by launching the application with the sandboxify command line utility.
$ SECCOMP_SYSCALL_ALLOW="brk:arch_prctl:uname:readlink:fstat:write:exit_group" sandboxify ./helloworld
adding brk to the process seccomp filter
adding arch_prctl to the process seccomp filter
adding uname to the process seccomp filter
adding readlink to the process seccomp filter
adding fstat to the process seccomp filter
adding write to the process seccomp filter
adding exit_group to the process seccomp filter
Hello, world!The allow list here is longer than the dynamically linked example, and that is instructive rather than incidental. A statically linked binary has no loader to do its setup, so it makes calls the dynamic version never would, including brk for the heap, arch_prctl for the architecture setup, uname and readlink for identifying the environment.
The mechanism underneath is different too. The README says that under the hood sandboxify uses the PTRACE_O_SUSPEND_SECCOMP ptrace option, therefore requiring the CAP_SYS_ADMIN capability to operate properly, and that depending on the target system Yama LSM configuration sandboxify might also require the CAP_SYS_PTRACE capability.
Those two capabilities are a meaningful difference from the preload path. LD_PRELOAD needs no special privilege at all, since it is an ordinary environment variable that the invoking user controls. sandboxify needs privileges that are normally reserved for the system administrator, which means it cannot be used by an unprivileged user to confine their own processes, and it introduces a ptrace based mechanism where the preload path installs the filter directly in the target process.
For most programs the dynamically linked path is the one you want, and sandboxify is there for the cases where the binary was deliberately linked statically, which is common in security sensitive software precisely because it removes loader dependencies.
Finding the right allow list with log mode
The section the README calls permissive (log) mode is the practical answer to the hardest part of using this, which is that you do not know what a program calls until you have broken it.
The mechanism is SECCOMP_DEFAULT_ACTION=log, and the example shows it with an empty allow list:
$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libsandbox.so SECCOMP_SYSCALL_ALLOW="" SECCOMP_DEFAULT_ACTION=log ./helloworld
Hello, world!The filter allows no system call at all, and yet the program runs and prints its output. Every violation is logged rather than fatal, so the program completes and you collect the full list of calls it attempted.
The violations are visible in dmesg or in auditd if it is running on the system, and the audit lines carry the syscall number that you need to write the allow list:
[Mon Apr 6 12:04:02 2020] audit: type=1326 audit(1586171042.680:138): auid=1000 uid=1000 gid=1000 ses=2 subj=kernel pid=3284 comm="helloworld" exe="/home/ignat/git/sandbox/helloworld" sig=0 arch=c000003e syscall=5 compat=0 ip=0x7f21a2d42af3 code=0x7ffc0000
[Mon Apr 6 12:04:02 2020] audit: type=1326 audit(1586171042.680:139): auid=1000 uid=1000 gid=1000 ses=2 subj=kernel pid=3284 comm="helloworld" exe="/home/ignat/git/sandbox/helloworld" sig=0 arch=c000003e syscall=1 compat=0 ip=0x7f21a2d43504 code=0x7ffc0000
[Mon Apr 6 12:04:02 2020] audit: type=1326 audit(1586171042.680:140): auid=1000 uid=1000 gid=1000 ses=2 subj=kernel pThe sig=0 in these lines, against sig=31 in the killing example, is the difference between a logged violation and a fatal one.
This is the workflow the README is steering you towards, and it is a good one. Log first with a broad or empty allow list, collect the calls, write the list, then switch to the default action and let the program be killed on anything it was not expecting.
The alternative approaches differ in kind. A container gives the process its own namespaces, filesystem and network view, which is a much broader boundary than a system call filter, at the cost of a full runtime and an image to maintain. systemd's SystemCallFilter= applies a filter without any preload, but it comes with the implicit whitelist the README criticises and it only applies to units systemd starts. Writing a seccomp filter by hand in your own code gives exact control with no preload and no ptrace, at the cost of having to modify the program or wrap it in a launcher you write yourself. sandbox sits between the last two: it gives you the filter without writing code, at the price of either a preload or a privileged helper.
Editorial conclusion
Adopt sandbox if you need to confine a binary you did not write, including a proprietary one, and can accept building libseccomp from upstream source and deploying a preloaded shared library alongside it. Do not adopt it as a container or namespace isolation layer, because it filters system calls rather than isolating resources, and expect to iterate on the allow list before you enforce it. Before you run anything under a kill policy, use SECCOMP_DEFAULT_ACTION=log to record the calls the program actually makes, since the README is explicit that violations logged this way are what you need to define the filter. The licence is BSD-3-Clause, the repository publishes no GitHub releases, and the last push was on 2026-04-23.
Frequently asked questions
What does cloudflare/sandbox do?
It provides a way to enable basic seccomp system call filtering in any application, including a proprietary one, via environment variables. It is similar to systemd's SystemCallFilter= functionality but without some of systemd's limitations, and it can give tighter filtering for dynamically linked binaries.
How do I install and build sandbox?
Download libseccomp 2.4.3 and build it in the libseccomp directory with ./configure --enable-shared=no and make, then run make in the project directory. The README recommends building libseccomp from upstream source rather than using a distribution version.
How do I set a seccomp filter for a dynamically linked program?
Set LD_PRELOAD to the path of libsandbox.so and define SECCOMP_SYSCALL_ALLOW with a colon separated list of allowed system calls, or SECCOMP_SYSCALL_DENY with a list of blocked ones. Any call outside the allow list kills the process.
How do I sandbox a statically linked binary?
Launch it with the sandboxify utility instead of using LD_PRELOAD. The README notes that sandboxify uses the PTRACE_O_SUSPEND_SECCOMP ptrace option, so it requires CAP_SYS_ADMIN and may also require CAP_SYS_PTRACE depending on the Yama LSM configuration.
How do I find out which system calls a program makes?
Set SECCOMP_DEFAULT_ACTION=log so violations are logged rather than fatal, and monitor the results with dmesg or auditd. The audit lines carry the syscall numbers you need to write the allow list.
Can I make the seccomp filter permanent without recompiling the program?
Yes. Use patchelf --add-needed with the path to libsandbox.so to add it as a runtime dependency, which ldd will then show. The environment variable is still needed to select the policy, since the library installs no filter until SECCOMP_SYSCALL_ALLOW or SECCOMP_SYSCALL_DENY is defined.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cloudflare-sandbox)Was this useful?