Re: [PATCH/RFC] nfsd: rate limit requests that result in -ESTALE from the filesystem
From: "Chuck Lever" <cel@kernel.org>
Date: 2026-01-25 21:53:21
Also in:
linux-fsdevel, linux-nfs
On Fri, Jan 23, 2026, at 6:09 PM, NeilBrown wrote:
From: NeilBrown <neil@brown.name> Subject: [PATCH] nfsd: rate limit requests that result in -ESTALE from the filesystem NFS file handles typically contain a 32 bit generation number which is randomly generated by the filesystem when the inode (or inode number) is allocated. This makes it hard to guess correct file handles. Hard but not impossible on a low latency network with a high speed server.
Is this claim accurate for commonly-exported filesystems? Looking
at the kernel sources:
- btrfs sets i_generation = trans->transid (a sequential counter
starting from 1, incremented per transaction)
- ext2 uses sbi->s_next_generation++ (sequential counter seeded
randomly at mount)
- ocfs2 uses osb->s_next_generation++ (sequential counter)
For btrfs in particular, the generation number contains zero bits of
randomness. An attacker who can estimate the filesystem age or
transaction count can predict valid generation numbers with high
probability. Does the "1 year" security estimate hold for btrfs
exports, or is that filesystem effectively unprotected by this
mitigation?
Filehandles already contain 32 bits of randomness.
This appears true for ext4, f2fs, XFS v3+, and tmpfs (which use get_random_u32()), but not for btrfs, ext2, or ocfs2 as noted above. Additionally, NFS re-exports (fs/nfs/export.c) use no generation number at all. The nfs_encode_fh() function embeds the backing server's filehandle verbatim rather than generating a local generation number. Protection depends entirely on what the backing NFS server uses. If someone re-exports a btrfs-backed NFS mount, the sequential transaction IDs on the backing server provide minimal protection. We can't legitimately extend your cryptanalysis to non-Linux servers that back re-exported NFS mounts.
The only known attack methodology is to guess the inode number of a file of interest, then iterate over all possible generation numbers.
Is generation number brute-forcing the optimal attack? Inode numbers are typically sequential and the valid range is much smaller than 2^32. Could an attacker first enumerate valid inode numbers (which also triggers ESTALE and the rate limit), then brute-force the generation for interesting inodes? The rate limit slows both phases equally, but inode enumeration requires far fewer guesses.
quoted hunk ↗ jump to hunk
diff --git a/fs/nfsd/netns.h b/fs/nfsd/netns.h index 9fa600602658..f7229d1f9d86 100644 --- a/fs/nfsd/netns.h +++ b/fs/nfsd/netns.h@@ -219,6 +219,8 @@ struct nfsd_net { /* last time an admin-revoke happened for NFSv4.0 */ time64_t nfs40_last_revoke; + struct mutex estale_rate_limit_mutex; + #if IS_ENABLED(CONFIG_NFS_LOCALIO) /* Local clients to be invalidated when net is shut down */ spinlock_t local_clients_lock;diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c index 7587c64bf26d..55d25d9b414f 100644 --- a/fs/nfsd/nfsctl.c +++ b/fs/nfsd/nfsctl.c@@ -2198,6 +2198,7 @@ static __net_init int nfsd_net_init(struct net*net) nfsd4_init_leases_net(nn); get_random_bytes(&nn->siphash_key, sizeof(nn->siphash_key)); seqlock_init(&nn->writeverf_lock); + mutex_init(&nn->estale_rate_limit_mutex); #if IS_ENABLED(CONFIG_NFS_LOCALIO) spin_lock_init(&nn->local_clients_lock); INIT_LIST_HEAD(&nn->local_clients);diff --git a/fs/nfsd/nfsfh.c b/fs/nfsd/nfsfh.c index ed85dd43da18..7032f65fe21a 100644 --- a/fs/nfsd/nfsfh.c +++ b/fs/nfsd/nfsfh.c@@ -244,6 +244,8 @@ static __be32 nfsd_set_fh_dentry(struct svc_rqst*rqstp, struct net *net, data_left, fileid_type, 0, nfsd_acceptable, exp); if (IS_ERR_OR_NULL(dentry)) { + struct nfsd_net *nn = net_generic(net, nfsd_net_id); + trace_nfsd_set_fh_dentry_badhandle(rqstp, fhp, dentry ? PTR_ERR(dentry) : -ESTALE); switch (PTR_ERR(dentry)) {@@ -252,6 +254,19 @@ static __be32 nfsd_set_fh_dentry(struct svc_rqst*rqstp, struct net *net, break; default: dentry = ERR_PTR(-ESTALE); + /* We limit ESTALE returns to 1 every + * 15 milliseconds (across all threads) to + * prevent a client from guessing the + * correct (32 bit) generation number + * for an given inode in significantly + * less than 1 year. This ensures clients + * can only access files for which they + * are allowed to access a path from the + * exported root. + */ + mutex_lock(&nn->estale_rate_limit_mutex); + msleep(15); + mutex_unlock(&nn->estale_rate_limit_mutex);
The global mutex serializes all ESTALE errors across all nfsd threads in the network namespace. The impact on legitimate workloads must be carefully considered. For example, during file deletion or migration, multiple clients may encounter genuinely stale file handles simultaneously. With N concurrent ESTALE errors, the total delay becomes N * 15ms as each thread waits for the mutex. With 100 concurrent stale handle accesses, this adds 1.5 seconds of accumulated latency. Additionally, an attacker does not need to successfully guess file handles to cause harm. Simply sending requests with invalid file handles at a modest rate (e.g., 10/sec) can monopolize 15% of the mutex time, slowing all legitimate ESTALE processing on the server. Would per-client rate limiting (e.g., a token bucket per client IP) avoid penalizing legitimate operations while still throttling attacks? Or, as Jeff suggested, narrowing the blast radius to per-inode limiting? Lastly, we've spilled a large number of electrons trying to remove needless delays of nfsd threads (eg, waiting synchronously for NFS clients to respond to NFSv4 callbacks) because a malicious client can use this delay as a timing attack to determine the (maximum) number of available nfsd threads and simply keep them all busy, preventing the server from doing useful work.
} } }
I think given the gaps in covering exports of first-tier file systems such as btrfs, the unknowns in protecting re-exported NFS mounts, and the increase in the surface of timing attacks, this approach needs significant refinement before I would feel comfortable with it. -- Chuck Lever