]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
filelock: add support for ignoring deleg breaks for dir change events
authorJeff Layton <jlayton@kernel.org>
Tue, 28 Apr 2026 07:09:46 +0000 (08:09 +0100)
committerChristian Brauner <brauner@kernel.org>
Fri, 15 May 2026 17:24:32 +0000 (19:24 +0200)
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 <jack@suse.cz>
Signed-off-by: Jeff Layton <jlayton@kernel.org>
Link: https://patch.msgid.link/20260428-dir-deleg-v3-2-5a0780ba9def@kernel.org
Acked-by: Chuck Lever <chuck.lever@oracle.com>
Signed-off-by: Christian Brauner <brauner@kernel.org>
fs/attr.c
fs/locks.c
fs/namei.c
fs/posix_acl.c
fs/xattr.c
include/linux/filelock.h
include/trace/events/filelock.h

index ded221defae6a4ba35b8565d8d527d6c4e861859..4f437fabb7f0f59682895d64dc8301b9927ce39d 100644 (file)
--- 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;
        }
index 3306892900c4cce4e953b9f80c95743c76350daf..31086d6153e2712c3e8c66c56211967e3c3d680a 100644 (file)
@@ -1583,29 +1583,63 @@ trace:
 }
 
 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;
+
+       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(flc, &ctx->flc_lease, flc_list) {
-               if (leases_conflict(flc, &breaker->c))
-                       return true;
+       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 false;
+       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 @@ restart:
                 */
                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;
        }
index c7fac83c9a85ef250bb424af0a91d581ee9f4919..3a3a2e5e77a0fa485c3c7ca028b299ddc653c5d4 100644 (file)
@@ -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;
        }
index 12591c95c9256185b6fdbf7f32929dfdf70fc54b..b4bfe4ddf64ea5caede69f5737715e63a965eeee 100644 (file)
@@ -1126,7 +1126,7 @@ retry_deleg:
        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 @@ retry_deleg:
        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;
 
index 09ecbaaa16608a80b770058002c3d8a329e47e98..efdcf2a4858577eb0dce3e7841e865e78a85eb53 100644 (file)
@@ -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;
 
index 5f0a2fb31450607ae23d7f06b6372f4f63c1bf62..9dd4e67a6f308710fbc0fe8354218d8285468ef2 100644 (file)
@@ -4,19 +4,22 @@
 
 #include <linux/fs.h>
 
-#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;
index 1167748862449ef6ff04c40b568ea8c3bbb08207..718b5c3f173705361a238a9e443fde3ee3646a80 100644 (file)
                { 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,                           \