]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
landlock: Control pathname UNIX domain socket resolution by path
authorGünther Noack <gnoack3000@gmail.com>
Fri, 27 Mar 2026 16:48:29 +0000 (17:48 +0100)
committerMickaël Salaün <mic@digikod.net>
Tue, 7 Apr 2026 16:51:06 +0000 (18:51 +0200)
* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which
  controls the lookup operations for named UNIX domain sockets.  The
  resolution happens during connect() and sendmsg() (depending on
  socket type).
* Change access_mask_t from u16 to u32 (see below)
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a
  LSM hook.  Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptations to keep the tests working.
* Document the design rationale for scoped access rights,
  and cross-reference it from the header documentation.

With this access right, access is granted if either of the following
conditions is met:

* The target socket's filesystem path was allow-listed using a
  LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which
  LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is
the same error as it is returned if the user does not have the write
bit in the traditional UNIX file system permissions of that file.

The access_mask_t type grows from u16 to u32 to make space for the new
access right.  This also doubles the size of struct layer_access_masks
from 32 byte to 64 byte.  To avoid memory layout inconsistencies between
architectures (especially m68k), pack and align struct access_masks [2].

Document the (possible future) interaction between scoped flags and
other access rights in struct landlock_ruleset_attr, and summarize the
rationale, as discussed in code review leading up to [3].

This feature was created with substantial discussion and input from
Justin Suess, Tingmao Wang and Mickaël Salaün.

Cc: Tingmao Wang <m@maowtm.org>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Kuniyuki Iwashima <kuniyu@google.com>
Suggested-by: Jann Horn <jannh@google.com>
Link[1]: https://github.com/landlock-lsm/linux/issues/36
Link[2]: https://lore.kernel.org/all/20260401.Re1Eesu1Yaij@digikod.net/
Link[3]: https://lore.kernel.org/all/20260205.8531e4005118@gnoack.org/
Signed-off-by: Günther Noack <gnoack3000@gmail.com>
Acked-by: Sebastian Andrzej Siewior <bigeasy@linutronix.de>
Link: https://lore.kernel.org/r/20260327164838.38231-5-gnoack3000@gmail.com
[mic: Fix kernel-doc formatting, pack and align access_masks]
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Documentation/security/landlock.rst
include/uapi/linux/landlock.h
security/landlock/access.h
security/landlock/audit.c
security/landlock/fs.c
security/landlock/limits.h
security/landlock/syscalls.c
tools/testing/selftests/landlock/base_test.c
tools/testing/selftests/landlock/fs_test.c

index 3e4d4d04cfae4ed22649d312aeb5f6d16c028b28..c5186526e76f6cf1954a73fc3219e88fe6409b9e 100644 (file)
@@ -7,7 +7,7 @@ Landlock LSM: kernel documentation
 ==================================
 
 :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,
@@ -89,6 +89,46 @@ 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).
 
+.. _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
 =====
 
index d37603efc273fd22c5baeebec436e49f746fd5db..10a346e55e95fd56f8dcf6faa172425749883fda 100644 (file)
@@ -250,6 +250,26 @@ struct landlock_net_port_attr {
  *
  *   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
@@ -335,6 +355,7 @@ struct landlock_net_port_attr {
 #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 */
 
 /**
index 42c95747d7bd68c2138afce2ac45ef46d0c58e4f..c19d5bc1394431ae31ecf41fa6cfda1b93f45cc3 100644 (file)
@@ -34,7 +34,7 @@
        LANDLOCK_ACCESS_FS_IOCTL_DEV)
 /* clang-format on */
 
-typedef u16 access_mask_t;
+typedef u32 access_mask_t;
 
 /* Makes sure all filesystem access rights can be stored. */
 static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS);
@@ -50,7 +50,7 @@ struct access_masks {
        access_mask_t fs : LANDLOCK_NUM_ACCESS_FS;
        access_mask_t net : LANDLOCK_NUM_ACCESS_NET;
        access_mask_t scope : LANDLOCK_NUM_SCOPE;
-};
+} __packed __aligned(sizeof(u32));
 
 union access_masks_all {
        struct access_masks masks;
index 60ff217ab95b5dcf4aee907a5d2971aa0ad7cfd8..8d0edf94037d720620f0ed1bb8d2aaea7dfcd9f5 100644 (file)
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
        [BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
        [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
        [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+       [BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
index 97065d51685a916a6bd39466ad87b9dfbafb1242..fcf69b3d734d6b262316f7700918a1587fde2fdb 100644 (file)
@@ -27,6 +27,7 @@
 #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>
@@ -36,6 +37,7 @@
 #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>
 
@@ -314,7 +316,8 @@ retry:
        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 */
 
 /*
@@ -1557,6 +1560,130 @@ static int hook_path_truncate(const struct path *const path)
        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 */
 
 /**
@@ -1834,6 +1961,7 @@ static struct security_hook_list landlock_hooks[] __ro_after_init = {
        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),
index eb584f47288d655cc8d48ef1eedd4151631294ad..b454ad73b15e815d42793ba66e97f2452f92cba5 100644 (file)
@@ -19,7 +19,7 @@
 #define LANDLOCK_MAX_NUM_LAYERS                16
 #define LANDLOCK_MAX_NUM_RULES         U32_MAX
 
-#define LANDLOCK_LAST_ACCESS_FS                LANDLOCK_ACCESS_FS_IOCTL_DEV
+#define LANDLOCK_LAST_ACCESS_FS                LANDLOCK_ACCESS_FS_RESOLVE_UNIX
 #define LANDLOCK_MASK_ACCESS_FS                ((LANDLOCK_LAST_ACCESS_FS << 1) - 1)
 #define LANDLOCK_NUM_ACCESS_FS         __const_hweight64(LANDLOCK_MASK_ACCESS_FS)
 
index 5ebd606e84e69d288b88a6c8c259863455cafd60..accfd2e5a0cd42dfc82095ce57082332326a281c 100644 (file)
@@ -166,7 +166,7 @@ static const struct file_operations ruleset_fops = {
  * If the change involves a fix that requires userspace awareness, also update
  * the errata documentation in Documentation/userspace-api/landlock.rst .
  */
-const int landlock_abi_version = 8;
+const int landlock_abi_version = 9;
 
 /**
  * sys_landlock_create_ruleset - Create a new ruleset
index 0fea236ef4bd18f10f23f36c8949cca7f07da52b..30d37234086c4bc9528fce5af92e8759edbaba16 100644 (file)
@@ -76,7 +76,7 @@ TEST(abi_version)
        const struct landlock_ruleset_attr ruleset_attr = {
                .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
        };
-       ASSERT_EQ(8, landlock_create_ruleset(NULL, 0,
+       ASSERT_EQ(9, landlock_create_ruleset(NULL, 0,
                                             LANDLOCK_CREATE_RULESET_VERSION));
 
        ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
index 968a91c927a46af71e07e9d5e3bb43d77284a583..b318627e75619b1a9a5c3a26d077fbacc6e72270 100644 (file)
@@ -575,9 +575,10 @@ TEST_F_FORK(layout1, inval)
        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)
 
-#define ACCESS_LAST LANDLOCK_ACCESS_FS_IOCTL_DEV
+#define ACCESS_LAST LANDLOCK_ACCESS_FS_RESOLVE_UNIX
 
 #define ACCESS_ALL ( \
        ACCESS_FILE | \