One of Landlock's strengths is that you can deploy the same program on
multiple kernel versions, and make it use the best available
sandboxing on each.

This "best effort" approach is already implemented for you [in the
Go-Landlock
library](https://pkg.go.dev/github.com/landlock-lsm/go-landlock/landlock#Config.BestEffort)
and [in the Rust Landlock
library](https://landlock.io/rust-landlock/landlock/#compatibility).
But what if you need to implement it yourself?

> **Note:**
> This article assumes previous knowledge of Landlock - you can read more about the abstract model in the [Landlock File System Access Model](https://docs.google.com/document/d/1SkFpl_Xxyl4E6G2uYIlzL0gY2PFo-Nl8ikblLvnpvlU/edit#) document which I've written previously, or in the [official Landlock documentation](https://docs.kernel.org/userspace-api/landlock.html).
{.info}

## ABI versions and matching access rights

<!-- TODO: Link the man page here when it's part of the man page -->
Landlock exposes its feature set in the form of a numeric Landlock ABI
version.

In order to implement the fallback correctly, you need to know which
file system access rights are supported in which ABI version.

The (simplified) compatibility chart as of Linux 6.2 is:

ABI   | Linux   | File system access rights
------|---------|---------------------------
1     | 5.13    | almost all of the basic file operations (compare [kernel docs](https://docs.kernel.org/userspace-api/landlock.html#filesystem-flags))
2     | 5.19    | +`LANDLOCK_ACCESS_FS_REFER` (reparenting files)
3     | 6.2     | +`LANDLOCK_ACCESS_FS_TRUNCATE` (truncating files)

Further below, we will define this support matrix in C.

## The problem: Refer is different

**The backwards compatibility works differently for "refer" than for
other access rights**.

<div class="sidenote">Skip this section if you are only interested in the C code.</div>

Each Landlock ABI version is characterized by three disjoint sets of
file system operations: The *Always Forbidden* operations, the
*Configurable* operations and the *Always Permitted* operations.

The *Configurable* operations are the ones which already have names in
the `landlock.h` header.  With increasing ABI versions, all relevant
operations should become "Configurable", in particular the "Always
permitted" operations.

In ABI v1, an set of useful file system operations is configurable in
Landlock, but there is also a longer list of known operations which is
always permitted (Landlock can not restrict them). Finally, some more
complicated interactions are always forbidden because they would
interfere with Landlock's guarantees:

![](/images/ll-permissions.svg)

When introducing ABI v2, the "refer" operation, which was previously
forbidden, became configurable:

![](/images/ll-permissions-v2.svg)

With ABI v2, it is now possible to reparent (link(2) or rename(2))
files between different directories, as long as the involved files and
directories meet certain constraints about their access rights.

The behavior of "refer" is different to "truncate" and other future access rights, which fall back to *Always Permitted* in earlier ABI versions, and so "refer" needs a different fallback logic.

> If a program needs to do a "refer" (reparent files between directories),
> using ABI v1 is not an option.
{.rule}

<!-- todo: mention man page -->
The canonical documentation for this interaction is the [kernel
documentation](https://docs.kernel.org/userspace-api/landlock.html#filesystem-flags).

## Implementation of best-effort fallback in C

To implement the fallback logic, we will (a) define the compatibility
table, and then, (b) depending on whether your program needs to do
file reparenting, implement a slightly different fallback logic based
on that compatibility table.

<div class="sidenote">This question decides which approach to use.</div>

The question to ask is:

> Does your program need to do any file reparenting ("refer")?

File reparenting means: Creating hard links or moving files or
directories, between *different*(!) directories (compare [the kernel
documentation](https://docs.kernel.org/userspace-api/landlock.html#filesystem-flags)).

### Common part: ABI version compatibility table

We define a small C array to hold the file system access rights which
are supported by the different Landlock ABI versions.  We store one
`__u64` bitmask for each ABI version.

```
__u64 landlock_fs_access_rights[] = {
  (1ULL << 13) - 1,  /* ABI v1                 */
  (1ULL << 14) - 1,  /* ABI v2: add "refer"    */
  (1ULL << 15) - 1,  /* ABI v3: add "truncate" */
};
```

I'm keeping it short here for brevity, but you can also use the constants from [linux/landlock.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/landlock.h?h=v6.2).

### Case 1: the sandboxed program does *not* need to reparent files

<div class="sidenote">Use this approach, if you don't use link(2) and rename(2) across directory boundaries.</div>

If the sandboxed program does *not* need to reparent files, the
best-effort logic is to simply remove the unsupported rights from `ruleset_attr.handled_access_fs`.

This code needs to be inserted *after* you have filled a `struct
landlock_ruleset_attr` variable, but before you create the ruleset
from it:

```
int abi = landlock_create_ruleset(NULL, 0,
                                  LANDLOCK_CREATE_RULESET_VERSION);
if (abi <= 0) {
  /*
   * This kernel does not let us use Landlock.
   * Best effort: Give up, and do not restrict the process.
   */
  return 0;
}
if (abi > ARRAY_SIZE(landlock_fs_access_rights)) {
  /* Kernel from the future: Treat as highest known ABI version. */
  abi = ARRAY_SIZE(landlock_fs_access_rights);
}
ruleset_attr.handled_access_fs &= landlock_fs_access_rights[abi-1];
```

<div class="sidenote">Don't forget this!</div>

Additionally, before you add a "path beneath" Landlock rule to the
ruleset, make sure that the requested access rights fit into the
previously restricted `handled_access_fs` rights:

```
path_beneath.allowed_access &= ruleset_attr.handled_access_fs;
```

### Case 2: the sandboxed program needs to reparent files

If the sandboxed program *does* need to reparent files, the
best-effort logic works the same as above, *but it has a special case
for ABI version 1*.  In this ABI version, file reparenting does not
work under Landlock at all, and so we have to give up on it.

This code needs to be inserted *after* you have filled a `struct
landlock_ruleset_attr` variable, but before you create the ruleset
from it:

```
int abi = landlock_create_ruleset(NULL, 0,
                                  LANDLOCK_CREATE_RULESET_VERSION);
switch (abi) {
  case -1:
  case 0:  /* should not happen */
    /*
     * This kernel does not let us use Landlock.
     * Best effort: Give up, and do not restrict the process.
     */
    return 0;
  case 1:
    /*
     * This kernel only supports Landlock ABI v1.
     * We need the "refer" right, but it's not supported in ABI v1.
     * Best effort: Give up, and do not restrict the process.
     */
    return 0;
  default:
    if (abi > ARRAY_SIZE(landlock_fs_access_rights)) {
      /* 
       * A kernel with Landlock suppport from the future!
       * Treat it as the highest known ABI version.
       */
      abi = ARRAY_SIZE(landlock_fs_access_rights);
    }
    ruleset_attr.handled_access_fs &= landlock_fs_access_rights[abi-1];
}
```

Additionally, before you add a "path beneath" Landlock rule to the
ruleset, make sure that the requested access rights fit into the
previously restricted `handled_access_fs` rights:

```
path_beneath.allowed_access &= ruleset_attr.handled_access_fs;
```

### Case 3: Sometimes this, sometimes that

This is the complicated case which we also implemented in the Go and
Rust Landlock libraries.  These libraries figure out at runtime which
of the two cases we are in, and then use the adequate approach as
above.

## Conclusion

In the long run, when Landlock has stabilized more, and as older
kernels become less relevant, this complication will hopefully go
away.  Until then, I'm hopeful that most users can simply use the
simple approach in Case 1 above, because their programs do not need
the "refer" right.

## References

* I've previously written about this more in the abstract at: [Landlock File System Access
Model](https://docs.google.com/document/d/1SkFpl_Xxyl4E6G2uYIlzL0gY2PFo-Nl8ikblLvnpvlU/edit#).
* https://landlock.io/ - Official Landlock page
* [Landlock kernel documentation](https://docs.kernel.org/userspace-api/landlock.html)

Thank you Mickaël, for pointing out some issues in this article!
