[PATCH v6 2/5] fscrypt: allow filesystems to disable symlink target caching
From: Alex Markuze
Date: Sun Oct 25 2026 - 14:46:26 EST
The VFS can return a decrypted symlink target from inode->i_link without
calling the filesystem's ->get_link(). A filesystem that revalidates
access in ->get_link() therefore needs a way to prevent that cache from
bypassing its checks. CephFS quarantine requires revalidation even for
readlinkat() through an O_PATH descriptor opened before quarantine.
Add a filesystem opt-out from decrypted symlink target caching. The
option is fixed before inode creation and prevents both creation-time
and lookup-time cache installation. Uncached targets use the existing
delayed-free callback; cache readers need no special handling because
the inode has no cached target.
The Ceph quarantine access patch selects this option. Encrypted symlink
readlink, traversal and lstat then allocate and decrypt a temporary
target on each access. Filesystems that do not opt out retain caching.
Signed-off-by: Alex Markuze <amarkuze@xxxxxxxxxx>
---
fs/crypto/hooks.c | 28 +++++++++++++++-------------
include/linux/fscrypt.h | 7 +++++++
2 files changed, 22 insertions(+), 13 deletions(-)
diff --git a/fs/crypto/hooks.c b/fs/crypto/hooks.c
index a7a8a3f581a0460537f5ff955bc8b5d30573a033..6bc7ac82ec3470d633f2203a40c00a422b8e76a3 100644
--- a/fs/crypto/hooks.c
+++ b/fs/crypto/hooks.c
@@ -321,11 +321,13 @@ int __fscrypt_encrypt_symlink(struct inode *inode, const char *target,
*/
sd->encrypted_path[ciphertext_len] = '\0';
- /* Cache the plaintext symlink target for later use by get_link() */
- err = -ENOMEM;
- inode->i_link = kmemdup(target, len + 1, GFP_NOFS);
- if (!inode->i_link)
- goto err_free_sd;
+ /* Cache the plaintext target unless the filesystem revalidates it. */
+ if (!inode->i_sb->s_cop->no_symlink_cache) {
+ err = -ENOMEM;
+ inode->i_link = kmemdup(target, len + 1, GFP_NOFS);
+ if (!inode->i_link)
+ goto err_free_sd;
+ }
if (!disk_link->name)
disk_link->name = (unsigned char *)sd;
@@ -411,12 +413,12 @@ const char *fscrypt_get_symlink(struct inode *inode, const void *caddr,
pstr.name[pstr.len] = '\0';
/*
- * Cache decrypted symlink targets in i_link for later use. Don't cache
- * symlink targets encoded without the key, since those become outdated
- * once the key is added. This pairs with the READ_ONCE() above and in
- * the VFS path lookup code.
+ * Cache decrypted targets unless the filesystem revalidates them.
+ * Don't cache targets encoded without the key, since those become
+ * outdated once the key is added. This pairs with the READ_ONCE()
+ * above and in the VFS path lookup code.
*/
- if (!has_key ||
+ if (inode->i_sb->s_cop->no_symlink_cache || !has_key ||
cmpxchg_release(&inode->i_link, NULL, pstr.name) != NULL)
set_delayed_call(done, kfree_link, pstr.name);
@@ -442,9 +444,9 @@ EXPORT_SYMBOL_GPL(fscrypt_get_symlink);
* This requires reading the symlink target from disk if needed, setting up the
* inode's encryption key if possible, and then decrypting or encoding the
* symlink target. This makes lstat() more heavyweight than is normally the
- * case. However, decrypted symlink targets will be cached in ->i_link, so
- * usually the symlink won't have to be read and decrypted again later if/when
- * it is actually followed, readlink() is called, or lstat() is called again.
+ * case. Unless the filesystem sets no_symlink_cache, decrypted targets
+ * are cached in ->i_link, avoiding another read and decryption when the
+ * symlink is followed, readlink() is called, or lstat() is called again.
*
* Return: 0 on success, -errno on failure
*/
diff --git a/include/linux/fscrypt.h b/include/linux/fscrypt.h
index 28f2108e29f3976ebe861c425b129ac0acec479b..ab77e685c75c5d1132db82e5cfa718ba6b2847fe 100644
--- a/include/linux/fscrypt.h
+++ b/include/linux/fscrypt.h
@@ -108,6 +108,13 @@ struct fscrypt_operations {
unsigned int supports_subblock_data_units : 1;
/*
+ * Don't populate ->i_link, so every access calls ->get_link() and the
+ * filesystem can revalidate access. Set before creating any inodes;
+ * this option cannot invalidate previously cached targets.
+ */
+ unsigned int no_symlink_cache : 1;
+
+ /*
* This field exists only for backwards compatibility reasons and should
* only be set by the filesystems that are setting it already. It
* contains the filesystem-specific key description prefix that is