Landlock-Sharp

Testing sandboxed code

Two things make Landlock-Sharp tests different from a typical unit test:

  1. The sandbox is irrevocable. Once a thread is sandboxed, it stays sandboxed for the rest of its life. Tests that enforce a sandbox must run on a thread they can throw away.
  2. The platform matters. Landlock only works on Linux x86-64 with kernel ≥ 5.13. Tests must be skipped or no-op'd on every other host.

This page covers patterns for both. The library's own test suite follows them and is a good reference.


Pattern 1 — guard on IsSupported()

The cheapest way to make a test portable is to short-circuit when Landlock isn't there:

[TestMethod]
public void CreatesRulesetSuccessfully()
{
    if (!Landlock.IsSupported())
    {
        Assert.Inconclusive("Landlock not supported on this host");
        return;
    }

    var sandbox = Landlock.CreateRuleset(Landlock.FileSystem.CORE);
    Assert.IsNotNull(sandbox);
}

Assert.Inconclusive (MSTest) or [SkipOnPlatform] (xUnit) keeps the suite green on Windows, macOS, ARM Linux, and old kernels.


Pattern 2 — enforce on a throw-away thread

Enforcing Landlock on the main test-runner thread would poison every test that runs after it. The fix is to enforce inside a thread you start and join just for that test:

[TestMethod]
public void EnforceBlocksOutsidePaths()
{
    if (!Landlock.IsSupported())
    {
        Assert.Inconclusive("Landlock not supported");
        return;
    }

    bool blocked = false;

    var t = new Thread(() =>
    {
        Landlock.CreateRuleset(Landlock.FileSystem.CORE)
            .AddPathBeneathRule("/tmp", Landlock.FileSystem.READ_FILE)
            .Enforce();

        try   { File.ReadAllText("/etc/hostname"); blocked = false; }
        catch { blocked = true; }
    });

    t.Start();
    t.Join();

    Assert.IsTrue(blocked);
}

When t exits, the kernel cleans up its Landlock domain. The rest of the test run is unaffected. This is exactly the approach used in LandlockTests.LandlockEnforcesFileRestrictions.

Don't use `Task.Run`

The task scheduler picks threads from the .NET thread pool. Sandboxing a pooled thread leaks the restriction to whatever Task runs on that thread next. Use new Thread(...) so you own the thread's lifetime.


Pattern 3 — assert that something should be blocked

Most useful Landlock tests have the shape "this access used to work, sandboxing makes it fail." Run the test twice — once with the sandbox, once without — and assert the symmetric outcomes:

private static bool CanRead(string path)
{
    try   { File.ReadAllText(path); return true; }
    catch { return false; }
}

[TestMethod]
public void SandboxBlocksUnlistedPaths()
{
    if (!Landlock.IsSupported()) { Assert.Inconclusive("…"); return; }

    bool inside  = false;
    bool outside = false;

    var sandboxed = new Thread(() =>
    {
        Landlock.CreateRuleset(Landlock.FileSystem.CORE)
            .AddPathBeneathRule("/tmp", Landlock.FileSystem.READ_FILE, Landlock.FileSystem.READ_DIR)
            .Enforce();

        inside  = CanRead("/tmp/hello.txt");
        outside = CanRead("/etc/hostname");
    });

    File.WriteAllText("/tmp/hello.txt", "hi");

    sandboxed.Start();
    sandboxed.Join();

    Assert.IsTrue(inside,   "Read inside the allow-list should succeed");
    Assert.IsFalse(outside, "Read outside the allow-list should be blocked");
}

Pattern 4 — ABI gating

Some tests only make sense above a particular kernel version. Combine Landlock.GetAbiVersion() with Assert.Inconclusive:

[TestMethod]
public void TcpConnectIsBlocked()
{
    if (!Landlock.IsSupported() || Landlock.GetAbiVersion() < 4)
    {
        Assert.Inconclusive("Requires Landlock ABI ≥ 4 (kernel 6.7+)");
        return;
    }

    // … test that uses Landlock.Network.* …
}

The full ABI → kernel table is on the ABI versions page.


Running the suite in CI

Two CI-side things make Landlock tests reliable:

  • Pin the runner image. Newer GitHub Actions / Azure Pipelines / GitLab CI Ubuntu images get newer kernels — and therefore higher Landlock ABIs — over time. Pin to a specific image label if you want stable feature coverage. Lookup tables for runner-image kernel versions live in each provider's docs.
  • Run inside privileged containers. A few container runtimes (e.g. Docker with seccomp profiles that block prctl(PR_SET_NO_NEW_PRIVS)) interfere with Landlock. If IsSupported() returns false inside the container but true on the host, check the container's seccomp profile.

The Landlock-Sharp repo runs its own tests on Ubuntu in Azure Pipelines — see .devops/azure-pipelines.yml in the repo for the current setup.


Cross-reference

© 2026 Landlock-Sharp. All rights reserved.