diff mbox series

[v5,10/14] digest_cache: Add support for directories

Message ID 20240905150543.3766895-11-roberto.sassu@huaweicloud.com (mailing list archive)
State New
Headers show
Series integrity: Introduce the Integrity Digest Cache | expand

Commit Message

Roberto Sassu Sept. 5, 2024, 3:05 p.m. UTC
From: Roberto Sassu <roberto.sassu@huawei.com>

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

This done in three steps. First, a directory digest cache is created like
the other digest caches. The only differences are that this digest cache
has the IS_DIR bit set, to distinguish it from those created from regular
files and, consequently, that it stores a list of directory entries file
names instead of hash tables for digests.

Second, the directory digest cache is populated with current directory
entries, by calling digest_cache_dir_add_entries().

Finally, digest_cache_dir_lookup_digest() is called with the directory
digest cache passed as argument, to iteratively search on each digest cache
for each directory entry.

The function first calls digest_cache_dir_create() to create/obtain the
current directory digest cache for the directory. If this function returns
a different one than the one passed, it means that the directory was
modified between the get and lookup operations, and it uses the new one.

Then, digest_cache_dir_lookup_digest() starts the lookup and iteratively
searches the passed digest in each directory entry. If there is no digest
cache associated to the current directory entry,
digest_cache_dir_lookup_digest() creates/obtains one by calling
digest_cache_create(). It also keeps a digest cache reference, so that it
is available for next searches.

The iteration stops when the digest is found. Since the digest cache
containing the digest has been found, digest_cache_dir_update_dig_user()
is called to replace dig_user, set by digest_cache_get() to the directory
digest cache, with the found one. It can also happen that dig_user is
updated with a directory digest cache, if the digest is not found and there
was a directory modification between get and lookup.

digest_cache_dir_lookup_digest() returns the digest cache reference of the
current directory entry as the uintptr_t type, so that callers
of digest_cache_lookup() don't mistakenly try to call digest_cache_put()
with that reference.

The returned digest cache reference can be converted back to
(struct digest_cache *) and used to retrieve information about the
digest cache containing the digest, which is not known in advance in the
case of directories until the digest search is performed.

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

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 security/integrity/digest_cache/Makefile   |   2 +-
 security/integrity/digest_cache/dir.c      | 273 +++++++++++++++++++++
 security/integrity/digest_cache/htable.c   |   4 +
 security/integrity/digest_cache/internal.h |  43 ++++
 security/integrity/digest_cache/main.c     |  13 +
 5 files changed, 334 insertions(+), 1 deletion(-)
 create mode 100644 security/integrity/digest_cache/dir.c
diff mbox series

Patch

diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile
index 77dd98a1a07d..aef7f97d1407 100644
--- a/security/integrity/digest_cache/Makefile
+++ b/security/integrity/digest_cache/Makefile
@@ -4,7 +4,7 @@ 
 
 obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o
 
-digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o
+digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o
 
 digest_cache-y += parsers/tlv.o
 digest_cache-y += parsers/rpm.o
diff --git a/security/integrity/digest_cache/dir.c b/security/integrity/digest_cache/dir.c
new file mode 100644
index 000000000000..f0a2cb9618ba
--- /dev/null
+++ b/security/integrity/digest_cache/dir.c
@@ -0,0 +1,273 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@huawei.com>
+ *
+ * Manage digest caches from directories.
+ */
+
+#define pr_fmt(fmt) "digest_cache: "fmt
+#include <linux/init_task.h>
+#include <linux/namei.h>
+
+#include "internal.h"
+
+/**
+ * digest_cache_dir_iter - Digest cache directory iterator
+ * @__ctx: iterate_dir() context
+ * @name: Name of file in the accessed directory
+ * @namelen: String length of @name
+ * @offset: Current position in the directory stream (see man readdir)
+ * @ino: Inode number
+ * @d_type: File type
+ *
+ * This function stores the names of the files in the containing directory in
+ * a linked list. If they are in the format <seq num>-<format>-<name>, this
+ * function orders them by seq num, so that digest lists are processed in the
+ * desired order. Otherwise, if <seq num>- is not included, it adds the name at
+ * the end of the list.
+ *
+ * Return: True to continue processing, false to stop.
+ */
+static bool digest_cache_dir_iter(struct dir_context *__ctx, const char *name,
+				  int namelen, loff_t offset, u64 ino,
+				  unsigned int d_type)
+{
+	struct readdir_callback *ctx = container_of(__ctx, typeof(*ctx), ctx);
+	struct dir_entry *new_entry, *p;
+	unsigned int seq_num;
+	char *separator;
+	int ret;
+
+	if (!strcmp(name, ".") || !strcmp(name, ".."))
+		return true;
+
+	if (d_type != DT_REG)
+		return true;
+
+	new_entry = kmalloc(sizeof(*new_entry) + namelen + 1, GFP_KERNEL);
+	if (!new_entry)
+		return false;
+
+	memcpy(new_entry->name, name, namelen);
+	new_entry->name[namelen] = '\0';
+	new_entry->seq_num = UINT_MAX;
+	new_entry->digest_cache = NULL;
+	mutex_init(&new_entry->digest_cache_mutex);
+
+	if (new_entry->name[0] < '0' || new_entry->name[0] > '9')
+		goto out;
+
+	separator = strchr(new_entry->name, '-');
+	if (!separator)
+		goto out;
+
+	*separator = '\0';
+	ret = kstrtouint(new_entry->name, 10, &seq_num);
+	*separator = '-';
+	if (ret < 0)
+		goto out;
+
+	new_entry->seq_num = seq_num;
+
+	list_for_each_entry(p, ctx->head, list) {
+		if (seq_num <= p->seq_num) {
+			list_add(&new_entry->list, p->list.prev);
+			pr_debug("Added %s before %s in dir list\n",
+				 new_entry->name, p->name);
+			return true;
+		}
+	}
+out:
+	list_add_tail(&new_entry->list, ctx->head);
+	pr_debug("Added %s to tail of dir list\n", new_entry->name);
+	return true;
+}
+
+/**
+ * digest_cache_dir_add_entries - Add dir entries to a dir digest cache
+ * @digest_cache: Dir digest cache
+ * @digest_list_path: Path structure of the digest list directory
+ *
+ * This function iterates over the entries of a directory, and creates a linked
+ * list of file names from that directory.
+ *
+ * Return: Zero on success, a POSIX error code otherwise.
+ */
+int digest_cache_dir_add_entries(struct digest_cache *digest_cache,
+				 struct path *digest_list_path)
+{
+	struct file *dir_file;
+	struct readdir_callback buf = {
+		.ctx.actor = digest_cache_dir_iter,
+		.ctx.pos = 0,
+		.head = &digest_cache->dir_entries,
+	};
+	int ret;
+
+	dir_file = dentry_open(digest_list_path, O_RDONLY, &init_cred);
+	if (IS_ERR(dir_file)) {
+		pr_debug("Cannot access %s, ret: %ld\n", digest_cache->path_str,
+			 PTR_ERR(dir_file));
+		return PTR_ERR(dir_file);
+	}
+
+	ret = iterate_dir(dir_file, &buf.ctx);
+	if (ret < 0)
+		pr_debug("Failed to iterate directory %s\n",
+			 digest_cache->path_str);
+
+	fput(dir_file);
+	return ret;
+}
+
+/**
+ * digest_cache_dir_create - Create and initialize a directory digest cache
+ * @dentry: Dentry of the file whose digest is looked up
+ * @digest_list_path: Path structure of the digest list directory (updated)
+ * @path_str: Path string of the digest list directory
+ *
+ * This function creates and initializes (or obtains if it already exists) a
+ * directory digest cache. It updates the path that digest cache was
+ * created/obtained from, so that the caller can use it to perform lookup
+ * operations.
+ *
+ * Return: A directory digest cache on success, NULL otherwise.
+ */
+static struct digest_cache *
+digest_cache_dir_create(struct dentry *dentry, struct path *digest_list_path,
+			char *path_str)
+{
+	struct digest_cache *digest_cache;
+	int ret;
+
+	ret = kern_path(path_str, 0, digest_list_path);
+	if (ret < 0) {
+		pr_debug("Cannot find path %s\n", path_str);
+		return NULL;
+	}
+
+	digest_cache = digest_cache_create(dentry, digest_list_path, path_str,
+					   "");
+	if (digest_cache)
+		digest_cache = digest_cache_init(dentry, digest_cache);
+
+	return digest_cache;
+}
+
+/**
+ * digest_cache_dir_update_dig_user - Update dig_user with passed digest cache
+ * @dentry: Dentry of the file whose digest is looked up
+ * @digest_cache: Dir digest cache
+ *
+ * This function updates dig_user of the inode being verified, with the passed
+ * digest cache. The digest cache can differ if the directory inode was evicted
+ * or modified, or if the digest searched was found in a directory entry. In the
+ * latter case, dig_user is replaced with the digest cache of that directory
+ * entry.
+ */
+static void digest_cache_dir_update_dig_user(struct dentry *dentry,
+					     struct digest_cache *digest_cache)
+{
+	struct inode *inode = d_backing_inode(dentry);
+	struct digest_cache_security *dig_sec;
+
+	dig_sec = digest_cache_get_security(inode);
+	if (unlikely(!dig_sec))
+		return;
+
+	/* Serialize accesses to inode for which the digest cache is used. */
+	mutex_lock(&dig_sec->dig_user_mutex);
+	if (dig_sec->dig_user != digest_cache) {
+		digest_cache_put(dig_sec->dig_user);
+		dig_sec->dig_user = digest_cache_ref(digest_cache);
+	}
+	mutex_unlock(&dig_sec->dig_user_mutex);
+}
+
+/**
+ * digest_cache_dir_lookup_digest - Lookup a digest
+ * @dentry: Dentry of the file whose digest is looked up
+ * @digest_cache: Dir digest cache
+ * @digest: Digest to search
+ * @algo: Algorithm of the digest to search
+ *
+ * This function iterates over the linked list created by
+ * digest_cache_dir_add_entries() and looks up the digest in the digest cache
+ * of each entry.
+ *
+ * Return: A positive uintptr_t value if the digest if found, zero otherwise.
+ */
+uintptr_t digest_cache_dir_lookup_digest(struct dentry *dentry,
+					 struct digest_cache *digest_cache,
+					 u8 *digest, enum hash_algo algo)
+{
+	struct dir_entry *dir_entry;
+	struct digest_cache *dir_cache, *cache, *found = NULL;
+	struct path digest_list_path;
+	int ret;
+
+	/* Try to reacquire the dir digest cache, and use the new if changed. */
+	dir_cache = digest_cache_dir_create(dentry, &digest_list_path,
+					    digest_cache->path_str);
+	if (!dir_cache)
+		return 0UL;
+
+	/* Continue to use the new one. */
+	list_for_each_entry(dir_entry, &dir_cache->dir_entries, list) {
+		mutex_lock(&dir_entry->digest_cache_mutex);
+		if (!dir_entry->digest_cache) {
+			cache = digest_cache_create(dentry, &digest_list_path,
+						    dir_cache->path_str,
+						    dir_entry->name);
+			if (cache)
+				cache = digest_cache_init(dentry, cache);
+
+			/* Ignore digest caches that cannot be instantiated. */
+			if (!cache) {
+				mutex_unlock(&dir_entry->digest_cache_mutex);
+				continue;
+			}
+
+			/* Consume extra ref. from digest_cache_create(). */
+			dir_entry->digest_cache = cache;
+		}
+		mutex_unlock(&dir_entry->digest_cache_mutex);
+
+		ret = digest_cache_htable_lookup(dentry,
+						 dir_entry->digest_cache,
+						 digest, algo);
+		if (!ret) {
+			found = dir_entry->digest_cache;
+			break;
+		}
+	}
+
+	digest_cache_dir_update_dig_user(dentry, found ?: dir_cache);
+
+	digest_cache_put(dir_cache);
+	path_put(&digest_list_path);
+	return (uintptr_t)found;
+}
+
+/**
+ * digest_cache_dir_free - Free the stored file list and put digest caches
+ * @digest_cache: Dir digest cache
+ *
+ * This function frees the file list created by digest_cache_dir_add_entries(),
+ * and puts the digest cache of each directory entry, if a reference exists.
+ */
+void digest_cache_dir_free(struct digest_cache *digest_cache)
+{
+	struct dir_entry *p, *q;
+
+	list_for_each_entry_safe(p, q, &digest_cache->dir_entries, list) {
+		if (p->digest_cache)
+			digest_cache_put(p->digest_cache);
+
+		list_del(&p->list);
+		mutex_destroy(&p->digest_cache_mutex);
+		kfree(p);
+	}
+}
diff --git a/security/integrity/digest_cache/htable.c b/security/integrity/digest_cache/htable.c
index 385f81047b0c..1aa884ef18f0 100644
--- a/security/integrity/digest_cache/htable.c
+++ b/security/integrity/digest_cache/htable.c
@@ -209,6 +209,10 @@  uintptr_t digest_cache_lookup(struct dentry *dentry,
 {
 	int ret;
 
+	if (test_bit(IS_DIR, &digest_cache->flags))
+		return digest_cache_dir_lookup_digest(dentry, digest_cache,
+						      digest, algo);
+
 	ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo);
 	if (ret < 0)
 		return 0UL;
diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h
index 9083a87374a5..34a26576a5aa 100644
--- a/security/integrity/digest_cache/internal.h
+++ b/security/integrity/digest_cache/internal.h
@@ -17,6 +17,39 @@ 
 #define INIT_IN_PROGRESS	0	/* Digest cache being initialized. */
 #define INIT_STARTED		1	/* Digest cache init started. */
 #define INVALID			2	/* Digest cache marked as invalid. */
+#define IS_DIR			3	/* Digest cache created from dir. */
+
+/**
+ * struct readdir_callback - Structure to store information for dir iteration
+ * @ctx: Context structure
+ * @head: Head of linked list of directory entries
+ *
+ * This structure stores information to be passed from the iterate_dir() caller
+ * to the directory iterator.
+ */
+struct readdir_callback {
+	struct dir_context ctx;
+	struct list_head *head;
+};
+
+/**
+ * struct dir_entry - Directory entry
+ * @list: Linked list of directory entries
+ * @digest_cache: Digest cache associated to the directory entry
+ * @digest_cache_mutex: Protects @digest_cache
+ * @seq_num: Sequence number of the directory entry from file name
+ * @name: File name of the directory entry
+ *
+ * This structure represents a directory entry with a digest cache created
+ * from that entry.
+ */
+struct dir_entry {
+	struct list_head list;
+	struct digest_cache *digest_cache;
+	struct mutex digest_cache_mutex;
+	unsigned int seq_num;
+	char name[];
+};
 
 /**
  * struct digest_cache_verif
@@ -84,6 +117,7 @@  struct htable {
 /**
  * struct digest_cache - Digest cache
  * @htables: Hash tables (one per algorithm)
+ * @dir_entries: List of files in a directory and the digest cache
  * @ref_count: Number of references to the digest cache
  * @path_str: Path of the digest list the digest cache was created from
  * @flags: Control flags
@@ -95,6 +129,7 @@  struct htable {
  */
 struct digest_cache {
 	struct list_head htables;
+	struct list_head dir_entries;
 	atomic_t ref_count;
 	char *path_str;
 	unsigned long flags;
@@ -208,4 +243,12 @@  size_t digest_cache_strip_modsig(__u8 *data, size_t data_len);
 /* verif.c */
 void digest_cache_verif_free(struct digest_cache *digest_cache);
 
+/* dir.c */
+int digest_cache_dir_add_entries(struct digest_cache *digest_cache,
+				 struct path *digest_cache_path);
+uintptr_t digest_cache_dir_lookup_digest(struct dentry *dentry,
+					 struct digest_cache *digest_cache,
+					 u8 *digest, enum hash_algo algo);
+void digest_cache_dir_free(struct digest_cache *digest_cache);
+
 #endif /* _DIGEST_CACHE_INTERNAL_H */
diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c
index fda6ac599a2d..d050ebad3cbb 100644
--- a/security/integrity/digest_cache/main.c
+++ b/security/integrity/digest_cache/main.c
@@ -54,6 +54,7 @@  static struct digest_cache *digest_cache_alloc_init(char *path_str,
 	INIT_LIST_HEAD(&digest_cache->htables);
 	INIT_LIST_HEAD(&digest_cache->verif_data);
 	spin_lock_init(&digest_cache->verif_data_lock);
+	INIT_LIST_HEAD(&digest_cache->dir_entries);
 
 	pr_debug("New digest cache %s (ref count: %d)\n",
 		 digest_cache->path_str, atomic_read(&digest_cache->ref_count));
@@ -71,6 +72,7 @@  static void digest_cache_free(struct digest_cache *digest_cache)
 {
 	digest_cache_htable_free(digest_cache);
 	digest_cache_verif_free(digest_cache);
+	digest_cache_dir_free(digest_cache);
 
 	pr_debug("Freed digest cache %s\n", digest_cache->path_str);
 	kfree(digest_cache->path_str);
@@ -283,6 +285,17 @@  struct digest_cache *digest_cache_init(struct dentry *dentry,
 			/* Prevent usage of partially-populated digest cache. */
 			set_bit(INVALID, &digest_cache->flags);
 		}
+	} else if (S_ISDIR(inode->i_mode)) {
+		set_bit(IS_DIR, &digest_cache->flags);
+
+		ret = digest_cache_dir_add_entries(digest_cache,
+					&digest_cache->digest_list_path);
+		if (ret < 0) {
+			pr_debug("Failed to add dir entries to dir digest cache, ret: %d (keep digest cache)\n",
+				 ret);
+			/* Prevent usage of partially-populated digest cache. */
+			set_bit(INVALID, &digest_cache->flags);
+		}
 	}
 
 	path_put(&digest_cache->digest_list_path);