From patchwork Tue Dec 1 19:23:11 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 335416 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-21.7 required=3.0 tests=BAYES_00, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, MENTIONS_GIT_HOSTING, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 18FD6C83020 for ; Tue, 1 Dec 2020 19:26:09 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id C4C4B20758 for ; Tue, 1 Dec 2020 19:26:08 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S2392387AbgLATYb (ORCPT ); Tue, 1 Dec 2020 14:24:31 -0500 Received: from smtp-190a.mail.infomaniak.ch ([185.125.25.10]:39511 "EHLO smtp-190a.mail.infomaniak.ch" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S2392378AbgLATYR (ORCPT ); Tue, 1 Dec 2020 14:24:17 -0500 Received: from smtp-3-0000.mail.infomaniak.ch (unknown [10.4.36.107]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4ClsTD0f8kzlh8Tt; Tue, 1 Dec 2020 20:23:28 +0100 (CET) Received: from localhost (unknown [23.97.221.149]) by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4ClsTC4NGNzlh8TC; Tue, 1 Dec 2020 20:23:27 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: James Morris , Jann Horn , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Al Viro , Andy Lutomirski , Anton Ivanov , Arnd Bergmann , Casey Schaufler , Jeff Dike , Jonathan Corbet , Kees Cook , Michael Kerrisk , Richard Weinberger , Shuah Khan , Vincent Dagonneau , kernel-hardening@lists.openwall.com, linux-api@vger.kernel.org, linux-arch@vger.kernel.org, linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, linux-security-module@vger.kernel.org, x86@kernel.org, =?utf-8?q?Mick?= =?utf-8?b?YcOrbCBTYWxhw7xu?= Subject: [PATCH v25 01/12] landlock: Add object management Date: Tue, 1 Dec 2020 20:23:11 +0100 Message-Id: <20201201192322.213239-2-mic@digikod.net> X-Mailer: git-send-email 2.29.2 In-Reply-To: <20201201192322.213239-1-mic@digikod.net> References: <20201201192322.213239-1-mic@digikod.net> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org From: Mickaël Salaün A Landlock object enables to identify a kernel object (e.g. an inode). A Landlock rule is a set of access rights allowed on an object. Rules are grouped in rulesets that may be tied to a set of processes (i.e. subjects) to enforce a scoped access-control (i.e. a domain). Because Landlock's goal is to empower any process (especially unprivileged ones) to sandbox themselves, we cannot rely on a system-wide object identification such as file extended attributes. Indeed, we need innocuous, composable and modular access-controls. The main challenge with these constraints is to identify kernel objects while this identification is useful (i.e. when a security policy makes use of this object). But this identification data should be freed once no policy is using it. This ephemeral tagging should not and may not be written in the filesystem. We then need to manage the lifetime of a rule according to the lifetime of its objects. To avoid a global lock, this implementation make use of RCU and counters to safely reference objects. A following commit uses this generic object management for inodes. Cc: James Morris Cc: Kees Cook Cc: Serge E. Hallyn Signed-off-by: Mickaël Salaün Reviewed-by: Jann Horn --- Changes since v24: * Fix typo in comment (spotted by Jann Horn). * Add Reviewed-by: Jann Horn Changes since v23: * Update landlock_create_object() to return error codes instead of NULL. This help error handling in callers. * When using make oldconfig with a previous configuration already including the CONFIG_LSM variable, no question is asked to update its content. Update the Kconfig help to warn about LSM stacking configuration. * Constify variable (spotted by Vincent Dagonneau). Changes since v22: * Fix spelling (spotted by Jann Horn). Changes since v21: * Update Kconfig help. * Clean up comments. Changes since v18: * Account objects to kmemcg. Changes since v14: * Simplify the object, rule and ruleset management at the expense of a less aggressive memory freeing (contributed by Jann Horn, with additional modifications): - Remove object->list aggregating the rules tied to an object. - Remove landlock_get_object(), landlock_drop_object(), {get,put}_object_cleaner() and landlock_rule_is_disabled(). - Rewrite landlock_put_object() to use a more simple mechanism (no tricky RCU). - Replace enum landlock_object_type and landlock_release_object() with landlock_object_underops->release() - Adjust unions and Sparse annotations. Cf. https://lore.kernel.org/lkml/CAG48ez21bEn0wL1bbmTiiu8j9jP5iEWtHOwz4tURUJ+ki0ydYw@mail.gmail.com/ * Merge struct landlock_rule into landlock_ruleset_elem to simplify the rule management. * Constify variables. * Improve kernel documentation. * Cosmetic variable renames. * Remove the "default" in the Kconfig (suggested by Jann Horn). * Only use refcount_inc() through getter helpers. * Update Kconfig description. Changes since v13: * New dedicated implementation, removing the need for eBPF. Previous changes: https://lore.kernel.org/lkml/20190721213116.23476-6-mic@digikod.net/ --- MAINTAINERS | 10 +++++ security/Kconfig | 1 + security/Makefile | 2 + security/landlock/Kconfig | 21 +++++++++ security/landlock/Makefile | 3 ++ security/landlock/object.c | 67 ++++++++++++++++++++++++++++ security/landlock/object.h | 91 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 195 insertions(+) create mode 100644 security/landlock/Kconfig create mode 100644 security/landlock/Makefile create mode 100644 security/landlock/object.c create mode 100644 security/landlock/object.h diff --git a/MAINTAINERS b/MAINTAINERS index 2daa6ee673f7..9fce015d8102 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9821,6 +9821,16 @@ F: net/core/sock_map.c F: net/ipv4/tcp_bpf.c F: net/ipv4/udp_bpf.c +LANDLOCK SECURITY MODULE +M: Mickaël Salaün +L: linux-security-module@vger.kernel.org +S: Supported +W: https://landlock.io +T: git https://github.com/landlock-lsm/linux.git +F: security/landlock/ +K: landlock +K: LANDLOCK + LANTIQ / INTEL Ethernet drivers M: Hauke Mehrtens L: netdev@vger.kernel.org diff --git a/security/Kconfig b/security/Kconfig index 7561f6f99f1d..15a4342b5d01 100644 --- a/security/Kconfig +++ b/security/Kconfig @@ -238,6 +238,7 @@ source "security/loadpin/Kconfig" source "security/yama/Kconfig" source "security/safesetid/Kconfig" source "security/lockdown/Kconfig" +source "security/landlock/Kconfig" source "security/integrity/Kconfig" diff --git a/security/Makefile b/security/Makefile index 3baf435de541..c688f4907a1b 100644 --- a/security/Makefile +++ b/security/Makefile @@ -13,6 +13,7 @@ subdir-$(CONFIG_SECURITY_LOADPIN) += loadpin subdir-$(CONFIG_SECURITY_SAFESETID) += safesetid subdir-$(CONFIG_SECURITY_LOCKDOWN_LSM) += lockdown subdir-$(CONFIG_BPF_LSM) += bpf +subdir-$(CONFIG_SECURITY_LANDLOCK) += landlock # always enable default capabilities obj-y += commoncap.o @@ -32,6 +33,7 @@ obj-$(CONFIG_SECURITY_SAFESETID) += safesetid/ obj-$(CONFIG_SECURITY_LOCKDOWN_LSM) += lockdown/ obj-$(CONFIG_CGROUPS) += device_cgroup.o obj-$(CONFIG_BPF_LSM) += bpf/ +obj-$(CONFIG_SECURITY_LANDLOCK) += landlock/ # Object integrity file lists subdir-$(CONFIG_INTEGRITY) += integrity diff --git a/security/landlock/Kconfig b/security/landlock/Kconfig new file mode 100644 index 000000000000..ea58e6208afa --- /dev/null +++ b/security/landlock/Kconfig @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: GPL-2.0-only + +config SECURITY_LANDLOCK + bool "Landlock support" + depends on SECURITY + select SECURITY_PATH + help + Landlock is a safe sandboxing mechanism which enables processes to + restrict themselves (and their future children) by gradually + enforcing tailored access control policies. A security policy is a + set of access rights (e.g. open a file in read-only, make a + directory, etc.) tied to a file hierarchy. Such policy can be configured + and enforced by any processes for themselves thanks to dedicated system + calls: landlock_create_ruleset(), landlock_add_rule(), and + landlock_enforce_ruleset_current(). + + See Documentation/userspace-api/landlock.rst for further information. + + If you are unsure how to answer this question, answer N. Otherwise, you + should also prepend "landlock," to the content of CONFIG_LSM to enable + Landlock at boot time. diff --git a/security/landlock/Makefile b/security/landlock/Makefile new file mode 100644 index 000000000000..cb6deefbf4c0 --- /dev/null +++ b/security/landlock/Makefile @@ -0,0 +1,3 @@ +obj-$(CONFIG_SECURITY_LANDLOCK) := landlock.o + +landlock-y := object.o diff --git a/security/landlock/object.c b/security/landlock/object.c new file mode 100644 index 000000000000..d674fdf9ff04 --- /dev/null +++ b/security/landlock/object.c @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Object management + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "object.h" + +struct landlock_object *landlock_create_object( + const struct landlock_object_underops *const underops, + void *const underobj) +{ + struct landlock_object *new_object; + + if (WARN_ON_ONCE(!underops || !underobj)) + return ERR_PTR(-ENOENT); + new_object = kzalloc(sizeof(*new_object), GFP_KERNEL_ACCOUNT); + if (!new_object) + return ERR_PTR(-ENOMEM); + refcount_set(&new_object->usage, 1); + spin_lock_init(&new_object->lock); + new_object->underops = underops; + new_object->underobj = underobj; + return new_object; +} + +/* + * The caller must own the object (i.e. thanks to object->usage) to safely put + * it. + */ +void landlock_put_object(struct landlock_object *const object) +{ + /* + * The call to @object->underops->release(object) might sleep, e.g. + * because of iput(). + */ + might_sleep(); + if (!object) + return; + + /* + * If the @object's refcount cannot drop to zero, we can just decrement + * the refcount without holding a lock. Otherwise, the decrement must + * happen under @object->lock for synchronization with things like + * get_inode_object(). + */ + if (refcount_dec_and_lock(&object->usage, &object->lock)) { + __acquire(&object->lock); + /* + * With @object->lock initially held, remove the reference from + * @object->underobj to @object (if it still exists). + */ + object->underops->release(object); + kfree_rcu(object, rcu_free); + } +} diff --git a/security/landlock/object.h b/security/landlock/object.h new file mode 100644 index 000000000000..1d6edbf939e2 --- /dev/null +++ b/security/landlock/object.h @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Object management + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_OBJECT_H +#define _SECURITY_LANDLOCK_OBJECT_H + +#include +#include +#include + +struct landlock_object; + +/** + * struct landlock_object_underops - Operations on an underlying object + */ +struct landlock_object_underops { + /** + * @release: Releases the underlying object (e.g. iput() for an inode). + */ + void (*release)(struct landlock_object *const object) + __releases(object->lock); +}; + +/** + * struct landlock_object - Security blob tied to a kernel object + * + * The goal of this structure is to enable to tie a set of ephemeral access + * rights (pertaining to different domains) to a kernel object (e.g an inode) + * in a safe way. This imply to handle concurrent use and modification. + * + * The lifetime of a &struct landlock_object depends of the rules referring to + * it. + */ +struct landlock_object { + /** + * @usage: This counter is used to tie an object to the rules matching + * it or to keep it alive while adding a new rule. If this counter + * reaches zero, this struct must not be modified, but this counter can + * still be read from within an RCU read-side critical section. When + * adding a new rule to an object with a usage counter of zero, we must + * wait until the pointer to this object is set to NULL (or recycled). + */ + refcount_t usage; + /** + * @lock: Guards against concurrent modifications. This lock must be + * held from the time @usage drops to zero until any weak references + * from @underobj to this object have been cleaned up. + * + * Lock ordering: inode->i_lock nests inside this. + */ + spinlock_t lock; + /** + * @underobj: Used when cleaning up an object and to mark an object as + * tied to its underlying kernel structure. This pointer is protected + * by @lock. Cf. landlock_release_inodes() and release_inode(). + */ + void *underobj; + union { + /** + * @rcu_free: Enables lockless use of @usage, @lock and + * @underobj from within an RCU read-side critical section. + * @rcu_free and @underops are only used by + * landlock_put_object(). + */ + struct rcu_head rcu_free; + /** + * @underops: Enables landlock_put_object() to release the + * underlying object (e.g. inode). + */ + const struct landlock_object_underops *underops; + }; +}; + +struct landlock_object *landlock_create_object( + const struct landlock_object_underops *const underops, + void *const underobj); + +void landlock_put_object(struct landlock_object *const object); + +static inline void landlock_get_object(struct landlock_object *const object) +{ + if (object) + refcount_inc(&object->usage); +} + +#endif /* _SECURITY_LANDLOCK_OBJECT_H */ From patchwork Tue Dec 1 19:23:13 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 335414 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-16.7 required=3.0 tests=BAYES_00, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 55F8DC83019 for ; Tue, 1 Dec 2020 19:26:08 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 2382B20758 for ; Tue, 1 Dec 2020 19:26:08 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S2392407AbgLATYb (ORCPT ); Tue, 1 Dec 2020 14:24:31 -0500 Received: from smtp-1908.mail.infomaniak.ch ([185.125.25.8]:54883 "EHLO smtp-1908.mail.infomaniak.ch" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S2392392AbgLATYT (ORCPT ); Tue, 1 Dec 2020 14:24:19 -0500 Received: from smtp-3-0001.mail.infomaniak.ch (unknown [10.4.36.108]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4ClsTH563vzlhggH; Tue, 1 Dec 2020 20:23:31 +0100 (CET) Received: from localhost (unknown [23.97.221.149]) by smtp-3-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4ClsTH2791zlh8T5; Tue, 1 Dec 2020 20:23:31 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: James Morris , Jann Horn , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Al Viro , Andy Lutomirski , Anton Ivanov , Arnd Bergmann , Casey Schaufler , Jeff Dike , Jonathan Corbet , Kees Cook , Michael Kerrisk , Richard Weinberger , Shuah Khan , Vincent Dagonneau , kernel-hardening@lists.openwall.com, linux-api@vger.kernel.org, linux-arch@vger.kernel.org, linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, linux-security-module@vger.kernel.org, x86@kernel.org, =?utf-8?q?Mick?= =?utf-8?b?YcOrbCBTYWxhw7xu?= Subject: [PATCH v25 03/12] landlock: Set up the security framework and manage credentials Date: Tue, 1 Dec 2020 20:23:13 +0100 Message-Id: <20201201192322.213239-4-mic@digikod.net> X-Mailer: git-send-email 2.29.2 In-Reply-To: <20201201192322.213239-1-mic@digikod.net> References: <20201201192322.213239-1-mic@digikod.net> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org From: Mickaël Salaün Process's credentials point to a Landlock domain, which is underneath implemented with a ruleset. In the following commits, this domain is used to check and enforce the ptrace and filesystem security policies. A domain is inherited from a parent to its child the same way a thread inherits a seccomp policy. Cc: James Morris Cc: Kees Cook Cc: Serge E. Hallyn Signed-off-by: Mickaël Salaün Reviewed-by: Jann Horn --- Changes since v23: * Add an early check for the current domain in hook_cred_free() to avoid superfluous call. * Cosmetic cleanup to make the code more readable. Changes since v22: * Add Reviewed-by: Jann Horn Changes since v21: * Fix copyright dates. Changes since v17: * Constify returned domain pointers from landlock_get_current_domain() and landlock_get_task_domain() helpers. Changes since v15: * Optimize landlocked() for current thread. * Display the greeting message when everything is initialized. Changes since v14: * Uses pr_fmt from common.h . * Constify variables. * Remove useless NULL initialization. Changes since v13: * totally get ride of the seccomp dependency * only keep credential management and LSM setup. Previous changes: https://lore.kernel.org/lkml/20191104172146.30797-4-mic@digikod.net/ --- security/Kconfig | 10 +++---- security/landlock/Makefile | 3 +- security/landlock/common.h | 20 +++++++++++++ security/landlock/cred.c | 46 ++++++++++++++++++++++++++++++ security/landlock/cred.h | 58 ++++++++++++++++++++++++++++++++++++++ security/landlock/setup.c | 31 ++++++++++++++++++++ security/landlock/setup.h | 16 +++++++++++ 7 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 security/landlock/common.h create mode 100644 security/landlock/cred.c create mode 100644 security/landlock/cred.h create mode 100644 security/landlock/setup.c create mode 100644 security/landlock/setup.h diff --git a/security/Kconfig b/security/Kconfig index 15a4342b5d01..0ced7fd33e4d 100644 --- a/security/Kconfig +++ b/security/Kconfig @@ -278,11 +278,11 @@ endchoice config LSM string "Ordered list of enabled LSMs" - default "lockdown,yama,loadpin,safesetid,integrity,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK - default "lockdown,yama,loadpin,safesetid,integrity,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR - default "lockdown,yama,loadpin,safesetid,integrity,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO - default "lockdown,yama,loadpin,safesetid,integrity,bpf" if DEFAULT_SECURITY_DAC - default "lockdown,yama,loadpin,safesetid,integrity,selinux,smack,tomoyo,apparmor,bpf" + default "landlock,lockdown,yama,loadpin,safesetid,integrity,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK + default "landlock,lockdown,yama,loadpin,safesetid,integrity,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR + default "landlock,lockdown,yama,loadpin,safesetid,integrity,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO + default "landlock,lockdown,yama,loadpin,safesetid,integrity,bpf" if DEFAULT_SECURITY_DAC + default "landlock,lockdown,yama,loadpin,safesetid,integrity,selinux,smack,tomoyo,apparmor,bpf" help A comma-separated list of LSMs, in initialization order. Any LSMs left off this list will be ignored. This can be diff --git a/security/landlock/Makefile b/security/landlock/Makefile index d846eba445bb..041ea242e627 100644 --- a/security/landlock/Makefile +++ b/security/landlock/Makefile @@ -1,3 +1,4 @@ obj-$(CONFIG_SECURITY_LANDLOCK) := landlock.o -landlock-y := object.o ruleset.o +landlock-y := setup.o object.o ruleset.o \ + cred.o diff --git a/security/landlock/common.h b/security/landlock/common.h new file mode 100644 index 000000000000..5dc0fe15707d --- /dev/null +++ b/security/landlock/common.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Common constants and helpers + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_COMMON_H +#define _SECURITY_LANDLOCK_COMMON_H + +#define LANDLOCK_NAME "landlock" + +#ifdef pr_fmt +#undef pr_fmt +#endif + +#define pr_fmt(fmt) LANDLOCK_NAME ": " fmt + +#endif /* _SECURITY_LANDLOCK_COMMON_H */ diff --git a/security/landlock/cred.c b/security/landlock/cred.c new file mode 100644 index 000000000000..76bd13ff85b9 --- /dev/null +++ b/security/landlock/cred.c @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Credential hooks + * + * Copyright © 2017-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#include +#include + +#include "common.h" +#include "cred.h" +#include "ruleset.h" +#include "setup.h" + +static int hook_cred_prepare(struct cred *const new, + const struct cred *const old, const gfp_t gfp) +{ + struct landlock_ruleset *const old_dom = landlock_cred(old)->domain; + + if (old_dom) { + landlock_get_ruleset(old_dom); + landlock_cred(new)->domain = old_dom; + } + return 0; +} + +static void hook_cred_free(struct cred *const cred) +{ + struct landlock_ruleset *const dom = landlock_cred(cred)->domain; + + if (dom) + landlock_put_ruleset_deferred(dom); +} + +static struct security_hook_list landlock_hooks[] __lsm_ro_after_init = { + LSM_HOOK_INIT(cred_prepare, hook_cred_prepare), + LSM_HOOK_INIT(cred_free, hook_cred_free), +}; + +__init void landlock_add_hooks_cred(void) +{ + security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks), + LANDLOCK_NAME); +} diff --git a/security/landlock/cred.h b/security/landlock/cred.h new file mode 100644 index 000000000000..f059c5c49583 --- /dev/null +++ b/security/landlock/cred.h @@ -0,0 +1,58 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Credential hooks + * + * Copyright © 2019-2020 Mickaël Salaün + * Copyright © 2019-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_CRED_H +#define _SECURITY_LANDLOCK_CRED_H + +#include +#include +#include + +#include "ruleset.h" +#include "setup.h" + +struct landlock_cred_security { + struct landlock_ruleset *domain; +}; + +static inline struct landlock_cred_security *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) +{ + return landlock_cred(current_cred())->domain; +} + +/* + * The call needs to come from an RCU read-side critical section. + */ +static inline const struct landlock_ruleset *landlock_get_task_domain( + const struct task_struct *const task) +{ + return landlock_cred(__task_cred(task))->domain; +} + +static inline bool landlocked(const struct task_struct *const task) +{ + bool has_dom; + + if (task == current) + return !!landlock_get_current_domain(); + + rcu_read_lock(); + has_dom = !!landlock_get_task_domain(task); + rcu_read_unlock(); + return has_dom; +} + +__init void landlock_add_hooks_cred(void); + +#endif /* _SECURITY_LANDLOCK_CRED_H */ diff --git a/security/landlock/setup.c b/security/landlock/setup.c new file mode 100644 index 000000000000..39ee1766f175 --- /dev/null +++ b/security/landlock/setup.c @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Security framework setup + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#include +#include + +#include "common.h" +#include "cred.h" +#include "setup.h" + +struct lsm_blob_sizes landlock_blob_sizes __lsm_ro_after_init = { + .lbs_cred = sizeof(struct landlock_cred_security), +}; + +static int __init landlock_init(void) +{ + landlock_add_hooks_cred(); + pr_info("Up and running.\n"); + return 0; +} + +DEFINE_LSM(LANDLOCK_NAME) = { + .name = LANDLOCK_NAME, + .init = landlock_init, + .blobs = &landlock_blob_sizes, +}; diff --git a/security/landlock/setup.h b/security/landlock/setup.h new file mode 100644 index 000000000000..9fdbf33fcc33 --- /dev/null +++ b/security/landlock/setup.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Security framework setup + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_SETUP_H +#define _SECURITY_LANDLOCK_SETUP_H + +#include + +extern struct lsm_blob_sizes landlock_blob_sizes; + +#endif /* _SECURITY_LANDLOCK_SETUP_H */ From patchwork Tue Dec 1 19:23:16 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 335418 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-16.7 required=3.0 tests=BAYES_00, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 674A9C83028 for ; Tue, 1 Dec 2020 19:25:22 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 3522521D7A for ; Tue, 1 Dec 2020 19:25:22 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S2392395AbgLATZN (ORCPT ); Tue, 1 Dec 2020 14:25:13 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:35588 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1731351AbgLATZD (ORCPT ); Tue, 1 Dec 2020 14:25:03 -0500 Received: from smtp-8fac.mail.infomaniak.ch (smtp-8fac.mail.infomaniak.ch [IPv6:2001:1600:4:17::8fac]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 13334C061A4B for ; Tue, 1 Dec 2020 11:23:38 -0800 (PST) Received: from smtp-2-0001.mail.infomaniak.ch (unknown [10.5.36.108]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4ClsTN6FJ6zlhjDS; Tue, 1 Dec 2020 20:23:36 +0100 (CET) Received: from localhost (unknown [23.97.221.149]) by smtp-2-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4ClsTM4GjVzlh8TJ; Tue, 1 Dec 2020 20:23:35 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: James Morris , Jann Horn , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Al Viro , Andy Lutomirski , Anton Ivanov , Arnd Bergmann , Casey Schaufler , Jeff Dike , Jonathan Corbet , Kees Cook , Michael Kerrisk , Richard Weinberger , Shuah Khan , Vincent Dagonneau , kernel-hardening@lists.openwall.com, linux-api@vger.kernel.org, linux-arch@vger.kernel.org, linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, linux-security-module@vger.kernel.org, x86@kernel.org, =?utf-8?q?Mick?= =?utf-8?b?YcOrbCBTYWxhw7xu?= Subject: [PATCH v25 06/12] fs,security: Add sb_delete hook Date: Tue, 1 Dec 2020 20:23:16 +0100 Message-Id: <20201201192322.213239-7-mic@digikod.net> X-Mailer: git-send-email 2.29.2 In-Reply-To: <20201201192322.213239-1-mic@digikod.net> References: <20201201192322.213239-1-mic@digikod.net> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org From: Mickaël Salaün The sb_delete security hook is called when shutting down a superblock, which may be useful to release kernel objects tied to the superblock's lifetime (e.g. inodes). This new hook is needed by Landlock to release (ephemerally) tagged struct inodes. This comes from the unprivileged nature of Landlock described in the next commit. Cc: Al Viro Cc: James Morris Cc: Kees Cook Cc: Serge E. Hallyn Signed-off-by: Mickaël Salaün Reviewed-by: Jann Horn --- Changes since v22: * Add Reviewed-by: Jann Horn Changes since v17: * Initial patch to replace the direct call to landlock_release_inodes() (requested by James Morris). https://lore.kernel.org/lkml/alpine.LRH.2.21.2005150536440.7929@namei.org/ --- fs/super.c | 1 + include/linux/lsm_hook_defs.h | 1 + include/linux/lsm_hooks.h | 2 ++ include/linux/security.h | 4 ++++ security/security.c | 5 +++++ 5 files changed, 13 insertions(+) diff --git a/fs/super.c b/fs/super.c index 98bb0629ee10..751cad8c081f 100644 --- a/fs/super.c +++ b/fs/super.c @@ -454,6 +454,7 @@ void generic_shutdown_super(struct super_block *sb) evict_inodes(sb); /* only nonzero refcount inodes can have marks */ fsnotify_sb_delete(sb); + security_sb_delete(sb); if (sb->s_dio_done_wq) { destroy_workqueue(sb->s_dio_done_wq); diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h index 32a940117e7a..1ba9b4dfecb3 100644 --- a/include/linux/lsm_hook_defs.h +++ b/include/linux/lsm_hook_defs.h @@ -59,6 +59,7 @@ LSM_HOOK(int, 0, fs_context_dup, struct fs_context *fc, LSM_HOOK(int, -ENOPARAM, fs_context_parse_param, struct fs_context *fc, struct fs_parameter *param) LSM_HOOK(int, 0, sb_alloc_security, struct super_block *sb) +LSM_HOOK(void, LSM_RET_VOID, sb_delete, struct super_block *sb) LSM_HOOK(void, LSM_RET_VOID, sb_free_security, struct super_block *sb) LSM_HOOK(void, LSM_RET_VOID, sb_free_mnt_opts, void *mnt_opts) LSM_HOOK(int, 0, sb_eat_lsm_opts, char *orig, void **mnt_opts) diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h index ff0f03a45c56..dbfcec05a176 100644 --- a/include/linux/lsm_hooks.h +++ b/include/linux/lsm_hooks.h @@ -108,6 +108,8 @@ * allocated. * @sb contains the super_block structure to be modified. * Return 0 if operation was successful. + * @sb_delete: + * Release objects tied to a superblock (e.g. inodes). * @sb_free_security: * Deallocate and clear the sb->s_security field. * @sb contains the super_block structure to be modified. diff --git a/include/linux/security.h b/include/linux/security.h index bc2725491560..a4603b62d444 100644 --- a/include/linux/security.h +++ b/include/linux/security.h @@ -287,6 +287,7 @@ void security_bprm_committed_creds(struct linux_binprm *bprm); int security_fs_context_dup(struct fs_context *fc, struct fs_context *src_fc); int security_fs_context_parse_param(struct fs_context *fc, struct fs_parameter *param); int security_sb_alloc(struct super_block *sb); +void security_sb_delete(struct super_block *sb); void security_sb_free(struct super_block *sb); void security_free_mnt_opts(void **mnt_opts); int security_sb_eat_lsm_opts(char *options, void **mnt_opts); @@ -619,6 +620,9 @@ static inline int security_sb_alloc(struct super_block *sb) return 0; } +static inline void security_sb_delete(struct super_block *sb) +{ } + static inline void security_sb_free(struct super_block *sb) { } diff --git a/security/security.c b/security/security.c index 4ffd6c3af9d7..4563e7a79216 100644 --- a/security/security.c +++ b/security/security.c @@ -899,6 +899,11 @@ int security_sb_alloc(struct super_block *sb) return rc; } +void security_sb_delete(struct super_block *sb) +{ + call_void_hook(sb_delete, sb); +} + void security_sb_free(struct super_block *sb) { call_void_hook(sb_free_security, sb); From patchwork Tue Dec 1 19:23:17 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 335417 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-21.7 required=3.0 tests=BAYES_00, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, MENTIONS_GIT_HOSTING, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 48DB6C83030 for ; Tue, 1 Dec 2020 19:25:24 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id E97B6221E2 for ; Tue, 1 Dec 2020 19:25:23 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S2392392AbgLATYc (ORCPT ); Tue, 1 Dec 2020 14:24:32 -0500 Received: from smtp-8fab.mail.infomaniak.ch ([83.166.143.171]:44371 "EHLO smtp-8fab.mail.infomaniak.ch" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S2404344AbgLATY2 (ORCPT ); Tue, 1 Dec 2020 14:24:28 -0500 Received: from smtp-3-0000.mail.infomaniak.ch (unknown [10.4.36.107]) by smtp-2-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4ClsTQ0xlYzlhSZ8; Tue, 1 Dec 2020 20:23:38 +0100 (CET) Received: from localhost (unknown [23.97.221.149]) by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4ClsTP5gG4zlh8TB; Tue, 1 Dec 2020 20:23:37 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: James Morris , Jann Horn , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Al Viro , Andy Lutomirski , Anton Ivanov , Arnd Bergmann , Casey Schaufler , Jeff Dike , Jonathan Corbet , Kees Cook , Michael Kerrisk , Richard Weinberger , Shuah Khan , Vincent Dagonneau , kernel-hardening@lists.openwall.com, linux-api@vger.kernel.org, linux-arch@vger.kernel.org, linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, linux-security-module@vger.kernel.org, x86@kernel.org, =?utf-8?q?Mick?= =?utf-8?b?YcOrbCBTYWxhw7xu?= Subject: [PATCH v25 07/12] landlock: Support filesystem access-control Date: Tue, 1 Dec 2020 20:23:17 +0100 Message-Id: <20201201192322.213239-8-mic@digikod.net> X-Mailer: git-send-email 2.29.2 In-Reply-To: <20201201192322.213239-1-mic@digikod.net> References: <20201201192322.213239-1-mic@digikod.net> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org From: Mickaël Salaün Thanks to the Landlock objects and ruleset, it is possible to identify inodes according to a process's domain. To enable an unprivileged process to express a file hierarchy, it first needs to open a directory (or a file) and pass this file descriptor to the kernel through landlock_add_rule(2). When checking if a file access request is allowed, we walk from the requested dentry to the real root, following the different mount layers. The access to each "tagged" inodes are collected according to their rule layer level, and ANDed to create access to the requested file hierarchy. This makes possible to identify a lot of files without tagging every inodes nor modifying the filesystem, while still following the view and understanding the user has from the filesystem. Add a new ARCH_EPHEMERAL_INODES for UML because it currently does not keep the same struct inodes for the same inodes whereas these inodes are in use. This commit adds a minimal set of supported filesystem access-control which doesn't enable to restrict all file-related actions. This is the result of multiple discussions to minimize the code of Landlock to ease review. Thanks to the Landlock design, extending this access-control without breaking user space will not be a problem. Moreover, seccomp filters can be used to restrict the use of syscall families which may not be currently handled by Landlock. Cc: Al Viro Cc: Anton Ivanov Cc: James Morris Cc: Jann Horn Cc: Jeff Dike Cc: Kees Cook Cc: Richard Weinberger Cc: Serge E. Hallyn Signed-off-by: Mickaël Salaün --- Changes since v24: * Use the new struct landlock_rule and landlock_layer to not mix accesses from different layers. Revert "Enforce deterministic interleaved path rules" from v24, and fix the layer check. This enables to follow a sane semantic: an access is granted if, for each policy layer, at least one rule encountered on the pathwalk grants the access, regardless of their position in the layer stack (suggested by Jann Horn). See layout1.interleaved_masked_accesses tests from tools/testing/selftests/landlock/fs_test.c for corner cases. * Add build-time checks for layers. * Use the new landlock_insert_rule() API. Changes since v23: * Enforce deterministic interleaved path rules. To have consistent layered rules, granting access to a path implies that all accesses tied to inodes, from the requested file to the real root, must be checked. Otherwise, stacked rules may result to overzealous restrictions. By excluding the ability to add exceptions in the same layer (e.g. /a allowed, /a/b denied, and /a/b/c allowed), we get deterministic interleaved path rules. This removes an optimization which could be replaced by a proper cache mechanism. This also further simplifies and explain check_access_path_continue(). * Fix memory allocation error handling in landlock_create_object() calls. This prevent to inadvertently hold an inode. * In get_inode_object(), improve comments, make code more readable and move kfree() call out of the lock window. * Use the simplified landlock_insert_rule() API. Changes since v22: * Simplify check_access_path_continue() (suggested by Jann Horn). * Remove prefetch() call for now (suggested by Jann Horn). * Fix spelling and remove superfluous comment (spotted by Jann Horn). * Cosmetic variable renaming. Changes since v21: * Rename ARCH_EPHEMERAL_STATES to ARCH_EPHEMERAL_INODES (suggested by James Morris). * Remove the LANDLOCK_ACCESS_FS_CHROOT right because chroot(2) (which requires CAP_SYS_CHROOT) doesn't enable to bypass Landlock (as tests demonstrate it), and because it is often used by sandboxes, it would be counterproductive to forbid it. This also reduces the code size. * Clean up documentation. Changes since v19: * Fix spelling (spotted by Randy Dunlap). Changes since v18: * Remove useless include. * Fix spelling. Changes since v17: * Replace landlock_release_inodes() with security_sb_delete() (requested by James Morris). * Replace struct super_block->s_landlock_inode_refs with the LSM infrastructure management of the superblock (requested by James Morris). * Fix mknod restriction with a zero mode (spotted by Vincent Dagonneau). * Minimize executed code in path_mknod and file_open hooks when the current tasks is not sandboxed. * Remove useless checks on the file pointer and inode in hook_file_open() . * Constify domain pointers. * Rename inode_landlock() to landlock_inode(). * Import include/uapi/linux/landlock.h and _LANDLOCK_ACCESS_FS_* from the ruleset and domain management patch. * Explain the rational of this minimal set of access-control. https://lore.kernel.org/lkml/f646e1c7-33cf-333f-070c-0a40ad0468cd@digikod.net/ Changes since v16: * Add ARCH_EPHEMERAL_STATES and enable it for UML. Changes since v15: * Replace layer_levels and layer_depth with a bitfield of layers: this enables to properly manage superset and subset of access rights, whatever their order in the stack of layers. Cf. https://lore.kernel.org/lkml/e07fe473-1801-01cc-12ae-b3167f95250e@digikod.net/ * Allow to open pipes and similar special files through /proc/self/fd/. * Properly handle internal filesystems such as nsfs: always allow these kind of roots because disconnected path cannot be evaluated. * Remove the LANDLOCK_ACCESS_FS_LINK_TO and LANDLOCK_ACCESS_FS_RENAME_{TO,FROM}, but use the LANDLOCK_ACCESS_FS_REMOVE_{FILE,DIR} and LANDLOCK_ACCESS_FS_MAKE_* instead. Indeed, it is not possible for now (and not really useful) to express the semantic of a source and a destination. * Check access rights to remove a directory or a file with rename(2). * Forbid reparenting when linking or renaming. This is needed to easily protect against possible privilege escalation by changing the place of a file or directory in relation to an enforced access policy (from the set of layers). This will be relaxed in the future. * Update hooks to take into account replacement of the object's self and beneath access bitfields with one. Simplify the code. * Check file related access rights. * Check d_is_negative() instead of !d_backing_inode() in check_access_path_continue(), and continue the path walk while there is no mapped inode e.g., with rename(2). * Check private inode in check_access_path(). * Optimize get_file_access() when dealing with a directory. * Add missing atomic.h . Changes since v14: * Simplify the object, rule and ruleset management at the expense of a less aggressive memory freeing (contributed by Jann Horn, with additional modifications): - Rewrite release_inode() to use inode->sb->s_landlock_inode_refs. - Remove useless checks in landlock_release_inodes(), clean object pointer according to the new struct landlock_object and wait for all iput() to complete. - Rewrite get_inode_object() according to the new struct landlock_object. If there is a race-condition when cleaning up an object, we retry until the concurrent thread finished the object cleaning. Cf. https://lore.kernel.org/lkml/CAG48ez21bEn0wL1bbmTiiu8j9jP5iEWtHOwz4tURUJ+ki0ydYw@mail.gmail.com/ * Fix nested domains by implementing a notion of layer level and depth: - Check for matching level ranges when walking through a file path. - Only allow access if every layer granted the access request. * Handles files without mount points (e.g. pipes). * Hardens path walk by checking inode pointer values. * Prefetches d_parent when walking to the root directory. * Remove useless inode_alloc_security hook() (suggested by Jann Horn): already initialized by lsm_inode_alloc(). * Remove the inode_free_security hook. * Remove access checks that may be required for FD-only requests: truncate, getattr, lock, chmod, chown, chgrp, ioctl. This will be handle in a future evolution of Landlock, but right now the goal is to lighten the code to ease review. * Constify variables. * Move ABI checks into syscall.c . * Cosmetic variable renames. Changes since v11: * Add back, revamp and make a fully working filesystem access-control based on paths and inodes. * Remove the eBPF dependency. Previous changes: https://lore.kernel.org/lkml/20190721213116.23476-6-mic@digikod.net/ --- MAINTAINERS | 1 + arch/Kconfig | 7 + arch/um/Kconfig | 1 + include/uapi/linux/landlock.h | 75 ++++ security/landlock/Kconfig | 2 +- security/landlock/Makefile | 2 +- security/landlock/fs.c | 635 ++++++++++++++++++++++++++++++++++ security/landlock/fs.h | 60 ++++ security/landlock/setup.c | 7 + security/landlock/setup.h | 2 + 10 files changed, 790 insertions(+), 2 deletions(-) create mode 100644 include/uapi/linux/landlock.h create mode 100644 security/landlock/fs.c create mode 100644 security/landlock/fs.h diff --git a/MAINTAINERS b/MAINTAINERS index 9fce015d8102..674a06639852 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9827,6 +9827,7 @@ L: linux-security-module@vger.kernel.org S: Supported W: https://landlock.io T: git https://github.com/landlock-lsm/linux.git +F: include/uapi/linux/landlock.h F: security/landlock/ K: landlock K: LANDLOCK diff --git a/arch/Kconfig b/arch/Kconfig index 56b6ccc0e32d..7da605bd966d 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -884,6 +884,13 @@ config COMPAT_32BIT_TIME config ARCH_NO_PREEMPT bool +config ARCH_EPHEMERAL_INODES + def_bool n + help + An arch should select this symbol if it doesn't keep track of inode + instances on its own, but instead relies on something else (e.g. the host + kernel for an UML kernel). + config ARCH_SUPPORTS_RT bool diff --git a/arch/um/Kconfig b/arch/um/Kconfig index 4b799fad8b48..082d0207a7be 100644 --- a/arch/um/Kconfig +++ b/arch/um/Kconfig @@ -5,6 +5,7 @@ menu "UML-specific options" config UML bool default y + select ARCH_EPHEMERAL_INODES select ARCH_HAS_KCOV select ARCH_NO_PREEMPT select HAVE_ARCH_AUDITSYSCALL diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h new file mode 100644 index 000000000000..d547bd49fe38 --- /dev/null +++ b/include/uapi/linux/landlock.h @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * Landlock - User space API + * + * Copyright © 2017-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _UAPI__LINUX_LANDLOCK_H__ +#define _UAPI__LINUX_LANDLOCK_H__ + +/** + * DOC: fs_access + * + * A set of actions on kernel objects may be defined by an attribute (e.g. + * &struct landlock_path_beneath_attr) including a bitmask of access. + * + * Filesystem flags + * ~~~~~~~~~~~~~~~~ + * + * These flags enable to restrict a sandboxed process to a set of actions on + * files and directories. Files or directories opened before the sandboxing + * are not subject to these restrictions. + * + * A file can only receive these access rights: + * + * - %LANDLOCK_ACCESS_FS_EXECUTE: Execute a file. + * - %LANDLOCK_ACCESS_FS_WRITE_FILE: Open a file with write access. + * - %LANDLOCK_ACCESS_FS_READ_FILE: Open a file with read access. + * + * A directory can receive access rights related to files or directories. The + * following access right is applied to the directory itself, and the + * directories beneath it: + * + * - %LANDLOCK_ACCESS_FS_READ_DIR: Open a directory or list its content. + * + * However, the following access rights only apply to the content of a + * directory, not the directory itself: + * + * - %LANDLOCK_ACCESS_FS_REMOVE_DIR: Remove an empty directory or rename one. + * - %LANDLOCK_ACCESS_FS_REMOVE_FILE: Unlink (or rename) a file. + * - %LANDLOCK_ACCESS_FS_MAKE_CHAR: Create (or rename or link) a character + * device. + * - %LANDLOCK_ACCESS_FS_MAKE_DIR: Create (or rename) a directory. + * - %LANDLOCK_ACCESS_FS_MAKE_REG: Create (or rename or link) a regular file. + * - %LANDLOCK_ACCESS_FS_MAKE_SOCK: Create (or rename or link) a UNIX domain + * socket. + * - %LANDLOCK_ACCESS_FS_MAKE_FIFO: Create (or rename or link) a named pipe. + * - %LANDLOCK_ACCESS_FS_MAKE_BLOCK: Create (or rename or link) a block device. + * - %LANDLOCK_ACCESS_FS_MAKE_SYM: Create (or rename or link) a symbolic link. + * + * .. warning:: + * + * It is currently not possible to restrict some file-related actions + * accessible through these syscall families: :manpage:`chdir(2)`, + * :manpage:`truncate(2)`, :manpage:`stat(2)`, :manpage:`flock(2)`, + * :manpage:`chmod(2)`, :manpage:`chown(2)`, :manpage:`setxattr(2)`, + * :manpage:`ioctl(2)`, :manpage:`fcntl(2)`. + * Future Landlock evolutions will enable to restrict them. + */ +#define LANDLOCK_ACCESS_FS_EXECUTE (1ULL << 0) +#define LANDLOCK_ACCESS_FS_WRITE_FILE (1ULL << 1) +#define LANDLOCK_ACCESS_FS_READ_FILE (1ULL << 2) +#define LANDLOCK_ACCESS_FS_READ_DIR (1ULL << 3) +#define LANDLOCK_ACCESS_FS_REMOVE_DIR (1ULL << 4) +#define LANDLOCK_ACCESS_FS_REMOVE_FILE (1ULL << 5) +#define LANDLOCK_ACCESS_FS_MAKE_CHAR (1ULL << 6) +#define LANDLOCK_ACCESS_FS_MAKE_DIR (1ULL << 7) +#define LANDLOCK_ACCESS_FS_MAKE_REG (1ULL << 8) +#define LANDLOCK_ACCESS_FS_MAKE_SOCK (1ULL << 9) +#define LANDLOCK_ACCESS_FS_MAKE_FIFO (1ULL << 10) +#define LANDLOCK_ACCESS_FS_MAKE_BLOCK (1ULL << 11) +#define LANDLOCK_ACCESS_FS_MAKE_SYM (1ULL << 12) + +#endif /* _UAPI__LINUX_LANDLOCK_H__ */ diff --git a/security/landlock/Kconfig b/security/landlock/Kconfig index ea58e6208afa..43e5b0bb0706 100644 --- a/security/landlock/Kconfig +++ b/security/landlock/Kconfig @@ -2,7 +2,7 @@ config SECURITY_LANDLOCK bool "Landlock support" - depends on SECURITY + depends on SECURITY && !ARCH_EPHEMERAL_INODES select SECURITY_PATH help Landlock is a safe sandboxing mechanism which enables processes to diff --git a/security/landlock/Makefile b/security/landlock/Makefile index f1d1eb72fa76..92e3d80ab8ed 100644 --- a/security/landlock/Makefile +++ b/security/landlock/Makefile @@ -1,4 +1,4 @@ obj-$(CONFIG_SECURITY_LANDLOCK) := landlock.o landlock-y := setup.o object.o ruleset.o \ - cred.o ptrace.o + cred.o ptrace.o fs.o diff --git a/security/landlock/fs.c b/security/landlock/fs.c new file mode 100644 index 000000000000..d913151e6fe8 --- /dev/null +++ b/security/landlock/fs.c @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Filesystem management and hooks + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "cred.h" +#include "fs.h" +#include "object.h" +#include "ruleset.h" +#include "setup.h" + +/* Underlying object management */ + +static void release_inode(struct landlock_object *const object) + __releases(object->lock) +{ + struct inode *const inode = object->underobj; + struct super_block *sb; + + if (!inode) { + spin_unlock(&object->lock); + return; + } + + spin_lock(&inode->i_lock); + /* + * Make sure that if the filesystem is concurrently unmounted, + * hook_sb_delete() will wait for us to finish iput(). + */ + sb = inode->i_sb; + atomic_long_inc(&landlock_superblock(sb)->inode_refs); + rcu_assign_pointer(landlock_inode(inode)->object, NULL); + spin_unlock(&inode->i_lock); + spin_unlock(&object->lock); + /* + * Now, new rules can safely be tied to @inode. + */ + + iput(inode); + if (atomic_long_dec_and_test(&landlock_superblock(sb)->inode_refs)) + wake_up_var(&landlock_superblock(sb)->inode_refs); +} + +static const struct landlock_object_underops landlock_fs_underops = { + .release = release_inode +}; + +/* Ruleset management */ + +static struct landlock_object *get_inode_object(struct inode *const inode) +{ + struct landlock_object *object, *new_object; + struct landlock_inode_security *inode_sec = landlock_inode(inode); + + rcu_read_lock(); +retry: + object = rcu_dereference(inode_sec->object); + if (object) { + if (likely(refcount_inc_not_zero(&object->usage))) { + rcu_read_unlock(); + return object; + } + /* + * We are racing with release_inode(), the object is going + * away. Wait for release_inode(), then retry. + */ + spin_lock(&object->lock); + spin_unlock(&object->lock); + goto retry; + } + rcu_read_unlock(); + + /* + * If there is no object tied to @inode, then create a new one (without + * holding any locks). + */ + new_object = landlock_create_object(&landlock_fs_underops, inode); + if (IS_ERR(new_object)) + return new_object; + + spin_lock(&inode->i_lock); + object = rcu_dereference_protected(inode_sec->object, + lockdep_is_held(&inode->i_lock)); + if (unlikely(object)) { + /* Someone else just created the object, bail out and retry. */ + spin_unlock(&inode->i_lock); + kfree(new_object); + + rcu_read_lock(); + goto retry; + } + + rcu_assign_pointer(inode_sec->object, new_object); + /* + * @inode will be released by hook_sb_delete() on its superblock + * shutdown. + */ + ihold(inode); + spin_unlock(&inode->i_lock); + return new_object; +} + +/* All access rights which can be tied to files. */ +#define ACCESS_FILE ( \ + LANDLOCK_ACCESS_FS_EXECUTE | \ + LANDLOCK_ACCESS_FS_WRITE_FILE | \ + LANDLOCK_ACCESS_FS_READ_FILE) + +/* + * @path: Should have been checked by get_path_from_fd(). + */ +int landlock_append_fs_rule(struct landlock_ruleset *const ruleset, + const struct path *const path, u32 access_rights) +{ + int err; + struct landlock_object *object; + + /* Files only get access rights that make sense. */ + if (!d_is_dir(path->dentry) && (access_rights | ACCESS_FILE) != + ACCESS_FILE) + return -EINVAL; + + /* Transforms relative access rights to absolute ones. */ + access_rights |= _LANDLOCK_ACCESS_FS_MASK & ~ruleset->fs_access_mask; + object = get_inode_object(d_backing_inode(path->dentry)); + if (IS_ERR(object)) + return PTR_ERR(object); + mutex_lock(&ruleset->lock); + err = landlock_insert_rule(ruleset, object, access_rights); + mutex_unlock(&ruleset->lock); + /* + * No need to check for an error because landlock_insert_rule() + * increments the refcount for the new object if needed. + */ + landlock_put_object(object); + return err; +} + +/* Access-control management */ + +static bool check_access_path_continue( + const struct landlock_ruleset *const domain, + const struct path *const path, const u32 access_request, + u64 *const layer_mask) +{ + const struct landlock_rule *rule; + const struct inode *inode; + size_t i; + + if (d_is_negative(path->dentry)) + /* Continues to walk while there is no mapped inode. */ + return true; + inode = d_backing_inode(path->dentry); + rcu_read_lock(); + rule = landlock_find_rule(domain, + rcu_dereference(landlock_inode(inode)->object)); + rcu_read_unlock(); + if (!rule) + return true; + + /* + * An access is granted if, for each policy layer, at least one rule + * encountered on the pathwalk grants the access, regardless of their + * position in the layer stack. We must then check not-yet-seen layers + * for each inode, from the last one added to the first one. + */ + for (i = 0; i < rule->nb_layers; i++) { + const struct landlock_layer *const layer = &rule->layers[i]; + const u64 layer_level = BIT_ULL(layer->level - 1); + + if (!(layer_level & *layer_mask)) + continue; + if ((layer->access & access_request) != access_request) + return false; + *layer_mask &= ~layer_level; + } + return true; +} + +static inline void build_check_layer(void) +{ + const struct landlock_layer layer = { + .level = ~0, + .access = ~0, + }; + + /* Make sure all layers can be stored. */ + BUILD_BUG_ON(layer.level < LANDLOCK_MAX_NB_LAYERS); + + /* Make sure all accesses can be stored. */ + BUILD_BUG_ON(layer.access < _LANDLOCK_ACCESS_FS_MASK); +} + +static int check_access_path(const struct landlock_ruleset *const domain, + const struct path *const path, u32 access_request) +{ + bool allowed = false; + struct path walker_path; + u64 layer_mask; + + /* Make sure all layers can be checked. */ + BUILD_BUG_ON((sizeof(layer_mask) * BITS_PER_BYTE) < LANDLOCK_MAX_NB_LAYERS); + build_check_layer(); + + if (WARN_ON_ONCE(!domain || !path)) + return 0; + /* + * Allows access to pseudo filesystems that will never be mountable + * (e.g. sockfs, pipefs), but can still be reachable through + * /proc/self/fd . + */ + if ((path->dentry->d_sb->s_flags & SB_NOUSER) || + (d_is_positive(path->dentry) && + unlikely(IS_PRIVATE(d_backing_inode(path->dentry))))) + return 0; + if (WARN_ON_ONCE(domain->nb_layers < 1)) + return -EACCES; + + layer_mask = GENMASK_ULL(domain->nb_layers - 1, 0); + /* + * An access request which is not handled by the domain should be + * allowed. + */ + access_request &= domain->fs_access_mask; + if (access_request == 0) + return 0; + walker_path = *path; + path_get(&walker_path); + /* + * We need to walk through all the hierarchy to not miss any relevant + * restriction. + */ + while (check_access_path_continue(domain, &walker_path, access_request, + &layer_mask)) { + struct dentry *parent_dentry; + + /* Stops when a rule from each layer granted access. */ + if (layer_mask == 0) { + allowed = true; + break; + } + +jump_up: + /* + * Does not work with orphaned/private mounts like overlayfs + * layers for now (cf. ovl_path_real() and ovl_path_open()). + */ + if (walker_path.dentry == walker_path.mnt->mnt_root) { + if (follow_up(&walker_path)) { + /* Ignores hidden mount points. */ + goto jump_up; + } else { + /* + * Stops at the real root. Denies access + * because not all layers have granted access. + */ + allowed = false; + break; + } + } + if (unlikely(IS_ROOT(walker_path.dentry))) { + /* + * Stops at disconnected root directories. Only allows + * access to internal filesystems (e.g. nsfs which is + * reachable through /proc/self/ns). + */ + allowed = !!(walker_path.mnt->mnt_flags & MNT_INTERNAL); + break; + } + parent_dentry = dget_parent(walker_path.dentry); + dput(walker_path.dentry); + walker_path.dentry = parent_dentry; + } + path_put(&walker_path); + return allowed ? 0 : -EACCES; +} + +static inline int current_check_access_path(const struct path *const path, + const u32 access_request) +{ + const struct landlock_ruleset *const dom = + landlock_get_current_domain(); + + if (!dom) + return 0; + return check_access_path(dom, path, access_request); +} + +/* Super-block hooks */ + +/* + * Release the inodes used in a security policy. + * + * Cf. fsnotify_unmount_inodes() + */ +static void hook_sb_delete(struct super_block *const sb) +{ + struct inode *inode, *iput_inode = NULL; + + if (!landlock_initialized) + return; + + spin_lock(&sb->s_inode_list_lock); + list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { + struct landlock_inode_security *inode_sec = + landlock_inode(inode); + struct landlock_object *object; + bool do_put = false; + + rcu_read_lock(); + object = rcu_dereference(inode_sec->object); + if (!object) { + rcu_read_unlock(); + continue; + } + + spin_lock(&object->lock); + if (object->underobj) { + object->underobj = NULL; + do_put = true; + spin_lock(&inode->i_lock); + rcu_assign_pointer(inode_sec->object, NULL); + spin_unlock(&inode->i_lock); + } + spin_unlock(&object->lock); + rcu_read_unlock(); + if (!do_put) + /* + * A concurrent iput() in release_inode() is ongoing + * and we will just wait for it to finish. + */ + continue; + + /* + * At this point, we own the ihold() reference that was + * originally set up by get_inode_object(). Therefore we can + * drop the list lock and know that the inode won't disappear + * from under us until the next loop walk. + */ + spin_unlock(&sb->s_inode_list_lock); + /* + * We can now actually put the previous inode, which is not + * needed anymore for the loop walk. + */ + if (iput_inode) + iput(iput_inode); + iput_inode = inode; + spin_lock(&sb->s_inode_list_lock); + } + spin_unlock(&sb->s_inode_list_lock); + if (iput_inode) + iput(iput_inode); + + /* + * Wait for pending iput() in release_inode(). + */ + wait_var_event(&landlock_superblock(sb)->inode_refs, !atomic_long_read( + &landlock_superblock(sb)->inode_refs)); +} + +/* + * Because a Landlock security policy is defined according to the filesystem + * layout (i.e. the mount namespace), changing it may grant access to files not + * previously allowed. + * + * To make it simple, deny any filesystem layout modification by landlocked + * processes. Non-landlocked processes may still change the namespace of a + * landlocked process, but this kind of threat must be handled by a system-wide + * access-control security policy. + * + * This could be lifted in the future if Landlock can safely handle mount + * namespace updates requested by a landlocked process. Indeed, we could + * update the current domain (which is currently read-only) by taking into + * account the accesses of the source and the destination of a new mount point. + * However, it would also require to make all the child domains dynamically + * inherit these new constraints. Anyway, for backward compatibility reasons, + * a dedicated user space option would be required (e.g. as a ruleset command + * option). + */ +static int hook_sb_mount(const char *const dev_name, + const struct path *const path, const char *const type, + const unsigned long flags, void *const data) +{ + if (!landlock_get_current_domain()) + return 0; + return -EPERM; +} + +static int hook_move_mount(const struct path *const from_path, + const struct path *const to_path) +{ + if (!landlock_get_current_domain()) + return 0; + return -EPERM; +} + +/* + * Removing a mount point may reveal a previously hidden file hierarchy, which + * may then grant access to files, which may have previously been forbidden. + */ +static int hook_sb_umount(struct vfsmount *const mnt, const int flags) +{ + if (!landlock_get_current_domain()) + return 0; + return -EPERM; +} + +static int hook_sb_remount(struct super_block *const sb, void *const mnt_opts) +{ + if (!landlock_get_current_domain()) + return 0; + return -EPERM; +} + +/* + * pivot_root(2), like mount(2), changes the current mount namespace. It must + * then be forbidden for a landlocked process. + * + * However, chroot(2) may be allowed because it only changes the relative root + * directory of the current process. Moreover, it can be used to restrict the + * view of the filesystem. + */ +static int hook_sb_pivotroot(const struct path *const old_path, + const struct path *const new_path) +{ + if (!landlock_get_current_domain()) + return 0; + return -EPERM; +} + +/* Path hooks */ + +static inline u32 get_mode_access(const umode_t mode) +{ + switch (mode & S_IFMT) { + case S_IFLNK: + return LANDLOCK_ACCESS_FS_MAKE_SYM; + case 0: + /* A zero mode translates to S_IFREG. */ + case S_IFREG: + return LANDLOCK_ACCESS_FS_MAKE_REG; + case S_IFDIR: + return LANDLOCK_ACCESS_FS_MAKE_DIR; + case S_IFCHR: + return LANDLOCK_ACCESS_FS_MAKE_CHAR; + case S_IFBLK: + return LANDLOCK_ACCESS_FS_MAKE_BLOCK; + case S_IFIFO: + return LANDLOCK_ACCESS_FS_MAKE_FIFO; + case S_IFSOCK: + return LANDLOCK_ACCESS_FS_MAKE_SOCK; + default: + WARN_ON_ONCE(1); + return 0; + } +} + +/* + * Creating multiple links or renaming may lead to privilege escalations if not + * handled properly. Indeed, we must be sure that the source doesn't gain more + * privileges by being accessible from the destination. This is getting more + * complex when dealing with multiple layers. The whole picture can be seen as + * a multilayer partial ordering problem. A future version of Landlock will + * deal with that. + */ +static int hook_path_link(struct dentry *const old_dentry, + const struct path *const new_dir, + struct dentry *const new_dentry) +{ + const struct landlock_ruleset *const dom = + landlock_get_current_domain(); + + if (!dom) + return 0; + /* The mount points are the same for old and new paths, cf. EXDEV. */ + if (old_dentry->d_parent != new_dir->dentry) + /* For now, forbid reparenting. */ + return -EACCES; + if (unlikely(d_is_negative(old_dentry))) + return -EACCES; + return check_access_path(dom, new_dir, + get_mode_access(d_backing_inode(old_dentry)->i_mode)); +} + +static inline u32 maybe_remove(const struct dentry *const dentry) +{ + if (d_is_negative(dentry)) + return 0; + return d_is_dir(dentry) ? LANDLOCK_ACCESS_FS_REMOVE_DIR : + LANDLOCK_ACCESS_FS_REMOVE_FILE; +} + +static int hook_path_rename(const struct path *const old_dir, + struct dentry *const old_dentry, + const struct path *const new_dir, + struct dentry *const new_dentry) +{ + const struct landlock_ruleset *const dom = + landlock_get_current_domain(); + + if (!dom) + return 0; + /* The mount points are the same for old and new paths, cf. EXDEV. */ + if (old_dir->dentry != new_dir->dentry) + /* For now, forbid reparenting. */ + return -EACCES; + if (WARN_ON_ONCE(d_is_negative(old_dentry))) + return -EACCES; + /* RENAME_EXCHANGE is handled because directories are the same. */ + return check_access_path(dom, old_dir, maybe_remove(old_dentry) | + maybe_remove(new_dentry) | + get_mode_access(d_backing_inode(old_dentry)->i_mode)); +} + +static int hook_path_mkdir(const struct path *const dir, + struct dentry *const dentry, const umode_t mode) +{ + return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_DIR); +} + +static int hook_path_mknod(const struct path *const dir, + struct dentry *const dentry, const umode_t mode, + const unsigned int dev) +{ + const struct landlock_ruleset *const dom = + landlock_get_current_domain(); + + if (!dom) + return 0; + return check_access_path(dom, dir, get_mode_access(mode)); +} + +static int hook_path_symlink(const struct path *const dir, + struct dentry *const dentry, const char *const old_name) +{ + return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_SYM); +} + +static int hook_path_unlink(const struct path *const dir, + struct dentry *const dentry) +{ + return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_FILE); +} + +static int hook_path_rmdir(const struct path *const dir, + struct dentry *const dentry) +{ + return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_DIR); +} + +/* File hooks */ + +static inline u32 get_file_access(const struct file *const file) +{ + u32 access = 0; + + if (file->f_mode & FMODE_READ) { + /* A directory can only be opened in read mode. */ + if (S_ISDIR(file_inode(file)->i_mode)) + return LANDLOCK_ACCESS_FS_READ_DIR; + access = LANDLOCK_ACCESS_FS_READ_FILE; + } + if (file->f_mode & FMODE_WRITE) + access |= LANDLOCK_ACCESS_FS_WRITE_FILE; + /* __FMODE_EXEC is indeed part of f_flags, not f_mode. */ + if (file->f_flags & __FMODE_EXEC) + access |= LANDLOCK_ACCESS_FS_EXECUTE; + return access; +} + +static int hook_file_open(struct file *const file) +{ + const struct landlock_ruleset *const dom = + landlock_get_current_domain(); + + if (!dom) + return 0; + /* + * Because a file may be opened with O_PATH, get_file_access() may + * return 0. This case will be handled with a future Landlock + * evolution. + */ + return current_check_access_path(&file->f_path, get_file_access(file)); +} + +static struct security_hook_list landlock_hooks[] __lsm_ro_after_init = { + LSM_HOOK_INIT(sb_delete, hook_sb_delete), + LSM_HOOK_INIT(sb_mount, hook_sb_mount), + LSM_HOOK_INIT(move_mount, hook_move_mount), + LSM_HOOK_INIT(sb_umount, hook_sb_umount), + LSM_HOOK_INIT(sb_remount, hook_sb_remount), + LSM_HOOK_INIT(sb_pivotroot, hook_sb_pivotroot), + + LSM_HOOK_INIT(path_link, hook_path_link), + LSM_HOOK_INIT(path_rename, hook_path_rename), + LSM_HOOK_INIT(path_mkdir, hook_path_mkdir), + LSM_HOOK_INIT(path_mknod, hook_path_mknod), + LSM_HOOK_INIT(path_symlink, hook_path_symlink), + LSM_HOOK_INIT(path_unlink, hook_path_unlink), + LSM_HOOK_INIT(path_rmdir, hook_path_rmdir), + + LSM_HOOK_INIT(file_open, hook_file_open), +}; + +__init void landlock_add_hooks_fs(void) +{ + security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks), + LANDLOCK_NAME); +} diff --git a/security/landlock/fs.h b/security/landlock/fs.h new file mode 100644 index 000000000000..58b462eb7f10 --- /dev/null +++ b/security/landlock/fs.h @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Filesystem management and hooks + * + * Copyright © 2017-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_FS_H +#define _SECURITY_LANDLOCK_FS_H + +#include +#include +#include +#include + +#include "ruleset.h" +#include "setup.h" + +#define _LANDLOCK_ACCESS_FS_LAST LANDLOCK_ACCESS_FS_MAKE_SYM +#define _LANDLOCK_ACCESS_FS_MASK ((_LANDLOCK_ACCESS_FS_LAST << 1) - 1) + +struct landlock_inode_security { + /* + * @object: Weak pointer to an allocated object. All writes (i.e. + * creating a new object or removing one) are protected by the + * underlying inode->i_lock. Disassociating @object from the inode is + * additionally protected by @object->lock, from the time @object's + * usage refcount drops to zero to the time this pointer is nulled out. + * Cf. release_inode(). + */ + struct landlock_object __rcu *object; +}; + +struct landlock_superblock_security { + /* + * @inode_refs: References to Landlock underlying objects. + * Cf. struct super_block->s_fsnotify_inode_refs . + */ + atomic_long_t inode_refs; +}; + +static inline struct landlock_inode_security *landlock_inode( + const struct inode *const inode) +{ + return inode->i_security + landlock_blob_sizes.lbs_inode; +} + +static inline struct landlock_superblock_security *landlock_superblock( + const struct super_block *const superblock) +{ + return superblock->s_security + landlock_blob_sizes.lbs_superblock; +} + +__init void landlock_add_hooks_fs(void); + +int landlock_append_fs_rule(struct landlock_ruleset *const ruleset, + const struct path *const path, u32 access_hierarchy); + +#endif /* _SECURITY_LANDLOCK_FS_H */ diff --git a/security/landlock/setup.c b/security/landlock/setup.c index 5e7540fdeefa..722cbea82324 100644 --- a/security/landlock/setup.c +++ b/security/landlock/setup.c @@ -11,17 +11,24 @@ #include "common.h" #include "cred.h" +#include "fs.h" #include "ptrace.h" #include "setup.h" +bool landlock_initialized __lsm_ro_after_init = false; + struct lsm_blob_sizes landlock_blob_sizes __lsm_ro_after_init = { .lbs_cred = sizeof(struct landlock_cred_security), + .lbs_inode = sizeof(struct landlock_inode_security), + .lbs_superblock = sizeof(struct landlock_superblock_security), }; static int __init landlock_init(void) { landlock_add_hooks_cred(); landlock_add_hooks_ptrace(); + landlock_add_hooks_fs(); + landlock_initialized = true; pr_info("Up and running.\n"); return 0; } diff --git a/security/landlock/setup.h b/security/landlock/setup.h index 9fdbf33fcc33..1daffab1ab4b 100644 --- a/security/landlock/setup.h +++ b/security/landlock/setup.h @@ -11,6 +11,8 @@ #include +extern bool landlock_initialized; + extern struct lsm_blob_sizes landlock_blob_sizes; #endif /* _SECURITY_LANDLOCK_SETUP_H */ From patchwork Tue Dec 1 19:23:20 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 335412 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-16.7 required=3.0 tests=BAYES_00, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 54898C843E7 for ; Tue, 1 Dec 2020 19:26:16 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 1B4B7204EC for ; Tue, 1 Dec 2020 19:26:16 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S2404336AbgLATZu (ORCPT ); Tue, 1 Dec 2020 14:25:50 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:35720 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1727431AbgLATZs (ORCPT ); Tue, 1 Dec 2020 14:25:48 -0500 Received: from smtp-bc0a.mail.infomaniak.ch (smtp-bc0a.mail.infomaniak.ch [IPv6:2001:1600:4:17::bc0a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 4680FC08E860; Tue, 1 Dec 2020 11:23:43 -0800 (PST) Received: from smtp-3-0001.mail.infomaniak.ch (unknown [10.4.36.108]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4ClsTV0dslzlhl6T; Tue, 1 Dec 2020 20:23:42 +0100 (CET) Received: from localhost (unknown [23.97.221.149]) by smtp-3-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4ClsTT54sXzlh8TF; Tue, 1 Dec 2020 20:23:41 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: James Morris , Jann Horn , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Al Viro , Andy Lutomirski , Anton Ivanov , Arnd Bergmann , Casey Schaufler , Jeff Dike , Jonathan Corbet , Kees Cook , Michael Kerrisk , Richard Weinberger , Shuah Khan , Vincent Dagonneau , kernel-hardening@lists.openwall.com, linux-api@vger.kernel.org, linux-arch@vger.kernel.org, linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, linux-security-module@vger.kernel.org, x86@kernel.org, =?utf-8?q?Mick?= =?utf-8?b?YcOrbCBTYWxhw7xu?= Subject: [PATCH v25 10/12] selftests/landlock: Add user space tests Date: Tue, 1 Dec 2020 20:23:20 +0100 Message-Id: <20201201192322.213239-11-mic@digikod.net> X-Mailer: git-send-email 2.29.2 In-Reply-To: <20201201192322.213239-1-mic@digikod.net> References: <20201201192322.213239-1-mic@digikod.net> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org From: Mickaël Salaün Test all Landlock system calls, ptrace hooks semantic and filesystem access-control. Test coverage for security/landlock/ is 94% of lines. The code not covered only deals with internal kernel errors (e.g. memory allocation) and race conditions. Cc: James Morris Cc: Jann Horn Cc: Kees Cook Cc: Serge E. Hallyn Cc: Shuah Khan Signed-off-by: Mickaël Salaün Reviewed-by: Vincent Dagonneau --- Changes since v24: * Revert the ruleset_overlap test from v24: check that access righs are ORed together when building a ruleset. Keep the extra checks added with v24. * Revert inherit_subset test from v24: use the automatic ORing of access rights for the same file. * Update interleaved_masked_accesses test (added with v24) to stop when all layers allowed at least one time an inode in the path walk. * Extend interleaved_masked_accesses test with new tricky interleaved layers which would not work as intended with (allow or deny) bitmask layer implementations. * Simplify and rename test_path*() to test_open*() to make easier the diagnostic in case of unattended errors. * Replace most call to open(2) with a call to test_open(), which reduces the number of lines and make tests more readable. * Fix erroneous check in inherit_superset. Changes since v23: * Add an interleaved_masked_accesses test to check corner cases for interleaved layered ruleset combinations. * Update ruleset_overlap and inherit_subset tests to follow the new intersect access rights behavior. * Extend the inherit_superset test to check that layers are handled as expected in the superset use case, which complete the inherit_subset checks. * Fix comment (spotted by Vincent Dagonneau). Changes since v22: * Extend and add a new test to better check rules applied to the root directory: rule_over_root_allow_then_deny, rule_over_root_deny. * Change the signature of test_path*() to make the calls clearer. Changes since v21: * Remove layout1.chroot test and update layout1.unhandled_access to not rely on LANDLOCK_ACCESS_FS_CHROOT. * Clean up comments. Changes since v20: * Update with new syscalls and type names. * Use the full syscall interfaces: explicitely set the "flags" field to zero. * Update the empty_path_beneath_attr test to check for EFAULT. * Update and merge tests for the simplified copy_min_struct_from_user(). * Clean up makefile. * Rename some types and variables in a more consistent way. Changes since v19: * Update with the new Landlock syscalls. * Fix device creation. * Check the new landlock_attr_features members: last_rule_type and last_target_type . * Constify variables. Changes since v18: * Replace ruleset_rw.inval with layout1.inval to avoid inexistent test layout. * Use the new FIXTURE_VARIANT for ptrace_test: makes the tests more readable and usable. * Add ARRAY_SIZE() macro to please checkpatch. Changes since v17: * Add new test for mknod with a zero mode. * Use memset(3) to initialize attr_features in base_test. Changes since v16: * Add new unpriv_enforce_without_no_new_privs test: check that ruleset enforcing is forbiden without no_new_privs and CAP_SYS_ADMIN. * Drop capabilities when useful. * Check the new size_attr_features field from struct landlock_attr_features. * Update the empty_or_same_ruleset test to check complementary empty ruleset. * Update base_test according to the new attribute structures and fix the inconsistent_attr test accordingly. * Switch syscall attribute pointer and size arguments. * Rename test files with a "_test" suffix. Changes since v14: * Add new tests: - superset: check new layer bitmask. - max_layers: check maximum number of layers. - release_inodes: check that umount work well. - empty_or_same_ruleset. - inconsistent_attr: checks copy_to_user limits. - in ruleset_rw.inval to check ruleset FD. - proc_unlinked_file: check file access through /proc/self/fd . - file_access_rights: check that a file can only get consistent access rights. - unpriv: check that NO_NEW_PRIVS or CAP_SYS_ADMIN is required. - check pipe access through /proc/self/fd . - check move_mount(2). - check ruleset file descriptor properties. - proc_nsfs: extend to check that internal filesystems (e.g. nsfs) are allowed. * Double-check read and write effective actions. * Fix potential desynchronization between the kernel sources and installed headers by overriding the build step in the Makefile. This also enable to build with Clang. * Add two files in the test directories (for link test and rename test). * Remove test for ruleset's show_fdinfo(). * Replace EBADR with EBADFD. * Update tests accordingly to the changes of rename and link rights. * Fix (now) illegal access rights tied to files. * Update rename and link tests. * Remove superfluous '\n' in TH_LOG() calls. * Make assert calls consistent and readable. * Fix the execute test. * Make tests future-proof. * Cosmetic fixes. Changes since v14: * Add new tests: - Compatibility: empty_attr_{ruleset,path_beneath,enforce} to check minimal attr size. - Access types: link_to, rename_from, rename_to, rmdir, unlink, make_char, make_block, make_reg, make_sock, make_fifo, make_sym, make_dir, chroot, execute. - Test privilege escalation prevention by enforcing a nested rule, on a parent directory, with less restrictions than one on a child directory. - Test for empty and more than 32-bits allowed_access * Merge the two test mount hierarchies. * Complete relative path tests by combining chdir and chroot. * Adjust tests: - Remove the layout1/extend_ruleset_with_denied_path test. - Extend layout1/whitelist test with checks on file. - Add and use create_dir_and_file(). * Only use read/write checks but not stat(2) for tests. * Rename test.h to common.h and improve it. * Rename path name to make them more consistent, easy to understand and make them in a common directory. * Make create_ruleset() more generic. * Constify variables. * Re-add static global variables. * Remove useless openat(2). * Fix and complete kernel config. * Set umask and clean up file modes. * Clean up open flags. * Improve Makefile. * Fix spelling. * Improve comments and error messages. Changes since v13: * Add back the filesystem tests (from v10) and extend them. * Add tests for the new syscall. Previous changes: https://lore.kernel.org/lkml/20191104172146.30797-7-mic@digikod.net/ --- tools/testing/selftests/Makefile | 1 + tools/testing/selftests/landlock/.gitignore | 2 + tools/testing/selftests/landlock/Makefile | 24 + tools/testing/selftests/landlock/base_test.c | 117 ++ tools/testing/selftests/landlock/common.h | 113 ++ tools/testing/selftests/landlock/config | 5 + tools/testing/selftests/landlock/fs_test.c | 1798 +++++++++++++++++ .../testing/selftests/landlock/ptrace_test.c | 307 +++ tools/testing/selftests/landlock/true.c | 5 + 9 files changed, 2372 insertions(+) create mode 100644 tools/testing/selftests/landlock/.gitignore create mode 100644 tools/testing/selftests/landlock/Makefile create mode 100644 tools/testing/selftests/landlock/base_test.c create mode 100644 tools/testing/selftests/landlock/common.h create mode 100644 tools/testing/selftests/landlock/config create mode 100644 tools/testing/selftests/landlock/fs_test.c create mode 100644 tools/testing/selftests/landlock/ptrace_test.c create mode 100644 tools/testing/selftests/landlock/true.c diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index d9c283503159..f40a34430652 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -26,6 +26,7 @@ TARGETS += ir TARGETS += kcmp TARGETS += kexec TARGETS += kvm +TARGETS += landlock TARGETS += lib TARGETS += livepatch TARGETS += lkdtm diff --git a/tools/testing/selftests/landlock/.gitignore b/tools/testing/selftests/landlock/.gitignore new file mode 100644 index 000000000000..470203a7cd73 --- /dev/null +++ b/tools/testing/selftests/landlock/.gitignore @@ -0,0 +1,2 @@ +/*_test +/true diff --git a/tools/testing/selftests/landlock/Makefile b/tools/testing/selftests/landlock/Makefile new file mode 100644 index 000000000000..a99596ca9882 --- /dev/null +++ b/tools/testing/selftests/landlock/Makefile @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: GPL-2.0 + +CFLAGS += -Wall -O2 + +src_test := $(wildcard *_test.c) + +TEST_GEN_PROGS := $(src_test:.c=) + +TEST_GEN_PROGS_EXTENDED := true + +KSFT_KHDR_INSTALL := 1 +OVERRIDE_TARGETS := 1 +include ../lib.mk + +khdr_dir = $(top_srcdir)/usr/include + +$(khdr_dir)/linux/landlock.h: khdr + @: + +$(OUTPUT)/true: true.c + $(LINK.c) $< $(LDLIBS) -o $@ -static + +$(OUTPUT)/%_test: %_test.c $(khdr_dir)/linux/landlock.h ../kselftest_harness.h common.h + $(LINK.c) $< $(LDLIBS) -o $@ -lcap -I$(khdr_dir) diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c new file mode 100644 index 000000000000..cb35eb91516b --- /dev/null +++ b/tools/testing/selftests/landlock/base_test.c @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Common user space base + * + * Copyright © 2017-2020 Mickaël Salaün + * Copyright © 2019-2020 ANSSI + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#include "common.h" + +#ifndef O_PATH +#define O_PATH 010000000 +#endif + +TEST(inconsistent_attr) { + const long page_size = sysconf(_SC_PAGESIZE); + char *const buf = malloc(page_size + 1); + struct landlock_ruleset_attr *const ruleset_attr = (void *)buf; + + ASSERT_NE(NULL, buf); + + /* Checks copy_from_user(). */ + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 0, 0)); + /* The size if less than sizeof(struct landlock_attr_enforce). */ + ASSERT_EQ(EINVAL, errno); + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 1, 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(NULL, 1, 0)); + /* The size if less than sizeof(struct landlock_attr_enforce). */ + ASSERT_EQ(EFAULT, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(NULL, + sizeof(struct landlock_ruleset_attr), 0)); + ASSERT_EQ(EFAULT, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size + 1, 0)); + ASSERT_EQ(E2BIG, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, + sizeof(struct landlock_ruleset_attr), 0)); + ASSERT_EQ(ENOMSG, errno); + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size, 0)); + ASSERT_EQ(ENOMSG, errno); + + /* Checks non-zero value. */ + buf[page_size - 2] = '.'; + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size, 0)); + ASSERT_EQ(E2BIG, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size + 1, 0)); + ASSERT_EQ(E2BIG, errno); + + free(buf); +} + +TEST(empty_path_beneath_attr) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE, + }; + const int ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + + ASSERT_LE(0, ruleset_fd); + + /* Similar to struct landlock_path_beneath_attr.parent_fd = 0 */ + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + NULL, 0)); + ASSERT_EQ(EFAULT, errno); + ASSERT_EQ(0, close(ruleset_fd)); +} + +TEST(inval_fd_enforce) { + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + ASSERT_EQ(-1, landlock_enforce_ruleset_current(-1, 0)); + ASSERT_EQ(EBADF, errno); +} + +TEST(unpriv_enforce_without_no_new_privs) { + int err; + + disable_caps(_metadata); + err = landlock_enforce_ruleset_current(-1, 0); + ASSERT_EQ(errno, EPERM); + ASSERT_EQ(err, -1); +} + +TEST(ruleset_fd) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, + }; + int ruleset_fd; + char buf; + + disable_caps(_metadata); + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(-1, write(ruleset_fd, ".", 1)); + ASSERT_EQ(EINVAL, errno); + ASSERT_EQ(-1, read(ruleset_fd, &buf, 1)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(0, close(ruleset_fd)); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h new file mode 100644 index 000000000000..d5efeb25b61f --- /dev/null +++ b/tools/testing/selftests/landlock/common.h @@ -0,0 +1,113 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Landlock test helpers + * + * Copyright © 2017-2020 Mickaël Salaün + * Copyright © 2019-2020 ANSSI + */ + +#include +#include +#include +#include + +#include "../kselftest_harness.h" + +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) +#endif + +#ifndef landlock_create_ruleset +static inline int landlock_create_ruleset( + const struct landlock_ruleset_attr *const attr, + const size_t size, const __u32 flags) +{ + errno = 0; + return syscall(__NR_landlock_create_ruleset, attr, size, flags); +} +#endif + +#ifndef landlock_add_rule +static inline int landlock_add_rule(const int ruleset_fd, + const enum landlock_rule_type rule_type, + const void *const rule_attr, const __u32 flags) +{ + errno = 0; + return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, + rule_attr, flags); +} +#endif + +#ifndef landlock_enforce_ruleset_current +static inline int landlock_enforce_ruleset_current(const int ruleset_fd, + const __u32 flags) +{ + errno = 0; + return syscall(__NR_landlock_enforce_ruleset_current, ruleset_fd, + flags); +} +#endif + +static void disable_caps(struct __test_metadata *const _metadata) +{ + cap_t cap_p; + /* Only these three capabilities are useful for the tests. */ + const cap_value_t caps[] = { + CAP_MKNOD, + CAP_SYS_ADMIN, + CAP_SYS_CHROOT, + }; + + cap_p = cap_get_proc(); + ASSERT_NE(NULL, cap_p) { + TH_LOG("Failed to cap_get_proc: %s", strerror(errno)); + } + ASSERT_NE(-1, cap_clear(cap_p)) { + TH_LOG("Failed to cap_clear: %s", strerror(errno)); + } + ASSERT_NE(-1, cap_set_flag(cap_p, CAP_PERMITTED, ARRAY_SIZE(caps), + caps, CAP_SET)) { + TH_LOG("Failed to cap_set_flag: %s", strerror(errno)); + } + ASSERT_NE(-1, cap_set_proc(cap_p)) { + TH_LOG("Failed to cap_set_proc: %s", strerror(errno)); + } + ASSERT_NE(-1, cap_free(cap_p)) { + TH_LOG("Failed to cap_free: %s", strerror(errno)); + } +} + +static void effective_cap(struct __test_metadata *const _metadata, + const cap_value_t caps, const cap_flag_value_t value) +{ + cap_t cap_p; + + cap_p = cap_get_proc(); + ASSERT_NE(NULL, cap_p) { + TH_LOG("Failed to cap_get_proc: %s", strerror(errno)); + } + ASSERT_NE(-1, cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &caps, value)) { + TH_LOG("Failed to cap_set_flag: %s", strerror(errno)); + } + ASSERT_NE(-1, cap_set_proc(cap_p)) { + TH_LOG("Failed to cap_set_proc: %s", strerror(errno)); + } + ASSERT_NE(-1, cap_free(cap_p)) { + TH_LOG("Failed to cap_free: %s", strerror(errno)); + } +} + +/* We cannot put such helpers in a library because of kselftest_harness.h . */ +__attribute__((__unused__)) +static void set_cap(struct __test_metadata *const _metadata, + const cap_value_t caps) +{ + effective_cap(_metadata, caps, CAP_SET); +} + +__attribute__((__unused__)) +static void clear_cap(struct __test_metadata *const _metadata, + const cap_value_t caps) +{ + effective_cap(_metadata, caps, CAP_CLEAR); +} diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config new file mode 100644 index 000000000000..042298105821 --- /dev/null +++ b/tools/testing/selftests/landlock/config @@ -0,0 +1,5 @@ +CONFIG_SECURITY_LANDLOCK=y +CONFIG_SECURITY_PATH=y +CONFIG_SECURITY=y +CONFIG_SHMEM=y +CONFIG_TMPFS=y diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c new file mode 100644 index 000000000000..0363c3ad8568 --- /dev/null +++ b/tools/testing/selftests/landlock/fs_test.c @@ -0,0 +1,1798 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Filesystem + * + * Copyright © 2017-2020 Mickaël Salaün + * Copyright © 2020 ANSSI + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" + +#define TMP_DIR "tmp/" +#define FILE_1 "file1" +#define FILE_2 "file2" +#define BINARY_PATH "./true" + +/* Paths (sibling number and depth) */ +static const char dir_s1d1[] = TMP_DIR "s1d1"; +static const char file1_s1d1[] = TMP_DIR "s1d1/" FILE_1; +static const char file2_s1d1[] = TMP_DIR "s1d1/" FILE_2; +static const char dir_s1d2[] = TMP_DIR "s1d1/s1d2"; +static const char file1_s1d2[] = TMP_DIR "s1d1/s1d2/" FILE_1; +static const char file2_s1d2[] = TMP_DIR "s1d1/s1d2/" FILE_2; +static const char dir_s1d3[] = TMP_DIR "s1d1/s1d2/s1d3"; +static const char file1_s1d3[] = TMP_DIR "s1d1/s1d2/s1d3/" FILE_1; +static const char file2_s1d3[] = TMP_DIR "s1d1/s1d2/s1d3/" FILE_2; + +static const char dir_s2d1[] = TMP_DIR "s2d1"; +static const char file1_s2d1[] = TMP_DIR "s2d1/" FILE_1; +static const char dir_s2d2[] = TMP_DIR "s2d1/s2d2"; +static const char file1_s2d2[] = TMP_DIR "s2d1/s2d2/" FILE_1; +static const char dir_s2d3[] = TMP_DIR "s2d1/s2d2/s2d3"; +static const char file1_s2d3[] = TMP_DIR "s2d1/s2d2/s2d3/" FILE_1; +static const char file2_s2d3[] = TMP_DIR "s2d1/s2d2/s2d3/" FILE_2; + +static const char dir_s3d1[] = TMP_DIR "s3d1"; +/* dir_s3d2 is a mount point. */ +static const char dir_s3d2[] = TMP_DIR "s3d1/s3d2"; +static const char dir_s3d3[] = TMP_DIR "s3d1/s3d2/s3d3"; + +static void create_dir_and_file(struct __test_metadata *const _metadata, + const char *const dir_path) +{ + int file_fd; + char *const file1_path = alloca(strlen(dir_path) + sizeof(FILE_1) + 2); + char *const file2_path = alloca(strlen(dir_path) + sizeof(FILE_2) + 2); + + strcpy(file1_path, dir_path); + strcat(file1_path, "/"); + strcat(file1_path, FILE_1); + + strcpy(file2_path, dir_path); + strcat(file2_path, "/"); + strcat(file2_path, FILE_2); + + ASSERT_EQ(0, mkdir(dir_path, 0700)) { + TH_LOG("Failed to create directory \"%s\": %s", dir_path, + strerror(errno)); + } + file_fd = open(file1_path, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, + 0700); + ASSERT_LE(0, file_fd); + ASSERT_EQ(0, close(file_fd)); + + file_fd = open(file2_path, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, + 0700); + ASSERT_LE(0, file_fd); + ASSERT_EQ(0, close(file_fd)); +} + +static void delete_dir_and_file(const char *const dir_path) +{ + char *const file1_path = alloca(strlen(dir_path) + + sizeof(FILE_1) + 2); + char *const file2_path = alloca(strlen(dir_path) + + sizeof(FILE_2) + 2); + + strcpy(file1_path, dir_path); + strcat(file1_path, "/"); + strcat(file1_path, FILE_1); + + strcpy(file2_path, dir_path); + strcat(file2_path, "/"); + strcat(file2_path, FILE_2); + + unlink(file1_path); + unlink(file2_path); + /* file1_path may be a directory, cf. layout1/make_directory. */ + rmdir(file1_path); + rmdir(dir_path); +} + +static void cleanup_layout1(struct __test_metadata *const _metadata) +{ + delete_dir_and_file(dir_s1d3); + delete_dir_and_file(dir_s1d2); + delete_dir_and_file(dir_s1d1); + + delete_dir_and_file(dir_s2d3); + delete_dir_and_file(dir_s2d2); + delete_dir_and_file(dir_s2d1); + + delete_dir_and_file(dir_s3d3); + set_cap(_metadata, CAP_SYS_ADMIN); + umount(dir_s3d2); + clear_cap(_metadata, CAP_SYS_ADMIN); + delete_dir_and_file(dir_s3d2); + delete_dir_and_file(dir_s3d1); + + delete_dir_and_file(TMP_DIR); +} + +FIXTURE(layout1) { +}; + +FIXTURE_SETUP(layout1) +{ + disable_caps(_metadata); + cleanup_layout1(_metadata); + + /* Do not pollute the rest of the system. */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, unshare(CLONE_NEWNS)); + clear_cap(_metadata, CAP_SYS_ADMIN); + umask(0077); + create_dir_and_file(_metadata, TMP_DIR); + + create_dir_and_file(_metadata, dir_s1d1); + create_dir_and_file(_metadata, dir_s1d2); + create_dir_and_file(_metadata, dir_s1d3); + + create_dir_and_file(_metadata, dir_s2d1); + create_dir_and_file(_metadata, dir_s2d2); + create_dir_and_file(_metadata, dir_s2d3); + + create_dir_and_file(_metadata, dir_s3d1); + create_dir_and_file(_metadata, dir_s3d2); + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount("tmp", dir_s3d2, "tmpfs", 0, "size=4m,mode=700")); + clear_cap(_metadata, CAP_SYS_ADMIN); + create_dir_and_file(_metadata, dir_s3d3); +} + +FIXTURE_TEARDOWN(layout1) +{ + /* + * cleanup_layout1() would be denied here, use TEST(cleanup) instead. + */ +} + +/* + * This helper enables to use the ASSERT_* macros and print the line number + * pointing to the test caller. + */ +static int test_open_rel(const int dirfd, const char *const path, const int flags) +{ + int fd; + + /* Works with file and directories. */ + fd = openat(dirfd, path, flags | O_CLOEXEC); + if (fd < 0) + return errno; + if (close(fd) == 0) + return 0; + /* + * Mixing error codes from close(2) and open(2) should not lead to any + * (access type) confusion for this test. + */ + return errno; +} + +static int test_open(const char *const path, const int flags) +{ + return test_open_rel(AT_FDCWD, path, flags); +} + +TEST_F(layout1, no_restriction) +{ + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + + ASSERT_EQ(0, test_open(dir_s2d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s2d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s2d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s2d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s2d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s2d3, O_RDONLY)); + + ASSERT_EQ(0, test_open(dir_s3d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d3, O_RDONLY)); +} + +TEST_F(layout1, inval) +{ + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + .parent_fd = -1, + }; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }; + int ruleset_fd; + + path_beneath.parent_fd = open(dir_s1d2, O_PATH | O_DIRECTORY | + O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + + ruleset_fd = open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + /* Returns EBADF because ruleset_fd contains O_PATH. */ + ASSERT_EQ(EBADF, errno); + ASSERT_EQ(0, close(ruleset_fd)); + + ruleset_fd = open(dir_s1d1, O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + /* Returns EBADFD because ruleset_fd is not a valid ruleset. */ + ASSERT_EQ(EBADFD, errno); + ASSERT_EQ(0, close(ruleset_fd)); + + /* Gets a real ruleset. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(0, close(path_beneath.parent_fd)); + + /* Tests without O_PATH. */ + path_beneath.parent_fd = open(dir_s1d2, O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(EBADFD, errno); + ASSERT_EQ(0, close(path_beneath.parent_fd)); + + /* Checks unhandled allowed_access. */ + path_beneath.parent_fd = open(dir_s1d2, O_PATH | O_DIRECTORY | + O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + + /* Test with legitimate values. */ + path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_EXECUTE; + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(EINVAL, errno); + path_beneath.allowed_access &= ~LANDLOCK_ACCESS_FS_EXECUTE; + + /* Test with unknown (64-bits) value. */ + path_beneath.allowed_access |= (1ULL << 60); + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(EINVAL, errno); + path_beneath.allowed_access &= ~(1ULL << 60); + + /* Test with no access. */ + path_beneath.allowed_access = 0; + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + path_beneath.allowed_access &= ~(1ULL << 60); + + ASSERT_EQ(0, close(path_beneath.parent_fd)); + + /* Enforces the ruleset. */ + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_enforce_ruleset_current(ruleset_fd, 0)); + + ASSERT_EQ(0, close(ruleset_fd)); +} + +#define ACCESS_FILE ( \ + LANDLOCK_ACCESS_FS_EXECUTE | \ + LANDLOCK_ACCESS_FS_WRITE_FILE | \ + LANDLOCK_ACCESS_FS_READ_FILE) + +#define ACCESS_LAST LANDLOCK_ACCESS_FS_MAKE_SYM + +#define ACCESS_ALL ( \ + ACCESS_FILE | \ + LANDLOCK_ACCESS_FS_READ_DIR | \ + LANDLOCK_ACCESS_FS_REMOVE_DIR | \ + LANDLOCK_ACCESS_FS_REMOVE_FILE | \ + LANDLOCK_ACCESS_FS_MAKE_CHAR | \ + LANDLOCK_ACCESS_FS_MAKE_DIR | \ + LANDLOCK_ACCESS_FS_MAKE_REG | \ + LANDLOCK_ACCESS_FS_MAKE_SOCK | \ + LANDLOCK_ACCESS_FS_MAKE_FIFO | \ + LANDLOCK_ACCESS_FS_MAKE_BLOCK | \ + ACCESS_LAST) + +TEST_F(layout1, file_access_rights) +{ + __u64 access; + int err; + struct landlock_path_beneath_attr path_beneath = {}; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = ACCESS_ALL, + }; + const int ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + + ASSERT_LE(0, ruleset_fd); + + /* Tests access rights for files. */ + path_beneath.parent_fd = open(file1_s1d2, O_PATH | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + for (access = 1; access <= ACCESS_LAST; access <<= 1) { + path_beneath.allowed_access = access; + err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0); + if ((access | ACCESS_FILE) == ACCESS_FILE) { + ASSERT_EQ(0, err); + } else { + ASSERT_EQ(-1, err); + ASSERT_EQ(EINVAL, errno); + } + } + ASSERT_EQ(0, close(path_beneath.parent_fd)); +} + +static void add_path_beneath(struct __test_metadata *const _metadata, + const int ruleset_fd, const __u64 allowed_access, + const char *const path) +{ + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = allowed_access, + }; + + path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd) { + TH_LOG("Failed to open directory \"%s\": %s", path, + strerror(errno)); + } + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)) { + TH_LOG("Failed to update the ruleset with \"%s\": %s", path, + strerror(errno)); + } + ASSERT_EQ(0, close(path_beneath.parent_fd)); +} + +struct rule { + const char *path; + __u64 access; +}; + +#define ACCESS_RO ( \ + LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_READ_DIR) + +#define ACCESS_RW ( \ + ACCESS_RO | \ + LANDLOCK_ACCESS_FS_WRITE_FILE) + +static int create_ruleset(struct __test_metadata *const _metadata, + const __u64 handled_access_fs, const struct rule rules[]) +{ + int ruleset_fd, i; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = handled_access_fs, + }; + + ASSERT_NE(NULL, rules) { + TH_LOG("No rule list"); + } + ASSERT_NE(NULL, rules[0].path) { + TH_LOG("Empty rule list"); + } + + 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)); + } + + for (i = 0; rules[i].path; i++) { + add_path_beneath(_metadata, ruleset_fd, rules[i].access, + rules[i].path); + } + return ruleset_fd; +} + +static void enforce_ruleset(struct __test_metadata *const _metadata, + const int ruleset_fd) +{ + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_enforce_ruleset_current(ruleset_fd, 0)) { + TH_LOG("Failed to enforce ruleset: %s", strerror(errno)); + } +} + +TEST_F(layout1, proc_nsfs) +{ + const struct rule rules[] = { + { + .path = "/dev/null", + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {} + }; + struct landlock_path_beneath_attr path_beneath; + const int ruleset_fd = create_ruleset(_metadata, rules[0].access | + LANDLOCK_ACCESS_FS_READ_DIR, rules); + + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, test_open("/proc/self/ns/mnt", O_RDONLY)); + + enforce_ruleset(_metadata, ruleset_fd); + + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/dev", O_RDONLY)); + ASSERT_EQ(0, test_open("/dev/null", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/dev/full", O_RDONLY)); + + ASSERT_EQ(EACCES, test_open("/proc", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/proc/self", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/proc/self/ns", O_RDONLY)); + /* + * Because nsfs is an internal filesystem, /proc/self/ns/mnt is a + * disconnected path. Such path cannot be identified and must then be + * allowed. + */ + ASSERT_EQ(0, test_open("/proc/self/ns/mnt", O_RDONLY)); + + /* + * Checks that it is not possible to add nsfs-like filesystem + * references to a ruleset. + */ + path_beneath.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + path_beneath.parent_fd = open("/proc/self/ns/mnt", O_PATH | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(EBADFD, errno); + ASSERT_EQ(0, close(path_beneath.parent_fd)); +} + +static void drop_privileges(struct __test_metadata *const _metadata) +{ + cap_t caps; + const cap_value_t cap_val = CAP_SYS_ADMIN; + + caps = cap_get_proc(); + ASSERT_NE(NULL, caps); + ASSERT_EQ(0, cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap_val, + CAP_CLEAR)); + ASSERT_EQ(0, cap_set_proc(caps)); + ASSERT_EQ(0, cap_free(caps)); +} + +TEST_F(layout1, unpriv) { + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = ACCESS_RO, + }, + {} + }; + int ruleset_fd; + + drop_privileges(_metadata); + ruleset_fd = create_ruleset(_metadata, ACCESS_RO, rules); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(-1, landlock_enforce_ruleset_current(ruleset_fd, 0)); + ASSERT_EQ(EPERM, errno); + + /* enforce_ruleset() calls prctl(no_new_privs). */ + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); +} + +TEST_F(layout1, whitelist) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = ACCESS_RO, + }, + { + .path = file1_s2d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + char buf; + int reg_fd; + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Tests on a directory. */ + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + + /* Tests on a file. */ + ASSERT_EQ(EACCES, test_open(dir_s2d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s2d2, O_RDONLY)); + + /* Checks effective read and write actions. */ + reg_fd = open(file1_s2d2, O_RDWR | O_CLOEXEC); + ASSERT_LE(0, reg_fd); + ASSERT_EQ(1, write(reg_fd, ".", 1)); + ASSERT_LE(0, lseek(reg_fd, 0, SEEK_SET)); + ASSERT_EQ(1, read(reg_fd, &buf, 1)); + ASSERT_EQ('.', buf); + ASSERT_EQ(0, close(reg_fd)); + + /* Just in case, double-checks effective actions. */ + reg_fd = open(file1_s2d2, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, reg_fd); + ASSERT_EQ(-1, write(reg_fd, &buf, 1)); + ASSERT_EQ(EBADF, errno); + ASSERT_EQ(0, close(reg_fd)); +} + +TEST_F(layout1, unhandled_access) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = ACCESS_RO, + }, + {} + }; + /* Here, we only handle read accesses, not write accesses. */ + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RO, rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* + * Because the policy does not handle LANDLOCK_ACCESS_FS_WRITE_FILE, + * opening for write-only should be allowed, but not read-write. + */ + ASSERT_EQ(0, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDWR)); + + ASSERT_EQ(0, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDWR)); +} + +TEST_F(layout1, ruleset_overlap) +{ + const struct rule rules[] = { + /* These rules should be ORed among them. */ + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks s1d1 hierarchy. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d2 hierarchy. */ + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDWR)); + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d3 hierarchy. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); +} + +TEST_F(layout1, interleaved_masked_accesses) +{ + /* + * Checks overly restrictive rules: + * layer 1: allows R s1d1/s1d2/s1d3/file1 + * layer 2: allows R s1d1/s1d2/s1d3 + * denies R s1d1/s1d2 + * layer 3: allows R s1d1 + * layer 4: denies W s1d1/s1d2 + * layer 5: allows R s1d1/s1d2 + * layer 6: denies R s1d1/s1d2 + */ + const struct rule layer1_read[] = { + /* Allows access to file1_s1d3 with the first layer. */ + { + .path = file1_s1d3, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {} + }; + const struct rule layer2_read[] = { + /* Start by granting access via its parent directory... */ + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + /* ...but also denies access via its grandparent directory. */ + { + .path = dir_s1d2, + .access = 0, + }, + {} + }; + const struct rule layer3_read[] = { + /* Allows access via its great-grandparent directory. */ + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {} + }; + const struct rule layer4_write[] = { + /* + * Try to confuse the deny access by denying write (but not + * read) access via its grandparent directory. + */ + { + .path = dir_s1d2, + .access = 0, + }, + {} + }; + const struct rule layer5_read[] = { + /* + * Try to override layer2's deny read access by explicitly + * allowing read access via file1_s1d3's grandparent. + */ + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {} + }; + const struct rule layer6_read[] = { + /* + * Finally, denies read access to file1_s1d3 via its + * grandparent. + */ + { + .path = dir_s1d2, + .access = 0, + }, + {} + }; + int ruleset_fd; + + ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, + layer1_read); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks that access is granted for file1_s1d3 with layer 1. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY)); + + ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, + layer2_read); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks that previous access rights are unchanged with layer 2. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY)); + + ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, + layer3_read); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks that previous access rights are unchanged with layer 3. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY)); + + /* This time, creates a write-only rule. */ + ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_WRITE_FILE, + layer4_write); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* + * Checks that the only change with layer 4 is that write access is + * denied. + */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY)); + + ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, + layer5_read); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks that previous access rights are unchanged with layer 5. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + + ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, + layer6_read); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks read access is now denied with layer 6. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); +} + +TEST_F(layout1, inherit_subset) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Write access is forbidden. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + /* Readdir access is allowed. */ + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* Write access is forbidden. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + /* Readdir access is allowed. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + + /* + * Tests shared rule extension: the following rules should not grant + * any new access, only remove some. Once enforced, these rules are + * ANDed with the previous ones. + */ + add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_WRITE_FILE, + dir_s1d2); + /* + * According to ruleset_fd, dir_s1d2 should now have the + * LANDLOCK_ACCESS_FS_READ_FILE and LANDLOCK_ACCESS_FS_WRITE_FILE + * access rights (even if this directory is opened a second time). + * However, when enforcing this updated ruleset, the ruleset tied to + * the current process (i.e. its domain) will still only have the + * dir_s1d2 with LANDLOCK_ACCESS_FS_READ_FILE and + * LANDLOCK_ACCESS_FS_READ_DIR accesses, but + * LANDLOCK_ACCESS_FS_WRITE_FILE must not be allowed because it would + * be a privilege escalation. + */ + enforce_ruleset(_metadata, ruleset_fd); + + /* Same tests and results as above. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d2. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d3. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + + /* + * Try to get more privileges by adding new access rights to the parent + * directory: dir_s1d1. + */ + add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, dir_s1d1); + enforce_ruleset(_metadata, ruleset_fd); + + /* Same tests and results as above. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d2. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d3. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + + /* + * Now, dir_s1d3 get a new rule tied to it, only allowing + * LANDLOCK_ACCESS_FS_WRITE_FILE. The (kernel internal) difference is + * that there was no rule tied to it before. + */ + add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_WRITE_FILE, + dir_s1d3); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* + * Same tests and results as above, except for open(dir_s1d3) which is + * now denied because the new rule mask the rule previously inherited + * from dir_s1d2. + */ + + /* Same tests and results as above. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d2. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d3. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + /* Readdir of dir_s1d3 is now forbidden too. */ + ASSERT_EQ(EACCES, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); +} + +TEST_F(layout1, inherit_superset) +{ + const struct rule rules[] = { + { + .path = dir_s1d3, + .access = ACCESS_RO, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + + /* Readdir access is denied for dir_s1d2. */ + ASSERT_EQ(EACCES, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + /* Readdir access is allowed for dir_s1d3. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + /* File access is allowed for file1_s1d3. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + + /* Now dir_s1d2, parent of dir_s1d3, gets a new rule tied to it. */ + add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, dir_s1d2); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Readdir access is still denied for dir_s1d2. */ + ASSERT_EQ(EACCES, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + /* Readdir access is still allowed for dir_s1d3. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + /* File access is still allowed for file1_s1d3. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); +} + +TEST_F(layout1, max_layers) +{ + int i, err; + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = ACCESS_RO, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + for (i = 0; i < 64; i++) + enforce_ruleset(_metadata, ruleset_fd); + + for (i = 0; i < 2; i++) { + err = landlock_enforce_ruleset_current(ruleset_fd, 0); + ASSERT_EQ(-1, err); + ASSERT_EQ(E2BIG, errno); + } + EXPECT_EQ(0, close(ruleset_fd)); +} + +TEST_F(layout1, empty_or_same_ruleset) +{ + struct landlock_ruleset_attr ruleset_attr = {}; + int ruleset_fd; + + /* Tests empty handled_access_fs. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(-1, ruleset_fd); + ASSERT_EQ(ENOMSG, errno); + + /* Enforces policy which deny read access to all files. */ + ruleset_attr.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE; + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + + /* Nests a policy which deny read access to all directories. */ + ruleset_attr.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR; + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY)); + + /* Enforces a second time with the same ruleset. */ + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); +} + +TEST_F(layout1, rule_on_mountpoint) +{ + const struct rule rules[] = { + { + .path = dir_s1d1, + .access = ACCESS_RO, + }, + { + /* dir_s3d2 is a mount point. */ + .path = dir_s3d2, + .access = ACCESS_RO, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + + ASSERT_EQ(EACCES, test_open(dir_s2d1, O_RDONLY)); + + ASSERT_EQ(EACCES, test_open(dir_s3d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d3, O_RDONLY)); +} + +TEST_F(layout1, rule_over_mountpoint) +{ + const struct rule rules[] = { + { + .path = dir_s1d1, + .access = ACCESS_RO, + }, + { + /* dir_s3d2 is a mount point. */ + .path = dir_s3d1, + .access = ACCESS_RO, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + + ASSERT_EQ(EACCES, test_open(dir_s2d1, O_RDONLY)); + + ASSERT_EQ(0, test_open(dir_s3d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d3, O_RDONLY)); +} + +/* + * This test verifies that we can apply a landlock rule on the root directory + * (which might require special handling). + */ +TEST_F(layout1, rule_over_root_allow_then_deny) +{ + struct rule rules[] = { + { + .path = "/", + .access = ACCESS_RO, + }, + {} + }; + int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks allowed access. */ + ASSERT_EQ(0, test_open("/", O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + + rules[0].access = LANDLOCK_ACCESS_FS_READ_FILE; + ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks denied access (on a directory). */ + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY)); +} + +TEST_F(layout1, rule_over_root_deny) +{ + const struct rule rules[] = { + { + .path = "/", + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks denied access (on a directory). */ + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY)); +} + +TEST_F(layout1, rule_inside_mount_ns) +{ + const struct rule rules[] = { + { + .path = "s3d3", + .access = ACCESS_RO, + }, + {} + }; + int ruleset_fd; + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL)); + ASSERT_EQ(0, syscall(SYS_pivot_root, dir_s3d2, dir_s3d3)) { + TH_LOG("Failed to pivot_root into \"%s\": %s", dir_s3d2, + strerror(errno)); + }; + ASSERT_EQ(0, chdir("/")); + clear_cap(_metadata, CAP_SYS_ADMIN); + + ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(0, test_open("s3d3", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); +} + +TEST_F(layout1, mount_and_pivot) +{ + const struct rule rules[] = { + { + .path = dir_s3d2, + .access = ACCESS_RO, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(-1, mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL)); + ASSERT_EQ(EPERM, errno); + ASSERT_EQ(-1, syscall(SYS_pivot_root, dir_s3d2, dir_s3d3)); + ASSERT_EQ(EPERM, errno); +} + +TEST_F(layout1, move_mount) +{ + const struct rule rules[] = { + { + .path = dir_s3d2, + .access = ACCESS_RO, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL)); + ASSERT_EQ(0, syscall(SYS_move_mount, AT_FDCWD, dir_s3d2, AT_FDCWD, + dir_s1d2, 0)) { + TH_LOG("Failed to move_mount: %s", strerror(errno)); + } + ASSERT_EQ(0, syscall(SYS_move_mount, AT_FDCWD, dir_s1d2, AT_FDCWD, + dir_s3d2, 0)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(-1, syscall(SYS_move_mount, AT_FDCWD, dir_s3d2, AT_FDCWD, + dir_s1d2, 0)); + ASSERT_EQ(EPERM, errno); +} + +TEST_F(layout1, release_inodes) +{ + const struct rule rules[] = { + { + .path = dir_s1d1, + .access = ACCESS_RO, + }, + { + .path = dir_s3d2, + .access = ACCESS_RO, + }, + { + .path = dir_s3d3, + .access = ACCESS_RO, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + /* Unmount a file hierarchy while it is being used by a ruleset. */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, umount(dir_s3d2)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(dir_s3d2, O_RDONLY)); + /* This dir_s3d3 would not be allowed and does not exist anyway. */ + ASSERT_EQ(ENOENT, test_open(dir_s3d3, O_RDONLY)); +} + +enum relative_access { + REL_OPEN, + REL_CHDIR, + REL_CHROOT_ONLY, + REL_CHROOT_CHDIR, +}; + +static void test_relative_path(struct __test_metadata *const _metadata, + const enum relative_access rel) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = ACCESS_RO, + }, + { + .path = dir_s2d2, + .access = ACCESS_RO, + }, + {} + }; + int dirfd; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + ASSERT_LE(0, ruleset_fd); + switch (rel) { + case REL_OPEN: + case REL_CHDIR: + break; + case REL_CHROOT_ONLY: + ASSERT_EQ(0, chdir(dir_s2d2)); + break; + case REL_CHROOT_CHDIR: + ASSERT_EQ(0, chdir(dir_s1d2)); + break; + default: + ASSERT_TRUE(false); + return; + } + + set_cap(_metadata, CAP_SYS_CHROOT); + enforce_ruleset(_metadata, ruleset_fd); + + switch (rel) { + case REL_OPEN: + dirfd = open(dir_s1d2, O_DIRECTORY); + ASSERT_LE(0, dirfd); + break; + case REL_CHDIR: + ASSERT_EQ(0, chdir(dir_s1d2)); + dirfd = AT_FDCWD; + break; + case REL_CHROOT_ONLY: + /* Do chroot into dir_s1d2 (relative to dir_s2d2). */ + ASSERT_EQ(0, chroot("../../s1d1/s1d2")) { + TH_LOG("Failed to chroot: %s", strerror(errno)); + } + dirfd = AT_FDCWD; + break; + case REL_CHROOT_CHDIR: + /* Do chroot into dir_s1d2. */ + ASSERT_EQ(0, chroot(".")) { + TH_LOG("Failed to chroot: %s", strerror(errno)); + } + dirfd = AT_FDCWD; + break; + } + + ASSERT_EQ((rel == REL_CHROOT_CHDIR) ? 0 : EACCES, + test_open_rel(dirfd, "..", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, ".", O_RDONLY)); + + if (rel == REL_CHROOT_ONLY) { + /* The current directory is dir_s2d2. */ + ASSERT_EQ(0, test_open_rel(dirfd, "./s2d3", O_RDONLY)); + } else { + /* The current directory is dir_s1d2. */ + ASSERT_EQ(0, test_open_rel(dirfd, "./s1d3", O_RDONLY)); + } + + if (rel != REL_CHROOT_CHDIR) { + ASSERT_EQ(EACCES, test_open_rel(dirfd, "../../s1d1", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "../../s1d1/s1d2", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "../../s1d1/s1d2/s1d3", O_RDONLY)); + + ASSERT_EQ(EACCES, test_open_rel(dirfd, "../../s2d1", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "../../s2d1/s2d2", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "../../s2d1/s2d2/s2d3", O_RDONLY)); + } + + if (rel == REL_OPEN) + EXPECT_EQ(0, close(dirfd)); + EXPECT_EQ(0, close(ruleset_fd)); +} + +TEST_F(layout1, relative_open) +{ + test_relative_path(_metadata, REL_OPEN); +} + +TEST_F(layout1, relative_chdir) +{ + test_relative_path(_metadata, REL_CHDIR); +} + +TEST_F(layout1, relative_chroot_only) +{ + test_relative_path(_metadata, REL_CHROOT_ONLY); +} + +TEST_F(layout1, relative_chroot_chdir) +{ + test_relative_path(_metadata, REL_CHROOT_CHDIR); +} + +static void copy_binary(struct __test_metadata *const _metadata, + const char *const dst_path) +{ + int dst_fd, src_fd; + struct stat statbuf; + + dst_fd = open(dst_path, O_WRONLY | O_TRUNC | O_CLOEXEC); + ASSERT_LE(0, dst_fd) { + TH_LOG("Failed to open \"%s\": %s", dst_path, + strerror(errno)); + } + src_fd = open(BINARY_PATH, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, src_fd) { + TH_LOG("Failed to open \"" BINARY_PATH "\": %s", + strerror(errno)); + } + ASSERT_EQ(0, fstat(src_fd, &statbuf)); + ASSERT_EQ(statbuf.st_size, sendfile(dst_fd, src_fd, 0, + statbuf.st_size)); + ASSERT_EQ(0, close(src_fd)); + ASSERT_EQ(0, close(dst_fd)); +} + +static void test_execute(struct __test_metadata *const _metadata, + const char *const path, const int ret) +{ + int status; + char *const argv[] = {(char *)path, NULL}; + const pid_t child = fork(); + + ASSERT_LE(0, child); + if (child == 0) { + ASSERT_EQ(ret, execve(path, argv, NULL)) { + TH_LOG("Failed to execute \"%s\": %s", path, + strerror(errno)); + }; + ASSERT_EQ(EACCES, errno); + _exit(_metadata->passed ? 2 : 1); + return; + } + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFEXITED(status)); + ASSERT_EQ(ret ? 2 : 0, WEXITSTATUS(status)) { + TH_LOG("Unexpected return code for \"%s\": %s", path, + strerror(errno)); + }; +} + +TEST_F(layout1, execute) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_EXECUTE, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, rules[0].access, + rules); + + ASSERT_LE(0, ruleset_fd); + copy_binary(_metadata, file1_s1d1); + copy_binary(_metadata, file1_s1d2); + copy_binary(_metadata, file1_s1d3); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + test_execute(_metadata, file1_s1d1, -1); + test_execute(_metadata, file1_s1d2, 0); + test_execute(_metadata, file1_s1d3, 0); +} + +TEST_F(layout1, link) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_MAKE_REG, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, rules[0].access, + rules); + + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file1_s1d3)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(-1, link(file2_s1d1, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + /* Denies linking because of reparenting. */ + ASSERT_EQ(-1, link(file1_s2d1, file1_s1d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, link(file2_s1d2, file1_s1d3)); + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, link(file2_s1d2, file1_s1d2)) { + TH_LOG("Failed to link file to \"%s\": %s", file2_s1d2, + strerror(errno)); + }; + ASSERT_EQ(0, link(file2_s1d3, file1_s1d3)); +} + +TEST_F(layout1, rename_file) +{ + const struct rule rules[] = { + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, rules[0].access, + rules); + + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Replaces file. */ + ASSERT_EQ(-1, rename(file1_s2d3, file1_s1d3)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rename(file1_s2d1, file1_s1d3)); + ASSERT_EQ(EACCES, errno); + /* Same parent. */ + ASSERT_EQ(0, rename(file2_s2d3, file1_s2d3)) { + TH_LOG("Failed to rename file \"%s\": %s", file2_s2d3, + strerror(errno)); + }; + + /* Renames files. */ + ASSERT_EQ(-1, rename(file1_s2d2, file1_s1d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, unlink(file1_s1d3)); + ASSERT_EQ(-1, rename(file1_s2d1, file1_s1d3)); + ASSERT_EQ(EACCES, errno); + /* Same parent. */ + ASSERT_EQ(0, rename(file2_s1d3, file1_s1d3)); +} + +TEST_F(layout1, rename_dir) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REMOVE_DIR, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_REMOVE_DIR, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, rules[0].access, + rules); + + ASSERT_LE(0, ruleset_fd); + + /* Empties dir_s1d3. */ + ASSERT_EQ(0, unlink(file1_s1d3)); + ASSERT_EQ(0, unlink(file2_s1d3)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Renames directory. */ + ASSERT_EQ(-1, rename(dir_s2d3, dir_s1d3)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, rename(dir_s1d3, file1_s1d2)) { + TH_LOG("Failed to rename directory \"%s\": %s", dir_s1d3, + strerror(errno)); + }; + ASSERT_EQ(0, rmdir(file1_s1d2)); +} + +TEST_F(layout1, rmdir) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REMOVE_DIR, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, rules[0].access, + rules); + + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file1_s1d3)); + ASSERT_EQ(0, unlink(file2_s1d3)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(0, rmdir(dir_s1d3)); + /* dir_s1d2 itself cannot be removed. */ + ASSERT_EQ(-1, rmdir(dir_s1d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rmdir(dir_s1d1)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(layout1, unlink) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, rules[0].access, + rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(-1, unlink(file1_s1d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, unlink(file1_s1d2)) { + TH_LOG("Failed to unlink file \"%s\": %s", file1_s1d2, + strerror(errno)); + }; + ASSERT_EQ(0, unlink(file1_s1d3)); +} + +static void test_make_file(struct __test_metadata *const _metadata, + const __u64 access, const mode_t mode, const dev_t dev) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = access, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, access, rules); + + ASSERT_LE(0, ruleset_fd); + + unlink(file1_s1d1); + unlink(file1_s1d2); + unlink(file1_s1d3); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(-1, mknod(file1_s1d1, mode | 0400, dev)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, mknod(file1_s1d2, mode | 0400, dev)) { + TH_LOG("Failed to make file \"%s\": %s", + file1_s1d2, strerror(errno)); + }; + ASSERT_EQ(0, mknod(file1_s1d3, mode | 0400, dev)); +} + +TEST_F(layout1, make_char) +{ + /* Creates a /dev/null device. */ + set_cap(_metadata, CAP_MKNOD); + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_CHAR, S_IFCHR, + makedev(1, 3)); +} + +TEST_F(layout1, make_block) +{ + /* Creates a /dev/loop0 device. */ + set_cap(_metadata, CAP_MKNOD); + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_BLOCK, S_IFBLK, + makedev(7, 0)); +} + +TEST_F(layout1, make_reg) +{ + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, S_IFREG, 0); + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, 0, 0); +} + +TEST_F(layout1, make_sock) +{ + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_SOCK, S_IFSOCK, 0); +} + +TEST_F(layout1, make_fifo) +{ + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_FIFO, S_IFIFO, 0); +} + +TEST_F(layout1, make_sym) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_MAKE_SYM, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, rules[0].access, + rules); + + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file1_s1d3)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(-1, symlink("none", file1_s1d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, symlink("none", file1_s1d2)) { + TH_LOG("Failed to make symlink \"%s\": %s", + file1_s1d2, strerror(errno)); + }; + ASSERT_EQ(0, symlink("none", file1_s1d3)); +} + +TEST_F(layout1, make_dir) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_MAKE_DIR, + }, + {} + }; + const int ruleset_fd = create_ruleset(_metadata, rules[0].access, + rules); + + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file1_s1d3)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Uses file_* as directory names. */ + ASSERT_EQ(-1, mkdir(file1_s1d1, 0700)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, mkdir(file1_s1d2, 0700)) { + TH_LOG("Failed to make directory \"%s\": %s", + file1_s1d2, strerror(errno)); + }; + ASSERT_EQ(0, mkdir(file1_s1d3, 0700)); +} + +static int open_proc_fd(struct __test_metadata *const _metadata, const int fd, + const int open_flags) +{ + static const char path_template[] = "/proc/self/fd/%d"; + char procfd_path[sizeof(path_template) + 10]; + const int procfd_path_size = snprintf(procfd_path, sizeof(procfd_path), + path_template, fd); + + ASSERT_LT(procfd_path_size, sizeof(procfd_path)); + return open(procfd_path, open_flags); +} + +TEST_F(layout1, proc_unlinked_file) +{ + const struct rule rules[] = { + { + .path = file1_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {} + }; + int reg_fd, proc_fd; + const int ruleset_fd = create_ruleset(_metadata, + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDWR)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + reg_fd = open(file1_s1d2, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, reg_fd); + ASSERT_EQ(0, unlink(file1_s1d2)); + + proc_fd = open_proc_fd(_metadata, reg_fd, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, proc_fd); + EXPECT_EQ(0, close(proc_fd)); + + proc_fd = open_proc_fd(_metadata, reg_fd, O_RDWR | O_CLOEXEC); + ASSERT_EQ(-1, proc_fd) { + TH_LOG("Successfully opened /proc/self/fd/%d: %s", + reg_fd, strerror(errno)); + } + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, close(reg_fd)); +} + +TEST_F(layout1, proc_pipe) +{ + int proc_fd; + int pipe_fds[2]; + char buf = '\0'; + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {} + }; + /* Limits read and write access to files tied to the filesystem. */ + const int ruleset_fd = create_ruleset(_metadata, rules[0].access, + rules); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks enforcement for normal files. */ + ASSERT_EQ(0, test_open(file1_s1d2, O_RDWR)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDWR)); + + /* Checks access to pipes through FD. */ + ASSERT_EQ(0, pipe(pipe_fds)); + ASSERT_EQ(1, write(pipe_fds[1], ".", 1)) { + TH_LOG("Failed to write in pipe: %s", strerror(errno)); + } + ASSERT_EQ(1, read(pipe_fds[0], &buf, 1)); + ASSERT_EQ('.', buf); + + /* Checks write access to pipe through /proc/self/fd . */ + proc_fd = open_proc_fd(_metadata, pipe_fds[1], O_WRONLY | O_CLOEXEC); + ASSERT_LE(0, proc_fd); + ASSERT_EQ(1, write(proc_fd, ".", 1)) { + TH_LOG("Failed to write through /proc/self/fd/%d: %s", + pipe_fds[1], strerror(errno)); + } + EXPECT_EQ(0, close(proc_fd)); + + /* Checks read access to pipe through /proc/self/fd . */ + proc_fd = open_proc_fd(_metadata, pipe_fds[0], O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, proc_fd); + buf = '\0'; + ASSERT_EQ(1, read(proc_fd, &buf, 1)) { + TH_LOG("Failed to read through /proc/self/fd/%d: %s", + pipe_fds[1], strerror(errno)); + } + EXPECT_EQ(0, close(proc_fd)); + + EXPECT_EQ(0, close(pipe_fds[0])); + EXPECT_EQ(0, close(pipe_fds[1])); +} + +TEST(cleanup) +{ + cleanup_layout1(_metadata); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/ptrace_test.c b/tools/testing/selftests/landlock/ptrace_test.c new file mode 100644 index 000000000000..f1451b305181 --- /dev/null +++ b/tools/testing/selftests/landlock/ptrace_test.c @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Ptrace + * + * Copyright © 2017-2020 Mickaël Salaün + * Copyright © 2019-2020 ANSSI + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" + +static void create_domain(struct __test_metadata *const _metadata) +{ + int ruleset_fd; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, + }; + struct landlock_path_beneath_attr path_beneath_attr = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE, + }; + + 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)); + } + path_beneath_attr.parent_fd = open("/tmp", O_PATH | O_NOFOLLOW | + O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath_attr.parent_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath_attr, 0)); + ASSERT_EQ(0, errno); + ASSERT_EQ(0, close(path_beneath_attr.parent_fd)); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, errno); + + ASSERT_EQ(0, landlock_enforce_ruleset_current(ruleset_fd, 0)); + ASSERT_EQ(0, errno); + + ASSERT_EQ(0, close(ruleset_fd)); +} + +FIXTURE(hierarchy) { }; + +FIXTURE_VARIANT(hierarchy) { + const bool domain_both; + const bool domain_parent; + const bool domain_child; +}; + +/* + * Test multiple tracing combinations between a parent process P1 and a child + * process P2. + * + * Yama's scoped ptrace is presumed disabled. If enabled, this optional + * restriction is enforced in addition to any Landlock check, which means that + * all P2 requests to trace P1 would be denied. + */ + +/* + * No domain + * + * P1-. P1 -> P2 : allow + * \ P2 -> P1 : allow + * 'P2 + */ +FIXTURE_VARIANT_ADD(hierarchy, allow_without_domain) { + .domain_both = false, + .domain_parent = false, + .domain_child = false, +}; + +/* + * Child domain + * + * P1--. P1 -> P2 : allow + * \ P2 -> P1 : deny + * .'-----. + * | P2 | + * '------' + */ +FIXTURE_VARIANT_ADD(hierarchy, allow_with_one_domain) { + .domain_both = false, + .domain_parent = false, + .domain_child = true, +}; + +/* + * Parent domain + * .------. + * | P1 --. P1 -> P2 : deny + * '------' \ P2 -> P1 : allow + * ' + * P2 + */ +FIXTURE_VARIANT_ADD(hierarchy, deny_with_parent_domain) { + .domain_both = false, + .domain_parent = true, + .domain_child = false, +}; + +/* + * Parent + child domain (siblings) + * .------. + * | P1 ---. P1 -> P2 : deny + * '------' \ P2 -> P1 : deny + * .---'--. + * | P2 | + * '------' + */ +FIXTURE_VARIANT_ADD(hierarchy, deny_with_sibling_domain) { + .domain_both = false, + .domain_parent = true, + .domain_child = true, +}; + +/* + * Same domain (inherited) + * .-------------. + * | P1----. | P1 -> P2 : allow + * | \ | P2 -> P1 : allow + * | ' | + * | P2 | + * '-------------' + */ +FIXTURE_VARIANT_ADD(hierarchy, allow_sibling_domain) { + .domain_both = true, + .domain_parent = false, + .domain_child = false, +}; + +/* + * Inherited + child domain + * .-----------------. + * | P1----. | P1 -> P2 : allow + * | \ | P2 -> P1 : deny + * | .-'----. | + * | | P2 | | + * | '------' | + * '-----------------' + */ +FIXTURE_VARIANT_ADD(hierarchy, allow_with_nested_domain) { + .domain_both = true, + .domain_parent = false, + .domain_child = true, +}; + +/* + * Inherited + parent domain + * .-----------------. + * |.------. | P1 -> P2 : deny + * || P1 ----. | P2 -> P1 : allow + * |'------' \ | + * | ' | + * | P2 | + * '-----------------' + */ +FIXTURE_VARIANT_ADD(hierarchy, deny_with_nested_and_parent_domain) { + .domain_both = true, + .domain_parent = true, + .domain_child = false, +}; + +/* + * Inherited + parent and child domain (siblings) + * .-----------------. + * | .------. | P1 -> P2 : deny + * | | P1 . | P2 -> P1 : deny + * | '------'\ | + * | \ | + * | .--'---. | + * | | P2 | | + * | '------' | + * '-----------------' + */ +FIXTURE_VARIANT_ADD(hierarchy, deny_with_forked_domain) { + .domain_both = true, + .domain_parent = true, + .domain_child = true, +}; + +FIXTURE_SETUP(hierarchy) +{ } + +FIXTURE_TEARDOWN(hierarchy) +{ } + +/* test PTRACE_TRACEME and PTRACE_ATTACH for parent and child */ +TEST_F(hierarchy, trace) +{ + pid_t child, parent; + int status; + int pipe_child[2], pipe_parent[2]; + char buf_parent; + + disable_caps(_metadata); + + parent = getpid(); + ASSERT_EQ(0, pipe(pipe_child)); + ASSERT_EQ(0, pipe(pipe_parent)); + if (variant->domain_both) + create_domain(_metadata); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + char buf_child; + + EXPECT_EQ(0, close(pipe_parent[1])); + EXPECT_EQ(0, close(pipe_child[0])); + if (variant->domain_child) + create_domain(_metadata); + + /* sync #1 */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)) { + TH_LOG("Failed to read() sync #1 from parent"); + } + ASSERT_EQ('.', buf_child); + + /* Tests the parent protection. */ + ASSERT_EQ(variant->domain_child ? -1 : 0, + ptrace(PTRACE_ATTACH, parent, NULL, 0)); + if (variant->domain_child) { + ASSERT_EQ(EPERM, errno); + } else { + ASSERT_EQ(parent, waitpid(parent, &status, 0)); + ASSERT_EQ(1, WIFSTOPPED(status)); + ASSERT_EQ(0, ptrace(PTRACE_DETACH, parent, NULL, 0)); + } + + /* sync #2 */ + ASSERT_EQ(1, write(pipe_child[1], ".", 1)) { + TH_LOG("Failed to write() sync #2 to parent"); + } + + /* Tests traceme. */ + ASSERT_EQ(variant->domain_parent ? -1 : 0, + ptrace(PTRACE_TRACEME)); + if (variant->domain_parent) { + ASSERT_EQ(EPERM, errno); + } else { + ASSERT_EQ(0, raise(SIGSTOP)); + } + + /* sync #3 */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)) { + TH_LOG("Failed to read() sync #3 from parent"); + } + ASSERT_EQ('.', buf_child); + _exit(_metadata->passed ? EXIT_SUCCESS : EXIT_FAILURE); + } + + EXPECT_EQ(0, close(pipe_child[1])); + EXPECT_EQ(0, close(pipe_parent[0])); + if (variant->domain_parent) + create_domain(_metadata); + + /* sync #1 */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)) { + TH_LOG("Failed to write() sync #1 to child"); + } + + /* Tests the parent protection. */ + /* sync #2 */ + ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1)) { + TH_LOG("Failed to read() sync #2 from child"); + } + ASSERT_EQ('.', buf_parent); + + /* Tests traceme. */ + if (!variant->domain_parent) { + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFSTOPPED(status)); + ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0)); + } + /* Tests attach. */ + ASSERT_EQ(variant->domain_parent ? -1 : 0, + ptrace(PTRACE_ATTACH, child, NULL, 0)); + if (variant->domain_parent) { + ASSERT_EQ(EPERM, errno); + } else { + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFSTOPPED(status)); + ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0)); + } + + /* sync #3 */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)) { + TH_LOG("Failed to write() sync #3 to child"); + } + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || WEXITSTATUS(status)) + _metadata->passed = 0; +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/true.c b/tools/testing/selftests/landlock/true.c new file mode 100644 index 000000000000..3f9ccbf52783 --- /dev/null +++ b/tools/testing/selftests/landlock/true.c @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0 +int main(void) +{ + return 0; +} From patchwork Tue Dec 1 19:23:21 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 335415 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-16.7 required=3.0 tests=BAYES_00, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 91D8DC83029 for ; Tue, 1 Dec 2020 19:26:10 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 646E420758 for ; Tue, 1 Dec 2020 19:26:10 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S2392451AbgLATZ2 (ORCPT ); Tue, 1 Dec 2020 14:25:28 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:35588 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S2392436AbgLATZY (ORCPT ); Tue, 1 Dec 2020 14:25:24 -0500 Received: from smtp-42a9.mail.infomaniak.ch (smtp-42a9.mail.infomaniak.ch [IPv6:2001:1600:3:17::42a9]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 02FDFC08E864 for ; Tue, 1 Dec 2020 11:23:45 -0800 (PST) Received: from smtp-3-0000.mail.infomaniak.ch (unknown [10.4.36.107]) by smtp-2-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4ClsTW2V9kzlhX9w; Tue, 1 Dec 2020 20:23:43 +0100 (CET) Received: from localhost (unknown [23.97.221.149]) by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4ClsTW00L6zlh8TC; Tue, 1 Dec 2020 20:23:42 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: James Morris , Jann Horn , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Al Viro , Andy Lutomirski , Anton Ivanov , Arnd Bergmann , Casey Schaufler , Jeff Dike , Jonathan Corbet , Kees Cook , Michael Kerrisk , Richard Weinberger , Shuah Khan , Vincent Dagonneau , kernel-hardening@lists.openwall.com, linux-api@vger.kernel.org, linux-arch@vger.kernel.org, linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, linux-security-module@vger.kernel.org, x86@kernel.org, =?utf-8?q?Mick?= =?utf-8?b?YcOrbCBTYWxhw7xu?= Subject: [PATCH v25 11/12] samples/landlock: Add a sandbox manager example Date: Tue, 1 Dec 2020 20:23:21 +0100 Message-Id: <20201201192322.213239-12-mic@digikod.net> X-Mailer: git-send-email 2.29.2 In-Reply-To: <20201201192322.213239-1-mic@digikod.net> References: <20201201192322.213239-1-mic@digikod.net> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org From: Mickaël Salaün Add a basic sandbox tool to launch a command which can only access a whitelist of file hierarchies in a read-only or read-write way. Cc: James Morris Cc: Jann Horn Cc: Kees Cook Cc: Serge E. Hallyn Signed-off-by: Mickaël Salaün --- Changes since v23: * Re-add hints to help users understand the required kernel configuration. This was removed with the removal of landlock_get_features(2). Changes since v21: * Remove LANDLOCK_ACCESS_FS_CHROOT. * Clean up help. Changes since v20: * Update with new syscalls and type names. * Update errno check for EOPNOTSUPP. * Use the full syscall interfaces: explicitely set the "flags" field to zero. Changes since v19: * Update with the new Landlock syscalls. * Comply with commit 5f2fb52fac15 ("kbuild: rename hostprogs-y/always to hostprogs/always-y"). Changes since v16: * Switch syscall attribute pointer and size arguments. Changes since v15: * Update access right names. * Properly assign access right to files according to the new related syscall restriction. * Replace "select" with "depends on" HEADERS_INSTALL (suggested by Randy Dunlap). Changes since v14: * Fix Kconfig dependency. * Remove access rights that may be required for FD-only requests: mmap, truncate, getattr, lock, chmod, chown, chgrp, ioctl. * Fix useless hardcoded syscall number. * Use execvpe(). * Follow symlinks. * Extend help with common file paths. * Constify variables. * Clean up comments. * Improve error message. Changes since v11: * Add back the filesystem sandbox manager and update it to work with the new Landlock syscall. Previous changes: https://lore.kernel.org/lkml/20190721213116.23476-9-mic@digikod.net/ --- samples/Kconfig | 7 ++ samples/Makefile | 1 + samples/landlock/.gitignore | 1 + samples/landlock/Makefile | 15 +++ samples/landlock/sandboxer.c | 236 +++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+) create mode 100644 samples/landlock/.gitignore create mode 100644 samples/landlock/Makefile create mode 100644 samples/landlock/sandboxer.c diff --git a/samples/Kconfig b/samples/Kconfig index 0ed6e4d71d87..e6129496ced5 100644 --- a/samples/Kconfig +++ b/samples/Kconfig @@ -124,6 +124,13 @@ config SAMPLE_HIDRAW bool "hidraw sample" depends on CC_CAN_LINK && HEADERS_INSTALL +config SAMPLE_LANDLOCK + bool "Build Landlock sample code" + depends on HEADERS_INSTALL + help + Build a simple Landlock sandbox manager able to launch a process + restricted by a user-defined filesystem access control. + config SAMPLE_PIDFD bool "pidfd sample" depends on CC_CAN_LINK && HEADERS_INSTALL diff --git a/samples/Makefile b/samples/Makefile index c3392a595e4b..087e0988ccc5 100644 --- a/samples/Makefile +++ b/samples/Makefile @@ -11,6 +11,7 @@ obj-$(CONFIG_SAMPLE_KDB) += kdb/ obj-$(CONFIG_SAMPLE_KFIFO) += kfifo/ obj-$(CONFIG_SAMPLE_KOBJECT) += kobject/ obj-$(CONFIG_SAMPLE_KPROBES) += kprobes/ +subdir-$(CONFIG_SAMPLE_LANDLOCK) += landlock obj-$(CONFIG_SAMPLE_LIVEPATCH) += livepatch/ subdir-$(CONFIG_SAMPLE_PIDFD) += pidfd obj-$(CONFIG_SAMPLE_QMI_CLIENT) += qmi/ diff --git a/samples/landlock/.gitignore b/samples/landlock/.gitignore new file mode 100644 index 000000000000..f43668b2d318 --- /dev/null +++ b/samples/landlock/.gitignore @@ -0,0 +1 @@ +/sandboxer diff --git a/samples/landlock/Makefile b/samples/landlock/Makefile new file mode 100644 index 000000000000..21eda5774948 --- /dev/null +++ b/samples/landlock/Makefile @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: BSD-3-Clause + +hostprogs := sandboxer + +always-y := $(hostprogs) + +KBUILD_HOSTCFLAGS += -I$(objtree)/usr/include + +.PHONY: all clean + +all: + $(MAKE) -C ../.. samples/landlock/ + +clean: + $(MAKE) -C ../.. M=samples/landlock/ clean diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c new file mode 100644 index 000000000000..127fb063c23a --- /dev/null +++ b/samples/landlock/sandboxer.c @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Simple Landlock sandbox manager able to launch a process restricted by a + * user-defined filesystem access control. + * + * Copyright © 2017-2020 Mickaël Salaün + * Copyright © 2020 ANSSI + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef landlock_create_ruleset +static inline int landlock_create_ruleset( + const struct landlock_ruleset_attr *const attr, + const size_t size, const __u32 flags) +{ + errno = 0; + return syscall(__NR_landlock_create_ruleset, attr, size, flags); +} +#endif + +#ifndef landlock_add_rule +static inline int landlock_add_rule(const int ruleset_fd, + const enum landlock_rule_type rule_type, + const void *const rule_attr, const __u32 flags) +{ + errno = 0; + return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, + rule_attr, flags); +} +#endif + +#ifndef landlock_enforce_ruleset_current +static inline int landlock_enforce_ruleset_current(const int ruleset_fd, + const __u32 flags) +{ + errno = 0; + return syscall(__NR_landlock_enforce_ruleset_current, ruleset_fd, + flags); +} +#endif + +#define ENV_FS_RO_NAME "LL_FS_RO" +#define ENV_FS_RW_NAME "LL_FS_RW" +#define ENV_PATH_TOKEN ":" + +static int parse_path(char *env_path, const char ***const path_list) +{ + int i, path_nb = 0; + + if (env_path) { + path_nb++; + for (i = 0; env_path[i]; i++) { + if (env_path[i] == ENV_PATH_TOKEN[0]) + path_nb++; + } + } + *path_list = malloc(path_nb * sizeof(**path_list)); + for (i = 0; i < path_nb; i++) + (*path_list)[i] = strsep(&env_path, ENV_PATH_TOKEN); + + return path_nb; +} + +#define ACCESS_FILE ( \ + LANDLOCK_ACCESS_FS_EXECUTE | \ + LANDLOCK_ACCESS_FS_WRITE_FILE | \ + LANDLOCK_ACCESS_FS_READ_FILE) + +static int populate_ruleset( + const char *const env_var, const int ruleset_fd, + const __u64 allowed_access) +{ + int path_nb, i; + char *env_path_name; + const char **path_list = NULL; + struct landlock_path_beneath_attr path_beneath = { + .parent_fd = -1, + }; + + env_path_name = getenv(env_var); + if (!env_path_name) { + fprintf(stderr, "Missing environment variable %s\n", env_var); + return 1; + } + env_path_name = strdup(env_path_name); + unsetenv(env_var); + path_nb = parse_path(env_path_name, &path_list); + if (path_nb == 1 && path_list[0][0] == '\0') { + fprintf(stderr, "Missing path in %s\n", env_var); + goto err_free_name; + } + + for (i = 0; i < path_nb; i++) { + struct stat statbuf; + + path_beneath.parent_fd = open(path_list[i], O_PATH | + O_CLOEXEC); + if (path_beneath.parent_fd < 0) { + fprintf(stderr, "Failed to open \"%s\": %s\n", + path_list[i], + strerror(errno)); + goto err_free_name; + } + if (fstat(path_beneath.parent_fd, &statbuf)) { + close(path_beneath.parent_fd); + goto err_free_name; + } + path_beneath.allowed_access = allowed_access; + if (!S_ISDIR(statbuf.st_mode)) + path_beneath.allowed_access &= ACCESS_FILE; + if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)) { + fprintf(stderr, "Failed to update the ruleset with \"%s\": %s\n", + path_list[i], strerror(errno)); + close(path_beneath.parent_fd); + goto err_free_name; + } + close(path_beneath.parent_fd); + } + free(env_path_name); + return 0; + +err_free_name: + free(env_path_name); + return 1; +} + +#define ACCESS_FS_ROUGHLY_READ ( \ + LANDLOCK_ACCESS_FS_EXECUTE | \ + LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_READ_DIR) + +#define ACCESS_FS_ROUGHLY_WRITE ( \ + LANDLOCK_ACCESS_FS_WRITE_FILE | \ + LANDLOCK_ACCESS_FS_REMOVE_DIR | \ + LANDLOCK_ACCESS_FS_REMOVE_FILE | \ + LANDLOCK_ACCESS_FS_MAKE_CHAR | \ + LANDLOCK_ACCESS_FS_MAKE_DIR | \ + LANDLOCK_ACCESS_FS_MAKE_REG | \ + LANDLOCK_ACCESS_FS_MAKE_SOCK | \ + LANDLOCK_ACCESS_FS_MAKE_FIFO | \ + LANDLOCK_ACCESS_FS_MAKE_BLOCK | \ + LANDLOCK_ACCESS_FS_MAKE_SYM) + +int main(const int argc, char *const argv[], char *const *const envp) +{ + const char *cmd_path; + char *const *cmd_argv; + int ruleset_fd; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = ACCESS_FS_ROUGHLY_READ | + ACCESS_FS_ROUGHLY_WRITE, + }; + + if (argc < 2) { + fprintf(stderr, "usage: %s=\"...\" %s=\"...\" %s [args]...\n\n", + ENV_FS_RO_NAME, ENV_FS_RW_NAME, argv[0]); + fprintf(stderr, "Launch a command in a restricted environment.\n\n"); + fprintf(stderr, "Environment variables containing paths, " + "each separated by a colon:\n"); + fprintf(stderr, "* %s: list of paths allowed to be used in a read-only way.\n", + ENV_FS_RO_NAME); + fprintf(stderr, "* %s: list of paths allowed to be used in a read-write way.\n", + ENV_FS_RO_NAME); + fprintf(stderr, "\nexample:\n" + "%s=\"/bin:/lib:/usr:/proc:/etc:/dev/urandom\" " + "%s=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" " + "%s bash -i\n", + ENV_FS_RO_NAME, ENV_FS_RW_NAME, argv[0]); + return 1; + } + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) { + perror("Failed to create a ruleset"); + switch (errno) { + case ENOSYS: + fprintf(stderr, "Hint: Landlock is not supported by the current kernel. " + "To support it, build the kernel with " + "CONFIG_SECURITY_LANDLOCK=y and prepend " + "\"landlock,\" to the content of CONFIG_LSM.\n"); + break; + case EOPNOTSUPP: + fprintf(stderr, "Hint: Landlock is currently disabled. " + "It can be enabled in the kernel configuration by " + "prepending \"landlock,\" to the content of CONFIG_LSM, " + "or at boot time by setting the same content to the " + "\"lsm\" kernel parameter.\n"); + break; + } + return 1; + } + if (populate_ruleset(ENV_FS_RO_NAME, ruleset_fd, + ACCESS_FS_ROUGHLY_READ)) { + goto err_close_ruleset; + } + if (populate_ruleset(ENV_FS_RW_NAME, ruleset_fd, + ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE)) { + goto err_close_ruleset; + } + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("Failed to restrict privileges"); + goto err_close_ruleset; + } + if (landlock_enforce_ruleset_current(ruleset_fd, 0)) { + perror("Failed to enforce ruleset"); + goto err_close_ruleset; + } + close(ruleset_fd); + + cmd_path = argv[1]; + cmd_argv = argv + 1; + execvpe(cmd_path, cmd_argv, envp); + fprintf(stderr, "Failed to execute \"%s\": %s\n", cmd_path, + strerror(errno)); + fprintf(stderr, "Hint: access to the binary, the interpreter or " + "shared libraries may be denied.\n"); + return 1; + +err_close_ruleset: + close(ruleset_fd); + return 1; +} From patchwork Tue Dec 1 19:23:22 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 335413 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-21.7 required=3.0 tests=BAYES_00, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, MENTIONS_GIT_HOSTING, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 01014C8303D for ; Tue, 1 Dec 2020 19:26:14 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id A10EB2151B for ; Tue, 1 Dec 2020 19:26:14 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S2392351AbgLATZt (ORCPT ); Tue, 1 Dec 2020 14:25:49 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:35728 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730229AbgLATZt (ORCPT ); Tue, 1 Dec 2020 14:25:49 -0500 Received: from smtp-42aa.mail.infomaniak.ch (smtp-42aa.mail.infomaniak.ch [IPv6:2001:1600:4:17::42aa]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 553C4C094241 for ; Tue, 1 Dec 2020 11:23:46 -0800 (PST) Received: from smtp-3-0001.mail.infomaniak.ch (unknown [10.4.36.108]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4ClsTX4x1zzlhkpq; Tue, 1 Dec 2020 20:23:44 +0100 (CET) Received: from localhost (unknown [23.97.221.149]) by smtp-3-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4ClsTX28c9zlh8T4; Tue, 1 Dec 2020 20:23:44 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: James Morris , Jann Horn , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Al Viro , Andy Lutomirski , Anton Ivanov , Arnd Bergmann , Casey Schaufler , Jeff Dike , Jonathan Corbet , Kees Cook , Michael Kerrisk , Richard Weinberger , Shuah Khan , Vincent Dagonneau , kernel-hardening@lists.openwall.com, linux-api@vger.kernel.org, linux-arch@vger.kernel.org, linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, linux-security-module@vger.kernel.org, x86@kernel.org, =?utf-8?q?Mick?= =?utf-8?b?YcOrbCBTYWxhw7xu?= Subject: [PATCH v25 12/12] landlock: Add user and kernel documentation Date: Tue, 1 Dec 2020 20:23:22 +0100 Message-Id: <20201201192322.213239-13-mic@digikod.net> X-Mailer: git-send-email 2.29.2 In-Reply-To: <20201201192322.213239-1-mic@digikod.net> References: <20201201192322.213239-1-mic@digikod.net> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org From: Mickaël Salaün This documentation can be built with the Sphinx framework. Cc: James Morris Cc: Kees Cook Cc: Serge E. Hallyn Signed-off-by: Mickaël Salaün Reviewed-by: Vincent Dagonneau Reviewed-by: Jann Horn --- Changes since v24: * Add Reviewed-by: Jann Horn * Add a paragraph to explain how the ruleset layers work. * Bump date. Changes since v23: * Explain limitations for the maximum number of stacked ruleset, and the memory usage restrictions. Changes since v22: * Fix spelling and remove obsolete sentence (spotted by Jann Horn). * Bump date. Changes since v21: * Move the user space documentation to userspace-api/landlock.rst and the kernel documentation to security/landlock.rst . * Add license headers. * Add last update dates. * Update MAINTAINERS file. * Add (back) links to git.kernel.org . * Fix spelling. Changes since v20: * Update examples and documentation with the new syscalls. Changes since v19: * Update examples and documentation with the new syscalls. Changes since v15: * Add current limitations. Changes since v14: * Fix spelling (contributed by Randy Dunlap). * Extend documentation about inheritance and explain layer levels. * Remove the use of now-removed access rights. * Use GitHub links. * Improve kernel documentation. * Add section for tests. * Update example. Changes since v13: * Rewrote the documentation according to the major revamp. Previous changes: https://lore.kernel.org/lkml/20191104172146.30797-8-mic@digikod.net/ --- Documentation/security/index.rst | 1 + Documentation/security/landlock.rst | 79 +++++++ Documentation/userspace-api/index.rst | 1 + Documentation/userspace-api/landlock.rst | 280 +++++++++++++++++++++++ MAINTAINERS | 2 + 5 files changed, 363 insertions(+) create mode 100644 Documentation/security/landlock.rst create mode 100644 Documentation/userspace-api/landlock.rst diff --git a/Documentation/security/index.rst b/Documentation/security/index.rst index 8129405eb2cc..16335de04e8c 100644 --- a/Documentation/security/index.rst +++ b/Documentation/security/index.rst @@ -16,3 +16,4 @@ Security Documentation siphash tpm/index digsig + landlock diff --git a/Documentation/security/landlock.rst b/Documentation/security/landlock.rst new file mode 100644 index 000000000000..92317b2ad732 --- /dev/null +++ b/Documentation/security/landlock.rst @@ -0,0 +1,79 @@ +.. SPDX-License-Identifier: GPL-2.0 +.. Copyright © 2017-2020 Mickaël Salaün +.. Copyright © 2019-2020 ANSSI + +================================== +Landlock LSM: kernel documentation +================================== + +:Author: Mickaël Salaün +:Date: December 2020 + +Landlock's goal is to create scoped access-control (i.e. sandboxing). To +harden a whole system, this feature should be available to any process, +including unprivileged ones. Because such process may be compromised or +backdoored (i.e. untrusted), Landlock's features must be safe to use from the +kernel and other processes point of view. Landlock's interface must therefore +expose a minimal attack surface. + +Landlock is designed to be usable by unprivileged processes while following the +system security policy enforced by other access control mechanisms (e.g. DAC, +LSM). Indeed, a Landlock rule shall not interfere with other access-controls +enforced on the system, only add more restrictions. + +Any user can enforce Landlock rulesets on their processes. They are merged and +evaluated according to the inherited ones in a way that ensures that only more +constraints can be added. + +User space documentation can be found here: :doc:`/userspace-api/landlock`. + +Guiding principles for safe access controls +=========================================== + +* A Landlock rule shall be focused on access control on kernel objects instead + of syscall filtering (i.e. syscall arguments), which is the purpose of + seccomp-bpf. +* To avoid multiple kinds of side-channel attacks (e.g. leak of security + policies, CPU-based attacks), Landlock rules shall not be able to + programmatically communicate with user space. +* Kernel access check shall not slow down access request from unsandboxed + processes. +* Computation related to Landlock operations (e.g. enforcing a ruleset) shall + only impact the processes requesting them. + +Tests +===== + +Userspace tests for backward compatibility, ptrace restrictions and filesystem +support can be found here: `tools/testing/selftests/landlock/`_. + +Kernel structures +================= + +Object +------ + +.. kernel-doc:: security/landlock/object.h + :identifiers: + +Ruleset and domain +------------------ + +A domain is a read-only ruleset tied to a set of subjects (i.e. tasks' +credentials). Each time a ruleset is enforced on a task, the current domain is +duplicated and the ruleset is imported as a new layer of rules in the new +domain. Indeed, once in a domain, each rule is tied to a layer level. To +grant access to an object, at least one rule of each layer must allow the +requested action on the object. A task can then only transit to a new domain +which is the intersection of the constraints from the current domain and those +of a ruleset provided by the task. + +The definition of a subject is implicit for a task sandboxing itself, which +makes the reasoning much easier and helps avoid pitfalls. + +.. kernel-doc:: security/landlock/ruleset.h + :identifiers: + +.. Links +.. _tools/testing/selftests/landlock/: + https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/tools/testing/selftests/landlock/ diff --git a/Documentation/userspace-api/index.rst b/Documentation/userspace-api/index.rst index acd2cc2a538d..01f1748ab569 100644 --- a/Documentation/userspace-api/index.rst +++ b/Documentation/userspace-api/index.rst @@ -18,6 +18,7 @@ place where this information is gathered. no_new_privs seccomp_filter + landlock unshare spec_ctrl accelerators/ocxl diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst new file mode 100644 index 000000000000..84aea5478acc --- /dev/null +++ b/Documentation/userspace-api/landlock.rst @@ -0,0 +1,280 @@ +.. SPDX-License-Identifier: GPL-2.0 +.. Copyright © 2017-2020 Mickaël Salaün +.. Copyright © 2019-2020 ANSSI + +===================================== +Landlock: unprivileged access control +===================================== + +:Author: Mickaël Salaün +:Date: December 2020 + +The goal of Landlock is to enable to restrict ambient rights (e.g. global +filesystem access) for a set of processes. Because Landlock is a stackable +LSM, it makes possible to create safe security sandboxes as new security layers +in addition to the existing system-wide access-controls. This kind of sandbox +is expected to help mitigate the security impact of bugs or +unexpected/malicious behaviors in user space applications. Landlock empowers +any process, including unprivileged ones, to securely restrict themselves. + +Landlock rules +============== + +A Landlock rule describes an action on an object. An object is currently a +file hierarchy, and the related filesystem actions are defined in `Access +rights`_. A set of rules is aggregated in a ruleset, which can then restrict +the thread enforcing it, and its future children. + +Defining and enforcing a security policy +---------------------------------------- + +We first need to create the ruleset that will contain our rules. For this +example, the ruleset will contain rules which only allow read actions, but +write actions will be denied. The ruleset then needs to handle both of these +kind of actions. + +.. code-block:: c + + int ruleset_fd; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_WRITE_FILE | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR | + LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_MAKE_CHAR | + LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_MAKE_SOCK | + LANDLOCK_ACCESS_FS_MAKE_FIFO | + LANDLOCK_ACCESS_FS_MAKE_BLOCK | + LANDLOCK_ACCESS_FS_MAKE_SYM, + }; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) { + perror("Failed to create a ruleset"); + return 1; + } + +We can now add a new rule to this ruleset thanks to the returned file +descriptor referring to this ruleset. The rule will only allow reading the +file hierarchy ``/usr``. Without another rule, write actions would then be +denied by the ruleset. To add ``/usr`` to the ruleset, we open it with the +``O_PATH`` flag and fill the &struct landlock_path_beneath_attr with this file +descriptor. + +.. code-block:: c + + int err; + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, + }; + + path_beneath.parent_fd = open("/usr", O_PATH | O_CLOEXEC); + if (path_beneath.parent_fd < 0) { + perror("Failed to open file"); + close(ruleset_fd); + return 1; + } + err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0); + close(path_beneath.parent_fd); + if (err) { + perror("Failed to update ruleset"); + close(ruleset_fd); + return 1; + } + +We now have a ruleset with one rule allowing read access to ``/usr`` while +denying all other handled accesses for the filesystem. The next step is to +restrict the current thread from gaining more privileges (e.g. thanks to a SUID +binary). + +.. code-block:: c + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("Failed to restrict privileges"); + close(ruleset_fd); + return 1; + } + +The current thread is now ready to sandbox itself with the ruleset. + +.. code-block:: c + + if (landlock_enforce_ruleset_current(ruleset_fd, 0)) { + perror("Failed to enforce ruleset"); + close(ruleset_fd); + return 1; + } + close(ruleset_fd); + +If the `landlock_enforce_ruleset_current` system call succeeds, the current +thread is now restricted and this policy will be enforced on all its +subsequently created children as well. Once a thread is landlocked, there is +no way to remove its security policy; only adding more restrictions is allowed. +These threads are now in a new Landlock domain, merge of their parent one (if +any) with the new ruleset. + +Full working code can be found in `samples/landlock/sandboxer.c`_. + +Inheritance +----------- + +Every new thread resulting from a :manpage:`clone(2)` inherits Landlock domain +restrictions from its parent. This is similar to the seccomp inheritance (cf. +:doc:`/userspace-api/seccomp_filter`) or any other LSM dealing with task's +:manpage:`credentials(7)`. For instance, one process's thread may apply +Landlock rules to itself, but they will not be automatically applied to other +sibling threads (unlike POSIX thread credential changes, cf. +:manpage:`nptl(7)`). + +When a thread sandboxes itself, we have the guarantee that the related security +policy will stay enforced on all this thread's descendants. This allows +creating standalone and modular security policies per application, which will +automatically be composed between themselves according to their runtime parent +policies. + +A sandboxed thread can add more constraints to itself with a new enforced +ruleset. This complementary policy inherits from the previous enforced +rulesets. Access to a file path is granted if, for each policy layer, at least +one rule encountered on the path (from the file to the root) grants the access. + +Ptrace restrictions +------------------- + +A sandboxed process has less privileges than a non-sandboxed process and must +then be subject to additional restrictions when manipulating another process. +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. + +Kernel interface +================ + +Access rights +------------- + +.. kernel-doc:: include/uapi/linux/landlock.h + :identifiers: fs_access + +Creating a new ruleset +---------------------- + +.. kernel-doc:: security/landlock/syscall.c + :identifiers: sys_landlock_create_ruleset + +.. kernel-doc:: include/uapi/linux/landlock.h + :identifiers: landlock_ruleset_attr + +Extending a ruleset +------------------- + +.. kernel-doc:: security/landlock/syscall.c + :identifiers: sys_landlock_add_rule + +.. kernel-doc:: include/uapi/linux/landlock.h + :identifiers: landlock_rule_type landlock_path_beneath_attr + +Enforcing a ruleset +------------------- + +.. kernel-doc:: security/landlock/syscall.c + :identifiers: sys_landlock_enforce_ruleset_current + +Current limitations +=================== + +Ruleset layers +-------------- + +There is a limit of 64 layers of stacked rulesets. This can be an issue for a +task willing to enforce a new ruleset in complement to its 64 inherited +rulesets. Once this limit is reached, sys_landlock_enforce_ruleset_current() +returns E2BIG. It is then strongly suggested to carefully build rulesets once +in the life of a thread, especially for applications able to launch other +applications which may also want to sandbox themselves (e.g. shells, container +managers, etc.). + +Memory usage +------------ + +Kernel memory allocated to create rulesets is accounted and can be restricted +by the :doc:`/admin-guide/cgroup-v1/memory`. + +File renaming and linking +------------------------- + +Because Landlock targets unprivileged access controls, it is needed to properly +handle composition of rules. Such property also implies rules nesting. +Properly handling multiple layers of ruleset, each one of them able to restrict +access to files, also implies to inherit the ruleset restrictions from a parent +to its hierarchy. Because files are identified and restricted by their +hierarchy, moving or linking a file from one directory to another imply to +propagate the hierarchy constraints. To protect against privilege escalations +through renaming or linking, and for the sack of simplicity, Landlock currently +limits linking and renaming to the same directory. Future Landlock evolutions +will enable more flexibility for renaming and linking, with dedicated ruleset +flags. + +OverlayFS +--------- + +An OverlayFS mount point consists of upper and lower layers. It is currently +not possible to reliably infer which underlying file hierarchy matches an +OverlayFS path composed of such layers. It is then not currently possible to +track the source of an indirect access request, and then not possible to +properly identify and allow an unified OverlayFS hierarchy. Restricting files +in an OverlayFS mount point works, but files allowed in one layer may not be +allowed in a related OverlayFS mount point. A future Landlock evolution will +make possible to properly work with OverlayFS, according to a dedicated ruleset +flag. + + +Special filesystems +------------------- + +Access to regular files and directories can be restricted by Landlock, +according to the handled accesses of a ruleset. However, files which do not +come from a user-visible filesystem (e.g. pipe, socket), but can still be +accessed through /proc/self/fd/, cannot currently be restricted. Likewise, +some special kernel filesystems such as nsfs which can be accessed through +/proc/self/ns/, cannot currently be restricted. For now, these kind of special +paths are then always allowed. Future Landlock evolutions will enable to +restrict such paths, with dedicated ruleset flags. + +Questions and answers +===================== + +What about user space sandbox managers? +--------------------------------------- + +Using user space process to enforce restrictions on kernel resources can lead +to race conditions or inconsistent evaluations (i.e. `Incorrect mirroring of +the OS code and state +`_). + +What about namespaces and containers? +------------------------------------- + +Namespaces can help create sandboxes but they are not designed for +access-control and then miss useful features for such use case (e.g. no +fine-grained restrictions). Moreover, their complexity can lead to security +issues, especially when untrusted processes can manipulate them (cf. +`Controlling access to user namespaces `_). + +Additional documentation +======================== + +* :doc:`/security/landlock` +* https://landlock.io + +.. Links +.. _samples/landlock/sandboxer.c: + https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/samples/landlock/sandboxer.c diff --git a/MAINTAINERS b/MAINTAINERS index 674a06639852..aa872992765a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9827,6 +9827,8 @@ L: linux-security-module@vger.kernel.org S: Supported W: https://landlock.io T: git https://github.com/landlock-lsm/linux.git +F: Documentation/security/landlock.rst +F: Documentation/userspace-api/landlock.rst F: include/uapi/linux/landlock.h F: security/landlock/ K: landlock