SYDTUTORIAL(7)

# NAME

*sydtutorial* - A tutorial introduction to Syd

# SYNOPSIS

*syd* [-acefhlmpqtxEPV] [--] {command [arg...]}

# DESCRIPTION

Syd intercepts system calls made by Linux processes and decides,
according to a set of rules, whether each call should proceed, be
denied, or be emulated. It does this without kernel modules, without
setuid binaries, and without eBPF, using only _seccomp_(2) user
notification, _ptrace_(2), _landlock_(7), and _namespaces_(7).

Run Syd with no arguments and it drops you 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 must opt in to every operation the
sandboxed process is allowed to perform.

This tutorial walks through Syd's sandbox rules, starting from the
simplest case ("allow everything and run") through incrementally tighter
configurations. It is 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.

The examples are tested against Syd 3.51.0 on Linux 6.19. You can type
them verbatim on your own system.

# 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) and _pidfd_send_signal_(2), needed for
  file descriptor operations and signal delivery via process file
  descriptors. _openat2_(2) is also required for safe path resolution
  with *RESOLVE_BENEATH*, *RESOLVE_NO_SYMLINKS*, and
  *RESOLVE_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.
- Linux 6.2 (optional): *ALG_SET_KEY_BY_KEY_SERIAL*, needed only for
  Crypt sandboxing; not required for general use.

The following kernel configuration options must be enabled:

- *CONFIG_SECCOMP* and *CONFIG_SECCOMP_FILTER* are required for system
  call interception via _seccomp_(2).
- *CONFIG_SECURITY_LANDLOCK* is required for _landlock_(7) filesystem
  and network access control. This option must be set to _y_ at kernel
  build time, and _landlock_ must appear in the boot-time *CONFIG_LSM*
  list (or be appended via the _lsm=_ 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_DIAG* is required for UNIX domain socket diagnostics,
  which Syd uses to identify peer processes on UNIX sockets.
- *CONFIG_CROSS_MEMORY_ATTACH* is recommended; enables
  _process_vm_readv_(2) and _process_vm_writev_(2) for reading and writing
  process memory. Unlike _proc_pid_mem_(5), cross memory attach honours
  the address space permissions of the target process, providing a safer
  mechanism for inspecting system call arguments. If
  *CONFIG_CROSS_MEMORY_ATTACH* is not available, Syd falls back to
  _proc_pid_mem_(5) automatically when the *SYD_PROC_PID_MEM_FALLBACK*
  environment variable is set, refer to _syd_(1) manual page for
  details.
- *CONFIG_KCMP* is recommended; enables _kcmp_(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*.

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, the set of vDSO calls available, 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.

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 a reduced attack surface.

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.

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.

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.

## 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 serve as building blocks for others. For example, the
_linux_ profile provides a common set of rules for Linux systems and is
included by the _user_, _paludis_, and _oci_ profiles.

The _user_ profile is the default for the login shell and is suitable
for general interactive use. The _immutable_ profile treats the entire
root filesystem as read-only, permitting writes only to
explicitly allowed locations. The _off_ profile disables all
sandboxing.

To examine the rules that a profile contains:

```
$ syd-cat -p user
```

Multiple profiles can be combined on the command line; later profiles
override rules from earlier ones:

```
$ syd -pimmutable -mallow/write+/var/cache/\*\*\* make install
```

## -m Flag

The _-m_ flag passes individual sandbox commands on the command line.
Each _-m_ takes one command as documented in _syd_(2). All _-p_, _-P_,
and _-m_ flags are processed in the order they are given on the command
line. Because Syd uses a "last match wins" rule resolution strategy
(documented in _syd_(2)), later flags override earlier ones for the same
sandbox category regardless of type.

A basic example enables Write Sandboxing atop the _off_ profile:

```
$ syd -poff -msandbox/write:on -mallow/write+/tmp/\*\*\* touch /tmp/hello
$ echo $?
0
```

Here _-poff_ disables all sandboxing, _-msandbox/write:on_ re-enables
Write Sandboxing, and _-mallow/write+/tmp/\*\*\*_ adds _/tmp_ and
everything below it to the write allowlist. Because _/tmp/hello_
matches the allow rule, _touch_(1) succeeds.

Multiple _-m_ flags for the same category layer in order. You can first
allow a broad directory tree and then deny a subtree within 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, including
the system call that was 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. Reversing the order would
produce the opposite result, the allow would override the deny.

The _-m_ rules layer atop the selected profile. Without _-poff_ or
another profile that allows execution, the default sandbox denies
_execve_(2) before any write rule has a chance to take effect:

```
$ syd -msandbox/write:on touch /tmp/hello
syd: exec error: Permission denied
```

This is a common mistake when first using Syd. Always start from a
profile that permits execution, then layer restrictions with _-m_. The
_off_ profile followed by selective sandbox enables is one approach; the
_user_ profile with additional deny rules is another.

# CONFIGURATION

The previous section introduced profiles through the _-p_ flag and
individual commands through _-m_. This section covers the full
configuration machinery: what profiles contain, how configuration files
work, how rules are resolved, and how patterns match paths.

## Profile anatomy

A profile is a named set of sandbox commands compiled into the Syd
binary. To inspect its contents, pass 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
```

The _off_ profile consists of exactly two commands: one that turns off
all primary sandbox categories, and one that turns off every secondary
category. Compare this with the _user_ profile:

```
$ syd-cat -puser
# Syd profile: User "user"
# Number of rules: 18
include_profile linux
include_profile landlock
include_profile local
include_profile nomagic
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_
append+${SYD_HOME}/.*history
```

Several features are visible here. The _include_profile_ directive
includes other profiles by name: _linux_, _landlock_, _local_,
_nomagic_, _rand_, and _tty_ are all pulled in, making the _user_
profile a composition of lower-level building blocks. Environment
variables such as *${SYD_HOME}* and *${SYD_GID}* are expanded at parse
time; Syd sets these automatically before loading the profile. Refer to
the *ENVIRONMENT* section of _syd_(5) manual page. The notation
*${SYD_USER_LPATH:-on}* provides a default value: if the variable is
unset, the value _on_ is used.

## Profile Stacking

Multiple _-p_ flags can appear on the command line. Profiles are loaded
in order, and because Syd uses a last-match-wins strategy, later
profiles override rules from earlier ones. This allows incremental
refinement:

```
$ syd -puser -pimmutable ls /
ls: cannot open directory '/': Permission denied
$ echo $?
2
```

The _user_ profile permits reading most of the filesystem, but the
_immutable_ profile, loaded second, remounts system directories
read-only inside a _mount_namespaces_(7) and applies stricter access
rules that override the _user_ defaults.

Some profiles have one-character shortcuts. These shortcuts can be
combined into a single _-p_ argument:

```
$ syd -puiq ...
```

This stacks the _user_ (u), _immutable_ (i), and _quiet_ (q) profiles.
The full list of profiles and their shortcuts is documented in the
*PROFILES* section of _syd_(5), and can always be queried with _syd-cat -plist_.

## Configuration Files

Configuration files provide the same commands as _-m_ flags, one per
line. Comments begin with _#_; blank lines are ignored.  The file
extension must be _.syd-3_, reflecting the current API version.

A minimal configuration 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
```

Multiple _-P_ flags can be specified. All _-p_, _-P_, and _-m_
arguments are processed strictly in the order they appear on the command
line--there is no precedence between them. A _-m_ that appears before a
_-P_ takes effect first.

The _syd-cat_(1) utility can parse and validate configuration files
independently of Syd itself. Pass one or more file paths and it will
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 contains errors, _syd-cat_(1) exits with a non-zero status
and an error message, making it useful for testing configuration before
deploying it.

Configuration files support two inclusion directives:

- _include_ _path_ 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 are detected by caching device
  and inode numbers.
- _include_profile_ _name_ includes a built-in profile by name,
  exactly as if _-p_ had 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. This prevents accidental over-permissive
rules. Use *${VAR:-default}* to supply fallback values.

## Pattern Matching

Path rules use _glob_(3p) patterns. The 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/foo_ but not _/tmp/foo/bar_.
- _/tmp/\*\*_ matches files in immediate subdirectories of _/tmp_.
- _/tmp/\*\*\*_ matches _/tmp_ itself, _/tmp/foo_, _/tmp/foo/bar_, and
  so on to arbitrary depth.

## Rule Ordering

Syd evaluates rules in the order they appear. For multiple rules that
match 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.

This means you can start with a broad allow and carve out exceptions
with later deny rules, or start restrictive and add targeted allows.

## Runtime Configuration

In addition to startup-time configuration, Syd supports runtime
reconfiguration through magic _stat_(2) calls. A sandboxed process can
issue:

```
test -c /dev/syd/sandbox/read:on
```

This _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_. 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
OpenBSD _pledge_(2). When the lock is _on_ or _read_, runtime changes
are not accepted.

## 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.  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.

To lock the system-wide configuration so that per-user files cannot
weaken it, place _lock:on_ or _lock:drop_ at the end of
_/etc/user.syd-3_. After the lock is set, no further configuration
changes are accepted, neither from subsequent files nor from runtime
_stat_(2) calls.

# SANDBOX LOCK

The sandbox lock controls whether and how sandbox rules can be modified
after Syd starts executing the sandboxed process. It is set with the
_lock_ command (documented in _syd_(2)) and 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_ seals the sandbox policy entirely. No runtime configuration
is accepted from any source:

```
$ syd -poff -mlock:on sh -c 'test -c /dev/syd/sandbox/write:on && echo "enabled" || echo "locked out"'
locked out
```

_lock:off_ leaves the sandbox fully open to runtime changes. Any
process in the sandbox can issue _stat_(2) commands on _/dev/syd/_ paths
to modify the policy.

_lock:exec_ sets the lock to _on_ for all processes except the initial
exec child. This 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.

_lock:ipc_ 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_ permits commands that further restrict the sandbox but
rejects commands that would loosen it. This is 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_ makes the _syd_(2) virtual system call API available in
read-only mode. The sandboxed process can query the current policy
state through the _open_(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.

## 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) with *O_RDONLY* or *O_RDWR*, _getxattr_(2)
  and related extended attribute reads.
- _write_ -- _open_(2) with *O_WRONLY* or *O_RDWR*.
- _exec_ -- _execve_(2), _execveat_(2), _mmap_(2) with *PROT_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 from
  _stat_ to prevent unhiding of hidden paths.

Syd also provides fine-grained categories for specific operations:

- _create_ -- _creat_(2), _open_(2) with *O_CREAT*, _memfd_create_(2).
- _delete_ -- _unlink_(2), _unlinkat_(2) without *AT_REMOVEDIR*.
- _rename_ -- _rename_(2), _renameat_(2), _link_(2), _linkat_(2).
- _symlink_ -- _symlink_(2), _symlinkat_(2).
- _truncate_ -- _truncate_(2), _fallocate_(2), _open_(2) with *O_TRUNC*.
- _chdir_ -- _chdir_(2), _fchdir_(2).
- _readdir_ -- _open_(2) on existing directories.
- _mkdir_ -- _mkdir_(2), _mkdirat_(2).
- _rmdir_ -- _rmdir_(2), _unlinkat_(2) with *AT_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) with *O_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 the _pledge_(2) _rpath_
  promise. In _pledge_(2), _rpath_ also covers _stat_(2),
  _access_(2), _readlinkat_(2), and _chdir_(2); Syd separates
  those into the _lpath_ set.
- _wpath_ -- write, truncate. Named after the _pledge_(2) _wpath_
  promise.
- _cpath_ -- create, delete, rename. Named after the _pledge_(2)
  _cpath_ promise, which also covers _mkdir_(2) and _rmdir_(2);
  Syd separates those into the _tpath_ set.
- _fattr_ -- chmod, chattr, utime. Named after the _pledge_(2)
  _fattr_ promise. In _pledge_(2), _fattr_ also covers
  _chown_(2) and _fchown_(2); Syd separates ownership changes
  into the _fown_ set.
- _fown_ -- chown, chgrp. A Syd-specific set that splits
  ownership changes out of the _pledge_(2) _fattr_ promise.
- _dpath_ -- mkbdev, mkcdev. Named after the _pledge_(2) _dpath_
  promise, which covers _mknod_(2) and _mkfifo_(2). Syd narrows
  this set to block and character device creation only; FIFOs are
  in the separate _spath_ set.
- _spath_ -- mkfifo, symlink. A Syd-specific set; _pledge_(2)
  places _mkfifo_(2) under _dpath_ and _symlink_(2) under _cpath_.
- _tpath_ -- mkdir, rmdir. A Syd-specific set that splits
  directory creation and removal out of the _pledge_(2) _cpath_
  promise.
- _lpath_ -- walk, stat, chdir, notify. A Syd-specific set with
  no _pledge_(2) analogue. It corresponds to the path visibility
  controls of _unveil_(2) and groups the categories responsible
  for path lookup, metadata access, and directory change.
- _net_ -- net/bind, net/connect, net/sendfd.
- _inet_ -- net/bind, net/connect. Named after the _pledge_(2)
  _inet_ promise.
- _all_ -- every category.
- _all-x_ -- every category except _exec_.

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 with _EACCES_ (default).
- _filter_ -- Deny the system call silently, without logging.
- _warn_ -- Allow the system call but log a warning (learning
  mode, used by _pandora_(1)).
- _kill_ -- Deny the system call and terminate the offending
  process with *SIGKILL* (see _signal_(7)).
- _stop_ -- Deny the system call and send *SIGSTOP* to the
  offending process (see _signal_(7)).
- _abort_ -- Deny the system call and send *SIGABRT* to the
  offending process (see _signal_(7)). Unlike *SIGKILL*,
  *SIGABRT* can be caught, so this action should only be used
  for debugging in trusted environments where a _core_(5) dump
  file is useful.
- _panic_ -- Deny the system call and panic the Syd emulator
  thread. Currently equivalent to _deny_.
- _exit_ -- Log a warning and exit Syd immediately with the deny
  _errno_(3) as exit value. All sandbox processes are terminated:
  direct children receive *SIGKILL* via the parent death signal
  (see _PR_SET_PDEATHSIG_(2const)), traced processes are killed
  via *PTRACE_O_EXITKILL* (see _ptrace_(2)), and closing the
  _seccomp_(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

Where hiding makes a path invisible, masking replaces its contents. The
_mask_ command redirects _open_(2) calls on matching paths to a
different file--by default _/dev/null_. At the _stat_(2) boundary, a
masked path returns the metadata of the mask target, not the original
file.

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_.

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.

Masked paths are also protected against filesystem writes in the same
manner as append-only paths. 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.

## Append-only Paths

The _append_ command marks paths as append-only, providing protection
equivalent to the Linux inode append-only attribute (set with
_chattr_(1) _+a_). Unlike a simple *O_APPEND* flag, the protection is
comprehensive:

- _open_(2) with a writable access mode is denied with *EPERM*
  ("Operation not permitted") unless *O_APPEND* is set. _open_(2) with
  *O_TRUNC* is denied regardless of the access mode.
- Metadata and namespace operations are denied with *EPERM* ("Operation
  not permitted"): _chmod_(2), _chown_(2), _rename_(2), _link_(2),
  _unlink_(2), _truncate_(2), _setxattr_(2), _removexattr_(2), _utime_(2),
  and their variants.
- Clearing *O_APPEND* via _fcntl_(2) *F_SETFL* is denied with *EPERM*
  ("Operation not permitted").
- Writable shared memory mappings via _mmap_(2) with *MAP_SHARED* and
*PROT_WRITE* are denied with *EPERM* ("Operation not permitted").
- _pwritev2_(2) with the *RWF_NOAPPEND* flag is denied with *EOPNOTSUPP*
  ("Operation not supported") via a _seccomp_(2) filter.

The result is that an append-only path can only grow by appending data;
it cannot be truncated, overwritten, renamed, deleted, or have its
metadata changed. This is useful for log files and shell history. The
_user_ profile uses this to protect shell history files:

```
append+${SYD_HOME}/.*history
```

The glob pattern matches _~/.bash_history_, _~/.zsh_history_,
_~/.python_history_, and similar files.

## 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_, protects shell history as append-only, 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
append+${SYD_HOME}/.*history
```

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.

# 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.

## 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.

## 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}
```

## 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.

# NETWORK SANDBOXING

Syd confines network operations through three categories:

- _net/bind_ -- _bind_(2), UNIX domain socket creation via
  _mknod_(2), and _socketpair_(2).
- _net/connect_ -- _connect_(2), _sendto_(2), _sendmsg_(2), and
  _sendmmsg_(2).
- _net/sendfd_ -- Sending file descriptors via _sendmsg_(2) and
  _sendmmsg_(2) with *SCM_RIGHTS*.

Supported socket families are UNIX, IPv4, IPv6, Netlink, and KCAPI. The
option _trace/allow_unsupp_socket:1_ passes through sockets of
unsupported types.

## 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_.

## 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.

## 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.

## 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.

# 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.

## 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 via
  _dlopen_(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.

## 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 any _ahash_
or _shash_ listed in _proc_crypto_(5), 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) with *PROT_EXEC*
  (typically _dlopen_(3)).

Helper tools:

- _syd-sum_(1) calculates checksums of files.
- _syd-path_(1) generates integrity force rules for all binaries
  under *PATH*.

Refer to _syd_(2) manual page for the full _force_ command
documentation.

## 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.

# SECURITY HARDENINGS

## Memory-Deny-Write-Execute

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 are rejected by the kernel-level _seccomp_(2) filter, which
terminates the offending process with *SIGSYS* (see _signal_(7)).

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.

To 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).

The standalone tool _syd-mdwe_(1) applies MDWE protection to a single
command without the full Syd sandbox.

## 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_, return *ENOMEM* ("Out of memory"); change it with
  _default/mem_, e.g. _default/mem:kill_ to terminate with *SIGKILL*.
- _mem/vm_max_ -- Maximum virtual memory per process.

Memory use is estimated from _/proc/pid/smaps_rollup_ summing _Pss_,
*Private_Dirty*, and *Shared_Dirty*.

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 with *SIGKILL*); change it with _default/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.

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 _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.

Private _proc_(5) is mounted with _hidepid=4_ and _subset=pid_ for
process hiding.

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 set*uid and set*gid 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.

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.

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_.

# ADVANCED TOPICS

## Crypt Sandboxing

Crypt Sandboxing provides transparent file encryption using AES-CTR with
HMAC-SHA256 authentication. When _sandbox/crypt:on_ is set, files
matching _glob_(3p) patterns specified by _crypt+_ are encrypted on
write and decrypted on read. Configuration:

```
sandbox/crypt:on
crypt/key:${SYD_KEY_ID}
crypt+${HOME}/Documents/***
```

Encryption keys are managed via _keyrings_(7). Use _syd-key_(1) to
generate keys and save them to a keyring. The environment variable
holding the key serial ID must start with _SYD\__ to avoid leaking into
the sandbox. Encryption uses zero-copy _splice_(2) and _tee_(2) to keep
plaintext out of user-space memory. The _syd_aes_ threads are confined
by a strict _seccomp_(2) filter that denies _read_(2), _open_(2), and
_socket_(2).

Each encrypted file has the format:

- _\\x7fSYD3_ -- 5-byte magic header.
- HMAC tag -- 32-byte SHA256 authentication tag.
- IV -- 16-byte random initialisation vector.
- Ciphertext -- AES-CTR encrypted content.

Decryption uses _memfd_create_(2) with *MFD_NOEXEC_SEAL* (Linux >= 6.3)
to prevent decrypted content from bypassing exec, force, or TPE
sandboxing. For large files, set _crypt/tmp_ to a secure backing
directory.

Utilities: _syd-aes_(1) for standalone encrypt/decrypt, _syd-key_(1) for
key generation. Refer to _syd_(7) manual page for the full Crypt
Sandboxing documentation.

## 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.

```
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.

## 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.

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.

# 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.

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_.

## 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-3_
. _default.syd-3_
. The built-in _oci_ profile.

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.

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.

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

Syd's _-x_ flag enables trace mode (dry run) by applying the built-in
_trace_ profile. This profile turns off the sandbox lock, enables Force
and ioctl sandboxing, and sets the default action for all sandbox
categories to _warn_: system calls that would normally be denied are
allowed, but Syd logs a detailed JSON warning for each violation. Use
_syd-cat -p trace_ to view the full list of rules in the trace profile.

## pandora

_pandora_(1) is Syd's log inspector and profile writer. It has two
subcommands:

_pandora profile_ executes a command under Syd's trace mode, reads the
violation log through an internal pipe, and writes a sandbox profile:

```
$ pandora profile -o app.syd-3 -- ./my-application
```

The _-s_ flag passes options to Syd during init and may be repeated.
Each _-s_ value is 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_ reads an existing Syd log and produces a profile. The
input source is set 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_. If new violations appear
under the generated profile, repeat the profiling step to refine.

See https://lib.rs/pandora_box for the project homepage.

# LOGGING

## Log Levels

Syd has eight log levels: _emerg_, _alert_, _crit_, _error_, _warn_,
_notice_, _info_, and _debug_. The level is set with *SYD_LOG* or the
_log/level_ command. Logs go to standard error by default; set
*SYD_LOG_FD* to redirect to another file descriptor (negative values
disable logging).

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 from its ring
buffer. This enables tools such as _pandora_ to read Syd's access
violation logs from inside the sandbox using standard _dmesg_(1). Enable
this emulation 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* (256K on x86_64, 16K on aarch64, 8K
on arm); set *SYD_LOG_BUF_LEN* to a human-readable size (e.g. _64K_,
_1M_) to allocate a larger heap-based ring buffer.

## JSON Output

Syd logs in JSON lines. 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.

## Exit Codes

Syd exits with the same code as the sandbox process. If the sandbox
process is killed by a signal, Syd exits with 128 plus the signal
number. If Syd itself encounters an error, it exits with the
corresponding _errno_(3) value. Sandbox timeout produces exit code 124.

## Export Seccomp Filters

Use _syd -Epfc_ to print Syd's _seccomp_(2) filters in human-readable
Pseudo Filter Code (PFC). Use _syd -Ebpf_ for raw Berkeley Packet
Filter format.

# UTILITIES

Syd ships with a suite of utilities. Each utility has its own manual
page. The utilities are grouped by function below.

- Sandboxing: _syd-lock_(1) (run under _landlock_(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).
- Crypto: _syd-aes_(1) (AES-CTR encrypt/decrypt),
  _syd-key_(1) (generate keys for _keyrings_(7)),
  _syd-sum_(1) (file checksums), _syd-path_(1) (Force
  sandboxing rules for PATH binaries).
- 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) (parse _syd_(5) configuration),
  _syd-env_(1) (run command with another process's environment).
- System: _syd-sys_(1) (lookup syscalls, errnos, ioctls),
  _syd-uts_(1) (kernel information), _syd-cpu_(1) (CPU count),
  _syd-info_(1) (system information), _syd-utc_(1) (UTC time),
  _syd-fs_(1) (filesystem type), _syd-dns_(1) (DNS resolver),
  _syd-net_(1) (aggregate IP networks), _syd-sec_(1) (secure
  bits), _syd-tty_(1) (controlling terminal), _syd-aux_(1)
  (auxiliary vector), _syd-rnd_(1) (AT_RANDOM bytes).
- 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-sh_(1) (confined
  shell), _syd-fork_(1) (fork stress test),
  _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-tck_(1)
  (measure runtime in hardware ticks), _syd-poc_(1) (sandbox
  break demonstrations), _syd-bit_(1) (bit flipping),
  _syd-asm_(1) (disassemble CPU instructions).

# 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 bugs/patches can be
submitted to https://gitlab.exherbo.org/groups/sydbox/-/issues. Discuss
in #sydbox on Libera Chat or in #sydbox:mailstation.de on Matrix.
