Landlock-Sharp

What is Landlock?

Landlock is a Linux Security Module (LSM) that lets unprivileged processes restrict themselves. A process declares the access rights it wants to keep, names the resources it should still be able to touch, and asks the kernel to enforce that contract on itself. Once enforced, the restriction is permanent for that process and all of its descendants.

This page is a tour of Landlock's mental model from the C# binding's perspective. For the canonical reference, see the Landlock kernel documentation and the landlock(7) man page.


Key properties

  • Unprivileged. No root, no capabilities, no setuid binary. Any process can sandbox itself. See the kernel doc's "Sandbox" section for the security model.
  • Allow-list. Once a right is "handled" by a ruleset, every access requiring that right is denied unless an explicit rule re-grants it.
  • Layered. Calling Enforce() again later only narrows the sandbox. There is no API to widen it.
  • Inheritable. Child threads and child processes inherit every active Landlock domain. No setrlimit-style decay.
  • No-op when irrelevant. If a process never touches the filesystem (or never opens a TCP socket), enforcing filesystem (or network) rules has zero runtime cost.
  • Versioned. New access rights are added in new kernel releases. The binding negotiates the ABI version at runtime — see ABI versions.

Three syscalls, three steps

Landlock is intentionally tiny. The full kernel surface is three system calls, used in exactly this order:

sequenceDiagram participant App as Your code participant K as Kernel App->>K: landlock_create_ruleset(handled_access) K-->>App: ruleset fd App->>K: landlock_add_rule(fd, PATH_BENEATH, "/data") App->>K: landlock_add_rule(fd, NET_PORT, 443) App->>K: landlock_restrict_self(fd) K-->>App: current thread is now sandboxed Note over App,K: All further syscalls are filtered<br/>by the active Landlock domain.

The Landlock-Sharp C# API mirrors this directly:

Kernel syscall C# method
landlock_create_ruleset Landlock.CreateRuleset(...)
landlock_add_rule(..., PATH_BENEATH, ...) sandbox.AddPathBeneathRule(...)
landlock_add_rule(..., NET_PORT, ...) sandbox.AddPortRule(...)
landlock_restrict_self sandbox.Enforce(...)

For the syscalls themselves, see landlock_create_ruleset(2), landlock_add_rule(2), and landlock_restrict_self(2).


Handled vs allowed

The trickiest bit of Landlock for newcomers: the ruleset declares which rights it handles, the rules grant exemptions.

// Ruleset HANDLES read+write — both are now denied by default.
var sandbox = Landlock.CreateRuleset(
    Landlock.FileSystem.READ_FILE,
    Landlock.FileSystem.WRITE_FILE);

// Rule re-GRANTS read inside /etc — but write is still denied everywhere.
sandbox.AddPathBeneathRule("/etc", Landlock.FileSystem.READ_FILE);

sandbox.Enforce();

File.ReadAllText("/etc/hostname");      // OK
File.WriteAllText("/etc/hostname", ""); // EACCES
File.ReadAllText("/proc/version");      // EACCES (outside /etc)
Process.Start("/bin/ls");               // OK — EXECUTE was never handled

A right not in the ruleset is invisible to Landlock. A right in the ruleset that doesn't appear in any rule is blocked everywhere.

For the formal definition of "handled access rights" vs "allowed access rights", see the landlock(7) man page — Access rights section.


What Landlock can restrict

The binding exposes three categories, mirroring the kernel's:

  • Landlock.FileSystem — read, write, execute, create, delete, rename, truncate, and ioctl on the filesystem. Rule type: AddPathBeneathRule(path, ...).
  • Landlock.NetworkBIND_TCP and CONNECT_TCP on specific port numbers (kernel ≥ 6.7). Rule type: AddPortRule(port, ...).
  • Landlock.Scope — IPC isolation: block inbound connections via abstract Unix sockets and inbound signals from outside the sandbox (kernel ≥ 6.12). No per-resource rules — scopes are all-or-nothing.

For the per-right semantics see the "Access rights" and "Network flags" sections of landlock(7).


What Landlock can not restrict

Landlock is deliberately narrow. It does not sandbox:

  • Arbitrary syscalls (use seccomp for that).
  • UDP, raw sockets, or non-TCP traffic (TCP only as of kernel 6.7).
  • mount / umount / pivot_root / chroot escapes (use user namespaces or kernel capabilities).
  • Resource limits — memory, CPU, file descriptors (use cgroups / setrlimit).
  • Reading /proc/self/* for the calling process.

The intent is to layer Landlock with other Linux security primitives. The landlock.io tutorial and the kernel docs discuss the boundaries in more detail.


What the C# binding adds on top

The library is intentionally close to the syscalls. Beyond the marshalling and the fluent style, the only logic worth knowing is:

  • IsSupported() — three-way runtime check (Linux + x86-64 + ABI ≥ 1) so callers can branch safely on any platform.
  • GetAbiVersion() — the result of landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION). Negative means unsupported.
  • Automatic ABI filtering. When you ask for, say, Landlock.FileSystem.IOCTL_DEV on a kernel that only supports ABI 4, the binding silently drops that flag instead of erroring out — your code keeps working on older kernels. See ABI versions for the details and how to opt out.
  • Enforced-once guard. Trying to add a rule after Enforce() throws — matching the kernel's behaviour but with a clearer exception message.

Next: ABI versions

Now that you know what Landlock does, jump into ABI versions to learn how feature gating works, or to API overview to see the full surface of the C# binding.

© 2026 Landlock-Sharp. All rights reserved.