mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2024-12-28 16:53:49 +00:00
landlock: Add abstract UNIX socket scoping
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>
This commit is contained in:
parent
a430d95c5e
commit
21d52e295a
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)[],
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user