Filesystem rules
Filesystem rules are the bread and butter of Landlock. You declare which filesystem operations the sandbox should handle when creating the ruleset, then grant exemptions for specific directory trees with AddPathBeneathRule.
For the kernel-side semantics — exactly what counts as READ_FILE vs READ_DIR, why removing a directory needs a separate flag, etc. — see the "Filesystem flags" section of landlock(7).
Declaring handled access
CreateRuleset takes a list of Landlock.FileSystem values. Anything you list is denied by default; anything you omit is left untouched by Landlock.
// Option 1 — the safe default: handle every right available on the kernel
var sandbox = Landlock.CreateRuleset(Landlock.FileSystem.CORE);
// Option 2 — handle a specific subset
var sandbox = Landlock.CreateRuleset(
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.WRITE_FILE,
Landlock.FileSystem.EXECUTE);
FileSystem.CORE expands to every filesystem right defined in the current kernel's ABI except IOCTL_DEV. Add IOCTL_DEV explicitly if you want it.
Why the `CORE` shorthand exists
Listing every flag manually is error-prone, and forgetting one (e.g. MAKE_SYM) leaves a small hole in your sandbox. CORE defaults to "everything reasonable" and grows automatically as kernels add new rights — see ABI versions.
Allowing a directory tree
AddPathBeneathRule(parentPath, ...allowedActions) re-grants specific rights inside parentPath and everything beneath it. The path must be a directory — the kernel opens it with O_PATH to capture a stable reference, even if the path is later moved or unmounted.
sandbox.AddPathBeneathRule(
"/var/lib/myapp/data",
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.READ_DIR);
The allowed actions must be a subset of the rights that the ruleset handles. You cannot grant WRITE_FILE here unless WRITE_FILE (or CORE) was passed to CreateRuleset.
Read-only, write-only, execute-only
The flags are independent — combine them to express the access pattern you want.
// Read-only data
sandbox.AddPathBeneathRule(
"/usr/share/myapp",
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.READ_DIR);
// Read-write scratch directory
sandbox.AddPathBeneathRule(
"/var/cache/myapp",
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.READ_DIR,
Landlock.FileSystem.WRITE_FILE,
Landlock.FileSystem.MAKE_REG,
Landlock.FileSystem.MAKE_DIR,
Landlock.FileSystem.REMOVE_FILE,
Landlock.FileSystem.REMOVE_DIR,
Landlock.FileSystem.TRUNCATE);
// Executable directory — let the app exec plugins from here
sandbox.AddPathBeneathRule(
"/opt/myapp/plugins",
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.EXECUTE);
For the per-flag semantics — e.g. that MAKE_REG is needed to create regular files, MAKE_DIR to create directories, TRUNCATE to truncate (not the same as WRITE_FILE!) — see landlock(7).
Chaining rules
AddPathBeneathRule returns this, so calls fluently chain:
Landlock.CreateRuleset(Landlock.FileSystem.CORE)
.AddPathBeneathRule("/usr", Landlock.FileSystem.READ_FILE, Landlock.FileSystem.READ_DIR, Landlock.FileSystem.EXECUTE)
.AddPathBeneathRule("/etc", Landlock.FileSystem.READ_FILE, Landlock.FileSystem.READ_DIR)
.AddPathBeneathRule("/var/lib/myapp", Landlock.FileSystem.READ_FILE, Landlock.FileSystem.READ_DIR, Landlock.FileSystem.WRITE_FILE)
.AddPathBeneathRule("/tmp", Landlock.FileSystem.READ_FILE, Landlock.FileSystem.WRITE_FILE, Landlock.FileSystem.MAKE_REG)
.Enforce();
Overlapping paths
If two rules name the same path, or one is nested inside another, the effective rights are the union of the rights granted by every applicable rule. Landlock walks the path's ancestor chain and accumulates.
sandbox.AddPathBeneathRule("/var/lib/myapp", Landlock.FileSystem.READ_FILE);
sandbox.AddPathBeneathRule("/var/lib/myapp/cache",
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.WRITE_FILE);
// /var/lib/myapp -> read
// /var/lib/myapp/cache -> read + write (union of both rules)
There is no "deny" rule. Once a right has been granted at a parent level, you cannot revoke it at a child path. Express that case by tightening the ruleset instead — e.g. only handle WRITE_FILE and only grant write in the directories that should be writable.
Path resolution: when the rule binds
The kernel opens the path with O_PATH at the time AddPathBeneathRule is called and stores the resulting file reference — not the path string. That means:
- If
/var/lib/myapp/datais a symlink at the time of the call, the symlink target is captured. - If the directory is later renamed, the rule continues to apply to whatever inode it pointed at.
- If the directory is later removed and recreated, the new directory is not covered.
This matches the kernel's behaviour as described in landlock_add_rule(2).
Paths must exist at rule time
AddPathBeneathRule throws Win32Exception("open O_PATH(\"…\") failed") if the directory doesn't exist (errno = ENOENT). Create the directory before adding the rule.
A complete example
A typical web service: allow reads from system libraries, reads of config, read-write on its data directory, write-only logs.
using Sandbox;
public static class AppSandbox
{
public static void Apply(string dataDir, string logDir)
{
if (!Landlock.IsSupported()) return;
Landlock.CreateRuleset(Landlock.FileSystem.CORE)
// System libs and binaries — read + execute
.AddPathBeneathRule("/usr",
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.READ_DIR,
Landlock.FileSystem.EXECUTE)
.AddPathBeneathRule("/lib",
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.READ_DIR,
Landlock.FileSystem.EXECUTE)
.AddPathBeneathRule("/lib64",
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.READ_DIR,
Landlock.FileSystem.EXECUTE)
// Config — read only
.AddPathBeneathRule("/etc",
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.READ_DIR)
// Data — full read/write
.AddPathBeneathRule(dataDir,
Landlock.FileSystem.READ_FILE,
Landlock.FileSystem.READ_DIR,
Landlock.FileSystem.WRITE_FILE,
Landlock.FileSystem.MAKE_REG,
Landlock.FileSystem.MAKE_DIR,
Landlock.FileSystem.REMOVE_FILE,
Landlock.FileSystem.REMOVE_DIR,
Landlock.FileSystem.TRUNCATE)
// Logs — append-only-ish (no read, no delete)
.AddPathBeneathRule(logDir,
Landlock.FileSystem.WRITE_FILE,
Landlock.FileSystem.MAKE_REG)
.Enforce();
}
}
Anything outside these trees — /proc, /sys, /root, /home/..., the user's SSH keys — is blocked.