Commit e1b061b4 authored by Linus Torvalds's avatar Linus Torvalds

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
parents 24f772de 1ca98081
...@@ -8,7 +8,7 @@ Landlock: unprivileged access control ...@@ -8,7 +8,7 @@ Landlock: unprivileged access control
===================================== =====================================
:Author: Mickaël Salaün :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 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 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. ...@@ -81,6 +81,9 @@ to be explicit about the denied-by-default access rights.
.handled_access_net = .handled_access_net =
LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_BIND_TCP |
LANDLOCK_ACCESS_NET_CONNECT_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 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: ...@@ -119,6 +122,11 @@ version, and only use the available subset of access rights:
case 4: case 4:
/* Removes LANDLOCK_ACCESS_FS_IOCTL_DEV for ABI < 5 */ /* Removes LANDLOCK_ACCESS_FS_IOCTL_DEV for ABI < 5 */
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_IOCTL_DEV; 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. 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 ...@@ -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, 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. 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 Truncating files
---------------- ----------------
...@@ -404,7 +444,7 @@ Access rights ...@@ -404,7 +444,7 @@ Access rights
------------- -------------
.. kernel-doc:: include/uapi/linux/landlock.h .. kernel-doc:: include/uapi/linux/landlock.h
:identifiers: fs_access net_access :identifiers: fs_access net_access scope
Creating a new ruleset Creating a new ruleset
---------------------- ----------------------
...@@ -541,6 +581,20 @@ earlier ABI. ...@@ -541,6 +581,20 @@ earlier ABI.
Starting with the Landlock ABI version 5, it is possible to restrict the use of 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. :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:
Kernel support Kernel support
......
...@@ -44,6 +44,12 @@ struct landlock_ruleset_attr { ...@@ -44,6 +44,12 @@ struct landlock_ruleset_attr {
* flags`_). * flags`_).
*/ */
__u64 handled_access_net; __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 { ...@@ -274,4 +280,28 @@ struct landlock_net_port_attr {
#define LANDLOCK_ACCESS_NET_BIND_TCP (1ULL << 0) #define LANDLOCK_ACCESS_NET_BIND_TCP (1ULL << 0)
#define LANDLOCK_ACCESS_NET_CONNECT_TCP (1ULL << 1) #define LANDLOCK_ACCESS_NET_CONNECT_TCP (1ULL << 1)
/* clang-format on */ /* 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 */ #endif /* _UAPI_LINUX_LANDLOCK_H */
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
#include <fcntl.h> #include <fcntl.h>
#include <linux/landlock.h> #include <linux/landlock.h>
#include <linux/prctl.h> #include <linux/prctl.h>
#include <linux/socket.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
...@@ -22,6 +23,7 @@ ...@@ -22,6 +23,7 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/syscall.h> #include <sys/syscall.h>
#include <unistd.h> #include <unistd.h>
#include <stdbool.h>
#ifndef landlock_create_ruleset #ifndef landlock_create_ruleset
static inline int static inline int
...@@ -55,6 +57,7 @@ static inline int landlock_restrict_self(const int ruleset_fd, ...@@ -55,6 +57,7 @@ static inline int landlock_restrict_self(const int ruleset_fd,
#define ENV_FS_RW_NAME "LL_FS_RW" #define ENV_FS_RW_NAME "LL_FS_RW"
#define ENV_TCP_BIND_NAME "LL_TCP_BIND" #define ENV_TCP_BIND_NAME "LL_TCP_BIND"
#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT" #define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
#define ENV_SCOPED_NAME "LL_SCOPED"
#define ENV_DELIMITER ":" #define ENV_DELIMITER ":"
static int parse_path(char *env_path, const char ***const path_list) static int parse_path(char *env_path, const char ***const path_list)
...@@ -184,6 +187,55 @@ static int populate_ruleset_net(const char *const env_var, const int ruleset_fd, ...@@ -184,6 +187,55 @@ static int populate_ruleset_net(const char *const env_var, const int ruleset_fd,
return ret; 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 */ /* clang-format off */
#define ACCESS_FS_ROUGHLY_READ ( \ #define ACCESS_FS_ROUGHLY_READ ( \
...@@ -208,7 +260,7 @@ static int populate_ruleset_net(const char *const env_var, const int ruleset_fd, ...@@ -208,7 +260,7 @@ static int populate_ruleset_net(const char *const env_var, const int ruleset_fd,
/* clang-format on */ /* 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) 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) ...@@ -223,14 +275,16 @@ int main(const int argc, char *const argv[], char *const *const envp)
.handled_access_fs = access_fs_rw, .handled_access_fs = access_fs_rw,
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
LANDLOCK_ACCESS_NET_CONNECT_TCP, LANDLOCK_ACCESS_NET_CONNECT_TCP,
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL,
}; };
if (argc < 2) { if (argc < 2) {
fprintf(stderr, fprintf(stderr,
"usage: %s=\"...\" %s=\"...\" %s=\"...\" %s=\"...\"%s " "usage: %s=\"...\" %s=\"...\" %s=\"...\" %s=\"...\" %s=\"...\" %s "
"<cmd> [args]...\n\n", "<cmd> [args]...\n\n",
ENV_FS_RO_NAME, ENV_FS_RW_NAME, ENV_TCP_BIND_NAME, 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, fprintf(stderr,
"Execute a command in a restricted environment.\n\n"); "Execute a command in a restricted environment.\n\n");
fprintf(stderr, fprintf(stderr,
...@@ -251,15 +305,18 @@ int main(const int argc, char *const argv[], char *const *const envp) ...@@ -251,15 +305,18 @@ int main(const int argc, char *const argv[], char *const *const envp)
fprintf(stderr, fprintf(stderr,
"* %s: list of ports allowed to connect (client).\n", "* %s: list of ports allowed to connect (client).\n",
ENV_TCP_CONNECT_NAME); ENV_TCP_CONNECT_NAME);
fprintf(stderr, "* %s: list of scoped IPCs.\n",
ENV_SCOPED_NAME);
fprintf(stderr, fprintf(stderr,
"\nexample:\n" "\nexample:\n"
"%s=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" " "%s=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" "
"%s=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" " "%s=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" "
"%s=\"9418\" " "%s=\"9418\" "
"%s=\"80:443\" " "%s=\"80:443\" "
"%s=\"a:s\" "
"%s bash -i\n\n", "%s bash -i\n\n",
ENV_FS_RO_NAME, ENV_FS_RW_NAME, ENV_TCP_BIND_NAME, 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, fprintf(stderr,
"This sandboxer can use Landlock features " "This sandboxer can use Landlock features "
"up to ABI version %d.\n", "up to ABI version %d.\n",
...@@ -327,6 +384,11 @@ int main(const int argc, char *const argv[], char *const *const envp) ...@@ -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 */ /* Removes LANDLOCK_ACCESS_FS_IOCTL_DEV for ABI < 5 */
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_IOCTL_DEV; 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, fprintf(stderr,
"Hint: You should update the running kernel " "Hint: You should update the running kernel "
"to leverage Landlock features " "to leverage Landlock features "
...@@ -358,6 +420,9 @@ int main(const int argc, char *const argv[], char *const *const envp) ...@@ -358,6 +420,9 @@ int main(const int argc, char *const argv[], char *const *const envp)
~LANDLOCK_ACCESS_NET_CONNECT_TCP; ~LANDLOCK_ACCESS_NET_CONNECT_TCP;
} }
if (check_ruleset_scope(ENV_SCOPED_NAME, &ruleset_attr))
return 1;
ruleset_fd = ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) { if (ruleset_fd < 0) {
......
...@@ -26,7 +26,7 @@ landlock_cred(const struct cred *cred) ...@@ -26,7 +26,7 @@ landlock_cred(const struct cred *cred)
return cred->security + landlock_blob_sizes.lbs_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; return landlock_cred(current_cred())->domain;
} }
......
...@@ -1639,6 +1639,29 @@ static int hook_file_ioctl_compat(struct file *file, unsigned int cmd, ...@@ -1639,6 +1639,29 @@ static int hook_file_ioctl_compat(struct file *file, unsigned int cmd,
return -EACCES; 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 = { static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(inode_free_security_rcu, hook_inode_free_security_rcu), 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 = { ...@@ -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_truncate, hook_file_truncate),
LSM_HOOK_INIT(file_ioctl, hook_file_ioctl), LSM_HOOK_INIT(file_ioctl, hook_file_ioctl),
LSM_HOOK_INIT(file_ioctl_compat, hook_file_ioctl_compat), 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) __init void landlock_add_fs_hooks(void)
......
...@@ -52,6 +52,13 @@ struct landlock_file_security { ...@@ -52,6 +52,13 @@ struct landlock_file_security {
* needed to authorize later operations on the open file. * needed to authorize later operations on the open file.
*/ */
access_mask_t allowed_access; 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;
}; };
/** /**
......
...@@ -26,6 +26,9 @@ ...@@ -26,6 +26,9 @@
#define LANDLOCK_MASK_ACCESS_NET ((LANDLOCK_LAST_ACCESS_NET << 1) - 1) #define LANDLOCK_MASK_ACCESS_NET ((LANDLOCK_LAST_ACCESS_NET << 1) - 1)
#define LANDLOCK_NUM_ACCESS_NET __const_hweight64(LANDLOCK_MASK_ACCESS_NET) #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 */ /* clang-format on */
#endif /* _SECURITY_LANDLOCK_LIMITS_H */ #endif /* _SECURITY_LANDLOCK_LIMITS_H */
...@@ -52,12 +52,13 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers) ...@@ -52,12 +52,13 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers)
struct landlock_ruleset * struct landlock_ruleset *
landlock_create_ruleset(const access_mask_t fs_access_mask, 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; struct landlock_ruleset *new_ruleset;
/* Informs about useless 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); return ERR_PTR(-ENOMSG);
new_ruleset = create_ruleset(1); new_ruleset = create_ruleset(1);
if (IS_ERR(new_ruleset)) if (IS_ERR(new_ruleset))
...@@ -66,6 +67,8 @@ landlock_create_ruleset(const access_mask_t fs_access_mask, ...@@ -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); landlock_add_fs_access_mask(new_ruleset, fs_access_mask, 0);
if (net_access_mask) if (net_access_mask)
landlock_add_net_access_mask(new_ruleset, net_access_mask, 0); 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; return new_ruleset;
} }
......
...@@ -35,6 +35,8 @@ typedef u16 access_mask_t; ...@@ -35,6 +35,8 @@ typedef u16 access_mask_t;
static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS); static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS);
/* Makes sure all network access rights can be stored. */ /* Makes sure all network access rights can be stored. */
static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_NET); 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. */ /* Makes sure for_each_set_bit() and for_each_clear_bit() calls are OK. */
static_assert(sizeof(unsigned long) >= sizeof(access_mask_t)); static_assert(sizeof(unsigned long) >= sizeof(access_mask_t));
...@@ -42,6 +44,7 @@ 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 { struct access_masks {
access_mask_t fs : LANDLOCK_NUM_ACCESS_FS; access_mask_t fs : LANDLOCK_NUM_ACCESS_FS;
access_mask_t net : LANDLOCK_NUM_ACCESS_NET; access_mask_t net : LANDLOCK_NUM_ACCESS_NET;
access_mask_t scope : LANDLOCK_NUM_SCOPE;
}; };
typedef u16 layer_mask_t; typedef u16 layer_mask_t;
...@@ -233,7 +236,8 @@ struct landlock_ruleset { ...@@ -233,7 +236,8 @@ struct landlock_ruleset {
struct landlock_ruleset * struct landlock_ruleset *
landlock_create_ruleset(const access_mask_t access_mask_fs, 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(struct landlock_ruleset *const ruleset);
void landlock_put_ruleset_deferred(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, ...@@ -280,6 +284,17 @@ landlock_add_net_access_mask(struct landlock_ruleset *const ruleset,
ruleset->access_masks[layer_level].net |= net_mask; 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 static inline access_mask_t
landlock_get_raw_fs_access_mask(const struct landlock_ruleset *const ruleset, landlock_get_raw_fs_access_mask(const struct landlock_ruleset *const ruleset,
const u16 layer_level) const u16 layer_level)
...@@ -303,6 +318,13 @@ landlock_get_net_access_mask(const struct landlock_ruleset *const ruleset, ...@@ -303,6 +318,13 @@ landlock_get_net_access_mask(const struct landlock_ruleset *const ruleset,
return ruleset->access_masks[layer_level].net; 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, bool landlock_unmask_layers(const struct landlock_rule *const rule,
const access_mask_t access_request, const access_mask_t access_request,
layer_mask_t (*const layer_masks)[], layer_mask_t (*const layer_masks)[],
......
...@@ -97,8 +97,9 @@ static void build_check_abi(void) ...@@ -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_fs);
ruleset_size += sizeof(ruleset_attr.handled_access_net); 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) != 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.allowed_access);
path_beneath_size += sizeof(path_beneath_attr.parent_fd); path_beneath_size += sizeof(path_beneath_attr.parent_fd);
...@@ -149,7 +150,7 @@ static const struct file_operations ruleset_fops = { ...@@ -149,7 +150,7 @@ static const struct file_operations ruleset_fops = {
.write = fop_dummy_write, .write = fop_dummy_write,
}; };
#define LANDLOCK_ABI_VERSION 5 #define LANDLOCK_ABI_VERSION 6
/** /**
* sys_landlock_create_ruleset - Create a new ruleset * sys_landlock_create_ruleset - Create a new ruleset
...@@ -170,8 +171,9 @@ static const struct file_operations ruleset_fops = { ...@@ -170,8 +171,9 @@ static const struct file_operations ruleset_fops = {
* Possible returned errors are: * Possible returned errors are:
* *
* - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
* - %EINVAL: unknown @flags, or unknown access, or too small @size; * - %EINVAL: unknown @flags, or unknown access, or unknown scope, or too small @size;
* - %E2BIG or %EFAULT: @attr or @size inconsistencies; * - %E2BIG: @attr or @size inconsistencies;
* - %EFAULT: @attr or @size inconsistencies;
* - %ENOMSG: empty &landlock_ruleset_attr.handled_access_fs. * - %ENOMSG: empty &landlock_ruleset_attr.handled_access_fs.
*/ */
SYSCALL_DEFINE3(landlock_create_ruleset, SYSCALL_DEFINE3(landlock_create_ruleset,
...@@ -213,9 +215,14 @@ SYSCALL_DEFINE3(landlock_create_ruleset, ...@@ -213,9 +215,14 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
LANDLOCK_MASK_ACCESS_NET) LANDLOCK_MASK_ACCESS_NET)
return -EINVAL; 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. */ /* Checks arguments and transforms to kernel struct. */
ruleset = landlock_create_ruleset(ruleset_attr.handled_access_fs, 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)) if (IS_ERR(ruleset))
return PTR_ERR(ruleset); return PTR_ERR(ruleset);
......
...@@ -13,9 +13,12 @@ ...@@ -13,9 +13,12 @@
#include <linux/lsm_hooks.h> #include <linux/lsm_hooks.h>
#include <linux/rcupdate.h> #include <linux/rcupdate.h>
#include <linux/sched.h> #include <linux/sched.h>
#include <net/af_unix.h>
#include <net/sock.h>
#include "common.h" #include "common.h"
#include "cred.h" #include "cred.h"
#include "fs.h"
#include "ruleset.h" #include "ruleset.h"
#include "setup.h" #include "setup.h"
#include "task.h" #include "task.h"
...@@ -108,9 +111,199 @@ static int hook_ptrace_traceme(struct task_struct *const parent) ...@@ -108,9 +111,199 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
return task_ptrace(parent, current); 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 = { static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check), LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check),
LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme), 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) __init void landlock_add_task_hooks(void)
......
...@@ -76,7 +76,7 @@ TEST(abi_version) ...@@ -76,7 +76,7 @@ TEST(abi_version)
const struct landlock_ruleset_attr ruleset_attr = { const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, .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)); LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
* Copyright © 2021 Microsoft Corporation * Copyright © 2021 Microsoft Corporation
*/ */
#include <arpa/inet.h>
#include <errno.h> #include <errno.h>
#include <linux/landlock.h> #include <linux/landlock.h>
#include <linux/securebits.h> #include <linux/securebits.h>
...@@ -14,11 +15,14 @@ ...@@ -14,11 +15,14 @@
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/syscall.h> #include <sys/syscall.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
#include "../kselftest_harness.h" #include "../kselftest_harness.h"
#define TMP_DIR "tmp"
#ifndef __maybe_unused #ifndef __maybe_unused
#define __maybe_unused __attribute__((__unused__)) #define __maybe_unused __attribute__((__unused__))
#endif #endif
...@@ -226,3 +230,38 @@ enforce_ruleset(struct __test_metadata *const _metadata, const int ruleset_fd) ...@@ -226,3 +230,38 @@ enforce_ruleset(struct __test_metadata *const _metadata, const int ruleset_fd)
TH_LOG("Failed to enforce ruleset: %s", strerror(errno)); 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';
}
...@@ -59,7 +59,6 @@ int open_tree(int dfd, const char *filename, unsigned int flags) ...@@ -59,7 +59,6 @@ int open_tree(int dfd, const char *filename, unsigned int flags)
#define RENAME_EXCHANGE (1 << 1) #define RENAME_EXCHANGE (1 << 1)
#endif #endif
#define TMP_DIR "tmp"
#define BINARY_PATH "./true" #define BINARY_PATH "./true"
/* Paths (sibling number and depth) */ /* Paths (sibling number and depth) */
......
...@@ -36,30 +36,6 @@ enum sandbox_type { ...@@ -36,30 +36,6 @@ enum sandbox_type {
TCP_SANDBOX, 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, static int set_service(struct service_fixture *const srv,
const struct protocol_variant prot, const struct protocol_variant prot,
const unsigned short index) const unsigned short index)
...@@ -92,12 +68,7 @@ static int set_service(struct service_fixture *const srv, ...@@ -92,12 +68,7 @@ static int set_service(struct service_fixture *const srv,
return 0; return 0;
case AF_UNIX: case AF_UNIX:
srv->unix_addr.sun_family = prot.domain; set_unix_address(srv, index);
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';
return 0; return 0;
} }
return 1; return 1;
......
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Abstract UNIX socket
*
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <sched.h>
#include <signal.h>
#include <stddef.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>
#include "common.h"
#include "scoped_common.h"
/* Number of pending connections queue to be hold. */
const short backlog = 10;
static void create_fs_domain(struct __test_metadata *const _metadata)
{
int ruleset_fd;
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
};
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
EXPECT_LE(0, ruleset_fd)
{
TH_LOG("Failed to create a ruleset: %s", strerror(errno));
}
EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
EXPECT_EQ(0, close(ruleset_fd));
}
FIXTURE(scoped_domains)
{
struct service_fixture stream_address, dgram_address;
};
#include "scoped_base_variants.h"
FIXTURE_SETUP(scoped_domains)
{
drop_caps(_metadata);
memset(&self->stream_address, 0, sizeof(self->stream_address));
memset(&self->dgram_address, 0, sizeof(self->dgram_address));
set_unix_address(&self->stream_address, 0);
set_unix_address(&self->dgram_address, 1);
}
FIXTURE_TEARDOWN(scoped_domains)
{
}
/*
* Test unix_stream_connect() and unix_may_send() for a child connecting to its
* parent, when they have scoped domain or no domain.
*/
TEST_F(scoped_domains, connect_to_parent)
{
pid_t child;
bool can_connect_to_parent;
int status;
int pipe_parent[2];
int stream_server, dgram_server;
/*
* can_connect_to_parent is true if a child process can connect to its
* parent process. This depends on the child process not being isolated
* from the parent with a dedicated Landlock domain.
*/
can_connect_to_parent = !variant->domain_child;
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
if (variant->domain_both) {
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
if (!__test_passed(_metadata))
return;
}
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int err;
int stream_client, dgram_client;
char buf_child;
EXPECT_EQ(0, close(pipe_parent[1]));
if (variant->domain_child)
create_scoped_domain(
_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
stream_client = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_client);
dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_client);
/* Waits for the server. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
err = connect(stream_client, &self->stream_address.unix_addr,
self->stream_address.unix_addr_len);
if (can_connect_to_parent) {
EXPECT_EQ(0, err);
} else {
EXPECT_EQ(-1, err);
EXPECT_EQ(EPERM, errno);
}
EXPECT_EQ(0, close(stream_client));
err = connect(dgram_client, &self->dgram_address.unix_addr,
self->dgram_address.unix_addr_len);
if (can_connect_to_parent) {
EXPECT_EQ(0, err);
} else {
EXPECT_EQ(-1, err);
EXPECT_EQ(EPERM, errno);
}
EXPECT_EQ(0, close(dgram_client));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
if (variant->domain_parent)
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
stream_server = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_server);
dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_server);
ASSERT_EQ(0, bind(stream_server, &self->stream_address.unix_addr,
self->stream_address.unix_addr_len));
ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr,
self->dgram_address.unix_addr_len));
ASSERT_EQ(0, listen(stream_server, backlog));
/* Signals to child that the parent is listening. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
ASSERT_EQ(child, waitpid(child, &status, 0));
EXPECT_EQ(0, close(stream_server));
EXPECT_EQ(0, close(dgram_server));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
/*
* Test unix_stream_connect() and unix_may_send() for a parent connecting to
* its child, when they have scoped domain or no domain.
*/
TEST_F(scoped_domains, connect_to_child)
{
pid_t child;
bool can_connect_to_child;
int err_stream, err_dgram, errno_stream, errno_dgram, status;
int pipe_child[2], pipe_parent[2];
char buf;
int stream_client, dgram_client;
/*
* can_connect_to_child is true if a parent process can connect to its
* child process. The parent process is not isolated from the child
* with a dedicated Landlock domain.
*/
can_connect_to_child = !variant->domain_parent;
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
if (variant->domain_both) {
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
if (!__test_passed(_metadata))
return;
}
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int stream_server, dgram_server;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(pipe_child[0]));
if (variant->domain_child)
create_scoped_domain(
_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
/* Waits for the parent to be in a domain, if any. */
ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
stream_server = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_server);
dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_server);
ASSERT_EQ(0,
bind(stream_server, &self->stream_address.unix_addr,
self->stream_address.unix_addr_len));
ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr,
self->dgram_address.unix_addr_len));
ASSERT_EQ(0, listen(stream_server, backlog));
/* Signals to the parent that child is listening. */
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
/* Waits to connect. */
ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
EXPECT_EQ(0, close(stream_server));
EXPECT_EQ(0, close(dgram_server));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_child[1]));
EXPECT_EQ(0, close(pipe_parent[0]));
if (variant->domain_parent)
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
/* Signals that the parent is in a domain, if any. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
stream_client = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_client);
dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_client);
/* Waits for the child to listen */
ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
err_stream = connect(stream_client, &self->stream_address.unix_addr,
self->stream_address.unix_addr_len);
errno_stream = errno;
err_dgram = connect(dgram_client, &self->dgram_address.unix_addr,
self->dgram_address.unix_addr_len);
errno_dgram = errno;
if (can_connect_to_child) {
EXPECT_EQ(0, err_stream);
EXPECT_EQ(0, err_dgram);
} else {
EXPECT_EQ(-1, err_stream);
EXPECT_EQ(-1, err_dgram);
EXPECT_EQ(EPERM, errno_stream);
EXPECT_EQ(EPERM, errno_dgram);
}
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(stream_client));
EXPECT_EQ(0, close(dgram_client));
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
FIXTURE(scoped_vs_unscoped)
{
struct service_fixture parent_stream_address, parent_dgram_address,
child_stream_address, child_dgram_address;
};
#include "scoped_multiple_domain_variants.h"
FIXTURE_SETUP(scoped_vs_unscoped)
{
drop_caps(_metadata);
memset(&self->parent_stream_address, 0,
sizeof(self->parent_stream_address));
set_unix_address(&self->parent_stream_address, 0);
memset(&self->parent_dgram_address, 0,
sizeof(self->parent_dgram_address));
set_unix_address(&self->parent_dgram_address, 1);
memset(&self->child_stream_address, 0,
sizeof(self->child_stream_address));
set_unix_address(&self->child_stream_address, 2);
memset(&self->child_dgram_address, 0,
sizeof(self->child_dgram_address));
set_unix_address(&self->child_dgram_address, 3);
}
FIXTURE_TEARDOWN(scoped_vs_unscoped)
{
}
/*
* Test unix_stream_connect and unix_may_send for parent, child and
* grand child processes when they can have scoped or non-scoped domains.
*/
TEST_F(scoped_vs_unscoped, unix_scoping)
{
pid_t child;
int status;
bool can_connect_to_parent, can_connect_to_child;
int pipe_parent[2];
int stream_server_parent, dgram_server_parent;
can_connect_to_child = (variant->domain_grand_child != SCOPE_SANDBOX);
can_connect_to_parent = (can_connect_to_child &&
(variant->domain_children != SCOPE_SANDBOX));
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
if (variant->domain_all == OTHER_SANDBOX)
create_fs_domain(_metadata);
else if (variant->domain_all == SCOPE_SANDBOX)
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int stream_server_child, dgram_server_child;
int pipe_child[2];
pid_t grand_child;
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
if (variant->domain_children == OTHER_SANDBOX)
create_fs_domain(_metadata);
else if (variant->domain_children == SCOPE_SANDBOX)
create_scoped_domain(
_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
grand_child = fork();
ASSERT_LE(0, grand_child);
if (grand_child == 0) {
char buf;
int stream_err, dgram_err, stream_errno, dgram_errno;
int stream_client, dgram_client;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(pipe_child[1]));
if (variant->domain_grand_child == OTHER_SANDBOX)
create_fs_domain(_metadata);
else if (variant->domain_grand_child == SCOPE_SANDBOX)
create_scoped_domain(
_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
stream_client = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_client);
dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_client);
ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
stream_err = connect(
stream_client,
&self->child_stream_address.unix_addr,
self->child_stream_address.unix_addr_len);
stream_errno = errno;
dgram_err = connect(
dgram_client,
&self->child_dgram_address.unix_addr,
self->child_dgram_address.unix_addr_len);
dgram_errno = errno;
if (can_connect_to_child) {
EXPECT_EQ(0, stream_err);
EXPECT_EQ(0, dgram_err);
} else {
EXPECT_EQ(-1, stream_err);
EXPECT_EQ(-1, dgram_err);
EXPECT_EQ(EPERM, stream_errno);
EXPECT_EQ(EPERM, dgram_errno);
}
EXPECT_EQ(0, close(stream_client));
stream_client = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_client);
/* Datagram sockets can "reconnect". */
ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
stream_err = connect(
stream_client,
&self->parent_stream_address.unix_addr,
self->parent_stream_address.unix_addr_len);
stream_errno = errno;
dgram_err = connect(
dgram_client,
&self->parent_dgram_address.unix_addr,
self->parent_dgram_address.unix_addr_len);
dgram_errno = errno;
if (can_connect_to_parent) {
EXPECT_EQ(0, stream_err);
EXPECT_EQ(0, dgram_err);
} else {
EXPECT_EQ(-1, stream_err);
EXPECT_EQ(-1, dgram_err);
EXPECT_EQ(EPERM, stream_errno);
EXPECT_EQ(EPERM, dgram_errno);
}
EXPECT_EQ(0, close(stream_client));
EXPECT_EQ(0, close(dgram_client));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_child[0]));
if (variant->domain_child == OTHER_SANDBOX)
create_fs_domain(_metadata);
else if (variant->domain_child == SCOPE_SANDBOX)
create_scoped_domain(
_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
stream_server_child = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_server_child);
dgram_server_child = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_server_child);
ASSERT_EQ(0, bind(stream_server_child,
&self->child_stream_address.unix_addr,
self->child_stream_address.unix_addr_len));
ASSERT_EQ(0, bind(dgram_server_child,
&self->child_dgram_address.unix_addr,
self->child_dgram_address.unix_addr_len));
ASSERT_EQ(0, listen(stream_server_child, backlog));
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
ASSERT_EQ(grand_child, waitpid(grand_child, &status, 0));
EXPECT_EQ(0, close(stream_server_child))
EXPECT_EQ(0, close(dgram_server_child));
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
if (variant->domain_parent == OTHER_SANDBOX)
create_fs_domain(_metadata);
else if (variant->domain_parent == SCOPE_SANDBOX)
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
stream_server_parent = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_server_parent);
dgram_server_parent = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_server_parent);
ASSERT_EQ(0, bind(stream_server_parent,
&self->parent_stream_address.unix_addr,
self->parent_stream_address.unix_addr_len));
ASSERT_EQ(0, bind(dgram_server_parent,
&self->parent_dgram_address.unix_addr,
self->parent_dgram_address.unix_addr_len));
ASSERT_EQ(0, listen(stream_server_parent, backlog));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
ASSERT_EQ(child, waitpid(child, &status, 0));
EXPECT_EQ(0, close(stream_server_parent));
EXPECT_EQ(0, close(dgram_server_parent));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
FIXTURE(outside_socket)
{
struct service_fixture address, transit_address;
};
FIXTURE_VARIANT(outside_socket)
{
const bool child_socket;
const int type;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(outside_socket, allow_dgram_child) {
/* clang-format on */
.child_socket = true,
.type = SOCK_DGRAM,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(outside_socket, deny_dgram_server) {
/* clang-format on */
.child_socket = false,
.type = SOCK_DGRAM,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(outside_socket, allow_stream_child) {
/* clang-format on */
.child_socket = true,
.type = SOCK_STREAM,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(outside_socket, deny_stream_server) {
/* clang-format on */
.child_socket = false,
.type = SOCK_STREAM,
};
FIXTURE_SETUP(outside_socket)
{
drop_caps(_metadata);
memset(&self->transit_address, 0, sizeof(self->transit_address));
set_unix_address(&self->transit_address, 0);
memset(&self->address, 0, sizeof(self->address));
set_unix_address(&self->address, 1);
}
FIXTURE_TEARDOWN(outside_socket)
{
}
/*
* Test unix_stream_connect and unix_may_send for parent and child processes
* when connecting socket has different domain than the process using it.
*/
TEST_F(outside_socket, socket_with_different_domain)
{
pid_t child;
int err, status;
int pipe_child[2], pipe_parent[2];
char buf_parent;
int server_socket;
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int client_socket;
char buf_child;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(pipe_child[0]));
/* Client always has a domain. */
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
if (variant->child_socket) {
int data_socket, passed_socket, stream_server;
passed_socket = socket(AF_UNIX, variant->type, 0);
ASSERT_LE(0, passed_socket);
stream_server = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_server);
ASSERT_EQ(0, bind(stream_server,
&self->transit_address.unix_addr,
self->transit_address.unix_addr_len));
ASSERT_EQ(0, listen(stream_server, backlog));
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
data_socket = accept(stream_server, NULL, NULL);
ASSERT_LE(0, data_socket);
ASSERT_EQ(0, send_fd(data_socket, passed_socket));
EXPECT_EQ(0, close(passed_socket));
EXPECT_EQ(0, close(stream_server));
}
client_socket = socket(AF_UNIX, variant->type, 0);
ASSERT_LE(0, client_socket);
/* Waits for parent signal for connection. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
err = connect(client_socket, &self->address.unix_addr,
self->address.unix_addr_len);
if (variant->child_socket) {
EXPECT_EQ(0, err);
} else {
EXPECT_EQ(-1, err);
EXPECT_EQ(EPERM, errno);
}
EXPECT_EQ(0, close(client_socket));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_child[1]));
EXPECT_EQ(0, close(pipe_parent[0]));
if (variant->child_socket) {
int client_child = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, client_child);
ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
ASSERT_EQ(0, connect(client_child,
&self->transit_address.unix_addr,
self->transit_address.unix_addr_len));
server_socket = recv_fd(client_child);
EXPECT_EQ(0, close(client_child));
} else {
server_socket = socket(AF_UNIX, variant->type, 0);
}
ASSERT_LE(0, server_socket);
/* Server always has a domain. */
create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
ASSERT_EQ(0, bind(server_socket, &self->address.unix_addr,
self->address.unix_addr_len));
if (variant->type == SOCK_STREAM)
ASSERT_EQ(0, listen(server_socket, backlog));
/* Signals to child that the parent is listening. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
ASSERT_EQ(child, waitpid(child, &status, 0));
EXPECT_EQ(0, close(server_socket));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
static const char stream_path[] = TMP_DIR "/stream.sock";
static const char dgram_path[] = TMP_DIR "/dgram.sock";
/* clang-format off */
FIXTURE(various_address_sockets) {};
/* clang-format on */
FIXTURE_VARIANT(various_address_sockets)
{
const int domain;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_scoped_domain) {
/* clang-format on */
.domain = SCOPE_SANDBOX,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_other_domain) {
/* clang-format on */
.domain = OTHER_SANDBOX,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_no_domain) {
/* clang-format on */
.domain = NO_SANDBOX,
};
FIXTURE_SETUP(various_address_sockets)
{
drop_caps(_metadata);
umask(0077);
ASSERT_EQ(0, mkdir(TMP_DIR, 0700));
}
FIXTURE_TEARDOWN(various_address_sockets)
{
EXPECT_EQ(0, unlink(stream_path));
EXPECT_EQ(0, unlink(dgram_path));
EXPECT_EQ(0, rmdir(TMP_DIR));
}
TEST_F(various_address_sockets, scoped_pathname_sockets)
{
socklen_t size_stream, size_dgram;
pid_t child;
int status;
char buf_child, buf_parent;
int pipe_parent[2];
int unnamed_sockets[2];
int stream_pathname_socket, dgram_pathname_socket,
stream_abstract_socket, dgram_abstract_socket, data_socket;
struct service_fixture stream_abstract_addr, dgram_abstract_addr;
struct sockaddr_un stream_pathname_addr = {
.sun_family = AF_UNIX,
};
struct sockaddr_un dgram_pathname_addr = {
.sun_family = AF_UNIX,
};
/* Pathname address. */
snprintf(stream_pathname_addr.sun_path,
sizeof(stream_pathname_addr.sun_path), "%s", stream_path);
size_stream = offsetof(struct sockaddr_un, sun_path) +
strlen(stream_pathname_addr.sun_path);
snprintf(dgram_pathname_addr.sun_path,
sizeof(dgram_pathname_addr.sun_path), "%s", dgram_path);
size_dgram = offsetof(struct sockaddr_un, sun_path) +
strlen(dgram_pathname_addr.sun_path);
/* Abstract address. */
memset(&stream_abstract_addr, 0, sizeof(stream_abstract_addr));
set_unix_address(&stream_abstract_addr, 0);
memset(&dgram_abstract_addr, 0, sizeof(dgram_abstract_addr));
set_unix_address(&dgram_abstract_addr, 1);
/* Unnamed address for datagram socket. */
ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM, 0, unnamed_sockets));
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int err;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(unnamed_sockets[1]));
if (variant->domain == SCOPE_SANDBOX)
create_scoped_domain(
_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
else if (variant->domain == OTHER_SANDBOX)
create_fs_domain(_metadata);
/* Waits for parent to listen. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
EXPECT_EQ(0, close(pipe_parent[0]));
/* Checks that we can send data through a datagram socket. */
ASSERT_EQ(1, write(unnamed_sockets[0], "a", 1));
EXPECT_EQ(0, close(unnamed_sockets[0]));
/* Connects with pathname sockets. */
stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_pathname_socket);
ASSERT_EQ(0, connect(stream_pathname_socket,
&stream_pathname_addr, size_stream));
ASSERT_EQ(1, write(stream_pathname_socket, "b", 1));
EXPECT_EQ(0, close(stream_pathname_socket));
/* Sends without connection. */
dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_pathname_socket);
err = sendto(dgram_pathname_socket, "c", 1, 0,
&dgram_pathname_addr, size_dgram);
EXPECT_EQ(1, err);
/* Sends with connection. */
ASSERT_EQ(0, connect(dgram_pathname_socket,
&dgram_pathname_addr, size_dgram));
ASSERT_EQ(1, write(dgram_pathname_socket, "d", 1));
EXPECT_EQ(0, close(dgram_pathname_socket));
/* Connects with abstract sockets. */
stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_abstract_socket);
err = connect(stream_abstract_socket,
&stream_abstract_addr.unix_addr,
stream_abstract_addr.unix_addr_len);
if (variant->domain == SCOPE_SANDBOX) {
EXPECT_EQ(-1, err);
EXPECT_EQ(EPERM, errno);
} else {
EXPECT_EQ(0, err);
ASSERT_EQ(1, write(stream_abstract_socket, "e", 1));
}
EXPECT_EQ(0, close(stream_abstract_socket));
/* Sends without connection. */
dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_abstract_socket);
err = sendto(dgram_abstract_socket, "f", 1, 0,
&dgram_abstract_addr.unix_addr,
dgram_abstract_addr.unix_addr_len);
if (variant->domain == SCOPE_SANDBOX) {
EXPECT_EQ(-1, err);
EXPECT_EQ(EPERM, errno);
} else {
EXPECT_EQ(1, err);
}
/* Sends with connection. */
err = connect(dgram_abstract_socket,
&dgram_abstract_addr.unix_addr,
dgram_abstract_addr.unix_addr_len);
if (variant->domain == SCOPE_SANDBOX) {
EXPECT_EQ(-1, err);
EXPECT_EQ(EPERM, errno);
} else {
EXPECT_EQ(0, err);
ASSERT_EQ(1, write(dgram_abstract_socket, "g", 1));
}
EXPECT_EQ(0, close(dgram_abstract_socket));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(unnamed_sockets[0]));
/* Sets up pathname servers. */
stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_pathname_socket);
ASSERT_EQ(0, bind(stream_pathname_socket, &stream_pathname_addr,
size_stream));
ASSERT_EQ(0, listen(stream_pathname_socket, backlog));
dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_pathname_socket);
ASSERT_EQ(0, bind(dgram_pathname_socket, &dgram_pathname_addr,
size_dgram));
/* Sets up abstract servers. */
stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, stream_abstract_socket);
ASSERT_EQ(0,
bind(stream_abstract_socket, &stream_abstract_addr.unix_addr,
stream_abstract_addr.unix_addr_len));
dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, dgram_abstract_socket);
ASSERT_EQ(0, bind(dgram_abstract_socket, &dgram_abstract_addr.unix_addr,
dgram_abstract_addr.unix_addr_len));
ASSERT_EQ(0, listen(stream_abstract_socket, backlog));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(pipe_parent[1]));
/* Reads from unnamed socket. */
ASSERT_EQ(1, read(unnamed_sockets[1], &buf_parent, sizeof(buf_parent)));
ASSERT_EQ('a', buf_parent);
EXPECT_LE(0, close(unnamed_sockets[1]));
/* Reads from pathname sockets. */
data_socket = accept(stream_pathname_socket, NULL, NULL);
ASSERT_LE(0, data_socket);
ASSERT_EQ(1, read(data_socket, &buf_parent, sizeof(buf_parent)));
ASSERT_EQ('b', buf_parent);
EXPECT_EQ(0, close(data_socket));
EXPECT_EQ(0, close(stream_pathname_socket));
ASSERT_EQ(1,
read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent)));
ASSERT_EQ('c', buf_parent);
ASSERT_EQ(1,
read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent)));
ASSERT_EQ('d', buf_parent);
EXPECT_EQ(0, close(dgram_pathname_socket));
if (variant->domain != SCOPE_SANDBOX) {
/* Reads from abstract sockets if allowed to send. */
data_socket = accept(stream_abstract_socket, NULL, NULL);
ASSERT_LE(0, data_socket);
ASSERT_EQ(1,
read(data_socket, &buf_parent, sizeof(buf_parent)));
ASSERT_EQ('e', buf_parent);
EXPECT_EQ(0, close(data_socket));
ASSERT_EQ(1, read(dgram_abstract_socket, &buf_parent,
sizeof(buf_parent)));
ASSERT_EQ('f', buf_parent);
ASSERT_EQ(1, read(dgram_abstract_socket, &buf_parent,
sizeof(buf_parent)));
ASSERT_EQ('g', buf_parent);
}
/* Waits for all abstract socket tests. */
ASSERT_EQ(child, waitpid(child, &status, 0));
EXPECT_EQ(0, close(stream_abstract_socket));
EXPECT_EQ(0, close(dgram_abstract_socket));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
TEST(datagram_sockets)
{
struct service_fixture connected_addr, non_connected_addr;
int server_conn_socket, server_unconn_socket;
int pipe_parent[2], pipe_child[2];
int status;
char buf;
pid_t child;
drop_caps(_metadata);
memset(&connected_addr, 0, sizeof(connected_addr));
set_unix_address(&connected_addr, 0);
memset(&non_connected_addr, 0, sizeof(non_connected_addr));
set_unix_address(&non_connected_addr, 1);
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) {
int client_conn_socket, client_unconn_socket;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(pipe_child[0]));
client_conn_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
client_unconn_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, client_conn_socket);
ASSERT_LE(0, client_unconn_socket);
/* Waits for parent to listen. */
ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
ASSERT_EQ(0,
connect(client_conn_socket, &connected_addr.unix_addr,
connected_addr.unix_addr_len));
/*
* Both connected and non-connected sockets can send data when
* the domain is not scoped.
*/
ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0));
ASSERT_EQ(1, sendto(client_unconn_socket, ".", 1, 0,
&non_connected_addr.unix_addr,
non_connected_addr.unix_addr_len));
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
/* Scopes the domain. */
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
/*
* Connected socket sends data to the receiver, but the
* non-connected socket must fail to send data.
*/
ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0));
ASSERT_EQ(-1, sendto(client_unconn_socket, ".", 1, 0,
&non_connected_addr.unix_addr,
non_connected_addr.unix_addr_len));
ASSERT_EQ(EPERM, errno);
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
EXPECT_EQ(0, close(client_conn_socket));
EXPECT_EQ(0, close(client_unconn_socket));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(pipe_child[1]));
server_conn_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
server_unconn_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, server_conn_socket);
ASSERT_LE(0, server_unconn_socket);
ASSERT_EQ(0, bind(server_conn_socket, &connected_addr.unix_addr,
connected_addr.unix_addr_len));
ASSERT_EQ(0, bind(server_unconn_socket, &non_connected_addr.unix_addr,
non_connected_addr.unix_addr_len));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
/* Waits for child to test. */
ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
ASSERT_EQ(1, recv(server_conn_socket, &buf, 1, 0));
ASSERT_EQ(1, recv(server_unconn_socket, &buf, 1, 0));
/*
* Connected datagram socket will receive data, but
* non-connected datagram socket does not receive data.
*/
ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
ASSERT_EQ(1, recv(server_conn_socket, &buf, 1, 0));
/* Waits for all tests to finish. */
ASSERT_EQ(child, waitpid(child, &status, 0));
EXPECT_EQ(0, close(server_conn_socket));
EXPECT_EQ(0, close(server_unconn_socket));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
TEST(self_connect)
{
struct service_fixture connected_addr, non_connected_addr;
int connected_socket, non_connected_socket, status;
pid_t child;
drop_caps(_metadata);
memset(&connected_addr, 0, sizeof(connected_addr));
set_unix_address(&connected_addr, 0);
memset(&non_connected_addr, 0, sizeof(non_connected_addr));
set_unix_address(&non_connected_addr, 1);
connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
non_connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
ASSERT_LE(0, connected_socket);
ASSERT_LE(0, non_connected_socket);
ASSERT_EQ(0, bind(connected_socket, &connected_addr.unix_addr,
connected_addr.unix_addr_len));
ASSERT_EQ(0, bind(non_connected_socket, &non_connected_addr.unix_addr,
non_connected_addr.unix_addr_len));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
/* Child's domain is scoped. */
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
/*
* The child inherits the sockets, and cannot connect or
* send data to them.
*/
ASSERT_EQ(-1,
connect(connected_socket, &connected_addr.unix_addr,
connected_addr.unix_addr_len));
ASSERT_EQ(EPERM, errno);
ASSERT_EQ(-1, sendto(connected_socket, ".", 1, 0,
&connected_addr.unix_addr,
connected_addr.unix_addr_len));
ASSERT_EQ(EPERM, errno);
ASSERT_EQ(-1, sendto(non_connected_socket, ".", 1, 0,
&non_connected_addr.unix_addr,
non_connected_addr.unix_addr_len));
ASSERT_EQ(EPERM, errno);
EXPECT_EQ(0, close(connected_socket));
EXPECT_EQ(0, close(non_connected_socket));
_exit(_metadata->exit_code);
return;
}
/* Waits for all tests to finish. */
ASSERT_EQ(child, waitpid(child, &status, 0));
EXPECT_EQ(0, close(connected_socket));
EXPECT_EQ(0, close(non_connected_socket));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
TEST_HARNESS_MAIN
/* 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,
};
/* 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));
}
/* 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 */
};
// 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
// 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment