SYDTUTORIAL(7)
๐ sydtutorial โ a whole tutorial intro to Syd, bestie edition ๐
okay so this is sydtutorial(7), and it's literally your new bestie's guide to Syd. Buckle up, we're doing this in parts and part one is already so much. ๐ซถ
โจ SYNOPSIS โจ
syd [-acefhlmpqtxEPV] [--] {command [arg...]}
๐ธ DESCRIPTION ๐ธ
Syd intercepts system calls made by Linux processes and then decides โ according to a set of rules โ whether each call should proceed, be denied, or be emulated. And the way it does this is honestly iconic: no kernel modules, no setuid binaries, no eBPF. Just seccomp(2) user notification, ptrace(2), landlock(7), and namespaces(7). That's the whole squad. ๐
Run Syd with no arguments and it drops you straight into a login shell. Run it with a command and it sandboxes that command:
$ syd -poff -- echo hello
hello
The -poff selects the "off" profile, which disables all sandboxing. Without -poff, Syd denies everything by default, including exec:
$ syd -- true
syd: exec error: Permission denied
$ echo $?
13
Exit code 13 is EACCES ("Permission denied"). This is what "secure by default" looks like in practice: you have to opt in to every operation the sandboxed process is allowed to perform. no cap, nothing is free here. ๐
This tutorial walks through Syd's sandbox rules, starting from the simplest case ("allow everything and run") through incrementally tighter configurations. It's written for someone who has used the Linux command line and has heard of system calls, but has never touched seccomp(2), landlock(7), or any sandboxing tool. That's you! you're the target audience! welcome! ๐
The examples are tested against Syd 3.51.0 on Linux 6.19. You can type them verbatim on your own system. fr fr, copy-paste energy. โจ
๐ฆ GETTING STARTED ๐ฆ
๐ Prerequisites ๐
Syd requires a Linux kernel with seccomp(2) user notification support. The following kernel features are required, listed with the minimum kernel version that introduced each one:
- Linux 5.0:
SECCOMP_RET_USER_NOTIF, allowing a supervisor process to intercept system calls and respond on behalf of the caller. - Linux 5.5:
SECCOMP_USER_NOTIF_FLAG_CONTINUE, needed to let intercepted system calls proceed unmodified after inspection. - Linux 5.6:
pidfd_getfd(2)andpidfd_send_signal(2), needed for file descriptor operations and signal delivery via process file descriptors.openat2(2)is also required for safe path resolution withRESOLVE_BENEATH,RESOLVE_NO_SYMLINKS, andRESOLVE_NO_MAGICLINKS. - Linux 5.9:
SECCOMP_IOCTL_NOTIF_ADDFD, needed to inject file descriptors into the address space of a sandboxed process during system call emulation. - Linux 5.19:
SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV, which places the intercepted thread in a killable wait state during notification handling; this eliminates a class of unkillable-process bugs and is required for production use.
The following kernel configuration options must be enabled:
CONFIG_SECCOMPandCONFIG_SECCOMP_FILTERare required for system call interception via seccomp(2).CONFIG_SECURITY_LANDLOCKis required for landlock(7) filesystem and network access control. This option must be set toyat kernel build time, and landlock must appear in the boot-timeCONFIG_LSMlist (or be appended via thelsm=kernel command line parameter). Most major distributions enable landlock(7) by default, including Ubuntu (since 20.04), Fedora (since 35), Arch Linux, and Debian Sid.CONFIG_UNIX_DIAGis required for UNIX domain socket diagnostics, which Syd uses to identify peer processes on UNIX sockets.CONFIG_CROSS_MEMORY_ATTACHis recommended; enablesprocess_vm_readv(2)andprocess_vm_writev(2)for reading and writing process memory. Unlikeproc_pid_mem(5), cross memory attach honours the address space permissions of the target process, providing a safer mechanism for inspecting system call arguments. IfCONFIG_CROSS_MEMORY_ATTACHis not available, Syd falls back toproc_pid_mem(5)automatically when theSYD_PROC_PID_MEM_FALLBACKenvironment variable is set, refer to syd(1) manual page for details.CONFIG_KCMPis recommended; enableskcmp(2), which Syd uses to determine whether two file descriptors refer to the same open file description across processes and to check whether two processes share the same address space.
Syd is written in Rust. Building from source requires a Rust toolchain (edition 2024, Rust 1.83 or later) and libseccomp headers. ๐ฆโจ
๐ง Installation ๐ง
The quickest path to a working Syd installation is Cargo, the Rust package manager:
$ cargo install --locked syd
For OCI container runtime support (currently available on x86_64 and aarch64), enable the oci feature:
$ cargo install --locked --features oci syd
If you are working from a git checkout, run:
$ make install
This compiles an optimized release build of Syd and all companion utilities and installs them, along with man pages and Vim syntax files, under ~/.local. The resulting binaries are statically linked by default and can be copied to other systems without additional dependencies. Ensure that ~/.local/bin is in your PATH. ๐ (yes this is your reminder, go check your PATH bestie)
To build with OCI support from a git checkout:
$ make CARGOFEATS=oci install
๐ Kernel support ๐
After installation, run syd --check to print a diagnostic summary of your system's sandboxing capabilities:
$ syd --check
syd 3.51.0 (Crazy Goldberg)
Rock solid application kernel
...
LibSeccomp: v2.9.9 api:7
Landlock ABI 7 is fully enforced.
User namespaces are supported.
Cross memory attach is supported.
Memory sealing is supported.
...
LSMs: capability, landlock, lockdown, yama, bpf.
This output lists the seccomp(2) API level, the landlock(7) ABI version, namespaces(7) support, which Linux Security Modules (LSMs) are active, open file descriptor limits, and the kernel version together with its supported features. If Syd depends on a kernel capability that is absent, this command will tell you. it's giving "diagnostic bestie", it literally snitches on your kernel. ๐
To query the landlock(7) ABI version in isolation:
$ syd-lock -V
๐ Login shell ๐
When invoked with no positional arguments, Syd enters login shell mode. It loads the builtin user profile and spawns a restricted bash(1) session:
$ syd
bash-5.3$
The shell Syd starts is not an ordinary bash session. As defined in src/config.rs, the default command is:
/usr/bin/env HISTFILE= /usr/bin/bash --login --noprofile --norc
--restricted
Several properties of this invocation are worth noting. First, HISTFILE is set to the empty string, which disables command history. No record of the session is written to disk. Second, the --noprofile and --norc flags suppress ~/.bash_profile, ~/.bashrc, and /etc/profile, preventing user and system startup scripts from modifying the sandbox environment. Third, the --restricted flag activates restricted shell mode (rbash), which among other things prohibits changing directories with cd, redirecting output, and modifying PATH. Together, these flags produce a minimal, hardened shell with minimal capabilities. tiny shell, big boundaries. ๐
The login shell applies the user profile, which enables sandbox rules for common interactive use. Try a few commands to see what the profile permits:
bash-5.3$ pwd
/proc/42/fdinfo
bash-5.3$ ls -la
ls: cannot open directory '.': No such file or directory
bash-5.3$ echo hello
hello
bash-5.3$ ls /
ls: cannot open directory '/': Permission denied
bash-5.3$ cat /etc/hostname
cat: /etc/hostname: No such file or directory
bash-5.3$ exit
logout
Several things happened here. First, pwd reports a path under proc_pid_fdinfo(5). This is Syd's own proc(5) directory, the sandbox manager's process ID. Syd restricts access to its own proc(5) entries to prevent sandboxed processes from inspecting or interfering with the sandbox itself (refer to the SECURITY section of syd(7)). Consequently, ls -la cannot open the directory: it returns ENOENT ("No such file or directory") because the path is hidden by proc(5) restrictions. The shell effectively starts in a location that exists in the kernel's VFS but is invisible to the sandboxed process. spooky? no, iconic. ๐
The echo builtin works because builtins do not invoke execve(2); they run inside the shell process itself. The ls / command is an external binary whose execution the user profile permits, but reading the root directory is denied by the profile's read sandbox rules. The cat /etc/hostname result is more subtle: it reports ENOENT ("No such file or directory") rather than EACCES ("Permission denied"). This happens because Syd's Stat Sandboxing hides the file entirely, stat(2) returns ENOENT ("No such file or directory") and getdents64(2) omits the entry from directory listings, so from the process's perspective the file does not exist. it's not "forbidden", it's just gone. gaslight, gatekeep, girlboss. ๐
The SYD_SHELL environment variable overrides the default shell command:
$ SYD_SHELL=/bin/sh syd
$
โญ Single command โญ
Syd's command line parsing follows POSIX conventions (options first, then positional arguments), so the -- separator is not required. You can sandbox a single command by providing it directly:
$ syd true
syd: exec error: Permission denied
$ echo $?
13
Without a profile, Syd denies execve(2) and returns exit code 13 aka EACCES ("Permission denied"). The syd-sys(1) utility can translate between numbers and names for system calls, errno(3) values, ioctl(2) requests, open(2) flags, and signal(7) numbers. It can also list UNIX domain socket inodes via netlink(7). For example:
$ syd-sys -e 13
13 EACCES Permission denied
$ syd-sys 1
write 1
This is the default: every operation is forbidden unless a rule explicitly permits it. To run a command that actually executes, select a profile:
$ syd -poff echo hello
hello
The -poff flag loads the off profile, which disables all sandbox categories. This is useful for verifying that Syd itself is working before adding restrictions. vibe check first, rules later. ๐ฆ
๐ฅ Profiles ๐ฅ
Profiles are pre-defined sets of sandbox rules compiled into the Syd binary. Each profile configures which sandboxing categories are active and which paths, addresses, and system calls are allowed or denied. The PROFILES section of syd(5) manual page documents the full set of available profiles and their intended use.
To list the available profiles:
$ syd-cat -p list
chrome
container
core
cwd
debug
enforce
firefox
fs
gui
hide
immutable
landlock
lang
ldd
lib
linux
ltp
nix
nixstore
...
Some profiles are literally the blueprint for others, bestie. Like, the linux profile is the common-rules starter pack for Linux systems, and it gets included by the user, paludis, and oci profiles. Teamwork!
The user profile is the default for the login shell, and it's the one for general interactive use (iykyk). The immutable profile treats the entire root filesystem as read-only โ writes only where it explicitly says you can. And the off profile? It just disables all sandboxing. Napping. ๐ค
Wanna peek at what rules a profile is actually holding?
$ syd-cat -p user
And you can stack multiple profiles on the command line โ later profiles override the earlier ones, periodt:
$ syd -pimmutable -mallow/write+/var/cache/*** make install
๐ธ -m Flag ๐ธ
Okay so the -m flag is how you pass individual sandbox commands straight on the command line. Each -m takes exactly one command, as documented in syd(2). All -p, -P, and -m flags get processed in the order you typed them, and because Syd rocks a "last match wins" resolution strategy (again, syd(2)), later flags override earlier ones for the same sandbox category โ doesn't matter what type they are. ๐
Basic example, enabling Write Sandboxing on top of the off profile:
$ syd -poff -msandbox/write:on -mallow/write+/tmp/*** touch /tmp/hello
$ echo $?
0
So -poff kills all sandboxing, -msandbox/write:on brings Write Sandboxing back to life, and -mallow/write+/tmp/*** adds /tmp and everything below it to the write allowlist. Since /tmp/hello matches the allow rule, touch(1) slays and succeeds. โจ
Multiple -m flags for the same category layer in order. Like, you can allow a big directory tree first and then deny a little subtree inside it:
$ mkdir -p /tmp/secret
$ syd -poff -msandbox/write:on -mallow/write+/tmp/*** -mdeny/write+/tmp/secret/*** touch /tmp/secret/plans
{"ctx":"access","cap":"write","act":"deny","sys":"openat", "path":"/tmp/secret/plans", "tip":"configure `allow/write+/tmp/secret/plans'"}
touch: cannot touch '/tmp/secret/plans': Permission denied
$ echo $?
1
Syd logs the denied access as a JSON object on standard error, and it's giving full receipts: the syscall that got denied (openat), the path, and a tip field suggesting how to allow it. The deny rule for /tmp/secret comes after the allow rule for /tmp, so the deny wins. Flip the order and you get the opposite result โ the allow would override the deny. No cap.
Quick heads-up: the-mrules layer on top of the selected profile. Without-poffor another profile that allows execution, the default sandbox deniesexecve(2)before any write rule even gets a chance to do anything:
$ syd -msandbox/write:on touch /tmp/hello
syd: exec error: Permission denied
This is the classic first-timer mistake, fr fr. Always start from a profile that permits execution, then layer restrictions with -m. The off profile followed by selective sandbox enables is one path; the user profile with extra deny rules is another. ๐ซถ
๐ฆ CONFIGURATION ๐ฆ
The last section introduced profiles via -p and individual commands via -m. Now we're going full lore dump: what profiles actually contain, how configuration files work, how rules get resolved, and how patterns match paths. Buckle up, bestie. ๐
โญ Profile Anatomy โญ
A profile is a named set of sandbox commands compiled right into the Syd binary. To snoop on its contents, hand its name to syd-cat(1):
$ syd-cat -poff
# Syd profile: Off
# Number of rules: 2
# Copyright (c) 2023, 2024 Ali Polatel <alip@chesswob.org>
# SPDX-License-Identifier: GPL-3.0
sandbox/all:off
sandbox/fs,ioctl,lock,net,mem,pid,pty,force,tpe:off
So the off profile is literally just two commands: one turning off all primary sandbox categories, one turning off every secondary category. Iconic minimalism. Now compare it to the user profile:
$ syd-cat -puser
# Syd profile: User "user"
# Number of rules: 17
include_profile linux
include_profile landlock
include_profile local
include_profile nomagic
include_profile noscm
include_profile rand
include_profile tty
sandbox/lpath:${SYD_USER_LPATH:-on}
trace/allow_safe_syslog:true
tpe/negate:1
tpe/user_owned:1
tpe/gid:${SYD_GID}
trace/force_umask:7177
allow/lock/all+${SYD_HOME}
allow/all+${SYD_HOME}/**
allow/lpath,rpath+${SYD_HOME}/***
deny/all+${SYD_HOME}/**/.*/***
allow/all+${SYD_HOME}/**/._history_
Several things are doing the most here. The include_profile directive pulls in other profiles by name โ linux, landlock, local, nomagic, noscm, rand, and tty all get dragged in, which makes the user profile a whole composition of lower-level building blocks. Environment variables like ${SYD_HOME} and ${SYD_GID} get expanded at parse time; Syd sets these automatically before loading the profile (check the ENVIRONMENT section of syd(5), bestie). And the notation ${SYD_USER_LPATH:-on} gives you a default value โ if the variable is unset, the value on is used. Cozy. ๐ง
๐ Profile Stacking ๐
Multiple -p flags can appear on the command line. Profiles load in order, and since Syd does last-match-wins, later profiles override the earlier ones. Incremental refinement, we love to see it:
$ syd -puser -pimmutable ls /
ls: cannot open directory '/': Permission denied
$ echo $?
2
The user profile permits reading most of the filesystem, but immutable, loaded second, remounts system directories read-only inside a mount_namespaces(7) and applies stricter access rules that override the user defaults. Rip reading /. ๐ญ
Some profiles have one-character shortcuts too, and you can combine them into a single -p argument:
$ syd -puiq ...
That stacks the user (u), immutable (i), and quiet (q) profiles. The full list of profiles and their shortcuts lives in the PROFILES section of syd(5), and you can always query it with syd-cat -plist. ๐
๐ Configuration Files ๐
Configuration files give you the same commands as -m flags, one per line. Comments start with #; blank lines are ignored. Oh and the file extension must be .syd-3, reflecting the current API version. Rules are rules.
A minimal config file that confines writes to /tmp:
# /tmp/example.syd-3: Allow writes under /tmp only
sandbox/write:on
allow/write+/tmp/***
Load it with the -P flag:
$ syd -poff -P/tmp/example.syd-3 touch /tmp/syd_test_file
$ echo $?
0
You can specify multiple -P flags. All -p, -P, and -m arguments are processed strictly in the order they appear on the command line โ there is no precedence between them. So a -m that appears before a -P takes effect first. Periodt.
The syd-cat(1) utility can parse and validate configuration files all on its own, independent of Syd itself. Pass it one or more file paths and it'll report syntax errors or print the resolved sandbox state:
$ syd-cat /tmp/example.syd-3
Syd:
Sandbox ID: ?
...
Glob Rules: (1.66K, total 1, highest precedence first)
1. Action: allow, Capability: write, Pattern: `/tmp'
...
If the file's got errors, syd-cat(1) exits with a non-zero status and an error message โ which makes it genuinely useful for testing config before you deploy it. Test before you ship, bestie. ๐ซถ
Configuration files support two inclusion directives:
includepath โ includes another configuration file. Relative paths are resolved from the directory of the including file, not the current working directory. The included file must not be writable by group or others, for security. Circular includes get detected by caching device and inode numbers.include_profilename โ includes a built-in profile by name, exactly as if-phad been specified.
Environment variables are expanded in all arguments using shellexpand syntax. If a variable is unset, Syd aborts with an error rather than expanding to the empty string โ which prevents accidental over-permissive rules. Use ${VAR:-default} to supply fallback values. ๐ฆ
๐ Pattern Matching ๐
Path rules use glob(3p) patterns. Standard wildcards apply: * matches any sequence of characters within a single path component, ? matches a single character, and [...] matches a character class.
Syd extends standard globbing with the triple-star pattern ***, which matches the prefix directory itself and everything below it to arbitrary depth. A pattern like /tmp/*** first matches the directory /tmp on its own, then matches any path beneath it. The three wildcard levels are:
/tmp/*matches/tmp/foobut not/tmp/foo/bar./tmp/**matches files in immediate subdirectories of/tmp./tmp/***matches/tmpitself,/tmp/foo,/tmp/foo/bar, and so on to arbitrary depth.
Know the difference, it's important. ๐ฏ
๐ฅ Rule Ordering ๐ฅ
Syd evaluates rules in the order they appear. For multiple rules matching the same path, the last matching rule determines the outcome. All -p, -P, and -m arguments are processed strictly in command-line order; there is no precedence between them.
Which means you can start with a broad allow and carve out exceptions with later deny rules, or start restrictive and add targeted allows. Your sandbox, your rules. ๐
๐ธ Runtime Configuration ๐ธ
Besides startup-time config, Syd supports runtime reconfiguration through magic stat(2) calls. A sandboxed process can literally just do:
test -c /dev/syd/sandbox/read:on
That stat(2) call on the virtual path /dev/syd/sandbox/read:on enables read sandboxing at runtime. The stat(2) interface accepts the same commands as -m and is documented in syd(2). Runtime configuration is permitted when the sandbox lock is off, exec, ipc, or drop. Let's break that down:
- With
lock:off, any process in the sandbox can issue runtime commands. - With
lock:exec, only the initial exec child retains this ability. - With
lock:ipc, commands must be sent through the IPC socket. - With
lock:drop, commands can only reduce privileges โ commands relaxing the sandbox policy aren't permitted. This mode is similar to OpenBSDpledge(2).
When the lock is on or read, runtime changes are not accepted. Nope. Not happening. ๐
๐ User Profile and Configuration Files ๐
The user profile โ loaded by default in login shell mode โ searches for two additional configuration files at startup:
/etc/user.syd-3โ system-wide rules applied to all users.~/.user.syd-3โ per-user rules.
These files are parsed after the user profile itself. And because last-match-wins semantics apply, rules in ~/.user.syd-3 override rules in /etc/user.syd-3, which in turn override the built-in user profile defaults. Layers on layers on layers.
Wanna lock the system-wide config so per-user files can't weaken it? Droplock:onorlock:dropat the end of/etc/user.syd-3. After the lock is set, no further configuration changes are accepted โ neither from subsequent files nor from runtimestat(2)calls. Locked in. ๐
๐ SANDBOX LOCK ๐
Okay so, the sandbox lock is literally the bouncer for runtime changes: it controls whether and how sandbox rules can be modified after Syd starts executing the sandboxed process. Itโs set with the lock command (documented in syd(2)), and it has six possible states: on, off, exec, ipc, drop, and read.
Single-character abbreviations are also accepted: 1, 0, x, i, d, and r.
Specifying lock without a value is equivalent to lock:on.
๐ Lock States ๐
lock:onโ This seals the sandbox policy entirely. No runtime configuration is accepted from any source, periodt.
$ syd -poff -mlock:on sh -c 'test -c /dev/syd/sandbox/write:on && echo "enabled" || echo "locked out"'
locked out
lock:offโ This leaves the sandbox fully open to runtime changes. Any process in the sandbox can issuestat(2)commands on/dev/syd/paths to modify the policy.lock:execโ This sets the lock toonfor all processes except the initial exec child. It allows the initial process to configure the sandbox at startup and then seal it:
$ syd -poff -mlock:exec sh -c 'test -c /dev/syd/sandbox/write:on && echo "write on"; test -c /dev/syd/lock:on && echo "locked"; test -c /dev/syd/sandbox/read:on && echo "read on" || echo "config rejected after lock"'
write on
locked
config rejected after lock
The initial shell enables write sandboxing at runtime, then transitions to lock:on. After that, the attempt to enable read sandboxing is rejected. No take-backs era.
lock:ipcโ This restricts runtime commands to the IPC socket. The IPC socket is a UNIX domain socket whose accessibility depends on the sandbox ACL rules. Processes that cannot reach the socket cannot modify the policy.lock:dropโ This permits commands that further restrict the sandbox but rejects commands that would loosen it. Itโs useful for processes that need to progressively tighten their own confinement:
$ syd -poff -mlock:drop sh -c 'test -c /dev/syd/sandbox/write:on && echo "write on"'
write on
Enabling a sandbox category counts as dropping privileges, so the command is accepted.
lock:readโ This makes thesyd(2)virtual system call API available in read-only mode. The sandboxed process can query the current policy state through theopen(2)hooks but cannot modify it.
๐ Default Lock State ๐
If no lock command appears in any profile, configuration file, or -m argument, Syd defaults to lock:on at the moment it executes the initial sandbox process. This ensures that the sandbox policy is sealed by default. Secure by default, bestie.
๐ธ Lock Transitions and Sealing ๐ธ
Transitions from lock:off, lock:exec, lock:ipc, and lock:drop into lock:on or lock:read are one-way. Once the lock reaches on or read, the sandbox policy is sealed in memory using mseal(2) and cannot be changed.
Transitions between lock:on and lock:read are not permitted.
๐ฆ PATH SANDBOXING ๐ฆ
Syd intercepts system calls that operate on filesystem paths and checks them against per-category allow and deny lists. Each sandbox category corresponds to a class of file operations and can be enabled or disabled independently.
โจ Sandbox Categories โจ
The primary path sandbox categories are:
readโopen(2)withO_RDONLYorO_RDWR,getxattr(2)and related extended attribute reads.writeโopen(2)withO_WRONLYorO_RDWR.execโexecve(2),execveat(2),mmap(2)withPROT_EXEC, and dynamic library loading.statโstat(2),statx(2),access(2),readlink(2),getdents64(2), and related metadata calls.walkโ Path traversal during canonicalization, split fromstatto prevent unhiding of hidden paths.
Syd also provides fine-grained categories for specific operations:
createโcreat(2),open(2)withO_CREAT,memfd_create(2).deleteโunlink(2),unlinkat(2)withoutAT_REMOVEDIR.renameโrename(2),renameat(2),link(2),linkat(2).symlinkโsymlink(2),symlinkat(2).truncateโtruncate(2),fallocate(2),open(2)withO_TRUNC.chdirโchdir(2),fchdir(2).readdirโopen(2)on existing directories.mkdirโmkdir(2),mkdirat(2).rmdirโrmdir(2),unlinkat(2)withAT_REMOVEDIR.chown,chgrpโchown(2),fchownat(2)and variants.chmodโchmod(2),fchmodat(2),fchmodat2(2).chattrโsetxattr(2),removexattr(2)and variants.chrootโchroot(2).notifyโfanotify_mark(2),inotify_add_watch(2).utimeโutimensat(2),utimes(2).mkdevโmknod(2)for block devices.mkfifoโmknod(2)for FIFOs.mktempโopen(2)withO_TMPFILE.
Refer to syd(7) manual page for the complete list of system calls filtered by each category.
๐ Sandbox Category Sets ๐
Working with individual categories can be verbose. Syd provides shorthand names inspired by the promise names of OpenBSD's pledge(2) and FreeBSD's capsicum rights(4freebsd). These names group related categories into sets that can be used anywhere a category name is accepted:
rpathโread,readdir. Named after thepledge(2)rpathpromise. Inpledge(2),rpathalso coversstat(2),access(2),readlinkat(2), andchdir(2); Syd separates those into thelpathset.wpathโwrite,truncate. Named after thepledge(2)wpathpromise.cpathโcreate,delete,rename. Named after thepledge(2)cpathpromise, which also coversmkdir(2)andrmdir(2); Syd separates those into thetpathset.fattrโchmod,chattr,utime. Named after thepledge(2)fattrpromise. Inpledge(2),fattralso coverschown(2)andfchown(2); Syd separates ownership changes into thefownset.fownโchown,chgrp. A Syd-specific set that splits ownership changes out of thepledge(2)fattrpromise.dpathโmkbdev,mkcdev. Named after thepledge(2)dpathpromise, which coversmknod(2)andmkfifo(2). Syd narrows this set to block and character device creation only; FIFOs are in the separatespathset.spathโmkfifo,symlink. A Syd-specific set;pledge(2)placesmkfifo(2)underdpathandsymlink(2)undercpath.tpathโmkdir,rmdir. A Syd-specific set that splits directory creation and removal out of thepledge(2)cpathpromise.lpathโwalk,stat,chdir,notify. A Syd-specific set with nopledge(2)analogue. It corresponds to the path visibility controls ofunveil(2)and groups the categories responsible for path lookup, metadata access, and directory change.netโnet/bind,net/connect.inetโnet/bind,net/connect. Named after thepledge(2)inetpromise.passfdโsendfd,recvfd.allโ every category.all-xโ every category exceptexec.
Each set also has a lock/ variant that controls the corresponding landlock(7) access rights rather than the seccomp(2) sandbox rules. For instance, lock/rpath controls landlock(7) read and readdir access rights, while rpath controls the seccomp(2) read and readdir sandbox categories.
These sets make rules more concise. Compare the two equivalent rules:
deny/read,readdir,write,truncate,create,delete,rename+${HOME}/.ssh/***
deny/rpath,wpath,cpath+${HOME}/.ssh/***
๐ Enabling Sandbox Categories ๐
Categories are enabled with the sandbox/ command and can be grouped with commas:
sandbox/read,write,exec:on
The shorthand sandbox/all:on enables every primary category at once. Individual categories can then be turned off selectively.
๐ฅ Allow and Deny Rules ๐ฅ
Once a category is enabled, all operations in that category are denied by default. The default action can be changed per category with the default/ command. The available actions are:
allowโ Permit the system call.denyโ Deny the system call withEACCES(default).filterโ Deny the system call silently, without logging.warnโ Allow the system call but log a warning (learning mode, used bypandora(1)).killโ Deny the system call and terminate the offending process withSIGKILL(seesignal(7)).stopโ Deny the system call and sendSIGSTOPto the offending process (seesignal(7)).abortโ Deny the system call and sendSIGABRTto the offending process (seesignal(7)). UnlikeSIGKILL,SIGABRTcan be caught, so this action should only be used for debugging in trusted environments where acore(5)dump file is useful.panicโ Deny the system call and panic the Syd emulator thread. Currently equivalent todeny.exitโ Log a warning and exit Syd immediately with thedenyerrno(3)as exit value. All sandbox processes are terminated: direct children receiveSIGKILLvia the parent death signal (seePR_SET_PDEATHSIG(2const)), traced processes are killed viaPTRACE_O_EXITKILL(seeptrace(2)), and closing theseccomp(2)notification file descriptor causes any pending system calls to fail.
Refer to syd(7) for the full description of sandbox actions and syd(2) for the default/ command documentation.
Allow rules open specific paths; deny rules close them. Both use glob(3p) patterns as described in the Pattern Matching section.
The following example enables write sandboxing, allows writes under /tmp, and then attempts to write outside the allowed area:
$ syd -poff -msandbox/write:on -mallow/write+/tmp/*** -mallow/read+/*** touch /tmp/pathtest
$ echo $?
0
$ syd -poff -msandbox/write:on -mallow/read+/*** touch /home/alip/forbidden
{"ctx":"access","cap":"write","act":"deny","sys":"openat","path":"/home/alip/forbidden","tip":"configure `allow/write+/home/alip/forbidden'"}
touch: cannot touch '/home/alip/forbidden': Permission denied
The first touch(1) succeeds because /tmp/pathtest matches the allow rule. The second is denied because no allow rule covers /home/alip/forbidden.
๐ซถ Path Hiding ๐ซถ
The stat and walk categories can hide files and directories from the sandboxed process entirely. When a stat(2) call is denied, Syd returns ENOENT ("No such file or directory") to the caller, making the path appear non-existent:
$ syd -poff -msandbox/stat:on -mallow/stat+/*** -mdeny/stat+/etc/shadow ls -la /etc/shadow
ls: cannot access '/etc/shadow': No such file or directory
The process receives no indication that /etc/shadow exists. Programs that enumerate directory contents via getdents64(2) also have the hidden entries filtered out.
The walk category complements stat by preventing hidden paths from being discovered during path canonicalization. Without walk, a process could detect a hidden path by traversing through it (e.g. resolving /etc/shadow/../passwd). Together, stat and walk provide a complete path hiding mechanism analogous to unveil(2) on OpenBSD.
๐ซถ Path Masking ๐ซถ
Okay so picture this: hiding makes a path invisible, but masking is a whole different vibe โ it replaces the contents. The mask command redirects open(2) calls on matching paths to a different file (by default /dev/null, iconic). At the stat(2) boundary, a masked path returns the metadata of the mask target, not the original file. Slay.
A masked /etc/hostname reads as empty:
$ syd -poff -mmask+/etc/hostname -mallow/read+/*** cat /etc/hostname
$ echo $?
0
The cat(1) call succeeds but produces no output because open(2) returns a file descriptor to /dev/null. A stat(2) call on the same path returns the mask target's metadata:
$ syd -poff -msandbox/lpath:on -mallow/lpath+/*** -mmask+/etc/hostname -mallow/read+/*** stat /etc/hostname
File: /etc/hostname
Size: 0 Blocks: 0 IO Block: 4096 character special file
Device: 0,6 Inode: 4 Links: 1 Device type: 1,3
Access: (0666/crw-rw-rw-) Uid: ( 0/ root) Gid: ( 0/ root)
The file name still reads /etc/hostname, but the metadata reports a character special file with device 1,3 which are the attributes of /dev/null. Literally a full identity swap, no cap.
The default mask target can be changed by appending a colon-separated path. For example, masking with /dev/zero causes reads to return zero bytes:
$ syd -poff -mmask+/etc/hostname:/dev/zero -mallow/read+/*** sh -c 'head -c 8 /etc/hostname | xxd'
00000000: 0000 0000 0000 0000 ........
For directories, a second colon-separated path specifies the directory target:
mask+/proc/acpi/***:/dev/null:/var/empty
This masks regular files under /proc/acpi with /dev/null and subdirectories with /var/empty. Mask targets must be fully canonicalized paths without symbolic links. No shortcuts, bestie. ๐
Masked paths are also protected against filesystem writes. The file cannot be truncated, overwritten, renamed, deleted, or have its metadata changed.
The mask command does not require creating a mount_namespaces(7), providing a non-privileged alternative to bind mounts. Mask commands can also be specified dynamically after startup using the syd(2) API, allowing for incremental confinement. Incremental glow-up confinement, honestly. โจ
๐ Practical Example ๐
The following configuration sandboxes a build tool. It allows reading and stat access everywhere, restricts writes to the build directory, terminates with SIGKILL any process that attempts to access ~/.ssh or ~/.gnupg, hides /etc/shadow from stat, masks /etc/hostname with /dev/null, and permits execution only from /usr:
include_profile tty
sandbox/read,write,exec,stat:on
allow/read+/***
allow/stat+/***
allow/write+/home/alip/project/build/***
allow/exec+/usr/***
kill/rpath,wpath,cpath+${HOME}/.ssh/***
kill/rpath,wpath,cpath+${HOME}/.gnupg/***
kill/stat+/etc/shadow
mask+/etc/hostname
Any attempt to write outside /home/alip/project/build, execute a binary from outside /usr, or access ~/.ssh is terminated with SIGKILL. A stat(2) call on /etc/shadow returns ENOENT ("No such file or directory"), and reading /etc/hostname yields empty output. The vibes are immaculate and the boundaries are enforced. ๐ฅ
๐ LOCK SANDBOXING ๐
Lock Sandboxing uses the Landlock Linux Security Module (landlock(7)) for kernel-enforced unprivileged access control. Unlike the seccomp(2) based sandbox, Landlock rules are enforced entirely in kernel space and apply to the Syd process itself. A compromised Syd process is still confined by the Landlock sandbox, making Lock sandboxing a second layer of defence. Double protection? We love to see it. ๐ซถ
๐ฆ Lock Categories ๐ฆ
Lock Sandboxing is enabled by default. The off profile disables it with sandbox/lock:off. Paths and port ranges are populated using lock/ categories:
allow/lock/read+/usr
allow/lock/read,write+/tmp
allow/lock/exec+/usr/bin
allow/lock/bind+8080
allow/lock/connect+0-65535
The available lock/ categories are: lock/read, lock/write, lock/exec, lock/ioctl, lock/create, lock/delete, lock/rename, lock/symlink, lock/truncate, lock/readdir, lock/mkdir, lock/rmdir, lock/mkdev, lock/mkfifo, and lock/bind. The shorthand lock/all stands for the union of all these categories, and lock/all-x stands for all except lock/exec.
Lock category sets group related lock/ categories, mirroring the structure of the seccomp(2) category sets but covering only the operations that landlock(7) can enforce:
lock/rpathโlock/read,lock/readdir.lock/wpathโlock/write,lock/truncate.lock/cpathโlock/create,lock/delete,lock/rename.lock/dpathโlock/mkbdev,lock/mkcdev.lock/spathโlock/mkfifo,lock/symlink.lock/tpathโlock/mkdir,lock/rmdir.lock/netโlock/bind,lock/connect.lock/inetโlock/bind,lock/connect.lock/bnetโlock/bind.lock/cnetโlock/connect.
Notably, there are no lock/fattr, lock/fown, or lock/lpath sets because landlock(7) does not govern ownership, attribute changes, or path traversal. It's giving "we only enforce what we can actually enforce," and honestly? Respect. ๐
๐ Network Port Rules ๐
As of version 3.29.0, Landlock network confinement is supported. Use allow/lock/bind+port and allow/lock/connect+port to allowlist specific ports. A closed range port1-port2 is also accepted:
allow/lock/bind+8080-8090
allow/lock/connect+443
UNIX domain socket creation, renames, and links can be confined using the lock/bind category with an absolute path:
allow/lock/bind+/run/user/${SYD_UID}
Ports and paths, both getting their little permission lists. Main character energy. ๐
๐ง Compatibility levels ๐ง
The default compatibility level is kill (since version 3.35.0), which maps to Hard Requirement: paths specified in lock/ rules must exist, and missing paths cause a fatal ENOENT error. The level can be changed at startup with default/lock:
killโ Hard Requirement; missing paths are fatal (default).denyโ Soft Requirement; missing paths produce a warning but are skipped.warnโ Best Effort; log a warning for missing paths and skip them, apply whatever the running kernel ABI supports.
Refer to syd(2) manual page for the full default/lock documentation.
๐ธ syd-lock Utility ๐ธ
The syd-lock(1) utility runs a single command under a Landlock sandbox without Syd's full seccomp(2) machinery. The -l flag specifies categories and paths in the same syntax as lock/ commands:
$ syd-lock -l read,exec+/ -l write+/tmp wget -O /tmp/file https://example.com
This confines wget(1) to read and execute from /, write only to /tmp, and deny all other filesystem access at the Landlock level. Use syd-lock -V to print the Landlock ABI version supported by the running kernel. Cute, focused, Landlock-only. Periodt. ๐
๐ NETWORK SANDBOXING ๐
Okay so Syd locks down network stuff through four categories, and yes bestie, you need to know all four:
net/bindโ coversbind(2), UNIX domain socket creation viamknod(2), andsocketpair(2).net/connectโ coversconnect(2),sendto(2),sendmsg(2), andsendmmsg(2).sendfdโ sending file descriptors viasendmsg(2)andsendmmsg(2)withSCM_RIGHTS. The file descriptor being sent is matched, not the peer it is sent to.recvfdโ receiving file descriptors viarecvmsg(2)andrecvmmsg(2)withSCM_RIGHTS. The file descriptor being received is matched, not the peer it is received from.
Supported socket families are UNIX, IPv4, IPv6, Netlink, and KCAPI. The option trace/allow_unsupp_socket:1 passes through sockets of unsupported types. Lowkey important if you're dealing with weird socket types and you don't want everything to explode ๐ฅ.
๐ Address Matching ๐
Network rules use a simple address scheme. UNIX and abstract UNIX socket addresses use glob(3p) patterns. IPv4 and IPv6 addresses use CIDR notation followed by a port range separated by !:
allow/net/connect+192.168.1.0/24!80-443
deny/net/bind+0.0.0.0/0!0-1023
allow/net/bind+/run/user/${SYD_UID}/***
A port range can be a single port (80) or a closed range (1024-65535). UNIX domain socket paths always start with /, abstract sockets are prefixed with @, and unnamed sockets use the dummy path !unnamed. Slay, now you know the whole address vibe โจ.
โ๏ธ Dial Strings โ๏ธ
Network rules also accept Plan9/go style dial(3) strings in the form network!netaddr!service, which add protocol filtering on top of address matching:
allow/net/connect+tcp!127.0.0.1!80
allow/net/connect+udp!9.9.9.9!53
allow/net/bind+tcp6!::1!8080
allow/net/connect+udp!loopback!*
allow/net/connect+unix!/run/foo.sock
allow/net/bind+unix!@dbus-*
allow/net/bind,net/connect+unix!!unnamed
Network names tcp, tcp4, and tcp6 match TCP sockets only, udp, udp4, and udp6 match UDP sockets only, and net, net4, and net6 match any protocol. The 4 and 6 suffixes constrain the address family. Netaddr is an IP address, a CIDR network, an address alias (see below), or the wildcard * matching any address. Service is a port range or the wildcard * matching any port. Network names unix, unixgram, and unixpacket take a glob(3p) pattern instead, matching UNIX socket addresses with the usual conventions: Paths start with /, abstract socket names are prefixed with @, and unnamed sockets use the dummy address !unnamed. Addresses in the plain format match any protocol, so existing configuration keeps working as before. No cap, your old config is safe ๐ซถ.
๐ท๏ธ Address Aliases ๐ท๏ธ
Syd provides aliases for common address ranges to avoid hardcoding CIDR blocks:
anyโ0.0.0.0/0+::/0(all IPv4 and IPv6).loopbackโ127.0.0.0/8+::1/128.localโ RFC 1918 private ranges:10.0.0.0/8,172.16.0.0/12,192.168.0.0/16, and IPv6 equivalents.linklocalโ169.254.0.0/16+fe80::/10.multicastโ224.0.0.0/4+ff00::/8.
Each alias also has 4 and 6 variants (e.g. loopback4, loopback6) to target a single address family. It's giving "I memorized my CIDR blocks" but make it convenient ๐
.
๐งช Practical Example ๐งช
The following enables network sandboxing and allows only outbound connections to loopback on port 80. A connection to an external address is denied:
$ syd -poff -msandbox/net:on -mallow/read+/*** -mallow/net/connect+loopback!80
curl -so /dev/null http://1.1.1.1
{"cap":"net/connect","act":"deny","sys":"connect","addr":"1.1.1.1!80",
"tip":"configure `allow/net/connect+1.1.1.1!80'"}
The curl(1) connection to 1.1.1.1 is denied because only loopback port 80 is allowed. Allowing HTTPS outbound to any address is as simple as:
allow/net/connect+any!443
Refer to syd(7) manual page for the full network sandboxing documentation and syd(2) manual page for the address matching syntax. Iconic cross-reference behavior, honestly ๐ฅ.
๐ซ IP Blocklists ๐ซ
The block command maintains a set of IP networks that are blocked on connect(2), sendto(2), sendmsg(2), sendmmsg(2), and checked against source addresses returned by accept(2) and accept4(2). Use block+ and block- to add and remove networks. Syd can import IP blocklists in ipset and netset formats directly from configuration:
include /usr/src/blocklist-ipsets/feodo.ipset
include /usr/src/blocklist-ipsets/dshield.netset
block!
The block! command aggregates the imported networks to reduce memory consumption and improve matching performance. Use block^ to clear the blocklist. Refer to syd(2) manual page for the full block command documentation. Memory-efficient and matching-efficient? We love to see it ๐ธ.
โก EXECUTION CONTROL โก
๐ Exec Sandboxing ๐
The exec category confines binary execution and dynamic library loading. The filtered system calls are execve(2), execveat(2), mmap(2), mmap2(2), and memfd_create(2). For scripts, both the script and its interpreter are checked. Dynamic libraries linked to ELF executables are checked at exec time, and mmap(2) calls with PROT_EXEC (typically dlopen(3)) are checked at runtime. Enable it with sandbox/exec:on and allowlist trusted paths:
sandbox/exec:on
allow/exec+/usr/***
Any attempt to execute a binary outside the allowed paths is denied with EACCES ("Permission denied"):
$ syd -poff -msandbox/exec:on -mallow/exec+/usr/*** \
-mallow/read+/*** /tmp/test_echo hello
{"cap":"exec","act":"deny","sys":"execve","path":"/tmp/test_echo",
"tip":"configure `allow/exec+/tmp/test_echo'"}
syd: exec error: Permission denied
The default action for exec violations can be changed with default/exec, for example default/exec:kill terminates the process with SIGKILL on any exec violation. That's the "not me, not today" setting ๐.
๐ก๏ธ Trusted Path Execution ๐ก๏ธ
Trusted Path Execution (TPE) restricts execution to binaries that reside in trusted directories. Enable it with sandbox/tpe:on. A binary is trusted if both the file and its parent directory satisfy:
- Not writable by group or others.
- Owned by root (optional, enable with
tpe/root_owned:1). - Owned by the current user or root (optional, enable with
tpe/user_owned:1). - On the root filesystem (optional, enable with
tpe/root_mount:1).
If these criteria are not met, execution is denied with EACCES ("Permission denied"). The default action can be changed with default/tpe, for example default/tpe:kill terminates the offending process with SIGKILL (see signal(7)).
TPE checks at three stages:
execve(2)/execveat(2)system call entry to check scripts.ptrace(2)exec event to check the ELF executable and dynamic loader.mmap(2)when dynamic libraries are mapped, typically viadlopen(3).
By default, TPE applies to all users. To restrict it to a specific group, set tpe/gid to the untrusted group ID. The tpe/negate option inverts this logic, making the specified group trusted instead.
Syd's TPE implementation is based on HardenedBSD's, which is inspired by GrSecurity's TPE. Refer to syd(2) manual page for the full list of tpe/ options. Trusted directory check? We stan a secure queen ๐.
๐ช Force Sandboxing ๐ช
Force sandboxing verifies binary integrity at execution time. Enable it with sandbox/force:on. The force command populates an Integrity Force map that associates file paths with checksums:
force+/usr/bin/curl:sha256:a1b2c3...hexdigest...:deny
The format is force+/path:algorithm:hashhex:action where :action is optional and defaults to deny. Available algorithms are the supported userspace hashes (see syd-sum(1) -a list), e.g. sha256, sha3-512, blake2b-256, crc32c. Available actions are warn, filter, deny (the default), panic, stop, abort, kill, and exit. Use force-/path to remove an entry, or force^ to clear the map.
Upon execve(2), Syd computes the checksum of the target binary and compares it against the map. A mismatch triggers the configured action. Beyond execve(2), Force sandboxing also checks:
- Dynamic libraries linked to ELF executables.
- Libraries loaded at runtime via
mmap(2)withPROT_EXEC(typicallydlopen(3)).
Helper tools:
syd-sum(1)calculates checksums of files.syd-path(1)generates integrity force rules for all binaries underPATH.
Refer to syd(2) manual page for the full force command documentation. Integrity check at exec time? Obsessed, honestly ๐ฆ.
๐จ SegvGuard ๐จ
SegvGuard blocks execution of binaries that crash repeatedly, mitigating brute-force exploitation attacks. Inspired by HardenedBSD's implementation with identical defaults:
segvguard/maxcrashesโ maximum crashes before suspension (default:5).segvguard/expiryโ time window for counting crashes in seconds (default:120, i.e. 2 minutes).segvguard/suspensionโ suspension duration in seconds (default:600, i.e. 10 minutes).
If a sandboxed process receives a crash signal segvguard/maxcrashes times within segvguard/expiry seconds, subsequent attempts to execute the same binary are denied for segvguard/suspension seconds. Disable SegvGuard by setting segvguard/expiry:0.
The trigger signals are SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGIOT, SIGKILL, SIGQUIT, SIGSEGV, SIGSYS, SIGTRAP, SIGXCPU, and SIGXFSZ (see signal(7)). SIGKILL is intentionally included even though it does not produce a core(5) dump, so that kill sandbox rules trigger SegvGuard.
SegvGuard depends on ptrace(2) and can be disabled by setting trace/allow_unsafe_ptrace:1. Refer to syd(2) for the full list of segvguard/ options and syd(7) for further reading. Crashes have consequences now, periodt ๐.
๐ SECURITY HARDENINGS ๐
โจ Memory-Deny-Write-Execute โจ
okay so first up, Syd enforces W^X (Write XOR Execute) memory protection by default using PR_SET_MDWE (see PR_SET_MDWE(2const)) and seccomp(2) filters on mmap(2), mmap2(2), mprotect(2), pkey_mprotect(2), and shmat(2). Memory mappings that are simultaneously writable and executable get absolutely rejected by the kernel-level seccomp(2) filter, which terminates the offending process with SIGSYS (see signal(7)). like, it does not play ๐
Syd also validates file descriptor writability during executable memory mapping to prevent a W^X bypass where writable file descriptors could modify executable code after mapping. no sneaky loopholes bestie ๐ฆ
Wanna relax this restriction? use trace/allow_unsafe_exec_memory:1 at startup. Even with this option, Syd still calls PR_SET_MDWE but sets PR_MDWE_NO_INHERIT to prevent propagation to child processes on fork(2). still locked down, just vibes-ier
The standalone tool syd-mdwe(1) applies MDWE protection to a single command without the full Syd sandbox. cute little one-job icon โจ
๐ธ Memory and PID sandboxing ๐ธ
Memory sandboxing limits per-process memory consumption by checking allocations on brk(2), mmap(2), mmap2(2), and mremap(2):
mem/maxโ Maximum physical memory per process. The default action is deny, returnENOMEM("Out of memory"); change it withdefault/mem, e.g.default/mem:killto terminate withSIGKILL.mem/vm_maxโ Maximum virtual memory per process.
Memory use is estimated from /proc/pid/smaps_rollup summing Pss, Private_Dirty, and Shared_Dirty. math is mathing ๐งฎ
PID sandboxing limits the number of tasks by checking fork(2), vfork(2), clone(2), and clone3(2):
pid/maxโ maximum concurrent tasks. The default action is kill (terminate withSIGKILL); change it withdefault/pid.
Best coupled with unshare/pid:1 so the count applies per PID namespace. Both memory and PID sandboxing are not alternatives to cgroups(7); use cgroups(7) when available. fr fr, use the real tool when u have it ๐ซถ
Refer to syd(2) manual page for mem/ and pid/ option documentation.
๐ฆ Namespace Isolation ๐ฆ
Syd isolates sandboxed processes using Linux namespaces(7). Enable namespaces with unshare/ commands:
unshare/user:1โuser_namespace(7).unshare/mount:1โmount_namespaces(7).unshare/pid:1โpid_namespaces(7).unshare/net:1โnetwork_namespaces(7).unshare/uts:1โuts_namespaces(7)(hostname).unshare/ipc:1โipc_namespaces(7)unshare/cgroup:1โcgroup_namespaces(7)unshare/time:1โtime_namespaces(7)(resets boot clock).
The bind command creates bind mounts inside the mount namespace. The format is bind+source:target:options where options is a comma-separated list of ro, noexec, nosuid, nodev, nosymfollow, noatime, nodiratime, and relatime. If the source is not an absolute path, it is interpreted as a filesystem type:
# Read-only bind mount of / onto itself
bind+/:/:ro
# Private tmpfs on /tmp
bind+tmpfs:/tmp:noexec,size=16M
# Cgroup filesystem
bind+cgroup2:/sys/fs/cgroup:nodev,noexec,nosuid
# Overlay mount
bind+overlay:/mnt:lowerdir=/lower,upperdir=/upper,workdir=/work
the overlay one is giving main character energy ngl ๐
The root command changes the root mount at startup using pivot_root(2). Use root:tmpfs (or root:ramfs) to build an empty mount namespace from a private temporary filesystem mounted with nodev, noexec, nosuid, nosymfollow, noatime, and mode=700. Destination paths of bind commands are interpreted relative to the root directory. clean slate, immaculate vibes โจ
Private proc(5) is mounted with hidepid=4 and subset=pid for process hiding. peek-a-boo but make it a no ๐
Namespace creation by sandboxed processes is denied by default to prevent path sandboxing bypass. Use trace/allow_unsafe_namespace to selectively allow specific namespace types. Similarly, mount(2) and umount2(2) are denied unless a mount namespace is active.
Refer to syd(7) manual page for the full namespace isolation documentation and syd(2) manual page for the bind and root command reference.
๐ SafeSetID ๐
SafeSetID controls UID and GID transitions. To allow a specific transition, e.g. root to nobody:
setuid+0:65534
setgid+0:65534
All setuid and setgid system calls with target UID <= 11 (typically the operator user) or GID <= 14 (typically the uucp group) are denied by a kernel-level seccomp(2) filter, even if Syd itself is compromised. After the first successful transition, Syd drops CAP_SETUID / CAP_SETGID so only one transition is permitted per Syd lifetime. Subsequent transitions in the sandbox process continue to the UID/GID that Syd transitioned to, supporting daemons like nginx(1) that spawn unprivileged workers. one (1) glow-up per lifetime, periodt ๐
Refer to syd(2) manual page for the full setuid and setgid command documentation.
โญ PTY Sandboxing โญ
PTY Sandboxing runs the target process inside a dedicated pseudoterminal managed by syd-pty(1), isolating terminal I/O from the host TTY. I/O is proxied via an edge-triggered epoll(7) loop with zero-copy splice(2). A seccomp(2) filter allows only safe PTY ioctls (e.g. TIOCGWINSZ, TIOCSWINSZ) and denies dangerous ones such as TIOCSTI (terminal input injection). landlock(7) further restricts filesystem and network access for the PTY helper. zero-copy and zero trust, iconic combo ๐ฅ
PTY Sandboxing is enabled by default (sandbox/pty:on) but only activates when both standard input and standard output are terminals (see isatty(3)). In non-interactive contexts such as pipes or cron(8) jobs, PTY sandboxing is silently skipped. Syd is a multicall binary: it re-executes itself via proc_pid_exe(5) with argv[0] set to syd-pty to spawn the helper process. Disable PTY Sandboxing with sandbox/pty:off. multicall binary is such a flex ๐ฆ
๐ ADVANCED TOPICS ๐
๐ Proxy Sandboxing ๐
Proxy Sandboxing routes all network traffic through a designated SOCKS proxy. Enable it with sandbox/proxy:on, which implies unshare/net:1 to isolate the network namespace. Syd re-executes itself via proc_pid_exe(5) with argv[0] set to syd-tor to spawn the proxy helper. everything goes through the proxy, no side quests โจ
sandbox/proxy:on
proxy/port:9050
proxy/ext/host:127.0.0.1
proxy/ext/port:9050
As of version 3.34.1, an external UNIX domain socket may be used instead: proxy/ext/unix:/path/socks5.sock. Traffic is proxied using zero-copy transfers and edge-triggered epoll(7). ๐ซถ
๐ /dev/syd API ๐
Sandboxed processes communicate with Syd at runtime through virtual paths under /dev/syd/. The stat(2) system call on these paths delivers sandbox commands, while open(2) and read(2) retrieve sandbox state:
/* Set a sandbox command at runtime */
struct stat buf;
stat("/dev/syd/allow/read+/tmp/***", &buf);
/* Lock the sandbox */
stat("/dev/syd/lock:on", &buf);
/* Query the sandbox configuration (read-only) */
int fd = open("/dev/syd", O_RDONLY);
The interface is only available when the sandbox lock permits it. Refer to syd(2) manual page for the complete virtual path reference.
The sandbox lock modes (on, off, exec, ipc, read, drop) are described in the PATH SANDBOXING section above. Refer to syd(2) manual page for the full lock command documentation. iykyk ๐
๐ Ghost Mode ๐
Ghost mode is an irreversible transition to near-seccomp strict mode. A sandboxed process enters Ghost mode by calling stat(2) on /dev/syd/ghost. Syd then closes the seccomp_unotify(2) file descriptor, elevating all previously hooked system calls to a kernel-level deny with ENOSYS ("Function not implemented"). The monitor and emulator threads exit, and the main thread simply waits for the sandbox process to terminate. like, it's gone. it's a ghost. it's not coming back ๐ป
Ghost mode cannot be entered once the sandbox lock is set to on or read, but it works with lock:drop. As an alternative, setting the process dumpable attribute to zero via PR_SET_DUMPABLE(2const) achieves a similar effect because Syd can no longer access the per-process proc(5) directory. two roads, same ghostly destination ๐ฆ
๐ง CONTAINER INTEGRATION ๐ง
๐ธ syd-oci ๐ธ
syd-oci(1) is an OCI container runtime built on top of youki(1). It integrates Syd's sandbox into standard container workflows and is compatible with docker(1) and podman(1). Build Syd with the oci Cargo feature to obtain syd-oci. containers but make them sandboxed โจ
To use with docker(1), add the runtime to /etc/docker/daemon.json:
{
"runtimes": { "syd-oci": { "path": "/bin/syd-oci" } },
"default-runtime": "syd-oci"
}
Then run containers with docker run --runtime=syd-oci alpine. For podman(1), pass --runtime=/bin/syd-oci. one flag and ur done, slay ๐ซถ
๐ OCI Configuration ๐
syd-oci(1) searches for sandbox configuration in the following order, using the first file it finds:
${hostname}.${domainname}.syd-3${domainname}.syd-3${hostname}.syd-3default.syd-3- The built-in
ociprofile.
The configuration directory is /etc/syd/oci for system-wide containers, or ${XDG_CONFIG_HOME}/syd/oci for rootless containers. Set SYD_OCI_NO_CONFIG to skip file lookup and fall through to the built-in oci profile. first match wins, no debates ๐
The include directives in these files are resolved within the container image. This allows storing Force sandboxing checksums of executables and their dynamic libraries inside the image itself for binary verification at runtime. checksums in the image is such a serve ngl ๐ฅ
Use syd-cat -p oci to view the built-in OCI profile. The profile is designed to be combined with pandora and learning mode. ๐
๐ LEARNING MODE ๐
๐ง Trace Mode
ok so Syd's -x flag is literally the training-wheels era, bestie โ it turns on trace mode (aka dry run) by applying the built-in trace profile. And what does that profile even do? Let me tell you: it turns off the sandbox lock, turns on Force and ioctl sandboxing, and sets the default action for all sandbox categories to warn. Translation? Syscalls that would normally get denied are actually allowed โ but Syd logs a detailed JSON warning for every single violation, like a little snitch journal. ๐ซถ
Wanna see every rule in the trace profile with your own eyes? Run:
syd-cat -p trace
and behold. โจ
๐ pandora
pandora(1) is Syd's log inspector and profile writer โ she's giving dual-threat main character energy. She's got two subcommands:
pandora profile runs a command under Syd's trace mode, reads the violation log through an internal pipe, and writes out a sandbox profile. Like this:
$ pandora profile -o app.syd-3 -- ./my-application
The -s flag passes options to Syd during init, and โ important โ it may be repeated. Each -s value gets forwarded to Syd as a single dash-prefixed argument. This maps to Syd's -m (inline config), -p (profile), and -P (config file) flags:
$ pandora profile -s mtrace/allow_unsafe_exec_memory:1
-o app.syd-3 -- ./my-application
$ pandora profile -s P./base.syd-3
-o app.syd-3 -- ./my-application
$ pandora profile -s puser
-o app.syd-3 -- ./my-application
pandora inspect is the other one โ she reads an existing Syd log and produces a profile from it. You pick the input source with -i: a file path, - for standard input, or syslog to read from Syd's syslog(2) ring buffer via dmesg(1):
$ pandora inspect -i violations.log -o app.syd-3
$ pandora inspect -i syslog -o app.syd-3
The generated profile is a valid syd(5) configuration file. Load it with syd -P ./app.syd-3 -- ./my-application. And if new violations show up under the generated profile? Just repeat the profiling step to refine it. Iteration is self-care. ๐
Project homepage? That's https://lib.rs/pandora_box โ go say hi. ๐ธ
๐ LOGGING ๐
โญ Log Levels
Syd has eight log levels and I need you to know all eight by heart: emerg, alert, crit, error, warn, notice, info, and debug. You set the level with SYD_LOG or the log/level command. Logs go to standard error by default โ but if you want them somewhere else, set SYD_LOG_FD to redirect to another file descriptor (and yes, negative values disable logging entirely).
Now here's the cool part. Syd maintains its own syslog(2) ring buffer where all log messages are stored in kernel format (<LEVEL>[boottime] message). Access to /dev/kmsg and /proc/kmsg is denied with EPERM ("Operation not permitted"), so dmesg(1) falls back to the syslog(2) system call โ which Syd intercepts via seccomp(2) notify and serves straight from its ring buffer. ๐ญ iconic. This is what lets tools like pandora read Syd's access violation logs from inside the sandbox using standard dmesg(1). Turn this emulation on at startup with:
trace/allow_safe_syslog:1
The default ring buffer is stack-allocated with an architecture-dependent size that mirrors Linux CONFIG_LOG_BUF_SHIFT โ that's 256K on x86_64, 16K on aarch64, and 8K on arm. Want a bigger one? Set SYD_LOG_BUF_LEN to a human-readable size (like 64K or 1M) and Syd allocates a larger heap-based ring buffer instead. Upgrade era. ๐ฅ
๐ฆ JSON Output
Syd logs in JSON lines, so every entry is one tidy little line. Key fields in access violation entries:
idโ Sandbox ID (128 hex characters).sidโ Sandbox name (human-readable).ctxโ Context:access,safesetid,segvguard, etc.capโ Sandbox capability (e.g.read,write,exec).actโ Sandbox action:allow,warn,deny,kill, etc.sysโ System call name.pidโ Process ID.pathโ Path argument of the system call.addrโ Network address (e.g.127.0.0.1!22).cmdโ Process command line.cwdโ Current working directory.uidโ User ID.timeโ ISO 8601 timestamp (YYYYMMDDThhmmssZ).tipโ Suggested sandbox command to allow the access.
That tip field is honestly the bestie of the group โ it tells you what to do next. ๐
๐ Exit Codes
Syd exits with the same code as the sandbox process. If the sandbox process gets killed by a signal, Syd exits with 128 plus the signal number. If Syd itself hits an error, it exits with the corresponding errno(3) value. And a sandbox timeout gives you exit code 124. Memorize it, iykyk. ๐
๐ Export Seccomp Filters
Want to peek at the actual filters? Use syd -Epfc to print Syd's seccomp(2) filters in human-readable Pseudo Filter Code (PFC). Or use syd -Ebpf for the raw Berkeley Packet Filter format. Both, no cap, are extremely satisfying to look at. ๐
๐ซถ UTILITIES ๐ซถ
Syd ships with a whole suite of utilities โ each one has its own manual page, so this is basically the friend group roster. They're grouped by function below:
- Sandboxing:
syd-lock(1)(run underlandlock(7)),syd-mdwe(1)(Memory-Deny-Write-Execute),syd-tsc(1)(deny timestamp counter),syd-pds(1)(parent death signal),syd-ofd(1)(OFD file locking),syd-pause(1)(block until signaled). - Integrity:
syd-sum(1)(file checksums),syd-path(1)(Force sandboxing rules forPATHbinaries). - Inspection:
syd-ls(1)(list capabilities, syscalls, ioctls),syd-stat(1)(process information in JSON),syd-elf(1)(ELF file details),syd-ldd(1)(secure shared object dependencies),syd-cap(1)(Linux capabilities),syd-fd(1)(remote file descriptors),syd-mem(1)(process memory usage),syd-x(1)(check executability),syd-cat(1)(parsesyd(5)configuration). - System:
syd-sys(1)(lookup syscalls, errnos, ioctls),syd-uts(1)(kernel information),syd-info(1)(system information),syd-utc(1)(UTC time),syd-fs(1)(filesystem type),syd-net(1)(aggregate IP networks),syd-sec(1)(secure bits),syd-tty(1)(controlling terminal),syd-aux(1)(auxiliary vector). - Data:
syd-hex(1)(hex encode/decode),syd-read(1)(resolve symlinks),syd-size(1)(parse human-formatted sizes). - Execution:
syd-exec(1)(construct sandbox exec commands),syd-run(1)(run inside a container),syd-emacs(1)(run Emacs under Syd). - Container:
syd-oci(1)(OCI container runtime),syd-pty(1)(PTY forwarder),syd-tor(1)(SOCKS proxy forwarder). - Testing:
syd-test(1)(integration tests),syd-poc(1)(sandbox break demonstrations).
Like, the range? Immaculate. Every niche got a utility. ๐
๐ธ SEE ALSO ๐ธ
syd(1),syd(2),syd(5),syd(7)- Syd homepage: https://sydbox.exherbo.org
๐ AUTHORS ๐
Maintained by Ali Polatel. Up-to-date sources can be found at https://gitlab.exherbo.org/sydbox/sydbox.git and on Radicle at rad:z38HCnbmcDegA2BMxuPaPRPMdp6wF. Bugs/patches can be submitted to https://gitlab.exherbo.org/groups/sydbox/-/issues. Come discuss in #sydbox on Libera Chat or in #sydbox:mailstation.de on Matrix. ๐
And that's a wrap on SYDTUTORIAL(7) โ yes, you made it all the way to the end, I'm so proud of you bestie ๐ฅน Go forth and sandbox responsibly. This little beauty comes from the SYDTUTORIAL(7) source, dated 2026-09-01. Screenshot the date, it's basically vintage now. โจ๐๐ฆ
๐ฆ where this page comes from
- arch rolling: syd 3.59.0-1 ยท GPL-3.0-only
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms