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:
- landlock_create_ruleset(2) to create the ruleset file descriptor and to define the actions that should be forbidden under the ruleset
- landlock_add_rule(2) to add exceptions to the ruleset, such as:
- files and directories that should still be accessible (LandlockFileSystemControl)
- TCP ports that should still be bindable and connectable (LandlockNetworkPortControl)
- (unstable feature) Socket types that should still be creatable (LandlockSocketTypeControl)
- landlock_restrict_self(2) to enforce the ruleset.
Additionally, the following two steps are likely needed as well:
- Setting the processes’ “No new privs” flag with prctl(2). It is required for the final restriction step.
- Probing the available Landlock ABI version with landlock_create_ruleset(2) (LandlockBestEffortMode).
Enforcement steps
In more detail, the enforcement steps are as follows:
- Probe the Landlock ABI
- Enable “no privileges” mode
- Create the ruleset
- Add rules to the ruleset
- Enforce the ruleset
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.
- On
ENOSYS
orEOPNOTSUPP
, Landlock can not be used and we fall back to doing nothing. - On other errors, we return the error. These should not happen.
- If a Landlock ABI version is returned, we truncate it to the highest ABI version we know about. This way, the same compiled program can run on future kernel versions as well.
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).
- On success, this system call returns a file descriptor which represents the ruleset.
- The
struct landlock_ruleset_attr
defines the access rights that will be forbidden by default under the ruleset.
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:
LANDLOCK_RULE_PATH_BENEATH
(struct landlock_path_beneath_attr
) - See LandlockFileSystemControlLANDLOCK_RULE_NET_PORT
(struct landlock_rule_net_port
) - See LandlockNetworkPortRule- (unstable:)
LANDLOCK_ACCESS_SOCKET_CREATE
(struct landlock_socket_attr
) - See LandlockSocketTypeControl
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:
- The access rights passed in
attr
must always be a subset of the corresponding access rights which we used earlier in theruleset_attr
. - The
LANDLOCK_ACCESS_FS_REFER
right can only be granted by passing it in a rule, not by omitting it in theruleset_attr
. See LandlockBestEffortMode for details. - Access rights for directory manipulation can only be granted for directories, not for other files. For example, if you try to allow
LANDLOCK_ACCESS_FS_READ_DIR
for a regular file, it’ll yieldEINVAL
. See Landlock’s documentation for file system flags for the full list.
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;