[PATCH v4 2/5] fscrypt: allow filesystems to disable symlink target caching

From: Alex Markuze

Date: Tue Oct 20 2026 - 08:59:04 EST


fscrypt caches decrypted symlink targets in inode->i_link. The VFS
can return that target without calling the filesystem's ->get_link(),
so access checks in that callback cannot revoke a previously cached
link. CephFS subvolume quarantine needs those checks even when
readlinkat() uses an empty path on an O_PATH descriptor opened before
quarantine.

Add a filesystem opt-out that prevents both creation-time and lookup-
time cache installation. Leave cache readers unchanged: the option is
fixed before inode creation, so they find no cached target. Uncached
targets use the existing delayed-free callback.

Opting out costs a temporary allocation and decryption on each access
to an encrypted symlink, including readlink, traversal and lstat. Other
filesystems keep their existing caching behavior. The Ceph opt-in is
in the quarantine enforcement patch in this series.

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 a7a8a3f581a0..6bc7ac82ec34 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 28f2108e29f3..ab77e685c75c 100644
--- a/include/linux/fscrypt.h
+++ b/include/linux/fscrypt.h
@@ -107,6 +107,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