From 89330d3a60f7ddb9cf42873bb5249d6a736a0eab Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:45 +0100 Subject: [PATCH 1/8] filelock: pass current blocking lease to trace_break_lease_block() rather than "new_fl" The break_lease_block tracepoint currently just shows the type of "new_fl", which we can predict from the "flags" value. Switch it to display info about "fl" instead, as that's the file_lease on which the code is blocking. For trace_break_lease_unblock(), pass it a NULL pointer. "fl" may have been freed by that point, and passing it the info in new_fl is deceptive. Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-1-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/locks.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/locks.c b/fs/locks.c index fead53474c30..3306892900c4 100644 --- a/fs/locks.c +++ b/fs/locks.c @@ -1691,7 +1691,7 @@ int __break_lease(struct inode *inode, unsigned int flags) } else break_time++; locks_insert_block(&fl->c, &new_fl->c, leases_conflict); - trace_break_lease_block(inode, new_fl); + trace_break_lease_block(inode, fl); spin_unlock(&ctx->flc_lock); percpu_up_read(&file_rwsem); @@ -1702,7 +1702,7 @@ int __break_lease(struct inode *inode, unsigned int flags) percpu_down_read(&file_rwsem); spin_lock(&ctx->flc_lock); - trace_break_lease_unblock(inode, new_fl); + trace_break_lease_unblock(inode, NULL); __locks_delete_block(&new_fl->c); if (error >= 0) { /* From 24cbf43337f46329ddda5983bc3c585174a020ee Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:46 +0100 Subject: [PATCH 2/8] filelock: add support for ignoring deleg breaks for dir change events If a NFS client requests a directory delegation with a notification bitmask covering directory change events, the server shouldn't recall the delegation. Instead the client will be notified of the change after the fact. Add support for ignoring lease breaks on directory changes. Add a new flags parameter to try_break_deleg() and teach __break_lease how to ignore certain types of delegation break events. Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-2-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/attr.c | 2 +- fs/locks.c | 86 ++++++++++++++++++++++----------- fs/namei.c | 31 ++++++------ fs/posix_acl.c | 4 +- fs/xattr.c | 4 +- include/linux/filelock.h | 53 ++++++++++++++------ include/trace/events/filelock.h | 5 +- 7 files changed, 122 insertions(+), 63 deletions(-) diff --git a/fs/attr.c b/fs/attr.c index ded221defae6..4f437fabb7f0 100644 --- a/fs/attr.c +++ b/fs/attr.c @@ -547,7 +547,7 @@ int notify_change(struct mnt_idmap *idmap, struct dentry *dentry, * breaking the delegation in this case. */ if (!(ia_valid & ATTR_DELEG)) { - error = try_break_deleg(inode, delegated_inode); + error = try_break_deleg(inode, 0, delegated_inode); if (error) return error; } diff --git a/fs/locks.c b/fs/locks.c index 3306892900c4..31086d6153e2 100644 --- a/fs/locks.c +++ b/fs/locks.c @@ -1583,29 +1583,63 @@ static bool leases_conflict(struct file_lock_core *lc, struct file_lock_core *bc } static bool -any_leases_conflict(struct inode *inode, struct file_lease *breaker) +ignore_dir_deleg_break(struct file_lease *fl, unsigned int flags) { - struct file_lock_context *ctx = inode->i_flctx; - struct file_lock_core *flc; + if ((flags & LEASE_BREAK_DIR_CREATE) && (fl->c.flc_flags & FL_IGN_DIR_CREATE)) + return true; + if ((flags & LEASE_BREAK_DIR_DELETE) && (fl->c.flc_flags & FL_IGN_DIR_DELETE)) + return true; + if ((flags & LEASE_BREAK_DIR_RENAME) && (fl->c.flc_flags & FL_IGN_DIR_RENAME)) + return true; - lockdep_assert_held(&ctx->flc_lock); - - list_for_each_entry(flc, &ctx->flc_lease, flc_list) { - if (leases_conflict(flc, &breaker->c)) - return true; - } return false; } +static unsigned int +break_lease_flags_to_type(unsigned int flags) +{ + if (flags & LEASE_BREAK_LEASE) + return FL_LEASE; + else if (flags & LEASE_BREAK_DELEG) + return FL_DELEG; + else if (flags & LEASE_BREAK_LAYOUT) + return FL_LAYOUT; + else + return 0; + +} + +static struct file_lease * +first_visible_lease(struct inode *inode, struct file_lease *new_fl, unsigned int flags) +{ + struct file_lock_context *ctx = locks_inode_context(inode); + struct file_lease *fl; + + lockdep_assert_held(&ctx->flc_lock); + + list_for_each_entry(fl, &ctx->flc_lease, c.flc_list) { + if (!leases_conflict(&fl->c, &new_fl->c)) + continue; + if (S_ISDIR(inode->i_mode) && ignore_dir_deleg_break(fl, flags)) + continue; + return fl; + } + return NULL; +} + + /** - * __break_lease - revoke all outstanding leases on file - * @inode: the inode of the file to return - * @flags: LEASE_BREAK_* flags + * __break_lease - revoke all outstanding leases on file + * @inode: the inode of the file to return + * @flags: LEASE_BREAK_* flags * - * break_lease (inlined for speed) has checked there already is at least - * some kind of lock (maybe a lease) on this file. Leases are broken on - * a call to open() or truncate(). This function can block waiting for the - * lease break unless you specify LEASE_BREAK_NONBLOCK. + * break_lease (inlined for speed) has checked there already is at least + * some kind of lock (maybe a lease) on this file. Leases and Delegations + * are broken on a call to open() or truncate(). Delegations are also + * broken on any event that would change the ctime. Directory delegations + * are broken whenever the directory changes (unless the delegation is set + * up to ignore the event). This function can block waiting for the lease + * break unless you specify LEASE_BREAK_NONBLOCK. */ int __break_lease(struct inode *inode, unsigned int flags) { @@ -1617,13 +1651,8 @@ int __break_lease(struct inode *inode, unsigned int flags) bool want_write = !(flags & LEASE_BREAK_OPEN_RDONLY); int error = 0; - if (flags & LEASE_BREAK_LEASE) - type = FL_LEASE; - else if (flags & LEASE_BREAK_DELEG) - type = FL_DELEG; - else if (flags & LEASE_BREAK_LAYOUT) - type = FL_LAYOUT; - else + type = break_lease_flags_to_type(flags); + if (!type) return -EINVAL; new_fl = lease_alloc(NULL, type, want_write ? F_WRLCK : F_RDLCK); @@ -1642,7 +1671,7 @@ int __break_lease(struct inode *inode, unsigned int flags) time_out_leases(inode, &dispose); - if (!any_leases_conflict(inode, new_fl)) + if (!first_visible_lease(inode, new_fl, flags)) goto out; break_time = 0; @@ -1655,6 +1684,8 @@ int __break_lease(struct inode *inode, unsigned int flags) list_for_each_entry_safe(fl, tmp, &ctx->flc_lease, c.flc_list) { if (!leases_conflict(&fl->c, &new_fl->c)) continue; + if (S_ISDIR(inode->i_mode) && ignore_dir_deleg_break(fl, flags)) + continue; if (want_write) { if (fl->c.flc_flags & FL_UNLOCK_PENDING) continue; @@ -1670,7 +1701,8 @@ int __break_lease(struct inode *inode, unsigned int flags) locks_delete_lock_ctx(&fl->c, &dispose); } - if (list_empty(&ctx->flc_lease)) + fl = first_visible_lease(inode, new_fl, flags); + if (!fl) goto out; if (flags & LEASE_BREAK_NONBLOCK) { @@ -1680,7 +1712,6 @@ int __break_lease(struct inode *inode, unsigned int flags) } restart: - fl = list_first_entry(&ctx->flc_lease, struct file_lease, c.flc_list); break_time = fl->fl_break_time; if (break_time != 0) { if (time_after(jiffies, break_time)) { @@ -1711,7 +1742,8 @@ int __break_lease(struct inode *inode, unsigned int flags) */ if (error == 0) time_out_leases(inode, &dispose); - if (any_leases_conflict(inode, new_fl)) + fl = first_visible_lease(inode, new_fl, flags); + if (fl) goto restart; error = 0; } diff --git a/fs/namei.c b/fs/namei.c index c7fac83c9a85..3a3a2e5e77a0 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -4198,7 +4198,7 @@ int vfs_create(struct mnt_idmap *idmap, struct dentry *dentry, umode_t mode, error = security_inode_create(dir, dentry, mode); if (error) return error; - error = try_break_deleg(dir, di); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, di); if (error) return error; error = dir->i_op->create(idmap, dir, dentry, mode, true); @@ -4497,7 +4497,7 @@ static struct dentry *lookup_open(struct nameidata *nd, struct file *file, /* Negative dentry, just create the file */ if (!dentry->d_inode && (open_flag & O_CREAT)) { /* but break the directory lease first! */ - error = try_break_deleg(dir_inode, delegated_inode); + error = try_break_deleg(dir_inode, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) goto out_dput; @@ -5113,7 +5113,7 @@ int vfs_mknod(struct mnt_idmap *idmap, struct inode *dir, if (error) return error; - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) return error; @@ -5254,7 +5254,7 @@ struct dentry *vfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, if (max_links && dir->i_nlink >= max_links) goto err; - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) goto err; @@ -5359,7 +5359,7 @@ int vfs_rmdir(struct mnt_idmap *idmap, struct inode *dir, if (error) goto out; - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_DELETE, delegated_inode); if (error) goto out; @@ -5489,10 +5489,10 @@ int vfs_unlink(struct mnt_idmap *idmap, struct inode *dir, else { error = security_inode_unlink(dir, dentry); if (!error) { - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_DELETE, delegated_inode); if (error) goto out; - error = try_break_deleg(target, delegated_inode); + error = try_break_deleg(target, 0, delegated_inode); if (error) goto out; error = dir->i_op->unlink(dir, dentry); @@ -5636,7 +5636,7 @@ int vfs_symlink(struct mnt_idmap *idmap, struct inode *dir, if (error) return error; - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) return error; @@ -5767,9 +5767,9 @@ int vfs_link(struct dentry *old_dentry, struct mnt_idmap *idmap, else if (max_links && inode->i_nlink >= max_links) error = -EMLINK; else { - error = try_break_deleg(dir, delegated_inode); + error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (!error) - error = try_break_deleg(inode, delegated_inode); + error = try_break_deleg(inode, 0, delegated_inode); if (!error) error = dir->i_op->link(old_dentry, dir, new_dentry); } @@ -6033,21 +6033,24 @@ int vfs_rename(struct renamedata *rd) old_dir->i_nlink >= max_links) goto out; } - error = try_break_deleg(old_dir, delegated_inode); + error = try_break_deleg(old_dir, + old_dir == new_dir ? LEASE_BREAK_DIR_RENAME : + LEASE_BREAK_DIR_DELETE, + delegated_inode); if (error) goto out; if (new_dir != old_dir) { - error = try_break_deleg(new_dir, delegated_inode); + error = try_break_deleg(new_dir, LEASE_BREAK_DIR_CREATE, delegated_inode); if (error) goto out; } if (!is_dir) { - error = try_break_deleg(source, delegated_inode); + error = try_break_deleg(source, 0, delegated_inode); if (error) goto out; } if (target && !new_is_dir) { - error = try_break_deleg(target, delegated_inode); + error = try_break_deleg(target, 0, delegated_inode); if (error) goto out; } diff --git a/fs/posix_acl.c b/fs/posix_acl.c index 12591c95c925..b4bfe4ddf64e 100644 --- a/fs/posix_acl.c +++ b/fs/posix_acl.c @@ -1126,7 +1126,7 @@ int vfs_set_acl(struct mnt_idmap *idmap, struct dentry *dentry, if (error) goto out_inode_unlock; - error = try_break_deleg(inode, &delegated_inode); + error = try_break_deleg(inode, 0, &delegated_inode); if (error) goto out_inode_unlock; @@ -1234,7 +1234,7 @@ int vfs_remove_acl(struct mnt_idmap *idmap, struct dentry *dentry, if (error) goto out_inode_unlock; - error = try_break_deleg(inode, &delegated_inode); + error = try_break_deleg(inode, 0, &delegated_inode); if (error) goto out_inode_unlock; diff --git a/fs/xattr.c b/fs/xattr.c index 09ecbaaa1660..efdcf2a48585 100644 --- a/fs/xattr.c +++ b/fs/xattr.c @@ -306,7 +306,7 @@ __vfs_setxattr_locked(struct mnt_idmap *idmap, struct dentry *dentry, if (error) goto out; - error = try_break_deleg(inode, delegated_inode); + error = try_break_deleg(inode, 0, delegated_inode); if (error) goto out; @@ -564,7 +564,7 @@ __vfs_removexattr_locked(struct mnt_idmap *idmap, if (error) goto out; - error = try_break_deleg(inode, delegated_inode); + error = try_break_deleg(inode, 0, delegated_inode); if (error) goto out; diff --git a/include/linux/filelock.h b/include/linux/filelock.h index 5f0a2fb31450..9dd4e67a6f30 100644 --- a/include/linux/filelock.h +++ b/include/linux/filelock.h @@ -4,19 +4,22 @@ #include -#define FL_POSIX 1 -#define FL_FLOCK 2 -#define FL_DELEG 4 /* NFSv4 delegation */ -#define FL_ACCESS 8 /* not trying to lock, just looking */ -#define FL_EXISTS 16 /* when unlocking, test for existence */ -#define FL_LEASE 32 /* lease held on this file */ -#define FL_CLOSE 64 /* unlock on close */ -#define FL_SLEEP 128 /* A blocking lock */ -#define FL_DOWNGRADE_PENDING 256 /* Lease is being downgraded */ -#define FL_UNLOCK_PENDING 512 /* Lease is being broken */ -#define FL_OFDLCK 1024 /* lock is "owned" by struct file */ -#define FL_LAYOUT 2048 /* outstanding pNFS layout */ -#define FL_RECLAIM 4096 /* reclaiming from a reboot server */ +#define FL_POSIX BIT(0) /* POSIX lock */ +#define FL_FLOCK BIT(1) /* BSD lock */ +#define FL_DELEG BIT(2) /* NFSv4 delegation */ +#define FL_ACCESS BIT(3) /* not trying to lock, just looking */ +#define FL_EXISTS BIT(4) /* when unlocking, test for existence */ +#define FL_LEASE BIT(5) /* file lease */ +#define FL_CLOSE BIT(6) /* unlock on close */ +#define FL_SLEEP BIT(7) /* A blocking lock */ +#define FL_DOWNGRADE_PENDING BIT(8) /* Lease is being downgraded */ +#define FL_UNLOCK_PENDING BIT(9) /* Lease is being broken */ +#define FL_OFDLCK BIT(10) /* POSIX lock "owned" by struct file */ +#define FL_LAYOUT BIT(11) /* outstanding pNFS layout */ +#define FL_RECLAIM BIT(12) /* reclaiming from a reboot server */ +#define FL_IGN_DIR_CREATE BIT(13) /* ignore DIR_CREATE events */ +#define FL_IGN_DIR_DELETE BIT(14) /* ignore DIR_DELETE events */ +#define FL_IGN_DIR_RENAME BIT(15) /* ignore DIR_RENAME events */ #define FL_CLOSE_POSIX (FL_POSIX | FL_CLOSE) @@ -222,6 +225,10 @@ struct file_lease *locks_alloc_lease(void); #define LEASE_BREAK_LAYOUT BIT(2) // break layouts only #define LEASE_BREAK_NONBLOCK BIT(3) // non-blocking break #define LEASE_BREAK_OPEN_RDONLY BIT(4) // readonly open event +#define LEASE_BREAK_DIR_CREATE BIT(5) // dir deleg create event +#define LEASE_BREAK_DIR_DELETE BIT(6) // dir deleg delete event +#define LEASE_BREAK_DIR_RENAME BIT(7) // dir deleg rename event + int __break_lease(struct inode *inode, unsigned int flags); void lease_get_mtime(struct inode *, struct timespec64 *time); @@ -516,12 +523,26 @@ static inline bool is_delegated(struct delegated_inode *di) return di->di_inode; } -static inline int try_break_deleg(struct inode *inode, +/** + * try_break_deleg - do a non-blocking delegation break + * @inode: inode that should have its delegations broken + * @flags: extra LEASE_BREAK_* flags to pass to break_deleg() + * @di: returns pointer to delegated inode (may be NULL) + * + * Break delegations in a non-blocking fashion. If there are + * outstanding delegations and @di is set, then an extra reference + * will be taken on @inode and @di->di_inode will be populated so + * that it may be waited upon. + * + * Returns 0 if there is no need to wait or an error. If -EWOULDBLOCK + * is returned, then @di will be populated (if non-NULL). + */ +static inline int try_break_deleg(struct inode *inode, unsigned int flags, struct delegated_inode *di) { int ret; - ret = break_deleg(inode, LEASE_BREAK_NONBLOCK); + ret = break_deleg(inode, flags | LEASE_BREAK_NONBLOCK); if (ret == -EWOULDBLOCK && di) { di->di_inode = inode; ihold(inode); @@ -574,7 +595,7 @@ static inline int break_deleg(struct inode *inode, unsigned int flags) return 0; } -static inline int try_break_deleg(struct inode *inode, +static inline int try_break_deleg(struct inode *inode, unsigned int flags, struct delegated_inode *delegated_inode) { return 0; diff --git a/include/trace/events/filelock.h b/include/trace/events/filelock.h index 116774886244..718b5c3f1737 100644 --- a/include/trace/events/filelock.h +++ b/include/trace/events/filelock.h @@ -28,7 +28,10 @@ { FL_DOWNGRADE_PENDING, "FL_DOWNGRADE_PENDING" }, \ { FL_UNLOCK_PENDING, "FL_UNLOCK_PENDING" }, \ { FL_OFDLCK, "FL_OFDLCK" }, \ - { FL_RECLAIM, "FL_RECLAIM"}) + { FL_RECLAIM, "FL_RECLAIM" }, \ + { FL_IGN_DIR_CREATE, "FL_IGN_DIR_CREATE" }, \ + { FL_IGN_DIR_DELETE, "FL_IGN_DIR_DELETE" }, \ + { FL_IGN_DIR_RENAME, "FL_IGN_DIR_RENAME" }) #define show_fl_type(val) \ __print_symbolic(val, \ From e39026a86b485b052ddff1145f5601fc0d04305f Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:47 +0100 Subject: [PATCH 3/8] filelock: add a tracepoint to start of break_lease() ...mostly to show the LEASE_BREAK_* flags. Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-3-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/locks.c | 2 ++ include/trace/events/filelock.h | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/fs/locks.c b/fs/locks.c index 31086d6153e2..afa80fe6e9ff 100644 --- a/fs/locks.c +++ b/fs/locks.c @@ -1651,6 +1651,8 @@ int __break_lease(struct inode *inode, unsigned int flags) bool want_write = !(flags & LEASE_BREAK_OPEN_RDONLY); int error = 0; + trace_break_lease(inode, flags); + type = break_lease_flags_to_type(flags); if (!type) return -EINVAL; diff --git a/include/trace/events/filelock.h b/include/trace/events/filelock.h index 718b5c3f1737..f2bb6b7aa281 100644 --- a/include/trace/events/filelock.h +++ b/include/trace/events/filelock.h @@ -120,6 +120,39 @@ DEFINE_EVENT(filelock_lock, flock_lock_inode, TP_PROTO(struct inode *inode, struct file_lock *fl, int ret), TP_ARGS(inode, fl, ret)); +#define show_lease_break_flags(val) \ + __print_flags(val, "|", \ + { LEASE_BREAK_LEASE, "LEASE" }, \ + { LEASE_BREAK_DELEG, "DELEG" }, \ + { LEASE_BREAK_LAYOUT, "LAYOUT" }, \ + { LEASE_BREAK_NONBLOCK, "NONBLOCK" }, \ + { LEASE_BREAK_OPEN_RDONLY, "OPEN_RDONLY" }, \ + { LEASE_BREAK_DIR_CREATE, "DIR_CREATE" }, \ + { LEASE_BREAK_DIR_DELETE, "DIR_DELETE" }, \ + { LEASE_BREAK_DIR_RENAME, "DIR_RENAME" }) + +TRACE_EVENT(break_lease, + TP_PROTO(struct inode *inode, unsigned int flags), + + TP_ARGS(inode, flags), + + TP_STRUCT__entry( + __field(unsigned long, i_ino) + __field(dev_t, s_dev) + __field(unsigned int, flags) + ), + + TP_fast_assign( + __entry->s_dev = inode->i_sb->s_dev; + __entry->i_ino = inode->i_ino; + __entry->flags = flags; + ), + + TP_printk("dev=0x%x:0x%x ino=0x%lx flags=%s", + MAJOR(__entry->s_dev), MINOR(__entry->s_dev), + __entry->i_ino, show_lease_break_flags(__entry->flags)) +); + DECLARE_EVENT_CLASS(filelock_lease, TP_PROTO(struct inode *inode, struct file_lease *fl), From 95825fdcc0b0e868571d1c755f6a6971caf9b7cb Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:48 +0100 Subject: [PATCH 4/8] filelock: add an inode_lease_ignore_mask helper Add a new routine that returns a mask of all dir change events that are currently ignored by any leases. nfsd will use this to determine how to configure the fsnotify_mark mask. Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-4-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/locks.c | 32 ++++++++++++++++++++++++++++++++ include/linux/filelock.h | 1 + 2 files changed, 33 insertions(+) diff --git a/fs/locks.c b/fs/locks.c index afa80fe6e9ff..6e4ff7fcec05 100644 --- a/fs/locks.c +++ b/fs/locks.c @@ -1582,6 +1582,38 @@ static bool leases_conflict(struct file_lock_core *lc, struct file_lock_core *bc return rc; } +#define IGNORE_MASK (FL_IGN_DIR_CREATE | FL_IGN_DIR_DELETE | FL_IGN_DIR_RENAME) + +/** + * inode_lease_ignore_mask - return union of all ignored inode events for this inode + * @inode: inode of which to get ignore mask + * + * Walk the list of leases, and return the result of all of + * their FL_IGN_DIR_* bits or'ed together. + */ +u32 +inode_lease_ignore_mask(struct inode *inode) +{ + struct file_lock_context *ctx; + struct file_lock_core *flc; + u32 mask = 0; + + ctx = locks_inode_context(inode); + if (!ctx) + return 0; + + spin_lock(&ctx->flc_lock); + list_for_each_entry(flc, &ctx->flc_lease, flc_list) { + mask |= flc->flc_flags & IGNORE_MASK; + /* If we already have everything, we can stop */ + if (mask == IGNORE_MASK) + break; + } + spin_unlock(&ctx->flc_lock); + return mask; +} +EXPORT_SYMBOL_GPL(inode_lease_ignore_mask); + static bool ignore_dir_deleg_break(struct file_lease *fl, unsigned int flags) { diff --git a/include/linux/filelock.h b/include/linux/filelock.h index 9dd4e67a6f30..6e125902c58a 100644 --- a/include/linux/filelock.h +++ b/include/linux/filelock.h @@ -236,6 +236,7 @@ int generic_setlease(struct file *, int, struct file_lease **, void **priv); int kernel_setlease(struct file *, int, struct file_lease **, void **); int vfs_setlease(struct file *, int, struct file_lease **, void **); int lease_modify(struct file_lease *, int, struct list_head *); +u32 inode_lease_ignore_mask(struct inode *inode); struct notifier_block; int lease_register_notifier(struct notifier_block *); From ad4489dcd08dcfbc32ea6e6a4f558cdd459bd80c Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:49 +0100 Subject: [PATCH 5/8] fsnotify: new tracepoint in fsnotify() Add a tracepoint so we can see exactly how this is being called. Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-5-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/notify/fsnotify.c | 5 ++++ include/trace/events/fsnotify.h | 51 +++++++++++++++++++++++++++++++++ include/trace/misc/fsnotify.h | 35 ++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 include/trace/events/fsnotify.h create mode 100644 include/trace/misc/fsnotify.h diff --git a/fs/notify/fsnotify.c b/fs/notify/fsnotify.c index b7198c4744e3..0031f45e702b 100644 --- a/fs/notify/fsnotify.c +++ b/fs/notify/fsnotify.c @@ -14,6 +14,9 @@ #include #include "fsnotify.h" +#define CREATE_TRACE_POINTS +#include + /* * Clear all of the marks on an inode when it is being evicted from core */ @@ -504,6 +507,8 @@ int fsnotify(__u32 mask, const void *data, int data_type, struct inode *dir, int ret = 0; __u32 test_mask, marks_mask = 0; + trace_fsnotify(mask, data, data_type, dir, file_name, inode, cookie); + if (path) mnt = real_mount(path->mnt); diff --git a/include/trace/events/fsnotify.h b/include/trace/events/fsnotify.h new file mode 100644 index 000000000000..341bbd57a39b --- /dev/null +++ b/include/trace/events/fsnotify.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#undef TRACE_SYSTEM +#define TRACE_SYSTEM fsnotify + +#if !defined(_TRACE_FSNOTIFY_H) || defined(TRACE_HEADER_MULTI_READ) +#define _TRACE_FSNOTIFY_H + +#include + +#include + +TRACE_EVENT(fsnotify, + TP_PROTO(__u32 mask, const void *data, int data_type, + struct inode *dir, const struct qstr *file_name, + struct inode *inode, u32 cookie), + + TP_ARGS(mask, data, data_type, dir, file_name, inode, cookie), + + TP_STRUCT__entry( + __field(__u32, mask) + __field(unsigned long, dir_ino) + __field(unsigned long, ino) + __field(dev_t, s_dev) + __field(int, data_type) + __field(u32, cookie) + __string(file_name, file_name ? (const char *)file_name->name : "") + ), + + TP_fast_assign( + __entry->mask = mask; + __entry->dir_ino = dir ? dir->i_ino : 0; + __entry->ino = inode ? inode->i_ino : 0; + __entry->s_dev = dir ? dir->i_sb->s_dev : + inode ? inode->i_sb->s_dev : 0; + __entry->data_type = data_type; + __entry->cookie = cookie; + __assign_str(file_name); + ), + + TP_printk("dev=%d:%d dir=%lu ino=%lu data_type=%d cookie=0x%x mask=0x%x %s name=%s", + MAJOR(__entry->s_dev), MINOR(__entry->s_dev), + __entry->dir_ino, __entry->ino, + __entry->data_type, __entry->cookie, + __entry->mask, show_fsnotify_mask(__entry->mask), + __get_str(file_name)) +); + +#endif /* _TRACE_FSNOTIFY_H */ + +/* This part must be outside protection */ +#include diff --git a/include/trace/misc/fsnotify.h b/include/trace/misc/fsnotify.h new file mode 100644 index 000000000000..a201e1bd6d8c --- /dev/null +++ b/include/trace/misc/fsnotify.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Display helpers for fsnotify events + */ + +#include + +#define show_fsnotify_mask(mask) \ + __print_flags(mask, "|", \ + { FS_ACCESS, "ACCESS" }, \ + { FS_MODIFY, "MODIFY" }, \ + { FS_ATTRIB, "ATTRIB" }, \ + { FS_CLOSE_WRITE, "CLOSE_WRITE" }, \ + { FS_CLOSE_NOWRITE, "CLOSE_NOWRITE" }, \ + { FS_OPEN, "OPEN" }, \ + { FS_MOVED_FROM, "MOVED_FROM" }, \ + { FS_MOVED_TO, "MOVED_TO" }, \ + { FS_CREATE, "CREATE" }, \ + { FS_DELETE, "DELETE" }, \ + { FS_DELETE_SELF, "DELETE_SELF" }, \ + { FS_MOVE_SELF, "MOVE_SELF" }, \ + { FS_OPEN_EXEC, "OPEN_EXEC" }, \ + { FS_UNMOUNT, "UNMOUNT" }, \ + { FS_Q_OVERFLOW, "Q_OVERFLOW" }, \ + { FS_ERROR, "ERROR" }, \ + { FS_OPEN_PERM, "OPEN_PERM" }, \ + { FS_ACCESS_PERM, "ACCESS_PERM" }, \ + { FS_OPEN_EXEC_PERM, "OPEN_EXEC_PERM" }, \ + { FS_PRE_ACCESS, "PRE_ACCESS" }, \ + { FS_MNT_ATTACH, "MNT_ATTACH" }, \ + { FS_MNT_DETACH, "MNT_DETACH" }, \ + { FS_EVENT_ON_CHILD, "EVENT_ON_CHILD" }, \ + { FS_RENAME, "RENAME" }, \ + { FS_DN_MULTISHOT, "DN_MULTISHOT" }, \ + { FS_ISDIR, "ISDIR" }) From 12ffbb117b64d9f4b1b02521010a5f2953a1972a Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:50 +0100 Subject: [PATCH 6/8] fsnotify: add fsnotify_modify_mark_mask() nfsd needs to be able to modify the mask on an existing mark when new directory delegations are set or unset. Add an exported function that allows the caller to set and clear bits in the mark->mask, and does the recalculation if something changed. Suggested-by: Jan Kara Reviewed-by: Jan Kara Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-6-5a0780ba9def@kernel.org Acked-by: Jan Kara Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- fs/notify/mark.c | 29 +++++++++++++++++++++++++++++ include/linux/fsnotify_backend.h | 1 + 2 files changed, 30 insertions(+) diff --git a/fs/notify/mark.c b/fs/notify/mark.c index c2ed5b11b0fe..b1e73c6fd382 100644 --- a/fs/notify/mark.c +++ b/fs/notify/mark.c @@ -310,6 +310,35 @@ void fsnotify_recalc_mask(struct fsnotify_mark_connector *conn) fsnotify_conn_set_children_dentry_flags(conn); } +/** + * fsnotify_modify_mark_mask - set and/or clear flags in a mark's mask + * @mark: mark to be modified + * @set: bits to be set in mask + * @clear: bits to be cleared in mask + * + * Modify a fsnotify_mark mask as directed, and update its associated conn. + * The caller is expected to hold a reference to the mark. + */ +void fsnotify_modify_mark_mask(struct fsnotify_mark *mark, u32 set, u32 clear) +{ + bool recalc = false; + u32 mask; + + WARN_ON_ONCE(clear & set); + + spin_lock(&mark->lock); + mask = mark->mask; + mark->mask |= set; + mark->mask &= ~clear; + if (mark->mask != mask) + recalc = true; + spin_unlock(&mark->lock); + + if (recalc) + fsnotify_recalc_mask(mark->connector); +} +EXPORT_SYMBOL_GPL(fsnotify_modify_mark_mask); + /* Free all connectors queued for freeing once SRCU period ends */ static void fsnotify_connector_destroy_workfn(struct work_struct *work) { diff --git a/include/linux/fsnotify_backend.h b/include/linux/fsnotify_backend.h index 95985400d3d8..66e185bd1b1b 100644 --- a/include/linux/fsnotify_backend.h +++ b/include/linux/fsnotify_backend.h @@ -917,6 +917,7 @@ extern void fsnotify_get_mark(struct fsnotify_mark *mark); extern void fsnotify_put_mark(struct fsnotify_mark *mark); extern void fsnotify_finish_user_wait(struct fsnotify_iter_info *iter_info); extern bool fsnotify_prepare_user_wait(struct fsnotify_iter_info *iter_info); +extern void fsnotify_modify_mark_mask(struct fsnotify_mark *mark, u32 set, u32 clear); static inline void fsnotify_init_event(struct fsnotify_event *event) { From 010043003c0c81c0cedde267076cbe1e0911db49 Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 28 Apr 2026 08:09:51 +0100 Subject: [PATCH 7/8] fsnotify: add FSNOTIFY_EVENT_RENAME data type Add a new fsnotify_rename_data struct and FSNOTIFY_EVENT_RENAME data type that carries both the moved dentry and the inode that was overwritten by the rename (if any). Update fsnotify_data_inode(), fsnotify_data_dentry(), and fsnotify_data_sb() to handle the new type, and add a new fsnotify_data_rename_target() helper for extracting the overwritten target inode. Update fsnotify_move() to use the new data type for FS_RENAME and FS_MOVED_TO events, passing the overwritten target inode through the event data. FS_MOVED_FROM is unchanged since the source directory doesn't need overwrite information. This is done so that fsnotify consumers like nfsd can atomically observe the overwritten file when a rename replaces an existing entry, without needing a separate FS_DELETE event. Assisted-by: Claude (Anthropic Claude Code) Reviewed-by: Jan Kara Reviewed-by: Amir Goldstein Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260428-dir-deleg-v3-7-5a0780ba9def@kernel.org Acked-by: Chuck Lever Signed-off-by: Christian Brauner --- include/linux/fsnotify.h | 8 ++++++-- include/linux/fsnotify_backend.h | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/include/linux/fsnotify.h b/include/linux/fsnotify.h index 079c18bcdbde..bda798bc67bc 100644 --- a/include/linux/fsnotify.h +++ b/include/linux/fsnotify.h @@ -257,6 +257,10 @@ static inline void fsnotify_move(struct inode *old_dir, struct inode *new_dir, __u32 new_dir_mask = FS_MOVED_TO; __u32 rename_mask = FS_RENAME; const struct qstr *new_name = &moved->d_name; + struct fsnotify_rename_data rd = { + .moved = moved, + .target = target, + }; if (isdir) { old_dir_mask |= FS_ISDIR; @@ -265,12 +269,12 @@ static inline void fsnotify_move(struct inode *old_dir, struct inode *new_dir, } /* Event with information about both old and new parent+name */ - fsnotify_name(rename_mask, moved, FSNOTIFY_EVENT_DENTRY, + fsnotify_name(rename_mask, &rd, FSNOTIFY_EVENT_RENAME, old_dir, old_name, 0); fsnotify_name(old_dir_mask, source, FSNOTIFY_EVENT_INODE, old_dir, old_name, fs_cookie); - fsnotify_name(new_dir_mask, source, FSNOTIFY_EVENT_INODE, + fsnotify_name(new_dir_mask, &rd, FSNOTIFY_EVENT_RENAME, new_dir, new_name, fs_cookie); if (target) diff --git a/include/linux/fsnotify_backend.h b/include/linux/fsnotify_backend.h index 66e185bd1b1b..f8c8fb7f34ae 100644 --- a/include/linux/fsnotify_backend.h +++ b/include/linux/fsnotify_backend.h @@ -311,6 +311,7 @@ enum fsnotify_data_type { FSNOTIFY_EVENT_DENTRY, FSNOTIFY_EVENT_MNT, FSNOTIFY_EVENT_ERROR, + FSNOTIFY_EVENT_RENAME, }; struct fs_error_report { @@ -335,6 +336,11 @@ struct fsnotify_mnt { u64 mnt_id; }; +struct fsnotify_rename_data { + struct dentry *moved; /* the dentry that was renamed */ + struct inode *target; /* inode overwritten by rename, or NULL */ +}; + static inline struct inode *fsnotify_data_inode(const void *data, int data_type) { switch (data_type) { @@ -348,6 +354,8 @@ static inline struct inode *fsnotify_data_inode(const void *data, int data_type) return d_inode(file_range_path(data)->dentry); case FSNOTIFY_EVENT_ERROR: return ((struct fs_error_report *)data)->inode; + case FSNOTIFY_EVENT_RENAME: + return d_inode(((const struct fsnotify_rename_data *)data)->moved); default: return NULL; } @@ -363,6 +371,8 @@ static inline struct dentry *fsnotify_data_dentry(const void *data, int data_typ return ((const struct path *)data)->dentry; case FSNOTIFY_EVENT_FILE_RANGE: return file_range_path(data)->dentry; + case FSNOTIFY_EVENT_RENAME: + return ((struct fsnotify_rename_data *)data)->moved; default: return NULL; } @@ -395,6 +405,8 @@ static inline struct super_block *fsnotify_data_sb(const void *data, return file_range_path(data)->dentry->d_sb; case FSNOTIFY_EVENT_ERROR: return ((struct fs_error_report *) data)->sb; + case FSNOTIFY_EVENT_RENAME: + return ((const struct fsnotify_rename_data *)data)->moved->d_sb; default: return NULL; } @@ -430,6 +442,14 @@ static inline struct fs_error_report *fsnotify_data_error_report( } } +static inline struct inode *fsnotify_data_rename_target(const void *data, + int data_type) +{ + if (data_type == FSNOTIFY_EVENT_RENAME) + return ((const struct fsnotify_rename_data *)data)->target; + return NULL; +} + static inline const struct file_range *fsnotify_data_file_range( const void *data, int data_type) From 246bc86d0fd891273a8502314f158eab23af823c Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Sat, 16 May 2026 07:11:23 -0400 Subject: [PATCH 8/8] filelock: move LEASE_BREAK_* flags out of #ifdef CONFIG_FILE_LOCKING This was causing a build break when CONFIG_FILE_LOCKING was disabled. Move the LEASE_BREAK_* flags into the non-#ifdef'ed part of the file. Reported-by: kernel test robot Closes: https://lore.kernel.org/oe-kbuild-all/202605161232.1lY6pZoM-lkp@intel.com/ Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260516-dir-deleg-fix-v1-1-1b68f0aa990a@kernel.org Signed-off-by: Christian Brauner --- include/linux/filelock.h | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/include/linux/filelock.h b/include/linux/filelock.h index 6e125902c58a..7a7a6e8a9a08 100644 --- a/include/linux/filelock.h +++ b/include/linux/filelock.h @@ -29,6 +29,15 @@ */ #define FILE_LOCK_DEFERRED 1 +#define LEASE_BREAK_LEASE BIT(0) // break leases and delegations +#define LEASE_BREAK_DELEG BIT(1) // break delegations only +#define LEASE_BREAK_LAYOUT BIT(2) // break layouts only +#define LEASE_BREAK_NONBLOCK BIT(3) // non-blocking break +#define LEASE_BREAK_OPEN_RDONLY BIT(4) // readonly open event +#define LEASE_BREAK_DIR_CREATE BIT(5) // dir deleg create event +#define LEASE_BREAK_DIR_DELETE BIT(6) // dir deleg delete event +#define LEASE_BREAK_DIR_RENAME BIT(7) // dir deleg rename event + struct file_lock; struct file_lease; @@ -219,17 +228,6 @@ int locks_lock_inode_wait(struct inode *inode, struct file_lock *fl); void locks_init_lease(struct file_lease *); void locks_free_lease(struct file_lease *fl); struct file_lease *locks_alloc_lease(void); - -#define LEASE_BREAK_LEASE BIT(0) // break leases and delegations -#define LEASE_BREAK_DELEG BIT(1) // break delegations only -#define LEASE_BREAK_LAYOUT BIT(2) // break layouts only -#define LEASE_BREAK_NONBLOCK BIT(3) // non-blocking break -#define LEASE_BREAK_OPEN_RDONLY BIT(4) // readonly open event -#define LEASE_BREAK_DIR_CREATE BIT(5) // dir deleg create event -#define LEASE_BREAK_DIR_DELETE BIT(6) // dir deleg delete event -#define LEASE_BREAK_DIR_RENAME BIT(7) // dir deleg rename event - - int __break_lease(struct inode *inode, unsigned int flags); void lease_get_mtime(struct inode *, struct timespec64 *time); int generic_setlease(struct file *, int, struct file_lease **, void **priv);