linux-security-module.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH v4 00/14] security: digest_cache LSM
@ 2024-04-15 14:24 Roberto Sassu
  2024-04-15 14:24 ` [PATCH v4 01/14] lib: Add TLV parser Roberto Sassu
                   ` (15 more replies)
  0 siblings, 16 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

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 the large user
base, due to requiring higher than desired constraints on performance,
flexibility and configurability, that only security conscious people are
willing to accept.

This is where the new digest_cache LSM comes into play, it offers
additional support for new and existing integrity solutions, to make
them faster and easier to deploy.

The full documentation with the motivation and the solution details can be
found in patch 14.

The IMA integration patch set will be introduced separately. Also a PoC
based on the current version of IPE can be provided.

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
  security: Introduce the digest_cache LSM
  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
  digest_cache: Notify digest cache events
  selftests/digest_cache: Add selftests for digest_cache LSM
  docs: Add documentation of the digest_cache LSM

 Documentation/security/digest_cache.rst       | 763 ++++++++++++++++
 Documentation/security/index.rst              |   1 +
 MAINTAINERS                                   |  16 +
 include/linux/digest_cache.h                  | 117 +++
 include/linux/kernel_read_file.h              |   1 +
 include/linux/tlv_parser.h                    |  28 +
 include/uapi/linux/lsm.h                      |   1 +
 include/uapi/linux/tlv_digest_list.h          |  72 ++
 include/uapi/linux/tlv_parser.h               |  59 ++
 include/uapi/linux/xattr.h                    |   6 +
 lib/Kconfig                                   |   3 +
 lib/Makefile                                  |   3 +
 lib/tlv_parser.c                              | 214 +++++
 lib/tlv_parser.h                              |  17 +
 security/Kconfig                              |  11 +-
 security/Makefile                             |   1 +
 security/digest_cache/Kconfig                 |  33 +
 security/digest_cache/Makefile                |  11 +
 security/digest_cache/dir.c                   | 252 ++++++
 security/digest_cache/htable.c                | 268 ++++++
 security/digest_cache/internal.h              | 290 +++++++
 security/digest_cache/main.c                  | 570 ++++++++++++
 security/digest_cache/modsig.c                |  66 ++
 security/digest_cache/notifier.c              | 135 +++
 security/digest_cache/parsers/parsers.h       |  15 +
 security/digest_cache/parsers/rpm.c           | 223 +++++
 security/digest_cache/parsers/tlv.c           | 299 +++++++
 security/digest_cache/populate.c              | 163 ++++
 security/digest_cache/reset.c                 | 235 +++++
 security/digest_cache/secfs.c                 |  87 ++
 security/digest_cache/verif.c                 | 119 +++
 security/security.c                           |   3 +-
 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 | 815 ++++++++++++++++++
 tools/testing/selftests/digest_cache/common.c |  78 ++
 tools/testing/selftests/digest_cache/common.h | 135 +++
 .../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     | 564 ++++++++++++
 .../selftests/lsm/lsm_list_modules_test.c     |   3 +
 46 files changed, 6047 insertions(+), 6 deletions(-)
 create mode 100644 Documentation/security/digest_cache.rst
 create mode 100644 include/linux/digest_cache.h
 create mode 100644 include/linux/tlv_parser.h
 create mode 100644 include/uapi/linux/tlv_digest_list.h
 create mode 100644 include/uapi/linux/tlv_parser.h
 create mode 100644 lib/tlv_parser.c
 create mode 100644 lib/tlv_parser.h
 create mode 100644 security/digest_cache/Kconfig
 create mode 100644 security/digest_cache/Makefile
 create mode 100644 security/digest_cache/dir.c
 create mode 100644 security/digest_cache/htable.c
 create mode 100644 security/digest_cache/internal.h
 create mode 100644 security/digest_cache/main.c
 create mode 100644 security/digest_cache/modsig.c
 create mode 100644 security/digest_cache/notifier.c
 create mode 100644 security/digest_cache/parsers/parsers.h
 create mode 100644 security/digest_cache/parsers/rpm.c
 create mode 100644 security/digest_cache/parsers/tlv.c
 create mode 100644 security/digest_cache/populate.c
 create mode 100644 security/digest_cache/reset.c
 create mode 100644 security/digest_cache/secfs.c
 create mode 100644 security/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

-- 
2.34.1


^ permalink raw reply	[flat|nested] 43+ messages in thread

* [PATCH v4 01/14] lib: Add TLV parser
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 19:19   ` Jarkko Sakkinen
  2024-04-15 14:24 ` [PATCH v4 02/14] security: Introduce the digest_cache LSM Roberto Sassu
                   ` (14 subsequent siblings)
  15 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

Add a parser of a generic 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) |
+--------------+------------+------------------+

Each adopter can define its own data types and fields. The TLV parser does
not need to be aware of those, and calls a callback function with the
callback data, both supplied by the adopter, for every encountered field
during parsing. The adopter can decide in the callback function how each
defined field should be handled/parsed.

Normally, calling tlv_parse() is sufficient for most of the use cases. In
addition, tlv_parse_hdr() and tlv_parse_data() are also provided for more
advanced use cases.

Nesting TLVs is also possible, a 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      |  28 +++++
 include/uapi/linux/tlv_parser.h |  59 +++++++++
 lib/Kconfig                     |   3 +
 lib/Makefile                    |   3 +
 lib/tlv_parser.c                | 214 ++++++++++++++++++++++++++++++++
 lib/tlv_parser.h                |  17 +++
 7 files changed, 332 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 96a3b60cfc67..b1ca23ab8732 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22148,6 +22148,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..565743b3cb30
--- /dev/null
+++ b/include/linux/tlv_parser.h
@@ -0,0 +1,28 @@
+/* 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 int (*parse_callback)(void *, __u64, const __u8 *, __u64);
+
+int tlv_parse_hdr(const __u8 **data, size_t *data_len, __u64 *parsed_data_type,
+		  __u64 *parsed_num_entries, __u64 *parsed_total_len,
+		  const char **data_types, __u64 num_data_types);
+int tlv_parse_data(parse_callback callback, void *callback_data,
+		   __u64 num_entries, const __u8 *data, size_t data_len,
+		   const char **fields, __u64 num_fields);
+int tlv_parse(__u64 expected_data_type, parse_callback callback,
+	      void *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..3968c96f2518
--- /dev/null
+++ b/include/uapi/linux/tlv_parser.h
@@ -0,0 +1,59 @@
+/* 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) |
+ * +--------------+------------+------------------+
+ */
+
+/**
+ * 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 entry of the data part of TLV data format.
+ */
+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 5ddda7c2ed9b..d4d3c4435427 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -785,3 +785,6 @@ config POLYNOMIAL
 
 config FIRMWARE_TABLE
 	bool
+
+config TLV_PARSER
+	bool
diff --git a/lib/Makefile b/lib/Makefile
index 6b09731d8e61..23f0b770a639 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -444,3 +444,6 @@ $(obj)/$(TEST_FORTIFY_LOG): $(addprefix $(obj)/, $(TEST_FORTIFY_LOGS)) FORCE
 ifeq ($(CONFIG_FORTIFY_SOURCE),y)
 $(obj)/string.o: $(obj)/$(TEST_FORTIFY_LOG)
 endif
+
+obj-$(CONFIG_TLV_PARSER) += tlv_parser.o
+CFLAGS_tlv_parser.o += -I lib
diff --git a/lib/tlv_parser.c b/lib/tlv_parser.c
new file mode 100644
index 000000000000..9565542932eb
--- /dev/null
+++ b/lib/tlv_parser.c
@@ -0,0 +1,214 @@
+// 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
+ * @data: Data to parse (updated)
+ * @data_len: Length of @data (updated)
+ * @parsed_data_type: Parsed data type (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 data type, number of entries and the total data length extracted from the
+ * header.
+ *
+ * Return: Zero on success, a negative value on error.
+ */
+int tlv_parse_hdr(const __u8 **data, size_t *data_len, __u64 *parsed_data_type,
+		  __u64 *parsed_num_entries, __u64 *parsed_total_len,
+		  const char **data_types, __u64 num_data_types)
+{
+	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 0;
+}
+
+/**
+ * tlv_parse_data - Parse TLV data
+ * @callback: Callback function to call to parse the entries
+ * @callback_data: Opaque data to supply to the 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 callback function decides how to process data depending on the field.
+ *
+ * Return: Zero on success, a negative value on error.
+ */
+int tlv_parse_data(parse_callback callback, void *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);
+
+	/* Finite termination on num_entries. */
+	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 = callback(callback_data, parsed_field, data_ptr, len);
+		if (ret < 0) {
+			pr_debug("Parsing of field %s failed, ret: %d\n",
+				 fields[parsed_field], ret);
+			return ret;
+		}
+
+		data_ptr += len;
+		data_len -= len;
+	}
+
+	if (data_len) {
+		pr_debug("Excess data: %lu bytes\n", data_len);
+		return -EBADMSG;
+	}
+
+	return 0;
+}
+
+/**
+ * tlv_parse - Parse data in TLV format
+ * @expected_data_type: Desired data type
+ * @callback: Callback function to call to parse the data entries
+ * @callback_data: Opaque data to supply to the 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 the header has
+ * the same data type as the expected one.
+ *
+ * Return: Zero on success, a negative value on error.
+ */
+int tlv_parse(__u64 expected_data_type, parse_callback callback,
+	      void *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_data_type, parsed_num_entries, parsed_total_len;
+	const __u8 *data_ptr = data;
+	int ret = 0;
+
+	pr_debug("Start parsing data blob, size: %lu, expected data type: %s\n",
+		 data_len, data_types[expected_data_type]);
+
+	while (data_len) {
+		ret = tlv_parse_hdr(&data_ptr, &data_len, &parsed_data_type,
+				    &parsed_num_entries, &parsed_total_len,
+				    data_types, num_data_types);
+		if (ret < 0)
+			goto out;
+
+		/* Skip data with a different data type than expected. */
+		if (parsed_data_type != expected_data_type) {
+			/*
+			 * tlv_parse_hdr() already checked that
+			 * parsed_total_len <= data_len.
+			 */
+			data_ptr += parsed_total_len;
+			data_len -= parsed_total_len;
+			continue;
+		}
+
+		pr_debug("Found data type %s at offset %ld\n",
+			 data_types[parsed_data_type], data_ptr - data);
+
+		ret = tlv_parse_data(callback, 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 */
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 02/14] security: Introduce the digest_cache LSM
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
  2024-04-15 14:24 ` [PATCH v4 01/14] lib: Add TLV parser Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 19:31   ` Jarkko Sakkinen
  2024-04-15 14:24 ` [PATCH v4 03/14] digest_cache: Add securityfs interface Roberto Sassu
                   ` (13 subsequent siblings)
  15 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

Introduce the digest_cache LSM, to collect digests from various sources
(called digest lists), and to store them in kernel memory, in a set of hash
tables forming a digest cache. Extracted digests can be used as reference
values for integrity verification of file data or metadata.

A digest cache has three types of references: in the inode security blob of
the digest list the digest cache was created from (dig_owner field); in the
security blob of the inodes for which the digest cache is requested
(dig_user field); a reference returned by digest_cache_get().

References are released with digest_cache_put(), in the first two cases
when inodes are evicted from memory, in the last case when that function is
explicitly called. Obtaining a digest cache reference means that the digest
cache remains valid and cannot be freed until releasing it and until the
total number of references (stored in the digest cache) becomes zero.

When digest_cache_get() is called on an inode to compare its digest with
a reference value, the digest_cache LSM knows which digest cache to get
from the new security.digest_list xattr added to that inode, which contains
the file name of the desired digest list digests will be extracted from.

All digest lists are expected to be in the same directory, defined in the
kernel config, and modifiable (with a later patch) at run-time through
securityfs. When the digest_cache LSM reads the security.digest_list xattr,
it uses its value as last path component, appended to the default path
(unless the default path is a file). If an inode does not have that xattr,
the default path is considered as the final destination.

The default path can be either a file or a directory. If it is a file, the
digest_cache LSM always uses the same digest cache from that file to verify
all inodes (the xattr, if present, is ignored). If it is a directory, and
the inode to verify does not have the xattr, a subsequent patch will make
it possible to iterate and lookup on the digest caches created from each
directory entry.

Digest caches are created on demand, only when digest_cache_get() is
called. The first time a digest cache is requested, the digest_cache LSM
creates it and sets its reference in the dig_owner and dig_user fields of
the respective inode security blobs. On the next requests, the previously
set reference is returned, after incrementing the reference count.

Since there might be multiple digest_cache_get() calls for the same inode,
or for different inodes pointing to the same digest list, dig_owner_mutex
and dig_user_mutex have been introduced to protect the check and assignment
of the digest cache reference in the inode security blob.

Contenders that didn't get the lock also have to wait until the digest
cache is fully instantiated (when the bit INIT_IN_PROGRESS is cleared).
Dig_owner_mutex cannot be used for waiting on the instantiation to avoid
lock inversion with the inode lock for directories.

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 MAINTAINERS                                   |   6 +
 include/linux/digest_cache.h                  |  32 ++
 include/uapi/linux/lsm.h                      |   1 +
 include/uapi/linux/xattr.h                    |   3 +
 security/Kconfig                              |  11 +-
 security/Makefile                             |   1 +
 security/digest_cache/Kconfig                 |  16 +
 security/digest_cache/Makefile                |   7 +
 security/digest_cache/internal.h              |  86 ++++
 security/digest_cache/main.c                  | 404 ++++++++++++++++++
 security/security.c                           |   3 +-
 .../selftests/lsm/lsm_list_modules_test.c     |   3 +
 12 files changed, 567 insertions(+), 6 deletions(-)
 create mode 100644 include/linux/digest_cache.h
 create mode 100644 security/digest_cache/Kconfig
 create mode 100644 security/digest_cache/Makefile
 create mode 100644 security/digest_cache/internal.h
 create mode 100644 security/digest_cache/main.c

diff --git a/MAINTAINERS b/MAINTAINERS
index b1ca23ab8732..72801a88449c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6193,6 +6193,12 @@ L:	linux-gpio@vger.kernel.org
 S:	Maintained
 F:	drivers/gpio/gpio-gpio-mm.c
 
+DIGEST_CACHE LSM
+M:	Roberto Sassu <roberto.sassu@huawei.com>
+L:	linux-security-module@vger.kernel.org
+S:	Maintained
+F:	security/digest_cache/
+
 DIGITEQ AUTOMOTIVE MGB4 V4L2 DRIVER
 M:	Martin Tuma <martin.tuma@digiteqautomotive.com>
 L:	linux-media@vger.kernel.org
diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
new file mode 100644
index 000000000000..e79f94a60b0f
--- /dev/null
+++ b/include/linux/digest_cache.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@huawei.com>
+ *
+ * Public API of the digest_cache LSM.
+ */
+
+#ifndef _LINUX_DIGEST_CACHE_H
+#define _LINUX_DIGEST_CACHE_H
+
+#include <linux/fs.h>
+
+struct digest_cache;
+
+#ifdef CONFIG_SECURITY_DIGEST_CACHE
+struct digest_cache *digest_cache_get(struct dentry *dentry);
+void digest_cache_put(struct digest_cache *digest_cache);
+
+#else
+static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
+{
+	return NULL;
+}
+
+static inline void digest_cache_put(struct digest_cache *digest_cache)
+{
+}
+
+#endif /* CONFIG_SECURITY_DIGEST_CACHE */
+#endif /* _LINUX_DIGEST_CACHE_H */
diff --git a/include/uapi/linux/lsm.h b/include/uapi/linux/lsm.h
index 33d8c9f4aa6b..832b3aea5c26 100644
--- a/include/uapi/linux/lsm.h
+++ b/include/uapi/linux/lsm.h
@@ -64,6 +64,7 @@ struct lsm_ctx {
 #define LSM_ID_LANDLOCK		110
 #define LSM_ID_IMA		111
 #define LSM_ID_EVM		112
+#define LSM_ID_DIGEST_CACHE	113
 
 /*
  * LSM_ATTR_XXX definitions identify different LSM attributes
diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h
index 9463db2dfa9d..8a58cf4bce65 100644
--- a/include/uapi/linux/xattr.h
+++ b/include/uapi/linux/xattr.h
@@ -54,6 +54,9 @@
 #define XATTR_IMA_SUFFIX "ima"
 #define XATTR_NAME_IMA XATTR_SECURITY_PREFIX XATTR_IMA_SUFFIX
 
+#define XATTR_DIGEST_LIST_SUFFIX "digest_list"
+#define XATTR_NAME_DIGEST_LIST XATTR_SECURITY_PREFIX XATTR_DIGEST_LIST_SUFFIX
+
 #define XATTR_SELINUX_SUFFIX "selinux"
 #define XATTR_NAME_SELINUX XATTR_SECURITY_PREFIX XATTR_SELINUX_SUFFIX
 
diff --git a/security/Kconfig b/security/Kconfig
index 52c9af08ad35..99f99cbd94cc 100644
--- a/security/Kconfig
+++ b/security/Kconfig
@@ -194,6 +194,7 @@ source "security/yama/Kconfig"
 source "security/safesetid/Kconfig"
 source "security/lockdown/Kconfig"
 source "security/landlock/Kconfig"
+source "security/digest_cache/Kconfig"
 
 source "security/integrity/Kconfig"
 
@@ -233,11 +234,11 @@ endchoice
 
 config LSM
 	string "Ordered list of enabled LSMs"
-	default "landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
-	default "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
-	default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
-	default "landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
-	default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
+	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
+	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
+	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
+	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
+	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
 	help
 	  A comma-separated list of LSMs, in initialization order.
 	  Any LSMs left off this list, except for those with order
diff --git a/security/Makefile b/security/Makefile
index 59f238490665..e9b43e7b715a 100644
--- a/security/Makefile
+++ b/security/Makefile
@@ -25,6 +25,7 @@ obj-$(CONFIG_SECURITY_LOCKDOWN_LSM)	+= lockdown/
 obj-$(CONFIG_CGROUPS)			+= device_cgroup.o
 obj-$(CONFIG_BPF_LSM)			+= bpf/
 obj-$(CONFIG_SECURITY_LANDLOCK)		+= landlock/
+obj-$(CONFIG_SECURITY_DIGEST_CACHE)	+= digest_cache/
 
 # Object integrity file lists
 obj-$(CONFIG_INTEGRITY)			+= integrity/
diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
new file mode 100644
index 000000000000..e53fbf0779d6
--- /dev/null
+++ b/security/digest_cache/Kconfig
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0
+config SECURITY_DIGEST_CACHE
+	bool "Digest_cache LSM"
+	default n
+	help
+	  This option enables an LSM maintaining a cache of digests
+	  (e.g. of file data or metadata).
+
+	  This LSM can support other kernel components in making access
+	  control decisions.
+
+config DIGEST_LIST_DEFAULT_PATH
+	string
+	default "/etc/digest_lists"
+	help
+	  Default directory where digest_cache LSM expects to find digest lists.
diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
new file mode 100644
index 000000000000..48848c41253e
--- /dev/null
+++ b/security/digest_cache/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Makefile for building the digest_cache LSM.
+
+obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
+
+digest_cache-y := main.o
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
new file mode 100644
index 000000000000..5f04844af3a5
--- /dev/null
+++ b/security/digest_cache/internal.h
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@huawei.com>
+ *
+ * Internal header of the digest_cache LSM.
+ */
+
+#ifndef _DIGEST_CACHE_INTERNAL_H
+#define _DIGEST_CACHE_INTERNAL_H
+
+#include <linux/lsm_hooks.h>
+#include <linux/digest_cache.h>
+
+/* Digest cache bits in flags. */
+#define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
+
+/**
+ * struct digest_cache - Digest cache
+ * @ref_count: Number of references to the digest cache
+ * @path_str: Path of the digest list the digest cache was created from
+ * @flags: Control flags
+ *
+ * This structure represents a cache of digests extracted from a digest list.
+ */
+struct digest_cache {
+	atomic_t ref_count;
+	char *path_str;
+	unsigned long flags;
+};
+
+/**
+ * struct digest_cache_security - Digest cache pointers in inode security blob
+ * @dig_owner: Digest cache created from this inode
+ * @dig_owner_mutex: Protects @dig_owner
+ * @dig_user: Digest cache requested for this inode
+ * @dig_user_mutex: Protects @dig_user
+ *
+ * This structure contains references to digest caches, protected by their
+ * respective mutex.
+ */
+struct digest_cache_security {
+	struct digest_cache *dig_owner;
+	struct mutex dig_owner_mutex;
+	struct digest_cache *dig_user;
+	struct mutex dig_user_mutex;
+};
+
+extern struct lsm_blob_sizes digest_cache_blob_sizes;
+extern char *default_path_str;
+
+static inline struct digest_cache_security *
+digest_cache_get_security(const struct inode *inode)
+{
+	if (unlikely(!inode->i_security))
+		return NULL;
+
+	return inode->i_security + digest_cache_blob_sizes.lbs_inode;
+}
+
+static inline struct digest_cache *
+digest_cache_ref(struct digest_cache *digest_cache)
+{
+	atomic_inc(&digest_cache->ref_count);
+	pr_debug("Ref (+) digest cache %s (ref count: %d)\n",
+		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
+	return digest_cache;
+}
+
+static inline struct digest_cache *
+digest_cache_unref(struct digest_cache *digest_cache)
+{
+	bool ref_is_zero = atomic_dec_and_test(&digest_cache->ref_count);
+
+	pr_debug("Ref (-) digest cache %s (ref count: %d)\n",
+		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
+	return (ref_is_zero) ? digest_cache : NULL;
+}
+
+/* main.c */
+struct digest_cache *digest_cache_create(struct dentry *dentry,
+					 struct path *digest_list_path,
+					 char *path_str, char *filename);
+
+#endif /* _DIGEST_CACHE_INTERNAL_H */
diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
new file mode 100644
index 000000000000..14dba8915e99
--- /dev/null
+++ b/security/digest_cache/main.c
@@ -0,0 +1,404 @@
+// 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 digest_cache LSM.
+ */
+
+#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 = 1;
+static struct kmem_cache *digest_cache_cache __read_mostly;
+
+char *default_path_str = CONFIG_DIGEST_LIST_DEFAULT_PATH;
+
+/**
+ * digest_cache_alloc_init - Allocate and initialize a new digest cache
+ * @path_str: Path string of the digest list
+ * @filename: Digest list file name (can be an empty string)
+ *
+ * This function allocates and initializes a new digest cache.
+ *
+ * Return: A digest_cache structure on success, NULL on error.
+ */
+static struct digest_cache *digest_cache_alloc_init(char *path_str,
+						    char *filename)
+{
+	struct digest_cache *digest_cache;
+
+	digest_cache = kmem_cache_alloc(digest_cache_cache, GFP_KERNEL);
+	if (!digest_cache)
+		return digest_cache;
+
+	digest_cache->path_str = kasprintf(GFP_KERNEL, "%s%s%s", path_str,
+					   filename[0] ? "/" : "", filename);
+	if (!digest_cache->path_str) {
+		kmem_cache_free(digest_cache_cache, digest_cache);
+		return NULL;
+	}
+
+	atomic_set(&digest_cache->ref_count, 1);
+	digest_cache->flags = 0UL;
+
+	pr_debug("New digest cache %s (ref count: %d)\n",
+		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
+
+	return digest_cache;
+}
+
+/**
+ * digest_cache_free - Free all memory occupied by the digest cache
+ * @digest_cache: Digest cache
+ *
+ * This function frees the memory occupied by the digest cache.
+ */
+static void digest_cache_free(struct digest_cache *digest_cache)
+{
+	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
+	kfree(digest_cache->path_str);
+	kmem_cache_free(digest_cache_cache, digest_cache);
+}
+
+/**
+ * digest_cache_create - Create a digest cache
+ * @dentry: Dentry of the inode for which the digest cache will be used
+ * @digest_list_path: Path structure of the digest list
+ * @path_str: Path string of the digest list
+ * @filename: Digest list file name (can be an empty string)
+ *
+ * This function first locates, from the passed path, the digest list inode
+ * from which the digest cache will be created or retrieved (if it already
+ * exists).
+ *
+ * If dig_owner is NULL in the inode security blob, this function creates a
+ * new digest cache with reference count set to 1 (reference returned), sets
+ * it to dig_owner and consequently increments again the digest cache reference
+ * count.
+ *
+ * Otherwise, it simply increments the reference count of the existing
+ * dig_owner, since that reference is returned to the caller.
+ *
+ * Incrementing the reference count twice before calling path_put() ensures
+ * that the digest cache returned is valid even if the inode is evicted from
+ * memory (which decreases the reference count).
+ *
+ * Releasing the dig_owner_mutex lock does not mean that the digest cache is
+ * ready for use. digest_cache_create() callers that found a partially
+ * instantiated digest cache have to wait until the INIT_IN_PROGRESS bit is
+ * cleared by the caller that is actually creating that digest cache.
+ *
+ * Return: A new digest cache on success, NULL on error.
+ */
+struct digest_cache *digest_cache_create(struct dentry *dentry,
+					 struct path *digest_list_path,
+					 char *path_str, char *filename)
+{
+	struct path file_path;
+	struct digest_cache *digest_cache = NULL;
+	struct digest_cache_security *dig_sec;
+	struct inode *inode = d_backing_inode(digest_list_path->dentry);
+	bool dig_owner_exists = false;
+	int ret;
+
+	if (S_ISDIR(d_backing_inode(digest_list_path->dentry)->i_mode) &&
+	    filename[0]) {
+		ret = vfs_path_lookup(digest_list_path->dentry,
+				      digest_list_path->mnt, filename, 0,
+				      &file_path);
+		if (ret < 0) {
+			pr_debug("Cannot find digest list %s/%s\n", path_str,
+				 filename);
+			return NULL;
+		}
+
+		digest_list_path = &file_path;
+		inode = d_backing_inode(file_path.dentry);
+
+		/*
+		 * Cannot request a digest cache for the same inode the
+		 * digest cache is populated from.
+		 */
+		if (d_backing_inode(dentry) == inode) {
+			pr_debug("Cannot request a digest cache for %s and use it as digest list\n",
+				 dentry->d_name.name);
+			goto out;
+		}
+
+		/* 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",
+				 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);
+		dig_owner_exists = true;
+		mutex_unlock(&dig_sec->dig_owner_mutex);
+		goto exists;
+	}
+
+	/* Ref. count is already 1 for this reference. */
+	digest_cache = digest_cache_alloc_init(path_str, filename);
+	if (!digest_cache) {
+		mutex_unlock(&dig_sec->dig_owner_mutex);
+		goto out;
+	}
+
+	/* Increment ref. count for reference set to dig_owner. */
+	dig_sec->dig_owner = digest_cache_ref(digest_cache);
+
+	/* Make the other lock contenders wait until creation complete. */
+	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
+	mutex_unlock(&dig_sec->dig_owner_mutex);
+
+	/* Creation complete, notify the other lock contenders. */
+	clear_and_wake_up_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
+exists:
+	if (dig_owner_exists)
+		/* Wait until creation complete. */
+		wait_on_bit(&dig_sec->dig_owner->flags, INIT_IN_PROGRESS,
+			    TASK_UNINTERRUPTIBLE);
+out:
+	if (digest_list_path == &file_path)
+		path_put(&file_path);
+
+	return digest_cache;
+}
+
+/**
+ * digest_cache_new - Retrieve digest list file name and request digest cache
+ * @dentry: Dentry of the inode for which the digest cache will be used
+ *
+ * This function locates the default path. If it is a file, it directly creates
+ * a digest cache from it. Otherwise, it reads the digest list file name from
+ * the security.digest_list xattr and requests the creation of a digest cache
+ * with that file name. If security.digest_list is not found, this function
+ * requests the creation of a digest cache on the parent directory.
+ *
+ * Return: A new digest cache on success, NULL on error.
+ */
+static struct digest_cache *digest_cache_new(struct dentry *dentry)
+{
+	char filename[NAME_MAX + 1] = { 0 };
+	struct digest_cache *digest_cache = NULL;
+	struct path default_path;
+	int ret;
+
+	ret = kern_path(default_path_str, 0, &default_path);
+	if (ret < 0) {
+		pr_debug("Cannot find path %s\n", default_path_str);
+		return NULL;
+	}
+
+	/* The default path is a file, no need to get xattr. */
+	if (S_ISREG(d_backing_inode(default_path.dentry)->i_mode)) {
+		pr_debug("Default path %s is a file, not reading %s xattr\n",
+			 default_path_str, XATTR_NAME_DIGEST_LIST);
+		goto create;
+	} else if (!S_ISDIR(d_backing_inode(default_path.dentry)->i_mode)) {
+		pr_debug("Default path %s must be either a file or a directory\n",
+			 default_path_str);
+		goto out;
+	}
+
+	ret = vfs_getxattr(&nop_mnt_idmap, dentry, XATTR_NAME_DIGEST_LIST,
+			   filename, sizeof(filename) - 1);
+	if (ret <= 0) {
+		pr_debug("Digest list path not found for file %s, using %s\n",
+			 dentry->d_name.name, default_path_str);
+		goto create;
+	}
+
+	if (strchr(filename, '/')) {
+		pr_debug("%s xattr should contain only a file name, got: %s\n",
+			 XATTR_NAME_DIGEST_LIST, filename);
+		goto out;
+	}
+
+	pr_debug("Found %s xattr in %s, default path: %s, digest list: %s\n",
+		 XATTR_NAME_DIGEST_LIST, dentry->d_name.name, default_path_str,
+		 filename);
+create:
+	digest_cache = digest_cache_create(dentry, &default_path,
+					   default_path_str, filename);
+out:
+	path_put(&default_path);
+	return digest_cache;
+}
+
+/**
+ * digest_cache_get - Get a digest cache for a given inode
+ * @dentry: Dentry of the inode for which the digest cache will be used
+ *
+ * This function tries to find a digest cache from the inode security blob of
+ * the passed dentry (dig_user field). If a digest cache was not found, it calls
+ * digest_cache_new() to create a new one. In both cases, it increments the
+ * digest cache reference count before returning the reference to the caller.
+ *
+ * The caller is responsible to call digest_cache_put() to release the digest
+ * cache reference returned.
+ *
+ * Lock dig_user_mutex to protect against concurrent requests to obtain a digest
+ * cache for the same inode, and to make other contenders wait until the first
+ * requester finishes the process.
+ *
+ * Return: A digest cache on success, NULL otherwise.
+ */
+struct digest_cache *digest_cache_get(struct dentry *dentry)
+{
+	struct digest_cache_security *dig_sec;
+	struct digest_cache *digest_cache = NULL;
+	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);
+
+struct lsm_blob_sizes digest_cache_blob_sizes __ro_after_init = {
+	.lbs_inode = sizeof(struct digest_cache_security),
+};
+
+/**
+ * digest_cache_inode_alloc_security - Initialize inode security blob
+ * @inode: Inode for which the security blob is initialized
+ *
+ * This function initializes the digest_cache_security structure, directly
+ * stored in the inode security blob.
+ *
+ * Return: Zero.
+ */
+static int digest_cache_inode_alloc_security(struct inode *inode)
+{
+	struct digest_cache_security *dig_sec;
+
+	/* The inode security blob is always allocated here. */
+	dig_sec = digest_cache_get_security(inode);
+	mutex_init(&dig_sec->dig_owner_mutex);
+	mutex_init(&dig_sec->dig_user_mutex);
+	return 0;
+}
+
+/**
+ * digest_cache_inode_free_security - Release the digest cache references
+ * @inode: Inode for which the digest cache references are released
+ *
+ * Since the inode is being evicted, this function releases the non-needed
+ * references to the digest caches stored in the digest_cache_security
+ * structure.
+ */
+static void digest_cache_inode_free_security(struct inode *inode)
+{
+	struct digest_cache_security *dig_sec;
+
+	dig_sec = digest_cache_get_security(inode);
+	if (!dig_sec)
+		return;
+
+	mutex_destroy(&dig_sec->dig_owner_mutex);
+	mutex_destroy(&dig_sec->dig_user_mutex);
+	if (dig_sec->dig_owner)
+		digest_cache_put(dig_sec->dig_owner);
+	if (dig_sec->dig_user)
+		digest_cache_put(dig_sec->dig_user);
+}
+
+static struct security_hook_list digest_cache_hooks[] __ro_after_init = {
+	LSM_HOOK_INIT(inode_alloc_security, digest_cache_inode_alloc_security),
+	LSM_HOOK_INIT(inode_free_security, digest_cache_inode_free_security),
+};
+
+/**
+ * digest_cache_init_once - Initialize the digest cache structure
+ * @foo: Digest cache structure to initialize
+ *
+ * This function fills the digest cache structure with zeros.
+ */
+static void digest_cache_init_once(void *foo)
+{
+	struct digest_cache *digest_cache = (struct digest_cache *)foo;
+
+	memset(digest_cache, 0, sizeof(*digest_cache));
+}
+
+static const struct lsm_id digest_cache_lsmid = {
+	.name = "digest_cache",
+	.id = LSM_ID_DIGEST_CACHE,
+};
+
+/**
+ * digest_cache_init - Initialize the digest_cache LSM
+ *
+ * Initialize the digest_cache LSM, by instantiating a cache for the
+ * digest_cache structure and by registering the digest_cache LSM hooks.
+ */
+static int __init digest_cache_init(void)
+{
+	digest_cache_cache = kmem_cache_create("digest_cache_cache",
+					       sizeof(struct digest_cache),
+					       0, SLAB_PANIC,
+					       digest_cache_init_once);
+
+	security_add_hooks(digest_cache_hooks, ARRAY_SIZE(digest_cache_hooks),
+			   &digest_cache_lsmid);
+	return 0;
+}
+
+DEFINE_LSM(digest_cache) = {
+	.name = "digest_cache",
+	.enabled = &digest_cache_enabled,
+	.init = digest_cache_init,
+	.blobs = &digest_cache_blob_sizes,
+};
diff --git a/security/security.c b/security/security.c
index cbdc9bebe802..cb084ed58617 100644
--- a/security/security.c
+++ b/security/security.c
@@ -50,7 +50,8 @@
 	(IS_ENABLED(CONFIG_BPF_LSM) ? 1 : 0) + \
 	(IS_ENABLED(CONFIG_SECURITY_LANDLOCK) ? 1 : 0) + \
 	(IS_ENABLED(CONFIG_IMA) ? 1 : 0) + \
-	(IS_ENABLED(CONFIG_EVM) ? 1 : 0))
+	(IS_ENABLED(CONFIG_EVM) ? 1 : 0) + \
+	(IS_ENABLED(CONFIG_SECURITY_DIGEST_CACHE) ? 1 : 0))
 
 /*
  * These are descriptions of the reasons that can be passed to the
diff --git a/tools/testing/selftests/lsm/lsm_list_modules_test.c b/tools/testing/selftests/lsm/lsm_list_modules_test.c
index 4d5d4cee2586..d00831edc582 100644
--- a/tools/testing/selftests/lsm/lsm_list_modules_test.c
+++ b/tools/testing/selftests/lsm/lsm_list_modules_test.c
@@ -128,6 +128,9 @@ TEST(correct_lsm_list_modules)
 		case LSM_ID_EVM:
 			name = "evm";
 			break;
+		case LSM_ID_DIGEST_CACHE:
+			name = "digest_cache";
+			break;
 		default:
 			name = "INVALID";
 			break;
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 03/14] digest_cache: Add securityfs interface
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
  2024-04-15 14:24 ` [PATCH v4 01/14] lib: Add TLV parser Roberto Sassu
  2024-04-15 14:24 ` [PATCH v4 02/14] security: Introduce the digest_cache LSM Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 19:32   ` Jarkko Sakkinen
  2024-04-15 14:24 ` [PATCH v4 04/14] digest_cache: Add hash tables and operations Roberto Sassu
                   ` (12 subsequent siblings)
  15 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

Add the digest_cache_path file in securityfs, 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/digest_cache/Kconfig    |  4 ++
 security/digest_cache/Makefile   |  2 +-
 security/digest_cache/internal.h |  1 +
 security/digest_cache/main.c     | 10 +++-
 security/digest_cache/secfs.c    | 87 ++++++++++++++++++++++++++++++++
 5 files changed, 102 insertions(+), 2 deletions(-)
 create mode 100644 security/digest_cache/secfs.c

diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
index e53fbf0779d6..dfabe5d6e3ca 100644
--- a/security/digest_cache/Kconfig
+++ b/security/digest_cache/Kconfig
@@ -14,3 +14,7 @@ config DIGEST_LIST_DEFAULT_PATH
 	default "/etc/digest_lists"
 	help
 	  Default directory where digest_cache LSM 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/digest_cache/Makefile b/security/digest_cache/Makefile
index 48848c41253e..1330655e33b1 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -4,4 +4,4 @@
 
 obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 
-digest_cache-y := main.o
+digest_cache-y := main.o secfs.o
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
index 5f04844af3a5..bbf5eefe5c82 100644
--- a/security/digest_cache/internal.h
+++ b/security/digest_cache/internal.h
@@ -49,6 +49,7 @@ struct digest_cache_security {
 
 extern struct lsm_blob_sizes digest_cache_blob_sizes;
 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)
diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
index 14dba8915e99..661c8d106791 100644
--- a/security/digest_cache/main.c
+++ b/security/digest_cache/main.c
@@ -18,6 +18,9 @@ static struct kmem_cache *digest_cache_cache __read_mostly;
 
 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
@@ -274,9 +277,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. */
@@ -386,6 +392,8 @@ static const struct lsm_id digest_cache_lsmid = {
  */
 static int __init digest_cache_init(void)
 {
+	init_rwsem(&default_path_sem);
+
 	digest_cache_cache = kmem_cache_create("digest_cache_cache",
 					       sizeof(struct digest_cache),
 					       0, SLAB_PANIC,
diff --git a/security/digest_cache/secfs.c b/security/digest_cache/secfs.c
new file mode 100644
index 000000000000..d3a37bf3588e
--- /dev/null
+++ b/security/digest_cache/secfs.c
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@huawei.com>
+ *
+ * Implement the securityfs interface of the digest_cache LSM.
+ */
+
+#define pr_fmt(fmt) "DIGEST CACHE: "fmt
+#include <linux/security.h>
+
+#include "internal.h"
+
+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,
+};
+
+static int __init digest_cache_path_init(void)
+{
+	default_path_dentry = securityfs_create_file("digest_cache_path", 0660,
+						     NULL, NULL,
+						     &default_path_ops);
+	if (IS_ERR(default_path_dentry))
+		return -EFAULT;
+
+	return 0;
+}
+
+late_initcall(digest_cache_path_init);
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 04/14] digest_cache: Add hash tables and operations
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (2 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 03/14] digest_cache: Add securityfs interface Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 19:36   ` Jarkko Sakkinen
  2024-04-15 14:24 ` [PATCH v4 05/14] digest_cache: Populate the digest cache from a digest list Roberto Sassu
                   ` (11 subsequent siblings)
  15 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

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 digest_cache LSM free the hash
tables at the time a digest cache is freed.

Add digest_cache_htable_lookup() to search a digest in the hash table of a
digest cache for a given algorithm.

Add digest_cache_lookup() to the public API, to let users of the
digest_cache LSM search a digest in a digest cache and, in a subsequent
patch, to search it in the digest caches for each directory entry.

Return the digest cache containing the digest, as a different type,
digest_cache_found_t to avoid it being accidentally put. Also, introduce
digest_cache_from_found_t() to explicitly convert it back to a digest cache
for further use (e.g. retrieving verification data, introduced later).

Finally, add digest_cache_hash_key() to compute the hash table key from the
first two bytes of the digest (modulo the number of slots).

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 include/linux/digest_cache.h     |  34 +++++
 security/digest_cache/Kconfig    |  11 ++
 security/digest_cache/Makefile   |   2 +-
 security/digest_cache/htable.c   | 250 +++++++++++++++++++++++++++++++
 security/digest_cache/internal.h |  43 ++++++
 security/digest_cache/main.c     |   3 +
 6 files changed, 342 insertions(+), 1 deletion(-)
 create mode 100644 security/digest_cache/htable.c

diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
index e79f94a60b0f..4872700ac205 100644
--- a/include/linux/digest_cache.h
+++ b/include/linux/digest_cache.h
@@ -11,12 +11,39 @@
 #define _LINUX_DIGEST_CACHE_H
 
 #include <linux/fs.h>
+#include <crypto/hash_info.h>
 
 struct digest_cache;
 
+/**
+ * typedef digest_cache_found_t - Digest cache reference as numeric value
+ *
+ * This new type represents a digest cache reference that should not be put.
+ */
+typedef unsigned long digest_cache_found_t;
+
+/**
+ * digest_cache_from_found_t - Convert digest_cache_found_t to digest cache ptr
+ * @found: digest_cache_found_t value
+ *
+ * Convert the digest_cache_found_t returned by digest_cache_lookup() to a
+ * digest cache pointer, so that it can be passed to the other functions of the
+ * API.
+ *
+ * Return: Digest cache pointer.
+ */
+static inline struct digest_cache *
+digest_cache_from_found_t(digest_cache_found_t found)
+{
+	return (struct digest_cache *)found;
+}
+
 #ifdef CONFIG_SECURITY_DIGEST_CACHE
 struct digest_cache *digest_cache_get(struct dentry *dentry);
 void digest_cache_put(struct digest_cache *digest_cache);
+digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
+					 struct digest_cache *digest_cache,
+					 u8 *digest, enum hash_algo algo);
 
 #else
 static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
@@ -28,5 +55,12 @@ static inline void digest_cache_put(struct digest_cache *digest_cache)
 {
 }
 
+static inline digest_cache_found_t
+digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache,
+		    u8 *digest, enum hash_algo algo)
+{
+	return 0UL;
+}
+
 #endif /* CONFIG_SECURITY_DIGEST_CACHE */
 #endif /* _LINUX_DIGEST_CACHE_H */
diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
index dfabe5d6e3ca..71017954e5c5 100644
--- a/security/digest_cache/Kconfig
+++ b/security/digest_cache/Kconfig
@@ -18,3 +18,14 @@ config DIGEST_LIST_DEFAULT_PATH
 	  It can be changed at run-time, by writing the new path to the
 	  securityfs interface. Digest caches created with the old path are
 	  not affected by the change.
+
+config DIGEST_CACHE_HTABLE_DEPTH
+	int
+	default 30
+	help
+	  Desired average depth of the collision list in the digest cache
+	  hash tables.
+
+	  A smaller number will increase the amount of hash table slots, and
+	  make the search faster. A bigger number will decrease the number of
+	  hash table slots, but make the search slower.
diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
index 1330655e33b1..7e00c53d8f55 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -4,4 +4,4 @@
 
 obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 
-digest_cache-y := main.o secfs.o
+digest_cache-y := main.o secfs.o htable.o
diff --git a/security/digest_cache/htable.c b/security/digest_cache/htable.c
new file mode 100644
index 000000000000..d2d5d8f5e5b1
--- /dev/null
+++ b/security/digest_cache/htable.c
@@ -0,0 +1,250 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <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 digest cache
+ * @algo: Algorithm of the digests
+ *
+ * This function allocates and initializes the hash table for a given algorithm.
+ * The number of slots depends on the number of digests to add to the digest
+ * cache, and the constant CONFIG_DIGEST_CACHE_HTABLE_DEPTH stating the desired
+ * average depth of the collision list.
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
+			     enum hash_algo algo)
+{
+	struct htable *h;
+	int i;
+
+	h = lookup_htable(digest_cache, algo);
+	if (h)
+		return 0;
+
+	h = kmalloc(sizeof(*h), GFP_KERNEL);
+	if (!h)
+		return -ENOMEM;
+
+	h->num_slots = DIV_ROUND_UP(num_digests,
+				    CONFIG_DIGEST_CACHE_HTABLE_DEPTH);
+	h->slots = kmalloc_array(h->num_slots, sizeof(*h->slots), GFP_KERNEL);
+	if (!h->slots) {
+		kfree(h);
+		return -ENOMEM;
+	}
+
+	for (i = 0; i < h->num_slots; i++)
+		INIT_HLIST_HEAD(&h->slots[i]);
+
+	h->num_digests = 0;
+	h->algo = algo;
+
+	list_add_tail(&h->next, &digest_cache->htables);
+
+	pr_debug("Initialized hash table for digest list %s, digests: %llu, slots: %u, algo: %s\n",
+		 digest_cache->path_str, num_digests, h->num_slots,
+		 hash_algo_name[algo]);
+	return 0;
+}
+
+/**
+ * digest_cache_htable_add - Add a new digest to the digest cache
+ * @digest_cache: Digest cache
+ * @digest: Digest to add
+ * @algo: Algorithm of digest
+ *
+ * This function, invoked by a digest list parser, adds a digest extracted
+ * from a digest list to the digest cache.
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest,
+			    enum hash_algo algo)
+{
+	struct htable *h;
+	struct digest_cache_entry *entry;
+	unsigned int key;
+	int digest_len;
+
+	h = lookup_htable(digest_cache, algo);
+	if (!h) {
+		pr_debug("No hash table for algorithm %s was found in digest cache %s, initialize one\n",
+			 hash_algo_name[algo], digest_cache->path_str);
+		return -ENOENT;
+	}
+
+	digest_len = hash_digest_size[algo];
+
+	entry = kmalloc(sizeof(*entry) + digest_len, GFP_KERNEL);
+	if (!entry)
+		return -ENOMEM;
+
+	memcpy(entry->digest, digest, digest_len);
+
+	key = digest_cache_hash_key(digest, h->num_slots);
+	hlist_add_head(&entry->hnext, &h->slots[key]);
+	h->num_digests++;
+	pr_debug("Added digest %s:%*phN to digest cache %s, num of %s digests: %llu\n",
+		 hash_algo_name[algo], digest_len, digest,
+		 digest_cache->path_str, hash_algo_name[algo], h->num_digests);
+	return 0;
+}
+
+/**
+ * digest_cache_htable_lookup - Search a digest in the digest cache
+ * @dentry: Dentry of the file whose digest is looked up
+ * @digest_cache: Digest cache
+ * @digest: Digest to search
+ * @algo: Algorithm of the digest to search
+ *
+ * This function searches the passed digest and algorithm in the passed digest
+ * cache.
+ *
+ * Return: Zero if the digest is found, -ENOENT if not.
+ */
+int digest_cache_htable_lookup(struct dentry *dentry,
+			       struct digest_cache *digest_cache, u8 *digest,
+			       enum hash_algo algo)
+{
+	struct digest_cache_entry *entry;
+	struct htable *h;
+	unsigned int key;
+	int digest_len;
+	int search_depth = 0;
+
+	h = lookup_htable(digest_cache, algo);
+	if (!h)
+		return -ENOENT;
+
+	digest_len = hash_digest_size[algo];
+	key = digest_cache_hash_key(digest, h->num_slots);
+
+	hlist_for_each_entry(entry, &h->slots[key], hnext) {
+		if (!memcmp(entry->digest, digest, digest_len)) {
+			pr_debug("Cache hit at depth %d for file %s, digest %s:%*phN in digest cache %s\n",
+				 search_depth, dentry->d_name.name,
+				 hash_algo_name[algo], digest_len, digest,
+				 digest_cache->path_str);
+
+			return 0;
+		}
+
+		search_depth++;
+	}
+
+	pr_debug("Cache miss for file %s, digest %s:%*phN in digest cache %s\n",
+		 dentry->d_name.name, hash_algo_name[algo], digest_len, digest,
+		 digest_cache->path_str);
+	return -ENOENT;
+}
+
+/**
+ * digest_cache_lookup - Search a digest in the digest cache
+ * @dentry: Dentry of the file whose digest is looked up
+ * @digest_cache: Digest cache
+ * @digest: Digest to search
+ * @algo: Algorithm of the digest to search
+ *
+ * This function calls digest_cache_htable_lookup() to search a digest in the
+ * passed digest cache, obtained with digest_cache_get().
+ *
+ * It returns the digest cache reference as the digest_cache_found_t type, to
+ * avoid that the digest cache is accidentally put. The digest_cache_found_t
+ * type can be converted back to a digest cache pointer, by
+ * calling digest_cache_from_found_t().
+ *
+ * Return: A positive digest_cache_found_t if the digest is found, zero if not.
+ */
+digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
+					 struct digest_cache *digest_cache,
+					 u8 *digest, enum hash_algo algo)
+{
+	int ret;
+
+	ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo);
+	return (!ret) ? (digest_cache_found_t)digest_cache : 0UL;
+}
+EXPORT_SYMBOL_GPL(digest_cache_lookup);
+
+/**
+ * digest_cache_htable_free - Free the hash tables
+ * @digest_cache: Digest cache
+ *
+ * This function removes all digests from all hash tables in the digest cache,
+ * and frees the memory.
+ */
+void digest_cache_htable_free(struct digest_cache *digest_cache)
+{
+	struct htable *h, *h_tmp;
+	struct digest_cache_entry *p;
+	struct hlist_node *q;
+	int i;
+
+	list_for_each_entry_safe(h, h_tmp, &digest_cache->htables, next) {
+		for (i = 0; i < h->num_slots; i++) {
+			hlist_for_each_entry_safe(p, q, &h->slots[i], hnext) {
+				hlist_del(&p->hnext);
+				pr_debug("Removed digest %s:%*phN from digest cache %s\n",
+					 hash_algo_name[h->algo],
+					 hash_digest_size[h->algo], p->digest,
+					 digest_cache->path_str);
+				kfree(p);
+			}
+		}
+
+		list_del(&h->next);
+		kfree(h->slots);
+		kfree(h);
+	}
+}
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
index bbf5eefe5c82..f6ffeaa25288 100644
--- a/security/digest_cache/internal.h
+++ b/security/digest_cache/internal.h
@@ -16,8 +16,40 @@
 /* Digest cache bits in flags. */
 #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
 
+/**
+ * struct digest_cache_entry - Entry of a digest cache hash table
+ * @hnext: Pointer to the next element in the collision list
+ * @digest: Stored digest
+ *
+ * This structure represents an entry of a digest cache hash table, storing a
+ * digest.
+ */
+struct digest_cache_entry {
+	struct hlist_node hnext;
+	u8 digest[];
+} __packed;
+
+/**
+ * struct htable - Hash table
+ * @next: Next hash table in the linked list
+ * @slots: Hash table slots
+ * @num_slots: Number of slots
+ * @num_digests: Number of digests stored in the hash table
+ * @algo: Algorithm of the digests
+ *
+ * This structure is a hash table storing digests of file 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
@@ -25,6 +57,7 @@
  * This structure represents a cache of digests extracted from a digest list.
  */
 struct digest_cache {
+	struct list_head htables;
 	atomic_t ref_count;
 	char *path_str;
 	unsigned long flags;
@@ -84,4 +117,14 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
 					 struct path *digest_list_path,
 					 char *path_str, char *filename);
 
+/* htable.c */
+int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
+			     enum hash_algo algo);
+int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest,
+			    enum hash_algo algo);
+int digest_cache_htable_lookup(struct dentry *dentry,
+			       struct digest_cache *digest_cache, u8 *digest,
+			       enum hash_algo algo);
+void digest_cache_htable_free(struct digest_cache *digest_cache);
+
 #endif /* _DIGEST_CACHE_INTERNAL_H */
diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
index 661c8d106791..0b201af6432c 100644
--- a/security/digest_cache/main.c
+++ b/security/digest_cache/main.c
@@ -48,6 +48,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
 
 	atomic_set(&digest_cache->ref_count, 1);
 	digest_cache->flags = 0UL;
+	INIT_LIST_HEAD(&digest_cache->htables);
 
 	pr_debug("New digest cache %s (ref count: %d)\n",
 		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
@@ -63,6 +64,8 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
  */
 static void digest_cache_free(struct digest_cache *digest_cache)
 {
+	digest_cache_htable_free(digest_cache);
+
 	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
 	kfree(digest_cache->path_str);
 	kmem_cache_free(digest_cache_cache, digest_cache);
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 05/14] digest_cache: Populate the digest cache from a digest list
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (3 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 04/14] digest_cache: Add hash tables and operations Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 14:24 ` [PATCH v4 06/14] digest_cache: Parse tlv digest lists Roberto Sassu
                   ` (10 subsequent siblings)
  15 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

Introduce digest_cache_populate() to populate the digest cache from a
digest list.

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_create(). 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/digest_cache/Makefile   |   2 +-
 security/digest_cache/internal.h |  26 ++++++
 security/digest_cache/main.c     |  18 ++++
 security/digest_cache/modsig.c   |  66 ++++++++++++++
 security/digest_cache/populate.c | 149 +++++++++++++++++++++++++++++++
 6 files changed, 261 insertions(+), 1 deletion(-)
 create mode 100644 security/digest_cache/modsig.c
 create mode 100644 security/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/digest_cache/Makefile b/security/digest_cache/Makefile
index 7e00c53d8f55..c1452437d02f 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -4,4 +4,4 @@
 
 obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 
-digest_cache-y := main.o secfs.o htable.o
+digest_cache-y := main.o secfs.o htable.o populate.o modsig.o
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
index f6ffeaa25288..cc6752a8683e 100644
--- a/security/digest_cache/internal.h
+++ b/security/digest_cache/internal.h
@@ -15,6 +15,24 @@
 
 /* Digest cache bits in flags. */
 #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
+#define INVALID			1	/* 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
@@ -127,4 +145,12 @@ 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, char *path_str,
+			  char *filename);
+
+/* modsig.c */
+size_t digest_cache_strip_modsig(__u8 *data, size_t data_len);
+
 #endif /* _DIGEST_CACHE_INTERNAL_H */
diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
index 0b201af6432c..6e9ed1b5040a 100644
--- a/security/digest_cache/main.c
+++ b/security/digest_cache/main.c
@@ -172,6 +172,17 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
 	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
 	mutex_unlock(&dig_sec->dig_owner_mutex);
 
+	if (S_ISREG(inode->i_mode)) {
+		ret = digest_cache_populate(digest_cache, digest_list_path,
+					    path_str, filename);
+		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);
+		}
+	}
+
 	/* Creation complete, notify the other lock contenders. */
 	clear_and_wake_up_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
 exists:
@@ -179,6 +190,13 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
 		/* Wait until creation complete. */
 		wait_on_bit(&dig_sec->dig_owner->flags, INIT_IN_PROGRESS,
 			    TASK_UNINTERRUPTIBLE);
+
+	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;
+	}
 out:
 	if (digest_list_path == &file_path)
 		path_put(&file_path);
diff --git a/security/digest_cache/modsig.c b/security/digest_cache/modsig.c
new file mode 100644
index 000000000000..3bdda00d8bb2
--- /dev/null
+++ b/security/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/digest_cache/populate.c b/security/digest_cache/populate.c
new file mode 100644
index 000000000000..415e638f587b
--- /dev/null
+++ b/security/digest_cache/populate.c
@@ -0,0 +1,149 @@
+// 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/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
+ * @filename: Digest list file name (can be an empty string)
+ * @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, char *filename,
+					  void *data, size_t data_len)
+{
+	char *format, *next_sep;
+	int ret = -EINVAL;
+
+	if (!filename[0]) {
+		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%s%s, format: %.*s, size: %ld\n", path_str,
+		 filename[0] ? "/" : "", filename, (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
+ * @path_str: Path string of the digest list
+ * @filename: Digest list file name (can be an empty string)
+ *
+ * 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, char *path_str,
+			  char *filename)
+{
+	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%s%s, ret: %ld\n",
+			 path_str, filename[0] ? "/" : "", filename,
+			 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%s%s, ret: %d\n",
+			 path_str, filename[0] ? "/" : "", filename, 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, path_str, filename,
+					     data, data_len);
+	if (ret < 0)
+		pr_debug("Error parsing digest list %s%s%s, ret: %d\n",
+			 path_str, filename[0] ? "/" : "", filename, ret);
+
+	vfree(data);
+	return ret;
+}
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 06/14] digest_cache: Parse tlv digest lists
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (4 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 05/14] digest_cache: Populate the digest cache from a digest list Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 14:24 ` [PATCH v4 07/14] digest_cache: Parse rpm " Roberto Sassu
                   ` (9 subsequent siblings)
  15 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

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_file_callback() to handle the DIGEST_LIST_FILE
fields, DIGEST_LIST_ALGO and DIGEST_LIST_ENTRY, and the respective field
parsers parse_digest_list_algo() and parse_digest_list_entry().

Also introduce digest_list_entry_data_callback(), to handle the
DIGEST_LIST_ENTRY_DATA (nested) fields, DIGEST_LIST_ENTRY_DIGEST and
DIGEST_LIST_ENTRY_PATH, and the respective field parsers
parse_digest_list_entry_digest() and parse_digest_list_entry_path().

The TLV parser itself is defined in lib/tlv_parser.c.

Both the TLV parser and the tlv digest list parser have been formally
verified with Frama-C (https://frama-c.com/).

The analysis has been done on this file:

https://github.com/robertosassu/rpm-formal/blob/main/validate_tlv.c

Here is the result of the analysis:

[eva:summary] ====== ANALYSIS SUMMARY ======
---------------------------------------------------------------------------
13 functions analyzed (out of 13): 100% coverage.
In these functions, 240 statements reached (out of 254): 94% coverage.
---------------------------------------------------------------------------
Some errors and warnings have been raised during the analysis:
  by the Eva analyzer:      0 errors    4 warnings
  by the Frama-C kernel:    0 errors    0 warnings
---------------------------------------------------------------------------
0 alarms generated by the analysis.
---------------------------------------------------------------------------
Evaluation of the logical properties reached by the analysis:
  Assertions        5 valid     0 unknown     0 invalid      5 total
  Preconditions    24 valid     0 unknown     0 invalid     24 total
100% of the logical properties reached have been proven.
---------------------------------------------------------------------------

The warnings are:

[eva] validate_tlv.c:437: Warning:
  this partitioning parameter cannot be evaluated safely on all states
[eva] validate_tlv.c:445: Warning:
  this partitioning parameter cannot be evaluated safely on all states
[eva] validate_tlv.c:354: Warning:
  this partitioning parameter cannot be evaluated safely on all states
[eva] validate_tlv.c:382: Warning:
  this partitioning parameter cannot be evaluated safely on all states

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 include/uapi/linux/tlv_digest_list.h    |  72 ++++++
 security/digest_cache/Kconfig           |   1 +
 security/digest_cache/Makefile          |   2 +
 security/digest_cache/parsers/parsers.h |  13 ++
 security/digest_cache/parsers/tlv.c     | 299 ++++++++++++++++++++++++
 security/digest_cache/populate.c        |   4 +
 6 files changed, 391 insertions(+)
 create mode 100644 include/uapi/linux/tlv_digest_list.h
 create mode 100644 security/digest_cache/parsers/parsers.h
 create mode 100644 security/digest_cache/parsers/tlv.c

diff --git a/include/uapi/linux/tlv_digest_list.h b/include/uapi/linux/tlv_digest_list.h
new file mode 100644
index 000000000000..8c97a46901c1
--- /dev/null
+++ b/include/uapi/linux/tlv_digest_list.h
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * Copyright (C) 2017-2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <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/digest_cache/Kconfig b/security/digest_cache/Kconfig
index 71017954e5c5..cb4fa44e8f2a 100644
--- a/security/digest_cache/Kconfig
+++ b/security/digest_cache/Kconfig
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 config SECURITY_DIGEST_CACHE
 	bool "Digest_cache LSM"
+	select TLV_PARSER
 	default n
 	help
 	  This option enables an LSM maintaining a cache of digests
diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
index c1452437d02f..a383b6ef2550 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -5,3 +5,5 @@
 obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 
 digest_cache-y := main.o secfs.o htable.o populate.o modsig.o
+
+digest_cache-y += parsers/tlv.o
diff --git a/security/digest_cache/parsers/parsers.h b/security/digest_cache/parsers/parsers.h
new file mode 100644
index 000000000000..1bbae426ab9f
--- /dev/null
+++ b/security/digest_cache/parsers/parsers.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <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/digest_cache/parsers/tlv.c b/security/digest_cache/parsers/tlv.c
new file mode 100644
index 000000000000..97e2c36b93a8
--- /dev/null
+++ b/security/digest_cache/parsers/tlv.c
@@ -0,0 +1,299 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2017-2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@huawei.com>
+ *
+ * Parse a tlv digest list.
+ */
+
+#define pr_fmt(fmt) "TLV DIGEST LIST: "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__)
+
+const char *digest_list_types_str[] = {
+	FOR_EACH_DIGEST_LIST_TYPE(GENERATE_STRING)
+};
+
+const char *digest_list_fields_str[] = {
+	FOR_EACH_DIGEST_LIST_FIELD(GENERATE_STRING)
+};
+
+const char *digest_list_entry_types_str[] = {
+	FOR_EACH_DIGEST_LIST_ENTRY_TYPE(GENERATE_STRING)
+};
+
+const char *digest_list_entry_fields_str[] = {
+	FOR_EACH_DIGEST_LIST_ENTRY_FIELD(GENERATE_STRING)
+};
+
+struct tlv_callback_data {
+	struct digest_cache *digest_cache;
+	u64 parsed_data_type;
+	u64 parsed_num_entries;
+	u64 parsed_total_len;
+	enum hash_algo algo;
+};
+
+/**
+ * parse_digest_list_entry_digest - Parse DIGEST_LIST_ENTRY_DIGEST field
+ * @tlv_data: Parser callback data
+ * @field: Field identifier
+ * @field_data: Field data
+ * @field_data_len: Length of @field_data
+ *
+ * This function parses the DIGEST_LIST_ENTRY_DIGEST field (file digest).
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+static int parse_digest_list_entry_digest(struct tlv_callback_data *tlv_data,
+					  enum digest_list_entry_fields field,
+					  const u8 *field_data,
+					  u64 field_data_len)
+{
+	int ret;
+
+	kenter(",%u,%llu", field, field_data_len);
+
+	if (tlv_data->algo == HASH_ALGO__LAST) {
+		pr_debug("Digest algo not set\n");
+		ret = -EBADMSG;
+		goto out;
+	}
+
+	if (field_data_len != hash_digest_size[tlv_data->algo]) {
+		pr_debug("Unexpected data length %llu, expected %d\n",
+			 field_data_len, hash_digest_size[tlv_data->algo]);
+		ret = -EBADMSG;
+		goto out;
+	}
+
+	ret = digest_cache_htable_add(tlv_data->digest_cache, (u8 *)field_data,
+				      tlv_data->algo);
+out:
+	kleave(" = %d", ret);
+	return ret;
+}
+
+/**
+ * parse_digest_list_entry_path - Parse DIGEST_LIST_ENTRY_PATH field
+ * @tlv_data: Parser callback data
+ * @field: Field identifier
+ * @field_data: Field data
+ * @field_data_len: Length of @field_data
+ *
+ * This function handles the DIGEST_LIST_ENTRY_PATH field (file path). It
+ * currently does not parse the data.
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+static int parse_digest_list_entry_path(struct tlv_callback_data *tlv_data,
+					enum digest_list_entry_fields field,
+					const u8 *field_data,
+					u64 field_data_len)
+{
+	kenter(",%u,%llu", field, field_data_len);
+
+	kleave(" = 0");
+	return 0;
+}
+
+/**
+ * digest_list_entry_data_callback - DIGEST_LIST_ENTRY_DATA callback
+ * @callback_data: Callback data
+ * @field: Field identifier
+ * @field_data: Field data
+ * @field_data_len: Length of @field_data
+ *
+ * This callback handles the fields of DIGEST_LIST_ENTRY_DATA (nested) data,
+ * and calls the appropriate parser.
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+static int digest_list_entry_data_callback(void *callback_data, u64 field,
+					   const u8 *field_data,
+					   u64 field_data_len)
+{
+	struct tlv_callback_data *tlv_data;
+	int ret;
+
+	tlv_data = (struct tlv_callback_data *)callback_data;
+
+	switch (field) {
+	case DIGEST_LIST_ENTRY_DIGEST:
+		ret = parse_digest_list_entry_digest(tlv_data, field,
+						     field_data,
+						     field_data_len);
+		break;
+	case DIGEST_LIST_ENTRY_PATH:
+		ret = parse_digest_list_entry_path(tlv_data, field, field_data,
+						   field_data_len);
+		break;
+	default:
+		pr_debug("Unhandled field %s\n",
+			 digest_list_entry_fields_str[field]);
+		/* Just ignore non-relevant fields. */
+		ret = 0;
+		break;
+	}
+
+	return ret;
+}
+
+/**
+ * parse_digest_list_algo - Parse DIGEST_LIST_ALGO field
+ * @tlv_data: Parser callback data
+ * @field: Field identifier
+ * @field_data: Field data
+ * @field_data_len: Length of @field_data
+ *
+ * This function parses the DIGEST_LIST_ALGO field (digest algorithm).
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+static int parse_digest_list_algo(struct tlv_callback_data *tlv_data,
+				  enum digest_list_fields field,
+				  const u8 *field_data, u64 field_data_len)
+{
+	u64 algo;
+	int ret;
+
+	kenter(",%u,%llu", field, field_data_len);
+
+	if (field_data_len != sizeof(u64)) {
+		pr_debug("Unexpected data length %llu, expected %lu\n",
+			 field_data_len, sizeof(u64));
+		ret = -EBADMSG;
+		goto out;
+	}
+
+	algo = __be64_to_cpu(*(u64 *)field_data);
+
+	if (algo >= HASH_ALGO__LAST) {
+		pr_debug("Unexpected digest algo %llu\n", algo);
+		ret = -EBADMSG;
+		goto out;
+	}
+
+	ret = digest_cache_htable_init(tlv_data->digest_cache,
+				       tlv_data->parsed_num_entries, algo);
+	if (ret < 0)
+		goto out;
+
+	tlv_data->algo = algo;
+
+	pr_debug("Digest algo: %s\n", hash_algo_name[algo]);
+out:
+	kleave(" = %d", ret);
+	return ret;
+}
+
+/**
+ * parse_digest_list_entry - Parse DIGEST_LIST_ENTRY field
+ * @tlv_data: Parser callback data
+ * @field: Field identifier
+ * @field_data: Field data
+ * @field_data_len: Length of @field_data
+ *
+ * This function parses the DIGEST_LIST_ENTRY field.
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+static int parse_digest_list_entry(struct tlv_callback_data *tlv_data,
+				   enum digest_list_fields field,
+				   const u8 *field_data, u64 field_data_len)
+{
+	int ret;
+
+	kenter(",%u,%llu", field, field_data_len);
+
+	ret = tlv_parse(DIGEST_LIST_ENTRY_DATA, digest_list_entry_data_callback,
+			tlv_data, field_data, field_data_len,
+			digest_list_entry_types_str, DIGEST_LIST_ENTRY__LAST,
+			digest_list_entry_fields_str,
+			DIGEST_LIST_ENTRY_FIELD__LAST);
+
+	kleave(" = %d", ret);
+	return ret;
+}
+
+/**
+ * digest_list_file_callback - DIGEST_LIST_FILE callback
+ * @callback_data: Callback data
+ * @field: Field identifier
+ * @field_data: Field data
+ * @field_data_len: Length of @field_data
+ *
+ * This callback handles the fields of DIGEST_LIST_FILE data, and calls the
+ * appropriate parser.
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+static int digest_list_file_callback(void *callback_data, u64 field,
+				     const u8 *field_data, u64 field_data_len)
+{
+	struct tlv_callback_data *tlv_data;
+	int ret;
+
+	tlv_data = (struct tlv_callback_data *)callback_data;
+
+	switch (field) {
+	case DIGEST_LIST_ALGO:
+		ret = parse_digest_list_algo(tlv_data, field, field_data,
+					     field_data_len);
+		break;
+	case DIGEST_LIST_ENTRY:
+		ret = parse_digest_list_entry(tlv_data, field, field_data,
+					      field_data_len);
+		break;
+	default:
+		pr_debug("Unhandled field %s\n",
+			 digest_list_fields_str[field]);
+		/* Just ignore non-relevant fields. */
+		ret = 0;
+		break;
+	}
+
+	return ret;
+}
+
+/**
+ * digest_list_parse_tlv - Parse a tlv digest list
+ * @digest_cache: Digest cache
+ * @data: Data to parse
+ * @data_len: Length of @data
+ *
+ * This function parses a tlv digest list.
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+int digest_list_parse_tlv(struct digest_cache *digest_cache, const u8 *data,
+			  size_t data_len)
+{
+	struct tlv_callback_data tlv_data = {
+		.digest_cache = digest_cache,
+		.algo = HASH_ALGO__LAST
+	};
+	int ret;
+
+	ret = tlv_parse_hdr(&data, &data_len, &tlv_data.parsed_data_type,
+			    &tlv_data.parsed_num_entries,
+			    &tlv_data.parsed_total_len,
+			    digest_list_types_str, DIGEST_LIST__LAST);
+	if (ret < 0)
+		return ret;
+
+	if (tlv_data.parsed_data_type != DIGEST_LIST_FILE)
+		return 0;
+
+	return tlv_parse_data(digest_list_file_callback, &tlv_data,
+			      tlv_data.parsed_num_entries, data, data_len,
+			      digest_list_fields_str, DIGEST_LIST_FIELD__LAST);
+}
diff --git a/security/digest_cache/populate.c b/security/digest_cache/populate.c
index 415e638f587b..13645ec4bb2b 100644
--- a/security/digest_cache/populate.c
+++ b/security/digest_cache/populate.c
@@ -12,6 +12,7 @@
 #include <linux/kernel_read_file.h>
 
 #include "internal.h"
+#include "parsers/parsers.h"
 
 /**
  * digest_cache_parse_digest_list - Parse a digest list
@@ -65,6 +66,9 @@ static int digest_cache_parse_digest_list(struct digest_cache *digest_cache,
 		 filename[0] ? "/" : "", filename, (int)(next_sep - format),
 		 format, data_len);
 
+	if (!strncmp(format, "tlv-", 4))
+		ret = digest_list_parse_tlv(digest_cache, data, data_len);
+
 	return ret;
 }
 
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 07/14] digest_cache: Parse rpm digest lists
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (5 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 06/14] digest_cache: Parse tlv digest lists Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 14:24 ` [PATCH v4 08/14] digest_cache: Add management of verification data Roberto Sassu
                   ` (8 subsequent siblings)
  15 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

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 add 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, 228 statements reached (out of 246): 92% 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/digest_cache/Makefile          |   1 +
 security/digest_cache/parsers/parsers.h |   2 +
 security/digest_cache/parsers/rpm.c     | 223 ++++++++++++++++++++++++
 security/digest_cache/populate.c        |   2 +
 4 files changed, 228 insertions(+)
 create mode 100644 security/digest_cache/parsers/rpm.c

diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
index a383b6ef2550..eca4076497e6 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -7,3 +7,4 @@ obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 digest_cache-y := main.o secfs.o htable.o populate.o modsig.o
 
 digest_cache-y += parsers/tlv.o
+digest_cache-y += parsers/rpm.o
diff --git a/security/digest_cache/parsers/parsers.h b/security/digest_cache/parsers/parsers.h
index 1bbae426ab9f..3f00d29ed92a 100644
--- a/security/digest_cache/parsers/parsers.h
+++ b/security/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/digest_cache/parsers/rpm.c b/security/digest_cache/parsers/rpm.c
new file mode 100644
index 000000000000..6c7fe9c9121c
--- /dev/null
+++ b/security/digest_cache/parsers/rpm.c
@@ -0,0 +1,223 @@
+// 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) "RPM DIGEST LIST: "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;
+} __packed;
+
+struct rpm_entryinfo {
+	s32 tag;
+	u32 type;
+	s32 offset;
+	u32 count;
+} __packed;
+
+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);
+
+	/* Finite termination 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++) {
+		if (data_len < sizeof(*entry))
+			return -EINVAL;
+
+		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;
+
+	/* Finite termination 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 -EINVAL;
+		}
+
+		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/digest_cache/populate.c b/security/digest_cache/populate.c
index 13645ec4bb2b..1770c8385017 100644
--- a/security/digest_cache/populate.c
+++ b/security/digest_cache/populate.c
@@ -68,6 +68,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;
 }
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 08/14] digest_cache: Add management of verification data
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (6 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 07/14] digest_cache: Parse rpm " Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 14:24 ` [PATCH v4 09/14] digest_cache: Add support for directories Roberto Sassu
                   ` (7 subsequent siblings)
  15 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

The digest_cache LSM 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
whether the digest cache itself was created from authentic data.

Introduce digest_cache_verif_set() to let the same LSMs (or a chosen
integrity provider) evaluate the digest list being read during the creation
of the digest cache, by implementing the kernel_post_read_file LSM hook,
and let them attach their verification data to that digest cache.

Reserve space in the file descriptor security blob for the digest cache
pointer. Also introduce digest_cache_to_file_sec() to set that pointer
before calling kernel_read_file() in digest_cache_populate(), and
digest_cache_from_file_sec() to retrieve the pointer back from the file
descriptor passed by LSMs with digest_cache_verif_set().

Multiple providers are supported, in the event there are multiple
integrity LSMs active. Each provider should also provide an unique verifier
ID as an argument to digest_cache_verif_set(), so that verification data
can be distinguished.

A caller of digest_cache_get() can retrieve back the verification data by
calling digest_cache_verif_get() and passing a digest cache pointer and the
desired verifier ID.

Since directory digest caches are not populated themselves, LSMs have to do
a lookup first to get the digest cache containing the digest, call
digest_cache_from_found_t() to convert the returned digest_cache_found_t
type to a digest cache pointer, and pass that to digest_cache_verif_get().

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 include/linux/digest_cache.h     |  17 +++++
 security/digest_cache/Makefile   |   2 +-
 security/digest_cache/internal.h |  40 +++++++++++
 security/digest_cache/main.c     |   5 ++
 security/digest_cache/populate.c |   2 +
 security/digest_cache/verif.c    | 116 +++++++++++++++++++++++++++++++
 6 files changed, 181 insertions(+), 1 deletion(-)
 create mode 100644 security/digest_cache/verif.c

diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
index 4872700ac205..9db8128513ca 100644
--- a/include/linux/digest_cache.h
+++ b/include/linux/digest_cache.h
@@ -44,6 +44,10 @@ void digest_cache_put(struct digest_cache *digest_cache);
 digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
 					 struct digest_cache *digest_cache,
 					 u8 *digest, enum hash_algo algo);
+int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
+			   size_t size);
+void *digest_cache_verif_get(struct digest_cache *digest_cache,
+			     const char *verif_id);
 
 #else
 static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
@@ -62,5 +66,18 @@ digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache,
 	return 0UL;
 }
 
+static inline int digest_cache_verif_set(struct file *file,
+					 const char *verif_id, void *data,
+					 size_t size)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline void *digest_cache_verif_get(struct digest_cache *digest_cache,
+					   const char *verif_id)
+{
+	return NULL;
+}
+
 #endif /* CONFIG_SECURITY_DIGEST_CACHE */
 #endif /* _LINUX_DIGEST_CACHE_H */
diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
index eca4076497e6..37a473c7bc28 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -4,7 +4,7 @@
 
 obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 
-digest_cache-y := main.o secfs.o htable.o populate.o modsig.o
+digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o
 
 digest_cache-y += parsers/tlv.o
 digest_cache-y += parsers/rpm.o
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
index cc6752a8683e..4929d25e7972 100644
--- a/security/digest_cache/internal.h
+++ b/security/digest_cache/internal.h
@@ -17,6 +17,21 @@
 #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
 #define INVALID			1	/* Digest cache marked as invalid. */
 
+/**
+ * struct digest_cache_verif
+ * @list: Linked list
+ * @verif_id: Identifier of who verified the digest list
+ * @data: Opaque data set by the digest list verifier
+ *
+ * This structure contains opaque data containing the result of verification
+ * of the digest list by a verifier.
+ */
+struct digest_cache_verif {
+	struct list_head list;
+	char *verif_id;
+	void *data;
+};
+
 /**
  * struct read_work - Structure to schedule reading a digest list
  * @work: Work structure
@@ -71,6 +86,8 @@ struct htable {
  * @ref_count: Number of references to the digest cache
  * @path_str: Path of the digest list the digest cache was created from
  * @flags: Control flags
+ * @verif_data: Verification data regarding the digest list
+ * @mutex: Protect digest cache modifications
  *
  * This structure represents a cache of digests extracted from a digest list.
  */
@@ -79,6 +96,8 @@ struct digest_cache {
 	atomic_t ref_count;
 	char *path_str;
 	unsigned long flags;
+	struct list_head verif_data;
+	struct mutex mutex;
 };
 
 /**
@@ -130,6 +149,24 @@ digest_cache_unref(struct digest_cache *digest_cache)
 	return (ref_is_zero) ? digest_cache : NULL;
 }
 
+static inline void digest_cache_to_file_sec(const struct file *file,
+					    struct digest_cache *digest_cache)
+{
+	struct digest_cache **digest_cache_sec;
+
+	digest_cache_sec = file->f_security + digest_cache_blob_sizes.lbs_file;
+	*digest_cache_sec = digest_cache;
+}
+
+static inline struct digest_cache *
+digest_cache_from_file_sec(const struct file *file)
+{
+	struct digest_cache **digest_cache_sec;
+
+	digest_cache_sec = file->f_security + digest_cache_blob_sizes.lbs_file;
+	return *digest_cache_sec;
+}
+
 /* main.c */
 struct digest_cache *digest_cache_create(struct dentry *dentry,
 					 struct path *digest_list_path,
@@ -153,4 +190,7 @@ int digest_cache_populate(struct digest_cache *digest_cache,
 /* modsig.c */
 size_t digest_cache_strip_modsig(__u8 *data, size_t data_len);
 
+/* verif.c */
+void digest_cache_verif_free(struct digest_cache *digest_cache);
+
 #endif /* _DIGEST_CACHE_INTERNAL_H */
diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
index 6e9ed1b5040a..f3475b36e566 100644
--- a/security/digest_cache/main.c
+++ b/security/digest_cache/main.c
@@ -49,6 +49,8 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
 	atomic_set(&digest_cache->ref_count, 1);
 	digest_cache->flags = 0UL;
 	INIT_LIST_HEAD(&digest_cache->htables);
+	INIT_LIST_HEAD(&digest_cache->verif_data);
+	mutex_init(&digest_cache->mutex);
 
 	pr_debug("New digest cache %s (ref count: %d)\n",
 		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
@@ -65,6 +67,8 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
 static void digest_cache_free(struct digest_cache *digest_cache)
 {
 	digest_cache_htable_free(digest_cache);
+	digest_cache_verif_free(digest_cache);
+	mutex_destroy(&digest_cache->mutex);
 
 	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
 	kfree(digest_cache->path_str);
@@ -336,6 +340,7 @@ EXPORT_SYMBOL_GPL(digest_cache_put);
 
 struct lsm_blob_sizes digest_cache_blob_sizes __ro_after_init = {
 	.lbs_inode = sizeof(struct digest_cache_security),
+	.lbs_file = sizeof(struct digest_cache *),
 };
 
 /**
diff --git a/security/digest_cache/populate.c b/security/digest_cache/populate.c
index 1770c8385017..9c2fc2295310 100644
--- a/security/digest_cache/populate.c
+++ b/security/digest_cache/populate.c
@@ -123,6 +123,8 @@ int digest_cache_populate(struct digest_cache *digest_cache,
 		return PTR_ERR(file);
 	}
 
+	digest_cache_to_file_sec(file, digest_cache);
+
 	w.data = NULL;
 	w.file = file;
 	INIT_WORK_ONSTACK(&w.work, digest_cache_read_digest_list);
diff --git a/security/digest_cache/verif.c b/security/digest_cache/verif.c
new file mode 100644
index 000000000000..04023240d3b4
--- /dev/null
+++ b/security/digest_cache/verif.c
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <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.
+ *
+ * Return: Zero on success, -ENOMEM if out of memory.
+ */
+int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
+			   size_t size)
+{
+	struct digest_cache *digest_cache = digest_cache_from_file_sec(file);
+	struct digest_cache_verif *new_verif;
+
+	/*
+	 * All allocations must be atomic (non-sleepable) since kprobe does not
+	 * allow otherwise (kprobe is needed for testing).
+	 */
+	new_verif = kzalloc(sizeof(*new_verif), GFP_ATOMIC);
+	if (!new_verif)
+		return -ENOMEM;
+
+	new_verif->verif_id = kstrdup(verif_id, GFP_ATOMIC);
+	if (!new_verif->verif_id) {
+		free_verif(new_verif);
+		return -ENOMEM;
+	}
+
+	new_verif->data = kmemdup(data, size, GFP_ATOMIC);
+	if (!new_verif->data) {
+		free_verif(new_verif);
+		return -ENOMEM;
+	}
+
+	mutex_lock(&digest_cache->mutex);
+	list_add_tail_rcu(&new_verif->list, &digest_cache->verif_data);
+	mutex_unlock(&digest_cache->mutex);
+	return 0;
+}
+EXPORT_SYMBOL_GPL(digest_cache_verif_set);
+
+/**
+ * digest_cache_verif_get - Get digest cache verification data
+ * @digest_cache: Digest cache
+ * @verif_id: Verifier ID
+ *
+ * This function returns the verification data previously set by a verifier
+ * with digest_cache_verif_set().
+ *
+ * Return: Verification data if found, NULL otherwise.
+ */
+void *digest_cache_verif_get(struct digest_cache *digest_cache,
+			     const char *verif_id)
+{
+	struct digest_cache_verif *verif;
+	void *verif_data = NULL;
+
+	rcu_read_lock();
+	list_for_each_entry_rcu(verif, &digest_cache->verif_data, list) {
+		if (!strcmp(verif->verif_id, verif_id)) {
+			verif_data = verif->data;
+			break;
+		}
+	}
+	rcu_read_unlock();
+
+	return verif_data;
+}
+EXPORT_SYMBOL_GPL(digest_cache_verif_get);
+
+/**
+ * digest_cache_verif_free - Free all digest_cache_verif structures
+ * @digest_cache: Digest cache
+ *
+ * This function frees the space allocated for all digest_cache_verif
+ * structures in the digest cache.
+ */
+void digest_cache_verif_free(struct digest_cache *digest_cache)
+{
+	struct digest_cache_verif *p, *q;
+
+	/* No need to lock, called when nobody else has a digest cache ref. */
+	list_for_each_entry_safe(p, q, &digest_cache->verif_data, list) {
+		list_del(&p->list);
+		free_verif(p);
+	}
+}
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 09/14] digest_cache: Add support for directories
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (7 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 08/14] digest_cache: Add management of verification data Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 19:39   ` Jarkko Sakkinen
  2024-04-15 14:24 ` [PATCH v4 10/14] digest cache: Prefetch digest lists if requested Roberto Sassu
                   ` (6 subsequent siblings)
  15 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

In the environments where xattrs are not available (e.g. in the initial ram
disk), the digest_cache LSM cannot precisely determine which digest list in
a directory contains the desired reference digest. However, although
slower, it would be desirable to search the digest in all digest lists of
that directory.

This done in two steps. When a digest cache is being created,
digest_cache_create() invokes digest_cache_dir_create(), to generate the
list of current directory entries. Entries are placed in the list in
ascending order by the <seq num> if prepended to the file name, or at the
end of the list if not.

The resulting digest cache has the IS_DIR bit set, to distinguish it from
the digest caches created from regular files.

Second, when a digest is searched in a directory digest cache,
digest_cache_lookup() invokes digest_cache_dir_lookup_digest() to
iteratively search that digest in each directory entry generated by
digest_cache_dir_create().

That list is stable, even if new files are added or deleted from that
directory. A subsequent patch will invalidate the digest cache, forcing
next callers of digest_cache_get() to get a new directory digest cache with
the updated list of directory entries.

If the current directory entry does not have a digest cache reference,
digest_cache_dir_lookup_digest() invokes digest_cache_create() to create a
new digest cache for that entry. In either case,
digest_cache_dir_lookup_digest() calls then digest_cache_htable_lookup()
with the new/existing digest cache to search the digest. Check and
assignment of the digest cache in a directory entry is protected by the
per entry digest_cache_mutex.

The iteration stops when the digest is found. In that case,
digest_cache_dir_lookup_digest() returns the digest cache reference of the
current directory entry as the digest_cache_found_t type, so that callers
of digest_cache_lookup() don't mistakenly try to call digest_cache_put()
with that reference.

This new reference type will be used to retrieve information about the
digest cache containing the digest, which is not known in advance until the
digest search is performed.

The order of the list of directory entries influences the speed of the
digest search. A search terminates faster if less digest caches have to be
created. One way to optimize it could be to order the list of digest lists
in the same way of when they are requested at boot.

Finally, digest_cache_dir_free() releases the digest cache references
stored in the list of directory entries, and frees the list itself.

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 security/digest_cache/Makefile   |   2 +-
 security/digest_cache/dir.c      | 193 +++++++++++++++++++++++++++++++
 security/digest_cache/htable.c   |  22 +++-
 security/digest_cache/internal.h |  45 +++++++
 security/digest_cache/main.c     |  12 ++
 5 files changed, 271 insertions(+), 3 deletions(-)
 create mode 100644 security/digest_cache/dir.c

diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
index 37a473c7bc28..e417da0383ab 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -4,7 +4,7 @@
 
 obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 
-digest_cache-y := main.o secfs.o htable.o populate.o modsig.o 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/digest_cache/dir.c b/security/digest_cache/dir.c
new file mode 100644
index 000000000000..7bfcdd5f7ef1
--- /dev/null
+++ b/security/digest_cache/dir.c
@@ -0,0 +1,193 @@
+// 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 "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_create - Create a directory digest cache
+ * @digest_cache: 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_create(struct digest_cache *digest_cache,
+			    struct path *digest_list_path)
+{
+	struct file *dir_file;
+	struct readdir_callback buf = {
+		.ctx.actor = digest_cache_dir_iter,
+	};
+	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);
+	}
+
+	buf.head = &digest_cache->dir_entries;
+	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_lookup_digest - Lookup a digest
+ * @dentry: Dentry of the file whose digest is looked up
+ * @digest_list_path: Path structure of the digest list directory
+ * @digest_cache: 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_create() and looks up the digest in the digest cache of
+ * each entry.
+ *
+ * Return: A digest_cache_found_t value if the digest if found, zero otherwise.
+ */
+digest_cache_found_t
+digest_cache_dir_lookup_digest(struct dentry *dentry,
+			       struct path *digest_list_path,
+			       struct digest_cache *digest_cache, u8 *digest,
+			       enum hash_algo algo)
+{
+	struct digest_cache *cache;
+	struct dir_entry *dir_entry;
+	int ret;
+
+	list_for_each_entry(dir_entry, &digest_cache->dir_entries, list) {
+		mutex_lock(&dir_entry->digest_cache_mutex);
+		if (!dir_entry->digest_cache) {
+			cache = digest_cache_create(dentry, digest_list_path,
+						    digest_cache->path_str,
+						    dir_entry->name);
+			/* 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)
+			return (digest_cache_found_t)dir_entry->digest_cache;
+	}
+
+	return 0UL;
+}
+
+/**
+ * digest_cache_dir_free - Free the stored file list and put digest caches
+ * @digest_cache: Digest cache
+ *
+ * This function frees the file list created by digest_cache_create(), and puts
+ * the digest cache 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/digest_cache/htable.c b/security/digest_cache/htable.c
index d2d5d8f5e5b1..8cf7400dfcf4 100644
--- a/security/digest_cache/htable.c
+++ b/security/digest_cache/htable.c
@@ -8,6 +8,8 @@
  */
 
 #define pr_fmt(fmt) "DIGEST CACHE: "fmt
+#include <linux/namei.h>
+
 #include "internal.h"
 
 /**
@@ -210,10 +212,26 @@ digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
 					 struct digest_cache *digest_cache,
 					 u8 *digest, enum hash_algo algo)
 {
+	struct path digest_list_path;
+	digest_cache_found_t found;
 	int ret;
 
-	ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo);
-	return (!ret) ? (digest_cache_found_t)digest_cache : 0UL;
+	if (!test_bit(IS_DIR, &digest_cache->flags)) {
+		ret = digest_cache_htable_lookup(dentry, digest_cache, digest,
+						 algo);
+		return (!ret) ? (digest_cache_found_t)digest_cache : 0UL;
+	}
+
+	ret = kern_path(digest_cache->path_str, 0, &digest_list_path);
+	if (ret < 0) {
+		pr_debug("Cannot find path %s\n", digest_cache->path_str);
+		return 0UL;
+	}
+
+	found = digest_cache_dir_lookup_digest(dentry, &digest_list_path,
+					       digest_cache, digest, algo);
+	path_put(&digest_list_path);
+	return found;
 }
 EXPORT_SYMBOL_GPL(digest_cache_lookup);
 
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
index 4929d25e7972..b7afca8e04da 100644
--- a/security/digest_cache/internal.h
+++ b/security/digest_cache/internal.h
@@ -16,6 +16,39 @@
 /* Digest cache bits in flags. */
 #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
 #define INVALID			1	/* Digest cache marked as invalid. */
+#define IS_DIR			2	/* 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[];
+} __packed;
 
 /**
  * struct digest_cache_verif
@@ -83,6 +116,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
@@ -93,6 +127,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;
@@ -193,4 +228,14 @@ 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_create(struct digest_cache *digest_cache,
+			    struct path *digest_list_path);
+digest_cache_found_t
+digest_cache_dir_lookup_digest(struct dentry *dentry,
+			       struct path *digest_list_path,
+			       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/digest_cache/main.c b/security/digest_cache/main.c
index f3475b36e566..15f1486610a3 100644
--- a/security/digest_cache/main.c
+++ b/security/digest_cache/main.c
@@ -50,6 +50,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
 	digest_cache->flags = 0UL;
 	INIT_LIST_HEAD(&digest_cache->htables);
 	INIT_LIST_HEAD(&digest_cache->verif_data);
+	INIT_LIST_HEAD(&digest_cache->dir_entries);
 	mutex_init(&digest_cache->mutex);
 
 	pr_debug("New digest cache %s (ref count: %d)\n",
@@ -68,6 +69,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);
 	mutex_destroy(&digest_cache->mutex);
 
 	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
@@ -185,6 +187,16 @@ struct digest_cache *digest_cache_create(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, &dig_sec->dig_owner->flags);
+
+		ret = digest_cache_dir_create(digest_cache, digest_list_path);
+		if (ret < 0) {
+			pr_debug("Failed to create dir digest cache, ret: %d (keep digest cache)\n",
+				 ret);
+			/* Prevent usage of partially-populated digest cache. */
+			set_bit(INVALID, &dig_sec->dig_owner->flags);
+		}
 	}
 
 	/* Creation complete, notify the other lock contenders. */
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 10/14] digest cache: Prefetch digest lists if requested
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (8 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 09/14] digest_cache: Add support for directories Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 19:42   ` Jarkko Sakkinen
  2024-04-15 14:24 ` [PATCH v4 11/14] digest_cache: Reset digest cache on file/directory change Roberto Sassu
                   ` (5 subsequent siblings)
  15 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

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 digest_cache LSM is not exempted from this issue. Under the assumption
that only the digest list is measured, and file measurements are omitted if
their digest is found in that digest list, a PCR can be predictable only if
all files belong to the same digest list. Otherwise, it will still be
unpredictable, since files accessed in a non-deterministic order will cause
digest lists to be measured in a non-deterministic order too.

Overcome this issue, if prefetching is enabled, by searching a digest list
file name in digest_list_dir_lookup_filename() among the entries of the
linked list built by digest_cache_dir_create(). If the file name does not
match, read the digest list to trigger its measurement. Otherwise, also
create a digest cache and return that to the caller. Release the extra
reference of the directory digest cache in digest_cache_new(), since it was
only used for the search and it is not going to be returned.

Prefetching needs to be explicitly enabled by setting the new
security.dig_prefetch xattr to 1 in the directory containing the digest
lists. The newly introduced function digest_cache_prefetch_requested()
checks first if the DIR_PREFETCH bit is set in dig_owner, otherwise it
reads the xattr. digest_cache_create() sets DIR_PREFETCH in dig_owner, if
prefetching is enabled, before declaring the digest cache as initialized.

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 include/uapi/linux/xattr.h       |  3 +
 security/digest_cache/dir.c      | 55 +++++++++++++++++-
 security/digest_cache/internal.h | 11 +++-
 security/digest_cache/main.c     | 95 +++++++++++++++++++++++++++++++-
 security/digest_cache/populate.c |  8 ++-
 security/digest_cache/verif.c    |  5 +-
 6 files changed, 170 insertions(+), 7 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/digest_cache/dir.c b/security/digest_cache/dir.c
index 7bfcdd5f7ef1..a7d203c15386 100644
--- a/security/digest_cache/dir.c
+++ b/security/digest_cache/dir.c
@@ -54,6 +54,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;
@@ -127,6 +128,7 @@ int digest_cache_dir_create(struct digest_cache *digest_cache,
  * @digest_cache: Digest cache
  * @digest: Digest to search
  * @algo: Algorithm of the digest to search
+ * @filename: File name of the digest list to search
  *
  * This function iterates over the linked list created by
  * digest_cache_dir_create() and looks up the digest in the digest cache of
@@ -149,7 +151,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
 		if (!dir_entry->digest_cache) {
 			cache = digest_cache_create(dentry, digest_list_path,
 						    digest_cache->path_str,
-						    dir_entry->name);
+						    dir_entry->name, false,
+						    false);
 			/* Ignore digest caches that cannot be instantiated. */
 			if (!cache) {
 				mutex_unlock(&dir_entry->digest_cache_mutex);
@@ -158,6 +161,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
 
 			/* Consume extra ref. from digest_cache_create(). */
 			dir_entry->digest_cache = cache;
+			/* Digest list was read, mark entry as prefetched. */
+			dir_entry->prefetched = true;
 		}
 		mutex_unlock(&dir_entry->digest_cache_mutex);
 
@@ -171,6 +176,54 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
 	return 0UL;
 }
 
+/**
+ * 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: Digest cache
+ * @filename: File name of the digest list to search
+ *
+ * This function iterates over the linked list created by
+ * digest_cache_dir_create() 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 returns the digest cache pointer from
+ * digest_cache_create() to the caller.
+ *
+ * Return: A digest cache pointer if the digest list if found, NULL otherwise.
+ */
+struct digest_cache *
+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);
+
+		dir_entry->prefetched = true;
+		mutex_unlock(&dir_entry->digest_cache_mutex);
+
+		if (filename_found)
+			return cache;
+	}
+
+	return NULL;
+}
+
 /**
  * digest_cache_dir_free - Free the stored file list and put digest caches
  * @digest_cache: Digest cache
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
index b7afca8e04da..c13b35f6b2c0 100644
--- a/security/digest_cache/internal.h
+++ b/security/digest_cache/internal.h
@@ -17,6 +17,7 @@
 #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
 #define INVALID			1	/* Digest cache marked as invalid. */
 #define IS_DIR			2	/* Digest cache created from dir. */
+#define DIR_PREFETCH		3	/* Prefetching requested for dir. */
 
 /**
  * struct readdir_callback - Structure to store information for dir iteration
@@ -37,6 +38,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
@@ -47,6 +49,7 @@ struct dir_entry {
 	struct digest_cache *digest_cache;
 	struct mutex digest_cache_mutex;
 	unsigned int seq_num;
+	bool prefetched;
 	char name[];
 } __packed;
 
@@ -205,7 +208,8 @@ 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);
 
 /* htable.c */
 int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
@@ -236,6 +240,11 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
 			       struct path *digest_list_path,
 			       struct digest_cache *digest_cache, u8 *digest,
 			       enum hash_algo algo);
+struct digest_cache *
+digest_cache_dir_lookup_filename(struct dentry *dentry,
+				 struct path *digest_list_path,
+				 struct digest_cache *digest_cache,
+				 char *filename);
 void digest_cache_dir_free(struct digest_cache *digest_cache);
 
 #endif /* _DIGEST_CACHE_INTERNAL_H */
diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
index 15f1486610a3..a5616fd07c1d 100644
--- a/security/digest_cache/main.c
+++ b/security/digest_cache/main.c
@@ -83,6 +83,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
@@ -109,7 +111,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;
@@ -148,6 +151,16 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
 				 dentry->d_name.name);
 			goto out;
 		}
+
+		if (prefetch) {
+			/* Fine to fail, we are just prefetching. */
+			ret = digest_cache_populate(NULL, digest_list_path,
+						    path_str, filename);
+			pr_debug("Digest list %s/%s %s prefetched\n",
+				 path_str, filename,
+				 !ret ? "has been" : "cannot be");
+			goto out;
+		}
 	}
 
 	dig_sec = digest_cache_get_security(inode);
@@ -176,6 +189,11 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
 
 	/* Make the other lock contenders wait until creation complete. */
 	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
+
+	/* Set DIR_PREFETCH if prefetching was requested. */
+	if (prefetch_req)
+		set_bit(DIR_PREFETCH, &digest_cache->flags);
+
 	mutex_unlock(&dig_sec->dig_owner_mutex);
 
 	if (S_ISREG(inode->i_mode)) {
@@ -220,6 +238,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 new security.dig_prefetch xattr.
+ *
+ * Return: True if prefetching is requested, false otherwise.
+ */
+static 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
@@ -230,13 +294,19 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
  * with that file name. If security.digest_list is not found, this function
  * requests the creation of a digest cache on the parent directory.
  *
+ * On prefetching, if the default path is a directory and if
+ * security.digest_list is found, this function first retrieves the directory
+ * digest cache, and then calls digest_cache_dir_lookup_filename() to retrieve
+ * the desired digest cache in that directory.
+ *
  * Return: A new digest cache on success, NULL on error.
  */
 static struct digest_cache *digest_cache_new(struct dentry *dentry)
 {
 	char filename[NAME_MAX + 1] = { 0 };
-	struct digest_cache *digest_cache = NULL;
+	struct digest_cache *digest_cache = NULL, *found;
 	struct path default_path;
+	bool prefetch_req = false;
 	int ret;
 
 	ret = kern_path(default_path_str, 0, &default_path);
@@ -273,9 +343,28 @@ static struct digest_cache *digest_cache_new(struct dentry *dentry)
 	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);
+
+	if (filename[0])
+		prefetch_req = digest_cache_prefetch_requested(&default_path,
+							default_path_str);
 create:
+	/* On prefetching, retrieve the directory digest cache. */
 	digest_cache = digest_cache_create(dentry, &default_path,
-					   default_path_str, filename);
+					   default_path_str,
+					   !prefetch_req ? filename : "",
+					   prefetch_req, false);
+	if (!digest_cache)
+		goto out;
+
+	if (prefetch_req) {
+		/* Find the digest cache with a matching file name. */
+		found = digest_cache_dir_lookup_filename(dentry, &default_path,
+							 digest_cache,
+							 filename);
+		/* Release ref. to the directory digest cache. */
+		digest_cache_put(digest_cache);
+		digest_cache = found;
+	}
 out:
 	path_put(&default_path);
 	return digest_cache;
diff --git a/security/digest_cache/populate.c b/security/digest_cache/populate.c
index 9c2fc2295310..17e7b011c367 100644
--- a/security/digest_cache/populate.c
+++ b/security/digest_cache/populate.c
@@ -143,6 +143,12 @@ int digest_cache_populate(struct digest_cache *digest_cache,
 		return ret;
 	}
 
+	/* The caller wants just to read digest lists. */
+	if (!digest_cache) {
+		ret = 0;
+		goto out_vfree;
+	}
+
 	data_len = digest_cache_strip_modsig(data, ret);
 
 	/* Digest list parsers initialize the hash table and add the digests. */
@@ -151,7 +157,7 @@ int digest_cache_populate(struct digest_cache *digest_cache,
 	if (ret < 0)
 		pr_debug("Error parsing digest list %s%s%s, ret: %d\n",
 			 path_str, filename[0] ? "/" : "", filename, ret);
-
+out_vfree:
 	vfree(data);
 	return ret;
 }
diff --git a/security/digest_cache/verif.c b/security/digest_cache/verif.c
index 04023240d3b4..c42ae93261e2 100644
--- a/security/digest_cache/verif.c
+++ b/security/digest_cache/verif.c
@@ -33,7 +33,7 @@ static void free_verif(struct digest_cache_verif *verif)
  * This function lets a verifier supply verification data about a digest list
  * being read to populate the digest cache.
  *
- * Return: Zero on success, -ENOMEM if out of memory.
+ * Return: Zero on success, -ENOMEM if out of memory, -ENOENT on prefetching.
  */
 int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
 			   size_t size)
@@ -41,6 +41,9 @@ int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
 	struct digest_cache *digest_cache = digest_cache_from_file_sec(file);
 	struct digest_cache_verif *new_verif;
 
+	if (!digest_cache)
+		return -ENOENT;
+
 	/*
 	 * All allocations must be atomic (non-sleepable) since kprobe does not
 	 * allow otherwise (kprobe is needed for testing).
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 11/14] digest_cache: Reset digest cache on file/directory change
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (9 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 10/14] digest cache: Prefetch digest lists if requested Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 19:44   ` Jarkko Sakkinen
  2024-04-15 14:24 ` [PATCH v4 12/14] digest_cache: Notify digest cache events Roberto Sassu
                   ` (4 subsequent siblings)
  15 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

Register six new LSM hooks, path_truncate, file_release, inode_unlink,
inode_rename, inode_post_setxattr and inode_post_removexattr, to monitor
digest lists/directory modifications.

If an action affects a digest list or the parent directory, the new LSM
hook implementations call digest_cache_reset_owner() to set the RESET bit
(if unset) on the digest cache pointed to by dig_owner in the inode
security blob. This will cause next calls to digest_cache_get() and
digest_cache_create() to respectively put and clear dig_user and dig_owner,
and request a new digest cache.

If an action affects a file using a digest cache, the new LSM hook
implementations call digest_cache_reset_user() to set the RESET_USER bit
(if unset) on the digest cache pointed to by dig_user in the inode security
blob. This will cause next calls to digest_cache_get() to put and clear
dig_user, and retrieve the digest cache again.

That does not affect other users of the old digest cache, since that one
remains valid as long as the reference count is greater than zero. However,
they will be notified in a subsequent patch about the reset, so that they
can eventually request a new digest cache.

Recreating a file digest cache means reading the digest list again and
extracting the digests. Recreating a directory digest cache, instead, does
not mean recreating the digest cache for directory entries, since those
digest caches are likely already stored in the inode security blob. It
would happen however for new files.

Dig_owner reset for file 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 is being closed, inode_unlink, when a
digest list is removed, and inode_rename when a digest list is renamed.

Dig_owner reset for directory digest caches is done on file_release, when a
new digest list is written in the digest list directory, on inode_unlink,
when a digest list is deleted from that directory, and finally on
inode_rename, when a digest list is moved to/from that directory.

Dig_user reset 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.

With the exception of file_release, which will always be executed (cannot
be denied), and inode_post_setxattr and inode_post_removexattr, which are
executed after the actual operation, the other LSM hooks are not optimal,
since the digest_cache LSM does not know whether or not the operation will
be allowed also by other LSMs. If the operation is denied, the digest_cache
LSM would do an unnecessary reset.

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 security/digest_cache/Kconfig    |   1 +
 security/digest_cache/Makefile   |   3 +-
 security/digest_cache/dir.c      |   6 +
 security/digest_cache/internal.h |  14 +++
 security/digest_cache/main.c     |  19 +++
 security/digest_cache/reset.c    | 197 +++++++++++++++++++++++++++++++
 6 files changed, 239 insertions(+), 1 deletion(-)
 create mode 100644 security/digest_cache/reset.c

diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
index cb4fa44e8f2a..54ba3a585073 100644
--- a/security/digest_cache/Kconfig
+++ b/security/digest_cache/Kconfig
@@ -2,6 +2,7 @@
 config SECURITY_DIGEST_CACHE
 	bool "Digest_cache LSM"
 	select TLV_PARSER
+	select SECURITY_PATH
 	default n
 	help
 	  This option enables an LSM maintaining a cache of digests
diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
index e417da0383ab..3d5e600a2c45 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -4,7 +4,8 @@
 
 obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 
-digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o
+digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o \
+		  reset.o
 
 digest_cache-y += parsers/tlv.o
 digest_cache-y += parsers/rpm.o
diff --git a/security/digest_cache/dir.c b/security/digest_cache/dir.c
index a7d203c15386..937177660242 100644
--- a/security/digest_cache/dir.c
+++ b/security/digest_cache/dir.c
@@ -148,6 +148,12 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
 
 	list_for_each_entry(dir_entry, &digest_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,
 						    digest_cache->path_str,
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
index c13b35f6b2c0..c816929c4743 100644
--- a/security/digest_cache/internal.h
+++ b/security/digest_cache/internal.h
@@ -18,6 +18,8 @@
 #define INVALID			1	/* Digest cache marked as invalid. */
 #define IS_DIR			2	/* Digest cache created from dir. */
 #define DIR_PREFETCH		3	/* Prefetching requested for dir. */
+#define RESET			4	/* Digest cache to be recreated. */
+#define RESET_USER		5	/* Dig_user pointer to be released. */
 
 /**
  * struct readdir_callback - Structure to store information for dir iteration
@@ -247,4 +249,16 @@ digest_cache_dir_lookup_filename(struct dentry *dentry,
 				 char *filename);
 void digest_cache_dir_free(struct digest_cache *digest_cache);
 
+/* reset.c */
+int digest_cache_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/digest_cache/main.c b/security/digest_cache/main.c
index a5616fd07c1d..ce3518a33c80 100644
--- a/security/digest_cache/main.c
+++ b/security/digest_cache/main.c
@@ -169,6 +169,11 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
 
 	/* Serialize check and assignment of dig_owner. */
 	mutex_lock(&dig_sec->dig_owner_mutex);
+	if (dig_sec->dig_owner && test_bit(RESET, &dig_sec->dig_owner->flags)) {
+		digest_cache_put(dig_sec->dig_owner);
+		dig_sec->dig_owner = NULL;
+	}
+
 	if (dig_sec->dig_owner) {
 		/* Increment ref. count for reference returned to the caller. */
 		digest_cache = digest_cache_ref(dig_sec->dig_owner);
@@ -403,6 +408,13 @@ 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) ||
+	     test_bit(RESET_USER, &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(). */
@@ -491,6 +503,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/digest_cache/reset.c b/security/digest_cache/reset.c
new file mode 100644
index 000000000000..e07222b0e0e8
--- /dev/null
+++ b/security/digest_cache/reset.c
@@ -0,0 +1,197 @@
+// 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_owner - Reset dig_owner
+ * @inode: Inode of the digest list/directory containing the digest list
+ * @reason: Reason for reset
+ *
+ * This function sets the RESET bit of the digest cache pointed to by dig_owner
+ * (if unset), so that digest_cache_get() and digest_cache_create() respectively
+ * release and clear dig_user and dig_owner in the inode security blob. This
+ * causes new callers of digest_cache_get() to get a new digest cache.
+ */
+static void digest_cache_reset_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 &&
+	    !test_and_set_bit(RESET, &dig_sec->dig_owner->flags))
+		pr_debug("Resetting %s (dig_owner), reason: %s\n",
+			 dig_sec->dig_owner->path_str, reason);
+	mutex_unlock(&dig_sec->dig_owner_mutex);
+}
+
+/**
+ * digest_cache_reset_user - Reset dig_user
+ * @inode: Inode of the file using the digest cache
+ * @reason: Reason for reset
+ *
+ * This function sets the RESET_USER bit (if unset), so that digest_cache_get()
+ * clears the dig_user pointer in the inode security blob and determines again
+ * the digest list inode to get a digest cache from.
+ */
+static void digest_cache_reset_user(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_user_mutex);
+	if (dig_sec->dig_user &&
+	    !test_and_set_bit(RESET_USER, &dig_sec->dig_user->flags))
+		pr_debug("Resetting %s (dig_user), reason: %s\n",
+			 dig_sec->dig_user->path_str, reason);
+	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, it resets the inode dig_owner, to force rebuilding the digest
+ * cache.
+ *
+ * Return: Zero.
+ */
+int digest_cache_path_truncate(const struct path *path)
+{
+	struct inode *inode = d_backing_inode(path->dentry);
+
+	if (!S_ISREG(inode->i_mode))
+		return 0;
+
+	digest_cache_reset_owner(inode, "file_truncate");
+	return 0;
+}
+
+/**
+ * digest_cache_file_release - Last reference of a file desc is being released
+ * @file: File descriptor
+ *
+ * This function is called when the last reference of a file descriptor is
+ * being released. If the inode is a regular file and was opened for write, or
+ * the parent inode is the digest list directory and the file was created, it
+ * resets the inode dig_owner, to force rebuilding the digest cache.
+ */
+void digest_cache_file_release(struct file *file)
+{
+	struct inode *dir = d_backing_inode(file_dentry(file)->d_parent);
+
+	if (!S_ISREG(file_inode(file)->i_mode) || !(file->f_mode & FMODE_WRITE))
+		return;
+
+	digest_cache_reset_owner(file_inode(file), "file_file_release");
+	if (file->f_mode & FMODE_CREATED)
+		digest_cache_reset_owner(dir, "dir_file_release");
+}
+
+/**
+ * digest_cache_inode_unlink - An inode is being removed
+ * @dir: Inode of the affected directory
+ * @dentry: Dentry of the inode being removed
+ *
+ * This function is called when an existing inode is being removed. If the
+ * inode is a digest list, or the parent inode is the digest list directory and
+ * the inode is a regular file, it resets the affected inode dig_owner, to force
+ * rebuilding the digest cache.
+ *
+ * Return: Zero.
+ */
+int digest_cache_inode_unlink(struct inode *dir, struct dentry *dentry)
+{
+	struct inode *inode = d_backing_inode(dentry);
+
+	if (!S_ISREG(inode->i_mode))
+		return 0;
+
+	digest_cache_reset_owner(inode, "file_unlink");
+	digest_cache_reset_owner(dir, "dir_unlink");
+	return 0;
+}
+
+/**
+ * digest_cache_inode_rename - An inode is being renamed
+ * @old_dir: Inode of the directory containing the inode being renamed
+ * @old_dentry: Dentry of the inode being renamed
+ * @new_dir: Directory where the inode will be placed into
+ * @new_dentry: Dentry of the inode after being renamed
+ *
+ * This function is called when an existing inode is being moved from a
+ * directory to another (rename). If the inode is a digest list, or that inode
+ * is moved from/to the digest list directory, it resets the affected inode
+ * dig_owner, to force rebuilding the digest cache.
+ *
+ * Return: Zero.
+ */
+int digest_cache_inode_rename(struct inode *old_dir, struct dentry *old_dentry,
+			      struct inode *new_dir, struct dentry *new_dentry)
+{
+	struct inode *old_inode = d_backing_inode(old_dentry);
+
+	if (!S_ISREG(old_inode->i_mode))
+		return 0;
+
+	digest_cache_reset_owner(old_inode, "file_rename");
+	digest_cache_reset_owner(old_dir, "dir_rename_from");
+	digest_cache_reset_owner(new_dir, "dir_rename_to");
+	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
+ * resets 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_reset_user(d_backing_inode(dentry), "file_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 resets 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_reset_user(d_backing_inode(dentry), "file_removexattr");
+}
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 12/14] digest_cache: Notify digest cache events
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (10 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 11/14] digest_cache: Reset digest cache on file/directory change Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 14:24 ` [PATCH v4 13/14] selftests/digest_cache: Add selftests for digest_cache LSM Roberto Sassu
                   ` (3 subsequent siblings)
  15 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

Introduce digest_cache_register_notifier() and
digest_cache_unregister_notifier() to let the subscribers know about events
occurring on digest caches. Subscribers should provide a callback, to be
invoked for every notification.

Introduce a notification list for each digest cache, and populate it with
inodes passed to digest_cache_get(), when dig_user of that inode is still
NULL so that there are no duplicates.

Then, on setting the RESET bit, emit a DIGEST_CACHE_RESET notification,
passing the affected digest cache and inodes, so that subscribers can
eventually invalidate a cached security decision on that inode.

On a file digest cache reset, emit a DIGEST_CACHE_RESET notification also
for the parent directory digest cache and the inodes using it, since
requestors might have looked up digests through that digest cache. Those
requestors will see changes by performing another lookup.

On setting the RESET_USER bit, emit a notification only for the inode
signaled by the LSM hook, since only the link between that inode and its
digest cache is changing (due to changing the security.digest_list xattr).

Finally, free the notification list with digest_cache_notify_inodes_free(),
when the digest cache is freed.

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 include/linux/digest_cache.h     |  34 ++++++++
 security/digest_cache/Makefile   |   2 +-
 security/digest_cache/internal.h |  26 ++++++
 security/digest_cache/main.c     |  12 +++
 security/digest_cache/notifier.c | 135 +++++++++++++++++++++++++++++++
 security/digest_cache/reset.c    |  42 +++++++++-
 6 files changed, 248 insertions(+), 3 deletions(-)
 create mode 100644 security/digest_cache/notifier.c

diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
index 9db8128513ca..950f3a58a861 100644
--- a/include/linux/digest_cache.h
+++ b/include/linux/digest_cache.h
@@ -15,6 +15,28 @@
 
 struct digest_cache;
 
+/**
+ * enum digest_cache_event - Events occurring on a digest cache
+ *
+ * This enum lists all the events occurring on a digest cache, to be notified
+ * outside the digest_cache LSM.
+ */
+enum digest_cache_event {
+	DIGEST_CACHE_RESET,
+};
+
+/**
+ * struct digest_cache_event_data - Information on digest cache events
+ * @digest_cache: Digest cache
+ * @inode: Inode for which the digest cache was requested
+ *
+ * This structure holds information about events occurring on a digest cache.
+ */
+struct digest_cache_event_data {
+	struct digest_cache *digest_cache;
+	struct inode *inode;
+};
+
 /**
  * typedef digest_cache_found_t - Digest cache reference as numeric value
  *
@@ -48,6 +70,8 @@ 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);
+int digest_cache_register_notifier(struct notifier_block *nb);
+int digest_cache_unregister_notifier(struct notifier_block *nb);
 
 #else
 static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
@@ -79,5 +103,15 @@ static inline void *digest_cache_verif_get(struct digest_cache *digest_cache,
 	return NULL;
 }
 
+static inline int digest_cache_register_notifier(struct notifier_block *nb)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int digest_cache_unregister_notifier(struct notifier_block *nb)
+{
+	return -EOPNOTSUPP;
+}
+
 #endif /* CONFIG_SECURITY_DIGEST_CACHE */
 #endif /* _LINUX_DIGEST_CACHE_H */
diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
index 3d5e600a2c45..7577b099c170 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -5,7 +5,7 @@
 obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 
 digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o \
-		  reset.o
+		  reset.o notifier.o
 
 digest_cache-y += parsers/tlv.o
 digest_cache-y += parsers/rpm.o
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
index c816929c4743..d930132cc963 100644
--- a/security/digest_cache/internal.h
+++ b/security/digest_cache/internal.h
@@ -21,6 +21,19 @@
 #define RESET			4	/* Digest cache to be recreated. */
 #define RESET_USER		5	/* Dig_user pointer to be released. */
 
+/**
+ * struct notify_inode - Structure with inode for which notification is emitted
+ * @list: Linked list
+ * @inode: Inode for which a notification is emitted
+ *
+ * This structure contains an inode for which a notification of a digest cache
+ * event is emitted.
+ */
+struct notify_inode {
+	struct list_head list;
+	struct inode *inode;
+};
+
 /**
  * struct readdir_callback - Structure to store information for dir iteration
  * @ctx: Context structure
@@ -126,6 +139,7 @@ struct htable {
  * @path_str: Path of the digest list the digest cache was created from
  * @flags: Control flags
  * @verif_data: Verification data regarding the digest list
+ * @notify_inodes: List of inodes for which a notification is emitted
  * @mutex: Protect digest cache modifications
  *
  * This structure represents a cache of digests extracted from a digest list.
@@ -137,6 +151,7 @@ struct digest_cache {
 	char *path_str;
 	unsigned long flags;
 	struct list_head verif_data;
+	struct list_head notify_inodes;
 	struct mutex mutex;
 };
 
@@ -261,4 +276,15 @@ void digest_cache_inode_post_setxattr(struct dentry *dentry, const char *name,
 void digest_cache_inode_post_removexattr(struct dentry *dentry,
 					 const char *name);
 
+/* notifier.c */
+int digest_cache_notify_inode_add(struct digest_cache *digest_cache,
+				  struct inode *inode);
+void digest_cache_notify_inodes_free(struct digest_cache *digest_cache);
+void digest_cache_notify(struct digest_cache *digest_cache, struct inode *inode,
+			 enum digest_cache_event event);
+void digest_cache_notify_multiple(struct digest_cache *digest_cache,
+				  enum digest_cache_event event);
+int digest_cache_register_notifier(struct notifier_block *nb);
+int digest_cache_unregister_notifier(struct notifier_block *nb);
+
 #endif /* _DIGEST_CACHE_INTERNAL_H */
diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
index ce3518a33c80..6fe0864f938f 100644
--- a/security/digest_cache/main.c
+++ b/security/digest_cache/main.c
@@ -51,6 +51,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);
 	INIT_LIST_HEAD(&digest_cache->dir_entries);
+	INIT_LIST_HEAD(&digest_cache->notify_inodes);
 	mutex_init(&digest_cache->mutex);
 
 	pr_debug("New digest cache %s (ref count: %d)\n",
@@ -70,6 +71,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);
+	digest_cache_notify_inodes_free(digest_cache);
 	mutex_destroy(&digest_cache->mutex);
 
 	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
@@ -398,6 +400,7 @@ 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);
+	int ret;
 
 	if (!digest_cache_enabled)
 		return NULL;
@@ -420,6 +423,15 @@ struct digest_cache *digest_cache_get(struct dentry *dentry)
 		/* 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) {
+			ret = digest_cache_notify_inode_add(dig_sec->dig_user,
+							    inode);
+			if (ret < 0) {
+				digest_cache_put(dig_sec->dig_user);
+				dig_sec->dig_user = NULL;
+			}
+		}
 	}
 
 	if (dig_sec->dig_user)
diff --git a/security/digest_cache/notifier.c b/security/digest_cache/notifier.c
new file mode 100644
index 000000000000..06e4730e4434
--- /dev/null
+++ b/security/digest_cache/notifier.c
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@huawei.com>
+ *
+ * Implement the notifier of the digest_cache LSM.
+ */
+
+#define pr_fmt(fmt) "DIGEST CACHE: "fmt
+
+#include "internal.h"
+
+static BLOCKING_NOTIFIER_HEAD(chain);
+
+static const char * const events_str[] = {
+	[DIGEST_CACHE_RESET] = __stringify(DIGEST_CACHE_RESET),
+};
+
+/**
+ * digest_cache_notify_inode_add - Add inode to digest cache notification list
+ * @digest_cache: Digest cache
+ * @inode: Inode to add
+ *
+ * This function adds an inode to the digest cache notification list, so that
+ * notifications are emitted for that inode.
+ *
+ * Return: Zero on success, -ENOMEM on error.
+ */
+int digest_cache_notify_inode_add(struct digest_cache *digest_cache,
+				  struct inode *inode)
+{
+	struct notify_inode *notify_inode;
+
+	notify_inode = kmalloc(sizeof(*notify_inode), GFP_KERNEL);
+	if (!notify_inode)
+		return -ENOMEM;
+
+	notify_inode->inode = inode;
+
+	mutex_lock(&digest_cache->mutex);
+	list_add_tail(&notify_inode->list, &digest_cache->notify_inodes);
+	pr_debug("Added inode %lu to notification list of digest cache %s\n",
+		 inode->i_ino, digest_cache->path_str);
+	mutex_unlock(&digest_cache->mutex);
+	return 0;
+}
+
+/**
+ * digest_cache_notify_inodes_free - Free digest cache notification list
+ * @digest_cache: Digest cache
+ *
+ * This function removes all inodes from the notification list of the passed
+ * digest cache and frees the memory. Does not need locking, since it is called
+ * only at the time the digest cache is freed.
+ */
+void digest_cache_notify_inodes_free(struct digest_cache *digest_cache)
+{
+	struct notify_inode *p, *q;
+
+	list_for_each_entry_safe(p, q, &digest_cache->notify_inodes, list) {
+		list_del(&p->list);
+		pr_debug("Removed inode %lu from notification list of digest cache %s\n",
+			 p->inode->i_ino, digest_cache->path_str);
+		kfree(p);
+	}
+}
+
+/**
+ * digest_cache_notify - Emit notification for an inode
+ * @digest_cache: Digest cache
+ * @inode: Inode for which a digest cache is used
+ * @event: Event to notify
+ *
+ * This function emits a notification of a digest cache event for an affected
+ * inode.
+ */
+void digest_cache_notify(struct digest_cache *digest_cache, struct inode *inode,
+			 enum digest_cache_event event)
+{
+	struct digest_cache_event_data event_data = {
+		.digest_cache = digest_cache,
+		.inode = inode,
+	};
+
+	pr_debug("Notify event %s for inode %lu using digest cache %s\n",
+		 events_str[event], inode->i_ino, digest_cache->path_str);
+
+	blocking_notifier_call_chain(&chain, event, &event_data);
+}
+
+/**
+ * digest_cache_notify_multiple - Emit notification for all inodes in the list
+ * @digest_cache: Digest cache
+ * @event: Event to notify
+ *
+ * This function emits a notification for all inodes in the notification list
+ * of the passed digest cache.
+ */
+void digest_cache_notify_multiple(struct digest_cache *digest_cache,
+				  enum digest_cache_event event)
+{
+	struct notify_inode *p;
+
+	list_for_each_entry(p, &digest_cache->notify_inodes, list)
+		digest_cache_notify(digest_cache, p->inode, event);
+}
+
+/**
+ * digest_cache_register_notifier() - Register a digest cache notifier
+ * @nb: Notifier block with the callback
+ *
+ * This function registers a new notifier for events occurring on digest caches.
+ *
+ * Return: Zero on success, -EEXIST on error.
+ */
+int digest_cache_register_notifier(struct notifier_block *nb)
+{
+	return blocking_notifier_chain_register(&chain, nb);
+}
+EXPORT_SYMBOL(digest_cache_register_notifier);
+
+/**
+ * digest_cache_unregister_notifier() - Unregister a digest cache notifier
+ * @nb: Notifier block with the callback
+ *
+ * This function unregisters a previously registered notifier.
+ *
+ * Return: Zero on success, -ENOENT on error.
+ */
+int digest_cache_unregister_notifier(struct notifier_block *nb)
+{
+	return blocking_notifier_chain_unregister(&chain, nb);
+}
+EXPORT_SYMBOL(digest_cache_unregister_notifier);
diff --git a/security/digest_cache/reset.c b/security/digest_cache/reset.c
index e07222b0e0e8..bb3b4108ba3b 100644
--- a/security/digest_cache/reset.c
+++ b/security/digest_cache/reset.c
@@ -10,6 +10,29 @@
 #define pr_fmt(fmt) "DIGEST CACHE: "fmt
 #include "internal.h"
 
+/**
+ * digest_cache_notify_dir_entry_reset - Notify dir entry reset
+ * @dir: Directory containing the digest lists
+ *
+ * Emit a notification for the directory containing the digest lists about a
+ * reset occurring on a directory entry.
+ */
+
+static void digest_cache_notify_dir_entry_reset(struct inode *dir)
+{
+	struct digest_cache_security *dir_dig_sec;
+
+	dir_dig_sec = digest_cache_get_security(dir);
+	if (unlikely(!dir_dig_sec))
+		return;
+
+	mutex_lock(&dir_dig_sec->dig_owner_mutex);
+	if (dir_dig_sec->dig_owner)
+		digest_cache_notify_multiple(dir_dig_sec->dig_owner,
+					     DIGEST_CACHE_RESET);
+	mutex_unlock(&dir_dig_sec->dig_owner_mutex);
+}
+
 /**
  * digest_cache_reset_owner - Reset dig_owner
  * @inode: Inode of the digest list/directory containing the digest list
@@ -19,6 +42,9 @@
  * (if unset), so that digest_cache_get() and digest_cache_create() respectively
  * release and clear dig_user and dig_owner in the inode security blob. This
  * causes new callers of digest_cache_get() to get a new digest cache.
+ *
+ * After setting RESET, it emits a notification for all inodes using the digest
+ * cache.
  */
 static void digest_cache_reset_owner(struct inode *inode, const char *reason)
 {
@@ -30,9 +56,12 @@ static void digest_cache_reset_owner(struct inode *inode, const char *reason)
 
 	mutex_lock(&dig_sec->dig_owner_mutex);
 	if (dig_sec->dig_owner &&
-	    !test_and_set_bit(RESET, &dig_sec->dig_owner->flags))
+	    !test_and_set_bit(RESET, &dig_sec->dig_owner->flags)) {
 		pr_debug("Resetting %s (dig_owner), reason: %s\n",
 			 dig_sec->dig_owner->path_str, reason);
+		digest_cache_notify_multiple(dig_sec->dig_owner,
+					     DIGEST_CACHE_RESET);
+	}
 	mutex_unlock(&dig_sec->dig_owner_mutex);
 }
 
@@ -55,9 +84,12 @@ static void digest_cache_reset_user(struct inode *inode, const char *reason)
 
 	mutex_lock(&dig_sec->dig_user_mutex);
 	if (dig_sec->dig_user &&
-	    !test_and_set_bit(RESET_USER, &dig_sec->dig_user->flags))
+	    !test_and_set_bit(RESET_USER, &dig_sec->dig_user->flags)) {
 		pr_debug("Resetting %s (dig_user), reason: %s\n",
 			 dig_sec->dig_user->path_str, reason);
+		digest_cache_notify(dig_sec->dig_user, inode,
+				    DIGEST_CACHE_RESET);
+	}
 	mutex_unlock(&dig_sec->dig_user_mutex);
 }
 
@@ -74,11 +106,14 @@ static void digest_cache_reset_user(struct inode *inode, const char *reason)
 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_owner(inode, "file_truncate");
+	/* Dir digest cache users should know a dir entry changed. */
+	digest_cache_notify_dir_entry_reset(dir);
 	return 0;
 }
 
@@ -101,6 +136,9 @@ void digest_cache_file_release(struct file *file)
 	digest_cache_reset_owner(file_inode(file), "file_file_release");
 	if (file->f_mode & FMODE_CREATED)
 		digest_cache_reset_owner(dir, "dir_file_release");
+	else
+		/* Dir digest cache users should know a dir entry changed. */
+		digest_cache_notify_dir_entry_reset(dir);
 }
 
 /**
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 13/14] selftests/digest_cache: Add selftests for digest_cache LSM
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (11 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 12/14] digest_cache: Notify digest cache events Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 19:47   ` Jarkko Sakkinen
  2024-04-15 14:24 ` [PATCH v4 14/14] docs: Add documentation of the " Roberto Sassu
                   ` (2 subsequent siblings)
  15 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

Add tests to verify the correctness of the digest_cache LSM, in all_test.c.

Add the kernel module digest_cache_kern.ko, to let all_test call the API
of the digest_cache LSM through the newly introduced digest_cache_test file
in securityfs.

Test coverage information:

File 'security/digest_cache/notifier.c'
Lines executed:100.00% of 31
File 'security/digest_cache/reset.c'
Lines executed:98.36% of 61
File 'security/digest_cache/main.c'
Lines executed:90.29% of 206
File 'security/digest_cache/modsig.c'
Lines executed:42.86% of 21
File 'security/digest_cache/htable.c'
Lines executed:93.02% of 86
File 'security/digest_cache/populate.c'
Lines executed:92.86% of 56
File 'security/digest_cache/verif.c'
Lines executed:89.74% of 39
File 'security/digest_cache/dir.c'
Lines executed:90.62% of 96
File 'security/digest_cache/secfs.c'
Lines executed:57.14% of 21
File 'security/digest_cache/parsers/tlv.c'
Lines executed:79.75% of 79
File 'security/digest_cache/parsers/rpm.c'
Lines executed:88.46% of 78

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 MAINTAINERS                                   |   1 +
 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 | 815 ++++++++++++++++++
 tools/testing/selftests/digest_cache/common.c |  78 ++
 tools/testing/selftests/digest_cache/common.h | 135 +++
 .../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     | 564 ++++++++++++
 14 files changed, 1969 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/MAINTAINERS b/MAINTAINERS
index 72801a88449c..d7f700da009e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6198,6 +6198,7 @@ M:	Roberto Sassu <roberto.sassu@huawei.com>
 L:	linux-security-module@vger.kernel.org
 S:	Maintained
 F:	security/digest_cache/
+F:	tools/testing/selftests/digest_cache/
 
 DIGITEQ AUTOMOTIVE MGB4 V4L2 DRIVER
 M:	Martin Tuma <martin.tuma@digiteqautomotive.com>
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 15b6a111c3be..3c5965a62d28 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -13,6 +13,7 @@ TARGETS += core
 TARGETS += cpufreq
 TARGETS += cpu-hotplug
 TARGETS += damon
+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..6b1e0d3c08cf
--- /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)
+	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..9f45e522c43c
--- /dev/null
+++ b/tools/testing/selftests/digest_cache/all_test.c
@@ -0,0 +1,815 @@
+// 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 digest_cache LSM.
+ */
+
+#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;
+	int notify_inodesfd;
+};
+
+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 notify inodes interface. */
+	self->notify_inodesfd = open(DIGEST_CACHE_NOTIFY_INODES_INTERFACE,
+				     O_RDWR, 0600);
+	ASSERT_NE(-1, self->notify_inodesfd);
+
+	/* 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 notify inodes interface. */
+	close(self->notify_inodesfd);
+
+	/* 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 notify_inode_init(struct _test_data_shared_data *self,
+			      struct __test_metadata *_metadata)
+{
+	/* Clear buffer. */
+	ASSERT_EQ(1, write(self->notify_inodesfd, "1", 1));
+}
+
+static void notify_inodes_check(struct _test_data_shared_data *self,
+				struct __test_metadata *_metadata,
+				char *filenames)
+{
+	char notify_inodes_buf[1024] = { 0 };
+	char notify_inodes_buf_kernel[1024] = { 0 };
+	char *filename, *filenames_copy, *buf_ptr = notify_inodes_buf;
+	struct stat st;
+	int fd;
+
+	ASSERT_LT(0, read(self->notify_inodesfd, notify_inodes_buf_kernel,
+			  sizeof(notify_inodes_buf_kernel)));
+
+	filenames_copy = strdup(filenames);
+	ASSERT_NE(NULL, filenames_copy);
+
+	while ((filename = strsep(&filenames_copy, ","))) {
+		fd = openat(self->base_dirfd, filename, O_RDONLY);
+		ASSERT_NE(-1, fd);
+		ASSERT_EQ(0, fstat(fd, &st));
+		close(fd);
+
+		buf_ptr += snprintf(buf_ptr,
+				    sizeof(notify_inodes_buf) -
+				    (buf_ptr - notify_inodes_buf), "%s%lu",
+				    notify_inodes_buf[0] ? "," : "", st.st_ino);
+	}
+
+	free(filenames_copy);
+
+	ASSERT_EQ(0, strcmp(notify_inodes_buf, notify_inodes_buf_kernel));
+}
+
+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));
+
+	notify_inode_init(self, _metadata);
+
+	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));
+
+		notify_inodes_check(self, _metadata, "file");
+		return;
+	default:
+		break;
+	}
+
+	ASSERT_NE(0, query_test(self->kernfd, self->base_dir, "file",
+				HASH_ALGO_SHA1, 0, 1));
+
+	notify_inodes_check(self, _metadata, "file");
+}
+
+/*
+ * 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)
+{
+	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;
+			}
+		}
+
+		ASSERT_EQ(expected_ret, query_test(self->kernfd, self->base_dir,
+						   "file", 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 digest_list_filename[NAME_MAX + 1];
+	int i, removed[10];
+
+	for (i = 0; i < 10; i++) {
+		snprintf(digest_list_filename, sizeof(digest_list_filename),
+			 "tlv-digest_list%d", 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", NULL));
+	/* The second file is to have duplicate notifications (file and dir). */
+	ASSERT_EQ(0, create_file(self->base_dirfd, "file2",
+				 "tlv-digest_list7"));
+	/* The query adds file2 inode to the file digest cache notif. list. */
+	ASSERT_NE(0, query_test(self->kernfd, self->base_dir, "file2",
+				HASH_ALGO_SHA1, 0, 1));
+
+	query_test_with_failures(self, _metadata, 0, 10, removed, 0);
+
+	notify_inode_init(self, _metadata);
+	ASSERT_EQ(0, unlinkat(self->digest_lists_dirfd, "tlv-digest_list7", 0));
+	/* File notification comes before directory notification. */
+	notify_inodes_check(self, _metadata, "file2,file");
+
+	removed[0] = 7;
+
+	query_test_with_failures(self, _metadata, 0, 10, removed, 1);
+
+	notify_inode_init(self, _metadata);
+	ASSERT_EQ(0, renameat(self->digest_lists_dirfd, "tlv-digest_list6",
+			      self->base_dirfd, "tlv-digest_list6"));
+	notify_inodes_check(self, _metadata, "file");
+
+	removed[1] = 6;
+
+	query_test_with_failures(self, _metadata, 0, 10, removed, 2);
+
+	notify_inode_init(self, _metadata);
+	ASSERT_EQ(0, renameat(self->base_dirfd, "tlv-digest_list6",
+			      self->digest_lists_dirfd, "tlv-digest_list6"));
+	notify_inodes_check(self, _metadata, "file");
+
+	query_test_with_failures(self, _metadata, 0, 10, removed, 1);
+
+	notify_inode_init(self, _metadata);
+	ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, "tlv-digest_list10",
+				  HASH_ALGO_SHA1, 10, 1, TLV_NO_FAILURE));
+	notify_inodes_check(self, _metadata, "file");
+
+	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));
+	}
+
+	ASSERT_EQ(0, create_file(self->base_dirfd, "file", NULL));
+
+	for (i = 0; i < 11; i++) {
+		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], digest_lists_kernel[1024] = { 0 };
+	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, sizeof(digest_lists)));
+
+	for (i = start_number; i <= end_number; i++) {
+		if (digest_lists_kernel[0])
+			strcat(digest_lists_kernel, ",");
+
+		snprintf(digest_list_filename, sizeof(digest_list_filename),
+			 "%d-tlv-digest_list%d", i, i);
+		strcat(digest_lists_kernel, 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], digest_lists_kernel[1024] = { 0 };
+	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, sizeof(digest_lists)));
+
+	for (i = 0; i < NUM_DIGEST_LISTS_PREFETCH; i++) {
+		if (digest_lists_kernel[0])
+			strcat(digest_lists_kernel, ",");
+
+		snprintf(digest_list_filename, sizeof(digest_list_filename),
+			 "%d-tlv-digest_list%d", i, i);
+		strcat(digest_lists_kernel, 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..2123f7d937ce
--- /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 digest_cache LSM.
+ */
+
+#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] = "filenames",
+	[VERIF_NUMBER] = "number",
+	[VERIF_PREFETCH] = "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..e52e4b137807
--- /dev/null
+++ b/tools/testing/selftests/digest_cache/common.h
@@ -0,0 +1,135 @@
+/* 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_TEST_INTERFACE "/sys/kernel/security/digest_cache_test"
+#define DIGEST_CACHE_PATH_INTERFACE "/sys/kernel/security/digest_cache_path"
+#define DIGEST_CACHE_NOTIFY_INODES_INTERFACE  \
+	"/sys/kernel/security/digest_cache_notify_inodes"
+#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;
+} __attribute__ ((__packed__));
+
+struct rpm_entryinfo {
+	s32 tag;
+	u32 type;
+	s32 offset;
+	u32 count;
+} __attribute__ ((__packed__));
+
+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..1bacadad6b6a
--- /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 digest_cache LSM.
+ */
+
+#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..075a06cc4f8e
--- /dev/null
+++ b/tools/testing/selftests/digest_cache/config
@@ -0,0 +1 @@
+CONFIG_SECURITY_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..7215ef638e66
--- /dev/null
+++ b/tools/testing/selftests/digest_cache/testmod/kern.c
@@ -0,0 +1,564 @@
+// 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 digest_cache LSM.
+ */
+
+#define pr_fmt(fmt) "DIGEST CACHE TEST: "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, *notify_inodes;
+static struct digest_cache *digest_cache;
+static digest_cache_found_t found;
+static int cur_verif_index;
+static u8 prefetch_buf[4096];
+static u8 notify_inodes_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, "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, "number", &number, sizeof(number));
+}
+
+static ssize_t filenames_read(struct file *file, char __user *buf,
+			      size_t datalen, loff_t *ppos)
+{
+	loff_t _ppos = 0;
+	char *filenames_list;
+
+	filenames_list = digest_cache_verif_get(found ?
+				digest_cache_from_found_t(found) : digest_cache,
+				verifs_str[VERIF_FILENAMES]);
+	if (!filenames_list)
+		return -ENOENT;
+
+	return simple_read_from_buffer(buf, datalen, &_ppos, filenames_list,
+				       strlen(filenames_list) + 1);
+}
+
+static ssize_t number_read(struct file *file, char __user *buf, size_t datalen,
+			   loff_t *ppos)
+{
+	loff_t _ppos = 0;
+	u64 *number;
+	char temp[20];
+	ssize_t len;
+
+	number = digest_cache_verif_get(found ?
+					digest_cache_from_found_t(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 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, "probe_digest_cache", "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 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 int test_digest_cache_change(struct notifier_block *nb,
+				    unsigned long event, void *data)
+{
+	struct digest_cache_event_data *event_data = data;
+	char i_ino_str[10];
+
+	if (event != DIGEST_CACHE_RESET)
+		return NOTIFY_DONE;
+
+	if (notify_inodes_buf[0])
+		strlcat(notify_inodes_buf, ",", sizeof(notify_inodes_buf));
+
+	snprintf(i_ino_str, sizeof(i_ino_str), "%lu", event_data->inode->i_ino);
+	strlcat(notify_inodes_buf, i_ino_str, sizeof(notify_inodes_buf));
+	return 0;
+}
+
+static struct notifier_block digest_cache_notifier = {
+	.notifier_call = test_digest_cache_change,
+};
+
+static ssize_t write_notify_inodes(struct file *file, const char __user *buf,
+			     size_t datalen, loff_t *ppos)
+{
+	memset(notify_inodes_buf, 0, sizeof(notify_inodes_buf));
+	return datalen;
+}
+
+static ssize_t read_notify_inodes(struct file *file, char __user *buf,
+				  size_t datalen, loff_t *ppos)
+{
+	loff_t _ppos = 0;
+
+	return simple_read_from_buffer(buf, datalen, &_ppos, notify_inodes_buf,
+				       strlen(notify_inodes_buf) + 1);
+}
+
+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 const struct file_operations digest_cache_notify_inodes_ops = {
+	.open = generic_file_open,
+	.write = write_notify_inodes,
+	.read = read_notify_inodes,
+	.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;
+	}
+
+	notify_inodes = securityfs_create_file("digest_cache_notify_inodes",
+					       0660, NULL, NULL,
+					       &digest_cache_notify_inodes_ops);
+	if (IS_ERR(notify_inodes)) {
+		ret = PTR_ERR(notify_inodes);
+		goto out_test;
+	}
+
+	ret = digest_cache_register_notifier(&digest_cache_notifier);
+	if (ret < 0)
+		goto out_notify_inodes;
+
+	return 0;
+
+out_notify_inodes:
+	securityfs_remove(notify_inodes);
+out_test:
+	securityfs_remove(test);
+out_kprobe:
+	unregister_kprobe(&kp);
+	return ret;
+}
+
+static void __exit digest_cache_test_fini(void)
+{
+	if (digest_cache)
+		digest_cache_put(digest_cache);
+
+	digest_cache_unregister_notifier(&digest_cache_notifier);
+	securityfs_remove(notify_inodes);
+	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");
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* [PATCH v4 14/14] docs: Add documentation of the digest_cache LSM
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (12 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 13/14] selftests/digest_cache: Add selftests for digest_cache LSM Roberto Sassu
@ 2024-04-15 14:24 ` Roberto Sassu
  2024-04-15 19:18 ` [PATCH v4 00/14] security: " Jarkko Sakkinen
  2024-04-16  4:49 ` Bagas Sanjaya
  15 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:24 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

From: Roberto Sassu <roberto.sassu@huawei.com>

Add the documentation of the digest_cache LSM in Documentation/security.

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 Documentation/security/digest_cache.rst | 763 ++++++++++++++++++++++++
 Documentation/security/index.rst        |   1 +
 MAINTAINERS                             |   1 +
 3 files changed, 765 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..f7c2b1bcf25b
--- /dev/null
+++ b/Documentation/security/digest_cache.rst
@@ -0,0 +1,763 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+================
+Digest_cache LSM
+================
+
+Introduction
+============
+
+Integrity detection and protection has long been a desirable feature, to
+reach a large user base and mitigate the risk of flaws in the software and
+attacks.
+
+However, while solutions exist, they struggle to reach the large user base,
+due to requiring higher than desired constraints on performance,
+flexibility and configurability, that only security conscious people are
+willing to accept.
+
+This is where the new digest_cache LSM comes into play, it offers
+additional support for new and existing integrity solutions, to make them
+faster and easier to deploy.
+
+
+Motivation
+==========
+
+The digest_cache LSM helps to address two important shortcomings of the
+Integrity Measurement Architecture (IMA): predictability of the Platform
+Configuration Registers (PCRs), and the provisioning of reference values to
+compare the calculated file digest against.
+
+Remote attestation, according to Trusted Computing Group (TCG)
+specifications, is done by replicating the PCR extend operation in
+software with the digests in the event log (in this case the IMA
+measurement list), and by comparing the obtained value with the PCR value
+signed by the TPM with the quote operation.
+
+Due to how the extend operation is performed, if measurements are done in
+a different order, the final PCR value will be different. That means that
+if measurements are done in parallel, there is no way to predict what the
+final PCR value will be, making impossible to seal data to a PCR value. If
+the PCR value was predictable, a system could for example prove its
+integrity by unsealing and using its private key, without sending every
+time the full list of measurements.
+
+Provisioning reference values for file digests is also a difficult task.
+The solution so far was to add file signatures to RPM packages, and
+possibly to DEB packages, so that IMA can verify them. While this undoubtly
+works, it also requires Linux distribution vendors to support the feature
+by rebuilding all their packages, and eventually extending their PKI to
+perform the additional signatures. It could also require developers extra
+work to deal with the additional data.
+
+On the other hand, since often packages carry the file digests themselves,
+it won't be actually needed to add file signatures. If the kernel was able
+to extract the file digests by itself, all the tasks mentioned above for
+the Linux distribution vendors won't be needed too. All current and past
+Linux distributions can be easily retrofitted to enable IMA appraisal with
+the file digests from the packages.
+
+Narrowing down the scope of a package parser to only extract specific
+information makes it small enough to accurately verify that it cannot harm
+the kernel. In fact, the parsers included with the digest_cache LSM have
+been verified with the formal verification tool Frama-C, albeit with a
+limited buffer size (the verification time grows considerably with bigger
+buffer sizes). The parsers with the Frama-C assertions are available here:
+
+https://github.com/robertosassu/rpm-formal/
+
+Frama-C asserts that the parsers don't read beyond their assigned buffer
+for any byte combination.
+
+An additional mitigation against corrupted digest lists consists in
+verifying the signature of the package first, before attempting to extract
+the file digests.
+
+
+Solution
+========
+
+The digest_cache LSM can help IMA to extend a PCR in a deterministic way.
+If IMA knows that a file comes from a Linux distribution, it can measure
+files in a different way: measure the list of digests coming from the
+distribution (e.g. RPM package headers), and subsequently measure a file if
+it is not found in that list.
+
+If the system executes known files, it does not matter in which order they
+are executed, because the PCR is not extended. That however means that the
+lists of digests must be measured in a deterministic way. The digest_cache
+LSM has a prefetching mechanism to make this happen, consisting in
+sequentially reading digest lists in a directory until it finds the
+requested one.
+
+The resulting IMA measurement list however has a disadvantage: it does not
+tell to remote verifiers whether files with digest in the measured digest
+lists have been accessed or not and when. Also the IMA measurement list
+would change after a software update.
+
+The digest_cache LSM can also help IMA for appraisal. Currently, IMA has
+to evaluate the signature of each file individually, and expects that the
+Linux vendors include those signatures together with the files in the
+packages.
+
+With the digest_cache LSM, IMA can simply lookup in the list of digests
+extracted from package headers, once the signature of those headers has
+been verified. The same approach can be followed by other LSMs, such as
+Integrity Policy Enforcement (IPE).
+
+
+Design
+======
+
+Digest cache
+------------
+
+Main idea
+~~~~~~~~~
+
+The digest_cache LSM 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,
+the integrity state of the digest list, and the inodes for which the digest
+cache is used.
+
+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 is
+evaluated independently by other LSMs, for example by verifying the
+signature of the digest list, and is provided to the digest_cache LSM
+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 digest_cache LSM can extract digests from a digest list, only if it has
+a parser for its format. Currently, it supports a TLV-based and the RPM
+package header formats, and can support more in the future.
+
+
+Digest list lookup
+~~~~~~~~~~~~~~~~~~
+
+In order to build a digest cache and return it to the caller for performing
+a query, the digest_cache LSM 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 digest_cache LSM 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 digest_cache LSM does not know which digest list to select,
+and creates an iterator with a snapshot of the directory entries. During a
+query, the digest_cache LSMs 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 digest_cache LSM 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 digest_cache LSM still creates a directory
+iterator like in (2). It reads digest lists with a file name that does not
+match the security.digest_list xattr, to trigger a measurement, and creates
+a digest cache from the matching one. This is also known as the prefetching
+mechanism, introduced later.
+
+
+Digest cache creation
+~~~~~~~~~~~~~~~~~~~~~
+
+Digest list naming convention
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Once the digest_cache LSM selected the digest list to use, it reads the
+file and calls the appropriate parser to extract the digests, based on the
+filename 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
+digest_cache LSM 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 iterator.
+
+Digest cache create API
+^^^^^^^^^^^^^^^^^^^^^^^
+
+The digest_cache LSM offers an API for parsers to initialize and add
+digests to the digest cache hash tables.
+
+It exposes digest_cache_htable_init() to initialize a hash table for a
+given algorithm, and to size it depending on the number of digests to add,
+normally known by the parsers 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.
+
+It also exposes digest_cache_htable_add(), to let parsers add extracted
+digests to the new hash table. If parsers need to add digests created with
+different algorithms, they can create as many hash tables as they need.
+
+Finally, parsers can also call digest_cache_htable_lookup() to lookup a
+digest in the passed digest_cache.
+
+Digest cache caching on create
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Since the same digest cache can be requested multiple times for
+verification of different inodes (e.g. installed files belonging to the
+same software package), a pointer to the newly created digest cache (named
+dig_owner) is stored in the inode security blob of the digest list.
+
+Dig_owner check and assignment is protected by the dig_owner_mutex, also
+stored in the inode security blob. The first requestor instantiates and
+populates the new digest cache. The other lock contenders wait until the
+lock is released and until the first requestor clears the INIT_IN_PROGRESS
+bit in the digest cache bit mask. The latter is needed to avoid lock
+inversion with the code tracking changes on digest lists/default directory.
+
+
+Digest cache request
+~~~~~~~~~~~~~~~~~~~~
+
+Users of the digest_cache LSM can request a digest cache by calling
+digest_cache_get(), passing the inode for which they need a digest cache,
+and can release it with digest_cache_put() once they are done. As mentioned
+above, the digest_cache LSM determines which digest list the digest cache
+should be built/retrieved from.
+
+Digest cache caching on request
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To avoid having to find the digest list inode for every digest_cache_get()
+call, also a pointer to the retrieved digest cache (named dig_user) is
+stored in the security blob of the inode for which the digest cache is
+requested.
+
+Dig_user is also protected by its own dig_user_mutex (stored in the same
+inode security blob) for check and assignment. Multiple requestors of a
+digest cache for the same inode have to wait until the first requestor
+finds the digest list inode and obtains the digest cache.
+
+Digest cache reference count
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Since digest cache pointers are stored in the inode security blobs and
+returned to the digest_cache_get() callers, the digest_cache LSM must track
+how many pointers are around, to avoid freeing a digest cache while it is
+still in use.
+
+The digest_cache LSM records the number of such pointers in a per digest
+cache reference count, and increments it every time the pointer is stored
+in a new inode security blob (either dig_owner or dig_user), or returned by
+digest_cache_get(), and decrements it when an inode is evicted from memory
+or a caller of digest_cache_get() calls digest_cache_put().
+
+
+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 (digest_cache_found_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 the digest_cache LSM took the option (2) (multiple digest lists in the
+default directory but which one to use is unknown), digest_cache_get()
+returns an iterator instead, to be passed to digest_cache_lookup().
+
+Only the latter finally returns the digest cache containing the searched
+digest. If the digest is not found, digest_cache_lookup() returns zero.
+
+
+Verification data
+~~~~~~~~~~~~~~~~~
+
+Until now, the caller of the digest_cache LSM 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 digest_cache LSM 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.
+
+The digest_cache LSM 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.
+
+Those integrity providers should implement the kernel_post_read_file LSM
+hook and call digest_cache_verif_set(), passing the digest list file
+descriptor, the unique ID and their evaluation result of the digest list.
+
+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 an
+iterator, that call results in a NULL pointer, since the iterator is
+not populated with 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
+conversion to a digest cache pointer (with digest_cache_from_found_t()).
+
+
+Tracking 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 digest_cache LSM 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 and
+sets the RESET bit.
+
+The next time that digest cache is requested, both dig_user and dig_owner
+are passed to digest_cache_put() and cleared. A new digest cache is
+created, as if there wasn't one in the first place.
+
+For the last two hooks, the RESET_USER bit is set instead, to limit
+clearing dig_user, since only retrieval of the digest list could change
+after modifying the security.digest_list xattr, and not the digest cache
+itself.
+
+Nothing changes for 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 not be freed until those
+callers also call digest_cache_put() and the reference count reaches zero.
+
+Notify 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 based on that digest cache.
+
+IMA for example, would not be able to recheck a file digest against a
+modified digest cache, since it is not aware of the reset in the first
+place.
+
+Introduce a subscription-based notification mechanism, that dispatches to
+the interested parties events which include the type of event (e.g. reset)
+and the digest cache and inodes affected. A user of the digest_cache LSM
+can become a subscriber by calling digest_cache_register_notifier() and can
+unsubscribe by calling digest_cache_unregister_notifier().
+
+During a digest_cache_get(), add the inode for which the digest cache was
+requested to a notification list of the same digest cache. When the RESET
+bit is set, emit a event for each inode in that notification list, so that
+IMA and the other integrity providers can eventually invalidate their
+cached security decision on that inode.
+
+On a file digest cache reset, notify also users of the parent directory
+digest cache, since they might have looked up digests through that digest
+cache. Those users will see changes by performing another lookup.
+
+When the RESET_USER bit is set, emit a notification just for the inode
+signalled by the LSM hook, since the operation causing a reset
+(set/removexattr) only affects the link between the inode and the digest
+cache, and not the digest cache itself.
+
+Prefetching mechanism
+~~~~~~~~~~~~~~~~~~~~~
+
+One of the objectives of the digest_cache LSM 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 list of the directory entries of the iterator
+created for the default 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 iterator
+(entries with lower seq nums are before entries with higher seq nums).
+Digest lists without that prefix are added at the end of iterator list,
+in the same order as iterate_dir() shows them.
+
+With ``[<seq num>-]``, the latency of digest cache creation when the
+prefetching mechanism is enabled can be significantly reduced for example
+by ordering digest lists by their appearance in the IMA measurement list,
+since that list reflects the order in which digest lists are requested at
+boot.
+
+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 iterator.
+
+
+Data structures and API
+=======================
+
+Data structures
+---------------
+
+These are the data structures defined and used internally by the
+digest_cache LSM.
+
+.. kernel-doc:: security/digest_cache/internal.h
+
+
+Public API
+----------
+
+This API is meant to be used by users of the digest_cache LSM.
+
+.. kernel-doc:: include/linux/digest_cache.h
+		:identifiers: digest_cache_found_t
+		              digest_cache_from_found_t
+
+.. kernel-doc:: security/digest_cache/main.c
+		:identifiers: digest_cache_get digest_cache_put
+
+.. kernel-doc:: security/digest_cache/htable.c
+		:identifiers: digest_cache_lookup
+
+.. kernel-doc:: security/digest_cache/verif.c
+		:identifiers: digest_cache_verif_set digest_cache_verif_get
+
+.. kernel-doc:: security/digest_cache/notifier.c
+		:identifiers: digest_cache_register_notifier
+			      digest_cache_unregister_notifier
+
+
+Parser API
+----------
+
+This API is meant to be used by digest list parsers.
+
+.. kernel-doc:: security/digest_cache/htable.c
+		:identifiers: digest_cache_htable_init
+		              digest_cache_htable_add
+			      digest_cache_htable_lookup
+
+
+Digest List Formats
+===================
+
+tlv
+---
+
+The Type-Length-Value (TLV) format was chosen for its extensibility.
+Additional fields can be added without breaking compatibility with old
+versions of the parser.
+
+The layout of a tlv digest list is the following::
+
+ [header: DIGEST_LIST_FILE, num fields, total len]
+ [field: DIGEST_LIST_ALGO, length, value]
+ [field: DIGEST_LIST_ENTRY#1, length, value (below)]
+  |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len]
+  |- [DIGEST_LIST_ENTRY_DIGEST#1, length, file digest]
+  |- [DIGEST_LIST_ENTRY_PATH#1, length, file path]
+ [field: DIGEST_LIST_ENTRY#N, length, value (below)]
+  |- [header: DIGEST_LIST_ENTRY_DATA, num fields, total len]
+  |- [DIGEST_LIST_ENTRY_DIGEST#N, length, file digest]
+  |- [DIGEST_LIST_ENTRY_PATH#N, length, file path]
+
+DIGEST_LIST_ALGO is a field to specify the algorithm of the file digest.
+DIGEST_LIST_ENTRY is a nested TLV structure with the following fields:
+DIGEST_LIST_ENTRY_DIGEST contains the file digest; DIGEST_LIST_ENTRY_PATH
+contains the file path.
+
+
+rpm
+---
+
+The rpm digest list is basically a subset of the RPM package header.
+Its format is::
+
+ [RPM magic number]
+ [RPMTAG_IMMUTABLE]
+
+RPMTAG_IMMUTABLE is a section of the full RPM header containing the part
+of the header that was signed, and whose signature is stored in the
+RPMTAG_RSAHEADER section.
+
+
+Appended Signature
+------------------
+
+Digest lists can have a module-style appended signature, that can be used
+for appraisal with IMA. The signature type can be PKCS#7, as for kernel
+modules, or a different type.
+
+
+History
+=======
+
+The original name of this work was IMA Digest Lists, which was somehow
+considered too invasive. The code was moved to a separate component named
+DIGLIM (DIGest Lists Integrity Module), with the purpose of removing the
+complexity away of IMA, and also adding the possibility of using it with
+other kernel components (e.g. Integrity Policy Enforcement, or IPE).
+
+The design changed significantly, so DIGLIM was renamed to digest_cache
+LSM, as the name better reflects what the new component does.
+
+Since it was originally proposed, in 2017, this work grew up a lot thanks
+to various comments/suggestions. It became integrally part of the openEuler
+distribution since end of 2020.
+
+The most important difference between the old the current version is moving
+from a centralized repository of file digests to a per-package repository.
+This significantly reduces the memory pressure, since digest lists are
+loaded into kernel memory only when they are actually needed. Also, file
+digests are automatically unloaded from kernel memory at the same time
+inodes are evicted from memory during reclamation.
+
+
+Performance
+===========
+
+System specification
+--------------------
+
+The tests have been performed on a Fedora 38 virtual machine with 4 cores
+(AMD EPYC-Rome, no hyperthreading), 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 digest_cache LSM has been tested with an ad-hoc benchmark tool that
+creates 20000 files with a random size up to 100 bytes and randomly adds
+their digest to one of 303 digest lists. The number of digest lists has
+been derived from the ratio (66) digests/packages (124174/1883) found in
+the testing virtual machine (hence, 20000/66 = 303). IMA signatures have
+been done with ECDSA NIST P-384.
+
+The benchmark tool then creates a list of 20000 files to be accessed,
+randomly chosen (there can be duplicates). This is necessary to make the
+results reproducible across reboots (by always replaying the same
+operations). The benchmark reads (sequentially and in parallel) the files
+from the list 2 times, flushing the kernel caches before each read.
+
+Each test has been performed 5 times, and the average value is taken.
+
+
+Purpose of the benchmark
+------------------------
+
+The purpose of the benchmark is to show the performance difference of IMA
+between the current behavior, and by using the digest_cache LSM.
+
+
+IMA measurement policy: no cache
+--------------------------------
+
+.. code-block:: bash
+
+ measure func=FILE_CHECK fowner=2001 pcr=12
+
+
+IMA measurement policy: cache
+-----------------------------
+
+.. code-block:: bash
+
+ measure func=DIGEST_LIST_CHECK pcr=12
+ measure func=FILE_CHECK fowner=2001 digest_cache=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 | 33.65 / 102.51 / 47.13 |   84170   |
+ +--------------------+-------+------------------------+-----------+
+ | cache, no prefetch |   304 | 34.04 / 33.32 / 33.09  |   81159   |
+ +--------------------+-------+------------------------+-----------+
+ | cache, prefetch    |   304 | 34.02 / 33.31 / 33.15  |   81122   |
+ +--------------------+-------+------------------------+-----------+
+
+The table shows that 12313 measurements (boot_aggregate + files) have been
+made without the digest cache, and 304 with the digest cache
+(boot_aggregate + digest lists). Consequently, the memory occupation
+without the cache is higher due to the higher number of measurements.
+
+Not surprisingly, for the same reason, also the test time is significantly
+higher without the digest cache when the physical or virtual TPM is used.
+
+In terms of pure performance, first number in the third column, it can be
+seen that there are not really performance differences between using or not
+using the digest cache.
+
+Prefetching does not add overhead, also because digest lists were ordered
+according to their appearance in the IMA measurement list (which minimize
+the digest lists to prefetch).
+
+
+Parallel
+~~~~~~~~
+
+This test was performed reading files in parallel, not waiting for the
+current read to terminate.
+
+::
+
+                      +-------+-----------------------+-----------+
+                      | meas. | time no/p/vTPM (sec.) | slab (KB) |
+ +--------------------+-------+-----------------------+-----------+
+ | no cache           | 12313 | 14.08 / 79.09 / 22.70 |   85138   |
+ +--------------------+-------+-----------------------+-----------+
+ | cache, no prefetch |   304 | 14.44 / 15.11 / 14.96 |   85777   |
+ +--------------------+-------+-----------------------+-----------+
+ | cache, prefetch    |   304 | 14.30 / 15.41 / 14.40 |   83294   |
+ +--------------------+-------+-----------------------+-----------+
+
+Also in this case, the physical TPM causes the biggest delay especially
+without digest cache, where a higher number of measurements need to be
+extended in the TPM.
+
+The digest_cache LSM does not introduce a noticeable overhead in all
+scenarios.
+
+
+IMA appraisal policy: no cache
+------------------------------
+
+.. code-block:: bash
+
+ appraise func=FILE_CHECK fowner=2001
+
+
+IMA appraisal policy: cache
+---------------------------
+
+.. code-block:: bash
+
+ appraise func=DIGEST_LIST_CHECK
+ appraise func=FILE_CHECK fowner=2001 digest_cache=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    |    96.74    |   78827   |
+ +----------------------------+-------------+-------------+-----------+
+ | appraise (cache)           | 12312 + 303 |    33.09    |   80854   |
+ +----------------------------+-------------+-------------+-----------+
+ | appraise (cache, prefetch) | 12312 + 303 |    33.42    |   81050   |
+ +----------------------------+-------------+-------------+-----------+
+
+This test shows a huge performance difference from verifying the signature
+of 12312 files as opposed to just verifying the signature of 303 digest
+lists, and looking up the digest of the files being read.
+
+There are some differences in terms of memory occupation, which is quite
+expected due to the fact that we have to take into account the digest
+caches loaded in memory, while with the standard appraisal they don't
+exist.
+
+
+Parallel
+~~~~~~~~
+
+This test was performed reading files in parallel, not waiting for the
+current read to terminate.
+
+::
+
+                              +-------------+-------------+-----------+
+                              |    files    | time (sec.) | slab (KB) |
+ +----------------------------+-------------+-------------+-----------+
+ | appraise (ECDSA sig)       |    12312    |    27.68    |   80596   |
+ +----------------------------+-------------+-------------+-----------+
+ | appraise (cache)           | 12313 + 303 |    14.96    |   80778   |
+ +----------------------------+-------------+-------------+-----------+
+ | appraise (cache, prefetch) | 12313 + 303 |    14.78    |   83354   |
+ +----------------------------+-------------+-------------+-----------+
+
+The difference is less marked when performing the read in parallel. Also,
+more memory seems to be occupied in the prefetch case.
+
+
+How to Test
+===========
+
+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 d7f700da009e..67b1fb3ab0ac 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6197,6 +6197,7 @@ DIGEST_CACHE LSM
 M:	Roberto Sassu <roberto.sassu@huawei.com>
 L:	linux-security-module@vger.kernel.org
 S:	Maintained
+F:	Documentation/security/digest_cache.rst
 F:	security/digest_cache/
 F:	tools/testing/selftests/digest_cache/
 
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 00/14] security: digest_cache LSM
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (13 preceding siblings ...)
  2024-04-15 14:24 ` [PATCH v4 14/14] docs: Add documentation of the " Roberto Sassu
@ 2024-04-15 19:18 ` Jarkko Sakkinen
  2024-04-16  6:56   ` Roberto Sassu
  2024-04-16  4:49 ` Bagas Sanjaya
  15 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-15 19:18 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> 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 the large user
> base, due to requiring higher than desired constraints on performance,
> flexibility and configurability, that only security conscious people are
> willing to accept.
>
> This is where the new digest_cache LSM comes into play, it offers
> additional support for new and existing integrity solutions, to make
> them faster and easier to deploy.

Sorry for nitpicking but what are the existing integrity solutions, 
and how does it help with this struggle? I.e. what is the gist here?

BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 01/14] lib: Add TLV parser
  2024-04-15 14:24 ` [PATCH v4 01/14] lib: Add TLV parser Roberto Sassu
@ 2024-04-15 19:19   ` Jarkko Sakkinen
  2024-04-15 21:07     ` Randy Dunlap
  0 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-15 19:19 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> From: Roberto Sassu <roberto.sassu@huawei.com>
>
> Add a parser of a generic TLV format:

What is TLV?

BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 02/14] security: Introduce the digest_cache LSM
  2024-04-15 14:24 ` [PATCH v4 02/14] security: Introduce the digest_cache LSM Roberto Sassu
@ 2024-04-15 19:31   ` Jarkko Sakkinen
  2024-04-16  7:09     ` Roberto Sassu
  0 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-15 19:31 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> From: Roberto Sassu <roberto.sassu@huawei.com>
>
> Introduce the digest_cache LSM, to collect digests from various sources
> (called digest lists), and to store them in kernel memory, in a set of hash
> tables forming a digest cache. Extracted digests can be used as reference
> values for integrity verification of file data or metadata.
>
> A digest cache has three types of references: in the inode security blob of
> the digest list the digest cache was created from (dig_owner field); in the
> security blob of the inodes for which the digest cache is requested
> (dig_user field); a reference returned by digest_cache_get().
>
> References are released with digest_cache_put(), in the first two cases
> when inodes are evicted from memory, in the last case when that function is
> explicitly called. Obtaining a digest cache reference means that the digest
> cache remains valid and cannot be freed until releasing it and until the
> total number of references (stored in the digest cache) becomes zero.
>
> When digest_cache_get() is called on an inode to compare its digest with
> a reference value, the digest_cache LSM knows which digest cache to get
> from the new security.digest_list xattr added to that inode, which contains
> the file name of the desired digest list digests will be extracted from.
>
> All digest lists are expected to be in the same directory, defined in the
> kernel config, and modifiable (with a later patch) at run-time through
> securityfs. When the digest_cache LSM reads the security.digest_list xattr,
> it uses its value as last path component, appended to the default path
> (unless the default path is a file). If an inode does not have that xattr,
> the default path is considered as the final destination.
>
> The default path can be either a file or a directory. If it is a file, the
> digest_cache LSM always uses the same digest cache from that file to verify
> all inodes (the xattr, if present, is ignored). If it is a directory, and
> the inode to verify does not have the xattr, a subsequent patch will make
> it possible to iterate and lookup on the digest caches created from each
> directory entry.
>
> Digest caches are created on demand, only when digest_cache_get() is
> called. The first time a digest cache is requested, the digest_cache LSM
> creates it and sets its reference in the dig_owner and dig_user fields of
> the respective inode security blobs. On the next requests, the previously
> set reference is returned, after incrementing the reference count.
>
> Since there might be multiple digest_cache_get() calls for the same inode,
> or for different inodes pointing to the same digest list, dig_owner_mutex
> and dig_user_mutex have been introduced to protect the check and assignment
> of the digest cache reference in the inode security blob.
>
> Contenders that didn't get the lock also have to wait until the digest
> cache is fully instantiated (when the bit INIT_IN_PROGRESS is cleared).
> Dig_owner_mutex cannot be used for waiting on the instantiation to avoid
> lock inversion with the inode lock for directories.
>
> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> ---
>  MAINTAINERS                                   |   6 +
>  include/linux/digest_cache.h                  |  32 ++
>  include/uapi/linux/lsm.h                      |   1 +
>  include/uapi/linux/xattr.h                    |   3 +
>  security/Kconfig                              |  11 +-
>  security/Makefile                             |   1 +
>  security/digest_cache/Kconfig                 |  16 +
>  security/digest_cache/Makefile                |   7 +
>  security/digest_cache/internal.h              |  86 ++++
>  security/digest_cache/main.c                  | 404 ++++++++++++++++++
>  security/security.c                           |   3 +-
>  .../selftests/lsm/lsm_list_modules_test.c     |   3 +
>  12 files changed, 567 insertions(+), 6 deletions(-)
>  create mode 100644 include/linux/digest_cache.h
>  create mode 100644 security/digest_cache/Kconfig
>  create mode 100644 security/digest_cache/Makefile
>  create mode 100644 security/digest_cache/internal.h
>  create mode 100644 security/digest_cache/main.c
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index b1ca23ab8732..72801a88449c 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -6193,6 +6193,12 @@ L:	linux-gpio@vger.kernel.org
>  S:	Maintained
>  F:	drivers/gpio/gpio-gpio-mm.c
>  
> +DIGEST_CACHE LSM
> +M:	Roberto Sassu <roberto.sassu@huawei.com>
> +L:	linux-security-module@vger.kernel.org
> +S:	Maintained
> +F:	security/digest_cache/
> +
>  DIGITEQ AUTOMOTIVE MGB4 V4L2 DRIVER
>  M:	Martin Tuma <martin.tuma@digiteqautomotive.com>
>  L:	linux-media@vger.kernel.org

Nit: afaik, MAINTAINER updates should be split.

> diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
> new file mode 100644
> index 000000000000..e79f94a60b0f
> --- /dev/null
> +++ b/include/linux/digest_cache.h
> @@ -0,0 +1,32 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
> + *
> + * Author: Roberto Sassu <roberto.sassu@huawei.com>

Nit: Git has an author field in commit granularity so adding author
fields to files is sort of old world.

> + *
> + * Public API of the digest_cache LSM.
> + */
> +
> +#ifndef _LINUX_DIGEST_CACHE_H
> +#define _LINUX_DIGEST_CACHE_H
> +
> +#include <linux/fs.h>
> +
> +struct digest_cache;

Is this declaration necessary?

I don't think you need forward declaration here as this does compile:

#include <stdio.h>

struct digest_cache *digest_cache_get(void)
{
        return NULL;
}

int main(void)
{
        return (long)digest_cache_get();
}


> +
> +#ifdef CONFIG_SECURITY_DIGEST_CACHE
> +struct digest_cache *digest_cache_get(struct dentry *dentry);
> +void digest_cache_put(struct digest_cache *digest_cache);
> +
> +#else
> +static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
> +{
> +	return NULL;
> +}
> +
> +static inline void digest_cache_put(struct digest_cache *digest_cache)
> +{
> +}
> +
> +#endif /* CONFIG_SECURITY_DIGEST_CACHE */
> +#endif /* _LINUX_DIGEST_CACHE_H */
> diff --git a/include/uapi/linux/lsm.h b/include/uapi/linux/lsm.h
> index 33d8c9f4aa6b..832b3aea5c26 100644
> --- a/include/uapi/linux/lsm.h
> +++ b/include/uapi/linux/lsm.h
> @@ -64,6 +64,7 @@ struct lsm_ctx {
>  #define LSM_ID_LANDLOCK		110
>  #define LSM_ID_IMA		111
>  #define LSM_ID_EVM		112
> +#define LSM_ID_DIGEST_CACHE	113
>  
>  /*
>   * LSM_ATTR_XXX definitions identify different LSM attributes
> diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h
> index 9463db2dfa9d..8a58cf4bce65 100644
> --- a/include/uapi/linux/xattr.h
> +++ b/include/uapi/linux/xattr.h
> @@ -54,6 +54,9 @@
>  #define XATTR_IMA_SUFFIX "ima"
>  #define XATTR_NAME_IMA XATTR_SECURITY_PREFIX XATTR_IMA_SUFFIX
>  
> +#define XATTR_DIGEST_LIST_SUFFIX "digest_list"
> +#define XATTR_NAME_DIGEST_LIST XATTR_SECURITY_PREFIX XATTR_DIGEST_LIST_SUFFIX
> +
>  #define XATTR_SELINUX_SUFFIX "selinux"
>  #define XATTR_NAME_SELINUX XATTR_SECURITY_PREFIX XATTR_SELINUX_SUFFIX
>  
> diff --git a/security/Kconfig b/security/Kconfig
> index 52c9af08ad35..99f99cbd94cc 100644
> --- a/security/Kconfig
> +++ b/security/Kconfig
> @@ -194,6 +194,7 @@ source "security/yama/Kconfig"
>  source "security/safesetid/Kconfig"
>  source "security/lockdown/Kconfig"
>  source "security/landlock/Kconfig"
> +source "security/digest_cache/Kconfig"
>  
>  source "security/integrity/Kconfig"
>  
> @@ -233,11 +234,11 @@ endchoice
>  
>  config LSM
>  	string "Ordered list of enabled LSMs"
> -	default "landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
> -	default "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
> -	default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
> -	default "landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
> -	default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
>  	help
>  	  A comma-separated list of LSMs, in initialization order.
>  	  Any LSMs left off this list, except for those with order
> diff --git a/security/Makefile b/security/Makefile
> index 59f238490665..e9b43e7b715a 100644
> --- a/security/Makefile
> +++ b/security/Makefile
> @@ -25,6 +25,7 @@ obj-$(CONFIG_SECURITY_LOCKDOWN_LSM)	+= lockdown/
>  obj-$(CONFIG_CGROUPS)			+= device_cgroup.o
>  obj-$(CONFIG_BPF_LSM)			+= bpf/
>  obj-$(CONFIG_SECURITY_LANDLOCK)		+= landlock/
> +obj-$(CONFIG_SECURITY_DIGEST_CACHE)	+= digest_cache/
>  
>  # Object integrity file lists
>  obj-$(CONFIG_INTEGRITY)			+= integrity/
> diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
> new file mode 100644
> index 000000000000..e53fbf0779d6
> --- /dev/null
> +++ b/security/digest_cache/Kconfig
> @@ -0,0 +1,16 @@
> +# SPDX-License-Identifier: GPL-2.0
> +config SECURITY_DIGEST_CACHE
> +	bool "Digest_cache LSM"
> +	default n
> +	help
> +	  This option enables an LSM maintaining a cache of digests
> +	  (e.g. of file data or metadata).
> +
> +	  This LSM can support other kernel components in making access
> +	  control decisions.
> +
> +config DIGEST_LIST_DEFAULT_PATH
> +	string
> +	default "/etc/digest_lists"
> +	help
> +	  Default directory where digest_cache LSM expects to find digest lists.
> diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
> new file mode 100644
> index 000000000000..48848c41253e
> --- /dev/null
> +++ b/security/digest_cache/Makefile
> @@ -0,0 +1,7 @@
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# Makefile for building the digest_cache LSM.
> +
> +obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
> +
> +digest_cache-y := main.o
> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> new file mode 100644
> index 000000000000..5f04844af3a5
> --- /dev/null
> +++ b/security/digest_cache/internal.h
> @@ -0,0 +1,86 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
> + *
> + * Author: Roberto Sassu <roberto.sassu@huawei.com>

ditto

> + *
> + * Internal header of the digest_cache LSM.
> + */
> +
> +#ifndef _DIGEST_CACHE_INTERNAL_H
> +#define _DIGEST_CACHE_INTERNAL_H
> +
> +#include <linux/lsm_hooks.h>
> +#include <linux/digest_cache.h>
> +
> +/* Digest cache bits in flags. */
> +#define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
> +
> +/**
> + * struct digest_cache - Digest cache
> + * @ref_count: Number of references to the digest cache
> + * @path_str: Path of the digest list the digest cache was created from
> + * @flags: Control flags
> + *
> + * This structure represents a cache of digests extracted from a digest list.
> + */
> +struct digest_cache {
> +	atomic_t ref_count;
> +	char *path_str;
> +	unsigned long flags;
> +};
> +
> +/**
> + * struct digest_cache_security - Digest cache pointers in inode security blob
> + * @dig_owner: Digest cache created from this inode
> + * @dig_owner_mutex: Protects @dig_owner
> + * @dig_user: Digest cache requested for this inode
> + * @dig_user_mutex: Protects @dig_user
> + *
> + * This structure contains references to digest caches, protected by their
> + * respective mutex.
> + */
> +struct digest_cache_security {
> +	struct digest_cache *dig_owner;
> +	struct mutex dig_owner_mutex;
> +	struct digest_cache *dig_user;
> +	struct mutex dig_user_mutex;
> +};
> +
> +extern struct lsm_blob_sizes digest_cache_blob_sizes;
> +extern char *default_path_str;
> +
> +static inline struct digest_cache_security *
> +digest_cache_get_security(const struct inode *inode)
> +{
> +	if (unlikely(!inode->i_security))
> +		return NULL;
> +
> +	return inode->i_security + digest_cache_blob_sizes.lbs_inode;
> +}
> +
> +static inline struct digest_cache *
> +digest_cache_ref(struct digest_cache *digest_cache)
> +{
> +	atomic_inc(&digest_cache->ref_count);
> +	pr_debug("Ref (+) digest cache %s (ref count: %d)\n",
> +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
> +	return digest_cache;
> +}
> +
> +static inline struct digest_cache *
> +digest_cache_unref(struct digest_cache *digest_cache)
> +{
> +	bool ref_is_zero = atomic_dec_and_test(&digest_cache->ref_count);
> +
> +	pr_debug("Ref (-) digest cache %s (ref count: %d)\n",
> +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
> +	return (ref_is_zero) ? digest_cache : NULL;
> +}
> +
> +/* main.c */
> +struct digest_cache *digest_cache_create(struct dentry *dentry,
> +					 struct path *digest_list_path,
> +					 char *path_str, char *filename);
> +
> +#endif /* _DIGEST_CACHE_INTERNAL_H */
> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
> new file mode 100644
> index 000000000000..14dba8915e99
> --- /dev/null
> +++ b/security/digest_cache/main.c
> @@ -0,0 +1,404 @@
> +// 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 digest_cache LSM.
> + */
> +
> +#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 = 1;
> +static struct kmem_cache *digest_cache_cache __read_mostly;
> +
> +char *default_path_str = CONFIG_DIGEST_LIST_DEFAULT_PATH;
> +
> +/**
> + * digest_cache_alloc_init - Allocate and initialize a new digest cache
> + * @path_str: Path string of the digest list
> + * @filename: Digest list file name (can be an empty string)
> + *
> + * This function allocates and initializes a new digest cache.
> + *
> + * Return: A digest_cache structure on success, NULL on error.
> + */
> +static struct digest_cache *digest_cache_alloc_init(char *path_str,
> +						    char *filename)
> +{
> +	struct digest_cache *digest_cache;
> +
> +	digest_cache = kmem_cache_alloc(digest_cache_cache, GFP_KERNEL);
> +	if (!digest_cache)
> +		return digest_cache;
> +
> +	digest_cache->path_str = kasprintf(GFP_KERNEL, "%s%s%s", path_str,
> +					   filename[0] ? "/" : "", filename);
> +	if (!digest_cache->path_str) {
> +		kmem_cache_free(digest_cache_cache, digest_cache);
> +		return NULL;
> +	}
> +
> +	atomic_set(&digest_cache->ref_count, 1);
> +	digest_cache->flags = 0UL;
> +
> +	pr_debug("New digest cache %s (ref count: %d)\n",
> +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));

Nit: kretprobe can be used to grab the same information easil and
do e.g. statistics and stuff like that. Traces for return values
are not very useful.


> +
> +	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);

ditto for the above trace

> +	kfree(digest_cache->path_str);
> +	kmem_cache_free(digest_cache_cache, digest_cache);
> +}
> +
> +/**
> + * digest_cache_create - Create a digest cache
> + * @dentry: Dentry of the inode for which the digest cache will be used
> + * @digest_list_path: Path structure of the digest list
> + * @path_str: Path string of the digest list
> + * @filename: Digest list file name (can be an empty string)
> + *
> + * This function first locates, from the passed path, the digest list inode
> + * from which the digest cache will be created or retrieved (if it already
> + * exists).
> + *
> + * If dig_owner is NULL in the inode security blob, this function creates a
> + * new digest cache with reference count set to 1 (reference returned), sets
> + * it to dig_owner and consequently increments again the digest cache reference
> + * count.
> + *
> + * Otherwise, it simply increments the reference count of the existing
> + * dig_owner, since that reference is returned to the caller.
> + *
> + * Incrementing the reference count twice before calling path_put() ensures
> + * that the digest cache returned is valid even if the inode is evicted from
> + * memory (which decreases the reference count).
> + *
> + * Releasing the dig_owner_mutex lock does not mean that the digest cache is
> + * ready for use. digest_cache_create() callers that found a partially
> + * instantiated digest cache have to wait until the INIT_IN_PROGRESS bit is
> + * cleared by the caller that is actually creating that digest cache.
> + *
> + * Return: A new digest cache on success, NULL on error.
> + */
> +struct digest_cache *digest_cache_create(struct dentry *dentry,
> +					 struct path *digest_list_path,
> +					 char *path_str, char *filename)
> +{
> +	struct path file_path;
> +	struct digest_cache *digest_cache = NULL;
> +	struct digest_cache_security *dig_sec;
> +	struct inode *inode = d_backing_inode(digest_list_path->dentry);
> +	bool dig_owner_exists = false;
> +	int ret;
> +
> +	if (S_ISDIR(d_backing_inode(digest_list_path->dentry)->i_mode) &&
> +	    filename[0]) {
> +		ret = vfs_path_lookup(digest_list_path->dentry,
> +				      digest_list_path->mnt, filename, 0,
> +				      &file_path);
> +		if (ret < 0) {
> +			pr_debug("Cannot find digest list %s/%s\n", path_str,
> +				 filename);
> +			return NULL;
> +		}
> +
> +		digest_list_path = &file_path;
> +		inode = d_backing_inode(file_path.dentry);
> +
> +		/*
> +		 * Cannot request a digest cache for the same inode the
> +		 * digest cache is populated from.
> +		 */
> +		if (d_backing_inode(dentry) == inode) {
> +			pr_debug("Cannot request a digest cache for %s and use it as digest list\n",
> +				 dentry->d_name.name);
> +			goto out;
> +		}
> +
> +		/* 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",
> +				 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);
> +		dig_owner_exists = true;
> +		mutex_unlock(&dig_sec->dig_owner_mutex);
> +		goto exists;
> +	}
> +
> +	/* Ref. count is already 1 for this reference. */
> +	digest_cache = digest_cache_alloc_init(path_str, filename);
> +	if (!digest_cache) {
> +		mutex_unlock(&dig_sec->dig_owner_mutex);
> +		goto out;
> +	}
> +
> +	/* Increment ref. count for reference set to dig_owner. */
> +	dig_sec->dig_owner = digest_cache_ref(digest_cache);
> +
> +	/* Make the other lock contenders wait until creation complete. */
> +	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
> +	mutex_unlock(&dig_sec->dig_owner_mutex);
> +
> +	/* Creation complete, notify the other lock contenders. */
> +	clear_and_wake_up_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
> +exists:
> +	if (dig_owner_exists)
> +		/* Wait until creation complete. */
> +		wait_on_bit(&dig_sec->dig_owner->flags, INIT_IN_PROGRESS,
> +			    TASK_UNINTERRUPTIBLE);
> +out:
> +	if (digest_list_path == &file_path)
> +		path_put(&file_path);
> +
> +	return digest_cache;
> +}
> +
> +/**
> + * digest_cache_new - Retrieve digest list file name and request digest cache
> + * @dentry: Dentry of the inode for which the digest cache will be used
> + *
> + * This function locates the default path. If it is a file, it directly creates
> + * a digest cache from it. Otherwise, it reads the digest list file name from
> + * the security.digest_list xattr and requests the creation of a digest cache
> + * with that file name. If security.digest_list is not found, this function
> + * requests the creation of a digest cache on the parent directory.
> + *
> + * Return: A new digest cache on success, NULL on error.
> + */
> +static struct digest_cache *digest_cache_new(struct dentry *dentry)
> +{
> +	char filename[NAME_MAX + 1] = { 0 };
> +	struct digest_cache *digest_cache = NULL;
> +	struct path default_path;
> +	int ret;
> +
> +	ret = kern_path(default_path_str, 0, &default_path);
> +	if (ret < 0) {
> +		pr_debug("Cannot find path %s\n", default_path_str);
> +		return NULL;
> +	}
> +
> +	/* The default path is a file, no need to get xattr. */
> +	if (S_ISREG(d_backing_inode(default_path.dentry)->i_mode)) {
> +		pr_debug("Default path %s is a file, not reading %s xattr\n",
> +			 default_path_str, XATTR_NAME_DIGEST_LIST);
> +		goto create;
> +	} else if (!S_ISDIR(d_backing_inode(default_path.dentry)->i_mode)) {
> +		pr_debug("Default path %s must be either a file or a directory\n",
> +			 default_path_str);
> +		goto out;
> +	}
> +
> +	ret = vfs_getxattr(&nop_mnt_idmap, dentry, XATTR_NAME_DIGEST_LIST,
> +			   filename, sizeof(filename) - 1);
> +	if (ret <= 0) {
> +		pr_debug("Digest list path not found for file %s, using %s\n",
> +			 dentry->d_name.name, default_path_str);
> +		goto create;
> +	}
> +
> +	if (strchr(filename, '/')) {
> +		pr_debug("%s xattr should contain only a file name, got: %s\n",
> +			 XATTR_NAME_DIGEST_LIST, filename);
> +		goto out;
> +	}
> +
> +	pr_debug("Found %s xattr in %s, default path: %s, digest list: %s\n",
> +		 XATTR_NAME_DIGEST_LIST, dentry->d_name.name, default_path_str,
> +		 filename);
> +create:
> +	digest_cache = digest_cache_create(dentry, &default_path,
> +					   default_path_str, filename);
> +out:
> +	path_put(&default_path);
> +	return digest_cache;
> +}
> +
> +/**
> + * digest_cache_get - Get a digest cache for a given inode
> + * @dentry: Dentry of the inode for which the digest cache will be used
> + *
> + * This function tries to find a digest cache from the inode security blob of
> + * the passed dentry (dig_user field). If a digest cache was not found, it calls
> + * digest_cache_new() to create a new one. In both cases, it increments the
> + * digest cache reference count before returning the reference to the caller.
> + *
> + * The caller is responsible to call digest_cache_put() to release the digest
> + * cache reference returned.
> + *
> + * Lock dig_user_mutex to protect against concurrent requests to obtain a digest
> + * cache for the same inode, and to make other contenders wait until the first
> + * requester finishes the process.
> + *
> + * Return: A digest cache on success, NULL otherwise.
> + */
> +struct digest_cache *digest_cache_get(struct dentry *dentry)
> +{
> +	struct digest_cache_security *dig_sec;
> +	struct digest_cache *digest_cache = NULL;
> +	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);
> +
> +struct lsm_blob_sizes digest_cache_blob_sizes __ro_after_init = {
> +	.lbs_inode = sizeof(struct digest_cache_security),
> +};
> +
> +/**
> + * digest_cache_inode_alloc_security - Initialize inode security blob
> + * @inode: Inode for which the security blob is initialized
> + *
> + * This function initializes the digest_cache_security structure, directly
> + * stored in the inode security blob.
> + *
> + * Return: Zero.
> + */
> +static int digest_cache_inode_alloc_security(struct inode *inode)
> +{
> +	struct digest_cache_security *dig_sec;
> +
> +	/* The inode security blob is always allocated here. */
> +	dig_sec = digest_cache_get_security(inode);
> +	mutex_init(&dig_sec->dig_owner_mutex);
> +	mutex_init(&dig_sec->dig_user_mutex);
> +	return 0;
> +}
> +
> +/**
> + * digest_cache_inode_free_security - Release the digest cache references
> + * @inode: Inode for which the digest cache references are released
> + *
> + * Since the inode is being evicted, this function releases the non-needed
> + * references to the digest caches stored in the digest_cache_security
> + * structure.
> + */
> +static void digest_cache_inode_free_security(struct inode *inode)
> +{
> +	struct digest_cache_security *dig_sec;
> +
> +	dig_sec = digest_cache_get_security(inode);
> +	if (!dig_sec)
> +		return;
> +
> +	mutex_destroy(&dig_sec->dig_owner_mutex);
> +	mutex_destroy(&dig_sec->dig_user_mutex);
> +	if (dig_sec->dig_owner)
> +		digest_cache_put(dig_sec->dig_owner);
> +	if (dig_sec->dig_user)
> +		digest_cache_put(dig_sec->dig_user);
> +}
> +
> +static struct security_hook_list digest_cache_hooks[] __ro_after_init = {
> +	LSM_HOOK_INIT(inode_alloc_security, digest_cache_inode_alloc_security),
> +	LSM_HOOK_INIT(inode_free_security, digest_cache_inode_free_security),
> +};
> +
> +/**
> + * digest_cache_init_once - Initialize the digest cache structure
> + * @foo: Digest cache structure to initialize
> + *
> + * This function fills the digest cache structure with zeros.
> + */
> +static void digest_cache_init_once(void *foo)
> +{
> +	struct digest_cache *digest_cache = (struct digest_cache *)foo;
> +
> +	memset(digest_cache, 0, sizeof(*digest_cache));
> +}
> +
> +static const struct lsm_id digest_cache_lsmid = {
> +	.name = "digest_cache",
> +	.id = LSM_ID_DIGEST_CACHE,
> +};
> +
> +/**
> + * digest_cache_init - Initialize the digest_cache LSM
> + *
> + * Initialize the digest_cache LSM, by instantiating a cache for the
> + * digest_cache structure and by registering the digest_cache LSM hooks.
> + */
> +static int __init digest_cache_init(void)
> +{
> +	digest_cache_cache = kmem_cache_create("digest_cache_cache",
> +					       sizeof(struct digest_cache),
> +					       0, SLAB_PANIC,
> +					       digest_cache_init_once);
> +
> +	security_add_hooks(digest_cache_hooks, ARRAY_SIZE(digest_cache_hooks),
> +			   &digest_cache_lsmid);
> +	return 0;
> +}
> +
> +DEFINE_LSM(digest_cache) = {
> +	.name = "digest_cache",
> +	.enabled = &digest_cache_enabled,
> +	.init = digest_cache_init,
> +	.blobs = &digest_cache_blob_sizes,
> +};
> diff --git a/security/security.c b/security/security.c
> index cbdc9bebe802..cb084ed58617 100644
> --- a/security/security.c
> +++ b/security/security.c
> @@ -50,7 +50,8 @@
>  	(IS_ENABLED(CONFIG_BPF_LSM) ? 1 : 0) + \
>  	(IS_ENABLED(CONFIG_SECURITY_LANDLOCK) ? 1 : 0) + \
>  	(IS_ENABLED(CONFIG_IMA) ? 1 : 0) + \
> -	(IS_ENABLED(CONFIG_EVM) ? 1 : 0))
> +	(IS_ENABLED(CONFIG_EVM) ? 1 : 0) + \
> +	(IS_ENABLED(CONFIG_SECURITY_DIGEST_CACHE) ? 1 : 0))
>  
>  /*
>   * These are descriptions of the reasons that can be passed to the
> diff --git a/tools/testing/selftests/lsm/lsm_list_modules_test.c b/tools/testing/selftests/lsm/lsm_list_modules_test.c
> index 4d5d4cee2586..d00831edc582 100644
> --- a/tools/testing/selftests/lsm/lsm_list_modules_test.c
> +++ b/tools/testing/selftests/lsm/lsm_list_modules_test.c
> @@ -128,6 +128,9 @@ TEST(correct_lsm_list_modules)
>  		case LSM_ID_EVM:
>  			name = "evm";
>  			break;
> +		case LSM_ID_DIGEST_CACHE:
> +			name = "digest_cache";
> +			break;
>  		default:
>  			name = "INVALID";
>  			break;

BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 03/14] digest_cache: Add securityfs interface
  2024-04-15 14:24 ` [PATCH v4 03/14] digest_cache: Add securityfs interface Roberto Sassu
@ 2024-04-15 19:32   ` Jarkko Sakkinen
  2024-04-16 10:15     ` Roberto Sassu
  0 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-15 19:32 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> From: Roberto Sassu <roberto.sassu@huawei.com>
>
> Add the digest_cache_path file in securityfs, 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/digest_cache/Kconfig    |  4 ++
>  security/digest_cache/Makefile   |  2 +-
>  security/digest_cache/internal.h |  1 +
>  security/digest_cache/main.c     | 10 +++-
>  security/digest_cache/secfs.c    | 87 ++++++++++++++++++++++++++++++++
>  5 files changed, 102 insertions(+), 2 deletions(-)
>  create mode 100644 security/digest_cache/secfs.c
>
> diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
> index e53fbf0779d6..dfabe5d6e3ca 100644
> --- a/security/digest_cache/Kconfig
> +++ b/security/digest_cache/Kconfig
> @@ -14,3 +14,7 @@ config DIGEST_LIST_DEFAULT_PATH
>  	default "/etc/digest_lists"
>  	help
>  	  Default directory where digest_cache LSM 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/digest_cache/Makefile b/security/digest_cache/Makefile
> index 48848c41253e..1330655e33b1 100644
> --- a/security/digest_cache/Makefile
> +++ b/security/digest_cache/Makefile
> @@ -4,4 +4,4 @@
>  
>  obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
>  
> -digest_cache-y := main.o
> +digest_cache-y := main.o secfs.o
> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> index 5f04844af3a5..bbf5eefe5c82 100644
> --- a/security/digest_cache/internal.h
> +++ b/security/digest_cache/internal.h
> @@ -49,6 +49,7 @@ struct digest_cache_security {
>  
>  extern struct lsm_blob_sizes digest_cache_blob_sizes;
>  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)
> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
> index 14dba8915e99..661c8d106791 100644
> --- a/security/digest_cache/main.c
> +++ b/security/digest_cache/main.c
> @@ -18,6 +18,9 @@ static struct kmem_cache *digest_cache_cache __read_mostly;
>  
>  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
> @@ -274,9 +277,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. */
> @@ -386,6 +392,8 @@ static const struct lsm_id digest_cache_lsmid = {
>   */
>  static int __init digest_cache_init(void)
>  {
> +	init_rwsem(&default_path_sem);
> +
>  	digest_cache_cache = kmem_cache_create("digest_cache_cache",
>  					       sizeof(struct digest_cache),
>  					       0, SLAB_PANIC,
> diff --git a/security/digest_cache/secfs.c b/security/digest_cache/secfs.c
> new file mode 100644
> index 000000000000..d3a37bf3588e
> --- /dev/null
> +++ b/security/digest_cache/secfs.c
> @@ -0,0 +1,87 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
> + *
> + * Author: Roberto Sassu <roberto.sassu@huawei.com>
> + *
> + * Implement the securityfs interface of the digest_cache LSM.
> + */
> +
> +#define pr_fmt(fmt) "DIGEST CACHE: "fmt
> +#include <linux/security.h>
> +
> +#include "internal.h"
> +
> +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,
> +};
> +
> +static int __init digest_cache_path_init(void)
> +{
> +	default_path_dentry = securityfs_create_file("digest_cache_path", 0660,
> +						     NULL, NULL,
> +						     &default_path_ops);
> +	if (IS_ERR(default_path_dentry))
> +		return -EFAULT;

Nit: when overwriting error value with another error value it would be
best to document it with an inline comment. Otherwise, it is fine.

> +
> +	return 0;
> +}
> +
> +late_initcall(digest_cache_path_init);


BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 04/14] digest_cache: Add hash tables and operations
  2024-04-15 14:24 ` [PATCH v4 04/14] digest_cache: Add hash tables and operations Roberto Sassu
@ 2024-04-15 19:36   ` Jarkko Sakkinen
  2024-04-16 10:28     ` Roberto Sassu
  0 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-15 19:36 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> 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 digest_cache LSM free the hash
> tables at the time a digest cache is freed.
>
> Add digest_cache_htable_lookup() to search a digest in the hash table of a
> digest cache for a given algorithm.
>
> Add digest_cache_lookup() to the public API, to let users of the
> digest_cache LSM search a digest in a digest cache and, in a subsequent
> patch, to search it in the digest caches for each directory entry.
>
> Return the digest cache containing the digest, as a different type,
> digest_cache_found_t to avoid it being accidentally put. Also, introduce
> digest_cache_from_found_t() to explicitly convert it back to a digest cache
> for further use (e.g. retrieving verification data, introduced later).
>
> Finally, add digest_cache_hash_key() to compute the hash table key from the
> first two bytes of the digest (modulo the number of slots).
>
> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> ---
>  include/linux/digest_cache.h     |  34 +++++
>  security/digest_cache/Kconfig    |  11 ++
>  security/digest_cache/Makefile   |   2 +-
>  security/digest_cache/htable.c   | 250 +++++++++++++++++++++++++++++++
>  security/digest_cache/internal.h |  43 ++++++
>  security/digest_cache/main.c     |   3 +
>  6 files changed, 342 insertions(+), 1 deletion(-)
>  create mode 100644 security/digest_cache/htable.c
>
> diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
> index e79f94a60b0f..4872700ac205 100644
> --- a/include/linux/digest_cache.h
> +++ b/include/linux/digest_cache.h
> @@ -11,12 +11,39 @@
>  #define _LINUX_DIGEST_CACHE_H
>  
>  #include <linux/fs.h>
> +#include <crypto/hash_info.h>
>  
>  struct digest_cache;
>  
> +/**
> + * typedef digest_cache_found_t - Digest cache reference as numeric value
> + *
> + * This new type represents a digest cache reference that should not be put.
> + */
> +typedef unsigned long digest_cache_found_t;
> +
> +/**
> + * digest_cache_from_found_t - Convert digest_cache_found_t to digest cache ptr
> + * @found: digest_cache_found_t value
> + *
> + * Convert the digest_cache_found_t returned by digest_cache_lookup() to a
> + * digest cache pointer, so that it can be passed to the other functions of the
> + * API.
> + *
> + * Return: Digest cache pointer.
> + */
> +static inline struct digest_cache *
> +digest_cache_from_found_t(digest_cache_found_t found)
> +{
> +	return (struct digest_cache *)found;
> +}
> +
>  #ifdef CONFIG_SECURITY_DIGEST_CACHE
>  struct digest_cache *digest_cache_get(struct dentry *dentry);
>  void digest_cache_put(struct digest_cache *digest_cache);
> +digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
> +					 struct digest_cache *digest_cache,
> +					 u8 *digest, enum hash_algo algo);
>  
>  #else
>  static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
> @@ -28,5 +55,12 @@ static inline void digest_cache_put(struct digest_cache *digest_cache)
>  {
>  }
>  
> +static inline digest_cache_found_t
> +digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache,
> +		    u8 *digest, enum hash_algo algo)
> +{
> +	return 0UL;
> +}
> +
>  #endif /* CONFIG_SECURITY_DIGEST_CACHE */
>  #endif /* _LINUX_DIGEST_CACHE_H */
> diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
> index dfabe5d6e3ca..71017954e5c5 100644
> --- a/security/digest_cache/Kconfig
> +++ b/security/digest_cache/Kconfig
> @@ -18,3 +18,14 @@ config DIGEST_LIST_DEFAULT_PATH
>  	  It can be changed at run-time, by writing the new path to the
>  	  securityfs interface. Digest caches created with the old path are
>  	  not affected by the change.
> +
> +config DIGEST_CACHE_HTABLE_DEPTH
> +	int
> +	default 30
> +	help
> +	  Desired average depth of the collision list in the digest cache
> +	  hash tables.
> +
> +	  A smaller number will increase the amount of hash table slots, and
> +	  make the search faster. A bigger number will decrease the number of
> +	  hash table slots, but make the search slower.
> diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
> index 1330655e33b1..7e00c53d8f55 100644
> --- a/security/digest_cache/Makefile
> +++ b/security/digest_cache/Makefile
> @@ -4,4 +4,4 @@
>  
>  obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
>  
> -digest_cache-y := main.o secfs.o
> +digest_cache-y := main.o secfs.o htable.o
> diff --git a/security/digest_cache/htable.c b/security/digest_cache/htable.c
> new file mode 100644
> index 000000000000..d2d5d8f5e5b1
> --- /dev/null
> +++ b/security/digest_cache/htable.c
> @@ -0,0 +1,250 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
> + *
> + * Author: Roberto Sassu <roberto.sassu@huawei.com>
> + *
> + * Implement hash table operations for the digest cache.
> + */
> +
> +#define pr_fmt(fmt) "DIGEST CACHE: "fmt

For easier grepping from kernel tree i'd suggest to name this accordingly i.e.
just "digest_cache".

> +#include "internal.h"
> +
> +/**
> + * digest_cache_hash_key - Compute hash key
> + * @digest: Digest cache
> + * @num_slots: Number of slots in the hash table
> + *
> + * This function computes a hash key based on the first two bytes of the
> + * digest and the number of slots of the hash table.
> + *
> + * Return: Hash key.
> + */
> +static inline unsigned int digest_cache_hash_key(u8 *digest,
> +						 unsigned int num_slots)
> +{
> +	/* Same as ima_hash_key() but parametrized. */
> +	return (digest[0] | digest[1] << 8) % num_slots;
> +}
> +
> +/**
> + * lookup_htable - Lookup a hash table
> + * @digest_cache: Digest cache
> + * @algo: Algorithm of the desired hash table
> + *
> + * This function searches the hash table for a given algorithm in the digest
> + * cache.
> + *
> + * Return: A hash table if found, NULL otherwise.
> + */
> +static struct htable *lookup_htable(struct digest_cache *digest_cache,
> +				    enum hash_algo algo)
> +{
> +	struct htable *h;
> +
> +	list_for_each_entry(h, &digest_cache->htables, next)
> +		if (h->algo == algo)
> +			return h;
> +
> +	return NULL;
> +}
> +
> +/**
> + * digest_cache_htable_init - Allocate and initialize the hash table
> + * @digest_cache: Digest cache
> + * @num_digests: Number of digests to add to the digest cache
> + * @algo: Algorithm of the digests
> + *
> + * This function allocates and initializes the hash table for a given algorithm.
> + * The number of slots depends on the number of digests to add to the digest
> + * cache, and the constant CONFIG_DIGEST_CACHE_HTABLE_DEPTH stating the desired
> + * average depth of the collision list.
> + *
> + * Return: Zero on success, a POSIX error code otherwise.
> + */
> +int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
> +			     enum hash_algo algo)
> +{
> +	struct htable *h;
> +	int i;
> +
> +	h = lookup_htable(digest_cache, algo);
> +	if (h)
> +		return 0;
> +
> +	h = kmalloc(sizeof(*h), GFP_KERNEL);
> +	if (!h)
> +		return -ENOMEM;
> +
> +	h->num_slots = DIV_ROUND_UP(num_digests,
> +				    CONFIG_DIGEST_CACHE_HTABLE_DEPTH);
> +	h->slots = kmalloc_array(h->num_slots, sizeof(*h->slots), GFP_KERNEL);
> +	if (!h->slots) {
> +		kfree(h);
> +		return -ENOMEM;
> +	}
> +
> +	for (i = 0; i < h->num_slots; i++)
> +		INIT_HLIST_HEAD(&h->slots[i]);
> +
> +	h->num_digests = 0;
> +	h->algo = algo;
> +
> +	list_add_tail(&h->next, &digest_cache->htables);
> +
> +	pr_debug("Initialized hash table for digest list %s, digests: %llu, slots: %u, algo: %s\n",
> +		 digest_cache->path_str, num_digests, h->num_slots,
> +		 hash_algo_name[algo]);
> +	return 0;
> +}
> +
> +/**
> + * digest_cache_htable_add - Add a new digest to the digest cache
> + * @digest_cache: Digest cache
> + * @digest: Digest to add
> + * @algo: Algorithm of digest
> + *
> + * This function, invoked by a digest list parser, adds a digest extracted
> + * from a digest list to the digest cache.
> + *
> + * Return: Zero on success, a POSIX error code otherwise.
> + */
> +int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest,
> +			    enum hash_algo algo)
> +{
> +	struct htable *h;
> +	struct digest_cache_entry *entry;
> +	unsigned int key;
> +	int digest_len;
> +
> +	h = lookup_htable(digest_cache, algo);
> +	if (!h) {
> +		pr_debug("No hash table for algorithm %s was found in digest cache %s, initialize one\n",
> +			 hash_algo_name[algo], digest_cache->path_str);
> +		return -ENOENT;
> +	}
> +
> +	digest_len = hash_digest_size[algo];
> +
> +	entry = kmalloc(sizeof(*entry) + digest_len, GFP_KERNEL);
> +	if (!entry)
> +		return -ENOMEM;
> +
> +	memcpy(entry->digest, digest, digest_len);
> +
> +	key = digest_cache_hash_key(digest, h->num_slots);
> +	hlist_add_head(&entry->hnext, &h->slots[key]);
> +	h->num_digests++;
> +	pr_debug("Added digest %s:%*phN to digest cache %s, num of %s digests: %llu\n",
> +		 hash_algo_name[algo], digest_len, digest,
> +		 digest_cache->path_str, hash_algo_name[algo], h->num_digests);
> +	return 0;
> +}
> +
> +/**
> + * digest_cache_htable_lookup - Search a digest in the digest cache
> + * @dentry: Dentry of the file whose digest is looked up
> + * @digest_cache: Digest cache
> + * @digest: Digest to search
> + * @algo: Algorithm of the digest to search
> + *
> + * This function searches the passed digest and algorithm in the passed digest
> + * cache.
> + *
> + * Return: Zero if the digest is found, -ENOENT if not.
> + */
> +int digest_cache_htable_lookup(struct dentry *dentry,
> +			       struct digest_cache *digest_cache, u8 *digest,
> +			       enum hash_algo algo)
> +{
> +	struct digest_cache_entry *entry;
> +	struct htable *h;
> +	unsigned int key;
> +	int digest_len;
> +	int search_depth = 0;
> +
> +	h = lookup_htable(digest_cache, algo);
> +	if (!h)
> +		return -ENOENT;
> +
> +	digest_len = hash_digest_size[algo];
> +	key = digest_cache_hash_key(digest, h->num_slots);
> +
> +	hlist_for_each_entry(entry, &h->slots[key], hnext) {
> +		if (!memcmp(entry->digest, digest, digest_len)) {
> +			pr_debug("Cache hit at depth %d for file %s, digest %s:%*phN in digest cache %s\n",
> +				 search_depth, dentry->d_name.name,
> +				 hash_algo_name[algo], digest_len, digest,
> +				 digest_cache->path_str);
> +
> +			return 0;
> +		}
> +
> +		search_depth++;
> +	}
> +
> +	pr_debug("Cache miss for file %s, digest %s:%*phN in digest cache %s\n",
> +		 dentry->d_name.name, hash_algo_name[algo], digest_len, digest,
> +		 digest_cache->path_str);
> +	return -ENOENT;
> +}
> +
> +/**
> + * digest_cache_lookup - Search a digest in the digest cache
> + * @dentry: Dentry of the file whose digest is looked up
> + * @digest_cache: Digest cache
> + * @digest: Digest to search
> + * @algo: Algorithm of the digest to search
> + *
> + * This function calls digest_cache_htable_lookup() to search a digest in the
> + * passed digest cache, obtained with digest_cache_get().
> + *
> + * It returns the digest cache reference as the digest_cache_found_t type, to
> + * avoid that the digest cache is accidentally put. The digest_cache_found_t
> + * type can be converted back to a digest cache pointer, by
> + * calling digest_cache_from_found_t().
> + *
> + * Return: A positive digest_cache_found_t if the digest is found, zero if not.
> + */
> +digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
> +					 struct digest_cache *digest_cache,
> +					 u8 *digest, enum hash_algo algo)
> +{
> +	int ret;
> +
> +	ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo);
> +	return (!ret) ? (digest_cache_found_t)digest_cache : 0UL;
> +}
> +EXPORT_SYMBOL_GPL(digest_cache_lookup);
> +
> +/**
> + * digest_cache_htable_free - Free the hash tables
> + * @digest_cache: Digest cache
> + *
> + * This function removes all digests from all hash tables in the digest cache,
> + * and frees the memory.
> + */
> +void digest_cache_htable_free(struct digest_cache *digest_cache)
> +{
> +	struct htable *h, *h_tmp;
> +	struct digest_cache_entry *p;
> +	struct hlist_node *q;
> +	int i;
> +
> +	list_for_each_entry_safe(h, h_tmp, &digest_cache->htables, next) {
> +		for (i = 0; i < h->num_slots; i++) {
> +			hlist_for_each_entry_safe(p, q, &h->slots[i], hnext) {
> +				hlist_del(&p->hnext);
> +				pr_debug("Removed digest %s:%*phN from digest cache %s\n",
> +					 hash_algo_name[h->algo],
> +					 hash_digest_size[h->algo], p->digest,
> +					 digest_cache->path_str);
> +				kfree(p);
> +			}
> +		}
> +
> +		list_del(&h->next);
> +		kfree(h->slots);
> +		kfree(h);
> +	}
> +}
> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> index bbf5eefe5c82..f6ffeaa25288 100644
> --- a/security/digest_cache/internal.h
> +++ b/security/digest_cache/internal.h
> @@ -16,8 +16,40 @@
>  /* Digest cache bits in flags. */
>  #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
>  
> +/**
> + * struct digest_cache_entry - Entry of a digest cache hash table
> + * @hnext: Pointer to the next element in the collision list
> + * @digest: Stored digest
> + *
> + * This structure represents an entry of a digest cache hash table, storing a
> + * digest.
> + */
> +struct digest_cache_entry {
> +	struct hlist_node hnext;
> +	u8 digest[];
> +} __packed;

Please correct me if I'm wrong but I don't think __packed has any use
here as the definition of hlist_node is:


struct hlist_node {
	struct hlist_node *next, **pprev;
};

It is naturally aligned to begin with.


> +
> +/**
> + * 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
> @@ -25,6 +57,7 @@
>   * This structure represents a cache of digests extracted from a digest list.
>   */
>  struct digest_cache {
> +	struct list_head htables;
>  	atomic_t ref_count;
>  	char *path_str;
>  	unsigned long flags;
> @@ -84,4 +117,14 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>  					 struct path *digest_list_path,
>  					 char *path_str, char *filename);
>  
> +/* htable.c */
> +int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
> +			     enum hash_algo algo);
> +int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest,
> +			    enum hash_algo algo);
> +int digest_cache_htable_lookup(struct dentry *dentry,
> +			       struct digest_cache *digest_cache, u8 *digest,
> +			       enum hash_algo algo);
> +void digest_cache_htable_free(struct digest_cache *digest_cache);
> +
>  #endif /* _DIGEST_CACHE_INTERNAL_H */
> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
> index 661c8d106791..0b201af6432c 100644
> --- a/security/digest_cache/main.c
> +++ b/security/digest_cache/main.c
> @@ -48,6 +48,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
>  
>  	atomic_set(&digest_cache->ref_count, 1);
>  	digest_cache->flags = 0UL;
> +	INIT_LIST_HEAD(&digest_cache->htables);
>  
>  	pr_debug("New digest cache %s (ref count: %d)\n",
>  		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
> @@ -63,6 +64,8 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
>   */
>  static void digest_cache_free(struct digest_cache *digest_cache)
>  {
> +	digest_cache_htable_free(digest_cache);
> +
>  	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
>  	kfree(digest_cache->path_str);
>  	kmem_cache_free(digest_cache_cache, digest_cache);


BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 09/14] digest_cache: Add support for directories
  2024-04-15 14:24 ` [PATCH v4 09/14] digest_cache: Add support for directories Roberto Sassu
@ 2024-04-15 19:39   ` Jarkko Sakkinen
  2024-04-16 10:30     ` Roberto Sassu
  0 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-15 19:39 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> From: Roberto Sassu <roberto.sassu@huawei.com>
>
> In the environments where xattrs are not available (e.g. in the initial ram
> disk), the digest_cache LSM cannot precisely determine which digest list in
> a directory contains the desired reference digest. However, although
> slower, it would be desirable to search the digest in all digest lists of
> that directory.
>
> This done in two steps. When a digest cache is being created,
> digest_cache_create() invokes digest_cache_dir_create(), to generate the
> list of current directory entries. Entries are placed in the list in
> ascending order by the <seq num> if prepended to the file name, or at the
> end of the list if not.
>
> The resulting digest cache has the IS_DIR bit set, to distinguish it from
> the digest caches created from regular files.
>
> Second, when a digest is searched in a directory digest cache,
> digest_cache_lookup() invokes digest_cache_dir_lookup_digest() to
> iteratively search that digest in each directory entry generated by
> digest_cache_dir_create().
>
> That list is stable, even if new files are added or deleted from that
> directory. A subsequent patch will invalidate the digest cache, forcing
> next callers of digest_cache_get() to get a new directory digest cache with
> the updated list of directory entries.
>
> If the current directory entry does not have a digest cache reference,
> digest_cache_dir_lookup_digest() invokes digest_cache_create() to create a
> new digest cache for that entry. In either case,
> digest_cache_dir_lookup_digest() calls then digest_cache_htable_lookup()
> with the new/existing digest cache to search the digest. Check and
> assignment of the digest cache in a directory entry is protected by the
> per entry digest_cache_mutex.
>
> The iteration stops when the digest is found. In that case,
> digest_cache_dir_lookup_digest() returns the digest cache reference of the
> current directory entry as the digest_cache_found_t type, so that callers
> of digest_cache_lookup() don't mistakenly try to call digest_cache_put()
> with that reference.
>
> This new reference type will be used to retrieve information about the
> digest cache containing the digest, which is not known in advance until the
> digest search is performed.
>
> The order of the list of directory entries influences the speed of the
> digest search. A search terminates faster if less digest caches have to be
> created. One way to optimize it could be to order the list of digest lists
> in the same way of when they are requested at boot.
>
> Finally, digest_cache_dir_free() releases the digest cache references
> stored in the list of directory entries, and frees the list itself.
>
> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> ---
>  security/digest_cache/Makefile   |   2 +-
>  security/digest_cache/dir.c      | 193 +++++++++++++++++++++++++++++++
>  security/digest_cache/htable.c   |  22 +++-
>  security/digest_cache/internal.h |  45 +++++++
>  security/digest_cache/main.c     |  12 ++
>  5 files changed, 271 insertions(+), 3 deletions(-)
>  create mode 100644 security/digest_cache/dir.c
>
> diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
> index 37a473c7bc28..e417da0383ab 100644
> --- a/security/digest_cache/Makefile
> +++ b/security/digest_cache/Makefile
> @@ -4,7 +4,7 @@
>  
>  obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
>  
> -digest_cache-y := main.o secfs.o htable.o populate.o modsig.o 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/digest_cache/dir.c b/security/digest_cache/dir.c
> new file mode 100644
> index 000000000000..7bfcdd5f7ef1
> --- /dev/null
> +++ b/security/digest_cache/dir.c
> @@ -0,0 +1,193 @@
> +// 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 "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_create - Create a directory digest cache
> + * @digest_cache: 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_create(struct digest_cache *digest_cache,
> +			    struct path *digest_list_path)
> +{
> +	struct file *dir_file;
> +	struct readdir_callback buf = {
> +		.ctx.actor = digest_cache_dir_iter,
> +	};
> +	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);
> +	}
> +
> +	buf.head = &digest_cache->dir_entries;
> +	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_lookup_digest - Lookup a digest
> + * @dentry: Dentry of the file whose digest is looked up
> + * @digest_list_path: Path structure of the digest list directory
> + * @digest_cache: 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_create() and looks up the digest in the digest cache of
> + * each entry.
> + *
> + * Return: A digest_cache_found_t value if the digest if found, zero otherwise.
> + */
> +digest_cache_found_t
> +digest_cache_dir_lookup_digest(struct dentry *dentry,
> +			       struct path *digest_list_path,
> +			       struct digest_cache *digest_cache, u8 *digest,
> +			       enum hash_algo algo)
> +{
> +	struct digest_cache *cache;
> +	struct dir_entry *dir_entry;
> +	int ret;
> +
> +	list_for_each_entry(dir_entry, &digest_cache->dir_entries, list) {
> +		mutex_lock(&dir_entry->digest_cache_mutex);
> +		if (!dir_entry->digest_cache) {
> +			cache = digest_cache_create(dentry, digest_list_path,
> +						    digest_cache->path_str,
> +						    dir_entry->name);
> +			/* 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)
> +			return (digest_cache_found_t)dir_entry->digest_cache;
> +	}
> +
> +	return 0UL;
> +}
> +
> +/**
> + * digest_cache_dir_free - Free the stored file list and put digest caches
> + * @digest_cache: Digest cache
> + *
> + * This function frees the file list created by digest_cache_create(), and puts
> + * the digest cache 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/digest_cache/htable.c b/security/digest_cache/htable.c
> index d2d5d8f5e5b1..8cf7400dfcf4 100644
> --- a/security/digest_cache/htable.c
> +++ b/security/digest_cache/htable.c
> @@ -8,6 +8,8 @@
>   */
>  
>  #define pr_fmt(fmt) "DIGEST CACHE: "fmt
> +#include <linux/namei.h>
> +
>  #include "internal.h"
>  
>  /**
> @@ -210,10 +212,26 @@ digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
>  					 struct digest_cache *digest_cache,
>  					 u8 *digest, enum hash_algo algo)
>  {
> +	struct path digest_list_path;
> +	digest_cache_found_t found;
>  	int ret;
>  
> -	ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo);
> -	return (!ret) ? (digest_cache_found_t)digest_cache : 0UL;
> +	if (!test_bit(IS_DIR, &digest_cache->flags)) {
> +		ret = digest_cache_htable_lookup(dentry, digest_cache, digest,
> +						 algo);
> +		return (!ret) ? (digest_cache_found_t)digest_cache : 0UL;

s/(!ret)/!ret/

I'd consider tho just use plain if-statement, ternary operator is best
to be avoided other than macros

> +	}
> +
> +	ret = kern_path(digest_cache->path_str, 0, &digest_list_path);
> +	if (ret < 0) {
> +		pr_debug("Cannot find path %s\n", digest_cache->path_str);
> +		return 0UL;
> +	}
> +
> +	found = digest_cache_dir_lookup_digest(dentry, &digest_list_path,
> +					       digest_cache, digest, algo);
> +	path_put(&digest_list_path);
> +	return found;
>  }
>  EXPORT_SYMBOL_GPL(digest_cache_lookup);
>  
> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> index 4929d25e7972..b7afca8e04da 100644
> --- a/security/digest_cache/internal.h
> +++ b/security/digest_cache/internal.h
> @@ -16,6 +16,39 @@
>  /* Digest cache bits in flags. */
>  #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
>  #define INVALID			1	/* Digest cache marked as invalid. */
> +#define IS_DIR			2	/* 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[];
> +} __packed;
>  
>  /**
>   * struct digest_cache_verif
> @@ -83,6 +116,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
> @@ -93,6 +127,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;
> @@ -193,4 +228,14 @@ 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_create(struct digest_cache *digest_cache,
> +			    struct path *digest_list_path);
> +digest_cache_found_t
> +digest_cache_dir_lookup_digest(struct dentry *dentry,
> +			       struct path *digest_list_path,
> +			       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/digest_cache/main.c b/security/digest_cache/main.c
> index f3475b36e566..15f1486610a3 100644
> --- a/security/digest_cache/main.c
> +++ b/security/digest_cache/main.c
> @@ -50,6 +50,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
>  	digest_cache->flags = 0UL;
>  	INIT_LIST_HEAD(&digest_cache->htables);
>  	INIT_LIST_HEAD(&digest_cache->verif_data);
> +	INIT_LIST_HEAD(&digest_cache->dir_entries);
>  	mutex_init(&digest_cache->mutex);
>  
>  	pr_debug("New digest cache %s (ref count: %d)\n",
> @@ -68,6 +69,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);
>  	mutex_destroy(&digest_cache->mutex);
>  
>  	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
> @@ -185,6 +187,16 @@ struct digest_cache *digest_cache_create(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, &dig_sec->dig_owner->flags);
> +
> +		ret = digest_cache_dir_create(digest_cache, digest_list_path);
> +		if (ret < 0) {
> +			pr_debug("Failed to create dir digest cache, ret: %d (keep digest cache)\n",
> +				 ret);
> +			/* Prevent usage of partially-populated digest cache. */
> +			set_bit(INVALID, &dig_sec->dig_owner->flags);
> +		}
>  	}
>  
>  	/* Creation complete, notify the other lock contenders. */


BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 10/14] digest cache: Prefetch digest lists if requested
  2024-04-15 14:24 ` [PATCH v4 10/14] digest cache: Prefetch digest lists if requested Roberto Sassu
@ 2024-04-15 19:42   ` Jarkko Sakkinen
  2024-04-16 10:34     ` Roberto Sassu
  0 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-15 19:42 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> 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 digest_cache LSM is not exempted from this issue. Under the assumption
> that only the digest list is measured, and file measurements are omitted if
> their digest is found in that digest list, a PCR can be predictable only if
> all files belong to the same digest list. Otherwise, it will still be
> unpredictable, since files accessed in a non-deterministic order will cause
> digest lists to be measured in a non-deterministic order too.
>
> Overcome this issue, if prefetching is enabled, by searching a digest list
> file name in digest_list_dir_lookup_filename() among the entries of the
> linked list built by digest_cache_dir_create(). If the file name does not
> match, read the digest list to trigger its measurement. Otherwise, also
> create a digest cache and return that to the caller. Release the extra
> reference of the directory digest cache in digest_cache_new(), since it was
> only used for the search and it is not going to be returned.
>
> Prefetching needs to be explicitly enabled by setting the new
> security.dig_prefetch xattr to 1 in the directory containing the digest
> lists. The newly introduced function digest_cache_prefetch_requested()
> checks first if the DIR_PREFETCH bit is set in dig_owner, otherwise it
> reads the xattr. digest_cache_create() sets DIR_PREFETCH in dig_owner, if
> prefetching is enabled, before declaring the digest cache as initialized.
>
> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> ---
>  include/uapi/linux/xattr.h       |  3 +
>  security/digest_cache/dir.c      | 55 +++++++++++++++++-
>  security/digest_cache/internal.h | 11 +++-
>  security/digest_cache/main.c     | 95 +++++++++++++++++++++++++++++++-
>  security/digest_cache/populate.c |  8 ++-
>  security/digest_cache/verif.c    |  5 +-
>  6 files changed, 170 insertions(+), 7 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/digest_cache/dir.c b/security/digest_cache/dir.c
> index 7bfcdd5f7ef1..a7d203c15386 100644
> --- a/security/digest_cache/dir.c
> +++ b/security/digest_cache/dir.c
> @@ -54,6 +54,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;
> @@ -127,6 +128,7 @@ int digest_cache_dir_create(struct digest_cache *digest_cache,
>   * @digest_cache: Digest cache
>   * @digest: Digest to search
>   * @algo: Algorithm of the digest to search
> + * @filename: File name of the digest list to search
>   *
>   * This function iterates over the linked list created by
>   * digest_cache_dir_create() and looks up the digest in the digest cache of
> @@ -149,7 +151,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>  		if (!dir_entry->digest_cache) {
>  			cache = digest_cache_create(dentry, digest_list_path,
>  						    digest_cache->path_str,
> -						    dir_entry->name);
> +						    dir_entry->name, false,
> +						    false);
>  			/* Ignore digest caches that cannot be instantiated. */
>  			if (!cache) {
>  				mutex_unlock(&dir_entry->digest_cache_mutex);
> @@ -158,6 +161,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>  
>  			/* Consume extra ref. from digest_cache_create(). */
>  			dir_entry->digest_cache = cache;
> +			/* Digest list was read, mark entry as prefetched. */
> +			dir_entry->prefetched = true;
>  		}
>  		mutex_unlock(&dir_entry->digest_cache_mutex);
>  
> @@ -171,6 +176,54 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>  	return 0UL;
>  }
>  
> +/**
> + * 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: Digest cache
> + * @filename: File name of the digest list to search
> + *
> + * This function iterates over the linked list created by
> + * digest_cache_dir_create() 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 returns the digest cache pointer from
> + * digest_cache_create() to the caller.
> + *
> + * Return: A digest cache pointer if the digest list if found, NULL otherwise.
> + */
> +struct digest_cache *
> +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);
> +
> +		dir_entry->prefetched = true;
> +		mutex_unlock(&dir_entry->digest_cache_mutex);
> +
> +		if (filename_found)
> +			return cache;
> +	}
> +
> +	return NULL;
> +}
> +
>  /**
>   * digest_cache_dir_free - Free the stored file list and put digest caches
>   * @digest_cache: Digest cache
> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> index b7afca8e04da..c13b35f6b2c0 100644
> --- a/security/digest_cache/internal.h
> +++ b/security/digest_cache/internal.h
> @@ -17,6 +17,7 @@
>  #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
>  #define INVALID			1	/* Digest cache marked as invalid. */
>  #define IS_DIR			2	/* Digest cache created from dir. */
> +#define DIR_PREFETCH		3	/* Prefetching requested for dir. */
>  
>  /**
>   * struct readdir_callback - Structure to store information for dir iteration
> @@ -37,6 +38,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
> @@ -47,6 +49,7 @@ struct dir_entry {
>  	struct digest_cache *digest_cache;
>  	struct mutex digest_cache_mutex;
>  	unsigned int seq_num;
> +	bool prefetched;
>  	char name[];
>  } __packed;
>  
> @@ -205,7 +208,8 @@ 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);
>  
>  /* htable.c */
>  int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
> @@ -236,6 +240,11 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>  			       struct path *digest_list_path,
>  			       struct digest_cache *digest_cache, u8 *digest,
>  			       enum hash_algo algo);
> +struct digest_cache *
> +digest_cache_dir_lookup_filename(struct dentry *dentry,
> +				 struct path *digest_list_path,
> +				 struct digest_cache *digest_cache,
> +				 char *filename);
>  void digest_cache_dir_free(struct digest_cache *digest_cache);
>  
>  #endif /* _DIGEST_CACHE_INTERNAL_H */
> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
> index 15f1486610a3..a5616fd07c1d 100644
> --- a/security/digest_cache/main.c
> +++ b/security/digest_cache/main.c
> @@ -83,6 +83,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
> @@ -109,7 +111,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)


This has enough parameters to consider separate "digest_cache_descriptor" 
or whatever. In some architectures this is beyond register parameters,
which is IMHO good threshold to consider that.

This will make e.g. tracing easier as you have to map only one parameter
to a known struct to inspect the values.


>  {
>  	struct path file_path;
>  	struct digest_cache *digest_cache = NULL;
> @@ -148,6 +151,16 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>  				 dentry->d_name.name);
>  			goto out;
>  		}
> +
> +		if (prefetch) {
> +			/* Fine to fail, we are just prefetching. */
> +			ret = digest_cache_populate(NULL, digest_list_path,
> +						    path_str, filename);
> +			pr_debug("Digest list %s/%s %s prefetched\n",
> +				 path_str, filename,
> +				 !ret ? "has been" : "cannot be");
> +			goto out;
> +		}
>  	}
>  
>  	dig_sec = digest_cache_get_security(inode);
> @@ -176,6 +189,11 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>  
>  	/* Make the other lock contenders wait until creation complete. */
>  	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
> +
> +	/* Set DIR_PREFETCH if prefetching was requested. */
> +	if (prefetch_req)
> +		set_bit(DIR_PREFETCH, &digest_cache->flags);
> +
>  	mutex_unlock(&dig_sec->dig_owner_mutex);
>  
>  	if (S_ISREG(inode->i_mode)) {
> @@ -220,6 +238,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 new security.dig_prefetch xattr.
> + *
> + * Return: True if prefetching is requested, false otherwise.
> + */
> +static 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
> @@ -230,13 +294,19 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>   * with that file name. If security.digest_list is not found, this function
>   * requests the creation of a digest cache on the parent directory.
>   *
> + * On prefetching, if the default path is a directory and if
> + * security.digest_list is found, this function first retrieves the directory
> + * digest cache, and then calls digest_cache_dir_lookup_filename() to retrieve
> + * the desired digest cache in that directory.
> + *
>   * Return: A new digest cache on success, NULL on error.
>   */
>  static struct digest_cache *digest_cache_new(struct dentry *dentry)
>  {
>  	char filename[NAME_MAX + 1] = { 0 };
> -	struct digest_cache *digest_cache = NULL;
> +	struct digest_cache *digest_cache = NULL, *found;
>  	struct path default_path;
> +	bool prefetch_req = false;
>  	int ret;
>  
>  	ret = kern_path(default_path_str, 0, &default_path);
> @@ -273,9 +343,28 @@ static struct digest_cache *digest_cache_new(struct dentry *dentry)
>  	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);
> +
> +	if (filename[0])
> +		prefetch_req = digest_cache_prefetch_requested(&default_path,
> +							default_path_str);
>  create:
> +	/* On prefetching, retrieve the directory digest cache. */
>  	digest_cache = digest_cache_create(dentry, &default_path,
> -					   default_path_str, filename);
> +					   default_path_str,
> +					   !prefetch_req ? filename : "",
> +					   prefetch_req, false);
> +	if (!digest_cache)
> +		goto out;
> +
> +	if (prefetch_req) {
> +		/* Find the digest cache with a matching file name. */
> +		found = digest_cache_dir_lookup_filename(dentry, &default_path,
> +							 digest_cache,
> +							 filename);
> +		/* Release ref. to the directory digest cache. */
> +		digest_cache_put(digest_cache);
> +		digest_cache = found;
> +	}
>  out:
>  	path_put(&default_path);
>  	return digest_cache;
> diff --git a/security/digest_cache/populate.c b/security/digest_cache/populate.c
> index 9c2fc2295310..17e7b011c367 100644
> --- a/security/digest_cache/populate.c
> +++ b/security/digest_cache/populate.c
> @@ -143,6 +143,12 @@ int digest_cache_populate(struct digest_cache *digest_cache,
>  		return ret;
>  	}
>  
> +	/* The caller wants just to read digest lists. */
> +	if (!digest_cache) {
> +		ret = 0;
> +		goto out_vfree;
> +	}
> +
>  	data_len = digest_cache_strip_modsig(data, ret);
>  
>  	/* Digest list parsers initialize the hash table and add the digests. */
> @@ -151,7 +157,7 @@ int digest_cache_populate(struct digest_cache *digest_cache,
>  	if (ret < 0)
>  		pr_debug("Error parsing digest list %s%s%s, ret: %d\n",
>  			 path_str, filename[0] ? "/" : "", filename, ret);
> -
> +out_vfree:
>  	vfree(data);
>  	return ret;
>  }
> diff --git a/security/digest_cache/verif.c b/security/digest_cache/verif.c
> index 04023240d3b4..c42ae93261e2 100644
> --- a/security/digest_cache/verif.c
> +++ b/security/digest_cache/verif.c
> @@ -33,7 +33,7 @@ static void free_verif(struct digest_cache_verif *verif)
>   * This function lets a verifier supply verification data about a digest list
>   * being read to populate the digest cache.
>   *
> - * Return: Zero on success, -ENOMEM if out of memory.
> + * Return: Zero on success, -ENOMEM if out of memory, -ENOENT on prefetching.
>   */
>  int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
>  			   size_t size)
> @@ -41,6 +41,9 @@ int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
>  	struct digest_cache *digest_cache = digest_cache_from_file_sec(file);
>  	struct digest_cache_verif *new_verif;
>  
> +	if (!digest_cache)
> +		return -ENOENT;
> +
>  	/*
>  	 * All allocations must be atomic (non-sleepable) since kprobe does not
>  	 * allow otherwise (kprobe is needed for testing).


BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 11/14] digest_cache: Reset digest cache on file/directory change
  2024-04-15 14:24 ` [PATCH v4 11/14] digest_cache: Reset digest cache on file/directory change Roberto Sassu
@ 2024-04-15 19:44   ` Jarkko Sakkinen
  2024-04-16 10:37     ` Roberto Sassu
  0 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-15 19:44 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> From: Roberto Sassu <roberto.sassu@huawei.com>
>
> Register six new LSM hooks, path_truncate, file_release, inode_unlink,
> inode_rename, inode_post_setxattr and inode_post_removexattr, to monitor
> digest lists/directory modifications.
>
> If an action affects a digest list or the parent directory, the new LSM
> hook implementations call digest_cache_reset_owner() to set the RESET bit
> (if unset) on the digest cache pointed to by dig_owner in the inode
> security blob. This will cause next calls to digest_cache_get() and
> digest_cache_create() to respectively put and clear dig_user and dig_owner,
> and request a new digest cache.
>
> If an action affects a file using a digest cache, the new LSM hook
> implementations call digest_cache_reset_user() to set the RESET_USER bit
> (if unset) on the digest cache pointed to by dig_user in the inode security
> blob. This will cause next calls to digest_cache_get() to put and clear
> dig_user, and retrieve the digest cache again.
>
> That does not affect other users of the old digest cache, since that one
> remains valid as long as the reference count is greater than zero. However,
> they will be notified in a subsequent patch about the reset, so that they
> can eventually request a new digest cache.
>
> Recreating a file digest cache means reading the digest list again and
> extracting the digests. Recreating a directory digest cache, instead, does
> not mean recreating the digest cache for directory entries, since those
> digest caches are likely already stored in the inode security blob. It
> would happen however for new files.
>
> Dig_owner reset for file 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 is being closed, inode_unlink, when a
> digest list is removed, and inode_rename when a digest list is renamed.
>
> Dig_owner reset for directory digest caches is done on file_release, when a
> new digest list is written in the digest list directory, on inode_unlink,
> when a digest list is deleted from that directory, and finally on
> inode_rename, when a digest list is moved to/from that directory.
>
> Dig_user reset 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.
>
> With the exception of file_release, which will always be executed (cannot
> be denied), and inode_post_setxattr and inode_post_removexattr, which are
> executed after the actual operation, the other LSM hooks are not optimal,
> since the digest_cache LSM does not know whether or not the operation will
> be allowed also by other LSMs. If the operation is denied, the digest_cache
> LSM would do an unnecessary reset.
>
> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> ---
>  security/digest_cache/Kconfig    |   1 +
>  security/digest_cache/Makefile   |   3 +-
>  security/digest_cache/dir.c      |   6 +
>  security/digest_cache/internal.h |  14 +++
>  security/digest_cache/main.c     |  19 +++
>  security/digest_cache/reset.c    | 197 +++++++++++++++++++++++++++++++
>  6 files changed, 239 insertions(+), 1 deletion(-)
>  create mode 100644 security/digest_cache/reset.c
>
> diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
> index cb4fa44e8f2a..54ba3a585073 100644
> --- a/security/digest_cache/Kconfig
> +++ b/security/digest_cache/Kconfig
> @@ -2,6 +2,7 @@
>  config SECURITY_DIGEST_CACHE
>  	bool "Digest_cache LSM"
>  	select TLV_PARSER
> +	select SECURITY_PATH
>  	default n
>  	help
>  	  This option enables an LSM maintaining a cache of digests
> diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
> index e417da0383ab..3d5e600a2c45 100644
> --- a/security/digest_cache/Makefile
> +++ b/security/digest_cache/Makefile
> @@ -4,7 +4,8 @@
>  
>  obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
>  
> -digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o
> +digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o \
> +		  reset.o
>  
>  digest_cache-y += parsers/tlv.o
>  digest_cache-y += parsers/rpm.o
> diff --git a/security/digest_cache/dir.c b/security/digest_cache/dir.c
> index a7d203c15386..937177660242 100644
> --- a/security/digest_cache/dir.c
> +++ b/security/digest_cache/dir.c
> @@ -148,6 +148,12 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>  
>  	list_for_each_entry(dir_entry, &digest_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,
>  						    digest_cache->path_str,
> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> index c13b35f6b2c0..c816929c4743 100644
> --- a/security/digest_cache/internal.h
> +++ b/security/digest_cache/internal.h
> @@ -18,6 +18,8 @@
>  #define INVALID			1	/* Digest cache marked as invalid. */
>  #define IS_DIR			2	/* Digest cache created from dir. */
>  #define DIR_PREFETCH		3	/* Prefetching requested for dir. */
> +#define RESET			4	/* Digest cache to be recreated. */
> +#define RESET_USER		5	/* Dig_user pointer to be released. */
>  
>  /**
>   * struct readdir_callback - Structure to store information for dir iteration
> @@ -247,4 +249,16 @@ digest_cache_dir_lookup_filename(struct dentry *dentry,
>  				 char *filename);
>  void digest_cache_dir_free(struct digest_cache *digest_cache);
>  
> +/* reset.c */
> +int digest_cache_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/digest_cache/main.c b/security/digest_cache/main.c
> index a5616fd07c1d..ce3518a33c80 100644
> --- a/security/digest_cache/main.c
> +++ b/security/digest_cache/main.c
> @@ -169,6 +169,11 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>  
>  	/* Serialize check and assignment of dig_owner. */
>  	mutex_lock(&dig_sec->dig_owner_mutex);
> +	if (dig_sec->dig_owner && test_bit(RESET, &dig_sec->dig_owner->flags)) {
> +		digest_cache_put(dig_sec->dig_owner);
> +		dig_sec->dig_owner = NULL;
> +	}
> +
>  	if (dig_sec->dig_owner) {
>  		/* Increment ref. count for reference returned to the caller. */
>  		digest_cache = digest_cache_ref(dig_sec->dig_owner);
> @@ -403,6 +408,13 @@ 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) ||
> +	     test_bit(RESET_USER, &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(). */
> @@ -491,6 +503,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/digest_cache/reset.c b/security/digest_cache/reset.c
> new file mode 100644
> index 000000000000..e07222b0e0e8
> --- /dev/null
> +++ b/security/digest_cache/reset.c
> @@ -0,0 +1,197 @@
> +// 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_owner - Reset dig_owner
> + * @inode: Inode of the digest list/directory containing the digest list
> + * @reason: Reason for reset
> + *
> + * This function sets the RESET bit of the digest cache pointed to by dig_owner
> + * (if unset), so that digest_cache_get() and digest_cache_create() respectively
> + * release and clear dig_user and dig_owner in the inode security blob. This
> + * causes new callers of digest_cache_get() to get a new digest cache.
> + */
> +static void digest_cache_reset_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 &&
> +	    !test_and_set_bit(RESET, &dig_sec->dig_owner->flags))
> +		pr_debug("Resetting %s (dig_owner), reason: %s\n",
> +			 dig_sec->dig_owner->path_str, reason);
> +	mutex_unlock(&dig_sec->dig_owner_mutex);
> +}
> +
> +/**
> + * digest_cache_reset_user - Reset dig_user
> + * @inode: Inode of the file using the digest cache
> + * @reason: Reason for reset
> + *
> + * This function sets the RESET_USER bit (if unset), so that digest_cache_get()
> + * clears the dig_user pointer in the inode security blob and determines again
> + * the digest list inode to get a digest cache from.
> + */
> +static void digest_cache_reset_user(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_user_mutex);
> +	if (dig_sec->dig_user &&
> +	    !test_and_set_bit(RESET_USER, &dig_sec->dig_user->flags))
> +		pr_debug("Resetting %s (dig_user), reason: %s\n",
> +			 dig_sec->dig_user->path_str, reason);
> +	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, it resets the inode dig_owner, to force rebuilding the digest
> + * cache.
> + *
> + * Return: Zero.
> + */
> +int digest_cache_path_truncate(const struct path *path)
> +{
> +	struct inode *inode = d_backing_inode(path->dentry);
> +
> +	if (!S_ISREG(inode->i_mode))
> +		return 0;
> +
> +	digest_cache_reset_owner(inode, "file_truncate");
> +	return 0;
> +}
> +
> +/**
> + * digest_cache_file_release - Last reference of a file desc is being released
> + * @file: File descriptor
> + *
> + * This function is called when the last reference of a file descriptor is
> + * being released. If the inode is a regular file and was opened for write, or
> + * the parent inode is the digest list directory and the file was created, it
> + * resets the inode dig_owner, to force rebuilding the digest cache.
> + */
> +void digest_cache_file_release(struct file *file)
> +{
> +	struct inode *dir = d_backing_inode(file_dentry(file)->d_parent);
> +
> +	if (!S_ISREG(file_inode(file)->i_mode) || !(file->f_mode & FMODE_WRITE))
> +		return;
> +
> +	digest_cache_reset_owner(file_inode(file), "file_file_release");
> +	if (file->f_mode & FMODE_CREATED)
> +		digest_cache_reset_owner(dir, "dir_file_release");
> +}
> +
> +/**
> + * digest_cache_inode_unlink - An inode is being removed
> + * @dir: Inode of the affected directory
> + * @dentry: Dentry of the inode being removed
> + *
> + * This function is called when an existing inode is being removed. If the
> + * inode is a digest list, or the parent inode is the digest list directory and
> + * the inode is a regular file, it resets the affected inode dig_owner, to force
> + * rebuilding the digest cache.
> + *
> + * Return: Zero.
> + */
> +int digest_cache_inode_unlink(struct inode *dir, struct dentry *dentry)
> +{
> +	struct inode *inode = d_backing_inode(dentry);
> +
> +	if (!S_ISREG(inode->i_mode))
> +		return 0;
> +
> +	digest_cache_reset_owner(inode, "file_unlink");
> +	digest_cache_reset_owner(dir, "dir_unlink");
> +	return 0;
> +}
> +
> +/**
> + * digest_cache_inode_rename - An inode is being renamed
> + * @old_dir: Inode of the directory containing the inode being renamed
> + * @old_dentry: Dentry of the inode being renamed
> + * @new_dir: Directory where the inode will be placed into
> + * @new_dentry: Dentry of the inode after being renamed
> + *
> + * This function is called when an existing inode is being moved from a
> + * directory to another (rename). If the inode is a digest list, or that inode
> + * is moved from/to the digest list directory, it resets the affected inode
> + * dig_owner, to force rebuilding the digest cache.
> + *
> + * Return: Zero.
> + */
> +int digest_cache_inode_rename(struct inode *old_dir, struct dentry *old_dentry,
> +			      struct inode *new_dir, struct dentry *new_dentry)
> +{
> +	struct inode *old_inode = d_backing_inode(old_dentry);
> +
> +	if (!S_ISREG(old_inode->i_mode))
> +		return 0;
> +
> +	digest_cache_reset_owner(old_inode, "file_rename");
> +	digest_cache_reset_owner(old_dir, "dir_rename_from");
> +	digest_cache_reset_owner(new_dir, "dir_rename_to");
> +	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
> + * resets 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_reset_user(d_backing_inode(dentry), "file_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 resets 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)

nit:

void digest_cache_inode_post_removexattr(struct dentry *dentry, const char *name)

would be fine too

> +{
> +	if (strcmp(name, XATTR_NAME_DIGEST_LIST))
> +		return;
> +
> +	digest_cache_reset_user(d_backing_inode(dentry), "file_removexattr");
> +}


BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 13/14] selftests/digest_cache: Add selftests for digest_cache LSM
  2024-04-15 14:24 ` [PATCH v4 13/14] selftests/digest_cache: Add selftests for digest_cache LSM Roberto Sassu
@ 2024-04-15 19:47   ` Jarkko Sakkinen
  2024-04-16 10:39     ` Roberto Sassu
  0 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-15 19:47 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> From: Roberto Sassu <roberto.sassu@huawei.com>
>
> Add tests to verify the correctness of the digest_cache LSM, in all_test.c.
>
> Add the kernel module digest_cache_kern.ko, to let all_test call the API
> of the digest_cache LSM through the newly introduced digest_cache_test file
> in securityfs.
>
> Test coverage information:
>
> File 'security/digest_cache/notifier.c'
> Lines executed:100.00% of 31
> File 'security/digest_cache/reset.c'
> Lines executed:98.36% of 61
> File 'security/digest_cache/main.c'
> Lines executed:90.29% of 206
> File 'security/digest_cache/modsig.c'
> Lines executed:42.86% of 21
> File 'security/digest_cache/htable.c'
> Lines executed:93.02% of 86
> File 'security/digest_cache/populate.c'
> Lines executed:92.86% of 56
> File 'security/digest_cache/verif.c'
> Lines executed:89.74% of 39
> File 'security/digest_cache/dir.c'
> Lines executed:90.62% of 96
> File 'security/digest_cache/secfs.c'
> Lines executed:57.14% of 21
> File 'security/digest_cache/parsers/tlv.c'
> Lines executed:79.75% of 79
> File 'security/digest_cache/parsers/rpm.c'
> Lines executed:88.46% of 78
>
> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> ---
>  MAINTAINERS                                   |   1 +
>  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 | 815 ++++++++++++++++++
>  tools/testing/selftests/digest_cache/common.c |  78 ++
>  tools/testing/selftests/digest_cache/common.h | 135 +++
>  .../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     | 564 ++++++++++++
>  14 files changed, 1969 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/MAINTAINERS b/MAINTAINERS
> index 72801a88449c..d7f700da009e 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -6198,6 +6198,7 @@ M:	Roberto Sassu <roberto.sassu@huawei.com>
>  L:	linux-security-module@vger.kernel.org
>  S:	Maintained
>  F:	security/digest_cache/
> +F:	tools/testing/selftests/digest_cache/
>  
A common convetion is to have one patch with MAINTAINERS update in the
tail. This is now sprinkled to multiple patches which is not good.

>  DIGITEQ AUTOMOTIVE MGB4 V4L2 DRIVER
>  M:	Martin Tuma <martin.tuma@digiteqautomotive.com>
> diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
> index 15b6a111c3be..3c5965a62d28 100644
> --- a/tools/testing/selftests/Makefile
> +++ b/tools/testing/selftests/Makefile
> @@ -13,6 +13,7 @@ TARGETS += core
>  TARGETS += cpufreq
>  TARGETS += cpu-hotplug
>  TARGETS += damon
> +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..6b1e0d3c08cf
> --- /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)
> +	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..9f45e522c43c
> --- /dev/null
> +++ b/tools/testing/selftests/digest_cache/all_test.c
> @@ -0,0 +1,815 @@
> +// 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 digest_cache LSM.
> + */
> +
> +#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;
> +	int notify_inodesfd;
> +};
> +
> +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 notify inodes interface. */
> +	self->notify_inodesfd = open(DIGEST_CACHE_NOTIFY_INODES_INTERFACE,
> +				     O_RDWR, 0600);
> +	ASSERT_NE(-1, self->notify_inodesfd);
> +
> +	/* 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 notify inodes interface. */
> +	close(self->notify_inodesfd);
> +
> +	/* 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 notify_inode_init(struct _test_data_shared_data *self,
> +			      struct __test_metadata *_metadata)
> +{
> +	/* Clear buffer. */
> +	ASSERT_EQ(1, write(self->notify_inodesfd, "1", 1));
> +}
> +
> +static void notify_inodes_check(struct _test_data_shared_data *self,
> +				struct __test_metadata *_metadata,
> +				char *filenames)
> +{
> +	char notify_inodes_buf[1024] = { 0 };
> +	char notify_inodes_buf_kernel[1024] = { 0 };
> +	char *filename, *filenames_copy, *buf_ptr = notify_inodes_buf;
> +	struct stat st;
> +	int fd;
> +
> +	ASSERT_LT(0, read(self->notify_inodesfd, notify_inodes_buf_kernel,
> +			  sizeof(notify_inodes_buf_kernel)));
> +
> +	filenames_copy = strdup(filenames);
> +	ASSERT_NE(NULL, filenames_copy);
> +
> +	while ((filename = strsep(&filenames_copy, ","))) {
> +		fd = openat(self->base_dirfd, filename, O_RDONLY);
> +		ASSERT_NE(-1, fd);
> +		ASSERT_EQ(0, fstat(fd, &st));
> +		close(fd);
> +
> +		buf_ptr += snprintf(buf_ptr,
> +				    sizeof(notify_inodes_buf) -
> +				    (buf_ptr - notify_inodes_buf), "%s%lu",
> +				    notify_inodes_buf[0] ? "," : "", st.st_ino);
> +	}
> +
> +	free(filenames_copy);
> +
> +	ASSERT_EQ(0, strcmp(notify_inodes_buf, notify_inodes_buf_kernel));
> +}
> +
> +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));
> +
> +	notify_inode_init(self, _metadata);
> +
> +	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));
> +
> +		notify_inodes_check(self, _metadata, "file");
> +		return;
> +	default:
> +		break;
> +	}
> +
> +	ASSERT_NE(0, query_test(self->kernfd, self->base_dir, "file",
> +				HASH_ALGO_SHA1, 0, 1));
> +
> +	notify_inodes_check(self, _metadata, "file");
> +}
> +
> +/*
> + * 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)
> +{
> +	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;
> +			}
> +		}
> +
> +		ASSERT_EQ(expected_ret, query_test(self->kernfd, self->base_dir,
> +						   "file", 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 digest_list_filename[NAME_MAX + 1];
> +	int i, removed[10];
> +
> +	for (i = 0; i < 10; i++) {
> +		snprintf(digest_list_filename, sizeof(digest_list_filename),
> +			 "tlv-digest_list%d", 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", NULL));
> +	/* The second file is to have duplicate notifications (file and dir). */
> +	ASSERT_EQ(0, create_file(self->base_dirfd, "file2",
> +				 "tlv-digest_list7"));
> +	/* The query adds file2 inode to the file digest cache notif. list. */
> +	ASSERT_NE(0, query_test(self->kernfd, self->base_dir, "file2",
> +				HASH_ALGO_SHA1, 0, 1));
> +
> +	query_test_with_failures(self, _metadata, 0, 10, removed, 0);
> +
> +	notify_inode_init(self, _metadata);
> +	ASSERT_EQ(0, unlinkat(self->digest_lists_dirfd, "tlv-digest_list7", 0));
> +	/* File notification comes before directory notification. */
> +	notify_inodes_check(self, _metadata, "file2,file");
> +
> +	removed[0] = 7;
> +
> +	query_test_with_failures(self, _metadata, 0, 10, removed, 1);
> +
> +	notify_inode_init(self, _metadata);
> +	ASSERT_EQ(0, renameat(self->digest_lists_dirfd, "tlv-digest_list6",
> +			      self->base_dirfd, "tlv-digest_list6"));
> +	notify_inodes_check(self, _metadata, "file");
> +
> +	removed[1] = 6;
> +
> +	query_test_with_failures(self, _metadata, 0, 10, removed, 2);
> +
> +	notify_inode_init(self, _metadata);
> +	ASSERT_EQ(0, renameat(self->base_dirfd, "tlv-digest_list6",
> +			      self->digest_lists_dirfd, "tlv-digest_list6"));
> +	notify_inodes_check(self, _metadata, "file");
> +
> +	query_test_with_failures(self, _metadata, 0, 10, removed, 1);
> +
> +	notify_inode_init(self, _metadata);
> +	ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, "tlv-digest_list10",
> +				  HASH_ALGO_SHA1, 10, 1, TLV_NO_FAILURE));
> +	notify_inodes_check(self, _metadata, "file");
> +
> +	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));
> +	}
> +
> +	ASSERT_EQ(0, create_file(self->base_dirfd, "file", NULL));
> +
> +	for (i = 0; i < 11; i++) {
> +		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], digest_lists_kernel[1024] = { 0 };
> +	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, sizeof(digest_lists)));
> +
> +	for (i = start_number; i <= end_number; i++) {
> +		if (digest_lists_kernel[0])
> +			strcat(digest_lists_kernel, ",");
> +
> +		snprintf(digest_list_filename, sizeof(digest_list_filename),
> +			 "%d-tlv-digest_list%d", i, i);
> +		strcat(digest_lists_kernel, 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], digest_lists_kernel[1024] = { 0 };
> +	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, sizeof(digest_lists)));
> +
> +	for (i = 0; i < NUM_DIGEST_LISTS_PREFETCH; i++) {
> +		if (digest_lists_kernel[0])
> +			strcat(digest_lists_kernel, ",");
> +
> +		snprintf(digest_list_filename, sizeof(digest_list_filename),
> +			 "%d-tlv-digest_list%d", i, i);
> +		strcat(digest_lists_kernel, 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..2123f7d937ce
> --- /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 digest_cache LSM.
> + */
> +
> +#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] = "filenames",
> +	[VERIF_NUMBER] = "number",
> +	[VERIF_PREFETCH] = "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..e52e4b137807
> --- /dev/null
> +++ b/tools/testing/selftests/digest_cache/common.h
> @@ -0,0 +1,135 @@
> +/* 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_TEST_INTERFACE "/sys/kernel/security/digest_cache_test"
> +#define DIGEST_CACHE_PATH_INTERFACE "/sys/kernel/security/digest_cache_path"
> +#define DIGEST_CACHE_NOTIFY_INODES_INTERFACE  \
> +	"/sys/kernel/security/digest_cache_notify_inodes"
> +#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;
> +} __attribute__ ((__packed__));
> +
> +struct rpm_entryinfo {
> +	s32 tag;
> +	u32 type;
> +	s32 offset;
> +	u32 count;
> +} __attribute__ ((__packed__));
> +
> +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..1bacadad6b6a
> --- /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 digest_cache LSM.
> + */
> +
> +#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..075a06cc4f8e
> --- /dev/null
> +++ b/tools/testing/selftests/digest_cache/config
> @@ -0,0 +1 @@
> +CONFIG_SECURITY_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..7215ef638e66
> --- /dev/null
> +++ b/tools/testing/selftests/digest_cache/testmod/kern.c
> @@ -0,0 +1,564 @@
> +// 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 digest_cache LSM.
> + */
> +
> +#define pr_fmt(fmt) "DIGEST CACHE TEST: "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, *notify_inodes;
> +static struct digest_cache *digest_cache;
> +static digest_cache_found_t found;
> +static int cur_verif_index;
> +static u8 prefetch_buf[4096];
> +static u8 notify_inodes_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, "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, "number", &number, sizeof(number));
> +}
> +
> +static ssize_t filenames_read(struct file *file, char __user *buf,
> +			      size_t datalen, loff_t *ppos)
> +{
> +	loff_t _ppos = 0;
> +	char *filenames_list;
> +
> +	filenames_list = digest_cache_verif_get(found ?
> +				digest_cache_from_found_t(found) : digest_cache,
> +				verifs_str[VERIF_FILENAMES]);
> +	if (!filenames_list)
> +		return -ENOENT;
> +
> +	return simple_read_from_buffer(buf, datalen, &_ppos, filenames_list,
> +				       strlen(filenames_list) + 1);
> +}
> +
> +static ssize_t number_read(struct file *file, char __user *buf, size_t datalen,
> +			   loff_t *ppos)
> +{
> +	loff_t _ppos = 0;
> +	u64 *number;
> +	char temp[20];
> +	ssize_t len;
> +
> +	number = digest_cache_verif_get(found ?
> +					digest_cache_from_found_t(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 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, "probe_digest_cache", "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 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 int test_digest_cache_change(struct notifier_block *nb,
> +				    unsigned long event, void *data)
> +{
> +	struct digest_cache_event_data *event_data = data;
> +	char i_ino_str[10];
> +
> +	if (event != DIGEST_CACHE_RESET)
> +		return NOTIFY_DONE;
> +
> +	if (notify_inodes_buf[0])
> +		strlcat(notify_inodes_buf, ",", sizeof(notify_inodes_buf));
> +
> +	snprintf(i_ino_str, sizeof(i_ino_str), "%lu", event_data->inode->i_ino);
> +	strlcat(notify_inodes_buf, i_ino_str, sizeof(notify_inodes_buf));
> +	return 0;
> +}
> +
> +static struct notifier_block digest_cache_notifier = {
> +	.notifier_call = test_digest_cache_change,
> +};
> +
> +static ssize_t write_notify_inodes(struct file *file, const char __user *buf,
> +			     size_t datalen, loff_t *ppos)
> +{
> +	memset(notify_inodes_buf, 0, sizeof(notify_inodes_buf));
> +	return datalen;
> +}
> +
> +static ssize_t read_notify_inodes(struct file *file, char __user *buf,
> +				  size_t datalen, loff_t *ppos)
> +{
> +	loff_t _ppos = 0;
> +
> +	return simple_read_from_buffer(buf, datalen, &_ppos, notify_inodes_buf,
> +				       strlen(notify_inodes_buf) + 1);
> +}
> +
> +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 const struct file_operations digest_cache_notify_inodes_ops = {
> +	.open = generic_file_open,
> +	.write = write_notify_inodes,
> +	.read = read_notify_inodes,
> +	.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;
> +	}
> +
> +	notify_inodes = securityfs_create_file("digest_cache_notify_inodes",
> +					       0660, NULL, NULL,
> +					       &digest_cache_notify_inodes_ops);
> +	if (IS_ERR(notify_inodes)) {
> +		ret = PTR_ERR(notify_inodes);
> +		goto out_test;
> +	}
> +
> +	ret = digest_cache_register_notifier(&digest_cache_notifier);
> +	if (ret < 0)
> +		goto out_notify_inodes;
> +
> +	return 0;
> +
> +out_notify_inodes:
> +	securityfs_remove(notify_inodes);
> +out_test:
> +	securityfs_remove(test);
> +out_kprobe:
> +	unregister_kprobe(&kp);
> +	return ret;
> +}
> +
> +static void __exit digest_cache_test_fini(void)
> +{
> +	if (digest_cache)
> +		digest_cache_put(digest_cache);
> +
> +	digest_cache_unregister_notifier(&digest_cache_notifier);
> +	securityfs_remove(notify_inodes);
> +	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");


BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 01/14] lib: Add TLV parser
  2024-04-15 19:19   ` Jarkko Sakkinen
@ 2024-04-15 21:07     ` Randy Dunlap
  2024-04-16 14:23       ` Jarkko Sakkinen
  0 siblings, 1 reply; 43+ messages in thread
From: Randy Dunlap @ 2024-04-15 21:07 UTC (permalink / raw)
  To: Jarkko Sakkinen, Roberto Sassu, corbet, paul, jmorris, serge,
	akpm, shuah, mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu



On 4/15/24 12:19 PM, Jarkko Sakkinen wrote:
> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
>> From: Roberto Sassu <roberto.sassu@huawei.com>
>>
>> Add a parser of a generic TLV format:
> 
> What is TLV?

type-length-value

i.e., a descriptor that contains a value.

IIUC.

-- 
#Randy
https://people.kernel.org/tglx/notes-about-netiquette
https://subspace.kernel.org/etiquette.html

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 00/14] security: digest_cache LSM
  2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
                   ` (14 preceding siblings ...)
  2024-04-15 19:18 ` [PATCH v4 00/14] security: " Jarkko Sakkinen
@ 2024-04-16  4:49 ` Bagas Sanjaya
       [not found]   ` <66201cd2.df0a0220.a8ad5.6fbaSMTPIN_ADDED_BROKEN@mx.google.com>
  15 siblings, 1 reply; 43+ messages in thread
From: Bagas Sanjaya @ 2024-04-16  4:49 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

[-- Attachment #1: Type: text/plain, Size: 1152 bytes --]

On Mon, Apr 15, 2024 at 04:24:22PM +0200, Roberto Sassu wrote:
> 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 the large user
> base, due to requiring higher than desired constraints on performance,
> flexibility and configurability, that only security conscious people are
> willing to accept.
> 
> This is where the new digest_cache LSM comes into play, it offers
> additional support for new and existing integrity solutions, to make
> them faster and easier to deploy.
> 
> The full documentation with the motivation and the solution details can be
> found in patch 14.
> 
> The IMA integration patch set will be introduced separately. Also a PoC
> based on the current version of IPE can be provided.
> 

I can't cleanly apply this series (conflict on patch [13/14]). Can you
point out the base commit of this series?

Confused...

-- 
An old man doll... just what I always wanted! - Clara

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 00/14] security: digest_cache LSM
  2024-04-15 19:18 ` [PATCH v4 00/14] security: " Jarkko Sakkinen
@ 2024-04-16  6:56   ` Roberto Sassu
  0 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-16  6:56 UTC (permalink / raw)
  To: Jarkko Sakkinen, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon, 2024-04-15 at 22:18 +0300, Jarkko Sakkinen wrote:
> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> > 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 the large user
> > base, due to requiring higher than desired constraints on performance,
> > flexibility and configurability, that only security conscious people are
> > willing to accept.
> > 
> > This is where the new digest_cache LSM comes into play, it offers
> > additional support for new and existing integrity solutions, to make
> > them faster and easier to deploy.
> 
> Sorry for nitpicking but what are the existing integrity solutions, 
> and how does it help with this struggle? I.e. what is the gist here?

No worries... please have a look at patch 14. It should have all the
information.

Thanks

Roberto


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 02/14] security: Introduce the digest_cache LSM
  2024-04-15 19:31   ` Jarkko Sakkinen
@ 2024-04-16  7:09     ` Roberto Sassu
  2024-04-16 14:33       ` Jarkko Sakkinen
  0 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-16  7:09 UTC (permalink / raw)
  To: Jarkko Sakkinen, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Mon, 2024-04-15 at 22:31 +0300, Jarkko Sakkinen wrote:
> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> > From: Roberto Sassu <roberto.sassu@huawei.com>
> > 
> > Introduce the digest_cache LSM, to collect digests from various sources
> > (called digest lists), and to store them in kernel memory, in a set of hash
> > tables forming a digest cache. Extracted digests can be used as reference
> > values for integrity verification of file data or metadata.
> > 
> > A digest cache has three types of references: in the inode security blob of
> > the digest list the digest cache was created from (dig_owner field); in the
> > security blob of the inodes for which the digest cache is requested
> > (dig_user field); a reference returned by digest_cache_get().
> > 
> > References are released with digest_cache_put(), in the first two cases
> > when inodes are evicted from memory, in the last case when that function is
> > explicitly called. Obtaining a digest cache reference means that the digest
> > cache remains valid and cannot be freed until releasing it and until the
> > total number of references (stored in the digest cache) becomes zero.
> > 
> > When digest_cache_get() is called on an inode to compare its digest with
> > a reference value, the digest_cache LSM knows which digest cache to get
> > from the new security.digest_list xattr added to that inode, which contains
> > the file name of the desired digest list digests will be extracted from.
> > 
> > All digest lists are expected to be in the same directory, defined in the
> > kernel config, and modifiable (with a later patch) at run-time through
> > securityfs. When the digest_cache LSM reads the security.digest_list xattr,
> > it uses its value as last path component, appended to the default path
> > (unless the default path is a file). If an inode does not have that xattr,
> > the default path is considered as the final destination.
> > 
> > The default path can be either a file or a directory. If it is a file, the
> > digest_cache LSM always uses the same digest cache from that file to verify
> > all inodes (the xattr, if present, is ignored). If it is a directory, and
> > the inode to verify does not have the xattr, a subsequent patch will make
> > it possible to iterate and lookup on the digest caches created from each
> > directory entry.
> > 
> > Digest caches are created on demand, only when digest_cache_get() is
> > called. The first time a digest cache is requested, the digest_cache LSM
> > creates it and sets its reference in the dig_owner and dig_user fields of
> > the respective inode security blobs. On the next requests, the previously
> > set reference is returned, after incrementing the reference count.
> > 
> > Since there might be multiple digest_cache_get() calls for the same inode,
> > or for different inodes pointing to the same digest list, dig_owner_mutex
> > and dig_user_mutex have been introduced to protect the check and assignment
> > of the digest cache reference in the inode security blob.
> > 
> > Contenders that didn't get the lock also have to wait until the digest
> > cache is fully instantiated (when the bit INIT_IN_PROGRESS is cleared).
> > Dig_owner_mutex cannot be used for waiting on the instantiation to avoid
> > lock inversion with the inode lock for directories.
> > 
> > Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> > ---
> >  MAINTAINERS                                   |   6 +
> >  include/linux/digest_cache.h                  |  32 ++
> >  include/uapi/linux/lsm.h                      |   1 +
> >  include/uapi/linux/xattr.h                    |   3 +
> >  security/Kconfig                              |  11 +-
> >  security/Makefile                             |   1 +
> >  security/digest_cache/Kconfig                 |  16 +
> >  security/digest_cache/Makefile                |   7 +
> >  security/digest_cache/internal.h              |  86 ++++
> >  security/digest_cache/main.c                  | 404 ++++++++++++++++++
> >  security/security.c                           |   3 +-
> >  .../selftests/lsm/lsm_list_modules_test.c     |   3 +
> >  12 files changed, 567 insertions(+), 6 deletions(-)
> >  create mode 100644 include/linux/digest_cache.h
> >  create mode 100644 security/digest_cache/Kconfig
> >  create mode 100644 security/digest_cache/Makefile
> >  create mode 100644 security/digest_cache/internal.h
> >  create mode 100644 security/digest_cache/main.c
> > 
> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index b1ca23ab8732..72801a88449c 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -6193,6 +6193,12 @@ L:	linux-gpio@vger.kernel.org
> >  S:	Maintained
> >  F:	drivers/gpio/gpio-gpio-mm.c
> >  
> > +DIGEST_CACHE LSM
> > +M:	Roberto Sassu <roberto.sassu@huawei.com>
> > +L:	linux-security-module@vger.kernel.org
> > +S:	Maintained
> > +F:	security/digest_cache/
> > +
> >  DIGITEQ AUTOMOTIVE MGB4 V4L2 DRIVER
> >  M:	Martin Tuma <martin.tuma@digiteqautomotive.com>
> >  L:	linux-media@vger.kernel.org
> 
> Nit: afaik, MAINTAINER updates should be split.

Ok, didn't know...

> > diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
> > new file mode 100644
> > index 000000000000..e79f94a60b0f
> > --- /dev/null
> > +++ b/include/linux/digest_cache.h
> > @@ -0,0 +1,32 @@
> > +/* SPDX-License-Identifier: GPL-2.0 */
> > +/*
> > + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
> > + *
> > + * Author: Roberto Sassu <roberto.sassu@huawei.com>
> 
> Nit: Git has an author field in commit granularity so adding author
> fields to files is sort of old world.

Still like it if you don't mind...

What happens when you get the source code from non-git?

> > + *
> > + * Public API of the digest_cache LSM.
> > + */
> > +
> > +#ifndef _LINUX_DIGEST_CACHE_H
> > +#define _LINUX_DIGEST_CACHE_H
> > +
> > +#include <linux/fs.h>
> > +
> > +struct digest_cache;
> 
> Is this declaration necessary?

I thought so. Users of the digest_cache LSM do:

struct digest_cache *digest_cache;


digest_cache = digest_cache_get(dentry);


and so on. Those users don't know the internal layout of the
digest_cache structure, but still pass it to the various functions.

> I don't think you need forward declaration here as this does compile:
> 
> #include <stdio.h>
> 
> struct digest_cache *digest_cache_get(void)
> {
>         return NULL;
> }
> 
> int main(void)
> {
>         return (long)digest_cache_get();
> }
> 
> 
> > +
> > +#ifdef CONFIG_SECURITY_DIGEST_CACHE
> > +struct digest_cache *digest_cache_get(struct dentry *dentry);
> > +void digest_cache_put(struct digest_cache *digest_cache);
> > +
> > +#else
> > +static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
> > +{
> > +	return NULL;
> > +}
> > +
> > +static inline void digest_cache_put(struct digest_cache *digest_cache)
> > +{
> > +}
> > +
> > +#endif /* CONFIG_SECURITY_DIGEST_CACHE */
> > +#endif /* _LINUX_DIGEST_CACHE_H */
> > diff --git a/include/uapi/linux/lsm.h b/include/uapi/linux/lsm.h
> > index 33d8c9f4aa6b..832b3aea5c26 100644
> > --- a/include/uapi/linux/lsm.h
> > +++ b/include/uapi/linux/lsm.h
> > @@ -64,6 +64,7 @@ struct lsm_ctx {
> >  #define LSM_ID_LANDLOCK		110
> >  #define LSM_ID_IMA		111
> >  #define LSM_ID_EVM		112
> > +#define LSM_ID_DIGEST_CACHE	113
> >  
> >  /*
> >   * LSM_ATTR_XXX definitions identify different LSM attributes
> > diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h
> > index 9463db2dfa9d..8a58cf4bce65 100644
> > --- a/include/uapi/linux/xattr.h
> > +++ b/include/uapi/linux/xattr.h
> > @@ -54,6 +54,9 @@
> >  #define XATTR_IMA_SUFFIX "ima"
> >  #define XATTR_NAME_IMA XATTR_SECURITY_PREFIX XATTR_IMA_SUFFIX
> >  
> > +#define XATTR_DIGEST_LIST_SUFFIX "digest_list"
> > +#define XATTR_NAME_DIGEST_LIST XATTR_SECURITY_PREFIX XATTR_DIGEST_LIST_SUFFIX
> > +
> >  #define XATTR_SELINUX_SUFFIX "selinux"
> >  #define XATTR_NAME_SELINUX XATTR_SECURITY_PREFIX XATTR_SELINUX_SUFFIX
> >  
> > diff --git a/security/Kconfig b/security/Kconfig
> > index 52c9af08ad35..99f99cbd94cc 100644
> > --- a/security/Kconfig
> > +++ b/security/Kconfig
> > @@ -194,6 +194,7 @@ source "security/yama/Kconfig"
> >  source "security/safesetid/Kconfig"
> >  source "security/lockdown/Kconfig"
> >  source "security/landlock/Kconfig"
> > +source "security/digest_cache/Kconfig"
> >  
> >  source "security/integrity/Kconfig"
> >  
> > @@ -233,11 +234,11 @@ endchoice
> >  
> >  config LSM
> >  	string "Ordered list of enabled LSMs"
> > -	default "landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
> > -	default "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
> > -	default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
> > -	default "landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
> > -	default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
> > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
> > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
> > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
> > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
> > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
> >  	help
> >  	  A comma-separated list of LSMs, in initialization order.
> >  	  Any LSMs left off this list, except for those with order
> > diff --git a/security/Makefile b/security/Makefile
> > index 59f238490665..e9b43e7b715a 100644
> > --- a/security/Makefile
> > +++ b/security/Makefile
> > @@ -25,6 +25,7 @@ obj-$(CONFIG_SECURITY_LOCKDOWN_LSM)	+= lockdown/
> >  obj-$(CONFIG_CGROUPS)			+= device_cgroup.o
> >  obj-$(CONFIG_BPF_LSM)			+= bpf/
> >  obj-$(CONFIG_SECURITY_LANDLOCK)		+= landlock/
> > +obj-$(CONFIG_SECURITY_DIGEST_CACHE)	+= digest_cache/
> >  
> >  # Object integrity file lists
> >  obj-$(CONFIG_INTEGRITY)			+= integrity/
> > diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
> > new file mode 100644
> > index 000000000000..e53fbf0779d6
> > --- /dev/null
> > +++ b/security/digest_cache/Kconfig
> > @@ -0,0 +1,16 @@
> > +# SPDX-License-Identifier: GPL-2.0
> > +config SECURITY_DIGEST_CACHE
> > +	bool "Digest_cache LSM"
> > +	default n
> > +	help
> > +	  This option enables an LSM maintaining a cache of digests
> > +	  (e.g. of file data or metadata).
> > +
> > +	  This LSM can support other kernel components in making access
> > +	  control decisions.
> > +
> > +config DIGEST_LIST_DEFAULT_PATH
> > +	string
> > +	default "/etc/digest_lists"
> > +	help
> > +	  Default directory where digest_cache LSM expects to find digest lists.
> > diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
> > new file mode 100644
> > index 000000000000..48848c41253e
> > --- /dev/null
> > +++ b/security/digest_cache/Makefile
> > @@ -0,0 +1,7 @@
> > +# SPDX-License-Identifier: GPL-2.0
> > +#
> > +# Makefile for building the digest_cache LSM.
> > +
> > +obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
> > +
> > +digest_cache-y := main.o
> > diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> > new file mode 100644
> > index 000000000000..5f04844af3a5
> > --- /dev/null
> > +++ b/security/digest_cache/internal.h
> > @@ -0,0 +1,86 @@
> > +/* SPDX-License-Identifier: GPL-2.0 */
> > +/*
> > + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
> > + *
> > + * Author: Roberto Sassu <roberto.sassu@huawei.com>
> 
> ditto
> 
> > + *
> > + * Internal header of the digest_cache LSM.
> > + */
> > +
> > +#ifndef _DIGEST_CACHE_INTERNAL_H
> > +#define _DIGEST_CACHE_INTERNAL_H
> > +
> > +#include <linux/lsm_hooks.h>
> > +#include <linux/digest_cache.h>
> > +
> > +/* Digest cache bits in flags. */
> > +#define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
> > +
> > +/**
> > + * struct digest_cache - Digest cache
> > + * @ref_count: Number of references to the digest cache
> > + * @path_str: Path of the digest list the digest cache was created from
> > + * @flags: Control flags
> > + *
> > + * This structure represents a cache of digests extracted from a digest list.
> > + */
> > +struct digest_cache {
> > +	atomic_t ref_count;
> > +	char *path_str;
> > +	unsigned long flags;
> > +};
> > +
> > +/**
> > + * struct digest_cache_security - Digest cache pointers in inode security blob
> > + * @dig_owner: Digest cache created from this inode
> > + * @dig_owner_mutex: Protects @dig_owner
> > + * @dig_user: Digest cache requested for this inode
> > + * @dig_user_mutex: Protects @dig_user
> > + *
> > + * This structure contains references to digest caches, protected by their
> > + * respective mutex.
> > + */
> > +struct digest_cache_security {
> > +	struct digest_cache *dig_owner;
> > +	struct mutex dig_owner_mutex;
> > +	struct digest_cache *dig_user;
> > +	struct mutex dig_user_mutex;
> > +};
> > +
> > +extern struct lsm_blob_sizes digest_cache_blob_sizes;
> > +extern char *default_path_str;
> > +
> > +static inline struct digest_cache_security *
> > +digest_cache_get_security(const struct inode *inode)
> > +{
> > +	if (unlikely(!inode->i_security))
> > +		return NULL;
> > +
> > +	return inode->i_security + digest_cache_blob_sizes.lbs_inode;
> > +}
> > +
> > +static inline struct digest_cache *
> > +digest_cache_ref(struct digest_cache *digest_cache)
> > +{
> > +	atomic_inc(&digest_cache->ref_count);
> > +	pr_debug("Ref (+) digest cache %s (ref count: %d)\n",
> > +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
> > +	return digest_cache;
> > +}
> > +
> > +static inline struct digest_cache *
> > +digest_cache_unref(struct digest_cache *digest_cache)
> > +{
> > +	bool ref_is_zero = atomic_dec_and_test(&digest_cache->ref_count);
> > +
> > +	pr_debug("Ref (-) digest cache %s (ref count: %d)\n",
> > +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
> > +	return (ref_is_zero) ? digest_cache : NULL;
> > +}
> > +
> > +/* main.c */
> > +struct digest_cache *digest_cache_create(struct dentry *dentry,
> > +					 struct path *digest_list_path,
> > +					 char *path_str, char *filename);
> > +
> > +#endif /* _DIGEST_CACHE_INTERNAL_H */
> > diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
> > new file mode 100644
> > index 000000000000..14dba8915e99
> > --- /dev/null
> > +++ b/security/digest_cache/main.c
> > @@ -0,0 +1,404 @@
> > +// 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 digest_cache LSM.
> > + */
> > +
> > +#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 = 1;
> > +static struct kmem_cache *digest_cache_cache __read_mostly;
> > +
> > +char *default_path_str = CONFIG_DIGEST_LIST_DEFAULT_PATH;
> > +
> > +/**
> > + * digest_cache_alloc_init - Allocate and initialize a new digest cache
> > + * @path_str: Path string of the digest list
> > + * @filename: Digest list file name (can be an empty string)
> > + *
> > + * This function allocates and initializes a new digest cache.
> > + *
> > + * Return: A digest_cache structure on success, NULL on error.
> > + */
> > +static struct digest_cache *digest_cache_alloc_init(char *path_str,
> > +						    char *filename)
> > +{
> > +	struct digest_cache *digest_cache;
> > +
> > +	digest_cache = kmem_cache_alloc(digest_cache_cache, GFP_KERNEL);
> > +	if (!digest_cache)
> > +		return digest_cache;
> > +
> > +	digest_cache->path_str = kasprintf(GFP_KERNEL, "%s%s%s", path_str,
> > +					   filename[0] ? "/" : "", filename);
> > +	if (!digest_cache->path_str) {
> > +		kmem_cache_free(digest_cache_cache, digest_cache);
> > +		return NULL;
> > +	}
> > +
> > +	atomic_set(&digest_cache->ref_count, 1);
> > +	digest_cache->flags = 0UL;
> > +
> > +	pr_debug("New digest cache %s (ref count: %d)\n",
> > +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
> 
> Nit: kretprobe can be used to grab the same information easil and
> do e.g. statistics and stuff like that. Traces for return values
> are not very useful.

I like to do dynamic debug, and I can control what part I want to
debug. All these messages allowed me to debug efficiently when there
was any issue. Would be a pity to remove them.

Thanks

Roberto

> > +
> > +	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);
> 
> ditto for the above trace
> 
> > +	kfree(digest_cache->path_str);
> > +	kmem_cache_free(digest_cache_cache, digest_cache);
> > +}
> > +
> > +/**
> > + * digest_cache_create - Create a digest cache
> > + * @dentry: Dentry of the inode for which the digest cache will be used
> > + * @digest_list_path: Path structure of the digest list
> > + * @path_str: Path string of the digest list
> > + * @filename: Digest list file name (can be an empty string)
> > + *
> > + * This function first locates, from the passed path, the digest list inode
> > + * from which the digest cache will be created or retrieved (if it already
> > + * exists).
> > + *
> > + * If dig_owner is NULL in the inode security blob, this function creates a
> > + * new digest cache with reference count set to 1 (reference returned), sets
> > + * it to dig_owner and consequently increments again the digest cache reference
> > + * count.
> > + *
> > + * Otherwise, it simply increments the reference count of the existing
> > + * dig_owner, since that reference is returned to the caller.
> > + *
> > + * Incrementing the reference count twice before calling path_put() ensures
> > + * that the digest cache returned is valid even if the inode is evicted from
> > + * memory (which decreases the reference count).
> > + *
> > + * Releasing the dig_owner_mutex lock does not mean that the digest cache is
> > + * ready for use. digest_cache_create() callers that found a partially
> > + * instantiated digest cache have to wait until the INIT_IN_PROGRESS bit is
> > + * cleared by the caller that is actually creating that digest cache.
> > + *
> > + * Return: A new digest cache on success, NULL on error.
> > + */
> > +struct digest_cache *digest_cache_create(struct dentry *dentry,
> > +					 struct path *digest_list_path,
> > +					 char *path_str, char *filename)
> > +{
> > +	struct path file_path;
> > +	struct digest_cache *digest_cache = NULL;
> > +	struct digest_cache_security *dig_sec;
> > +	struct inode *inode = d_backing_inode(digest_list_path->dentry);
> > +	bool dig_owner_exists = false;
> > +	int ret;
> > +
> > +	if (S_ISDIR(d_backing_inode(digest_list_path->dentry)->i_mode) &&
> > +	    filename[0]) {
> > +		ret = vfs_path_lookup(digest_list_path->dentry,
> > +				      digest_list_path->mnt, filename, 0,
> > +				      &file_path);
> > +		if (ret < 0) {
> > +			pr_debug("Cannot find digest list %s/%s\n", path_str,
> > +				 filename);
> > +			return NULL;
> > +		}
> > +
> > +		digest_list_path = &file_path;
> > +		inode = d_backing_inode(file_path.dentry);
> > +
> > +		/*
> > +		 * Cannot request a digest cache for the same inode the
> > +		 * digest cache is populated from.
> > +		 */
> > +		if (d_backing_inode(dentry) == inode) {
> > +			pr_debug("Cannot request a digest cache for %s and use it as digest list\n",
> > +				 dentry->d_name.name);
> > +			goto out;
> > +		}
> > +
> > +		/* 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",
> > +				 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);
> > +		dig_owner_exists = true;
> > +		mutex_unlock(&dig_sec->dig_owner_mutex);
> > +		goto exists;
> > +	}
> > +
> > +	/* Ref. count is already 1 for this reference. */
> > +	digest_cache = digest_cache_alloc_init(path_str, filename);
> > +	if (!digest_cache) {
> > +		mutex_unlock(&dig_sec->dig_owner_mutex);
> > +		goto out;
> > +	}
> > +
> > +	/* Increment ref. count for reference set to dig_owner. */
> > +	dig_sec->dig_owner = digest_cache_ref(digest_cache);
> > +
> > +	/* Make the other lock contenders wait until creation complete. */
> > +	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
> > +	mutex_unlock(&dig_sec->dig_owner_mutex);
> > +
> > +	/* Creation complete, notify the other lock contenders. */
> > +	clear_and_wake_up_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
> > +exists:
> > +	if (dig_owner_exists)
> > +		/* Wait until creation complete. */
> > +		wait_on_bit(&dig_sec->dig_owner->flags, INIT_IN_PROGRESS,
> > +			    TASK_UNINTERRUPTIBLE);
> > +out:
> > +	if (digest_list_path == &file_path)
> > +		path_put(&file_path);
> > +
> > +	return digest_cache;
> > +}
> > +
> > +/**
> > + * digest_cache_new - Retrieve digest list file name and request digest cache
> > + * @dentry: Dentry of the inode for which the digest cache will be used
> > + *
> > + * This function locates the default path. If it is a file, it directly creates
> > + * a digest cache from it. Otherwise, it reads the digest list file name from
> > + * the security.digest_list xattr and requests the creation of a digest cache
> > + * with that file name. If security.digest_list is not found, this function
> > + * requests the creation of a digest cache on the parent directory.
> > + *
> > + * Return: A new digest cache on success, NULL on error.
> > + */
> > +static struct digest_cache *digest_cache_new(struct dentry *dentry)
> > +{
> > +	char filename[NAME_MAX + 1] = { 0 };
> > +	struct digest_cache *digest_cache = NULL;
> > +	struct path default_path;
> > +	int ret;
> > +
> > +	ret = kern_path(default_path_str, 0, &default_path);
> > +	if (ret < 0) {
> > +		pr_debug("Cannot find path %s\n", default_path_str);
> > +		return NULL;
> > +	}
> > +
> > +	/* The default path is a file, no need to get xattr. */
> > +	if (S_ISREG(d_backing_inode(default_path.dentry)->i_mode)) {
> > +		pr_debug("Default path %s is a file, not reading %s xattr\n",
> > +			 default_path_str, XATTR_NAME_DIGEST_LIST);
> > +		goto create;
> > +	} else if (!S_ISDIR(d_backing_inode(default_path.dentry)->i_mode)) {
> > +		pr_debug("Default path %s must be either a file or a directory\n",
> > +			 default_path_str);
> > +		goto out;
> > +	}
> > +
> > +	ret = vfs_getxattr(&nop_mnt_idmap, dentry, XATTR_NAME_DIGEST_LIST,
> > +			   filename, sizeof(filename) - 1);
> > +	if (ret <= 0) {
> > +		pr_debug("Digest list path not found for file %s, using %s\n",
> > +			 dentry->d_name.name, default_path_str);
> > +		goto create;
> > +	}
> > +
> > +	if (strchr(filename, '/')) {
> > +		pr_debug("%s xattr should contain only a file name, got: %s\n",
> > +			 XATTR_NAME_DIGEST_LIST, filename);
> > +		goto out;
> > +	}
> > +
> > +	pr_debug("Found %s xattr in %s, default path: %s, digest list: %s\n",
> > +		 XATTR_NAME_DIGEST_LIST, dentry->d_name.name, default_path_str,
> > +		 filename);
> > +create:
> > +	digest_cache = digest_cache_create(dentry, &default_path,
> > +					   default_path_str, filename);
> > +out:
> > +	path_put(&default_path);
> > +	return digest_cache;
> > +}
> > +
> > +/**
> > + * digest_cache_get - Get a digest cache for a given inode
> > + * @dentry: Dentry of the inode for which the digest cache will be used
> > + *
> > + * This function tries to find a digest cache from the inode security blob of
> > + * the passed dentry (dig_user field). If a digest cache was not found, it calls
> > + * digest_cache_new() to create a new one. In both cases, it increments the
> > + * digest cache reference count before returning the reference to the caller.
> > + *
> > + * The caller is responsible to call digest_cache_put() to release the digest
> > + * cache reference returned.
> > + *
> > + * Lock dig_user_mutex to protect against concurrent requests to obtain a digest
> > + * cache for the same inode, and to make other contenders wait until the first
> > + * requester finishes the process.
> > + *
> > + * Return: A digest cache on success, NULL otherwise.
> > + */
> > +struct digest_cache *digest_cache_get(struct dentry *dentry)
> > +{
> > +	struct digest_cache_security *dig_sec;
> > +	struct digest_cache *digest_cache = NULL;
> > +	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);
> > +
> > +struct lsm_blob_sizes digest_cache_blob_sizes __ro_after_init = {
> > +	.lbs_inode = sizeof(struct digest_cache_security),
> > +};
> > +
> > +/**
> > + * digest_cache_inode_alloc_security - Initialize inode security blob
> > + * @inode: Inode for which the security blob is initialized
> > + *
> > + * This function initializes the digest_cache_security structure, directly
> > + * stored in the inode security blob.
> > + *
> > + * Return: Zero.
> > + */
> > +static int digest_cache_inode_alloc_security(struct inode *inode)
> > +{
> > +	struct digest_cache_security *dig_sec;
> > +
> > +	/* The inode security blob is always allocated here. */
> > +	dig_sec = digest_cache_get_security(inode);
> > +	mutex_init(&dig_sec->dig_owner_mutex);
> > +	mutex_init(&dig_sec->dig_user_mutex);
> > +	return 0;
> > +}
> > +
> > +/**
> > + * digest_cache_inode_free_security - Release the digest cache references
> > + * @inode: Inode for which the digest cache references are released
> > + *
> > + * Since the inode is being evicted, this function releases the non-needed
> > + * references to the digest caches stored in the digest_cache_security
> > + * structure.
> > + */
> > +static void digest_cache_inode_free_security(struct inode *inode)
> > +{
> > +	struct digest_cache_security *dig_sec;
> > +
> > +	dig_sec = digest_cache_get_security(inode);
> > +	if (!dig_sec)
> > +		return;
> > +
> > +	mutex_destroy(&dig_sec->dig_owner_mutex);
> > +	mutex_destroy(&dig_sec->dig_user_mutex);
> > +	if (dig_sec->dig_owner)
> > +		digest_cache_put(dig_sec->dig_owner);
> > +	if (dig_sec->dig_user)
> > +		digest_cache_put(dig_sec->dig_user);
> > +}
> > +
> > +static struct security_hook_list digest_cache_hooks[] __ro_after_init = {
> > +	LSM_HOOK_INIT(inode_alloc_security, digest_cache_inode_alloc_security),
> > +	LSM_HOOK_INIT(inode_free_security, digest_cache_inode_free_security),
> > +};
> > +
> > +/**
> > + * digest_cache_init_once - Initialize the digest cache structure
> > + * @foo: Digest cache structure to initialize
> > + *
> > + * This function fills the digest cache structure with zeros.
> > + */
> > +static void digest_cache_init_once(void *foo)
> > +{
> > +	struct digest_cache *digest_cache = (struct digest_cache *)foo;
> > +
> > +	memset(digest_cache, 0, sizeof(*digest_cache));
> > +}
> > +
> > +static const struct lsm_id digest_cache_lsmid = {
> > +	.name = "digest_cache",
> > +	.id = LSM_ID_DIGEST_CACHE,
> > +};
> > +
> > +/**
> > + * digest_cache_init - Initialize the digest_cache LSM
> > + *
> > + * Initialize the digest_cache LSM, by instantiating a cache for the
> > + * digest_cache structure and by registering the digest_cache LSM hooks.
> > + */
> > +static int __init digest_cache_init(void)
> > +{
> > +	digest_cache_cache = kmem_cache_create("digest_cache_cache",
> > +					       sizeof(struct digest_cache),
> > +					       0, SLAB_PANIC,
> > +					       digest_cache_init_once);
> > +
> > +	security_add_hooks(digest_cache_hooks, ARRAY_SIZE(digest_cache_hooks),
> > +			   &digest_cache_lsmid);
> > +	return 0;
> > +}
> > +
> > +DEFINE_LSM(digest_cache) = {
> > +	.name = "digest_cache",
> > +	.enabled = &digest_cache_enabled,
> > +	.init = digest_cache_init,
> > +	.blobs = &digest_cache_blob_sizes,
> > +};
> > diff --git a/security/security.c b/security/security.c
> > index cbdc9bebe802..cb084ed58617 100644
> > --- a/security/security.c
> > +++ b/security/security.c
> > @@ -50,7 +50,8 @@
> >  	(IS_ENABLED(CONFIG_BPF_LSM) ? 1 : 0) + \
> >  	(IS_ENABLED(CONFIG_SECURITY_LANDLOCK) ? 1 : 0) + \
> >  	(IS_ENABLED(CONFIG_IMA) ? 1 : 0) + \
> > -	(IS_ENABLED(CONFIG_EVM) ? 1 : 0))
> > +	(IS_ENABLED(CONFIG_EVM) ? 1 : 0) + \
> > +	(IS_ENABLED(CONFIG_SECURITY_DIGEST_CACHE) ? 1 : 0))
> >  
> >  /*
> >   * These are descriptions of the reasons that can be passed to the
> > diff --git a/tools/testing/selftests/lsm/lsm_list_modules_test.c b/tools/testing/selftests/lsm/lsm_list_modules_test.c
> > index 4d5d4cee2586..d00831edc582 100644
> > --- a/tools/testing/selftests/lsm/lsm_list_modules_test.c
> > +++ b/tools/testing/selftests/lsm/lsm_list_modules_test.c
> > @@ -128,6 +128,9 @@ TEST(correct_lsm_list_modules)
> >  		case LSM_ID_EVM:
> >  			name = "evm";
> >  			break;
> > +		case LSM_ID_DIGEST_CACHE:
> > +			name = "digest_cache";
> > +			break;
> >  		default:
> >  			name = "INVALID";
> >  			break;
> 
> BR, Jarkko


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 03/14] digest_cache: Add securityfs interface
  2024-04-15 19:32   ` Jarkko Sakkinen
@ 2024-04-16 10:15     ` Roberto Sassu
  2024-04-16 14:38       ` Jarkko Sakkinen
  0 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-16 10:15 UTC (permalink / raw)
  To: Jarkko Sakkinen, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On 4/15/2024 9:32 PM, Jarkko Sakkinen wrote:
> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
>> From: Roberto Sassu <roberto.sassu@huawei.com>
>>
>> Add the digest_cache_path file in securityfs, 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/digest_cache/Kconfig    |  4 ++
>>   security/digest_cache/Makefile   |  2 +-
>>   security/digest_cache/internal.h |  1 +
>>   security/digest_cache/main.c     | 10 +++-
>>   security/digest_cache/secfs.c    | 87 ++++++++++++++++++++++++++++++++
>>   5 files changed, 102 insertions(+), 2 deletions(-)
>>   create mode 100644 security/digest_cache/secfs.c
>>
>> diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
>> index e53fbf0779d6..dfabe5d6e3ca 100644
>> --- a/security/digest_cache/Kconfig
>> +++ b/security/digest_cache/Kconfig
>> @@ -14,3 +14,7 @@ config DIGEST_LIST_DEFAULT_PATH
>>   	default "/etc/digest_lists"
>>   	help
>>   	  Default directory where digest_cache LSM 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/digest_cache/Makefile b/security/digest_cache/Makefile
>> index 48848c41253e..1330655e33b1 100644
>> --- a/security/digest_cache/Makefile
>> +++ b/security/digest_cache/Makefile
>> @@ -4,4 +4,4 @@
>>   
>>   obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
>>   
>> -digest_cache-y := main.o
>> +digest_cache-y := main.o secfs.o
>> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
>> index 5f04844af3a5..bbf5eefe5c82 100644
>> --- a/security/digest_cache/internal.h
>> +++ b/security/digest_cache/internal.h
>> @@ -49,6 +49,7 @@ struct digest_cache_security {
>>   
>>   extern struct lsm_blob_sizes digest_cache_blob_sizes;
>>   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)
>> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
>> index 14dba8915e99..661c8d106791 100644
>> --- a/security/digest_cache/main.c
>> +++ b/security/digest_cache/main.c
>> @@ -18,6 +18,9 @@ static struct kmem_cache *digest_cache_cache __read_mostly;
>>   
>>   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
>> @@ -274,9 +277,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. */
>> @@ -386,6 +392,8 @@ static const struct lsm_id digest_cache_lsmid = {
>>    */
>>   static int __init digest_cache_init(void)
>>   {
>> +	init_rwsem(&default_path_sem);
>> +
>>   	digest_cache_cache = kmem_cache_create("digest_cache_cache",
>>   					       sizeof(struct digest_cache),
>>   					       0, SLAB_PANIC,
>> diff --git a/security/digest_cache/secfs.c b/security/digest_cache/secfs.c
>> new file mode 100644
>> index 000000000000..d3a37bf3588e
>> --- /dev/null
>> +++ b/security/digest_cache/secfs.c
>> @@ -0,0 +1,87 @@
>> +// SPDX-License-Identifier: GPL-2.0
>> +/*
>> + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
>> + *
>> + * Author: Roberto Sassu <roberto.sassu@huawei.com>
>> + *
>> + * Implement the securityfs interface of the digest_cache LSM.
>> + */
>> +
>> +#define pr_fmt(fmt) "DIGEST CACHE: "fmt
>> +#include <linux/security.h>
>> +
>> +#include "internal.h"
>> +
>> +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,
>> +};
>> +
>> +static int __init digest_cache_path_init(void)
>> +{
>> +	default_path_dentry = securityfs_create_file("digest_cache_path", 0660,
>> +						     NULL, NULL,
>> +						     &default_path_ops);
>> +	if (IS_ERR(default_path_dentry))
>> +		return -EFAULT;
> 
> Nit: when overwriting error value with another error value it would be
> best to document it with an inline comment. Otherwise, it is fine.

Seems to make sense to return the right error. Will check why this one 
(I probably took from somewhere).

Thanks

Roberto

>> +
>> +	return 0;
>> +}
>> +
>> +late_initcall(digest_cache_path_init);
> 
> 
> BR, Jarkko


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 04/14] digest_cache: Add hash tables and operations
  2024-04-15 19:36   ` Jarkko Sakkinen
@ 2024-04-16 10:28     ` Roberto Sassu
  0 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-16 10:28 UTC (permalink / raw)
  To: Jarkko Sakkinen, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On 4/15/2024 9:36 PM, Jarkko Sakkinen wrote:
> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
>> 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 digest_cache LSM free the hash
>> tables at the time a digest cache is freed.
>>
>> Add digest_cache_htable_lookup() to search a digest in the hash table of a
>> digest cache for a given algorithm.
>>
>> Add digest_cache_lookup() to the public API, to let users of the
>> digest_cache LSM search a digest in a digest cache and, in a subsequent
>> patch, to search it in the digest caches for each directory entry.
>>
>> Return the digest cache containing the digest, as a different type,
>> digest_cache_found_t to avoid it being accidentally put. Also, introduce
>> digest_cache_from_found_t() to explicitly convert it back to a digest cache
>> for further use (e.g. retrieving verification data, introduced later).
>>
>> Finally, add digest_cache_hash_key() to compute the hash table key from the
>> first two bytes of the digest (modulo the number of slots).
>>
>> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
>> ---
>>   include/linux/digest_cache.h     |  34 +++++
>>   security/digest_cache/Kconfig    |  11 ++
>>   security/digest_cache/Makefile   |   2 +-
>>   security/digest_cache/htable.c   | 250 +++++++++++++++++++++++++++++++
>>   security/digest_cache/internal.h |  43 ++++++
>>   security/digest_cache/main.c     |   3 +
>>   6 files changed, 342 insertions(+), 1 deletion(-)
>>   create mode 100644 security/digest_cache/htable.c
>>
>> diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
>> index e79f94a60b0f..4872700ac205 100644
>> --- a/include/linux/digest_cache.h
>> +++ b/include/linux/digest_cache.h
>> @@ -11,12 +11,39 @@
>>   #define _LINUX_DIGEST_CACHE_H
>>   
>>   #include <linux/fs.h>
>> +#include <crypto/hash_info.h>
>>   
>>   struct digest_cache;
>>   
>> +/**
>> + * typedef digest_cache_found_t - Digest cache reference as numeric value
>> + *
>> + * This new type represents a digest cache reference that should not be put.
>> + */
>> +typedef unsigned long digest_cache_found_t;
>> +
>> +/**
>> + * digest_cache_from_found_t - Convert digest_cache_found_t to digest cache ptr
>> + * @found: digest_cache_found_t value
>> + *
>> + * Convert the digest_cache_found_t returned by digest_cache_lookup() to a
>> + * digest cache pointer, so that it can be passed to the other functions of the
>> + * API.
>> + *
>> + * Return: Digest cache pointer.
>> + */
>> +static inline struct digest_cache *
>> +digest_cache_from_found_t(digest_cache_found_t found)
>> +{
>> +	return (struct digest_cache *)found;
>> +}
>> +
>>   #ifdef CONFIG_SECURITY_DIGEST_CACHE
>>   struct digest_cache *digest_cache_get(struct dentry *dentry);
>>   void digest_cache_put(struct digest_cache *digest_cache);
>> +digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
>> +					 struct digest_cache *digest_cache,
>> +					 u8 *digest, enum hash_algo algo);
>>   
>>   #else
>>   static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
>> @@ -28,5 +55,12 @@ static inline void digest_cache_put(struct digest_cache *digest_cache)
>>   {
>>   }
>>   
>> +static inline digest_cache_found_t
>> +digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache,
>> +		    u8 *digest, enum hash_algo algo)
>> +{
>> +	return 0UL;
>> +}
>> +
>>   #endif /* CONFIG_SECURITY_DIGEST_CACHE */
>>   #endif /* _LINUX_DIGEST_CACHE_H */
>> diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
>> index dfabe5d6e3ca..71017954e5c5 100644
>> --- a/security/digest_cache/Kconfig
>> +++ b/security/digest_cache/Kconfig
>> @@ -18,3 +18,14 @@ config DIGEST_LIST_DEFAULT_PATH
>>   	  It can be changed at run-time, by writing the new path to the
>>   	  securityfs interface. Digest caches created with the old path are
>>   	  not affected by the change.
>> +
>> +config DIGEST_CACHE_HTABLE_DEPTH
>> +	int
>> +	default 30
>> +	help
>> +	  Desired average depth of the collision list in the digest cache
>> +	  hash tables.
>> +
>> +	  A smaller number will increase the amount of hash table slots, and
>> +	  make the search faster. A bigger number will decrease the number of
>> +	  hash table slots, but make the search slower.
>> diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
>> index 1330655e33b1..7e00c53d8f55 100644
>> --- a/security/digest_cache/Makefile
>> +++ b/security/digest_cache/Makefile
>> @@ -4,4 +4,4 @@
>>   
>>   obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
>>   
>> -digest_cache-y := main.o secfs.o
>> +digest_cache-y := main.o secfs.o htable.o
>> diff --git a/security/digest_cache/htable.c b/security/digest_cache/htable.c
>> new file mode 100644
>> index 000000000000..d2d5d8f5e5b1
>> --- /dev/null
>> +++ b/security/digest_cache/htable.c
>> @@ -0,0 +1,250 @@
>> +// SPDX-License-Identifier: GPL-2.0
>> +/*
>> + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
>> + *
>> + * Author: Roberto Sassu <roberto.sassu@huawei.com>
>> + *
>> + * Implement hash table operations for the digest cache.
>> + */
>> +
>> +#define pr_fmt(fmt) "DIGEST CACHE: "fmt
> 
> For easier grepping from kernel tree i'd suggest to name this accordingly i.e.
> just "digest_cache".

Ok, no problem.

>> +#include "internal.h"
>> +
>> +/**
>> + * digest_cache_hash_key - Compute hash key
>> + * @digest: Digest cache
>> + * @num_slots: Number of slots in the hash table
>> + *
>> + * This function computes a hash key based on the first two bytes of the
>> + * digest and the number of slots of the hash table.
>> + *
>> + * Return: Hash key.
>> + */
>> +static inline unsigned int digest_cache_hash_key(u8 *digest,
>> +						 unsigned int num_slots)
>> +{
>> +	/* Same as ima_hash_key() but parametrized. */
>> +	return (digest[0] | digest[1] << 8) % num_slots;
>> +}
>> +
>> +/**
>> + * lookup_htable - Lookup a hash table
>> + * @digest_cache: Digest cache
>> + * @algo: Algorithm of the desired hash table
>> + *
>> + * This function searches the hash table for a given algorithm in the digest
>> + * cache.
>> + *
>> + * Return: A hash table if found, NULL otherwise.
>> + */
>> +static struct htable *lookup_htable(struct digest_cache *digest_cache,
>> +				    enum hash_algo algo)
>> +{
>> +	struct htable *h;
>> +
>> +	list_for_each_entry(h, &digest_cache->htables, next)
>> +		if (h->algo == algo)
>> +			return h;
>> +
>> +	return NULL;
>> +}
>> +
>> +/**
>> + * digest_cache_htable_init - Allocate and initialize the hash table
>> + * @digest_cache: Digest cache
>> + * @num_digests: Number of digests to add to the digest cache
>> + * @algo: Algorithm of the digests
>> + *
>> + * This function allocates and initializes the hash table for a given algorithm.
>> + * The number of slots depends on the number of digests to add to the digest
>> + * cache, and the constant CONFIG_DIGEST_CACHE_HTABLE_DEPTH stating the desired
>> + * average depth of the collision list.
>> + *
>> + * Return: Zero on success, a POSIX error code otherwise.
>> + */
>> +int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
>> +			     enum hash_algo algo)
>> +{
>> +	struct htable *h;
>> +	int i;
>> +
>> +	h = lookup_htable(digest_cache, algo);
>> +	if (h)
>> +		return 0;
>> +
>> +	h = kmalloc(sizeof(*h), GFP_KERNEL);
>> +	if (!h)
>> +		return -ENOMEM;
>> +
>> +	h->num_slots = DIV_ROUND_UP(num_digests,
>> +				    CONFIG_DIGEST_CACHE_HTABLE_DEPTH);
>> +	h->slots = kmalloc_array(h->num_slots, sizeof(*h->slots), GFP_KERNEL);
>> +	if (!h->slots) {
>> +		kfree(h);
>> +		return -ENOMEM;
>> +	}
>> +
>> +	for (i = 0; i < h->num_slots; i++)
>> +		INIT_HLIST_HEAD(&h->slots[i]);
>> +
>> +	h->num_digests = 0;
>> +	h->algo = algo;
>> +
>> +	list_add_tail(&h->next, &digest_cache->htables);
>> +
>> +	pr_debug("Initialized hash table for digest list %s, digests: %llu, slots: %u, algo: %s\n",
>> +		 digest_cache->path_str, num_digests, h->num_slots,
>> +		 hash_algo_name[algo]);
>> +	return 0;
>> +}
>> +
>> +/**
>> + * digest_cache_htable_add - Add a new digest to the digest cache
>> + * @digest_cache: Digest cache
>> + * @digest: Digest to add
>> + * @algo: Algorithm of digest
>> + *
>> + * This function, invoked by a digest list parser, adds a digest extracted
>> + * from a digest list to the digest cache.
>> + *
>> + * Return: Zero on success, a POSIX error code otherwise.
>> + */
>> +int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest,
>> +			    enum hash_algo algo)
>> +{
>> +	struct htable *h;
>> +	struct digest_cache_entry *entry;
>> +	unsigned int key;
>> +	int digest_len;
>> +
>> +	h = lookup_htable(digest_cache, algo);
>> +	if (!h) {
>> +		pr_debug("No hash table for algorithm %s was found in digest cache %s, initialize one\n",
>> +			 hash_algo_name[algo], digest_cache->path_str);
>> +		return -ENOENT;
>> +	}
>> +
>> +	digest_len = hash_digest_size[algo];
>> +
>> +	entry = kmalloc(sizeof(*entry) + digest_len, GFP_KERNEL);
>> +	if (!entry)
>> +		return -ENOMEM;
>> +
>> +	memcpy(entry->digest, digest, digest_len);
>> +
>> +	key = digest_cache_hash_key(digest, h->num_slots);
>> +	hlist_add_head(&entry->hnext, &h->slots[key]);
>> +	h->num_digests++;
>> +	pr_debug("Added digest %s:%*phN to digest cache %s, num of %s digests: %llu\n",
>> +		 hash_algo_name[algo], digest_len, digest,
>> +		 digest_cache->path_str, hash_algo_name[algo], h->num_digests);
>> +	return 0;
>> +}
>> +
>> +/**
>> + * digest_cache_htable_lookup - Search a digest in the digest cache
>> + * @dentry: Dentry of the file whose digest is looked up
>> + * @digest_cache: Digest cache
>> + * @digest: Digest to search
>> + * @algo: Algorithm of the digest to search
>> + *
>> + * This function searches the passed digest and algorithm in the passed digest
>> + * cache.
>> + *
>> + * Return: Zero if the digest is found, -ENOENT if not.
>> + */
>> +int digest_cache_htable_lookup(struct dentry *dentry,
>> +			       struct digest_cache *digest_cache, u8 *digest,
>> +			       enum hash_algo algo)
>> +{
>> +	struct digest_cache_entry *entry;
>> +	struct htable *h;
>> +	unsigned int key;
>> +	int digest_len;
>> +	int search_depth = 0;
>> +
>> +	h = lookup_htable(digest_cache, algo);
>> +	if (!h)
>> +		return -ENOENT;
>> +
>> +	digest_len = hash_digest_size[algo];
>> +	key = digest_cache_hash_key(digest, h->num_slots);
>> +
>> +	hlist_for_each_entry(entry, &h->slots[key], hnext) {
>> +		if (!memcmp(entry->digest, digest, digest_len)) {
>> +			pr_debug("Cache hit at depth %d for file %s, digest %s:%*phN in digest cache %s\n",
>> +				 search_depth, dentry->d_name.name,
>> +				 hash_algo_name[algo], digest_len, digest,
>> +				 digest_cache->path_str);
>> +
>> +			return 0;
>> +		}
>> +
>> +		search_depth++;
>> +	}
>> +
>> +	pr_debug("Cache miss for file %s, digest %s:%*phN in digest cache %s\n",
>> +		 dentry->d_name.name, hash_algo_name[algo], digest_len, digest,
>> +		 digest_cache->path_str);
>> +	return -ENOENT;
>> +}
>> +
>> +/**
>> + * digest_cache_lookup - Search a digest in the digest cache
>> + * @dentry: Dentry of the file whose digest is looked up
>> + * @digest_cache: Digest cache
>> + * @digest: Digest to search
>> + * @algo: Algorithm of the digest to search
>> + *
>> + * This function calls digest_cache_htable_lookup() to search a digest in the
>> + * passed digest cache, obtained with digest_cache_get().
>> + *
>> + * It returns the digest cache reference as the digest_cache_found_t type, to
>> + * avoid that the digest cache is accidentally put. The digest_cache_found_t
>> + * type can be converted back to a digest cache pointer, by
>> + * calling digest_cache_from_found_t().
>> + *
>> + * Return: A positive digest_cache_found_t if the digest is found, zero if not.
>> + */
>> +digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
>> +					 struct digest_cache *digest_cache,
>> +					 u8 *digest, enum hash_algo algo)
>> +{
>> +	int ret;
>> +
>> +	ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo);
>> +	return (!ret) ? (digest_cache_found_t)digest_cache : 0UL;
>> +}
>> +EXPORT_SYMBOL_GPL(digest_cache_lookup);
>> +
>> +/**
>> + * digest_cache_htable_free - Free the hash tables
>> + * @digest_cache: Digest cache
>> + *
>> + * This function removes all digests from all hash tables in the digest cache,
>> + * and frees the memory.
>> + */
>> +void digest_cache_htable_free(struct digest_cache *digest_cache)
>> +{
>> +	struct htable *h, *h_tmp;
>> +	struct digest_cache_entry *p;
>> +	struct hlist_node *q;
>> +	int i;
>> +
>> +	list_for_each_entry_safe(h, h_tmp, &digest_cache->htables, next) {
>> +		for (i = 0; i < h->num_slots; i++) {
>> +			hlist_for_each_entry_safe(p, q, &h->slots[i], hnext) {
>> +				hlist_del(&p->hnext);
>> +				pr_debug("Removed digest %s:%*phN from digest cache %s\n",
>> +					 hash_algo_name[h->algo],
>> +					 hash_digest_size[h->algo], p->digest,
>> +					 digest_cache->path_str);
>> +				kfree(p);
>> +			}
>> +		}
>> +
>> +		list_del(&h->next);
>> +		kfree(h->slots);
>> +		kfree(h);
>> +	}
>> +}
>> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
>> index bbf5eefe5c82..f6ffeaa25288 100644
>> --- a/security/digest_cache/internal.h
>> +++ b/security/digest_cache/internal.h
>> @@ -16,8 +16,40 @@
>>   /* Digest cache bits in flags. */
>>   #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
>>   
>> +/**
>> + * struct digest_cache_entry - Entry of a digest cache hash table
>> + * @hnext: Pointer to the next element in the collision list
>> + * @digest: Stored digest
>> + *
>> + * This structure represents an entry of a digest cache hash table, storing a
>> + * digest.
>> + */
>> +struct digest_cache_entry {
>> +	struct hlist_node hnext;
>> +	u8 digest[];
>> +} __packed;
> 
> Please correct me if I'm wrong but I don't think __packed has any use
> here as the definition of hlist_node is:
> 
> 
> struct hlist_node {
> 	struct hlist_node *next, **pprev;
> };
> 
> It is naturally aligned to begin with.

You're right. __packed is not needed (no reordering).

Thanks

Roberto

>> +
>> +/**
>> + * 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
>> @@ -25,6 +57,7 @@
>>    * This structure represents a cache of digests extracted from a digest list.
>>    */
>>   struct digest_cache {
>> +	struct list_head htables;
>>   	atomic_t ref_count;
>>   	char *path_str;
>>   	unsigned long flags;
>> @@ -84,4 +117,14 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>>   					 struct path *digest_list_path,
>>   					 char *path_str, char *filename);
>>   
>> +/* htable.c */
>> +int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
>> +			     enum hash_algo algo);
>> +int digest_cache_htable_add(struct digest_cache *digest_cache, u8 *digest,
>> +			    enum hash_algo algo);
>> +int digest_cache_htable_lookup(struct dentry *dentry,
>> +			       struct digest_cache *digest_cache, u8 *digest,
>> +			       enum hash_algo algo);
>> +void digest_cache_htable_free(struct digest_cache *digest_cache);
>> +
>>   #endif /* _DIGEST_CACHE_INTERNAL_H */
>> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
>> index 661c8d106791..0b201af6432c 100644
>> --- a/security/digest_cache/main.c
>> +++ b/security/digest_cache/main.c
>> @@ -48,6 +48,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
>>   
>>   	atomic_set(&digest_cache->ref_count, 1);
>>   	digest_cache->flags = 0UL;
>> +	INIT_LIST_HEAD(&digest_cache->htables);
>>   
>>   	pr_debug("New digest cache %s (ref count: %d)\n",
>>   		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
>> @@ -63,6 +64,8 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
>>    */
>>   static void digest_cache_free(struct digest_cache *digest_cache)
>>   {
>> +	digest_cache_htable_free(digest_cache);
>> +
>>   	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
>>   	kfree(digest_cache->path_str);
>>   	kmem_cache_free(digest_cache_cache, digest_cache);
> 
> 
> BR, Jarkko


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 09/14] digest_cache: Add support for directories
  2024-04-15 19:39   ` Jarkko Sakkinen
@ 2024-04-16 10:30     ` Roberto Sassu
  0 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-16 10:30 UTC (permalink / raw)
  To: Jarkko Sakkinen, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On 4/15/2024 9:39 PM, Jarkko Sakkinen wrote:
> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
>> From: Roberto Sassu <roberto.sassu@huawei.com>
>>
>> In the environments where xattrs are not available (e.g. in the initial ram
>> disk), the digest_cache LSM cannot precisely determine which digest list in
>> a directory contains the desired reference digest. However, although
>> slower, it would be desirable to search the digest in all digest lists of
>> that directory.
>>
>> This done in two steps. When a digest cache is being created,
>> digest_cache_create() invokes digest_cache_dir_create(), to generate the
>> list of current directory entries. Entries are placed in the list in
>> ascending order by the <seq num> if prepended to the file name, or at the
>> end of the list if not.
>>
>> The resulting digest cache has the IS_DIR bit set, to distinguish it from
>> the digest caches created from regular files.
>>
>> Second, when a digest is searched in a directory digest cache,
>> digest_cache_lookup() invokes digest_cache_dir_lookup_digest() to
>> iteratively search that digest in each directory entry generated by
>> digest_cache_dir_create().
>>
>> That list is stable, even if new files are added or deleted from that
>> directory. A subsequent patch will invalidate the digest cache, forcing
>> next callers of digest_cache_get() to get a new directory digest cache with
>> the updated list of directory entries.
>>
>> If the current directory entry does not have a digest cache reference,
>> digest_cache_dir_lookup_digest() invokes digest_cache_create() to create a
>> new digest cache for that entry. In either case,
>> digest_cache_dir_lookup_digest() calls then digest_cache_htable_lookup()
>> with the new/existing digest cache to search the digest. Check and
>> assignment of the digest cache in a directory entry is protected by the
>> per entry digest_cache_mutex.
>>
>> The iteration stops when the digest is found. In that case,
>> digest_cache_dir_lookup_digest() returns the digest cache reference of the
>> current directory entry as the digest_cache_found_t type, so that callers
>> of digest_cache_lookup() don't mistakenly try to call digest_cache_put()
>> with that reference.
>>
>> This new reference type will be used to retrieve information about the
>> digest cache containing the digest, which is not known in advance until the
>> digest search is performed.
>>
>> The order of the list of directory entries influences the speed of the
>> digest search. A search terminates faster if less digest caches have to be
>> created. One way to optimize it could be to order the list of digest lists
>> in the same way of when they are requested at boot.
>>
>> Finally, digest_cache_dir_free() releases the digest cache references
>> stored in the list of directory entries, and frees the list itself.
>>
>> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
>> ---
>>   security/digest_cache/Makefile   |   2 +-
>>   security/digest_cache/dir.c      | 193 +++++++++++++++++++++++++++++++
>>   security/digest_cache/htable.c   |  22 +++-
>>   security/digest_cache/internal.h |  45 +++++++
>>   security/digest_cache/main.c     |  12 ++
>>   5 files changed, 271 insertions(+), 3 deletions(-)
>>   create mode 100644 security/digest_cache/dir.c
>>
>> diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
>> index 37a473c7bc28..e417da0383ab 100644
>> --- a/security/digest_cache/Makefile
>> +++ b/security/digest_cache/Makefile
>> @@ -4,7 +4,7 @@
>>   
>>   obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
>>   
>> -digest_cache-y := main.o secfs.o htable.o populate.o modsig.o 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/digest_cache/dir.c b/security/digest_cache/dir.c
>> new file mode 100644
>> index 000000000000..7bfcdd5f7ef1
>> --- /dev/null
>> +++ b/security/digest_cache/dir.c
>> @@ -0,0 +1,193 @@
>> +// 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 "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_create - Create a directory digest cache
>> + * @digest_cache: 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_create(struct digest_cache *digest_cache,
>> +			    struct path *digest_list_path)
>> +{
>> +	struct file *dir_file;
>> +	struct readdir_callback buf = {
>> +		.ctx.actor = digest_cache_dir_iter,
>> +	};
>> +	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);
>> +	}
>> +
>> +	buf.head = &digest_cache->dir_entries;
>> +	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_lookup_digest - Lookup a digest
>> + * @dentry: Dentry of the file whose digest is looked up
>> + * @digest_list_path: Path structure of the digest list directory
>> + * @digest_cache: 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_create() and looks up the digest in the digest cache of
>> + * each entry.
>> + *
>> + * Return: A digest_cache_found_t value if the digest if found, zero otherwise.
>> + */
>> +digest_cache_found_t
>> +digest_cache_dir_lookup_digest(struct dentry *dentry,
>> +			       struct path *digest_list_path,
>> +			       struct digest_cache *digest_cache, u8 *digest,
>> +			       enum hash_algo algo)
>> +{
>> +	struct digest_cache *cache;
>> +	struct dir_entry *dir_entry;
>> +	int ret;
>> +
>> +	list_for_each_entry(dir_entry, &digest_cache->dir_entries, list) {
>> +		mutex_lock(&dir_entry->digest_cache_mutex);
>> +		if (!dir_entry->digest_cache) {
>> +			cache = digest_cache_create(dentry, digest_list_path,
>> +						    digest_cache->path_str,
>> +						    dir_entry->name);
>> +			/* 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)
>> +			return (digest_cache_found_t)dir_entry->digest_cache;
>> +	}
>> +
>> +	return 0UL;
>> +}
>> +
>> +/**
>> + * digest_cache_dir_free - Free the stored file list and put digest caches
>> + * @digest_cache: Digest cache
>> + *
>> + * This function frees the file list created by digest_cache_create(), and puts
>> + * the digest cache 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/digest_cache/htable.c b/security/digest_cache/htable.c
>> index d2d5d8f5e5b1..8cf7400dfcf4 100644
>> --- a/security/digest_cache/htable.c
>> +++ b/security/digest_cache/htable.c
>> @@ -8,6 +8,8 @@
>>    */
>>   
>>   #define pr_fmt(fmt) "DIGEST CACHE: "fmt
>> +#include <linux/namei.h>
>> +
>>   #include "internal.h"
>>   
>>   /**
>> @@ -210,10 +212,26 @@ digest_cache_found_t digest_cache_lookup(struct dentry *dentry,
>>   					 struct digest_cache *digest_cache,
>>   					 u8 *digest, enum hash_algo algo)
>>   {
>> +	struct path digest_list_path;
>> +	digest_cache_found_t found;
>>   	int ret;
>>   
>> -	ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo);
>> -	return (!ret) ? (digest_cache_found_t)digest_cache : 0UL;
>> +	if (!test_bit(IS_DIR, &digest_cache->flags)) {
>> +		ret = digest_cache_htable_lookup(dentry, digest_cache, digest,
>> +						 algo);
>> +		return (!ret) ? (digest_cache_found_t)digest_cache : 0UL;
> 
> s/(!ret)/!ret/
> 
> I'd consider tho just use plain if-statement, ternary operator is best
> to be avoided other than macros

Ok.

Thanks

Roberto

>> +	}
>> +
>> +	ret = kern_path(digest_cache->path_str, 0, &digest_list_path);
>> +	if (ret < 0) {
>> +		pr_debug("Cannot find path %s\n", digest_cache->path_str);
>> +		return 0UL;
>> +	}
>> +
>> +	found = digest_cache_dir_lookup_digest(dentry, &digest_list_path,
>> +					       digest_cache, digest, algo);
>> +	path_put(&digest_list_path);
>> +	return found;
>>   }
>>   EXPORT_SYMBOL_GPL(digest_cache_lookup);
>>   
>> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
>> index 4929d25e7972..b7afca8e04da 100644
>> --- a/security/digest_cache/internal.h
>> +++ b/security/digest_cache/internal.h
>> @@ -16,6 +16,39 @@
>>   /* Digest cache bits in flags. */
>>   #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
>>   #define INVALID			1	/* Digest cache marked as invalid. */
>> +#define IS_DIR			2	/* 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[];
>> +} __packed;
>>   
>>   /**
>>    * struct digest_cache_verif
>> @@ -83,6 +116,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
>> @@ -93,6 +127,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;
>> @@ -193,4 +228,14 @@ 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_create(struct digest_cache *digest_cache,
>> +			    struct path *digest_list_path);
>> +digest_cache_found_t
>> +digest_cache_dir_lookup_digest(struct dentry *dentry,
>> +			       struct path *digest_list_path,
>> +			       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/digest_cache/main.c b/security/digest_cache/main.c
>> index f3475b36e566..15f1486610a3 100644
>> --- a/security/digest_cache/main.c
>> +++ b/security/digest_cache/main.c
>> @@ -50,6 +50,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str,
>>   	digest_cache->flags = 0UL;
>>   	INIT_LIST_HEAD(&digest_cache->htables);
>>   	INIT_LIST_HEAD(&digest_cache->verif_data);
>> +	INIT_LIST_HEAD(&digest_cache->dir_entries);
>>   	mutex_init(&digest_cache->mutex);
>>   
>>   	pr_debug("New digest cache %s (ref count: %d)\n",
>> @@ -68,6 +69,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);
>>   	mutex_destroy(&digest_cache->mutex);
>>   
>>   	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
>> @@ -185,6 +187,16 @@ struct digest_cache *digest_cache_create(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, &dig_sec->dig_owner->flags);
>> +
>> +		ret = digest_cache_dir_create(digest_cache, digest_list_path);
>> +		if (ret < 0) {
>> +			pr_debug("Failed to create dir digest cache, ret: %d (keep digest cache)\n",
>> +				 ret);
>> +			/* Prevent usage of partially-populated digest cache. */
>> +			set_bit(INVALID, &dig_sec->dig_owner->flags);
>> +		}
>>   	}
>>   
>>   	/* Creation complete, notify the other lock contenders. */
> 
> 
> BR, Jarkko


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 10/14] digest cache: Prefetch digest lists if requested
  2024-04-15 19:42   ` Jarkko Sakkinen
@ 2024-04-16 10:34     ` Roberto Sassu
  2024-04-16 14:47       ` Jarkko Sakkinen
  0 siblings, 1 reply; 43+ messages in thread
From: Roberto Sassu @ 2024-04-16 10:34 UTC (permalink / raw)
  To: Jarkko Sakkinen, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On 4/15/2024 9:42 PM, Jarkko Sakkinen wrote:
> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
>> 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 digest_cache LSM is not exempted from this issue. Under the assumption
>> that only the digest list is measured, and file measurements are omitted if
>> their digest is found in that digest list, a PCR can be predictable only if
>> all files belong to the same digest list. Otherwise, it will still be
>> unpredictable, since files accessed in a non-deterministic order will cause
>> digest lists to be measured in a non-deterministic order too.
>>
>> Overcome this issue, if prefetching is enabled, by searching a digest list
>> file name in digest_list_dir_lookup_filename() among the entries of the
>> linked list built by digest_cache_dir_create(). If the file name does not
>> match, read the digest list to trigger its measurement. Otherwise, also
>> create a digest cache and return that to the caller. Release the extra
>> reference of the directory digest cache in digest_cache_new(), since it was
>> only used for the search and it is not going to be returned.
>>
>> Prefetching needs to be explicitly enabled by setting the new
>> security.dig_prefetch xattr to 1 in the directory containing the digest
>> lists. The newly introduced function digest_cache_prefetch_requested()
>> checks first if the DIR_PREFETCH bit is set in dig_owner, otherwise it
>> reads the xattr. digest_cache_create() sets DIR_PREFETCH in dig_owner, if
>> prefetching is enabled, before declaring the digest cache as initialized.
>>
>> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
>> ---
>>   include/uapi/linux/xattr.h       |  3 +
>>   security/digest_cache/dir.c      | 55 +++++++++++++++++-
>>   security/digest_cache/internal.h | 11 +++-
>>   security/digest_cache/main.c     | 95 +++++++++++++++++++++++++++++++-
>>   security/digest_cache/populate.c |  8 ++-
>>   security/digest_cache/verif.c    |  5 +-
>>   6 files changed, 170 insertions(+), 7 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/digest_cache/dir.c b/security/digest_cache/dir.c
>> index 7bfcdd5f7ef1..a7d203c15386 100644
>> --- a/security/digest_cache/dir.c
>> +++ b/security/digest_cache/dir.c
>> @@ -54,6 +54,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;
>> @@ -127,6 +128,7 @@ int digest_cache_dir_create(struct digest_cache *digest_cache,
>>    * @digest_cache: Digest cache
>>    * @digest: Digest to search
>>    * @algo: Algorithm of the digest to search
>> + * @filename: File name of the digest list to search
>>    *
>>    * This function iterates over the linked list created by
>>    * digest_cache_dir_create() and looks up the digest in the digest cache of
>> @@ -149,7 +151,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>>   		if (!dir_entry->digest_cache) {
>>   			cache = digest_cache_create(dentry, digest_list_path,
>>   						    digest_cache->path_str,
>> -						    dir_entry->name);
>> +						    dir_entry->name, false,
>> +						    false);
>>   			/* Ignore digest caches that cannot be instantiated. */
>>   			if (!cache) {
>>   				mutex_unlock(&dir_entry->digest_cache_mutex);
>> @@ -158,6 +161,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>>   
>>   			/* Consume extra ref. from digest_cache_create(). */
>>   			dir_entry->digest_cache = cache;
>> +			/* Digest list was read, mark entry as prefetched. */
>> +			dir_entry->prefetched = true;
>>   		}
>>   		mutex_unlock(&dir_entry->digest_cache_mutex);
>>   
>> @@ -171,6 +176,54 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>>   	return 0UL;
>>   }
>>   
>> +/**
>> + * 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: Digest cache
>> + * @filename: File name of the digest list to search
>> + *
>> + * This function iterates over the linked list created by
>> + * digest_cache_dir_create() 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 returns the digest cache pointer from
>> + * digest_cache_create() to the caller.
>> + *
>> + * Return: A digest cache pointer if the digest list if found, NULL otherwise.
>> + */
>> +struct digest_cache *
>> +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);
>> +
>> +		dir_entry->prefetched = true;
>> +		mutex_unlock(&dir_entry->digest_cache_mutex);
>> +
>> +		if (filename_found)
>> +			return cache;
>> +	}
>> +
>> +	return NULL;
>> +}
>> +
>>   /**
>>    * digest_cache_dir_free - Free the stored file list and put digest caches
>>    * @digest_cache: Digest cache
>> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
>> index b7afca8e04da..c13b35f6b2c0 100644
>> --- a/security/digest_cache/internal.h
>> +++ b/security/digest_cache/internal.h
>> @@ -17,6 +17,7 @@
>>   #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
>>   #define INVALID			1	/* Digest cache marked as invalid. */
>>   #define IS_DIR			2	/* Digest cache created from dir. */
>> +#define DIR_PREFETCH		3	/* Prefetching requested for dir. */
>>   
>>   /**
>>    * struct readdir_callback - Structure to store information for dir iteration
>> @@ -37,6 +38,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
>> @@ -47,6 +49,7 @@ struct dir_entry {
>>   	struct digest_cache *digest_cache;
>>   	struct mutex digest_cache_mutex;
>>   	unsigned int seq_num;
>> +	bool prefetched;
>>   	char name[];
>>   } __packed;
>>   
>> @@ -205,7 +208,8 @@ 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);
>>   
>>   /* htable.c */
>>   int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
>> @@ -236,6 +240,11 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>>   			       struct path *digest_list_path,
>>   			       struct digest_cache *digest_cache, u8 *digest,
>>   			       enum hash_algo algo);
>> +struct digest_cache *
>> +digest_cache_dir_lookup_filename(struct dentry *dentry,
>> +				 struct path *digest_list_path,
>> +				 struct digest_cache *digest_cache,
>> +				 char *filename);
>>   void digest_cache_dir_free(struct digest_cache *digest_cache);
>>   
>>   #endif /* _DIGEST_CACHE_INTERNAL_H */
>> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
>> index 15f1486610a3..a5616fd07c1d 100644
>> --- a/security/digest_cache/main.c
>> +++ b/security/digest_cache/main.c
>> @@ -83,6 +83,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
>> @@ -109,7 +111,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)
> 
> 
> This has enough parameters to consider separate "digest_cache_descriptor"
> or whatever. In some architectures this is beyond register parameters,
> which is IMHO good threshold to consider that.
> 
> This will make e.g. tracing easier as you have to map only one parameter
> to a known struct to inspect the values.

Uhm, seems still good (6 parameters). Sure, I can make a structure to 
encapsulate the parameters, but will be used only for this purpose. Not 
sure the code understandability will improve.

Thanks

Roberto

>>   {
>>   	struct path file_path;
>>   	struct digest_cache *digest_cache = NULL;
>> @@ -148,6 +151,16 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>>   				 dentry->d_name.name);
>>   			goto out;
>>   		}
>> +
>> +		if (prefetch) {
>> +			/* Fine to fail, we are just prefetching. */
>> +			ret = digest_cache_populate(NULL, digest_list_path,
>> +						    path_str, filename);
>> +			pr_debug("Digest list %s/%s %s prefetched\n",
>> +				 path_str, filename,
>> +				 !ret ? "has been" : "cannot be");
>> +			goto out;
>> +		}
>>   	}
>>   
>>   	dig_sec = digest_cache_get_security(inode);
>> @@ -176,6 +189,11 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>>   
>>   	/* Make the other lock contenders wait until creation complete. */
>>   	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
>> +
>> +	/* Set DIR_PREFETCH if prefetching was requested. */
>> +	if (prefetch_req)
>> +		set_bit(DIR_PREFETCH, &digest_cache->flags);
>> +
>>   	mutex_unlock(&dig_sec->dig_owner_mutex);
>>   
>>   	if (S_ISREG(inode->i_mode)) {
>> @@ -220,6 +238,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 new security.dig_prefetch xattr.
>> + *
>> + * Return: True if prefetching is requested, false otherwise.
>> + */
>> +static 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
>> @@ -230,13 +294,19 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>>    * with that file name. If security.digest_list is not found, this function
>>    * requests the creation of a digest cache on the parent directory.
>>    *
>> + * On prefetching, if the default path is a directory and if
>> + * security.digest_list is found, this function first retrieves the directory
>> + * digest cache, and then calls digest_cache_dir_lookup_filename() to retrieve
>> + * the desired digest cache in that directory.
>> + *
>>    * Return: A new digest cache on success, NULL on error.
>>    */
>>   static struct digest_cache *digest_cache_new(struct dentry *dentry)
>>   {
>>   	char filename[NAME_MAX + 1] = { 0 };
>> -	struct digest_cache *digest_cache = NULL;
>> +	struct digest_cache *digest_cache = NULL, *found;
>>   	struct path default_path;
>> +	bool prefetch_req = false;
>>   	int ret;
>>   
>>   	ret = kern_path(default_path_str, 0, &default_path);
>> @@ -273,9 +343,28 @@ static struct digest_cache *digest_cache_new(struct dentry *dentry)
>>   	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);
>> +
>> +	if (filename[0])
>> +		prefetch_req = digest_cache_prefetch_requested(&default_path,
>> +							default_path_str);
>>   create:
>> +	/* On prefetching, retrieve the directory digest cache. */
>>   	digest_cache = digest_cache_create(dentry, &default_path,
>> -					   default_path_str, filename);
>> +					   default_path_str,
>> +					   !prefetch_req ? filename : "",
>> +					   prefetch_req, false);
>> +	if (!digest_cache)
>> +		goto out;
>> +
>> +	if (prefetch_req) {
>> +		/* Find the digest cache with a matching file name. */
>> +		found = digest_cache_dir_lookup_filename(dentry, &default_path,
>> +							 digest_cache,
>> +							 filename);
>> +		/* Release ref. to the directory digest cache. */
>> +		digest_cache_put(digest_cache);
>> +		digest_cache = found;
>> +	}
>>   out:
>>   	path_put(&default_path);
>>   	return digest_cache;
>> diff --git a/security/digest_cache/populate.c b/security/digest_cache/populate.c
>> index 9c2fc2295310..17e7b011c367 100644
>> --- a/security/digest_cache/populate.c
>> +++ b/security/digest_cache/populate.c
>> @@ -143,6 +143,12 @@ int digest_cache_populate(struct digest_cache *digest_cache,
>>   		return ret;
>>   	}
>>   
>> +	/* The caller wants just to read digest lists. */
>> +	if (!digest_cache) {
>> +		ret = 0;
>> +		goto out_vfree;
>> +	}
>> +
>>   	data_len = digest_cache_strip_modsig(data, ret);
>>   
>>   	/* Digest list parsers initialize the hash table and add the digests. */
>> @@ -151,7 +157,7 @@ int digest_cache_populate(struct digest_cache *digest_cache,
>>   	if (ret < 0)
>>   		pr_debug("Error parsing digest list %s%s%s, ret: %d\n",
>>   			 path_str, filename[0] ? "/" : "", filename, ret);
>> -
>> +out_vfree:
>>   	vfree(data);
>>   	return ret;
>>   }
>> diff --git a/security/digest_cache/verif.c b/security/digest_cache/verif.c
>> index 04023240d3b4..c42ae93261e2 100644
>> --- a/security/digest_cache/verif.c
>> +++ b/security/digest_cache/verif.c
>> @@ -33,7 +33,7 @@ static void free_verif(struct digest_cache_verif *verif)
>>    * This function lets a verifier supply verification data about a digest list
>>    * being read to populate the digest cache.
>>    *
>> - * Return: Zero on success, -ENOMEM if out of memory.
>> + * Return: Zero on success, -ENOMEM if out of memory, -ENOENT on prefetching.
>>    */
>>   int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
>>   			   size_t size)
>> @@ -41,6 +41,9 @@ int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
>>   	struct digest_cache *digest_cache = digest_cache_from_file_sec(file);
>>   	struct digest_cache_verif *new_verif;
>>   
>> +	if (!digest_cache)
>> +		return -ENOENT;
>> +
>>   	/*
>>   	 * All allocations must be atomic (non-sleepable) since kprobe does not
>>   	 * allow otherwise (kprobe is needed for testing).
> 
> 
> BR, Jarkko


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 11/14] digest_cache: Reset digest cache on file/directory change
  2024-04-15 19:44   ` Jarkko Sakkinen
@ 2024-04-16 10:37     ` Roberto Sassu
  0 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-16 10:37 UTC (permalink / raw)
  To: Jarkko Sakkinen, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On 4/15/2024 9:44 PM, Jarkko Sakkinen wrote:
> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
>> From: Roberto Sassu <roberto.sassu@huawei.com>
>>
>> Register six new LSM hooks, path_truncate, file_release, inode_unlink,
>> inode_rename, inode_post_setxattr and inode_post_removexattr, to monitor
>> digest lists/directory modifications.
>>
>> If an action affects a digest list or the parent directory, the new LSM
>> hook implementations call digest_cache_reset_owner() to set the RESET bit
>> (if unset) on the digest cache pointed to by dig_owner in the inode
>> security blob. This will cause next calls to digest_cache_get() and
>> digest_cache_create() to respectively put and clear dig_user and dig_owner,
>> and request a new digest cache.
>>
>> If an action affects a file using a digest cache, the new LSM hook
>> implementations call digest_cache_reset_user() to set the RESET_USER bit
>> (if unset) on the digest cache pointed to by dig_user in the inode security
>> blob. This will cause next calls to digest_cache_get() to put and clear
>> dig_user, and retrieve the digest cache again.
>>
>> That does not affect other users of the old digest cache, since that one
>> remains valid as long as the reference count is greater than zero. However,
>> they will be notified in a subsequent patch about the reset, so that they
>> can eventually request a new digest cache.
>>
>> Recreating a file digest cache means reading the digest list again and
>> extracting the digests. Recreating a directory digest cache, instead, does
>> not mean recreating the digest cache for directory entries, since those
>> digest caches are likely already stored in the inode security blob. It
>> would happen however for new files.
>>
>> Dig_owner reset for file 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 is being closed, inode_unlink, when a
>> digest list is removed, and inode_rename when a digest list is renamed.
>>
>> Dig_owner reset for directory digest caches is done on file_release, when a
>> new digest list is written in the digest list directory, on inode_unlink,
>> when a digest list is deleted from that directory, and finally on
>> inode_rename, when a digest list is moved to/from that directory.
>>
>> Dig_user reset 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.
>>
>> With the exception of file_release, which will always be executed (cannot
>> be denied), and inode_post_setxattr and inode_post_removexattr, which are
>> executed after the actual operation, the other LSM hooks are not optimal,
>> since the digest_cache LSM does not know whether or not the operation will
>> be allowed also by other LSMs. If the operation is denied, the digest_cache
>> LSM would do an unnecessary reset.
>>
>> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
>> ---
>>   security/digest_cache/Kconfig    |   1 +
>>   security/digest_cache/Makefile   |   3 +-
>>   security/digest_cache/dir.c      |   6 +
>>   security/digest_cache/internal.h |  14 +++
>>   security/digest_cache/main.c     |  19 +++
>>   security/digest_cache/reset.c    | 197 +++++++++++++++++++++++++++++++
>>   6 files changed, 239 insertions(+), 1 deletion(-)
>>   create mode 100644 security/digest_cache/reset.c
>>
>> diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
>> index cb4fa44e8f2a..54ba3a585073 100644
>> --- a/security/digest_cache/Kconfig
>> +++ b/security/digest_cache/Kconfig
>> @@ -2,6 +2,7 @@
>>   config SECURITY_DIGEST_CACHE
>>   	bool "Digest_cache LSM"
>>   	select TLV_PARSER
>> +	select SECURITY_PATH
>>   	default n
>>   	help
>>   	  This option enables an LSM maintaining a cache of digests
>> diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
>> index e417da0383ab..3d5e600a2c45 100644
>> --- a/security/digest_cache/Makefile
>> +++ b/security/digest_cache/Makefile
>> @@ -4,7 +4,8 @@
>>   
>>   obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
>>   
>> -digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o
>> +digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o \
>> +		  reset.o
>>   
>>   digest_cache-y += parsers/tlv.o
>>   digest_cache-y += parsers/rpm.o
>> diff --git a/security/digest_cache/dir.c b/security/digest_cache/dir.c
>> index a7d203c15386..937177660242 100644
>> --- a/security/digest_cache/dir.c
>> +++ b/security/digest_cache/dir.c
>> @@ -148,6 +148,12 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
>>   
>>   	list_for_each_entry(dir_entry, &digest_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,
>>   						    digest_cache->path_str,
>> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
>> index c13b35f6b2c0..c816929c4743 100644
>> --- a/security/digest_cache/internal.h
>> +++ b/security/digest_cache/internal.h
>> @@ -18,6 +18,8 @@
>>   #define INVALID			1	/* Digest cache marked as invalid. */
>>   #define IS_DIR			2	/* Digest cache created from dir. */
>>   #define DIR_PREFETCH		3	/* Prefetching requested for dir. */
>> +#define RESET			4	/* Digest cache to be recreated. */
>> +#define RESET_USER		5	/* Dig_user pointer to be released. */
>>   
>>   /**
>>    * struct readdir_callback - Structure to store information for dir iteration
>> @@ -247,4 +249,16 @@ digest_cache_dir_lookup_filename(struct dentry *dentry,
>>   				 char *filename);
>>   void digest_cache_dir_free(struct digest_cache *digest_cache);
>>   
>> +/* reset.c */
>> +int digest_cache_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/digest_cache/main.c b/security/digest_cache/main.c
>> index a5616fd07c1d..ce3518a33c80 100644
>> --- a/security/digest_cache/main.c
>> +++ b/security/digest_cache/main.c
>> @@ -169,6 +169,11 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
>>   
>>   	/* Serialize check and assignment of dig_owner. */
>>   	mutex_lock(&dig_sec->dig_owner_mutex);
>> +	if (dig_sec->dig_owner && test_bit(RESET, &dig_sec->dig_owner->flags)) {
>> +		digest_cache_put(dig_sec->dig_owner);
>> +		dig_sec->dig_owner = NULL;
>> +	}
>> +
>>   	if (dig_sec->dig_owner) {
>>   		/* Increment ref. count for reference returned to the caller. */
>>   		digest_cache = digest_cache_ref(dig_sec->dig_owner);
>> @@ -403,6 +408,13 @@ 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) ||
>> +	     test_bit(RESET_USER, &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(). */
>> @@ -491,6 +503,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/digest_cache/reset.c b/security/digest_cache/reset.c
>> new file mode 100644
>> index 000000000000..e07222b0e0e8
>> --- /dev/null
>> +++ b/security/digest_cache/reset.c
>> @@ -0,0 +1,197 @@
>> +// 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_owner - Reset dig_owner
>> + * @inode: Inode of the digest list/directory containing the digest list
>> + * @reason: Reason for reset
>> + *
>> + * This function sets the RESET bit of the digest cache pointed to by dig_owner
>> + * (if unset), so that digest_cache_get() and digest_cache_create() respectively
>> + * release and clear dig_user and dig_owner in the inode security blob. This
>> + * causes new callers of digest_cache_get() to get a new digest cache.
>> + */
>> +static void digest_cache_reset_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 &&
>> +	    !test_and_set_bit(RESET, &dig_sec->dig_owner->flags))
>> +		pr_debug("Resetting %s (dig_owner), reason: %s\n",
>> +			 dig_sec->dig_owner->path_str, reason);
>> +	mutex_unlock(&dig_sec->dig_owner_mutex);
>> +}
>> +
>> +/**
>> + * digest_cache_reset_user - Reset dig_user
>> + * @inode: Inode of the file using the digest cache
>> + * @reason: Reason for reset
>> + *
>> + * This function sets the RESET_USER bit (if unset), so that digest_cache_get()
>> + * clears the dig_user pointer in the inode security blob and determines again
>> + * the digest list inode to get a digest cache from.
>> + */
>> +static void digest_cache_reset_user(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_user_mutex);
>> +	if (dig_sec->dig_user &&
>> +	    !test_and_set_bit(RESET_USER, &dig_sec->dig_user->flags))
>> +		pr_debug("Resetting %s (dig_user), reason: %s\n",
>> +			 dig_sec->dig_user->path_str, reason);
>> +	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, it resets the inode dig_owner, to force rebuilding the digest
>> + * cache.
>> + *
>> + * Return: Zero.
>> + */
>> +int digest_cache_path_truncate(const struct path *path)
>> +{
>> +	struct inode *inode = d_backing_inode(path->dentry);
>> +
>> +	if (!S_ISREG(inode->i_mode))
>> +		return 0;
>> +
>> +	digest_cache_reset_owner(inode, "file_truncate");
>> +	return 0;
>> +}
>> +
>> +/**
>> + * digest_cache_file_release - Last reference of a file desc is being released
>> + * @file: File descriptor
>> + *
>> + * This function is called when the last reference of a file descriptor is
>> + * being released. If the inode is a regular file and was opened for write, or
>> + * the parent inode is the digest list directory and the file was created, it
>> + * resets the inode dig_owner, to force rebuilding the digest cache.
>> + */
>> +void digest_cache_file_release(struct file *file)
>> +{
>> +	struct inode *dir = d_backing_inode(file_dentry(file)->d_parent);
>> +
>> +	if (!S_ISREG(file_inode(file)->i_mode) || !(file->f_mode & FMODE_WRITE))
>> +		return;
>> +
>> +	digest_cache_reset_owner(file_inode(file), "file_file_release");
>> +	if (file->f_mode & FMODE_CREATED)
>> +		digest_cache_reset_owner(dir, "dir_file_release");
>> +}
>> +
>> +/**
>> + * digest_cache_inode_unlink - An inode is being removed
>> + * @dir: Inode of the affected directory
>> + * @dentry: Dentry of the inode being removed
>> + *
>> + * This function is called when an existing inode is being removed. If the
>> + * inode is a digest list, or the parent inode is the digest list directory and
>> + * the inode is a regular file, it resets the affected inode dig_owner, to force
>> + * rebuilding the digest cache.
>> + *
>> + * Return: Zero.
>> + */
>> +int digest_cache_inode_unlink(struct inode *dir, struct dentry *dentry)
>> +{
>> +	struct inode *inode = d_backing_inode(dentry);
>> +
>> +	if (!S_ISREG(inode->i_mode))
>> +		return 0;
>> +
>> +	digest_cache_reset_owner(inode, "file_unlink");
>> +	digest_cache_reset_owner(dir, "dir_unlink");
>> +	return 0;
>> +}
>> +
>> +/**
>> + * digest_cache_inode_rename - An inode is being renamed
>> + * @old_dir: Inode of the directory containing the inode being renamed
>> + * @old_dentry: Dentry of the inode being renamed
>> + * @new_dir: Directory where the inode will be placed into
>> + * @new_dentry: Dentry of the inode after being renamed
>> + *
>> + * This function is called when an existing inode is being moved from a
>> + * directory to another (rename). If the inode is a digest list, or that inode
>> + * is moved from/to the digest list directory, it resets the affected inode
>> + * dig_owner, to force rebuilding the digest cache.
>> + *
>> + * Return: Zero.
>> + */
>> +int digest_cache_inode_rename(struct inode *old_dir, struct dentry *old_dentry,
>> +			      struct inode *new_dir, struct dentry *new_dentry)
>> +{
>> +	struct inode *old_inode = d_backing_inode(old_dentry);
>> +
>> +	if (!S_ISREG(old_inode->i_mode))
>> +		return 0;
>> +
>> +	digest_cache_reset_owner(old_inode, "file_rename");
>> +	digest_cache_reset_owner(old_dir, "dir_rename_from");
>> +	digest_cache_reset_owner(new_dir, "dir_rename_to");
>> +	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
>> + * resets 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_reset_user(d_backing_inode(dentry), "file_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 resets 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)
> 
> nit:
> 
> void digest_cache_inode_post_removexattr(struct dentry *dentry, const char *name)
> 
> would be fine too

I still prefer to stay within 80 characters.

Thanks

Roberto

>> +{
>> +	if (strcmp(name, XATTR_NAME_DIGEST_LIST))
>> +		return;
>> +
>> +	digest_cache_reset_user(d_backing_inode(dentry), "file_removexattr");
>> +}
> 
> 
> BR, Jarkko


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 13/14] selftests/digest_cache: Add selftests for digest_cache LSM
  2024-04-15 19:47   ` Jarkko Sakkinen
@ 2024-04-16 10:39     ` Roberto Sassu
  0 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-16 10:39 UTC (permalink / raw)
  To: Jarkko Sakkinen, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On 4/15/2024 9:47 PM, Jarkko Sakkinen wrote:
> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
>> From: Roberto Sassu <roberto.sassu@huawei.com>
>>
>> Add tests to verify the correctness of the digest_cache LSM, in all_test.c.
>>
>> Add the kernel module digest_cache_kern.ko, to let all_test call the API
>> of the digest_cache LSM through the newly introduced digest_cache_test file
>> in securityfs.
>>
>> Test coverage information:
>>
>> File 'security/digest_cache/notifier.c'
>> Lines executed:100.00% of 31
>> File 'security/digest_cache/reset.c'
>> Lines executed:98.36% of 61
>> File 'security/digest_cache/main.c'
>> Lines executed:90.29% of 206
>> File 'security/digest_cache/modsig.c'
>> Lines executed:42.86% of 21
>> File 'security/digest_cache/htable.c'
>> Lines executed:93.02% of 86
>> File 'security/digest_cache/populate.c'
>> Lines executed:92.86% of 56
>> File 'security/digest_cache/verif.c'
>> Lines executed:89.74% of 39
>> File 'security/digest_cache/dir.c'
>> Lines executed:90.62% of 96
>> File 'security/digest_cache/secfs.c'
>> Lines executed:57.14% of 21
>> File 'security/digest_cache/parsers/tlv.c'
>> Lines executed:79.75% of 79
>> File 'security/digest_cache/parsers/rpm.c'
>> Lines executed:88.46% of 78
>>
>> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
>> ---
>>   MAINTAINERS                                   |   1 +
>>   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 | 815 ++++++++++++++++++
>>   tools/testing/selftests/digest_cache/common.c |  78 ++
>>   tools/testing/selftests/digest_cache/common.h | 135 +++
>>   .../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     | 564 ++++++++++++
>>   14 files changed, 1969 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/MAINTAINERS b/MAINTAINERS
>> index 72801a88449c..d7f700da009e 100644
>> --- a/MAINTAINERS
>> +++ b/MAINTAINERS
>> @@ -6198,6 +6198,7 @@ M:	Roberto Sassu <roberto.sassu@huawei.com>
>>   L:	linux-security-module@vger.kernel.org
>>   S:	Maintained
>>   F:	security/digest_cache/
>> +F:	tools/testing/selftests/digest_cache/
>>   
> A common convetion is to have one patch with MAINTAINERS update in the
> tail. This is now sprinkled to multiple patches which is not good.

Ok.

Thanks

Roberto

>>   DIGITEQ AUTOMOTIVE MGB4 V4L2 DRIVER
>>   M:	Martin Tuma <martin.tuma@digiteqautomotive.com>
>> diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
>> index 15b6a111c3be..3c5965a62d28 100644
>> --- a/tools/testing/selftests/Makefile
>> +++ b/tools/testing/selftests/Makefile
>> @@ -13,6 +13,7 @@ TARGETS += core
>>   TARGETS += cpufreq
>>   TARGETS += cpu-hotplug
>>   TARGETS += damon
>> +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..6b1e0d3c08cf
>> --- /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)
>> +	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..9f45e522c43c
>> --- /dev/null
>> +++ b/tools/testing/selftests/digest_cache/all_test.c
>> @@ -0,0 +1,815 @@
>> +// 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 digest_cache LSM.
>> + */
>> +
>> +#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;
>> +	int notify_inodesfd;
>> +};
>> +
>> +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 notify inodes interface. */
>> +	self->notify_inodesfd = open(DIGEST_CACHE_NOTIFY_INODES_INTERFACE,
>> +				     O_RDWR, 0600);
>> +	ASSERT_NE(-1, self->notify_inodesfd);
>> +
>> +	/* 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 notify inodes interface. */
>> +	close(self->notify_inodesfd);
>> +
>> +	/* 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 notify_inode_init(struct _test_data_shared_data *self,
>> +			      struct __test_metadata *_metadata)
>> +{
>> +	/* Clear buffer. */
>> +	ASSERT_EQ(1, write(self->notify_inodesfd, "1", 1));
>> +}
>> +
>> +static void notify_inodes_check(struct _test_data_shared_data *self,
>> +				struct __test_metadata *_metadata,
>> +				char *filenames)
>> +{
>> +	char notify_inodes_buf[1024] = { 0 };
>> +	char notify_inodes_buf_kernel[1024] = { 0 };
>> +	char *filename, *filenames_copy, *buf_ptr = notify_inodes_buf;
>> +	struct stat st;
>> +	int fd;
>> +
>> +	ASSERT_LT(0, read(self->notify_inodesfd, notify_inodes_buf_kernel,
>> +			  sizeof(notify_inodes_buf_kernel)));
>> +
>> +	filenames_copy = strdup(filenames);
>> +	ASSERT_NE(NULL, filenames_copy);
>> +
>> +	while ((filename = strsep(&filenames_copy, ","))) {
>> +		fd = openat(self->base_dirfd, filename, O_RDONLY);
>> +		ASSERT_NE(-1, fd);
>> +		ASSERT_EQ(0, fstat(fd, &st));
>> +		close(fd);
>> +
>> +		buf_ptr += snprintf(buf_ptr,
>> +				    sizeof(notify_inodes_buf) -
>> +				    (buf_ptr - notify_inodes_buf), "%s%lu",
>> +				    notify_inodes_buf[0] ? "," : "", st.st_ino);
>> +	}
>> +
>> +	free(filenames_copy);
>> +
>> +	ASSERT_EQ(0, strcmp(notify_inodes_buf, notify_inodes_buf_kernel));
>> +}
>> +
>> +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));
>> +
>> +	notify_inode_init(self, _metadata);
>> +
>> +	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));
>> +
>> +		notify_inodes_check(self, _metadata, "file");
>> +		return;
>> +	default:
>> +		break;
>> +	}
>> +
>> +	ASSERT_NE(0, query_test(self->kernfd, self->base_dir, "file",
>> +				HASH_ALGO_SHA1, 0, 1));
>> +
>> +	notify_inodes_check(self, _metadata, "file");
>> +}
>> +
>> +/*
>> + * 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)
>> +{
>> +	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;
>> +			}
>> +		}
>> +
>> +		ASSERT_EQ(expected_ret, query_test(self->kernfd, self->base_dir,
>> +						   "file", 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 digest_list_filename[NAME_MAX + 1];
>> +	int i, removed[10];
>> +
>> +	for (i = 0; i < 10; i++) {
>> +		snprintf(digest_list_filename, sizeof(digest_list_filename),
>> +			 "tlv-digest_list%d", 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", NULL));
>> +	/* The second file is to have duplicate notifications (file and dir). */
>> +	ASSERT_EQ(0, create_file(self->base_dirfd, "file2",
>> +				 "tlv-digest_list7"));
>> +	/* The query adds file2 inode to the file digest cache notif. list. */
>> +	ASSERT_NE(0, query_test(self->kernfd, self->base_dir, "file2",
>> +				HASH_ALGO_SHA1, 0, 1));
>> +
>> +	query_test_with_failures(self, _metadata, 0, 10, removed, 0);
>> +
>> +	notify_inode_init(self, _metadata);
>> +	ASSERT_EQ(0, unlinkat(self->digest_lists_dirfd, "tlv-digest_list7", 0));
>> +	/* File notification comes before directory notification. */
>> +	notify_inodes_check(self, _metadata, "file2,file");
>> +
>> +	removed[0] = 7;
>> +
>> +	query_test_with_failures(self, _metadata, 0, 10, removed, 1);
>> +
>> +	notify_inode_init(self, _metadata);
>> +	ASSERT_EQ(0, renameat(self->digest_lists_dirfd, "tlv-digest_list6",
>> +			      self->base_dirfd, "tlv-digest_list6"));
>> +	notify_inodes_check(self, _metadata, "file");
>> +
>> +	removed[1] = 6;
>> +
>> +	query_test_with_failures(self, _metadata, 0, 10, removed, 2);
>> +
>> +	notify_inode_init(self, _metadata);
>> +	ASSERT_EQ(0, renameat(self->base_dirfd, "tlv-digest_list6",
>> +			      self->digest_lists_dirfd, "tlv-digest_list6"));
>> +	notify_inodes_check(self, _metadata, "file");
>> +
>> +	query_test_with_failures(self, _metadata, 0, 10, removed, 1);
>> +
>> +	notify_inode_init(self, _metadata);
>> +	ASSERT_EQ(0, gen_tlv_list(self->digest_lists_dirfd, "tlv-digest_list10",
>> +				  HASH_ALGO_SHA1, 10, 1, TLV_NO_FAILURE));
>> +	notify_inodes_check(self, _metadata, "file");
>> +
>> +	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));
>> +	}
>> +
>> +	ASSERT_EQ(0, create_file(self->base_dirfd, "file", NULL));
>> +
>> +	for (i = 0; i < 11; i++) {
>> +		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], digest_lists_kernel[1024] = { 0 };
>> +	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, sizeof(digest_lists)));
>> +
>> +	for (i = start_number; i <= end_number; i++) {
>> +		if (digest_lists_kernel[0])
>> +			strcat(digest_lists_kernel, ",");
>> +
>> +		snprintf(digest_list_filename, sizeof(digest_list_filename),
>> +			 "%d-tlv-digest_list%d", i, i);
>> +		strcat(digest_lists_kernel, 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], digest_lists_kernel[1024] = { 0 };
>> +	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, sizeof(digest_lists)));
>> +
>> +	for (i = 0; i < NUM_DIGEST_LISTS_PREFETCH; i++) {
>> +		if (digest_lists_kernel[0])
>> +			strcat(digest_lists_kernel, ",");
>> +
>> +		snprintf(digest_list_filename, sizeof(digest_list_filename),
>> +			 "%d-tlv-digest_list%d", i, i);
>> +		strcat(digest_lists_kernel, 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..2123f7d937ce
>> --- /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 digest_cache LSM.
>> + */
>> +
>> +#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] = "filenames",
>> +	[VERIF_NUMBER] = "number",
>> +	[VERIF_PREFETCH] = "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..e52e4b137807
>> --- /dev/null
>> +++ b/tools/testing/selftests/digest_cache/common.h
>> @@ -0,0 +1,135 @@
>> +/* 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_TEST_INTERFACE "/sys/kernel/security/digest_cache_test"
>> +#define DIGEST_CACHE_PATH_INTERFACE "/sys/kernel/security/digest_cache_path"
>> +#define DIGEST_CACHE_NOTIFY_INODES_INTERFACE  \
>> +	"/sys/kernel/security/digest_cache_notify_inodes"
>> +#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;
>> +} __attribute__ ((__packed__));
>> +
>> +struct rpm_entryinfo {
>> +	s32 tag;
>> +	u32 type;
>> +	s32 offset;
>> +	u32 count;
>> +} __attribute__ ((__packed__));
>> +
>> +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..1bacadad6b6a
>> --- /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 digest_cache LSM.
>> + */
>> +
>> +#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..075a06cc4f8e
>> --- /dev/null
>> +++ b/tools/testing/selftests/digest_cache/config
>> @@ -0,0 +1 @@
>> +CONFIG_SECURITY_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..7215ef638e66
>> --- /dev/null
>> +++ b/tools/testing/selftests/digest_cache/testmod/kern.c
>> @@ -0,0 +1,564 @@
>> +// 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 digest_cache LSM.
>> + */
>> +
>> +#define pr_fmt(fmt) "DIGEST CACHE TEST: "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, *notify_inodes;
>> +static struct digest_cache *digest_cache;
>> +static digest_cache_found_t found;
>> +static int cur_verif_index;
>> +static u8 prefetch_buf[4096];
>> +static u8 notify_inodes_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, "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, "number", &number, sizeof(number));
>> +}
>> +
>> +static ssize_t filenames_read(struct file *file, char __user *buf,
>> +			      size_t datalen, loff_t *ppos)
>> +{
>> +	loff_t _ppos = 0;
>> +	char *filenames_list;
>> +
>> +	filenames_list = digest_cache_verif_get(found ?
>> +				digest_cache_from_found_t(found) : digest_cache,
>> +				verifs_str[VERIF_FILENAMES]);
>> +	if (!filenames_list)
>> +		return -ENOENT;
>> +
>> +	return simple_read_from_buffer(buf, datalen, &_ppos, filenames_list,
>> +				       strlen(filenames_list) + 1);
>> +}
>> +
>> +static ssize_t number_read(struct file *file, char __user *buf, size_t datalen,
>> +			   loff_t *ppos)
>> +{
>> +	loff_t _ppos = 0;
>> +	u64 *number;
>> +	char temp[20];
>> +	ssize_t len;
>> +
>> +	number = digest_cache_verif_get(found ?
>> +					digest_cache_from_found_t(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 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, "probe_digest_cache", "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 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 int test_digest_cache_change(struct notifier_block *nb,
>> +				    unsigned long event, void *data)
>> +{
>> +	struct digest_cache_event_data *event_data = data;
>> +	char i_ino_str[10];
>> +
>> +	if (event != DIGEST_CACHE_RESET)
>> +		return NOTIFY_DONE;
>> +
>> +	if (notify_inodes_buf[0])
>> +		strlcat(notify_inodes_buf, ",", sizeof(notify_inodes_buf));
>> +
>> +	snprintf(i_ino_str, sizeof(i_ino_str), "%lu", event_data->inode->i_ino);
>> +	strlcat(notify_inodes_buf, i_ino_str, sizeof(notify_inodes_buf));
>> +	return 0;
>> +}
>> +
>> +static struct notifier_block digest_cache_notifier = {
>> +	.notifier_call = test_digest_cache_change,
>> +};
>> +
>> +static ssize_t write_notify_inodes(struct file *file, const char __user *buf,
>> +			     size_t datalen, loff_t *ppos)
>> +{
>> +	memset(notify_inodes_buf, 0, sizeof(notify_inodes_buf));
>> +	return datalen;
>> +}
>> +
>> +static ssize_t read_notify_inodes(struct file *file, char __user *buf,
>> +				  size_t datalen, loff_t *ppos)
>> +{
>> +	loff_t _ppos = 0;
>> +
>> +	return simple_read_from_buffer(buf, datalen, &_ppos, notify_inodes_buf,
>> +				       strlen(notify_inodes_buf) + 1);
>> +}
>> +
>> +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 const struct file_operations digest_cache_notify_inodes_ops = {
>> +	.open = generic_file_open,
>> +	.write = write_notify_inodes,
>> +	.read = read_notify_inodes,
>> +	.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;
>> +	}
>> +
>> +	notify_inodes = securityfs_create_file("digest_cache_notify_inodes",
>> +					       0660, NULL, NULL,
>> +					       &digest_cache_notify_inodes_ops);
>> +	if (IS_ERR(notify_inodes)) {
>> +		ret = PTR_ERR(notify_inodes);
>> +		goto out_test;
>> +	}
>> +
>> +	ret = digest_cache_register_notifier(&digest_cache_notifier);
>> +	if (ret < 0)
>> +		goto out_notify_inodes;
>> +
>> +	return 0;
>> +
>> +out_notify_inodes:
>> +	securityfs_remove(notify_inodes);
>> +out_test:
>> +	securityfs_remove(test);
>> +out_kprobe:
>> +	unregister_kprobe(&kp);
>> +	return ret;
>> +}
>> +
>> +static void __exit digest_cache_test_fini(void)
>> +{
>> +	if (digest_cache)
>> +		digest_cache_put(digest_cache);
>> +
>> +	digest_cache_unregister_notifier(&digest_cache_notifier);
>> +	securityfs_remove(notify_inodes);
>> +	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");
> 
> 
> BR, Jarkko


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 01/14] lib: Add TLV parser
  2024-04-15 21:07     ` Randy Dunlap
@ 2024-04-16 14:23       ` Jarkko Sakkinen
  0 siblings, 0 replies; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-16 14:23 UTC (permalink / raw)
  To: Randy Dunlap, Roberto Sassu, corbet, paul, jmorris, serge, akpm,
	shuah, mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Tue Apr 16, 2024 at 12:07 AM EEST, Randy Dunlap wrote:
>
>
> On 4/15/24 12:19 PM, Jarkko Sakkinen wrote:
> > On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> >> From: Roberto Sassu <roberto.sassu@huawei.com>
> >>
> >> Add a parser of a generic TLV format:
> > 
> > What is TLV?
>
> type-length-value
>
> i.e., a descriptor that contains a value.
>
> IIUC.

Thanks Randy. If it had been "time-length-value (TLV)" then it would
have been obvious :-)

BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 02/14] security: Introduce the digest_cache LSM
  2024-04-16  7:09     ` Roberto Sassu
@ 2024-04-16 14:33       ` Jarkko Sakkinen
  2024-04-17 17:00         ` Roberto Sassu
  0 siblings, 1 reply; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-16 14:33 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Tue Apr 16, 2024 at 10:09 AM EEST, Roberto Sassu wrote:
> On Mon, 2024-04-15 at 22:31 +0300, Jarkko Sakkinen wrote:
> > On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> > > From: Roberto Sassu <roberto.sassu@huawei.com>
> > > 
> > > Introduce the digest_cache LSM, to collect digests from various sources
> > > (called digest lists), and to store them in kernel memory, in a set of hash
> > > tables forming a digest cache. Extracted digests can be used as reference
> > > values for integrity verification of file data or metadata.
> > > 
> > > A digest cache has three types of references: in the inode security blob of
> > > the digest list the digest cache was created from (dig_owner field); in the
> > > security blob of the inodes for which the digest cache is requested
> > > (dig_user field); a reference returned by digest_cache_get().
> > > 
> > > References are released with digest_cache_put(), in the first two cases
> > > when inodes are evicted from memory, in the last case when that function is
> > > explicitly called. Obtaining a digest cache reference means that the digest
> > > cache remains valid and cannot be freed until releasing it and until the
> > > total number of references (stored in the digest cache) becomes zero.
> > > 
> > > When digest_cache_get() is called on an inode to compare its digest with
> > > a reference value, the digest_cache LSM knows which digest cache to get
> > > from the new security.digest_list xattr added to that inode, which contains
> > > the file name of the desired digest list digests will be extracted from.
> > > 
> > > All digest lists are expected to be in the same directory, defined in the
> > > kernel config, and modifiable (with a later patch) at run-time through
> > > securityfs. When the digest_cache LSM reads the security.digest_list xattr,
> > > it uses its value as last path component, appended to the default path
> > > (unless the default path is a file). If an inode does not have that xattr,
> > > the default path is considered as the final destination.
> > > 
> > > The default path can be either a file or a directory. If it is a file, the
> > > digest_cache LSM always uses the same digest cache from that file to verify
> > > all inodes (the xattr, if present, is ignored). If it is a directory, and
> > > the inode to verify does not have the xattr, a subsequent patch will make
> > > it possible to iterate and lookup on the digest caches created from each
> > > directory entry.
> > > 
> > > Digest caches are created on demand, only when digest_cache_get() is
> > > called. The first time a digest cache is requested, the digest_cache LSM
> > > creates it and sets its reference in the dig_owner and dig_user fields of
> > > the respective inode security blobs. On the next requests, the previously
> > > set reference is returned, after incrementing the reference count.
> > > 
> > > Since there might be multiple digest_cache_get() calls for the same inode,
> > > or for different inodes pointing to the same digest list, dig_owner_mutex
> > > and dig_user_mutex have been introduced to protect the check and assignment
> > > of the digest cache reference in the inode security blob.
> > > 
> > > Contenders that didn't get the lock also have to wait until the digest
> > > cache is fully instantiated (when the bit INIT_IN_PROGRESS is cleared).
> > > Dig_owner_mutex cannot be used for waiting on the instantiation to avoid
> > > lock inversion with the inode lock for directories.
> > > 
> > > Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> > > ---
> > >  MAINTAINERS                                   |   6 +
> > >  include/linux/digest_cache.h                  |  32 ++
> > >  include/uapi/linux/lsm.h                      |   1 +
> > >  include/uapi/linux/xattr.h                    |   3 +
> > >  security/Kconfig                              |  11 +-
> > >  security/Makefile                             |   1 +
> > >  security/digest_cache/Kconfig                 |  16 +
> > >  security/digest_cache/Makefile                |   7 +
> > >  security/digest_cache/internal.h              |  86 ++++
> > >  security/digest_cache/main.c                  | 404 ++++++++++++++++++
> > >  security/security.c                           |   3 +-
> > >  .../selftests/lsm/lsm_list_modules_test.c     |   3 +
> > >  12 files changed, 567 insertions(+), 6 deletions(-)
> > >  create mode 100644 include/linux/digest_cache.h
> > >  create mode 100644 security/digest_cache/Kconfig
> > >  create mode 100644 security/digest_cache/Makefile
> > >  create mode 100644 security/digest_cache/internal.h
> > >  create mode 100644 security/digest_cache/main.c
> > > 
> > > diff --git a/MAINTAINERS b/MAINTAINERS
> > > index b1ca23ab8732..72801a88449c 100644
> > > --- a/MAINTAINERS
> > > +++ b/MAINTAINERS
> > > @@ -6193,6 +6193,12 @@ L:	linux-gpio@vger.kernel.org
> > >  S:	Maintained
> > >  F:	drivers/gpio/gpio-gpio-mm.c
> > >  
> > > +DIGEST_CACHE LSM
> > > +M:	Roberto Sassu <roberto.sassu@huawei.com>
> > > +L:	linux-security-module@vger.kernel.org
> > > +S:	Maintained
> > > +F:	security/digest_cache/
> > > +
> > >  DIGITEQ AUTOMOTIVE MGB4 V4L2 DRIVER
> > >  M:	Martin Tuma <martin.tuma@digiteqautomotive.com>
> > >  L:	linux-media@vger.kernel.org
> > 
> > Nit: afaik, MAINTAINER updates should be split.
>
> Ok, didn't know...

Yeah, it makes sense when you think it as a contract (or similar)
that you commit maintaining the full set of changes.

>
> > > diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
> > > new file mode 100644
> > > index 000000000000..e79f94a60b0f
> > > --- /dev/null
> > > +++ b/include/linux/digest_cache.h
> > > @@ -0,0 +1,32 @@
> > > +/* SPDX-License-Identifier: GPL-2.0 */
> > > +/*
> > > + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
> > > + *
> > > + * Author: Roberto Sassu <roberto.sassu@huawei.com>
> > 
> > Nit: Git has an author field in commit granularity so adding author
> > fields to files is sort of old world.
>
> Still like it if you don't mind...
>
> What happens when you get the source code from non-git?

Not strongly opposing this this but over time there will be
multiple contributors, which maps to multiple authors. Any
company trusting just what says in the header is probably
missing a legal department.

>
> > > + *
> > > + * Public API of the digest_cache LSM.
> > > + */
> > > +
> > > +#ifndef _LINUX_DIGEST_CACHE_H
> > > +#define _LINUX_DIGEST_CACHE_H
> > > +
> > > +#include <linux/fs.h>
> > > +
> > > +struct digest_cache;
> > 
> > Is this declaration necessary?
>
> I thought so. Users of the digest_cache LSM do:
>
> struct digest_cache *digest_cache;
>
>
> digest_cache = digest_cache_get(dentry);
>
>
> and so on. Those users don't know the internal layout of the
> digest_cache structure, but still pass it to the various functions.

According to my test this also fully works:

#include <stdio.h>

struct digest_cache *digest_cache_get(void)
{
        return NULL;
}

int main(void)
{
        struct digest_cache *foo;

        foo = digest_cache_get();

        return 0;
}

> > I don't think you need forward declaration here as this does compile:
> > 
> > #include <stdio.h>
> > 
> > struct digest_cache *digest_cache_get(void)
> > {
> >         return NULL;
> > }
> > 
> > int main(void)
> > {
> >         return (long)digest_cache_get();
> > }
> > 
> > 
> > > +
> > > +#ifdef CONFIG_SECURITY_DIGEST_CACHE
> > > +struct digest_cache *digest_cache_get(struct dentry *dentry);
> > > +void digest_cache_put(struct digest_cache *digest_cache);
> > > +
> > > +#else
> > > +static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
> > > +{
> > > +	return NULL;
> > > +}
> > > +
> > > +static inline void digest_cache_put(struct digest_cache *digest_cache)
> > > +{
> > > +}
> > > +
> > > +#endif /* CONFIG_SECURITY_DIGEST_CACHE */
> > > +#endif /* _LINUX_DIGEST_CACHE_H */
> > > diff --git a/include/uapi/linux/lsm.h b/include/uapi/linux/lsm.h
> > > index 33d8c9f4aa6b..832b3aea5c26 100644
> > > --- a/include/uapi/linux/lsm.h
> > > +++ b/include/uapi/linux/lsm.h
> > > @@ -64,6 +64,7 @@ struct lsm_ctx {
> > >  #define LSM_ID_LANDLOCK		110
> > >  #define LSM_ID_IMA		111
> > >  #define LSM_ID_EVM		112
> > > +#define LSM_ID_DIGEST_CACHE	113
> > >  
> > >  /*
> > >   * LSM_ATTR_XXX definitions identify different LSM attributes
> > > diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h
> > > index 9463db2dfa9d..8a58cf4bce65 100644
> > > --- a/include/uapi/linux/xattr.h
> > > +++ b/include/uapi/linux/xattr.h
> > > @@ -54,6 +54,9 @@
> > >  #define XATTR_IMA_SUFFIX "ima"
> > >  #define XATTR_NAME_IMA XATTR_SECURITY_PREFIX XATTR_IMA_SUFFIX
> > >  
> > > +#define XATTR_DIGEST_LIST_SUFFIX "digest_list"
> > > +#define XATTR_NAME_DIGEST_LIST XATTR_SECURITY_PREFIX XATTR_DIGEST_LIST_SUFFIX
> > > +
> > >  #define XATTR_SELINUX_SUFFIX "selinux"
> > >  #define XATTR_NAME_SELINUX XATTR_SECURITY_PREFIX XATTR_SELINUX_SUFFIX
> > >  
> > > diff --git a/security/Kconfig b/security/Kconfig
> > > index 52c9af08ad35..99f99cbd94cc 100644
> > > --- a/security/Kconfig
> > > +++ b/security/Kconfig
> > > @@ -194,6 +194,7 @@ source "security/yama/Kconfig"
> > >  source "security/safesetid/Kconfig"
> > >  source "security/lockdown/Kconfig"
> > >  source "security/landlock/Kconfig"
> > > +source "security/digest_cache/Kconfig"
> > >  
> > >  source "security/integrity/Kconfig"
> > >  
> > > @@ -233,11 +234,11 @@ endchoice
> > >  
> > >  config LSM
> > >  	string "Ordered list of enabled LSMs"
> > > -	default "landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
> > > -	default "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
> > > -	default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
> > > -	default "landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
> > > -	default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
> > > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
> > > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
> > > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
> > > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
> > > +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
> > >  	help
> > >  	  A comma-separated list of LSMs, in initialization order.
> > >  	  Any LSMs left off this list, except for those with order
> > > diff --git a/security/Makefile b/security/Makefile
> > > index 59f238490665..e9b43e7b715a 100644
> > > --- a/security/Makefile
> > > +++ b/security/Makefile
> > > @@ -25,6 +25,7 @@ obj-$(CONFIG_SECURITY_LOCKDOWN_LSM)	+= lockdown/
> > >  obj-$(CONFIG_CGROUPS)			+= device_cgroup.o
> > >  obj-$(CONFIG_BPF_LSM)			+= bpf/
> > >  obj-$(CONFIG_SECURITY_LANDLOCK)		+= landlock/
> > > +obj-$(CONFIG_SECURITY_DIGEST_CACHE)	+= digest_cache/
> > >  
> > >  # Object integrity file lists
> > >  obj-$(CONFIG_INTEGRITY)			+= integrity/
> > > diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
> > > new file mode 100644
> > > index 000000000000..e53fbf0779d6
> > > --- /dev/null
> > > +++ b/security/digest_cache/Kconfig
> > > @@ -0,0 +1,16 @@
> > > +# SPDX-License-Identifier: GPL-2.0
> > > +config SECURITY_DIGEST_CACHE
> > > +	bool "Digest_cache LSM"
> > > +	default n
> > > +	help
> > > +	  This option enables an LSM maintaining a cache of digests
> > > +	  (e.g. of file data or metadata).
> > > +
> > > +	  This LSM can support other kernel components in making access
> > > +	  control decisions.
> > > +
> > > +config DIGEST_LIST_DEFAULT_PATH
> > > +	string
> > > +	default "/etc/digest_lists"
> > > +	help
> > > +	  Default directory where digest_cache LSM expects to find digest lists.
> > > diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
> > > new file mode 100644
> > > index 000000000000..48848c41253e
> > > --- /dev/null
> > > +++ b/security/digest_cache/Makefile
> > > @@ -0,0 +1,7 @@
> > > +# SPDX-License-Identifier: GPL-2.0
> > > +#
> > > +# Makefile for building the digest_cache LSM.
> > > +
> > > +obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
> > > +
> > > +digest_cache-y := main.o
> > > diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> > > new file mode 100644
> > > index 000000000000..5f04844af3a5
> > > --- /dev/null
> > > +++ b/security/digest_cache/internal.h
> > > @@ -0,0 +1,86 @@
> > > +/* SPDX-License-Identifier: GPL-2.0 */
> > > +/*
> > > + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
> > > + *
> > > + * Author: Roberto Sassu <roberto.sassu@huawei.com>
> > 
> > ditto
> > 
> > > + *
> > > + * Internal header of the digest_cache LSM.
> > > + */
> > > +
> > > +#ifndef _DIGEST_CACHE_INTERNAL_H
> > > +#define _DIGEST_CACHE_INTERNAL_H
> > > +
> > > +#include <linux/lsm_hooks.h>
> > > +#include <linux/digest_cache.h>
> > > +
> > > +/* Digest cache bits in flags. */
> > > +#define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
> > > +
> > > +/**
> > > + * struct digest_cache - Digest cache
> > > + * @ref_count: Number of references to the digest cache
> > > + * @path_str: Path of the digest list the digest cache was created from
> > > + * @flags: Control flags
> > > + *
> > > + * This structure represents a cache of digests extracted from a digest list.
> > > + */
> > > +struct digest_cache {
> > > +	atomic_t ref_count;
> > > +	char *path_str;
> > > +	unsigned long flags;
> > > +};
> > > +
> > > +/**
> > > + * struct digest_cache_security - Digest cache pointers in inode security blob
> > > + * @dig_owner: Digest cache created from this inode
> > > + * @dig_owner_mutex: Protects @dig_owner
> > > + * @dig_user: Digest cache requested for this inode
> > > + * @dig_user_mutex: Protects @dig_user
> > > + *
> > > + * This structure contains references to digest caches, protected by their
> > > + * respective mutex.
> > > + */
> > > +struct digest_cache_security {
> > > +	struct digest_cache *dig_owner;
> > > +	struct mutex dig_owner_mutex;
> > > +	struct digest_cache *dig_user;
> > > +	struct mutex dig_user_mutex;
> > > +};
> > > +
> > > +extern struct lsm_blob_sizes digest_cache_blob_sizes;
> > > +extern char *default_path_str;
> > > +
> > > +static inline struct digest_cache_security *
> > > +digest_cache_get_security(const struct inode *inode)
> > > +{
> > > +	if (unlikely(!inode->i_security))
> > > +		return NULL;
> > > +
> > > +	return inode->i_security + digest_cache_blob_sizes.lbs_inode;
> > > +}
> > > +
> > > +static inline struct digest_cache *
> > > +digest_cache_ref(struct digest_cache *digest_cache)
> > > +{
> > > +	atomic_inc(&digest_cache->ref_count);
> > > +	pr_debug("Ref (+) digest cache %s (ref count: %d)\n",
> > > +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
> > > +	return digest_cache;
> > > +}
> > > +
> > > +static inline struct digest_cache *
> > > +digest_cache_unref(struct digest_cache *digest_cache)
> > > +{
> > > +	bool ref_is_zero = atomic_dec_and_test(&digest_cache->ref_count);
> > > +
> > > +	pr_debug("Ref (-) digest cache %s (ref count: %d)\n",
> > > +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
> > > +	return (ref_is_zero) ? digest_cache : NULL;
> > > +}
> > > +
> > > +/* main.c */
> > > +struct digest_cache *digest_cache_create(struct dentry *dentry,
> > > +					 struct path *digest_list_path,
> > > +					 char *path_str, char *filename);
> > > +
> > > +#endif /* _DIGEST_CACHE_INTERNAL_H */
> > > diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
> > > new file mode 100644
> > > index 000000000000..14dba8915e99
> > > --- /dev/null
> > > +++ b/security/digest_cache/main.c
> > > @@ -0,0 +1,404 @@
> > > +// 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 digest_cache LSM.
> > > + */
> > > +
> > > +#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 = 1;
> > > +static struct kmem_cache *digest_cache_cache __read_mostly;
> > > +
> > > +char *default_path_str = CONFIG_DIGEST_LIST_DEFAULT_PATH;
> > > +
> > > +/**
> > > + * digest_cache_alloc_init - Allocate and initialize a new digest cache
> > > + * @path_str: Path string of the digest list
> > > + * @filename: Digest list file name (can be an empty string)
> > > + *
> > > + * This function allocates and initializes a new digest cache.
> > > + *
> > > + * Return: A digest_cache structure on success, NULL on error.
> > > + */
> > > +static struct digest_cache *digest_cache_alloc_init(char *path_str,
> > > +						    char *filename)
> > > +{
> > > +	struct digest_cache *digest_cache;
> > > +
> > > +	digest_cache = kmem_cache_alloc(digest_cache_cache, GFP_KERNEL);
> > > +	if (!digest_cache)
> > > +		return digest_cache;
> > > +
> > > +	digest_cache->path_str = kasprintf(GFP_KERNEL, "%s%s%s", path_str,
> > > +					   filename[0] ? "/" : "", filename);
> > > +	if (!digest_cache->path_str) {
> > > +		kmem_cache_free(digest_cache_cache, digest_cache);
> > > +		return NULL;
> > > +	}
> > > +
> > > +	atomic_set(&digest_cache->ref_count, 1);
> > > +	digest_cache->flags = 0UL;
> > > +
> > > +	pr_debug("New digest cache %s (ref count: %d)\n",
> > > +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
> > 
> > Nit: kretprobe can be used to grab the same information easil and
> > do e.g. statistics and stuff like that. Traces for return values
> > are not very useful.
>
> I like to do dynamic debug, and I can control what part I want to
> debug. All these messages allowed me to debug efficiently when there
> was any issue. Would be a pity to remove them.

OK, in this case I missed the fact that the function is static so you can
ignore it!

>
> Thanks
>
> Roberto
>
> > > +
> > > +	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);
> > 
> > ditto for the above trace
> > 
> > > +	kfree(digest_cache->path_str);
> > > +	kmem_cache_free(digest_cache_cache, digest_cache);
> > > +}
> > > +
> > > +/**
> > > + * digest_cache_create - Create a digest cache
> > > + * @dentry: Dentry of the inode for which the digest cache will be used
> > > + * @digest_list_path: Path structure of the digest list
> > > + * @path_str: Path string of the digest list
> > > + * @filename: Digest list file name (can be an empty string)
> > > + *
> > > + * This function first locates, from the passed path, the digest list inode
> > > + * from which the digest cache will be created or retrieved (if it already
> > > + * exists).
> > > + *
> > > + * If dig_owner is NULL in the inode security blob, this function creates a
> > > + * new digest cache with reference count set to 1 (reference returned), sets
> > > + * it to dig_owner and consequently increments again the digest cache reference
> > > + * count.
> > > + *
> > > + * Otherwise, it simply increments the reference count of the existing
> > > + * dig_owner, since that reference is returned to the caller.
> > > + *
> > > + * Incrementing the reference count twice before calling path_put() ensures
> > > + * that the digest cache returned is valid even if the inode is evicted from
> > > + * memory (which decreases the reference count).
> > > + *
> > > + * Releasing the dig_owner_mutex lock does not mean that the digest cache is
> > > + * ready for use. digest_cache_create() callers that found a partially
> > > + * instantiated digest cache have to wait until the INIT_IN_PROGRESS bit is
> > > + * cleared by the caller that is actually creating that digest cache.
> > > + *
> > > + * Return: A new digest cache on success, NULL on error.
> > > + */
> > > +struct digest_cache *digest_cache_create(struct dentry *dentry,
> > > +					 struct path *digest_list_path,
> > > +					 char *path_str, char *filename)
> > > +{
> > > +	struct path file_path;
> > > +	struct digest_cache *digest_cache = NULL;
> > > +	struct digest_cache_security *dig_sec;
> > > +	struct inode *inode = d_backing_inode(digest_list_path->dentry);
> > > +	bool dig_owner_exists = false;
> > > +	int ret;
> > > +
> > > +	if (S_ISDIR(d_backing_inode(digest_list_path->dentry)->i_mode) &&
> > > +	    filename[0]) {
> > > +		ret = vfs_path_lookup(digest_list_path->dentry,
> > > +				      digest_list_path->mnt, filename, 0,
> > > +				      &file_path);
> > > +		if (ret < 0) {
> > > +			pr_debug("Cannot find digest list %s/%s\n", path_str,
> > > +				 filename);
> > > +			return NULL;
> > > +		}
> > > +
> > > +		digest_list_path = &file_path;
> > > +		inode = d_backing_inode(file_path.dentry);
> > > +
> > > +		/*
> > > +		 * Cannot request a digest cache for the same inode the
> > > +		 * digest cache is populated from.
> > > +		 */
> > > +		if (d_backing_inode(dentry) == inode) {
> > > +			pr_debug("Cannot request a digest cache for %s and use it as digest list\n",
> > > +				 dentry->d_name.name);
> > > +			goto out;
> > > +		}
> > > +
> > > +		/* 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",
> > > +				 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);
> > > +		dig_owner_exists = true;
> > > +		mutex_unlock(&dig_sec->dig_owner_mutex);
> > > +		goto exists;
> > > +	}
> > > +
> > > +	/* Ref. count is already 1 for this reference. */
> > > +	digest_cache = digest_cache_alloc_init(path_str, filename);
> > > +	if (!digest_cache) {
> > > +		mutex_unlock(&dig_sec->dig_owner_mutex);
> > > +		goto out;
> > > +	}
> > > +
> > > +	/* Increment ref. count for reference set to dig_owner. */
> > > +	dig_sec->dig_owner = digest_cache_ref(digest_cache);
> > > +
> > > +	/* Make the other lock contenders wait until creation complete. */
> > > +	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
> > > +	mutex_unlock(&dig_sec->dig_owner_mutex);
> > > +
> > > +	/* Creation complete, notify the other lock contenders. */
> > > +	clear_and_wake_up_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
> > > +exists:
> > > +	if (dig_owner_exists)
> > > +		/* Wait until creation complete. */
> > > +		wait_on_bit(&dig_sec->dig_owner->flags, INIT_IN_PROGRESS,
> > > +			    TASK_UNINTERRUPTIBLE);
> > > +out:
> > > +	if (digest_list_path == &file_path)
> > > +		path_put(&file_path);
> > > +
> > > +	return digest_cache;
> > > +}
> > > +
> > > +/**
> > > + * digest_cache_new - Retrieve digest list file name and request digest cache
> > > + * @dentry: Dentry of the inode for which the digest cache will be used
> > > + *
> > > + * This function locates the default path. If it is a file, it directly creates
> > > + * a digest cache from it. Otherwise, it reads the digest list file name from
> > > + * the security.digest_list xattr and requests the creation of a digest cache
> > > + * with that file name. If security.digest_list is not found, this function
> > > + * requests the creation of a digest cache on the parent directory.
> > > + *
> > > + * Return: A new digest cache on success, NULL on error.
> > > + */
> > > +static struct digest_cache *digest_cache_new(struct dentry *dentry)
> > > +{
> > > +	char filename[NAME_MAX + 1] = { 0 };
> > > +	struct digest_cache *digest_cache = NULL;
> > > +	struct path default_path;
> > > +	int ret;
> > > +
> > > +	ret = kern_path(default_path_str, 0, &default_path);
> > > +	if (ret < 0) {
> > > +		pr_debug("Cannot find path %s\n", default_path_str);
> > > +		return NULL;
> > > +	}
> > > +
> > > +	/* The default path is a file, no need to get xattr. */
> > > +	if (S_ISREG(d_backing_inode(default_path.dentry)->i_mode)) {
> > > +		pr_debug("Default path %s is a file, not reading %s xattr\n",
> > > +			 default_path_str, XATTR_NAME_DIGEST_LIST);
> > > +		goto create;
> > > +	} else if (!S_ISDIR(d_backing_inode(default_path.dentry)->i_mode)) {
> > > +		pr_debug("Default path %s must be either a file or a directory\n",
> > > +			 default_path_str);
> > > +		goto out;
> > > +	}
> > > +
> > > +	ret = vfs_getxattr(&nop_mnt_idmap, dentry, XATTR_NAME_DIGEST_LIST,
> > > +			   filename, sizeof(filename) - 1);
> > > +	if (ret <= 0) {
> > > +		pr_debug("Digest list path not found for file %s, using %s\n",
> > > +			 dentry->d_name.name, default_path_str);
> > > +		goto create;
> > > +	}
> > > +
> > > +	if (strchr(filename, '/')) {
> > > +		pr_debug("%s xattr should contain only a file name, got: %s\n",
> > > +			 XATTR_NAME_DIGEST_LIST, filename);
> > > +		goto out;
> > > +	}
> > > +
> > > +	pr_debug("Found %s xattr in %s, default path: %s, digest list: %s\n",
> > > +		 XATTR_NAME_DIGEST_LIST, dentry->d_name.name, default_path_str,
> > > +		 filename);
> > > +create:
> > > +	digest_cache = digest_cache_create(dentry, &default_path,
> > > +					   default_path_str, filename);
> > > +out:
> > > +	path_put(&default_path);
> > > +	return digest_cache;
> > > +}
> > > +
> > > +/**
> > > + * digest_cache_get - Get a digest cache for a given inode
> > > + * @dentry: Dentry of the inode for which the digest cache will be used
> > > + *
> > > + * This function tries to find a digest cache from the inode security blob of
> > > + * the passed dentry (dig_user field). If a digest cache was not found, it calls
> > > + * digest_cache_new() to create a new one. In both cases, it increments the
> > > + * digest cache reference count before returning the reference to the caller.
> > > + *
> > > + * The caller is responsible to call digest_cache_put() to release the digest
> > > + * cache reference returned.
> > > + *
> > > + * Lock dig_user_mutex to protect against concurrent requests to obtain a digest
> > > + * cache for the same inode, and to make other contenders wait until the first
> > > + * requester finishes the process.
> > > + *
> > > + * Return: A digest cache on success, NULL otherwise.
> > > + */
> > > +struct digest_cache *digest_cache_get(struct dentry *dentry)
> > > +{
> > > +	struct digest_cache_security *dig_sec;
> > > +	struct digest_cache *digest_cache = NULL;
> > > +	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);
> > > +
> > > +struct lsm_blob_sizes digest_cache_blob_sizes __ro_after_init = {
> > > +	.lbs_inode = sizeof(struct digest_cache_security),
> > > +};
> > > +
> > > +/**
> > > + * digest_cache_inode_alloc_security - Initialize inode security blob
> > > + * @inode: Inode for which the security blob is initialized
> > > + *
> > > + * This function initializes the digest_cache_security structure, directly
> > > + * stored in the inode security blob.
> > > + *
> > > + * Return: Zero.
> > > + */
> > > +static int digest_cache_inode_alloc_security(struct inode *inode)
> > > +{
> > > +	struct digest_cache_security *dig_sec;
> > > +
> > > +	/* The inode security blob is always allocated here. */
> > > +	dig_sec = digest_cache_get_security(inode);
> > > +	mutex_init(&dig_sec->dig_owner_mutex);
> > > +	mutex_init(&dig_sec->dig_user_mutex);
> > > +	return 0;
> > > +}
> > > +
> > > +/**
> > > + * digest_cache_inode_free_security - Release the digest cache references
> > > + * @inode: Inode for which the digest cache references are released
> > > + *
> > > + * Since the inode is being evicted, this function releases the non-needed
> > > + * references to the digest caches stored in the digest_cache_security
> > > + * structure.
> > > + */
> > > +static void digest_cache_inode_free_security(struct inode *inode)
> > > +{
> > > +	struct digest_cache_security *dig_sec;
> > > +
> > > +	dig_sec = digest_cache_get_security(inode);
> > > +	if (!dig_sec)
> > > +		return;
> > > +
> > > +	mutex_destroy(&dig_sec->dig_owner_mutex);
> > > +	mutex_destroy(&dig_sec->dig_user_mutex);
> > > +	if (dig_sec->dig_owner)
> > > +		digest_cache_put(dig_sec->dig_owner);
> > > +	if (dig_sec->dig_user)
> > > +		digest_cache_put(dig_sec->dig_user);
> > > +}
> > > +
> > > +static struct security_hook_list digest_cache_hooks[] __ro_after_init = {
> > > +	LSM_HOOK_INIT(inode_alloc_security, digest_cache_inode_alloc_security),
> > > +	LSM_HOOK_INIT(inode_free_security, digest_cache_inode_free_security),
> > > +};
> > > +
> > > +/**
> > > + * digest_cache_init_once - Initialize the digest cache structure
> > > + * @foo: Digest cache structure to initialize
> > > + *
> > > + * This function fills the digest cache structure with zeros.
> > > + */
> > > +static void digest_cache_init_once(void *foo)
> > > +{
> > > +	struct digest_cache *digest_cache = (struct digest_cache *)foo;
> > > +
> > > +	memset(digest_cache, 0, sizeof(*digest_cache));
> > > +}
> > > +
> > > +static const struct lsm_id digest_cache_lsmid = {
> > > +	.name = "digest_cache",
> > > +	.id = LSM_ID_DIGEST_CACHE,
> > > +};
> > > +
> > > +/**
> > > + * digest_cache_init - Initialize the digest_cache LSM
> > > + *
> > > + * Initialize the digest_cache LSM, by instantiating a cache for the
> > > + * digest_cache structure and by registering the digest_cache LSM hooks.
> > > + */
> > > +static int __init digest_cache_init(void)
> > > +{
> > > +	digest_cache_cache = kmem_cache_create("digest_cache_cache",
> > > +					       sizeof(struct digest_cache),
> > > +					       0, SLAB_PANIC,
> > > +					       digest_cache_init_once);
> > > +
> > > +	security_add_hooks(digest_cache_hooks, ARRAY_SIZE(digest_cache_hooks),
> > > +			   &digest_cache_lsmid);
> > > +	return 0;
> > > +}
> > > +
> > > +DEFINE_LSM(digest_cache) = {
> > > +	.name = "digest_cache",
> > > +	.enabled = &digest_cache_enabled,
> > > +	.init = digest_cache_init,
> > > +	.blobs = &digest_cache_blob_sizes,
> > > +};
> > > diff --git a/security/security.c b/security/security.c
> > > index cbdc9bebe802..cb084ed58617 100644
> > > --- a/security/security.c
> > > +++ b/security/security.c
> > > @@ -50,7 +50,8 @@
> > >  	(IS_ENABLED(CONFIG_BPF_LSM) ? 1 : 0) + \
> > >  	(IS_ENABLED(CONFIG_SECURITY_LANDLOCK) ? 1 : 0) + \
> > >  	(IS_ENABLED(CONFIG_IMA) ? 1 : 0) + \
> > > -	(IS_ENABLED(CONFIG_EVM) ? 1 : 0))
> > > +	(IS_ENABLED(CONFIG_EVM) ? 1 : 0) + \
> > > +	(IS_ENABLED(CONFIG_SECURITY_DIGEST_CACHE) ? 1 : 0))
> > >  
> > >  /*
> > >   * These are descriptions of the reasons that can be passed to the
> > > diff --git a/tools/testing/selftests/lsm/lsm_list_modules_test.c b/tools/testing/selftests/lsm/lsm_list_modules_test.c
> > > index 4d5d4cee2586..d00831edc582 100644
> > > --- a/tools/testing/selftests/lsm/lsm_list_modules_test.c
> > > +++ b/tools/testing/selftests/lsm/lsm_list_modules_test.c
> > > @@ -128,6 +128,9 @@ TEST(correct_lsm_list_modules)
> > >  		case LSM_ID_EVM:
> > >  			name = "evm";
> > >  			break;
> > > +		case LSM_ID_DIGEST_CACHE:
> > > +			name = "digest_cache";
> > > +			break;
> > >  		default:
> > >  			name = "INVALID";
> > >  			break;
> > 
> > BR, Jarkko

BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 03/14] digest_cache: Add securityfs interface
  2024-04-16 10:15     ` Roberto Sassu
@ 2024-04-16 14:38       ` Jarkko Sakkinen
  0 siblings, 0 replies; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-16 14:38 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Tue Apr 16, 2024 at 1:15 PM EEST, Roberto Sassu wrote:
> On 4/15/2024 9:32 PM, Jarkko Sakkinen wrote:
> > On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> >> From: Roberto Sassu <roberto.sassu@huawei.com>
> >>
> >> Add the digest_cache_path file in securityfs, 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/digest_cache/Kconfig    |  4 ++
> >>   security/digest_cache/Makefile   |  2 +-
> >>   security/digest_cache/internal.h |  1 +
> >>   security/digest_cache/main.c     | 10 +++-
> >>   security/digest_cache/secfs.c    | 87 ++++++++++++++++++++++++++++++++
> >>   5 files changed, 102 insertions(+), 2 deletions(-)
> >>   create mode 100644 security/digest_cache/secfs.c
> >>
> >> diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
> >> index e53fbf0779d6..dfabe5d6e3ca 100644
> >> --- a/security/digest_cache/Kconfig
> >> +++ b/security/digest_cache/Kconfig
> >> @@ -14,3 +14,7 @@ config DIGEST_LIST_DEFAULT_PATH
> >>   	default "/etc/digest_lists"
> >>   	help
> >>   	  Default directory where digest_cache LSM 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/digest_cache/Makefile b/security/digest_cache/Makefile
> >> index 48848c41253e..1330655e33b1 100644
> >> --- a/security/digest_cache/Makefile
> >> +++ b/security/digest_cache/Makefile
> >> @@ -4,4 +4,4 @@
> >>   
> >>   obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
> >>   
> >> -digest_cache-y := main.o
> >> +digest_cache-y := main.o secfs.o
> >> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> >> index 5f04844af3a5..bbf5eefe5c82 100644
> >> --- a/security/digest_cache/internal.h
> >> +++ b/security/digest_cache/internal.h
> >> @@ -49,6 +49,7 @@ struct digest_cache_security {
> >>   
> >>   extern struct lsm_blob_sizes digest_cache_blob_sizes;
> >>   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)
> >> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
> >> index 14dba8915e99..661c8d106791 100644
> >> --- a/security/digest_cache/main.c
> >> +++ b/security/digest_cache/main.c
> >> @@ -18,6 +18,9 @@ static struct kmem_cache *digest_cache_cache __read_mostly;
> >>   
> >>   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
> >> @@ -274,9 +277,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. */
> >> @@ -386,6 +392,8 @@ static const struct lsm_id digest_cache_lsmid = {
> >>    */
> >>   static int __init digest_cache_init(void)
> >>   {
> >> +	init_rwsem(&default_path_sem);
> >> +
> >>   	digest_cache_cache = kmem_cache_create("digest_cache_cache",
> >>   					       sizeof(struct digest_cache),
> >>   					       0, SLAB_PANIC,
> >> diff --git a/security/digest_cache/secfs.c b/security/digest_cache/secfs.c
> >> new file mode 100644
> >> index 000000000000..d3a37bf3588e
> >> --- /dev/null
> >> +++ b/security/digest_cache/secfs.c
> >> @@ -0,0 +1,87 @@
> >> +// SPDX-License-Identifier: GPL-2.0
> >> +/*
> >> + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
> >> + *
> >> + * Author: Roberto Sassu <roberto.sassu@huawei.com>
> >> + *
> >> + * Implement the securityfs interface of the digest_cache LSM.
> >> + */
> >> +
> >> +#define pr_fmt(fmt) "DIGEST CACHE: "fmt
> >> +#include <linux/security.h>
> >> +
> >> +#include "internal.h"
> >> +
> >> +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,
> >> +};
> >> +
> >> +static int __init digest_cache_path_init(void)
> >> +{
> >> +	default_path_dentry = securityfs_create_file("digest_cache_path", 0660,
> >> +						     NULL, NULL,
> >> +						     &default_path_ops);
> >> +	if (IS_ERR(default_path_dentry))
> >> +		return -EFAULT;
> > 
> > Nit: when overwriting error value with another error value it would be
> > best to document it with an inline comment. Otherwise, it is fine.
>
> Seems to make sense to return the right error. Will check why this one 
> (I probably took from somewhere).

Yeah, I mean often when I read legacy code from kernel and find
places like these I spend even few hours finding the root for
doing something like this so it really has value to do it when
the code is still fresh :-) Nothing wrong in the action itself
when it makes sense given the circumstances.

BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 10/14] digest cache: Prefetch digest lists if requested
  2024-04-16 10:34     ` Roberto Sassu
@ 2024-04-16 14:47       ` Jarkko Sakkinen
  0 siblings, 0 replies; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-16 14:47 UTC (permalink / raw)
  To: Roberto Sassu, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On Tue Apr 16, 2024 at 1:34 PM EEST, Roberto Sassu wrote:
> On 4/15/2024 9:42 PM, Jarkko Sakkinen wrote:
> > On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
> >> 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 digest_cache LSM is not exempted from this issue. Under the assumption
> >> that only the digest list is measured, and file measurements are omitted if
> >> their digest is found in that digest list, a PCR can be predictable only if
> >> all files belong to the same digest list. Otherwise, it will still be
> >> unpredictable, since files accessed in a non-deterministic order will cause
> >> digest lists to be measured in a non-deterministic order too.
> >>
> >> Overcome this issue, if prefetching is enabled, by searching a digest list
> >> file name in digest_list_dir_lookup_filename() among the entries of the
> >> linked list built by digest_cache_dir_create(). If the file name does not
> >> match, read the digest list to trigger its measurement. Otherwise, also
> >> create a digest cache and return that to the caller. Release the extra
> >> reference of the directory digest cache in digest_cache_new(), since it was
> >> only used for the search and it is not going to be returned.
> >>
> >> Prefetching needs to be explicitly enabled by setting the new
> >> security.dig_prefetch xattr to 1 in the directory containing the digest
> >> lists. The newly introduced function digest_cache_prefetch_requested()
> >> checks first if the DIR_PREFETCH bit is set in dig_owner, otherwise it
> >> reads the xattr. digest_cache_create() sets DIR_PREFETCH in dig_owner, if
> >> prefetching is enabled, before declaring the digest cache as initialized.
> >>
> >> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> >> ---
> >>   include/uapi/linux/xattr.h       |  3 +
> >>   security/digest_cache/dir.c      | 55 +++++++++++++++++-
> >>   security/digest_cache/internal.h | 11 +++-
> >>   security/digest_cache/main.c     | 95 +++++++++++++++++++++++++++++++-
> >>   security/digest_cache/populate.c |  8 ++-
> >>   security/digest_cache/verif.c    |  5 +-
> >>   6 files changed, 170 insertions(+), 7 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/digest_cache/dir.c b/security/digest_cache/dir.c
> >> index 7bfcdd5f7ef1..a7d203c15386 100644
> >> --- a/security/digest_cache/dir.c
> >> +++ b/security/digest_cache/dir.c
> >> @@ -54,6 +54,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;
> >> @@ -127,6 +128,7 @@ int digest_cache_dir_create(struct digest_cache *digest_cache,
> >>    * @digest_cache: Digest cache
> >>    * @digest: Digest to search
> >>    * @algo: Algorithm of the digest to search
> >> + * @filename: File name of the digest list to search
> >>    *
> >>    * This function iterates over the linked list created by
> >>    * digest_cache_dir_create() and looks up the digest in the digest cache of
> >> @@ -149,7 +151,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
> >>   		if (!dir_entry->digest_cache) {
> >>   			cache = digest_cache_create(dentry, digest_list_path,
> >>   						    digest_cache->path_str,
> >> -						    dir_entry->name);
> >> +						    dir_entry->name, false,
> >> +						    false);
> >>   			/* Ignore digest caches that cannot be instantiated. */
> >>   			if (!cache) {
> >>   				mutex_unlock(&dir_entry->digest_cache_mutex);
> >> @@ -158,6 +161,8 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
> >>   
> >>   			/* Consume extra ref. from digest_cache_create(). */
> >>   			dir_entry->digest_cache = cache;
> >> +			/* Digest list was read, mark entry as prefetched. */
> >> +			dir_entry->prefetched = true;
> >>   		}
> >>   		mutex_unlock(&dir_entry->digest_cache_mutex);
> >>   
> >> @@ -171,6 +176,54 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
> >>   	return 0UL;
> >>   }
> >>   
> >> +/**
> >> + * 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: Digest cache
> >> + * @filename: File name of the digest list to search
> >> + *
> >> + * This function iterates over the linked list created by
> >> + * digest_cache_dir_create() 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 returns the digest cache pointer from
> >> + * digest_cache_create() to the caller.
> >> + *
> >> + * Return: A digest cache pointer if the digest list if found, NULL otherwise.
> >> + */
> >> +struct digest_cache *
> >> +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);
> >> +
> >> +		dir_entry->prefetched = true;
> >> +		mutex_unlock(&dir_entry->digest_cache_mutex);
> >> +
> >> +		if (filename_found)
> >> +			return cache;
> >> +	}
> >> +
> >> +	return NULL;
> >> +}
> >> +
> >>   /**
> >>    * digest_cache_dir_free - Free the stored file list and put digest caches
> >>    * @digest_cache: Digest cache
> >> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
> >> index b7afca8e04da..c13b35f6b2c0 100644
> >> --- a/security/digest_cache/internal.h
> >> +++ b/security/digest_cache/internal.h
> >> @@ -17,6 +17,7 @@
> >>   #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
> >>   #define INVALID			1	/* Digest cache marked as invalid. */
> >>   #define IS_DIR			2	/* Digest cache created from dir. */
> >> +#define DIR_PREFETCH		3	/* Prefetching requested for dir. */
> >>   
> >>   /**
> >>    * struct readdir_callback - Structure to store information for dir iteration
> >> @@ -37,6 +38,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
> >> @@ -47,6 +49,7 @@ struct dir_entry {
> >>   	struct digest_cache *digest_cache;
> >>   	struct mutex digest_cache_mutex;
> >>   	unsigned int seq_num;
> >> +	bool prefetched;
> >>   	char name[];
> >>   } __packed;
> >>   
> >> @@ -205,7 +208,8 @@ 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);
> >>   
> >>   /* htable.c */
> >>   int digest_cache_htable_init(struct digest_cache *digest_cache, u64 num_digests,
> >> @@ -236,6 +240,11 @@ digest_cache_dir_lookup_digest(struct dentry *dentry,
> >>   			       struct path *digest_list_path,
> >>   			       struct digest_cache *digest_cache, u8 *digest,
> >>   			       enum hash_algo algo);
> >> +struct digest_cache *
> >> +digest_cache_dir_lookup_filename(struct dentry *dentry,
> >> +				 struct path *digest_list_path,
> >> +				 struct digest_cache *digest_cache,
> >> +				 char *filename);
> >>   void digest_cache_dir_free(struct digest_cache *digest_cache);
> >>   
> >>   #endif /* _DIGEST_CACHE_INTERNAL_H */
> >> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
> >> index 15f1486610a3..a5616fd07c1d 100644
> >> --- a/security/digest_cache/main.c
> >> +++ b/security/digest_cache/main.c
> >> @@ -83,6 +83,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
> >> @@ -109,7 +111,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)
> > 
> > 
> > This has enough parameters to consider separate "digest_cache_descriptor"
> > or whatever. In some architectures this is beyond register parameters,
> > which is IMHO good threshold to consider that.
> > 
> > This will make e.g. tracing easier as you have to map only one parameter
> > to a known struct to inspect the values.
>
> Uhm, seems still good (6 parameters). Sure, I can make a structure to 
> encapsulate the parameters, but will be used only for this purpose. Not 
> sure the code understandability will improve.

I don't want to dictate this! So if you think it is best like this I'm
cool too. So make your call and I won't come back to nag further :-)

>
> Thanks
>
> Roberto
>
> >>   {
> >>   	struct path file_path;
> >>   	struct digest_cache *digest_cache = NULL;
> >> @@ -148,6 +151,16 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
> >>   				 dentry->d_name.name);
> >>   			goto out;
> >>   		}
> >> +
> >> +		if (prefetch) {
> >> +			/* Fine to fail, we are just prefetching. */
> >> +			ret = digest_cache_populate(NULL, digest_list_path,
> >> +						    path_str, filename);
> >> +			pr_debug("Digest list %s/%s %s prefetched\n",
> >> +				 path_str, filename,
> >> +				 !ret ? "has been" : "cannot be");
> >> +			goto out;
> >> +		}
> >>   	}
> >>   
> >>   	dig_sec = digest_cache_get_security(inode);
> >> @@ -176,6 +189,11 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
> >>   
> >>   	/* Make the other lock contenders wait until creation complete. */
> >>   	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
> >> +
> >> +	/* Set DIR_PREFETCH if prefetching was requested. */
> >> +	if (prefetch_req)
> >> +		set_bit(DIR_PREFETCH, &digest_cache->flags);
> >> +
> >>   	mutex_unlock(&dig_sec->dig_owner_mutex);
> >>   
> >>   	if (S_ISREG(inode->i_mode)) {
> >> @@ -220,6 +238,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 new security.dig_prefetch xattr.
> >> + *
> >> + * Return: True if prefetching is requested, false otherwise.
> >> + */
> >> +static 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
> >> @@ -230,13 +294,19 @@ struct digest_cache *digest_cache_create(struct dentry *dentry,
> >>    * with that file name. If security.digest_list is not found, this function
> >>    * requests the creation of a digest cache on the parent directory.
> >>    *
> >> + * On prefetching, if the default path is a directory and if
> >> + * security.digest_list is found, this function first retrieves the directory
> >> + * digest cache, and then calls digest_cache_dir_lookup_filename() to retrieve
> >> + * the desired digest cache in that directory.
> >> + *
> >>    * Return: A new digest cache on success, NULL on error.
> >>    */
> >>   static struct digest_cache *digest_cache_new(struct dentry *dentry)
> >>   {
> >>   	char filename[NAME_MAX + 1] = { 0 };
> >> -	struct digest_cache *digest_cache = NULL;
> >> +	struct digest_cache *digest_cache = NULL, *found;
> >>   	struct path default_path;
> >> +	bool prefetch_req = false;
> >>   	int ret;
> >>   
> >>   	ret = kern_path(default_path_str, 0, &default_path);
> >> @@ -273,9 +343,28 @@ static struct digest_cache *digest_cache_new(struct dentry *dentry)
> >>   	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);
> >> +
> >> +	if (filename[0])
> >> +		prefetch_req = digest_cache_prefetch_requested(&default_path,
> >> +							default_path_str);
> >>   create:
> >> +	/* On prefetching, retrieve the directory digest cache. */
> >>   	digest_cache = digest_cache_create(dentry, &default_path,
> >> -					   default_path_str, filename);
> >> +					   default_path_str,
> >> +					   !prefetch_req ? filename : "",
> >> +					   prefetch_req, false);
> >> +	if (!digest_cache)
> >> +		goto out;
> >> +
> >> +	if (prefetch_req) {
> >> +		/* Find the digest cache with a matching file name. */
> >> +		found = digest_cache_dir_lookup_filename(dentry, &default_path,
> >> +							 digest_cache,
> >> +							 filename);
> >> +		/* Release ref. to the directory digest cache. */
> >> +		digest_cache_put(digest_cache);
> >> +		digest_cache = found;
> >> +	}
> >>   out:
> >>   	path_put(&default_path);
> >>   	return digest_cache;
> >> diff --git a/security/digest_cache/populate.c b/security/digest_cache/populate.c
> >> index 9c2fc2295310..17e7b011c367 100644
> >> --- a/security/digest_cache/populate.c
> >> +++ b/security/digest_cache/populate.c
> >> @@ -143,6 +143,12 @@ int digest_cache_populate(struct digest_cache *digest_cache,
> >>   		return ret;
> >>   	}
> >>   
> >> +	/* The caller wants just to read digest lists. */
> >> +	if (!digest_cache) {
> >> +		ret = 0;
> >> +		goto out_vfree;
> >> +	}
> >> +
> >>   	data_len = digest_cache_strip_modsig(data, ret);
> >>   
> >>   	/* Digest list parsers initialize the hash table and add the digests. */
> >> @@ -151,7 +157,7 @@ int digest_cache_populate(struct digest_cache *digest_cache,
> >>   	if (ret < 0)
> >>   		pr_debug("Error parsing digest list %s%s%s, ret: %d\n",
> >>   			 path_str, filename[0] ? "/" : "", filename, ret);
> >> -
> >> +out_vfree:
> >>   	vfree(data);
> >>   	return ret;
> >>   }
> >> diff --git a/security/digest_cache/verif.c b/security/digest_cache/verif.c
> >> index 04023240d3b4..c42ae93261e2 100644
> >> --- a/security/digest_cache/verif.c
> >> +++ b/security/digest_cache/verif.c
> >> @@ -33,7 +33,7 @@ static void free_verif(struct digest_cache_verif *verif)
> >>    * This function lets a verifier supply verification data about a digest list
> >>    * being read to populate the digest cache.
> >>    *
> >> - * Return: Zero on success, -ENOMEM if out of memory.
> >> + * Return: Zero on success, -ENOMEM if out of memory, -ENOENT on prefetching.
> >>    */
> >>   int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
> >>   			   size_t size)
> >> @@ -41,6 +41,9 @@ int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
> >>   	struct digest_cache *digest_cache = digest_cache_from_file_sec(file);
> >>   	struct digest_cache_verif *new_verif;
> >>   
> >> +	if (!digest_cache)
> >> +		return -ENOENT;
> >> +
> >>   	/*
> >>   	 * All allocations must be atomic (non-sleepable) since kprobe does not
> >>   	 * allow otherwise (kprobe is needed for testing).
> > 
> > 
> > BR, Jarkko


BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 02/14] security: Introduce the digest_cache LSM
  2024-04-16 14:33       ` Jarkko Sakkinen
@ 2024-04-17 17:00         ` Roberto Sassu
  0 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-17 17:00 UTC (permalink / raw)
  To: Jarkko Sakkinen, corbet, paul, jmorris, serge, akpm, shuah,
	mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold, Roberto Sassu

On 4/16/2024 4:33 PM, Jarkko Sakkinen wrote:
> On Tue Apr 16, 2024 at 10:09 AM EEST, Roberto Sassu wrote:
>> On Mon, 2024-04-15 at 22:31 +0300, Jarkko Sakkinen wrote:
>>> On Mon Apr 15, 2024 at 5:24 PM EEST, Roberto Sassu wrote:
>>>> From: Roberto Sassu <roberto.sassu@huawei.com>
>>>>
>>>> Introduce the digest_cache LSM, to collect digests from various sources
>>>> (called digest lists), and to store them in kernel memory, in a set of hash
>>>> tables forming a digest cache. Extracted digests can be used as reference
>>>> values for integrity verification of file data or metadata.
>>>>
>>>> A digest cache has three types of references: in the inode security blob of
>>>> the digest list the digest cache was created from (dig_owner field); in the
>>>> security blob of the inodes for which the digest cache is requested
>>>> (dig_user field); a reference returned by digest_cache_get().
>>>>
>>>> References are released with digest_cache_put(), in the first two cases
>>>> when inodes are evicted from memory, in the last case when that function is
>>>> explicitly called. Obtaining a digest cache reference means that the digest
>>>> cache remains valid and cannot be freed until releasing it and until the
>>>> total number of references (stored in the digest cache) becomes zero.
>>>>
>>>> When digest_cache_get() is called on an inode to compare its digest with
>>>> a reference value, the digest_cache LSM knows which digest cache to get
>>>> from the new security.digest_list xattr added to that inode, which contains
>>>> the file name of the desired digest list digests will be extracted from.
>>>>
>>>> All digest lists are expected to be in the same directory, defined in the
>>>> kernel config, and modifiable (with a later patch) at run-time through
>>>> securityfs. When the digest_cache LSM reads the security.digest_list xattr,
>>>> it uses its value as last path component, appended to the default path
>>>> (unless the default path is a file). If an inode does not have that xattr,
>>>> the default path is considered as the final destination.
>>>>
>>>> The default path can be either a file or a directory. If it is a file, the
>>>> digest_cache LSM always uses the same digest cache from that file to verify
>>>> all inodes (the xattr, if present, is ignored). If it is a directory, and
>>>> the inode to verify does not have the xattr, a subsequent patch will make
>>>> it possible to iterate and lookup on the digest caches created from each
>>>> directory entry.
>>>>
>>>> Digest caches are created on demand, only when digest_cache_get() is
>>>> called. The first time a digest cache is requested, the digest_cache LSM
>>>> creates it and sets its reference in the dig_owner and dig_user fields of
>>>> the respective inode security blobs. On the next requests, the previously
>>>> set reference is returned, after incrementing the reference count.
>>>>
>>>> Since there might be multiple digest_cache_get() calls for the same inode,
>>>> or for different inodes pointing to the same digest list, dig_owner_mutex
>>>> and dig_user_mutex have been introduced to protect the check and assignment
>>>> of the digest cache reference in the inode security blob.
>>>>
>>>> Contenders that didn't get the lock also have to wait until the digest
>>>> cache is fully instantiated (when the bit INIT_IN_PROGRESS is cleared).
>>>> Dig_owner_mutex cannot be used for waiting on the instantiation to avoid
>>>> lock inversion with the inode lock for directories.
>>>>
>>>> Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
>>>> ---
>>>>   MAINTAINERS                                   |   6 +
>>>>   include/linux/digest_cache.h                  |  32 ++
>>>>   include/uapi/linux/lsm.h                      |   1 +
>>>>   include/uapi/linux/xattr.h                    |   3 +
>>>>   security/Kconfig                              |  11 +-
>>>>   security/Makefile                             |   1 +
>>>>   security/digest_cache/Kconfig                 |  16 +
>>>>   security/digest_cache/Makefile                |   7 +
>>>>   security/digest_cache/internal.h              |  86 ++++
>>>>   security/digest_cache/main.c                  | 404 ++++++++++++++++++
>>>>   security/security.c                           |   3 +-
>>>>   .../selftests/lsm/lsm_list_modules_test.c     |   3 +
>>>>   12 files changed, 567 insertions(+), 6 deletions(-)
>>>>   create mode 100644 include/linux/digest_cache.h
>>>>   create mode 100644 security/digest_cache/Kconfig
>>>>   create mode 100644 security/digest_cache/Makefile
>>>>   create mode 100644 security/digest_cache/internal.h
>>>>   create mode 100644 security/digest_cache/main.c
>>>>
>>>> diff --git a/MAINTAINERS b/MAINTAINERS
>>>> index b1ca23ab8732..72801a88449c 100644
>>>> --- a/MAINTAINERS
>>>> +++ b/MAINTAINERS
>>>> @@ -6193,6 +6193,12 @@ L:	linux-gpio@vger.kernel.org
>>>>   S:	Maintained
>>>>   F:	drivers/gpio/gpio-gpio-mm.c
>>>>   
>>>> +DIGEST_CACHE LSM
>>>> +M:	Roberto Sassu <roberto.sassu@huawei.com>
>>>> +L:	linux-security-module@vger.kernel.org
>>>> +S:	Maintained
>>>> +F:	security/digest_cache/
>>>> +
>>>>   DIGITEQ AUTOMOTIVE MGB4 V4L2 DRIVER
>>>>   M:	Martin Tuma <martin.tuma@digiteqautomotive.com>
>>>>   L:	linux-media@vger.kernel.org
>>>
>>> Nit: afaik, MAINTAINER updates should be split.
>>
>> Ok, didn't know...
> 
> Yeah, it makes sense when you think it as a contract (or similar)
> that you commit maintaining the full set of changes.
> 
>>
>>>> diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
>>>> new file mode 100644
>>>> index 000000000000..e79f94a60b0f
>>>> --- /dev/null
>>>> +++ b/include/linux/digest_cache.h
>>>> @@ -0,0 +1,32 @@
>>>> +/* SPDX-License-Identifier: GPL-2.0 */
>>>> +/*
>>>> + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
>>>> + *
>>>> + * Author: Roberto Sassu <roberto.sassu@huawei.com>
>>>
>>> Nit: Git has an author field in commit granularity so adding author
>>> fields to files is sort of old world.
>>
>> Still like it if you don't mind...
>>
>> What happens when you get the source code from non-git?
> 
> Not strongly opposing this this but over time there will be
> multiple contributors, which maps to multiple authors. Any
> company trusting just what says in the header is probably
> missing a legal department.
> 
>>
>>>> + *
>>>> + * Public API of the digest_cache LSM.
>>>> + */
>>>> +
>>>> +#ifndef _LINUX_DIGEST_CACHE_H
>>>> +#define _LINUX_DIGEST_CACHE_H
>>>> +
>>>> +#include <linux/fs.h>
>>>> +
>>>> +struct digest_cache;
>>>
>>> Is this declaration necessary?
>>
>> I thought so. Users of the digest_cache LSM do:
>>
>> struct digest_cache *digest_cache;
>>
>>
>> digest_cache = digest_cache_get(dentry);
>>
>>
>> and so on. Those users don't know the internal layout of the
>> digest_cache structure, but still pass it to the various functions.
> 
> According to my test this also fully works:
> 
> #include <stdio.h>
> 
> struct digest_cache *digest_cache_get(void)
> {
>          return NULL;
> }
> 
> int main(void)
> {
>          struct digest_cache *foo;
> 
>          foo = digest_cache_get();
> 
>          return 0;
> }

Yes, it builds. Will remove the unnecessary line.

Thanks

Roberto

>>> I don't think you need forward declaration here as this does compile:
>>>
>>> #include <stdio.h>
>>>
>>> struct digest_cache *digest_cache_get(void)
>>> {
>>>          return NULL;
>>> }
>>>
>>> int main(void)
>>> {
>>>          return (long)digest_cache_get();
>>> }
>>>
>>>
>>>> +
>>>> +#ifdef CONFIG_SECURITY_DIGEST_CACHE
>>>> +struct digest_cache *digest_cache_get(struct dentry *dentry);
>>>> +void digest_cache_put(struct digest_cache *digest_cache);
>>>> +
>>>> +#else
>>>> +static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
>>>> +{
>>>> +	return NULL;
>>>> +}
>>>> +
>>>> +static inline void digest_cache_put(struct digest_cache *digest_cache)
>>>> +{
>>>> +}
>>>> +
>>>> +#endif /* CONFIG_SECURITY_DIGEST_CACHE */
>>>> +#endif /* _LINUX_DIGEST_CACHE_H */
>>>> diff --git a/include/uapi/linux/lsm.h b/include/uapi/linux/lsm.h
>>>> index 33d8c9f4aa6b..832b3aea5c26 100644
>>>> --- a/include/uapi/linux/lsm.h
>>>> +++ b/include/uapi/linux/lsm.h
>>>> @@ -64,6 +64,7 @@ struct lsm_ctx {
>>>>   #define LSM_ID_LANDLOCK		110
>>>>   #define LSM_ID_IMA		111
>>>>   #define LSM_ID_EVM		112
>>>> +#define LSM_ID_DIGEST_CACHE	113
>>>>   
>>>>   /*
>>>>    * LSM_ATTR_XXX definitions identify different LSM attributes
>>>> diff --git a/include/uapi/linux/xattr.h b/include/uapi/linux/xattr.h
>>>> index 9463db2dfa9d..8a58cf4bce65 100644
>>>> --- a/include/uapi/linux/xattr.h
>>>> +++ b/include/uapi/linux/xattr.h
>>>> @@ -54,6 +54,9 @@
>>>>   #define XATTR_IMA_SUFFIX "ima"
>>>>   #define XATTR_NAME_IMA XATTR_SECURITY_PREFIX XATTR_IMA_SUFFIX
>>>>   
>>>> +#define XATTR_DIGEST_LIST_SUFFIX "digest_list"
>>>> +#define XATTR_NAME_DIGEST_LIST XATTR_SECURITY_PREFIX XATTR_DIGEST_LIST_SUFFIX
>>>> +
>>>>   #define XATTR_SELINUX_SUFFIX "selinux"
>>>>   #define XATTR_NAME_SELINUX XATTR_SECURITY_PREFIX XATTR_SELINUX_SUFFIX
>>>>   
>>>> diff --git a/security/Kconfig b/security/Kconfig
>>>> index 52c9af08ad35..99f99cbd94cc 100644
>>>> --- a/security/Kconfig
>>>> +++ b/security/Kconfig
>>>> @@ -194,6 +194,7 @@ source "security/yama/Kconfig"
>>>>   source "security/safesetid/Kconfig"
>>>>   source "security/lockdown/Kconfig"
>>>>   source "security/landlock/Kconfig"
>>>> +source "security/digest_cache/Kconfig"
>>>>   
>>>>   source "security/integrity/Kconfig"
>>>>   
>>>> @@ -233,11 +234,11 @@ endchoice
>>>>   
>>>>   config LSM
>>>>   	string "Ordered list of enabled LSMs"
>>>> -	default "landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
>>>> -	default "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
>>>> -	default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
>>>> -	default "landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
>>>> -	default "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
>>>> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK
>>>> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR
>>>> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO
>>>> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,bpf" if DEFAULT_SECURITY_DAC
>>>> +	default "digest_cache,landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,bpf"
>>>>   	help
>>>>   	  A comma-separated list of LSMs, in initialization order.
>>>>   	  Any LSMs left off this list, except for those with order
>>>> diff --git a/security/Makefile b/security/Makefile
>>>> index 59f238490665..e9b43e7b715a 100644
>>>> --- a/security/Makefile
>>>> +++ b/security/Makefile
>>>> @@ -25,6 +25,7 @@ obj-$(CONFIG_SECURITY_LOCKDOWN_LSM)	+= lockdown/
>>>>   obj-$(CONFIG_CGROUPS)			+= device_cgroup.o
>>>>   obj-$(CONFIG_BPF_LSM)			+= bpf/
>>>>   obj-$(CONFIG_SECURITY_LANDLOCK)		+= landlock/
>>>> +obj-$(CONFIG_SECURITY_DIGEST_CACHE)	+= digest_cache/
>>>>   
>>>>   # Object integrity file lists
>>>>   obj-$(CONFIG_INTEGRITY)			+= integrity/
>>>> diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
>>>> new file mode 100644
>>>> index 000000000000..e53fbf0779d6
>>>> --- /dev/null
>>>> +++ b/security/digest_cache/Kconfig
>>>> @@ -0,0 +1,16 @@
>>>> +# SPDX-License-Identifier: GPL-2.0
>>>> +config SECURITY_DIGEST_CACHE
>>>> +	bool "Digest_cache LSM"
>>>> +	default n
>>>> +	help
>>>> +	  This option enables an LSM maintaining a cache of digests
>>>> +	  (e.g. of file data or metadata).
>>>> +
>>>> +	  This LSM can support other kernel components in making access
>>>> +	  control decisions.
>>>> +
>>>> +config DIGEST_LIST_DEFAULT_PATH
>>>> +	string
>>>> +	default "/etc/digest_lists"
>>>> +	help
>>>> +	  Default directory where digest_cache LSM expects to find digest lists.
>>>> diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
>>>> new file mode 100644
>>>> index 000000000000..48848c41253e
>>>> --- /dev/null
>>>> +++ b/security/digest_cache/Makefile
>>>> @@ -0,0 +1,7 @@
>>>> +# SPDX-License-Identifier: GPL-2.0
>>>> +#
>>>> +# Makefile for building the digest_cache LSM.
>>>> +
>>>> +obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
>>>> +
>>>> +digest_cache-y := main.o
>>>> diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
>>>> new file mode 100644
>>>> index 000000000000..5f04844af3a5
>>>> --- /dev/null
>>>> +++ b/security/digest_cache/internal.h
>>>> @@ -0,0 +1,86 @@
>>>> +/* SPDX-License-Identifier: GPL-2.0 */
>>>> +/*
>>>> + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
>>>> + *
>>>> + * Author: Roberto Sassu <roberto.sassu@huawei.com>
>>>
>>> ditto
>>>
>>>> + *
>>>> + * Internal header of the digest_cache LSM.
>>>> + */
>>>> +
>>>> +#ifndef _DIGEST_CACHE_INTERNAL_H
>>>> +#define _DIGEST_CACHE_INTERNAL_H
>>>> +
>>>> +#include <linux/lsm_hooks.h>
>>>> +#include <linux/digest_cache.h>
>>>> +
>>>> +/* Digest cache bits in flags. */
>>>> +#define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
>>>> +
>>>> +/**
>>>> + * struct digest_cache - Digest cache
>>>> + * @ref_count: Number of references to the digest cache
>>>> + * @path_str: Path of the digest list the digest cache was created from
>>>> + * @flags: Control flags
>>>> + *
>>>> + * This structure represents a cache of digests extracted from a digest list.
>>>> + */
>>>> +struct digest_cache {
>>>> +	atomic_t ref_count;
>>>> +	char *path_str;
>>>> +	unsigned long flags;
>>>> +};
>>>> +
>>>> +/**
>>>> + * struct digest_cache_security - Digest cache pointers in inode security blob
>>>> + * @dig_owner: Digest cache created from this inode
>>>> + * @dig_owner_mutex: Protects @dig_owner
>>>> + * @dig_user: Digest cache requested for this inode
>>>> + * @dig_user_mutex: Protects @dig_user
>>>> + *
>>>> + * This structure contains references to digest caches, protected by their
>>>> + * respective mutex.
>>>> + */
>>>> +struct digest_cache_security {
>>>> +	struct digest_cache *dig_owner;
>>>> +	struct mutex dig_owner_mutex;
>>>> +	struct digest_cache *dig_user;
>>>> +	struct mutex dig_user_mutex;
>>>> +};
>>>> +
>>>> +extern struct lsm_blob_sizes digest_cache_blob_sizes;
>>>> +extern char *default_path_str;
>>>> +
>>>> +static inline struct digest_cache_security *
>>>> +digest_cache_get_security(const struct inode *inode)
>>>> +{
>>>> +	if (unlikely(!inode->i_security))
>>>> +		return NULL;
>>>> +
>>>> +	return inode->i_security + digest_cache_blob_sizes.lbs_inode;
>>>> +}
>>>> +
>>>> +static inline struct digest_cache *
>>>> +digest_cache_ref(struct digest_cache *digest_cache)
>>>> +{
>>>> +	atomic_inc(&digest_cache->ref_count);
>>>> +	pr_debug("Ref (+) digest cache %s (ref count: %d)\n",
>>>> +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
>>>> +	return digest_cache;
>>>> +}
>>>> +
>>>> +static inline struct digest_cache *
>>>> +digest_cache_unref(struct digest_cache *digest_cache)
>>>> +{
>>>> +	bool ref_is_zero = atomic_dec_and_test(&digest_cache->ref_count);
>>>> +
>>>> +	pr_debug("Ref (-) digest cache %s (ref count: %d)\n",
>>>> +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
>>>> +	return (ref_is_zero) ? digest_cache : NULL;
>>>> +}
>>>> +
>>>> +/* main.c */
>>>> +struct digest_cache *digest_cache_create(struct dentry *dentry,
>>>> +					 struct path *digest_list_path,
>>>> +					 char *path_str, char *filename);
>>>> +
>>>> +#endif /* _DIGEST_CACHE_INTERNAL_H */
>>>> diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
>>>> new file mode 100644
>>>> index 000000000000..14dba8915e99
>>>> --- /dev/null
>>>> +++ b/security/digest_cache/main.c
>>>> @@ -0,0 +1,404 @@
>>>> +// 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 digest_cache LSM.
>>>> + */
>>>> +
>>>> +#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 = 1;
>>>> +static struct kmem_cache *digest_cache_cache __read_mostly;
>>>> +
>>>> +char *default_path_str = CONFIG_DIGEST_LIST_DEFAULT_PATH;
>>>> +
>>>> +/**
>>>> + * digest_cache_alloc_init - Allocate and initialize a new digest cache
>>>> + * @path_str: Path string of the digest list
>>>> + * @filename: Digest list file name (can be an empty string)
>>>> + *
>>>> + * This function allocates and initializes a new digest cache.
>>>> + *
>>>> + * Return: A digest_cache structure on success, NULL on error.
>>>> + */
>>>> +static struct digest_cache *digest_cache_alloc_init(char *path_str,
>>>> +						    char *filename)
>>>> +{
>>>> +	struct digest_cache *digest_cache;
>>>> +
>>>> +	digest_cache = kmem_cache_alloc(digest_cache_cache, GFP_KERNEL);
>>>> +	if (!digest_cache)
>>>> +		return digest_cache;
>>>> +
>>>> +	digest_cache->path_str = kasprintf(GFP_KERNEL, "%s%s%s", path_str,
>>>> +					   filename[0] ? "/" : "", filename);
>>>> +	if (!digest_cache->path_str) {
>>>> +		kmem_cache_free(digest_cache_cache, digest_cache);
>>>> +		return NULL;
>>>> +	}
>>>> +
>>>> +	atomic_set(&digest_cache->ref_count, 1);
>>>> +	digest_cache->flags = 0UL;
>>>> +
>>>> +	pr_debug("New digest cache %s (ref count: %d)\n",
>>>> +		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
>>>
>>> Nit: kretprobe can be used to grab the same information easil and
>>> do e.g. statistics and stuff like that. Traces for return values
>>> are not very useful.
>>
>> I like to do dynamic debug, and I can control what part I want to
>> debug. All these messages allowed me to debug efficiently when there
>> was any issue. Would be a pity to remove them.
> 
> OK, in this case I missed the fact that the function is static so you can
> ignore it!
> 
>>
>> Thanks
>>
>> Roberto
>>
>>>> +
>>>> +	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);
>>>
>>> ditto for the above trace
>>>
>>>> +	kfree(digest_cache->path_str);
>>>> +	kmem_cache_free(digest_cache_cache, digest_cache);
>>>> +}
>>>> +
>>>> +/**
>>>> + * digest_cache_create - Create a digest cache
>>>> + * @dentry: Dentry of the inode for which the digest cache will be used
>>>> + * @digest_list_path: Path structure of the digest list
>>>> + * @path_str: Path string of the digest list
>>>> + * @filename: Digest list file name (can be an empty string)
>>>> + *
>>>> + * This function first locates, from the passed path, the digest list inode
>>>> + * from which the digest cache will be created or retrieved (if it already
>>>> + * exists).
>>>> + *
>>>> + * If dig_owner is NULL in the inode security blob, this function creates a
>>>> + * new digest cache with reference count set to 1 (reference returned), sets
>>>> + * it to dig_owner and consequently increments again the digest cache reference
>>>> + * count.
>>>> + *
>>>> + * Otherwise, it simply increments the reference count of the existing
>>>> + * dig_owner, since that reference is returned to the caller.
>>>> + *
>>>> + * Incrementing the reference count twice before calling path_put() ensures
>>>> + * that the digest cache returned is valid even if the inode is evicted from
>>>> + * memory (which decreases the reference count).
>>>> + *
>>>> + * Releasing the dig_owner_mutex lock does not mean that the digest cache is
>>>> + * ready for use. digest_cache_create() callers that found a partially
>>>> + * instantiated digest cache have to wait until the INIT_IN_PROGRESS bit is
>>>> + * cleared by the caller that is actually creating that digest cache.
>>>> + *
>>>> + * Return: A new digest cache on success, NULL on error.
>>>> + */
>>>> +struct digest_cache *digest_cache_create(struct dentry *dentry,
>>>> +					 struct path *digest_list_path,
>>>> +					 char *path_str, char *filename)
>>>> +{
>>>> +	struct path file_path;
>>>> +	struct digest_cache *digest_cache = NULL;
>>>> +	struct digest_cache_security *dig_sec;
>>>> +	struct inode *inode = d_backing_inode(digest_list_path->dentry);
>>>> +	bool dig_owner_exists = false;
>>>> +	int ret;
>>>> +
>>>> +	if (S_ISDIR(d_backing_inode(digest_list_path->dentry)->i_mode) &&
>>>> +	    filename[0]) {
>>>> +		ret = vfs_path_lookup(digest_list_path->dentry,
>>>> +				      digest_list_path->mnt, filename, 0,
>>>> +				      &file_path);
>>>> +		if (ret < 0) {
>>>> +			pr_debug("Cannot find digest list %s/%s\n", path_str,
>>>> +				 filename);
>>>> +			return NULL;
>>>> +		}
>>>> +
>>>> +		digest_list_path = &file_path;
>>>> +		inode = d_backing_inode(file_path.dentry);
>>>> +
>>>> +		/*
>>>> +		 * Cannot request a digest cache for the same inode the
>>>> +		 * digest cache is populated from.
>>>> +		 */
>>>> +		if (d_backing_inode(dentry) == inode) {
>>>> +			pr_debug("Cannot request a digest cache for %s and use it as digest list\n",
>>>> +				 dentry->d_name.name);
>>>> +			goto out;
>>>> +		}
>>>> +
>>>> +		/* 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",
>>>> +				 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);
>>>> +		dig_owner_exists = true;
>>>> +		mutex_unlock(&dig_sec->dig_owner_mutex);
>>>> +		goto exists;
>>>> +	}
>>>> +
>>>> +	/* Ref. count is already 1 for this reference. */
>>>> +	digest_cache = digest_cache_alloc_init(path_str, filename);
>>>> +	if (!digest_cache) {
>>>> +		mutex_unlock(&dig_sec->dig_owner_mutex);
>>>> +		goto out;
>>>> +	}
>>>> +
>>>> +	/* Increment ref. count for reference set to dig_owner. */
>>>> +	dig_sec->dig_owner = digest_cache_ref(digest_cache);
>>>> +
>>>> +	/* Make the other lock contenders wait until creation complete. */
>>>> +	set_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
>>>> +	mutex_unlock(&dig_sec->dig_owner_mutex);
>>>> +
>>>> +	/* Creation complete, notify the other lock contenders. */
>>>> +	clear_and_wake_up_bit(INIT_IN_PROGRESS, &dig_sec->dig_owner->flags);
>>>> +exists:
>>>> +	if (dig_owner_exists)
>>>> +		/* Wait until creation complete. */
>>>> +		wait_on_bit(&dig_sec->dig_owner->flags, INIT_IN_PROGRESS,
>>>> +			    TASK_UNINTERRUPTIBLE);
>>>> +out:
>>>> +	if (digest_list_path == &file_path)
>>>> +		path_put(&file_path);
>>>> +
>>>> +	return digest_cache;
>>>> +}
>>>> +
>>>> +/**
>>>> + * digest_cache_new - Retrieve digest list file name and request digest cache
>>>> + * @dentry: Dentry of the inode for which the digest cache will be used
>>>> + *
>>>> + * This function locates the default path. If it is a file, it directly creates
>>>> + * a digest cache from it. Otherwise, it reads the digest list file name from
>>>> + * the security.digest_list xattr and requests the creation of a digest cache
>>>> + * with that file name. If security.digest_list is not found, this function
>>>> + * requests the creation of a digest cache on the parent directory.
>>>> + *
>>>> + * Return: A new digest cache on success, NULL on error.
>>>> + */
>>>> +static struct digest_cache *digest_cache_new(struct dentry *dentry)
>>>> +{
>>>> +	char filename[NAME_MAX + 1] = { 0 };
>>>> +	struct digest_cache *digest_cache = NULL;
>>>> +	struct path default_path;
>>>> +	int ret;
>>>> +
>>>> +	ret = kern_path(default_path_str, 0, &default_path);
>>>> +	if (ret < 0) {
>>>> +		pr_debug("Cannot find path %s\n", default_path_str);
>>>> +		return NULL;
>>>> +	}
>>>> +
>>>> +	/* The default path is a file, no need to get xattr. */
>>>> +	if (S_ISREG(d_backing_inode(default_path.dentry)->i_mode)) {
>>>> +		pr_debug("Default path %s is a file, not reading %s xattr\n",
>>>> +			 default_path_str, XATTR_NAME_DIGEST_LIST);
>>>> +		goto create;
>>>> +	} else if (!S_ISDIR(d_backing_inode(default_path.dentry)->i_mode)) {
>>>> +		pr_debug("Default path %s must be either a file or a directory\n",
>>>> +			 default_path_str);
>>>> +		goto out;
>>>> +	}
>>>> +
>>>> +	ret = vfs_getxattr(&nop_mnt_idmap, dentry, XATTR_NAME_DIGEST_LIST,
>>>> +			   filename, sizeof(filename) - 1);
>>>> +	if (ret <= 0) {
>>>> +		pr_debug("Digest list path not found for file %s, using %s\n",
>>>> +			 dentry->d_name.name, default_path_str);
>>>> +		goto create;
>>>> +	}
>>>> +
>>>> +	if (strchr(filename, '/')) {
>>>> +		pr_debug("%s xattr should contain only a file name, got: %s\n",
>>>> +			 XATTR_NAME_DIGEST_LIST, filename);
>>>> +		goto out;
>>>> +	}
>>>> +
>>>> +	pr_debug("Found %s xattr in %s, default path: %s, digest list: %s\n",
>>>> +		 XATTR_NAME_DIGEST_LIST, dentry->d_name.name, default_path_str,
>>>> +		 filename);
>>>> +create:
>>>> +	digest_cache = digest_cache_create(dentry, &default_path,
>>>> +					   default_path_str, filename);
>>>> +out:
>>>> +	path_put(&default_path);
>>>> +	return digest_cache;
>>>> +}
>>>> +
>>>> +/**
>>>> + * digest_cache_get - Get a digest cache for a given inode
>>>> + * @dentry: Dentry of the inode for which the digest cache will be used
>>>> + *
>>>> + * This function tries to find a digest cache from the inode security blob of
>>>> + * the passed dentry (dig_user field). If a digest cache was not found, it calls
>>>> + * digest_cache_new() to create a new one. In both cases, it increments the
>>>> + * digest cache reference count before returning the reference to the caller.
>>>> + *
>>>> + * The caller is responsible to call digest_cache_put() to release the digest
>>>> + * cache reference returned.
>>>> + *
>>>> + * Lock dig_user_mutex to protect against concurrent requests to obtain a digest
>>>> + * cache for the same inode, and to make other contenders wait until the first
>>>> + * requester finishes the process.
>>>> + *
>>>> + * Return: A digest cache on success, NULL otherwise.
>>>> + */
>>>> +struct digest_cache *digest_cache_get(struct dentry *dentry)
>>>> +{
>>>> +	struct digest_cache_security *dig_sec;
>>>> +	struct digest_cache *digest_cache = NULL;
>>>> +	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);
>>>> +
>>>> +struct lsm_blob_sizes digest_cache_blob_sizes __ro_after_init = {
>>>> +	.lbs_inode = sizeof(struct digest_cache_security),
>>>> +};
>>>> +
>>>> +/**
>>>> + * digest_cache_inode_alloc_security - Initialize inode security blob
>>>> + * @inode: Inode for which the security blob is initialized
>>>> + *
>>>> + * This function initializes the digest_cache_security structure, directly
>>>> + * stored in the inode security blob.
>>>> + *
>>>> + * Return: Zero.
>>>> + */
>>>> +static int digest_cache_inode_alloc_security(struct inode *inode)
>>>> +{
>>>> +	struct digest_cache_security *dig_sec;
>>>> +
>>>> +	/* The inode security blob is always allocated here. */
>>>> +	dig_sec = digest_cache_get_security(inode);
>>>> +	mutex_init(&dig_sec->dig_owner_mutex);
>>>> +	mutex_init(&dig_sec->dig_user_mutex);
>>>> +	return 0;
>>>> +}
>>>> +
>>>> +/**
>>>> + * digest_cache_inode_free_security - Release the digest cache references
>>>> + * @inode: Inode for which the digest cache references are released
>>>> + *
>>>> + * Since the inode is being evicted, this function releases the non-needed
>>>> + * references to the digest caches stored in the digest_cache_security
>>>> + * structure.
>>>> + */
>>>> +static void digest_cache_inode_free_security(struct inode *inode)
>>>> +{
>>>> +	struct digest_cache_security *dig_sec;
>>>> +
>>>> +	dig_sec = digest_cache_get_security(inode);
>>>> +	if (!dig_sec)
>>>> +		return;
>>>> +
>>>> +	mutex_destroy(&dig_sec->dig_owner_mutex);
>>>> +	mutex_destroy(&dig_sec->dig_user_mutex);
>>>> +	if (dig_sec->dig_owner)
>>>> +		digest_cache_put(dig_sec->dig_owner);
>>>> +	if (dig_sec->dig_user)
>>>> +		digest_cache_put(dig_sec->dig_user);
>>>> +}
>>>> +
>>>> +static struct security_hook_list digest_cache_hooks[] __ro_after_init = {
>>>> +	LSM_HOOK_INIT(inode_alloc_security, digest_cache_inode_alloc_security),
>>>> +	LSM_HOOK_INIT(inode_free_security, digest_cache_inode_free_security),
>>>> +};
>>>> +
>>>> +/**
>>>> + * digest_cache_init_once - Initialize the digest cache structure
>>>> + * @foo: Digest cache structure to initialize
>>>> + *
>>>> + * This function fills the digest cache structure with zeros.
>>>> + */
>>>> +static void digest_cache_init_once(void *foo)
>>>> +{
>>>> +	struct digest_cache *digest_cache = (struct digest_cache *)foo;
>>>> +
>>>> +	memset(digest_cache, 0, sizeof(*digest_cache));
>>>> +}
>>>> +
>>>> +static const struct lsm_id digest_cache_lsmid = {
>>>> +	.name = "digest_cache",
>>>> +	.id = LSM_ID_DIGEST_CACHE,
>>>> +};
>>>> +
>>>> +/**
>>>> + * digest_cache_init - Initialize the digest_cache LSM
>>>> + *
>>>> + * Initialize the digest_cache LSM, by instantiating a cache for the
>>>> + * digest_cache structure and by registering the digest_cache LSM hooks.
>>>> + */
>>>> +static int __init digest_cache_init(void)
>>>> +{
>>>> +	digest_cache_cache = kmem_cache_create("digest_cache_cache",
>>>> +					       sizeof(struct digest_cache),
>>>> +					       0, SLAB_PANIC,
>>>> +					       digest_cache_init_once);
>>>> +
>>>> +	security_add_hooks(digest_cache_hooks, ARRAY_SIZE(digest_cache_hooks),
>>>> +			   &digest_cache_lsmid);
>>>> +	return 0;
>>>> +}
>>>> +
>>>> +DEFINE_LSM(digest_cache) = {
>>>> +	.name = "digest_cache",
>>>> +	.enabled = &digest_cache_enabled,
>>>> +	.init = digest_cache_init,
>>>> +	.blobs = &digest_cache_blob_sizes,
>>>> +};
>>>> diff --git a/security/security.c b/security/security.c
>>>> index cbdc9bebe802..cb084ed58617 100644
>>>> --- a/security/security.c
>>>> +++ b/security/security.c
>>>> @@ -50,7 +50,8 @@
>>>>   	(IS_ENABLED(CONFIG_BPF_LSM) ? 1 : 0) + \
>>>>   	(IS_ENABLED(CONFIG_SECURITY_LANDLOCK) ? 1 : 0) + \
>>>>   	(IS_ENABLED(CONFIG_IMA) ? 1 : 0) + \
>>>> -	(IS_ENABLED(CONFIG_EVM) ? 1 : 0))
>>>> +	(IS_ENABLED(CONFIG_EVM) ? 1 : 0) + \
>>>> +	(IS_ENABLED(CONFIG_SECURITY_DIGEST_CACHE) ? 1 : 0))
>>>>   
>>>>   /*
>>>>    * These are descriptions of the reasons that can be passed to the
>>>> diff --git a/tools/testing/selftests/lsm/lsm_list_modules_test.c b/tools/testing/selftests/lsm/lsm_list_modules_test.c
>>>> index 4d5d4cee2586..d00831edc582 100644
>>>> --- a/tools/testing/selftests/lsm/lsm_list_modules_test.c
>>>> +++ b/tools/testing/selftests/lsm/lsm_list_modules_test.c
>>>> @@ -128,6 +128,9 @@ TEST(correct_lsm_list_modules)
>>>>   		case LSM_ID_EVM:
>>>>   			name = "evm";
>>>>   			break;
>>>> +		case LSM_ID_DIGEST_CACHE:
>>>> +			name = "digest_cache";
>>>> +			break;
>>>>   		default:
>>>>   			name = "INVALID";
>>>>   			break;
>>>
>>> BR, Jarkko
> 
> BR, Jarkko


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 00/14] security: digest_cache LSM
       [not found]   ` <66201cd2.df0a0220.a8ad5.6fbaSMTPIN_ADDED_BROKEN@mx.google.com>
@ 2024-04-19 11:18     ` Bagas Sanjaya
  2024-04-19 20:05       ` Jarkko Sakkinen
  2024-04-19 23:29       ` Roberto Sassu
  0 siblings, 2 replies; 43+ messages in thread
From: Bagas Sanjaya @ 2024-04-19 11:18 UTC (permalink / raw)
  To: Roberto Sassu, Roberto Sassu, corbet, paul, jmorris, serge, akpm,
	shuah, mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold

On 4/18/24 02:02, Roberto Sassu wrote:
> 
> 72374d71c315
> 
> Roberto
> 

Still FTA (fail to apply), unfortunately.

-- 
An old man doll... just what I always wanted! - Clara


^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 00/14] security: digest_cache LSM
  2024-04-19 11:18     ` Bagas Sanjaya
@ 2024-04-19 20:05       ` Jarkko Sakkinen
  2024-04-19 23:29       ` Roberto Sassu
  1 sibling, 0 replies; 43+ messages in thread
From: Jarkko Sakkinen @ 2024-04-19 20:05 UTC (permalink / raw)
  To: Bagas Sanjaya, Roberto Sassu, Roberto Sassu, corbet, paul,
	jmorris, serge, akpm, shuah, mcoquelin.stm32, alexandre.torgue,
	mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold

On Fri Apr 19, 2024 at 2:18 PM EEST, Bagas Sanjaya wrote:
> On 4/18/24 02:02, Roberto Sassu wrote:
> > 
> > 72374d71c315
> > 
> > Roberto
> > 
>
> Still FTA (fail to apply), unfortunately.

Robert, quick suggestion.

Maybe pick recent rc for the patch set, rebase on top of that and
document that to the cover letter?

In git: "git rebase <version>" when on branch with the patches
applied.

BR, Jarkko

^ permalink raw reply	[flat|nested] 43+ messages in thread

* Re: [PATCH v4 00/14] security: digest_cache LSM
  2024-04-19 11:18     ` Bagas Sanjaya
  2024-04-19 20:05       ` Jarkko Sakkinen
@ 2024-04-19 23:29       ` Roberto Sassu
  1 sibling, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-19 23:29 UTC (permalink / raw)
  To: Bagas Sanjaya, Roberto Sassu, corbet, paul, jmorris, serge, akpm,
	shuah, mcoquelin.stm32, alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, zohar, dmitry.kasatkin, linux-integrity, wufan, pbrobinson,
	zbyszek, hch, mjg59, pmatilai, jannh, dhowells, jikos, mkoutny,
	ppavlu, petr.vorel, mzerqung, kgold

On 4/19/2024 4:18 AM, Bagas Sanjaya wrote:
> On 4/18/24 02:02, Roberto Sassu wrote:
>>
>> 72374d71c315
>>
>> Roberto
>>
> 
> Still FTA (fail to apply), unfortunately.

Sorry, looks like I didn't regenerate the patches after rebasing to the 
latest kernel. The current ones are still based on 6.8-rc3, but they 
still require some additional patches that I picked up.

Will send the new version with Jarkko suggestions implemented.

Thanks

Roberto



^ permalink raw reply	[flat|nested] 43+ messages in thread

* [PATCH v4 00/14] security: digest_cache LSM
@ 2024-04-15 14:16 Roberto Sassu
  0 siblings, 0 replies; 43+ messages in thread
From: Roberto Sassu @ 2024-04-15 14:16 UTC (permalink / raw)
  To: corbet, paul, jmorris, serge, akpm, shuah, mcoquelin.stm32,
	alexandre.torgue, mic
  Cc: linux-security-module, linux-doc, linux-kernel, linux-kselftest,
	bpf, Roberto Sassu

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 the large user
base, due to requiring higher than desired constraints on performance,
flexibility and configurability, that only security conscious people are
willing to accept.

This is where the new digest_cache LSM comes into play, it offers
additional support for new and existing integrity solutions, to make
them faster and easier to deploy.

The full documentation with the motivation and the solution details can be
found in patch 14.

The IMA integration patch set will be introduced separately. Also a PoC
based on the current version of IPE can be provided.

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
  security: Introduce the digest_cache LSM
  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
  digest_cache: Notify digest cache events
  selftests/digest_cache: Add selftests for digest_cache LSM
  docs: Add documentation of the digest_cache LSM

 Documentation/security/digest_cache.rst       | 763 ++++++++++++++++
 Documentation/security/index.rst              |   1 +
 MAINTAINERS                                   |  16 +
 include/linux/digest_cache.h                  | 117 +++
 include/linux/kernel_read_file.h              |   1 +
 include/linux/tlv_parser.h                    |  28 +
 include/uapi/linux/lsm.h                      |   1 +
 include/uapi/linux/tlv_digest_list.h          |  72 ++
 include/uapi/linux/tlv_parser.h               |  59 ++
 include/uapi/linux/xattr.h                    |   6 +
 lib/Kconfig                                   |   3 +
 lib/Makefile                                  |   3 +
 lib/tlv_parser.c                              | 214 +++++
 lib/tlv_parser.h                              |  17 +
 security/Kconfig                              |  11 +-
 security/Makefile                             |   1 +
 security/digest_cache/Kconfig                 |  33 +
 security/digest_cache/Makefile                |  11 +
 security/digest_cache/dir.c                   | 252 ++++++
 security/digest_cache/htable.c                | 268 ++++++
 security/digest_cache/internal.h              | 290 +++++++
 security/digest_cache/main.c                  | 570 ++++++++++++
 security/digest_cache/modsig.c                |  66 ++
 security/digest_cache/notifier.c              | 135 +++
 security/digest_cache/parsers/parsers.h       |  15 +
 security/digest_cache/parsers/rpm.c           | 223 +++++
 security/digest_cache/parsers/tlv.c           | 299 +++++++
 security/digest_cache/populate.c              | 163 ++++
 security/digest_cache/reset.c                 | 235 +++++
 security/digest_cache/secfs.c                 |  87 ++
 security/digest_cache/verif.c                 | 119 +++
 security/security.c                           |   3 +-
 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 | 815 ++++++++++++++++++
 tools/testing/selftests/digest_cache/common.c |  78 ++
 tools/testing/selftests/digest_cache/common.h | 135 +++
 .../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     | 564 ++++++++++++
 .../selftests/lsm/lsm_list_modules_test.c     |   3 +
 46 files changed, 6047 insertions(+), 6 deletions(-)
 create mode 100644 Documentation/security/digest_cache.rst
 create mode 100644 include/linux/digest_cache.h
 create mode 100644 include/linux/tlv_parser.h
 create mode 100644 include/uapi/linux/tlv_digest_list.h
 create mode 100644 include/uapi/linux/tlv_parser.h
 create mode 100644 lib/tlv_parser.c
 create mode 100644 lib/tlv_parser.h
 create mode 100644 security/digest_cache/Kconfig
 create mode 100644 security/digest_cache/Makefile
 create mode 100644 security/digest_cache/dir.c
 create mode 100644 security/digest_cache/htable.c
 create mode 100644 security/digest_cache/internal.h
 create mode 100644 security/digest_cache/main.c
 create mode 100644 security/digest_cache/modsig.c
 create mode 100644 security/digest_cache/notifier.c
 create mode 100644 security/digest_cache/parsers/parsers.h
 create mode 100644 security/digest_cache/parsers/rpm.c
 create mode 100644 security/digest_cache/parsers/tlv.c
 create mode 100644 security/digest_cache/populate.c
 create mode 100644 security/digest_cache/reset.c
 create mode 100644 security/digest_cache/secfs.c
 create mode 100644 security/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

-- 
2.34.1


^ permalink raw reply	[flat|nested] 43+ messages in thread

end of thread, other threads:[~2024-04-19 23:30 UTC | newest]

Thread overview: 43+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-04-15 14:24 [PATCH v4 00/14] security: digest_cache LSM Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 01/14] lib: Add TLV parser Roberto Sassu
2024-04-15 19:19   ` Jarkko Sakkinen
2024-04-15 21:07     ` Randy Dunlap
2024-04-16 14:23       ` Jarkko Sakkinen
2024-04-15 14:24 ` [PATCH v4 02/14] security: Introduce the digest_cache LSM Roberto Sassu
2024-04-15 19:31   ` Jarkko Sakkinen
2024-04-16  7:09     ` Roberto Sassu
2024-04-16 14:33       ` Jarkko Sakkinen
2024-04-17 17:00         ` Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 03/14] digest_cache: Add securityfs interface Roberto Sassu
2024-04-15 19:32   ` Jarkko Sakkinen
2024-04-16 10:15     ` Roberto Sassu
2024-04-16 14:38       ` Jarkko Sakkinen
2024-04-15 14:24 ` [PATCH v4 04/14] digest_cache: Add hash tables and operations Roberto Sassu
2024-04-15 19:36   ` Jarkko Sakkinen
2024-04-16 10:28     ` Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 05/14] digest_cache: Populate the digest cache from a digest list Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 06/14] digest_cache: Parse tlv digest lists Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 07/14] digest_cache: Parse rpm " Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 08/14] digest_cache: Add management of verification data Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 09/14] digest_cache: Add support for directories Roberto Sassu
2024-04-15 19:39   ` Jarkko Sakkinen
2024-04-16 10:30     ` Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 10/14] digest cache: Prefetch digest lists if requested Roberto Sassu
2024-04-15 19:42   ` Jarkko Sakkinen
2024-04-16 10:34     ` Roberto Sassu
2024-04-16 14:47       ` Jarkko Sakkinen
2024-04-15 14:24 ` [PATCH v4 11/14] digest_cache: Reset digest cache on file/directory change Roberto Sassu
2024-04-15 19:44   ` Jarkko Sakkinen
2024-04-16 10:37     ` Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 12/14] digest_cache: Notify digest cache events Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 13/14] selftests/digest_cache: Add selftests for digest_cache LSM Roberto Sassu
2024-04-15 19:47   ` Jarkko Sakkinen
2024-04-16 10:39     ` Roberto Sassu
2024-04-15 14:24 ` [PATCH v4 14/14] docs: Add documentation of the " Roberto Sassu
2024-04-15 19:18 ` [PATCH v4 00/14] security: " Jarkko Sakkinen
2024-04-16  6:56   ` Roberto Sassu
2024-04-16  4:49 ` Bagas Sanjaya
     [not found]   ` <66201cd2.df0a0220.a8ad5.6fbaSMTPIN_ADDED_BROKEN@mx.google.com>
2024-04-19 11:18     ` Bagas Sanjaya
2024-04-19 20:05       ` Jarkko Sakkinen
2024-04-19 23:29       ` Roberto Sassu
  -- strict thread matches above, loose matches on Subject: below --
2024-04-15 14:16 Roberto Sassu

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).