From 3ab428ea16afdc36ab80125cb7aca6e897b9c688 Mon Sep 17 00:00:00 2001 From: Ameer Hamza Date: Thu, 2 Apr 2026 23:58:40 +0500 Subject: [PATCH] NFSD: return ESTALE for snapdir entries when export lookup fails When nfsd_cross_mnt() crosses into a mounted ZFS snapshot but rqst_exp_get_by_name() fails to resolve the sub-export (-ENOENT), the error is silently converted to success and the automount stub dentry is returned to the caller. The stub has simple_dir_operations and its file handle is encoded with gen=1 (snapshot is mounted). This 44-byte gen=1 handle becomes a permanent trap: zfsctl_snapdir_vget() sees gen=1 matching d_mountpoint=true, returns the stub inode, and READDIR returns NFS4_OK with zero entries. The client caches this empty result indefinitely since there is no error signal to trigger re-resolution. The empty directory persists until change_info4 updates (e.g., manual snapshot creation on the server). For zfs_snapdir exports, return -ESTALE instead of silently falling back to the automount stub. This causes the client to re-resolve via LOOKUP. Signed-off-by: Ameer Hamza --- fs/nfsd/vfs.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c index dd7cf0012fbe..4fcf9e8be418 100644 --- a/fs/nfsd/vfs.c +++ b/fs/nfsd/vfs.c @@ -204,8 +204,23 @@ nfsd_cross_mnt(struct svc_rqst *rqstp, struct dentry **dpp, * allowed without an explicit export of the new * directory. */ - if (err == -ENOENT && !(exp->ex_flags & NFSEXP_V4ROOT)) - err = 0; + if (err == -ENOENT && !(exp->ex_flags & NFSEXP_V4ROOT)) { +#ifdef CONFIG_TRUENAS + /* + * For ZFS snapshot entries under a zfs_snapdir + * export, the fallback dentry is an automount + * stub with simple_dir_operations that returns + * empty READDIR (NFS4_OK, zero entries). The + * client caches this silently with no error + * signal to trigger re-resolution. Return ESTALE + * so the client retries via LOOKUP. + */ + if (is_snapdir) + err = -ESTALE; + else +#endif /* CONFIG_TRUENAS */ + err = 0; + } path_put(&path); goto out; }