diff mbox series

[v3,11/13] digest_cache: Reset digest cache on file/directory change

Message ID 20240209140917.846878-12-roberto.sassu@huaweicloud.com
State New
Headers show
Series security: digest_cache LSM | expand

Commit Message

Roberto Sassu Feb. 9, 2024, 2:09 p.m. UTC
From: Roberto Sassu <roberto.sassu@huawei.com>

Register five new LSM hooks, file_open, path_truncate, file_release,
inode_unlink and inode_rename, to monitor digest lists/directory
modifications.

If an action affects a digest list or the parent directory, the new LSM
hook implementations call digest_cache_reset() to set the RESET bit on the
digest cache. This will cause next calls to digest_cache_get() and
digest_cache_create() to respectively put and clear dig_user and dig_owner,
and request a new digest cache.

That does not affect other users of the old digest cache, since that one
remains valid as long as the reference count is greater than zero. However,
they can explicitly call the new function digest_cache_was_reset(), to
check if the RESET bit was set on the digest cache reference they hold.

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

File digest cache reset is done on file_open, when a digest list is opened
for write, path_truncate, when a digest list is truncated (there is no
inode_truncate, file_truncate does not catch operations through the
truncate() system call), inode_unlink, when a digest list is removed, and
inode_rename when a digest list is renamed.

Directory digest cache reset is done on file_release, when a digest list is
written in the digest list directory, on inode_unlink, when a digest list
is deleted from that directory, and finally on inode_rename, when a digest
list is moved to/from that directory.

With the exception of file_release, which will always be executed (cannot
be denied), the other LSM hooks are not optimal, since the digest_cache LSM
does not know whether or not the operation will be allowed also by other
LSMs. If the operation is denied, the digest_cache LSM would do an
unnecessary reset.

Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
---
 include/linux/digest_cache.h     |   6 ++
 security/digest_cache/Kconfig    |   1 +
 security/digest_cache/Makefile   |   3 +-
 security/digest_cache/internal.h |   9 ++
 security/digest_cache/main.c     |  15 +++
 security/digest_cache/reset.c    | 168 +++++++++++++++++++++++++++++++
 6 files changed, 201 insertions(+), 1 deletion(-)
 create mode 100644 security/digest_cache/reset.c
diff mbox series

Patch

diff --git a/include/linux/digest_cache.h b/include/linux/digest_cache.h
index 9db8128513ca..db3052c71b7a 100644
--- a/include/linux/digest_cache.h
+++ b/include/linux/digest_cache.h
@@ -48,6 +48,7 @@  int digest_cache_verif_set(struct file *file, const char *verif_id, void *data,
 			   size_t size);
 void *digest_cache_verif_get(struct digest_cache *digest_cache,
 			     const char *verif_id);
+bool digest_cache_was_reset(struct digest_cache *digest_cache);
 
 #else
 static inline struct digest_cache *digest_cache_get(struct dentry *dentry)
@@ -79,5 +80,10 @@  static inline void *digest_cache_verif_get(struct digest_cache *digest_cache,
 	return NULL;
 }
 
+static inline bool digest_cache_was_reset(struct digest_cache *digest_cache)
+{
+	return false;
+}
+
 #endif /* CONFIG_SECURITY_DIGEST_CACHE */
 #endif /* _LINUX_DIGEST_CACHE_H */
diff --git a/security/digest_cache/Kconfig b/security/digest_cache/Kconfig
index dc9ed8f0f883..cd397eb64140 100644
--- a/security/digest_cache/Kconfig
+++ b/security/digest_cache/Kconfig
@@ -2,6 +2,7 @@ 
 config SECURITY_DIGEST_CACHE
 	bool "Digest_cache LSM"
 	select TLV_PARSER
+	select SECURITY_PATH
 	default n
 	help
 	   This option enables an LSM maintaining a cache of digests
diff --git a/security/digest_cache/Makefile b/security/digest_cache/Makefile
index e417da0383ab..3d5e600a2c45 100644
--- a/security/digest_cache/Makefile
+++ b/security/digest_cache/Makefile
@@ -4,7 +4,8 @@ 
 
 obj-$(CONFIG_SECURITY_DIGEST_CACHE) += digest_cache.o
 
-digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o
+digest_cache-y := main.o secfs.o htable.o populate.o modsig.o verif.o dir.o \
+		  reset.o
 
 digest_cache-y += parsers/tlv.o
 digest_cache-y += parsers/rpm.o
diff --git a/security/digest_cache/internal.h b/security/digest_cache/internal.h
index bbef5ab83107..0517e648fbb7 100644
--- a/security/digest_cache/internal.h
+++ b/security/digest_cache/internal.h
@@ -18,6 +18,7 @@ 
 #define INVALID			1	/* Digest cache marked as invalid. */
 #define IS_DIR			2	/* Digest cache created from dir. */
 #define DIR_PREFETCH		3	/* Prefetching requested for dir. */
+#define RESET			4	/* Digest cache to be recreated. */
 
 /**
  * struct readdir_callback - Structure to store information for dir iteration
@@ -247,4 +248,12 @@  digest_cache_dir_lookup_filename(struct dentry *dentry,
 				 char *filename);
 void digest_cache_dir_free(struct digest_cache *digest_cache);
 
+/* reset.c */
+int digest_cache_file_open(struct file *file);
+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);
+
 #endif /* _DIGEST_CACHE_INTERNAL_H */
diff --git a/security/digest_cache/main.c b/security/digest_cache/main.c
index e6598f81074a..b192628e30db 100644
--- a/security/digest_cache/main.c
+++ b/security/digest_cache/main.c
@@ -162,6 +162,11 @@  struct digest_cache *digest_cache_create(struct dentry *dentry,
 
 	/* Serialize check and assignment of dig_owner. */
 	mutex_lock(&dig_sec->dig_owner_mutex);
+	if (dig_sec->dig_owner && test_bit(RESET, &dig_sec->dig_owner->flags)) {
+		digest_cache_put(dig_sec->dig_owner);
+		dig_sec->dig_owner = NULL;
+	}
+
 	if (dig_sec->dig_owner) {
 		/* Increment ref. count for reference returned to the caller. */
 		digest_cache = digest_cache_ref(dig_sec->dig_owner);
@@ -394,6 +399,11 @@  struct digest_cache *digest_cache_get(struct dentry *dentry)
 
 	/* 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(). */
@@ -482,6 +492,11 @@  static void digest_cache_inode_free_security(struct inode *inode)
 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, digest_cache_inode_free_security),
+	LSM_HOOK_INIT(file_open, digest_cache_file_open),
+	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),
 };
 
 /**
diff --git a/security/digest_cache/reset.c b/security/digest_cache/reset.c
new file mode 100644
index 000000000000..b4968ac993f0
--- /dev/null
+++ b/security/digest_cache/reset.c
@@ -0,0 +1,168 @@ 
+// 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_was_reset - Report whether or not the digest cache was reset
+ * @digest_cache: Digest cache
+ *
+ * This function reports whether or not the RESET bit was set in the digest
+ * cache.
+ *
+ * It is meant to be used by digest_cache LSM users holding a reference of a
+ * digest cache, which might need to take additional actions depending on
+ * whether or not that digest cache was reset.
+ *
+ * Return: True if the digest cache was reset, false otherwise.
+ */
+bool digest_cache_was_reset(struct digest_cache *digest_cache)
+{
+	return test_bit(RESET, &digest_cache->flags);
+}
+EXPORT_SYMBOL_GPL(digest_cache_was_reset);
+
+/**
+ * digest_cache_reset - Reset the digest cache
+ * @inode: Inode of the digest list/directory containing the digest list
+ * @reason: Reason for reset
+ *
+ * This function sets the RESET bit in the digest cache, so that
+ * digest_cache_get() and digest_cache_create() respectively release and clear
+ * dig_user and dig_owner in the inode security blob. This causes new callers
+ * of digest_cache_get() to get a new digest cache.
+ */
+static void digest_cache_reset(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 %s, reason: %s\n",
+			 dig_sec->dig_owner->path_str, reason);
+		set_bit(RESET, &dig_sec->dig_owner->flags);
+	}
+	mutex_unlock(&dig_sec->dig_owner_mutex);
+}
+
+/**
+ * digest_cache_file_open - A file is being opened
+ * @file: File descriptor
+ *
+ * This function is called when a file is opened. If the inode is a digest list
+ * and is opened for write, it resets the inode dig_owner, to force rebuilding
+ * the digest cache.
+ *
+ * Return: Zero.
+ */
+int digest_cache_file_open(struct file *file)
+{
+	if (!S_ISREG(file_inode(file)->i_mode) || !(file->f_mode & FMODE_WRITE))
+		return 0;
+
+	digest_cache_reset(file_inode(file), "file_open_write");
+	return 0;
+}
+
+/**
+ * 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, it resets the inode 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);
+
+	if (!S_ISREG(inode->i_mode))
+		return 0;
+
+	digest_cache_reset(inode, "file_truncate");
+	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 parent inode is the digest list directory, the inode
+ * is a regular file and was opened for write, it resets the inode dig_owner,
+ * to force rebuilding the digest cache.
+ */
+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))
+		return;
+
+	digest_cache_reset(dir, "dir_file_release");
+}
+
+/**
+ * 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, 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))
+		return 0;
+
+	digest_cache_reset(inode, "file_unlink");
+	digest_cache_reset(dir, "dir_unlink");
+	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 that inode
+ * is 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))
+		return 0;
+
+	digest_cache_reset(old_inode, "file_rename");
+	digest_cache_reset(old_dir, "dir_rename_from");
+	digest_cache_reset(new_dir, "dir_rename_to");
+	return 0;
+}