summaryrefslogtreecommitdiff
path: root/security
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2024-09-24 10:40:11 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2024-09-24 10:40:11 -0700
commite1b061b444fb01c237838f0d8238653afe6a8094 (patch)
treea98ecdbdd1d490c1611073d886586a288f3ed498 /security
parent24f772dec31591f9268a9c9e4943dc5dc47eaf9b (diff)
parent1ca980815e1f284dddcb5e678c91bbd3e3f3a6a6 (diff)
Merge tag 'landlock-6.12-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux
Pull landlock updates from Mickaël Salaün: "We can now scope a Landlock domain thanks to a new "scoped" field that can deny interactions with resources outside of this domain. The LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET flag denies connections to an abstract UNIX socket created outside of the current scoped domain, and the LANDLOCK_SCOPE_SIGNAL flag denies sending a signal to processes outside of the current scoped domain. These restrictions also apply to nested domains according to their scope. The related changes will also be useful to support other kind of IPC isolations" * tag 'landlock-6.12-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux: landlock: Document LANDLOCK_SCOPE_SIGNAL samples/landlock: Add support for signal scoping selftests/landlock: Test signal created by out-of-bound message selftests/landlock: Test signal scoping for threads selftests/landlock: Test signal scoping landlock: Add signal scoping landlock: Document LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET samples/landlock: Add support for abstract UNIX socket scoping selftests/landlock: Test inherited restriction of abstract UNIX socket selftests/landlock: Test connected and unconnected datagram UNIX socket selftests/landlock: Test UNIX sockets with any address formats selftests/landlock: Test abstract UNIX socket scoping selftests/landlock: Test handling of unknown scope landlock: Add abstract UNIX socket scoping
Diffstat (limited to 'security')
-rw-r--r--security/landlock/cred.h2
-rw-r--r--security/landlock/fs.c25
-rw-r--r--security/landlock/fs.h7
-rw-r--r--security/landlock/limits.h3
-rw-r--r--security/landlock/ruleset.c7
-rw-r--r--security/landlock/ruleset.h24
-rw-r--r--security/landlock/syscalls.c17
-rw-r--r--security/landlock/task.c193
8 files changed, 269 insertions, 9 deletions
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index af89ab00e6d1..bf755459838a 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -26,7 +26,7 @@ landlock_cred(const struct cred *cred)
return cred->security + landlock_blob_sizes.lbs_cred;
}
-static inline const struct landlock_ruleset *landlock_get_current_domain(void)
+static inline struct landlock_ruleset *landlock_get_current_domain(void)
{
return landlock_cred(current_cred())->domain;
}
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 0804f76a67be..7d79fc8abe21 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -1639,6 +1639,29 @@ static int hook_file_ioctl_compat(struct file *file, unsigned int cmd,
return -EACCES;
}
+static void hook_file_set_fowner(struct file *file)
+{
+ struct landlock_ruleset *new_dom, *prev_dom;
+
+ /*
+ * Lock already held by __f_setown(), see commit 26f204380a3c ("fs: Fix
+ * file_set_fowner LSM hook inconsistencies").
+ */
+ lockdep_assert_held(&file_f_owner(file)->lock);
+ new_dom = landlock_get_current_domain();
+ landlock_get_ruleset(new_dom);
+ prev_dom = landlock_file(file)->fown_domain;
+ landlock_file(file)->fown_domain = new_dom;
+
+ /* Called in an RCU read-side critical section. */
+ landlock_put_ruleset_deferred(prev_dom);
+}
+
+static void hook_file_free_security(struct file *file)
+{
+ landlock_put_ruleset_deferred(landlock_file(file)->fown_domain);
+}
+
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(inode_free_security_rcu, hook_inode_free_security_rcu),
@@ -1663,6 +1686,8 @@ static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(file_truncate, hook_file_truncate),
LSM_HOOK_INIT(file_ioctl, hook_file_ioctl),
LSM_HOOK_INIT(file_ioctl_compat, hook_file_ioctl_compat),
+ LSM_HOOK_INIT(file_set_fowner, hook_file_set_fowner),
+ LSM_HOOK_INIT(file_free_security, hook_file_free_security),
};
__init void landlock_add_fs_hooks(void)
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index 488e4813680a..1487e1f023a1 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -52,6 +52,13 @@ struct landlock_file_security {
* needed to authorize later operations on the open file.
*/
access_mask_t allowed_access;
+ /**
+ * @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.
+ */
+ struct landlock_ruleset *fown_domain;
};
/**
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 4eb643077a2a..15f7606066c8 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -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_SIGNAL
+#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 */
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 6ff232f58618..a93bdbf52fff 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -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;
}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 0f1b5b4c8f6b..61bdbc550172 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -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)[],
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 00b63971ab64..f5a0e7182ec0 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -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);
diff --git a/security/landlock/task.c b/security/landlock/task.c
index 849f5123610b..4acbd7c40eee 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -13,9 +13,12 @@
#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"
+#include "fs.h"
#include "ruleset.h"
#include "setup.h"
#include "task.h"
@@ -108,9 +111,199 @@ 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 int hook_task_kill(struct task_struct *const p,
+ struct kernel_siginfo *const info, const int sig,
+ const struct cred *const cred)
+{
+ bool is_scoped;
+ const struct landlock_ruleset *dom;
+
+ if (cred) {
+ /* Dealing with USB IO. */
+ dom = landlock_cred(cred)->domain;
+ } else {
+ dom = landlock_get_current_domain();
+ }
+
+ /* Quick return for non-landlocked tasks. */
+ if (!dom)
+ return 0;
+
+ rcu_read_lock();
+ is_scoped = domain_is_scoped(dom, landlock_get_task_domain(p),
+ LANDLOCK_SCOPE_SIGNAL);
+ rcu_read_unlock();
+ if (is_scoped)
+ return -EPERM;
+
+ return 0;
+}
+
+static int hook_file_send_sigiotask(struct task_struct *tsk,
+ struct fown_struct *fown, int signum)
+{
+ const struct landlock_ruleset *dom;
+ bool is_scoped = false;
+
+ /* Lock already held by send_sigio() and send_sigurg(). */
+ lockdep_assert_held(&fown->lock);
+ dom = landlock_file(fown->file)->fown_domain;
+
+ /* Quick return for unowned socket. */
+ if (!dom)
+ return 0;
+
+ rcu_read_lock();
+ is_scoped = domain_is_scoped(dom, landlock_get_task_domain(tsk),
+ LANDLOCK_SCOPE_SIGNAL);
+ rcu_read_unlock();
+ if (is_scoped)
+ 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),
+
+ LSM_HOOK_INIT(task_kill, hook_task_kill),
+ LSM_HOOK_INIT(file_send_sigiotask, hook_file_send_sigiotask),
};
__init void landlock_add_task_hooks(void)