From patchwork Fri Feb 9 14:09:06 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Roberto Sassu X-Patchwork-Id: 771671 Received: from frasgout12.his.huawei.com (frasgout12.his.huawei.com [14.137.139.154]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 2C75469976; Fri, 9 Feb 2024 14:10:20 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=14.137.139.154 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707487824; cv=none; b=Z3ZV9CIKTgV+8tmhFB5V6iyj6uuB+5Y9Uye+uvqMNCzHxCs9ro89IdEiMnQpn+hgJNnjVs2yZ5Tw/vvH/TTZEl8xcdswnZVLL8e7s8HWnqUEGR/JVG/IkN8pmtJ4w8FXVURaKjujb47kLz+DZHcwrahozw8v3h9kFgDbjCXPNj0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707487824; c=relaxed/simple; bh=FtGdvX9YI63Ar23I2/KUGB7L2F297IaeVuCkyiw9Z18=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=BNngzK2q9fiPohTERrF1FqzJMRYE0vnZXztb9O4LwwzRvanymJeQ5WAowuwN7bGFSyxaqSUqXDpalRCb680P4Qd2QWavcTj4GECIN517OSfsM2sYhN1hf7FBQbKQD2n1sBbyBimp+EX6pEUQWqKL93xAZVDWsXUDvmY4So+nQuk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com; spf=pass smtp.mailfrom=huaweicloud.com; arc=none smtp.client-ip=14.137.139.154 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=huaweicloud.com Received: from mail.maildlp.com (unknown [172.18.186.29]) by frasgout12.his.huawei.com (SkyGuard) with ESMTP id 4TWZyH6gLWz9xtPR; Fri, 9 Feb 2024 21:51:19 +0800 (CST) Received: from mail02.huawei.com (unknown [7.182.16.47]) by mail.maildlp.com (Postfix) with ESMTP id 51FD7140732; Fri, 9 Feb 2024 22:10:13 +0800 (CST) Received: from huaweicloud.com (unknown [10.204.63.22]) by APP1 (Coremail) with SMTP id LxC2BwAHshoZMsZlvXMuAg--.65105S4; Fri, 09 Feb 2024 15:10:12 +0100 (CET) From: Roberto Sassu To: corbet@lwn.net, paul@paul-moore.com, jmorris@namei.org, serge@hallyn.com, shuah@kernel.org, mcoquelin.stm32@gmail.com, alexandre.torgue@foss.st.com, mic@digikod.net Cc: linux-security-module@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, bpf@vger.kernel.org, zohar@linux.ibm.com, dmitry.kasatkin@gmail.com, linux-integrity@vger.kernel.org, wufan@linux.microsoft.com, pbrobinson@gmail.com, zbyszek@in.waw.pl, hch@lst.de, mjg59@srcf.ucam.org, pmatilai@redhat.com, jannh@google.com, dhowells@redhat.com, jikos@kernel.org, mkoutny@suse.com, ppavlu@suse.com, petr.vorel@gmail.com, petrtesarik@huaweicloud.com, Roberto Sassu Subject: [PATCH v3 02/13] security: Introduce the digest_cache LSM Date: Fri, 9 Feb 2024 15:09:06 +0100 Message-Id: <20240209140917.846878-3-roberto.sassu@huaweicloud.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> References: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> Precedence: bulk X-Mailing-List: linux-kselftest@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-CM-TRANSID: LxC2BwAHshoZMsZlvXMuAg--.65105S4 X-Coremail-Antispam: 1UD129KBjvAXoWfuw4kXF4xXFWUXFWxtw4ruFg_yoW5XF1fAo ZYyF47Jw10gFy7ur4kCF17Aa17u3ZYq3yxAr1kJrWDZF1IvFyDJasrCa1DJFy5Jr18GF97 A34kJw4UJFWUtr93n29KB7ZKAUJUUUU8529EdanIXcx71UUUUU7v73VFW2AGmfu7bjvjm3 AaLaJ3UjIYCTnIWjp_UUUOx7AC8VAFwI0_Wr0E3s1l1xkIjI8I6I8E6xAIw20EY4v20xva j40_Wr0E3s1l1IIY67AEw4v_Jr0_Jr4l82xGYIkIc2x26280x7IE14v26r15M28IrcIa0x kI8VCY1x0267AKxVW5JVCq3wA2ocxC64kIII0Yj41l84x0c7CEw4AK67xGY2AK021l84AC jcxK6xIIjxv20xvE14v26r1j6r1xM28EF7xvwVC0I7IYx2IY6xkF7I0E14v26F4j6r4UJw A2z4x0Y4vEx4A2jsIE14v26r4j6F4UM28EF7xvwVC2z280aVCY1x0267AKxVW8Jr0_Cr1U M2AIxVAIcxkEcVAq07x20xvEncxIr21l5I8CrVACY4xI64kE6c02F40Ex7xfMcIj6xIIjx v20xvE14v26r1j6r18McIj6I8E87Iv67AKxVWUJVW8JwAm72CE4IkC6x0Yz7v_Jr0_Gr1l F7xvr2IYc2Ij64vIr41lF7I21c0EjII2zVCS5cI20VAGYxC7M4IIrI8v6xkF7I0E8cxan2 IY04v7MxkF7I0En4kS14v26r4a6rW5MxAIw28IcxkI7VAKI48JMxC20s026xCaFVCjc4AY 6r1j6r4UMI8I3I0E5I8CrVAFwI0_Jr0_Jr4lx2IqxVCjr7xvwVAFwI0_JrI_JrWlx4CE17 CEb7AF67AKxVW8ZVWrXwCIc40Y0x0EwIxGrwCI42IY6xIIjxv20xvE14v26r1j6r1xMIIF 0xvE2Ix0cI8IcVCY1x0267AKxVWxJVW8Jr1lIxAIcVCF04k26cxKx2IYs7xG6r1j6r1xMI IF0xvEx4A2jsIE14v26r4j6F4UMIIF0xvEx4A2jsIEc7CjxVAFwI0_Gr1j6F4UJbIYCTnI WIevJa73UjIFyTuYvjTRNiSHDUUUU X-CM-SenderInfo: purev21wro2thvvxqx5xdzvxpfor3voofrz/1tbiAQAIBF1jj5o4CgABsV From: Roberto Sassu Introduce the digest_cache LSM, to collect digests from various sources (called digest lists), and to store them in kernel memory, in a set of hash tables forming a digest cache. Extracted digests can be used as reference values for integrity verification of file content or metadata. A digest cache has three types of references: in the inode security blob of the digest list the digest cache was created from (dig_owner field); in the security blob of the inodes for which the digest cache is requested (dig_user field); a reference returned by digest_cache_get(). References are released with digest_cache_put(), in the first two cases when inodes are evicted from memory, in the last case when that function is explicitly called. Obtaining a digest cache reference means that the digest cache remains valid and cannot be freed until releasing it and until the total number of references (stored in the digest cache) becomes zero. When digest_cache_get() is called on an inode to compare its digest with a reference value, the digest_cache LSM knows which digest cache to get from the new security.digest_list xattr added to that inode, which contains the file name of the desired digest list digests will be extracted from. All digest lists are expected to be in the same directory, defined in the kernel config, and modifiable (with a later patch) at run-time through securityfs. When the digest_cache LSM reads the security.digest_list xattr, it uses its value as last path component, appended to the default path (unless the default path is a file). If an inode does not have that xattr, the default path is considered as the final destination. The default path can be either a file or a directory. If it is a file, the digest_cache LSM always uses the same digest cache from that file to verify all inodes (the xattr, if present, is ignored). If it is a directory, and the inode to verify does not have the xattr, a subsequent patch will make it possible to iterate and lookup on the digest caches created from each directory entry. Digest caches are created on demand, only when digest_cache_get() is called. The first time a digest cache is requested, the digest_cache LSM creates it and sets its reference in the dig_owner and dig_user fields of the respective inode security blobs. On the next requests, the previously set reference is returned, after incrementing the reference count. Since there might be multiple digest_cache_get() calls for the same inode, or for different inodes pointing to the same digest list, dig_owner_mutex and dig_user_mutex have been introduced to protect the check and assignment of the digest cache reference in the inode security blob. Contenders that didn't get the lock also have to wait until the digest cache is fully instantiated (when the bit INIT_IN_PROGRESS is cleared). Dig_owner_mutex cannot be used for waiting on the instantiation to avoid lock inversion with the inode lock for directories. Signed-off-by: Roberto Sassu --- MAINTAINERS | 6 + include/linux/digest_cache.h | 32 ++ include/uapi/linux/lsm.h | 1 + include/uapi/linux/xattr.h | 3 + security/Kconfig | 11 +- security/Makefile | 1 + security/digest_cache/Kconfig | 17 + security/digest_cache/Makefile | 7 + security/digest_cache/internal.h | 86 ++++ security/digest_cache/main.c | 397 ++++++++++++++++++ security/security.c | 3 +- .../selftests/lsm/lsm_list_modules_test.c | 3 + 12 files changed, 561 insertions(+), 6 deletions(-) create mode 100644 include/linux/digest_cache.h create mode 100644 security/digest_cache/Kconfig create mode 100644 security/digest_cache/Makefile create mode 100644 security/digest_cache/internal.h create mode 100644 security/digest_cache/main.c diff --git a/MAINTAINERS b/MAINTAINERS index d3d13fb1004b..3e3236dd0b26 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6155,6 +6155,12 @@ L: linux-gpio@vger.kernel.org S: Maintained F: drivers/gpio/gpio-gpio-mm.c +DIGEST_CACHE LSM +M: Roberto Sassu +L: linux-security-module@vger.kernel.org +S: Maintained +F: security/digest_cache/ + DIGITEQ AUTOMOTIVE MGB4 V4L2 DRIVER M: Martin Tuma L: linux-media@vger.kernel.org diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h new file mode 100644 index 000000000000..e79f94a60b0f --- /dev/null +++ b/include/linux/digest_cache.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu + * + * Public API of the digest_cache LSM. + */ + +#ifndef _LINUX_DIGEST_CACHE_H +#define _LINUX_DIGEST_CACHE_H + +#include + +struct digest_cache; + +#ifdef CONFIG_SECURITY_DIGEST_CACHE +struct digest_cache *digest_cache_get(struct dentry *dentry); +void digest_cache_put(struct digest_cache *digest_cache); + +#else +static inline struct digest_cache *digest_cache_get(struct dentry *dentry) +{ + return NULL; +} + +static inline void digest_cache_put(struct digest_cache *digest_cache) +{ +} + +#endif /* CONFIG_SECURITY_DIGEST_CACHE */ +#endif /* _LINUX_DIGEST_CACHE_H */ diff --git a/include/uapi/linux/lsm.h b/include/uapi/linux/lsm.h index f8aef9ade549..205d969f20ff 100644 --- a/include/uapi/linux/lsm.h +++ b/include/uapi/linux/lsm.h @@ -62,6 +62,7 @@ struct lsm_ctx { #define LSM_ID_LOCKDOWN 108 #define LSM_ID_BPF 109 #define LSM_ID_LANDLOCK 110 +#define LSM_ID_DIGEST_CACHE 111 /* * LSM_ATTR_XXX definitions identify different LSM attributes diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h index 9463db2dfa9d..8a58cf4bce65 100644 --- a/include/uapi/linux/xattr.h +++ b/include/uapi/linux/xattr.h @@ -54,6 +54,9 @@ #define XATTR_IMA_SUFFIX "ima" #define XATTR_NAME_IMA XATTR_SECURITY_PREFIX XATTR_IMA_SUFFIX +#define XATTR_DIGEST_LIST_SUFFIX "digest_list" +#define XATTR_NAME_DIGEST_LIST XATTR_SECURITY_PREFIX XATTR_DIGEST_LIST_SUFFIX + #define XATTR_SELINUX_SUFFIX "selinux" #define XATTR_NAME_SELINUX XATTR_SECURITY_PREFIX XATTR_SELINUX_SUFFIX diff --git a/security/Kconfig b/security/Kconfig index 52c9af08ad35..99f99cbd94cc 100644 --- a/security/Kconfig +++ b/security/Kconfig @@ -194,6 +194,7 @@ source "security/yama/Kconfig" source "security/safesetid/Kconfig" source "security/lockdown/Kconfig" source "security/landlock/Kconfig" +source "security/digest_cache/Kconfig" source "security/integrity/Kconfig" @@ -233,11 +234,11 @@ endchoice config LSM string "Ordered list of enabled LSMs" - default "landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK - default "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR - default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO - default "landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC - default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf" + default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK + default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR + default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO + default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC + default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf" help A comma-separated list of LSMs, in initialization order. Any LSMs left off this list, except for those with order diff --git a/security/Makefile b/security/Makefile index 59f238490665..e9b43e7b715a 100644 --- a/security/Makefile +++ b/security/Makefile @@ -25,6 +25,7 @@ obj-$(CONFIG_SECURITY_LOCKDOWN_LSM) += lockdown/ obj-$(CONFIG_CGROUPS) += device_cgroup.o obj-$(CONFIG_BPF_LSM) += bpf/ obj-$(CONFIG_SECURITY_LANDLOCK) += landlock/ +obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache/ # Object integrity file lists obj-$(CONFIG_INTEGRITY) += integrity/ diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig new file mode 100644 index 000000000000..0c47d5151f07 --- /dev/null +++ b/security/digest_cache/Kconfig @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0 +config SECURITY_DIGEST_CACHE + bool "Digest_cache LSM" + default n + help + This option enables an LSM maintaining a cache of digests + (e.g. of file content or metadata). + + This LSM can support other kernel components in making access + control decisions. + +config DIGEST_LIST_DEFAULT_PATH + string + default "/etc/digest_lists" + help + Default directory where digest_cache LSM expects to find digest + lists. diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile new file mode 100644 index 000000000000..48848c41253e --- /dev/null +++ b/security/digest_cache/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for building the digest_cache LSM. + +obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o + +digest_cache-y := main.o diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h new file mode 100644 index 000000000000..5f04844af3a5 --- /dev/null +++ b/security/digest_cache/internal.h @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu + * + * Internal header of the digest_cache LSM. + */ + +#ifndef _DIGEST_CACHE_INTERNAL_H +#define _DIGEST_CACHE_INTERNAL_H + +#include +#include + +/* Digest cache bits in flags. */ +#define INIT_IN_PROGRESS 0 /* Digest cache being initialized. */ + +/** + * struct digest_cache - Digest cache + * @ref_count: Number of references to the digest cache + * @path_str: Path of the digest list the digest cache was created from + * @flags: Control flags + * + * This structure represents a cache of digests extracted from a digest list. + */ +struct digest_cache { + atomic_t ref_count; + char *path_str; + unsigned long flags; +}; + +/** + * struct digest_cache_security - Digest cache pointers in inode security blob + * @dig_owner: Digest cache created from this inode + * @dig_owner_mutex: Protects @dig_owner + * @dig_user: Digest cache requested for this inode + * @dig_user_mutex: Protects @dig_user + * + * This structure contains references to digest caches, protected by their + * respective mutex. + */ +struct digest_cache_security { + struct digest_cache *dig_owner; + struct mutex dig_owner_mutex; + struct digest_cache *dig_user; + struct mutex dig_user_mutex; +}; + +extern struct lsm_blob_sizes digest_cache_blob_sizes; +extern char *default_path_str; + +static inline struct digest_cache_security * +digest_cache_get_security(const struct inode *inode) +{ + if (unlikely(!inode->i_security)) + return NULL; + + return inode->i_security + digest_cache_blob_sizes.lbs_inode; +} + +static inline struct digest_cache * +digest_cache_ref(struct digest_cache *digest_cache) +{ + atomic_inc(&digest_cache->ref_count); + pr_debug("Ref (+) digest cache %s (ref count: %d)\n", + digest_cache->path_str, atomic_read(&digest_cache->ref_count)); + return digest_cache; +} + +static inline struct digest_cache * +digest_cache_unref(struct digest_cache *digest_cache) +{ + bool ref_is_zero = atomic_dec_and_test(&digest_cache->ref_count); + + pr_debug("Ref (-) digest cache %s (ref count: %d)\n", + digest_cache->path_str, atomic_read(&digest_cache->ref_count)); + return (ref_is_zero) ? digest_cache : NULL; +} + +/* main.c */ +struct digest_cache *digest_cache_create(struct dentry *dentry, + struct path *digest_list_path, + char *path_str, char *filename); + +#endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c new file mode 100644 index 000000000000..166798e6247e --- /dev/null +++ b/security/digest_cache/main.c @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu + * + * Implement the main code of the digest_cache LSM. + */ + +#define pr_fmt(fmt) "DIGEST CACHE: "fmt +#include +#include + +#include "internal.h" + +static int digest_cache_enabled __ro_after_init = 1; +static struct kmem_cache *digest_cache_cache __read_mostly; + +char *default_path_str = CONFIG_DIGEST_LIST_DEFAULT_PATH; + +/** + * digest_cache_alloc_init - Allocate and initialize a new digest cache + * @path_str: Path string of the digest list + * @filename: Digest list file name (can be an empty string) + * + * This function allocates and initializes a new digest cache. + * + * Return: A digest_cache structure on success, NULL on error. + */ +static struct digest_cache *digest_cache_alloc_init(char *path_str, + char *filename) +{ + struct digest_cache *digest_cache; + + digest_cache = kmem_cache_alloc(digest_cache_cache, GFP_KERNEL); + if (!digest_cache) + return digest_cache; + + digest_cache->path_str = kasprintf(GFP_KERNEL, "%s%s%s", path_str, + filename[0] ? "/" : "", filename); + if (!digest_cache->path_str) { + kmem_cache_free(digest_cache_cache, digest_cache); + return NULL; + } + + atomic_set(&digest_cache->ref_count, 1); + digest_cache->flags = 0UL; + + pr_debug("New digest cache %s (ref count: %d)\n", + digest_cache->path_str, atomic_read(&digest_cache->ref_count)); + + return digest_cache; +} + +/** + * digest_cache_free - Free all memory occupied by the digest cache + * @digest_cache: Digest cache + * + * This function frees the memory occupied by the digest cache. + */ +static void digest_cache_free(struct digest_cache *digest_cache) +{ + pr_debug("Freed digest cache %s\n", digest_cache->path_str); + kfree(digest_cache->path_str); + kmem_cache_free(digest_cache_cache, digest_cache); +} + +/** + * digest_cache_create - Create a digest cache + * @dentry: Dentry of the inode for which the digest cache will be used + * @digest_list_path: Path structure of the digest list + * @path_str: Path string of the digest list + * @filename: Digest list file name (can be an empty string) + * + * This function first locates, from the passed path, the digest list inode + * from which the digest cache will be created or retrieved (if it already + * exists). + * + * If dig_owner is NULL in the inode security blob, this function creates a + * new digest cache with reference count set to 1 (reference returned), sets + * it to dig_owner and consequently increments again the digest cache reference + * count. + * + * Otherwise, it simply increments the reference count of the existing + * dig_owner, since that reference is returned to the caller. + * + * Incrementing the reference count twice before calling path_put() ensures + * that the digest cache returned is valid even if the inode is evicted from + * memory (which decreases the reference count). + * + * Releasing the dig_owner_mutex lock does not mean that the digest cache is + * ready for use. digest_cache_create() callers that found a partially + * instantiated digest cache have to wait until the INIT_IN_PROGRESS bit is + * cleared by the caller that is actually creating that digest cache. + * + * Return: A new digest cache on success, NULL on error. + */ +struct digest_cache *digest_cache_create(struct dentry *dentry, + struct path *digest_list_path, + char *path_str, char *filename) +{ + struct path file_path; + struct digest_cache *digest_cache = NULL; + struct digest_cache_security *dig_sec; + struct inode *inode = d_backing_inode(digest_list_path->dentry); + bool dig_owner_exists = false; + int ret; + + if (S_ISDIR(d_backing_inode(digest_list_path->dentry)->i_mode) && + filename[0]) { + ret = vfs_path_lookup(digest_list_path->dentry, + digest_list_path->mnt, filename, 0, + &file_path); + if (ret < 0) { + pr_debug("Cannot find digest list %s/%s\n", path_str, + filename); + return NULL; + } + + digest_list_path = &file_path; + inode = d_backing_inode(file_path.dentry); + + /* + * Cannot request a digest cache for the same inode the + * digest cache is populated from. + */ + if (d_backing_inode(dentry) == inode) + return NULL; + + /* No support for nested directories. */ + if (!S_ISREG(inode->i_mode)) + return NULL; + } + + dig_sec = digest_cache_get_security(inode); + if (unlikely(!dig_sec)) + goto out; + + /* Serialize check and assignment of dig_owner. */ + mutex_lock(&dig_sec->dig_owner_mutex); + if (dig_sec->dig_owner) { + /* Increment ref. count for reference returned to the caller. */ + digest_cache = digest_cache_ref(dig_sec->dig_owner); + dig_owner_exists = true; + mutex_unlock(&dig_sec->dig_owner_mutex); + goto exists; + } + + /* Ref. count is already 1 for this reference. */ + digest_cache = digest_cache_alloc_init(path_str, filename); + if (!digest_cache) { + mutex_unlock(&dig_sec->dig_owner_mutex); + goto out; + } + + /* Increment ref. count for reference set to dig_owner. */ + dig_sec->dig_owner = digest_cache_ref(digest_cache); + + /* Make the other lock contenders wait until creation complete. */ + set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags); + mutex_unlock(&dig_sec->dig_owner_mutex); + + /* Creation complete, notify the other lock contenders. */ + clear_and_wake_up_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags); +exists: + if (dig_owner_exists) + /* Wait until creation complete. */ + wait_on_bit(&dig_sec->dig_owner->flags, INIT_IN_PROGRESS, + TASK_UNINTERRUPTIBLE); +out: + if (digest_list_path == &file_path) + path_put(&file_path); + + return digest_cache; +} + +/** + * digest_cache_new - Retrieve digest list file name and request digest cache + * @dentry: Dentry of the inode for which the digest cache will be used + * + * This function locates the default path. If it is a file, it directly creates + * a digest cache from it. Otherwise, it reads the digest list file name from + * the security.digest_list xattr and requests the creation of a digest cache + * with that file name. If security.digest_list is not found, this function + * requests the creation of a digest cache on the parent directory. + * + * Return: A new digest cache on success, NULL on error. + */ +static struct digest_cache *digest_cache_new(struct dentry *dentry) +{ + char filename[NAME_MAX + 1] = { 0 }; + struct digest_cache *digest_cache = NULL; + struct path default_path; + int ret; + + ret = kern_path(default_path_str, 0, &default_path); + if (ret < 0) { + pr_debug("Cannot find path %s\n", default_path_str); + return NULL; + } + + /* The default path is a file, no need to get xattr. */ + if (S_ISREG(d_backing_inode(default_path.dentry)->i_mode)) { + pr_debug("Default path %s is a file, not reading %s xattr\n", + default_path_str, XATTR_NAME_DIGEST_LIST); + goto create; + } else if (!S_ISDIR(d_backing_inode(default_path.dentry)->i_mode)) { + pr_debug("Default path %s must be either a file or a directory\n", + default_path_str); + goto out; + } + + ret = vfs_getxattr(&nop_mnt_idmap, dentry, XATTR_NAME_DIGEST_LIST, + filename, sizeof(filename) - 1); + if (ret <= 0) { + pr_debug("Digest list path not found for file %s, using %s\n", + dentry->d_name.name, default_path_str); + goto create; + } + + if (strchr(filename, '/')) { + pr_debug("%s xattr should contain only a file name, got: %s\n", + XATTR_NAME_DIGEST_LIST, filename); + goto out; + } + + pr_debug("Found %s xattr in %s, default path: %s, digest list: %s\n", + XATTR_NAME_DIGEST_LIST, dentry->d_name.name, default_path_str, + filename); +create: + digest_cache = digest_cache_create(dentry, &default_path, + default_path_str, filename); +out: + path_put(&default_path); + return digest_cache; +} + +/** + * digest_cache_get - Get a digest cache for a given inode + * @dentry: Dentry of the inode for which the digest cache will be used + * + * This function tries to find a digest cache from the inode security blob of + * the passed dentry (dig_user field). If a digest cache was not found, it calls + * digest_cache_new() to create a new one. In both cases, it increments the + * digest cache reference count before returning the reference to the caller. + * + * The caller is responsible to call digest_cache_put() to release the digest + * cache reference returned. + * + * Lock dig_user_mutex to protect against concurrent requests to obtain a digest + * cache for the same inode, and to make other contenders wait until the first + * requester finishes the process. + * + * Return: A digest cache on success, NULL otherwise. + */ +struct digest_cache *digest_cache_get(struct dentry *dentry) +{ + struct digest_cache_security *dig_sec; + struct digest_cache *digest_cache = NULL; + + if (!digest_cache_enabled) + return NULL; + + dig_sec = digest_cache_get_security(d_backing_inode(dentry)); + if (unlikely(!dig_sec)) + return NULL; + + /* Serialize accesses to inode for which the digest cache is used. */ + mutex_lock(&dig_sec->dig_user_mutex); + if (!dig_sec->dig_user) + /* Consume extra reference from digest_cache_create(). */ + dig_sec->dig_user = digest_cache_new(dentry); + + if (dig_sec->dig_user) + /* Increment ref. count for reference returned to the caller. */ + digest_cache = digest_cache_ref(dig_sec->dig_user); + + mutex_unlock(&dig_sec->dig_user_mutex); + return digest_cache; +} +EXPORT_SYMBOL_GPL(digest_cache_get); + +/** + * digest_cache_put - Release a digest cache reference + * @digest_cache: Digest cache + * + * This function decrements the reference count of the digest cache passed as + * argument. If the reference count reaches zero, it calls digest_cache_free() + * to free the digest cache. + */ +void digest_cache_put(struct digest_cache *digest_cache) +{ + struct digest_cache *to_free; + + to_free = digest_cache_unref(digest_cache); + if (!to_free) + return; + + digest_cache_free(to_free); +} +EXPORT_SYMBOL_GPL(digest_cache_put); + +struct lsm_blob_sizes digest_cache_blob_sizes __ro_after_init = { + .lbs_inode = sizeof(struct digest_cache_security), +}; + +/** + * digest_cache_inode_alloc_security - Initialize inode security blob + * @inode: Inode for which the security blob is initialized + * + * This function initializes the digest_cache_security structure, directly + * stored in the inode security blob. + * + * Return: Zero. + */ +static int digest_cache_inode_alloc_security(struct inode *inode) +{ + struct digest_cache_security *dig_sec; + + /* The inode security blob is always allocated here. */ + dig_sec = digest_cache_get_security(inode); + mutex_init(&dig_sec->dig_owner_mutex); + mutex_init(&dig_sec->dig_user_mutex); + return 0; +} + +/** + * digest_cache_inode_free_security - Release the digest cache references + * @inode: Inode for which the digest cache references are released + * + * Since the inode is being evicted, this function releases the non-needed + * references to the digest_caches stored in the digest_cache_security + * structure. + */ +static void digest_cache_inode_free_security(struct inode *inode) +{ + struct digest_cache_security *dig_sec; + + dig_sec = digest_cache_get_security(inode); + if (!dig_sec) + return; + + mutex_destroy(&dig_sec->dig_owner_mutex); + mutex_destroy(&dig_sec->dig_user_mutex); + if (dig_sec->dig_owner) + digest_cache_put(dig_sec->dig_owner); + if (dig_sec->dig_user) + digest_cache_put(dig_sec->dig_user); +} + +static struct security_hook_list digest_cache_hooks[] __ro_after_init = { + LSM_HOOK_INIT(inode_alloc_security, digest_cache_inode_alloc_security), + LSM_HOOK_INIT(inode_free_security, digest_cache_inode_free_security), +}; + +/** + * digest_cache_init_once - Initialize the digest cache structure + * @foo: Digest cache structure to initialize + * + * This function fills the digest cache structure with zeros. + */ +static void digest_cache_init_once(void *foo) +{ + struct digest_cache *digest_cache = (struct digest_cache *)foo; + + memset(digest_cache, 0, sizeof(*digest_cache)); +} + +static const struct lsm_id digest_cache_lsmid = { + .name = "digest_cache", + .id = LSM_ID_DIGEST_CACHE, +}; + +/** + * digest_cache_init - Initialize the digest_cache LSM + * + * Initialize the digest_cache LSM, by instantiating a cache for the + * digest_cache structure and by registering the digest_cache LSM hooks. + */ +static int __init digest_cache_init(void) +{ + digest_cache_cache = kmem_cache_create("digest_cache_cache", + sizeof(struct digest_cache), + 0, SLAB_PANIC, + digest_cache_init_once); + + security_add_hooks(digest_cache_hooks, ARRAY_SIZE(digest_cache_hooks), + &digest_cache_lsmid); + return 0; +} + +DEFINE_LSM(digest_cache) = { + .name = "digest_cache", + .enabled = &digest_cache_enabled, + .init = digest_cache_init, + .blobs = &digest_cache_blob_sizes, +}; diff --git a/security/security.c b/security/security.c index da827d7cbad8..570f52d534f8 100644 --- a/security/security.c +++ b/security/security.c @@ -50,7 +50,8 @@ (IS_ENABLED(CONFIG_SECURITY_SAFESETID) ? 1 : 0) + \ (IS_ENABLED(CONFIG_SECURITY_LOCKDOWN_LSM) ? 1 : 0) + \ (IS_ENABLED(CONFIG_BPF_LSM) ? 1 : 0) + \ - (IS_ENABLED(CONFIG_SECURITY_LANDLOCK) ? 1 : 0)) + (IS_ENABLED(CONFIG_SECURITY_LANDLOCK) ? 1 : 0) + \ + (IS_ENABLED(CONFIG_SECURITY_DIGEST_CACHE) ? 1 : 0)) /* * These are descriptions of the reasons that can be passed to the diff --git a/tools/testing/selftests/lsm/lsm_list_modules_test.c b/tools/testing/selftests/lsm/lsm_list_modules_test.c index 9df29b1e3497..24fb9c038434 100644 --- a/tools/testing/selftests/lsm/lsm_list_modules_test.c +++ b/tools/testing/selftests/lsm/lsm_list_modules_test.c @@ -122,6 +122,9 @@ TEST(correct_lsm_list_modules) case LSM_ID_LANDLOCK: name = "landlock"; break; + case LSM_ID_DIGEST_CACHE: + name = "digest_cache"; + break; default: name = "INVALID"; break; From patchwork Fri Feb 9 14:09:08 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Roberto Sassu X-Patchwork-Id: 771670 Received: from frasgout11.his.huawei.com (frasgout11.his.huawei.com [14.137.139.23]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 99E6A69D27; Fri, 9 Feb 2024 14:10:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=14.137.139.23 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707487844; cv=none; b=JU+jdk5x0dldZcAbKUoFibbxWT+YvcabPaNMPoELJ8tvF2jjKt30T+nmB1aab+/orlGMEZGlOXE2+1kLgWO6WlFp/cTQi+QzaVd5G6CoTlyBT92860HRO8sRhGIYgOjw9nGUBtqaskW3Un7ixF8pX+yJXaVDPzv5jx1rKYpwLKo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707487844; c=relaxed/simple; bh=Kcz9A1rzsP9JIxUepj+z5GOrl2+8OTtMHIe3Vpyi+Hw=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=tRtHqPS3hG2IeRlrvlCfDV8FBty9UTFKxKPr0mditVh0bIKDMp8rgRr6l8Y9KzyJHGUq5aBS5ADd1jt0j55PAV91M7UUQupvzuLgPsxEAY/SO+jMvWYvCtMoY8f7C0GTSi6cxFRXw14TOaVnr1G2ZYG+eTz836Ua/0DeKP5oltY= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com; spf=pass smtp.mailfrom=huaweicloud.com; arc=none smtp.client-ip=14.137.139.23 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=huaweicloud.com Received: from mail.maildlp.com (unknown [172.18.186.51]) by frasgout11.his.huawei.com (SkyGuard) with ESMTP id 4TWb3503J2z9y4TS; Fri, 9 Feb 2024 21:55:29 +0800 (CST) Received: from mail02.huawei.com (unknown [7.182.16.47]) by mail.maildlp.com (Postfix) with ESMTP id A7800140132; Fri, 9 Feb 2024 22:10:39 +0800 (CST) Received: from huaweicloud.com (unknown [10.204.63.22]) by APP1 (Coremail) with SMTP id LxC2BwAHshoZMsZlvXMuAg--.65105S6; Fri, 09 Feb 2024 15:10:39 +0100 (CET) From: Roberto Sassu To: corbet@lwn.net, paul@paul-moore.com, jmorris@namei.org, serge@hallyn.com, shuah@kernel.org, mcoquelin.stm32@gmail.com, alexandre.torgue@foss.st.com, mic@digikod.net Cc: linux-security-module@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, bpf@vger.kernel.org, zohar@linux.ibm.com, dmitry.kasatkin@gmail.com, linux-integrity@vger.kernel.org, wufan@linux.microsoft.com, pbrobinson@gmail.com, zbyszek@in.waw.pl, hch@lst.de, mjg59@srcf.ucam.org, pmatilai@redhat.com, jannh@google.com, dhowells@redhat.com, jikos@kernel.org, mkoutny@suse.com, ppavlu@suse.com, petr.vorel@gmail.com, petrtesarik@huaweicloud.com, Roberto Sassu Subject: [PATCH v3 04/13] digest_cache: Add hash tables and operations Date: Fri, 9 Feb 2024 15:09:08 +0100 Message-Id: <20240209140917.846878-5-roberto.sassu@huaweicloud.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> References: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> Precedence: bulk X-Mailing-List: linux-kselftest@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-CM-TRANSID: LxC2BwAHshoZMsZlvXMuAg--.65105S6 X-Coremail-Antispam: 1UD129KBjvAXoW3tF4kCrWrGFW7KrWDKryUKFg_yoW8Xw48to Z0kF4UJw18WFy3ur4DCF17Aa1Uu34rt34xAr1kJrWDZ3WktryUJ3ZrCFn8JFy3Xry8GrZ7 Aw1kJ3y7Jr48tr93n29KB7ZKAUJUUUU8529EdanIXcx71UUUUU7v73VFW2AGmfu7bjvjm3 AaLaJ3UjIYCTnIWjp_UUUOV7AC8VAFwI0_Wr0E3s1l1xkIjI8I6I8E6xAIw20EY4v20xva j40_Wr0E3s1l1IIY67AEw4v_Jr0_Jr4l82xGYIkIc2x26280x7IE14v26r126s0DM28Irc Ia0xkI8VCY1x0267AKxVW5JVCq3wA2ocxC64kIII0Yj41l84x0c7CEw4AK67xGY2AK021l 84ACjcxK6xIIjxv20xvE14v26r1I6r4UM28EF7xvwVC0I7IYx2IY6xkF7I0E14v26r4UJV WxJr1l84ACjcxK6I8E87Iv67AKxVW8JVWxJwA2z4x0Y4vEx4A2jsIEc7CjxVAFwI0_Cr1j 6rxdM2AIxVAIcxkEcVAq07x20xvEncxIr21l5I8CrVACY4xI64kE6c02F40Ex7xfMcIj6x IIjxv20xvE14v26r1j6r18McIj6I8E87Iv67AKxVWUJVW8JwAm72CE4IkC6x0Yz7v_Jr0_ Gr1lF7xvr2IYc2Ij64vIr41lF7I21c0EjII2zVCS5cI20VAGYxC7M4IIrI8v6xkF7I0E8c xan2IY04v7MxkF7I0En4kS14v26r4a6rW5MxAIw28IcxkI7VAKI48JMxC20s026xCaFVCj c4AY6r1j6r4UMI8I3I0E5I8CrVAFwI0_Jr0_Jr4lx2IqxVCjr7xvwVAFwI0_JrI_JrWlx4 CE17CEb7AF67AKxVW8ZVWrXwCIc40Y0x0EwIxGrwCI42IY6xIIjxv20xvE14v26r1j6r1x MIIF0xvE2Ix0cI8IcVCY1x0267AKxVW8Jr0_Cr1UMIIF0xvE42xK8VAvwI8IcIk0rVWUJV WUCwCI42IY6I8E87Iv67AKxVW8JVWxJwCI42IY6I8E87Iv6xkF7I0E14v26F4UJVW0obIY CTnIWIevJa73UjIFyTuYvjTRNdb1DUUUU X-CM-SenderInfo: purev21wro2thvvxqx5xdzvxpfor3voofrz/1tbiAgAIBF1jj5Y4egADso From: Roberto Sassu Add a linked list of hash tables to the digest cache, one per algorithm, containing the digests extracted from digest lists. The number of hash table slots is determined by dividing the number of digests to add to the average depth of the collision list defined with CONFIG_DIGEST_CACHE_HTABLE_DEPTH (currently set to 30). It can be changed in the kernel configuration. Add digest_cache_htable_init() and digest_cache_htable_add(), to be called by digest list parsers, in order to allocate the hash tables and to add extracted digests. Add digest_cache_htable_free(), to let the digest_cache LSM free the hash tables at the time a digest cache is freed. Add digest_cache_htable_lookup() to search a digest in the hash table of a digest cache for a given algorithm. Add digest_cache_lookup() to the public API, to let users of the digest_cache LSM search a digest in a digest cache and, in a subsequent patch, to search it in the digest caches for each directory entry. Return the digest cache containing the digest, as a different type, digest_cache_found_t to avoid it being accidentally put. Also, introduce digest_cache_from_found_t() to explicitly convert it back to a digest cache for further use (e.g. retrieving verification data, introduced later). Finally, add digest_cache_hash_key() to compute the hash table key from the first two bytes of the digest (modulo the number of slots). Signed-off-by: Roberto Sassu --- include/linux/digest_cache.h | 34 +++++ security/digest_cache/Kconfig | 11 ++ security/digest_cache/Makefile | 2 +- security/digest_cache/htable.c | 250 +++++++++++++++++++++++++++++++ security/digest_cache/internal.h | 43 ++++++ security/digest_cache/main.c | 3 + 6 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 security/digest_cache/htable.c diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h index e79f94a60b0f..4872700ac205 100644 --- a/include/linux/digest_cache.h +++ b/include/linux/digest_cache.h @@ -11,12 +11,39 @@ #define _LINUX_DIGEST_CACHE_H #include +#include struct digest_cache; +/** + * typedef digest_cache_found_t - Digest cache reference as numeric value + * + * This new type represents a digest cache reference that should not be put. + */ +typedef unsigned long digest_cache_found_t; + +/** + * digest_cache_from_found_t - Convert digest_cache_found_t to digest cache ptr + * @found: digest_cache_found_t value + * + * Convert the digest_cache_found_t returned by digest_cache_lookup() to a + * digest cache pointer, so that it can be passed to the other functions of the + * API. + * + * Return: Digest cache pointer. + */ +static inline struct digest_cache * +digest_cache_from_found_t(digest_cache_found_t found) +{ + return (struct digest_cache *)found; +} + #ifdef CONFIG_SECURITY_DIGEST_CACHE struct digest_cache *digest_cache_get(struct dentry *dentry); void digest_cache_put(struct digest_cache *digest_cache); +digest_cache_found_t digest_cache_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo); #else static inline struct digest_cache *digest_cache_get(struct dentry *dentry) @@ -28,5 +55,12 @@ static inline void digest_cache_put(struct digest_cache *digest_cache) { } +static inline digest_cache_found_t +digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo) +{ + return 0UL; +} + #endif /* CONFIG_SECURITY_DIGEST_CACHE */ #endif /* _LINUX_DIGEST_CACHE_H */ diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig index 2f361c7844df..c7e77cf2c173 100644 --- a/security/digest_cache/Kconfig +++ b/security/digest_cache/Kconfig @@ -19,3 +19,14 @@ config DIGEST_LIST_DEFAULT_PATH It can be changed at run-time, by writing the new path to the securityfs interface. Digest caches created with the old path are not affected by the change. + +config DIGEST_CACHE_HTABLE_DEPTH + int + default 30 + help + Desired average depth of the collision list in the digest cache + hash tables. + + A smaller number will increase the amount of hash table slots, and + make the search faster. A bigger number will decrease the number of + hash table slots, but make the search slower. diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile index 1330655e33b1..7e00c53d8f55 100644 --- a/security/digest_cache/Makefile +++ b/security/digest_cache/Makefile @@ -4,4 +4,4 @@ obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o -digest_cache-y := main.o secfs.o +digest_cache-y := main.o secfs.o htable.o diff --git a/security/digest_cache/htable.c b/security/digest_cache/htable.c new file mode 100644 index 000000000000..d2d5d8f5e5b1 --- /dev/null +++ b/security/digest_cache/htable.c @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu + * + * Implement hash table operations for the digest cache. + */ + +#define pr_fmt(fmt) "DIGEST CACHE: "fmt +#include "internal.h" + +/** + * digest_cache_hash_key - Compute hash key + * @digest: Digest cache + * @num_slots: Number of slots in the hash table + * + * This function computes a hash key based on the first two bytes of the + * digest and the number of slots of the hash table. + * + * Return: Hash key. + */ +static inline unsigned int digest_cache_hash_key(u8 *digest, + unsigned int num_slots) +{ + /* Same as ima_hash_key() but parametrized. */ + return (digest[0] | digest[1] << 8) % num_slots; +} + +/** + * lookup_htable - Lookup a hash table + * @digest_cache: Digest cache + * @algo: Algorithm of the desired hash table + * + * This function searches the hash table for a given algorithm in the digest + * cache. + * + * Return: A hash table if found, NULL otherwise. + */ +static struct htable *lookup_htable(struct digest_cache *digest_cache, + enum hash_algo algo) +{ + struct htable *h; + + list_for_each_entry(h, &digest_cache->htables, next) + if (h->algo == algo) + return h; + + return NULL; +} + +/** + * digest_cache_htable_init - Allocate and initialize the hash table + * @digest_cache: Digest cache + * @num_digests: Number of digests to add to the digest cache + * @algo: Algorithm of the digests + * + * This function allocates and initializes the hash table for a given algorithm. + * The number of slots depends on the number of digests to add to the digest + * cache, and the constant CONFIG_DIGEST_CACHE_HTABLE_DEPTH stating the desired + * average depth of the collision list. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests, + enum hash_algo algo) +{ + struct htable *h; + int i; + + h = lookup_htable(digest_cache, algo); + if (h) + return 0; + + h = kmalloc(sizeof(*h), GFP_KERNEL); + if (!h) + return -ENOMEM; + + h->num_slots = DIV_ROUND_UP(num_digests, + CONFIG_DIGEST_CACHE_HTABLE_DEPTH); + h->slots = kmalloc_array(h->num_slots, sizeof(*h->slots), GFP_KERNEL); + if (!h->slots) { + kfree(h); + return -ENOMEM; + } + + for (i = 0; i < h->num_slots; i++) + INIT_HLIST_HEAD(&h->slots[i]); + + h->num_digests = 0; + h->algo = algo; + + list_add_tail(&h->next, &digest_cache->htables); + + pr_debug("Initialized hash table for digest list %s, digests: %llu, slots: %u, algo: %s\n", + digest_cache->path_str, num_digests, h->num_slots, + hash_algo_name[algo]); + return 0; +} + +/** + * digest_cache_htable_add - Add a new digest to the digest cache + * @digest_cache: Digest cache + * @digest: Digest to add + * @algo: Algorithm of digest + * + * This function, invoked by a digest list parser, adds a digest extracted + * from a digest list to the digest cache. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo) +{ + struct htable *h; + struct digest_cache_entry *entry; + unsigned int key; + int digest_len; + + h = lookup_htable(digest_cache, algo); + if (!h) { + pr_debug("No hash table for algorithm %s was found in digest cache %s, initialize one\n", + hash_algo_name[algo], digest_cache->path_str); + return -ENOENT; + } + + digest_len = hash_digest_size[algo]; + + entry = kmalloc(sizeof(*entry) + digest_len, GFP_KERNEL); + if (!entry) + return -ENOMEM; + + memcpy(entry->digest, digest, digest_len); + + key = digest_cache_hash_key(digest, h->num_slots); + hlist_add_head(&entry->hnext, &h->slots[key]); + h->num_digests++; + pr_debug("Added digest %s:%*phN to digest cache %s, num of %s digests: %llu\n", + hash_algo_name[algo], digest_len, digest, + digest_cache->path_str, hash_algo_name[algo], h->num_digests); + return 0; +} + +/** + * digest_cache_htable_lookup - Search a digest in the digest cache + * @dentry: Dentry of the file whose digest is looked up + * @digest_cache: Digest cache + * @digest: Digest to search + * @algo: Algorithm of the digest to search + * + * This function searches the passed digest and algorithm in the passed digest + * cache. + * + * Return: Zero if the digest is found, -ENOENT if not. + */ +int digest_cache_htable_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo) +{ + struct digest_cache_entry *entry; + struct htable *h; + unsigned int key; + int digest_len; + int search_depth = 0; + + h = lookup_htable(digest_cache, algo); + if (!h) + return -ENOENT; + + digest_len = hash_digest_size[algo]; + key = digest_cache_hash_key(digest, h->num_slots); + + hlist_for_each_entry(entry, &h->slots[key], hnext) { + if (!memcmp(entry->digest, digest, digest_len)) { + pr_debug("Cache hit at depth %d for file %s, digest %s:%*phN in digest cache %s\n", + search_depth, dentry->d_name.name, + hash_algo_name[algo], digest_len, digest, + digest_cache->path_str); + + return 0; + } + + search_depth++; + } + + pr_debug("Cache miss for file %s, digest %s:%*phN in digest cache %s\n", + dentry->d_name.name, hash_algo_name[algo], digest_len, digest, + digest_cache->path_str); + return -ENOENT; +} + +/** + * digest_cache_lookup - Search a digest in the digest cache + * @dentry: Dentry of the file whose digest is looked up + * @digest_cache: Digest cache + * @digest: Digest to search + * @algo: Algorithm of the digest to search + * + * This function calls digest_cache_htable_lookup() to search a digest in the + * passed digest cache, obtained with digest_cache_get(). + * + * It returns the digest cache reference as the digest_cache_found_t type, to + * avoid that the digest cache is accidentally put. The digest_cache_found_t + * type can be converted back to a digest cache pointer, by + * calling digest_cache_from_found_t(). + * + * Return: A positive digest_cache_found_t if the digest is found, zero if not. + */ +digest_cache_found_t digest_cache_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo) +{ + int ret; + + ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo); + return (!ret) ? (digest_cache_found_t)digest_cache : 0UL; +} +EXPORT_SYMBOL_GPL(digest_cache_lookup); + +/** + * digest_cache_htable_free - Free the hash tables + * @digest_cache: Digest cache + * + * This function removes all digests from all hash tables in the digest cache, + * and frees the memory. + */ +void digest_cache_htable_free(struct digest_cache *digest_cache) +{ + struct htable *h, *h_tmp; + struct digest_cache_entry *p; + struct hlist_node *q; + int i; + + list_for_each_entry_safe(h, h_tmp, &digest_cache->htables, next) { + for (i = 0; i < h->num_slots; i++) { + hlist_for_each_entry_safe(p, q, &h->slots[i], hnext) { + hlist_del(&p->hnext); + pr_debug("Removed digest %s:%*phN from digest cache %s\n", + hash_algo_name[h->algo], + hash_digest_size[h->algo], p->digest, + digest_cache->path_str); + kfree(p); + } + } + + list_del(&h->next); + kfree(h->slots); + kfree(h); + } +} diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h index bbf5eefe5c82..ee6487acce1d 100644 --- a/security/digest_cache/internal.h +++ b/security/digest_cache/internal.h @@ -16,8 +16,40 @@ /* Digest cache bits in flags. */ #define INIT_IN_PROGRESS 0 /* Digest cache being initialized. */ +/** + * struct digest_cache_entry - Entry of a digest cache hash table + * @hnext: Pointer to the next element in the collision list + * @digest: Stored digest + * + * This structure represents an entry of a digest cache hash table, storing a + * digest. + */ +struct digest_cache_entry { + struct hlist_node hnext; + u8 digest[]; +} __packed; + +/** + * struct htable - Hash table + * @next: Next hash table in the linked list + * @slots: Hash table slots + * @num_slots: Number of slots + * @num_digests: Number of digests stored in the hash table + * @algo: Algorithm of the digests + * + * This structure is a hash table storing digests of file content or metadata. + */ +struct htable { + struct list_head next; + struct hlist_head *slots; + unsigned int num_slots; + u64 num_digests; + enum hash_algo algo; +}; + /** * struct digest_cache - Digest cache + * @htables: Hash tables (one per algorithm) * @ref_count: Number of references to the digest cache * @path_str: Path of the digest list the digest cache was created from * @flags: Control flags @@ -25,6 +57,7 @@ * This structure represents a cache of digests extracted from a digest list. */ struct digest_cache { + struct list_head htables; atomic_t ref_count; char *path_str; unsigned long flags; @@ -84,4 +117,14 @@ struct digest_cache *digest_cache_create(struct dentry *dentry, struct path *digest_list_path, char *path_str, char *filename); +/* htable.c */ +int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests, + enum hash_algo algo); +int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo); +int digest_cache_htable_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo); +void digest_cache_htable_free(struct digest_cache *digest_cache); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c index 8bae2e91ac4c..fa435b6566fa 100644 --- a/security/digest_cache/main.c +++ b/security/digest_cache/main.c @@ -48,6 +48,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str, atomic_set(&digest_cache->ref_count, 1); digest_cache->flags = 0UL; + INIT_LIST_HEAD(&digest_cache->htables); pr_debug("New digest cache %s (ref count: %d)\n", digest_cache->path_str, atomic_read(&digest_cache->ref_count)); @@ -63,6 +64,8 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str, */ static void digest_cache_free(struct digest_cache *digest_cache) { + digest_cache_htable_free(digest_cache); + pr_debug("Freed digest cache %s\n", digest_cache->path_str); kfree(digest_cache->path_str); kmem_cache_free(digest_cache_cache, digest_cache); From patchwork Fri Feb 9 14:09:10 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Roberto Sassu X-Patchwork-Id: 771669 Received: from frasgout13.his.huawei.com (frasgout13.his.huawei.com [14.137.139.46]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 37FFE67E65; Fri, 9 Feb 2024 14:11:19 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=14.137.139.46 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707487882; cv=none; b=MsrF3w3njlAysgK1xELCoh5ob37zE2SRRkW9fJNoaRXntJ2dA8xgKGIlYgWvgMyUvHzH4BEjhrbUKfbgTk8qomPi8YAev12ED7w/7fJi+y2C28epJ1qfNif+kuDz/OjmhWMVl5cRR1pAWDXktiYv1bzsjx4eAuf1FqMtHkWj7Os= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707487882; c=relaxed/simple; bh=A+xt0lE9Nh5hSsYLRCbR0zGd7981S5WYca5iMesQ97E=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=VFXf8mGcGXsXN2fHuskJtBimsehkOFrU02a9DumVd7h6EvUpXr1IokQDaoGALyFjW5aBpthm+WfHf0bbIBv4zemSyk3eIsIw5+307refKLAjzhZAXtEhqk5KcIPX31GHXqX10JtQnOEc6K4PewNp7K87gosIQIk4RHATPkZw1Vo= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com; spf=pass smtp.mailfrom=huaweicloud.com; arc=none smtp.client-ip=14.137.139.46 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=huaweicloud.com Received: from mail.maildlp.com (unknown [172.18.186.51]) by frasgout13.his.huawei.com (SkyGuard) with ESMTP id 4TWb3r5QW9z9yB7W; Fri, 9 Feb 2024 21:56:08 +0800 (CST) Received: from mail02.huawei.com (unknown [7.182.16.47]) by mail.maildlp.com (Postfix) with ESMTP id 40B3E14059F; Fri, 9 Feb 2024 22:11:07 +0800 (CST) Received: from huaweicloud.com (unknown [10.204.63.22]) by APP1 (Coremail) with SMTP id LxC2BwAHshoZMsZlvXMuAg--.65105S8; Fri, 09 Feb 2024 15:11:06 +0100 (CET) From: Roberto Sassu To: corbet@lwn.net, paul@paul-moore.com, jmorris@namei.org, serge@hallyn.com, shuah@kernel.org, mcoquelin.stm32@gmail.com, alexandre.torgue@foss.st.com, mic@digikod.net Cc: linux-security-module@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, bpf@vger.kernel.org, zohar@linux.ibm.com, dmitry.kasatkin@gmail.com, linux-integrity@vger.kernel.org, wufan@linux.microsoft.com, pbrobinson@gmail.com, zbyszek@in.waw.pl, hch@lst.de, mjg59@srcf.ucam.org, pmatilai@redhat.com, jannh@google.com, dhowells@redhat.com, jikos@kernel.org, mkoutny@suse.com, ppavlu@suse.com, petr.vorel@gmail.com, petrtesarik@huaweicloud.com, Roberto Sassu Subject: [PATCH v3 06/13] digest_cache: Parse tlv digest lists Date: Fri, 9 Feb 2024 15:09:10 +0100 Message-Id: <20240209140917.846878-7-roberto.sassu@huaweicloud.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> References: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> Precedence: bulk X-Mailing-List: linux-kselftest@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-CM-TRANSID: LxC2BwAHshoZMsZlvXMuAg--.65105S8 X-Coremail-Antispam: 1UD129KBjvAXoW3uF13Cw43uFWxZF43Zw48Xrb_yoW8Ww1kGo Z0vF4UAw4rtrsF9F4kCF13Ar4rG3yYqFyrAw4fGr4DW3WrtFy5ta1kCa15Ga98Zw1rtFZF yr18J3yFqrWUKrs7n29KB7ZKAUJUUUU8529EdanIXcx71UUUUU7v73VFW2AGmfu7bjvjm3 AaLaJ3UjIYCTnIWjp_UUUOm7kC6x804xWl14x267AKxVWrJVCq3wAFc2x0x2IEx4CE42xK 8VAvwI8IcIk0rVWrJVCq3wAFIxvE14AKwVWUJVWUGwA2048vs2IY020E87I2jVAFwI0_JF 0E3s1l82xGYIkIc2x26xkF7I0E14v26ryj6s0DM28lY4IEw2IIxxk0rwA2F7IY1VAKz4vE j48ve4kI8wA2z4x0Y4vE2Ix0cI8IcVAFwI0_JFI_Gr1l84ACjcxK6xIIjxv20xvEc7CjxV AFwI0_Gr1j6F4UJwA2z4x0Y4vEx4A2jsIE14v26r4j6F4UM28EF7xvwVC2z280aVCY1x02 67AKxVWxJr0_GcWle2I262IYc4CY6c8Ij28IcVAaY2xG8wAqx4xG64xvF2IEw4CE5I8CrV C2j2WlYx0E2Ix0cI8IcVAFwI0_Jr0_Jr4lYx0Ex4A2jsIE14v26r1j6r4UMcvjeVCFs4IE 7xkEbVWUJVW8JwACjcxG0xvY0x0EwIxGrwACjI8F5VA0II8E6IAqYI8I648v4I1lFIxGxc IEc7CjxVA2Y2ka0xkIwI1lc7CjxVAaw2AFwI0_GFv_Wryl42xK82IYc2Ij64vIr41l4I8I 3I0E4IkC6x0Yz7v_Jr0_Gr1lx2IqxVAqx4xG67AKxVWUJVWUGwC20s026x8GjcxK67AKxV WUGVWUWwC2zVAF1VAY17CE14v26r4a6rW5MIIYY7kG6xAYrwCIc40Y0x0EwIxGrwCI42IY 6xIIjxv20xvE14v26r1I6r4UMIIF0xvE2Ix0cI8IcVCY1x0267AKxVW8Jr0_Cr1UMIIF0x vE42xK8VAvwI8IcIk0rVWUJVWUCwCI42IY6I8E87Iv67AKxVW8JVWxJwCI42IY6I8E87Iv 6xkF7I0E14v26F4UJVW0obIYCTnIWIevJa73UjIFyTuYvjxU76pBUUUUU X-CM-SenderInfo: purev21wro2thvvxqx5xdzvxpfor3voofrz/1tbiAQAIBF1jj5o4EQAAsP From: Roberto Sassu Add digest_list_parse_tlv(), to parse TLV-formatted (Type Length Value) digest lists. Their structure is: [header: DIGEST_LIST_FILE, num fields, total len] [field: DIGEST_LIST_ALGO, length, value] [field: DIGEST_LIST_ENTRY#1, length, value (below)] |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len] |- [DIGEST_LIST_ENTRY_DIGEST#1, length, file digest] |- [DIGEST_LIST_ENTRY_PATH#1, length, file path] [field: DIGEST_LIST_ENTRY#N, length, value (below)] |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len] |- [DIGEST_LIST_ENTRY_DIGEST#N, length, file digest] |- [DIGEST_LIST_ENTRY_PATH#N, length, file path] DIGEST_LIST_ALGO must have a fixed length of sizeof(u64). The data of the DIGEST_LIST_ENTRY field are itself in TLV format. Currently defined fields are sufficient for measurement/appraisal of file content. More fields will be introduced later for file metadata. Introduce digest_list_file_callback() to handle the DIGEST_LIST_FILE fields, DIGEST_LIST_ALGO and DIGEST_LIST_ENTRY, and the respective field parsers parse_digest_list_algo() and parse_digest_list_entry(). Also introduce digest_list_entry_data_callback(), to handle the DIGEST_LIST_ENTRY_DATA (nested) fields, DIGEST_LIST_ENTRY_DIGEST and DIGEST_LIST_ENTRY_PATH, and the respective field parsers parse_digest_list_entry_digest() and parse_digest_list_entry_path(). The TLV parser itself is defined in lib/tlv_parser.c. Both the TLV parser and the tlv digest list parser have been formally verified with Frama-C (https://frama-c.com/). The analysis has been done on this file: https://github.com/robertosassu/rpm-formal/blob/main/validate_tlv.c Here is the result of the analysis: [eva:summary] ====== ANALYSIS SUMMARY ====== --------------------------------------------------------------------------- 13 functions analyzed (out of 13): 100% coverage. In these functions, 240 statements reached (out of 254): 94% coverage. --------------------------------------------------------------------------- Some errors and warnings have been raised during the analysis: by the Eva analyzer: 0 errors 4 warnings by the Frama-C kernel: 0 errors 0 warnings --------------------------------------------------------------------------- 0 alarms generated by the analysis. --------------------------------------------------------------------------- Evaluation of the logical properties reached by the analysis: Assertions 5 valid 0 unknown 0 invalid 5 total Preconditions 24 valid 0 unknown 0 invalid 24 total 100% of the logical properties reached have been proven. --------------------------------------------------------------------------- The warnings are: [eva] validate_tlv.c:437: Warning: this partitioning parameter cannot be evaluated safely on all states [eva] validate_tlv.c:445: Warning: this partitioning parameter cannot be evaluated safely on all states [eva] validate_tlv.c:354: Warning: this partitioning parameter cannot be evaluated safely on all states [eva] validate_tlv.c:382: Warning: this partitioning parameter cannot be evaluated safely on all states Signed-off-by: Roberto Sassu --- include/uapi/linux/tlv_digest_list.h | 72 ++++++ security/digest_cache/Kconfig | 1 + security/digest_cache/Makefile | 2 + security/digest_cache/parsers/parsers.h | 13 ++ security/digest_cache/parsers/tlv.c | 299 ++++++++++++++++++++++++ security/digest_cache/populate.c | 4 + 6 files changed, 391 insertions(+) create mode 100644 include/uapi/linux/tlv_digest_list.h create mode 100644 security/digest_cache/parsers/parsers.h create mode 100644 security/digest_cache/parsers/tlv.c diff --git a/include/uapi/linux/tlv_digest_list.h b/include/uapi/linux/tlv_digest_list.h new file mode 100644 index 000000000000..8c97a46901c1 --- /dev/null +++ b/include/uapi/linux/tlv_digest_list.h @@ -0,0 +1,72 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * Copyright (C) 2017-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu + * + * Export definitions of the tlv digest list. + */ + +#ifndef _UAPI_LINUX_TLV_DIGEST_LIST_H +#define _UAPI_LINUX_TLV_DIGEST_LIST_H + +#include + +#define FOR_EACH_DIGEST_LIST_TYPE(DIGEST_LIST_TYPE) \ + DIGEST_LIST_TYPE(DIGEST_LIST_FILE) \ + DIGEST_LIST_TYPE(DIGEST_LIST__LAST) + +#define FOR_EACH_DIGEST_LIST_FIELD(DIGEST_LIST_FIELD) \ + DIGEST_LIST_FIELD(DIGEST_LIST_ALGO) \ + DIGEST_LIST_FIELD(DIGEST_LIST_ENTRY) \ + DIGEST_LIST_FIELD(DIGEST_LIST_FIELD__LAST) + +#define FOR_EACH_DIGEST_LIST_ENTRY_TYPE(DIGEST_LIST_ENTRY_TYPE) \ + DIGEST_LIST_ENTRY_TYPE(DIGEST_LIST_ENTRY_DATA) \ + DIGEST_LIST_ENTRY_TYPE(DIGEST_LIST_ENTRY__LAST) + +#define FOR_EACH_DIGEST_LIST_ENTRY_FIELD(DIGEST_LIST_ENTRY_FIELD) \ + DIGEST_LIST_ENTRY_FIELD(DIGEST_LIST_ENTRY_DIGEST) \ + DIGEST_LIST_ENTRY_FIELD(DIGEST_LIST_ENTRY_PATH) \ + DIGEST_LIST_ENTRY_FIELD(DIGEST_LIST_ENTRY_FIELD__LAST) + +#define GENERATE_ENUM(ENUM) ENUM, +#define GENERATE_STRING(STRING) #STRING, + +/** + * enum digest_list_types - Types of digest list + * + * Enumerates the types of digest list to parse. + */ +enum digest_list_types { + FOR_EACH_DIGEST_LIST_TYPE(GENERATE_ENUM) +}; + +/** + * enum digest_list_fields - Digest list fields + * + * Enumerates the digest list fields. + */ +enum digest_list_fields { + FOR_EACH_DIGEST_LIST_FIELD(GENERATE_ENUM) +}; + +/** + * enum digest_list_entry_types - Types of data stored in DIGEST_LIST_ENTRY + * + * Enumerates the types of data stored in DIGEST_LIST_ENTRY (nested TLV data). + */ +enum digest_list_entry_types { + FOR_EACH_DIGEST_LIST_ENTRY_TYPE(GENERATE_ENUM) +}; + +/** + * enum digest_list_entry_fields - DIGEST_LIST_ENTRY fields + * + * Enumerates the DIGEST_LIST_ENTRY fields. + */ +enum digest_list_entry_fields { + FOR_EACH_DIGEST_LIST_ENTRY_FIELD(GENERATE_ENUM) +}; + +#endif /* _UAPI_LINUX_TLV_DIGEST_LIST_H */ diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig index c7e77cf2c173..dc9ed8f0f883 100644 --- a/security/digest_cache/Kconfig +++ b/security/digest_cache/Kconfig @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 config SECURITY_DIGEST_CACHE bool "Digest_cache LSM" + select TLV_PARSER default n help This option enables an LSM maintaining a cache of digests diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile index c1452437d02f..a383b6ef2550 100644 --- a/security/digest_cache/Makefile +++ b/security/digest_cache/Makefile @@ -5,3 +5,5 @@ obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o digest_cache-y := main.o secfs.o htable.o populate.o modsig.o + +digest_cache-y += parsers/tlv.o diff --git a/security/digest_cache/parsers/parsers.h b/security/digest_cache/parsers/parsers.h new file mode 100644 index 000000000000..1bbae426ab9f --- /dev/null +++ b/security/digest_cache/parsers/parsers.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu + * + * Digest list parsers. + */ + +#include "../internal.h" + +int digest_list_parse_tlv(struct digest_cache *digest_cache, const u8 *data, + size_t data_len); diff --git a/security/digest_cache/parsers/tlv.c b/security/digest_cache/parsers/tlv.c new file mode 100644 index 000000000000..97e2c36b93a8 --- /dev/null +++ b/security/digest_cache/parsers/tlv.c @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2017-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu + * + * Parse a tlv digest list. + */ + +#define pr_fmt(fmt) "TLV DIGEST LIST: "fmt +#include +#include + +#include "parsers.h" + +#define kenter(FMT, ...) \ + pr_debug("==> %s(" FMT ")\n", __func__, ##__VA_ARGS__) +#define kleave(FMT, ...) \ + pr_debug("<== %s()" FMT "\n", __func__, ##__VA_ARGS__) + +const char *digest_list_types_str[] = { + FOR_EACH_DIGEST_LIST_TYPE(GENERATE_STRING) +}; + +const char *digest_list_fields_str[] = { + FOR_EACH_DIGEST_LIST_FIELD(GENERATE_STRING) +}; + +const char *digest_list_entry_types_str[] = { + FOR_EACH_DIGEST_LIST_ENTRY_TYPE(GENERATE_STRING) +}; + +const char *digest_list_entry_fields_str[] = { + FOR_EACH_DIGEST_LIST_ENTRY_FIELD(GENERATE_STRING) +}; + +struct tlv_callback_data { + struct digest_cache *digest_cache; + u64 parsed_data_type; + u64 parsed_num_entries; + u64 parsed_total_len; + enum hash_algo algo; +}; + +/** + * parse_digest_list_entry_digest - Parse DIGEST_LIST_ENTRY_DIGEST field + * @tlv_data: Parser callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This function parses the DIGEST_LIST_ENTRY_DIGEST field (file digest). + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int parse_digest_list_entry_digest(struct tlv_callback_data *tlv_data, + enum digest_list_entry_fields field, + const u8 *field_data, + u64 field_data_len) +{ + int ret; + + kenter(",%u,%llu", field, field_data_len); + + if (tlv_data->algo == HASH_ALGO__LAST) { + pr_debug("Digest algo not set\n"); + ret = -EBADMSG; + goto out; + } + + if (field_data_len != hash_digest_size[tlv_data->algo]) { + pr_debug("Unexpected data length %llu, expected %d\n", + field_data_len, hash_digest_size[tlv_data->algo]); + ret = -EBADMSG; + goto out; + } + + ret = digest_cache_htable_add(tlv_data->digest_cache, (u8 *)field_data, + tlv_data->algo); +out: + kleave(" = %d", ret); + return ret; +} + +/** + * parse_digest_list_entry_path - Parse DIGEST_LIST_ENTRY_PATH field + * @tlv_data: Parser callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This function handles the DIGEST_LIST_ENTRY_PATH field (file path). It + * currently does not parse the data. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int parse_digest_list_entry_path(struct tlv_callback_data *tlv_data, + enum digest_list_entry_fields field, + const u8 *field_data, + u64 field_data_len) +{ + kenter(",%u,%llu", field, field_data_len); + + kleave(" = 0"); + return 0; +} + +/** + * digest_list_entry_data_callback - DIGEST_LIST_ENTRY_DATA callback + * @callback_data: Callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This callback handles the fields of DIGEST_LIST_ENTRY_DATA (nested) data, + * and calls the appropriate parser. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int digest_list_entry_data_callback(void *callback_data, u64 field, + const u8 *field_data, + u64 field_data_len) +{ + struct tlv_callback_data *tlv_data; + int ret; + + tlv_data = (struct tlv_callback_data *)callback_data; + + switch (field) { + case DIGEST_LIST_ENTRY_DIGEST: + ret = parse_digest_list_entry_digest(tlv_data, field, + field_data, + field_data_len); + break; + case DIGEST_LIST_ENTRY_PATH: + ret = parse_digest_list_entry_path(tlv_data, field, field_data, + field_data_len); + break; + default: + pr_debug("Unhandled field %s\n", + digest_list_entry_fields_str[field]); + /* Just ignore non-relevant fields. */ + ret = 0; + break; + } + + return ret; +} + +/** + * parse_digest_list_algo - Parse DIGEST_LIST_ALGO field + * @tlv_data: Parser callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This function parses the DIGEST_LIST_ALGO field (digest algorithm). + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int parse_digest_list_algo(struct tlv_callback_data *tlv_data, + enum digest_list_fields field, + const u8 *field_data, u64 field_data_len) +{ + u64 algo; + int ret; + + kenter(",%u,%llu", field, field_data_len); + + if (field_data_len != sizeof(u64)) { + pr_debug("Unexpected data length %llu, expected %lu\n", + field_data_len, sizeof(u64)); + ret = -EBADMSG; + goto out; + } + + algo = __be64_to_cpu(*(u64 *)field_data); + + if (algo >= HASH_ALGO__LAST) { + pr_debug("Unexpected digest algo %llu\n", algo); + ret = -EBADMSG; + goto out; + } + + ret = digest_cache_htable_init(tlv_data->digest_cache, + tlv_data->parsed_num_entries, algo); + if (ret < 0) + goto out; + + tlv_data->algo = algo; + + pr_debug("Digest algo: %s\n", hash_algo_name[algo]); +out: + kleave(" = %d", ret); + return ret; +} + +/** + * parse_digest_list_entry - Parse DIGEST_LIST_ENTRY field + * @tlv_data: Parser callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This function parses the DIGEST_LIST_ENTRY field. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int parse_digest_list_entry(struct tlv_callback_data *tlv_data, + enum digest_list_fields field, + const u8 *field_data, u64 field_data_len) +{ + int ret; + + kenter(",%u,%llu", field, field_data_len); + + ret = tlv_parse(DIGEST_LIST_ENTRY_DATA, digest_list_entry_data_callback, + tlv_data, field_data, field_data_len, + digest_list_entry_types_str, DIGEST_LIST_ENTRY__LAST, + digest_list_entry_fields_str, + DIGEST_LIST_ENTRY_FIELD__LAST); + + kleave(" = %d", ret); + return ret; +} + +/** + * digest_list_file_callback - DIGEST_LIST_FILE callback + * @callback_data: Callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This callback handles the fields of DIGEST_LIST_FILE data, and calls the + * appropriate parser. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int digest_list_file_callback(void *callback_data, u64 field, + const u8 *field_data, u64 field_data_len) +{ + struct tlv_callback_data *tlv_data; + int ret; + + tlv_data = (struct tlv_callback_data *)callback_data; + + switch (field) { + case DIGEST_LIST_ALGO: + ret = parse_digest_list_algo(tlv_data, field, field_data, + field_data_len); + break; + case DIGEST_LIST_ENTRY: + ret = parse_digest_list_entry(tlv_data, field, field_data, + field_data_len); + break; + default: + pr_debug("Unhandled field %s\n", + digest_list_fields_str[field]); + /* Just ignore non-relevant fields. */ + ret = 0; + break; + } + + return ret; +} + +/** + * digest_list_parse_tlv - Parse a tlv digest list + * @digest_cache: Digest cache + * @data: Data to parse + * @data_len: Length of @data + * + * This function parses a tlv digest list. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_list_parse_tlv(struct digest_cache *digest_cache, const u8 *data, + size_t data_len) +{ + struct tlv_callback_data tlv_data = { + .digest_cache = digest_cache, + .algo = HASH_ALGO__LAST + }; + int ret; + + ret = tlv_parse_hdr(&data, &data_len, &tlv_data.parsed_data_type, + &tlv_data.parsed_num_entries, + &tlv_data.parsed_total_len, + digest_list_types_str, DIGEST_LIST__LAST); + if (ret < 0) + return ret; + + if (tlv_data.parsed_data_type != DIGEST_LIST_FILE) + return 0; + + return tlv_parse_data(digest_list_file_callback, &tlv_data, + tlv_data.parsed_num_entries, data, data_len, + digest_list_fields_str, DIGEST_LIST_FIELD__LAST); +} diff --git a/security/digest_cache/populate.c b/security/digest_cache/populate.c index 415e638f587b..13645ec4bb2b 100644 --- a/security/digest_cache/populate.c +++ b/security/digest_cache/populate.c @@ -12,6 +12,7 @@ #include #include "internal.h" +#include "parsers/parsers.h" /** * digest_cache_parse_digest_list - Parse a digest list @@ -65,6 +66,9 @@ static int digest_cache_parse_digest_list(struct digest_cache *digest_cache, filename[0] ? "/" : "", filename, (int)(next_sep - format), format, data_len); + if (!strncmp(format, "tlv-", 4)) + ret = digest_list_parse_tlv(digest_cache, data, data_len); + return ret; } From patchwork Fri Feb 9 14:09:12 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Roberto Sassu X-Patchwork-Id: 771668 Received: from frasgout12.his.huawei.com (frasgout12.his.huawei.com [14.137.139.154]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6FA916A320; Fri, 9 Feb 2024 14:11:46 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=14.137.139.154 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707487908; cv=none; b=WxVeKEXtiJzSbvPa8M7haDexMUCOg7+TYOLsrWmBfmU75I4cnwWuRl6VQ4s+b30KxidwMRlx/8RyaqBUU6OQV9U/BPotDVemisPbLIUZvAn4eKVjKhd23+Hcr3BlHfUNg1NNbZ+UH7G0+xpYCuHqzElVpcHkrVZu73PyYh4Zo4Q= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707487908; c=relaxed/simple; bh=WLMZH5azDphMOQPmm/BFk1TN38+Xep2BvMxGYFPSTVc=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=KgaGC6GnCqCrAnC+vq1IDjVuUZ4HzdB9UNrjNRskP7hYLneveKiYHOhqXAVBscuHB2d73sf1kgwQ8wngGXYGF9BCkIUxdcyawMsZze3yqnPZRPHjrJxZ/vgi3Keg9/BJKC/mgtF3UDOTm+JMMLCSZ/tdqfyGsbYs1GtKv96Hez4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com; spf=pass smtp.mailfrom=huaweicloud.com; arc=none smtp.client-ip=14.137.139.154 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=huaweicloud.com Received: from mail.maildlp.com (unknown [172.18.186.51]) by frasgout12.his.huawei.com (SkyGuard) with ESMTP id 4TWZzx0zJZz9xFmM; Fri, 9 Feb 2024 21:52:45 +0800 (CST) Received: from mail02.huawei.com (unknown [7.182.16.47]) by mail.maildlp.com (Postfix) with ESMTP id B15071405F9; Fri, 9 Feb 2024 22:11:33 +0800 (CST) Received: from huaweicloud.com (unknown [10.204.63.22]) by APP1 (Coremail) with SMTP id LxC2BwAHshoZMsZlvXMuAg--.65105S10; Fri, 09 Feb 2024 15:11:33 +0100 (CET) From: Roberto Sassu To: corbet@lwn.net, paul@paul-moore.com, jmorris@namei.org, serge@hallyn.com, shuah@kernel.org, mcoquelin.stm32@gmail.com, alexandre.torgue@foss.st.com, mic@digikod.net Cc: linux-security-module@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, bpf@vger.kernel.org, zohar@linux.ibm.com, dmitry.kasatkin@gmail.com, linux-integrity@vger.kernel.org, wufan@linux.microsoft.com, pbrobinson@gmail.com, zbyszek@in.waw.pl, hch@lst.de, mjg59@srcf.ucam.org, pmatilai@redhat.com, jannh@google.com, dhowells@redhat.com, jikos@kernel.org, mkoutny@suse.com, ppavlu@suse.com, petr.vorel@gmail.com, petrtesarik@huaweicloud.com, Roberto Sassu Subject: [PATCH v3 08/13] digest_cache: Add management of verification data Date: Fri, 9 Feb 2024 15:09:12 +0100 Message-Id: <20240209140917.846878-9-roberto.sassu@huaweicloud.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> References: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> Precedence: bulk X-Mailing-List: linux-kselftest@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-CM-TRANSID: LxC2BwAHshoZMsZlvXMuAg--.65105S10 X-Coremail-Antispam: 1UD129KBjvJXoW3uFWkJw1DAF4DGF4rCFy8Zrb_yoWkuF4Up3 s29F1Dtr4rZr13Jw17AF129r1rtFZ5tF47Jw48ur13ZF47Xr1jy3W8A34UZry5JrW8Wa17 tr47Kw1Uur1DXaDanT9S1TB71UUUUU7qnTZGkaVYY2UrUUUUjbIjqfuFe4nvWSU5nxnvy2 9KBjDU0xBIdaVrnRJUUUmS14x267AKxVWrJVCq3wAFc2x0x2IEx4CE42xK8VAvwI8IcIk0 rVWrJVCq3wAFIxvE14AKwVWUJVWUGwA2048vs2IY020E87I2jVAFwI0_JF0E3s1l82xGYI kIc2x26xkF7I0E14v26ryj6s0DM28lY4IEw2IIxxk0rwA2F7IY1VAKz4vEj48ve4kI8wA2 z4x0Y4vE2Ix0cI8IcVAFwI0_Gr0_Xr1l84ACjcxK6xIIjxv20xvEc7CjxVAFwI0_Gr1j6F 4UJwA2z4x0Y4vEx4A2jsIE14v26r4j6F4UM28EF7xvwVC2z280aVCY1x0267AKxVWxJr0_ GcWle2I262IYc4CY6c8Ij28IcVAaY2xG8wAqx4xG64xvF2IEw4CE5I8CrVC2j2WlYx0E2I x0cI8IcVAFwI0_Jr0_Jr4lYx0Ex4A2jsIE14v26r1j6r4UMcvjeVCFs4IE7xkEbVWUJVW8 JwACjcxG0xvY0x0EwIxGrwACjI8F5VA0II8E6IAqYI8I648v4I1lFIxGxcIEc7CjxVA2Y2 ka0xkIwI1lc7CjxVAaw2AFwI0_GFv_Wryl42xK82IYc2Ij64vIr41l4I8I3I0E4IkC6x0Y z7v_Jr0_Gr1lx2IqxVAqx4xG67AKxVWUJVWUGwC20s026x8GjcxK67AKxVWUGVWUWwC2zV AF1VAY17CE14v26r4a6rW5MIIYrxkI7VAKI48JMIIF0xvE2Ix0cI8IcVAFwI0_JFI_Gr1l IxAIcVC0I7IYx2IY6xkF7I0E14v26r4UJVWxJr1lIxAIcVCF04k26cxKx2IYs7xG6r1j6r 1xMIIF0xvEx4A2jsIE14v26r4j6F4UMIIF0xvEx4A2jsIEc7CjxVAFwI0_Cr1j6rxdYxBI daVFxhVjvjDU0xZFpf9x0pRQJ5wUUUUU= X-CM-SenderInfo: purev21wro2thvvxqx5xdzvxpfor3voofrz/1tbiAgAIBF1jj5Y4fwAEsq From: Roberto Sassu The digest_cache LSM can support other LSMs in their decisions of granting access to file content and metadata. However, the information alone about whether a digest was found in a digest cache might not be sufficient, because for example those LSMs wouldn't know whether the digest cache itself was created from authentic data. Introduce digest_cache_verif_set() to let the same LSMs (or a chosen integrity provider) evaluate the digest list being read during the creation of the digest cache, by implementing the kernel_post_read_file LSM hook, and let them attach their verification data to that digest cache. Reserve space in the file descriptor security blob for the digest cache pointer. Also introduce digest_cache_to_file_sec() to set that pointer before calling kernel_read_file() in digest_cache_populate(), and digest_cache_from_file_sec() to retrieve the pointer back from the file descriptor passed by LSMs with digest_cache_verif_set(). Multiple providers are supported, in the event there are multiple integrity LSMs active. Each provider should also provide an unique verifier ID as an argument to digest_cache_verif_set(), so that verification data can be distinguished. A caller of digest_cache_get() can retrieve back the verification data by calling digest_cache_verif_get() and passing a digest cache pointer and the desired verifier ID. Since directory digest caches are not populated themselves, LSMs have to do a lookup first to get the digest cache containing the digest, call digest_cache_from_found_t() to convert the returned digest_cache_found_t type to a digest cache pointer, and pass that to digest_cache_verif_get(). Signed-off-by: Roberto Sassu --- include/linux/digest_cache.h | 17 +++++ security/digest_cache/Makefile | 2 +- security/digest_cache/internal.h | 40 +++++++++++ security/digest_cache/main.c | 4 ++ security/digest_cache/populate.c | 2 + security/digest_cache/verif.c | 116 +++++++++++++++++++++++++++++++ 6 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 security/digest_cache/verif.c diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h index 4872700ac205..9db8128513ca 100644 --- a/include/linux/digest_cache.h +++ b/include/linux/digest_cache.h @@ -44,6 +44,10 @@ void digest_cache_put(struct digest_cache *digest_cache); digest_cache_found_t digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache, u8 *digest, enum hash_algo algo); +int digest_cache_verif_set(struct file *file, const char *verif_id, void *data, + size_t size); +void *digest_cache_verif_get(struct digest_cache *digest_cache, + const char *verif_id); #else static inline struct digest_cache *digest_cache_get(struct dentry *dentry) @@ -62,5 +66,18 @@ digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache, return 0UL; } +static inline int digest_cache_verif_set(struct file *file, + const char *verif_id, void *data, + size_t size) +{ + return -EOPNOTSUPP; +} + +static inline void *digest_cache_verif_get(struct digest_cache *digest_cache, + const char *verif_id) +{ + return NULL; +} + #endif /* CONFIG_SECURITY_DIGEST_CACHE */ #endif /* _LINUX_DIGEST_CACHE_H */ diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile index eca4076497e6..37a473c7bc28 100644 --- a/security/digest_cache/Makefile +++ b/security/digest_cache/Makefile @@ -4,7 +4,7 @@ obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o -digest_cache-y := main.o secfs.o htable.o populate.o modsig.o +digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o digest_cache-y += parsers/tlv.o digest_cache-y += parsers/rpm.o diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h index 0d9c01dd9bc8..a266925a6cce 100644 --- a/security/digest_cache/internal.h +++ b/security/digest_cache/internal.h @@ -17,6 +17,21 @@ #define INIT_IN_PROGRESS 0 /* Digest cache being initialized. */ #define INVALID 1 /* Digest cache marked as invalid. */ +/** + * struct digest_cache_verif + * @list: Linked list + * @verif_id: Identifier of who verified the digest list + * @data: Opaque data set by the digest list verifier + * + * This structure contains opaque data containing the result of verification + * of the digest list by a verifier. + */ +struct digest_cache_verif { + struct list_head list; + char *verif_id; + void *data; +}; + /** * struct read_work - Structure to schedule reading a digest list * @work: Work structure @@ -71,6 +86,8 @@ struct htable { * @ref_count: Number of references to the digest cache * @path_str: Path of the digest list the digest cache was created from * @flags: Control flags + * @verif_data: Verification data regarding the digest list + * @verif_data_lock: Protect concurrent verification data additions * * This structure represents a cache of digests extracted from a digest list. */ @@ -79,6 +96,8 @@ struct digest_cache { atomic_t ref_count; char *path_str; unsigned long flags; + struct list_head verif_data; + spinlock_t verif_data_lock; }; /** @@ -130,6 +149,24 @@ digest_cache_unref(struct digest_cache *digest_cache) return (ref_is_zero) ? digest_cache : NULL; } +static inline void digest_cache_to_file_sec(const struct file *file, + struct digest_cache *digest_cache) +{ + struct digest_cache **digest_cache_sec; + + digest_cache_sec = file->f_security + digest_cache_blob_sizes.lbs_file; + *digest_cache_sec = digest_cache; +} + +static inline struct digest_cache * +digest_cache_from_file_sec(const struct file *file) +{ + struct digest_cache **digest_cache_sec; + + digest_cache_sec = file->f_security + digest_cache_blob_sizes.lbs_file; + return *digest_cache_sec; +} + /* main.c */ struct digest_cache *digest_cache_create(struct dentry *dentry, struct path *digest_list_path, @@ -153,4 +190,7 @@ int digest_cache_populate(struct digest_cache *digest_cache, /* modsig.c */ size_t digest_cache_strip_modsig(__u8 *data, size_t data_len); +/* verif.c */ +void digest_cache_verif_free(struct digest_cache *digest_cache); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c index 8e9ab99081c3..d726832e5913 100644 --- a/security/digest_cache/main.c +++ b/security/digest_cache/main.c @@ -49,6 +49,8 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str, atomic_set(&digest_cache->ref_count, 1); digest_cache->flags = 0UL; INIT_LIST_HEAD(&digest_cache->htables); + INIT_LIST_HEAD(&digest_cache->verif_data); + spin_lock_init(&digest_cache->verif_data_lock); pr_debug("New digest cache %s (ref count: %d)\n", digest_cache->path_str, atomic_read(&digest_cache->ref_count)); @@ -65,6 +67,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str, static void digest_cache_free(struct digest_cache *digest_cache) { digest_cache_htable_free(digest_cache); + digest_cache_verif_free(digest_cache); pr_debug("Freed digest cache %s\n", digest_cache->path_str); kfree(digest_cache->path_str); @@ -329,6 +332,7 @@ EXPORT_SYMBOL_GPL(digest_cache_put); struct lsm_blob_sizes digest_cache_blob_sizes __ro_after_init = { .lbs_inode = sizeof(struct digest_cache_security), + .lbs_file = sizeof(struct digest_cache *), }; /** diff --git a/security/digest_cache/populate.c b/security/digest_cache/populate.c index 1770c8385017..9c2fc2295310 100644 --- a/security/digest_cache/populate.c +++ b/security/digest_cache/populate.c @@ -123,6 +123,8 @@ int digest_cache_populate(struct digest_cache *digest_cache, return PTR_ERR(file); } + digest_cache_to_file_sec(file, digest_cache); + w.data = NULL; w.file = file; INIT_WORK_ONSTACK(&w.work, digest_cache_read_digest_list); diff --git a/security/digest_cache/verif.c b/security/digest_cache/verif.c new file mode 100644 index 000000000000..dd480bdc805a --- /dev/null +++ b/security/digest_cache/verif.c @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu + * + * Manage verification data regarding digest lists. + */ + +#define pr_fmt(fmt) "DIGEST CACHE: "fmt +#include "internal.h" + +/** + * free_verif - Free a digest_cache_verif structure + * @verif: digest_cache_verif structure + * + * Free the space allocated for a digest_cache_verif structure. + */ +static void free_verif(struct digest_cache_verif *verif) +{ + kfree(verif->data); + kfree(verif->verif_id); + kfree(verif); +} + +/** + * digest_cache_verif_set - Set digest cache verification data + * @file: File descriptor of the digest list being read to populate digest cache + * @verif_id: Verifier ID + * @data: Verification data (opaque) + * @size: Size of @data + * + * This function lets a verifier supply verification data about a digest list + * being read to populate the digest cache. + * + * Return: Zero on success, -ENOMEM if out of memory. + */ +int digest_cache_verif_set(struct file *file, const char *verif_id, void *data, + size_t size) +{ + struct digest_cache *digest_cache = digest_cache_from_file_sec(file); + struct digest_cache_verif *new_verif; + + /* + * All allocations must be atomic (non-sleepable) since kprobe does not + * allow otherwise (kprobe is needed for testing). + */ + new_verif = kzalloc(sizeof(*new_verif), GFP_ATOMIC); + if (!new_verif) + return -ENOMEM; + + new_verif->verif_id = kstrdup(verif_id, GFP_ATOMIC); + if (!new_verif->verif_id) { + free_verif(new_verif); + return -ENOMEM; + } + + new_verif->data = kmemdup(data, size, GFP_ATOMIC); + if (!new_verif->data) { + free_verif(new_verif); + return -ENOMEM; + } + + spin_lock(&digest_cache->verif_data_lock); + list_add_tail_rcu(&new_verif->list, &digest_cache->verif_data); + spin_unlock(&digest_cache->verif_data_lock); + return 0; +} +EXPORT_SYMBOL_GPL(digest_cache_verif_set); + +/** + * digest_cache_verif_get - Get digest cache verification data + * @digest_cache: Digest cache + * @verif_id: Verifier ID + * + * This function returns the verification data previously set by a verifier + * with digest_cache_verif_set(). + * + * Return: Verification data if found, NULL otherwise. + */ +void *digest_cache_verif_get(struct digest_cache *digest_cache, + const char *verif_id) +{ + struct digest_cache_verif *verif; + void *verif_data = NULL; + + rcu_read_lock(); + list_for_each_entry_rcu(verif, &digest_cache->verif_data, list) { + if (!strcmp(verif->verif_id, verif_id)) { + verif_data = verif->data; + break; + } + } + rcu_read_unlock(); + + return verif_data; +} +EXPORT_SYMBOL_GPL(digest_cache_verif_get); + +/** + * digest_cache_verif_free - Free all digest_cache_verif structures + * @digest_cache: Digest cache + * + * This function frees the space allocated for all digest_cache_verif + * structures in the digest cache. + */ +void digest_cache_verif_free(struct digest_cache *digest_cache) +{ + struct digest_cache_verif *p, *q; + + /* No need to lock, called when nobody else has a digest cache ref. */ + list_for_each_entry_safe(p, q, &digest_cache->verif_data, list) { + list_del(&p->list); + free_verif(p); + } +} From patchwork Fri Feb 9 14:09:15 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Roberto Sassu X-Patchwork-Id: 771666 Received: from frasgout13.his.huawei.com (frasgout13.his.huawei.com [14.137.139.46]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D4FF469D23; Fri, 9 Feb 2024 14:13:30 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=14.137.139.46 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707488013; cv=none; b=RW8SbizIK24ek8dkJtDTakJ/7F8qOptM8UEuQQKMUsD2+uV7hMI9NpOw1AKcoQOaVvOBzjNe0zUkLpnPwbEl4swbFMlN61rY81yxH3hcrYeMSobEJos8l+URSfzmyIqVB4Jo8rUBR7K9DHbgVhsvacm/XKkc1A5f6bEmS3uNGh4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707488013; c=relaxed/simple; bh=sxG1qj6ZRW9AqSqo0SFNqKoBtRwSYTjvRUbjZeoJmlc=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=RIfYBhN86usqIxzCm/FvHquTXNtYwyxqs7P6efmjyHOS0GgyQeD/tcsD9lA9PBSuRqMkTSQzGhVZWW6JLKb5b8AOe4lMUhusprHNI6rhRnbLtorXkEwmdHhkQ5FRvyHUPf3OVoqylgoEJBvoYBMsjECZukuOZvpcY5duHoA5AJk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com; spf=pass smtp.mailfrom=huaweicloud.com; arc=none smtp.client-ip=14.137.139.46 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=huaweicloud.com Received: from mail.maildlp.com (unknown [172.18.186.29]) by frasgout13.his.huawei.com (SkyGuard) with ESMTP id 4TWb6M6FHHz9yB7b; Fri, 9 Feb 2024 21:58:19 +0800 (CST) Received: from mail02.huawei.com (unknown [7.182.16.27]) by mail.maildlp.com (Postfix) with ESMTP id 5E2F11406BF; Fri, 9 Feb 2024 22:13:18 +0800 (CST) Received: from huaweicloud.com (unknown [10.204.63.22]) by APP2 (Coremail) with SMTP id GxC2BwBnoCThMsZloMgpAg--.31110S3; Fri, 09 Feb 2024 15:13:17 +0100 (CET) From: Roberto Sassu To: corbet@lwn.net, paul@paul-moore.com, jmorris@namei.org, serge@hallyn.com, shuah@kernel.org, mcoquelin.stm32@gmail.com, alexandre.torgue@foss.st.com, mic@digikod.net Cc: linux-security-module@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, bpf@vger.kernel.org, zohar@linux.ibm.com, dmitry.kasatkin@gmail.com, linux-integrity@vger.kernel.org, wufan@linux.microsoft.com, pbrobinson@gmail.com, zbyszek@in.waw.pl, hch@lst.de, mjg59@srcf.ucam.org, pmatilai@redhat.com, jannh@google.com, dhowells@redhat.com, jikos@kernel.org, mkoutny@suse.com, ppavlu@suse.com, petr.vorel@gmail.com, petrtesarik@huaweicloud.com, Roberto Sassu Subject: [PATCH v3 11/13] digest_cache: Reset digest cache on file/directory change Date: Fri, 9 Feb 2024 15:09:15 +0100 Message-Id: <20240209140917.846878-12-roberto.sassu@huaweicloud.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> References: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> Precedence: bulk X-Mailing-List: linux-kselftest@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-CM-TRANSID: GxC2BwBnoCThMsZloMgpAg--.31110S3 X-Coremail-Antispam: 1UD129KBjvAXoW3ur1rZFyfXr1UWryxXF4UCFg_yoW8JFWfCo ZYvFsrXw18WFy5ZFs5CF17Aa9ru3yFgw1xArykGFW5ZF10vryUG3ZrC3WDJFy5Jr18Gr97 A34kX3y8JFWUtr97n29KB7ZKAUJUUUU8529EdanIXcx71UUUUU7v73VFW2AGmfu7bjvjm3 AaLaJ3UjIYCTnIWjp_UUUOx7AC8VAFwI0_Wr0E3s1l1xkIjI8I6I8E6xAIw20EY4v20xva j40_Wr0E3s1l1IIY67AEw4v_Jr0_Jr4l82xGYIkIc2x26280x7IE14v26r18M28IrcIa0x kI8VCY1x0267AKxVW5JVCq3wA2ocxC64kIII0Yj41l84x0c7CEw4AK67xGY2AK021l84AC jcxK6xIIjxv20xvE14v26r4j6ryUM28EF7xvwVC0I7IYx2IY6xkF7I0E14v26r4UJVWxJr 1l84ACjcxK6I8E87Iv67AKxVW8JVWxJwA2z4x0Y4vEx4A2jsIEc7CjxVAFwI0_Cr1j6rxd M2AIxVAIcxkEcVAq07x20xvEncxIr21l5I8CrVACY4xI64kE6c02F40Ex7xfMcIj6xIIjx v20xvE14v26r106r15McIj6I8E87Iv67AKxVWUJVW8JwAm72CE4IkC6x0Yz7v_Jr0_Gr1l F7xvr2IYc2Ij64vIr41lF7I21c0EjII2zVCS5cI20VAGYxC7M4IIrI8v6xkF7I0E8cxan2 IY04v7MxkF7I0En4kS14v26r4a6rW5MxAIw28IcxkI7VAKI48JMxC20s026xCaFVCjc4AY 6r1j6r4UMI8I3I0E5I8CrVAFwI0_Jr0_Jr4lx2IqxVCjr7xvwVAFwI0_JrI_JrWlx4CE17 CEb7AF67AKxVW8ZVWrXwCIc40Y0x0EwIxGrwCI42IY6xIIjxv20xvE14v26r4j6ryUMIIF 0xvE2Ix0cI8IcVCY1x0267AKxVW8Jr0_Cr1UMIIF0xvE42xK8VAvwI8IcIk0rVWUJVWUCw CI42IY6I8E87Iv67AKxVW8JVWxJwCI42IY6I8E87Iv6xkF7I0E14v26F4UJVW0obIYCTnI WIevJa73UjIFyTuYvjfUUYL9UUUUU X-CM-SenderInfo: purev21wro2thvvxqx5xdzvxpfor3voofrz/1tbiAgAIBF1jj5Y4hgAAsX From: Roberto Sassu Register five new LSM hooks, file_open, path_truncate, file_release, inode_unlink and inode_rename, to monitor digest lists/directory modifications. If an action affects a digest list or the parent directory, the new LSM hook implementations call digest_cache_reset() to set the RESET bit on the digest cache. This will cause next calls to digest_cache_get() and digest_cache_create() to respectively put and clear dig_user and dig_owner, and request a new digest cache. That does not affect other users of the old digest cache, since that one remains valid as long as the reference count is greater than zero. However, they can explicitly call the new function digest_cache_was_reset(), to check if the RESET bit was set on the digest cache reference they hold. Recreating a file digest cache means reading the digest list again and extracting the digests. Recreating a directory digest cache, instead, does not mean recreating the digest cache for directory entries, since those digest caches are likely already stored in the inode security blob. It would happen however for new files. File digest cache reset is done on file_open, when a digest list is opened for write, path_truncate, when a digest list is truncated (there is no inode_truncate, file_truncate does not catch operations through the truncate() system call), inode_unlink, when a digest list is removed, and inode_rename when a digest list is renamed. Directory digest cache reset is done on file_release, when a digest list is written in the digest list directory, on inode_unlink, when a digest list is deleted from that directory, and finally on inode_rename, when a digest list is moved to/from that directory. With the exception of file_release, which will always be executed (cannot be denied), the other LSM hooks are not optimal, since the digest_cache LSM does not know whether or not the operation will be allowed also by other LSMs. If the operation is denied, the digest_cache LSM would do an unnecessary reset. Signed-off-by: Roberto Sassu --- include/linux/digest_cache.h | 6 ++ security/digest_cache/Kconfig | 1 + security/digest_cache/Makefile | 3 +- security/digest_cache/internal.h | 9 ++ security/digest_cache/main.c | 15 +++ security/digest_cache/reset.c | 168 +++++++++++++++++++++++++++++++ 6 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 security/digest_cache/reset.c diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h index 9db8128513ca..db3052c71b7a 100644 --- a/include/linux/digest_cache.h +++ b/include/linux/digest_cache.h @@ -48,6 +48,7 @@ int digest_cache_verif_set(struct file *file, const char *verif_id, void *data, size_t size); void *digest_cache_verif_get(struct digest_cache *digest_cache, const char *verif_id); +bool digest_cache_was_reset(struct digest_cache *digest_cache); #else static inline struct digest_cache *digest_cache_get(struct dentry *dentry) @@ -79,5 +80,10 @@ static inline void *digest_cache_verif_get(struct digest_cache *digest_cache, return NULL; } +static inline bool digest_cache_was_reset(struct digest_cache *digest_cache) +{ + return false; +} + #endif /* CONFIG_SECURITY_DIGEST_CACHE */ #endif /* _LINUX_DIGEST_CACHE_H */ diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig index dc9ed8f0f883..cd397eb64140 100644 --- a/security/digest_cache/Kconfig +++ b/security/digest_cache/Kconfig @@ -2,6 +2,7 @@ config SECURITY_DIGEST_CACHE bool "Digest_cache LSM" select TLV_PARSER + select SECURITY_PATH default n help This option enables an LSM maintaining a cache of digests diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile index e417da0383ab..3d5e600a2c45 100644 --- a/security/digest_cache/Makefile +++ b/security/digest_cache/Makefile @@ -4,7 +4,8 @@ obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o -digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o +digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o \ + reset.o digest_cache-y += parsers/tlv.o digest_cache-y += parsers/rpm.o diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h index bbef5ab83107..0517e648fbb7 100644 --- a/security/digest_cache/internal.h +++ b/security/digest_cache/internal.h @@ -18,6 +18,7 @@ #define INVALID 1 /* Digest cache marked as invalid. */ #define IS_DIR 2 /* Digest cache created from dir. */ #define DIR_PREFETCH 3 /* Prefetching requested for dir. */ +#define RESET 4 /* Digest cache to be recreated. */ /** * struct readdir_callback - Structure to store information for dir iteration @@ -247,4 +248,12 @@ digest_cache_dir_lookup_filename(struct dentry *dentry, char *filename); void digest_cache_dir_free(struct digest_cache *digest_cache); +/* reset.c */ +int digest_cache_file_open(struct file *file); +int digest_cache_path_truncate(const struct path *path); +void digest_cache_file_release(struct file *file); +int digest_cache_inode_unlink(struct inode *dir, struct dentry *dentry); +int digest_cache_inode_rename(struct inode *old_dir, struct dentry *old_dentry, + struct inode *new_dir, struct dentry *new_dentry); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c index e6598f81074a..b192628e30db 100644 --- a/security/digest_cache/main.c +++ b/security/digest_cache/main.c @@ -162,6 +162,11 @@ struct digest_cache *digest_cache_create(struct dentry *dentry, /* Serialize check and assignment of dig_owner. */ mutex_lock(&dig_sec->dig_owner_mutex); + if (dig_sec->dig_owner && test_bit(RESET, &dig_sec->dig_owner->flags)) { + digest_cache_put(dig_sec->dig_owner); + dig_sec->dig_owner = NULL; + } + if (dig_sec->dig_owner) { /* Increment ref. count for reference returned to the caller. */ digest_cache = digest_cache_ref(dig_sec->dig_owner); @@ -394,6 +399,11 @@ struct digest_cache *digest_cache_get(struct dentry *dentry) /* Serialize accesses to inode for which the digest cache is used. */ mutex_lock(&dig_sec->dig_user_mutex); + if (dig_sec->dig_user && test_bit(RESET, &dig_sec->dig_user->flags)) { + digest_cache_put(dig_sec->dig_user); + dig_sec->dig_user = NULL; + } + if (!dig_sec->dig_user) { down_read(&default_path_sem); /* Consume extra reference from digest_cache_create(). */ @@ -482,6 +492,11 @@ static void digest_cache_inode_free_security(struct inode *inode) static struct security_hook_list digest_cache_hooks[] __ro_after_init = { LSM_HOOK_INIT(inode_alloc_security, digest_cache_inode_alloc_security), LSM_HOOK_INIT(inode_free_security, digest_cache_inode_free_security), + LSM_HOOK_INIT(file_open, digest_cache_file_open), + LSM_HOOK_INIT(path_truncate, digest_cache_path_truncate), + LSM_HOOK_INIT(file_release, digest_cache_file_release), + LSM_HOOK_INIT(inode_unlink, digest_cache_inode_unlink), + LSM_HOOK_INIT(inode_rename, digest_cache_inode_rename), }; /** diff --git a/security/digest_cache/reset.c b/security/digest_cache/reset.c new file mode 100644 index 000000000000..b4968ac993f0 --- /dev/null +++ b/security/digest_cache/reset.c @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu + * + * Reset digest cache on digest lists/directory modifications. + */ + +#define pr_fmt(fmt) "DIGEST CACHE: "fmt +#include "internal.h" + +/** + * digest_cache_was_reset - Report whether or not the digest cache was reset + * @digest_cache: Digest cache + * + * This function reports whether or not the RESET bit was set in the digest + * cache. + * + * It is meant to be used by digest_cache LSM users holding a reference of a + * digest cache, which might need to take additional actions depending on + * whether or not that digest cache was reset. + * + * Return: True if the digest cache was reset, false otherwise. + */ +bool digest_cache_was_reset(struct digest_cache *digest_cache) +{ + return test_bit(RESET, &digest_cache->flags); +} +EXPORT_SYMBOL_GPL(digest_cache_was_reset); + +/** + * digest_cache_reset - Reset the digest cache + * @inode: Inode of the digest list/directory containing the digest list + * @reason: Reason for reset + * + * This function sets the RESET bit in the digest cache, so that + * digest_cache_get() and digest_cache_create() respectively release and clear + * dig_user and dig_owner in the inode security blob. This causes new callers + * of digest_cache_get() to get a new digest cache. + */ +static void digest_cache_reset(struct inode *inode, const char *reason) +{ + struct digest_cache_security *dig_sec; + + dig_sec = digest_cache_get_security(inode); + if (unlikely(!dig_sec)) + return; + + mutex_lock(&dig_sec->dig_owner_mutex); + if (dig_sec->dig_owner) { + pr_debug("Resetting %s, reason: %s\n", + dig_sec->dig_owner->path_str, reason); + set_bit(RESET, &dig_sec->dig_owner->flags); + } + mutex_unlock(&dig_sec->dig_owner_mutex); +} + +/** + * digest_cache_file_open - A file is being opened + * @file: File descriptor + * + * This function is called when a file is opened. If the inode is a digest list + * and is opened for write, it resets the inode dig_owner, to force rebuilding + * the digest cache. + * + * Return: Zero. + */ +int digest_cache_file_open(struct file *file) +{ + if (!S_ISREG(file_inode(file)->i_mode) || !(file->f_mode & FMODE_WRITE)) + return 0; + + digest_cache_reset(file_inode(file), "file_open_write"); + return 0; +} + +/** + * digest_cache_path_truncate - A file is being truncated + * @path: File path + * + * This function is called when a file is being truncated. If the inode is a + * digest list, it resets the inode dig_owner, to force rebuilding the digest + * cache. + * + * Return: Zero. + */ +int digest_cache_path_truncate(const struct path *path) +{ + struct inode *inode = d_backing_inode(path->dentry); + + if (!S_ISREG(inode->i_mode)) + return 0; + + digest_cache_reset(inode, "file_truncate"); + return 0; +} + +/** + * digest_cache_file_release - Last reference of a file desc is being released + * @file: File descriptor + * + * This function is called when the last reference of a file descriptor is + * being released. If the parent inode is the digest list directory, the inode + * is a regular file and was opened for write, it resets the inode dig_owner, + * to force rebuilding the digest cache. + */ +void digest_cache_file_release(struct file *file) +{ + struct inode *dir = d_backing_inode(file_dentry(file)->d_parent); + + if (!S_ISREG(file_inode(file)->i_mode) || !(file->f_mode & FMODE_WRITE)) + return; + + digest_cache_reset(dir, "dir_file_release"); +} + +/** + * digest_cache_inode_unlink - An inode is being removed + * @dir: Inode of the affected directory + * @dentry: Dentry of the inode being removed + * + * This function is called when an existing inode is being removed. If the + * inode is a digest list, or the parent inode is the digest list directory and + * the inode is a regular file, it resets the affected inode dig_owner, to force + * rebuilding the digest cache. + * + * Return: Zero. + */ +int digest_cache_inode_unlink(struct inode *dir, struct dentry *dentry) +{ + struct inode *inode = d_backing_inode(dentry); + + if (!S_ISREG(inode->i_mode)) + return 0; + + digest_cache_reset(inode, "file_unlink"); + digest_cache_reset(dir, "dir_unlink"); + return 0; +} + +/** + * digest_cache_inode_rename - An inode is being renamed + * @old_dir: Inode of the directory containing the inode being renamed + * @old_dentry: Dentry of the inode being renamed + * @new_dir: Directory where the inode will be placed into + * @new_dentry: Dentry of the inode after being renamed + * + * This function is called when an existing inode is being moved from a + * directory to another (rename). If the inode is a digest list, or that inode + * is moved from/to the digest list directory, it resets the affected inode + * dig_owner, to force rebuilding the digest cache. + * + * Return: Zero. + */ +int digest_cache_inode_rename(struct inode *old_dir, struct dentry *old_dentry, + struct inode *new_dir, struct dentry *new_dentry) +{ + struct inode *old_inode = d_backing_inode(old_dentry); + + if (!S_ISREG(old_inode->i_mode)) + return 0; + + digest_cache_reset(old_inode, "file_rename"); + digest_cache_reset(old_dir, "dir_rename_from"); + digest_cache_reset(new_dir, "dir_rename_to"); + return 0; +} From patchwork Fri Feb 9 14:09:17 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Roberto Sassu X-Patchwork-Id: 771665 Received: from frasgout13.his.huawei.com (frasgout13.his.huawei.com [14.137.139.46]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9F98169D21; Fri, 9 Feb 2024 14:14:00 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=14.137.139.46 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707488043; cv=none; b=PhyDUwi3OB0dKnNTph/itptVCJC0KRwsUMqCL/oPSxpJ/C9GRqiGcDkAVi/ZFHdkDlJoBFIRcPBfaJ0BwkBCw/xRmszDglvOMWLkS43961b4vi/PmLugni4XE5yXdpytK/LVfquP+flHxCb8PkEiI5lCCeEDxKl0nlad42vfy5o= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1707488043; c=relaxed/simple; bh=6wqzNgp62owJUlql6tnI4hRFRErRJ9xngX6jLxJn+mA=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=tHd6UxaEA5q3zRXZ4HNQIzTl1lQrVBVG+0qWkWxv02IJeXQT1yTwZPJLUqDAm87rufVgM6wHqPOvifRWZVgiIMV+C/kXcrSPEjZdui6NDodnaqrth1S/aBkbFCSn50eKuGwrjgftd/FTN7Uw5VWvXCPQq2DFGSkxZY2GvMJrlrA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com; spf=pass smtp.mailfrom=huaweicloud.com; arc=none smtp.client-ip=14.137.139.46 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=huaweicloud.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=huaweicloud.com Received: from mail.maildlp.com (unknown [172.18.186.29]) by frasgout13.his.huawei.com (SkyGuard) with ESMTP id 4TWb6x5W38z9yBkR; Fri, 9 Feb 2024 21:58:49 +0800 (CST) Received: from mail02.huawei.com (unknown [7.182.16.27]) by mail.maildlp.com (Postfix) with ESMTP id 34B15140FB5; Fri, 9 Feb 2024 22:13:47 +0800 (CST) Received: from huaweicloud.com (unknown [10.204.63.22]) by APP2 (Coremail) with SMTP id GxC2BwBnoCThMsZloMgpAg--.31110S5; Fri, 09 Feb 2024 15:13:46 +0100 (CET) From: Roberto Sassu To: corbet@lwn.net, paul@paul-moore.com, jmorris@namei.org, serge@hallyn.com, shuah@kernel.org, mcoquelin.stm32@gmail.com, alexandre.torgue@foss.st.com, mic@digikod.net Cc: linux-security-module@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, bpf@vger.kernel.org, zohar@linux.ibm.com, dmitry.kasatkin@gmail.com, linux-integrity@vger.kernel.org, wufan@linux.microsoft.com, pbrobinson@gmail.com, zbyszek@in.waw.pl, hch@lst.de, mjg59@srcf.ucam.org, pmatilai@redhat.com, jannh@google.com, dhowells@redhat.com, jikos@kernel.org, mkoutny@suse.com, ppavlu@suse.com, petr.vorel@gmail.com, petrtesarik@huaweicloud.com, Roberto Sassu Subject: [PATCH v3 13/13] docs: Add documentation of the digest_cache LSM Date: Fri, 9 Feb 2024 15:09:17 +0100 Message-Id: <20240209140917.846878-14-roberto.sassu@huaweicloud.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> References: <20240209140917.846878-1-roberto.sassu@huaweicloud.com> Precedence: bulk X-Mailing-List: linux-kselftest@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-CM-TRANSID: GxC2BwBnoCThMsZloMgpAg--.31110S5 X-Coremail-Antispam: 1UD129KBjvAXoWftryDGw1xCr45Cry3ZFyrZwb_yoWrGr18to ZY9r4UAw18Kr15uF1kCFnrAw1UG3ZYgwn7Ar18tw45WF18XFWUG3WDC3WUGFy5Jr4rGr97 A34xJw48Jr1Dtrn3n29KB7ZKAUJUUUU8529EdanIXcx71UUUUU7v73VFW2AGmfu7bjvjm3 AaLaJ3UjIYCTnIWjp_UUUO97kC6x804xWl14x267AKxVWrJVCq3wAFc2x0x2IEx4CE42xK 8VAvwI8IcIk0rVWrJVCq3wAFIxvE14AKwVWUJVWUGwA2048vs2IY020E87I2jVAFwI0_Jr Wl82xGYIkIc2x26xkF7I0E14v26ryj6s0DM28lY4IEw2IIxxk0rwA2F7IY1VAKz4vEj48v e4kI8wA2z4x0Y4vE2Ix0cI8IcVAFwI0_Gr0_Xr1l84ACjcxK6xIIjxv20xvEc7CjxVAFwI 0_Gr1j6F4UJwA2z4x0Y4vEx4A2jsIE14v26r4j6F4UM28EF7xvwVC2z280aVCY1x0267AK xVWxJr0_GcWle2I262IYc4CY6c8Ij28IcVAaY2xG8wAqx4xG64xvF2IEw4CE5I8CrVC2j2 WlYx0E2Ix0cI8IcVAFwI0_JrI_JrylYx0Ex4A2jsIE14v26r1j6r4UMcvjeVCFs4IE7xkE bVWUJVW8JwACjcxG0xvY0x0EwIxGrwACjI8F5VA0II8E6IAqYI8I648v4I1lFIxGxcIEc7 CjxVA2Y2ka0xkIwI1lc7CjxVAaw2AFwI0_GFv_Wryl42xK82IYc2Ij64vIr41l4I8I3I0E 4IkC6x0Yz7v_Jr0_Gr1lx2IqxVAqx4xG67AKxVWUJVWUGwC20s026x8GjcxK67AKxVWUGV WUWwC2zVAF1VAY17CE14v26r4a6rW5MIIYY7kG6xAYrwCIc40Y0x0EwIxGrwCI42IY6xII jxv20xvE14v26r4j6ryUMIIF0xvE2Ix0cI8IcVCY1x0267AKxVW8Jr0_Cr1UMIIF0xvE42 xK8VAvwI8IcIk0rVWUJVWUCwCI42IY6I8E87Iv67AKxVW8JVWxJwCI42IY6I8E87Iv6xkF 7I0E14v26F4UJVW0obIYCTnIWIevJa73UjIFyTuYvjxUwOzVUUUUU X-CM-SenderInfo: purev21wro2thvvxqx5xdzvxpfor3voofrz/1tbiAQAIBF1jj5o4HQABsC From: Roberto Sassu Add the documentation of the digest_cache LSM in Documentation/security. Signed-off-by: Roberto Sassu --- Documentation/security/digest_cache.rst | 900 ++++++++++++++++++++++++ Documentation/security/index.rst | 1 + MAINTAINERS | 1 + 3 files changed, 902 insertions(+) create mode 100644 Documentation/security/digest_cache.rst diff --git a/Documentation/security/digest_cache.rst b/Documentation/security/digest_cache.rst new file mode 100644 index 000000000000..6a492e23495c --- /dev/null +++ b/Documentation/security/digest_cache.rst @@ -0,0 +1,900 @@ +.. SPDX-License-Identifier: GPL-2.0 + +================ +Digest_cache LSM +================ + +Introduction +============ + +Integrity detection and protection has long been a desirable feature, to +reach a large user base and mitigate the risk of flaws in the software and +attacks. + +However, while solutions exist, they struggle to reach the large user base, +due to requiring higher than desired constraints on performance, +flexibility and configurability, that only security conscious people are +willing to accept. + +This is where the new digest_cache LSM comes into play, it offers +additional support for new and existing integrity solutions, to make them +faster and easier to deploy. + + +Motivation +========== + +The digest_cache LSM helps to address two important shortcomings of the +Integrity Measurement Architecture (IMA): predictability of the Platform +Configuration Registers (PCRs), and the provisioning of reference values to +compare the calculated file digest against. + +Remote attestation, according to Trusted Computing Group (TCG) +specifications, is done by replicating the PCR extend operation in +software with the digests in the event log (in this case the IMA +measurement list), and by comparing the obtained value with the PCR value +signed by the TPM with the quote operation. + +Due to how the extend operation is performed, if measurements are done in +a different order, the final PCR value will be different. That means that +if measurements are done in parallel, there is no way to predict what the +final PCR value will be, making impossible to seal data to a PCR value. If +the PCR value was predictable, a system could for example prove its +integrity by unsealing and using its private key, without sending every +time the full list of measurements. + +Provisioning reference values for file digests is also a difficult task. +The solution so far was to add file signatures to RPM packages, and +possibly to DEB packages, so that IMA can verify them. While this undoubtly +works, it also requires Linux distribution vendors to support the feature +by rebuilding all their packages, and eventually extending their PKI to +perform the additional signatures. It could also require developers extra +work to deal with the additional data. + +On the other hand, since often packages carry the file digests themselves, +it won't be actually needed to add file signatures. If the kernel was able +to extract the file digests by itself, all the tasks mentioned above for +the Linux distribution vendors won't be needed too. All current and past +Linux distributions can be easily retrofitted to enable IMA appraisal with +the file digests from the packages. + +Narrowing down the scope of a package parser to only extract specific +information makes it small enough to accurately verify that it cannot harm +the kernel. In fact, the parsers included with the digest_cache LSM have +been verified with the formal verification tool Frama-C, albeit with a +limited buffer size (the verification time grows considerably with bigger +buffer sizes). The parsers with the Frama-C assertions are available here: + +https://github.com/robertosassu/rpm-formal/ + +Frama-C asserts that the parsers don't read beyond their assigned buffer +for any byte combination. + +An additional mitigation against corrupted digest lists consists in +verifying the signature of the package first, before attempting to extract +the file digests. + + +Solution +======== + +The digest_cache LSM can help IMA to extend a PCR in a deterministic way. +If IMA knows that a file comes from a Linux distribution, it can measure +files in a different way: measure the list of digests coming from the +distribution (e.g. RPM package headers), and subsequently measure a file if +it is not found in that list. + +If the system executes known files, it does not matter in which order they +are executed, because the PCR is not extended. That however means that the +lists of digests must be measured in a deterministic way. The digest_cache +LSM has a prefetching mechanism to make this happen, consisting in +sequentially reading digest lists in a directory until it finds the +requested one. + +The resulting IMA measurement list however has a disadvantage: it does not +tell to remote verifiers whether files with digest in the measured digest +lists have been accessed or not and when. Also the IMA measurement list +would change after a software update. + +The digest_cache LSM can also help IMA for appraisal. Currently, IMA has +to evaluate the signature of each file individually, and expects that the +Linux vendors include those signatures together with the files in the +packages. + +With the digest_cache LSM, IMA can simply lookup in the list of digests +extracted from package headers, once the signature of those headers has +been verified. The same approach can be followed by other LSMs, such as +Integrity Policy Enforcement (IPE). + + +Design +====== + +Digest cache +------------ + +The digest_cache LSM collects digests from various sources (called digest +lists), and stores them in kernel memory, in a set of hash tables forming a +digest cache. Extracted digests can be used as reference values for +integrity verification of file content or metadata. + +A digest cache has three types of references: in the inode security blob of +the digest list the digest cache was created from (dig_owner field); in the +security blob of the inodes for which the digest cache is requested +(dig_user field); a reference returned by digest_cache_get(). + +References are released with digest_cache_put(), in the first two cases +when inodes are evicted from memory, in the last case when that function is +explicitly called. Obtaining a digest cache reference means that the digest +cache remains valid and cannot be freed until releasing it and until the +total number of references (stored in the digest cache) becomes zero. + +When digest_cache_get() is called on an inode to compare its digest with +a reference value, the digest_cache LSM knows which digest cache to get +from the new security.digest_list xattr added to that inode, which contains +the file name of the desired digest list digests will be extracted from. + +All digest lists are expected to be in the same directory, defined in the +kernel config, and modifiable at run-time through securityfs. When the +digest_cache LSM reads the security.digest_list xattr, it uses its value as +last path component, appended to the default path (unless the default path +is a file). If an inode does not have that xattr, the default path is +considered as the final destination. + +The default path can be either a file or a directory. If it is a file, the +digest_cache LSM always uses the same digest cache from that file to verify +all inodes (the xattr, if present, is ignored). If it is a directory, and +the inode to verify does not have the xattr, the digest_cache LSM iterates +and looks up on the digest caches created from each directory entry. + +Digest caches are created on demand, only when digest_cache_get() is +called. The first time a digest cache is requested, the digest_cache LSM +creates it and sets its reference in the dig_owner and dig_user fields of +the respective inode security blobs. On the next requests, the previously +set reference is returned, after incrementing the reference count. + +Since there might be multiple digest_cache_get() calls for the same inode, +or for different inodes pointing to the same digest list, dig_owner_mutex +and dig_user_mutex have been introduced to protect the check and assignment +of the digest cache reference in the inode security blob. + +Contenders that didn't get the lock also have to wait until the digest +cache is fully instantiated (when the bit INIT_IN_PROGRESS is cleared). +Dig_owner_mutex cannot be used for waiting on the instantiation to avoid +lock inversion with the inode lock for directories. + + +Verification data +----------------- + +The digest_cache LSM can support other LSMs in their decisions of granting +access to file content and metadata. + +However, the information alone about whether a digest was found in a digest +cache might not be sufficient, because for example those LSMs wouldn't know +whether the digest cache itself was created from authentic data. + +Digest_cache_verif_set() lets the same LSMs (or a chosen integrity +provider) evaluate the digest list being read during the creation of the +digest cache, by implementing the kernel_post_read_file LSM hook, and lets +them attach their verification data to that digest cache. + +Space is reserved in the file descriptor security blob for the digest cache +pointer. Digest_cache_to_file_sec() sets that pointer before calling +kernel_read_file() in digest_cache_populate(), and +digest_cache_from_file_sec() retrieves the pointer back from the file +descriptor passed by LSMs with digest_cache_verif_set(). + +Multiple providers are supported, in the event there are multiple +integrity LSMs active. Each provider should also provide an unique verifier +ID as an argument to digest_cache_verif_set(), so that verification data +can be distinguished. + +A caller of digest_cache_get() can retrieve back the verification data by +calling digest_cache_verif_get() and passing a digest cache pointer and the +desired verifier ID. + +Since directory digest caches are not populated themselves, LSMs have to do +a lookup first to get the digest cache containing the digest, call +digest_cache_from_found_t() to convert the returned digest_cache_found_t +type to a digest cache pointer, and pass that to digest_cache_verif_get(). + + +Directories +----------- + +In the environments where xattrs are not available (e.g. in the initial ram +disk), the digest_cache LSM cannot precisely determine which digest list in +a directory contains the desired reference digest. However, although +slower, it would be desirable to search the digest in all digest lists of +that directory. + +This done in two steps. When a digest cache is being created, +digest_cache_create() invokes digest_cache_dir_create(), to generate the +list of current directory entries. Entries are placed in the list in +ascending order by the if prepended to the file name, or at the +end of the list if not. + +The resulting digest cache has the IS_DIR bit set, to distinguish it from +the digest caches created from regular files. + +Second, when a digest is searched in a directory digest cache, +digest_cache_lookup() invokes digest_cache_dir_lookup_digest() to +iteratively search that digest in each directory entry generated by +digest_cache_dir_create(). + +That list is stable, even if new files are added or deleted from that +directory. In that case, the digest_cache LSM will invalidate the digest +cache, forcing next callers of digest_cache_get() to get a new directory +digest cache with the updated list of directory entries. + +If the current directory entry does not have a digest cache reference, +digest_cache_dir_lookup_digest() invokes digest_cache_create() to create a +new digest cache for that entry. In either case, +digest_cache_dir_lookup_digest() calls then digest_cache_htable_lookup() +with the new/existing digest cache to search the digest. + +The iteration stops when the digest is found. In that case, +digest_cache_dir_lookup_digest() returns the digest cache reference of the +current directory entry as the digest_cache_found_t type, so that callers +of digest_cache_lookup() don't mistakenly try to call digest_cache_put() +with that reference. + +This new reference type will be used to retrieve information about the +digest cache containing the digest, which is not known in advance until the +digest search is performed. + +The order of the list of directory entries influences the speed of the +digest search. A search terminates faster if less digest caches have to be +created. One way to optimize it could be to order the list of digest lists +in the same way of when they are requested at boot. + +Finally, digest_cache_dir_free() releases the digest cache references +stored in the list of directory entries, and frees the list itself. + + +Prefetching +----------- + +A desirable goal when doing integrity measurements is that they are done +always in the same order across boots, so that the resulting PCR value +becomes predictable and suitable for sealing policies. However, due to +parallel execution of system services at boot, a deterministic order of +measurements is difficult to achieve. + +The digest_cache LSM is not exempted from this issue. Under the assumption +that only the digest list is measured, and file measurements are omitted if +their digest is found in that digest list, a PCR can be predictable only if +all files belong to the same digest list. Otherwise, it will still be +unpredictable, since files accessed in a non-deterministic order will cause +digest lists to be measured in a non-deterministic order too. + +The prefetching mechanism overcomes this issue by searching a digest list +file name in digest_list_dir_lookup_filename() among the entries of the +linked list built by digest_cache_dir_create(). If the file name does not +match, it reads the digest list to trigger its measurement. Otherwise, it +also creates a digest cache and returns that to the caller. + +Prefetching needs to be explicitly enabled by setting the new +security.dig_prefetch xattr to 1 in the directory containing the digest +lists. The newly introduced function digest_cache_prefetch_requested() +checks first if the DIR_PREFETCH bit is set in dig_owner, otherwise it +reads the xattr. digest_cache_create() sets DIR_PREFETCH in dig_owner, if +prefetching is enabled, before declaring the digest cache as initialized. + + +Tracking changes +---------------- + +The digest_cache LSM registers to five LSM hooks, file_open, path_truncate, +file_release, inode_unlink and inode_rename, to monitor digest lists and +directory modifications. + +If an action affects a digest list or the parent directory, these hooks +call digest_cache_reset() to set the RESET bit on the digest cache. This +will cause next calls to digest_cache_get() and digest_cache_create() to +respectively put and clear dig_user and dig_owner, and request a new +digest cache. + +That does not affect other users of the old digest cache, since that one +remains valid as long as the reference count is greater than zero. However, +they can explicitly call the new function digest_cache_was_reset(), to +check if the RESET bit was set on the digest cache reference they hold. + +Recreating a file digest cache means reading the digest list again and +extracting the digests. Recreating a directory digest cache, instead, does +not mean recreating the digest cache for directory entries, since those +digest caches are likely already stored in the inode security blob. It +would happen however for new files. + +File digest cache reset is done on file_open, when a digest list is opened +for write, path_truncate, when a digest list is truncated (there is no +inode_truncate, file_truncate does not catch operations through the +truncate() system call), inode_unlink, when a digest list is removed, and +inode_rename when a digest list is renamed. + +Directory digest cache reset is done on file_release, when a digest list is +written in the digest list directory, on inode_unlink, when a digest list +is deleted from that directory, and finally on inode_rename, when a digest +list is moved to/from that directory. + +With the exception of file_release, which will always be executed (cannot +be denied), the other LSM hooks are not optimal, since the digest_cache LSM +does not know whether or not the operation will be allowed also by other +LSMs. If the operation is denied, the digest_cache LSM would do an +unnecessary reset. + + +Data structures and API +======================= + +Data structures +--------------- + +These are the data structures defined and used internally by the +digest_cache LSM. + +.. kernel-doc:: security/digest_cache/internal.h + + +Public API +---------- + +This API is meant to be used by users of the digest_cache LSM. + +.. kernel-doc:: include/linux/digest_cache.h + :identifiers: digest_cache_found_t + digest_cache_from_found_t + +.. kernel-doc:: security/digest_cache/main.c + :identifiers: digest_cache_get digest_cache_put + +.. kernel-doc:: security/digest_cache/htable.c + :identifiers: digest_cache_lookup + +.. kernel-doc:: security/digest_cache/verif.c + :identifiers: digest_cache_verif_set digest_cache_verif_get + +.. kernel-doc:: security/digest_cache/reset.c + :identifiers: digest_cache_was_reset + + +Parser API +---------- + +This API is meant to be used by digest list parsers. + +.. kernel-doc:: security/digest_cache/htable.c + :identifiers: digest_cache_htable_init + digest_cache_htable_add + digest_cache_htable_lookup + + +Digest List Formats +=================== + +tlv +--- + +The Type-Length-Value (TLV) format was chosen for its extensibility. +Additional fields can be added without breaking compatibility with old +versions of the parser. + +The layout of a tlv digest list is the following:: + + [header: DIGEST_LIST_FILE, num fields, total len] + [field: DIGEST_LIST_ALGO, length, value] + [field: DIGEST_LIST_ENTRY#1, length, value (below)] + |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len] + |- [DIGEST_LIST_ENTRY_DIGEST#1, length, file digest] + |- [DIGEST_LIST_ENTRY_PATH#1, length, file path] + [field: DIGEST_LIST_ENTRY#N, length, value (below)] + |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len] + |- [DIGEST_LIST_ENTRY_DIGEST#N, length, file digest] + |- [DIGEST_LIST_ENTRY_PATH#N, length, file path] + +DIGEST_LIST_ALGO is a field to specify the algorithm of the file digest. +DIGEST_LIST_ENTRY is a nested TLV structure with the following fields: +DIGEST_LIST_ENTRY_DIGEST contains the file digest; DIGEST_LIST_ENTRY_PATH +contains the file path. + + +rpm +--- + +The rpm digest list is basically a subset of the RPM package header. +Its format is:: + + [RPM magic number] + [RPMTAG_IMMUTABLE] + +RPMTAG_IMMUTABLE is a section of the full RPM header containing the part +of the header that was signed, and whose signature is stored in the +RPMTAG_RSAHEADER section. + + +Appended Signature +------------------ + +Digest lists can have a module-style appended signature, that can be used +for appraisal with IMA. The signature type can be PKCS#7, as for kernel +modules, or a different type. + + +History +======= + +The original name of this work was IMA Digest Lists, which was somehow +considered too invasive. The code was moved to a separate component named +DIGLIM (DIGest Lists Integrity Module), with the purpose of removing the +complexity away of IMA, and also adding the possibility of using it with +other kernel components (e.g. Integrity Policy Enforcement, or IPE). + +The design changed significantly, so DIGLIM was renamed to digest_cache +LSM, as the name better reflects what the new component does. + +Since it was originally proposed, in 2017, this work grew up a lot thanks +to various comments/suggestions. It became integrally part of the openEuler +distribution since end of 2020. + +The most important difference between the old the current version is moving +from a centralized repository of file digests to a per-package repository. +This significantly reduces the memory pressure, since digest lists are +loaded into kernel memory only when they are actually needed. Also, file +digests are automatically unloaded from kernel memory at the same time +inodes are evicted from memory during reclamation. + + +Performance +=========== + +System specification +-------------------- + +The tests have been performed on a Fedora 38 virtual machine with 4 cores +(AMD EPYC-Rome, no hyperthreading), 4 GB of RAM, no TPM/TPM passthrough/ +emulated. The QEMU process has been pinned to 4 real CPU cores and its +priority was set to -20. + + +Benchmark tool +-------------- + +The digest_cache LSM has been tested with an ad-hoc benchmark tool that +creates 20000 files with a random size up to 100 bytes and randomly adds +their digest to one of 303 digest lists. The number of digest lists has +been derived from the ratio (66) digests/packages (124174/1883) found in +the testing virtual machine (hence, 20000/66 = 303). IMA signatures have +been done with ECDSA NIST P-384. + +The benchmark tool then creates a list of 20000 files to be accessed, +randomly chosen (there can be duplicates). This is necessary to make the +results reproducible across reboots (by always replaying the same +operations). The benchmark reads (sequentially and in parallel) the files +from the list 2 times, flushing the kernel caches before each read. + +Each test has been performed 5 times, and the average value is taken. + + +Purpose of the benchmark +------------------------ + +The purpose of the benchmark is to show the performance difference of IMA +between the current behavior, and by using the digest_cache LSM. + + +IMA measurement policy: no cache +-------------------------------- + +.. code-block:: bash + + measure func=FILE_CHECK fowner=2001 pcr=12 + + +IMA measurement policy: cache +----------------------------- + +.. code-block:: bash + + measure func=DIGEST_LIST_CHECK pcr=12 + measure func=FILE_CHECK fowner=2001 digest_cache=content pcr=12 + + +IMA Measurement Results +----------------------- + +Sequential +~~~~~~~~~~ + +This test was performed reading files sequentially, and waiting for the +current read to terminate before beginning a new one. + +:: + + +-------+------------------------+-----------+ + | meas. | time no/p/vTPM (sec.) | slab (KB) | + +--------------------+-------+------------------------+-----------+ + | no cache | 12313 | 33.65 / 102.51 / 47.13 | 84170 | + +--------------------+-------+------------------------+-----------+ + | cache, no prefetch | 304 | 34.04 / 33.32 / 33.09 | 81159 | + +--------------------+-------+------------------------+-----------+ + | cache, prefetch | 304 | 34.02 / 33.31 / 33.15 | 81122 | + +--------------------+-------+------------------------+-----------+ + +The table shows that 12313 measurements (boot_aggregate + files) have been +made without the digest cache, and 304 with the digest cache +(boot_aggregate + digest lists). Consequently, the memory occupation +without the cache is higher due to the higher number of measurements. + +Not surprisingly, for the same reason, also the test time is significantly +higher without the digest cache when the physical or virtual TPM is used. + +In terms of pure performance, first number in the third column, it can be +seen that there are not really performance differences between using or not +using the digest cache. + +Prefetching does not add overhead, also because digest lists were ordered +according to their appearance in the IMA measurement list (which minimize +the digest lists to prefetch). + + +Parallel +~~~~~~~~ + +This test was performed reading files in parallel, not waiting for the +current read to terminate. + +:: + + +-------+-----------------------+-----------+ + | meas. | time no/p/vTPM (sec.) | slab (KB) | + +--------------------+-------+-----------------------+-----------+ + | no cache | 12313 | 14.08 / 79.09 / 22.70 | 85138 | + +--------------------+-------+-----------------------+-----------+ + | cache, no prefetch | 304 | 14.44 / 15.11 / 14.96 | 85777 | + +--------------------+-------+-----------------------+-----------+ + | cache, prefetch | 304 | 14.30 / 15.41 / 14.40 | 83294 | + +--------------------+-------+-----------------------+-----------+ + +Also in this case, the physical TPM causes the biggest delay especially +without digest cache, where a higher number of measurements need to be +extended in the TPM. + +The digest_cache LSM does not introduce a noticeable overhead in all +scenarios. + + +IMA appraisal policy: no cache +------------------------------ + +.. code-block:: bash + + appraise func=FILE_CHECK fowner=2001 + + +IMA appraisal policy: cache +--------------------------- + +.. code-block:: bash + + appraise func=DIGEST_LIST_CHECK + appraise func=FILE_CHECK fowner=2001 digest_cache=content + + +IMA Appraisal Results +--------------------- + +Sequential +~~~~~~~~~~ + +This test was performed reading files sequentially, and waiting for the +current read to terminate before beginning a new one. + +:: + + +-------------+-------------+-----------+ + | files | time (sec.) | slab (KB) | + +----------------------------+-------------+-------------+-----------+ + | appraise (ECDSA sig) | 12312 | 96.74 | 78827 | + +----------------------------+-------------+-------------+-----------+ + | appraise (cache) | 12312 + 303 | 33.09 | 80854 | + +----------------------------+-------------+-------------+-----------+ + | appraise (cache, prefetch) | 12312 + 303 | 33.42 | 81050 | + +----------------------------+-------------+-------------+-----------+ + +This test shows a huge performance difference from verifying the signature +of 12312 files as opposed to just verifying the signature of 303 digest +lists, and looking up the digest of the files being read. + +There are some differences in terms of memory occupation, which is quite +expected due to the fact that we have to take into account the digest +caches loaded in memory, while with the standard appraisal they don't +exist. + + +Parallel +~~~~~~~~ + +This test was performed reading files in parallel, not waiting for the +current read to terminate. + +:: + + +-------------+-------------+-----------+ + | files | time (sec.) | slab (KB) | + +----------------------------+-------------+-------------+-----------+ + | appraise (ECDSA sig) | 12312 | 27.68 | 80596 | + +----------------------------+-------------+-------------+-----------+ + | appraise (cache) | 12313 + 303 | 14.96 | 80778 | + +----------------------------+-------------+-------------+-----------+ + | appraise (cache, prefetch) | 12313 + 303 | 14.78 | 83354 | + +----------------------------+-------------+-------------+-----------+ + +The difference is less marked when performing the read in parallel. Also, +more memory seems to be occupied in the prefetch case. + + +How to Test +=========== + +Additional patches need to be applied to the kernel. + +The patch to introduce the file_release LSM hook: + +https://lore.kernel.org/linux-integrity/20240115181809.885385-14-roberto.sassu@huaweicloud.com/ + +The patch set to use the PGP keys from the Linux distributions for +verifying the RPM header signatures: + +https://lore.kernel.org/linux-integrity/20230720153247.3755856-1-roberto.sassu@huaweicloud.com/ + +The same URL contains two GNUPG patches to be applied to the user space +program. + +The patch set to use the digest_cache LSM from IMA: + +https://github.com/robertosassu/linux/commits/digest_cache-lsm-v3-ima/ + +First, it is necessary to install the kernel headers in usr/ in the kernel +source directory: + +.. code-block:: bash + + $ make headers_install + +After, it is necessary to copy the new kernel headers (tlv_parser.h, +uasym_parser.h, tlv_digest_list.h) from usr/include/linux in the kernel +source directory to /usr/include/linux. + +Then, gpg must be rebuilt with the additional patches to convert the PGP +keys of the Linux distribution to the new user asymmetric key format: + +.. code-block:: bash + + $ gpg --conv-kernel >> certs/uasym_keys.bin + +This embeds the converted keys in the kernel image. + +Finally, the following kernel options must be enabled: + +.. code-block:: bash + + CONFIG_SECURITY_DIGEST_CACHE=y + CONFIG_UASYM_KEYS_SIGS=y + CONFIG_UASYM_PRELOAD_PUBLIC_KEYS=y + +and the kernel must be rebuilt with the patches applied. After reboot, it +is necessary to build and install the digest list tools downloadable from: + +https://github.com/linux-integrity/digest-cache-tools + +and to execute (as root): + +.. code-block:: bash + + # manage_digest_lists -o gen -d /etc/digest_lists -i rpmdb -f rpm + +The new gpg must also be installed in the system, as it will be used to +convert the PGP signatures of the RPM headers to the user asymmetric key +format. + +It is recommended to create an additional digest list with the following +files, by creating a file named ``list`` with the content: + +.. code-block:: bash + + /usr/bin/manage_digest_lists + /usr/lib64/libgen-tlv-list.so + /usr/lib64/libgen-rpm-list.so + /usr/lib64/libparse-rpm-list.so + /usr/lib64/libparse-tlv-list.so + +Then, to create the digest list, it is sufficient to execute: + +.. code-block:: bash + + # manage_digest_lists -i list -L -d /etc/digest_lists -o gen -f tlv + +Also, a digest list must be created for the modified gpg binary: + +.. code-block:: bash + + # manage_digest_lists -i /usr/bin/gpg -d /etc/digest_lists -o gen -f tlv + +If appraisal is enabled and in enforcing mode, it is necessary to sign the +new digest lists, with the sign-file tool in the scripts/ directory of the +kernel sources: + +.. code-block:: bash + + # scripts/sign-file sha256 certs/signing_key.pem certs/signing_key.pem /etc/digest_lists/tlv-list + # scripts/sign-file sha256 certs/signing_key.pem certs/signing_key.pem /etc/digest_lists/tlv-gpg + +The final step is to add security.digest_list to each file with: + +.. code-block:: bash + + # manage_digest_lists -i /etc/digest_lists -o add-xattr + +After that, it is possible to test the digest_cache LSM with the following +policy written to /etc/ima/ima-policy: + +.. code-block:: bash + + measure func=DIGEST_LIST_CHECK template=ima-modsig pcr=12 + dont_measure fsmagic=0x01021994 + measure func=BPRM_CHECK digest_cache=content pcr=12 + measure func=MMAP_CHECK digest_cache=content pcr=12 + +Tmpfs is excluded for now, until memfd is properly handled. The reason why +the DIGEST_LIST_CHECK rule is before the dont_measure is that otherwise +digest lists in the initial ram disk won't be processed. + +Before loading the policy, it is possible to enable dynamic debug to see +which operations are done by the digest_cache LSM: + +.. code-block:: bash + + # echo "file security/digest_cache/* +p" > /sys/kernel/debug/dynamic_debug/control + +Alternatively, the same strings can be set as value of the dyndbg= option +in the kernel command line. + +A preliminary test, before booting the system with the new policy, is to +supply the policy to IMA in the current system with: + +.. code-block:: bash + + # cat /etc/ima/ima-policy > /sys/kernel/security/ima/policy + +After executing some commands, it can be seen if the digest_cache LSM is +working by checking the IMA measurement list. If there are only digest +lists, it means that everything is working properly, and the system can be +rebooted. The instructions have been tested on a Fedora 38 OS. + +After boot, it is possible to check the content of the measurement list: + +.. code-block:: bash + + # cat /sys/kernel/security/ima/ascii_runtime_measurements + + +At this point, it is possible to enable the prefetching mechanism to make +the PCR predictable. The virtual machine must be configured with a TPM +(Emulated). + +To enable the prefetching mechanism, it is necessary to set +security.dig_prefetch to '1' for the /etc/digest_lists directory: + +.. code-block:: bash + + # setfattr -n security.dig_prefetch -v "1" /etc/digest_lists + +The final step is to reorder digest lists to be in the same order in which +they appear in the IMA measurement list. + +This can be done by executing the command: + +.. code-block:: bash + + # manage_digest_lists -i /sys/kernel/security/ima/ascii_runtime_measurements -d /etc/digest_lists -o add-seqnum + +Since we renamed the digest lists, we need to update security.digest_list +too: + +.. code-block:: bash + + # manage_digest_lists -i /etc/digest_lists -o add-xattr + +By rebooting several times, and just logging in (to execute the same +commands during each boot), it is possible to compare the PCR 12, and see +that it is always the same. That of course works only if the TPM is reset +at each boot (e.g. if the virtual machine has a virtual TPM) or if the code +is tested in the host environment. + +.. code-block:: bash + + # cat /sys/devices/LNXSYSTM:00/LNXSYBUS:00/MSFT0101:00/tpm/tpm0/pcr-sha256/12 + +The last step is to test IMA appraisal. This can be done by adding the +following lines to /etc/ima/ima-policy: + +.. code-block:: bash + + appraise func=DIGEST_LIST_CHECK appraise_type=imasig|modsig + dont_appraise fsmagic=0x01021994 + appraise func=BPRM_CHECK digest_cache=content + appraise func=MMAP_CHECK digest_cache=content + +The following test is to ensure that IMA prevents the execution of unknown +files: + +.. code-block:: bash + + # cp -a /bin/cat . + # ./cat + +That will work. But not on the modified binary: + +.. code-block:: bash + + # echo 1 >> cat + # ./cat + -bash: ./cat: Permission denied + +Execution will be denied, and a new entry in the measurement list will +appear (it would be probably ok to not add that entry, as access to the +file was denied): + +.. code-block:: bash + + 12 50b5a68bea0776a84eef6725f17ce474756e51c0 ima-ng sha256:15e1efee080fe54f5d7404af7e913de01671e745ce55215d89f3d6521d3884f0 /root/cat + +Finally, it is possible to test the shrinking of the digest cache, by +forcing the kernel to evict inodes from memory: + +.. code-block:: bash + + # echo 3 > /proc/sys/vm/drop_caches + +If dynamic debug was enabled, the kernel log should have messages like: + +.. code-block:: bash + + [ 313.032536] DIGEST CACHE: Removed digest sha256:102900208eef27b766380135906d431dba87edaa7ec6aa72e6ebd3dd67f3a97b from digest list /etc/digest_lists/rpm-libseccomp-2.5.3-4.fc38.x86_64 + +Optionally, it is possible to test IMA measurement/appraisal from the very +beginning of the boot process, for now by including all digest lists and the +IMA policy in the initial ram disk. In the future, there will be a dracut +patch for ``dracut_install`` to select only the necessary digest lists. + +This can be simply done by executing: + +.. code-block:: bash + + # dracut -f -I " /etc/ima/ima-policy " -i /etc/digest_lists/ /etc/digest_lists/ --nostrip --kver + +The --nostrip option is particularly important. If debugging symbols are +stripped from the binary, its digest no longer matches with the one from +the package, causing access denied. + +The final test is to try the default IMA measurement and appraisal +policies, so that there is no gap between when the system starts and when +the integrity evaluation is effective. The default policies actually will +be used only until systemd is able to load the custom policy to +measure/appraise binaries and shared libraries. It should be good enough +for the system to boot. + +The default IMA measurement and appraisal policies can be loaded at boot by +adding the following to the kernel command line: + +.. code-block:: bash + + ima_policy="tcb|appraise_tcb|digest_cache_measure|digest_cache_appraise" + +The ima-modsig template can be selected by adding to the kernel command +line: + +.. code-block:: bash + + ima_template=ima-modsig diff --git a/Documentation/security/index.rst b/Documentation/security/index.rst index 59f8fc106cb0..34933e13c509 100644 --- a/Documentation/security/index.rst +++ b/Documentation/security/index.rst @@ -19,3 +19,4 @@ Security Documentation digsig landlock secrets/index + digest_cache diff --git a/MAINTAINERS b/MAINTAINERS index ee52cc664bc7..076b6e9da194 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6159,6 +6159,7 @@ DIGEST_CACHE LSM M: Roberto Sassu L: linux-security-module@vger.kernel.org S: Maintained +F: Documentation/security/digest_cache.rst F: security/digest_cache/ F: tools/testing/selftests/digest_cache/