From: Roberto Sassu <roberto.sassu@xxxxxxxxxx> In the environments where xattrs are not available (e.g. in the initial ram disk), the Integrity Digest Cache cannot precisely determine which digest list in a directory contains the desired reference digest. However, although slower, it would be desirable to search the digest in all digest lists of that directory. This done in three steps. First, a directory digest cache is created like the other digest caches. The only differences are that this digest cache has the IS_DIR bit set, to distinguish it from those created from regular files and, consequently, that it stores a list of directory entries file names instead of hash tables for digests. Second, the directory digest cache is populated with current directory entries, by calling digest_cache_dir_add_entries(). Finally, digest_cache_dir_lookup_digest() is called with the directory digest cache passed as argument, to iteratively search on each digest cache for each directory entry. The function first calls digest_cache_dir_create() to create/obtain the current directory digest cache for the directory. If the latter returns a different one than the one passed, digest_cache_dir_lookup_digest() returns ERR_PTR(-EAGAIN) to let the caller of digest_cache_lookup() know that the directory digest cache changed, and make the caller perform another digest_cache_get() to get a fresh directory digest cache. If the passed and the current directory digest caches match, digest_cache_dir_lookup_digest() starts the lookup and iteratively searches the passed digest in each directory entry. If there is no digest cache associated to the current directory entry, digest_cache_dir_lookup_digest() creates/obtains one by calling digest_cache_create(). It also keeps a digest cache reference, so that it is available for next searches. The iteration stops when the digest is found. Finally, digest_cache_dir_free() releases the digest cache references stored in the list of directory entries, and frees the list itself. Signed-off-by: Roberto Sassu <roberto.sassu@xxxxxxxxxx> --- security/integrity/digest_cache/Makefile | 2 +- security/integrity/digest_cache/dir.c | 263 +++++++++++++++++++++ security/integrity/digest_cache/htable.c | 7 +- security/integrity/digest_cache/internal.h | 44 ++++ security/integrity/digest_cache/main.c | 13 + 5 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 security/integrity/digest_cache/dir.c diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile index 2a0f2500e227..d07ac2483504 100644 --- a/security/integrity/digest_cache/Makefile +++ b/security/integrity/digest_cache/Makefile @@ -6,6 +6,6 @@ obj-$(CONFIG_INTEGRITY_DIGEST_CACHE) += digest_cache.o obj-$(CONFIG_DIGEST_CACHE_TLV_PARSER) += parsers/tlv.o digest_cache-y := main.o secfs.o htable.o parsers.o populate.o modsig.o \ - verif.o + verif.o dir.o CFLAGS_parsers.o += -DPARSERS_DIR=\"$(MODLIB)/kernel/security/integrity/digest_cache/parsers\" diff --git a/security/integrity/digest_cache/dir.c b/security/integrity/digest_cache/dir.c new file mode 100644 index 000000000000..8f9e550c365f --- /dev/null +++ b/security/integrity/digest_cache/dir.c @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023-2024 Huawei Technologies Duesseldorf GmbH + * + * Author: Roberto Sassu <roberto.sassu@xxxxxxxxxx> + * + * Manage digest caches from directories. + */ + +#define pr_fmt(fmt) "digest_cache: "fmt +#include <linux/init_task.h> +#include <linux/namei.h> + +#include "internal.h" + +/** + * digest_cache_dir_iter - Digest cache directory iterator + * @__ctx: iterate_dir() context + * @name: Name of file in the accessed directory + * @namelen: String length of @name + * @offset: Current position in the directory stream (see man readdir) + * @ino: Inode number + * @d_type: File type + * + * This function stores the names of the files in the containing directory in + * a linked list. If they are in the format + * <seq num>-<digest list format>-<digest list name>, this function orders them + * by seq num, so that digest lists are processed in the desired order. + * Otherwise, if <seq num>- is not included, it adds the name at the end of + * the list. + * + * Return: True to continue processing, false to stop. + */ +static bool digest_cache_dir_iter(struct dir_context *__ctx, const char *name, + int namelen, loff_t offset, u64 ino, + unsigned int d_type) +{ + struct readdir_callback *ctx = container_of(__ctx, typeof(*ctx), ctx); + struct dir_entry *new_entry, *p; + unsigned int seq_num; + char *separator; + int ret; + + if (!strcmp(name, ".") || !strcmp(name, "..")) + return true; + + if (d_type != DT_REG) + return true; + + new_entry = kmalloc(sizeof(*new_entry) + namelen + 1, GFP_KERNEL); + if (!new_entry) + return false; + + memcpy(new_entry->name, name, namelen); + new_entry->name[namelen] = '\0'; + new_entry->seq_num = UINT_MAX; + new_entry->digest_cache = NULL; + mutex_init(&new_entry->digest_cache_mutex); + + if (new_entry->name[0] < '0' || new_entry->name[0] > '9') + goto out; + + separator = strchr(new_entry->name, '-'); + if (!separator) + goto out; + + *separator = '\0'; + ret = kstrtouint(new_entry->name, 10, &seq_num); + *separator = '-'; + if (ret < 0) + goto out; + + new_entry->seq_num = seq_num; + + list_for_each_entry(p, ctx->head, list) { + if (seq_num <= p->seq_num) { + list_add(&new_entry->list, p->list.prev); + pr_debug("Added %s before %s in dir list\n", + new_entry->name, p->name); + return true; + } + } +out: + list_add_tail(&new_entry->list, ctx->head); + pr_debug("Added %s to tail of dir list\n", new_entry->name); + return true; +} + +/** + * digest_cache_dir_add_entries - Add dir entries to a dir digest cache + * @digest_cache: Dir digest cache + * @digest_list_path: Path structure of the digest list directory + * + * This function iterates over the entries of a directory, and creates a linked + * list of file names from that directory. + * + * Return: Zero on success, a POSIX error code otherwise. + */ +int digest_cache_dir_add_entries(struct digest_cache *digest_cache, + struct path *digest_list_path) +{ + struct file *dir_file; + struct readdir_callback buf = { + .ctx.actor = digest_cache_dir_iter, + .ctx.pos = 0, + .head = &digest_cache->dir_entries, + }; + int ret; + + dir_file = kernel_file_open(digest_list_path, O_RDONLY, &init_cred); + if (IS_ERR(dir_file)) { + pr_debug("Cannot access %s, ret: %ld\n", digest_cache->path_str, + PTR_ERR(dir_file)); + return PTR_ERR(dir_file); + } + + ret = iterate_dir(dir_file, &buf.ctx); + if (ret < 0) + pr_debug("Failed to iterate directory %s\n", + digest_cache->path_str); + + fput(dir_file); + return ret; +} + +/** + * digest_cache_dir_create - Create and initialize a directory digest cache + * @dentry: Dentry of the file whose digest is looked up + * @dir_path: Path structure of the digest list directory (updated) + * @path_str: Path string of the digest list directory + * + * This function creates and initializes (or obtains if it already exists) a + * directory digest cache. It updates the path that digest cache was + * created/obtained from, so that the caller can use it to perform lookup + * operations. + * + * Return: A directory digest cache on success, NULL otherwise. + */ +static struct digest_cache *digest_cache_dir_create(struct dentry *dentry, + struct path *dir_path, + char *path_str) +{ + struct digest_cache *digest_cache; + struct path _dir_path; + int ret; + + ret = kern_path(path_str, 0, &_dir_path); + if (ret < 0) { + pr_debug("Cannot find path %s\n", path_str); + return NULL; + } + + digest_cache = digest_cache_create(dentry, &_dir_path, dir_path, + path_str, ""); + if (digest_cache) + digest_cache = digest_cache_init(dentry, dir_path, + digest_cache); + + path_put(&_dir_path); + return digest_cache; +} + +/** + * digest_cache_dir_lookup_digest - Lookup a digest + * @dentry: Dentry of the file whose digest is looked up + * @digest_cache: Dir digest cache + * @digest: Digest to search + * @algo: Algorithm of the digest to search + * + * This function iterates over the linked list created by + * digest_cache_dir_add_entries() and looks up the digest in the digest cache + * of each entry. + * + * Return: A digest cache reference if the digest is found, NULL if not, an + * error pointer if dir digest cache changed since last get. + */ +struct digest_cache * +digest_cache_dir_lookup_digest(struct dentry *dentry, + struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo) +{ + struct dir_entry *dir_entry; + struct digest_cache *dir_cache, *cache, *found = NULL; + struct path dir_path = { .dentry = NULL, .mnt = NULL }; + struct path digest_list_path; + int ret; + + /* Try to reacquire the dir digest cache, and check if changed. */ + dir_cache = digest_cache_dir_create(dentry, &dir_path, + digest_cache->path_str); + if (dir_cache != digest_cache) { + if (dir_cache) + found = ERR_PTR(-EAGAIN); + + goto out; + } + + list_for_each_entry(dir_entry, &dir_cache->dir_entries, list) { + mutex_lock(&dir_entry->digest_cache_mutex); + if (!dir_entry->digest_cache) { + digest_list_path.dentry = NULL; + digest_list_path.mnt = NULL; + + cache = digest_cache_create(dentry, &dir_path, + &digest_list_path, + dir_cache->path_str, + dir_entry->name); + if (cache) + cache = digest_cache_init(dentry, + &digest_list_path, + cache); + + if (digest_list_path.dentry) + path_put(&digest_list_path); + + /* Ignore digest caches that cannot be instantiated. */ + if (!cache) { + mutex_unlock(&dir_entry->digest_cache_mutex); + continue; + } + + /* Consume extra ref. from digest_cache_create(). */ + dir_entry->digest_cache = cache; + } + mutex_unlock(&dir_entry->digest_cache_mutex); + + ret = digest_cache_htable_lookup(dentry, + dir_entry->digest_cache, + digest, algo); + if (!ret) { + found = digest_cache_ref(dir_entry->digest_cache); + break; + } + } +out: + if (dir_cache) + digest_cache_put(dir_cache); + if (dir_path.dentry) + path_put(&dir_path); + + return found; +} + +/** + * digest_cache_dir_free - Free the stored file list and put digest caches + * @digest_cache: Dir digest cache + * + * This function frees the file list created by digest_cache_dir_add_entries(), + * and puts the digest cache of each directory entry, if a reference exists. + */ +void digest_cache_dir_free(struct digest_cache *digest_cache) +{ + struct dir_entry *p, *q; + + list_for_each_entry_safe(p, q, &digest_cache->dir_entries, list) { + if (p->digest_cache) + digest_cache_put(p->digest_cache); + + list_del(&p->list); + mutex_destroy(&p->digest_cache_mutex); + kfree(p); + } +} diff --git a/security/integrity/digest_cache/htable.c b/security/integrity/digest_cache/htable.c index 8aa6d50a0cb5..a01e24d7f198 100644 --- a/security/integrity/digest_cache/htable.c +++ b/security/integrity/digest_cache/htable.c @@ -202,7 +202,8 @@ EXPORT_SYMBOL_GPL(digest_cache_htable_lookup); * This function calls digest_cache_htable_lookup() to search a digest in the * passed digest cache, obtained with digest_cache_get(). * - * Return: A digest cache reference the digest is found, NULL if not. + * Return: A digest cache reference if the digest is found, NULL if not, an + * error pointer if dir digest cache changed since last get. */ struct digest_cache *digest_cache_lookup(struct dentry *dentry, struct digest_cache *digest_cache, @@ -210,6 +211,10 @@ struct digest_cache *digest_cache_lookup(struct dentry *dentry, { int ret; + if (test_bit(IS_DIR, &digest_cache->flags)) + return digest_cache_dir_lookup_digest(dentry, digest_cache, + digest, algo); + ret = digest_cache_htable_lookup(dentry, digest_cache, digest, algo); if (ret < 0) return NULL; diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h index c64e91b75a47..f849afe5e47b 100644 --- a/security/integrity/digest_cache/internal.h +++ b/security/integrity/digest_cache/internal.h @@ -17,6 +17,39 @@ #define INIT_IN_PROGRESS 0 /* Digest cache being initialized. */ #define INIT_STARTED 1 /* Digest cache init started. */ #define INVALID 2 /* Digest cache marked as invalid. */ +#define IS_DIR 3 /* Digest cache created from dir. */ + +/** + * struct readdir_callback - Structure to store information for dir iteration + * @ctx: Context structure + * @head: Head of linked list of directory entries + * + * This structure stores information to be passed from the iterate_dir() caller + * to the directory iterator. + */ +struct readdir_callback { + struct dir_context ctx; + struct list_head *head; +}; + +/** + * struct dir_entry - Directory entry + * @list: Linked list of directory entries + * @digest_cache: Digest cache associated to the directory entry + * @digest_cache_mutex: Protects @digest_cache + * @seq_num: Sequence number of the directory entry from file name + * @name: File name of the directory entry + * + * This structure represents a directory entry with a digest cache created + * from that entry. + */ +struct dir_entry { + struct list_head list; + struct digest_cache *digest_cache; + struct mutex digest_cache_mutex; + unsigned int seq_num; + char name[]; +}; /** * struct digest_cache_verif @@ -84,6 +117,7 @@ struct htable { /** * struct digest_cache - Digest cache * @htables: Hash tables (one per algorithm) + * @dir_entries: List of files in a directory and the digest cache * @ref_count: Number of references to the digest cache * @path_str: Path of the digest list the digest cache was created from * @flags: Control flags @@ -94,6 +128,7 @@ struct htable { */ struct digest_cache { struct list_head htables; + struct list_head dir_entries; atomic_t ref_count; char *path_str; unsigned long flags; @@ -213,4 +248,13 @@ size_t digest_cache_strip_modsig(__u8 *data, size_t data_len); /* verif.c */ void digest_cache_verif_free(struct digest_cache *digest_cache); +/* dir.c */ +int digest_cache_dir_add_entries(struct digest_cache *digest_cache, + struct path *digest_cache_path); +struct digest_cache * +digest_cache_dir_lookup_digest(struct dentry *dentry, + struct digest_cache *digest_cache, u8 *digest, + enum hash_algo algo); +void digest_cache_dir_free(struct digest_cache *digest_cache); + #endif /* _DIGEST_CACHE_INTERNAL_H */ diff --git a/security/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c index 11a0445592f0..9df819c323c7 100644 --- a/security/integrity/digest_cache/main.c +++ b/security/integrity/digest_cache/main.c @@ -54,6 +54,7 @@ static struct digest_cache *digest_cache_alloc_init(char *path_str, INIT_LIST_HEAD(&digest_cache->htables); INIT_LIST_HEAD(&digest_cache->verif_data); spin_lock_init(&digest_cache->verif_data_lock); + INIT_LIST_HEAD(&digest_cache->dir_entries); pr_debug("New digest cache %s (ref count: %d)\n", digest_cache->path_str, atomic_read(&digest_cache->ref_count)); @@ -71,6 +72,7 @@ static void digest_cache_free(struct digest_cache *digest_cache) { digest_cache_htable_free(digest_cache); digest_cache_verif_free(digest_cache); + digest_cache_dir_free(digest_cache); pr_debug("Freed digest cache %s\n", digest_cache->path_str); kfree(digest_cache->path_str); @@ -292,6 +294,17 @@ struct digest_cache *digest_cache_init(struct dentry *dentry, /* Prevent usage of partially-populated digest cache. */ set_bit(INVALID, &digest_cache->flags); } + } else if (S_ISDIR(inode->i_mode)) { + set_bit(IS_DIR, &digest_cache->flags); + + ret = digest_cache_dir_add_entries(digest_cache, + digest_list_path); + if (ret < 0) { + pr_debug("Failed to add dir entries to dir digest cache, ret: %d (keep digest cache)\n", + ret); + /* Prevent usage of partially-populated digest cache. */ + set_bit(INVALID, &digest_cache->flags); + } } /* Notify initialization complete. */ -- 2.47.0.118.gfd3785337b