From: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
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@xxxxxxxxxx>
---
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@xxxxxxxxxx>
+ *
+ * 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;