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. While it currently supports a TLV-based and the RPM formats, it can be easily extended to support more formats, such as DEBs. Focusing on just extracting digests keeps these parsers minimal and reasonably simple (e.g. the RPM parser has ~220 LOC). Included parsers have been verified for memory safety with the Frama-C static analyzer. The parsers with the Frama-C assertions are available here:
https://github.com/robertosassu/rpm-formal/
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 and RPM data formats, 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 fa8a4ce432e8 ("ima: fix buffer overrun in ima_eventdigest_init_common").
Changelog
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 (14): lib: Add TLV parser 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: Populate the digest cache from a digest list digest_cache: Parse tlv digest lists digest_cache: Parse rpm digest lists 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 | 814 ++++++++++++++++++ Documentation/security/index.rst | 1 + MAINTAINERS | 10 + include/linux/digest_cache.h | 58 ++ include/linux/kernel_read_file.h | 1 + include/linux/tlv_parser.h | 48 ++ include/uapi/linux/tlv_digest_list.h | 72 ++ include/uapi/linux/tlv_parser.h | 62 ++ include/uapi/linux/xattr.h | 6 + lib/Kconfig | 3 + lib/Makefile | 2 + lib/tlv_parser.c | 221 +++++ lib/tlv_parser.h | 17 + security/integrity/Kconfig | 1 + security/integrity/Makefile | 1 + security/integrity/digest_cache/Kconfig | 33 + security/integrity/digest_cache/Makefile | 11 + security/integrity/digest_cache/dir.c | 397 +++++++++ security/integrity/digest_cache/htable.c | 254 ++++++ security/integrity/digest_cache/internal.h | 277 ++++++ security/integrity/digest_cache/main.c | 559 ++++++++++++ security/integrity/digest_cache/modsig.c | 66 ++ .../integrity/digest_cache/parsers/parsers.h | 15 + security/integrity/digest_cache/parsers/rpm.c | 220 +++++ security/integrity/digest_cache/parsers/tlv.c | 341 ++++++++ security/integrity/digest_cache/populate.c | 157 ++++ security/integrity/digest_cache/reset.c | 227 +++++ security/integrity/digest_cache/secfs.c | 104 +++ security/integrity/digest_cache/verif.c | 131 +++ security/integrity/ima/ima.h | 1 + security/integrity/ima/ima_fs.c | 6 + security/integrity/ima/ima_main.c | 11 +- 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 | 749 ++++++++++++++++ tools/testing/selftests/digest_cache/common.c | 78 ++ tools/testing/selftests/digest_cache/common.h | 134 +++ .../selftests/digest_cache/common_user.c | 47 + .../selftests/digest_cache/common_user.h | 17 + tools/testing/selftests/digest_cache/config | 1 + .../selftests/digest_cache/generators.c | 248 ++++++ .../selftests/digest_cache/generators.h | 19 + .../selftests/digest_cache/testmod/Makefile | 16 + .../selftests/digest_cache/testmod/kern.c | 501 +++++++++++ 45 files changed, 5964 insertions(+), 1 deletion(-) 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/parsers.h create mode 100644 security/integrity/digest_cache/parsers/rpm.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:
+-----------------+------------------+-----------------+ | data type (u64) | num fields (u64) | total len (u64) | # header +--------------+--+---------+--------+---------+-------+ | field1 (u64) | len1 (u64) | value1 (u8 len1) | +--------------+------------+------------------+ | ... | ... | ... | # data +--------------+------------+------------------+ | fieldN (u64) | lenN (u64) | valueN (u8 lenN) | +--------------+------------+------------------+
[same as above, repeated N times]
Each adopter can define its own data types and 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 parsing each TLV header, call the header callback function with the callback data provided by the adopter. The latter can return 0, to skip processing of the TLV data, 1 to process the TLV data, or a negative value to stop processing the TLV data.
After processing a TLV data entry, call the data callback function also with the callback data provided by the adopter. The latter can decide how to interpret the TLV data entry depending on the field ID.
Nesting TLVs is also possible, the data 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 | 48 +++++++ include/uapi/linux/tlv_parser.h | 62 +++++++++ lib/Kconfig | 3 + lib/Makefile | 2 + lib/tlv_parser.c | 221 ++++++++++++++++++++++++++++++++ lib/tlv_parser.h | 17 +++ 7 files changed, 361 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 8766f3e5e87e..ba8d5c137bef 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23055,6 +23055,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..6d9a655d9ec9 --- /dev/null +++ b/include/linux/tlv_parser.h @@ -0,0 +1,48 @@ +/* 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 hdr_callback - Callback after parsing TLV header + * @callback_data: Opaque data to supply to the header callback function + * @data_type: TLV data type + * @num_entries: Number of TLV data entries + * @total_len: Total length of TLV data + * + * This callback is invoked after a TLV header is parsed. + * + * Return: 0 to skip processing, 1 to do processing, a negative value on error. + */ +typedef int (*hdr_callback)(void *callback_data, __u64 data_type, + __u64 num_entries, __u64 total_len); + +/** + * typedef data_callback - Callback after parsing TLV data entry + * @callback_data: Opaque data to supply to the data callback function + * @field: TLV field ID + * @field_data: Data of a TLV data field + * @field_len: Length of @field_data + * + * This callback is invoked after a TLV data entry is parsed. + * + * Return: 0 on success, a negative value on error. + */ +typedef int (*data_callback)(void *callback_data, __u64 field, + const __u8 *field_data, __u64 field_len); + +int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data, + data_callback data_callback, void *data_callback_data, + const __u8 *data, size_t data_len, const char **data_types, + __u64 num_data_types, const char **fields, __u64 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..fbd4fc403ac7 --- /dev/null +++ b/include/uapi/linux/tlv_parser.h @@ -0,0 +1,62 @@ +/* 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: + * + * +-----------------+------------------+-----------------+ + * | data type (u64) | num fields (u64) | total len (u64) | # header + * +--------------+--+---------+--------+---------+-------+ + * | field1 (u64) | len1 (u64) | value1 (u8 len1) | + * +--------------+------------+------------------+ + * | ... | ... | ... | # data + * +--------------+------------+------------------+ + * | fieldN (u64) | lenN (u64) | valueN (u8 lenN) | + * +--------------+------------+------------------+ + * + * [same as above, repeated N times] + * + */ + +/** + * struct tlv_hdr - Header of TLV format + * @data_type: Type of data to parse + * @num_entries: Number of data entries provided + * @_reserved: Reserved for future use (must be equal to zero) + * @total_len: Total length of the data blob, excluding the header + * + * This structure represents the header of the TLV data format. + */ +struct tlv_hdr { + __u64 data_type; + __u64 num_entries; + __u64 _reserved; + __u64 total_len; +} __attribute__((packed)); + +/** + * struct tlv_data_entry - Data entry of TLV format + * @field: Data field identifier + * @length: Data length + * @data: Data + * + * This structure represents a TLV data entry. + */ +struct tlv_data_entry { + __u64 field; + __u64 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 322bb127b4dc..c6c3614c4293 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -392,6 +392,8 @@ 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
# FORTIFY_SOURCE compile-time behavior tests TEST_FORTIFY_SRCS = $(wildcard $(src)/test_fortify/*-*.c) diff --git a/lib/tlv_parser.c b/lib/tlv_parser.c new file mode 100644 index 000000000000..5d54844ab8d7 --- /dev/null +++ b/lib/tlv_parser.c @@ -0,0 +1,221 @@ +// 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_hdr - Parse TLV header + * @hdr_callback: Callback function to call after parsing header + * @hdr_callback_data: Opaque data to supply to the header callback function + * @data: Data to parse (updated) + * @data_len: Length of @data (updated) + * @parsed_num_entries: Parsed number of data entries (updated) + * @parsed_total_len: Parsed length of TLV data, excluding the header (updated) + * @data_types: Array of data type strings + * @num_data_types: Number of elements of @data_types + * + * Parse the header of the TLV data format, move the data pointer to the TLV + * data part, decrease the data length by the length of the header, and provide + * the number of entries and the total data length extracted from the header. + * + * Before returning, call the header callback to let the callback supplier + * decide whether or not to process the subsequent TLV data. + * + * Return: 1 to process the data entries, 0 to skip, a negative value on error. + */ +static int tlv_parse_hdr(hdr_callback hdr_callback, void *hdr_callback_data, + const __u8 **data, size_t *data_len, + __u64 *parsed_num_entries, __u64 *parsed_total_len, + const char **data_types, __u64 num_data_types) +{ + __u64 parsed_data_type; + struct tlv_hdr *hdr; + + if (*data_len < sizeof(*hdr)) { + pr_debug("Data blob too short, %lu bytes, expected %lu\n", + *data_len, sizeof(*hdr)); + return -EBADMSG; + } + + hdr = (struct tlv_hdr *)*data; + + *data += sizeof(*hdr); + *data_len -= sizeof(*hdr); + + parsed_data_type = __be64_to_cpu(hdr->data_type); + if (parsed_data_type >= num_data_types) { + pr_debug("Invalid data type %llu, max: %llu\n", + parsed_data_type, num_data_types - 1); + return -EBADMSG; + } + + *parsed_num_entries = __be64_to_cpu(hdr->num_entries); + + if (hdr->_reserved != 0) { + pr_debug("_reserved must be zero\n"); + return -EBADMSG; + } + + *parsed_total_len = __be64_to_cpu(hdr->total_len); + if (*parsed_total_len > *data_len) { + pr_debug("Invalid total length %llu, expected: %lu\n", + *parsed_total_len, *data_len); + return -EBADMSG; + } + + pr_debug("Header: type: %s, num entries: %llu, total len: %lld\n", + data_types[parsed_data_type], *parsed_num_entries, + *parsed_total_len); + + return hdr_callback(hdr_callback_data, parsed_data_type, + *parsed_num_entries, *parsed_total_len); +} + +/** + * tlv_parse_data - Parse TLV data + * @data_callback: Callback function to call to parse the data entries + * @data_callback_data: Opaque data to supply to the data callback function + * @num_entries: Number of data entries to parse + * @data: Data to parse + * @data_len: Length of @data + * @fields: Array of field strings + * @num_fields: Number of elements of @fields + * + * Parse the data part of the TLV data format and call the supplied callback + * function for each data entry, passing also the opaque data pointer. + * + * The data callback function decides how to process data depending on the + * field. + * + * Return: 0 on success, a negative value on error. + */ +static int tlv_parse_data(data_callback data_callback, void *data_callback_data, + __u64 num_entries, const __u8 *data, size_t data_len, + const char **fields, __u64 num_fields) +{ + const __u8 *data_ptr = data; + struct tlv_data_entry *entry; + __u64 parsed_field, len, i, max_num_entries; + int ret; + + max_num_entries = data_len / sizeof(*entry); + + /* Possibly lower limit on num_entries loop. */ + if (num_entries > max_num_entries) + return -EBADMSG; + + for (i = 0; i < num_entries; i++) { + if (data_len < sizeof(*entry)) + return -EBADMSG; + + entry = (struct tlv_data_entry *)data_ptr; + data_ptr += sizeof(*entry); + data_len -= sizeof(*entry); + + parsed_field = __be64_to_cpu(entry->field); + if (parsed_field >= num_fields) { + pr_debug("Invalid field %llu, max: %llu\n", + parsed_field, num_fields - 1); + return -EBADMSG; + } + + len = __be64_to_cpu(entry->length); + + if (data_len < len) + return -EBADMSG; + + pr_debug("Data: field: %s, len: %llu\n", fields[parsed_field], + len); + + if (!len) + continue; + + ret = data_callback(data_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: %lu bytes\n", data_len); + return -EBADMSG; + } + + return 0; +} + +/** + * tlv_parse - Parse data in TLV format + * @hdr_callback: Callback function to call after parsing header + * @hdr_callback_data: Opaque data to supply to the header callback function + * @data_callback: Callback function to call to parse the data entries + * @data_callback_data: Opaque data to supply to the data callback function + * @data: Data to parse + * @data_len: Length of @data + * @data_types: Array of data type strings + * @num_data_types: Number of elements of @data_types + * @fields: Array of field strings + * @num_fields: Number of elements of @fields + * + * Parse data in TLV format and call tlv_parse_data() each time tlv_parse_hdr() + * returns 1. + * + * Return: 0 on success, a negative value on error. + */ +int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data, + data_callback data_callback, void *data_callback_data, + const __u8 *data, size_t data_len, const char **data_types, + __u64 num_data_types, const char **fields, __u64 num_fields) +{ + __u64 parsed_num_entries, parsed_total_len; + const __u8 *data_ptr = data; + int ret = 0; + + pr_debug("Start parsing data blob, size: %lu\n", data_len); + + while (data_len) { + ret = tlv_parse_hdr(hdr_callback, hdr_callback_data, &data_ptr, + &data_len, &parsed_num_entries, + &parsed_total_len, data_types, + num_data_types); + switch (ret) { + case 0: + /* + * tlv_parse_hdr() already checked that + * parsed_total_len <= data_len. + */ + data_ptr += parsed_total_len; + data_len -= parsed_total_len; + continue; + case 1: + break; + default: + goto out; + } + + ret = tlv_parse_data(data_callback, data_callback_data, + parsed_num_entries, data_ptr, + parsed_total_len, fields, num_fields); + if (ret < 0) + goto out; + + data_ptr += parsed_total_len; + data_len -= parsed_total_len; + } +out: + pr_debug("End of parsing data blob, ret: %d\n", ret); + return ret; +} diff --git a/lib/tlv_parser.h b/lib/tlv_parser.h new file mode 100644 index 000000000000..8fa8127bd13e --- /dev/null +++ b/lib/tlv_parser.h @@ -0,0 +1,17 @@ +/* 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/tlv_parser.h> + +#endif /* _LIB_TLV_PARSER_H */
On Thu, 05 Sep 2024, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Add a parser of a generic Type-Length-Value (TLV) format:
+-----------------+------------------+-----------------+ | data type (u64) | num fields (u64) | total len (u64) | # header +--------------+--+---------+--------+---------+-------+ | field1 (u64) | len1 (u64) | value1 (u8 len1) | +--------------+------------+------------------+ | ... | ... | ... | # data +--------------+------------+------------------+ | fieldN (u64) | lenN (u64) | valueN (u8 lenN) | +--------------+------------+------------------+
Okay, take this with a grain of salt. I'm actually not interested in your use case, but the generic part here. But hear me out.
Why do you need to have num fields in the header? I'd think the generic TLV would have tag/length/value, where value may contain more TLV, or not, depending on the use case specific tag. The same parser can parse everything recursively, with no special handling for headers. To me, that's the great part about TLV.
Also, making generic TLV have u64 tag and length is huge waste in most use cases. Saving one byte requires 16 bytes of tag and length. You could encode tag and length with UTF-8. Sure, it's wasteful if you need an enormous amount of tags or huge lengths, but it's efficient for most use cases.
Anyway, just my thoughts.
BR, Jani.
[same as above, repeated N times]
Each adopter can define its own data types and 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 parsing each TLV header, call the header callback function with the callback data provided by the adopter. The latter can return 0, to skip processing of the TLV data, 1 to process the TLV data, or a negative value to stop processing the TLV data.
After processing a TLV data entry, call the data callback function also with the callback data provided by the adopter. The latter can decide how to interpret the TLV data entry depending on the field ID.
Nesting TLVs is also possible, the data 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 | 48 +++++++ include/uapi/linux/tlv_parser.h | 62 +++++++++ lib/Kconfig | 3 + lib/Makefile | 2 + lib/tlv_parser.c | 221 ++++++++++++++++++++++++++++++++ lib/tlv_parser.h | 17 +++ 7 files changed, 361 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 8766f3e5e87e..ba8d5c137bef 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23055,6 +23055,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..6d9a655d9ec9 --- /dev/null +++ b/include/linux/tlv_parser.h @@ -0,0 +1,48 @@ +/* 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 hdr_callback - Callback after parsing TLV header
- @callback_data: Opaque data to supply to the header callback function
- @data_type: TLV data type
- @num_entries: Number of TLV data entries
- @total_len: Total length of TLV data
- This callback is invoked after a TLV header is parsed.
- Return: 0 to skip processing, 1 to do processing, a negative value on error.
- */
+typedef int (*hdr_callback)(void *callback_data, __u64 data_type,
__u64 num_entries, __u64 total_len);
+/**
- typedef data_callback - Callback after parsing TLV data entry
- @callback_data: Opaque data to supply to the data callback function
- @field: TLV field ID
- @field_data: Data of a TLV data field
- @field_len: Length of @field_data
- This callback is invoked after a TLV data entry is parsed.
- Return: 0 on success, a negative value on error.
- */
+typedef int (*data_callback)(void *callback_data, __u64 field,
const __u8 *field_data, __u64 field_len);
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 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..fbd4fc403ac7 --- /dev/null +++ b/include/uapi/linux/tlv_parser.h @@ -0,0 +1,62 @@ +/* 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:
- +-----------------+------------------+-----------------+
- | data type (u64) | num fields (u64) | total len (u64) | # header
- +--------------+--+---------+--------+---------+-------+
- | field1 (u64) | len1 (u64) | value1 (u8 len1) |
- +--------------+------------+------------------+
- | ... | ... | ... | # data
- +--------------+------------+------------------+
- | fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
- +--------------+------------+------------------+
- [same as above, repeated N times]
- */
+/**
- struct tlv_hdr - Header of TLV format
- @data_type: Type of data to parse
- @num_entries: Number of data entries provided
- @_reserved: Reserved for future use (must be equal to zero)
- @total_len: Total length of the data blob, excluding the header
- This structure represents the header of the TLV data format.
- */
+struct tlv_hdr {
- __u64 data_type;
- __u64 num_entries;
- __u64 _reserved;
- __u64 total_len;
+} __attribute__((packed));
+/**
- struct tlv_data_entry - Data entry of TLV format
- @field: Data field identifier
- @length: Data length
- @data: Data
- This structure represents a TLV data entry.
- */
+struct tlv_data_entry {
- __u64 field;
- __u64 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 322bb127b4dc..c6c3614c4293 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -392,6 +392,8 @@ 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 # FORTIFY_SOURCE compile-time behavior tests TEST_FORTIFY_SRCS = $(wildcard $(src)/test_fortify/*-*.c) diff --git a/lib/tlv_parser.c b/lib/tlv_parser.c new file mode 100644 index 000000000000..5d54844ab8d7 --- /dev/null +++ b/lib/tlv_parser.c @@ -0,0 +1,221 @@ +// 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_hdr - Parse TLV header
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data: Data to parse (updated)
- @data_len: Length of @data (updated)
- @parsed_num_entries: Parsed number of data entries (updated)
- @parsed_total_len: Parsed length of TLV data, excluding the header (updated)
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- Parse the header of the TLV data format, move the data pointer to the TLV
- data part, decrease the data length by the length of the header, and provide
- the number of entries and the total data length extracted from the header.
- Before returning, call the header callback to let the callback supplier
- decide whether or not to process the subsequent TLV data.
- Return: 1 to process the data entries, 0 to skip, a negative value on error.
- */
+static int tlv_parse_hdr(hdr_callback hdr_callback, void *hdr_callback_data,
const __u8 **data, size_t *data_len,
__u64 *parsed_num_entries, __u64 *parsed_total_len,
const char **data_types, __u64 num_data_types)
+{
- __u64 parsed_data_type;
- struct tlv_hdr *hdr;
- if (*data_len < sizeof(*hdr)) {
pr_debug("Data blob too short, %lu bytes, expected %lu\n",
*data_len, sizeof(*hdr));
return -EBADMSG;
- }
- hdr = (struct tlv_hdr *)*data;
- *data += sizeof(*hdr);
- *data_len -= sizeof(*hdr);
- parsed_data_type = __be64_to_cpu(hdr->data_type);
- if (parsed_data_type >= num_data_types) {
pr_debug("Invalid data type %llu, max: %llu\n",
parsed_data_type, num_data_types - 1);
return -EBADMSG;
- }
- *parsed_num_entries = __be64_to_cpu(hdr->num_entries);
- if (hdr->_reserved != 0) {
pr_debug("_reserved must be zero\n");
return -EBADMSG;
- }
- *parsed_total_len = __be64_to_cpu(hdr->total_len);
- if (*parsed_total_len > *data_len) {
pr_debug("Invalid total length %llu, expected: %lu\n",
*parsed_total_len, *data_len);
return -EBADMSG;
- }
- pr_debug("Header: type: %s, num entries: %llu, total len: %lld\n",
data_types[parsed_data_type], *parsed_num_entries,
*parsed_total_len);
- return hdr_callback(hdr_callback_data, parsed_data_type,
*parsed_num_entries, *parsed_total_len);
+}
+/**
- tlv_parse_data - Parse TLV data
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @num_entries: Number of data entries to parse
- @data: Data to parse
- @data_len: Length of @data
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse the data part of the TLV data format and call the supplied callback
- function for each data entry, passing also the opaque data pointer.
- The data callback function decides how to process data depending on the
- field.
- Return: 0 on success, a negative value on error.
- */
+static int tlv_parse_data(data_callback data_callback, void *data_callback_data,
__u64 num_entries, const __u8 *data, size_t data_len,
const char **fields, __u64 num_fields)
+{
- const __u8 *data_ptr = data;
- struct tlv_data_entry *entry;
- __u64 parsed_field, len, i, max_num_entries;
- int ret;
- max_num_entries = data_len / sizeof(*entry);
- /* Possibly lower limit on num_entries loop. */
- if (num_entries > max_num_entries)
return -EBADMSG;
- for (i = 0; i < num_entries; i++) {
if (data_len < sizeof(*entry))
return -EBADMSG;
entry = (struct tlv_data_entry *)data_ptr;
data_ptr += sizeof(*entry);
data_len -= sizeof(*entry);
parsed_field = __be64_to_cpu(entry->field);
if (parsed_field >= num_fields) {
pr_debug("Invalid field %llu, max: %llu\n",
parsed_field, num_fields - 1);
return -EBADMSG;
}
len = __be64_to_cpu(entry->length);
if (data_len < len)
return -EBADMSG;
pr_debug("Data: field: %s, len: %llu\n", fields[parsed_field],
len);
if (!len)
continue;
ret = data_callback(data_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: %lu bytes\n", data_len);
return -EBADMSG;
- }
- return 0;
+}
+/**
- tlv_parse - Parse data in TLV format
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @data: Data to parse
- @data_len: Length of @data
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse data in TLV format and call tlv_parse_data() each time tlv_parse_hdr()
- returns 1.
- Return: 0 on success, a negative value on error.
- */
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 num_fields)
+{
- __u64 parsed_num_entries, parsed_total_len;
- const __u8 *data_ptr = data;
- int ret = 0;
- pr_debug("Start parsing data blob, size: %lu\n", data_len);
- while (data_len) {
ret = tlv_parse_hdr(hdr_callback, hdr_callback_data, &data_ptr,
&data_len, &parsed_num_entries,
&parsed_total_len, data_types,
num_data_types);
switch (ret) {
case 0:
/*
* tlv_parse_hdr() already checked that
* parsed_total_len <= data_len.
*/
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
continue;
case 1:
break;
default:
goto out;
}
ret = tlv_parse_data(data_callback, data_callback_data,
parsed_num_entries, data_ptr,
parsed_total_len, fields, num_fields);
if (ret < 0)
goto out;
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
- }
+out:
- pr_debug("End of parsing data blob, ret: %d\n", ret);
- return ret;
+} diff --git a/lib/tlv_parser.h b/lib/tlv_parser.h new file mode 100644 index 000000000000..8fa8127bd13e --- /dev/null +++ b/lib/tlv_parser.h @@ -0,0 +1,17 @@ +/* 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/tlv_parser.h>
+#endif /* _LIB_TLV_PARSER_H */
On Thu, 2024-09-05 at 20:30 +0300, Jani Nikula wrote:
On Thu, 05 Sep 2024, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Add a parser of a generic Type-Length-Value (TLV) format:
+-----------------+------------------+-----------------+
data type (u64) | num fields (u64) | total len (u64) | # header
+--------------+--+---------+--------+---------+-------+
field1 (u64) | len1 (u64) | value1 (u8 len1) |
+--------------+------------+------------------+
... | ... | ... | # data
+--------------+------------+------------------+
fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
+--------------+------------+------------------+
Okay, take this with a grain of salt. I'm actually not interested in your use case, but the generic part here. But hear me out.
Why do you need to have num fields in the header? I'd think the generic TLV would have tag/length/value, where value may contain more TLV, or not, depending on the use case specific tag. The same parser can parse everything recursively, with no special handling for headers. To me, that's the great part about TLV.
Hi Jani
the purpose of the number of entries is that the kernel uses it to correctly size the hash table, before adding digests to it.
It also allows to group digests together and define attributes for all of them (e.g. the hash algorithm).
You can also have multiple data blocks in the same file, and the header allows to go to the ones of interest.
Also, making generic TLV have u64 tag and length is huge waste in most use cases. Saving one byte requires 16 bytes of tag and length. You could encode tag and length with UTF-8. Sure, it's wasteful if you need an enormous amount of tags or huge lengths, but it's efficient for most use cases.
You are right, it is a huge size. You can introduce new fields, but not modify the tag and length size for retrocompatibility.
Ok, I see your point for UTF-8. Let's see, I like how simple is the parser now.
Thanks
Roberto
Anyway, just my thoughts.
BR, Jani.
[same as above, repeated N times]
Each adopter can define its own data types and 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 parsing each TLV header, call the header callback function with the callback data provided by the adopter. The latter can return 0, to skip processing of the TLV data, 1 to process the TLV data, or a negative value to stop processing the TLV data.
After processing a TLV data entry, call the data callback function also with the callback data provided by the adopter. The latter can decide how to interpret the TLV data entry depending on the field ID.
Nesting TLVs is also possible, the data 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 | 48 +++++++ include/uapi/linux/tlv_parser.h | 62 +++++++++ lib/Kconfig | 3 + lib/Makefile | 2 + lib/tlv_parser.c | 221 ++++++++++++++++++++++++++++++++ lib/tlv_parser.h | 17 +++ 7 files changed, 361 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 8766f3e5e87e..ba8d5c137bef 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23055,6 +23055,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..6d9a655d9ec9 --- /dev/null +++ b/include/linux/tlv_parser.h @@ -0,0 +1,48 @@ +/* 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 hdr_callback - Callback after parsing TLV header
- @callback_data: Opaque data to supply to the header callback function
- @data_type: TLV data type
- @num_entries: Number of TLV data entries
- @total_len: Total length of TLV data
- This callback is invoked after a TLV header is parsed.
- Return: 0 to skip processing, 1 to do processing, a negative value on error.
- */
+typedef int (*hdr_callback)(void *callback_data, __u64 data_type,
__u64 num_entries, __u64 total_len);
+/**
- typedef data_callback - Callback after parsing TLV data entry
- @callback_data: Opaque data to supply to the data callback function
- @field: TLV field ID
- @field_data: Data of a TLV data field
- @field_len: Length of @field_data
- This callback is invoked after a TLV data entry is parsed.
- Return: 0 on success, a negative value on error.
- */
+typedef int (*data_callback)(void *callback_data, __u64 field,
const __u8 *field_data, __u64 field_len);
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 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..fbd4fc403ac7 --- /dev/null +++ b/include/uapi/linux/tlv_parser.h @@ -0,0 +1,62 @@ +/* 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:
- +-----------------+------------------+-----------------+
- | data type (u64) | num fields (u64) | total len (u64) | # header
- +--------------+--+---------+--------+---------+-------+
- | field1 (u64) | len1 (u64) | value1 (u8 len1) |
- +--------------+------------+------------------+
- | ... | ... | ... | # data
- +--------------+------------+------------------+
- | fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
- +--------------+------------+------------------+
- [same as above, repeated N times]
- */
+/**
- struct tlv_hdr - Header of TLV format
- @data_type: Type of data to parse
- @num_entries: Number of data entries provided
- @_reserved: Reserved for future use (must be equal to zero)
- @total_len: Total length of the data blob, excluding the header
- This structure represents the header of the TLV data format.
- */
+struct tlv_hdr {
- __u64 data_type;
- __u64 num_entries;
- __u64 _reserved;
- __u64 total_len;
+} __attribute__((packed));
+/**
- struct tlv_data_entry - Data entry of TLV format
- @field: Data field identifier
- @length: Data length
- @data: Data
- This structure represents a TLV data entry.
- */
+struct tlv_data_entry {
- __u64 field;
- __u64 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 322bb127b4dc..c6c3614c4293 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -392,6 +392,8 @@ 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 # FORTIFY_SOURCE compile-time behavior tests TEST_FORTIFY_SRCS = $(wildcard $(src)/test_fortify/*-*.c) diff --git a/lib/tlv_parser.c b/lib/tlv_parser.c new file mode 100644 index 000000000000..5d54844ab8d7 --- /dev/null +++ b/lib/tlv_parser.c @@ -0,0 +1,221 @@ +// 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_hdr - Parse TLV header
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data: Data to parse (updated)
- @data_len: Length of @data (updated)
- @parsed_num_entries: Parsed number of data entries (updated)
- @parsed_total_len: Parsed length of TLV data, excluding the header (updated)
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- Parse the header of the TLV data format, move the data pointer to the TLV
- data part, decrease the data length by the length of the header, and provide
- the number of entries and the total data length extracted from the header.
- Before returning, call the header callback to let the callback supplier
- decide whether or not to process the subsequent TLV data.
- Return: 1 to process the data entries, 0 to skip, a negative value on error.
- */
+static int tlv_parse_hdr(hdr_callback hdr_callback, void *hdr_callback_data,
const __u8 **data, size_t *data_len,
__u64 *parsed_num_entries, __u64 *parsed_total_len,
const char **data_types, __u64 num_data_types)
+{
- __u64 parsed_data_type;
- struct tlv_hdr *hdr;
- if (*data_len < sizeof(*hdr)) {
pr_debug("Data blob too short, %lu bytes, expected %lu\n",
*data_len, sizeof(*hdr));
return -EBADMSG;
- }
- hdr = (struct tlv_hdr *)*data;
- *data += sizeof(*hdr);
- *data_len -= sizeof(*hdr);
- parsed_data_type = __be64_to_cpu(hdr->data_type);
- if (parsed_data_type >= num_data_types) {
pr_debug("Invalid data type %llu, max: %llu\n",
parsed_data_type, num_data_types - 1);
return -EBADMSG;
- }
- *parsed_num_entries = __be64_to_cpu(hdr->num_entries);
- if (hdr->_reserved != 0) {
pr_debug("_reserved must be zero\n");
return -EBADMSG;
- }
- *parsed_total_len = __be64_to_cpu(hdr->total_len);
- if (*parsed_total_len > *data_len) {
pr_debug("Invalid total length %llu, expected: %lu\n",
*parsed_total_len, *data_len);
return -EBADMSG;
- }
- pr_debug("Header: type: %s, num entries: %llu, total len: %lld\n",
data_types[parsed_data_type], *parsed_num_entries,
*parsed_total_len);
- return hdr_callback(hdr_callback_data, parsed_data_type,
*parsed_num_entries, *parsed_total_len);
+}
+/**
- tlv_parse_data - Parse TLV data
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @num_entries: Number of data entries to parse
- @data: Data to parse
- @data_len: Length of @data
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse the data part of the TLV data format and call the supplied callback
- function for each data entry, passing also the opaque data pointer.
- The data callback function decides how to process data depending on the
- field.
- Return: 0 on success, a negative value on error.
- */
+static int tlv_parse_data(data_callback data_callback, void *data_callback_data,
__u64 num_entries, const __u8 *data, size_t data_len,
const char **fields, __u64 num_fields)
+{
- const __u8 *data_ptr = data;
- struct tlv_data_entry *entry;
- __u64 parsed_field, len, i, max_num_entries;
- int ret;
- max_num_entries = data_len / sizeof(*entry);
- /* Possibly lower limit on num_entries loop. */
- if (num_entries > max_num_entries)
return -EBADMSG;
- for (i = 0; i < num_entries; i++) {
if (data_len < sizeof(*entry))
return -EBADMSG;
entry = (struct tlv_data_entry *)data_ptr;
data_ptr += sizeof(*entry);
data_len -= sizeof(*entry);
parsed_field = __be64_to_cpu(entry->field);
if (parsed_field >= num_fields) {
pr_debug("Invalid field %llu, max: %llu\n",
parsed_field, num_fields - 1);
return -EBADMSG;
}
len = __be64_to_cpu(entry->length);
if (data_len < len)
return -EBADMSG;
pr_debug("Data: field: %s, len: %llu\n", fields[parsed_field],
len);
if (!len)
continue;
ret = data_callback(data_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: %lu bytes\n", data_len);
return -EBADMSG;
- }
- return 0;
+}
+/**
- tlv_parse - Parse data in TLV format
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @data: Data to parse
- @data_len: Length of @data
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse data in TLV format and call tlv_parse_data() each time tlv_parse_hdr()
- returns 1.
- Return: 0 on success, a negative value on error.
- */
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 num_fields)
+{
- __u64 parsed_num_entries, parsed_total_len;
- const __u8 *data_ptr = data;
- int ret = 0;
- pr_debug("Start parsing data blob, size: %lu\n", data_len);
- while (data_len) {
ret = tlv_parse_hdr(hdr_callback, hdr_callback_data, &data_ptr,
&data_len, &parsed_num_entries,
&parsed_total_len, data_types,
num_data_types);
switch (ret) {
case 0:
/*
* tlv_parse_hdr() already checked that
* parsed_total_len <= data_len.
*/
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
continue;
case 1:
break;
default:
goto out;
}
ret = tlv_parse_data(data_callback, data_callback_data,
parsed_num_entries, data_ptr,
parsed_total_len, fields, num_fields);
if (ret < 0)
goto out;
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
- }
+out:
- pr_debug("End of parsing data blob, ret: %d\n", ret);
- return ret;
+} diff --git a/lib/tlv_parser.h b/lib/tlv_parser.h new file mode 100644 index 000000000000..8fa8127bd13e --- /dev/null +++ b/lib/tlv_parser.h @@ -0,0 +1,17 @@ +/* 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/tlv_parser.h>
+#endif /* _LIB_TLV_PARSER_H */
On Fri, 06 Sep 2024, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Thu, 2024-09-05 at 20:30 +0300, Jani Nikula wrote:
On Thu, 05 Sep 2024, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Add a parser of a generic Type-Length-Value (TLV) format:
+-----------------+------------------+-----------------+
data type (u64) | num fields (u64) | total len (u64) | # header
+--------------+--+---------+--------+---------+-------+
field1 (u64) | len1 (u64) | value1 (u8 len1) |
+--------------+------------+------------------+
... | ... | ... | # data
+--------------+------------+------------------+
fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
+--------------+------------+------------------+
Okay, take this with a grain of salt. I'm actually not interested in your use case, but the generic part here. But hear me out.
Why do you need to have num fields in the header? I'd think the generic TLV would have tag/length/value, where value may contain more TLV, or not, depending on the use case specific tag. The same parser can parse everything recursively, with no special handling for headers. To me, that's the great part about TLV.
Hi Jani
the purpose of the number of entries is that the kernel uses it to correctly size the hash table, before adding digests to it.
It also allows to group digests together and define attributes for all of them (e.g. the hash algorithm).
You can also have multiple data blocks in the same file, and the header allows to go to the ones of interest.
Or you could have a TLV entry to define how many fields follow. With no dedicated header entries. You can choose what tags mean.
Oh, another angle is that you could have e.g. the lowest bit in the tag indicate whether the value is TLV too. So you can validate the TLV all the way down to the non-TLV values.
Also, making generic TLV have u64 tag and length is huge waste in most use cases. Saving one byte requires 16 bytes of tag and length. You could encode tag and length with UTF-8. Sure, it's wasteful if you need an enormous amount of tags or huge lengths, but it's efficient for most use cases.
You are right, it is a huge size. You can introduce new fields, but not modify the tag and length size for retrocompatibility.
Ok, I see your point for UTF-8. Let's see, I like how simple is the parser now.
If you like simplicity, you should not have a different header entry. ;)
See utf8encode() and utf8decode() in fs/unicode/mkutf8data.c for an implementation. Of course, this severely limits the max length from u64, to put it mildly, max being 0x10ffff. Maybe that's a limitation you don't want. But fixing at u64 seems bad too.
BR, Jani.
Thanks
Roberto
Anyway, just my thoughts.
BR, Jani.
[same as above, repeated N times]
Each adopter can define its own data types and 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 parsing each TLV header, call the header callback function with the callback data provided by the adopter. The latter can return 0, to skip processing of the TLV data, 1 to process the TLV data, or a negative value to stop processing the TLV data.
After processing a TLV data entry, call the data callback function also with the callback data provided by the adopter. The latter can decide how to interpret the TLV data entry depending on the field ID.
Nesting TLVs is also possible, the data 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 | 48 +++++++ include/uapi/linux/tlv_parser.h | 62 +++++++++ lib/Kconfig | 3 + lib/Makefile | 2 + lib/tlv_parser.c | 221 ++++++++++++++++++++++++++++++++ lib/tlv_parser.h | 17 +++ 7 files changed, 361 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 8766f3e5e87e..ba8d5c137bef 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23055,6 +23055,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..6d9a655d9ec9 --- /dev/null +++ b/include/linux/tlv_parser.h @@ -0,0 +1,48 @@ +/* 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 hdr_callback - Callback after parsing TLV header
- @callback_data: Opaque data to supply to the header callback function
- @data_type: TLV data type
- @num_entries: Number of TLV data entries
- @total_len: Total length of TLV data
- This callback is invoked after a TLV header is parsed.
- Return: 0 to skip processing, 1 to do processing, a negative value on error.
- */
+typedef int (*hdr_callback)(void *callback_data, __u64 data_type,
__u64 num_entries, __u64 total_len);
+/**
- typedef data_callback - Callback after parsing TLV data entry
- @callback_data: Opaque data to supply to the data callback function
- @field: TLV field ID
- @field_data: Data of a TLV data field
- @field_len: Length of @field_data
- This callback is invoked after a TLV data entry is parsed.
- Return: 0 on success, a negative value on error.
- */
+typedef int (*data_callback)(void *callback_data, __u64 field,
const __u8 *field_data, __u64 field_len);
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 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..fbd4fc403ac7 --- /dev/null +++ b/include/uapi/linux/tlv_parser.h @@ -0,0 +1,62 @@ +/* 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:
- +-----------------+------------------+-----------------+
- | data type (u64) | num fields (u64) | total len (u64) | # header
- +--------------+--+---------+--------+---------+-------+
- | field1 (u64) | len1 (u64) | value1 (u8 len1) |
- +--------------+------------+------------------+
- | ... | ... | ... | # data
- +--------------+------------+------------------+
- | fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
- +--------------+------------+------------------+
- [same as above, repeated N times]
- */
+/**
- struct tlv_hdr - Header of TLV format
- @data_type: Type of data to parse
- @num_entries: Number of data entries provided
- @_reserved: Reserved for future use (must be equal to zero)
- @total_len: Total length of the data blob, excluding the header
- This structure represents the header of the TLV data format.
- */
+struct tlv_hdr {
- __u64 data_type;
- __u64 num_entries;
- __u64 _reserved;
- __u64 total_len;
+} __attribute__((packed));
+/**
- struct tlv_data_entry - Data entry of TLV format
- @field: Data field identifier
- @length: Data length
- @data: Data
- This structure represents a TLV data entry.
- */
+struct tlv_data_entry {
- __u64 field;
- __u64 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 322bb127b4dc..c6c3614c4293 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -392,6 +392,8 @@ 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 # FORTIFY_SOURCE compile-time behavior tests TEST_FORTIFY_SRCS = $(wildcard $(src)/test_fortify/*-*.c) diff --git a/lib/tlv_parser.c b/lib/tlv_parser.c new file mode 100644 index 000000000000..5d54844ab8d7 --- /dev/null +++ b/lib/tlv_parser.c @@ -0,0 +1,221 @@ +// 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_hdr - Parse TLV header
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data: Data to parse (updated)
- @data_len: Length of @data (updated)
- @parsed_num_entries: Parsed number of data entries (updated)
- @parsed_total_len: Parsed length of TLV data, excluding the header (updated)
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- Parse the header of the TLV data format, move the data pointer to the TLV
- data part, decrease the data length by the length of the header, and provide
- the number of entries and the total data length extracted from the header.
- Before returning, call the header callback to let the callback supplier
- decide whether or not to process the subsequent TLV data.
- Return: 1 to process the data entries, 0 to skip, a negative value on error.
- */
+static int tlv_parse_hdr(hdr_callback hdr_callback, void *hdr_callback_data,
const __u8 **data, size_t *data_len,
__u64 *parsed_num_entries, __u64 *parsed_total_len,
const char **data_types, __u64 num_data_types)
+{
- __u64 parsed_data_type;
- struct tlv_hdr *hdr;
- if (*data_len < sizeof(*hdr)) {
pr_debug("Data blob too short, %lu bytes, expected %lu\n",
*data_len, sizeof(*hdr));
return -EBADMSG;
- }
- hdr = (struct tlv_hdr *)*data;
- *data += sizeof(*hdr);
- *data_len -= sizeof(*hdr);
- parsed_data_type = __be64_to_cpu(hdr->data_type);
- if (parsed_data_type >= num_data_types) {
pr_debug("Invalid data type %llu, max: %llu\n",
parsed_data_type, num_data_types - 1);
return -EBADMSG;
- }
- *parsed_num_entries = __be64_to_cpu(hdr->num_entries);
- if (hdr->_reserved != 0) {
pr_debug("_reserved must be zero\n");
return -EBADMSG;
- }
- *parsed_total_len = __be64_to_cpu(hdr->total_len);
- if (*parsed_total_len > *data_len) {
pr_debug("Invalid total length %llu, expected: %lu\n",
*parsed_total_len, *data_len);
return -EBADMSG;
- }
- pr_debug("Header: type: %s, num entries: %llu, total len: %lld\n",
data_types[parsed_data_type], *parsed_num_entries,
*parsed_total_len);
- return hdr_callback(hdr_callback_data, parsed_data_type,
*parsed_num_entries, *parsed_total_len);
+}
+/**
- tlv_parse_data - Parse TLV data
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @num_entries: Number of data entries to parse
- @data: Data to parse
- @data_len: Length of @data
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse the data part of the TLV data format and call the supplied callback
- function for each data entry, passing also the opaque data pointer.
- The data callback function decides how to process data depending on the
- field.
- Return: 0 on success, a negative value on error.
- */
+static int tlv_parse_data(data_callback data_callback, void *data_callback_data,
__u64 num_entries, const __u8 *data, size_t data_len,
const char **fields, __u64 num_fields)
+{
- const __u8 *data_ptr = data;
- struct tlv_data_entry *entry;
- __u64 parsed_field, len, i, max_num_entries;
- int ret;
- max_num_entries = data_len / sizeof(*entry);
- /* Possibly lower limit on num_entries loop. */
- if (num_entries > max_num_entries)
return -EBADMSG;
- for (i = 0; i < num_entries; i++) {
if (data_len < sizeof(*entry))
return -EBADMSG;
entry = (struct tlv_data_entry *)data_ptr;
data_ptr += sizeof(*entry);
data_len -= sizeof(*entry);
parsed_field = __be64_to_cpu(entry->field);
if (parsed_field >= num_fields) {
pr_debug("Invalid field %llu, max: %llu\n",
parsed_field, num_fields - 1);
return -EBADMSG;
}
len = __be64_to_cpu(entry->length);
if (data_len < len)
return -EBADMSG;
pr_debug("Data: field: %s, len: %llu\n", fields[parsed_field],
len);
if (!len)
continue;
ret = data_callback(data_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: %lu bytes\n", data_len);
return -EBADMSG;
- }
- return 0;
+}
+/**
- tlv_parse - Parse data in TLV format
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @data: Data to parse
- @data_len: Length of @data
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse data in TLV format and call tlv_parse_data() each time tlv_parse_hdr()
- returns 1.
- Return: 0 on success, a negative value on error.
- */
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 num_fields)
+{
- __u64 parsed_num_entries, parsed_total_len;
- const __u8 *data_ptr = data;
- int ret = 0;
- pr_debug("Start parsing data blob, size: %lu\n", data_len);
- while (data_len) {
ret = tlv_parse_hdr(hdr_callback, hdr_callback_data, &data_ptr,
&data_len, &parsed_num_entries,
&parsed_total_len, data_types,
num_data_types);
switch (ret) {
case 0:
/*
* tlv_parse_hdr() already checked that
* parsed_total_len <= data_len.
*/
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
continue;
case 1:
break;
default:
goto out;
}
ret = tlv_parse_data(data_callback, data_callback_data,
parsed_num_entries, data_ptr,
parsed_total_len, fields, num_fields);
if (ret < 0)
goto out;
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
- }
+out:
- pr_debug("End of parsing data blob, ret: %d\n", ret);
- return ret;
+} diff --git a/lib/tlv_parser.h b/lib/tlv_parser.h new file mode 100644 index 000000000000..8fa8127bd13e --- /dev/null +++ b/lib/tlv_parser.h @@ -0,0 +1,17 @@ +/* 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/tlv_parser.h>
+#endif /* _LIB_TLV_PARSER_H */
On Fri, 2024-09-06 at 11:06 +0300, Jani Nikula wrote:
On Fri, 06 Sep 2024, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Thu, 2024-09-05 at 20:30 +0300, Jani Nikula wrote:
On Thu, 05 Sep 2024, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Add a parser of a generic Type-Length-Value (TLV) format:
+-----------------+------------------+-----------------+
data type (u64) | num fields (u64) | total len (u64) | # header
+--------------+--+---------+--------+---------+-------+
field1 (u64) | len1 (u64) | value1 (u8 len1) |
+--------------+------------+------------------+
... | ... | ... | # data
+--------------+------------+------------------+
fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
+--------------+------------+------------------+
Okay, take this with a grain of salt. I'm actually not interested in your use case, but the generic part here. But hear me out.
Why do you need to have num fields in the header? I'd think the generic TLV would have tag/length/value, where value may contain more TLV, or not, depending on the use case specific tag. The same parser can parse everything recursively, with no special handling for headers. To me, that's the great part about TLV.
Hi Jani
the purpose of the number of entries is that the kernel uses it to correctly size the hash table, before adding digests to it.
It also allows to group digests together and define attributes for all of them (e.g. the hash algorithm).
You can also have multiple data blocks in the same file, and the header allows to go to the ones of interest.
Or you could have a TLV entry to define how many fields follow. With no dedicated header entries. You can choose what tags mean.
Ok, I guess it is possible to do this way too.
Oh, another angle is that you could have e.g. the lowest bit in the tag indicate whether the value is TLV too. So you can validate the TLV all the way down to the non-TLV values.
Not sure I follow, the parser associated to a TLV type knows if it is a nested TLV. Look at parse_digest_list_entry() in patch 7/14. The field parser is calling tlv_parse() inside.
The parsers are user-defined and invoked through callbacks, the TLV parser itself remains the same regardless of how data are structured.
Also, making generic TLV have u64 tag and length is huge waste in most use cases. Saving one byte requires 16 bytes of tag and length. You could encode tag and length with UTF-8. Sure, it's wasteful if you need an enormous amount of tags or huge lengths, but it's efficient for most use cases.
You are right, it is a huge size. You can introduce new fields, but not modify the tag and length size for retrocompatibility.
Ok, I see your point for UTF-8. Let's see, I like how simple is the parser now.
If you like simplicity, you should not have a different header entry. ;)
Good point.
See utf8encode() and utf8decode() in fs/unicode/mkutf8data.c for an implementation. Of course, this severely limits the max length from u64, to put it mildly, max being 0x10ffff. Maybe that's a limitation you don't want. But fixing at u64 seems bad too.
I can reduce to u32, I doubt that more will be necessary.
Thanks
Roberto
BR, Jani.
Thanks
Roberto
Anyway, just my thoughts.
BR, Jani.
[same as above, repeated N times]
Each adopter can define its own data types and 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 parsing each TLV header, call the header callback function with the callback data provided by the adopter. The latter can return 0, to skip processing of the TLV data, 1 to process the TLV data, or a negative value to stop processing the TLV data.
After processing a TLV data entry, call the data callback function also with the callback data provided by the adopter. The latter can decide how to interpret the TLV data entry depending on the field ID.
Nesting TLVs is also possible, the data 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 | 48 +++++++ include/uapi/linux/tlv_parser.h | 62 +++++++++ lib/Kconfig | 3 + lib/Makefile | 2 + lib/tlv_parser.c | 221 ++++++++++++++++++++++++++++++++ lib/tlv_parser.h | 17 +++ 7 files changed, 361 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 8766f3e5e87e..ba8d5c137bef 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23055,6 +23055,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..6d9a655d9ec9 --- /dev/null +++ b/include/linux/tlv_parser.h @@ -0,0 +1,48 @@ +/* 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 hdr_callback - Callback after parsing TLV header
- @callback_data: Opaque data to supply to the header callback function
- @data_type: TLV data type
- @num_entries: Number of TLV data entries
- @total_len: Total length of TLV data
- This callback is invoked after a TLV header is parsed.
- Return: 0 to skip processing, 1 to do processing, a negative value on error.
- */
+typedef int (*hdr_callback)(void *callback_data, __u64 data_type,
__u64 num_entries, __u64 total_len);
+/**
- typedef data_callback - Callback after parsing TLV data entry
- @callback_data: Opaque data to supply to the data callback function
- @field: TLV field ID
- @field_data: Data of a TLV data field
- @field_len: Length of @field_data
- This callback is invoked after a TLV data entry is parsed.
- Return: 0 on success, a negative value on error.
- */
+typedef int (*data_callback)(void *callback_data, __u64 field,
const __u8 *field_data, __u64 field_len);
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 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..fbd4fc403ac7 --- /dev/null +++ b/include/uapi/linux/tlv_parser.h @@ -0,0 +1,62 @@ +/* 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:
- +-----------------+------------------+-----------------+
- | data type (u64) | num fields (u64) | total len (u64) | # header
- +--------------+--+---------+--------+---------+-------+
- | field1 (u64) | len1 (u64) | value1 (u8 len1) |
- +--------------+------------+------------------+
- | ... | ... | ... | # data
- +--------------+------------+------------------+
- | fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
- +--------------+------------+------------------+
- [same as above, repeated N times]
- */
+/**
- struct tlv_hdr - Header of TLV format
- @data_type: Type of data to parse
- @num_entries: Number of data entries provided
- @_reserved: Reserved for future use (must be equal to zero)
- @total_len: Total length of the data blob, excluding the header
- This structure represents the header of the TLV data format.
- */
+struct tlv_hdr {
- __u64 data_type;
- __u64 num_entries;
- __u64 _reserved;
- __u64 total_len;
+} __attribute__((packed));
+/**
- struct tlv_data_entry - Data entry of TLV format
- @field: Data field identifier
- @length: Data length
- @data: Data
- This structure represents a TLV data entry.
- */
+struct tlv_data_entry {
- __u64 field;
- __u64 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 322bb127b4dc..c6c3614c4293 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -392,6 +392,8 @@ 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 # FORTIFY_SOURCE compile-time behavior tests TEST_FORTIFY_SRCS = $(wildcard $(src)/test_fortify/*-*.c) diff --git a/lib/tlv_parser.c b/lib/tlv_parser.c new file mode 100644 index 000000000000..5d54844ab8d7 --- /dev/null +++ b/lib/tlv_parser.c @@ -0,0 +1,221 @@ +// 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_hdr - Parse TLV header
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data: Data to parse (updated)
- @data_len: Length of @data (updated)
- @parsed_num_entries: Parsed number of data entries (updated)
- @parsed_total_len: Parsed length of TLV data, excluding the header (updated)
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- Parse the header of the TLV data format, move the data pointer to the TLV
- data part, decrease the data length by the length of the header, and provide
- the number of entries and the total data length extracted from the header.
- Before returning, call the header callback to let the callback supplier
- decide whether or not to process the subsequent TLV data.
- Return: 1 to process the data entries, 0 to skip, a negative value on error.
- */
+static int tlv_parse_hdr(hdr_callback hdr_callback, void *hdr_callback_data,
const __u8 **data, size_t *data_len,
__u64 *parsed_num_entries, __u64 *parsed_total_len,
const char **data_types, __u64 num_data_types)
+{
- __u64 parsed_data_type;
- struct tlv_hdr *hdr;
- if (*data_len < sizeof(*hdr)) {
pr_debug("Data blob too short, %lu bytes, expected %lu\n",
*data_len, sizeof(*hdr));
return -EBADMSG;
- }
- hdr = (struct tlv_hdr *)*data;
- *data += sizeof(*hdr);
- *data_len -= sizeof(*hdr);
- parsed_data_type = __be64_to_cpu(hdr->data_type);
- if (parsed_data_type >= num_data_types) {
pr_debug("Invalid data type %llu, max: %llu\n",
parsed_data_type, num_data_types - 1);
return -EBADMSG;
- }
- *parsed_num_entries = __be64_to_cpu(hdr->num_entries);
- if (hdr->_reserved != 0) {
pr_debug("_reserved must be zero\n");
return -EBADMSG;
- }
- *parsed_total_len = __be64_to_cpu(hdr->total_len);
- if (*parsed_total_len > *data_len) {
pr_debug("Invalid total length %llu, expected: %lu\n",
*parsed_total_len, *data_len);
return -EBADMSG;
- }
- pr_debug("Header: type: %s, num entries: %llu, total len: %lld\n",
data_types[parsed_data_type], *parsed_num_entries,
*parsed_total_len);
- return hdr_callback(hdr_callback_data, parsed_data_type,
*parsed_num_entries, *parsed_total_len);
+}
+/**
- tlv_parse_data - Parse TLV data
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @num_entries: Number of data entries to parse
- @data: Data to parse
- @data_len: Length of @data
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse the data part of the TLV data format and call the supplied callback
- function for each data entry, passing also the opaque data pointer.
- The data callback function decides how to process data depending on the
- field.
- Return: 0 on success, a negative value on error.
- */
+static int tlv_parse_data(data_callback data_callback, void *data_callback_data,
__u64 num_entries, const __u8 *data, size_t data_len,
const char **fields, __u64 num_fields)
+{
- const __u8 *data_ptr = data;
- struct tlv_data_entry *entry;
- __u64 parsed_field, len, i, max_num_entries;
- int ret;
- max_num_entries = data_len / sizeof(*entry);
- /* Possibly lower limit on num_entries loop. */
- if (num_entries > max_num_entries)
return -EBADMSG;
- for (i = 0; i < num_entries; i++) {
if (data_len < sizeof(*entry))
return -EBADMSG;
entry = (struct tlv_data_entry *)data_ptr;
data_ptr += sizeof(*entry);
data_len -= sizeof(*entry);
parsed_field = __be64_to_cpu(entry->field);
if (parsed_field >= num_fields) {
pr_debug("Invalid field %llu, max: %llu\n",
parsed_field, num_fields - 1);
return -EBADMSG;
}
len = __be64_to_cpu(entry->length);
if (data_len < len)
return -EBADMSG;
pr_debug("Data: field: %s, len: %llu\n", fields[parsed_field],
len);
if (!len)
continue;
ret = data_callback(data_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: %lu bytes\n", data_len);
return -EBADMSG;
- }
- return 0;
+}
+/**
- tlv_parse - Parse data in TLV format
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @data: Data to parse
- @data_len: Length of @data
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse data in TLV format and call tlv_parse_data() each time tlv_parse_hdr()
- returns 1.
- Return: 0 on success, a negative value on error.
- */
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 num_fields)
+{
- __u64 parsed_num_entries, parsed_total_len;
- const __u8 *data_ptr = data;
- int ret = 0;
- pr_debug("Start parsing data blob, size: %lu\n", data_len);
- while (data_len) {
ret = tlv_parse_hdr(hdr_callback, hdr_callback_data, &data_ptr,
&data_len, &parsed_num_entries,
&parsed_total_len, data_types,
num_data_types);
switch (ret) {
case 0:
/*
* tlv_parse_hdr() already checked that
* parsed_total_len <= data_len.
*/
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
continue;
case 1:
break;
default:
goto out;
}
ret = tlv_parse_data(data_callback, data_callback_data,
parsed_num_entries, data_ptr,
parsed_total_len, fields, num_fields);
if (ret < 0)
goto out;
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
- }
+out:
- pr_debug("End of parsing data blob, ret: %d\n", ret);
- return ret;
+} diff --git a/lib/tlv_parser.h b/lib/tlv_parser.h new file mode 100644 index 000000000000..8fa8127bd13e --- /dev/null +++ b/lib/tlv_parser.h @@ -0,0 +1,17 @@ +/* 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/tlv_parser.h>
+#endif /* _LIB_TLV_PARSER_H */
On Fri, 06 Sep 2024, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Fri, 2024-09-06 at 11:06 +0300, Jani Nikula wrote:
On Fri, 06 Sep 2024, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
On Thu, 2024-09-05 at 20:30 +0300, Jani Nikula wrote:
On Thu, 05 Sep 2024, Roberto Sassu roberto.sassu@huaweicloud.com wrote:
From: Roberto Sassu roberto.sassu@huawei.com
Add a parser of a generic Type-Length-Value (TLV) format:
+-----------------+------------------+-----------------+
data type (u64) | num fields (u64) | total len (u64) | # header
+--------------+--+---------+--------+---------+-------+
field1 (u64) | len1 (u64) | value1 (u8 len1) |
+--------------+------------+------------------+
... | ... | ... | # data
+--------------+------------+------------------+
fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
+--------------+------------+------------------+
Okay, take this with a grain of salt. I'm actually not interested in your use case, but the generic part here. But hear me out.
Why do you need to have num fields in the header? I'd think the generic TLV would have tag/length/value, where value may contain more TLV, or not, depending on the use case specific tag. The same parser can parse everything recursively, with no special handling for headers. To me, that's the great part about TLV.
Hi Jani
the purpose of the number of entries is that the kernel uses it to correctly size the hash table, before adding digests to it.
It also allows to group digests together and define attributes for all of them (e.g. the hash algorithm).
You can also have multiple data blocks in the same file, and the header allows to go to the ones of interest.
Or you could have a TLV entry to define how many fields follow. With no dedicated header entries. You can choose what tags mean.
Ok, I guess it is possible to do this way too.
Oh, another angle is that you could have e.g. the lowest bit in the tag indicate whether the value is TLV too. So you can validate the TLV all the way down to the non-TLV values.
Not sure I follow, the parser associated to a TLV type knows if it is a nested TLV. Look at parse_digest_list_entry() in patch 7/14. The field parser is calling tlv_parse() inside.
Ah, now you're talking about the specific use case, and I'm talking about the generic TLV parser. A generic parser does not know how the value is encoded. But you could encode tags in a way that told the parser whether the value contains TLV or just a blob, and the parser could traverse the hierarchy of TLV without tag specific parsers, and tell you if it's well formed or not (nested TLV sizes matching parent, etc.). Or you could recursively find tags, without callbacks. Or dump the structure, without callbacks.
The parsers are user-defined and invoked through callbacks, the TLV parser itself remains the same regardless of how data are structured.
Also, making generic TLV have u64 tag and length is huge waste in most use cases. Saving one byte requires 16 bytes of tag and length. You could encode tag and length with UTF-8. Sure, it's wasteful if you need an enormous amount of tags or huge lengths, but it's efficient for most use cases.
You are right, it is a huge size. You can introduce new fields, but not modify the tag and length size for retrocompatibility.
Ok, I see your point for UTF-8. Let's see, I like how simple is the parser now.
If you like simplicity, you should not have a different header entry. ;)
Good point.
See utf8encode() and utf8decode() in fs/unicode/mkutf8data.c for an implementation. Of course, this severely limits the max length from u64, to put it mildly, max being 0x10ffff. Maybe that's a limitation you don't want. But fixing at u64 seems bad too.
I can reduce to u32, I doubt that more will be necessary.
Up to you, really.
BR, Jani.
Thanks
Roberto
BR, Jani.
Thanks
Roberto
Anyway, just my thoughts.
BR, Jani.
[same as above, repeated N times]
Each adopter can define its own data types and 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 parsing each TLV header, call the header callback function with the callback data provided by the adopter. The latter can return 0, to skip processing of the TLV data, 1 to process the TLV data, or a negative value to stop processing the TLV data.
After processing a TLV data entry, call the data callback function also with the callback data provided by the adopter. The latter can decide how to interpret the TLV data entry depending on the field ID.
Nesting TLVs is also possible, the data 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 | 48 +++++++ include/uapi/linux/tlv_parser.h | 62 +++++++++ lib/Kconfig | 3 + lib/Makefile | 2 + lib/tlv_parser.c | 221 ++++++++++++++++++++++++++++++++ lib/tlv_parser.h | 17 +++ 7 files changed, 361 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 8766f3e5e87e..ba8d5c137bef 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23055,6 +23055,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..6d9a655d9ec9 --- /dev/null +++ b/include/linux/tlv_parser.h @@ -0,0 +1,48 @@ +/* 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 hdr_callback - Callback after parsing TLV header
- @callback_data: Opaque data to supply to the header callback function
- @data_type: TLV data type
- @num_entries: Number of TLV data entries
- @total_len: Total length of TLV data
- This callback is invoked after a TLV header is parsed.
- Return: 0 to skip processing, 1 to do processing, a negative value on error.
- */
+typedef int (*hdr_callback)(void *callback_data, __u64 data_type,
__u64 num_entries, __u64 total_len);
+/**
- typedef data_callback - Callback after parsing TLV data entry
- @callback_data: Opaque data to supply to the data callback function
- @field: TLV field ID
- @field_data: Data of a TLV data field
- @field_len: Length of @field_data
- This callback is invoked after a TLV data entry is parsed.
- Return: 0 on success, a negative value on error.
- */
+typedef int (*data_callback)(void *callback_data, __u64 field,
const __u8 *field_data, __u64 field_len);
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 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..fbd4fc403ac7 --- /dev/null +++ b/include/uapi/linux/tlv_parser.h @@ -0,0 +1,62 @@ +/* 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:
- +-----------------+------------------+-----------------+
- | data type (u64) | num fields (u64) | total len (u64) | # header
- +--------------+--+---------+--------+---------+-------+
- | field1 (u64) | len1 (u64) | value1 (u8 len1) |
- +--------------+------------+------------------+
- | ... | ... | ... | # data
- +--------------+------------+------------------+
- | fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
- +--------------+------------+------------------+
- [same as above, repeated N times]
- */
+/**
- struct tlv_hdr - Header of TLV format
- @data_type: Type of data to parse
- @num_entries: Number of data entries provided
- @_reserved: Reserved for future use (must be equal to zero)
- @total_len: Total length of the data blob, excluding the header
- This structure represents the header of the TLV data format.
- */
+struct tlv_hdr {
- __u64 data_type;
- __u64 num_entries;
- __u64 _reserved;
- __u64 total_len;
+} __attribute__((packed));
+/**
- struct tlv_data_entry - Data entry of TLV format
- @field: Data field identifier
- @length: Data length
- @data: Data
- This structure represents a TLV data entry.
- */
+struct tlv_data_entry {
- __u64 field;
- __u64 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 322bb127b4dc..c6c3614c4293 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -392,6 +392,8 @@ 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 # FORTIFY_SOURCE compile-time behavior tests TEST_FORTIFY_SRCS = $(wildcard $(src)/test_fortify/*-*.c) diff --git a/lib/tlv_parser.c b/lib/tlv_parser.c new file mode 100644 index 000000000000..5d54844ab8d7 --- /dev/null +++ b/lib/tlv_parser.c @@ -0,0 +1,221 @@ +// 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_hdr - Parse TLV header
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data: Data to parse (updated)
- @data_len: Length of @data (updated)
- @parsed_num_entries: Parsed number of data entries (updated)
- @parsed_total_len: Parsed length of TLV data, excluding the header (updated)
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- Parse the header of the TLV data format, move the data pointer to the TLV
- data part, decrease the data length by the length of the header, and provide
- the number of entries and the total data length extracted from the header.
- Before returning, call the header callback to let the callback supplier
- decide whether or not to process the subsequent TLV data.
- Return: 1 to process the data entries, 0 to skip, a negative value on error.
- */
+static int tlv_parse_hdr(hdr_callback hdr_callback, void *hdr_callback_data,
const __u8 **data, size_t *data_len,
__u64 *parsed_num_entries, __u64 *parsed_total_len,
const char **data_types, __u64 num_data_types)
+{
- __u64 parsed_data_type;
- struct tlv_hdr *hdr;
- if (*data_len < sizeof(*hdr)) {
pr_debug("Data blob too short, %lu bytes, expected %lu\n",
*data_len, sizeof(*hdr));
return -EBADMSG;
- }
- hdr = (struct tlv_hdr *)*data;
- *data += sizeof(*hdr);
- *data_len -= sizeof(*hdr);
- parsed_data_type = __be64_to_cpu(hdr->data_type);
- if (parsed_data_type >= num_data_types) {
pr_debug("Invalid data type %llu, max: %llu\n",
parsed_data_type, num_data_types - 1);
return -EBADMSG;
- }
- *parsed_num_entries = __be64_to_cpu(hdr->num_entries);
- if (hdr->_reserved != 0) {
pr_debug("_reserved must be zero\n");
return -EBADMSG;
- }
- *parsed_total_len = __be64_to_cpu(hdr->total_len);
- if (*parsed_total_len > *data_len) {
pr_debug("Invalid total length %llu, expected: %lu\n",
*parsed_total_len, *data_len);
return -EBADMSG;
- }
- pr_debug("Header: type: %s, num entries: %llu, total len: %lld\n",
data_types[parsed_data_type], *parsed_num_entries,
*parsed_total_len);
- return hdr_callback(hdr_callback_data, parsed_data_type,
*parsed_num_entries, *parsed_total_len);
+}
+/**
- tlv_parse_data - Parse TLV data
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @num_entries: Number of data entries to parse
- @data: Data to parse
- @data_len: Length of @data
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse the data part of the TLV data format and call the supplied callback
- function for each data entry, passing also the opaque data pointer.
- The data callback function decides how to process data depending on the
- field.
- Return: 0 on success, a negative value on error.
- */
+static int tlv_parse_data(data_callback data_callback, void *data_callback_data,
__u64 num_entries, const __u8 *data, size_t data_len,
const char **fields, __u64 num_fields)
+{
- const __u8 *data_ptr = data;
- struct tlv_data_entry *entry;
- __u64 parsed_field, len, i, max_num_entries;
- int ret;
- max_num_entries = data_len / sizeof(*entry);
- /* Possibly lower limit on num_entries loop. */
- if (num_entries > max_num_entries)
return -EBADMSG;
- for (i = 0; i < num_entries; i++) {
if (data_len < sizeof(*entry))
return -EBADMSG;
entry = (struct tlv_data_entry *)data_ptr;
data_ptr += sizeof(*entry);
data_len -= sizeof(*entry);
parsed_field = __be64_to_cpu(entry->field);
if (parsed_field >= num_fields) {
pr_debug("Invalid field %llu, max: %llu\n",
parsed_field, num_fields - 1);
return -EBADMSG;
}
len = __be64_to_cpu(entry->length);
if (data_len < len)
return -EBADMSG;
pr_debug("Data: field: %s, len: %llu\n", fields[parsed_field],
len);
if (!len)
continue;
ret = data_callback(data_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: %lu bytes\n", data_len);
return -EBADMSG;
- }
- return 0;
+}
+/**
- tlv_parse - Parse data in TLV format
- @hdr_callback: Callback function to call after parsing header
- @hdr_callback_data: Opaque data to supply to the header callback function
- @data_callback: Callback function to call to parse the data entries
- @data_callback_data: Opaque data to supply to the data callback function
- @data: Data to parse
- @data_len: Length of @data
- @data_types: Array of data type strings
- @num_data_types: Number of elements of @data_types
- @fields: Array of field strings
- @num_fields: Number of elements of @fields
- Parse data in TLV format and call tlv_parse_data() each time tlv_parse_hdr()
- returns 1.
- Return: 0 on success, a negative value on error.
- */
+int tlv_parse(hdr_callback hdr_callback, void *hdr_callback_data,
data_callback data_callback, void *data_callback_data,
const __u8 *data, size_t data_len, const char **data_types,
__u64 num_data_types, const char **fields, __u64 num_fields)
+{
- __u64 parsed_num_entries, parsed_total_len;
- const __u8 *data_ptr = data;
- int ret = 0;
- pr_debug("Start parsing data blob, size: %lu\n", data_len);
- while (data_len) {
ret = tlv_parse_hdr(hdr_callback, hdr_callback_data, &data_ptr,
&data_len, &parsed_num_entries,
&parsed_total_len, data_types,
num_data_types);
switch (ret) {
case 0:
/*
* tlv_parse_hdr() already checked that
* parsed_total_len <= data_len.
*/
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
continue;
case 1:
break;
default:
goto out;
}
ret = tlv_parse_data(data_callback, data_callback_data,
parsed_num_entries, data_ptr,
parsed_total_len, fields, num_fields);
if (ret < 0)
goto out;
data_ptr += parsed_total_len;
data_len -= parsed_total_len;
- }
+out:
- pr_debug("End of parsing data blob, ret: %d\n", ret);
- return ret;
+} diff --git a/lib/tlv_parser.h b/lib/tlv_parser.h new file mode 100644 index 000000000000..8fa8127bd13e --- /dev/null +++ b/lib/tlv_parser.h @@ -0,0 +1,17 @@ +/* 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/tlv_parser.h>
+#endif /* _LIB_TLV_PARSER_H */
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 security blob 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 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 | 30 ++ include/uapi/linux/xattr.h | 3 + security/integrity/Kconfig | 1 + security/integrity/Makefile | 1 + security/integrity/digest_cache/Kconfig | 17 + security/integrity/digest_cache/Makefile | 7 + security/integrity/digest_cache/internal.h | 90 +++++ security/integrity/digest_cache/main.c | 367 +++++++++++++++++++++ security/integrity/ima/ima.h | 1 + security/integrity/ima/ima_main.c | 8 +- 10 files changed, 524 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..86e6c5a0b896 --- /dev/null +++ b/include/linux/digest_cache.h @@ -0,0 +1,30 @@ +/* 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 +struct digest_cache *digest_cache_get(struct dentry *dentry); +void digest_cache_put(struct digest_cache *digest_cache); + +#else +static inline struct digest_cache *digest_cache_get(struct dentry *dentry) +{ + return NULL; +} + +static inline void digest_cache_put(struct digest_cache *digest_cache) +{ +} + +#endif /* CONFIG_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..5fbec491237b --- /dev/null +++ b/security/integrity/digest_cache/Kconfig @@ -0,0 +1,17 @@ +# 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 RPM headers, or from other sources. + +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..e8a13afaf2fc --- /dev/null +++ b/security/integrity/digest_cache/internal.h @@ -0,0 +1,90 @@ +/* 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 char *default_path_str; + +static inline struct digest_cache_security * +digest_cache_get_security(const struct inode *inode) +{ + if (unlikely(!inode->i_security)) + return NULL; + + return inode->i_security + inode_sec_offset; +} + +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; +} + +/* main.c */ +struct digest_cache *digest_cache_create(struct dentry *dentry, + 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); + +#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..60030df04a4d --- /dev/null +++ b/security/integrity/digest_cache/main.c @@ -0,0 +1,367 @@ +// 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; + +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 + * @digest_list_path: Path structure of the digest list + * @path_str: Path string of the digest list + * @filename: Digest list file name (can be an empty string) + * + * This function first locates, from the passed path, the digest list inode + * from which the digest cache will be created or retrieved (if it already + * exists). + * + * If dig_owner is NULL in the inode security blob, this function creates a + * new digest cache with reference count set to 1 (reference returned), sets + * it to dig_owner and consequently increments again the digest cache reference + * count. + * + * Otherwise, it simply increments the reference count of the existing + * dig_owner, since that reference is returned to the caller. + * + * 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 *digest_list_path, + char *path_str, char *filename) +{ + struct path file_path; + struct digest_cache *digest_cache = NULL; + struct digest_cache_security *dig_sec; + struct inode *inode = d_backing_inode(digest_list_path->dentry); + int ret; + + if (S_ISDIR(inode->i_mode) && filename[0]) { + ret = vfs_path_lookup(digest_list_path->dentry, + digest_list_path->mnt, filename, + LOOKUP_FOLLOW, &file_path); + if (ret < 0) { + pr_debug("Cannot find digest list %s/%s\n", path_str, + filename); + return NULL; + } + + digest_list_path = &file_path; + inode = d_backing_inode(file_path.dentry); + + /* 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", + file_path.dentry->d_name.name); + goto out; + } + } + + /* + * 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); + goto out; + } + + 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); + mutex_unlock(&dig_sec->dig_owner_mutex); + 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) { + mutex_unlock(&dig_sec->dig_owner_mutex); + goto out; + } + + /* Increment ref. count for reference returned to the caller. */ + digest_cache = digest_cache_ref(dig_sec->dig_owner); + mutex_unlock(&dig_sec->dig_owner_mutex); +out: + if (digest_list_path == &file_path) + path_put(&file_path); + + return digest_cache; +} + +/** + * digest_cache_new - Retrieve digest list file name and request digest cache + * @dentry: Dentry of the inode for which the digest cache will be used + * + * This function locates the default path. If it is a file, it directly creates + * a digest cache from it. Otherwise, it reads the digest list file name from + * the security.digest_list xattr and requests the creation of a digest cache + * with that file name. If security.digest_list is empty or not found, this + * function requests the creation of a digest cache on the parent directory. + * + * Return: A digest cache on success, NULL on error. + */ +static struct digest_cache *digest_cache_new(struct dentry *dentry) +{ + char filename[NAME_MAX + 1] = { 0 }; + struct digest_cache *digest_cache = NULL; + struct path default_path; + int ret; + + ret = kern_path(default_path_str, 0, &default_path); + if (ret < 0) { + pr_debug("Cannot find path %s\n", default_path_str); + return NULL; + } + + /* The default path is a file, no need to get xattr. */ + if (S_ISREG(d_backing_inode(default_path.dentry)->i_mode)) { + pr_debug("Default path %s is a file, not reading %s xattr\n", + default_path_str, XATTR_NAME_DIGEST_LIST); + goto create; + } else if (!S_ISDIR(d_backing_inode(default_path.dentry)->i_mode)) { + pr_debug("Default path %s must be either a file or a directory\n", + default_path_str); + goto out; + } + + ret = vfs_getxattr(&nop_mnt_idmap, dentry, XATTR_NAME_DIGEST_LIST, + filename, sizeof(filename) - 1); + if (ret <= 0) { + pr_debug("Digest list file name not found for file %s, using %s\n", + dentry->d_name.name, default_path_str); + if (ret && ret != -ENODATA) + goto out; + + goto create; + } + + if (strchr(filename, '/')) { + pr_debug("%s xattr should contain only a file name, got: %s\n", + XATTR_NAME_DIGEST_LIST, filename); + goto out; + } + + pr_debug("Found %s xattr in %s, default path: %s, digest list: %s\n", + XATTR_NAME_DIGEST_LIST, dentry->d_name.name, default_path_str, + filename); +create: + digest_cache = digest_cache_create(dentry, &default_path, + default_path_str, filename); +out: + path_put(&default_path); + return digest_cache; +} + +/** + * digest_cache_get - Get a digest cache for a given inode + * @dentry: Dentry of the inode for which the digest cache will be used + * + * This function tries to find a digest cache from the inode security blob of + * the passed dentry (dig_user field). If a digest cache was not found, it calls + * digest_cache_new() to create a new one. In both cases, it increments the + * digest cache reference count before returning the reference to the caller. + * + * The caller is responsible to call digest_cache_put() to release the digest + * cache reference returned. + * + * 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 dentry *dentry) +{ + struct digest_cache_security *dig_sec; + struct digest_cache *digest_cache = NULL; + struct inode *inode = d_backing_inode(dentry); + + if (!digest_cache_enabled) + 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); + + if (dig_sec->dig_user) + /* Increment ref. count for reference returned to the caller. */ + digest_cache = digest_cache_ref(dig_sec->dig_user); + + mutex_unlock(&dig_sec->dig_user_mutex); + + return digest_cache; +} +EXPORT_SYMBOL_GPL(digest_cache_get); + +/** + * digest_cache_put - Release a digest cache reference + * @digest_cache: Digest cache + * + * This function decrements the reference count of the digest cache passed as + * argument. If the reference count reaches zero, it calls digest_cache_free() + * to free the digest cache. + */ +void digest_cache_put(struct digest_cache *digest_cache) +{ + struct digest_cache *to_free; + + to_free = digest_cache_unref(digest_cache); + if (!to_free) + return; + + digest_cache_free(to_free); +} +EXPORT_SYMBOL_GPL(digest_cache_put); + +/** + * digest_cache_inode_alloc_security - Initialize inode security blob + * @inode: Inode for which the security blob is initialized + * + * This function initializes the digest_cache_security structure, directly + * stored in the inode security blob. + * + * Return: Zero. + */ +static int digest_cache_inode_alloc_security(struct inode *inode) +{ + struct digest_cache_security *dig_sec; + + /* The inode security blob is always allocated here. */ + dig_sec = digest_cache_get_security(inode); + mutex_init(&dig_sec->dig_owner_mutex); + mutex_init(&dig_sec->dig_user_mutex); + return 0; +} + +/** + * digest_cache_inode_free_security - Release the digest cache references + * @inode: Inode for which the digest cache references are released + * + * Since the inode is being evicted, this function releases the non-needed + * references to the digest caches stored in the digest_cache_security + * structure. + */ +static void digest_cache_inode_free_security(struct inode *inode) +{ + struct digest_cache_security *dig_sec; + + dig_sec = digest_cache_get_security(inode); + if (!dig_sec) + return; + + mutex_destroy(&dig_sec->dig_owner_mutex); + mutex_destroy(&dig_sec->dig_user_mutex); + if (dig_sec->dig_owner) + digest_cache_put(dig_sec->dig_owner); + if (dig_sec->dig_user) + digest_cache_put(dig_sec->dig_user); +} + +static struct security_hook_list digest_cache_hooks[] __ro_after_init = { + LSM_HOOK_INIT(inode_alloc_security, digest_cache_inode_alloc_security), + LSM_HOOK_INIT(inode_free_security, digest_cache_inode_free_security), +}; + +/** + * digest_cache_do_init - Initialize the Integrity Digest Cache + * @lsm_id: ID of LSM registering the LSM hooks + * @inode_offset: Offset in the inode 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) +{ + inode_sec_offset = inode_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 c51e24d24d1e..ad5c95cf22ac 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 f04f43af651c..7cbd78ca3be5 100644 --- a/security/integrity/ima/ima_main.c +++ b/security/integrity/ima/ima_main.c @@ -1206,11 +1206,17 @@ 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 *)); 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) +#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.
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.
To avoid that the inode the digest cache is created from is evicted or is different due to a path rename between creation and initialization, take the path at creation time, and use it at initialization time. Since the digest cache holds a path reference, the inode cannot be evicted or change. However, care must be taken to ensure that digest_cache_init() is always executed after digest_cache_create() or, otherwise, the path reference will not be released.
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 | 9 ++++ security/integrity/digest_cache/main.c | 51 ++++++++++++++++++++++ 2 files changed, 60 insertions(+)
diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index e8a13afaf2fc..54e118a2ef79 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -13,11 +13,17 @@ #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 * @path_str: Path of the digest list the digest cache was created from * @flags: Control flags + * @digest_list_path: Path structure of the digest list * * This structure represents a cache of digests extracted from a digest list. */ @@ -25,6 +31,7 @@ struct digest_cache { atomic_t ref_count; char *path_str; unsigned long flags; + struct path digest_list_path; };
/** @@ -84,6 +91,8 @@ digest_cache_unref(struct digest_cache *digest_cache) struct digest_cache *digest_cache_create(struct dentry *dentry, struct path *digest_list_path, char *path_str, char *filename); +struct digest_cache *digest_cache_init(struct dentry *dentry, + struct digest_cache *digest_cache); int __init digest_cache_do_init(const struct lsm_id *lsm_id, loff_t inode_offset);
diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index 60030df04a4d..188f1dcc880e 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -154,6 +154,14 @@ 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); + + /* Get the digest list path for initialization. */ + digest_cache->digest_list_path.dentry = digest_list_path->dentry; + digest_cache->digest_list_path.mnt = digest_list_path->mnt; + path_get(&digest_cache->digest_list_path); mutex_unlock(&dig_sec->dig_owner_mutex); out: if (digest_list_path == &file_path) @@ -226,6 +234,45 @@ 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_cache: Digest cache to initialize + * + * This function checks if the INIT_STARTED digest cache flag is set. If it is, + * it waits until the caller that saw INIT_STARTED unset completes the + * initialization. + * + * Otherwise, it 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. + */ +struct digest_cache *digest_cache_init(struct dentry *dentry, + struct digest_cache *digest_cache) +{ + /* Wait for digest cache initialization. */ + if (test_and_set_bit(INIT_STARTED, &digest_cache->flags)) { + wait_on_bit(&digest_cache->flags, INIT_IN_PROGRESS, + TASK_UNINTERRUPTIBLE); + goto out; + } + + path_put(&digest_cache->digest_list_path); + /* 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 * @dentry: Dentry of the inode for which the digest cache will be used @@ -268,6 +315,10 @@ struct digest_cache *digest_cache_get(struct dentry *dentry)
mutex_unlock(&dig_sec->dig_user_mutex);
+ if (digest_cache) + /* This must be always executed, or path ref. is not released.*/ + digest_cache = digest_cache_init(dentry, digest_cache); + return digest_cache; } EXPORT_SYMBOL_GPL(digest_cache_get);
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 5fbec491237b..6e61089acbe2 100644 --- a/security/integrity/digest_cache/Kconfig +++ b/security/integrity/digest_cache/Kconfig @@ -15,3 +15,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 54e118a2ef79..2fcfa9b4cf13 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -53,6 +53,7 @@ struct digest_cache_security {
extern loff_t inode_sec_offset; extern char *default_path_str; +extern struct rw_semaphore default_path_sem;
static inline struct digest_cache_security * digest_cache_get_security(const struct inode *inode) @@ -96,4 +97,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);
+/* 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 188f1dcc880e..0e25f573166a 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -20,6 +20,9 @@ loff_t inode_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 @@ -305,9 +308,12 @@ struct digest_cache *digest_cache_get(struct dentry *dentry)
/* Serialize accesses to inode for which the digest cache is used. */ mutex_lock(&dig_sec->dig_user_mutex); - if (!dig_sec->dig_user) + 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); + up_read(&default_path_sem); + }
if (dig_sec->dig_user) /* Increment ref. count for reference returned to the caller. */ @@ -404,6 +410,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) { + init_rwsem(&default_path_sem); + inode_sec_offset = inode_offset;
digest_cache_cache = kmem_cache_create("digest_cache_cache", diff --git a/security/integrity/digest_cache/secfs.c b/security/integrity/digest_cache/secfs.c new file mode 100644 index 000000000000..3267b2a83430 --- /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 initialize the securityfs interfaces, for configuration + * by user space. + * + * Initialize '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() and digest_cache_htable_add(), to be called by digest list parsers, in order to allocate the hash tables and to add extracted digests.
Add digest_cache_htable_free(), to let the Integrity Digest Cache free the hash tables at the time a digest cache is freed.
Add digest_cache_htable_lookup() to search a digest in the hash table of a digest cache for a given algorithm.
Add digest_cache_lookup() to the public API, to let users of the 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.
Return the digest cache containing the digest as uintptr_t, to avoid it being accidentally put. It should be cast back to (struct digest_cache *) in the functions that require it.
Finally, add digest_cache_hash_key() to compute the hash table key from the first two bytes of the digest (modulo the number of slots).
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- include/linux/digest_cache.h | 11 + security/integrity/digest_cache/Kconfig | 11 + security/integrity/digest_cache/Makefile | 2 +- security/integrity/digest_cache/htable.c | 250 +++++++++++++++++++++ security/integrity/digest_cache/internal.h | 43 ++++ security/integrity/digest_cache/main.c | 3 + 6 files changed, 319 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 86e6c5a0b896..53a7edc04310 100644 --- a/include/linux/digest_cache.h +++ b/include/linux/digest_cache.h @@ -11,10 +11,14 @@ #define _LINUX_DIGEST_CACHE_H
#include <linux/fs.h> +#include <crypto/hash_info.h>
#ifdef CONFIG_INTEGRITY_DIGEST_CACHE struct digest_cache *digest_cache_get(struct dentry *dentry); void digest_cache_put(struct digest_cache *digest_cache); +uintptr_t digest_cache_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo);
#else static inline struct digest_cache *digest_cache_get(struct dentry *dentry) @@ -26,5 +30,12 @@ static inline void digest_cache_put(struct digest_cache *digest_cache) { }
+static inline uintptr_t digest_cache_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo) +{ + return 0UL; +} + #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 6e61089acbe2..11097464540a 100644 --- a/security/integrity/digest_cache/Kconfig +++ b/security/integrity/digest_cache/Kconfig @@ -19,3 +19,14 @@ config DIGEST_LIST_DEFAULT_PATH It can be changed at run-time, by writing the new path to the securityfs interface. Digest caches created with the old path are not affected by the change. + +config DIGEST_CACHE_HTABLE_DEPTH + int + default 30 + help + Desired average depth of the collision list in the digest cache + hash tables. + + A smaller number will increase the amount of hash table slots, and + make the search faster. A bigger number will decrease the number of + hash table slots, but make the search slower. diff --git a/security/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..385f81047b0c --- /dev/null +++ b/security/integrity/digest_cache/htable.c @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu 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; + + h = lookup_htable(digest_cache, algo); + if (h) + return 0; + + h = kmalloc(sizeof(*h), GFP_KERNEL); + if (!h) + return -ENOMEM; + + h->num_slots = DIV_ROUND_UP(num_digests, + CONFIG_DIGEST_CACHE_HTABLE_DEPTH); + h->slots = kmalloc_array(h->num_slots, sizeof(*h->slots), GFP_KERNEL); + if (!h->slots) { + kfree(h); + return -ENOMEM; + } + + for (i = 0; i < h->num_slots; i++) + INIT_HLIST_HEAD(&h->slots[i]); + + h->num_digests = 0; + h->algo = algo; + + list_add_tail(&h->next, &digest_cache->htables); + + pr_debug("Initialized hash table for digest list %s, digests: %llu, slots: %u, algo: %s\n", + digest_cache->path_str, num_digests, h->num_slots, + hash_algo_name[algo]); + return 0; +} + +/** + * digest_cache_htable_add - Add a new digest to the digest cache + * @digest_cache: Digest cache + * @digest: Digest to add + * @algo: Algorithm of digest + * + * This function, invoked by a digest list parser, adds a digest extracted + * from a digest list to the digest cache. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo) +{ + struct htable *h; + struct digest_cache_entry *entry; + unsigned int key; + int digest_len; + + h = lookup_htable(digest_cache, algo); + if (!h) { + pr_debug("No hash table for algorithm %s was found in digest cache %s, initialize one\n", + hash_algo_name[algo], digest_cache->path_str); + return -ENOENT; + } + + digest_len = hash_digest_size[algo]; + + entry = kmalloc(sizeof(*entry) + digest_len, GFP_KERNEL); + if (!entry) + return -ENOMEM; + + memcpy(entry->digest, digest, digest_len); + + key = digest_cache_hash_key(digest, h->num_slots); + hlist_add_head(&entry->hnext, &h->slots[key]); + h->num_digests++; + pr_debug("Added digest %s:%*phN to digest cache %s, num of %s digests: %llu\n", + hash_algo_name[algo], digest_len, digest, + digest_cache->path_str, hash_algo_name[algo], h->num_digests); + return 0; +} + +/** + * digest_cache_htable_lookup - Search a digest in the digest cache + * @dentry: Dentry of the file whose digest is looked up + * @digest_cache: Digest cache + * @digest: Digest to search + * @algo: Algorithm of the digest to search + * + * This function searches the passed digest and algorithm in the 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; +} + +/** + * digest_cache_lookup - Search a digest in the digest cache + * @dentry: Dentry of the file whose digest is looked up + * @digest_cache: Digest cache + * @digest: Digest to search + * @algo: Algorithm of the digest to search + * + * This function calls digest_cache_htable_lookup() to search a digest in the + * passed digest cache, obtained with digest_cache_get(). + * + * It returns the digest cache reference as uintptr_t, to avoid that the digest + * cache is accidentally put. It should be cast to a digest_cache pointer where + * the function requires that. + * + * Return: A positive uintptr_t if the digest is found, zero if not. + */ +uintptr_t digest_cache_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, + u8 *digest, enum hash_algo algo) +{ + int ret; + + ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo); + if (ret < 0) + return 0UL; + + return (uintptr_t)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 2fcfa9b4cf13..f4b146a1bbaf 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 @@ -28,6 +60,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; @@ -100,4 +133,14 @@ 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 */ +int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests, + enum hash_algo algo); +int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo); +int digest_cache_htable_lookup(struct dentry *dentry, + struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo); +void digest_cache_htable_free(struct digest_cache *digest_cache); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index 0e25f573166a..a74fc1183332 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -50,6 +50,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)); @@ -65,6 +66,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
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 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.
The latter function, which at the moment does nothing, will be completed with calls to parsing functions selected from the digest list file name. It expects digest lists file names to be in the format:
[<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.
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 | 24 ++++ security/integrity/digest_cache/main.c | 16 +++ security/integrity/digest_cache/modsig.c | 66 ++++++++++ security/integrity/digest_cache/populate.c | 142 +++++++++++++++++++++ 6 files changed, 250 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 0092c913979d..1b91f9fba51c 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 htable.o +digest_cache-y := main.o secfs.o htable.o populate.o modsig.o diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index f4b146a1bbaf..f8ec51405bae 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 @@ -143,4 +160,11 @@ int digest_cache_htable_lookup(struct dentry *dentry, enum hash_algo algo); void digest_cache_htable_free(struct digest_cache *digest_cache);
+/* populate.c */ +int digest_cache_populate(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 a74fc1183332..6878ebe5b779 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -258,6 +258,9 @@ static struct digest_cache *digest_cache_new(struct dentry *dentry) struct digest_cache *digest_cache_init(struct dentry *dentry, struct digest_cache *digest_cache) { + struct inode *inode; + int ret; + /* Wait for digest cache initialization. */ if (test_and_set_bit(INIT_STARTED, &digest_cache->flags)) { wait_on_bit(&digest_cache->flags, INIT_IN_PROGRESS, @@ -265,6 +268,19 @@ struct digest_cache *digest_cache_init(struct dentry *dentry, goto out; }
+ inode = d_backing_inode(digest_cache->digest_list_path.dentry); + + if (S_ISREG(inode->i_mode)) { + ret = digest_cache_populate(digest_cache, + &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); + } + } + path_put(&digest_cache->digest_list_path); /* Notify initialization complete. */ clear_and_wake_up_bit(INIT_IN_PROGRESS, &digest_cache->flags); 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..c68c76971380 --- /dev/null +++ b/security/integrity/digest_cache/populate.c @@ -0,0 +1,142 @@ +// 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_parse_digest_list - Parse a digest list + * @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>-]<format>-<digest list name>. <seq num>- is + * optional. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int digest_cache_parse_digest_list(struct digest_cache *digest_cache, + char *path_str, void *data, + size_t data_len) +{ + char *filename, *format, *next_sep; + 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) + return ret; + + pr_debug("Parsing %s, format: %.*s, size: %ld\n", path_str, + (int)(next_sep - format), format, data_len); + + return ret; +} + +/** + * 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 + * @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 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 = dentry_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); + } + + 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(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
Add digest_list_parse_tlv(), to parse TLV-formatted (Type Length Value) digest lists. Their structure is:
[header: DIGEST_LIST_FILE, num fields, total len] [field: DIGEST_LIST_ALGO, length, value] [field: DIGEST_LIST_ENTRY#1, length, value (below)] |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len] |- [DIGEST_LIST_ENTRY_DIGEST#1, length, file digest] |- [DIGEST_LIST_ENTRY_PATH#1, length, file path] [field: DIGEST_LIST_ENTRY#N, length, value (below)] |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len] |- [DIGEST_LIST_ENTRY_DIGEST#N, length, file digest] |- [DIGEST_LIST_ENTRY_PATH#N, length, file path]
DIGEST_LIST_ALGO must have a fixed length of sizeof(u64).
The data of the DIGEST_LIST_ENTRY field are itself in TLV format.
Currently defined fields are sufficient for measurement/appraisal of file data. More fields will be introduced later for file metadata.
Introduce digest_list_hdr_callback(), to accept TLV data with type DIGEST_LIST_FILE, and to get the number of TLV data entries for initializing a new digest cache hash table. Introduce digest_list_data_callback() to handle the DIGEST_LIST_FILE fields, DIGEST_LIST_ALGO and DIGEST_LIST_ENTRY, and the respective field parsers parse_digest_list_algo() and parse_digest_list_entry().
Introduce digest_list_entry_hdr_callback() to accept nested TLV data (DIGEST_LIST_ENTRY data entry) with type DIGEST_LIST_ENTRY_DATA. Introduce digest_list_entry_data_callback(), to handle the DIGEST_LIST_ENTRY_DATA fields, DIGEST_LIST_ENTRY_DIGEST and DIGEST_LIST_ENTRY_PATH, and the respective field parsers parse_digest_list_entry_digest() and parse_digest_list_entry_path().
The TLV parser itself is defined in lib/tlv_parser.c.
Both the TLV parser and the tlv digest list parser have been formally verified with Frama-C (https://frama-c.com/).
The analysis has been done on this file:
https://github.com/robertosassu/rpm-formal/blob/main/validate_tlv.c
Here is the result of the analysis:
[eva:summary] ====== ANALYSIS SUMMARY ====== --------------------------------------------------------------------------- 15 functions analyzed (out of 15): 100% coverage. In these functions, 248 statements reached (out of 266): 93% coverage. --------------------------------------------------------------------------- Some errors and warnings have been raised during the analysis: by the Eva analyzer: 0 errors 4 warnings by the Frama-C kernel: 0 errors 0 warnings --------------------------------------------------------------------------- 0 alarms generated by the analysis. --------------------------------------------------------------------------- Evaluation of the logical properties reached by the analysis: Assertions 5 valid 0 unknown 0 invalid 5 total Preconditions 23 valid 0 unknown 0 invalid 23 total 100% of the logical properties reached have been proven. ---------------------------------------------------------------------------
The warnings are:
[eva] validate_tlv.c:359: Warning: this partitioning parameter cannot be evaluated safely on all states [eva] validate_tlv.c:387: Warning: this partitioning parameter cannot be evaluated safely on all states [eva] validate_tlv.c:443: Warning: this partitioning parameter cannot be evaluated safely on all states [eva] validate_tlv.c:451: 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 | 72 ++++ security/integrity/digest_cache/Kconfig | 1 + security/integrity/digest_cache/Makefile | 2 + .../integrity/digest_cache/parsers/parsers.h | 13 + security/integrity/digest_cache/parsers/tlv.c | 341 ++++++++++++++++++ security/integrity/digest_cache/populate.c | 4 + 6 files changed, 433 insertions(+) create mode 100644 include/uapi/linux/tlv_digest_list.h create mode 100644 security/integrity/digest_cache/parsers/parsers.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..8c97a46901c1 --- /dev/null +++ b/include/uapi/linux/tlv_digest_list.h @@ -0,0 +1,72 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * Copyright (C) 2017-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu 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_TYPE(DIGEST_LIST_TYPE) \ + DIGEST_LIST_TYPE(DIGEST_LIST_FILE) \ + DIGEST_LIST_TYPE(DIGEST_LIST__LAST) + +#define FOR_EACH_DIGEST_LIST_FIELD(DIGEST_LIST_FIELD) \ + DIGEST_LIST_FIELD(DIGEST_LIST_ALGO) \ + DIGEST_LIST_FIELD(DIGEST_LIST_ENTRY) \ + DIGEST_LIST_FIELD(DIGEST_LIST_FIELD__LAST) + +#define FOR_EACH_DIGEST_LIST_ENTRY_TYPE(DIGEST_LIST_ENTRY_TYPE) \ + DIGEST_LIST_ENTRY_TYPE(DIGEST_LIST_ENTRY_DATA) \ + DIGEST_LIST_ENTRY_TYPE(DIGEST_LIST_ENTRY__LAST) + +#define FOR_EACH_DIGEST_LIST_ENTRY_FIELD(DIGEST_LIST_ENTRY_FIELD) \ + DIGEST_LIST_ENTRY_FIELD(DIGEST_LIST_ENTRY_DIGEST) \ + DIGEST_LIST_ENTRY_FIELD(DIGEST_LIST_ENTRY_PATH) \ + DIGEST_LIST_ENTRY_FIELD(DIGEST_LIST_ENTRY_FIELD__LAST) + +#define GENERATE_ENUM(ENUM) ENUM, +#define GENERATE_STRING(STRING) #STRING, + +/** + * enum digest_list_types - Types of digest list + * + * Enumerates the types of digest list to parse. + */ +enum digest_list_types { + FOR_EACH_DIGEST_LIST_TYPE(GENERATE_ENUM) +}; + +/** + * enum digest_list_fields - Digest list fields + * + * Enumerates the digest list fields. + */ +enum digest_list_fields { + FOR_EACH_DIGEST_LIST_FIELD(GENERATE_ENUM) +}; + +/** + * enum digest_list_entry_types - Types of data stored in DIGEST_LIST_ENTRY + * + * Enumerates the types of data stored in DIGEST_LIST_ENTRY (nested TLV data). + */ +enum digest_list_entry_types { + FOR_EACH_DIGEST_LIST_ENTRY_TYPE(GENERATE_ENUM) +}; + +/** + * enum digest_list_entry_fields - DIGEST_LIST_ENTRY fields + * + * Enumerates the DIGEST_LIST_ENTRY fields. + */ +enum digest_list_entry_fields { + FOR_EACH_DIGEST_LIST_ENTRY_FIELD(GENERATE_ENUM) +}; + +#endif /* _UAPI_LINUX_TLV_DIGEST_LIST_H */ diff --git a/security/integrity/digest_cache/Kconfig b/security/integrity/digest_cache/Kconfig index 11097464540a..a1c0d0337d70 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 TLV_PARSER 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 1b91f9fba51c..5cf75c961e26 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -5,3 +5,5 @@ obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o
digest_cache-y := main.o secfs.o htable.o populate.o modsig.o + +digest_cache-y += parsers/tlv.o diff --git a/security/integrity/digest_cache/parsers/parsers.h b/security/integrity/digest_cache/parsers/parsers.h new file mode 100644 index 000000000000..1bbae426ab9f --- /dev/null +++ b/security/integrity/digest_cache/parsers/parsers.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Digest list parsers. + */ + +#include "../internal.h" + +int digest_list_parse_tlv(struct digest_cache *digest_cache, const u8 *data, + size_t data_len); diff --git a/security/integrity/digest_cache/parsers/tlv.c b/security/integrity/digest_cache/parsers/tlv.c new file mode 100644 index 000000000000..1c8cce92aded --- /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/tlv_parser.h> +#include <uapi/linux/tlv_digest_list.h> + +#include "parsers.h" + +#define kenter(FMT, ...) \ + pr_debug("==> %s(" FMT ")\n", __func__, ##__VA_ARGS__) +#define kleave(FMT, ...) \ + pr_debug("<== %s()" FMT "\n", __func__, ##__VA_ARGS__) + +static const char *digest_list_types_str[] = { + FOR_EACH_DIGEST_LIST_TYPE(GENERATE_STRING) +}; + +static const char *digest_list_fields_str[] = { + FOR_EACH_DIGEST_LIST_FIELD(GENERATE_STRING) +}; + +static const char *digest_list_entry_types_str[] = { + FOR_EACH_DIGEST_LIST_ENTRY_TYPE(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; + u64 parsed_num_entries; + 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, + u64 field_data_len) +{ + int ret; + + kenter(",%u,%llu", field, field_data_len); + + if (tlv_data->algo == HASH_ALGO__LAST) { + pr_debug("Digest algo not set\n"); + ret = -EBADMSG; + goto out; + } + + if (field_data_len != hash_digest_size[tlv_data->algo]) { + pr_debug("Unexpected data length %llu, expected %d\n", + field_data_len, hash_digest_size[tlv_data->algo]); + ret = -EBADMSG; + goto out; + } + + ret = digest_cache_htable_add(tlv_data->digest_cache, (u8 *)field_data, + tlv_data->algo); +out: + kleave(" = %d", ret); + return ret; +} + +/** + * parse_digest_list_entry_path - Parse DIGEST_LIST_ENTRY_PATH field + * @tlv_data: 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, + u64 field_data_len) +{ + kenter(",%u,%llu", field, field_data_len); + + kleave(" = 0"); + return 0; +} + +/** + * digest_list_entry_hdr_callback - DIGEST_LIST_ENTRY header callback + * @callback_data: Callback data + * @data_type: TLV data type + * @num_entries: Number of TLV data entries + * @total_len: Total length of TLV data + * + * This callback ensures that only TLV data with type DIGEST_LIST_ENTRY_DATA + * is processed. + * + * Return: 0 to skip processing the data, 1 to process the data. + */ +static int digest_list_entry_hdr_callback(void *callback_data, u64 data_type, + u64 num_entries, u64 total_len) +{ + if (data_type != DIGEST_LIST_ENTRY_DATA) + return 0; + + return 1; +} + +/** + * digest_list_entry_data_callback - DIGEST_LIST_ENTRY data callback + * @callback_data: Callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This callback handles the fields of DIGEST_LIST_ENTRY_DATA (nested) data, + * and calls the appropriate parser. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int digest_list_entry_data_callback(void *callback_data, u64 field, + const u8 *field_data, + u64 field_data_len) +{ + struct tlv_callback_data *tlv_data; + int ret; + + tlv_data = (struct tlv_callback_data *)callback_data; + + switch (field) { + case DIGEST_LIST_ENTRY_DIGEST: + ret = parse_digest_list_entry_digest(tlv_data, field, + field_data, + field_data_len); + break; + case DIGEST_LIST_ENTRY_PATH: + ret = parse_digest_list_entry_path(tlv_data, field, field_data, + field_data_len); + break; + default: + pr_debug("Unhandled field %s\n", + digest_list_entry_fields_str[field]); + /* Just ignore non-relevant fields. */ + ret = 0; + break; + } + + return ret; +} + +/** + * parse_digest_list_algo - Parse DIGEST_LIST_ALGO field + * @tlv_data: Callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This function parses the DIGEST_LIST_ALGO field (digest algorithm). + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int parse_digest_list_algo(struct tlv_callback_data *tlv_data, + enum digest_list_fields field, + const u8 *field_data, u64 field_data_len) +{ + u64 algo; + int ret; + + kenter(",%u,%llu", field, field_data_len); + + if (field_data_len != sizeof(u64)) { + pr_debug("Unexpected data length %llu, expected %lu\n", + field_data_len, sizeof(u64)); + ret = -EBADMSG; + goto out; + } + + algo = __be64_to_cpu(*(u64 *)field_data); + + if (algo >= HASH_ALGO__LAST) { + pr_debug("Unexpected digest algo %llu\n", algo); + ret = -EBADMSG; + goto out; + } + + ret = digest_cache_htable_init(tlv_data->digest_cache, + tlv_data->parsed_num_entries, algo); + if (ret < 0) + goto out; + + tlv_data->algo = algo; + + pr_debug("Digest algo: %s\n", hash_algo_name[algo]); +out: + kleave(" = %d", ret); + return ret; +} + +/** + * parse_digest_list_entry - Parse DIGEST_LIST_ENTRY field + * @tlv_data: Callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This function parses the DIGEST_LIST_ENTRY field. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int parse_digest_list_entry(struct tlv_callback_data *tlv_data, + enum digest_list_fields field, + const u8 *field_data, u64 field_data_len) +{ + int ret; + + kenter(",%u,%llu", field, field_data_len); + + ret = tlv_parse(digest_list_entry_hdr_callback, NULL, + digest_list_entry_data_callback, tlv_data, field_data, + field_data_len, digest_list_entry_types_str, + DIGEST_LIST_ENTRY__LAST, digest_list_entry_fields_str, + DIGEST_LIST_ENTRY_FIELD__LAST); + + kleave(" = %d", ret); + return ret; +} + +/** + * digest_list_hdr_callback - DIGEST_LIST header callback + * @callback_data: Callback data + * @data_type: TLV data type + * @num_entries: Number of TLV data entries + * @total_len: Total length of TLV data + * + * This callback ensures that only TLV data with type DIGEST_LIST_FILE + * is processed (one block), and stores the total number of TLV data entries + * to later initialize a new digest cache hash table. + * + * Return: 0 to skip processing the data, 1 to process the data, a POSIX error + * code otherwise. + */ +static int digest_list_hdr_callback(void *callback_data, u64 data_type, + u64 num_entries, u64 total_len) +{ + struct tlv_callback_data *tlv_data; + + tlv_data = (struct tlv_callback_data *)callback_data; + + if (data_type != DIGEST_LIST_FILE) + return 0; + + /* At the moment we process only one block. */ + if (tlv_data->parsed_num_entries) + return -EINVAL; + + tlv_data->parsed_num_entries = num_entries; + return 1; +} + +/** + * digest_list_data_callback - DIGEST_LIST data callback + * @callback_data: Callback data + * @field: Field identifier + * @field_data: Field data + * @field_data_len: Length of @field_data + * + * This callback handles the fields of DIGEST_LIST_FILE data, and calls the + * appropriate parser. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +static int digest_list_data_callback(void *callback_data, u64 field, + const u8 *field_data, u64 field_data_len) +{ + struct tlv_callback_data *tlv_data; + int ret; + + tlv_data = (struct tlv_callback_data *)callback_data; + + switch (field) { + case DIGEST_LIST_ALGO: + ret = parse_digest_list_algo(tlv_data, field, field_data, + field_data_len); + break; + case DIGEST_LIST_ENTRY: + ret = parse_digest_list_entry(tlv_data, field, field_data, + field_data_len); + break; + default: + pr_debug("Unhandled field %s\n", + digest_list_fields_str[field]); + /* Just ignore non-relevant fields. */ + ret = 0; + break; + } + + return ret; +} + +/** + * digest_list_parse_tlv - Parse a tlv digest list + * @digest_cache: Digest cache + * @data: Data to parse + * @data_len: Length of @data + * + * This function parses a tlv digest list. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_list_parse_tlv(struct digest_cache *digest_cache, const u8 *data, + size_t data_len) +{ + struct tlv_callback_data tlv_data = { + .digest_cache = digest_cache, + .algo = HASH_ALGO__LAST, + .parsed_num_entries = 0, + }; + + return tlv_parse(digest_list_hdr_callback, &tlv_data, + digest_list_data_callback, &tlv_data, data, data_len, + digest_list_types_str, DIGEST_LIST__LAST, + digest_list_fields_str, DIGEST_LIST_FIELD__LAST); +} diff --git a/security/integrity/digest_cache/populate.c b/security/integrity/digest_cache/populate.c index c68c76971380..c118658f547a 100644 --- a/security/integrity/digest_cache/populate.c +++ b/security/integrity/digest_cache/populate.c @@ -13,6 +13,7 @@ #include <linux/kernel_read_file.h>
#include "internal.h" +#include "parsers/parsers.h"
/** * digest_cache_parse_digest_list - Parse a digest list @@ -61,6 +62,9 @@ static int digest_cache_parse_digest_list(struct digest_cache *digest_cache, pr_debug("Parsing %s, format: %.*s, size: %ld\n", path_str, (int)(next_sep - format), format, data_len);
+ if (!strncmp(format, "tlv-", 4)) + ret = digest_list_parse_tlv(digest_cache, data, data_len); + return ret; }
From: Roberto Sassu roberto.sassu@huawei.com
Implement a simple parser of RPM headers, that extracts the digest and the algorithm of the packaged files from the RPMTAG_FILEDIGESTS and RPMTAG_FILEDIGESTALGO section, and adds them to the digest cache.
The rpm digest list parser has been 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_rpm.c
Here is the result of the analysis:
[eva:summary] ====== ANALYSIS SUMMARY ====== --------------------------------------------------------------------------- 7 functions analyzed (out of 7): 100% coverage. In these functions, 227 statements reached (out of 242): 93% coverage. --------------------------------------------------------------------------- No errors or warnings raised during the analysis. --------------------------------------------------------------------------- 0 alarms generated by the analysis. --------------------------------------------------------------------------- Evaluation of the logical properties reached by the analysis: Assertions 6 valid 0 unknown 0 invalid 6 total Preconditions 29 valid 0 unknown 0 invalid 29 total 100% of the logical properties reached have been proven. ---------------------------------------------------------------------------
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- security/integrity/digest_cache/Makefile | 1 + .../integrity/digest_cache/parsers/parsers.h | 2 + security/integrity/digest_cache/parsers/rpm.c | 220 ++++++++++++++++++ security/integrity/digest_cache/populate.c | 2 + 4 files changed, 225 insertions(+) create mode 100644 security/integrity/digest_cache/parsers/rpm.c
diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index 5cf75c961e26..681276a4c756 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -7,3 +7,4 @@ obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o digest_cache-y := main.o secfs.o htable.o populate.o modsig.o
digest_cache-y += parsers/tlv.o +digest_cache-y += parsers/rpm.o diff --git a/security/integrity/digest_cache/parsers/parsers.h b/security/integrity/digest_cache/parsers/parsers.h index 1bbae426ab9f..3f00d29ed92a 100644 --- a/security/integrity/digest_cache/parsers/parsers.h +++ b/security/integrity/digest_cache/parsers/parsers.h @@ -11,3 +11,5 @@
int digest_list_parse_tlv(struct digest_cache *digest_cache, const u8 *data, size_t data_len); +int digest_list_parse_rpm(struct digest_cache *digest_cache, const u8 *data, + size_t data_len); diff --git a/security/integrity/digest_cache/parsers/rpm.c b/security/integrity/digest_cache/parsers/rpm.c new file mode 100644 index 000000000000..6949d12707b8 --- /dev/null +++ b/security/integrity/digest_cache/parsers/rpm.c @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2017-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu roberto.sassu@huawei.com + * + * Parse an rpm digest list (RPM package header). + */ + +#define pr_fmt(fmt) "digest_cache RPM PARSER: "fmt +#include <linux/module.h> + +#include "parsers.h" + +#define RPMTAG_FILEDIGESTS 1035 +#define RPMTAG_FILEDIGESTALGO 5011 + +#define RPM_INT32_TYPE 4 +#define RPM_STRING_ARRAY_TYPE 8 + +struct rpm_hdr { + u32 magic; + u32 reserved; + u32 tags; + u32 datasize; +}; + +struct rpm_entryinfo { + s32 tag; + u32 type; + s32 offset; + u32 count; +}; + +enum pgp_algos { + DIGEST_ALGO_MD5 = 1, + DIGEST_ALGO_SHA1 = 2, + DIGEST_ALGO_RMD160 = 3, + /* 4, 5, 6, and 7 are reserved. */ + DIGEST_ALGO_SHA256 = 8, + DIGEST_ALGO_SHA384 = 9, + DIGEST_ALGO_SHA512 = 10, + DIGEST_ALGO_SHA224 = 11, +}; + +static const enum hash_algo pgp_algo_mapping[DIGEST_ALGO_SHA224 + 1] = { + [DIGEST_ALGO_MD5] = HASH_ALGO_MD5, + [DIGEST_ALGO_SHA1] = HASH_ALGO_SHA1, + [DIGEST_ALGO_RMD160] = HASH_ALGO_RIPE_MD_160, + [4] = HASH_ALGO__LAST, + [5] = HASH_ALGO__LAST, + [6] = HASH_ALGO__LAST, + [7] = HASH_ALGO__LAST, + [DIGEST_ALGO_SHA256] = HASH_ALGO_SHA256, + [DIGEST_ALGO_SHA384] = HASH_ALGO_SHA384, + [DIGEST_ALGO_SHA512] = HASH_ALGO_SHA512, + [DIGEST_ALGO_SHA224] = HASH_ALGO_SHA224, +}; + +/** + * digest_list_parse_rpm - Parse an rpm digest list + * @digest_cache: Digest cache + * @data: Data to parse + * @data_len: Length of @data + * + * This function parses an rpm digest list. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_list_parse_rpm(struct digest_cache *digest_cache, const u8 *data, + size_t data_len) +{ + const unsigned char rpm_header_magic[8] = { + 0x8e, 0xad, 0xe8, 0x01, 0x00, 0x00, 0x00, 0x00 + }; + const struct rpm_hdr *hdr; + const struct rpm_entryinfo *entry; + u32 tags, max_tags, datasize; + u32 digests_count, max_digests_count; + u32 digests_offset, algo_offset; + u32 digest_len, pkg_pgp_algo, i; + bool algo_offset_set = false, digests_offset_set = false; + enum hash_algo pkg_kernel_algo = HASH_ALGO_MD5; + u8 rpm_digest[SHA512_DIGEST_SIZE]; + int ret; + + if (data_len < sizeof(*hdr)) { + pr_debug("Not enough data for RPM header, current %ld, expected: %ld\n", + data_len, sizeof(*hdr)); + return -EINVAL; + } + + if (memcmp(data, rpm_header_magic, sizeof(rpm_header_magic))) { + pr_debug("RPM header magic mismatch\n"); + return -EINVAL; + } + + hdr = (const struct rpm_hdr *)data; + data += sizeof(*hdr); + data_len -= sizeof(*hdr); + + tags = __be32_to_cpu(hdr->tags); + max_tags = data_len / sizeof(*entry); + + /* Possibly lower limit on tags loop. */ + if (tags > max_tags) + return -EINVAL; + + datasize = __be32_to_cpu(hdr->datasize); + if (datasize != data_len - tags * sizeof(*entry)) + return -EINVAL; + + pr_debug("Scanning %d RPM header sections\n", tags); + for (i = 0; i < tags; i++) { + entry = (const struct rpm_entryinfo *)data; + data += sizeof(*entry); + data_len -= sizeof(*entry); + + switch (__be32_to_cpu(entry->tag)) { + case RPMTAG_FILEDIGESTS: + if (__be32_to_cpu(entry->type) != RPM_STRING_ARRAY_TYPE) + return -EINVAL; + + digests_offset = __be32_to_cpu(entry->offset); + digests_count = __be32_to_cpu(entry->count); + digests_offset_set = true; + + pr_debug("Found RPMTAG_FILEDIGESTS at offset %u, count: %u\n", + digests_offset, digests_count); + break; + case RPMTAG_FILEDIGESTALGO: + if (__be32_to_cpu(entry->type) != RPM_INT32_TYPE) + return -EINVAL; + + algo_offset = __be32_to_cpu(entry->offset); + algo_offset_set = true; + + pr_debug("Found RPMTAG_FILEDIGESTALGO at offset %u\n", + algo_offset); + break; + default: + break; + } + } + + if (!digests_offset_set) + return 0; + + if (algo_offset_set) { + if (algo_offset >= data_len) + return -EINVAL; + + if (data_len - algo_offset < sizeof(u32)) + return -EINVAL; + + pkg_pgp_algo = *(u32 *)&data[algo_offset]; + pkg_pgp_algo = __be32_to_cpu(pkg_pgp_algo); + if (pkg_pgp_algo > DIGEST_ALGO_SHA224) { + pr_debug("Unknown PGP algo %d\n", pkg_pgp_algo); + return -EINVAL; + } + + pkg_kernel_algo = pgp_algo_mapping[pkg_pgp_algo]; + if (pkg_kernel_algo >= HASH_ALGO__LAST) { + pr_debug("Unknown mapping for PGP algo %d\n", + pkg_pgp_algo); + return -EINVAL; + } + + pr_debug("Found mapping for PGP algo %d: %s\n", pkg_pgp_algo, + hash_algo_name[pkg_kernel_algo]); + } + + digest_len = hash_digest_size[pkg_kernel_algo]; + + if (digests_offset > data_len) + return -EINVAL; + + /* Worst case, every digest is a \0. */ + max_digests_count = data_len - digests_offset; + + /* Possibly lower limit on digests_count loop. */ + if (digests_count > max_digests_count) + return -EINVAL; + + ret = digest_cache_htable_init(digest_cache, digests_count, + pkg_kernel_algo); + if (ret < 0) + return ret; + + for (i = 0; i < digests_count; i++) { + if (digests_offset == data_len) + return -EINVAL; + + if (!data[digests_offset]) { + digests_offset++; + continue; + } + + if (data_len - digests_offset < digest_len * 2 + 1) + return -EINVAL; + + ret = hex2bin(rpm_digest, (const char *)&data[digests_offset], + digest_len); + if (ret < 0) { + pr_debug("Invalid hex format for digest %s\n", + &data[digests_offset]); + return ret; + } + + ret = digest_cache_htable_add(digest_cache, rpm_digest, + pkg_kernel_algo); + if (ret < 0) + return ret; + + digests_offset += digest_len * 2 + 1; + } + + return ret; +} diff --git a/security/integrity/digest_cache/populate.c b/security/integrity/digest_cache/populate.c index c118658f547a..1c68d957bf1d 100644 --- a/security/integrity/digest_cache/populate.c +++ b/security/integrity/digest_cache/populate.c @@ -64,6 +64,8 @@ static int digest_cache_parse_digest_list(struct digest_cache *digest_cache,
if (!strncmp(format, "tlv-", 4)) ret = digest_list_parse_tlv(digest_cache, data, data_len); + else if (!strncmp(format, "rpm-", 4)) + ret = digest_list_parse_rpm(digest_cache, data, data_len);
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.
Reserve space in the file descriptor security blob for the digest cache pointer (through IMA). Also introduce digest_cache_to_file_sec() to set that pointer before reading the digest list, and digest_cache_from_file_sec() to retrieve the pointer back from the file descriptor passed by LSMs with digest_cache_verif_set().
Multiple providers are supported, in the event there are multiple integrity LSMs active. Each provider should also provide 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 uintptr_t value cast to (struct digest_cache *) 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 | 2 +- security/integrity/digest_cache/internal.h | 43 ++++++- security/integrity/digest_cache/main.c | 8 +- security/integrity/digest_cache/populate.c | 2 + security/integrity/digest_cache/verif.c | 127 +++++++++++++++++++++ security/integrity/ima/ima_main.c | 5 +- 7 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 security/integrity/digest_cache/verif.c
diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h index 53a7edc04310..92e17100e9c9 100644 --- a/include/linux/digest_cache.h +++ b/include/linux/digest_cache.h @@ -19,6 +19,10 @@ void digest_cache_put(struct digest_cache *digest_cache); uintptr_t digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache, u8 *digest, enum hash_algo algo); +int digest_cache_verif_set(struct file *file, const char *verif_id, void *data, + size_t size); +void *digest_cache_verif_get(struct digest_cache *digest_cache, + const char *verif_id);
#else static inline struct digest_cache *digest_cache_get(struct dentry *dentry) @@ -37,5 +41,18 @@ static inline uintptr_t digest_cache_lookup(struct dentry *dentry, return 0UL; }
+static inline int digest_cache_verif_set(struct file *file, + const char *verif_id, void *data, + size_t size) +{ + return -EOPNOTSUPP; +} + +static inline void *digest_cache_verif_get(struct digest_cache *digest_cache, + const char *verif_id) +{ + return NULL; +} + #endif /* CONFIG_INTEGRITY_DIGEST_CACHE */ #endif /* _LINUX_DIGEST_CACHE_H */ diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index 681276a4c756..77dd98a1a07d 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -4,7 +4,7 @@
obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o
-digest_cache-y := main.o secfs.o htable.o populate.o modsig.o +digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o
digest_cache-y += parsers/tlv.o digest_cache-y += parsers/rpm.o diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index f8ec51405bae..9083a87374a5 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 @@ -73,6 +88,8 @@ struct htable { * @path_str: Path of the digest list the digest cache was created from * @flags: Control flags * @digest_list_path: Path structure of the digest list + * @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. */ @@ -82,6 +99,8 @@ struct digest_cache { char *path_str; unsigned long flags; struct path digest_list_path; + struct list_head verif_data; + spinlock_t verif_data_lock; };
/** @@ -102,6 +121,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;
@@ -138,6 +158,24 @@ digest_cache_unref(struct digest_cache *digest_cache) return (ref_is_zero) ? digest_cache : NULL; }
+static inline void digest_cache_to_file_sec(const struct file *file, + struct digest_cache *digest_cache) +{ + struct digest_cache **digest_cache_sec; + + digest_cache_sec = file->f_security + 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 *digest_list_path, @@ -145,7 +183,7 @@ struct digest_cache *digest_cache_create(struct dentry *dentry, struct digest_cache *digest_cache_init(struct dentry *dentry, struct digest_cache *digest_cache); int __init digest_cache_do_init(const struct lsm_id *lsm_id, - loff_t inode_offset); + loff_t inode_offset, loff_t file_offset);
/* secfs.c */ int __init digest_cache_secfs_init(struct dentry *dir); @@ -167,4 +205,7 @@ int digest_cache_populate(struct digest_cache *digest_cache, /* modsig.c */ size_t digest_cache_strip_modsig(__u8 *data, size_t data_len);
+/* verif.c */ +void digest_cache_verif_free(struct digest_cache *digest_cache); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index 6878ebe5b779..fda6ac599a2d 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -17,6 +17,7 @@ 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;
@@ -51,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)); @@ -67,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); @@ -421,17 +425,19 @@ static struct security_hook_list digest_cache_hooks[] __ro_after_init = { * 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 inode_offset, loff_t file_offset) { init_rwsem(&default_path_sem);
inode_sec_offset = inode_offset; + file_sec_offset = file_offset;
digest_cache_cache = kmem_cache_create("digest_cache_cache", sizeof(struct digest_cache), diff --git a/security/integrity/digest_cache/populate.c b/security/integrity/digest_cache/populate.c index 1c68d957bf1d..1ebf3a11f50b 100644 --- a/security/integrity/digest_cache/populate.c +++ b/security/integrity/digest_cache/populate.c @@ -115,6 +115,8 @@ int digest_cache_populate(struct digest_cache *digest_cache, return PTR_ERR(file); }
+ digest_cache_to_file_sec(file, digest_cache); + w.data = NULL; w.file = file; INIT_WORK_ONSTACK(&w.work, digest_cache_read_digest_list); diff --git a/security/integrity/digest_cache/verif.c b/security/integrity/digest_cache/verif.c new file mode 100644 index 000000000000..de47bd9dc388 --- /dev/null +++ b/security/integrity/digest_cache/verif.c @@ -0,0 +1,127 @@ +// 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; + + 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); + } +} diff --git a/security/integrity/ima/ima_main.c b/security/integrity/ima/ima_main.c index 7cbd78ca3be5..646d900828e0 100644 --- a/security/integrity/ima/ima_main.c +++ b/security/integrity/ima/ima_main.c @@ -1208,7 +1208,8 @@ static int __init init_ima_lsm(void) 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 *)); + sizeof(struct ima_iint_cache *), + ima_blob_sizes.lbs_file); return 0; }
@@ -1217,6 +1218,8 @@ struct lsm_blob_sizes ima_blob_sizes __ro_after_init = { #ifdef CONFIG_INTEGRITY_DIGEST_CACHE + sizeof(struct digest_cache_security) #endif + , + .lbs_file = sizeof(struct digest_cache *), };
DEFINE_LSM(ima) = {
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 this function returns a different one than the one passed, it means that the directory was modified between the get and lookup operations, and it uses the new one.
Then, 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. Since the digest cache containing the digest has been found, digest_cache_dir_update_dig_user() is called to replace dig_user, set by digest_cache_get() to the directory digest cache, with the found one. It can also happen that dig_user is updated with a directory digest cache, if the digest is not found and there was a directory modification between get and lookup.
digest_cache_dir_lookup_digest() returns the digest cache reference of the current directory entry as the uintptr_t type, so that callers of digest_cache_lookup() don't mistakenly try to call digest_cache_put() with that reference.
The returned digest cache reference can be converted back to (struct digest_cache *) and used to retrieve information about the digest cache containing the digest, which is not known in advance in the case of directories until the digest search is performed.
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 | 273 +++++++++++++++++++++ security/integrity/digest_cache/htable.c | 4 + security/integrity/digest_cache/internal.h | 43 ++++ security/integrity/digest_cache/main.c | 13 + 5 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 security/integrity/digest_cache/dir.c
diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index 77dd98a1a07d..aef7f97d1407 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -4,7 +4,7 @@
obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o
-digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o +digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o
digest_cache-y += parsers/tlv.o digest_cache-y += parsers/rpm.o diff --git a/security/integrity/digest_cache/dir.c b/security/integrity/digest_cache/dir.c new file mode 100644 index 000000000000..f0a2cb9618ba --- /dev/null +++ b/security/integrity/digest_cache/dir.c @@ -0,0 +1,273 @@ +// 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>-<format>-<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 = dentry_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 + * @digest_list_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 *digest_list_path, + char *path_str) +{ + struct digest_cache *digest_cache; + int ret; + + ret = kern_path(path_str, 0, digest_list_path); + if (ret < 0) { + pr_debug("Cannot find path %s\n", path_str); + return NULL; + } + + digest_cache = digest_cache_create(dentry, digest_list_path, path_str, + ""); + if (digest_cache) + digest_cache = digest_cache_init(dentry, digest_cache); + + return digest_cache; +} + +/** + * digest_cache_dir_update_dig_user - Update dig_user with passed digest cache + * @dentry: Dentry of the file whose digest is looked up + * @digest_cache: Dir digest cache + * + * This function updates dig_user of the inode being verified, with the passed + * digest cache. The digest cache can differ if the directory inode was evicted + * or modified, or if the digest searched was found in a directory entry. In the + * latter case, dig_user is replaced with the digest cache of that directory + * entry. + */ +static void digest_cache_dir_update_dig_user(struct dentry *dentry, + struct digest_cache *digest_cache) +{ + struct inode *inode = d_backing_inode(dentry); + struct digest_cache_security *dig_sec; + + dig_sec = digest_cache_get_security(inode); + if (unlikely(!dig_sec)) + return; + + /* Serialize accesses to inode for which the digest cache is used. */ + mutex_lock(&dig_sec->dig_user_mutex); + if (dig_sec->dig_user != digest_cache) { + digest_cache_put(dig_sec->dig_user); + dig_sec->dig_user = digest_cache_ref(digest_cache); + } + mutex_unlock(&dig_sec->dig_user_mutex); +} + +/** + * 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 positive uintptr_t value if the digest if found, zero otherwise. + */ +uintptr_t 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 digest_list_path; + int ret; + + /* Try to reacquire the dir digest cache, and use the new if changed. */ + dir_cache = digest_cache_dir_create(dentry, &digest_list_path, + digest_cache->path_str); + if (!dir_cache) + return 0UL; + + /* Continue to use the new one. */ + list_for_each_entry(dir_entry, &dir_cache->dir_entries, list) { + mutex_lock(&dir_entry->digest_cache_mutex); + if (!dir_entry->digest_cache) { + cache = digest_cache_create(dentry, &digest_list_path, + dir_cache->path_str, + dir_entry->name); + if (cache) + cache = digest_cache_init(dentry, cache); + + /* 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 = dir_entry->digest_cache; + break; + } + } + + digest_cache_dir_update_dig_user(dentry, found ?: dir_cache); + + digest_cache_put(dir_cache); + path_put(&digest_list_path); + return (uintptr_t)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 385f81047b0c..1aa884ef18f0 100644 --- a/security/integrity/digest_cache/htable.c +++ b/security/integrity/digest_cache/htable.c @@ -209,6 +209,10 @@ uintptr_t 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 0UL; diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index 9083a87374a5..34a26576a5aa 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 @@ -95,6 +129,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; @@ -208,4 +243,12 @@ 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); +uintptr_t 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 fda6ac599a2d..d050ebad3cbb 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); @@ -283,6 +285,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_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); + } }
path_put(&digest_cache->digest_list_path);
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 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.
The prefetching mechanism has been designed to be more like a aid to get predictable measurements, and assumes that digest lists remain the same over the time. Thus, it can fail in presence of concurrent VFS operations changing the inode the digest cache is created from. That does not affect the digest cache creation/initialization done by digest_cache_get(), since the digest cache holds a path reference until it is fully initialized.
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.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- include/uapi/linux/xattr.h | 3 + security/integrity/digest_cache/dir.c | 122 ++++++++++++++++++++- security/integrity/digest_cache/internal.h | 12 +- security/integrity/digest_cache/main.c | 91 ++++++++++++++- security/integrity/digest_cache/populate.c | 9 +- security/integrity/digest_cache/verif.c | 4 + 6 files changed, 233 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 f0a2cb9618ba..12d6af13d49e 100644 --- a/security/integrity/digest_cache/dir.c +++ b/security/integrity/digest_cache/dir.c @@ -55,6 +55,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; @@ -133,6 +134,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 * @@ -140,6 +146,7 @@ digest_cache_dir_create(struct dentry *dentry, struct path *digest_list_path, char *path_str) { struct digest_cache *digest_cache; + bool prefetch_req; int ret;
ret = kern_path(path_str, 0, digest_list_path); @@ -148,8 +155,11 @@ digest_cache_dir_create(struct dentry *dentry, struct path *digest_list_path, return NULL; }
+ prefetch_req = digest_cache_prefetch_requested(digest_list_path, + path_str); + digest_cache = digest_cache_create(dentry, digest_list_path, path_str, - ""); + "", prefetch_req, false); if (digest_cache) digest_cache = digest_cache_init(dentry, digest_cache);
@@ -220,7 +230,8 @@ uintptr_t digest_cache_dir_lookup_digest(struct dentry *dentry, if (!dir_entry->digest_cache) { cache = digest_cache_create(dentry, &digest_list_path, dir_cache->path_str, - dir_entry->name); + dir_entry->name, false, + false); if (cache) cache = digest_cache_init(dentry, cache);
@@ -232,6 +243,8 @@ uintptr_t 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);
@@ -251,6 +264,111 @@ uintptr_t digest_cache_dir_lookup_digest(struct dentry *dentry, return (uintptr_t)found; }
+/** + * digest_cache_dir_lookup_filename - Lookup a digest list + * @dentry: Dentry of the file whose digest list is looked up + * @digest_list_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 perform a full + * initialization of the requested digest cache. + */ +static void digest_cache_dir_lookup_filename(struct dentry *dentry, + struct path *digest_list_path, + struct digest_cache *digest_cache, + char *filename) +{ + struct digest_cache *cache; + struct dir_entry *dir_entry; + 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; + } + + cache = digest_cache_create(dentry, digest_list_path, + digest_cache->path_str, + dir_entry->name, false, + filename_found ? false : true); + if (cache) + cache = digest_cache_init(dentry, cache); + + 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 a directory + * @dentry: Dentry of the file whose digest list is looked up + * @digest_cache: Digest cache + * + * This function prefetches digest lists in a directory. + * + * 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; + 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) + return -ENOENT; + + digest_cache_dir_lookup_filename(dentry, &dir_path, dir_cache, + last + 1); + + digest_cache_put(dir_cache); + 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 34a26576a5aa..c90efdbf51ed 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 entries. */ +#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[]; };
@@ -214,7 +219,10 @@ digest_cache_from_file_sec(const struct file *file) /* main.c */ struct digest_cache *digest_cache_create(struct dentry *dentry, 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 digest_cache *digest_cache); int __init digest_cache_do_init(const struct lsm_id *lsm_id, @@ -249,6 +257,8 @@ int digest_cache_dir_add_entries(struct digest_cache *digest_cache, uintptr_t 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 d050ebad3cbb..e9c3d120b8d4 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -85,6 +85,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, the digest list inode * from which the digest cache will be created or retrieved (if it already @@ -105,7 +107,8 @@ static void digest_cache_free(struct digest_cache *digest_cache) */ struct digest_cache *digest_cache_create(struct dentry *dentry, struct path *digest_list_path, - char *path_str, char *filename) + char *path_str, char *filename, + bool prefetch_req, bool prefetch) { struct path file_path; struct digest_cache *digest_cache = NULL; @@ -157,6 +160,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) { @@ -166,10 +179,18 @@ 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); + } + /* Get the digest list path for initialization. */ digest_cache->digest_list_path.dentry = digest_list_path->dentry; digest_cache->digest_list_path.mnt = digest_list_path->mnt; @@ -182,6 +203,52 @@ struct digest_cache *digest_cache_create(struct dentry *dentry, 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 @@ -192,6 +259,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. + * * Return: A digest cache on success, NULL on error. */ static struct digest_cache *digest_cache_new(struct dentry *dentry) @@ -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); @@ -239,8 +313,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, - default_path_str, filename); + default_path_str, filename, + prefetch_req, false); out: path_put(&default_path); return digest_cache; @@ -357,9 +435,14 @@ struct digest_cache *digest_cache_get(struct dentry *dentry)
mutex_unlock(&dig_sec->dig_user_mutex);
- if (digest_cache) + if (digest_cache) { + /* Prefetch, digest cache should be initialized afterwards. */ + if (test_bit(FILE_PREFETCH, &digest_cache->flags)) + digest_cache_dir_prefetch(dentry, digest_cache); + /* This must be always executed, or path ref. is not released.*/ digest_cache = digest_cache_init(dentry, digest_cache); + }
return digest_cache; } diff --git a/security/integrity/digest_cache/populate.c b/security/integrity/digest_cache/populate.c index 1ebf3a11f50b..7360bcf55249 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" #include "parsers/parsers.h" @@ -135,6 +136,12 @@ int digest_cache_populate(struct digest_cache *digest_cache, 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. */ @@ -144,7 +151,7 @@ int digest_cache_populate(struct digest_cache *digest_cache, 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 de47bd9dc388..8a8e6483679f 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; + new_verif = kzalloc(sizeof(*new_verif), flags); if (!new_verif) return -ENOMEM;
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.
Callers of digest_cache_get() that previously obtained a reset digest cache are not affected, since that one remains valid as long as the reference count is greater than zero. If they want to know if a reset happened, they can record which digest cache they obtained with digest_cache_get() and compare with the new one from a subsequent digest_cache_get() call.
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 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 is interested that a digest is not found 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.
Signed-off-by: Roberto Sassu roberto.sassu@huawei.com --- security/integrity/digest_cache/Makefile | 3 +- security/integrity/digest_cache/dir.c | 6 + security/integrity/digest_cache/internal.h | 13 ++ security/integrity/digest_cache/main.c | 12 ++ security/integrity/digest_cache/reset.c | 227 +++++++++++++++++++++ 5 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 security/integrity/digest_cache/reset.c
diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index aef7f97d1407..8847c1ba7d5f 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -4,7 +4,8 @@
obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o
-digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o +digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o \ + reset.o
digest_cache-y += parsers/tlv.o digest_cache-y += parsers/rpm.o diff --git a/security/integrity/digest_cache/dir.c b/security/integrity/digest_cache/dir.c index 12d6af13d49e..6b64b7fdc426 100644 --- a/security/integrity/digest_cache/dir.c +++ b/security/integrity/digest_cache/dir.c @@ -227,6 +227,12 @@ uintptr_t digest_cache_dir_lookup_digest(struct dentry *dentry, /* Continue to use the new one. */ 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) { cache = digest_cache_create(dentry, &digest_list_path, dir_cache->path_str, diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index c90efdbf51ed..023e11640af1 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 entries. */ #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 @@ -261,4 +262,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 e9c3d120b8d4..99c2f1f85573 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -422,6 +422,11 @@ struct digest_cache *digest_cache_get(struct dentry *dentry)
/* Serialize accesses to inode for which the digest cache is used. */ mutex_lock(&dig_sec->dig_user_mutex); + if (dig_sec->dig_user && test_bit(RESET, &dig_sec->dig_user->flags)) { + digest_cache_put(dig_sec->dig_user); + dig_sec->dig_user = NULL; + } + if (!dig_sec->dig_user) { down_read(&default_path_sem); /* Consume extra reference from digest_cache_create(). */ @@ -515,6 +520,13 @@ static void digest_cache_inode_free_security(struct inode *inode) static struct security_hook_list digest_cache_hooks[] __ro_after_init = { LSM_HOOK_INIT(inode_alloc_security, digest_cache_inode_alloc_security), LSM_HOOK_INIT(inode_free_security, digest_cache_inode_free_security), + LSM_HOOK_INIT(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.50% of 200 File 'security/integrity/digest_cache/modsig.c' Lines executed:42.86% of 21 File 'security/integrity/digest_cache/htable.c' Lines executed:93.75% of 80 File 'security/integrity/digest_cache/populate.c' Lines executed:92.73% of 55 File 'security/integrity/digest_cache/verif.c' Lines executed:85.11% of 47 File 'security/integrity/digest_cache/dir.c' Lines executed:92.72% of 151 File 'security/integrity/digest_cache/secfs.c' Lines executed:56.00% of 25 File 'security/integrity/digest_cache/parsers/tlv.c' Lines executed:80.00% of 85 File 'security/integrity/digest_cache/parsers/rpm.c' Lines executed:88.31% of 77
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 | 749 ++++++++++++++++++ tools/testing/selftests/digest_cache/common.c | 78 ++ tools/testing/selftests/digest_cache/common.h | 134 ++++ .../selftests/digest_cache/common_user.c | 47 ++ .../selftests/digest_cache/common_user.h | 17 + tools/testing/selftests/digest_cache/config | 1 + .../selftests/digest_cache/generators.c | 248 ++++++ .../selftests/digest_cache/generators.h | 19 + .../selftests/digest_cache/testmod/Makefile | 16 + .../selftests/digest_cache/testmod/kern.c | 501 ++++++++++++ 13 files changed, 1838 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 bc8fe9e8f7f2..cc0b314a235f 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -15,6 +15,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..fc84917e9250 --- /dev/null +++ b/tools/testing/selftests/digest_cache/all_test.c @@ -0,0 +1,749 @@ +// 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; +}; + +FIXTURE_SETUP(shared_data) +{ + char cmd[1024]; + int 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]); + ASSERT_EQ(cmd_len, write(self->kernfd, cmd, cmd_len)); + } +} + +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) +{ + 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, 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; + + 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 enum pgp_algos get_pgp_algo(enum hash_algo algo) +{ + unsigned long i; + + for (i = DIGEST_ALGO_MD5; i < ARRAY_SIZE(pgp_algo_mapping); i++) + if (pgp_algo_mapping[i] == algo) + return i; + + return DIGEST_ALGO_SHA224 + 1; +} + +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)); + } else if (!strncmp(digest_list_filename, "rpm-", 4)) { + enum pgp_algos pgp_algo = get_pgp_algo(algo); + + if (pgp_algo == DIGEST_ALGO_SHA224 + 1) + return; + + ASSERT_EQ(0, gen_rpm_list(self->digest_lists_dirfd, + digest_list_filename, algo, pgp_algo, + start_number, num_digests, + (enum rpm_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); +} + +/* + * Verify that the rpm digest list parser returns success on well-formatted + * digest lists, for each defined hash algorithm. + */ +TEST_F(shared_data, rpm_parser_ok) +{ + enum hash_algo algo; + + /* Test every known algorithm. */ + for (algo = 0; algo < HASH_ALGO__LAST; algo++) + test_parser(self, _metadata, "rpm-digest_list", "file", algo, + 0, 5, RPM_NO_FAILURE); +} + +/* + * Verify that the rpm digest list parser returns failure on invalid digest + * lists. + */ +TEST_F(shared_data, rpm_parser_error) +{ + enum rpm_failures failure; + + /* Test every failure. */ + for (failure = 0; failure < RPM_FAILURE__LAST; failure++) + test_parser(self, _metadata, "rpm-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); +} + +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) +{ + 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) +{ + 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; + + 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) +{ + 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..fd3d2958cc15 --- /dev/null +++ b/tools/testing/selftests/digest_cache/common.h @@ -0,0 +1,134 @@ +/* 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 RPMTAG_FILEDIGESTS 1035 +#define RPMTAG_FILEDIGESTALGO 5011 + +#define RPM_INT32_TYPE 4 +#define RPM_STRING_ARRAY_TYPE 8 + +#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_HDR_LEN, + TLV_FAILURE_ALGO_MISMATCH, + TLV_FAILURE_NUM_DIGESTS, + TLV_FAILURE__LAST +}; + +enum rpm_failures { RPM_NO_FAILURE, + RPM_FAILURE_WRONG_MAGIC, + RPM_FAILURE_BAD_DATA_OFFSET, + RPM_FAILURE_WRONG_TAGS, + RPM_FAILURE_WRONG_DIGEST_COUNT, + RPM_FAILURE_DIGEST_WRONG_TYPE, + RPM_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 +}; + +enum pgp_algos { + DIGEST_ALGO_MD5 = 1, + DIGEST_ALGO_SHA1 = 2, + DIGEST_ALGO_RMD160 = 3, + /* 4, 5, 6, and 7 are reserved. */ + DIGEST_ALGO_SHA256 = 8, + DIGEST_ALGO_SHA384 = 9, + DIGEST_ALGO_SHA512 = 10, + DIGEST_ALGO_SHA224 = 11, +}; + +struct rpm_hdr { + u32 magic; + u32 reserved; + u32 tags; + u32 datasize; +}; + +struct rpm_entryinfo { + s32 tag; + u32 type; + s32 offset; + u32 count; +}; + +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..929420e1bbe6 --- /dev/null +++ b/tools/testing/selftests/digest_cache/common_user.c @@ -0,0 +1,47 @@ +// 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] + +const enum hash_algo pgp_algo_mapping[DIGEST_ALGO_SHA224 + 1] = { + [DIGEST_ALGO_MD5] = HASH_ALGO_MD5, + [DIGEST_ALGO_SHA1] = HASH_ALGO_SHA1, + [DIGEST_ALGO_RMD160] = HASH_ALGO_RIPE_MD_160, + [4] = HASH_ALGO__LAST, + [5] = HASH_ALGO__LAST, + [6] = HASH_ALGO__LAST, + [7] = HASH_ALGO__LAST, + [DIGEST_ALGO_SHA256] = HASH_ALGO_SHA256, + [DIGEST_ALGO_SHA384] = HASH_ALGO_SHA384, + [DIGEST_ALGO_SHA512] = HASH_ALGO_SHA512, + [DIGEST_ALGO_SHA224] = HASH_ALGO_SHA224, +}; + +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..4eef52cc5c27 --- /dev/null +++ b/tools/testing/selftests/digest_cache/common_user.h @@ -0,0 +1,17 @@ +/* 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" + +extern const enum hash_algo pgp_algo_mapping[DIGEST_ALGO_SHA224 + 1]; + +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..2822fc7ad1d3 --- /dev/null +++ b/tools/testing/selftests/digest_cache/config @@ -0,0 +1 @@ +CONFIG_INTEGRITY_DIGEST_CACHE=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..c7791a3589f2 --- /dev/null +++ b/tools/testing/selftests/digest_cache/generators.c @@ -0,0 +1,248 @@ +// 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) +{ + u64 _algo = __cpu_to_be64(algo); + u8 digest[MAX_DIGEST_SIZE] = { 0 }; + int digest_len = hash_digest_size[algo]; + int digest_len_to_copy = digest_len; + int ret, fd, i; + + struct tlv_data_entry algo_entry = { + .field = __cpu_to_be64(DIGEST_LIST_ALGO), + .length = __cpu_to_be64(sizeof(_algo)), + }; + + struct tlv_data_entry entry_digest = { + .field = __cpu_to_be64(DIGEST_LIST_ENTRY_DIGEST), + .length = __cpu_to_be64(digest_len), + }; + + struct tlv_hdr entry_hdr = { + .data_type = __cpu_to_be64(DIGEST_LIST_ENTRY_DATA), + ._reserved = 0, + .num_entries = __cpu_to_be64(1), + .total_len = __cpu_to_be64(sizeof(entry_digest) + digest_len), + }; + + struct tlv_data_entry entry_entry = { + .field = __cpu_to_be64(DIGEST_LIST_ENTRY), + .length = __cpu_to_be64(sizeof(entry_hdr) + + __be64_to_cpu(entry_hdr.total_len)), + }; + + struct tlv_hdr hdr = { + .data_type = __cpu_to_be64(DIGEST_LIST_FILE), + ._reserved = 0, + .num_entries = __cpu_to_be64(1 + num_digests), + .total_len = __cpu_to_be64(sizeof(algo_entry) + + __be64_to_cpu(algo_entry.length) + + num_digests * (sizeof(entry_entry) + + __be64_to_cpu(entry_entry.length))) + }; + + switch (failure) { + case TLV_FAILURE_ALGO_LEN: + algo_entry.length = algo_entry.length / 2; + break; + case TLV_FAILURE_HDR_LEN: + hdr.total_len--; + break; + case TLV_FAILURE_ALGO_MISMATCH: + _algo = __cpu_to_be64(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 *)&hdr, sizeof(hdr)); + if (ret != sizeof(hdr)) + 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; + + *(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_hdr, sizeof(entry_hdr)); + if (ret != sizeof(entry_hdr)) + return -errno; + + ret = write(fd, (u8 *)&entry_digest, sizeof(entry_digest)); + if (ret != sizeof(entry_digest)) + return -errno; + + ret = write(fd, digest, digest_len_to_copy); + if (ret != digest_len_to_copy) + return -errno; + + (*(u32 *)digest)++; + } + + close(fd); + return 0; +} + +int gen_rpm_list(int temp_dirfd, char *digest_list_filename, + enum hash_algo algo, enum pgp_algos pgp_algo, int start_number, + int num_digests, enum rpm_failures failure) +{ + u32 _pgp_algo = __cpu_to_be32(pgp_algo); + u8 digest[MAX_DIGEST_SIZE] = { 0 }; + char digest_str[MAX_DIGEST_SIZE * 2 + 1]; + struct rpm_hdr hdr; + struct rpm_entryinfo algo_entry, digest_entry; + int digest_len = hash_digest_size[algo]; + int ret, fd, d_len, i; + + d_len = hash_digest_size[algo] * 2 + 1; + + hdr.magic = __cpu_to_be32(0x8eade801); + hdr.reserved = 0; + hdr.tags = __cpu_to_be32(1); + + /* + * Skip the algo section, to ensure that the parser recognizes MD5 as + * the default hash algorithm. + */ + if (algo != HASH_ALGO_MD5) + hdr.tags = __cpu_to_be32(2); + + hdr.datasize = __cpu_to_be32(d_len * num_digests); + + if (algo != HASH_ALGO_MD5) + hdr.datasize = __cpu_to_be32(sizeof(u32) + d_len * num_digests); + + digest_entry.tag = __cpu_to_be32(RPMTAG_FILEDIGESTS); + digest_entry.type = __cpu_to_be32(RPM_STRING_ARRAY_TYPE); + digest_entry.offset = 0; + digest_entry.count = __cpu_to_be32(num_digests); + + algo_entry.tag = __cpu_to_be32(RPMTAG_FILEDIGESTALGO); + algo_entry.type = __cpu_to_be32(RPM_INT32_TYPE); + algo_entry.offset = __cpu_to_be32(d_len * num_digests); + algo_entry.count = __cpu_to_be32(1); + + switch (failure) { + case RPM_FAILURE_WRONG_MAGIC: + hdr.magic++; + break; + case RPM_FAILURE_BAD_DATA_OFFSET: + algo_entry.offset = __cpu_to_be32(UINT_MAX); + break; + case RPM_FAILURE_WRONG_TAGS: + hdr.tags = __cpu_to_be32(2 + 10); + break; + case RPM_FAILURE_WRONG_DIGEST_COUNT: + /* We need to go beyond the algorithm, to fail. */ + digest_entry.count = __cpu_to_be32(num_digests + 5); + break; + case RPM_FAILURE_DIGEST_WRONG_TYPE: + digest_entry.type = __cpu_to_be32(RPM_INT32_TYPE); + 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 *)&hdr, sizeof(hdr)); + if (ret != sizeof(hdr)) + return -errno; + + if (algo != HASH_ALGO_MD5) { + ret = write(fd, (u8 *)&algo_entry, sizeof(algo_entry)); + if (ret != sizeof(algo_entry)) + return -errno; + } + + ret = write(fd, (u8 *)&digest_entry, sizeof(digest_entry)); + if (ret != sizeof(digest_entry)) + return -errno; + + *(u32 *)digest = start_number; + + for (i = 0; i < num_digests; i++) { + bin2hex(digest_str, digest, digest_len); + + ret = write(fd, (u8 *)digest_str, d_len); + if (ret != d_len) + return -errno; + + (*(u32 *)digest)++; + } + + if (algo != HASH_ALGO_MD5) { + ret = write(fd, (u8 *)&_pgp_algo, sizeof(_pgp_algo)); + if (ret != sizeof(_pgp_algo)) + return -errno; + } + + 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..1c83e531b799 --- /dev/null +++ b/tools/testing/selftests/digest_cache/generators.h @@ -0,0 +1,19 @@ +/* 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 gen_rpm_list(int temp_dirfd, char *digest_list_filename, + enum hash_algo algo, enum pgp_algos pgp_algo, int start_number, + int num_digests, enum rpm_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..1c76deb384ad --- /dev/null +++ b/tools/testing/selftests/digest_cache/testmod/kern.c @@ -0,0 +1,501 @@ +// 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; +static uintptr_t 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 ? + (struct digest_cache *)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 ? (struct digest_cache *)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; + + w->ret = kern_path(w->path_str, 0, &path); + if (w->ret < 0) + return; + + digest_cache = digest_cache_get(path.dentry); + + 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; + 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: + found = 0UL; + + 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); + } + + digest_cache = digest_cache_get(path.dentry); + ret = digest_cache ? 0 : -ENOENT; + pr_debug("digest cache get %s, ret: %d\n", path_str, ret); + path_put(&path); + break; + case DIGEST_CACHE_LOOKUP: + 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) + 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; + } + ret = 0; + pr_debug("digest cache put, ret: %d\n", ret); + break; + case DIGEST_CACHE_ENABLE_VERIF: + case DIGEST_CACHE_DISABLE_VERIF: + 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_HAVE_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; + + 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: + unregister_kprobe(&kp); + return ret; +} + +static void __exit digest_cache_test_fini(void) +{ + if (digest_cache) + digest_cache_put(digest_cache); + + securityfs_remove(test); + 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");
On 9/5/24 08:05, Roberto Sassu wrote: ...
+module_init(digest_cache_test_init); +module_exit(digest_cache_test_fini); +MODULE_LICENSE("GPL");
Missing MODULE_DESCRIPTION()
Since commit 1fffe7a34c89 ("script: modpost: emit a warning when the description is missing"), a module without a MODULE_DESCRIPTION() will result in a warning when built with make W=1. Recently, multiple developers have been eradicating these warnings treewide, and very few are left, so please don't introduce a new one :)
On Thu, 2024-09-05 at 10:02 -0700, Jeff Johnson wrote:
On 9/5/24 08:05, Roberto Sassu wrote: ...
+module_init(digest_cache_test_init); +module_exit(digest_cache_test_fini); +MODULE_LICENSE("GPL");
Missing MODULE_DESCRIPTION()
Since commit 1fffe7a34c89 ("script: modpost: emit a warning when the description is missing"), a module without a MODULE_DESCRIPTION() will result in a warning when built with make W=1. Recently, multiple developers have been eradicating these warnings treewide, and very few are left, so please don't introduce a new one :)
Argh, thanks for the reminder! You mentioned in the PGP patch set.
Roberto
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 | 814 ++++++++++++++++++++++++ Documentation/security/index.rst | 1 + MAINTAINERS | 2 + 3 files changed, 817 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..ddc33e672090 --- /dev/null +++ b/Documentation/security/digest_cache.rst @@ -0,0 +1,814 @@ +.. 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. While +it currently supports a TLV-based and the RPM formats, it can be easily +extended to support more formats, such as DEBs. Focusing on just extracting +digests keeps these parsers minimal and reasonably simple (e.g. the RPM +parser has ~220 LOC). Included parsers have been verified for memory safety +with the Frama-C static analyzer. The parsers with the Frama-C assertions +are available here: + +https://github.com/robertosassu/rpm-formal/ + +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 assignemt 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() 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 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>-<file name> + +where format can be for example ``tlv`` or ``rpm``, which make the +Integrity Digest Cache call respectively the TLV or RPM parser. + +Alternatively, also the following format is supported:: + + <seq num>-<digest list format>-<file 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 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 numeric reference (uintptr_t type), +representing the digest cache containing the queried digest. It is not a +pointer, to avoid it being accidentally passed to 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. + +digest_cache_dir_lookup_digest(), if it finds a digest in a directory +digest cache, it calls digest_cache_dir_update_dig_user() to update +dig_user of the inode, whose digest is queried, with the digest cache +containing the digest. This ensures that next time digest_cache_get() is +called for the same inode, the digest is searched in the right digest +cache, instead of repeating the search in each directory entry. + + +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 +numeric reference of the digest cache containing the digest (thus populated +from a digest list), and pass it to digest_cache_verif_get() after casting +the value to a digest cache pointer. + + +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. + +Nothing changes for previous callers of digest_cache_get(), since they +still hold the old digest cache pointer, despite that has been replaced in +the inode security blobs. The old digest cache pointer will be still valid +until the callers decide to release that 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. + + +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 appended 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 + + +Public 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 + +.. 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 + + +Digest list formats +=================== + +tlv +--- + +The Type-Length-Value (TLV) format was chosen for its extensibility. +Additional fields can be added without breaking compatibility with old +versions of the parser. + +The layout of a tlv digest list is the following:: + + [header: DIGEST_LIST_FILE, num fields, total len] + [field: DIGEST_LIST_ALGO, length, value] + [field: DIGEST_LIST_ENTRY#1, length, value (below)] + |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len] + |- [DIGEST_LIST_ENTRY_DIGEST#1, length, file digest] + |- [DIGEST_LIST_ENTRY_PATH#1, length, file path] + [field: DIGEST_LIST_ENTRY#N, length, value (below)] + |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len] + |- [DIGEST_LIST_ENTRY_DIGEST#N, length, file digest] + |- [DIGEST_LIST_ENTRY_PATH#N, length, file path] + +DIGEST_LIST_ALGO is a field to specify the algorithm of the file digest. +DIGEST_LIST_ENTRY is a nested TLV structure with the following fields: +DIGEST_LIST_ENTRY_DIGEST contains the file digest; DIGEST_LIST_ENTRY_PATH +contains the file path. + + +rpm +--- + +The rpm digest list is basically a subset of the RPM package header. +Its format is:: + + [RPM magic number] + [RPMTAG_IMMUTABLE] + +RPMTAG_IMMUTABLE is a section of the full RPM header containing the part +of the header that was signed, and whose signature is stored in the +RPMTAG_RSAHEADER section. + + +Appended signature +------------------ + +Digest lists can have a module-style appended signature, that can be used +for appraisal with IMA. The signature type can be PKCS#7, as for kernel +modules, or a different type. + + +History +======= + +The original name of this work was IMA Digest Lists, which was somehow +considered too invasive. The code was moved to a separate component named +DIGLIM (DIGest Lists Integrity Module), with the purpose of removing the +complexity away of IMA, and also adding the possibility of using it with +other kernel components (e.g. Integrity Policy Enforcement, or IPE). + +The design changed significantly, so DIGLIM was renamed to 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. + +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, because digest lists were ordered +according to their appearance in the IMA measurement list (which minimize +the digest lists to prefetch). + + +Parallel +~~~~~~~~ + +This test was performed reading files in parallel, not waiting for the +current read to terminate. + +:: + + +-------+-----------------------+-----------+ + | meas. | time no/p/vTPM (sec.) | slab (KB) | + +--------------------+-------+-----------------------+-----------+ + | no cache | 12313 | 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 59f8fc106cb0..34933e13c509 100644 --- a/Documentation/security/index.rst +++ b/Documentation/security/index.rst @@ -19,3 +19,4 @@ Security Documentation digsig landlock secrets/index + digest_cache diff --git a/MAINTAINERS b/MAINTAINERS index ba8d5c137bef..3b212c747652 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11115,8 +11115,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/
INTEL 810/815 FRAMEBUFFER DRIVER M: Antonino Daplas adaplas@gmail.com
On Thu, Sep 05, 2024 at 05:05:29PM +0200, Roberto Sassu wrote:
Good morning, I hope the week is starting well for everyone
Apologies for the delay in getting these thoughts out, scrambling to catch up on my e-mail backlog.
I looped Linus in, secondary to the conversations surrounding the PGP verification infrastructure in the kernel, given that the primary use case at this time appears to be the digest cache and his concerns regarding that use.
Our proposed TSEM LSM, most recent submission here:
https://lore.kernel.org/linux-security-module/20240826103728.3378-1-greg@enj...
Is a superset of IMA functionality and depends heavily on file checksums, hence our interest and reflections in your efforts with this.
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.
No argument here, inherent in better and more effective security architectures is better useability, pure and simple.
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().
The future for trusted systems will not be in TPM's, as unpopular a notion as that may be in some circles. They represent a design from a quarter century ago that struggles to have relevance with our current system architectures.
If a TPM is present, TSEM will extend the security coefficients for the root modeling namespace into a PCR to establish a root of trust that the rest of the trust orchestration system can be built on. Ours is a worst case scenario beyond IMA since there is a coefficient generated for each LSM call that is being modeled.
We had to go to asynchronous updates through an ordered workqueue in order to have something less than abysmal performance, even with vTPM's running in a Xen hypervisor domain. This is without the current performance impacts being discussed with respect to HMAC based TPM session authentication.
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.
That, in and of itself, is certainly not the end of the world.
With TSEM we offer the notion of the 'state' of a security namespace, which is the extension sum of the security coefficients after they have been sorted in natural (big-endian) hash order. In this model you know what files have been accessed but you do not have a statement on temporal ordering of access.
Given scheduling artifacts, let alone the almost absolute ubiquity of multi-core, the simple TPM/TCG linear extension model seems to struggle with respect to any relevancy as a security metric.
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.
As we've noted above, TSEM would also be a potential consumer, which is why we wanted to seek clarifications on the architecture.
We've reviewed the patch set and the documentation, and will freely admit that we may still misunderstand all of this, but it would seem that the architecture, as it stands, would be subject to Time Of Measurement Time Of Use (TOMTOU) challenges.
The Time Of Measurement will be when the distribution generates an RPM, or equivalent construct, ie. .deb, and signs the digest list with their packaging key. What is elusive to us is how can their be an expectation that the file, on medium, when accessed (Time Of Use), matches the digest of the file that was signed by the distribution?
At a minimum, there would seem to be a need to have the kernel read and validate the on medium checksum of the file, as the in-kernel RPM parser reads each signature from the package list. At that point, as long as the kernel is running, the digest cache will represent a valid statement on the cryptographic checksum of a file held in the digest cache, as your patch series seem to have invalidation support well in hand.
After a system reboot, it would seem to be that all bets are off, and from a security perspective, there would be a need to re-verify that the on medium file checksums match those from a signed digest list. IMA has the ability to do protection against offline modification but you are then back to a possibly expensive operation on each file access.
We see in the thread on PGP infrastructure in the kernel you make the following statement:
"If the calculated digest of a file being accessed matches one extracted from the RPM header, access is granted otherwise it is denied."
Which would seem to imply that you do compute the on-medium checksum of each file and verify it against a reference value from the RPM header, but it isn't clear where that happens in the patch series. The only kernel based file read operation we could find is what appears to be a call to read the digest list files.
IMA already has the concept of a digest cache, as does TSEM. If you need to read a file in order to match its medium based checksum against the value from a package list, in order to avoid a TOMTOU condition, it is unclear how one gains a performance improvement. Unless of course the objective is to prime the digest cache at boot so that all subsequent integrity verifications are answered from cache rather than by computing the checksum at file access time.
In the thread on PGP access you indicate that all of this needs to be in the kernel in order to be tamper proof. FWIW, the kernel has the ability to know if kernel + userspace should be trusted at any given time, that is one of the security statements that we seek to offer with TSEM.
If the kernel can make a judgement, that in a limited execution context, such as system boot and initialization, that userspace has not acted in an untrusted manner, it can punt verification and parsing of RPM headers and priming of something like the digest cache to userspace.
Again, apologies if we misunderstand the architecture, any clarifications would be appreciated.
Have a good 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