]> www.infradead.org Git - users/jedix/linux-maple.git/commitdiff
landlock: Add abstract UNIX socket scoping
authorTahera Fahimi <fahimitahera@gmail.com>
Thu, 5 Sep 2024 00:13:55 +0000 (18:13 -0600)
committerMickaël Salaün <mic@digikod.net>
Mon, 16 Sep 2024 21:50:45 +0000 (23:50 +0200)
Introduce a new "scoped" member to landlock_ruleset_attr that can
specify LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET to restrict connection to
abstract UNIX sockets from a process outside of the socket's domain.

Two hooks are implemented to enforce these restrictions:
unix_stream_connect and unix_may_send.

Closes: https://github.com/landlock-lsm/linux/issues/7
Signed-off-by: Tahera Fahimi <fahimitahera@gmail.com>
Link: https://lore.kernel.org/r/5f7ad85243b78427242275b93481cfc7c127764b.1725494372.git.fahimitahera@gmail.com
[mic: Fix commit message formatting, improve documentation, simplify
hook_unix_may_send(), and cosmetic fixes including rename of
LANDLOCK_SCOPED_ABSTRACT_UNIX_SOCKET]
Co-developed-by: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
include/uapi/linux/landlock.h
security/landlock/limits.h
security/landlock/ruleset.c
security/landlock/ruleset.h
security/landlock/syscalls.c
security/landlock/task.c
tools/testing/selftests/landlock/base_test.c

index 2c8dbc74b95537df6bafbf1d5bb7145b2686b4e4..70edd17bafdc371a90f22835688d56598ba6be6a 100644 (file)
@@ -44,6 +44,12 @@ struct landlock_ruleset_attr {
         * flags`_).
         */
        __u64 handled_access_net;
+       /**
+        * @scoped: Bitmask of scopes (cf. `Scope flags`_)
+        * restricting a Landlock domain from accessing outside
+        * resources (e.g. IPCs).
+        */
+       __u64 scoped;
 };
 
 /*
@@ -274,4 +280,25 @@ struct landlock_net_port_attr {
 #define LANDLOCK_ACCESS_NET_BIND_TCP                   (1ULL << 0)
 #define LANDLOCK_ACCESS_NET_CONNECT_TCP                        (1ULL << 1)
 /* clang-format on */
+
+/**
+ * DOC: scope
+ *
+ * Scope flags
+ * ~~~~~~~~~~~
+ *
+ * These flags enable to isolate a sandboxed process from a set of IPC actions.
+ * Setting a flag for a ruleset will isolate the Landlock domain to forbid
+ * connections to resources outside the domain.
+ *
+ * Scopes:
+ *
+ * - %LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET: Restrict a sandboxed process from
+ *   connecting to an abstract UNIX socket created by a process outside the
+ *   related Landlock domain (e.g. a parent domain or a non-sandboxed process).
+ */
+/* clang-format off */
+#define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET            (1ULL << 0)
+/* clang-format on*/
+
 #endif /* _UAPI_LINUX_LANDLOCK_H */
index 4eb643077a2a6e31693d403af89be1046ad6337c..d74818003ed4d7b2fd267b858d050b4f55bddc98 100644 (file)
@@ -26,6 +26,9 @@
 #define LANDLOCK_MASK_ACCESS_NET       ((LANDLOCK_LAST_ACCESS_NET << 1) - 1)
 #define LANDLOCK_NUM_ACCESS_NET                __const_hweight64(LANDLOCK_MASK_ACCESS_NET)
 
+#define LANDLOCK_LAST_SCOPE            LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
+#define LANDLOCK_MASK_SCOPE            ((LANDLOCK_LAST_SCOPE << 1) - 1)
+#define LANDLOCK_NUM_SCOPE             __const_hweight64(LANDLOCK_MASK_SCOPE)
 /* clang-format on */
 
 #endif /* _SECURITY_LANDLOCK_LIMITS_H */
index 6ff232f586183d6b74c675a1f03555d7831b81f5..a93bdbf52fff856a346d7aa6bb3e4df1e61103c1 100644 (file)
@@ -52,12 +52,13 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers)
 
 struct landlock_ruleset *
 landlock_create_ruleset(const access_mask_t fs_access_mask,
-                       const access_mask_t net_access_mask)
+                       const access_mask_t net_access_mask,
+                       const access_mask_t scope_mask)
 {
        struct landlock_ruleset *new_ruleset;
 
        /* Informs about useless ruleset. */
-       if (!fs_access_mask && !net_access_mask)
+       if (!fs_access_mask && !net_access_mask && !scope_mask)
                return ERR_PTR(-ENOMSG);
        new_ruleset = create_ruleset(1);
        if (IS_ERR(new_ruleset))
@@ -66,6 +67,8 @@ landlock_create_ruleset(const access_mask_t fs_access_mask,
                landlock_add_fs_access_mask(new_ruleset, fs_access_mask, 0);
        if (net_access_mask)
                landlock_add_net_access_mask(new_ruleset, net_access_mask, 0);
+       if (scope_mask)
+               landlock_add_scope_mask(new_ruleset, scope_mask, 0);
        return new_ruleset;
 }
 
index 0f1b5b4c8f6b4136c4a02278cd46a0aef1e23065..61bdbc550172debcfd30ed3ea967a68bf8348317 100644 (file)
@@ -35,6 +35,8 @@ typedef u16 access_mask_t;
 static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS);
 /* Makes sure all network access rights can be stored. */
 static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_NET);
+/* Makes sure all scoped rights can be stored. */
+static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_SCOPE);
 /* Makes sure for_each_set_bit() and for_each_clear_bit() calls are OK. */
 static_assert(sizeof(unsigned long) >= sizeof(access_mask_t));
 
@@ -42,6 +44,7 @@ static_assert(sizeof(unsigned long) >= sizeof(access_mask_t));
 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;
 };
 
 typedef u16 layer_mask_t;
@@ -233,7 +236,8 @@ struct landlock_ruleset {
 
 struct landlock_ruleset *
 landlock_create_ruleset(const access_mask_t access_mask_fs,
-                       const access_mask_t access_mask_net);
+                       const access_mask_t access_mask_net,
+                       const access_mask_t scope_mask);
 
 void landlock_put_ruleset(struct landlock_ruleset *const ruleset);
 void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset);
@@ -280,6 +284,17 @@ landlock_add_net_access_mask(struct landlock_ruleset *const ruleset,
        ruleset->access_masks[layer_level].net |= net_mask;
 }
 
+static inline void
+landlock_add_scope_mask(struct landlock_ruleset *const ruleset,
+                       const access_mask_t scope_mask, const u16 layer_level)
+{
+       access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE;
+
+       /* Should already be checked in sys_landlock_create_ruleset(). */
+       WARN_ON_ONCE(scope_mask != mask);
+       ruleset->access_masks[layer_level].scope |= mask;
+}
+
 static inline access_mask_t
 landlock_get_raw_fs_access_mask(const struct landlock_ruleset *const ruleset,
                                const u16 layer_level)
@@ -303,6 +318,13 @@ landlock_get_net_access_mask(const struct landlock_ruleset *const ruleset,
        return ruleset->access_masks[layer_level].net;
 }
 
+static inline access_mask_t
+landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
+                       const u16 layer_level)
+{
+       return ruleset->access_masks[layer_level].scope;
+}
+
 bool landlock_unmask_layers(const struct landlock_rule *const rule,
                            const access_mask_t access_request,
                            layer_mask_t (*const layer_masks)[],
index ccc8bc6c158456ea9556620c43c169444e5b3da3..c67836841e461befafa54b49b64423ef8261ad68 100644 (file)
@@ -97,8 +97,9 @@ static void build_check_abi(void)
         */
        ruleset_size = sizeof(ruleset_attr.handled_access_fs);
        ruleset_size += sizeof(ruleset_attr.handled_access_net);
+       ruleset_size += sizeof(ruleset_attr.scoped);
        BUILD_BUG_ON(sizeof(ruleset_attr) != ruleset_size);
-       BUILD_BUG_ON(sizeof(ruleset_attr) != 16);
+       BUILD_BUG_ON(sizeof(ruleset_attr) != 24);
 
        path_beneath_size = sizeof(path_beneath_attr.allowed_access);
        path_beneath_size += sizeof(path_beneath_attr.parent_fd);
@@ -149,7 +150,7 @@ static const struct file_operations ruleset_fops = {
        .write = fop_dummy_write,
 };
 
-#define LANDLOCK_ABI_VERSION 5
+#define LANDLOCK_ABI_VERSION 6
 
 /**
  * sys_landlock_create_ruleset - Create a new ruleset
@@ -170,8 +171,9 @@ static const struct file_operations ruleset_fops = {
  * Possible returned errors are:
  *
  * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
- * - %EINVAL: unknown @flags, or unknown access, or too small @size;
- * - %E2BIG or %EFAULT: @attr or @size inconsistencies;
+ * - %EINVAL: unknown @flags, or unknown access, or unknown scope, or too small @size;
+ * - %E2BIG: @attr or @size inconsistencies;
+ * - %EFAULT: @attr or @size inconsistencies;
  * - %ENOMSG: empty &landlock_ruleset_attr.handled_access_fs.
  */
 SYSCALL_DEFINE3(landlock_create_ruleset,
@@ -213,9 +215,14 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
            LANDLOCK_MASK_ACCESS_NET)
                return -EINVAL;
 
+       /* Checks IPC scoping content (and 32-bits cast). */
+       if ((ruleset_attr.scoped | LANDLOCK_MASK_SCOPE) != LANDLOCK_MASK_SCOPE)
+               return -EINVAL;
+
        /* Checks arguments and transforms to kernel struct. */
        ruleset = landlock_create_ruleset(ruleset_attr.handled_access_fs,
-                                         ruleset_attr.handled_access_net);
+                                         ruleset_attr.handled_access_net,
+                                         ruleset_attr.scoped);
        if (IS_ERR(ruleset))
                return PTR_ERR(ruleset);
 
index 849f5123610b6f569bdc308a5847eed0a4eb5d1f..4f8013ca412ec6bcbf5d798ab8342c61c567dac4 100644 (file)
@@ -13,6 +13,8 @@
 #include <linux/lsm_hooks.h>
 #include <linux/rcupdate.h>
 #include <linux/sched.h>
+#include <net/af_unix.h>
+#include <net/sock.h>
 
 #include "common.h"
 #include "cred.h"
@@ -108,9 +110,144 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
        return task_ptrace(parent, current);
 }
 
+/**
+ * domain_is_scoped - Checks if the client domain is scoped in the same
+ *                   domain as the server.
+ *
+ * @client: IPC sender domain.
+ * @server: IPC receiver domain.
+ * @scope: The scope restriction criteria.
+ *
+ * Returns: True if the @client domain is scoped to access the @server,
+ * unless the @server is also scoped in the same domain as @client.
+ */
+static bool domain_is_scoped(const struct landlock_ruleset *const client,
+                            const struct landlock_ruleset *const server,
+                            access_mask_t scope)
+{
+       int client_layer, server_layer;
+       struct landlock_hierarchy *client_walker, *server_walker;
+
+       /* Quick return if client has no domain */
+       if (WARN_ON_ONCE(!client))
+               return false;
+
+       client_layer = client->num_layers - 1;
+       client_walker = client->hierarchy;
+       /*
+        * 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));
+
+       server_layer = server ? (server->num_layers - 1) : -1;
+       server_walker = server ? server->hierarchy : NULL;
+
+       /*
+        * Walks client's parent domains down to the same hierarchy level
+        * as the server's domain, and checks that none of these client's
+        * parent domains are scoped.
+        */
+       for (; client_layer > server_layer; client_layer--) {
+               if (landlock_get_scope_mask(client, client_layer) & scope)
+                       return true;
+
+               client_walker = client_walker->parent;
+       }
+       /*
+        * Walks server's parent domains down to the same hierarchy level as
+        * the client's domain.
+        */
+       for (; server_layer > client_layer; server_layer--)
+               server_walker = server_walker->parent;
+
+       for (; client_layer >= 0; client_layer--) {
+               if (landlock_get_scope_mask(client, client_layer) & scope) {
+                       /*
+                        * Client and server are at the same level in the
+                        * hierarchy. If the client is scoped, the request is
+                        * only allowed if this domain is also a server's
+                        * ancestor.
+                        */
+                       return server_walker != client_walker;
+               }
+               client_walker = client_walker->parent;
+               server_walker = server_walker->parent;
+       }
+       return false;
+}
+
+static bool sock_is_scoped(struct sock *const other,
+                          const struct landlock_ruleset *const domain)
+{
+       const struct landlock_ruleset *dom_other;
+
+       /* The credentials will not change. */
+       lockdep_assert_held(&unix_sk(other)->lock);
+       dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
+       return domain_is_scoped(domain, dom_other,
+                               LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+}
+
+static bool is_abstract_socket(struct sock *const sock)
+{
+       struct unix_address *addr = unix_sk(sock)->addr;
+
+       if (!addr)
+               return false;
+
+       if (addr->len >= offsetof(struct sockaddr_un, sun_path) + 1 &&
+           addr->name->sun_path[0] == '\0')
+               return true;
+
+       return false;
+}
+
+static int hook_unix_stream_connect(struct sock *const sock,
+                                   struct sock *const other,
+                                   struct sock *const newsk)
+{
+       const struct landlock_ruleset *const dom =
+               landlock_get_current_domain();
+
+       /* Quick return for non-landlocked tasks. */
+       if (!dom)
+               return 0;
+
+       if (is_abstract_socket(other) && sock_is_scoped(other, dom))
+               return -EPERM;
+
+       return 0;
+}
+
+static int hook_unix_may_send(struct socket *const sock,
+                             struct socket *const other)
+{
+       const struct landlock_ruleset *const dom =
+               landlock_get_current_domain();
+
+       if (!dom)
+               return 0;
+
+       /*
+        * Checks if this datagram socket was already allowed to be connected
+        * to other.
+        */
+       if (unix_peer(sock->sk) == other->sk)
+               return 0;
+
+       if (is_abstract_socket(other->sk) && sock_is_scoped(other->sk, dom))
+               return -EPERM;
+
+       return 0;
+}
+
 static struct security_hook_list landlock_hooks[] __ro_after_init = {
        LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check),
        LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme),
+
+       LSM_HOOK_INIT(unix_stream_connect, hook_unix_stream_connect),
+       LSM_HOOK_INIT(unix_may_send, hook_unix_may_send),
 };
 
 __init void landlock_add_task_hooks(void)
index 3b26bf3cf5b9a489e037f5b11b2a635ccb55b83a..1bc16fde2e8aea01860a39f37c0e5dc6e28b7f84 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(5, landlock_create_ruleset(NULL, 0,
+       ASSERT_EQ(6, landlock_create_ruleset(NULL, 0,
                                             LANDLOCK_CREATE_RULESET_VERSION));
 
        ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,