[PATCH v3 3/3] exfat: reclaim name filters under memory pressure

From: Yang Wen

Date: Wed Sep 02 2026 - 12:11:15 EST


Each active large directory can hold a 64 KiB name filter. Add a
per-superblock LRU and shrinker so filters can be reclaimed when memory is
tight.

Signed-off-by: Yang Wen <anmuxixixi@xxxxxxxxx>
---
fs/exfat/dir.c | 145 +++++++++++++++++++++++++++++++++++++++++++-
fs/exfat/exfat_fs.h | 10 +++
fs/exfat/super.c | 8 +++
3 files changed, 160 insertions(+), 3 deletions(-)

diff --git a/fs/exfat/dir.c b/fs/exfat/dir.c
index 5145f09bcee6..c7aa50d41e9f 100644
--- a/fs/exfat/dir.c
+++ b/fs/exfat/dir.c
@@ -9,6 +9,7 @@
#include <linux/buffer_head.h>
#include <linux/filelock.h>
#include <linux/hash.h>
+#include <linux/shrinker.h>
#include <linux/stringhash.h>

#include "exfat_raw.h"
@@ -91,12 +92,144 @@ static void exfat_name_filter_indexes(struct super_block *sb,
EXFAT_NAME_FILTER_ORDER);
}

+static unsigned long *
+exfat_name_filter_detach_locked(struct exfat_sb_info *sbi,
+ struct exfat_inode_info *ei)
+{
+ unsigned long *filter = ei->name_filter;
+
+ if (!filter)
+ return NULL;
+
+ ei->name_filter = NULL;
+ list_del_init(&ei->name_filter_lru);
+ sbi->name_filter_count--;
+ return filter;
+}
+
+static void exfat_name_filter_touch(struct exfat_inode_info *ei)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(ei->vfs_inode.i_sb);
+
+ spin_lock(&sbi->name_filter_lock);
+ if (ei->name_filter)
+ list_move_tail(&ei->name_filter_lru, &sbi->name_filter_lru);
+ spin_unlock(&sbi->name_filter_lock);
+}
+
void exfat_name_filter_free(struct inode *inode)
{
struct exfat_inode_info *ei = EXFAT_I(inode);
+ struct exfat_sb_info *sbi;
+ unsigned long *filter;

- kvfree(ei->name_filter);
- ei->name_filter = NULL;
+ if (!READ_ONCE(ei->name_filter))
+ return;
+
+ sbi = EXFAT_SB(inode->i_sb);
+ spin_lock(&sbi->name_filter_lock);
+ filter = exfat_name_filter_detach_locked(sbi, ei);
+ spin_unlock(&sbi->name_filter_lock);
+ kvfree(filter);
+}
+
+static unsigned long
+exfat_name_filter_count_objects(struct shrinker *shrinker,
+ struct shrink_control *sc)
+{
+ struct exfat_sb_info *sbi = shrinker->private_data;
+ unsigned long count;
+
+ spin_lock(&sbi->name_filter_lock);
+ count = sbi->name_filter_count;
+ spin_unlock(&sbi->name_filter_lock);
+
+ return count ? count : SHRINK_EMPTY;
+}
+
+static unsigned long
+exfat_name_filter_scan_objects(struct shrinker *shrinker,
+ struct shrink_control *sc)
+{
+ struct exfat_sb_info *sbi = shrinker->private_data;
+ unsigned long freed = 0;
+
+ /* Avoid reclaim recursion from a GFP_NOFS allocation under s_lock. */
+ if (!mutex_trylock(&sbi->s_lock)) {
+ sc->nr_scanned = 0;
+ return SHRINK_STOP;
+ }
+
+ while (freed < sc->nr_to_scan) {
+ struct exfat_inode_info *ei;
+ unsigned long *filter;
+
+ spin_lock(&sbi->name_filter_lock);
+ if (list_empty(&sbi->name_filter_lru)) {
+ spin_unlock(&sbi->name_filter_lock);
+ break;
+ }
+
+ ei = list_first_entry(&sbi->name_filter_lru,
+ struct exfat_inode_info,
+ name_filter_lru);
+ filter = exfat_name_filter_detach_locked(sbi, ei);
+ spin_unlock(&sbi->name_filter_lock);
+
+ kvfree(filter);
+ freed++;
+ cond_resched();
+ }
+
+ mutex_unlock(&sbi->s_lock);
+ sc->nr_scanned = freed;
+ return freed;
+}
+
+void exfat_name_filter_shrinker_register(struct super_block *sb)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(sb);
+ struct shrinker *shrinker;
+
+ shrinker = shrinker_alloc(SHRINKER_NONSLAB,
+ "exfat-name-filter:%s", sb->s_id);
+ if (!shrinker) {
+ exfat_warn(sb, "failed to allocate name filter shrinker");
+ return;
+ }
+
+ shrinker->count_objects = exfat_name_filter_count_objects;
+ shrinker->scan_objects = exfat_name_filter_scan_objects;
+ shrinker->private_data = sbi;
+ shrinker_register(shrinker);
+ sbi->name_filter_shrinker = shrinker;
+}
+
+void exfat_name_filter_shrinker_unregister(struct super_block *sb)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(sb);
+ struct shrinker *shrinker = sbi->name_filter_shrinker;
+
+ sbi->name_filter_shrinker = NULL;
+ shrinker_free(shrinker);
+
+ for (;;) {
+ struct exfat_inode_info *ei;
+ unsigned long *filter;
+
+ spin_lock(&sbi->name_filter_lock);
+ if (list_empty(&sbi->name_filter_lru)) {
+ spin_unlock(&sbi->name_filter_lock);
+ break;
+ }
+
+ ei = list_first_entry(&sbi->name_filter_lru,
+ struct exfat_inode_info,
+ name_filter_lru);
+ filter = exfat_name_filter_detach_locked(sbi, ei);
+ spin_unlock(&sbi->name_filter_lock);
+ kvfree(filter);
+ }
}

bool exfat_name_filter_maybe_contains(struct inode *inode,
@@ -108,6 +241,7 @@ bool exfat_name_filter_maybe_contains(struct inode *inode,
if (!ei->name_filter)
return true;

+ exfat_name_filter_touch(ei);
exfat_name_filter_indexes(inode->i_sb, name, indexes);
return test_bit(indexes[0], ei->name_filter) &&
test_bit(indexes[1], ei->name_filter) &&
@@ -123,6 +257,7 @@ void exfat_name_filter_add(struct inode *inode,
if (!ei->name_filter)
return;

+ exfat_name_filter_touch(ei);
exfat_name_filter_indexes(inode->i_sb, name, indexes);
__set_bit(indexes[0], ei->name_filter);
__set_bit(indexes[1], ei->name_filter);
@@ -145,7 +280,7 @@ static void exfat_build_name_filter(struct super_block *sb,
struct exfat_sb_info *sbi = EXFAT_SB(sb);
int i;

- if (ei->name_filter ||
+ if (!sbi->name_filter_shrinker || ei->name_filter ||
exfat_bytes_to_dentries(i_size_read(inode)) <
EXFAT_NAME_FILTER_MIN_DENTRIES)
return;
@@ -195,7 +330,11 @@ static void exfat_build_name_filter(struct super_block *sb,
}

complete:
+ spin_lock(&sbi->name_filter_lock);
ei->name_filter = filter;
+ list_add_tail(&ei->name_filter_lru, &sbi->name_filter_lru);
+ sbi->name_filter_count++;
+ spin_unlock(&sbi->name_filter_lock);
return;
abort:
kvfree(filter);
diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h
index 5187dee47cf1..b81cfbdd117f 100644
--- a/fs/exfat/exfat_fs.h
+++ b/fs/exfat/exfat_fs.h
@@ -14,6 +14,8 @@
#include <uapi/linux/exfat.h>
#include <linux/buffer_head.h>

+struct shrinker;
+
#define EXFAT_ROOT_INO 1

/*
@@ -260,6 +262,11 @@ struct exfat_sb_info {

spinlock_t inode_hash_lock;
struct hlist_head inode_hashtable[EXFAT_HASH_SIZE];
+ /* Protects name_filter_lru and name_filter_count. */
+ spinlock_t name_filter_lock;
+ struct list_head name_filter_lru;
+ unsigned long name_filter_count;
+ struct shrinker *name_filter_shrinker;
struct rcu_head rcu;
};

@@ -291,6 +298,7 @@ struct exfat_inode_info {
struct exfat_hint_femp hint_femp;
/* Complete, in-memory Bloom filter of directory names */
unsigned long *name_filter;
+ struct list_head name_filter_lru;

spinlock_t cache_lru_lock;
struct list_head cache_lru;
@@ -631,6 +639,8 @@ bool exfat_name_filter_maybe_contains(struct inode *inode,
void exfat_name_filter_add(struct inode *inode,
const struct exfat_uni_name *name);
void exfat_name_filter_free(struct inode *inode);
+void exfat_name_filter_shrinker_register(struct super_block *sb);
+void exfat_name_filter_shrinker_unregister(struct super_block *sb);

static inline int exfat_chain_advance(struct super_block *sb,
struct exfat_chain *chain, unsigned int step)
diff --git a/fs/exfat/super.c b/fs/exfat/super.c
index f73cf2d02365..3f9d880ec363 100644
--- a/fs/exfat/super.c
+++ b/fs/exfat/super.c
@@ -45,6 +45,7 @@ static void exfat_put_super(struct super_block *sb)
{
struct exfat_sb_info *sbi = EXFAT_SB(sb);

+ exfat_name_filter_shrinker_unregister(sb);
mutex_lock(&sbi->s_lock);
exfat_clear_volume_dirty(sb);
exfat_free_bitmap(sbi);
@@ -196,6 +197,7 @@ static struct inode *exfat_alloc_inode(struct super_block *sb)
return NULL;

ei->name_filter = NULL;
+ INIT_LIST_HEAD(&ei->name_filter_lru);
return &ei->vfs_inode;
}

@@ -732,6 +734,8 @@ static int exfat_fill_super(struct super_block *sb, struct fs_context *fc)
goto free_table;
}

+ exfat_name_filter_shrinker_register(sb);
+
return 0;

put_inode:
@@ -827,6 +831,10 @@ static int exfat_init_fs_context(struct fs_context *fc)

mutex_init(&sbi->s_lock);
mutex_init(&sbi->bitmap_lock);
+ spin_lock_init(&sbi->name_filter_lock);
+ INIT_LIST_HEAD(&sbi->name_filter_lru);
+ sbi->name_filter_count = 0;
+ sbi->name_filter_shrinker = NULL;
ratelimit_state_init(&sbi->ratelimit, DEFAULT_RATELIMIT_INTERVAL,
DEFAULT_RATELIMIT_BURST);

--
2.34.1