Skip to content

Commit

Permalink
Landlock: Add abstract unix socket connect restriction
Browse files Browse the repository at this point in the history
This patch introduces a new "scoped" attribute to the landlock_ruleset_attr
that can specify "LANDLOCK_SCOPED_ABSTRACT_UNIX_SOCKET" to scope
abstract Unix sockets from connecting to a process outside of
the same landlock domain. It implements two hooks, unix_stream_connect
and unix_may_send to enforce this restriction.

Closes: landlock-lsm#7
Signed-off-by: Tahera Fahimi <fahimitahera@gmail.com>
Link: https://lore.kernel.org/r/603cf546392f0cd35227f696527fd8f1d644cb31.1723615689.git.fahimitahera@gmail.com
[mic: Sync the abi_version test]
Signed-off-by: Mickaël Salaün <mic@digikod.net>
  • Loading branch information
tahifahimi authored and l0kod committed Aug 19, 2024
1 parent 47ac09b commit 07227e4
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 9 deletions.
27 changes: 27 additions & 0 deletions include/uapi/linux/landlock.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/*
Expand Down Expand Up @@ -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 restrict 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.
*
* IPCs with scoped actions:
*
* - %LANDLOCK_SCOPED_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_SCOPED_ABSTRACT_UNIX_SOCKET (1ULL << 0)
/* clang-format on*/
#endif /* _UAPI_LINUX_LANDLOCK_H */
3 changes: 3 additions & 0 deletions security/landlock/limits.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_SCOPED_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 */
7 changes: 5 additions & 2 deletions security/landlock/ruleset.c
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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;
}

Expand Down
23 changes: 22 additions & 1 deletion security/landlock/ruleset.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ 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));

/* Ruleset access masks. */
struct access_masks {
access_mask_t fs : LANDLOCK_NUM_ACCESS_FS;
access_mask_t net : LANDLOCK_NUM_ACCESS_NET;
access_mask_t scoped : LANDLOCK_NUM_SCOPE;
};

typedef u16 layer_mask_t;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -280,6 +284,16 @@ 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 scoped_mask = scope_mask & LANDLOCK_MASK_SCOPE;

WARN_ON_ONCE(scope_mask != scoped_mask);
ruleset->access_masks[layer_level].scoped |= scoped_mask;
}

static inline access_mask_t
landlock_get_raw_fs_access_mask(const struct landlock_ruleset *const ruleset,
const u16 layer_level)
Expand All @@ -303,6 +317,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].scoped;
}

bool landlock_unmask_layers(const struct landlock_rule *const rule,
const access_mask_t access_request,
layer_mask_t (*const layer_masks)[],
Expand Down
17 changes: 12 additions & 5 deletions security/landlock/syscalls.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
129 changes: 129 additions & 0 deletions security/landlock/task.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -108,9 +110,136 @@ 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.
*
* Return 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 dom)
{
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(dom, dom_other,
LANDLOCK_SCOPED_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[0].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-sandboxed processes */
if (!dom)
return 0;

if (is_abstract_socket(other))
if (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;

if (is_abstract_socket(other->sk))
if (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)
Expand Down
2 changes: 1 addition & 1 deletion tools/testing/selftests/landlock/base_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 07227e4

Please sign in to comment.