Landlock updates for v6.12-rc1

-----BEGIN PGP SIGNATURE-----
 
 iIYEABYKAC4WIQSVyBthFV4iTW/VU1/l49DojIL20gUCZvGpchAcbWljQGRpZ2lr
 b2QubmV0AAoJEOXj0OiMgvbSTzMBAIpcYKf75IyC4DXqiXlko508YdyI2YfYeWdd
 5yVZbSHgAP0aEFO4AOvJ26pPlGF+8zVIHq+HNAhrAalZBulxASePCA==
 =nsAF
 -----END PGP SIGNATURE-----

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
This commit is contained in:
Linus Torvalds 2024-09-24 10:40:11 -07:00
commit e1b061b444
21 changed files with 2359 additions and 47 deletions

View File

@ -8,7 +8,7 @@ Landlock: unprivileged access control
=====================================
:Author: Mickaël Salaün
:Date: July 2024
:Date: September 2024
The goal of Landlock is to enable to restrict ambient rights (e.g. global
filesystem or network access) for a set of processes. Because Landlock
@ -81,6 +81,9 @@ to be explicit about the denied-by-default access rights.
.handled_access_net =
LANDLOCK_ACCESS_NET_BIND_TCP |
LANDLOCK_ACCESS_NET_CONNECT_TCP,
.scoped =
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL,
};
Because we may not know on which kernel version an application will be
@ -119,6 +122,11 @@ version, and only use the available subset of access rights:
case 4:
/* Removes LANDLOCK_ACCESS_FS_IOCTL_DEV for ABI < 5 */
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_IOCTL_DEV;
__attribute__((fallthrough));
case 5:
/* Removes LANDLOCK_SCOPE_* for ABI < 6 */
ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL);
}
This enables to create an inclusive ruleset that will contain our rules.
@ -306,6 +314,38 @@ To be allowed to use :manpage:`ptrace(2)` and related syscalls on a target
process, a sandboxed process should have a subset of the target process rules,
which means the tracee must be in a sub-domain of the tracer.
IPC scoping
-----------
Similar to the implicit `Ptrace restrictions`_, we may want to further restrict
interactions between sandboxes. Each Landlock domain can be explicitly scoped
for a set of actions by specifying it on a ruleset. For example, if a
sandboxed process should not be able to :manpage:`connect(2)` to a
non-sandboxed process through abstract :manpage:`unix(7)` sockets, we can
specify such restriction with ``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET``.
Moreover, if a sandboxed process should not be able to send a signal to a
non-sandboxed process, we can specify this restriction with
``LANDLOCK_SCOPE_SIGNAL``.
A sandboxed process can connect to a non-sandboxed process when its domain is
not scoped. If a process's domain is scoped, it can only connect to sockets
created by processes in the same scope.
Moreover, If a process is scoped to send signal to a non-scoped process, it can
only send signals to processes in the same scope.
A connected datagram socket behaves like a stream socket when its domain is
scoped, meaning if the domain is scoped after the socket is connected , it can
still :manpage:`send(2)` data just like a stream socket. However, in the same
scenario, a non-connected datagram socket cannot send data (with
:manpage:`sendto(2)`) outside its scope.
A process with a scoped domain can inherit a socket created by a non-scoped
process. The process cannot connect to this socket since it has a scoped
domain.
IPC scoping does not support exceptions, so if a domain is scoped, no rules can
be added to allow access to resources or processes outside of the scope.
Truncating files
----------------
@ -404,7 +444,7 @@ Access rights
-------------
.. kernel-doc:: include/uapi/linux/landlock.h
:identifiers: fs_access net_access
:identifiers: fs_access net_access scope
Creating a new ruleset
----------------------
@ -541,6 +581,20 @@ earlier ABI.
Starting with the Landlock ABI version 5, it is possible to restrict the use of
:manpage:`ioctl(2)` using the new ``LANDLOCK_ACCESS_FS_IOCTL_DEV`` right.
Abstract UNIX socket scoping (ABI < 6)
--------------------------------------
Starting with the Landlock ABI version 6, it is possible to restrict
connections to an abstract :manpage:`unix(7)` socket by setting
``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET`` to the ``scoped`` ruleset attribute.
Signal scoping (ABI < 6)
------------------------
Starting with the Landlock ABI version 6, it is possible to restrict
:manpage:`signal(7)` sending by setting ``LANDLOCK_SCOPE_SIGNAL`` to the
``scoped`` ruleset attribute.
.. _kernel_support:
Kernel support

View 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,28 @@ 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).
* - %LANDLOCK_SCOPE_SIGNAL: Restrict a sandboxed process from sending a signal
* to another process outside the domain.
*/
/* clang-format off */
#define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0)
#define LANDLOCK_SCOPE_SIGNAL (1ULL << 1)
/* clang-format on*/
#endif /* _UAPI_LINUX_LANDLOCK_H */

View File

@ -14,6 +14,7 @@
#include <fcntl.h>
#include <linux/landlock.h>
#include <linux/prctl.h>
#include <linux/socket.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
@ -22,6 +23,7 @@
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdbool.h>
#ifndef landlock_create_ruleset
static inline int
@ -55,6 +57,7 @@ static inline int landlock_restrict_self(const int ruleset_fd,
#define ENV_FS_RW_NAME "LL_FS_RW"
#define ENV_TCP_BIND_NAME "LL_TCP_BIND"
#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
#define ENV_SCOPED_NAME "LL_SCOPED"
#define ENV_DELIMITER ":"
static int parse_path(char *env_path, const char ***const path_list)
@ -184,6 +187,55 @@ out_free_name:
return ret;
}
/* Returns true on error, false otherwise. */
static bool check_ruleset_scope(const char *const env_var,
struct landlock_ruleset_attr *ruleset_attr)
{
char *env_type_scope, *env_type_scope_next, *ipc_scoping_name;
bool error = false;
bool abstract_scoping = false;
bool signal_scoping = false;
/* Scoping is not supported by Landlock ABI */
if (!(ruleset_attr->scoped &
(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL)))
goto out_unset;
env_type_scope = getenv(env_var);
/* Scoping is not supported by the user */
if (!env_type_scope || strcmp("", env_type_scope) == 0)
goto out_unset;
env_type_scope = strdup(env_type_scope);
env_type_scope_next = env_type_scope;
while ((ipc_scoping_name =
strsep(&env_type_scope_next, ENV_DELIMITER))) {
if (strcmp("a", ipc_scoping_name) == 0 && !abstract_scoping) {
abstract_scoping = true;
} else if (strcmp("s", ipc_scoping_name) == 0 &&
!signal_scoping) {
signal_scoping = true;
} else {
fprintf(stderr, "Unknown or duplicate scope \"%s\"\n",
ipc_scoping_name);
error = true;
goto out_free_name;
}
}
out_free_name:
free(env_type_scope);
out_unset:
if (!abstract_scoping)
ruleset_attr->scoped &= ~LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
if (!signal_scoping)
ruleset_attr->scoped &= ~LANDLOCK_SCOPE_SIGNAL;
unsetenv(env_var);
return error;
}
/* clang-format off */
#define ACCESS_FS_ROUGHLY_READ ( \
@ -208,7 +260,7 @@ out_free_name:
/* clang-format on */
#define LANDLOCK_ABI_LAST 5
#define LANDLOCK_ABI_LAST 6
int main(const int argc, char *const argv[], char *const *const envp)
{
@ -223,14 +275,16 @@ int main(const int argc, char *const argv[], char *const *const envp)
.handled_access_fs = access_fs_rw,
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
LANDLOCK_ACCESS_NET_CONNECT_TCP,
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL,
};
if (argc < 2) {
fprintf(stderr,
"usage: %s=\"...\" %s=\"...\" %s=\"...\" %s=\"...\"%s "
"usage: %s=\"...\" %s=\"...\" %s=\"...\" %s=\"...\" %s=\"...\" %s "
"<cmd> [args]...\n\n",
ENV_FS_RO_NAME, ENV_FS_RW_NAME, ENV_TCP_BIND_NAME,
ENV_TCP_CONNECT_NAME, argv[0]);
ENV_TCP_CONNECT_NAME, ENV_SCOPED_NAME, argv[0]);
fprintf(stderr,
"Execute a command in a restricted environment.\n\n");
fprintf(stderr,
@ -251,15 +305,18 @@ int main(const int argc, char *const argv[], char *const *const envp)
fprintf(stderr,
"* %s: list of ports allowed to connect (client).\n",
ENV_TCP_CONNECT_NAME);
fprintf(stderr, "* %s: list of scoped IPCs.\n",
ENV_SCOPED_NAME);
fprintf(stderr,
"\nexample:\n"
"%s=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" "
"%s=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" "
"%s=\"9418\" "
"%s=\"80:443\" "
"%s=\"a:s\" "
"%s bash -i\n\n",
ENV_FS_RO_NAME, ENV_FS_RW_NAME, ENV_TCP_BIND_NAME,
ENV_TCP_CONNECT_NAME, argv[0]);
ENV_TCP_CONNECT_NAME, ENV_SCOPED_NAME, argv[0]);
fprintf(stderr,
"This sandboxer can use Landlock features "
"up to ABI version %d.\n",
@ -327,6 +384,11 @@ int main(const int argc, char *const argv[], char *const *const envp)
/* Removes LANDLOCK_ACCESS_FS_IOCTL_DEV for ABI < 5 */
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_IOCTL_DEV;
__attribute__((fallthrough));
case 5:
/* Removes LANDLOCK_SCOPE_* for ABI < 6 */
ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL);
fprintf(stderr,
"Hint: You should update the running kernel "
"to leverage Landlock features "
@ -358,6 +420,9 @@ int main(const int argc, char *const argv[], char *const *const envp)
~LANDLOCK_ACCESS_NET_CONNECT_TCP;
}
if (check_ruleset_scope(ENV_SCOPED_NAME, &ruleset_attr))
return 1;
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {

View File

@ -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;
}

View File

@ -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)

View File

@ -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;
};
/**

View 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_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 */

View 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;
}

View 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)[],

View 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);

View File

@ -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)

View 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,

View File

@ -7,6 +7,7 @@
* Copyright © 2021 Microsoft Corporation
*/
#include <arpa/inet.h>
#include <errno.h>
#include <linux/landlock.h>
#include <linux/securebits.h>
@ -14,11 +15,14 @@
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../kselftest_harness.h"
#define TMP_DIR "tmp"
#ifndef __maybe_unused
#define __maybe_unused __attribute__((__unused__))
#endif
@ -226,3 +230,38 @@ enforce_ruleset(struct __test_metadata *const _metadata, const int ruleset_fd)
TH_LOG("Failed to enforce ruleset: %s", strerror(errno));
}
}
struct protocol_variant {
int domain;
int type;
};
struct service_fixture {
struct protocol_variant protocol;
/* port is also stored in ipv4_addr.sin_port or ipv6_addr.sin6_port */
unsigned short port;
union {
struct sockaddr_in ipv4_addr;
struct sockaddr_in6 ipv6_addr;
struct {
struct sockaddr_un unix_addr;
socklen_t unix_addr_len;
};
};
};
static pid_t __maybe_unused sys_gettid(void)
{
return syscall(__NR_gettid);
}
static void __maybe_unused set_unix_address(struct service_fixture *const srv,
const unsigned short index)
{
srv->unix_addr.sun_family = AF_UNIX;
sprintf(srv->unix_addr.sun_path,
"_selftests-landlock-abstract-unix-tid%d-index%d", sys_gettid(),
index);
srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
srv->unix_addr.sun_path[0] = '\0';
}

View File

@ -59,7 +59,6 @@ int open_tree(int dfd, const char *filename, unsigned int flags)
#define RENAME_EXCHANGE (1 << 1)
#endif
#define TMP_DIR "tmp"
#define BINARY_PATH "./true"
/* Paths (sibling number and depth) */

View File

@ -36,30 +36,6 @@ enum sandbox_type {
TCP_SANDBOX,
};
struct protocol_variant {
int domain;
int type;
};
struct service_fixture {
struct protocol_variant protocol;
/* port is also stored in ipv4_addr.sin_port or ipv6_addr.sin6_port */
unsigned short port;
union {
struct sockaddr_in ipv4_addr;
struct sockaddr_in6 ipv6_addr;
struct {
struct sockaddr_un unix_addr;
socklen_t unix_addr_len;
};
};
};
static pid_t sys_gettid(void)
{
return syscall(__NR_gettid);
}
static int set_service(struct service_fixture *const srv,
const struct protocol_variant prot,
const unsigned short index)
@ -92,12 +68,7 @@ static int set_service(struct service_fixture *const srv,
return 0;
case AF_UNIX:
srv->unix_addr.sun_family = prot.domain;
sprintf(srv->unix_addr.sun_path,
"_selftests-landlock-net-tid%d-index%d", sys_gettid(),
index);
srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
srv->unix_addr.sun_path[0] = '\0';
set_unix_address(srv, index);
return 0;
}
return 1;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,156 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock scoped_domains variants
*
* See the hierarchy variants from ptrace_test.c
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
/* clang-format on */
FIXTURE_VARIANT(scoped_domains)
{
bool domain_both;
bool domain_parent;
bool domain_child;
};
/*
* No domain
*
* P1-. P1 -> P2 : allow
* \ P2 -> P1 : allow
* 'P2
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, without_domain) {
/* clang-format on */
.domain_both = false,
.domain_parent = false,
.domain_child = false,
};
/*
* Child domain
*
* P1--. P1 -> P2 : allow
* \ P2 -> P1 : deny
* .'-----.
* | P2 |
* '------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, child_domain) {
/* clang-format on */
.domain_both = false,
.domain_parent = false,
.domain_child = true,
};
/*
* Parent domain
* .------.
* | P1 --. P1 -> P2 : deny
* '------' \ P2 -> P1 : allow
* '
* P2
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, parent_domain) {
/* clang-format on */
.domain_both = false,
.domain_parent = true,
.domain_child = false,
};
/*
* Parent + child domain (siblings)
* .------.
* | P1 ---. P1 -> P2 : deny
* '------' \ P2 -> P1 : deny
* .---'--.
* | P2 |
* '------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, sibling_domain) {
/* clang-format on */
.domain_both = false,
.domain_parent = true,
.domain_child = true,
};
/*
* Same domain (inherited)
* .-------------.
* | P1----. | P1 -> P2 : allow
* | \ | P2 -> P1 : allow
* | ' |
* | P2 |
* '-------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, inherited_domain) {
/* clang-format on */
.domain_both = true,
.domain_parent = false,
.domain_child = false,
};
/*
* Inherited + child domain
* .-----------------.
* | P1----. | P1 -> P2 : allow
* | \ | P2 -> P1 : deny
* | .-'----. |
* | | P2 | |
* | '------' |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, nested_domain) {
/* clang-format on */
.domain_both = true,
.domain_parent = false,
.domain_child = true,
};
/*
* Inherited + parent domain
* .-----------------.
* |.------. | P1 -> P2 : deny
* || P1 ----. | P2 -> P1 : allow
* |'------' \ |
* | ' |
* | P2 |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, nested_and_parent_domain) {
/* clang-format on */
.domain_both = true,
.domain_parent = true,
.domain_child = false,
};
/*
* Inherited + parent and child domain (siblings)
* .-----------------.
* | .------. | P1 -> P2 : deny
* | | P1 . | P2 -> P1 : deny
* | '------'\ |
* | \ |
* | .--'---. |
* | | P2 | |
* | '------' |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, forked_domains) {
/* clang-format on */
.domain_both = true,
.domain_parent = true,
.domain_child = true,
};

View File

@ -0,0 +1,28 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock scope test helpers
*
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
#define _GNU_SOURCE
#include <sys/types.h>
static void create_scoped_domain(struct __test_metadata *const _metadata,
const __u16 scope)
{
int ruleset_fd;
const struct landlock_ruleset_attr ruleset_attr = {
.scoped = scope,
};
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd)
{
TH_LOG("Failed to create a ruleset: %s", strerror(errno));
}
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
}

View File

@ -0,0 +1,152 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock variants for three processes with various domains.
*
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
enum sandbox_type {
NO_SANDBOX,
SCOPE_SANDBOX,
/* Any other type of sandboxing domain */
OTHER_SANDBOX,
};
/* clang-format on */
FIXTURE_VARIANT(scoped_vs_unscoped)
{
const int domain_all;
const int domain_parent;
const int domain_children;
const int domain_child;
const int domain_grand_child;
};
/*
* .-----------------.
* | ####### | P3 -> P2 : allow
* | P1----# P2 # | P3 -> P1 : deny
* | # | # |
* | # P3 # |
* | ####### |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, deny_scoped) {
.domain_all = OTHER_SANDBOX,
.domain_parent = NO_SANDBOX,
.domain_children = SCOPE_SANDBOX,
.domain_child = NO_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* ###################
* # ####### # P3 -> P2 : allow
* # P1----# P2 # # P3 -> P1 : deny
* # # | # #
* # # P3 # #
* # ####### #
* ###################
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, all_scoped) {
.domain_all = SCOPE_SANDBOX,
.domain_parent = NO_SANDBOX,
.domain_children = SCOPE_SANDBOX,
.domain_child = NO_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* .-----------------.
* | .-----. | P3 -> P2 : allow
* | P1----| P2 | | P3 -> P1 : allow
* | | | |
* | | P3 | |
* | '-----' |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_other_domain) {
.domain_all = OTHER_SANDBOX,
.domain_parent = NO_SANDBOX,
.domain_children = OTHER_SANDBOX,
.domain_child = NO_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* .----. ###### P3 -> P2 : allow
* | P1 |----# P2 # P3 -> P1 : allow
* '----' ######
* |
* P3
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_one_domain) {
.domain_all = NO_SANDBOX,
.domain_parent = OTHER_SANDBOX,
.domain_children = NO_SANDBOX,
.domain_child = SCOPE_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* ###### .-----. P3 -> P2 : allow
* # P1 #----| P2 | P3 -> P1 : allow
* ###### '-----'
* |
* P3
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_grand_parent_scoped) {
.domain_all = NO_SANDBOX,
.domain_parent = SCOPE_SANDBOX,
.domain_children = NO_SANDBOX,
.domain_child = OTHER_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* ###### ###### P3 -> P2 : allow
* # P1 #----# P2 # P3 -> P1 : allow
* ###### ######
* |
* .----.
* | P3 |
* '----'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_parents_domain) {
.domain_all = NO_SANDBOX,
.domain_parent = SCOPE_SANDBOX,
.domain_children = NO_SANDBOX,
.domain_child = SCOPE_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* ###### P3 -> P2 : deny
* # P1 #----P2 P3 -> P1 : deny
* ###### |
* |
* ######
* # P3 #
* ######
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, deny_with_self_and_grandparent_domain) {
.domain_all = NO_SANDBOX,
.domain_parent = SCOPE_SANDBOX,
.domain_children = NO_SANDBOX,
.domain_child = NO_SANDBOX,
.domain_grand_child = SCOPE_SANDBOX,
/* clang-format on */
};

View File

@ -0,0 +1,484 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Signal Scoping
*
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <pthread.h>
#include <signal.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "common.h"
#include "scoped_common.h"
/* This variable is used for handling several signals. */
static volatile sig_atomic_t is_signaled;
/* clang-format off */
FIXTURE(scoping_signals) {};
/* clang-format on */
FIXTURE_VARIANT(scoping_signals)
{
int sig;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(scoping_signals, sigtrap) {
/* clang-format on */
.sig = SIGTRAP,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(scoping_signals, sigurg) {
/* clang-format on */
.sig = SIGURG,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(scoping_signals, sighup) {
/* clang-format on */
.sig = SIGHUP,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(scoping_signals, sigtstp) {
/* clang-format on */
.sig = SIGTSTP,
};
FIXTURE_SETUP(scoping_signals)
{
drop_caps(_metadata);
is_signaled = 0;
}
FIXTURE_TEARDOWN(scoping_signals)
{
}
static void scope_signal_handler(int sig, siginfo_t *info, void *ucontext)
{
if (sig == SIGTRAP || sig == SIGURG || sig == SIGHUP || sig == SIGTSTP)
is_signaled = 1;
}
/*
* In this test, a child process sends a signal to parent before and
* after getting scoped.
*/
TEST_F(scoping_signals, send_sig_to_parent)
{
int pipe_parent[2];
int status;
pid_t child;
pid_t parent = getpid();
struct sigaction action = {
.sa_sigaction = scope_signal_handler,
.sa_flags = SA_SIGINFO,
};
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
ASSERT_LE(0, sigaction(variant->sig, &action, NULL));
/* The process should not have already been signaled. */
EXPECT_EQ(0, is_signaled);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char buf_child;
int err;
EXPECT_EQ(0, close(pipe_parent[1]));
/*
* The child process can send signal to parent when
* domain is not scoped.
*/
err = kill(parent, variant->sig);
ASSERT_EQ(0, err);
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
EXPECT_EQ(0, close(pipe_parent[0]));
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
/*
* The child process cannot send signal to the parent
* anymore.
*/
err = kill(parent, variant->sig);
ASSERT_EQ(-1, err);
ASSERT_EQ(EPERM, errno);
/*
* No matter of the domain, a process should be able to
* send a signal to itself.
*/
ASSERT_EQ(0, is_signaled);
ASSERT_EQ(0, raise(variant->sig));
ASSERT_EQ(1, is_signaled);
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
/* Waits for a first signal to be received, without race condition. */
while (!is_signaled && !usleep(1))
;
ASSERT_EQ(1, is_signaled);
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(pipe_parent[1]));
is_signaled = 0;
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
EXPECT_EQ(0, is_signaled);
}
/* clang-format off */
FIXTURE(scoped_domains) {};
/* clang-format on */
#include "scoped_base_variants.h"
FIXTURE_SETUP(scoped_domains)
{
drop_caps(_metadata);
}
FIXTURE_TEARDOWN(scoped_domains)
{
}
/*
* This test ensures that a scoped process cannot send signal out of
* scoped domain.
*/
TEST_F(scoped_domains, check_access_signal)
{
pid_t child;
pid_t parent = getpid();
int status;
bool can_signal_child, can_signal_parent;
int pipe_parent[2], pipe_child[2];
char buf_parent;
int err;
can_signal_parent = !variant->domain_child;
can_signal_child = !variant->domain_parent;
if (variant->domain_both)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char buf_child;
EXPECT_EQ(0, close(pipe_child[0]));
EXPECT_EQ(0, close(pipe_parent[1]));
if (variant->domain_child)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
EXPECT_EQ(0, close(pipe_child[1]));
/* Waits for the parent to send signals. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
EXPECT_EQ(0, close(pipe_parent[0]));
err = kill(parent, 0);
if (can_signal_parent) {
ASSERT_EQ(0, err);
} else {
ASSERT_EQ(-1, err);
ASSERT_EQ(EPERM, errno);
}
/*
* No matter of the domain, a process should be able to
* send a signal to itself.
*/
ASSERT_EQ(0, raise(0));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(pipe_child[1]));
if (variant->domain_parent)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
EXPECT_EQ(0, close(pipe_child[0]));
err = kill(child, 0);
if (can_signal_child) {
ASSERT_EQ(0, err);
} else {
ASSERT_EQ(-1, err);
ASSERT_EQ(EPERM, errno);
}
ASSERT_EQ(0, raise(0));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(pipe_parent[1]));
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
static int thread_pipe[2];
enum thread_return {
THREAD_INVALID = 0,
THREAD_SUCCESS = 1,
THREAD_ERROR = 2,
};
void *thread_func(void *arg)
{
char buf;
if (read(thread_pipe[0], &buf, 1) != 1)
return (void *)THREAD_ERROR;
return (void *)THREAD_SUCCESS;
}
TEST(signal_scoping_threads)
{
pthread_t no_sandbox_thread, scoped_thread;
enum thread_return ret = THREAD_INVALID;
drop_caps(_metadata);
ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC));
ASSERT_EQ(0,
pthread_create(&no_sandbox_thread, NULL, thread_func, NULL));
/* Restricts the domain after creating the first thread. */
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(EPERM, pthread_kill(no_sandbox_thread, 0));
ASSERT_EQ(1, write(thread_pipe[1], ".", 1));
ASSERT_EQ(0, pthread_create(&scoped_thread, NULL, thread_func, NULL));
ASSERT_EQ(0, pthread_kill(scoped_thread, 0));
ASSERT_EQ(1, write(thread_pipe[1], ".", 1));
EXPECT_EQ(0, pthread_join(no_sandbox_thread, (void **)&ret));
EXPECT_EQ(THREAD_SUCCESS, ret);
EXPECT_EQ(0, pthread_join(scoped_thread, (void **)&ret));
EXPECT_EQ(THREAD_SUCCESS, ret);
EXPECT_EQ(0, close(thread_pipe[0]));
EXPECT_EQ(0, close(thread_pipe[1]));
}
const short backlog = 10;
static volatile sig_atomic_t signal_received;
static void handle_sigurg(int sig)
{
if (sig == SIGURG)
signal_received = 1;
else
signal_received = -1;
}
static int setup_signal_handler(int signal)
{
struct sigaction sa = {
.sa_handler = handle_sigurg,
};
if (sigemptyset(&sa.sa_mask))
return -1;
sa.sa_flags = SA_SIGINFO | SA_RESTART;
return sigaction(SIGURG, &sa, NULL);
}
/* clang-format off */
FIXTURE(fown) {};
/* clang-format on */
enum fown_sandbox {
SANDBOX_NONE,
SANDBOX_BEFORE_FORK,
SANDBOX_BEFORE_SETOWN,
SANDBOX_AFTER_SETOWN,
};
FIXTURE_VARIANT(fown)
{
const enum fown_sandbox sandbox_setown;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(fown, no_sandbox) {
/* clang-format on */
.sandbox_setown = SANDBOX_NONE,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(fown, sandbox_before_fork) {
/* clang-format on */
.sandbox_setown = SANDBOX_BEFORE_FORK,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(fown, sandbox_before_setown) {
/* clang-format on */
.sandbox_setown = SANDBOX_BEFORE_SETOWN,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(fown, sandbox_after_setown) {
/* clang-format on */
.sandbox_setown = SANDBOX_AFTER_SETOWN,
};
FIXTURE_SETUP(fown)
{
drop_caps(_metadata);
}
FIXTURE_TEARDOWN(fown)
{
}
/*
* Sending an out of bound message will trigger the SIGURG signal
* through file_send_sigiotask.
*/
TEST_F(fown, sigurg_socket)
{
int server_socket, recv_socket;
struct service_fixture server_address;
char buffer_parent;
int status;
int pipe_parent[2], pipe_child[2];
pid_t child;
memset(&server_address, 0, sizeof(server_address));
set_unix_address(&server_address, 0);
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
if (variant->sandbox_setown == SANDBOX_BEFORE_FORK)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int client_socket;
char buffer_child;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(pipe_child[0]));
ASSERT_EQ(0, setup_signal_handler(SIGURG));
client_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, client_socket);
/* Waits for the parent to listen. */
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
ASSERT_EQ(0, connect(client_socket, &server_address.unix_addr,
server_address.unix_addr_len));
/*
* Waits for the parent to accept the connection, sandbox
* itself, and call fcntl(2).
*/
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
/* May signal itself. */
ASSERT_EQ(1, send(client_socket, ".", 1, MSG_OOB));
EXPECT_EQ(0, close(client_socket));
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
EXPECT_EQ(0, close(pipe_child[1]));
/* Waits for the message to be received. */
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
EXPECT_EQ(0, close(pipe_parent[0]));
if (variant->sandbox_setown == SANDBOX_BEFORE_SETOWN) {
ASSERT_EQ(0, signal_received);
} else {
/*
* A signal is only received if fcntl(F_SETOWN) was
* called before any sandboxing or if the signal
* receiver is in the same domain.
*/
ASSERT_EQ(1, signal_received);
}
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(pipe_child[1]));
server_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, server_socket);
ASSERT_EQ(0, bind(server_socket, &server_address.unix_addr,
server_address.unix_addr_len));
ASSERT_EQ(0, listen(server_socket, backlog));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
recv_socket = accept(server_socket, NULL, NULL);
ASSERT_LE(0, recv_socket);
if (variant->sandbox_setown == SANDBOX_BEFORE_SETOWN)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
/*
* Sets the child to receive SIGURG for MSG_OOB. This uncommon use is
* a valid attack scenario which also simplifies this test.
*/
ASSERT_EQ(0, fcntl(recv_socket, F_SETOWN, child));
if (variant->sandbox_setown == SANDBOX_AFTER_SETOWN)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
/* Waits for the child to send MSG_OOB. */
ASSERT_EQ(1, read(pipe_child[0], &buffer_parent, 1));
EXPECT_EQ(0, close(pipe_child[0]));
ASSERT_EQ(1, recv(recv_socket, &buffer_parent, 1, MSG_OOB));
EXPECT_EQ(0, close(recv_socket));
EXPECT_EQ(0, close(server_socket));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(pipe_parent[1]));
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
TEST_HARNESS_MAIN

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Common scope restriction
*
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
#define _GNU_SOURCE
#include <errno.h>
#include <linux/landlock.h>
#include <sys/prctl.h>
#include "common.h"
#define ACCESS_LAST LANDLOCK_SCOPE_SIGNAL
TEST(ruleset_with_unknown_scope)
{
__u64 scoped_mask;
for (scoped_mask = 1ULL << 63; scoped_mask != ACCESS_LAST;
scoped_mask >>= 1) {
struct landlock_ruleset_attr ruleset_attr = {
.scoped = scoped_mask,
};
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0));
ASSERT_EQ(EINVAL, errno);
}
}
TEST_HARNESS_MAIN