==================================
:Author: Mickaël Salaün
-:Date: September 2025
+:Date: March 2026
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,
this avoids unattended bypasses through file descriptor passing (i.e. confused
deputy attack).
+.. _scoped-flags-interaction:
+
+Interaction between scoped flags and other access rights
+--------------------------------------------------------
+
+The ``scoped`` flags in &struct landlock_ruleset_attr restrict the
+use of *outgoing* IPC from the created Landlock domain, while they
+permit reaching out to IPC endpoints *within* the created Landlock
+domain.
+
+In the future, scoped flags *may* interact with other access rights,
+e.g. so that abstract UNIX sockets can be allow-listed by name, or so
+that signals can be allow-listed by signal number or target process.
+
+When introducing ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX``, we defined it to
+implicitly have the same scoping semantics as a
+``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` flag would have: connecting to
+UNIX sockets within the same domain (where
+``LANDLOCK_ACCESS_FS_RESOLVE_UNIX`` is used) is unconditionally
+allowed.
+
+The reasoning is:
+
+* Like other IPC mechanisms, connecting to named UNIX sockets in the
+ same domain should be expected and harmless. (If needed, users can
+ further refine their Landlock policies with nested domains or by
+ restricting ``LANDLOCK_ACCESS_FS_MAKE_SOCK``.)
+* We reserve the option to still introduce
+ ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` in the future. (This would
+ be useful if we wanted to have a Landlock rule to permit IPC access
+ to other Landlock domains.)
+* But we can postpone the point in time when users have to deal with
+ two interacting flags visible in the userspace API. (In particular,
+ it is possible that it won't be needed in practice, in which case we
+ can avoid the second flag altogether.)
+* If we *do* introduce ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` in the
+ future, setting this scoped flag in a ruleset does *not reduce* the
+ restrictions, because access within the same scope is already
+ allowed based on ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX``.
+
Tests
=====
*
* This access right is available since the fifth version of the Landlock
* ABI.
+ * - %LANDLOCK_ACCESS_FS_RESOLVE_UNIX: Look up pathname UNIX domain sockets
+ * (:manpage:`unix(7)`). On UNIX domain sockets, this restricts both calls to
+ * :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
+ * explicit recipient address.
+ *
+ * This access right only applies to connections to UNIX server sockets which
+ * were created outside of the newly created Landlock domain (e.g. from within
+ * a parent domain or from an unrestricted process). Newly created UNIX
+ * servers within the same Landlock domain continue to be accessible. In this
+ * regard, %LANDLOCK_ACCESS_FS_RESOLVE_UNIX has the same semantics as the
+ * ``LANDLOCK_SCOPE_*`` flags.
+ *
+ * If a resolve attempt is denied, the operation returns an ``EACCES`` error,
+ * in line with other filesystem access rights (but different to denials for
+ * abstract UNIX domain sockets).
+ *
+ * This access right is available since the ninth version of the Landlock ABI.
+ *
+ * The rationale for this design is described in
+ * :ref:`Documentation/security/landlock.rst <scoped-flags-interaction>`.
*
* Whether an opened file can be truncated with :manpage:`ftruncate(2)` or used
* with `ioctl(2)` is determined during :manpage:`open(2)`, in the same way as
#define LANDLOCK_ACCESS_FS_REFER (1ULL << 13)
#define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14)
#define LANDLOCK_ACCESS_FS_IOCTL_DEV (1ULL << 15)
+#define LANDLOCK_ACCESS_FS_RESOLVE_UNIX (1ULL << 16)
/* clang-format on */
/**
#include <linux/lsm_hooks.h>
#include <linux/mount.h>
#include <linux/namei.h>
+#include <linux/net.h>
#include <linux/path.h>
#include <linux/pid.h>
#include <linux/rcupdate.h>
#include <linux/types.h>
#include <linux/wait_bit.h>
#include <linux/workqueue.h>
+#include <net/af_unix.h>
#include <uapi/linux/fiemap.h>
#include <uapi/linux/landlock.h>
LANDLOCK_ACCESS_FS_WRITE_FILE | \
LANDLOCK_ACCESS_FS_READ_FILE | \
LANDLOCK_ACCESS_FS_TRUNCATE | \
- LANDLOCK_ACCESS_FS_IOCTL_DEV)
+ LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+ LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
/* clang-format on */
/*
return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
}
+/**
+ * unmask_scoped_access - Remove access right bits in @masks in all layers
+ * where @client and @server have the same domain
+ *
+ * This does the same as domain_is_scoped(), but unmasks bits in @masks.
+ * It can not return early as domain_is_scoped() does.
+ *
+ * A scoped access for a given access right bit is allowed iff, for all layer
+ * depths where the access bit is set, the client and server domain are the
+ * same. This function clears the access rights @access in @masks at all layer
+ * depths where the client and server domain are the same, so that, when they
+ * are all cleared, the access is allowed.
+ *
+ * @client: Client domain
+ * @server: Server domain
+ * @masks: Layer access masks to unmask
+ * @access: Access bits that control scoping
+ */
+static void unmask_scoped_access(const struct landlock_ruleset *const client,
+ const struct landlock_ruleset *const server,
+ struct layer_access_masks *const masks,
+ const access_mask_t access)
+{
+ int client_layer, server_layer;
+ const struct landlock_hierarchy *client_walker, *server_walker;
+
+ /* This should not happen. */
+ if (WARN_ON_ONCE(!client))
+ return;
+
+ /* Server has no Landlock domain; nothing to clear. */
+ if (!server)
+ return;
+
+ /*
+ * client_layer must be a signed integer with greater capacity
+ * than client->num_layers to ensure the following loop stops.
+ */
+ BUILD_BUG_ON(sizeof(client_layer) > sizeof(client->num_layers));
+
+ client_layer = client->num_layers - 1;
+ client_walker = client->hierarchy;
+ server_layer = server->num_layers - 1;
+ server_walker = server->hierarchy;
+
+ /*
+ * Clears the access bits at all layers where the client domain is the
+ * same as the server domain. We start the walk at min(client_layer,
+ * server_layer). The layer bits until there can not be cleared because
+ * either the client or the server domain is missing.
+ */
+ for (; client_layer > server_layer; client_layer--)
+ client_walker = client_walker->parent;
+
+ for (; server_layer > client_layer; server_layer--)
+ server_walker = server_walker->parent;
+
+ for (; client_layer >= 0; client_layer--) {
+ if (masks->access[client_layer] & access &&
+ client_walker == server_walker)
+ masks->access[client_layer] &= ~access;
+
+ client_walker = client_walker->parent;
+ server_walker = server_walker->parent;
+ }
+}
+
+static int hook_unix_find(const struct path *const path, struct sock *other,
+ int flags)
+{
+ const struct landlock_ruleset *dom_other;
+ const struct landlock_cred_security *subject;
+ struct layer_access_masks layer_masks;
+ struct landlock_request request = {};
+ static const struct access_masks fs_resolve_unix = {
+ .fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+ };
+
+ /* Lookup for the purpose of saving coredumps is OK. */
+ if (unlikely(flags & SOCK_COREDUMP))
+ return 0;
+
+ subject = landlock_get_applicable_subject(current_cred(),
+ fs_resolve_unix, NULL);
+
+ if (!subject)
+ return 0;
+
+ /*
+ * Ignoring return value: that the domains apply was already checked in
+ * landlock_get_applicable_subject() above.
+ */
+ landlock_init_layer_masks(subject->domain, fs_resolve_unix.fs,
+ &layer_masks, LANDLOCK_KEY_INODE);
+
+ /* Checks the layers in which we are connecting within the same domain. */
+ unix_state_lock(other);
+ if (unlikely(sock_flag(other, SOCK_DEAD) || !other->sk_socket ||
+ !other->sk_socket->file)) {
+ unix_state_unlock(other);
+ /*
+ * We rely on the caller to catch the (non-reversible) SOCK_DEAD
+ * condition and retry the lookup. If we returned an error
+ * here, the lookup would not get retried.
+ */
+ return 0;
+ }
+ dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
+
+ /* Access to the same (or a lower) domain is always allowed. */
+ unmask_scoped_access(subject->domain, dom_other, &layer_masks,
+ fs_resolve_unix.fs);
+ unix_state_unlock(other);
+
+ /* Checks the connections to allow-listed paths. */
+ if (is_access_to_paths_allowed(subject->domain, path,
+ fs_resolve_unix.fs, &layer_masks,
+ &request, NULL, 0, NULL, NULL, NULL))
+ return 0;
+
+ landlock_log_denial(subject, &request);
+ return -EACCES;
+}
+
/* File hooks */
/**
LSM_HOOK_INIT(path_unlink, hook_path_unlink),
LSM_HOOK_INIT(path_rmdir, hook_path_rmdir),
LSM_HOOK_INIT(path_truncate, hook_path_truncate),
+ LSM_HOOK_INIT(unix_find, hook_unix_find),
LSM_HOOK_INIT(file_alloc_security, hook_file_alloc_security),
LSM_HOOK_INIT(file_open, hook_file_open),