From: Roberto Sassu roberto.sassu@huawei.com
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 a large user base, due to requiring higher than desired constraints on performance, flexibility and configurability, that only security conscious people are willing to accept.
For example, IMA measurement requires the target platform to collect integrity measurements, and to protect them with the TPM, which introduces a noticeable overhead (up to 10x slower in a microbenchmark) on frequently used system calls, like the open().
IMA Appraisal currently requires individual files to be signed and verified, and Linux distributions to rebuild all packages to include file signatures (this approach has been adopted from Fedora 39+). Like a TPM, also signature verification introduces a significant overhead, especially if it is used to check the integrity of many files.
This is where the new Integrity Digest Cache comes into play, it offers additional support for new and existing integrity solutions, to make them faster and easier to deploy.
The Integrity Digest Cache can help IMA to reduce the number of TPM operations and to make them happen 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.
The performance improvement comes at the cost of IMA not reporting which files from installed packages were accessed, and in which temporal sequence. This approach might not be suitable for all use cases.
The Integrity Digest Cache can also help IMA for appraisal. IMA can simply lookup the calculated digest of an accessed file in the list of digests extracted from package headers, after verifying the header signature. It is sufficient to verify only one signature for all files in the package, as opposed to verifying a signature for each file.
The same approach can be followed by other LSMs, such as Integrity Policy Enforcement (IPE), and BPF LSM.
The Integrity Digest Cache is not tied to a specific package format. The kernel supports a TLV-based digest list format. More can be added through third-party kernel modules. The TLV parser has been verified for memory safety with the Frama-C static analyzer. The version with the Frama-C assertions is available here:
https://github.com/robertosassu/rpm-formal/blob/main/validate_tlv.c
Integrating the Integrity Digest Cache in IMA brings significant performance improvements: up to 67% and 79% for measurement respectively in sequential and parallel file reads; up to 65% and 43% for appraisal respectively in sequential and parallel file reads.
The performance can be further enhanced by using fsverity digests instead of conventional file digests, which would make IMA verify only the portion of the file to be read. However, at the moment, fsverity digests are not included in RPM packages. In this case, once rpm is extended to include them, Linux distributions still have to rebuild their packages.
The Integrity Digest Cache can support both digest types, so that the functionality is immediately available without waiting for Linux distributions to do the transition.
This patch set only includes the patches necessary to extract digests from a TLV-based data format, and exposes an API for LSMs to query them. A separate patch set will be provided to integrate it in IMA.
This patch set and the follow-up IMA integration can be tested by following the instructions at:
https://github.com/linux-integrity/digest-cache-tools
This patch set applies on top of:
https://git.kernel.org/pub/scm/linux/kernel/git/zohar/linux-integrity.git/lo...
with commit 08ae3e5f5fc8 ("integrity: Use static_assert() to check struct sizes").
Changelog
v5: - Remove the RPM parser and selftests (suggested by Linus) - Return digest cache pointer from digest_cache_lookup() - Export new Parser API, and allow registration of third-party digest list parsers (suggested by Mimi) - Reduce sizes in TLV format and remove TLV header (suggested by Jani Nikula) - Introduce new DIGEST_LIST_NUM_ENTRIES TLV field - Pass file descriptor instead of dentry in digest_cache_get() to properly detect potential deadlocks - Introduce digest_cache_opened_fd() to tell lockdep when it is safe to nest a mutex if digest_cache_get() is called with that mutex held - Add new patch to introduce ksys_finit_module() - Make the TLV parser as configurable (Y/N/m) with Kconfig (suggested by Mimi) - Don't store the path structure in the digest cache and pass it between creation and initialization of the digest cache - Remove digest_cache_dir_update_dig_user() and keep the digest cache retrieved during digest_cache_get() - Fail with an error pointer in digest_cache_dir_lookup_digest() if the current and passed directory digest cache don't match, or the digest cache was reset - Handle num_digest = 0 in digest_cache_htable_init() - Accept -EOPNOTSUPP error in digest_cache_new() - Implement inode_free_security_rcu LSM hook instead of inode_free_security - Move reservation of file descriptor security blob inside the #ifdef in init_ima_lsm() - Add test file_reset_again to check the error pointer returned by digest_cache_lookup() - Remove TLV_FAILURE_HDR_LEN TLV error test - Add missing MODULE_DESCRIPTION in kselftest kernel module (suggested by Jeff Johnson) - Replace dentry_open() with kernel_file_open() in populate.c and dir.c - Skip affected tests when CONFIG_DYNAMIC_FTRACE_WITH_ARGS=n
v4: - Rename digest_cache LSM to Integrity Digest Cache (suggested by Paul Moore) - Update documentation - Remove forward declaration of struct digest_cache in include/linux/digest_cache.h (suggested by Jarkko) - Add DIGEST_CACHE_FREE digest cache event for notification - Remove digest_cache_found_t typedef and use uintptr_t instead - Add header callback in TLV parser and unexport tlv_parse_hdr() and tlv_parse_data() - Plug the Integrity Digest Cache into the 'ima' LSM - Switch from constructor to zeroing the object cache - Remove notifier and detect digest cache changes by comparing pointers - Rename digest_cache_dir_create() to digest_cache_dir_add_entries() - Introduce digest_cache_dir_create() to create and initialize a directory digest cache - Introduce digest_cache_dir_update_dig_user() to update dig_user with a file digest cache on positive digest lookup - Use up to date directory digest cache, to take into account possible inode eviction for the old ones - Introduce digest_cache_dir_prefetch() to prefetch digest lists - Adjust component name in debug messages (suggested by Jarkko) - Add FILE_PREFETCH and FILE_READ digest cache flags, remove RESET_USER - Reintroduce spin lock for digest cache verification data (needed for the selftests) - Get inode and file descriptor security blob offsets from outside (IMA) - Avoid user-after-free in digest_cache_unref() by decrementing the ref. count after printing the debug message - Check for digest list lookup loops also for the parent directory - Put and clear dig_owner directly in digest_cache_reset_clear_owner() - Move digest cache initialization code from digest_cache_create() to digest_cache_init() - Hold the digest list path until the digest cache is initialized (to avoid premature inode eviction) - Avoid race condition on setting DIR_PREFETCH in the directory digest cache - Introduce digest_cache_dir_prefetch() and do it between digest cache creation and initialization (to avoid lock inversion) - Avoid unnecessary length check in digest_list_parse_rpm() - Declare arrays of strings in tlv parser as static - Emit reset for parent directory on directory entry modification - Rename digest_cache_reset_owner() to digest_cache_reset_clear_owner() and digest_cache_reset_user() to digest_cache_clear_user() - Execute digest_cache_file_release() either if FMODE_WRITE or FMODE_CREATED are set in the file descriptor f_mode - Determine in digest_cache_verif_set() which gfp flag to use depending on verifier ID - Update selftests
v3: - Rewrite documentation, and remove the installation instructions since they are now included in the README of digest-cache-tools - Add digest cache event notifier - Drop digest_cache_was_reset(), and send instead to asynchronous notifications - Fix digest_cache LSM Kconfig style issues (suggested by Randy Dunlap) - Propagate digest cache reset to directory entries - Destroy per directory entry mutex - Introduce RESET_USER bit, to clear the dig_user pointer on set/removexattr - Replace 'file content' with 'file data' (suggested by Mimi) - Introduce per digest cache mutex and replace verif_data_lock spinlock - Track changes of security.digest_list xattr - Stop tracking file_open and use file_release instead also for file writes - Add error messages in digest_cache_create() - Load/unload testing kernel module automatically during execution of test - Add tests for digest cache event notifier - Add test for ftruncate() - Remove DIGEST_CACHE_RESET_PREFETCH_BUF command in test and clear the buffer on read instead
v2: - Include the TLV parser in this patch set (from user asymmetric keys and signatures) - Move from IMA and make an independent LSM - Remove IMA-specific stuff from this patch set - Add per algorithm hash table - Expect all digest lists to be in the same directory and allow changing the default directory - Support digest lookup on directories, when there is no security.digest_list xattr - Add seq num to digest list file name, to impose ordering on directory iteration - Add a new data type DIGEST_LIST_ENTRY_DATA for the nested data in the tlv digest list format - Add the concept of verification data attached to digest caches - Add the reset mechanism to track changes on digest lists and directory containing the digest lists - Add kernel selftests
v1: - Add documentation in Documentation/security/integrity-digest-cache.rst - Pass the mask of IMA actions to digest_cache_alloc() - Add a reference count to the digest cache - Remove the path parameter from digest_cache_get(), and rely on the reference count to avoid the digest cache disappearing while being used - Rename the dentry_to_check parameter of digest_cache_get() to dentry - Rename digest_cache_get() to digest_cache_new() and add digest_cache_get() to set the digest cache in the iint of the inode for which the digest cache was requested - Add dig_owner and dig_user to the iint, to distinguish from which inode the digest cache was created from, and which is using it; consequently it makes the digest cache usable to measure/appraise other digest caches (support not yet enabled) - Add dig_owner_mutex and dig_user_mutex to serialize accesses to dig_owner and dig_user until they are initialized - Enforce strong synchronization and make the contenders wait until dig_owner and dig_user are assigned to the iint the first time - Move checking IMA actions on the digest list earlier, and fail if no action were performed (digest cache not usable) - Remove digest_cache_put(), not needed anymore with the introduction of the reference count - Fail immediately in digest_cache_lookup() if the digest algorithm is not set in the digest cache - Use 64 bit mask for IMA actions on the digest list instead of 8 bit - Return NULL in the inline version of digest_cache_get() - Use list_add_tail() instead of list_add() in the iterator - Copy the digest list path to a separate buffer in digest_cache_iter_dir() - Use digest list parsers verified with Frama-C - Explicitly disable (for now) the possibility in the IMA policy to use the digest cache to measure/appraise other digest lists - Replace exit(<value>) with return <value> in manage_digest_lists.c
Roberto Sassu (15): lib: Add TLV parser module: Introduce ksys_finit_module() integrity: Introduce the Integrity Digest Cache digest_cache: Initialize digest caches digest_cache: Add securityfs interface digest_cache: Add hash tables and operations digest_cache: Allow registration of digest list parsers digest_cache: Parse tlv digest lists digest_cache: Populate the digest cache from a digest list digest_cache: Add management of verification data digest_cache: Add support for directories digest cache: Prefetch digest lists if requested digest_cache: Reset digest cache on file/directory change selftests/digest_cache: Add selftests for the Integrity Digest Cache docs: Add documentation of the Integrity Digest Cache
Documentation/security/digest_cache.rst | 850 ++++++++++++++++++ Documentation/security/index.rst | 1 + MAINTAINERS | 10 + include/linux/digest_cache.h | 131 +++ include/linux/kernel_read_file.h | 1 + include/linux/syscalls.h | 10 + include/linux/tlv_parser.h | 32 + include/uapi/linux/tlv_digest_list.h | 47 + include/uapi/linux/tlv_parser.h | 41 + include/uapi/linux/xattr.h | 6 + kernel/module/main.c | 43 +- lib/Kconfig | 3 + lib/Makefile | 2 + lib/tlv_parser.c | 87 ++ lib/tlv_parser.h | 18 + security/integrity/Kconfig | 1 + security/integrity/Makefile | 1 + security/integrity/digest_cache/Kconfig | 43 + security/integrity/digest_cache/Makefile | 11 + security/integrity/digest_cache/dir.c | 400 +++++++++ security/integrity/digest_cache/htable.c | 260 ++++++ security/integrity/digest_cache/internal.h | 283 ++++++ security/integrity/digest_cache/main.c | 597 ++++++++++++ security/integrity/digest_cache/modsig.c | 66 ++ security/integrity/digest_cache/parsers.c | 257 ++++++ security/integrity/digest_cache/parsers/tlv.c | 341 +++++++ security/integrity/digest_cache/populate.c | 104 +++ security/integrity/digest_cache/reset.c | 227 +++++ security/integrity/digest_cache/secfs.c | 104 +++ security/integrity/digest_cache/verif.c | 135 +++ security/integrity/ima/ima.h | 1 + security/integrity/ima/ima_fs.c | 6 + security/integrity/ima/ima_main.c | 10 +- tools/testing/selftests/Makefile | 1 + .../testing/selftests/digest_cache/.gitignore | 3 + tools/testing/selftests/digest_cache/Makefile | 24 + .../testing/selftests/digest_cache/all_test.c | 769 ++++++++++++++++ tools/testing/selftests/digest_cache/common.c | 78 ++ tools/testing/selftests/digest_cache/common.h | 93 ++ .../selftests/digest_cache/common_user.c | 33 + .../selftests/digest_cache/common_user.h | 15 + tools/testing/selftests/digest_cache/config | 2 + .../selftests/digest_cache/generators.c | 130 +++ .../selftests/digest_cache/generators.h | 16 + .../selftests/digest_cache/testmod/Makefile | 16 + .../selftests/digest_cache/testmod/kern.c | 551 ++++++++++++ 46 files changed, 5849 insertions(+), 11 deletions(-) create mode 100644 Documentation/security/digest_cache.rst create mode 100644 include/linux/digest_cache.h create mode 100644 include/linux/tlv_parser.h create mode 100644 include/uapi/linux/tlv_digest_list.h create mode 100644 include/uapi/linux/tlv_parser.h create mode 100644 lib/tlv_parser.c create mode 100644 lib/tlv_parser.h create mode 100644 security/integrity/digest_cache/Kconfig create mode 100644 security/integrity/digest_cache/Makefile create mode 100644 security/integrity/digest_cache/dir.c create mode 100644 security/integrity/digest_cache/htable.c create mode 100644 security/integrity/digest_cache/internal.h create mode 100644 security/integrity/digest_cache/main.c create mode 100644 security/integrity/digest_cache/modsig.c create mode 100644 security/integrity/digest_cache/parsers.c create mode 100644 security/integrity/digest_cache/parsers/tlv.c create mode 100644 security/integrity/digest_cache/populate.c create mode 100644 security/integrity/digest_cache/reset.c create mode 100644 security/integrity/digest_cache/secfs.c create mode 100644 security/integrity/digest_cache/verif.c create mode 100644 tools/testing/selftests/digest_cache/.gitignore create mode 100644 tools/testing/selftests/digest_cache/Makefile create mode 100644 tools/testing/selftests/digest_cache/all_test.c create mode 100644 tools/testing/selftests/digest_cache/common.c create mode 100644 tools/testing/selftests/digest_cache/common.h create mode 100644 tools/testing/selftests/digest_cache/common_user.c create mode 100644 tools/testing/selftests/digest_cache/common_user.h create mode 100644 tools/testing/selftests/digest_cache/config create mode 100644 tools/testing/selftests/digest_cache/generators.c create mode 100644 tools/testing/selftests/digest_cache/generators.h create mode 100644 tools/testing/selftests/digest_cache/testmod/Makefile create mode 100644 tools/testing/selftests/digest_cache/testmod/kern.c
From: Roberto Sassu roberto.sassu@huawei.com
Add a parser of a generic Type-Length-Value (TLV) format:
+--------------+--+---------+--------+---------+ | field1 (u16) | len1 (u32) | value1 (u8 len1) | +--------------+------------+------------------+ | ... | ... | ... | +--------------+------------+------------------+ | fieldN (u16) | lenN (u32) | valueN (u8 lenN) | +--------------+------------+------------------+
Each adopter can define its own fields. The TLV parser does not need to be aware of those, but lets the adopter obtain the data and decide how to continue.
After processing a TLV entry, call the callback function also with the callback data provided by the adopter. The latter can decide how to interpret the TLV entry depending on the field ID.
Nesting TLVs is also possible, the callback function can call tlv_parse() to parse the inner structure.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- MAINTAINERS | 8 +++ include/linux/tlv_parser.h | 32 ++++++++++++ include/uapi/linux/tlv_parser.h | 41 ++++++++++++++++ lib/Kconfig | 3 ++ lib/Makefile | 2 + lib/tlv_parser.c | 87 +++++++++++++++++++++++++++++++++ lib/tlv_parser.h | 18 +++++++ 7 files changed, 191 insertions(+) create mode 100644 include/linux/tlv_parser.h create mode 100644 include/uapi/linux/tlv_parser.h create mode 100644 lib/tlv_parser.c create mode 100644 lib/tlv_parser.h
diff --git a/MAINTAINERS b/MAINTAINERS index a097afd76ded..1f7ffa1c9dbd 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23388,6 +23388,14 @@ W: http://sourceforge.net/projects/tlan/ F: Documentation/networking/device_drivers/ethernet/ti/tlan.rst F: drivers/net/ethernet/ti/tlan.*
+TLV PARSER +M: Roberto Sassu roberto.sassu@huawei.com +L: linux-kernel@vger.kernel.org +S: Maintained +F: include/linux/tlv_parser.h +F: include/uapi/linux/tlv_parser.h +F: lib/tlv_parser.* + TMIO/SDHI MMC DRIVER M: Wolfram Sang wsa+renesas@sang-engineering.com L: linux-mmc@vger.kernel.org diff --git a/include/linux/tlv_parser.h b/include/linux/tlv_parser.h new file mode 100644 index 000000000000..0c72742af548 --- /dev/null +++ b/include/linux/tlv_parser.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Header file of TLV parser. + */ + +#ifndef _LINUX_TLV_PARSER_H +#define _LINUX_TLV_PARSER_H + +#include <uapi/linux/tlv_parser.h> + +/** + * typedef callback - Callback after parsing TLV entry + * @callback_data: Opaque data to supply to the callback function + * @field: Field identifier + * @field_data: Field data + * @field_len: Length of @field_data + * + * This callback is invoked after a TLV entry is parsed. + * + * Return: Zero on success, a negative value on error. + */ +typedef int (*callback)(void *callback_data, __u16 field, + const __u8 *field_data, __u32 field_len); + +int tlv_parse(callback callback, void *callback_data, const __u8 *data, + size_t data_len, const char **fields, __u32 num_fields); + +#endif /* _LINUX_TLV_PARSER_H */ diff --git a/include/uapi/linux/tlv_parser.h b/include/uapi/linux/tlv_parser.h new file mode 100644 index 000000000000..171d0cfd2c4c --- /dev/null +++ b/include/uapi/linux/tlv_parser.h @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Implement the user space interface for the TLV parser. + */ + +#ifndef _UAPI_LINUX_TLV_PARSER_H +#define _UAPI_LINUX_TLV_PARSER_H + +#include <linux/types.h> + +/* + * TLV format: + * + * +--------------+--+---------+--------+---------+ + * | field1 (u16) | len1 (u32) | value1 (u8 len1) | + * +--------------+------------+------------------+ + * | ... | ... | ... | + * +--------------+------------+------------------+ + * | fieldN (u16) | lenN (u32) | valueN (u8 lenN) | + * +--------------+------------+------------------+ + */ + +/** + * struct tlv_entry - Entry of TLV format + * @field: Field identifier + * @length: Data length + * @data: Data + * + * This structure represents an entry of the TLV format. + */ +struct tlv_entry { + __u16 field; + __u32 length; + __u8 data[]; +} __attribute__((packed)); + +#endif /* _UAPI_LINUX_TLV_PARSER_H */ diff --git a/lib/Kconfig b/lib/Kconfig index b38849af6f13..9141dcfc1704 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -777,3 +777,6 @@ config POLYNOMIAL
config FIRMWARE_TABLE bool + +config TLV_PARSER + bool diff --git a/lib/Makefile b/lib/Makefile index 773adf88af41..630de494eab5 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -393,5 +393,7 @@ obj-$(CONFIG_USERCOPY_KUNIT_TEST) += usercopy_kunit.o obj-$(CONFIG_GENERIC_LIB_DEVMEM_IS_ALLOWED) += devmem_is_allowed.o
obj-$(CONFIG_FIRMWARE_TABLE) += fw_table.o +obj-$(CONFIG_TLV_PARSER) += tlv_parser.o +CFLAGS_tlv_parser.o += -I lib
subdir-$(CONFIG_FORTIFY_SOURCE) += test_fortify diff --git a/lib/tlv_parser.c b/lib/tlv_parser.c new file mode 100644 index 000000000000..dbbe08018b4d --- /dev/null +++ b/lib/tlv_parser.c @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Implement the TLV parser. + */ + +#define pr_fmt(fmt) "tlv_parser: "fmt +#include <tlv_parser.h> + +/** + * tlv_parse - Parse TLV data + * @callback: Callback function to call to parse the entries + * @callback_data: Opaque data to supply to the callback function + * @data: Data to parse + * @data_len: Length of @data + * @fields: Array of field strings + * @num_fields: Number of elements of @fields + * + * Parse the TLV data format and call the supplied callback function for each + * entry, passing also the opaque data pointer. + * + * The callback function decides how to process data depending on the field. + * + * Return: Zero on success, a negative value on error. + */ +int tlv_parse(callback callback, void *callback_data, const __u8 *data, + size_t data_len, const char **fields, __u32 num_fields) +{ + const __u8 *data_ptr = data; + struct tlv_entry *entry; + __u16 parsed_field; + __u32 len; + int ret; + + if (data_len > U32_MAX) { + pr_debug("Data too big, size: %zd\n", data_len); + return -E2BIG; + } + + while (data_len) { + if (data_len < sizeof(*entry)) + return -EBADMSG; + + entry = (struct tlv_entry *)data_ptr; + data_ptr += sizeof(*entry); + data_len -= sizeof(*entry); + + parsed_field = __be16_to_cpu(entry->field); + if (parsed_field >= num_fields) { + pr_debug("Invalid field %u, max: %u\n", + parsed_field, num_fields - 1); + return -EBADMSG; + } + + len = __be32_to_cpu(entry->length); + + if (data_len < len) + return -EBADMSG; + + pr_debug("Data: field: %s, len: %u\n", fields[parsed_field], + len); + + if (!len) + continue; + + ret = callback(callback_data, parsed_field, data_ptr, len); + if (ret < 0) { + pr_debug("Parsing of field %s failed, ret: %d\n", + fields[parsed_field], ret); + return ret; + } + + data_ptr += len; + data_len -= len; + } + + if (data_len) { + pr_debug("Excess data: %zu bytes\n", data_len); + return -EBADMSG; + } + + return 0; +} +EXPORT_SYMBOL_GPL(tlv_parse); diff --git a/lib/tlv_parser.h b/lib/tlv_parser.h new file mode 100644 index 000000000000..e663966deac5 --- /dev/null +++ b/lib/tlv_parser.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Header file of TLV parser. + */ + +#ifndef _LIB_TLV_PARSER_H +#define _LIB_TLV_PARSER_H + +#include <linux/kernel.h> +#include <linux/err.h> +#include <linux/limits.h> +#include <linux/tlv_parser.h> + +#endif /* _LIB_TLV_PARSER_H */
Hi--
On 11/19/24 2:49 AM, Roberto Sassu wrote:
+/**
- typedef callback - Callback after parsing TLV entry
Same as my other naming comment -- too generic. Maybe tlv_callback...
Thanks.
- @callback_data: Opaque data to supply to the callback function
- @field: Field identifier
- @field_data: Field data
- @field_len: Length of @field_data
- This callback is invoked after a TLV entry is parsed.
- Return: Zero on success, a negative value on error.
- */
+typedef int (*callback)(void *callback_data, __u16 field,
const __u8 *field_data, __u32 field_len);
From: Roberto Sassu roberto.sassu@huawei.com
Introduce ksys_finit_module() to let kernel components request a kernel module without requiring running modprobe.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- include/linux/syscalls.h | 10 ++++++++++ kernel/module/main.c | 43 ++++++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 10 deletions(-)
diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h index 5758104921e6..18bb346bb793 100644 --- a/include/linux/syscalls.h +++ b/include/linux/syscalls.h @@ -1233,6 +1233,16 @@ int ksys_ipc(unsigned int call, int first, unsigned long second, int compat_ksys_ipc(u32 call, int first, int second, u32 third, u32 ptr, u32 fifth);
+#ifdef CONFIG_MODULES +int ksys_finit_module(struct file *f, const char *kargs, int flags); +#else +static inline int ksys_finit_module(struct file *f, const char *kargs, + int flags) +{ + return -EOPNOTSUPP; +} +#endif + /* * The following kernel syscall equivalents are just wrappers to fs-internal * functions. Therefore, provide stubs to be inlined at the callsites. diff --git a/kernel/module/main.c b/kernel/module/main.c index 49b9bca9de12..81f79c9ea637 100644 --- a/kernel/module/main.c +++ b/kernel/module/main.c @@ -2852,7 +2852,7 @@ static int early_mod_check(struct load_info *info, int flags) * zero, and we rely on this for optional sections. */ static int load_module(struct load_info *info, const char __user *uargs, - int flags) + const char *kargs, int flags) { struct module *mod; bool module_allocated = false; @@ -2953,7 +2953,13 @@ static int load_module(struct load_info *info, const char __user *uargs, flush_module_icache(mod);
/* Now copy in args */ - mod->args = strndup_user(uargs, ~0UL >> 1); + if (kargs) { + mod->args = kstrndup(kargs, ~0UL >> 1, GFP_KERNEL); + if (!mod->args) + mod->args = ERR_PTR(-ENOMEM); + } else { + mod->args = strndup_user(uargs, ~0UL >> 1); + } if (IS_ERR(mod->args)) { err = PTR_ERR(mod->args); goto free_arch_cleanup; @@ -3083,7 +3089,7 @@ SYSCALL_DEFINE3(init_module, void __user *, umod, return err; }
- return load_module(&info, uargs, 0); + return load_module(&info, uargs, NULL, 0); }
struct idempotent { @@ -3170,7 +3176,8 @@ static int idempotent_wait_for_completion(struct idempotent *u) return u->ret; }
-static int init_module_from_file(struct file *f, const char __user * uargs, int flags) +static int init_module_from_file(struct file *f, const char __user * uargs, + const char *kargs, int flags) { struct load_info info = { }; void *buf = NULL; @@ -3195,10 +3202,11 @@ static int init_module_from_file(struct file *f, const char __user * uargs, int info.len = len; }
- return load_module(&info, uargs, flags); + return load_module(&info, uargs, kargs, flags); }
-static int idempotent_init_module(struct file *f, const char __user * uargs, int flags) +static int idempotent_init_module(struct file *f, const char __user * uargs, + const char *kargs, int flags) { struct idempotent idem;
@@ -3207,7 +3215,7 @@ static int idempotent_init_module(struct file *f, const char __user * uargs, int
/* Are we the winners of the race and get to do this? */ if (!idempotent(&idem, file_inode(f))) { - int ret = init_module_from_file(f, uargs, flags); + int ret = init_module_from_file(f, uargs, kargs, flags); return idempotent_complete(&idem, ret); }
@@ -3217,15 +3225,16 @@ static int idempotent_init_module(struct file *f, const char __user * uargs, int return idempotent_wait_for_completion(&idem); }
-SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) +static int _ksys_finit_module(struct file *f, int fd, const char __user * uargs, + const char *kargs, int flags) { int err; - struct fd f;
err = may_init_module(); if (err) return err;
+ /* fd = -1 if called from the kernel. */ pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags);
if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS @@ -3233,8 +3242,22 @@ SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) |MODULE_INIT_COMPRESSED_FILE)) return -EINVAL;
+ err = idempotent_init_module(f, uargs, kargs, flags); + return err; +} + +int ksys_finit_module(struct file *f, const char *kargs, int flags) +{ + return _ksys_finit_module(f, -1, NULL, kargs, flags); +} + +SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) +{ + int err; + struct fd f; + f = fdget(fd); - err = idempotent_init_module(fd_file(f), uargs, flags); + err = _ksys_finit_module(fd_file(f), fd, uargs, NULL, flags); fdput(f); return err; }
On Tue, Nov 19, 2024 at 11:49:09AM +0100, Roberto Sassu wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Introduce ksys_finit_module() to let kernel components request a kernel module without requiring running modprobe.
That does sound more than sketchy, even more so because the commit log completely fails to explain why you'd need to do that.
On Tue, 2024-11-19 at 13:14 +0100, Christoph Hellwig wrote:
On Tue, Nov 19, 2024 at 11:49:09AM +0100, Roberto Sassu wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Introduce ksys_finit_module() to let kernel components request a kernel module without requiring running modprobe.
That does sound more than sketchy, even more so because the commit log completely fails to explain why you'd need to do that.
With my solution, the kernel grants access to a file in user space depending on whether or not its calculated (or fsverity) digest is found in an application manifest provided by the software vendor.
However, what it happens is that in the early boot phase the parser is not loaded yet, and the kernel cannot extract the reference digests from the application manifest.
Thus, calling request_module() and consequently executing modprobe will fail, since the kernel does not have its reference digest yet.
Instead, loading the kernel module from the kernel itself works, because only the kernel module needs to be verified, and that can be done through its appended signature.
Roberto
On Tue, Nov 19, 2024 at 01:14:02PM +0100, Christoph Hellwig wrote:
On Tue, Nov 19, 2024 at 11:49:09AM +0100, Roberto Sassu wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Introduce ksys_finit_module() to let kernel components request a kernel module without requiring running modprobe.
That does sound more than sketchy, even more so because the commit log completely fails to explain why you'd need to do that.
I also don't think the commit log is correct, I don't see how the code is preventing calling modprobe, the indepotent check is intended to prevent duplicate module init calls which may allocate extra vmalloc space only to release it. You can test to see if your patch has any improvments by enabling MODULE_STATS and MODULE_DEBUG_AUTOLOAD_DUPS and check before / after results of /sys/kernel/debug/modules/stats , right now this patch and commit log is not telling me anything useful.
Luis
On Tue, 2024-11-19 at 12:10 -0800, Luis Chamberlain wrote:
On Tue, Nov 19, 2024 at 01:14:02PM +0100, Christoph Hellwig wrote:
On Tue, Nov 19, 2024 at 11:49:09AM +0100, Roberto Sassu wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Introduce ksys_finit_module() to let kernel components request a kernel module without requiring running modprobe.
That does sound more than sketchy, even more so because the commit log completely fails to explain why you'd need to do that.
I also don't think the commit log is correct, I don't see how the code is preventing calling modprobe, the indepotent check is intended to prevent duplicate module init calls which may allocate extra vmalloc space only to release it. You can test to see if your patch has any improvments by enabling MODULE_STATS and MODULE_DEBUG_AUTOLOAD_DUPS and check before / after results of /sys/kernel/debug/modules/stats , right now this patch and commit log is not telling me anything useful.
Maybe I misunderstood the code, but what causes modprobe to be executed in user space is a call to request_module().
In my patch, I simply ported the code of the finit_module() system call to _ksys_finit_module(), net the conversion from struct fd to struct file, which is kept in the system call code.
Also, from the kernel side, I'm providing a valid address for module arguments, and duplicating the string either with kmemdup() or strndup_user() in load_module(), depending on where the memory belongs to.
Again, maybe I misunderstood, but I'm not introducing any functional change to the current behavior, the kernel side also provides a file descriptor and module arguments as user space would do (e.g. by executing insmod).
As for the motivation, please have a look at my response to Christian:
https://lore.kernel.org/linux-integrity/ZzzvAPetAn7CUEvx@bombadil.infradead....
In addition, you could also see how ksys_finit_module() is used here:
https://lore.kernel.org/linux-integrity/20241119104922.2772571-8-roberto.sas...
Thanks
Roberto
On Wed, 2024-11-20 at 10:16 +0100, Roberto Sassu wrote:
On Tue, 2024-11-19 at 12:10 -0800, Luis Chamberlain wrote:
On Tue, Nov 19, 2024 at 01:14:02PM +0100, Christoph Hellwig wrote:
On Tue, Nov 19, 2024 at 11:49:09AM +0100, Roberto Sassu wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Introduce ksys_finit_module() to let kernel components request a kernel module without requiring running modprobe.
That does sound more than sketchy, even more so because the commit log completely fails to explain why you'd need to do that.
I also don't think the commit log is correct, I don't see how the code is preventing calling modprobe, the indepotent check is intended to prevent duplicate module init calls which may allocate extra vmalloc space only to release it. You can test to see if your patch has any improvments by enabling MODULE_STATS and MODULE_DEBUG_AUTOLOAD_DUPS and check before / after results of /sys/kernel/debug/modules/stats , right now this patch and commit log is not telling me anything useful.
Maybe I misunderstood the code, but what causes modprobe to be executed in user space is a call to request_module().
In my patch, I simply ported the code of the finit_module() system call to _ksys_finit_module(), net the conversion from struct fd to struct file, which is kept in the system call code.
Also, from the kernel side, I'm providing a valid address for module arguments, and duplicating the string either with kmemdup() or strndup_user() in load_module(), depending on where the memory belongs to.
Again, maybe I misunderstood, but I'm not introducing any functional change to the current behavior, the kernel side also provides a file descriptor and module arguments as user space would do (e.g. by executing insmod).
As for the motivation, please have a look at my response to Christian:
Christoph, of course.
Roberto
https://lore.kernel.org/linux-integrity/ZzzvAPetAn7CUEvx@bombadil.infradead....
In addition, you could also see how ksys_finit_module() is used here:
https://lore.kernel.org/linux-integrity/20241119104922.2772571-8-roberto.sas...
Thanks
Roberto
On Wed, Nov 20, 2024 at 10:16:23AM +0100, Roberto Sassu wrote:
Again, maybe I misunderstood, but I'm not introducing any functional change to the current behavior, the kernel side also provides a file descriptor and module arguments as user space would do (e.g. by executing insmod).
The commit log does an extremely poor job to make this clear, all you are doing here is adding a helper. The commit log should be super clear on that and it is not.
Luis
On Mon, 2024-11-25 at 15:40 -0800, Luis Chamberlain wrote:
On Wed, Nov 20, 2024 at 10:16:23AM +0100, Roberto Sassu wrote:
Again, maybe I misunderstood, but I'm not introducing any functional change to the current behavior, the kernel side also provides a file descriptor and module arguments as user space would do (e.g. by executing insmod).
The commit log does an extremely poor job to make this clear, all you are doing here is adding a helper. The commit log should be super clear on that and it is not.
Correct, yes. Will do a much better and clear commit message for next version.
Thanks
Roberto
From: Roberto Sassu roberto.sassu@huawei.com
Introduce the Integrity Digest Cache, 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 data or metadata.
The Integrity Digest Cache aims to be used by IMA and (later) by EVM to verify accessed files. It is not an LSM on its own, and requires IMA (when the feature is enabled) to reserve additional space in the inode and file descriptor security blobs and to call digest_cache_do_init() for registering additional LSM hooks.
The additional space in the inode security blob is used for storing the digest_cache_security structure, which contains two digest cache pointers: dig_owner, set if the digest cache was created from that inode; dig_user, set if the digest cache was used for verifying that inode. An inode security blob has both pointers set if digests are extracted from that inode, and the inode itself is verified with another inode. Check and assignment of those pointers are protected respectively with dig_owner_mutex and dig_user_mutex.
The additional space in the file descriptor security blob is used to store the digest cache pointer and to annotate the digest cache with the integrity information the digest cache is created from. It also makes it possible to identify from IMA hooks the file descriptors opened by the Integrity Digest Cache and to properly nest the mutex, by calling the new function digest_cache_opened_fd().
The only way for a user of the Integrity Digest Cache to obtain a digest cache is to call digest_cache_get(). The latter first checks if dig_user in the security blob of the passed inode is not NULL and, in that case, immediately returns the pointer after incrementing the digest cache reference count.
If dig_user is NULL, digest_cache_get() calls digest_cache_new(), which determines from which path the digest cache should be retrieved. If the default path (defined in the kernel config, or at run-time) is a file, digest_cache_new() uses it as the final destination. If the default path is a directory, the function attempts to read the security.digest_list xattr and, if found, uses its value as last path component. Lastly, if the xattr is not found or empty, it uses the directory as final destination. A subsequent patch will allow the holders of the returned digest cache to iteratively search a digest in each directory entry.
Finally, digest_cache_new() calls digest_cache_create(), which resolves the inode from the path, and checks if the dig_owner pointer is set. If it is, again, digest_cache_create() immediately returns after incrementing the digest cache reference count. If not, it calls digest_cache_alloc_init() to create a new digest cache. A subsequent patch will initialize it.
A holder of a digest cache can release its reference and consequently decrement the digest cache reference count by calling digest_cache_put(). If the reference count becomes zero, digest_cache_put() also calls digest_cache_free() to free the memory.
Inodes having either dig_owner and dig_user set are also holders of digest caches. Their reference is released when they are evicted from memory, by implementing the inode_free_security LSM hook. The inode_alloc_security LSM hook, instead, initializes the digest_cache_security structure.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- include/linux/digest_cache.h | 37 ++ include/uapi/linux/xattr.h | 3 + security/integrity/Kconfig | 1 + security/integrity/Makefile | 1 + security/integrity/digest_cache/Kconfig | 19 + security/integrity/digest_cache/Makefile | 7 + security/integrity/digest_cache/internal.h | 116 ++++++ security/integrity/digest_cache/main.c | 406 +++++++++++++++++++++ security/integrity/ima/ima.h | 1 + security/integrity/ima/ima_main.c | 10 +- 10 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 include/linux/digest_cache.h create mode 100644 security/integrity/digest_cache/Kconfig create mode 100644 security/integrity/digest_cache/Makefile create mode 100644 security/integrity/digest_cache/internal.h create mode 100644 security/integrity/digest_cache/main.c
diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h new file mode 100644 index 000000000000..1f88b61fb7cd --- /dev/null +++ b/include/linux/digest_cache.h @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Public API of the Integrity Digest Cache. + */ + +#ifndef _LINUX_DIGEST_CACHE_H +#define _LINUX_DIGEST_CACHE_H + +#include <linux/fs.h> + +#ifdef CONFIG_INTEGRITY_DIGEST_CACHE +/* Client API */ +struct digest_cache *digest_cache_get(struct file *file); +void digest_cache_put(struct digest_cache *digest_cache); +bool digest_cache_opened_fd(struct file *file); + +#else +static inline struct digest_cache *digest_cache_get(struct file *file) +{ + return NULL; +} + +static inline void digest_cache_put(struct digest_cache *digest_cache) +{ +} + +static inline bool digest_cache_opened_fd(struct file *file) +{ + return false; +} + +#endif /* CONFIG_INTEGRITY_DIGEST_CACHE */ +#endif /* _LINUX_DIGEST_CACHE_H */ 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/integrity/Kconfig b/security/integrity/Kconfig index 3c45f4f3455f..55b1311a48fa 100644 --- a/security/integrity/Kconfig +++ b/security/integrity/Kconfig @@ -132,5 +132,6 @@ config INTEGRITY_AUDIT
source "security/integrity/ima/Kconfig" source "security/integrity/evm/Kconfig" +source "security/integrity/digest_cache/Kconfig"
endif # if INTEGRITY diff --git a/security/integrity/Makefile b/security/integrity/Makefile index 92b63039c654..0fdabfc6c8ae 100644 --- a/security/integrity/Makefile +++ b/security/integrity/Makefile @@ -21,3 +21,4 @@ integrity-$(CONFIG_LOAD_PPC_KEYS) += platform_certs/efi_parser.o \ # The relative order of the 'ima' and 'evm' LSMs depends on the order below. obj-$(CONFIG_IMA) += ima/ obj-$(CONFIG_EVM) += evm/ +obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache/ diff --git a/security/integrity/digest_cache/Kconfig b/security/integrity/digest_cache/Kconfig new file mode 100644 index 000000000000..81f9d4ae749f --- /dev/null +++ b/security/integrity/digest_cache/Kconfig @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0 +config INTEGRITY_DIGEST_CACHE + bool "Integrity Digest Cache" + default n + help + This option enables a cache of reference digests (e.g. of file + data or metadata) to be used by IMA and (later) by EVM, to verify + accessed files. + + Digests can be extracted from supported data formats (e.g. a + TLV-based format). Support for more formats can be added through + third-party kernel modules. + +config DIGEST_LIST_DEFAULT_PATH + string + default "/etc/digest_lists" + help + Default path where the Integrity Digest Cache expects to find digest + lists. diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile new file mode 100644 index 000000000000..6a3f7cc6e106 --- /dev/null +++ b/security/integrity/digest_cache/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for building the Integrity Digest Cache. + +obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o + +digest_cache-y := main.o diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h new file mode 100644 index 000000000000..fa76ab2672ea --- /dev/null +++ b/security/integrity/digest_cache/internal.h @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Internal header of the Integrity Digest Cache. + */ + +#ifndef _DIGEST_CACHE_INTERNAL_H +#define _DIGEST_CACHE_INTERNAL_H + +#include <linux/lsm_hooks.h> +#include <linux/digest_cache.h> + +/** + * 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 loff_t inode_sec_offset; +extern loff_t file_sec_offset; +extern char *default_path_str; + +static inline struct digest_cache_security * +digest_cache_get_security_from_blob(void *inode_security) +{ + return inode_security + inode_sec_offset; +} + +static inline struct digest_cache_security * +digest_cache_get_security(const struct inode *inode) +{ + if (unlikely(!inode->i_security)) + return NULL; + + return digest_cache_get_security_from_blob(inode->i_security); +} + +static inline struct digest_cache * +digest_cache_ref(struct digest_cache *digest_cache) +{ + int ref_count = atomic_inc_return(&digest_cache->ref_count); + + pr_debug("Ref (+) digest cache %s (ref count: %d)\n", + digest_cache->path_str, ref_count); + return digest_cache; +} + +static inline struct digest_cache * +digest_cache_unref(struct digest_cache *digest_cache) +{ + bool ref_is_zero; + + /* Unreliable ref. count, but cannot decrement before print (UAF). */ + pr_debug("Ref (-) digest cache %s (ref count: %d)\n", + digest_cache->path_str, + atomic_read(&digest_cache->ref_count) - 1); + + ref_is_zero = atomic_dec_and_test(&digest_cache->ref_count); + 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 + file_sec_offset; + *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 + file_sec_offset; + return *digest_cache_sec; +} + +/* main.c */ +struct digest_cache *digest_cache_create(struct dentry *dentry, + struct path *default_path, + struct path *digest_list_path, + char *path_str, char *filename); +int __init digest_cache_do_init(const struct lsm_id *lsm_id, + loff_t inode_offset, loff_t file_offset); + +#endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c new file mode 100644 index 000000000000..c644cdc2ebd7 --- /dev/null +++ b/security/integrity/digest_cache/main.c @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Implement the main code of the Integrity Digest Cache. + */ + +#define pr_fmt(fmt) "digest_cache: "fmt +#include <linux/namei.h> +#include <linux/xattr.h> + +#include "internal.h" + +static int digest_cache_enabled __ro_after_init; +static struct kmem_cache *digest_cache_cache __read_mostly; + +loff_t inode_sec_offset; +loff_t file_sec_offset; + +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 new digest cache 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_zalloc(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 + * @default_path: Path structure of the default path + * @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 and filename, 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. + * + * This function locks dig_owner_mutex to protect against concurrent requests + * to obtain a digest cache from the same inode. + * + * Return: A digest cache on success, NULL on error. + */ +struct digest_cache *digest_cache_create(struct dentry *dentry, + struct path *default_path, + struct path *digest_list_path, + char *path_str, char *filename) +{ + struct digest_cache *digest_cache = NULL; + struct digest_cache_security *dig_sec; + struct inode *inode = d_backing_inode(default_path->dentry); + int ret; + + if (S_ISDIR(inode->i_mode) && filename[0]) { + ret = vfs_path_lookup(default_path->dentry, default_path->mnt, + filename, LOOKUP_FOLLOW, + digest_list_path); + if (ret < 0) { + pr_debug("Cannot find digest list %s/%s\n", path_str, + filename); + return NULL; + } + + inode = d_backing_inode(digest_list_path->dentry); + + /* No support for nested directories. */ + if (!S_ISREG(inode->i_mode)) { + pr_debug("%s is not a regular file (no support for nested directories)\n", + digest_list_path->dentry->d_name.name); + return NULL; + } + } else { + digest_list_path->dentry = default_path->dentry; + digest_list_path->mnt = default_path->mnt; + path_get(digest_list_path); + } + + /* + * Cannot request a digest cache for the same inode the digest cache + * is populated from. + */ + if (d_backing_inode(dentry) == inode) { + pr_debug("Cannot request a digest cache for %s and create the digest cache from it\n", + dentry->d_name.name); + 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); + goto out; + } + + /* Ref. count is already 1 for this reference. */ + dig_sec->dig_owner = digest_cache_alloc_init(path_str, filename); + if (!dig_sec->dig_owner) + goto out; + + /* Increment ref. count for reference returned to the caller. */ + digest_cache = digest_cache_ref(dig_sec->dig_owner); +out: + mutex_unlock(&dig_sec->dig_owner_mutex); + 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 + * @digest_list_path: Path structure of the digest list (updated) + * + * 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 empty or not found, this + * function requests the creation of a digest cache on the parent directory. + * + * This function passes to the caller the path of the digest list from which + * the digest cache was created (file or parent directory), so that + * digest_cache_init() can reuse the same path, and to prevent the inode from + * being evicted between creation and initialization. + * + * Return: A digest cache on success, NULL on error. + */ +static struct digest_cache *digest_cache_new(struct dentry *dentry, + struct path *digest_list_path) +{ + 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) { + if (ret && ret != -ENODATA && ret != -EOPNOTSUPP) { + pr_debug("Cannot get %s xattr from file %s, ret: %d\n", + XATTR_NAME_DIGEST_LIST, dentry->d_name.name, + ret); + goto out; + } + + pr_debug("Digest list file name 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, + digest_list_path, default_path_str, + filename); +out: + path_put(&default_path); + return digest_cache; +} + +/** + * digest_cache_get - Get a digest cache for a given inode + * @file: File descriptor of the inode for which the digest cache will be used + * + * This function tries to find a digest cache from the inode security blob from + * the passed file descriptor (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. + * + * This function locks dig_user_mutex to protect against concurrent requests + * to obtain a digest cache for the same inode. + * + * Return: A digest cache on success, NULL otherwise. + */ +struct digest_cache *digest_cache_get(struct file *file) +{ + struct digest_cache_security *dig_sec; + struct digest_cache *digest_cache = NULL; + struct inode *inode = file_inode(file); + struct dentry *dentry = file_dentry(file); + struct path digest_list_path = { .dentry = NULL, .mnt = NULL }; + + if (!digest_cache_enabled) + return NULL; + + /* Do not allow recursion for now. */ + if (digest_cache_opened_fd(file)) + return NULL; + + dig_sec = digest_cache_get_security(inode); + 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, &digest_list_path); + + 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); + + if (digest_list_path.dentry) + path_put(&digest_list_path); + + 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); + +/** + * digest_cache_opened_fd - Determine if fd is opened by Digest Cache + * @file: File descriptor + * + * Determine whether or not the file descriptor was internally opened by the + * Integrity Digest Cache. + * + * Return: True if it is opened by us, false otherwise. + */ +bool digest_cache_opened_fd(struct file *file) +{ + struct digest_cache *digest_cache = digest_cache_from_file_sec(file); + + return !!digest_cache; +} +EXPORT_SYMBOL_GPL(digest_cache_opened_fd); + +/** + * 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_rcu - Release the digest cache references + * @inode_security: Inode security blob + * + * 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_rcu(void *inode_security) +{ + struct digest_cache_security *dig_sec; + + dig_sec = digest_cache_get_security_from_blob(inode_security); + 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_rcu, + digest_cache_inode_free_security_rcu), +}; + +/** + * digest_cache_do_init - Initialize the Integrity Digest Cache + * @lsm_id: ID of LSM registering the LSM hooks + * @inode_offset: Offset in the inode security blob + * @file_offset: Offset in the file security blob + * + * Initialize the Integrity Digest Cache, by instantiating a cache for the + * digest_cache structure and by registering the LSM hooks as part of the + * calling LSM. + */ +int __init digest_cache_do_init(const struct lsm_id *lsm_id, + loff_t inode_offset, loff_t file_offset) +{ + inode_sec_offset = inode_offset; + file_sec_offset = file_offset; + + digest_cache_cache = kmem_cache_create("digest_cache_cache", + sizeof(struct digest_cache), + 0, SLAB_PANIC, NULL); + + security_add_hooks(digest_cache_hooks, ARRAY_SIZE(digest_cache_hooks), + lsm_id); + + digest_cache_enabled = 1; + return 0; +} diff --git a/security/integrity/ima/ima.h b/security/integrity/ima/ima.h index 3c323ca213d4..2eaa7f224da4 100644 --- a/security/integrity/ima/ima.h +++ b/security/integrity/ima/ima.h @@ -23,6 +23,7 @@ #include <crypto/hash_info.h>
#include "../integrity.h" +#include "../digest_cache/internal.h"
enum ima_show_type { IMA_SHOW_BINARY, IMA_SHOW_BINARY_NO_FIELD_LEN, IMA_SHOW_BINARY_OLD_STRING_FMT, IMA_SHOW_ASCII }; diff --git a/security/integrity/ima/ima_main.c b/security/integrity/ima/ima_main.c index 06132cf47016..f6cc0c7a6244 100644 --- a/security/integrity/ima/ima_main.c +++ b/security/integrity/ima/ima_main.c @@ -1206,11 +1206,19 @@ static int __init init_ima_lsm(void) ima_iintcache_init(); security_add_hooks(ima_hooks, ARRAY_SIZE(ima_hooks), &ima_lsmid); init_ima_appraise_lsm(&ima_lsmid); + if (IS_ENABLED(CONFIG_INTEGRITY_DIGEST_CACHE)) + digest_cache_do_init(&ima_lsmid, ima_blob_sizes.lbs_inode + + sizeof(struct ima_iint_cache *), + ima_blob_sizes.lbs_file); return 0; }
struct lsm_blob_sizes ima_blob_sizes __ro_after_init = { - .lbs_inode = sizeof(struct ima_iint_cache *), + .lbs_inode = sizeof(struct ima_iint_cache *) +#ifdef CONFIG_INTEGRITY_DIGEST_CACHE + + sizeof(struct digest_cache_security), + .lbs_file = sizeof(struct digest_cache *), +#endif };
DEFINE_LSM(ima) = {
From: Roberto Sassu roberto.sassu@huawei.com
Introduce digest_cache_init() to initialize created digest caches. Since initialization happens after releasing both the dig_owner_mutex and dig_user_mutex locks (to avoid a lock inversion with VFS locks), any caller of digest_cache_get() can potentially be in charge of initializing them, provided that it got the digest list path from digest_cache_new().
Introduce the INIT_STARTED flag, to atomically determine whether the digest cache is being initialized and eventually do it if the flag is not yet set.
Introduce the INIT_IN_PROGRESS flag, for the other callers to wait until the caller in charge of the initialization finishes initializing the digest cache. Set INIT_IN_PROGRESS in digest_cache_create() and clear it in digest_cache_init(). Finally, call clear_and_wake_up_bit() to wake up the other callers.
Finally, introduce the INVALID flag, to let the callers which didn't initialize the digest cache know that an error occurred during initialization and, consequently, prevent them from using that digest cache.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- security/integrity/digest_cache/internal.h | 8 ++++ security/integrity/digest_cache/main.c | 48 ++++++++++++++++++++++ 2 files changed, 56 insertions(+)
diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index fa76ab2672ea..29bf98a974f3 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -13,6 +13,11 @@ #include <linux/lsm_hooks.h> #include <linux/digest_cache.h>
+/* Digest cache bits in flags. */ +#define INIT_IN_PROGRESS 0 /* Digest cache being initialized. */ +#define INIT_STARTED 1 /* Digest cache init started. */ +#define INVALID 2 /* Digest cache marked as invalid. */ + /** * struct digest_cache - Digest cache * @ref_count: Number of references to the digest cache @@ -110,6 +115,9 @@ struct digest_cache *digest_cache_create(struct dentry *dentry, struct path *default_path, struct path *digest_list_path, char *path_str, char *filename); +struct digest_cache *digest_cache_init(struct dentry *dentry, + struct path *digest_list_path, + struct digest_cache *digest_cache); int __init digest_cache_do_init(const struct lsm_id *lsm_id, loff_t inode_offset, loff_t file_offset);
diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index c644cdc2ebd7..6e94cff2b0dc 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -156,6 +156,9 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
/* Increment ref. count for reference returned to the caller. */ digest_cache = digest_cache_ref(dig_sec->dig_owner); + + /* Make other digest cache requestors wait until creation complete. */ + set_bit(INIT_IN_PROGRESS, &digest_cache->flags); out: mutex_unlock(&dig_sec->dig_owner_mutex); return digest_cache; @@ -238,6 +241,47 @@ static struct digest_cache *digest_cache_new(struct dentry *dentry, return digest_cache; }
+/** + * digest_cache_init - Initialize 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 + * @digest_cache: Digest cache to initialize + * + * This function checks if the INIT_STARTED digest cache flag is set. If it is, + * or the caller didn't provide the digest list path, it waits until the caller + * that saw INIT_STARTED unset and had the path completes the initialization. + * + * The latter sets INIT_STARTED (atomically), performs the initialization, + * clears the INIT_IN_PROGRESS digest cache flag, and wakes up the other + * callers. + * + * Return: A valid and initialized digest cache on success, NULL otherwise. + */ +struct digest_cache *digest_cache_init(struct dentry *dentry, + struct path *digest_list_path, + struct digest_cache *digest_cache) +{ + /* Wait for digest cache initialization. */ + if (!digest_list_path->dentry || + test_and_set_bit(INIT_STARTED, &digest_cache->flags)) { + wait_on_bit(&digest_cache->flags, INIT_IN_PROGRESS, + TASK_UNINTERRUPTIBLE); + goto out; + } + + /* Notify initialization complete. */ + clear_and_wake_up_bit(INIT_IN_PROGRESS, &digest_cache->flags); +out: + if (test_bit(INVALID, &digest_cache->flags)) { + pr_debug("Digest cache %s is invalid, don't return it\n", + digest_cache->path_str); + digest_cache_put(digest_cache); + digest_cache = NULL; + } + + return digest_cache; +} + /** * digest_cache_get - Get a digest cache for a given inode * @file: File descriptor of the inode for which the digest cache will be used @@ -287,6 +331,10 @@ struct digest_cache *digest_cache_get(struct file *file)
mutex_unlock(&dig_sec->dig_user_mutex);
+ if (digest_cache) + digest_cache = digest_cache_init(dentry, &digest_list_path, + digest_cache); + if (digest_list_path.dentry) path_put(&digest_list_path);
From: Roberto Sassu roberto.sassu@huawei.com
Create the digest_cache directory in <securityfs>/integrity, and add the default_path file, to let root change/read the default path (file or directory) from where digest lists are looked up.
An RW semaphore prevents the default path from changing while digest_list_new() and read_default_path() are executed, so that those read a stable value. Multiple digest_list_new() and read_default_path() calls, instead, can be done in parallel, since they are the readers.
Changing the default path does not affect digest caches created with the old path.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- security/integrity/digest_cache/Kconfig | 4 + security/integrity/digest_cache/Makefile | 2 +- security/integrity/digest_cache/internal.h | 4 + security/integrity/digest_cache/main.c | 10 +- security/integrity/digest_cache/secfs.c | 104 +++++++++++++++++++++ security/integrity/ima/ima_fs.c | 6 ++ 6 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 security/integrity/digest_cache/secfs.c
diff --git a/security/integrity/digest_cache/Kconfig b/security/integrity/digest_cache/Kconfig index 81f9d4ae749f..a4bfa8287b8d 100644 --- a/security/integrity/digest_cache/Kconfig +++ b/security/integrity/digest_cache/Kconfig @@ -17,3 +17,7 @@ config DIGEST_LIST_DEFAULT_PATH help Default path where the Integrity Digest Cache expects to find digest lists. + + 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. diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index 6a3f7cc6e106..c351186d4e1e 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -4,4 +4,4 @@
obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o
-digest_cache-y := main.o +digest_cache-y := main.o secfs.o diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index 29bf98a974f3..82d9c894c8fc 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -52,6 +52,7 @@ struct digest_cache_security { extern loff_t inode_sec_offset; extern loff_t file_sec_offset; extern char *default_path_str; +extern struct rw_semaphore default_path_sem;
static inline struct digest_cache_security * digest_cache_get_security_from_blob(void *inode_security) @@ -121,4 +122,7 @@ struct digest_cache *digest_cache_init(struct dentry *dentry, int __init digest_cache_do_init(const struct lsm_id *lsm_id, loff_t inode_offset, loff_t file_offset);
+/* secfs.c */ +int __init digest_cache_secfs_init(struct dentry *dir); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index 6e94cff2b0dc..6724471914da 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -21,6 +21,9 @@ loff_t file_sec_offset;
char *default_path_str = CONFIG_DIGEST_LIST_DEFAULT_PATH;
+/* Protects default_path_str. */ +struct rw_semaphore default_path_sem; + /** * digest_cache_alloc_init - Allocate and initialize a new digest cache * @path_str: Path string of the digest list @@ -321,9 +324,12 @@ struct digest_cache *digest_cache_get(struct file *file)
/* Serialize accesses to inode for which the digest cache is used. */ mutex_lock(&dig_sec->dig_user_mutex); - if (!dig_sec->dig_user) + if (!dig_sec->dig_user) { + down_read(&default_path_sem); /* Consume extra reference from digest_cache_create(). */ dig_sec->dig_user = digest_cache_new(dentry, &digest_list_path); + up_read(&default_path_sem); + }
if (dig_sec->dig_user) /* Increment ref. count for reference returned to the caller. */ @@ -439,6 +445,8 @@ static struct security_hook_list digest_cache_hooks[] __ro_after_init = { int __init digest_cache_do_init(const struct lsm_id *lsm_id, loff_t inode_offset, loff_t file_offset) { + init_rwsem(&default_path_sem); + inode_sec_offset = inode_offset; file_sec_offset = file_offset;
diff --git a/security/integrity/digest_cache/secfs.c b/security/integrity/digest_cache/secfs.c new file mode 100644 index 000000000000..f158233f3492 --- /dev/null +++ b/security/integrity/digest_cache/secfs.c @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Implement the securityfs interface of the Integrity Digest Cache. + */ + +#define pr_fmt(fmt) "digest_cache: "fmt +#include <linux/security.h> + +#include "internal.h" + +static struct dentry *digest_cache_dir; +static struct dentry *default_path_dentry; + +/** + * write_default_path - Write default path + * @file: File descriptor of the securityfs file + * @buf: User space buffer + * @datalen: Amount of data to write + * @ppos: Current position in the file + * + * This function sets the new default path where digest lists can be found. + * Can be either a regular file or a directory. + * + * Return: Length of path written on success, a POSIX error code otherwise. + */ +static ssize_t write_default_path(struct file *file, const char __user *buf, + size_t datalen, loff_t *ppos) +{ + char *new_default_path_str; + + new_default_path_str = memdup_user_nul(buf, datalen); + if (IS_ERR(new_default_path_str)) + return PTR_ERR(new_default_path_str); + + down_write(&default_path_sem); + kfree_const(default_path_str); + default_path_str = new_default_path_str; + up_write(&default_path_sem); + return datalen; +} + +/** + * read_default_path - Read default path + * @file: File descriptor of the securityfs file + * @buf: User space buffer + * @datalen: Amount of data to read + * @ppos: Current position in the file + * + * This function returns the current default path where digest lists can be + * found. Can be either a regular file or a directory. + * + * Return: Length of path read on success, a POSIX error code otherwise. + */ +static ssize_t read_default_path(struct file *file, char __user *buf, + size_t datalen, loff_t *ppos) +{ + int ret; + + down_read(&default_path_sem); + ret = simple_read_from_buffer(buf, datalen, ppos, default_path_str, + strlen(default_path_str) + 1); + up_read(&default_path_sem); + return ret; +} + +static const struct file_operations default_path_ops = { + .open = generic_file_open, + .write = write_default_path, + .read = read_default_path, + .llseek = generic_file_llseek, +}; + +/** + * digest_cache_secfs_init - Initialize the securityfs interface + * @dir: Directory entry provided by the calling LSM + * + * This function initializes the securityfs interfaces, for configuration + * by user space. + * + * It creates 'default_path', allowing user space to change the default + * directory where digest lists are searched. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int __init digest_cache_secfs_init(struct dentry *dir) +{ + digest_cache_dir = securityfs_create_dir("digest_cache", dir); + if (IS_ERR(digest_cache_dir)) + return PTR_ERR(digest_cache_dir); + + default_path_dentry = securityfs_create_file("default_path", 0660, + digest_cache_dir, NULL, + &default_path_ops); + if (IS_ERR(default_path_dentry)) { + securityfs_remove(digest_cache_dir); + return PTR_ERR(default_path_dentry); + } + + return 0; +} diff --git a/security/integrity/ima/ima_fs.c b/security/integrity/ima/ima_fs.c index e4a79a9b2d58..be9e374e2cef 100644 --- a/security/integrity/ima/ima_fs.c +++ b/security/integrity/ima/ima_fs.c @@ -614,6 +614,12 @@ int __init ima_fs_init(void) goto out; }
+ if (IS_ENABLED(CONFIG_INTEGRITY_DIGEST_CACHE)) { + ret = digest_cache_secfs_init(integrity_dir); + if (ret < 0) + goto out; + } + return 0; out: securityfs_remove(ima_policy);
From: Roberto Sassu roberto.sassu@huawei.com
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(), digest_cache_htable_add() and digest_cache_htable_lookup() to the Parser API, so that digest list parsers can allocate the hash tables and add/lookup extracted digests.
Add digest_cache_htable_free(), to let the Integrity Digest Cache free the hash tables at the time a digest cache is freed.
Add digest_cache_lookup() to the Client API, to let users of the Integrity Digest Cache search a digest in a digest cache and, in a subsequent patch, to search it in the digest caches for each directory entry.
digest_cache_lookup() returns a digest cache reference that must be released by calling digest_cache_put().
Finally, introduce 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 roberto.sassu@huawei.com --- include/linux/digest_cache.h | 39 ++++ security/integrity/digest_cache/Kconfig | 11 + security/integrity/digest_cache/Makefile | 2 +- security/integrity/digest_cache/htable.c | 251 +++++++++++++++++++++ security/integrity/digest_cache/internal.h | 36 +++ security/integrity/digest_cache/main.c | 3 + 6 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 security/integrity/digest_cache/htable.c
diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h index 1f88b61fb7cd..59a42c04cbb8 100644 --- a/include/linux/digest_cache.h +++ b/include/linux/digest_cache.h @@ -11,12 +11,25 @@ #define _LINUX_DIGEST_CACHE_H
#include <linux/fs.h> +#include <crypto/hash_info.h>
#ifdef CONFIG_INTEGRITY_DIGEST_CACHE /* Client API */ struct digest_cache *digest_cache_get(struct file *file); void digest_cache_put(struct digest_cache *digest_cache); bool digest_cache_opened_fd(struct file *file); +struct digest_cache *digest_cache_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo); + +/* Parser API */ +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);
#else static inline struct digest_cache *digest_cache_get(struct file *file) @@ -33,5 +46,31 @@ static inline bool digest_cache_opened_fd(struct file *file) return false; }
+static inline struct digest_cache * +digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo) +{ + return NULL; +} + +static inline int digest_cache_htable_init(struct digest_cache *digest_cache, + u64 num_digests, enum hash_algo algo) +{ + return -EOPNOTSUPP; +} + +static inline int digest_cache_htable_add(struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo) +{ + return -EOPNOTSUPP; +} + +static inline int digest_cache_htable_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo) +{ + return -EOPNOTSUPP; +} + #endif /* CONFIG_INTEGRITY_DIGEST_CACHE */ #endif /* _LINUX_DIGEST_CACHE_H */ diff --git a/security/integrity/digest_cache/Kconfig b/security/integrity/digest_cache/Kconfig index a4bfa8287b8d..419011fb52c9 100644 --- a/security/integrity/digest_cache/Kconfig +++ b/security/integrity/digest_cache/Kconfig @@ -21,3 +21,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/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index c351186d4e1e..0092c913979d 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -4,4 +4,4 @@
obj-$(CONFIG_INTEGRITY_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/integrity/digest_cache/htable.c b/security/integrity/digest_cache/htable.c new file mode 100644 index 000000000000..8aa6d50a0cb5 --- /dev/null +++ b/security/integrity/digest_cache/htable.c @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * 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 hash table + * @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; + unsigned int i; + + if (!num_digests) + return -EINVAL; + + 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; +} +EXPORT_SYMBOL_GPL(digest_cache_htable_init); + +/** + * digest_cache_htable_add - Add a new digest to the digest cache + * @digest_cache: Digest cache + * @digest: Digest to add + * @algo: Algorithm of the digest + * + * This function 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; +} +EXPORT_SYMBOL_GPL(digest_cache_htable_add); + +/** + * 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 digest cache. + * + * Return: Zero if the digest is found, a POSIX error code otherwise. + */ +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 = hash_digest_size[algo]; + int search_depth = 0, ret = -ENOENT; + + h = lookup_htable(digest_cache, algo); + if (!h) + goto out; + + 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++; + } +out: + pr_debug("Cache miss for file %s, digest %s:%*phN not in digest cache %s\n", + dentry->d_name.name, hash_algo_name[algo], digest_len, digest, + digest_cache->path_str); + return ret; +} +EXPORT_SYMBOL_GPL(digest_cache_htable_lookup); + +/** + * 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(). + * + * Return: A digest cache reference the digest is found, NULL if not. + */ +struct digest_cache *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); + if (ret < 0) + return NULL; + + return digest_cache_ref(digest_cache); +} +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; + unsigned 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/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index 82d9c894c8fc..e14343e96caa 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -18,8 +18,40 @@ #define INIT_STARTED 1 /* Digest cache init started. */ #define INVALID 2 /* Digest cache marked as invalid. */
+/** + * 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[]; +}; + +/** + * 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 data 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 @@ -27,6 +59,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; @@ -125,4 +158,7 @@ int __init digest_cache_do_init(const struct lsm_id *lsm_id, /* secfs.c */ int __init digest_cache_secfs_init(struct dentry *dir);
+/* htable.c */ +void digest_cache_htable_free(struct digest_cache *digest_cache); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index 6724471914da..ebc5dc09a62b 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -51,6 +51,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)); @@ -66,6 +67,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: Roberto Sassu roberto.sassu@huawei.com
Allow kernel modules to register/deregister new digest list parsers, respectively through digest_cache_register_parser() and digest_cache_unregister_parser().
Those functions pass the new parser structure holding the linked list pointers and a parsing function with the new type parser_func.
Introduce digest_cache_parse_digest_list(), which determines the desired parser from the file name, looks up the parser among the registered ones with lookup_get_parser(), calls the parser-specific function along with the digest list data read by the kernel, and finally releases the kernel module reference with put_parser().
The expected digest list file name format is:
[<seq num>-]<digest list format>-<digest list name>
<seq-num>- is an optional prefix to impose in which order digest lists in a directory should be parsed.
Introduce load_parser() to load a kernel module containing a parser for the requested digest list format (compressed kernel modules are supported). Kernel modules are searched in the /lib/modules/<kernel ver>/security/integrity/digest_cache directory.
load_parser() calls ksys_finit_module() to load a kernel module directly from the kernel. request_module() cannot be used at this point, since the reference digests of modprobe and the linked libraries (required for IMA appraisal) might not be yet available, resulting in modprobe execution being denied.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- include/linux/digest_cache.h | 38 +++ security/integrity/digest_cache/Kconfig | 1 + security/integrity/digest_cache/Makefile | 4 +- security/integrity/digest_cache/internal.h | 5 + security/integrity/digest_cache/parsers.c | 257 +++++++++++++++++++++ 5 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 security/integrity/digest_cache/parsers.c
diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h index 59a42c04cbb8..a9d731990b7c 100644 --- a/include/linux/digest_cache.h +++ b/include/linux/digest_cache.h @@ -13,6 +13,32 @@ #include <linux/fs.h> #include <crypto/hash_info.h>
+struct digest_cache; + +/** + * typedef parser_func - Function to parse digest lists + * + * Define a function type to parse digest lists. + */ +typedef int (*parser_func)(struct digest_cache *digest_cache, const u8 *data, + size_t data_len); + +/** + * struct parser - Structure to store a function pointer to parse digest list + * @list: Linked list + * @owner: Kernel module owning the parser + * @name: Parser name (must match the format in the digest list file name) + * @func: Function pointer for parsing + * + * This structure stores a function pointer to parse a digest list. + */ +struct parser { + struct list_head list; + struct module *owner; + const char name[NAME_MAX + 1]; + parser_func func; +}; + #ifdef CONFIG_INTEGRITY_DIGEST_CACHE /* Client API */ struct digest_cache *digest_cache_get(struct file *file); @@ -30,6 +56,8 @@ int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest, int digest_cache_htable_lookup(struct dentry *dentry, struct digest_cache *digest_cache, u8 *digest, enum hash_algo algo); +int digest_cache_register_parser(struct parser *parser); +void digest_cache_unregister_parser(struct parser *parser);
#else static inline struct digest_cache *digest_cache_get(struct file *file) @@ -72,5 +100,15 @@ static inline int digest_cache_htable_lookup(struct dentry *dentry, return -EOPNOTSUPP; }
+static inline int digest_cache_register_parser(const char *name, + parser_func func) +{ + return -EOPNOTSUPP; +} + +static inline void digest_cache_unregister_parser(const char *name) +{ +} + #endif /* CONFIG_INTEGRITY_DIGEST_CACHE */ #endif /* _LINUX_DIGEST_CACHE_H */ diff --git a/security/integrity/digest_cache/Kconfig b/security/integrity/digest_cache/Kconfig index 419011fb52c9..65c07110911b 100644 --- a/security/integrity/digest_cache/Kconfig +++ b/security/integrity/digest_cache/Kconfig @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 config INTEGRITY_DIGEST_CACHE bool "Integrity Digest Cache" + select MODULE_DECOMPRESS if MODULE_COMPRESS default n help This option enables a cache of reference digests (e.g. of file diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index 0092c913979d..d68cae690241 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -4,4 +4,6 @@
obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o
-digest_cache-y := main.o secfs.o htable.o +digest_cache-y := main.o secfs.o htable.o parsers.o + +CFLAGS_parsers.o += -DPARSERS_DIR="$(MODLIB)/kernel/security/integrity/digest_cache/parsers" diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index e14343e96caa..e178549f9ff9 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -161,4 +161,9 @@ int __init digest_cache_secfs_init(struct dentry *dir); /* htable.c */ void digest_cache_htable_free(struct digest_cache *digest_cache);
+/* parsers.c */ +int digest_cache_parse_digest_list(struct dentry *dentry, + struct digest_cache *digest_cache, + char *path_str, void *data, size_t data_len); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/parsers.c b/security/integrity/digest_cache/parsers.c new file mode 100644 index 000000000000..744c9742a44e --- /dev/null +++ b/security/integrity/digest_cache/parsers.c @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Implement the code to register digest list parsers. + */ + +#define pr_fmt(fmt) "digest_cache: "fmt +#include <linux/init_task.h> +#include <linux/namei.h> +#include <uapi/linux/module.h> +#include <linux/syscalls.h> +#include <linux/vmalloc.h> + +#include "internal.h" + +static DEFINE_MUTEX(parsers_mutex); +static LIST_HEAD(parsers); + +/** + * load_parser - Load kernel module containing a parser + * @dentry: Dentry of the inode for which the digest cache will be used + * @digest_cache: Digest cache + * @name: Name of the parser to load + * + * This function opens a kernel module file in + * /lib/modules/<kernel ver>/security/integrity/digest_cache, and executes + * ksys_finit_module() to load the kernel module. After kernel module + * initialization, the parser should be found in the linked list of parsers. + * + * Return: Zero if kernel module is loaded, a POSIX error code otherwise. + */ +static int load_parser(struct dentry *dentry, struct digest_cache *digest_cache, + const char *name) +{ + char *compress_suffix = ""; + char *parser_path; + struct file *file; + struct path path; + int ret = 0, flags = 0; + + /* Must be kept in sync with kernel/module/Kconfig. */ + if (IS_ENABLED(CONFIG_MODULE_COMPRESS_GZIP)) + compress_suffix = ".gz"; + else if (IS_ENABLED(CONFIG_MODULE_COMPRESS_XZ)) + compress_suffix = ".xz"; + else if (IS_ENABLED(CONFIG_MODULE_COMPRESS_ZSTD)) + compress_suffix = ".zst"; + + if (strlen(compress_suffix)) + flags |= MODULE_INIT_COMPRESSED_FILE; + + parser_path = kasprintf(GFP_KERNEL, "%s/%s.ko%s", PARSERS_DIR, name, + compress_suffix); + if (!parser_path) + return -ENOMEM; + + ret = kern_path(parser_path, 0, &path); + if (ret < 0) { + pr_debug("Cannot find path %s\n", parser_path); + goto out; + } + + /* Cannot request a digest cache for the kernel module inode. */ + if (d_backing_inode(dentry) == d_backing_inode(path.dentry)) { + pr_debug("Cannot request a digest cache for kernel module %s\n", + dentry->d_name.name); + ret = -EBUSY; + goto out; + } + + file = kernel_file_open(&path, O_RDONLY, &init_cred); + if (IS_ERR(file)) { + pr_debug("Cannot open %s\n", parser_path); + ret = PTR_ERR(file); + goto out_path; + } + + /* Mark the file descriptor as ours. */ + digest_cache_to_file_sec(file, digest_cache); + + ret = ksys_finit_module(file, "", flags); + if (ret < 0) + pr_debug("Cannot load module %s\n", parser_path); + + fput(file); +out_path: + path_put(&path); +out: + kfree(parser_path); + return ret; +} + +/** + * lookup_get_parser - Lookup and get parser among registered ones + * @name: Name of the parser to search + * + * This function searches a parser among the registered ones, and returns it + * to the caller, after incrementing the kernel module reference count. + * + * Must be called with parser_mutex held. + * + * Return: A parser structure if parser is found and available, NULL otherwise. + */ +static struct parser *lookup_get_parser(const char *name) +{ + struct parser *entry, *found = NULL; + + list_for_each_entry(entry, &parsers, list) { + if (!strcmp(entry->name, name) && + try_module_get(entry->owner)) { + found = entry; + break; + } + } + + return found; +} + +/** + * put_parser - Put parser + * @parser: Parser to put + * + * This function decreases the kernel module reference count. + */ +static void put_parser(struct parser *parser) +{ + module_put(parser->owner); +} + +/** + * digest_cache_parse_digest_list - Parse a digest list + * @dentry: Dentry of the inode for which the digest cache will be used + * @digest_cache: Digest cache + * @path_str: Path string of the digest list + * @data: Data to parse + * @data_len: Length of @data + * + * This function selects a parser for a digest list depending on its file name, + * and calls the appropriate parsing function. It expects the file name to be + * in the format: [<seq num>-]<digest list format>-<digest list name>. + * <seq num>- is optional. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_cache_parse_digest_list(struct dentry *dentry, + struct digest_cache *digest_cache, + char *path_str, void *data, size_t data_len) +{ + char *filename, *format, *next_sep; + struct parser *parser; + char format_buf[sizeof(parser->name)]; + int ret = -EINVAL; + + filename = strrchr(path_str, '/'); + if (!filename) + return ret; + + filename++; + format = filename; + + /* + * Since we expect that all files start with a digest list format, this + * check is reliable to detect <seq num>. + */ + if (filename[0] >= '0' && filename[0] <= '9') { + format = strchr(filename, '-'); + if (!format) + return ret; + + format++; + } + + next_sep = strchr(format, '-'); + if (!next_sep || next_sep - format >= sizeof(format_buf)) + return ret; + + snprintf(format_buf, sizeof(format_buf), "%.*s", + (int)(next_sep - format), format); + + pr_debug("Parsing %s, format: %s, size: %ld\n", path_str, format_buf, + data_len); + + mutex_lock(&parsers_mutex); + parser = lookup_get_parser(format_buf); + mutex_unlock(&parsers_mutex); + + if (!parser) { + load_parser(dentry, digest_cache, format_buf); + + mutex_lock(&parsers_mutex); + parser = lookup_get_parser(format_buf); + mutex_unlock(&parsers_mutex); + + if (!parser) { + pr_debug("Digest list parser %s not found\n", + format_buf); + return -ENOENT; + } + } + + ret = parser->func(digest_cache, data, data_len); + put_parser(parser); + + return ret; +} + +/** + * digest_cache_register_parser - Register new parser + * @parser: Parser structure to register + * + * This function searches the parser name among the registered ones and, if not + * found, appends the parser to the linked list of parsers. + * + * Return: Zero on success, -EEXIST if a parser with the same name exists. + */ +int digest_cache_register_parser(struct parser *parser) +{ + struct parser *p; + int ret = 0; + + mutex_lock(&parsers_mutex); + p = lookup_get_parser(parser->name); + if (p) { + put_parser(p); + ret = -EEXIST; + goto out; + } + + list_add_tail(&parser->list, &parsers); +out: + pr_debug("Digest list parser '%s' %s registered\n", parser->name, + (ret < 0) ? "cannot be" : "successfully"); + + mutex_unlock(&parsers_mutex); + return ret; +} +EXPORT_SYMBOL_GPL(digest_cache_register_parser); + +/** + * digest_cache_unregister_parser - Unregister parser + * @parser: Parser structure to unregister + * + * This function removes the passed parser from the linked list of parsers. + */ +void digest_cache_unregister_parser(struct parser *parser) +{ + mutex_lock(&parsers_mutex); + list_del(&parser->list); + mutex_unlock(&parsers_mutex); + + pr_debug("Digest list parser '%s' successfully unregistered\n", + parser->name); +} +EXPORT_SYMBOL_GPL(digest_cache_unregister_parser);
Hi--
On 11/19/24 2:49 AM, Roberto Sassu wrote:
+/**
- struct parser - Structure to store a function pointer to parse digest list
- @list: Linked list
- @owner: Kernel module owning the parser
- @name: Parser name (must match the format in the digest list file name)
- @func: Function pointer for parsing
- This structure stores a function pointer to parse a digest list.
- */
+struct parser {
- struct list_head list;
- struct module *owner;
- const char name[NAME_MAX + 1];
- parser_func func;
+};
I would make the struct name not so generic -- maybe digest_parser ...
thanks.
On Tue, 2024-11-19 at 08:46 -0800, Randy Dunlap wrote:
Hi--
On 11/19/24 2:49 AM, Roberto Sassu wrote:
+/**
- struct parser - Structure to store a function pointer to parse digest list
- @list: Linked list
- @owner: Kernel module owning the parser
- @name: Parser name (must match the format in the digest list file name)
- @func: Function pointer for parsing
- This structure stores a function pointer to parse a digest list.
- */
+struct parser {
- struct list_head list;
- struct module *owner;
- const char name[NAME_MAX + 1];
- parser_func func;
+};
I would make the struct name not so generic -- maybe digest_parser ...
Hi
sure, thanks for the suggestion!
Roberto
On Tue, Nov 19, 2024 at 11:49:14AM +0100, Roberto Sassu wrote:
From: Roberto Sassu roberto.sassu@huawei.com Introduce load_parser() to load a kernel module containing a parser for the requested digest list format (compressed kernel modules are supported). Kernel modules are searched in the /lib/modules/<kernel ver>/security/integrity/digest_cache directory.
load_parser() calls ksys_finit_module() to load a kernel module directly from the kernel. request_module() cannot be used at this point, since the reference digests of modprobe and the linked libraries (required for IMA appraisal) might not be yet available, resulting in modprobe execution being denied.
You are doing a full solution implementation of loading modules in-kernel. Appraisals of modules is just part of the boot process, some module loading may need firmware to loading to get some functinality to work for example some firmware to get a network device up or a GPU driver. So module loading alone is not the only thing which may require IMA appraisal, and this solution only addresses modules. There are other things which may be needed other than firmware, eBPF programs are another example.
It sounds more like you want to provide or extend LSM hooks fit your architecture and make kernel_read_file() LSM hooks optionally use it to fit this model.
Because this is just for a *phase* in boot, which you've caught because a catch-22 situaton, where you didn't have your parsers loaded. Which is just a reflection that you hit that snag. It doesn't prove all snags will be caught yet.
And you only want to rely on this .. in-kernel loading solution only early on boot, is there a way to change this over to enable regular operation later?
Luis
On Mon, 2024-11-25 at 15:53 -0800, Luis Chamberlain wrote:
On Tue, Nov 19, 2024 at 11:49:14AM +0100, Roberto Sassu wrote:
From: Roberto Sassu roberto.sassu@huawei.com Introduce load_parser() to load a kernel module containing a parser for the requested digest list format (compressed kernel modules are supported). Kernel modules are searched in the /lib/modules/<kernel ver>/security/integrity/digest_cache directory.
load_parser() calls ksys_finit_module() to load a kernel module directly from the kernel. request_module() cannot be used at this point, since the reference digests of modprobe and the linked libraries (required for IMA appraisal) might not be yet available, resulting in modprobe execution being denied.
You are doing a full solution implementation of loading modules in-kernel. Appraisals of modules is just part of the boot process, some module loading may need firmware to loading to get some functinality to work for example some firmware to get a network device up or a GPU driver. So module loading alone is not the only thing which may require IMA appraisal, and this solution only addresses modules. There are other things which may be needed other than firmware, eBPF programs are another example.
Firmware, eBPF programs and so on are supposed to be verified with digest lists (or alternative methods, such as file signatures), once the required digest list parsers are loaded.
The parser is an exceptional case, because user space cannot be executed at this point. Once the parsers are loaded, verification of everything else proceeds as normal. Fortunately, in most cases kernel modules are signed, so digest lists are not required to verify them.
It sounds more like you want to provide or extend LSM hooks fit your architecture and make kernel_read_file() LSM hooks optionally use it to fit this model.
As far as the LSM infrastructure is concerned, I'm not adding new LSM hooks, nor extending/modifying the existing ones. The operations the Integrity Digest Cache is doing match the usage expectation by LSM (net denying access, as discussed with Paul Moore).
The Integrity Digest Cache is supposed to be used as a supporting tool for other LSMs to do regular access control based on file data and metadata integrity. In doing that, it still needs the LSM infrastructure to notify about filesystem changes, and to store additional information in the inode and file descriptor security blobs.
The kernel_post_read_file LSM hook should be implemented by another LSM to verify the integrity of a digest list, when the Integrity Digest Cache calls kernel_read_file() to read that digest list. That LSM is also responsible to provide the result of the integrity verification to the Integrity Digest Cache, so that the latter can give this information back to whoever wants to do a digest lookup from that digest list and also wants to know whether or not the digest list was authentic.
Because this is just for a *phase* in boot, which you've caught because a catch-22 situaton, where you didn't have your parsers loaded. Which is just a reflection that you hit that snag. It doesn't prove all snags will be caught yet.
Yes, that didn't happen earlier, since all the parsers were compiled built-in in the kernel. The Integrity Digest Cache already has a deadlock avoidance mechanism for digest lists.
Supporting kernel modules opened the road for new deadlocks, since one can ask a digest list to verify a kernel module, but that digest list requires the same kernel module. That is why the in-kernel mechanism is 100% reliable, because the Integrity Digest Cache marks the file descriptors it opens, and can recognize them, when those file descriptors are passed back to it by other LSMs (e.g. through the kernel_post_read_file LSM hook).
And you only want to rely on this .. in-kernel loading solution only early on boot, is there a way to change this over to enable regular operation later?
User space can voluntarily load new digest list parsers, but the Integrity Digest Cache cannot rely on it to be done. Also, using request_module() does not seem a good idea, since it wouldn't allow the Integrity Digest Cache to mark the file descriptor of kernel modules, and thus the Integrity Digest Cache could not determine whether or not a deadlock is happening.
Thanks
Roberto
On Tue, Nov 26, 2024 at 11:25:07AM +0100, Roberto Sassu wrote:
On Mon, 2024-11-25 at 15:53 -0800, Luis Chamberlain wrote:
Firmware, eBPF programs and so on are supposed
Keyword: "supposed".
As far as the LSM infrastructure is concerned, I'm not adding new LSM hooks, nor extending/modifying the existing ones. The operations the Integrity Digest Cache is doing match the usage expectation by LSM (net denying access, as discussed with Paul Moore).
If modules are the only proven exception to your security model you are not making the case for it clearly.
The Integrity Digest Cache is supposed to be used as a supporting tool for other LSMs to do regular access control based on file data and metadata integrity. In doing that, it still needs the LSM infrastructure to notify about filesystem changes, and to store additional information in the inode and file descriptor security blobs.
The kernel_post_read_file LSM hook should be implemented by another LSM to verify the integrity of a digest list, when the Integrity Digest Cache calls kernel_read_file() to read that digest list.
If LSM folks *do* agree that this work is *suplementing* LSMS then sure, it was not clear from the commit logs. But then you need to ensure the parsers are special snowflakes which won't ever incur other additional kernel_read_file() calls.
Supporting kernel modules opened the road for new deadlocks, since one can ask a digest list to verify a kernel module, but that digest list requires the same kernel module. That is why the in-kernel mechanism is 100% reliable,
Are users of this infrastructure really in need of modules for these parsers?
Luis
On Tue, 2024-11-26 at 11:04 -0800, Luis Chamberlain wrote:
On Tue, Nov 26, 2024 at 11:25:07AM +0100, Roberto Sassu wrote:
On Mon, 2024-11-25 at 15:53 -0800, Luis Chamberlain wrote:
Firmware, eBPF programs and so on are supposed
Keyword: "supposed".
It depends if they are in a policy. They can also be verified with other methods, such as file signatures.
For eBPF programs we are also in a need for a better way to measure/appraise them.
As far as the LSM infrastructure is concerned, I'm not adding new LSM hooks, nor extending/modifying the existing ones. The operations the Integrity Digest Cache is doing match the usage expectation by LSM (net denying access, as discussed with Paul Moore).
If modules are the only proven exception to your security model you are not making the case for it clearly.
The Integrity Digest Cache is not implementing any security model, this is demanded to other LSMs which might decide to use the Integrity Digest Cache based on a policy.
If we want to be super picky, the ksys_finit_module() helper is not calling security_kernel_module_request(), which is done when using request_module(). On the other hand, ksys_finit_module() is not triggering user space, as the description of the function states (anyway, apologies for not bringing up this earlier).
Net this, and we can discuss if it is more appropriate to call the LSM hook, the helper does not introduce any exception since security_file_open() is called when the kernel opens the file descriptor, and security_kernel_read_file() and security_kernel_post_read_file() are called in the same way regardless if it is user space doing insmod or the kernel calling ksys_finit_module().
The only exception is that the Integrity Digest Cache is unable to verify the kernel modules containing the parsers, but I believe this is fine because they are verified with their appended signature.
If there are any other concerns I'm missing, please let me know.
The Integrity Digest Cache is supposed to be used as a supporting tool for other LSMs to do regular access control based on file data and metadata integrity. In doing that, it still needs the LSM infrastructure to notify about filesystem changes, and to store additional information in the inode and file descriptor security blobs.
The kernel_post_read_file LSM hook should be implemented by another LSM to verify the integrity of a digest list, when the Integrity Digest Cache calls kernel_read_file() to read that digest list.
If LSM folks *do* agree that this work is *suplementing* LSMS then sure, it was not clear from the commit logs. But then you need to ensure the parsers are special snowflakes which won't ever incur other additional kernel_read_file() calls.
The Integrity Digest Cache was originally called digest_cache LSM, but was renamed due to Paul's concern that it is not a proper LSM enforcing a security model. If you are interested, I gave a talk at LSS NA 2024:
https://www.youtube.com/watch?v=aNwlKYSksg8
Given that the Integrity Digest Cache could not be standalone and use the LSM infrastructure facilities, it is going to be directly integrated in IMA, although it is not strictly necessary.
I planned to support IPE and BPF LSM as other users.
Uhm, let me clarify your last sentence a bit.
Let's assume that IMA is asked to verify a parser, when invoked through the kernel_post_read_file hook. IMA is not handling the exception, and is calling digest_cache_get() as usual. Normally, this would succeed, but because digest_cache_get() realizes that the file descriptor passed as argument is marked (i.e. it was opened by the Integrity Digest Cache itself), it returns NULL.
That means that IMA falls back on another verification method, which is verifying the appended signature.
The most important design principle that I introduced is that users of the Integrity Digest Cache don't need to be aware of any exception, everything is handled by the Integrity Digest Cache itself.
The same occurs when a kernel read occurs with file ID READING_DIGEST_LIST (introduced in this patch set). Yes, I forbid specifying an IMA policy which requires the Integrity Digest Cache to verify digest lists, but due to the need of handling kernel modules I decided to handle the exceptions in the Integrity Digest Cache itself (this is why now I'm passing a file descriptor to digest_cache_get() instead of a dentry).
Now, I'm trying to follow you on the additional kernel_read_file() calls. I agree with you, if a parser tries to open again the file that is being verified it would cause a deadlock in IMA (since the inode mutex is already locked for verifying the original file).
In the Integrity Digest Cache itself, this is not going to happen, since the file being verified with a digest cache is known and an internal open of the same file fails. If it is really necessary, we can pass the information to the parsers so that they are aware, it is just an additional parameter.
However, I was assuming that a parser just receives the data read by the Integrity Digest Cache, and just calls the Parser API to add the extracted digests to the new digest cache. Also this can be discussed, but I guess there is no immediate need.
Supporting kernel modules opened the road for new deadlocks, since one can ask a digest list to verify a kernel module, but that digest list requires the same kernel module. That is why the in-kernel mechanism is 100% reliable,
Are users of this infrastructure really in need of modules for these parsers?
I planned to postpone this to later, and introduced two parsers built- in (TLV and RPM). However, due to Linus's concern regarding the RPM parser, I moved it out in a kernel module.
Also, a parser cannot be in user space, since the trust anchor is in the kernel (the public keys and the signature verification mechanism), it is not something that can be established in the initial ram disk since the Integrity Digest Cache will be continously used in the running system (maybe more parsers will be loaded on demand depending on requests from user space).
And finally, the parser cannot run in user space, since it would be at the same level of what the kernel is verifying.
Thanks
Roberto
On Wed, Nov 27, 2024 at 10:51:11AM +0100, Roberto Sassu wrote:
For eBPF programs we are also in a need for a better way to measure/appraise them.
I am confused now, I was under the impression this "Integrity Digest Cache" is just a special thing for LSMs, and so I was under the impression that kernel_read_file() lsm hook already would take care of eBPF programs.
Now, I'm trying to follow you on the additional kernel_read_file() calls. I agree with you, if a parser tries to open again the file that is being verified it would cause a deadlock in IMA (since the inode mutex is already locked for verifying the original file).
Just document this on the parser as a requirement.
Supporting kernel modules opened the road for new deadlocks, since one can ask a digest list to verify a kernel module, but that digest list requires the same kernel module. That is why the in-kernel mechanism is 100% reliable,
Are users of this infrastructure really in need of modules for these parsers?
I planned to postpone this to later, and introduced two parsers built- in (TLV and RPM). However, due to Linus's concern regarding the RPM parser, I moved it out in a kernel module.
OK this should be part of the commit log, ie that it is not desirable to have an rpm parser in-kernel for some users.
Luis
On Wed, 2024-11-27 at 11:53 -0800, Luis Chamberlain wrote:
On Wed, Nov 27, 2024 at 10:51:11AM +0100, Roberto Sassu wrote:
For eBPF programs we are also in a need for a better way to measure/appraise them.
I am confused now, I was under the impression this "Integrity Digest Cache" is just a special thing for LSMs, and so I was under the impression that kernel_read_file() lsm hook already would take care of eBPF programs.
Yes, the problem is that eBPF programs are transformed in user space before they are sent to the kernel:
https://lwn.net/Articles/977394/
The Integrity Digest Cache can be used for the measurement/appraisal of the initial eBPF ELF file, when they are accessed from the filesystem, but the resulting blob sent to the kernel will be different.
Now, I'm trying to follow you on the additional kernel_read_file() calls. I agree with you, if a parser tries to open again the file that is being verified it would cause a deadlock in IMA (since the inode mutex is already locked for verifying the original file).
Just document this on the parser as a requirement.
Ok, will do.
Supporting kernel modules opened the road for new deadlocks, since one can ask a digest list to verify a kernel module, but that digest list requires the same kernel module. That is why the in-kernel mechanism is 100% reliable,
Are users of this infrastructure really in need of modules for these parsers?
I planned to postpone this to later, and introduced two parsers built- in (TLV and RPM). However, due to Linus's concern regarding the RPM parser, I moved it out in a kernel module.
OK this should be part of the commit log, ie that it is not desirable to have an rpm parser in-kernel for some users.
I understand. Will add in the commit log.
Just to clarify, we are not talking about the full blown librpm in the kernel, but a 243 LOC that I rewrote to obtain only the information I need. I also formally verified it with pseudo/totally random data with Frama-C:
https://github.com/robertosassu/rpm-formal/blob/main/validate_rpm.c
Thanks
Roberto
On Thu, Nov 28, 2024 at 09:23:57AM +0100, Roberto Sassu wrote:
On Wed, 2024-11-27 at 11:53 -0800, Luis Chamberlain wrote:
On Wed, Nov 27, 2024 at 10:51:11AM +0100, Roberto Sassu wrote:
For eBPF programs we are also in a need for a better way to measure/appraise them.
I am confused now, I was under the impression this "Integrity Digest Cache" is just a special thing for LSMs, and so I was under the impression that kernel_read_file() lsm hook already would take care of eBPF programs.
Yes, the problem is that eBPF programs are transformed in user space before they are sent to the kernel:
That issue seems to be orthogonal to your eandeavor though, which just supplements LSMS, right?
Anyway, in case this helps:
The Rust folks faced some slighty related challenges with our CRC validations for symbols, our CRC are slapped on with genksyms but this relies on the source code and with Rust the compiler may do final touches to data. And so DWARF is being used [1].
Although I am not sure of the state of eBPF DWARF support, there is also BTF support [0] and most distros are relying on it to make live introspection easier, and the output is much smaller. So could DWARF or BTF information from eBPF programs be used by the verifier in similar way to verify eBPF programs?
Note that to support BTF implicates DWARF and the leap of faith for Rust modversions support is that most distros will support DWARF, and so BTF can become the norm [2].
[0] https://www.kernel.org/doc/html/latest/bpf/btf.html [1] https://lwn.net/Articles/986892/ [2] https://lwn.net/Articles/991719/
Luis
On Thu, 2024-11-28 at 12:40 -0800, Luis Chamberlain wrote:
On Thu, Nov 28, 2024 at 09:23:57AM +0100, Roberto Sassu wrote:
On Wed, 2024-11-27 at 11:53 -0800, Luis Chamberlain wrote:
On Wed, Nov 27, 2024 at 10:51:11AM +0100, Roberto Sassu wrote:
For eBPF programs we are also in a need for a better way to measure/appraise them.
I am confused now, I was under the impression this "Integrity Digest Cache" is just a special thing for LSMs, and so I was under the impression that kernel_read_file() lsm hook already would take care of eBPF programs.
Yes, the problem is that eBPF programs are transformed in user space before they are sent to the kernel:
That issue seems to be orthogonal to your eandeavor though, which just supplements LSMS, right?
Yes, correct, the Integrity Digest Cache would be used to search whatever digest was calculated by LSMs.
Thanks
Roberto
Anyway, in case this helps:
The Rust folks faced some slighty related challenges with our CRC validations for symbols, our CRC are slapped on with genksyms but this relies on the source code and with Rust the compiler may do final touches to data. And so DWARF is being used [1].
Although I am not sure of the state of eBPF DWARF support, there is also BTF support [0] and most distros are relying on it to make live introspection easier, and the output is much smaller. So could DWARF or BTF information from eBPF programs be used by the verifier in similar way to verify eBPF programs?
Note that to support BTF implicates DWARF and the leap of faith for Rust modversions support is that most distros will support DWARF, and so BTF can become the norm [2].
[0] https://www.kernel.org/doc/html/latest/bpf/btf.html [1] https://lwn.net/Articles/986892/ [2] https://lwn.net/Articles/991719/
Luis
From: Roberto Sassu roberto.sassu@huawei.com
Add digest_list_parse_tlv(), to parse TLV-formatted (Type Length Value) digest lists. Their structure is:
[field: DIGEST_LIST_ALGO, length, value] [field: DIGEST_LIST_NUM_ENTRIES, length, value] [field: DIGEST_LIST_ENTRY#1, length, value (below)] |- [DIGEST_LIST_ENTRY_DIGEST#1, length, file digest] |- [DIGEST_LIST_ENTRY_PATH#1, length, file path] [field: DIGEST_LIST_ENTRY#N, length, value (below)] |- [DIGEST_LIST_ENTRY_DIGEST#N, length, file digest] |- [DIGEST_LIST_ENTRY_PATH#N, length, file path]
DIGEST_LIST_ALGO and DIGEST_LIST_NUM_ENTRIES must have a fixed length respectively of sizeof(u16) and sizeof(u32).
The data of the DIGEST_LIST_ENTRY field are itself in TLV format, for which the DIGEST_LIST_ENTRY_DIGEST and DIGEST_LIST_ENTRY_PATH fields are defined.
Currently defined fields are sufficient for measurement/appraisal of file data. More fields will be introduced later for file metadata.
Introduce digest_list_callback() to handle the digest list fields, DIGEST_LIST_ALGO, DIGEST_LIST_NUM_ENTRIES and DIGEST_LIST_ENTRY, and the respective field parsers parse_digest_list_algo(), parse_digest_list_num_entries() and parse_digest_list_entry().
Introduce digest_list_entry_callback(), to handle the DIGEST_LIST_ENTRY 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 implemented 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 ====== --------------------------------------------------------------------------- 12 functions analyzed (out of 12): 100% coverage. In these functions, 177 statements reached (out of 191): 92% coverage. --------------------------------------------------------------------------- Some errors and warnings have been raised during the analysis: by the Eva analyzer: 0 errors 2 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 22 valid 0 unknown 0 invalid 22 total 100% of the logical properties reached have been proven. ---------------------------------------------------------------------------
The warnings are:
[eva] validate_tlv.c:256: Warning: this partitioning parameter cannot be evaluated safely on all states [eva] validate_tlv.c:284: Warning: this partitioning parameter cannot be evaluated safely on all states
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- include/uapi/linux/tlv_digest_list.h | 47 +++ security/integrity/digest_cache/Kconfig | 8 + security/integrity/digest_cache/Makefile | 1 + security/integrity/digest_cache/parsers/tlv.c | 341 ++++++++++++++++++ 4 files changed, 397 insertions(+) create mode 100644 include/uapi/linux/tlv_digest_list.h create mode 100644 security/integrity/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..f2031cd70e64 --- /dev/null +++ b/include/uapi/linux/tlv_digest_list.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * Copyright (C) 2017-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Export definitions of the tlv digest list. + */ + +#ifndef _UAPI_LINUX_TLV_DIGEST_LIST_H +#define _UAPI_LINUX_TLV_DIGEST_LIST_H + +#include <linux/types.h> + +#define FOR_EACH_DIGEST_LIST_FIELD(DIGEST_LIST_FIELD) \ + DIGEST_LIST_FIELD(DIGEST_LIST_ALGO) \ + DIGEST_LIST_FIELD(DIGEST_LIST_NUM_ENTRIES) \ + DIGEST_LIST_FIELD(DIGEST_LIST_ENTRY) \ + DIGEST_LIST_FIELD(DIGEST_LIST_FIELD__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_fields - Digest list fields + * + * Enumerates the digest list fields. + */ +enum digest_list_fields { + FOR_EACH_DIGEST_LIST_FIELD(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/integrity/digest_cache/Kconfig b/security/integrity/digest_cache/Kconfig index 65c07110911b..972bcf8bb765 100644 --- a/security/integrity/digest_cache/Kconfig +++ b/security/integrity/digest_cache/Kconfig @@ -33,3 +33,11 @@ config DIGEST_CACHE_HTABLE_DEPTH 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. + +config DIGEST_CACHE_TLV_PARSER + tristate "TLV digest list parser" + depends on INTEGRITY_DIGEST_CACHE + select TLV_PARSER + help + Add support for parsing TLV-formatted (Type Length Value) + digest list. diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index d68cae690241..3b42b20d1bc0 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -3,6 +3,7 @@ # Makefile for building the Integrity Digest Cache.
obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o +obj-$(CONFIG_DIGEST_CACHE_TLV_PARSER) += parsers/tlv.o
digest_cache-y := main.o secfs.o htable.o parsers.o
diff --git a/security/integrity/digest_cache/parsers/tlv.c b/security/integrity/digest_cache/parsers/tlv.c new file mode 100644 index 000000000000..31e407f0a43b --- /dev/null +++ b/security/integrity/digest_cache/parsers/tlv.c @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2017-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Parse a tlv digest list. + */ + +#define pr_fmt(fmt) "digest_cache TLV PARSER: "fmt +#include <linux/module.h> +#include <linux/tlv_parser.h> +#include <linux/digest_cache.h> +#include <uapi/linux/tlv_digest_list.h> + +#define kenter(FMT, ...) \ + pr_debug("==> %s(" FMT ")\n", __func__, ##__VA_ARGS__) +#define kleave(FMT, ...) \ + pr_debug("<== %s()" FMT "\n", __func__, ##__VA_ARGS__) + +static const char *digest_list_fields_str[] = { + FOR_EACH_DIGEST_LIST_FIELD(GENERATE_STRING) +}; + +static const char *digest_list_entry_fields_str[] = { + FOR_EACH_DIGEST_LIST_ENTRY_FIELD(GENERATE_STRING) +}; + +struct tlv_callback_data { + struct digest_cache *digest_cache; + enum hash_algo algo; +}; + +/** + * parse_digest_list_entry_digest - Parse DIGEST_LIST_ENTRY_DIGEST field + * @tlv_data: 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, + __u32 field_data_len) +{ + int ret; + + kenter(",%u,%u", 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 %u, 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: 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. + */ +static int parse_digest_list_entry_path(struct tlv_callback_data *tlv_data, + enum digest_list_entry_fields field, + const __u8 *field_data, + __u32 field_data_len) +{ + kenter(",%u,%u", field, field_data_len); + + kleave(" = 0"); + return 0; +} + +/** + * digest_list_entry_callback - DIGEST_LIST_ENTRY 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 (nested) data, and + * calls the appropriate parser. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int digest_list_entry_callback(void *callback_data, __u16 field, + const __u8 *field_data, + __u32 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: 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, __u32 field_data_len) +{ + __u16 algo; + int ret = 0; + + kenter(",%u,%u", field, field_data_len); + + if (field_data_len != sizeof(__u16)) { + pr_debug("Unexpected data length %u, expected %zu\n", + field_data_len, sizeof(__u16)); + ret = -EBADMSG; + goto out; + } + + algo = __be16_to_cpu(*(__u16 *)field_data); + + if (algo >= HASH_ALGO__LAST) { + pr_debug("Unexpected digest algo %u\n", algo); + ret = -EBADMSG; + 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_num_entries - Parse DIGEST_LIST_NUM_ENTRIES field + * @tlv_data: Callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This function parses the DIGEST_LIST_NUM_ENTRIES field (digest list entries). + * This field must appear after DIGEST_LIST_ALGO. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int parse_digest_list_num_entries(struct tlv_callback_data *tlv_data, + enum digest_list_fields field, + const __u8 *field_data, + __u32 field_data_len) +{ + __u32 num_entries; + int ret; + + kenter(",%u,%u", field, field_data_len); + + if (field_data_len != sizeof(__u32)) { + pr_debug("Unexpected data length %u, expected %zu\n", + field_data_len, sizeof(__u32)); + ret = -EBADMSG; + goto out; + } + + if (tlv_data->algo == HASH_ALGO__LAST) { + pr_debug("Digest algo not yet initialized\n"); + ret = -EBADMSG; + goto out; + } + + num_entries = __be32_to_cpu(*(__u32 *)field_data); + + ret = digest_cache_htable_init(tlv_data->digest_cache, num_entries, + tlv_data->algo); +out: + kleave(" = %d", ret); + return ret; +} + +/** + * parse_digest_list_entry - Parse DIGEST_LIST_ENTRY field + * @tlv_data: 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, __u32 field_data_len) +{ + int ret; + + kenter(",%u,%u", field, field_data_len); + + ret = tlv_parse(digest_list_entry_callback, tlv_data, field_data, + field_data_len, digest_list_entry_fields_str, + DIGEST_LIST_ENTRY_FIELD__LAST); + + kleave(" = %d", ret); + return ret; +} + +/** + * digest_list_callback - Digest list callback + * @callback_data: Callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This callback handles the digest list fields, and calls the appropriate + * parser. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int digest_list_callback(void *callback_data, __u16 field, + const __u8 *field_data, __u32 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_NUM_ENTRIES: + ret = parse_digest_list_num_entries(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. + */ +static 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, + }; + + return tlv_parse(digest_list_callback, &tlv_data, data, data_len, + digest_list_fields_str, DIGEST_LIST_FIELD__LAST); +} + +static struct parser tlv_parser = { + .name = "tlv", + .owner = THIS_MODULE, + .func = digest_list_parse_tlv, +}; + +static int __init tlv_parser_init(void) +{ + return digest_cache_register_parser(&tlv_parser); +} + +static void __exit tlv_parser_exit(void) +{ + digest_cache_unregister_parser(&tlv_parser); +} + +module_init(tlv_parser_init); +module_exit(tlv_parser_exit); + +MODULE_AUTHOR("Roberto Sassu"); +MODULE_DESCRIPTION("TLV digest list parser"); +MODULE_LICENSE("GPL"); +MODULE_VERSION("1.0.0");
From: Roberto Sassu roberto.sassu@huawei.com
Introduce digest_cache_populate() to populate the digest cache from a digest list. Call it from digest_cache_init() if the inode is a regular file.
It opens the file, marks it for internal use with digest_cache_to_file_sec(), and then schedules a work to read the content (with new file type READING_DIGEST_LIST). Scheduling a work solves the problem of kernel_read_file() returning -EINTR.
Once the work is done, this function calls digest_cache_strip_modsig() to strip a module-style appended signature, if present, and finally calls digest_cache_parse_digest_list() to parse the data.
Failing to populate a digest cache causes it to be marked as invalid and to not be returned by digest_cache_init(). Dig_owner however is kept, to avoid an excessive number of retries, which would probably not succeed either.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- include/linux/kernel_read_file.h | 1 + security/integrity/digest_cache/Makefile | 2 +- security/integrity/digest_cache/internal.h | 25 ++++++ security/integrity/digest_cache/main.c | 16 ++++ security/integrity/digest_cache/modsig.c | 66 +++++++++++++++ security/integrity/digest_cache/populate.c | 97 ++++++++++++++++++++++ 6 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 security/integrity/digest_cache/modsig.c create mode 100644 security/integrity/digest_cache/populate.c
diff --git a/include/linux/kernel_read_file.h b/include/linux/kernel_read_file.h index 90451e2e12bd..85f602e49e2f 100644 --- a/include/linux/kernel_read_file.h +++ b/include/linux/kernel_read_file.h @@ -14,6 +14,7 @@ id(KEXEC_INITRAMFS, kexec-initramfs) \ id(POLICY, security-policy) \ id(X509_CERTIFICATE, x509-certificate) \ + id(DIGEST_LIST, digest-list) \ id(MAX_ID, )
#define __fid_enumify(ENUM, dummy) READING_ ## ENUM, diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index 3b42b20d1bc0..3b81edea065b 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -5,6 +5,6 @@ obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o obj-$(CONFIG_DIGEST_CACHE_TLV_PARSER) += parsers/tlv.o
-digest_cache-y := main.o secfs.o htable.o parsers.o +digest_cache-y := main.o secfs.o htable.o parsers.o populate.o modsig.o
CFLAGS_parsers.o += -DPARSERS_DIR="$(MODLIB)/kernel/security/integrity/digest_cache/parsers" diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index e178549f9ff9..2171ea8423ff 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -18,6 +18,23 @@ #define INIT_STARTED 1 /* Digest cache init started. */ #define INVALID 2 /* Digest cache marked as invalid. */
+/** + * struct read_work - Structure to schedule reading a digest list + * @work: Work structure + * @file: File descriptor of the digest list to read + * @data: Digest list data (updated) + * @ret: Return value from kernel_read_file() (updated) + * + * This structure contains the necessary information to schedule reading a + * digest list. + */ +struct read_work { + struct work_struct work; + struct file *file; + void *data; + int ret; +}; + /** * struct digest_cache_entry - Entry of a digest cache hash table * @hnext: Pointer to the next element in the collision list @@ -166,4 +183,12 @@ int digest_cache_parse_digest_list(struct dentry *dentry, struct digest_cache *digest_cache, char *path_str, void *data, size_t data_len);
+/* populate.c */ +int digest_cache_populate(struct dentry *dentry, + struct digest_cache *digest_cache, + struct path *digest_list_path); + +/* modsig.c */ +size_t digest_cache_strip_modsig(__u8 *data, size_t data_len); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index ebc5dc09a62b..ad0f34c7ef9b 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -267,6 +267,9 @@ struct digest_cache *digest_cache_init(struct dentry *dentry, struct path *digest_list_path, struct digest_cache *digest_cache) { + struct inode *inode; + int ret; + /* Wait for digest cache initialization. */ if (!digest_list_path->dentry || test_and_set_bit(INIT_STARTED, &digest_cache->flags)) { @@ -275,6 +278,19 @@ struct digest_cache *digest_cache_init(struct dentry *dentry, goto out; }
+ inode = d_backing_inode(digest_list_path->dentry); + + if (S_ISREG(inode->i_mode)) { + ret = digest_cache_populate(dentry, digest_cache, + digest_list_path); + if (ret < 0) { + pr_debug("Failed to populate digest cache %s ret: %d (keep digest cache)\n", + digest_cache->path_str, ret); + /* Prevent usage of partially-populated digest cache. */ + set_bit(INVALID, &digest_cache->flags); + } + } + /* Notify initialization complete. */ clear_and_wake_up_bit(INIT_IN_PROGRESS, &digest_cache->flags); out: diff --git a/security/integrity/digest_cache/modsig.c b/security/integrity/digest_cache/modsig.c new file mode 100644 index 000000000000..fa512c43a556 --- /dev/null +++ b/security/integrity/digest_cache/modsig.c @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2012 Red Hat, Inc. All Rights Reserved. + * Copyright (C) 2019 IBM Corporation + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Strip module-style appended signatures. + */ + +#define pr_fmt(fmt) "digest_cache: "fmt +#include <linux/module.h> +#include <linux/module_signature.h> + +#include "internal.h" + +/** + * digest_cache_strip_modsig - Strip module-style appended sig from digest list + * @data: Data to parse + * @data_len: Length of @data + * + * This function strips the module-style appended signature from a digest list, + * if present. + * + * Return: Size of stripped data on success, original size otherwise. + */ +size_t digest_cache_strip_modsig(__u8 *data, size_t data_len) +{ + const size_t marker_len = strlen(MODULE_SIG_STRING); + const struct module_signature *sig; + size_t parsed_data_len = data_len; + size_t sig_len; + const void *p; + + /* From ima_modsig.c */ + if (data_len <= marker_len + sizeof(*sig)) + return data_len; + + p = data + parsed_data_len - marker_len; + if (memcmp(p, MODULE_SIG_STRING, marker_len)) + return data_len; + + parsed_data_len -= marker_len; + sig = (const struct module_signature *)(p - sizeof(*sig)); + + /* From module_signature.c */ + if (be32_to_cpu(sig->sig_len) >= parsed_data_len - sizeof(*sig)) + return data_len; + + /* Unlike for module signatures, accept all signature types. */ + if (sig->algo != 0 || + sig->hash != 0 || + sig->signer_len != 0 || + sig->key_id_len != 0 || + sig->__pad[0] != 0 || + sig->__pad[1] != 0 || + sig->__pad[2] != 0) { + pr_debug("Signature info has unexpected non-zero params\n"); + return data_len; + } + + sig_len = be32_to_cpu(sig->sig_len); + parsed_data_len -= sig_len + sizeof(*sig); + return parsed_data_len; +} diff --git a/security/integrity/digest_cache/populate.c b/security/integrity/digest_cache/populate.c new file mode 100644 index 000000000000..54f7f95f5794 --- /dev/null +++ b/security/integrity/digest_cache/populate.c @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Implement the code to populate a digest cache. + */ + +#define pr_fmt(fmt) "digest_cache: "fmt +#include <linux/init_task.h> +#include <linux/vmalloc.h> +#include <linux/kernel_read_file.h> + +#include "internal.h" + +/** + * digest_cache_read_digest_list - Read a digest list + * @work: Work structure + * + * This function is invoked by schedule_work() to read a digest list. + * + * It does not return a value, but stores the result in the passed structure. + */ +static void digest_cache_read_digest_list(struct work_struct *work) +{ + struct read_work *w = container_of(work, struct read_work, work); + + w->ret = kernel_read_file(w->file, 0, &w->data, INT_MAX, NULL, + READING_DIGEST_LIST); +} + +/** + * digest_cache_populate - Populate a digest cache from a digest list + * @dentry: Dentry of the inode for which the digest cache will be used + * @digest_cache: Digest cache + * @digest_list_path: Path structure of the digest list + * + * This function opens the digest list for reading it. Then, it schedules a + * work to read the digest list and, once the work is done, it calls + * digest_cache_strip_modsig() to strip a module-style appended signature and + * digest_cache_parse_digest_list() for extracting and adding digests to the + * digest cache. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_cache_populate(struct dentry *dentry, + struct digest_cache *digest_cache, + struct path *digest_list_path) +{ + struct file *file; + void *data; + size_t data_len; + struct read_work w; + int ret; + + file = kernel_file_open(digest_list_path, O_RDONLY, &init_cred); + if (IS_ERR(file)) { + pr_debug("Unable to open digest list %s, ret: %ld\n", + digest_cache->path_str, PTR_ERR(file)); + return PTR_ERR(file); + } + + /* Mark the file descriptor as ours. */ + digest_cache_to_file_sec(file, digest_cache); + + w.data = NULL; + w.file = file; + INIT_WORK_ONSTACK(&w.work, digest_cache_read_digest_list); + + schedule_work(&w.work); + flush_work(&w.work); + destroy_work_on_stack(&w.work); + fput(file); + + ret = w.ret; + data = w.data; + + if (ret < 0) { + pr_debug("Unable to read digest list %s, ret: %d\n", + digest_cache->path_str, ret); + return ret; + } + + data_len = digest_cache_strip_modsig(data, ret); + + /* Digest list parsers initialize the hash table and add the digests. */ + ret = digest_cache_parse_digest_list(dentry, digest_cache, + digest_cache->path_str, data, + data_len); + if (ret < 0) + pr_debug("Error parsing digest list %s, ret: %d\n", + digest_cache->path_str, ret); + + vfree(data); + return ret; +}
From: Roberto Sassu roberto.sassu@huawei.com
The Integrity Digest Cache can support other LSMs in their decisions of granting access to file data 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 about the integrity of the digest list digests were extracted from.
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.
digest_cache_verif_set() receives as argument a file descriptor and calls digest_cache_from_file_sec() to obtain back the digest cache being created from that file descriptor. The digest cache being created was associated to the file descriptor by digest_cache_populate(), before reading the digest list from the kernel, by calling digest_cache_to_file_sec().
Multiple providers are supported, in the event there are multiple integrity LSMs active. Each provider should also provide a unique verifier ID as an argument to digest_cache_verif_set(), so that verification data can be distinguished. Concurrent set are protected by the verif_data_lock spinlock.
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, and pass the returned digest cache reference to digest_cache_verif_get().
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- include/linux/digest_cache.h | 17 +++ security/integrity/digest_cache/Makefile | 3 +- security/integrity/digest_cache/internal.h | 22 ++++ security/integrity/digest_cache/main.c | 3 + security/integrity/digest_cache/verif.c | 131 +++++++++++++++++++++ 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 security/integrity/digest_cache/verif.c
diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h index a9d731990b7c..d2483fe588be 100644 --- a/include/linux/digest_cache.h +++ b/include/linux/digest_cache.h @@ -47,6 +47,10 @@ bool digest_cache_opened_fd(struct file *file); struct digest_cache *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);
/* Parser API */ int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests, @@ -81,6 +85,19 @@ digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache, return NULL; }
+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; +} + static inline int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests, enum hash_algo algo) { diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index 3b81edea065b..2a0f2500e227 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -5,6 +5,7 @@ obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o obj-$(CONFIG_DIGEST_CACHE_TLV_PARSER) += parsers/tlv.o
-digest_cache-y := main.o secfs.o htable.o parsers.o populate.o modsig.o +digest_cache-y := main.o secfs.o htable.o parsers.o populate.o modsig.o \ + verif.o
CFLAGS_parsers.o += -DPARSERS_DIR="$(MODLIB)/kernel/security/integrity/digest_cache/parsers" diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index 2171ea8423ff..c64e91b75a47 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -18,6 +18,21 @@ #define INIT_STARTED 1 /* Digest cache init started. */ #define INVALID 2 /* 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 @@ -72,6 +87,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: Protects verification data modifications * * This structure represents a cache of digests extracted from a digest list. */ @@ -80,6 +97,8 @@ struct digest_cache { atomic_t ref_count; char *path_str; unsigned long flags; + struct list_head verif_data; + spinlock_t verif_data_lock; };
/** @@ -191,4 +210,7 @@ int digest_cache_populate(struct dentry *dentry, /* 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/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index ad0f34c7ef9b..11a0445592f0 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -52,6 +52,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)); @@ -68,6 +70,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); diff --git a/security/integrity/digest_cache/verif.c b/security/integrity/digest_cache/verif.c new file mode 100644 index 000000000000..03ebf0de764b --- /dev/null +++ b/security/integrity/digest_cache/verif.c @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * 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. Verifier ID must be unique. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +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, *verif; + /* All allocations done by kprobe must be atomic (non-sleepable). */ + gfp_t flags = !strncmp(verif_id, "kprobe", 6) ? GFP_ATOMIC : GFP_KERNEL; + int ret = 0; + + /* + * Zero the data, so that we can always call free_verif() to free a + * partially filled structure (if a pointer is NULL, will not be freed). + */ + new_verif = kzalloc(sizeof(*new_verif), flags); + if (!new_verif) + return -ENOMEM; + + new_verif->verif_id = kstrdup(verif_id, flags); + if (!new_verif->verif_id) { + free_verif(new_verif); + return -ENOMEM; + } + + new_verif->data = kmemdup(data, size, flags); + if (!new_verif->data) { + free_verif(new_verif); + return -ENOMEM; + } + + spin_lock(&digest_cache->verif_data_lock); + list_for_each_entry(verif, &digest_cache->verif_data, list) { + if (!strcmp(verif->verif_id, verif_id)) { + ret = -EEXIST; + goto out; + } + } + + list_add_tail_rcu(&new_verif->list, &digest_cache->verif_data); +out: + spin_unlock(&digest_cache->verif_data_lock); + + if (ret < 0) + free_verif(new_verif); + + return ret; +} +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: Roberto Sassu roberto.sassu@huawei.com
In the environments where xattrs are not available (e.g. in the initial ram disk), the Integrity Digest Cache 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 three steps. First, a directory digest cache is created like the other digest caches. The only differences are that this digest cache has the IS_DIR bit set, to distinguish it from those created from regular files and, consequently, that it stores a list of directory entries file names instead of hash tables for digests.
Second, the directory digest cache is populated with current directory entries, by calling digest_cache_dir_add_entries().
Finally, digest_cache_dir_lookup_digest() is called with the directory digest cache passed as argument, to iteratively search on each digest cache for each directory entry.
The function first calls digest_cache_dir_create() to create/obtain the current directory digest cache for the directory. If the latter returns a different one than the one passed, digest_cache_dir_lookup_digest() returns ERR_PTR(-EAGAIN) to let the caller of digest_cache_lookup() know that the directory digest cache changed, and make the caller perform another digest_cache_get() to get a fresh directory digest cache.
If the passed and the current directory digest caches match, digest_cache_dir_lookup_digest() starts the lookup and iteratively searches the passed digest in each directory entry. If there is no digest cache associated to the current directory entry, digest_cache_dir_lookup_digest() creates/obtains one by calling digest_cache_create(). It also keeps a digest cache reference, so that it is available for next searches. The iteration stops when the digest is found.
Finally, digest_cache_dir_free() releases the digest cache references stored in the list of directory entries, and frees the list itself.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- security/integrity/digest_cache/Makefile | 2 +- security/integrity/digest_cache/dir.c | 263 +++++++++++++++++++++ security/integrity/digest_cache/htable.c | 7 +- security/integrity/digest_cache/internal.h | 44 ++++ security/integrity/digest_cache/main.c | 13 + 5 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 security/integrity/digest_cache/dir.c
diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index 2a0f2500e227..d07ac2483504 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -6,6 +6,6 @@ obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o obj-$(CONFIG_DIGEST_CACHE_TLV_PARSER) += parsers/tlv.o
digest_cache-y := main.o secfs.o htable.o parsers.o populate.o modsig.o \ - verif.o + verif.o dir.o
CFLAGS_parsers.o += -DPARSERS_DIR="$(MODLIB)/kernel/security/integrity/digest_cache/parsers" diff --git a/security/integrity/digest_cache/dir.c b/security/integrity/digest_cache/dir.c new file mode 100644 index 000000000000..8f9e550c365f --- /dev/null +++ b/security/integrity/digest_cache/dir.c @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Manage digest caches from directories. + */ + +#define pr_fmt(fmt) "digest_cache: "fmt +#include <linux/init_task.h> +#include <linux/namei.h> + +#include "internal.h" + +/** + * digest_cache_dir_iter - Digest cache directory iterator + * @__ctx: iterate_dir() context + * @name: Name of file in the accessed directory + * @namelen: String length of @name + * @offset: Current position in the directory stream (see man readdir) + * @ino: Inode number + * @d_type: File type + * + * This function stores the names of the files in the containing directory in + * a linked list. If they are in the format + * <seq num>-<digest list format>-<digest list name>, this function orders them + * by seq num, so that digest lists are processed in the desired order. + * Otherwise, if <seq num>- is not included, it adds the name at the end of + * the list. + * + * Return: True to continue processing, false to stop. + */ +static bool digest_cache_dir_iter(struct dir_context *__ctx, const char *name, + int namelen, loff_t offset, u64 ino, + unsigned int d_type) +{ + struct readdir_callback *ctx = container_of(__ctx, typeof(*ctx), ctx); + struct dir_entry *new_entry, *p; + unsigned int seq_num; + char *separator; + int ret; + + if (!strcmp(name, ".") || !strcmp(name, "..")) + return true; + + if (d_type != DT_REG) + return true; + + new_entry = kmalloc(sizeof(*new_entry) + namelen + 1, GFP_KERNEL); + if (!new_entry) + return false; + + memcpy(new_entry->name, name, namelen); + new_entry->name[namelen] = '\0'; + new_entry->seq_num = UINT_MAX; + new_entry->digest_cache = NULL; + mutex_init(&new_entry->digest_cache_mutex); + + if (new_entry->name[0] < '0' || new_entry->name[0] > '9') + goto out; + + separator = strchr(new_entry->name, '-'); + if (!separator) + goto out; + + *separator = '\0'; + ret = kstrtouint(new_entry->name, 10, &seq_num); + *separator = '-'; + if (ret < 0) + goto out; + + new_entry->seq_num = seq_num; + + list_for_each_entry(p, ctx->head, list) { + if (seq_num <= p->seq_num) { + list_add(&new_entry->list, p->list.prev); + pr_debug("Added %s before %s in dir list\n", + new_entry->name, p->name); + return true; + } + } +out: + list_add_tail(&new_entry->list, ctx->head); + pr_debug("Added %s to tail of dir list\n", new_entry->name); + return true; +} + +/** + * digest_cache_dir_add_entries - Add dir entries to a dir digest cache + * @digest_cache: Dir digest cache + * @digest_list_path: Path structure of the digest list directory + * + * This function iterates over the entries of a directory, and creates a linked + * list of file names from that directory. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_cache_dir_add_entries(struct digest_cache *digest_cache, + struct path *digest_list_path) +{ + struct file *dir_file; + struct readdir_callback buf = { + .ctx.actor = digest_cache_dir_iter, + .ctx.pos = 0, + .head = &digest_cache->dir_entries, + }; + int ret; + + dir_file = kernel_file_open(digest_list_path, O_RDONLY, &init_cred); + if (IS_ERR(dir_file)) { + pr_debug("Cannot access %s, ret: %ld\n", digest_cache->path_str, + PTR_ERR(dir_file)); + return PTR_ERR(dir_file); + } + + ret = iterate_dir(dir_file, &buf.ctx); + if (ret < 0) + pr_debug("Failed to iterate directory %s\n", + digest_cache->path_str); + + fput(dir_file); + return ret; +} + +/** + * digest_cache_dir_create - Create and initialize a directory digest cache + * @dentry: Dentry of the file whose digest is looked up + * @dir_path: Path structure of the digest list directory (updated) + * @path_str: Path string of the digest list directory + * + * This function creates and initializes (or obtains if it already exists) a + * directory digest cache. It updates the path that digest cache was + * created/obtained from, so that the caller can use it to perform lookup + * operations. + * + * Return: A directory digest cache on success, NULL otherwise. + */ +static struct digest_cache *digest_cache_dir_create(struct dentry *dentry, + struct path *dir_path, + char *path_str) +{ + struct digest_cache *digest_cache; + struct path _dir_path; + int ret; + + ret = kern_path(path_str, 0, &_dir_path); + if (ret < 0) { + pr_debug("Cannot find path %s\n", path_str); + return NULL; + } + + digest_cache = digest_cache_create(dentry, &_dir_path, dir_path, + path_str, ""); + if (digest_cache) + digest_cache = digest_cache_init(dentry, dir_path, + digest_cache); + + path_put(&_dir_path); + return digest_cache; +} + +/** + * digest_cache_dir_lookup_digest - Lookup a digest + * @dentry: Dentry of the file whose digest is looked up + * @digest_cache: Dir digest cache + * @digest: Digest to search + * @algo: Algorithm of the digest to search + * + * This function iterates over the linked list created by + * digest_cache_dir_add_entries() and looks up the digest in the digest cache + * of each entry. + * + * Return: A digest cache reference if the digest is found, NULL if not, an + * error pointer if dir digest cache changed since last get. + */ +struct digest_cache * +digest_cache_dir_lookup_digest(struct dentry *dentry, + struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo) +{ + struct dir_entry *dir_entry; + struct digest_cache *dir_cache, *cache, *found = NULL; + struct path dir_path = { .dentry = NULL, .mnt = NULL }; + struct path digest_list_path; + int ret; + + /* Try to reacquire the dir digest cache, and check if changed. */ + dir_cache = digest_cache_dir_create(dentry, &dir_path, + digest_cache->path_str); + if (dir_cache != digest_cache) { + if (dir_cache) + found = ERR_PTR(-EAGAIN); + + goto out; + } + + list_for_each_entry(dir_entry, &dir_cache->dir_entries, list) { + mutex_lock(&dir_entry->digest_cache_mutex); + if (!dir_entry->digest_cache) { + digest_list_path.dentry = NULL; + digest_list_path.mnt = NULL; + + cache = digest_cache_create(dentry, &dir_path, + &digest_list_path, + dir_cache->path_str, + dir_entry->name); + if (cache) + cache = digest_cache_init(dentry, + &digest_list_path, + cache); + + if (digest_list_path.dentry) + path_put(&digest_list_path); + + /* Ignore digest caches that cannot be instantiated. */ + if (!cache) { + mutex_unlock(&dir_entry->digest_cache_mutex); + continue; + } + + /* Consume extra ref. from digest_cache_create(). */ + dir_entry->digest_cache = cache; + } + mutex_unlock(&dir_entry->digest_cache_mutex); + + ret = digest_cache_htable_lookup(dentry, + dir_entry->digest_cache, + digest, algo); + if (!ret) { + found = digest_cache_ref(dir_entry->digest_cache); + break; + } + } +out: + if (dir_cache) + digest_cache_put(dir_cache); + if (dir_path.dentry) + path_put(&dir_path); + + return found; +} + +/** + * digest_cache_dir_free - Free the stored file list and put digest caches + * @digest_cache: Dir digest cache + * + * This function frees the file list created by digest_cache_dir_add_entries(), + * and puts the digest cache of each directory entry, if a reference exists. + */ +void digest_cache_dir_free(struct digest_cache *digest_cache) +{ + struct dir_entry *p, *q; + + list_for_each_entry_safe(p, q, &digest_cache->dir_entries, list) { + if (p->digest_cache) + digest_cache_put(p->digest_cache); + + list_del(&p->list); + mutex_destroy(&p->digest_cache_mutex); + kfree(p); + } +} diff --git a/security/integrity/digest_cache/htable.c b/security/integrity/digest_cache/htable.c index 8aa6d50a0cb5..a01e24d7f198 100644 --- a/security/integrity/digest_cache/htable.c +++ b/security/integrity/digest_cache/htable.c @@ -202,7 +202,8 @@ EXPORT_SYMBOL_GPL(digest_cache_htable_lookup); * This function calls digest_cache_htable_lookup() to search a digest in the * passed digest cache, obtained with digest_cache_get(). * - * Return: A digest cache reference the digest is found, NULL if not. + * Return: A digest cache reference if the digest is found, NULL if not, an + * error pointer if dir digest cache changed since last get. */ struct digest_cache *digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache, @@ -210,6 +211,10 @@ struct digest_cache *digest_cache_lookup(struct dentry *dentry, { int ret;
+ if (test_bit(IS_DIR, &digest_cache->flags)) + return digest_cache_dir_lookup_digest(dentry, digest_cache, + digest, algo); + ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo); if (ret < 0) return NULL; diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index c64e91b75a47..f849afe5e47b 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -17,6 +17,39 @@ #define INIT_IN_PROGRESS 0 /* Digest cache being initialized. */ #define INIT_STARTED 1 /* Digest cache init started. */ #define INVALID 2 /* Digest cache marked as invalid. */ +#define IS_DIR 3 /* Digest cache created from dir. */ + +/** + * struct readdir_callback - Structure to store information for dir iteration + * @ctx: Context structure + * @head: Head of linked list of directory entries + * + * This structure stores information to be passed from the iterate_dir() caller + * to the directory iterator. + */ +struct readdir_callback { + struct dir_context ctx; + struct list_head *head; +}; + +/** + * struct dir_entry - Directory entry + * @list: Linked list of directory entries + * @digest_cache: Digest cache associated to the directory entry + * @digest_cache_mutex: Protects @digest_cache + * @seq_num: Sequence number of the directory entry from file name + * @name: File name of the directory entry + * + * This structure represents a directory entry with a digest cache created + * from that entry. + */ +struct dir_entry { + struct list_head list; + struct digest_cache *digest_cache; + struct mutex digest_cache_mutex; + unsigned int seq_num; + char name[]; +};
/** * struct digest_cache_verif @@ -84,6 +117,7 @@ struct htable { /** * struct digest_cache - Digest cache * @htables: Hash tables (one per algorithm) + * @dir_entries: List of files in a directory and the 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 @@ -94,6 +128,7 @@ struct htable { */ struct digest_cache { struct list_head htables; + struct list_head dir_entries; atomic_t ref_count; char *path_str; unsigned long flags; @@ -213,4 +248,13 @@ size_t digest_cache_strip_modsig(__u8 *data, size_t data_len); /* verif.c */ void digest_cache_verif_free(struct digest_cache *digest_cache);
+/* dir.c */ +int digest_cache_dir_add_entries(struct digest_cache *digest_cache, + struct path *digest_cache_path); +struct digest_cache * +digest_cache_dir_lookup_digest(struct dentry *dentry, + struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo); +void digest_cache_dir_free(struct digest_cache *digest_cache); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index 11a0445592f0..9df819c323c7 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -54,6 +54,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str, INIT_LIST_HEAD(&digest_cache->htables); INIT_LIST_HEAD(&digest_cache->verif_data); spin_lock_init(&digest_cache->verif_data_lock); + INIT_LIST_HEAD(&digest_cache->dir_entries);
pr_debug("New digest cache %s (ref count: %d)\n", digest_cache->path_str, atomic_read(&digest_cache->ref_count)); @@ -71,6 +72,7 @@ static void digest_cache_free(struct digest_cache *digest_cache) { digest_cache_htable_free(digest_cache); digest_cache_verif_free(digest_cache); + digest_cache_dir_free(digest_cache);
pr_debug("Freed digest cache %s\n", digest_cache->path_str); kfree(digest_cache->path_str); @@ -292,6 +294,17 @@ struct digest_cache *digest_cache_init(struct dentry *dentry, /* Prevent usage of partially-populated digest cache. */ set_bit(INVALID, &digest_cache->flags); } + } else if (S_ISDIR(inode->i_mode)) { + set_bit(IS_DIR, &digest_cache->flags); + + ret = digest_cache_dir_add_entries(digest_cache, + digest_list_path); + if (ret < 0) { + pr_debug("Failed to add dir entries to dir digest cache, ret: %d (keep digest cache)\n", + ret); + /* Prevent usage of partially-populated digest cache. */ + set_bit(INVALID, &digest_cache->flags); + } }
/* Notify initialization complete. */
From: Roberto Sassu roberto.sassu@huawei.com
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 Integrity Digest Cache is not exempted from this issue. If callers of digest_cache_get() pass file descriptors of inodes in an unpredictable order, this very likely will result in reading digest lists in an unpredictable order too.
Overcome this issue by introducing digest_cache_dir_prefetch() and digest_cache_dir_lookup_filename(), to sequentially search the digest list the digest cache will be populated from in the parent directory, instead of directly accessing it from the full path.
If during the file name search there is no match, read the digest list to trigger its measurement (if an appropriate policy exists). If there is a match, attempt to create and initialize the digest cache from the found file name.
This ensures that measurements are always done in the same order as the file names stored in the digest cache of the parent directory, regardless of which digest list was requested. However, the PCR still changes every time a not yet read digest list is requested. In any case, the possible PCR values are more manageable than in the unpredictable file access case, at most the number of digest lists. Of course, PCR values further change when digest lists are added/removed to/from the parent directory, or if the parent directory itself is modified through securityfs.
The prefetching mechanism has been designed to be more like a aid to get predictable measurements, and does not defend against path change attacks. It assumes that the path does not change between retrieving the digest cache associated to an inode and prefetching (i.e. the prefetching mechanism gets the same digest cache). If the path changed, and the prefetching mechanism retrieved a different digest cache, digest_cache_get() still continues to use the one it retrieved, and will ignore the prefetched one.
Prefetching is enabled by setting the newly introduced 'security.dig_prefetch' xattr to '1'. digest_cache_new() and digest_cache_dir_create() call digest_cache_prefetch_requested() to check it. They pass the check result to digest_cache_create(), which in turn sets the DIR_PREFETCH and FILE_PREFETCH bit respectively in the parent directory and regular file digest cache.
On subsequent calls to digest_cache_prefetch_requested(), the latter can just test for DIR_PREFETCH bit (faster).
digest_cache_get(), after digest cache creation, checks if the digest cache has the FILE_PREFETCH bit set and, if yes, calls digest_cache_dir_prefetch() to do prefetching. For just reading digest lists, digest_cache_dir_prefetch() asks digest_cache_create() to create special digest caches with the FILE_READ bit set, so that digest_cache_populate() does not attempt to extract digests. The special digest caches also are not attached to the corresponding inode.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- include/uapi/linux/xattr.h | 3 + security/integrity/digest_cache/dir.c | 135 ++++++++++++++++++++- security/integrity/digest_cache/internal.h | 12 +- security/integrity/digest_cache/main.c | 96 ++++++++++++++- security/integrity/digest_cache/populate.c | 9 +- security/integrity/digest_cache/verif.c | 4 + 6 files changed, 251 insertions(+), 8 deletions(-)
diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h index 8a58cf4bce65..8af33d38d9e8 100644 --- a/include/uapi/linux/xattr.h +++ b/include/uapi/linux/xattr.h @@ -57,6 +57,9 @@ #define XATTR_DIGEST_LIST_SUFFIX "digest_list" #define XATTR_NAME_DIGEST_LIST XATTR_SECURITY_PREFIX XATTR_DIGEST_LIST_SUFFIX
+#define XATTR_DIG_PREFETCH_SUFFIX "dig_prefetch" +#define XATTR_NAME_DIG_PREFETCH XATTR_SECURITY_PREFIX XATTR_DIG_PREFETCH_SUFFIX + #define XATTR_SELINUX_SUFFIX "selinux" #define XATTR_NAME_SELINUX XATTR_SECURITY_PREFIX XATTR_SELINUX_SUFFIX
diff --git a/security/integrity/digest_cache/dir.c b/security/integrity/digest_cache/dir.c index 8f9e550c365f..a292a9c25119 100644 --- a/security/integrity/digest_cache/dir.c +++ b/security/integrity/digest_cache/dir.c @@ -56,6 +56,7 @@ static bool digest_cache_dir_iter(struct dir_context *__ctx, const char *name, new_entry->seq_num = UINT_MAX; new_entry->digest_cache = NULL; mutex_init(&new_entry->digest_cache_mutex); + new_entry->prefetched = false;
if (new_entry->name[0] < '0' || new_entry->name[0] > '9') goto out; @@ -134,6 +135,11 @@ int digest_cache_dir_add_entries(struct digest_cache *digest_cache, * created/obtained from, so that the caller can use it to perform lookup * operations. * + * Before creating the digest cache, this function first calls + * digest_cache_prefetch_requested() to check if prefetching has been requested + * on the directory, and passes the result to digest_cache_create(), so that the + * latter sets the DIR_PREFETCH bit in the directory digest cache. + * * Return: A directory digest cache on success, NULL otherwise. */ static struct digest_cache *digest_cache_dir_create(struct dentry *dentry, @@ -142,6 +148,7 @@ static struct digest_cache *digest_cache_dir_create(struct dentry *dentry, { struct digest_cache *digest_cache; struct path _dir_path; + bool prefetch_req; int ret;
ret = kern_path(path_str, 0, &_dir_path); @@ -150,8 +157,10 @@ static struct digest_cache *digest_cache_dir_create(struct dentry *dentry, return NULL; }
+ prefetch_req = digest_cache_prefetch_requested(&_dir_path, path_str); + digest_cache = digest_cache_create(dentry, &_dir_path, dir_path, - path_str, ""); + path_str, "", prefetch_req, false); if (digest_cache) digest_cache = digest_cache_init(dentry, dir_path, digest_cache); @@ -204,7 +213,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry, cache = digest_cache_create(dentry, &dir_path, &digest_list_path, dir_cache->path_str, - dir_entry->name); + dir_entry->name, false, + false); if (cache) cache = digest_cache_init(dentry, &digest_list_path, @@ -221,6 +231,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
/* Consume extra ref. from digest_cache_create(). */ dir_entry->digest_cache = cache; + /* Digest list was read, mark entry as prefetched. */ + dir_entry->prefetched = true; } mutex_unlock(&dir_entry->digest_cache_mutex);
@@ -241,6 +253,125 @@ digest_cache_dir_lookup_digest(struct dentry *dentry, return found; }
+/** + * digest_cache_dir_lookup_filename - Lookup a digest list + * @dentry: Dentry of the file whose digest list is looked up + * @dir_path: Path structure of the digest list directory + * @digest_cache: Dir digest cache + * @filename: File name of the digest list to search + * + * This function iterates over the linked list created by + * digest_cache_dir_add_entries() and looks up a digest list with a matching + * file name among the entries. If there is no match, it prefetches (reads) the + * current digest list. Otherwise, it creates and initializes a new digest + * cache. + * + * The new digest cache is not returned, since it might have been created from + * a different inode than the one originally found during digest_cache_create(). + * + * If it is the right one, digest_cache_init() called from digest_cache_get() + * will go ahead and simply use the initialized digest cache. If for any reason + * the inode was different, digest_cache_init() will use the one previously + * returned by digest_cache_create(). + */ +static void digest_cache_dir_lookup_filename(struct dentry *dentry, + struct path *dir_path, + struct digest_cache *digest_cache, + char *filename) +{ + struct digest_cache *cache; + struct dir_entry *dir_entry; + struct path digest_list_path; + bool filename_found; + + list_for_each_entry(dir_entry, &digest_cache->dir_entries, list) { + mutex_lock(&dir_entry->digest_cache_mutex); + filename_found = !strcmp(dir_entry->name, filename); + if (!filename_found && dir_entry->prefetched) { + mutex_unlock(&dir_entry->digest_cache_mutex); + continue; + } + + digest_list_path.dentry = NULL; + digest_list_path.mnt = NULL; + + cache = digest_cache_create(dentry, dir_path, &digest_list_path, + digest_cache->path_str, + dir_entry->name, false, + filename_found ? false : true); + if (cache) + cache = digest_cache_init(dentry, &digest_list_path, + cache); + + if (digest_list_path.dentry) + path_put(&digest_list_path); + + if (!filename_found) + pr_debug("Digest list %s/%s %s prefetched\n", + digest_cache->path_str, dir_entry->name, + cache ? "has been" : "cannot be"); + + if (cache) + digest_cache_put(cache); + + dir_entry->prefetched = true; + mutex_unlock(&dir_entry->digest_cache_mutex); + + if (filename_found) + break; + } + + /* Prefetching done, no need to repeat. */ + clear_bit(FILE_PREFETCH, &digest_cache->flags); +} + +/** + * digest_cache_dir_prefetch - Prefetch digest lists in parent directory + * @dentry: Dentry of the file whose digest list is looked up + * @digest_cache: Digest cache + * + * This function prefetches the digest lists in the parent directory of the + * passed digest cache. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_cache_dir_prefetch(struct dentry *dentry, + struct digest_cache *digest_cache) +{ + struct digest_cache *dir_cache; + char *path_str, *last; + struct path dir_path = { .dentry = NULL, .mnt = NULL }; + int ret = 0; + + last = strrchr(digest_cache->path_str, '/'); + if (!last) + return -EINVAL; + + path_str = kstrndup(digest_cache->path_str, + last - digest_cache->path_str, GFP_KERNEL); + if (!path_str) + return -ENOMEM; + + dir_cache = digest_cache_dir_create(dentry, &dir_path, path_str); + + kfree(path_str); + + if (!dir_cache) { + ret = -ENOENT; + goto out; + } + + digest_cache_dir_lookup_filename(dentry, &dir_path, dir_cache, + last + 1); + + digest_cache_put(dir_cache); +out: + if (dir_path.dentry) + path_put(&dir_path); + + return ret; +} + /** * digest_cache_dir_free - Free the stored file list and put digest caches * @digest_cache: Dir digest cache diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index f849afe5e47b..5af7f6e7bdf6 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -18,6 +18,9 @@ #define INIT_STARTED 1 /* Digest cache init started. */ #define INVALID 2 /* Digest cache marked as invalid. */ #define IS_DIR 3 /* Digest cache created from dir. */ +#define DIR_PREFETCH 4 /* Prefetch enabled for dir. */ +#define FILE_PREFETCH 5 /* Prefetch enabled for dir entry. */ +#define FILE_READ 6 /* Digest cache for reading file. */
/** * struct readdir_callback - Structure to store information for dir iteration @@ -38,6 +41,7 @@ struct readdir_callback { * @digest_cache: Digest cache associated to the directory entry * @digest_cache_mutex: Protects @digest_cache * @seq_num: Sequence number of the directory entry from file name + * @prefetched: Whether the digest list has been already prefetched * @name: File name of the directory entry * * This structure represents a directory entry with a digest cache created @@ -48,6 +52,7 @@ struct dir_entry { struct digest_cache *digest_cache; struct mutex digest_cache_mutex; unsigned int seq_num; + bool prefetched; char name[]; };
@@ -219,7 +224,10 @@ digest_cache_from_file_sec(const struct file *file) struct digest_cache *digest_cache_create(struct dentry *dentry, struct path *default_path, struct path *digest_list_path, - char *path_str, char *filename); + char *path_str, char *filename, + bool prefetch_req, bool prefetch); +bool digest_cache_prefetch_requested(struct path *digest_list_path, + char *path_str); struct digest_cache *digest_cache_init(struct dentry *dentry, struct path *digest_list_path, struct digest_cache *digest_cache); @@ -255,6 +263,8 @@ struct digest_cache * digest_cache_dir_lookup_digest(struct dentry *dentry, struct digest_cache *digest_cache, u8 *digest, enum hash_algo algo); +int digest_cache_dir_prefetch(struct dentry *dentry, + struct digest_cache *digest_cache); void digest_cache_dir_free(struct digest_cache *digest_cache);
#endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index 9df819c323c7..ca1f0bc8ec94 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -86,6 +86,8 @@ static void digest_cache_free(struct digest_cache *digest_cache) * @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) + * @prefetch_req: Whether prefetching has been requested + * @prefetch: Whether prefetching of a digest list is being done * * This function first locates, from the passed path and filename, the digest * list inode from which the digest cache will be created or retrieved (if it @@ -107,7 +109,8 @@ static void digest_cache_free(struct digest_cache *digest_cache) struct digest_cache *digest_cache_create(struct dentry *dentry, struct path *default_path, struct path *digest_list_path, - char *path_str, char *filename) + char *path_str, char *filename, + bool prefetch_req, bool prefetch) { struct digest_cache *digest_cache = NULL; struct digest_cache_security *dig_sec; @@ -160,6 +163,16 @@ struct digest_cache *digest_cache_create(struct dentry *dentry, goto out; }
+ if (prefetch) { + digest_cache = digest_cache_alloc_init(path_str, filename); + if (digest_cache) { + set_bit(FILE_READ, &digest_cache->flags); + goto out_set; + } + + goto out; + } + /* Ref. count is already 1 for this reference. */ dig_sec->dig_owner = digest_cache_alloc_init(path_str, filename); if (!dig_sec->dig_owner) @@ -167,14 +180,68 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
/* Increment ref. count for reference returned to the caller. */ digest_cache = digest_cache_ref(dig_sec->dig_owner); - +out_set: /* Make other digest cache requestors wait until creation complete. */ set_bit(INIT_IN_PROGRESS, &digest_cache->flags); + + /* Set bit if prefetching was requested. */ + if (prefetch_req) { + if (S_ISREG(inode->i_mode)) + set_bit(FILE_PREFETCH, &digest_cache->flags); + else + set_bit(DIR_PREFETCH, &digest_cache->flags); + } out: mutex_unlock(&dig_sec->dig_owner_mutex); return digest_cache; }
+/** + * digest_cache_prefetch_requested - Verify if prefetching is requested + * @digest_list_path: Path structure of the digest list directory + * @path_str: Path string of the digest list directory + * + * This function verifies whether or not digest list prefetching is requested. + * If dig_owner exists in the inode security blob, it checks the DIR_PREFETCH + * bit (faster). Otherwise, it reads the security.dig_prefetch xattr. + * + * Return: True if prefetching is requested, false otherwise. + */ +bool digest_cache_prefetch_requested(struct path *digest_list_path, + char *path_str) +{ + struct digest_cache_security *dig_sec; + bool prefetch_req = false; + char prefetch_value; + struct inode *inode; + int ret; + + inode = d_backing_inode(digest_list_path->dentry); + dig_sec = digest_cache_get_security(inode); + if (unlikely(!dig_sec)) + return false; + + mutex_lock(&dig_sec->dig_owner_mutex); + if (dig_sec->dig_owner) { + /* Reliable test: DIR_PREFETCH set with dig_owner_mutex held. */ + prefetch_req = test_bit(DIR_PREFETCH, + &dig_sec->dig_owner->flags); + mutex_unlock(&dig_sec->dig_owner_mutex); + return prefetch_req; + } + mutex_unlock(&dig_sec->dig_owner_mutex); + + ret = vfs_getxattr(&nop_mnt_idmap, digest_list_path->dentry, + XATTR_NAME_DIG_PREFETCH, &prefetch_value, 1); + if (ret == 1 && prefetch_value == '1') { + pr_debug("Prefetching has been enabled for directory %s\n", + path_str); + prefetch_req = true; + } + + return prefetch_req; +} + /** * 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 @@ -186,6 +253,12 @@ struct digest_cache *digest_cache_create(struct dentry *dentry, * with that file name. If security.digest_list is empty or not found, this * function requests the creation of a digest cache on the parent directory. * + * In either case, this function first calls digest_cache_prefetch_requested() + * to check if prefetching has been requested on the parent directory, and + * passes the result to digest_cache_create(), so that the latter sets the + * FILE_PREFETCH and DIR_PREFETCH bit respectively in the file or directory + * digest caches. + * * This function passes to the caller the path of the digest list from which * the digest cache was created (file or parent directory), so that * digest_cache_init() can reuse the same path, and to prevent the inode from @@ -199,6 +272,7 @@ 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; + bool prefetch_req = false; int ret;
ret = kern_path(default_path_str, 0, &default_path); @@ -244,9 +318,12 @@ static struct digest_cache *digest_cache_new(struct dentry *dentry, XATTR_NAME_DIGEST_LIST, dentry->d_name.name, default_path_str, filename); create: + prefetch_req = digest_cache_prefetch_requested(&default_path, + default_path_str); + digest_cache = digest_cache_create(dentry, &default_path, digest_list_path, default_path_str, - filename); + filename, prefetch_req, false); out: path_put(&default_path); return digest_cache; @@ -372,9 +449,20 @@ struct digest_cache *digest_cache_get(struct file *file)
mutex_unlock(&dig_sec->dig_user_mutex);
- if (digest_cache) + if (digest_cache) { + /* + * Prefetching should also initialize our digest cache, if the + * path didn't change meanwhile (otherwise the prefetching + * mechanism will create and initialize the digest cache of a + * different inode). + */ + if (test_bit(FILE_PREFETCH, &digest_cache->flags)) + digest_cache_dir_prefetch(dentry, digest_cache); + + /* Make sure we initialize our digest cache. */ digest_cache = digest_cache_init(dentry, &digest_list_path, digest_cache); + }
if (digest_list_path.dentry) path_put(&digest_list_path); diff --git a/security/integrity/digest_cache/populate.c b/security/integrity/digest_cache/populate.c index 54f7f95f5794..b1646da07f36 100644 --- a/security/integrity/digest_cache/populate.c +++ b/security/integrity/digest_cache/populate.c @@ -11,6 +11,7 @@ #include <linux/init_task.h> #include <linux/vmalloc.h> #include <linux/kernel_read_file.h> +#include <linux/namei.h>
#include "internal.h"
@@ -82,6 +83,12 @@ int digest_cache_populate(struct dentry *dentry, return ret; }
+ /* The caller wants just to read digest lists. */ + if (test_bit(FILE_READ, &digest_cache->flags)) { + ret = 0; + goto out_vfree; + } + data_len = digest_cache_strip_modsig(data, ret);
/* Digest list parsers initialize the hash table and add the digests. */ @@ -91,7 +98,7 @@ int digest_cache_populate(struct dentry *dentry, if (ret < 0) pr_debug("Error parsing digest list %s, ret: %d\n", digest_cache->path_str, ret); - +out_vfree: vfree(data); return ret; } diff --git a/security/integrity/digest_cache/verif.c b/security/integrity/digest_cache/verif.c index 03ebf0de764b..2854bdab3e07 100644 --- a/security/integrity/digest_cache/verif.c +++ b/security/integrity/digest_cache/verif.c @@ -44,6 +44,10 @@ int digest_cache_verif_set(struct file *file, const char *verif_id, void *data, gfp_t flags = !strncmp(verif_id, "kprobe", 6) ? GFP_ATOMIC : GFP_KERNEL; int ret = 0;
+ /* Allows us to detect that we are prefetching in the tests. */ + if (test_bit(FILE_READ, &digest_cache->flags)) + return -ENOENT; + /* * Zero the data, so that we can always call free_verif() to free a * partially filled structure (if a pointer is NULL, will not be freed).
From: Roberto Sassu roberto.sassu@huawei.com
Register six new LSM hooks on behalf of the IMA LSM, path_truncate, file_release, inode_unlink, inode_rename, inode_post_setxattr and inode_post_removexattr, to monitor digest lists/parent directory modifications.
If an action affects a digest list or the parent directory, the new LSM hook implementations call digest_cache_reset_clear_owner() to set the RESET bit on the digest cache referenced by dig_owner in the inode security blob, and to put and clear dig_owner itself. This will also cause next calls to digest_cache_get() and digest_cache_dir_lookup_digest() to replace respectively dig_user and the directory entry digest cache.
If an action affects a file using a digest cache, the new LSM hook implementations call digest_cache_clear_user() to clear dig_user in the inode security blob. This will also cause next calls to digest_cache_get() to obtain a new digest cache, based on the updated location.
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 existing directory entries, since those digest caches are likely already stored in the inode security blob. It would happen however for new directory entries.
Dig_owner reset and clear for file/directory digest caches is done on path_truncate, when a digest list is truncated (there is no inode_truncate, file_truncate does not catch operations through the truncate() system call), file_release, when a digest list opened for write or created is being closed, inode_unlink, when a digest list is removed, and inode_rename when a digest list or the directory itself are renamed.
Directory digest caches are reset even if the current operation involves a file, since that operation might affect the result of the lookup done through them. For example, if one wants to know whether a digest is found or not in a directory, adding a new digest list to that directory could change the result.
Dig_user clear is always done on inode_post_setxattr and inode_post_removexattr, when the security.digest_list xattr is respectively set or removed from a file using a digest cache.
Callers of digest_cache_get() can still keep a digest cache after reset, since the reference count remains greater than zero even if dig_owner and dig_user are cleared. However, digest_cache_lookup() will notify the callers that a reset happened through an error pointer. Callers need to obtain a fresh digest cache with digest_cache_get() and repeat the lookup again.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- security/integrity/digest_cache/Makefile | 2 +- security/integrity/digest_cache/dir.c | 6 + security/integrity/digest_cache/htable.c | 6 +- security/integrity/digest_cache/internal.h | 13 ++ security/integrity/digest_cache/main.c | 12 ++ security/integrity/digest_cache/reset.c | 227 +++++++++++++++++++++ 6 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 security/integrity/digest_cache/reset.c
diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index d07ac2483504..f8afb85407a0 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -6,6 +6,6 @@ obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o obj-$(CONFIG_DIGEST_CACHE_TLV_PARSER) += parsers/tlv.o
digest_cache-y := main.o secfs.o htable.o parsers.o populate.o modsig.o \ - verif.o dir.o + verif.o dir.o reset.o
CFLAGS_parsers.o += -DPARSERS_DIR="$(MODLIB)/kernel/security/integrity/digest_cache/parsers" diff --git a/security/integrity/digest_cache/dir.c b/security/integrity/digest_cache/dir.c index a292a9c25119..d4fd5b5ef8fa 100644 --- a/security/integrity/digest_cache/dir.c +++ b/security/integrity/digest_cache/dir.c @@ -206,6 +206,12 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
list_for_each_entry(dir_entry, &dir_cache->dir_entries, list) { mutex_lock(&dir_entry->digest_cache_mutex); + if (dir_entry->digest_cache && + test_bit(RESET, &dir_entry->digest_cache->flags)) { + digest_cache_put(dir_entry->digest_cache); + dir_entry->digest_cache = NULL; + } + if (!dir_entry->digest_cache) { digest_list_path.dentry = NULL; digest_list_path.mnt = NULL; diff --git a/security/integrity/digest_cache/htable.c b/security/integrity/digest_cache/htable.c index a01e24d7f198..349aebf91360 100644 --- a/security/integrity/digest_cache/htable.c +++ b/security/integrity/digest_cache/htable.c @@ -203,7 +203,8 @@ EXPORT_SYMBOL_GPL(digest_cache_htable_lookup); * passed digest cache, obtained with digest_cache_get(). * * Return: A digest cache reference if the digest is found, NULL if not, an - * error pointer if dir digest cache changed since last get. + * error pointer if dir digest cache changed since last get, or digest + * cache was reset. */ struct digest_cache *digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache, @@ -211,6 +212,9 @@ struct digest_cache *digest_cache_lookup(struct dentry *dentry, { int ret;
+ if (test_bit(RESET, &digest_cache->flags)) + return ERR_PTR(-EAGAIN); + if (test_bit(IS_DIR, &digest_cache->flags)) return digest_cache_dir_lookup_digest(dentry, digest_cache, digest, algo); diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index 5af7f6e7bdf6..75f8b118c3db 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -21,6 +21,7 @@ #define DIR_PREFETCH 4 /* Prefetch enabled for dir. */ #define FILE_PREFETCH 5 /* Prefetch enabled for dir entry. */ #define FILE_READ 6 /* Digest cache for reading file. */ +#define RESET 7 /* Digest cache to be recreated. */
/** * struct readdir_callback - Structure to store information for dir iteration @@ -267,4 +268,16 @@ int digest_cache_dir_prefetch(struct dentry *dentry, struct digest_cache *digest_cache); void digest_cache_dir_free(struct digest_cache *digest_cache);
+/* reset.c */ +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); +void digest_cache_inode_post_setxattr(struct dentry *dentry, const char *name, + const void *value, size_t size, + int flags); +void digest_cache_inode_post_removexattr(struct dentry *dentry, + const char *name); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index ca1f0bc8ec94..0a167c82e308 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -436,6 +436,11 @@ struct digest_cache *digest_cache_get(struct file *file)
/* 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(). */ @@ -553,6 +558,13 @@ 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_rcu, digest_cache_inode_free_security_rcu), + 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), + LSM_HOOK_INIT(inode_post_setxattr, digest_cache_inode_post_setxattr), + LSM_HOOK_INIT(inode_post_removexattr, + digest_cache_inode_post_removexattr), };
/** diff --git a/security/integrity/digest_cache/reset.c b/security/integrity/digest_cache/reset.c new file mode 100644 index 000000000000..003c8ee96d72 --- /dev/null +++ b/security/integrity/digest_cache/reset.c @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Reset digest cache on digest lists/directory modifications. + */ + +#define pr_fmt(fmt) "digest_cache: "fmt +#include "internal.h" + +/** + * digest_cache_reset_clear_owner - Reset and clear dig_owner + * @inode: Inode of the digest list/directory containing the digest list + * @reason: Reason for reset and clear + * + * This function sets the RESET bit of the digest cache referenced by dig_owner + * of the passed inode, and puts and clears dig_owner. + * + * The next time they are called, digest_cache_get() and + * digest_cache_dir_lookup_digest() replace respectively dig_user and the digest + * cache of the directory entry. + */ +static void digest_cache_reset_clear_owner(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 and clearing %s (dig_owner), reason: %s\n", + dig_sec->dig_owner->path_str, reason); + set_bit(RESET, &dig_sec->dig_owner->flags); + digest_cache_put(dig_sec->dig_owner); + dig_sec->dig_owner = NULL; + } + mutex_unlock(&dig_sec->dig_owner_mutex); +} + +/** + * digest_cache_clear_user - Clear dig_user + * @inode: Inode of the file using the digest cache + * @filename: File name of the affected inode + * @reason: Reason for clear + * + * This function clears dig_user in the inode security blob, so that + * digest_cache_get() requests a new digest cache based on the updated digest + * list location. + */ +static void digest_cache_clear_user(struct inode *inode, const char *filename, + 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_user_mutex); + if (dig_sec->dig_user && !test_bit(RESET, &dig_sec->dig_user->flags)) { + pr_debug("Clearing %s (dig_user of %s), reason: %s\n", + dig_sec->dig_user->path_str, filename, reason); + digest_cache_put(dig_sec->dig_user); + dig_sec->dig_user = NULL; + } + mutex_unlock(&dig_sec->dig_user_mutex); +} + +/** + * 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 and/or the parent is a directory containing digest lists, it + * resets the inode and/or directory 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); + struct inode *dir = d_backing_inode(path->dentry->d_parent); + + if (!S_ISREG(inode->i_mode)) + return 0; + + digest_cache_reset_clear_owner(inode, "path_truncate(file)"); + digest_cache_reset_clear_owner(dir, "path_truncate(dir)"); + 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 inode is a regular file and was opened for write or + * was created, it resets the inode and the parent directory dig_owner, to + * force rebuilding the digest caches. + */ +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)) && + !(file->f_mode & FMODE_CREATED))) + return; + + digest_cache_reset_clear_owner(file_inode(file), "file_release(file)"); + digest_cache_reset_clear_owner(dir, "file_release(dir)"); +} + +/** + * 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/digest list directory, 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) && !S_ISDIR(inode->i_mode)) + return 0; + + digest_cache_reset_clear_owner(inode, S_ISREG(inode->i_mode) ? + "inode_unlink(file)" : + "inode_unlink(dir)"); + + if (S_ISREG(inode->i_mode)) + digest_cache_reset_clear_owner(dir, "inode_unlink(dir)"); + + 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 the digest + * list directory, or that inode is a digest list 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) && !S_ISDIR(old_inode->i_mode)) + return 0; + + digest_cache_reset_clear_owner(old_inode, S_ISREG(old_inode->i_mode) ? + "inode_rename(file)" : + "inode_rename(dir)"); + + if (S_ISREG(old_inode->i_mode)) { + digest_cache_reset_clear_owner(old_dir, + "inode_rename(from_dir)"); + digest_cache_reset_clear_owner(new_dir, + "inode_rename(to_dir)"); + } + + return 0; +} + +/** + * digest_cache_inode_post_setxattr - An xattr was set + * @dentry: File + * @name: Xattr name + * @value: Xattr value + * @size: Size of xattr value + * @flags: Flags + * + * This function is called after an xattr was set on an existing inode. If the + * inode points to a digest cache and the xattr set is security.digest_list, it + * puts and clears dig_user in the inode security blob, to force retrieving a + * fresh digest cache. + */ +void digest_cache_inode_post_setxattr(struct dentry *dentry, const char *name, + const void *value, size_t size, int flags) +{ + if (strcmp(name, XATTR_NAME_DIGEST_LIST)) + return; + + digest_cache_clear_user(d_backing_inode(dentry), dentry->d_name.name, + "inode_post_setxattr"); +} + +/** + * digest_cache_inode_post_removexattr - An xattr was removed + * @dentry: File + * @name: Xattr name + * + * This function is called after an xattr was removed from an existing inode. + * If the inode points to a digest cache and the xattr removed is + * security.digest_list, it puts and clears dig_user in the inode security + * blob, to force retrieving a fresh digest cache. + */ +void digest_cache_inode_post_removexattr(struct dentry *dentry, + const char *name) +{ + if (strcmp(name, XATTR_NAME_DIGEST_LIST)) + return; + + digest_cache_clear_user(d_backing_inode(dentry), dentry->d_name.name, + "inode_post_removexattr"); +}
From: Roberto Sassu roberto.sassu@huawei.com
Add tests to verify the correctness of the Integrity Digest Cache, in all_test.c.
Add the kernel module digest_cache_kern.ko, to let all_test call the API of the Integrity Digest Cache through the newly introduced digest_cache_test file in <securityfs>/integrity/digest_cache.
Test coverage information:
File 'security/integrity/digest_cache/reset.c' Lines executed:100.00% of 53 File 'security/integrity/digest_cache/main.c' Lines executed:90.29% of 206 File 'security/integrity/digest_cache/modsig.c' Lines executed:42.86% of 21 File 'security/integrity/digest_cache/htable.c' Lines executed:93.90% of 82 File 'security/integrity/digest_cache/populate.c' Lines executed:89.19% of 37 File 'security/integrity/digest_cache/verif.c' Lines executed:85.11% of 47 File 'security/integrity/digest_cache/dir.c' Lines executed:90.18% of 163 File 'security/integrity/digest_cache/parsers.c' Lines executed:45.74% of 94 File 'security/integrity/digest_cache/secfs.c' Lines executed:56.00% of 25 File 'security/integrity/digest_cache/parsers/tlv.c' Lines executed:75.26% of 97
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- tools/testing/selftests/Makefile | 1 + .../testing/selftests/digest_cache/.gitignore | 3 + tools/testing/selftests/digest_cache/Makefile | 24 + .../testing/selftests/digest_cache/all_test.c | 769 ++++++++++++++++++ tools/testing/selftests/digest_cache/common.c | 78 ++ tools/testing/selftests/digest_cache/common.h | 93 +++ .../selftests/digest_cache/common_user.c | 33 + .../selftests/digest_cache/common_user.h | 15 + tools/testing/selftests/digest_cache/config | 2 + .../selftests/digest_cache/generators.c | 130 +++ .../selftests/digest_cache/generators.h | 16 + .../selftests/digest_cache/testmod/Makefile | 16 + .../selftests/digest_cache/testmod/kern.c | 551 +++++++++++++ 13 files changed, 1731 insertions(+) create mode 100644 tools/testing/selftests/digest_cache/.gitignore create mode 100644 tools/testing/selftests/digest_cache/Makefile create mode 100644 tools/testing/selftests/digest_cache/all_test.c create mode 100644 tools/testing/selftests/digest_cache/common.c create mode 100644 tools/testing/selftests/digest_cache/common.h create mode 100644 tools/testing/selftests/digest_cache/common_user.c create mode 100644 tools/testing/selftests/digest_cache/common_user.h create mode 100644 tools/testing/selftests/digest_cache/config create mode 100644 tools/testing/selftests/digest_cache/generators.c create mode 100644 tools/testing/selftests/digest_cache/generators.h create mode 100644 tools/testing/selftests/digest_cache/testmod/Makefile create mode 100644 tools/testing/selftests/digest_cache/testmod/kern.c
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index b38199965f99..e6fc3c984923 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -16,6 +16,7 @@ TARGETS += cpu-hotplug TARGETS += damon TARGETS += devices/error_logs TARGETS += devices/probe +TARGETS += digest_cache TARGETS += dmabuf-heaps TARGETS += drivers/dma-buf TARGETS += drivers/s390x/uvdevice diff --git a/tools/testing/selftests/digest_cache/.gitignore b/tools/testing/selftests/digest_cache/.gitignore new file mode 100644 index 000000000000..392096e18f4e --- /dev/null +++ b/tools/testing/selftests/digest_cache/.gitignore @@ -0,0 +1,3 @@ +/*.mod +/*_test +/*.ko diff --git a/tools/testing/selftests/digest_cache/Makefile b/tools/testing/selftests/digest_cache/Makefile new file mode 100644 index 000000000000..1b819201f406 --- /dev/null +++ b/tools/testing/selftests/digest_cache/Makefile @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: GPL-2.0 +TEST_GEN_PROGS_EXTENDED = digest_cache_kern.ko +TEST_GEN_PROGS := all_test + +$(OUTPUT)/%.ko: $(wildcard common.[ch]) testmod/Makefile testmod/kern.c + $(call msg,MOD,,$@) + $(Q)$(MAKE) -C testmod + $(Q)cp testmod/digest_cache_kern.ko $@ + +LOCAL_HDRS += common.h common_user.h generators.h +CFLAGS += -ggdb -Wall -Wextra $(KHDR_INCLUDES) + +OVERRIDE_TARGETS := 1 +override define CLEAN + $(call msg,CLEAN) + $(Q)$(MAKE) -C testmod clean + rm -Rf $(TEST_GEN_PROGS) $(TEST_GEN_PROGS_EXTENDED) + rm -Rf $(OUTPUT)/common.o $(OUTPUT)/common_user.o $(OUTPUT)/generators.o + rm -Rf $(OUTPUT)/common.mod +endef + +include ../lib.mk + +$(OUTPUT)/all_test: common.c common.h common_user.c common_user.h generators.c diff --git a/tools/testing/selftests/digest_cache/all_test.c b/tools/testing/selftests/digest_cache/all_test.c new file mode 100644 index 000000000000..a283068f3cab --- /dev/null +++ b/tools/testing/selftests/digest_cache/all_test.c @@ -0,0 +1,769 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Implement the tests of the Integrity Digest Cache. + */ + +#include <errno.h> +#include <fcntl.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <limits.h> +#include <fts.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/xattr.h> +#include <sys/syscall.h> +#include <linux/module.h> + +#include "generators.h" + +#include "../kselftest_harness.h" +#include "../../../../include/uapi/linux/xattr.h" + +#define BASE_DIR_TEMPLATE "/tmp/digest_cache_test_dirXXXXXX" +#define DIGEST_LISTS_SUBDIR "digest_lists" +#define NUM_DIGEST_LISTS_PREFETCH MAX_WORKS + +FIXTURE(shared_data) { + char base_dir[sizeof(BASE_DIR_TEMPLATE)]; + char digest_lists_dir[sizeof(BASE_DIR_TEMPLATE) + + sizeof(DIGEST_LISTS_SUBDIR)]; + int base_dirfd, digest_lists_dirfd, kernfd, pathfd, cmd_len, no_kprobe; +}; + +FIXTURE_SETUP(shared_data) +{ + char cmd[1024]; + int ret, fd, i, cmd_len; + + /* Create the base directory. */ + snprintf(self->base_dir, sizeof(self->base_dir), BASE_DIR_TEMPLATE); + ASSERT_NE(NULL, mkdtemp(self->base_dir)); + + /* Open base directory. */ + self->base_dirfd = open(self->base_dir, O_RDONLY | O_DIRECTORY); + ASSERT_NE(-1, self->base_dirfd); + + /* Create the digest_lists subdirectory. */ + snprintf(self->digest_lists_dir, sizeof(self->digest_lists_dir), + "%s/%s", self->base_dir, DIGEST_LISTS_SUBDIR); + ASSERT_EQ(0, mkdirat(self->base_dirfd, DIGEST_LISTS_SUBDIR, 0600)); + self->digest_lists_dirfd = openat(self->base_dirfd, DIGEST_LISTS_SUBDIR, + O_RDONLY | O_DIRECTORY); + ASSERT_NE(-1, self->digest_lists_dirfd); + + fd = open("digest_cache_kern.ko", O_RDONLY); + ASSERT_LT(0, fd); + + ASSERT_EQ(0, syscall(SYS_finit_module, fd, "", 0)); + close(fd); + + /* Open kernel test interface. */ + self->kernfd = open(DIGEST_CACHE_TEST_INTERFACE, O_RDWR, 0600); + ASSERT_NE(-1, self->kernfd); + + /* Open kernel digest list path interface. */ + self->pathfd = open(DIGEST_CACHE_PATH_INTERFACE, O_RDWR, 0600); + ASSERT_NE(-1, self->pathfd); + + /* Write the path of the digest lists directory. */ + ASSERT_LT(0, write(self->pathfd, self->digest_lists_dir, + strlen(self->digest_lists_dir))); + + /* Ensure that no verifier is enabled at the beginning of a test. */ + for (i = 0; i < VERIF__LAST; i++) { + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s", + commands_str[DIGEST_CACHE_DISABLE_VERIF], + verifs_str[i]); + ret = write(self->kernfd, cmd, cmd_len); + if (ret < 0 && errno == EOPNOTSUPP) { + self->no_kprobe = 1; + break; + } + + ASSERT_EQ(cmd_len, ret); + } +} + +FIXTURE_TEARDOWN(shared_data) +{ + FTS *fts = NULL; + FTSENT *ftsent; + int fts_flags = (FTS_PHYSICAL | FTS_COMFOLLOW | FTS_NOCHDIR | FTS_XDEV); + char *paths[2] = { self->base_dir, NULL }; + char cmd[1024]; + int cmd_len; + + /* Close digest_lists subdirectory. */ + close(self->digest_lists_dirfd); + + /* Close base directory. */ + close(self->base_dirfd); + + /* Delete files and directories. */ + fts = fts_open(paths, fts_flags, NULL); + if (fts) { + while ((ftsent = fts_read(fts)) != NULL) { + switch (ftsent->fts_info) { + case FTS_DP: + rmdir(ftsent->fts_accpath); + break; + case FTS_F: + case FTS_SL: + case FTS_SLNONE: + case FTS_DEFAULT: + unlink(ftsent->fts_accpath); + break; + default: + break; + } + } + } + + /* Release digest cache reference, if the test was interrupted. */ + cmd_len = snprintf(cmd, sizeof(cmd), "%s", + commands_str[DIGEST_CACHE_PUT]); + write(self->kernfd, cmd, cmd_len); + + /* Close kernel test interface. */ + close(self->kernfd); + + /* Close kernel digest list path interface. */ + close(self->pathfd); + + syscall(SYS_delete_module, "digest_cache_kern", 0); +} + +static int _query_test(int kernfd, char *base_dir, char *filename, + enum hash_algo algo, int start_number, int num_digests, + bool write_digest_list, int digest_lists_dirfd, + char *digest_list_filename) +{ + u8 digest[MAX_DIGEST_SIZE] = { 0 }; + char digest_str[MAX_DIGEST_SIZE * 2 + 1] = { 0 }; + int digest_len = hash_digest_size[algo]; + char cmd[1024]; + int ret, fd, i, cmd_len; + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s/%s", + commands_str[DIGEST_CACHE_GET], base_dir, filename); + ret = write(kernfd, cmd, cmd_len); + if (ret != cmd_len) + return -errno; + + if (write_digest_list) { + fd = openat(digest_lists_dirfd, digest_list_filename, O_WRONLY); + if (fd < 0) + return -errno; + + close(fd); + } + + ret = 0; + + *(u32 *)digest = start_number; + + for (i = 0; i < num_digests; i++) { + bin2hex(digest_str, digest, digest_len); + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s/%s|%s:%s", + commands_str[DIGEST_CACHE_LOOKUP], base_dir, + filename, hash_algo_name[algo], digest_str); + ret = write(kernfd, cmd, cmd_len); + if (ret != cmd_len) { + ret = -errno; + goto out; + } else { + ret = 0; + } + + (*(u32 *)digest)++; + } +out: + cmd_len = snprintf(cmd, sizeof(cmd), "%s", + commands_str[DIGEST_CACHE_PUT]); + write(kernfd, cmd, cmd_len); + return ret; +} + +static int query_test(int kernfd, char *base_dir, char *filename, + enum hash_algo algo, int start_number, int num_digests) +{ + return _query_test(kernfd, base_dir, filename, algo, start_number, + num_digests, false, -1, NULL); +} + +static int query_test_write(int kernfd, char *base_dir, char *filename, + enum hash_algo algo, int start_number, + int num_digests, int digest_lists_dirfd, + char *digest_list_filename) +{ + return _query_test(kernfd, base_dir, filename, algo, start_number, + num_digests, true, digest_lists_dirfd, + digest_list_filename); +} + +static void test_parser(struct _test_data_shared_data *self, + struct __test_metadata *_metadata, + char *digest_list_filename, char *filename, + enum hash_algo algo, int start_number, int num_digests, + unsigned int failure) +{ + int expected_ret = (failure) ? -ENOENT : 0; + + if (!strncmp(digest_list_filename, "tlv-", 4)) { + ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, + digest_list_filename, algo, + start_number, num_digests, + (enum tlv_failures)failure)); + } + + ASSERT_EQ(0, create_file(self->base_dirfd, filename, + digest_list_filename)); + ASSERT_EQ(expected_ret, query_test(self->kernfd, self->base_dir, + filename, algo, start_number, + num_digests)); + + unlinkat(self->digest_lists_dirfd, digest_list_filename, 0); + unlinkat(self->base_dirfd, filename, 0); +} + +/* + * Verify that the tlv digest list parser returns success on well-formatted + * digest lists, for each defined hash algorithm. + */ +TEST_F(shared_data, tlv_parser_ok) +{ + enum hash_algo algo; + + /* Test every known algorithm. */ + for (algo = 0; algo < HASH_ALGO__LAST; algo++) + test_parser(self, _metadata, "tlv-digest_list", "file", algo, + 0, 5, TLV_NO_FAILURE); +} + +/* + * Verify that the tlv digest list parser returns failure on invalid digest + * lists. + */ +TEST_F(shared_data, tlv_parser_error) +{ + enum tlv_failures failure; + + /* Test every failure. */ + for (failure = 0; failure < TLV_FAILURE__LAST; failure++) + test_parser(self, _metadata, "tlv-digest_list", "file", + HASH_ALGO_SHA224, 0, 1, failure); +} + +static void test_default_path(struct _test_data_shared_data *self, + struct __test_metadata *_metadata, bool file) +{ + char path[PATH_MAX]; + size_t path_len; + + if (file) { + path_len = snprintf(path, sizeof(path), + "%s/%s/tlv-digest_list", self->base_dir, + DIGEST_LISTS_SUBDIR); + ASSERT_LT(0, write(self->pathfd, path, path_len)); + } + + ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, "tlv-digest_list", + HASH_ALGO_SHA1, 0, 1, TLV_NO_FAILURE)); + + ASSERT_EQ(0, create_file(self->base_dirfd, "file", NULL)); + + ASSERT_EQ(0, query_test(self->kernfd, self->base_dir, "file", + HASH_ALGO_SHA1, 0, 1)); +} + +/* + * Verify that the digest cache created from the default path (regular file) + * can be retrieved and used for lookup. + */ +TEST_F(shared_data, default_path_file) +{ + test_default_path(self, _metadata, true); +} + +/* + * Verify that the digest cache created from the default path (directory) + * can be retrieved and used for lookup. + */ +TEST_F(shared_data, default_path_dir) +{ + test_default_path(self, _metadata, false); +} + +static void test_file_changes(struct _test_data_shared_data *self, + struct __test_metadata *_metadata, + enum file_changes change) +{ + char digest_list_filename[] = "tlv-digest_list"; + char digest_list_filename_new[] = "tlv-digest_list6"; + char digest_list_filename_xattr[] = "tlv-digest_list7"; + char digest_list_path[sizeof(self->digest_lists_dir) + + sizeof(digest_list_filename)]; + int fd; + + ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, + digest_list_filename, HASH_ALGO_SHA1, 0, 1, + TLV_NO_FAILURE)); + + ASSERT_EQ(0, create_file(self->base_dirfd, "file", + digest_list_filename)); + + ASSERT_EQ(0, query_test(self->kernfd, self->base_dir, "file", + HASH_ALGO_SHA1, 0, 1)); + + switch (change) { + case FILE_WRITE: + fd = openat(self->digest_lists_dirfd, digest_list_filename, + O_WRONLY); + ASSERT_NE(-1, fd); + + ASSERT_EQ(4, write(fd, "1234", 4)); + close(fd); + break; + case FILE_TRUNCATE: + snprintf(digest_list_path, sizeof(digest_list_path), + "%s/%s", self->digest_lists_dir, digest_list_filename); + ASSERT_EQ(0, truncate(digest_list_path, 4)); + break; + case FILE_FTRUNCATE: + fd = openat(self->digest_lists_dirfd, digest_list_filename, + O_WRONLY); + ASSERT_NE(-1, fd); + ASSERT_EQ(0, ftruncate(fd, 4)); + close(fd); + break; + case FILE_UNLINK: + ASSERT_EQ(0, unlinkat(self->digest_lists_dirfd, + digest_list_filename, 0)); + break; + case FILE_RENAME: + ASSERT_EQ(0, renameat(self->digest_lists_dirfd, + digest_list_filename, + self->digest_lists_dirfd, + digest_list_filename_new)); + break; + case FILE_SETXATTR: + fd = openat(self->base_dirfd, "file", O_WRONLY); + ASSERT_NE(-1, fd); + + ASSERT_EQ(0, fsetxattr(fd, XATTR_NAME_DIGEST_LIST, + digest_list_filename_xattr, + strlen(digest_list_filename_xattr) + 1, + 0)); + close(fd); + break; + case FILE_REMOVEXATTR: + fd = openat(self->base_dirfd, "file", O_WRONLY); + ASSERT_NE(-1, fd); + + ASSERT_EQ(0, fremovexattr(fd, XATTR_NAME_DIGEST_LIST)); + close(fd); + + /* + * Removing security.digest_list does not cause a failure, + * the digest can be still retrieved via directory lookup. + */ + ASSERT_EQ(0, query_test(self->kernfd, self->base_dir, "file", + HASH_ALGO_SHA1, 0, 1)); + + return; + default: + break; + } + + ASSERT_NE(0, query_test(self->kernfd, self->base_dir, "file", + HASH_ALGO_SHA1, 0, 1)); +} + +/* + * Verify that operations on a digest list cause a reset of the digest cache, + * and that the digest is not found in the invalid/missing digest list. + */ +TEST_F(shared_data, file_reset) +{ + enum file_changes change; + + /* Test for every file change. */ + for (change = 0; change < FILE_CHANGE__LAST; change++) + test_file_changes(self, _metadata, change); +} + +/* + * Verify that digest lookup does not work after a reset, and attempt the + * query a second time, which should instead succeed. + */ +TEST_F(shared_data, file_reset_again) +{ + char digest_list_filename[] = "tlv-digest_list"; + + ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, + digest_list_filename, HASH_ALGO_SHA1, 0, 1, + TLV_NO_FAILURE)); + + ASSERT_EQ(0, create_file(self->base_dirfd, "file", + digest_list_filename)); + + ASSERT_NE(0, query_test_write(self->kernfd, self->base_dir, "file", + HASH_ALGO_SHA1, 0, 1, + self->digest_lists_dirfd, + digest_list_filename)); + + ASSERT_EQ(0, query_test(self->kernfd, self->base_dir, "file", + HASH_ALGO_SHA1, 0, 1)); +} + +static void query_test_with_failures(struct _test_data_shared_data *self, + struct __test_metadata *_metadata, + int start_number, int num_digests, + int *removed, int num_removed) +{ + char filename[NAME_MAX + 1]; + int i, j, expected_ret; + + for (i = start_number; i < start_number + num_digests; i++) { + expected_ret = 0; + + for (j = 0; j < num_removed; j++) { + if (removed[j] == i) { + expected_ret = -ENOENT; + break; + } + } + + snprintf(filename, sizeof(filename), "file%d", i); + + ASSERT_EQ(expected_ret, query_test(self->kernfd, self->base_dir, + filename, HASH_ALGO_SHA1, i, + 1)); + } +} + +/* + * Verify that changes in the digest list directory are monitored and that + * a digest cannot be found if the respective digest list file has been moved + * away from the directory, and that a digest can be found if the respective + * digest list has been moved/created in the directory. + */ +TEST_F(shared_data, dir_reset) +{ + char filename[NAME_MAX + 1]; + int i, removed[10]; + + for (i = 0; i < 10; i++) { + snprintf(filename, sizeof(filename), "file%d", i); + ASSERT_EQ(0, create_file(self->base_dirfd, filename, NULL)); + snprintf(filename, sizeof(filename), "tlv-digest_list%d", i); + ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, + filename, HASH_ALGO_SHA1, i, 1, + TLV_NO_FAILURE)); + } + + query_test_with_failures(self, _metadata, 0, 10, removed, 0); + + ASSERT_EQ(0, unlinkat(self->digest_lists_dirfd, "tlv-digest_list7", 0)); + + removed[0] = 7; + + query_test_with_failures(self, _metadata, 0, 10, removed, 1); + + ASSERT_EQ(0, renameat(self->digest_lists_dirfd, "tlv-digest_list6", + self->base_dirfd, "tlv-digest_list6")); + + removed[1] = 6; + + query_test_with_failures(self, _metadata, 0, 10, removed, 2); + + ASSERT_EQ(0, renameat(self->base_dirfd, "tlv-digest_list6", + self->digest_lists_dirfd, "tlv-digest_list6")); + + query_test_with_failures(self, _metadata, 0, 10, removed, 1); + + ASSERT_EQ(0, create_file(self->base_dirfd, "file10", NULL)); + ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, "tlv-digest_list10", + HASH_ALGO_SHA1, 10, 1, TLV_NO_FAILURE)); + + query_test_with_failures(self, _metadata, 0, 11, removed, 1); +} + +static void _check_verif_data(struct _test_data_shared_data *self, + struct __test_metadata *_metadata, + char *digest_list_filename, int num, + enum hash_algo algo, bool check_dir) +{ + char digest_list_filename_kernel[NAME_MAX + 1]; + char cmd[1024], number[20]; + u8 digest[MAX_DIGEST_SIZE] = { 0 }; + char digest_str[MAX_DIGEST_SIZE * 2 + 1] = { 0 }; + int len, cmd_len; + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s/file", + commands_str[DIGEST_CACHE_GET], self->base_dir); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + + /* + * If a directory digest cache was requested, we need to do a lookup, + * to make the kernel module retrieve verification data from the digest + * cache of the directory entry. + */ + if (check_dir) { + *(u32 *)digest = num; + + bin2hex(digest_str, digest, hash_digest_size[algo]); + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s/file|%s:%s", + commands_str[DIGEST_CACHE_LOOKUP], + self->base_dir, hash_algo_name[algo], + digest_str); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + } + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s", + commands_str[DIGEST_CACHE_SET_VERIF], + verifs_str[VERIF_FILENAMES]); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + + ASSERT_LT(0, read(self->kernfd, digest_list_filename_kernel, + sizeof(digest_list_filename_kernel))); + ASSERT_EQ(0, strcmp(digest_list_filename, digest_list_filename_kernel)); + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s", + commands_str[DIGEST_CACHE_SET_VERIF], + verifs_str[VERIF_NUMBER]); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + + len = read(self->kernfd, number, sizeof(number) - 1); + ASSERT_LT(0, len); + number[len] = '\0'; + ASSERT_EQ(num, atoi(number)); + + cmd_len = snprintf(cmd, sizeof(cmd), "%s", + commands_str[DIGEST_CACHE_PUT]); + write(self->kernfd, cmd, cmd_len); +} + +static void check_verif_data(struct _test_data_shared_data *self, + struct __test_metadata *_metadata) +{ + char digest_list_filename[NAME_MAX + 1]; + char cmd[1024]; + int i, cmd_len; + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s", + commands_str[DIGEST_CACHE_ENABLE_VERIF], + verifs_str[VERIF_FILENAMES]); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s", + commands_str[DIGEST_CACHE_ENABLE_VERIF], + verifs_str[VERIF_NUMBER]); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + + /* + * Reverse order is intentional, so that directory entries are created + * in the opposite order as when they are searched (when prefetching is + * requested). + */ + for (i = 10; i >= 0; i--) { + snprintf(digest_list_filename, sizeof(digest_list_filename), + "%d-tlv-digest_list%d", i, i); + ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, + digest_list_filename, HASH_ALGO_SHA1, + i, 1, TLV_NO_FAILURE)); + + ASSERT_EQ(0, create_file(self->base_dirfd, "file", + digest_list_filename)); + + _check_verif_data(self, _metadata, digest_list_filename, i, + HASH_ALGO_SHA1, false); + + ASSERT_EQ(0, unlinkat(self->base_dirfd, "file", 0)); + } + + for (i = 0; i < 11; i++) { + ASSERT_EQ(0, create_file(self->base_dirfd, "file", NULL)); + snprintf(digest_list_filename, sizeof(digest_list_filename), + "%d-tlv-digest_list%d", i, i); + _check_verif_data(self, _metadata, digest_list_filename, i, + HASH_ALGO_SHA1, true); + ASSERT_EQ(0, unlinkat(self->base_dirfd, "file", 0)); + } +} + +/* + * Verify that the correct verification data can be retrieved from the digest + * caches (without digest list prefetching). + */ +TEST_F(shared_data, verif_data_no_prefetch) +{ + if (self->no_kprobe) + SKIP(return, "CONFIG_DYNAMIC_FTRACE_WITH_ARGS not enabled"); + + check_verif_data(self, _metadata); +} + +/* + * Verify that the correct verification data can be retrieved from the digest + * caches (with digest list prefetching). + */ +TEST_F(shared_data, verif_data_prefetch) +{ + if (self->no_kprobe) + SKIP(return, "CONFIG_DYNAMIC_FTRACE_WITH_ARGS not enabled"); + + ASSERT_EQ(0, lsetxattr(self->base_dir, XATTR_NAME_DIG_PREFETCH, + "1", 1, 0)); + + check_verif_data(self, _metadata); +} + +static void check_prefetch_list(struct _test_data_shared_data *self, + struct __test_metadata *_metadata, + int start_number, int end_number) +{ + char digest_list_filename[NAME_MAX + 1], filename[NAME_MAX + 1]; + char digest_lists[1024] = { 0 }, digest_lists_kernel[1024]; + char cmd[1024]; + int i, cmd_len; + + snprintf(filename, sizeof(filename), "file%d", end_number); + snprintf(digest_list_filename, sizeof(digest_list_filename), + "%d-tlv-digest_list%d", end_number, end_number); + ASSERT_EQ(0, create_file(self->base_dirfd, filename, + digest_list_filename)); + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s/%s", + commands_str[DIGEST_CACHE_GET], self->base_dir, + filename); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + + ASSERT_LT(0, read(self->kernfd, digest_lists_kernel, + sizeof(digest_lists_kernel))); + + for (i = start_number; i <= end_number; i++) { + if (digest_lists[0]) + strcat(digest_lists, ","); + + snprintf(digest_list_filename, sizeof(digest_list_filename), + "%d-tlv-digest_list%d", i, i); + strcat(digest_lists, digest_list_filename); + } + + ASSERT_EQ(0, strcmp(digest_lists, digest_lists_kernel)); + + ASSERT_EQ(0, unlinkat(self->base_dirfd, filename, 0)); + + cmd_len = snprintf(cmd, sizeof(cmd), "%s", + commands_str[DIGEST_CACHE_PUT]); + write(self->kernfd, cmd, cmd_len); +} + +static void check_prefetch_list_async(struct _test_data_shared_data *self, + struct __test_metadata *_metadata) +{ + char digest_list_filename[NAME_MAX + 1], filename[NAME_MAX + 1]; + char digest_lists[1024] = { 0 }, digest_lists_kernel[1024]; + char cmd[1024]; + int i, cmd_len; + + for (i = 0; i < NUM_DIGEST_LISTS_PREFETCH; i++) { + snprintf(filename, sizeof(filename), "file%d", + NUM_DIGEST_LISTS_PREFETCH - 1 - i); + snprintf(digest_list_filename, sizeof(digest_list_filename), + "%d-tlv-digest_list%d", i, i); + ASSERT_EQ(0, create_file(self->base_dirfd, filename, + digest_list_filename)); + } + + /* Do batch of get/put to test the kernel for concurrent requests. */ + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s/file|%d|%d", + commands_str[DIGEST_CACHE_GET_PUT_ASYNC], + self->base_dir, 0, NUM_DIGEST_LISTS_PREFETCH - 1); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + + ASSERT_LT(0, read(self->kernfd, digest_lists_kernel, + sizeof(digest_lists_kernel))); + + for (i = 0; i < NUM_DIGEST_LISTS_PREFETCH; i++) { + if (digest_lists[0]) + strcat(digest_lists, ","); + + snprintf(digest_list_filename, sizeof(digest_list_filename), + "%d-tlv-digest_list%d", i, i); + strcat(digest_lists, digest_list_filename); + } + + ASSERT_EQ(0, strcmp(digest_lists, digest_lists_kernel)); +} + +static void prepare_prefetch(struct _test_data_shared_data *self, + struct __test_metadata *_metadata) +{ + char digest_list_filename[NAME_MAX + 1]; + char cmd[1024]; + int i, cmd_len; + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s", + commands_str[DIGEST_CACHE_ENABLE_VERIF], + verifs_str[VERIF_PREFETCH]); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + + cmd_len = snprintf(cmd, sizeof(cmd), "%s|%s", + commands_str[DIGEST_CACHE_SET_VERIF], + verifs_str[VERIF_PREFETCH]); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + + for (i = NUM_DIGEST_LISTS_PREFETCH - 1; i >= 0; i--) { + snprintf(digest_list_filename, sizeof(digest_list_filename), + "%d-tlv-digest_list%d", i, i); + ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, + digest_list_filename, HASH_ALGO_SHA1, + i, 1, TLV_NO_FAILURE)); + } + + ASSERT_EQ(0, fsetxattr(self->digest_lists_dirfd, + XATTR_NAME_DIG_PREFETCH, "1", 1, 0)); +} + +/* + * Verify that digest lists are prefetched when requested, in the correct order + * (synchronous version). + */ +TEST_F(shared_data, prefetch_sync) +{ + int i; + + if (self->no_kprobe) + SKIP(return, "CONFIG_DYNAMIC_FTRACE_WITH_ARGS not enabled"); + + prepare_prefetch(self, _metadata); + + for (i = 2; i < NUM_DIGEST_LISTS_PREFETCH; i += 3) + check_prefetch_list(self, _metadata, i - 2, i); +} + +/* + * Verify that digest lists are prefetched when requested, in the correct order + * (asynchronous version). + */ +TEST_F(shared_data, prefetch_async) +{ + if (self->no_kprobe) + SKIP(return, "CONFIG_DYNAMIC_FTRACE_WITH_ARGS not enabled"); + + prepare_prefetch(self, _metadata); + + check_prefetch_list_async(self, _metadata); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/digest_cache/common.c b/tools/testing/selftests/digest_cache/common.c new file mode 100644 index 000000000000..75c0ef50deb8 --- /dev/null +++ b/tools/testing/selftests/digest_cache/common.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Add common code for testing the Integrity Digest Cache. + */ + +#include "common.h" + +const char *commands_str[DIGEST_CACHE__LAST] = { + [DIGEST_CACHE_GET] = "get", + [DIGEST_CACHE_LOOKUP] = "lookup", + [DIGEST_CACHE_PUT] = "put", + [DIGEST_CACHE_ENABLE_VERIF] = "enable_verif", + [DIGEST_CACHE_DISABLE_VERIF] = "disable_verif", + [DIGEST_CACHE_SET_VERIF] = "set_verif", + [DIGEST_CACHE_GET_PUT_ASYNC] = "get_put_async", +}; + +const char *const hash_algo_name[HASH_ALGO__LAST] = { + [HASH_ALGO_MD4] = "md4", + [HASH_ALGO_MD5] = "md5", + [HASH_ALGO_SHA1] = "sha1", + [HASH_ALGO_RIPE_MD_160] = "rmd160", + [HASH_ALGO_SHA256] = "sha256", + [HASH_ALGO_SHA384] = "sha384", + [HASH_ALGO_SHA512] = "sha512", + [HASH_ALGO_SHA224] = "sha224", + [HASH_ALGO_RIPE_MD_128] = "rmd128", + [HASH_ALGO_RIPE_MD_256] = "rmd256", + [HASH_ALGO_RIPE_MD_320] = "rmd320", + [HASH_ALGO_WP_256] = "wp256", + [HASH_ALGO_WP_384] = "wp384", + [HASH_ALGO_WP_512] = "wp512", + [HASH_ALGO_TGR_128] = "tgr128", + [HASH_ALGO_TGR_160] = "tgr160", + [HASH_ALGO_TGR_192] = "tgr192", + [HASH_ALGO_SM3_256] = "sm3", + [HASH_ALGO_STREEBOG_256] = "streebog256", + [HASH_ALGO_STREEBOG_512] = "streebog512", + [HASH_ALGO_SHA3_256] = "sha3-256", + [HASH_ALGO_SHA3_384] = "sha3-384", + [HASH_ALGO_SHA3_512] = "sha3-512", +}; + +const int hash_digest_size[HASH_ALGO__LAST] = { + [HASH_ALGO_MD4] = MD5_DIGEST_SIZE, + [HASH_ALGO_MD5] = MD5_DIGEST_SIZE, + [HASH_ALGO_SHA1] = SHA1_DIGEST_SIZE, + [HASH_ALGO_RIPE_MD_160] = RMD160_DIGEST_SIZE, + [HASH_ALGO_SHA256] = SHA256_DIGEST_SIZE, + [HASH_ALGO_SHA384] = SHA384_DIGEST_SIZE, + [HASH_ALGO_SHA512] = SHA512_DIGEST_SIZE, + [HASH_ALGO_SHA224] = SHA224_DIGEST_SIZE, + [HASH_ALGO_RIPE_MD_128] = RMD128_DIGEST_SIZE, + [HASH_ALGO_RIPE_MD_256] = RMD256_DIGEST_SIZE, + [HASH_ALGO_RIPE_MD_320] = RMD320_DIGEST_SIZE, + [HASH_ALGO_WP_256] = WP256_DIGEST_SIZE, + [HASH_ALGO_WP_384] = WP384_DIGEST_SIZE, + [HASH_ALGO_WP_512] = WP512_DIGEST_SIZE, + [HASH_ALGO_TGR_128] = TGR128_DIGEST_SIZE, + [HASH_ALGO_TGR_160] = TGR160_DIGEST_SIZE, + [HASH_ALGO_TGR_192] = TGR192_DIGEST_SIZE, + [HASH_ALGO_SM3_256] = SM3256_DIGEST_SIZE, + [HASH_ALGO_STREEBOG_256] = STREEBOG256_DIGEST_SIZE, + [HASH_ALGO_STREEBOG_512] = STREEBOG512_DIGEST_SIZE, + [HASH_ALGO_SHA3_256] = SHA3_256_DIGEST_SIZE, + [HASH_ALGO_SHA3_384] = SHA3_384_DIGEST_SIZE, + [HASH_ALGO_SHA3_512] = SHA3_512_DIGEST_SIZE, +}; + +const char *verifs_str[] = { + [VERIF_FILENAMES] = "kprobe_filenames", + [VERIF_NUMBER] = "kprobe_number", + [VERIF_PREFETCH] = "kprobe_prefetch", +}; diff --git a/tools/testing/selftests/digest_cache/common.h b/tools/testing/selftests/digest_cache/common.h new file mode 100644 index 000000000000..3fd3c249ff6f --- /dev/null +++ b/tools/testing/selftests/digest_cache/common.h @@ -0,0 +1,93 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Header of common.c. + */ + +#ifndef _COMMON_H +#define _COMMON_H +#include <linux/types.h> + +#include "../../../../include/uapi/linux/hash_info.h" + +#define MD5_DIGEST_SIZE 16 +#define SHA1_DIGEST_SIZE 20 +#define RMD160_DIGEST_SIZE 20 +#define SHA256_DIGEST_SIZE 32 +#define SHA384_DIGEST_SIZE 48 +#define SHA512_DIGEST_SIZE 64 +#define SHA224_DIGEST_SIZE 28 +#define RMD128_DIGEST_SIZE 16 +#define RMD256_DIGEST_SIZE 32 +#define RMD320_DIGEST_SIZE 40 +#define WP256_DIGEST_SIZE 32 +#define WP384_DIGEST_SIZE 48 +#define WP512_DIGEST_SIZE 64 +#define TGR128_DIGEST_SIZE 16 +#define TGR160_DIGEST_SIZE 20 +#define TGR192_DIGEST_SIZE 24 +#define SM3256_DIGEST_SIZE 32 +#define STREEBOG256_DIGEST_SIZE 32 +#define STREEBOG512_DIGEST_SIZE 64 +#define SHA3_224_DIGEST_SIZE (224 / 8) +#define SHA3_256_DIGEST_SIZE (256 / 8) +#define SHA3_384_DIGEST_SIZE (384 / 8) +#define SHA3_512_DIGEST_SIZE (512 / 8) + +#define DIGEST_CACHE_DIR "/sys/kernel/security/integrity/digest_cache" +#define DIGEST_CACHE_TEST_INTERFACE "/sys/kernel/security/digest_cache_test" +#define DIGEST_CACHE_PATH_INTERFACE DIGEST_CACHE_DIR "/default_path" +#define MAX_DIGEST_SIZE 64 + +#define MAX_WORKS 21 + +typedef __u8 u8; +typedef __u16 u16; +typedef __u32 u32; +typedef __s32 s32; +typedef __u64 u64; + +enum commands { + DIGEST_CACHE_GET, // args: <path> + DIGEST_CACHE_LOOKUP, // args: <algo>|<digest> + DIGEST_CACHE_PUT, // args: + DIGEST_CACHE_ENABLE_VERIF, // args: <verif name> + DIGEST_CACHE_DISABLE_VERIF, // args: <verif name> + DIGEST_CACHE_SET_VERIF, // args: <verif name> + DIGEST_CACHE_GET_PUT_ASYNC, // args: <path>|<start#>|<end#> + DIGEST_CACHE__LAST, +}; + +enum tlv_failures { TLV_NO_FAILURE, + TLV_FAILURE_ALGO_LEN, + TLV_FAILURE_ALGO_MISMATCH, + TLV_FAILURE_NUM_DIGESTS, + TLV_FAILURE__LAST +}; + +enum file_changes { FILE_WRITE, + FILE_TRUNCATE, + FILE_FTRUNCATE, + FILE_UNLINK, + FILE_RENAME, + FILE_SETXATTR, + FILE_REMOVEXATTR, + FILE_CHANGE__LAST +}; + +enum VERIFS { + VERIF_FILENAMES, + VERIF_NUMBER, + VERIF_PREFETCH, + VERIF__LAST +}; + +extern const char *commands_str[DIGEST_CACHE__LAST]; +extern const char *const hash_algo_name[HASH_ALGO__LAST]; +extern const int hash_digest_size[HASH_ALGO__LAST]; +extern const char *verifs_str[VERIF__LAST]; + +#endif /* _COMMON_H */ diff --git a/tools/testing/selftests/digest_cache/common_user.c b/tools/testing/selftests/digest_cache/common_user.c new file mode 100644 index 000000000000..02f661b942f7 --- /dev/null +++ b/tools/testing/selftests/digest_cache/common_user.c @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Add common code in user space for testing the Integrity Digest Cache. + */ + +#include <stddef.h> + +#include "common_user.h" + +static const char hex_asc[] = "0123456789abcdef"; + +#define hex_asc_lo(x) hex_asc[((x) & 0x0f)] +#define hex_asc_hi(x) hex_asc[((x) & 0xf0) >> 4] + +static inline char *hex_byte_pack(char *buf, unsigned char byte) +{ + *buf++ = hex_asc_hi(byte); + *buf++ = hex_asc_lo(byte); + return buf; +} + +char *bin2hex(char *dst, const void *src, size_t count) +{ + const unsigned char *_src = src; + + while (count--) + dst = hex_byte_pack(dst, *_src++); + return dst; +} diff --git a/tools/testing/selftests/digest_cache/common_user.h b/tools/testing/selftests/digest_cache/common_user.h new file mode 100644 index 000000000000..8d1f6631ef83 --- /dev/null +++ b/tools/testing/selftests/digest_cache/common_user.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Header of common_user.c. + */ + +#include <linux/types.h> +#include <stddef.h> + +#include "common.h" + +char *bin2hex(char *dst, const void *src, size_t count); diff --git a/tools/testing/selftests/digest_cache/config b/tools/testing/selftests/digest_cache/config new file mode 100644 index 000000000000..7e0ea7d18828 --- /dev/null +++ b/tools/testing/selftests/digest_cache/config @@ -0,0 +1,2 @@ +CONFIG_INTEGRITY_DIGEST_CACHE=y +CONFIG_DYNAMIC_FTRACE=y diff --git a/tools/testing/selftests/digest_cache/generators.c b/tools/testing/selftests/digest_cache/generators.c new file mode 100644 index 000000000000..8c36c22e8bcc --- /dev/null +++ b/tools/testing/selftests/digest_cache/generators.c @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Generate digest lists for testing. + */ + +#include <stddef.h> +#include <fcntl.h> +#include <errno.h> +#include <limits.h> +#include <string.h> +#include <unistd.h> +#include <sys/xattr.h> +#include <asm/byteorder.h> + +#include "generators.h" +#include "../../../../include/uapi/linux/hash_info.h" +#include "../../../../include/uapi/linux/xattr.h" +#include "../../../../include/uapi/linux/tlv_digest_list.h" +#include "../../../../include/uapi/linux/tlv_parser.h" + +int gen_tlv_list(int temp_dirfd, char *digest_list_filename, + enum hash_algo algo, int start_number, int num_digests, + enum tlv_failures failure) +{ + __u16 _algo = __cpu_to_be16(algo); + __u32 _num_entries = __cpu_to_be32(num_digests); + u8 digest[MAX_DIGEST_SIZE] = { 0 }; + int digest_len = hash_digest_size[algo]; + int ret, fd, i; + + struct tlv_entry algo_entry = { + .field = __cpu_to_be16(DIGEST_LIST_ALGO), + .length = __cpu_to_be32(sizeof(_algo)), + }; + + struct tlv_entry num_entries_entry = { + .field = __cpu_to_be16(DIGEST_LIST_NUM_ENTRIES), + .length = __cpu_to_be32(sizeof(_num_entries)), + }; + + struct tlv_entry entry_digest = { + .field = __cpu_to_be16(DIGEST_LIST_ENTRY_DIGEST), + .length = __cpu_to_be32(digest_len), + }; + + struct tlv_entry entry_entry = { + .field = __cpu_to_be16(DIGEST_LIST_ENTRY), + .length = __cpu_to_be32(sizeof(entry_digest) + digest_len), + }; + + switch (failure) { + case TLV_FAILURE_ALGO_LEN: + algo_entry.length = algo_entry.length / 2; + break; + case TLV_FAILURE_ALGO_MISMATCH: + _algo = __cpu_to_be16(algo - 1); + break; + case TLV_FAILURE_NUM_DIGESTS: + num_digests = 0; + break; + default: + break; + } + + fd = openat(temp_dirfd, digest_list_filename, + O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd == -1) + return -errno; + + ret = write(fd, (u8 *)&algo_entry, sizeof(algo_entry)); + if (ret != sizeof(algo_entry)) + return -errno; + + ret = write(fd, (u8 *)&_algo, sizeof(_algo)); + if (ret != sizeof(_algo)) + return -errno; + + ret = write(fd, (u8 *)&num_entries_entry, sizeof(num_entries_entry)); + if (ret != sizeof(num_entries_entry)) + return -errno; + + ret = write(fd, (u8 *)&_num_entries, sizeof(_num_entries)); + if (ret != sizeof(_num_entries)) + return -errno; + + *(u32 *)digest = start_number; + + for (i = 0; i < num_digests; i++) { + ret = write(fd, (u8 *)&entry_entry, sizeof(entry_entry)); + if (ret != sizeof(entry_entry)) + return -errno; + + ret = write(fd, (u8 *)&entry_digest, sizeof(entry_digest)); + if (ret != sizeof(entry_digest)) + return -errno; + + ret = write(fd, digest, digest_len); + if (ret != digest_len) + return -errno; + + (*(u32 *)digest)++; + } + + close(fd); + return 0; +} + +int create_file(int temp_dirfd, char *filename, char *digest_list_filename) +{ + int ret = 0, fd; + + fd = openat(temp_dirfd, filename, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd == -1) + return -errno; + + if (!digest_list_filename) + goto out; + + ret = fsetxattr(fd, XATTR_NAME_DIGEST_LIST, digest_list_filename, + strlen(digest_list_filename) + 1, 0); + if (ret == -1) + ret = -errno; +out: + close(fd); + return ret; +} diff --git a/tools/testing/selftests/digest_cache/generators.h b/tools/testing/selftests/digest_cache/generators.h new file mode 100644 index 000000000000..eeeb3c020891 --- /dev/null +++ b/tools/testing/selftests/digest_cache/generators.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Header of generators.c. + */ + +#include "common.h" +#include "common_user.h" + +int gen_tlv_list(int temp_dirfd, char *digest_list_filename, + enum hash_algo algo, int start_number, int num_digests, + enum tlv_failures failure); +int create_file(int temp_dirfd, char *filename, char *digest_list_filename); diff --git a/tools/testing/selftests/digest_cache/testmod/Makefile b/tools/testing/selftests/digest_cache/testmod/Makefile new file mode 100644 index 000000000000..1ba1c7f08658 --- /dev/null +++ b/tools/testing/selftests/digest_cache/testmod/Makefile @@ -0,0 +1,16 @@ +KDIR ?= ../../../../.. + +MODULES = digest_cache_kern.ko + +obj-m += digest_cache_kern.o + +digest_cache_kern-y := kern.o ../common.o + +all: + +$(Q)$(MAKE) -C $(KDIR) M=$$PWD modules + +clean: + +$(Q)$(MAKE) -C $(KDIR) M=$$PWD clean + +install: all + +$(Q)$(MAKE) -C $(KDIR) M=$$PWD modules_install diff --git a/tools/testing/selftests/digest_cache/testmod/kern.c b/tools/testing/selftests/digest_cache/testmod/kern.c new file mode 100644 index 000000000000..4c215fd0b50f --- /dev/null +++ b/tools/testing/selftests/digest_cache/testmod/kern.c @@ -0,0 +1,551 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Implement the kernel module to interact with the Integrity Digest Cache. + */ + +#define pr_fmt(fmt) "digest_cache TESTMOD: "fmt +#include <linux/module.h> +#include <linux/namei.h> +#include <linux/security.h> +#include <linux/dynamic_debug.h> +#include <linux/digest_cache.h> +#include <linux/kprobes.h> +#include <linux/cpu.h> +#include <linux/kernel_read_file.h> +#include <crypto/hash_info.h> + +#include "../common.h" + +struct verif { + int (*update)(struct file *file); + ssize_t (*read)(struct file *file, char __user *buf, size_t datalen, + loff_t *ppos); + bool enabled; +}; + +struct read_work { + struct work_struct work; + char *path_str; + int ret; +}; + +static struct dentry *test; +static struct digest_cache *digest_cache, *found; +static int cur_verif_index; +static u8 prefetch_buf[4096]; +static struct read_work w[MAX_WORKS]; + +static int filenames_update(struct file *file) +{ + char *filename = (char *)file->f_path.dentry->d_name.name; + + return digest_cache_verif_set(file, verifs_str[VERIF_FILENAMES], + filename, strlen(filename) + 1); +} + +static int number_update(struct file *file) +{ + const char *filename = file_dentry(file)->d_name.name; + size_t filename_len = strlen(filename); + u64 number = U64_MAX; + int ret; + + while (filename_len) { + if (filename[filename_len - 1] < '0' || + filename[filename_len - 1] > '9') + break; + + filename_len--; + } + + ret = kstrtoull(filename + filename_len, 10, &number); + if (ret < 0) { + pr_debug("Failed to convert filename %s into number\n", + file_dentry(file)->d_name.name); + return ret; + } + + return digest_cache_verif_set(file, verifs_str[VERIF_NUMBER], &number, + sizeof(number)); +} + +static int prefetch_update(struct file *file) +{ + char *filename = (char *)file->f_path.dentry->d_name.name; + char *start_ptr = prefetch_buf, *end_ptr; + int ret; + + ret = digest_cache_verif_set(file, "kprobe_test", "1", 1); + if (!ret) { + /* Don't include duplicates of requested digest lists. */ + while ((end_ptr = strchrnul(start_ptr, ','))) { + if (end_ptr > start_ptr && + !strncmp(start_ptr, filename, end_ptr - start_ptr)) + return 0; + + if (!*end_ptr) + break; + + start_ptr = end_ptr + 1; + } + } + + if (prefetch_buf[0]) + strlcat(prefetch_buf, ",", sizeof(prefetch_buf)); + + strlcat(prefetch_buf, filename, sizeof(prefetch_buf)); + return 0; +} + +static ssize_t filenames_read(struct file *file, char __user *buf, + size_t datalen, loff_t *ppos) +{ + loff_t _ppos = 0; + char *filenames_list; + + filenames_list = digest_cache_verif_get(found ?: digest_cache, + verifs_str[VERIF_FILENAMES]); + if (!filenames_list) + return -ENOENT; + + return simple_read_from_buffer(buf, datalen, &_ppos, filenames_list, + strlen(filenames_list) + 1); +} + +static ssize_t number_read(struct file *file, char __user *buf, size_t datalen, + loff_t *ppos) +{ + loff_t _ppos = 0; + u64 *number; + char temp[20]; + ssize_t len; + + number = digest_cache_verif_get(found ?: digest_cache, + verifs_str[VERIF_NUMBER]); + if (!number) + return -ENOENT; + + len = snprintf(temp, sizeof(temp), "%llu", *number); + + return simple_read_from_buffer(buf, datalen, &_ppos, temp, len); +} + +static ssize_t prefetch_read(struct file *file, char __user *buf, + size_t datalen, loff_t *ppos) +{ + loff_t _ppos = 0; + ssize_t ret; + + ret = simple_read_from_buffer(buf, datalen, &_ppos, prefetch_buf, + strlen(prefetch_buf) + 1); + memset(prefetch_buf, 0, sizeof(prefetch_buf)); + return ret; +} + +static struct verif verifs_methods[] = { + [VERIF_FILENAMES] = { .update = filenames_update, + .read = filenames_read }, + [VERIF_NUMBER] = { .update = number_update, .read = number_read }, + [VERIF_PREFETCH] = { .update = prefetch_update, .read = prefetch_read }, +}; + +static void digest_cache_get_put_work(struct work_struct *work) +{ + struct read_work *w = container_of(work, struct read_work, work); + struct digest_cache *digest_cache; + struct path path; + struct file *file; + + w->ret = kern_path(w->path_str, 0, &path); + if (w->ret < 0) + return; + + file = kernel_file_open(&path, O_RDONLY, current_cred()); + if (IS_ERR(file)) { + path_put(&path); + return; + } + + digest_cache = digest_cache_get(file); + + fput(file); + path_put(&path); + + if (!digest_cache) { + w->ret = -ENOENT; + return; + } + + digest_cache_put(digest_cache); + w->ret = 0; +} + +static int digest_cache_get_put_async(char *path_str, int start_number, + int end_number) +{ + int ret = 0, i; + + cpus_read_lock(); + for (i = start_number; i <= end_number; i++) { + w[i].path_str = kasprintf(GFP_KERNEL, "%s%u", path_str, i); + if (!w[i].path_str) { + ret = -ENOMEM; + break; + } + + INIT_WORK_ONSTACK(&w[i].work, digest_cache_get_put_work); + schedule_work_on(i % num_online_cpus(), &w[i].work); + } + cpus_read_unlock(); + + for (i = start_number; i <= end_number; i++) { + if (!w[i].path_str) + continue; + + flush_work(&w[i].work); + destroy_work_on_stack(&w[i].work); + kfree(w[i].path_str); + w[i].path_str = NULL; + if (!ret) + ret = w[i].ret; + } + + return ret; +} + +static ssize_t write_request(struct file *file, const char __user *buf, + size_t datalen, loff_t *ppos) +{ + char *data, *data_ptr, *cmd_str, *path_str, *algo_str, *digest_str; + char *verif_name_str, *start_number_str, *end_number_str; + u8 digest[64]; + struct path path; + struct file *filp; + int ret, cmd, algo, verif_index, start_number, end_number; + + data = memdup_user_nul(buf, datalen); + if (IS_ERR(data)) + return PTR_ERR(data); + + data_ptr = data; + + cmd_str = strsep(&data_ptr, "|"); + if (!cmd_str) { + pr_debug("No command\n"); + ret = -EINVAL; + goto out; + } + + cmd = match_string(commands_str, DIGEST_CACHE__LAST, cmd_str); + if (cmd < 0) { + pr_err("Unknown command %s\n", cmd_str); + ret = -ENOENT; + goto out; + } + + switch (cmd) { + case DIGEST_CACHE_GET: + if (found) { + if (!IS_ERR(found)) + digest_cache_put(found); + + found = NULL; + } + + path_str = strsep(&data_ptr, "|"); + if (!path_str) { + pr_debug("No path\n"); + ret = -EINVAL; + goto out; + } + + ret = kern_path(path_str, 0, &path); + if (ret < 0) { + pr_debug("Cannot find file %s\n", path_str); + goto out; + } + + if (digest_cache) { + pr_debug("Digest cache exists, doing a put\n"); + digest_cache_put(digest_cache); + } + + filp = kernel_file_open(&path, O_RDONLY, current_cred()); + if (IS_ERR(filp)) { + pr_debug("Cannot open file %s\n", path_str); + path_put(&path); + goto out; + } + + digest_cache = digest_cache_get(filp); + ret = digest_cache ? 0 : -ENOENT; + pr_debug("digest cache get %s, ret: %d\n", path_str, ret); + fput(filp); + path_put(&path); + break; + case DIGEST_CACHE_LOOKUP: + if (found) { + if (!IS_ERR(found)) + digest_cache_put(found); + + found = NULL; + } + + if (!digest_cache) { + pr_debug("No digest cache\n"); + ret = -ENOENT; + goto out; + } + + path_str = strsep(&data_ptr, "|"); + if (!path_str) { + pr_debug("No path\n"); + ret = -EINVAL; + goto out; + } + + algo_str = strsep(&data_ptr, ":"); + digest_str = data_ptr; + + if (!algo_str || !digest_str) { + pr_debug("No algo or digest\n"); + ret = -EINVAL; + goto out; + } + + algo = match_string(hash_algo_name, HASH_ALGO__LAST, algo_str); + if (algo < 0) { + pr_err("Unknown algorithm %s", algo_str); + ret = -ENOENT; + goto out; + } + + ret = hex2bin(digest, digest_str, hash_digest_size[algo]); + if (ret < 0) { + pr_debug("Invalid digest %s\n", digest_str); + goto out; + } + + ret = kern_path(path_str, 0, &path); + if (ret < 0) { + pr_debug("Cannot find file %s\n", path_str); + goto out; + } + + ret = -ENOENT; + + found = digest_cache_lookup(path.dentry, digest_cache, digest, + algo); + path_put(&path); + if (found && !IS_ERR(found)) + ret = 0; + + pr_debug("%s:%s lookup %s, ret: %d\n", algo_str, digest_str, + path_str, ret); + break; + case DIGEST_CACHE_PUT: + if (digest_cache) { + digest_cache_put(digest_cache); + digest_cache = NULL; + } + + if (found) { + if (!IS_ERR(found)) + digest_cache_put(found); + + found = NULL; + } + ret = 0; + pr_debug("digest cache put, ret: %d\n", ret); + break; + case DIGEST_CACHE_ENABLE_VERIF: + case DIGEST_CACHE_DISABLE_VERIF: + if (!IS_ENABLED(CONFIG_DYNAMIC_FTRACE_WITH_ARGS)) { + ret = -EOPNOTSUPP; + goto out; + } + + memset(prefetch_buf, 0, sizeof(prefetch_buf)); + fallthrough; + case DIGEST_CACHE_SET_VERIF: + verif_name_str = strsep(&data_ptr, "|"); + if (!verif_name_str) { + pr_debug("No verifier name\n"); + ret = -EINVAL; + goto out; + } + + verif_index = match_string(verifs_str, ARRAY_SIZE(verifs_str), + verif_name_str); + if (verif_index < 0) { + pr_err("Unknown verifier name %s\n", verif_name_str); + ret = -ENOENT; + goto out; + } + + if (cmd == DIGEST_CACHE_ENABLE_VERIF) + verifs_methods[verif_index].enabled = true; + else if (cmd == DIGEST_CACHE_DISABLE_VERIF) + verifs_methods[verif_index].enabled = false; + else + cur_verif_index = verif_index; + + ret = 0; + pr_debug("digest cache %s %s, ret: %d\n", cmd_str, + verif_name_str, ret); + break; + case DIGEST_CACHE_GET_PUT_ASYNC: + path_str = strsep(&data_ptr, "|"); + if (!path_str) { + pr_debug("No path\n"); + ret = -EINVAL; + goto out; + } + + start_number_str = strsep(&data_ptr, "|"); + if (!start_number_str) { + pr_debug("No start number\n"); + ret = -EINVAL; + goto out; + } + + ret = kstrtoint(start_number_str, 10, &start_number); + if (ret < 0) { + pr_debug("Invalid start number %s\n", start_number_str); + ret = -EINVAL; + goto out; + } + + end_number_str = strsep(&data_ptr, "|"); + if (!end_number_str) { + pr_debug("No end number\n"); + ret = -EINVAL; + goto out; + } + + ret = kstrtoint(end_number_str, 10, &end_number); + if (ret < 0) { + pr_debug("Invalid end number %s\n", end_number_str); + ret = -EINVAL; + goto out; + } + + if (end_number - start_number >= MAX_WORKS) { + pr_debug("Too many works (%d), max %d\n", + end_number - start_number, MAX_WORKS - 1); + ret = -EINVAL; + goto out; + } + + ret = digest_cache_get_put_async(path_str, start_number, + end_number); + pr_debug("digest cache %s on %s, start: %d, end: %d, ret: %d\n", + cmd_str, path_str, start_number, end_number, ret); + break; + default: + ret = -EINVAL; + break; + } +out: + kfree(data); + return ret ?: datalen; +} + +static ssize_t read_request(struct file *file, char __user *buf, size_t datalen, + loff_t *ppos) +{ + return verifs_methods[cur_verif_index].read(file, buf, datalen, ppos); +} + +static const struct file_operations digest_cache_test_ops = { + .open = generic_file_open, + .write = write_request, + .read = read_request, + .llseek = generic_file_llseek, +}; + +static int __kprobes kernel_post_read_file_hook(struct kprobe *p, + struct pt_regs *regs) +{ +#ifdef CONFIG_DYNAMIC_FTRACE_WITH_ARGS + struct file *file = (struct file *)regs_get_kernel_argument(regs, 0); + enum kernel_read_file_id id = regs_get_kernel_argument(regs, 3); +#else + struct file *file = NULL; + enum kernel_read_file_id id = READING_UNKNOWN; +#endif + int ret, i; + + if (id != READING_DIGEST_LIST) + return 0; + + for (i = 0; i < ARRAY_SIZE(verifs_methods); i++) { + if (!verifs_methods[i].enabled) + continue; + + ret = verifs_methods[i].update(file); + if (ret < 0) + return 0; + } + + return 0; +} + +static struct kprobe kp = { + .symbol_name = "security_kernel_post_read_file", +}; + +static int __init digest_cache_test_init(void) +{ + int ret; + + kp.pre_handler = kernel_post_read_file_hook; + + if (IS_ENABLED(CONFIG_DYNAMIC_FTRACE_WITH_ARGS)) { + ret = register_kprobe(&kp); + if (ret < 0) { + pr_err("register_kprobe failed, returned %d\n", ret); + return ret; + } + } + + test = securityfs_create_file("digest_cache_test", 0660, NULL, NULL, + &digest_cache_test_ops); + if (IS_ERR(test)) { + ret = PTR_ERR(test); + goto out_kprobe; + } + + return 0; +out_kprobe: + if (IS_ENABLED(CONFIG_DYNAMIC_FTRACE_WITH_ARGS)) + unregister_kprobe(&kp); + return ret; +} + +static void __exit digest_cache_test_fini(void) +{ + if (digest_cache) + digest_cache_put(digest_cache); + + if (found) { + if (!IS_ERR(found)) + digest_cache_put(found); + + found = NULL; + } + + securityfs_remove(test); + if (IS_ENABLED(CONFIG_DYNAMIC_FTRACE_WITH_ARGS)) + unregister_kprobe(&kp); + pr_debug("kprobe at %p unregistered\n", kp.addr); +} + +module_init(digest_cache_test_init); +module_exit(digest_cache_test_fini); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Kernel module for testing Integrity Digest Cache");
From: Roberto Sassu roberto.sassu@huawei.com
Add the documentation of the Integrity Digest Cache in Documentation/security.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- Documentation/security/digest_cache.rst | 850 ++++++++++++++++++++++++ Documentation/security/index.rst | 1 + MAINTAINERS | 2 + 3 files changed, 853 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..54392fd2d3b3 --- /dev/null +++ b/Documentation/security/digest_cache.rst @@ -0,0 +1,850 @@ +.. SPDX-License-Identifier: GPL-2.0 + +====================== +Integrity Digest Cache +====================== + +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 a large user base, +due to requiring higher than desired constraints on performance, +flexibility and configurability, that only security conscious people are +willing to accept. + +For example, IMA measurement requires the target platform to collect +integrity measurements, and to protect them with the TPM, which introduces +a noticeable overhead (up to 10x slower in a microbenchmark) on frequently +used system calls, like the open(). + +IMA Appraisal currently requires individual files to be signed and +verified, and Linux distributions to rebuild all packages to include file +signatures (this approach has been adopted from Fedora 39+). Like a TPM, +also signature verification introduces a significant overhead, especially +if it is used to check the integrity of many files. + +This is where the new Integrity Digest Cache comes into play, it offers +additional support for new and existing integrity solutions, to make +them faster and easier to deploy. + +The Integrity Digest Cache can help IMA to reduce the number of TPM +operations and to make them happen 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. + +The performance improvement comes at the cost of IMA not reporting which +files from installed packages were accessed, and in which temporal +sequence. This approach might not be suitable for all use cases. + +The Integrity Digest Cache can also help IMA for appraisal. IMA can simply +lookup the calculated digest of an accessed file in the list of digests +extracted from package headers, after verifying the header signature. It is +sufficient to verify only one signature for all files in the package, as +opposed to verifying a signature for each file. + +The same approach can be followed by other LSMs, such as Integrity Policy +Enforcement (IPE), and BPF LSM. + +The Integrity Digest Cache is not tied to a specific package format. The +kernel supports a TLV-based digest list format. More can be added through +third-party kernel modules. The TLV parser has been verified for memory +safety with the Frama-C static analyzer. The version with the Frama-C +assertions is available here: + +https://github.com/robertosassu/rpm-formal/blob/main/validate_tlv.c + +Integrating the Integrity Digest Cache in IMA brings significant +performance improvements: up to 67% and 79% for measurement respectively in +sequential and parallel file reads; up to 65% and 43% for appraisal +respectively in sequential and parallel file reads. + +The performance can be further enhanced by using fsverity digests instead +of conventional file digests, which would make IMA verify only the portion +of the file to be read. However, at the moment, fsverity digests are not +included in RPM packages. In this case, once rpm is extended to include +them, Linux distributions still have to rebuild their packages. + +The Integrity Digest Cache can support both digest types, so that the +functionality is immediately available without waiting for Linux +distributions to do the transition. + + +Design +====== + +Main idea +--------- + +The Integrity Digest Cache extracts digests from a file, referred to as a +digest list, and stores them in kernel memory in a structure named +digest_cache. + +The digest_cache structure contains a set of per algorithm hash tables, +where digests are stored, the digest list pathname, a reference counter, +and the integrity state of the digest list. + +If a digest cache is created from a directory, its hash tables are empty +and instead it contains a snapshot of the directory entries discovered with +iterate_dir(). + +The integrity state of digest caches created from regular files, also +called verification data, is evaluated independently by LSMs, for example +by verifying the signature of the digest list, and is provided to the +Integrity Digest Cache through a dedicated API. + +The extracted digests can be used as reference values initially for +integrity verification of file data and at a later stage for integrity +verification of file metadata. + +The Integrity Digest Cache can extract digests from a digest list, provided +that it has a parser for its format. + + +Caching and reference counting +------------------------------ + +Creating a digest cache every time it is requested would introduce an +unnecessary overhead, due to repeating the same operation. For this reason, +the Integrity Digest Cache reserves space in the inode security blob +(through IMA) and stores two types of digest cache reference. + +If the digest cache was created from the same inode, the Integrity Digest +Cache stores in the inode security blob a reference called dig_owner, +because the inode owns the content. + +If the digest cache was requested for verifying an inode, the Integrity +Digest Cache stores a reference called dig_user, because the inode is a +user of the digest cache. + +An inode can have both types of reference set, if it is a digest list +to be verified with another digest list. + +Check and assignment of dig_owner and dig_user is protected respectively +with the dig_owner_mutex and dig_user_mutex mutexes. + +The digest cache reference count tracks how many references have been made +to that digest cache (whether that reference is stored in the inode +security blob, or is returned to a user of the Integrity Digest Cache). + +Only when the reference count reaches zero, i.e. all references have been +released, the digest cache can be freed. + + +Digest cache lifecycle +---------------------- + +Digest cache request +~~~~~~~~~~~~~~~~~~~~ + +The first step in order to query a digest from a digest list is to request +a digest cache, by calling digest_cache_get(). The Integrity Digest Cache +takes care of the digest cache creation and initialization processes, +transparently to the caller. + +The caller passes as argument to digest_cache_get() a file descriptor of +the inode that the caller intends to verify. The Integrity Digest Cache +first sees if there is a cached digest cache in that inode (dig_user +reference). If there is, it immediately returns the digest cache with the +reference count increased, since the reference is returned to the caller. + +Otherwise, it will perform the necessary steps (below) to obtain one. + + +Digest list lookup +~~~~~~~~~~~~~~~~~~ + +In order to build a digest cache and return it to the caller for performing +a query, the Integrity Digest Cache must know which digest list to use. +There are a few alternatives. + +(1) There is only one digest list and its path is specified as default +location at build-time in the kernel configuration or at run-time through +securityfs. The Integrity Digest Cache builds a single digest cache from +that digest list and returns it to the caller. + +(2) The default location is a directory containing multiple digest lists. +Unlike (1), the Integrity Digest Cache does not know which digest list to +select, and creates a directory digest cache with a snapshot of the +directory entries. During a query, the Integrity Digest Cache iteratively +creates a digest cache for each directory entry and searches for the digest +until there is a match. + +(3) Same as (2), but the digest list file name is stored as value of the +new security.digest_list xattr in the inode for which the digest cache is +requested. The Integrity Digest Cache can directly retrieve the digest list +using the default directory as the base path and the xattr value as last +path component. + +(4) Similar to (3), but the Integrity Digest Cache still creates a +directory digest cache like in (2). Then, it only reads the digest list if +the directory entry file name does not match the security.digest_list +xattr, to trigger a measurement or, otherwise, creates a digest cache from +the matching one. This is also known as the prefetching mechanism, +introduced later. + + +Digest cache creation +~~~~~~~~~~~~~~~~~~~~~ + +Once the Integrity Digest Cache selected the digest list to use, it looks +up the digest list inode through the VFS, verifies whether in the inode +security blob there is already a digest cache reference (dig_owner). + +If there is, it returns that to digest_cache_get() with the reference count +increased, which in turn will store it in dig_user and will increment the +reference count again before returning to the caller. + +If there isn't, it creates a new digest cache and performs the same steps +as if dig_owner exists. + + +Digest cache initialization +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The digest cache creation does not include initialization and adding the +digests. It cannot be done inside the dig_owner_mutex and dig_user_mutex, +to avoid lock inversion with the inode lock done by the VFS. + +Digest cache initialization is done by digest_cache_get() by +calling digest_cache_init(), after releasing the dig_user_mutex. Any +digest_cache_get() caller can potentially initialize a digest cache. + +To avoid multiple initialization attempts, callers that got the path of the +digest lists atomically test and set the INIT_STARTED atomic flag. The +first seeing the flag cleared is the one in charge of the initialization. +The other callers wait for another atomic flag, INIT_IN_PROGRESS to be +cleared to zero (it is set to one on digest cache creation). + +Failures during initialization can be detected by checking the INVALID flag +in the digest cache and, in this case, the digest cache is not returned to +the caller of digest_cache_get(). + + +Digest list naming convention +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The digest_cache_get() caller performing the digest cache initialization +reads the digest list and calls the appropriate parser to extract the +digests, based on the file name prefix. + +The expected digest list file name format is:: + + <digest list format>-<digest list name> + +where format can be for example ``tlv``, which makes the Integrity Digest +Cache call the TLV parser. + +Alternatively, also the following format is supported:: + + <seq num>-<digest list format>-<digest list name> + +``<seq num>-`` defines how directory entries should be ordered in the +directory digest cache. If present, directory entries are ordered in +ascending way by that number. + + +Digest list parsers +^^^^^^^^^^^^^^^^^^^ + +The Integrity Digest Cache supports an arbitrary number of parsers. New +parsers can be added through third-party kernel modules, which should +register a new parser name and function through the Parser API. + +The Integrity Digest Cache expects kernel modules containing the parsers to +be placed in +``/lib/modules/<kernel ver>/kernel/security/integrity/digest_cache``. If it +does not find the required parser, it attempts to load a kernel module with +the digest list format as file name, plus the .ko suffix and the +appropriate compression suffix obtained from the kernel configuration. + +The Integrity Digest Cache loads kernel modules with parsers from the +kernel itself by calling the new function ksys_finit_module(). This +solution is necessary because, if the kernel module loading is demanded to +user space, there might not be yet a digest cache required to verify the +user space code. + +Currently, the kernel only provides a TLV-based digest list format. The RPM +parser can be found in the digest-cache-tools software. + + +Digest list parsing +^^^^^^^^^^^^^^^^^^^ + +The selected digest list parser first calls digest_cache_htable_init() to +create the hash tables in the digest cache, once for each hash algorithm +of the digests to be added. digest_cache_htable_init() accepts as parameter +the number of digests to add, usually known before adding digests. + +The number of hash table slots is determined by dividing the number of +digests to add by the desired average collision depth. The latter can be +changed in the kernel configuration, to have a different tradeoff between +digest lookup speed and memory occupation. + +The parser then calls digest_cache_htable_add(), to add extracted digests +to the new hash tables. It can also call digest_cache_htable_lookup() to +check for duplicate digests. + + +Directory digest cache +^^^^^^^^^^^^^^^^^^^^^^ + +If the digest list location is a directory, digest_cache_init() calls +digest_cache_dir_add_entries(), which in turn calls iterate_dir() to +get the current directory entries and to add them to a linked list. + +When a digest is looked up on a directory digest cache, +digest_cache_dir_lookup_digest() will create a regular digest cache for +each directory entry and will lookup into it until it finds the digest. + +digest_cache_dir_lookup_digest() also gets a digest cache reference for +each directory entry, so that digest lookup is faster at the next call. + + +Digest lookup +~~~~~~~~~~~~~ + +After a caller of digest_cache_get() obtains the desired digest cache, it +can perform operations on it. The most important operation is querying for +a digest, which can be performed by calling digest_cache_lookup(). + +digest_cache_lookup() returns a reference of the digest cache containing +the queried digest, that must be freed by calling digest_cache_put(). + +If digest_cache_get() returned a directory digest cache, +digest_cache_lookup() cannot directly perform the search, since its hash +tables are empty. Instead, it calls digest_cache_dir_lookup_digest(), +which searches the digest in the digest cache of each directory entry. + +Between digest_cache_get() and digest_cache_lookup() there is still the +possibility that a concurrent VFS operation affects the digest cache +returned by digest_cache_get(). If that happened, digest_cache_lookup() +returns an error pointer to the caller, which in turn should call +digest_cache_get() and digest_cache_lookup() again. + + + +Verification data +~~~~~~~~~~~~~~~~~ + +Until now, the caller of the Integrity Digest Cache is assumed to always +trust the returned digest cache from being created from authentic data. Or, +there are security measures in place but not able to correlate reading a +digest list with building a digest cache from it. + +The Integrity Digest Cache introduces a new mechanism for integrity +providers to store verification data, i.e. their evaluation result of a +digest list. It also allows callers of digest_cache_get() to later retrieve +that information and decide whether or not they should use that digest +cache. + +It achieves that by reserving space in the file descriptor security blob, +and by setting the digest cache pointer in the digest list file descriptor +before the digest list is read by the kernel. + +Integrity providers should implement the kernel_post_read_file LSM hook and +call digest_cache_verif_set(), passing the same digest list file descriptor +on which the digest cache pointer was set, their unique ID and their +evaluation result of the digest list. + +The Integrity Digest Cache supports multiple integrity providers at the +same time, since multiple LSMs can implement the kernel_post_read_file LSM +hook. Each provider is expected to choose an unique ID, so that the +verification data can be given back through the same ID. + +Callers of digest_cache_get() can call digest_cache_verif_get() to get +the verification data, passing the returned digest cache pointer and the +desired integrity provider ID. However, if the digest cache returned was +created from a directory, that call results in a NULL pointer, since the +directory digest cache is not populated from any digest list. + +In that case, those callers have to call digest_cache_lookup() to get the +digest cache containing the digest (thus populated from a digest list), and +pass it to digest_cache_verif_get(). + + +Tracking digest cache changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After a digest cache has been built and its pointer has been set in the +inode security blob, it might happen that there are changes in the digest +lists, in the default directory and in the value of the +security.digest_list xattr. + +All these changes may influence which digest cache is returned to callers +of digest_cache_get() and which digests in the digest cache might be +searched. + +The Integrity Digest Cache monitors such changes by registering to multiple +LSM hooks (path_truncate, file_release, inode_unlink, inode_rename, +inode_post_setxattr and inode_post_removexattr). Except for the last two, +it accesses the dig_owner pointer in the affected inode security blob, sets +the RESET bit, puts the digest cache and clears dig_owner itself. + +The next time that digest cache is requested with digest_cache_get(), also +dig_user is put and cleared. The same happens in +digest_cache_dir_lookup_digest(), where the digest cache of a directory +entry is released and cleared as well. After a reset, a new digest cache is +created and returned, as if there wasn't one in the first place. + +For the last two hooks, when the security.digest_list xattr is modified, +dig_user is cleared so that at the next digest_cache_get() call a new +digest cache is retrieved, since the location of the digest list might have +changed. + +Previous callers of digest_cache_get() can still keep the reset digest +cache. However, digest_cache_lookup() will not perform a search on it, but +instead will return an error pointer, forcing the caller to get a fresh +digest cache. + + +Security decision update after digest cache changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While new calls to digest_cache_get() result in a new digest cache to be +returned, resetting the previous digest cache does not reflect in a reset +of possibly cached security decisions by users of the Integrity Digest +Cache. + +One possible way for those users to become aware of a digest cache change +is to store the digest cache pointer they used for a security decision, to +call digest_cache_get() again during a new file access and to compare the +two pointers. The previous pointer remains valid until the digest cache +is released. + +IMA stores the current digest cache pointer in its managed metadata. At +every file access, it calls digest_cache_get() again and compares the +returned pointer with the one previously stored. If the pointers are the +same, IMA continues to use the previous evaluation result. If not, it +performs the evaluation again. + +The cost of this check is very small. In the case where the digest cache +didn't change since the last digest_cache_get(), the cost is to check if +the dig_user pointer is not NULL, and to increment and decrement the digest +cache reference count. + +In terms of memory, this solution requires IMA to store an additional +pointer in its metadata. + + +Nested IMA calls +~~~~~~~~~~~~~~~~ + +The Integrity Digest Cache internally opens kernel modules required to +parse digest lists and the digest lists themselves. This causes IMA to be +called again recursively, to verify those files. The problem is that +digest_cache_get() is called with iint->mutex held. If the inode requested +in the recursive call is the same as the one in the first call, the kernel +would deadlock, because IMA would try to take an already locked mutex. + +Fortunately, this situation does not happen since the Integrity Digest +Cache ensures that the two inodes will never be the same or otherwise it +returns an error. Secondly, the Integrity Digest Cache offers the +digest_cache_opened_fd() function to let the caller know whether or not the +file descriptor passed as argument is managed by Integrity Digest Cache +itself. + +If digest_cache_opened_fd() returns true, it is safe to nest IMA calls, +without the risk of having deadlocks. mutex_lock() in process_measurement() +is replaced with mutex_lock_nested() with the result of +digest_cache_opened_fd() as second argument, so that lockdep does not emit +a warning. + + +Prefetching mechanism +~~~~~~~~~~~~~~~~~~~~~ + +One of the objectives of the Integrity Digest Cache is to make a TPM PCR +predictable, by having digest lists measured in a deterministic order. +Without the prefetching mechanism, digest lists are measured in a +non-deterministic order, since the inodes for which a digest cache can be +requested are accessed in a non-deterministic order too. + +The prefetching mechanism, when enabled by setting the new +security.dig_prefetch xattr to 1, forces digest lists to be looked up by +their file name in the digest cache created for the parent directory. + +The predictability of the PCR is ensured by reading both matching and +non-matching digest lists during the search, so that integrity providers +can measure them, and by only creating a digest cache for the matching one. +In this way, it does not matter if a digest list later in the list of +directory entries is requested before a earlier one, since all digest lists +until that point are measured anyway. + +However, while this mechanism ensures predictability of the PCR, it could +also introduce significant latencies, especially if the matching digest +list is very late in the list of directory entries. Before a digest cache +is returned from that digest list, hundreds or thousands of digest lists +could have to be read first. + +Then, the ``[<seq num>-]`` prefix in the digest list file name comes at +hand, since it determines the order of directory entries in the directory +digest cache (entries with lower seq nums are before entries with higher +seq nums). Digest lists without that prefix are added at the end of the +directory entries list, in the same order as iterate_dir() shows them. + +Boot performance can be greatly improved by looking at the IMA measurement +list and by seeing in which order digest lists are requested at boot. Then, +``[<seq num>-]`` can be prepended to directory entries depending on their +position in the measurement list. + +While digest lists can be requested in a slightly different order due to +the non-deterministic access to inodes, the differences should be minimal, +causing only fewer extra digest lists to be read before the right one is +found. + +Ordering directory entries can also improve digest queries requiring +iteration on all digest lists in the default directory. If directory +entries are ordered by their appearance in the IMA measurement list, a +digest is found faster because most likely it is searched in the same +order as when the IMA measurement list was recorded, and thus its +digest list comes earlier than the others in the list of the directory +entries of the directory digest cache. + + +Release a digest cache +~~~~~~~~~~~~~~~~~~~~~~ + +The Integrity Digest Cache uses the reference count mechanism to ensure +that a digest cache does not simply disappear when someone is using it. + +Either when an inode is evicted from memory, or a caller of +digest_cache_get() finished to use a digest cache, they should call +digest_cache_put() to signal to the Integrity Digest Cache that they are no +longer interested in that digest cache and that it can be eventually freed. + +A digest cache is freed when all digest cache users called +digest_cache_put(), and the reference count reached the value zero. + + +Formal verification of concurrency +================================== + +The Integrity Digest Cache has been designed to work in a heavily concurrent +environment, where code can be executed as a follow up of a VFS operation, +or upon a direct request by a user of the Integrity Digest Cache. + +For this reason, a sound locking mechanism is necessary to protect data +structures against concurrent accesses. + +The first verification of the locking mechanism was done with the in-kernel +lockdep, which can detect potential deadlocks and unsafe usage of the +locking primitives. + +There is an ongoing verification with a tool named Dartagnan, reachable at +the following URL: + +https://github.com/hernanponcedeleon/Dat3M + +This verification required porting the Integrity Digest Cache to user +space, and to simulate concurrent requests through the pthread library. + +Dartagnan explores all thread interleavings and checks for data races. In +addition to lockdep, it can also spot for example improperly guarded +variables. + + +Data structures and API +======================= + +Data structures +--------------- + +These are the data structures defined and used internally by the +Integrity Digest Cache. + +.. kernel-doc:: security/integrity/digest_cache/internal.h + + +Client API +---------- + +This API is meant to be used by users of the Integrity Digest Cache. + +.. kernel-doc:: security/integrity/digest_cache/main.c + :identifiers: digest_cache_get digest_cache_put + digest_cache_opened_fd + +.. kernel-doc:: security/integrity/digest_cache/htable.c + :identifiers: digest_cache_lookup + +.. kernel-doc:: security/integrity/digest_cache/verif.c + :identifiers: digest_cache_verif_set digest_cache_verif_get + + +Parser API +---------- + +This API is meant to be used by digest list parsers. + +.. kernel-doc:: security/integrity/digest_cache/htable.c + :identifiers: digest_cache_htable_init + digest_cache_htable_add + digest_cache_htable_lookup + +.. kernel-doc:: security/integrity/digest_cache/parsers.c + :identifiers: digest_cache_register_parser + digest_cache_unregister_parser + + +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:: + + [field: DIGEST_LIST_ALGO, length, value] + [field: DIGEST_LIST_NUM_ENTRIES, length, value] + [field: DIGEST_LIST_ENTRY#1, length, value (below)] + |- [DIGEST_LIST_ENTRY_DIGEST#1, length, file digest] + |- [DIGEST_LIST_ENTRY_PATH#1, length, file path] + [field: DIGEST_LIST_ENTRY#N, length, value (below)] + |- [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_NUM_ENTIES is a field to specify the number of +DIGEST_LIST_ENTRY records. 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. + + +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 Integrity Digest +Cache, 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 reduces the digest lookup time, since digests are searched in smaller +hash tables, and significantly reduces the memory pressure, since +digest lists are loaded into kernel memory only when they are actually +needed, and removed during reclamation. + + +Performance +=========== + +System specification +-------------------- + +The tests have been performed on a Fedora 38 virtual machine with 4 cores +(AMD EPYC-Rome, no hyperthreading), 16 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 Integrity Digest Cache 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 Integrity Digest Cache. + + +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=data 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 | 31.71 / 102.80 / 46.29 | 86802 | + +--------------------+-------+------------------------+-----------+ + | cache, no prefetch | 304 | 32.21 / 34.28 / 32.47 | 83709 | + +--------------------+-------+------------------------+-----------+ + | cache, prefetch | 304 | 32.67 / 34.47 / 32.67 | 83720 | + +--------------------+-------+------------------------+-----------+ + +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 +(with HMAC protection disabled). + +In terms of pure performance, first number in the third column, it can be +seen that there are not significant performance differences between using +or not using the digest cache. + +Prefetching adds little overhead, little because digest lists were ordered +according to their appearance in the IMA measurement list (which minimizes +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 | 15.84 / 79.26 / 23.43 | 87635 | + +--------------------+-------+-----------------------+-----------+ + | cache, no prefetch | 304 | 15.97 / 16.64 / 16.09 | 89890 | + +--------------------+-------+-----------------------+-----------+ + | cache, prefetch | 304 | 16.18 / 16.84 / 16.24 | 85738 | + +--------------------+-------+-----------------------+-----------+ + +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 Integrity Digest Cache 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=data + + +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 | 98.10 | 80842 | + +----------------------------+-------------+-------------+-----------+ + | appraise (cache) | 12312 + 303 | 34.09 | 83138 | + +----------------------------+-------------+-------------+-----------+ + | appraise (cache, prefetch) | 12312 + 303 | 34.08 | 83410 | + +----------------------------+-------------+-------------+-----------+ + +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 | 29.00 | 82255 | + +----------------------------+-------------+-------------+-----------+ + | appraise (cache) | 12313 + 303 | 16.51 | 88359 | + +----------------------------+-------------+-------------+-----------+ + | appraise (cache, prefetch) | 12313 + 303 | 17.08 | 86266 | + +----------------------------+-------------+-------------+-----------+ + +The difference is less marked when performing the read in parallel. Also, +more memory seems to be occupied in the non-prefetch case. + + +How to test +=========== + +Please follow the instructions here: + +https://github.com/linux-integrity/digest-cache-tools diff --git a/Documentation/security/index.rst b/Documentation/security/index.rst index 3e0a7114a862..3c703cbf1cca 100644 --- a/Documentation/security/index.rst +++ b/Documentation/security/index.rst @@ -20,3 +20,4 @@ Security Documentation landlock secrets/index ipe + digest_cache diff --git a/MAINTAINERS b/MAINTAINERS index 1f7ffa1c9dbd..cf749921120a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11279,8 +11279,10 @@ R: Eric Snowberg eric.snowberg@oracle.com L: linux-integrity@vger.kernel.org S: Supported T: git git://git.kernel.org/pub/scm/linux/kernel/git/zohar/linux-integrity.git +F: Documentation/security/digest_cache.rst F: security/integrity/ F: security/integrity/ima/ +F: tools/testing/selftests/digest_cache/
INTEGRITY POLICY ENFORCEMENT (IPE) M: Fan Wu wufan@linux.microsoft.com
On Tue, Nov 19, 2024 at 11:49:07AM +0100, Roberto Sassu wrote:
From: Roberto Sassu roberto.sassu@huawei.com v5:
- Add new patch to introduce ksys_finit_module()
Why?
Luis
On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
The Integrity Digest Cache can also help IMA for appraisal. IMA can simply lookup the calculated digest of an accessed file in the list of digests extracted from package headers, after verifying the header signature. It is sufficient to verify only one signature for all files in the package, as opposed to verifying a signature for each file.
Is there a way to maintain integrity over time? Today if a CVE is discovered in a signed program, the program hash can be added to the blacklist keyring. Later if IMA appraisal is used, the signature validation will fail just for that program. With the Integrity Digest Cache, is there a way to do this?
On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote:
On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
The Integrity Digest Cache can also help IMA for appraisal. IMA can simply lookup the calculated digest of an accessed file in the list of digests extracted from package headers, after verifying the header signature. It is sufficient to verify only one signature for all files in the package, as opposed to verifying a signature for each file.
Is there a way to maintain integrity over time? Today if a CVE is discovered in a signed program, the program hash can be added to the blacklist keyring. Later if IMA appraisal is used, the signature validation will fail just for that program. With the Integrity Digest Cache, is there a way to do this?
As far as I can see, the ima_check_blacklist() call is before ima_appraise_measurement(). If it fails, appraisal with the Integrity Digest Cache will not be done.
In the future, we might use the Integrity Digest Cache for blacklists too. Since a digest cache is reset on a file/directory change, IMA would have to revalidate the program digest against a new digest cache.
Thanks
Roberto
On Nov 26, 2024, at 3:41 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote:
On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
The Integrity Digest Cache can also help IMA for appraisal. IMA can simply lookup the calculated digest of an accessed file in the list of digests extracted from package headers, after verifying the header signature. It is sufficient to verify only one signature for all files in the package, as opposed to verifying a signature for each file.
Is there a way to maintain integrity over time? Today if a CVE is discovered in a signed program, the program hash can be added to the blacklist keyring. Later if IMA appraisal is used, the signature validation will fail just for that program. With the Integrity Digest Cache, is there a way to do this?
As far as I can see, the ima_check_blacklist() call is before ima_appraise_measurement(). If it fails, appraisal with the Integrity Digest Cache will not be done.
It is good the program hash would be checked beforehand and fail if it is contained on the list.
The .ima keyring may contain many keys. If one of the keys was later revoked and added to the .blacklist, wouldn't this be missed? It would be caught during signature validation when the file is later appraised, but now this step isn't taking place. Correct?
With IMA appraisal, it is easy to maintain authenticity but challenging to maintain integrity over time. In user-space there are constantly new CVEs. To maintain integrity over time, either keys need to be rotated in the .ima keyring or program hashes need to be frequently added to the .blacklist. If neither is done, for an end-user on a distro, IMA-appraisal basically guarantees authenticity.
While I understand the intent of the series is to increase performance, have you considered using this to give the end-user the ability to maintain integrity of their system? What I mean is, instead of trying to import anything from an RPM, just have the end-user provide this information in some format to the Digest Cache. User-space tools could be built to collect and format the data needed by the Digest Cache. This data may allow multiple versions of the same program. The data would then be signed by one of the system kernel keys (either something in the secondary or machine keyring), to maintain a root of trust. This would give the end-user the ability to have integrity however they see fit. This leaves the distro to provide signed programs and the end-user the ability to decide what level of software they want to run on their system. If something isn't in the Digest Cache, it gets bumped down to the traditional IMA-appraisal. I think it would simplify the problem you are trying to solve, especially around the missing kernel PGP code required for all this to work, since it wouldn't be necessary. With this approach, besides the performance gain, the end-user would gain the ability to maintain integrity that is enforced by the kernel.
On Tue, 2024-12-03 at 20:06 +0000, Eric Snowberg wrote:
On Nov 26, 2024, at 3:41 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote:
On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
The Integrity Digest Cache can also help IMA for appraisal. IMA can simply lookup the calculated digest of an accessed file in the list of digests extracted from package headers, after verifying the header signature. It is sufficient to verify only one signature for all files in the package, as opposed to verifying a signature for each file.
Is there a way to maintain integrity over time? Today if a CVE is discovered in a signed program, the program hash can be added to the blacklist keyring. Later if IMA appraisal is used, the signature validation will fail just for that program. With the Integrity Digest Cache, is there a way to do this?
As far as I can see, the ima_check_blacklist() call is before ima_appraise_measurement(). If it fails, appraisal with the Integrity Digest Cache will not be done.
It is good the program hash would be checked beforehand and fail if it is contained on the list.
The .ima keyring may contain many keys. If one of the keys was later revoked and added to the .blacklist, wouldn't this be missed? It would be caught during signature validation when the file is later appraised, but now this step isn't taking place. Correct?
For files included in the digest lists, yes, there won't be detection of later revocation of a key. However, it will still work at package level/digest list level, since they are still appraised with a signature.
We can add a mechanism (if it does not already exist) to invalidate the integrity status based on key revocation, which can be propagated to files verified with the affected digest lists.
With IMA appraisal, it is easy to maintain authenticity but challenging to maintain integrity over time. In user-space there are constantly new CVEs. To maintain integrity over time, either keys need to be rotated in the .ima keyring or program hashes need to be frequently added to the .blacklist. If neither is done, for an end-user on a distro, IMA-appraisal basically guarantees authenticity.
While I understand the intent of the series is to increase performance, have you considered using this to give the end-user the ability to maintain integrity of their system? What I mean is, instead of trying to import anything from an RPM, just have the end-user provide this information in some format to the Digest Cache. User-space tools could be built to collect and format
This is already possible, digest-cache-tools (https://github.com/linux-integrity/digest-cache-tools) already allow to create a digest list with the file a user wants.
But in this case, the user is vouching for having taken the correct measure of the file at the time it was added to the digest list. This would be instead automatically guaranteed by RPMs or other packages shipped with Linux distributions.
To mitigate the concerns of CVEs, we can probably implement a rollback prevention mechanism, which would not allow to load a previous version of a digest list.
the data needed by the Digest Cache. This data may allow multiple versions of the same program. The data would then be signed by one of the system kernel keys (either something in the secondary or machine keyring), to maintain a root of trust. This would give the end-user the ability to have integrity however they see fit. This leaves the distro to provide signed programs and the end-user the ability to decide what level of software they want to run on their system. If something isn't in the Digest Cache, it gets bumped down to the traditional IMA-appraisal. I think it would simplify the problem you are trying to solve,
All you say it is already possible. Users can generate and sign their digest lists, and add enroll their key to the kernel keyring.
especially around the missing kernel PGP code required for all this to work, since it wouldn't be necessary. With this approach, besides the performance gain, the end-user would gain the ability to maintain integrity that is enforced by the kernel.
For what I understood, Linus would not be against the
On Dec 4, 2024, at 3:44 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-12-03 at 20:06 +0000, Eric Snowberg wrote:
On Nov 26, 2024, at 3:41 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote:
On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
The Integrity Digest Cache can also help IMA for appraisal. IMA can simply lookup the calculated digest of an accessed file in the list of digests extracted from package headers, after verifying the header signature. It is sufficient to verify only one signature for all files in the package, as opposed to verifying a signature for each file.
Is there a way to maintain integrity over time? Today if a CVE is discovered in a signed program, the program hash can be added to the blacklist keyring. Later if IMA appraisal is used, the signature validation will fail just for that program. With the Integrity Digest Cache, is there a way to do this?
As far as I can see, the ima_check_blacklist() call is before ima_appraise_measurement(). If it fails, appraisal with the Integrity Digest Cache will not be done.
It is good the program hash would be checked beforehand and fail if it is contained on the list.
The .ima keyring may contain many keys. If one of the keys was later revoked and added to the .blacklist, wouldn't this be missed? It would be caught during signature validation when the file is later appraised, but now this step isn't taking place. Correct?
For files included in the digest lists, yes, there won't be detection of later revocation of a key. However, it will still work at package level/digest list level, since they are still appraised with a signature.
We can add a mechanism (if it does not already exist) to invalidate the integrity status based on key revocation, which can be propagated to files verified with the affected digest lists.
With IMA appraisal, it is easy to maintain authenticity but challenging to maintain integrity over time. In user-space there are constantly new CVEs. To maintain integrity over time, either keys need to be rotated in the .ima keyring or program hashes need to be frequently added to the .blacklist. If neither is done, for an end-user on a distro, IMA-appraisal basically guarantees authenticity.
While I understand the intent of the series is to increase performance, have you considered using this to give the end-user the ability to maintain integrity of their system? What I mean is, instead of trying to import anything from an RPM, just have the end-user provide this information in some format to the Digest Cache. User-space tools could be built to collect and format
This is already possible, digest-cache-tools (https://github.com/linux-integrity/digest-cache-tools) already allow to create a digest list with the file a user wants.
But in this case, the user is vouching for having taken the correct measure of the file at the time it was added to the digest list. This would be instead automatically guaranteed by RPMs or other packages shipped with Linux distributions.
To mitigate the concerns of CVEs, we can probably implement a rollback prevention mechanism, which would not allow to load a previous version of a digest list.
IMHO, pursuing this with the end-user being in control of what is contained within the Digest Cache vs what is contained in a distro would provide more value. Allowing the end-user to easily update their Digest Cache in some way without having to do any type of revocation for both old and vulnerable applications with CVEs would be very beneficial.
Is there a belief the Digest Cache would be used without signed kernel modules? Is the performance gain worth changing how kernel modules get loaded at boot? Couldn't this part just be dropped for easier acceptance? Integrity is already maintained with the current model of appended signatures.
On Thu, 2024-12-05 at 00:57 +0000, Eric Snowberg wrote:
On Dec 4, 2024, at 3:44 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-12-03 at 20:06 +0000, Eric Snowberg wrote:
On Nov 26, 2024, at 3:41 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote:
On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
The Integrity Digest Cache can also help IMA for appraisal. IMA can simply lookup the calculated digest of an accessed file in the list of digests extracted from package headers, after verifying the header signature. It is sufficient to verify only one signature for all files in the package, as opposed to verifying a signature for each file.
Is there a way to maintain integrity over time? Today if a CVE is discovered in a signed program, the program hash can be added to the blacklist keyring. Later if IMA appraisal is used, the signature validation will fail just for that program. With the Integrity Digest Cache, is there a way to do this?
As far as I can see, the ima_check_blacklist() call is before ima_appraise_measurement(). If it fails, appraisal with the Integrity Digest Cache will not be done.
It is good the program hash would be checked beforehand and fail if it is contained on the list.
The .ima keyring may contain many keys. If one of the keys was later revoked and added to the .blacklist, wouldn't this be missed? It would be caught during signature validation when the file is later appraised, but now this step isn't taking place. Correct?
For files included in the digest lists, yes, there won't be detection of later revocation of a key. However, it will still work at package level/digest list level, since they are still appraised with a signature.
We can add a mechanism (if it does not already exist) to invalidate the integrity status based on key revocation, which can be propagated to files verified with the affected digest lists.
With IMA appraisal, it is easy to maintain authenticity but challenging to maintain integrity over time. In user-space there are constantly new CVEs. To maintain integrity over time, either keys need to be rotated in the .ima keyring or program hashes need to be frequently added to the .blacklist. If neither is done, for an end-user on a distro, IMA-appraisal basically guarantees authenticity.
While I understand the intent of the series is to increase performance, have you considered using this to give the end-user the ability to maintain integrity of their system? What I mean is, instead of trying to import anything from an RPM, just have the end-user provide this information in some format to the Digest Cache. User-space tools could be built to collect and format
This is already possible, digest-cache-tools (https://github.com/linux-integrity/digest-cache-tools) already allow to create a digest list with the file a user wants.
But in this case, the user is vouching for having taken the correct measure of the file at the time it was added to the digest list. This would be instead automatically guaranteed by RPMs or other packages shipped with Linux distributions.
To mitigate the concerns of CVEs, we can probably implement a rollback prevention mechanism, which would not allow to load a previous version of a digest list.
IMHO, pursuing this with the end-user being in control of what is contained within the Digest Cache vs what is contained in a distro would provide more value. Allowing the end-user to easily update their Digest Cache in some way without having to do any type of revocation for both old and vulnerable applications with CVEs would be very beneficial.
Yes, deleting the digest list would invalidate any integrity result done with that digest list.
I developed also an rpm plugin that synchronizes the digest lists with installed software. Old vulnerable software cannot be verified anymore with the Integrity Digest Cache, since the rpm plugin deletes the old software digest lists.
https://github.com/linux-integrity/digest-cache-tools/blob/main/rpm-plugin/d...
The good thing is that the Integrity Digest Cache can be easily controlled with filesystem operations (it works similarly to security blobs attached to kernel objects, like inodes and file descriptors).
As soon as something changes (e.g. digest list written, link to the digest lists), this triggers a reset in the Integrity Digest Cache, so digest lists and files need to be verified again. Deleting the digest list causes the in-kernel digest cache to be wiped away too (when the reference count reaches zero).
Is there a belief the Digest Cache would be used without signed kernel modules? Is the performance gain worth changing how kernel modules get loaded at boot? Couldn't this part just be dropped for easier acceptance? Integrity is already maintained with the current model of appended signatures.
I don't like making exceptions in the design, and I recently realized that it should not be task of the users of the Integrity Digest Cache to limit themselves.
But the main problem was not the kernel modules themselves, but the infrastructure needed in user space to load them, which might not be available at the time a digest list parser needs to be loaded.
I hope ksys_finit_module() does not cause too much resistance (however I need to document it better, as others noted). It is just a different way to pass the same parameters of the finit_module() system call.
Thanks
Roberto
On Thu, 2024-12-05 at 09:53 +0100, Roberto Sassu wrote:
On Thu, 2024-12-05 at 00:57 +0000, Eric Snowberg wrote:
On Dec 4, 2024, at 3:44 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-12-03 at 20:06 +0000, Eric Snowberg wrote:
On Nov 26, 2024, at 3:41 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote:
> On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote: > > From: Roberto Sassu roberto.sassu@huawei.com > > The Integrity Digest Cache can also help IMA for appraisal. IMA can simply > lookup the calculated digest of an accessed file in the list of digests > extracted from package headers, after verifying the header signature. It is > sufficient to verify only one signature for all files in the package, as > opposed to verifying a signature for each file.
Is there a way to maintain integrity over time? Today if a CVE is discovered in a signed program, the program hash can be added to the blacklist keyring. Later if IMA appraisal is used, the signature validation will fail just for that program. With the Integrity Digest Cache, is there a way to do this?
As far as I can see, the ima_check_blacklist() call is before ima_appraise_measurement(). If it fails, appraisal with the Integrity Digest Cache will not be done.
It is good the program hash would be checked beforehand and fail if it is contained on the list.
The .ima keyring may contain many keys. If one of the keys was later revoked and added to the .blacklist, wouldn't this be missed? It would be caught during signature validation when the file is later appraised, but now this step isn't taking place. Correct?
For files included in the digest lists, yes, there won't be detection of later revocation of a key. However, it will still work at package level/digest list level, since they are still appraised with a signature.
We can add a mechanism (if it does not already exist) to invalidate the integrity status based on key revocation, which can be propagated to files verified with the affected digest lists.
With IMA appraisal, it is easy to maintain authenticity but challenging to maintain integrity over time. In user-space there are constantly new CVEs. To maintain integrity over time, either keys need to be rotated in the .ima keyring or program hashes need to be frequently added to the .blacklist. If neither is done, for an end-user on a distro, IMA-appraisal basically guarantees authenticity.
While I understand the intent of the series is to increase performance, have you considered using this to give the end-user the ability to maintain integrity of their system? What I mean is, instead of trying to import anything from an RPM, just have the end-user provide this information in some format to the Digest Cache. User-space tools could be built to collect and format
This is already possible, digest-cache-tools (https://github.com/linux-integrity/digest-cache-tools) already allow to create a digest list with the file a user wants.
But in this case, the user is vouching for having taken the correct measure of the file at the time it was added to the digest list. This would be instead automatically guaranteed by RPMs or other packages shipped with Linux distributions.
To mitigate the concerns of CVEs, we can probably implement a rollback prevention mechanism, which would not allow to load a previous version of a digest list.
IMHO, pursuing this with the end-user being in control of what is contained within the Digest Cache vs what is contained in a distro would provide more value. Allowing the end-user to easily update their Digest Cache in some way without having to do any type of revocation for both old and vulnerable applications with CVEs would be very beneficial.
Yes, deleting the digest list would invalidate any integrity result done with that digest list.
I developed also an rpm plugin that synchronizes the digest lists with installed software. Old vulnerable software cannot be verified anymore with the Integrity Digest Cache, since the rpm plugin deletes the old software digest lists.
https://github.com/linux-integrity/digest-cache-tools/blob/main/rpm-plugin/d...
The good thing is that the Integrity Digest Cache can be easily controlled with filesystem operations (it works similarly to security blobs attached to kernel objects, like inodes and file descriptors).
As soon as something changes (e.g. digest list written, link to the digest lists), this triggers a reset in the Integrity Digest Cache, so digest lists and files need to be verified again. Deleting the digest list causes the in-kernel digest cache to be wiped away too (when the reference count reaches zero).
Is there a belief the Digest Cache would be used without signed kernel modules? Is the performance gain worth changing how kernel modules get loaded at boot? Couldn't this part just be dropped for easier acceptance? Integrity is already maintained with the current model of appended signatures.
I don't like making exceptions in the design, and I recently realized that it should not be task of the users of the Integrity Digest Cache to limit themselves.
Forgot to mention that your use case is possible. The usage of the Integrity Digest Cache must be explicitly enabled in the IMA policy. It will be used if the matching rule has 'digest_cache=data' (its foreseen to be used also for metadata).
For kernel modules, it is sufficient to not provide that keyword for the MODULE_CHECK hook.
However, there is the possibility that you lose another advantage of the Integrity Digest Cache, the predictability of the IMA PCR. By not using digest caches, there is the risk that the IMA PCR will be unstable, due to loading kernel modules in a different order at each boot.
Roberto
But the main problem was not the kernel modules themselves, but the infrastructure needed in user space to load them, which might not be available at the time a digest list parser needs to be loaded.
I hope ksys_finit_module() does not cause too much resistance (however I need to document it better, as others noted). It is just a different way to pass the same parameters of the finit_module() system call.
Thanks
Roberto
On Dec 5, 2024, at 9:16 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Thu, 2024-12-05 at 09:53 +0100, Roberto Sassu wrote:
On Thu, 2024-12-05 at 00:57 +0000, Eric Snowberg wrote:
On Dec 4, 2024, at 3:44 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-12-03 at 20:06 +0000, Eric Snowberg wrote:
On Nov 26, 2024, at 3:41 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote: > >> On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote: >> >> From: Roberto Sassu roberto.sassu@huawei.com >> >> The Integrity Digest Cache can also help IMA for appraisal. IMA can simply >> lookup the calculated digest of an accessed file in the list of digests >> extracted from package headers, after verifying the header signature. It is >> sufficient to verify only one signature for all files in the package, as >> opposed to verifying a signature for each file. > > Is there a way to maintain integrity over time? Today if a CVE is discovered > in a signed program, the program hash can be added to the blacklist keyring. > Later if IMA appraisal is used, the signature validation will fail just for that > program. With the Integrity Digest Cache, is there a way to do this?
As far as I can see, the ima_check_blacklist() call is before ima_appraise_measurement(). If it fails, appraisal with the Integrity Digest Cache will not be done.
It is good the program hash would be checked beforehand and fail if it is contained on the list.
The .ima keyring may contain many keys. If one of the keys was later revoked and added to the .blacklist, wouldn't this be missed? It would be caught during signature validation when the file is later appraised, but now this step isn't taking place. Correct?
For files included in the digest lists, yes, there won't be detection of later revocation of a key. However, it will still work at package level/digest list level, since they are still appraised with a signature.
We can add a mechanism (if it does not already exist) to invalidate the integrity status based on key revocation, which can be propagated to files verified with the affected digest lists.
With IMA appraisal, it is easy to maintain authenticity but challenging to maintain integrity over time. In user-space there are constantly new CVEs. To maintain integrity over time, either keys need to be rotated in the .ima keyring or program hashes need to be frequently added to the .blacklist. If neither is done, for an end-user on a distro, IMA-appraisal basically guarantees authenticity.
While I understand the intent of the series is to increase performance, have you considered using this to give the end-user the ability to maintain integrity of their system? What I mean is, instead of trying to import anything from an RPM, just have the end-user provide this information in some format to the Digest Cache. User-space tools could be built to collect and format
This is already possible, digest-cache-tools (https://github.com/linux-integrity/digest-cache-tools) already allow to create a digest list with the file a user wants.
But in this case, the user is vouching for having taken the correct measure of the file at the time it was added to the digest list. This would be instead automatically guaranteed by RPMs or other packages shipped with Linux distributions.
To mitigate the concerns of CVEs, we can probably implement a rollback prevention mechanism, which would not allow to load a previous version of a digest list.
IMHO, pursuing this with the end-user being in control of what is contained within the Digest Cache vs what is contained in a distro would provide more value. Allowing the end-user to easily update their Digest Cache in some way without having to do any type of revocation for both old and vulnerable applications with CVEs would be very beneficial.
Yes, deleting the digest list would invalidate any integrity result done with that digest list.
I developed also an rpm plugin that synchronizes the digest lists with installed software. Old vulnerable software cannot be verified anymore with the Integrity Digest Cache, since the rpm plugin deletes the old software digest lists.
https://github.com/linux-integrity/digest-cache-tools/blob/main/rpm-plugin/d...
The good thing is that the Integrity Digest Cache can be easily controlled with filesystem operations (it works similarly to security blobs attached to kernel objects, like inodes and file descriptors).
As soon as something changes (e.g. digest list written, link to the digest lists), this triggers a reset in the Integrity Digest Cache, so digest lists and files need to be verified again. Deleting the digest list causes the in-kernel digest cache to be wiped away too (when the reference count reaches zero).
Is there a belief the Digest Cache would be used without signed kernel modules? Is the performance gain worth changing how kernel modules get loaded at boot? Couldn't this part just be dropped for easier acceptance? Integrity is already maintained with the current model of appended signatures.
I don't like making exceptions in the design, and I recently realized that it should not be task of the users of the Integrity Digest Cache to limit themselves.
Forgot to mention that your use case is possible. The usage of the Integrity Digest Cache must be explicitly enabled in the IMA policy. It will be used if the matching rule has 'digest_cache=data' (its foreseen to be used also for metadata).
I see a lot of benefit if metadata integrity could be maintained, but in the current form of this series, I don't think that is possible. The Digest Cache doesn't contain or enforce the file path, which would be necessary to maintain integrity. Here is an example of why it would be needed, say you have two applications that need a configuration file to start. The first application has an empty file where no configuration options are currently defined. Now there is a hash for an empty file in the Digest Cache. The second application can be started with an empty configuration file, however the end-user has added some options to it. If the configuration file for the second application is replaced with an empty file, it will not be detected, since the Digest Cache would see the empty file hash in its cache.
For kernel modules, it is sufficient to not provide that keyword for the MODULE_CHECK hook.
However, there is the possibility that you lose another advantage of the Integrity Digest Cache, the predictability of the IMA PCR. By not using digest caches, there is the risk that the IMA PCR will be unstable, due to loading kernel modules in a different order at each boot.
Understood, my recommendation was based on trying to narrow the series to help try to get something like this adopted quicker.
On Thu, 2024-12-05 at 19:41 +0000, Eric Snowberg wrote:
On Dec 5, 2024, at 9:16 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Thu, 2024-12-05 at 09:53 +0100, Roberto Sassu wrote:
On Thu, 2024-12-05 at 00:57 +0000, Eric Snowberg wrote:
On Dec 4, 2024, at 3:44 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-12-03 at 20:06 +0000, Eric Snowberg wrote:
> On Nov 26, 2024, at 3:41 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote: > > On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote: > > > > > On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote: > > > > > > From: Roberto Sassu roberto.sassu@huawei.com > > > > > > The Integrity Digest Cache can also help IMA for appraisal. IMA can simply > > > lookup the calculated digest of an accessed file in the list of digests > > > extracted from package headers, after verifying the header signature. It is > > > sufficient to verify only one signature for all files in the package, as > > > opposed to verifying a signature for each file. > > > > Is there a way to maintain integrity over time? Today if a CVE is discovered > > in a signed program, the program hash can be added to the blacklist keyring. > > Later if IMA appraisal is used, the signature validation will fail just for that > > program. With the Integrity Digest Cache, is there a way to do this? > > As far as I can see, the ima_check_blacklist() call is before > ima_appraise_measurement(). If it fails, appraisal with the Integrity > Digest Cache will not be done.
It is good the program hash would be checked beforehand and fail if it is contained on the list.
The .ima keyring may contain many keys. If one of the keys was later revoked and added to the .blacklist, wouldn't this be missed? It would be caught during signature validation when the file is later appraised, but now this step isn't taking place. Correct?
For files included in the digest lists, yes, there won't be detection of later revocation of a key. However, it will still work at package level/digest list level, since they are still appraised with a signature.
We can add a mechanism (if it does not already exist) to invalidate the integrity status based on key revocation, which can be propagated to files verified with the affected digest lists.
With IMA appraisal, it is easy to maintain authenticity but challenging to maintain integrity over time. In user-space there are constantly new CVEs. To maintain integrity over time, either keys need to be rotated in the .ima keyring or program hashes need to be frequently added to the .blacklist. If neither is done, for an end-user on a distro, IMA-appraisal basically guarantees authenticity.
While I understand the intent of the series is to increase performance, have you considered using this to give the end-user the ability to maintain integrity of their system? What I mean is, instead of trying to import anything from an RPM, just have the end-user provide this information in some format to the Digest Cache. User-space tools could be built to collect and format
This is already possible, digest-cache-tools (https://github.com/linux-integrity/digest-cache-tools) already allow to create a digest list with the file a user wants.
But in this case, the user is vouching for having taken the correct measure of the file at the time it was added to the digest list. This would be instead automatically guaranteed by RPMs or other packages shipped with Linux distributions.
To mitigate the concerns of CVEs, we can probably implement a rollback prevention mechanism, which would not allow to load a previous version of a digest list.
IMHO, pursuing this with the end-user being in control of what is contained within the Digest Cache vs what is contained in a distro would provide more value. Allowing the end-user to easily update their Digest Cache in some way without having to do any type of revocation for both old and vulnerable applications with CVEs would be very beneficial.
Yes, deleting the digest list would invalidate any integrity result done with that digest list.
I developed also an rpm plugin that synchronizes the digest lists with installed software. Old vulnerable software cannot be verified anymore with the Integrity Digest Cache, since the rpm plugin deletes the old software digest lists.
https://github.com/linux-integrity/digest-cache-tools/blob/main/rpm-plugin/d...
The good thing is that the Integrity Digest Cache can be easily controlled with filesystem operations (it works similarly to security blobs attached to kernel objects, like inodes and file descriptors).
As soon as something changes (e.g. digest list written, link to the digest lists), this triggers a reset in the Integrity Digest Cache, so digest lists and files need to be verified again. Deleting the digest list causes the in-kernel digest cache to be wiped away too (when the reference count reaches zero).
Is there a belief the Digest Cache would be used without signed kernel modules? Is the performance gain worth changing how kernel modules get loaded at boot? Couldn't this part just be dropped for easier acceptance? Integrity is already maintained with the current model of appended signatures.
I don't like making exceptions in the design, and I recently realized that it should not be task of the users of the Integrity Digest Cache to limit themselves.
Forgot to mention that your use case is possible. The usage of the Integrity Digest Cache must be explicitly enabled in the IMA policy. It will be used if the matching rule has 'digest_cache=data' (its foreseen to be used also for metadata).
I see a lot of benefit if metadata integrity could be maintained, but in the current form of this series, I don't think that is possible. The Digest Cache doesn't contain or enforce the file path, which would be necessary to maintain integrity. Here is an example of why it would be needed, say you have two applications that need a configuration file to start. The first application has an empty file where no configuration options are currently defined. Now there is a hash for an empty file in the Digest Cache. The second application can be started with an empty configuration file, however the end-user has added some options to it. If the configuration file for the second application is replaced with an empty file, it will not be detected, since the Digest Cache would see the empty file hash in its cache.
I was thinking more to store in the digest cache digests of metadata (including for example the expected SELinux label), that EVM can lookup.
In that way, the problem you foresee cannot happen: if you replace the file belonging to app2_t with the one belonging to app1_t, SELinux would deny the permission to access; if you change the SELinux label of the file, EVM will deny the access.
You can still go back to the initial state, for that a rollback prevention mechanism is needed (maybe EVM can remove the digest of the initial state from the digest cache when it sees an update?).
In general, the Integrity Digest Cache should be considered as an alternative mechanism to validate immutable files, or the initial state of mutable files. For mutable files, EVM HMAC will protect further updates.
Roberto
For kernel modules, it is sufficient to not provide that keyword for the MODULE_CHECK hook.
However, there is the possibility that you lose another advantage of the Integrity Digest Cache, the predictability of the IMA PCR. By not using digest caches, there is the risk that the IMA PCR will be unstable, due to loading kernel modules in a different order at each boot.
Understood, my recommendation was based on trying to narrow the series to help try to get something like this adopted quicker.
On Dec 6, 2024, at 3:06 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Thu, 2024-12-05 at 19:41 +0000, Eric Snowberg wrote:
On Dec 5, 2024, at 9:16 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Thu, 2024-12-05 at 09:53 +0100, Roberto Sassu wrote:
On Thu, 2024-12-05 at 00:57 +0000, Eric Snowberg wrote:
On Dec 4, 2024, at 3:44 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Tue, 2024-12-03 at 20:06 +0000, Eric Snowberg wrote: > >> On Nov 26, 2024, at 3:41 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote: >> >> On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote: >>> >>>> On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote: >>>> >>>> From: Roberto Sassu roberto.sassu@huawei.com >>>> >>>> The Integrity Digest Cache can also help IMA for appraisal. IMA can simply >>>> lookup the calculated digest of an accessed file in the list of digests >>>> extracted from package headers, after verifying the header signature. It is >>>> sufficient to verify only one signature for all files in the package, as >>>> opposed to verifying a signature for each file. >>> >>> Is there a way to maintain integrity over time? Today if a CVE is discovered >>> in a signed program, the program hash can be added to the blacklist keyring. >>> Later if IMA appraisal is used, the signature validation will fail just for that >>> program. With the Integrity Digest Cache, is there a way to do this? >> >> As far as I can see, the ima_check_blacklist() call is before >> ima_appraise_measurement(). If it fails, appraisal with the Integrity >> Digest Cache will not be done. > > > It is good the program hash would be checked beforehand and fail if it is > contained on the list. > > The .ima keyring may contain many keys. If one of the keys was later > revoked and added to the .blacklist, wouldn't this be missed? It would > be caught during signature validation when the file is later appraised, but > now this step isn't taking place. Correct?
For files included in the digest lists, yes, there won't be detection of later revocation of a key. However, it will still work at package level/digest list level, since they are still appraised with a signature.
We can add a mechanism (if it does not already exist) to invalidate the integrity status based on key revocation, which can be propagated to files verified with the affected digest lists.
> With IMA appraisal, it is easy to maintain authenticity but challenging to > maintain integrity over time. In user-space there are constantly new CVEs. > To maintain integrity over time, either keys need to be rotated in the .ima > keyring or program hashes need to be frequently added to the .blacklist. > If neither is done, for an end-user on a distro, IMA-appraisal basically > guarantees authenticity. > > While I understand the intent of the series is to increase performance, > have you considered using this to give the end-user the ability to maintain > integrity of their system? What I mean is, instead of trying to import anything > from an RPM, just have the end-user provide this information in some format > to the Digest Cache. User-space tools could be built to collect and format
This is already possible, digest-cache-tools (https://github.com/linux-integrity/digest-cache-tools) already allow to create a digest list with the file a user wants.
But in this case, the user is vouching for having taken the correct measure of the file at the time it was added to the digest list. This would be instead automatically guaranteed by RPMs or other packages shipped with Linux distributions.
To mitigate the concerns of CVEs, we can probably implement a rollback prevention mechanism, which would not allow to load a previous version of a digest list.
IMHO, pursuing this with the end-user being in control of what is contained within the Digest Cache vs what is contained in a distro would provide more value. Allowing the end-user to easily update their Digest Cache in some way without having to do any type of revocation for both old and vulnerable applications with CVEs would be very beneficial.
Yes, deleting the digest list would invalidate any integrity result done with that digest list.
I developed also an rpm plugin that synchronizes the digest lists with installed software. Old vulnerable software cannot be verified anymore with the Integrity Digest Cache, since the rpm plugin deletes the old software digest lists.
https://github.com/linux-integrity/digest-cache-tools/blob/main/rpm-plugin/d...
The good thing is that the Integrity Digest Cache can be easily controlled with filesystem operations (it works similarly to security blobs attached to kernel objects, like inodes and file descriptors).
As soon as something changes (e.g. digest list written, link to the digest lists), this triggers a reset in the Integrity Digest Cache, so digest lists and files need to be verified again. Deleting the digest list causes the in-kernel digest cache to be wiped away too (when the reference count reaches zero).
Is there a belief the Digest Cache would be used without signed kernel modules? Is the performance gain worth changing how kernel modules get loaded at boot? Couldn't this part just be dropped for easier acceptance? Integrity is already maintained with the current model of appended signatures.
I don't like making exceptions in the design, and I recently realized that it should not be task of the users of the Integrity Digest Cache to limit themselves.
Forgot to mention that your use case is possible. The usage of the Integrity Digest Cache must be explicitly enabled in the IMA policy. It will be used if the matching rule has 'digest_cache=data' (its foreseen to be used also for metadata).
I see a lot of benefit if metadata integrity could be maintained, but in the current form of this series, I don't think that is possible. The Digest Cache doesn't contain or enforce the file path, which would be necessary to maintain integrity. Here is an example of why it would be needed, say you have two applications that need a configuration file to start. The first application has an empty file where no configuration options are currently defined. Now there is a hash for an empty file in the Digest Cache. The second application can be started with an empty configuration file, however the end-user has added some options to it. If the configuration file for the second application is replaced with an empty file, it will not be detected, since the Digest Cache would see the empty file hash in its cache.
I was thinking more to store in the digest cache digests of metadata (including for example the expected SELinux label), that EVM can lookup.
In that way, the problem you foresee cannot happen: if you replace the file belonging to app2_t with the one belonging to app1_t, SELinux would deny the permission to access; if you change the SELinux label of the file, EVM will deny the access.
If two different applications have config files in /etc, wouldn't both files have the same SELinux label?
You can still go back to the initial state, for that a rollback prevention mechanism is needed (maybe EVM can remove the digest of the initial state from the digest cache when it sees an update?).
In general, the Integrity Digest Cache should be considered as an alternative mechanism to validate immutable files, or the initial state of mutable files. For mutable files, EVM HMAC will protect further updates.
In the example above, from a distro standpoint, most files contained in /etc are viewed as being mutable. However an end-user that wants to maintain integrity on their system wouldn't view it that way. They don't want config changes they have made to be backed out. In the current form they would view this series as an Authenticity Digest Cache. I'm just trying to show that this could be a lot more valuable to the end-user if some things were changed.
On Fri, 2024-12-06 at 15:15 +0000, Eric Snowberg wrote:
On Dec 6, 2024, at 3:06 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Thu, 2024-12-05 at 19:41 +0000, Eric Snowberg wrote:
On Dec 5, 2024, at 9:16 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Thu, 2024-12-05 at 09:53 +0100, Roberto Sassu wrote:
On Thu, 2024-12-05 at 00:57 +0000, Eric Snowberg wrote:
> On Dec 4, 2024, at 3:44 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote: > > On Tue, 2024-12-03 at 20:06 +0000, Eric Snowberg wrote: > > > > > On Nov 26, 2024, at 3:41 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote: > > > > > > On Tue, 2024-11-26 at 00:13 +0000, Eric Snowberg wrote: > > > > > > > > > On Nov 19, 2024, at 3:49 AM, Roberto Sassu roberto.sassu@huaweicloud.com wrote: > > > > > > > > > > From: Roberto Sassu roberto.sassu@huawei.com > > > > > > > > > > The Integrity Digest Cache can also help IMA for appraisal. IMA can simply > > > > > lookup the calculated digest of an accessed file in the list of digests > > > > > extracted from package headers, after verifying the header signature. It is > > > > > sufficient to verify only one signature for all files in the package, as > > > > > opposed to verifying a signature for each file. > > > > > > > > Is there a way to maintain integrity over time? Today if a CVE is discovered > > > > in a signed program, the program hash can be added to the blacklist keyring. > > > > Later if IMA appraisal is used, the signature validation will fail just for that > > > > program. With the Integrity Digest Cache, is there a way to do this? > > > > > > As far as I can see, the ima_check_blacklist() call is before > > > ima_appraise_measurement(). If it fails, appraisal with the Integrity > > > Digest Cache will not be done. > > > > > > It is good the program hash would be checked beforehand and fail if it is > > contained on the list. > > > > The .ima keyring may contain many keys. If one of the keys was later > > revoked and added to the .blacklist, wouldn't this be missed? It would > > be caught during signature validation when the file is later appraised, but > > now this step isn't taking place. Correct? > > For files included in the digest lists, yes, there won't be detection > of later revocation of a key. However, it will still work at package > level/digest list level, since they are still appraised with a > signature. > > We can add a mechanism (if it does not already exist) to invalidate the > integrity status based on key revocation, which can be propagated to > files verified with the affected digest lists. > > > With IMA appraisal, it is easy to maintain authenticity but challenging to > > maintain integrity over time. In user-space there are constantly new CVEs. > > To maintain integrity over time, either keys need to be rotated in the .ima > > keyring or program hashes need to be frequently added to the .blacklist. > > If neither is done, for an end-user on a distro, IMA-appraisal basically > > guarantees authenticity. > > > > While I understand the intent of the series is to increase performance, > > have you considered using this to give the end-user the ability to maintain > > integrity of their system? What I mean is, instead of trying to import anything > > from an RPM, just have the end-user provide this information in some format > > to the Digest Cache. User-space tools could be built to collect and format > > This is already possible, digest-cache-tools > (https://github.com/linux-integrity/digest-cache-tools) already allow > to create a digest list with the file a user wants. > > But in this case, the user is vouching for having taken the correct > measure of the file at the time it was added to the digest list. This > would be instead automatically guaranteed by RPMs or other packages > shipped with Linux distributions. > > To mitigate the concerns of CVEs, we can probably implement a rollback > prevention mechanism, which would not allow to load a previous version > of a digest list.
IMHO, pursuing this with the end-user being in control of what is contained within the Digest Cache vs what is contained in a distro would provide more value. Allowing the end-user to easily update their Digest Cache in some way without having to do any type of revocation for both old and vulnerable applications with CVEs would be very beneficial.
Yes, deleting the digest list would invalidate any integrity result done with that digest list.
I developed also an rpm plugin that synchronizes the digest lists with installed software. Old vulnerable software cannot be verified anymore with the Integrity Digest Cache, since the rpm plugin deletes the old software digest lists.
https://github.com/linux-integrity/digest-cache-tools/blob/main/rpm-plugin/d...
The good thing is that the Integrity Digest Cache can be easily controlled with filesystem operations (it works similarly to security blobs attached to kernel objects, like inodes and file descriptors).
As soon as something changes (e.g. digest list written, link to the digest lists), this triggers a reset in the Integrity Digest Cache, so digest lists and files need to be verified again. Deleting the digest list causes the in-kernel digest cache to be wiped away too (when the reference count reaches zero).
Is there a belief the Digest Cache would be used without signed kernel modules? Is the performance gain worth changing how kernel modules get loaded at boot? Couldn't this part just be dropped for easier acceptance? Integrity is already maintained with the current model of appended signatures.
I don't like making exceptions in the design, and I recently realized that it should not be task of the users of the Integrity Digest Cache to limit themselves.
Forgot to mention that your use case is possible. The usage of the Integrity Digest Cache must be explicitly enabled in the IMA policy. It will be used if the matching rule has 'digest_cache=data' (its foreseen to be used also for metadata).
I see a lot of benefit if metadata integrity could be maintained, but in the current form of this series, I don't think that is possible. The Digest Cache doesn't contain or enforce the file path, which would be necessary to maintain integrity. Here is an example of why it would be needed, say you have two applications that need a configuration file to start. The first application has an empty file where no configuration options are currently defined. Now there is a hash for an empty file in the Digest Cache. The second application can be started with an empty configuration file, however the end-user has added some options to it. If the configuration file for the second application is replaced with an empty file, it will not be detected, since the Digest Cache would see the empty file hash in its cache.
I was thinking more to store in the digest cache digests of metadata (including for example the expected SELinux label), that EVM can lookup.
In that way, the problem you foresee cannot happen: if you replace the file belonging to app2_t with the one belonging to app1_t, SELinux would deny the permission to access; if you change the SELinux label of the file, EVM will deny the access.
If two different applications have config files in /etc, wouldn't both files have the same SELinux label?
Likely, unless there is an application-specific policy.
You can still go back to the initial state, for that a rollback prevention mechanism is needed (maybe EVM can remove the digest of the initial state from the digest cache when it sees an update?).
In general, the Integrity Digest Cache should be considered as an alternative mechanism to validate immutable files, or the initial state of mutable files. For mutable files, EVM HMAC will protect further updates.
In the example above, from a distro standpoint, most files contained in /etc are viewed as being mutable. However an end-user that wants to maintain integrity on their system wouldn't view it that way. They don't want config changes they have made to be backed out. In the current form they would view this series as an Authenticity Digest Cache. I'm just trying to show that this could be a lot more valuable to the end-user if some things were changed.
I agree, I think the current patch set contains the minimum necessary, and it can grow depending on use cases/requirements from the community.
Thanks
Roberto
On Tue, Nov 19, 2024 at 11:49:07AM +0100, Roberto Sassu wrote:
Hi Roberto, I hope the week is going well for you.
From: Roberto Sassu roberto.sassu@huawei.com
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 a large user base, due to requiring higher than desired constraints on performance, flexibility and configurability, that only security conscious people are willing to accept.
For example, IMA measurement requires the target platform to collect integrity measurements, and to protect them with the TPM, which introduces a noticeable overhead (up to 10x slower in a microbenchmark) on frequently used system calls, like the open().
IMA Appraisal currently requires individual files to be signed and verified, and Linux distributions to rebuild all packages to include file signatures (this approach has been adopted from Fedora 39+). Like a TPM, also signature verification introduces a significant overhead, especially if it is used to check the integrity of many files.
This is where the new Integrity Digest Cache comes into play, it offers additional support for new and existing integrity solutions, to make them faster and easier to deploy.
The Integrity Digest Cache can help IMA to reduce the number of TPM operations and to make them happen 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.
The performance improvement comes at the cost of IMA not reporting which files from installed packages were accessed, and in which temporal sequence. This approach might not be suitable for all use cases.
The Integrity Digest Cache can also help IMA for appraisal. IMA can simply lookup the calculated digest of an accessed file in the list of digests extracted from package headers, after verifying the header signature. It is sufficient to verify only one signature for all files in the package, as opposed to verifying a signature for each file.
Roberto, a big picture question for you, our apologies if we completely misunderstand your patch series.
The performance benefit comes from the fact that the kernel doesn't have to read a file and calculate the cryptographic digest when the file is accessed. The 'trusted' digest value comes from a signed list of digests that a packaging entity provides and the kernel validates. So there is an integrity guarantee that the supplied digests were the same as when the package was built.
Is there a guarantee implemented, that we missed, that the on-disk file actually has the digest value that was initially generated by the packaging entity when the file is accessed operationally?
Secondly, and in a related issue, what happens in a container environment when a pathname is accessed that is actually a different file but with the same effective pathname as a file that is in the vendor validated digest list?
Once again, apologies, if we completely misinterpret the issues involved.
Have a good remainder of the week.
As always, Dr. Greg
The Quixote Project - Flailing at the Travails of Cybersecurity https://github.com/Quixote-Project
On Wed, 2024-11-27 at 11:30 -0600, Dr. Greg wrote:
On Tue, Nov 19, 2024 at 11:49:07AM +0100, Roberto Sassu wrote:
Hi Roberto, I hope the week is going well for you.
From: Roberto Sassu roberto.sassu@huawei.com
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 a large user base, due to requiring higher than desired constraints on performance, flexibility and configurability, that only security conscious people are willing to accept.
For example, IMA measurement requires the target platform to collect integrity measurements, and to protect them with the TPM, which introduces a noticeable overhead (up to 10x slower in a microbenchmark) on frequently used system calls, like the open().
IMA Appraisal currently requires individual files to be signed and verified, and Linux distributions to rebuild all packages to include file signatures (this approach has been adopted from Fedora 39+). Like a TPM, also signature verification introduces a significant overhead, especially if it is used to check the integrity of many files.
This is where the new Integrity Digest Cache comes into play, it offers additional support for new and existing integrity solutions, to make them faster and easier to deploy.
The Integrity Digest Cache can help IMA to reduce the number of TPM operations and to make them happen 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.
The performance improvement comes at the cost of IMA not reporting which files from installed packages were accessed, and in which temporal sequence. This approach might not be suitable for all use cases.
The Integrity Digest Cache can also help IMA for appraisal. IMA can simply lookup the calculated digest of an accessed file in the list of digests extracted from package headers, after verifying the header signature. It is sufficient to verify only one signature for all files in the package, as opposed to verifying a signature for each file.
Roberto, a big picture question for you, our apologies if we completely misunderstand your patch series.
Hi Greg
no need to apologise, happy to answer your questions.
The performance benefit comes from the fact that the kernel doesn't have to read a file and calculate the cryptographic digest when the file is accessed. The 'trusted' digest value comes from a signed list of digests that a packaging entity provides and the kernel validates. So there is an integrity guarantee that the supplied digests were the same as when the package was built.
The performance benefit (for appraisal with my benchmark: 65% with sequential file access and 43% with parallel file access) comes from verifying the ECDSA signature of 303 digest lists, as opposed to the ECDSA signature of 12312 files.
The additional performance boost due to switching from file data digest to fsverity digests is on top of that.
Is there a guarantee implemented, that we missed, that the on-disk file actually has the digest value that was initially generated by the packaging entity when the file is accessed operationally?
Yes, the guarantee is provided by IMA by measuring the actual file digest and searching it in a digest cache. The integration in IMA of the Integrity Digest Cache is done in a separate patch set:
https://lore.kernel.org/linux-security-module/20241119110103.2780453-1-rober...
The integrity evaluation result is invalidated when the file is modified, or when the digest list used to verify the file is modified too.
For fsverity, the guarantee similarly comes from searching the fsverity digest in a digest cache, but as opposed of IMA the integrity evaluation result does not need to be invalidated for a file write, since fsverity-protected files are accessible only in read-only mode. However, the result still needs to be invalidated if the digest list changes.
Secondly, and in a related issue, what happens in a container environment when a pathname is accessed that is actually a different file but with the same effective pathname as a file that is in the vendor validated digest list?
At the moment nothing, only the file data are evaluated. Currently, the Integrity Digest Cache does not store the pathnames associated to a digest. It can be done as an extension, if desired, and the pathnames can be compared.
Roberto
Once again, apologies, if we completely misinterpret the issues involved.
Have a good remainder of the week.
As always, Dr. Greg
The Quixote Project - Flailing at the Travails of Cybersecurity https://github.com/Quixote-Project
linux-kselftest-mirror@lists.linaro.org