Home
Why Sandlock
How It Works Use Cases Comparison Security Model
Docs
Documentation Home Getting Started CLI Reference Python SDK Sandbox Reference
Products
Overview Sandbox HTTP API Sandbox Scheduler
GitHub Schedule a Demo
Sandlock / Docs /Kernel Protections

Kernel Protections

Sandlock refuses to start when a protection it expects is unavailable, which is the right default and the wrong one for a fleet with mixed kernels. Two builder methods relax it, one protection at a time, and never silently.

Landlock did not arrive complete. Each release added access rights, and each is gated behind an ABI version the host kernel either provides or does not. Sandlock names six of them, and by default enforces all six.

If the host is too old for one, the sandbox fails to build with an error naming the protection and the kernel's actual ABI. That is deliberate: the alternative is a sandbox that quietly confines less than the policy says, which is the failure mode you learn about from an incident rather than from an error.

The six protections

Protection CLI name ABI Kernel What it stops
FsRefer fs-refer v2 5.19 Linking or renaming a file across directories
FsTruncate fs-truncate v3 6.2 Truncating a file through truncate, ftruncate, creat, or O_TRUNC
NetTcp net-tcp v4 6.7 TCP bind and connect outside the port allowlist
FsIoctlDev fs-ioctl-dev v5 6.10 ioctl on character and block devices
SignalScope signal-scope v6 6.12 Signalling a process outside the sandbox
AbstractUnixSocketScope abstract-unix-socket-scope v6 6.12 Connecting to an abstract UNIX socket outside the sandbox

This is why the site says Linux 6.12: it is the first kernel that provides all six. Everything below that needs at least one waiver.

Check the host first

sandlock check reports the running kernel's Landlock ABI and, for each protection, whether this host can provide it. Run it before you deploy rather than after a sandbox refuses to build.

shell
$ sandlock check

Three states

Every protection sits in one of three states. Unnamed protections are Strict, so a policy that says nothing about protections behaves exactly as it did before this API existed.

State On a kernel that supports it On a kernel that does not
Strict
the default
Enforced The sandbox fails to build, naming the protection and the host's ABI
Degradable
allow_degraded
Enforced Skipped, and reported as degraded
Disabled
disable
Not enforced Not enforced

The two waivers answer different questions. allow_degraded is about the fleet: enforce this wherever you can, and do not fail on the hosts that cannot. disable is about the workload: this program legitimately needs the capability the protection blocks, so do not enforce it anywhere.

Reaching for disable when you meant allow_degraded gives up the protection on every host, including the modern ones. It is the more expensive mistake of the two.

Setting them

Both waivers are last-wins per protection: a later call for the same protection supersedes the earlier one, so the two lists cannot contradict each other.

rust
use sandlock_core::{Protection, Sandbox};

// Build on kernels below 6.12, where the v6 scopes do not exist.
// On a kernel that has them, they are still enforced.
let sb = Sandbox::builder()
    .fs_read("/usr").fs_read("/lib")
    .fs_write("/tmp")
    .allow_degraded(Protection::SignalScope)
    .allow_degraded(Protection::AbstractUnixSocketScope)
    .build()?;
python
from sandlock import Sandbox, Protection

sandbox = Sandbox(
    fs_readable=["/usr", "/lib"],
    fs_writable=["/tmp"],
    allow_degraded=[
        Protection.SIGNAL_SCOPE,
        Protection.ABSTRACT_UNIX_SOCKET_SCOPE,
    ],
)
go
sb := &sandlock.Sandbox{
	FSReadable: []string{"/usr", "/lib"},
	FSWritable: []string{"/tmp"},
	AllowDegraded: []sandlock.Protection{
		sandlock.ProtectionSignalScope,
		sandlock.ProtectionAbstractUnixSocketScope,
	},
}
shell
# Both flags are repeatable.
$ sandlock run \
    --allow-degraded signal-scope \
    --allow-degraded abstract-unix-socket-scope \
    -r /usr -r /lib -w /tmp -- ./task.sh

# A workload that genuinely must signal a process outside the sandbox.
$ sandlock run --disable signal-scope \
    -r /usr -r /lib -w /tmp -- ./supervisor.sh

Reading the result back

A waiver is never silent. Sandbox::active_protections() returns each protection's resolved status, so a deployment can assert its posture rather than assume it.

Status Means
ActiveEnforced. The policy asked for it and the host provides it.
DegradedWaived by allow_degraded and skipped, because this host is too old.
DisabledWaived by disable, regardless of the host.
UnavailableStrict and unavailable. This is the state that failed the build.

The distinction between Degraded and Disabled is worth surfacing in your own monitoring. The first tells you a host is behind and will start enforcing again when it is upgraded. The second tells you a decision was made, and will keep applying after every upgrade until someone revisits it.

FsRefer cannot be disabled

disable(Protection::FsRefer) is rejected at build time with an Invalid error, and the reason is counterintuitive enough to be worth stating.

Landlock denies cross-directory rename and link by default in every ruleset, even when REFER is not handled. Controlled renaming within writable areas works precisely because Sandlock handles REFER and grants it on writable paths, which is what the strict and degradable states do. Un-handling it cannot loosen anything; it only removes the mechanism that makes rename work at all.

So disable here would do the opposite of what the name promises. Sandlock refuses rather than accept a call whose effect contradicts its meaning. If you wanted REFER enforced only where the kernel supports it, that is allow_degraded.

Waivers travel with a checkpoint

The resolved posture is part of a sandbox's checkpoint. A restored sandbox comes back with the protections it was built with, not the ones the new host happens to offer.

That matters for a scheduler that moves sandboxes between machines: restoring onto a newer kernel does not quietly start enforcing something the workload was written against, and restoring onto an older one does not quietly stop enforcing something the policy required.

Choosing what to waive

Every waiver is confinement you no longer have, so the useful question is what each one costs.

Waiving What the workload regains
SignalScopeThe ability to signal processes outside the sandbox, including ones belonging to other users' sessions on the same host.
AbstractUnixSocketScopeThe ability to connect to abstract UNIX sockets outside the sandbox, which is how a good deal of desktop and system IPC is reached.
FsIoctlDevDevice ioctl on any device node the filesystem rules already expose.
NetTcpKernel-level TCP port enforcement. Destination checks on the on-behalf path still apply, so this is narrower than it sounds, but the cheap path is gone.
FsTruncateTruncating files it can otherwise open, which turns a read grant into a way to destroy content.

A waiver is a threat-model change, not a compatibility flag. The two v6 scopes are the usual ones to degrade, because 6.12 is recent and they are the difference between running on it and not. The rest are worth an explicit decision: if you find yourself degrading FsTruncate to support a 6.0 host, the honest summary is that this fleet does not yet have the kernel Sandlock is built for.