@@ -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
new file mode 100644
@@ -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);
+ }
+}
@@ -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;
@@ -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 */
@@ -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);