diff mbox series

[v6,13/15] digest_cache: Reset digest cache on file/directory change

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

Commit Message

Roberto Sassu Nov. 19, 2024, 10:49 a.m. UTC
From: Roberto Sassu <roberto.sassu@huawei.com>

Register six new LSM hooks on behalf of the IMA LSM, path_truncate,
file_release, inode_unlink, inode_rename, inode_post_setxattr and
inode_post_removexattr, to monitor digest lists/parent directory
modifications.

If an action affects a digest list or the parent directory, the new LSM
hook implementations call digest_cache_reset_clear_owner() to set the RESET
bit on the digest cache referenced by dig_owner in the inode security blob,
and to put and clear dig_owner itself. This will also cause next calls to
digest_cache_get() and digest_cache_dir_lookup_digest() to replace
respectively dig_user and the directory entry digest cache.

If an action affects a file using a digest cache, the new LSM hook
implementations call digest_cache_clear_user() to clear dig_user in the
inode security blob. This will also cause next calls to digest_cache_get()
to obtain a new digest cache, based on the updated location.

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 existing directory entries, since
those digest caches are likely already stored in the inode security blob.
It would happen however for new directory entries.

Dig_owner reset and clear for file/directory 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 or created is
being closed, inode_unlink, when a digest list is removed, and inode_rename
when a digest list or the directory itself are renamed.

Directory digest caches are reset even if the current operation involves a
file, since that operation might affect the result of the lookup done
through them. For example, if one wants to know whether a digest is found
or not in a directory, adding a new digest list to that directory could
change the result.

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

Callers of digest_cache_get() can still keep a digest cache after reset,
since the reference count remains greater than zero even if dig_owner and
dig_user are cleared. However, digest_cache_lookup() will notify the
callers that a reset happened through an error pointer. Callers need to
obtain a fresh digest cache with digest_cache_get() and repeat the lookup
again.

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 security/integrity/digest_cache/Makefile   |   2 +-
 security/integrity/digest_cache/dir.c      |   6 +
 security/integrity/digest_cache/htable.c   |   6 +-
 security/integrity/digest_cache/internal.h |  13 ++
 security/integrity/digest_cache/main.c     |  12 ++
 security/integrity/digest_cache/reset.c    | 227 +++++++++++++++++++++
 6 files changed, 264 insertions(+), 2 deletions(-)
 create mode 100644 security/integrity/digest_cache/reset.c
diff mbox series

Patch

diff --git a/security/integrity/digest_cache/Makefile b/security/integrity/digest_cache/Makefile
index d07ac2483504..f8afb85407a0 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 dir.o
+		  verif.o dir.o reset.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
index a292a9c25119..d4fd5b5ef8fa 100644
--- a/security/integrity/digest_cache/dir.c
+++ b/security/integrity/digest_cache/dir.c
@@ -206,6 +206,12 @@  digest_cache_dir_lookup_digest(struct dentry *dentry,
 
 	list_for_each_entry(dir_entry, &dir_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) {
 			digest_list_path.dentry = NULL;
 			digest_list_path.mnt = NULL;
diff --git a/security/integrity/digest_cache/htable.c b/security/integrity/digest_cache/htable.c
index a01e24d7f198..349aebf91360 100644
--- a/security/integrity/digest_cache/htable.c
+++ b/security/integrity/digest_cache/htable.c
@@ -203,7 +203,8 @@  EXPORT_SYMBOL_GPL(digest_cache_htable_lookup);
  * passed digest cache, obtained with digest_cache_get().
  *
  * Return: A digest cache reference if the digest is found, NULL if not, an
- *         error pointer if dir digest cache changed since last get.
+ *         error pointer if dir digest cache changed since last get, or digest
+ *         cache was reset.
  */
 struct digest_cache *digest_cache_lookup(struct dentry *dentry,
 					 struct digest_cache *digest_cache,
@@ -211,6 +212,9 @@  struct digest_cache *digest_cache_lookup(struct dentry *dentry,
 {
 	int ret;
 
+	if (test_bit(RESET, &digest_cache->flags))
+		return ERR_PTR(-EAGAIN);
+
 	if (test_bit(IS_DIR, &digest_cache->flags))
 		return digest_cache_dir_lookup_digest(dentry, digest_cache,
 						      digest, algo);
diff --git a/security/integrity/digest_cache/internal.h b/security/integrity/digest_cache/internal.h
index 5af7f6e7bdf6..75f8b118c3db 100644
--- a/security/integrity/digest_cache/internal.h
+++ b/security/integrity/digest_cache/internal.h
@@ -21,6 +21,7 @@ 
 #define DIR_PREFETCH		4	/* Prefetch enabled for dir. */
 #define FILE_PREFETCH		5	/* Prefetch enabled for dir entry. */
 #define FILE_READ		6	/* Digest cache for reading file. */
+#define RESET			7	/* Digest cache to be recreated. */
 
 /**
  * struct readdir_callback - Structure to store information for dir iteration
@@ -267,4 +268,16 @@  int digest_cache_dir_prefetch(struct dentry *dentry,
 			      struct digest_cache *digest_cache);
 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/integrity/digest_cache/main.c b/security/integrity/digest_cache/main.c
index ca1f0bc8ec94..0a167c82e308 100644
--- a/security/integrity/digest_cache/main.c
+++ b/security/integrity/digest_cache/main.c
@@ -436,6 +436,11 @@  struct digest_cache *digest_cache_get(struct file *file)
 
 	/* 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)) {
+		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(). */
@@ -553,6 +558,13 @@  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_rcu,
 		      digest_cache_inode_free_security_rcu),
+	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/integrity/digest_cache/reset.c b/security/integrity/digest_cache/reset.c
new file mode 100644
index 000000000000..003c8ee96d72
--- /dev/null
+++ b/security/integrity/digest_cache/reset.c
@@ -0,0 +1,227 @@ 
+// 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_clear_owner - Reset and clear dig_owner
+ * @inode: Inode of the digest list/directory containing the digest list
+ * @reason: Reason for reset and clear
+ *
+ * This function sets the RESET bit of the digest cache referenced by dig_owner
+ * of the passed inode, and puts and clears dig_owner.
+ *
+ * The next time they are called, digest_cache_get() and
+ * digest_cache_dir_lookup_digest() replace respectively dig_user and the digest
+ * cache of the directory entry.
+ */
+static void digest_cache_reset_clear_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) {
+		pr_debug("Resetting and clearing %s (dig_owner), reason: %s\n",
+			 dig_sec->dig_owner->path_str, reason);
+		set_bit(RESET, &dig_sec->dig_owner->flags);
+		digest_cache_put(dig_sec->dig_owner);
+		dig_sec->dig_owner = NULL;
+	}
+	mutex_unlock(&dig_sec->dig_owner_mutex);
+}
+
+/**
+ * digest_cache_clear_user - Clear dig_user
+ * @inode: Inode of the file using the digest cache
+ * @filename: File name of the affected inode
+ * @reason: Reason for clear
+ *
+ * This function clears dig_user in the inode security blob, so that
+ * digest_cache_get() requests a new digest cache based on the updated digest
+ * list location.
+ */
+static void digest_cache_clear_user(struct inode *inode, const char *filename,
+				    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_bit(RESET, &dig_sec->dig_user->flags)) {
+		pr_debug("Clearing %s (dig_user of %s), reason: %s\n",
+			 dig_sec->dig_user->path_str, filename, reason);
+		digest_cache_put(dig_sec->dig_user);
+		dig_sec->dig_user = NULL;
+	}
+	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 and/or the parent is a directory containing digest lists, it
+ * resets the inode and/or directory 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);
+	struct inode *dir = d_backing_inode(path->dentry->d_parent);
+
+	if (!S_ISREG(inode->i_mode))
+		return 0;
+
+	digest_cache_reset_clear_owner(inode, "path_truncate(file)");
+	digest_cache_reset_clear_owner(dir, "path_truncate(dir)");
+	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
+ * was created, it resets the inode and the parent directory dig_owner, to
+ * force rebuilding the digest caches.
+ */
+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)) &&
+	      !(file->f_mode & FMODE_CREATED)))
+		return;
+
+	digest_cache_reset_clear_owner(file_inode(file), "file_release(file)");
+	digest_cache_reset_clear_owner(dir, "file_release(dir)");
+}
+
+/**
+ * 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/digest list directory, 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) && !S_ISDIR(inode->i_mode))
+		return 0;
+
+	digest_cache_reset_clear_owner(inode, S_ISREG(inode->i_mode) ?
+				       "inode_unlink(file)" :
+				       "inode_unlink(dir)");
+
+	if (S_ISREG(inode->i_mode))
+		digest_cache_reset_clear_owner(dir, "inode_unlink(dir)");
+
+	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 the digest
+ * list directory, or that inode is a digest list 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) && !S_ISDIR(old_inode->i_mode))
+		return 0;
+
+	digest_cache_reset_clear_owner(old_inode, S_ISREG(old_inode->i_mode) ?
+				       "inode_rename(file)" :
+				       "inode_rename(dir)");
+
+	if (S_ISREG(old_inode->i_mode)) {
+		digest_cache_reset_clear_owner(old_dir,
+					       "inode_rename(from_dir)");
+		digest_cache_reset_clear_owner(new_dir,
+					       "inode_rename(to_dir)");
+	}
+
+	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
+ * puts and clears 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_clear_user(d_backing_inode(dentry), dentry->d_name.name,
+				"inode_post_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 puts and clears 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_clear_user(d_backing_inode(dentry), dentry->d_name.name,
+				"inode_post_removexattr");
+}