diff options
author | Linus Torvalds <torvalds@linux-foundation.org> | 2024-09-24 20:40:11 +0300 |
---|---|---|
committer | Linus Torvalds <torvalds@linux-foundation.org> | 2024-09-24 20:40:11 +0300 |
commit | e1b061b444fb01c237838f0d8238653afe6a8094 (patch) | |
tree | a98ecdbdd1d490c1611073d886586a288f3ed498 | |
parent | 24f772dec31591f9268a9c9e4943dc5dc47eaf9b (diff) | |
parent | 1ca980815e1f284dddcb5e678c91bbd3e3f3a6a6 (diff) | |
download | linux-e1b061b444fb01c237838f0d8238653afe6a8094.tar.xz |
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
21 files changed, 2359 insertions, 47 deletions
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst index 37dafce8038b..c8d3e46badc5 100644 --- a/Documentation/userspace-api/landlock.rst +++ b/Documentation/userspace-api/landlock.rst @@ -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 diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h index 2c8dbc74b955..33745642f787 100644 --- a/include/uapi/linux/landlock.h +++ b/include/uapi/linux/landlock.h @@ -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 */ diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c index e8223c3e781a..f847e832ba14 100644 --- a/samples/landlock/sandboxer.c +++ b/samples/landlock/sandboxer.c @@ -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) { diff --git a/security/landlock/cred.h b/security/landlock/cred.h index af89ab00e6d1..bf755459838a 100644 --- a/security/landlock/cred.h +++ b/security/landlock/cred.h @@ -26,7 +26,7 @@ landlock_cred(const struct cred *cred) return cred->security + landlock_blob_sizes.lbs_cred; } -static inline const struct landlock_ruleset *landlock_get_current_domain(void) +static inline struct landlock_ruleset *landlock_get_current_domain(void) { return landlock_cred(current_cred())->domain; } diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 0804f76a67be..7d79fc8abe21 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1639,6 +1639,29 @@ static int hook_file_ioctl_compat(struct file *file, unsigned int cmd, return -EACCES; } +static void hook_file_set_fowner(struct file *file) +{ + struct landlock_ruleset *new_dom, *prev_dom; + + /* + * Lock already held by __f_setown(), see commit 26f204380a3c ("fs: Fix + * file_set_fowner LSM hook inconsistencies"). + */ + lockdep_assert_held(&file_f_owner(file)->lock); + new_dom = landlock_get_current_domain(); + landlock_get_ruleset(new_dom); + prev_dom = landlock_file(file)->fown_domain; + landlock_file(file)->fown_domain = new_dom; + + /* Called in an RCU read-side critical section. */ + landlock_put_ruleset_deferred(prev_dom); +} + +static void hook_file_free_security(struct file *file) +{ + landlock_put_ruleset_deferred(landlock_file(file)->fown_domain); +} + static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(inode_free_security_rcu, hook_inode_free_security_rcu), @@ -1663,6 +1686,8 @@ static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(file_truncate, hook_file_truncate), LSM_HOOK_INIT(file_ioctl, hook_file_ioctl), LSM_HOOK_INIT(file_ioctl_compat, hook_file_ioctl_compat), + LSM_HOOK_INIT(file_set_fowner, hook_file_set_fowner), + LSM_HOOK_INIT(file_free_security, hook_file_free_security), }; __init void landlock_add_fs_hooks(void) diff --git a/security/landlock/fs.h b/security/landlock/fs.h index 488e4813680a..1487e1f023a1 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -52,6 +52,13 @@ struct landlock_file_security { * needed to authorize later operations on the open file. */ access_mask_t allowed_access; + /** + * @fown_domain: Domain of the task that set the PID that may receive a + * signal e.g., SIGURG when writing MSG_OOB to the related socket. + * This pointer is protected by the related file->f_owner->lock, as for + * fown_struct's members: pid, uid, and euid. + */ + struct landlock_ruleset *fown_domain; }; /** diff --git a/security/landlock/limits.h b/security/landlock/limits.h index 4eb643077a2a..15f7606066c8 100644 --- a/security/landlock/limits.h +++ b/security/landlock/limits.h @@ -26,6 +26,9 @@ #define LANDLOCK_MASK_ACCESS_NET ((LANDLOCK_LAST_ACCESS_NET << 1) - 1) #define LANDLOCK_NUM_ACCESS_NET __const_hweight64(LANDLOCK_MASK_ACCESS_NET) +#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_SIGNAL +#define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1) +#define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE) /* clang-format on */ #endif /* _SECURITY_LANDLOCK_LIMITS_H */ diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index 6ff232f58618..a93bdbf52fff 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -52,12 +52,13 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers) struct landlock_ruleset * landlock_create_ruleset(const access_mask_t fs_access_mask, - const access_mask_t net_access_mask) + const access_mask_t net_access_mask, + const access_mask_t scope_mask) { struct landlock_ruleset *new_ruleset; /* Informs about useless ruleset. */ - if (!fs_access_mask && !net_access_mask) + if (!fs_access_mask && !net_access_mask && !scope_mask) return ERR_PTR(-ENOMSG); new_ruleset = create_ruleset(1); if (IS_ERR(new_ruleset)) @@ -66,6 +67,8 @@ landlock_create_ruleset(const access_mask_t fs_access_mask, landlock_add_fs_access_mask(new_ruleset, fs_access_mask, 0); if (net_access_mask) landlock_add_net_access_mask(new_ruleset, net_access_mask, 0); + if (scope_mask) + landlock_add_scope_mask(new_ruleset, scope_mask, 0); return new_ruleset; } diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index 0f1b5b4c8f6b..61bdbc550172 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -35,6 +35,8 @@ typedef u16 access_mask_t; static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS); /* Makes sure all network access rights can be stored. */ static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_NET); +/* Makes sure all scoped rights can be stored. */ +static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_SCOPE); /* Makes sure for_each_set_bit() and for_each_clear_bit() calls are OK. */ static_assert(sizeof(unsigned long) >= sizeof(access_mask_t)); @@ -42,6 +44,7 @@ static_assert(sizeof(unsigned long) >= sizeof(access_mask_t)); struct access_masks { access_mask_t fs : LANDLOCK_NUM_ACCESS_FS; access_mask_t net : LANDLOCK_NUM_ACCESS_NET; + access_mask_t scope : LANDLOCK_NUM_SCOPE; }; typedef u16 layer_mask_t; @@ -233,7 +236,8 @@ struct landlock_ruleset { struct landlock_ruleset * landlock_create_ruleset(const access_mask_t access_mask_fs, - const access_mask_t access_mask_net); + const access_mask_t access_mask_net, + const access_mask_t scope_mask); void landlock_put_ruleset(struct landlock_ruleset *const ruleset); void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset); @@ -280,6 +284,17 @@ landlock_add_net_access_mask(struct landlock_ruleset *const ruleset, ruleset->access_masks[layer_level].net |= net_mask; } +static inline void +landlock_add_scope_mask(struct landlock_ruleset *const ruleset, + const access_mask_t scope_mask, const u16 layer_level) +{ + access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE; + + /* Should already be checked in sys_landlock_create_ruleset(). */ + WARN_ON_ONCE(scope_mask != mask); + ruleset->access_masks[layer_level].scope |= mask; +} + static inline access_mask_t landlock_get_raw_fs_access_mask(const struct landlock_ruleset *const ruleset, const u16 layer_level) @@ -303,6 +318,13 @@ landlock_get_net_access_mask(const struct landlock_ruleset *const ruleset, return ruleset->access_masks[layer_level].net; } +static inline access_mask_t +landlock_get_scope_mask(const struct landlock_ruleset *const ruleset, + const u16 layer_level) +{ + return ruleset->access_masks[layer_level].scope; +} + bool landlock_unmask_layers(const struct landlock_rule *const rule, const access_mask_t access_request, layer_mask_t (*const layer_masks)[], diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index 00b63971ab64..f5a0e7182ec0 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -97,8 +97,9 @@ static void build_check_abi(void) */ ruleset_size = sizeof(ruleset_attr.handled_access_fs); ruleset_size += sizeof(ruleset_attr.handled_access_net); + ruleset_size += sizeof(ruleset_attr.scoped); BUILD_BUG_ON(sizeof(ruleset_attr) != ruleset_size); - BUILD_BUG_ON(sizeof(ruleset_attr) != 16); + BUILD_BUG_ON(sizeof(ruleset_attr) != 24); path_beneath_size = sizeof(path_beneath_attr.allowed_access); path_beneath_size += sizeof(path_beneath_attr.parent_fd); @@ -149,7 +150,7 @@ static const struct file_operations ruleset_fops = { .write = fop_dummy_write, }; -#define LANDLOCK_ABI_VERSION 5 +#define LANDLOCK_ABI_VERSION 6 /** * sys_landlock_create_ruleset - Create a new ruleset @@ -170,8 +171,9 @@ static const struct file_operations ruleset_fops = { * Possible returned errors are: * * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; - * - %EINVAL: unknown @flags, or unknown access, or too small @size; - * - %E2BIG or %EFAULT: @attr or @size inconsistencies; + * - %EINVAL: unknown @flags, or unknown access, or unknown scope, or too small @size; + * - %E2BIG: @attr or @size inconsistencies; + * - %EFAULT: @attr or @size inconsistencies; * - %ENOMSG: empty &landlock_ruleset_attr.handled_access_fs. */ SYSCALL_DEFINE3(landlock_create_ruleset, @@ -213,9 +215,14 @@ SYSCALL_DEFINE3(landlock_create_ruleset, LANDLOCK_MASK_ACCESS_NET) return -EINVAL; + /* Checks IPC scoping content (and 32-bits cast). */ + if ((ruleset_attr.scoped | LANDLOCK_MASK_SCOPE) != LANDLOCK_MASK_SCOPE) + return -EINVAL; + /* Checks arguments and transforms to kernel struct. */ ruleset = landlock_create_ruleset(ruleset_attr.handled_access_fs, - ruleset_attr.handled_access_net); + ruleset_attr.handled_access_net, + ruleset_attr.scoped); if (IS_ERR(ruleset)) return PTR_ERR(ruleset); diff --git a/security/landlock/task.c b/security/landlock/task.c index 849f5123610b..4acbd7c40eee 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -13,9 +13,12 @@ #include <linux/lsm_hooks.h> #include <linux/rcupdate.h> #include <linux/sched.h> +#include <net/af_unix.h> +#include <net/sock.h> #include "common.h" #include "cred.h" +#include "fs.h" #include "ruleset.h" #include "setup.h" #include "task.h" @@ -108,9 +111,199 @@ static int hook_ptrace_traceme(struct task_struct *const parent) return task_ptrace(parent, current); } +/** + * domain_is_scoped - Checks if the client domain is scoped in the same + * domain as the server. + * + * @client: IPC sender domain. + * @server: IPC receiver domain. + * @scope: The scope restriction criteria. + * + * Returns: True if the @client domain is scoped to access the @server, + * unless the @server is also scoped in the same domain as @client. + */ +static bool domain_is_scoped(const struct landlock_ruleset *const client, + const struct landlock_ruleset *const server, + access_mask_t scope) +{ + int client_layer, server_layer; + struct landlock_hierarchy *client_walker, *server_walker; + + /* Quick return if client has no domain */ + if (WARN_ON_ONCE(!client)) + return false; + + client_layer = client->num_layers - 1; + client_walker = client->hierarchy; + /* + * client_layer must be a signed integer with greater capacity + * than client->num_layers to ensure the following loop stops. + */ + BUILD_BUG_ON(sizeof(client_layer) > sizeof(client->num_layers)); + + server_layer = server ? (server->num_layers - 1) : -1; + server_walker = server ? server->hierarchy : NULL; + + /* + * Walks client's parent domains down to the same hierarchy level + * as the server's domain, and checks that none of these client's + * parent domains are scoped. + */ + for (; client_layer > server_layer; client_layer--) { + if (landlock_get_scope_mask(client, client_layer) & scope) + return true; + + client_walker = client_walker->parent; + } + /* + * Walks server's parent domains down to the same hierarchy level as + * the client's domain. + */ + for (; server_layer > client_layer; server_layer--) + server_walker = server_walker->parent; + + for (; client_layer >= 0; client_layer--) { + if (landlock_get_scope_mask(client, client_layer) & scope) { + /* + * Client and server are at the same level in the + * hierarchy. If the client is scoped, the request is + * only allowed if this domain is also a server's + * ancestor. + */ + return server_walker != client_walker; + } + client_walker = client_walker->parent; + server_walker = server_walker->parent; + } + return false; +} + +static bool sock_is_scoped(struct sock *const other, + const struct landlock_ruleset *const domain) +{ + const struct landlock_ruleset *dom_other; + + /* The credentials will not change. */ + lockdep_assert_held(&unix_sk(other)->lock); + dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain; + return domain_is_scoped(domain, dom_other, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); +} + +static bool is_abstract_socket(struct sock *const sock) +{ + struct unix_address *addr = unix_sk(sock)->addr; + + if (!addr) + return false; + + if (addr->len >= offsetof(struct sockaddr_un, sun_path) + 1 && + addr->name->sun_path[0] == '\0') + return true; + + return false; +} + +static int hook_unix_stream_connect(struct sock *const sock, + struct sock *const other, + struct sock *const newsk) +{ + const struct landlock_ruleset *const dom = + landlock_get_current_domain(); + + /* Quick return for non-landlocked tasks. */ + if (!dom) + return 0; + + if (is_abstract_socket(other) && sock_is_scoped(other, dom)) + return -EPERM; + + return 0; +} + +static int hook_unix_may_send(struct socket *const sock, + struct socket *const other) +{ + const struct landlock_ruleset *const dom = + landlock_get_current_domain(); + + if (!dom) + return 0; + + /* + * Checks if this datagram socket was already allowed to be connected + * to other. + */ + if (unix_peer(sock->sk) == other->sk) + return 0; + + if (is_abstract_socket(other->sk) && sock_is_scoped(other->sk, dom)) + return -EPERM; + + return 0; +} + +static int hook_task_kill(struct task_struct *const p, + struct kernel_siginfo *const info, const int sig, + const struct cred *const cred) +{ + bool is_scoped; + const struct landlock_ruleset *dom; + + if (cred) { + /* Dealing with USB IO. */ + dom = landlock_cred(cred)->domain; + } else { + dom = landlock_get_current_domain(); + } + + /* Quick return for non-landlocked tasks. */ + if (!dom) + return 0; + + rcu_read_lock(); + is_scoped = domain_is_scoped(dom, landlock_get_task_domain(p), + LANDLOCK_SCOPE_SIGNAL); + rcu_read_unlock(); + if (is_scoped) + return -EPERM; + + return 0; +} + +static int hook_file_send_sigiotask(struct task_struct *tsk, + struct fown_struct *fown, int signum) +{ + const struct landlock_ruleset *dom; + bool is_scoped = false; + + /* Lock already held by send_sigio() and send_sigurg(). */ + lockdep_assert_held(&fown->lock); + dom = landlock_file(fown->file)->fown_domain; + + /* Quick return for unowned socket. */ + if (!dom) + return 0; + + rcu_read_lock(); + is_scoped = domain_is_scoped(dom, landlock_get_task_domain(tsk), + LANDLOCK_SCOPE_SIGNAL); + rcu_read_unlock(); + if (is_scoped) + return -EPERM; + + return 0; +} + static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check), LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme), + + LSM_HOOK_INIT(unix_stream_connect, hook_unix_stream_connect), + LSM_HOOK_INIT(unix_may_send, hook_unix_may_send), + + LSM_HOOK_INIT(task_kill, hook_task_kill), + LSM_HOOK_INIT(file_send_sigiotask, hook_file_send_sigiotask), }; __init void landlock_add_task_hooks(void) diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c index 3b26bf3cf5b9..1bc16fde2e8a 100644 --- a/tools/testing/selftests/landlock/base_test.c +++ b/tools/testing/selftests/landlock/base_test.c @@ -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, diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h index 7e2b431b9f90..61056fa074bb 100644 --- a/tools/testing/selftests/landlock/common.h +++ b/tools/testing/selftests/landlock/common.h @@ -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'; +} diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index 7d063c652be1..6788762188fe 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -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) */ diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c index f21cfbbc3638..4e0aeb53b225 100644 --- a/tools/testing/selftests/landlock/net_test.c +++ b/tools/testing/selftests/landlock/net_test.c @@ -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; diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c new file mode 100644 index 000000000000..a6b59d2ab1b4 --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c @@ -0,0 +1,1041 @@ +// 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 diff --git a/tools/testing/selftests/landlock/scoped_base_variants.h b/tools/testing/selftests/landlock/scoped_base_variants.h new file mode 100644 index 000000000000..d3b1fa8a584e --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_base_variants.h @@ -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, +}; diff --git a/tools/testing/selftests/landlock/scoped_common.h b/tools/testing/selftests/landlock/scoped_common.h new file mode 100644 index 000000000000..a9a912d30c4d --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_common.h @@ -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)); +} diff --git a/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h b/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h new file mode 100644 index 000000000000..bcd9a83805d0 --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h @@ -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 */ +}; diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c new file mode 100644 index 000000000000..475ee62a832d --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_signal_test.c @@ -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 diff --git a/tools/testing/selftests/landlock/scoped_test.c b/tools/testing/selftests/landlock/scoped_test.c new file mode 100644 index 000000000000..b90f76ed0d9c --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_test.c @@ -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 |