Landlock LSM: kernel documentation

Author:

Mickaël Salaün

Date:

December 2022

Landlock’s goal is to create scoped access-control (i.e. sandboxing). To harden a whole system, this feature should be available to any process, including unprivileged ones. Because such a process may be compromised or backdoored (i.e. untrusted), Landlock’s features must be safe to use from the kernel and other processes point of view. Landlock’s interface must therefore expose a minimal attack surface.

Landlock is designed to be usable by unprivileged processes while following the system security policy enforced by other access control mechanisms (e.g. DAC, LSM). A Landlock rule shall not interfere with other access-controls enforced on the system, only add more restrictions.

Any user can enforce Landlock rulesets on their processes. They are merged and evaluated against inherited rulesets in a way that ensures that only more constraints can be added.

User space documentation can be found here: Landlock: unprivileged access control.

Guiding principles for safe access controls

  • A Landlock rule shall be focused on access control on kernel objects instead of syscall filtering (i.e. syscall arguments), which is the purpose of seccomp-bpf.

  • To avoid multiple kinds of side-channel attacks (e.g. leak of security policies, CPU-based attacks), Landlock rules shall not be able to programmatically communicate with user space.

  • Kernel access check shall not slow down access request from unsandboxed processes.

  • Computation related to Landlock operations (e.g. enforcing a ruleset) shall only impact the processes requesting them.

  • Resources (e.g. file descriptors) directly obtained from the kernel by a sandboxed process shall retain their scoped accesses (at the time of resource acquisition) whatever process uses them. Cf. File descriptor access rights.

Design choices

Inode access rights

All access rights are tied to an inode and what can be accessed through it. Reading the content of a directory does not imply to be allowed to read the content of a listed inode. Indeed, a file name is local to its parent directory, and an inode can be referenced by multiple file names thanks to (hard) links. Being able to unlink a file only has a direct impact on the directory, not the unlinked inode. This is the reason why LANDLOCK_ACCESS_FS_REMOVE_FILE or LANDLOCK_ACCESS_FS_REFER are not allowed to be tied to files but only to directories.

File descriptor access rights

Access rights are checked and tied to file descriptors at open time. The underlying principle is that equivalent sequences of operations should lead to the same results, when they are executed under the same Landlock domain.

Taking the LANDLOCK_ACCESS_FS_TRUNCATE right as an example, it may be allowed to open a file for writing without being allowed to ftruncate the resulting file descriptor if the related file hierarchy doesn’t grant that access right. The following sequences of operations have the same semantic and should then have the same result:

  • truncate(path);

  • int fd = open(path, O_WRONLY); ftruncate(fd); close(fd);

Similarly to file access modes (e.g. O_RDWR), Landlock access rights attached to file descriptors are retained even if they are passed between processes (e.g. through a Unix domain socket). Such access rights will then be enforced even if the receiving process is not sandboxed by Landlock. Indeed, this is required to keep access controls consistent over the whole system, and this avoids unattended bypasses through file descriptor passing (i.e. confused deputy attack).

Tests

Userspace tests for backward compatibility, ptrace restrictions and filesystem support can be found here: tools/testing/selftests/landlock/.

Kernel structures

Object

struct landlock_object_underops

Operations on an underlying object

Definition:

struct landlock_object_underops {
    void (*release)(struct landlock_object *const object) __releases(object->lock);
};

Members

release

Releases the underlying object (e.g. iput() for an inode).

struct landlock_object

Security blob tied to a kernel object

Definition:

struct landlock_object {
    refcount_t usage;
    spinlock_t lock;
    void *underobj;
    union {
        struct rcu_head rcu_free;
        const struct landlock_object_underops *underops;
    };
};

Members

usage

This counter is used to tie an object to the rules matching it or to keep it alive while adding a new rule. If this counter reaches zero, this struct must not be modified, but this counter can still be read from within an RCU read-side critical section. When adding a new rule to an object with a usage counter of zero, we must wait until the pointer to this object is set to NULL (or recycled).

lock

Protects against concurrent modifications. This lock must be held from the time usage drops to zero until any weak references from underobj to this object have been cleaned up.

Lock ordering: inode->i_lock nests inside this.

underobj

Used when cleaning up an object and to mark an object as tied to its underlying kernel structure. This pointer is protected by lock. Cf. landlock_release_inodes() and release_inode().

{unnamed_union}

anonymous

rcu_free

Enables lockless use of usage, lock and underobj from within an RCU read-side critical section. rcu_free and underops are only used by landlock_put_object().

underops

Enables landlock_put_object() to release the underlying object (e.g. inode).

Description

The goal of this structure is to enable to tie a set of ephemeral access rights (pertaining to different domains) to a kernel object (e.g an inode) in a safe way. This implies to handle concurrent use and modification.

The lifetime of a struct landlock_object depends on the rules referring to it.

Filesystem

struct landlock_inode_security

Inode security blob

Definition:

struct landlock_inode_security {
    struct landlock_object __rcu *object;
};

Members

object

Weak pointer to an allocated object. All assignments of a new object are protected by the underlying inode->i_lock. However, atomically disassociating object from the inode is only protected by object->lock, from the time object’s usage refcount drops to zero to the time this pointer is nulled out (cf. release_inode() and hook_sb_delete()). Indeed, such disassociation doesn’t require inode->i_lock thanks to the careful rcu_access_pointer() check performed by get_inode_object().

Description

Enable to reference a struct landlock_object tied to an inode (i.e. underlying object).

struct landlock_file_security

File security blob

Definition:

struct landlock_file_security {
    access_mask_t allowed_access;
    struct landlock_ruleset *fown_domain;
};

Members

allowed_access

Access rights that were available at the time of opening the file. This is not necessarily the full set of access rights available at that time, but it’s the necessary subset as needed to authorize later operations on the open file.

fown_domain

Domain of the task that set the PID that may receive a signal e.g., SIGURG when writing MSG_OOB to the related socket. This pointer is protected by the related file->f_owner->lock, as for fown_struct’s members: pid, uid, and euid.

Description

This information is populated when opening a file in hook_file_open, and tracks the relevant Landlock access rights that were available at the time of opening the file. Other LSM hooks use these rights in order to authorize operations on already opened files.

struct landlock_superblock_security

Superblock security blob

Definition:

struct landlock_superblock_security {
    atomic_long_t inode_refs;
};

Members

inode_refs

Number of pending inodes (from this superblock) that are being released by release_inode(). Cf. struct super_block->s_fsnotify_inode_refs .

Description

Enable hook_sb_delete() to wait for concurrent calls to release_inode().

Ruleset and domain

A domain is a read-only ruleset tied to a set of subjects (i.e. tasks’ credentials). Each time a ruleset is enforced on a task, the current domain is duplicated and the ruleset is imported as a new layer of rules in the new domain. Indeed, once in a domain, each rule is tied to a layer level. To grant access to an object, at least one rule of each layer must allow the requested action on the object. A task can then only transit to a new domain that is the intersection of the constraints from the current domain and those of a ruleset provided by the task.

The definition of a subject is implicit for a task sandboxing itself, which makes the reasoning much easier and helps avoid pitfalls.

struct landlock_layer

Access rights for a given layer

Definition:

struct landlock_layer {
    u16 level;
    access_mask_t access;
};

Members

level

Position of this layer in the layer stack.

access

Bitfield of allowed actions on the kernel object. They are relative to the object type (e.g. LANDLOCK_ACTION_FS_READ).

union landlock_key

Key of a ruleset’s red-black tree

Definition:

union landlock_key {
    struct landlock_object *object;
    uintptr_t data;
};

Members

object

Pointer to identify a kernel object (e.g. an inode).

data

Raw data to identify an arbitrary 32-bit value (e.g. a TCP port).

enum landlock_key_type

Type of union landlock_key

Constants

LANDLOCK_KEY_INODE

Type of landlock_ruleset.root_inode’s node keys.

LANDLOCK_KEY_NET_PORT

Type of landlock_ruleset.root_net_port’s node keys.

struct landlock_id

Unique rule identifier for a ruleset

Definition:

struct landlock_id {
    union landlock_key key;
    const enum landlock_key_type type;
};

Members

key

Identifies either a kernel object (e.g. an inode) or a raw value (e.g. a TCP port).

type

Type of a landlock_ruleset’s root tree.

struct landlock_rule

Access rights tied to an object

Definition:

struct landlock_rule {
    struct rb_node node;
    union landlock_key key;
    u32 num_layers;
    struct landlock_layer layers[] ;
};

Members

node

Node in the ruleset’s red-black tree.

key

A union to identify either a kernel object (e.g. an inode) or a raw data value (e.g. a network socket port). This is used as a key for this ruleset element. The pointer is set once and never modified. It always points to an allocated object because each rule increments the refcount of its object.

num_layers

Number of entries in layers.

layers

Stack of layers, from the latest to the newest, implemented as a flexible array member (FAM).

struct landlock_hierarchy

Node in a ruleset hierarchy

Definition:

struct landlock_hierarchy {
    struct landlock_hierarchy *parent;
    refcount_t usage;
};

Members

parent

Pointer to the parent node, or NULL if it is a root Landlock domain.

usage

Number of potential children domains plus their parent domain.

struct landlock_ruleset

Landlock ruleset

Definition:

struct landlock_ruleset {
    struct rb_root root_inode;
#if IS_ENABLED(CONFIG_INET);
    struct rb_root root_net_port;
#endif ;
    struct landlock_hierarchy *hierarchy;
    union {
        struct work_struct work_free;
        struct {
            struct mutex lock;
            refcount_t usage;
            u32 num_rules;
            u32 num_layers;
            struct access_masks access_masks[];
        };
    };
};

Members

root_inode

Root of a red-black tree containing struct landlock_rule nodes with inode object. Once a ruleset is tied to a process (i.e. as a domain), this tree is immutable until usage reaches zero.

root_net_port

Root of a red-black tree containing struct landlock_rule nodes with network port. Once a ruleset is tied to a process (i.e. as a domain), this tree is immutable until usage reaches zero.

hierarchy

Enables hierarchy identification even when a parent domain vanishes. This is needed for the ptrace protection.

{unnamed_union}

anonymous

work_free

Enables to free a ruleset within a lockless section. This is only used by landlock_put_ruleset_deferred() when usage reaches zero. The fields lock, usage, num_rules, num_layers and access_masks are then unused.

{unnamed_struct}

anonymous

lock

Protects against concurrent modifications of root, if usage is greater than zero.

usage

Number of processes (i.e. domains) or file descriptors referencing this ruleset.

num_rules

Number of non-overlapping (i.e. not for the same object) rules in this ruleset.

num_layers

Number of layers that are used in this ruleset. This enables to check that all the layers allow an access request. A value of 0 identifies a non-merged ruleset (i.e. not a domain).

access_masks

Contains the subset of filesystem and network actions that are restricted by a ruleset. A domain saves all layers of merged rulesets in a stack (FAM), starting from the first layer to the last one. These layers are used when merging rulesets, for user space backward compatibility (i.e. future-proof), and to properly handle merged rulesets without overlapping access rights. These layers are set once and never changed for the lifetime of the ruleset.

Description

This data structure must contain unique entries, be updatable, and quick to match an object.

struct access_masks landlock_union_access_masks(const struct landlock_ruleset *const domain)

Return all access rights handled in the domain

Parameters

const struct landlock_ruleset *const domain

Landlock ruleset (used as a domain)

Return

an access_masks result of the OR of all the domain’s access masks.

const struct landlock_ruleset *landlock_get_applicable_domain(const struct landlock_ruleset *const domain, const struct access_masks masks)

Return domain if it applies to (handles) at least one of the access rights specified in masks

Parameters

const struct landlock_ruleset *const domain

Landlock ruleset (used as a domain)

const struct access_masks masks

access masks

Return

domain if any access rights specified in masks is handled, or NULL otherwise.