Landlock Ruleset Enforcement

🛡️ This describes how to enable a Landlock sandbox in plain C. If you are using another programming language, you might want to look at the collection of Landlock libraries under SoftwareUsingLandlock.

Overview

A Landlock policy is described and enforced using a ruleset. The ruleset is an in-kernel structure which can be manipulated by a userspace process through a dedicated file descriptor.

To create and enforce a Landlock ruleset, the following system calls are needed:

Additionally, the following two steps are likely needed as well:

  Unrestricted process Create ruleset FD and define restrictions landlock_create_ruleset(2) (Optional) Add exceptions landlock_add_rule(2) Enforce ruleset landlock_restrict_self(2)   Landlock policy is enforced Set NO_NEW_PRIVS flag prctl(2) struct landlock_ruleset_attr struct landlock_path_beneath_attr struct landlock_net_port_attr   informs (Optional) Probe Landlock ABI version landlock_create_ruleset(2)

Enforcement steps

In more detail, the enforcement steps are as follows:

Probe for the available Landlock ABI version (optional)

This step is necessary if you do not know exactly which kernel your program will be running on. We can probe for Landlock’s

To probe for the Landlock ABI version using landlock_create_ruleset(2)’s LANDLOCK_CREATE_RULESET_VERSION flag.

int abi = syscall(SYS_landlock_create_ruleset, NULL, 0,
                  LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 0) {
  switch (errno) {
  case ENOSYS:     /* Landlock unsupported */
  case EOPNOTSUPP: /* Landlock disabled */
    return 0;
  }
  return -1;
}

if (abi > ARRAY_SIZE(landlock_fs_access_rights)) {
  /*
   * The kernel supports features that we don't know yet:
   * Treat it as the highest known Landlock version.
   */
  abi = ARRAY_SIZE(landlock_fs_access_rights);
}

We will use the determined ABI version later to fill the ruleset attributes.

Enable “no new privileges” mode (optional)

This process flag is a prerequisite for enforcing a Landlock ruleset. It might happen at any time during your program execution, but must be done before Landlock is enabled.

“No new privileges” mode keeps a process from gaining new privileges through execve(2) (i.e. if an executable file has the setuid flag). It is described in prctl(2) and the kernel documentation.

if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
  return -1;
}

Create the ruleset

We then create a Landlock ruleset with landlock_create_ruleset(2).

In many cases, we want to restrict all operations that are restrictable, so we use a compatibility table for the file system access rights which are supported by each Landlock ABI version (LandlockAbiVersioning), using the abi version variable which we determined in an earlier step. This fallback mode is also known as the LandlockBestEffortMode.

struct landlock_ruleset_attr ruleset_attr = {
    .handled_access_fs  = landlock_fs_access_rights[abi-1],
    .handled_access_net = landlock_net_access_rights[abi-1],
};
int ruleset_fd =
    syscall(SYS_landlock_create_ruleset, &ruleset_attr, sizeof(ruleset_attr), 0U);
if (ruleset_fd < 0) {
  return -1;
}

Populate exceptions in the ruleset (optional)

Now, we can (optionally) excempt desired operations to be permitted, even when the ruleset denies them by default. This is done by adding a rule.

Currently, the supported rule types are:

To add a rule, populate the respective struct landlock_*_attr and add it with the matching enum:

if (syscall(SYS_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &attr, 0) < 0) {
  res = -1;
  goto out;  /* In case of error, close the ruleset FD before returning. */
}

Important constraints:

Enforce the ruleset

We finally enforce the ruleset, using landlock_restrict_self(2).

In the case of an error, we take care to close the ruleset file descriptor before returning.

if (syscall(SYS_landlock_restrict_self, ruleset_fd, 0) < 0) {
  res = -1;
  goto out;
}

out:
close(ruleset_fd);
return res;