lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20250206054504.2950516-12-neilb@suse.de>
Date: Thu,  6 Feb 2025 16:42:48 +1100
From: NeilBrown <neilb@...e.de>
To: Alexander Viro <viro@...iv.linux.org.uk>,
	Christian Brauner <brauner@...nel.org>,
	Jan Kara <jack@...e.cz>,
	Linus Torvalds <torvalds@...ux-foundation.org>,
	Jeff Layton <jlayton@...nel.org>,
	Dave Chinner <david@...morbit.com>
Cc: linux-fsdevel@...r.kernel.org,
	linux-kernel@...r.kernel.org
Subject: [PATCH 11/19] VFS: Add ability to exclusively lock a dentry and use for create/remove  operations.

d_update_lock(), d_update_trylock(), d_update_unlock() are added which
can be used to get an exclusive lock on a dentry in preparation for
updating it.

As contention on a name is rare this is optimised for the uncontended
case.  A bit is set under the d_lock spinlock to claim as lock, and
wait_var_event_spinlock() is used when waiting is needed.  To avoid
sending a wakeup when not needed we have a second bit flag to indicate
if there are any waiters.

This locking is used in lookup_and_lock().

Once the exclusive "update" lock is obtained on the dentry we must make
sure it wasn't unlinked or renamed while we slept.  If it was we repeat
the lookup.

We also ensure that the parent isn't similarly locked.  This is will be
used to protect a directory during rmdir.

Signed-off-by: NeilBrown <neilb@...e.de>
---
 fs/dcache.c            |   5 +-
 fs/internal.h          |  18 +++++++
 fs/namei.c             | 110 ++++++++++++++++++++++++++++++++++++++++-
 include/linux/dcache.h |   4 ++
 4 files changed, 134 insertions(+), 3 deletions(-)

diff --git a/fs/dcache.c b/fs/dcache.c
index 37c0f655166d..e705696ca57e 100644
--- a/fs/dcache.c
+++ b/fs/dcache.c
@@ -1675,9 +1675,10 @@ EXPORT_SYMBOL(d_invalidate);
  * available. On a success the dentry is returned. The name passed in is
  * copied and the copy passed in may be reused after this call.
  */
- 
+
 static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
 {
+	static struct lock_class_key __key;
 	struct dentry *dentry;
 	char *dname;
 	int err;
@@ -1735,6 +1736,8 @@ static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
 	INIT_HLIST_NODE(&dentry->d_sib);
 	d_set_d_op(dentry, dentry->d_sb->s_d_op);
 
+	lockdep_init_map(&dentry->d_update_map, "DCACHE_PAR_UPDATE", &__key, 0);
+
 	if (dentry->d_op && dentry->d_op->d_init) {
 		err = dentry->d_op->d_init(dentry);
 		if (err) {
diff --git a/fs/internal.h b/fs/internal.h
index e7f02ae1e098..5cb9a34e26e8 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -225,6 +225,24 @@ extern struct dentry *__d_lookup_rcu(const struct dentry *parent,
 				const struct qstr *name, unsigned *seq);
 extern void d_genocide(struct dentry *);
 
+extern bool d_update_lock(struct dentry *dentry,
+			  struct dentry *base, const struct qstr *last,
+			  unsigned int subclass);
+
+extern bool d_update_trylock(struct dentry *dentry,
+			     struct dentry *base,
+			     const struct qstr *last);
+
+static inline void d_update_unlock(struct dentry *dentry)
+{
+	lock_map_release(&dentry->d_update_map);
+	spin_lock(&dentry->d_lock);
+	if (dentry->d_flags & DCACHE_PAR_WAITER)
+		wake_up_var_locked(&dentry->d_flags, &dentry->d_lock);
+	dentry->d_flags &= ~(DCACHE_PAR_UPDATE | DCACHE_PAR_WAITER);
+	spin_unlock(&dentry->d_lock);
+}
+
 /*
  * pipe.c
  */
diff --git a/fs/namei.c b/fs/namei.c
index eadde9de73bf..145ae07f9b8c 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -1750,6 +1750,110 @@ struct dentry *lookup_one_qstr(const struct qstr *name,
 }
 EXPORT_SYMBOL(lookup_one_qstr);
 
+/*
+ * dentry locking for updates.
+ * When modifying a directory the target dentry will be locked by
+ * setting DCACHE_PAR_UPDATE under ->d_lock.  If it is already set,
+ * DCACHE_PAR_WAITER is set to ensure a wakeup is sent, and we wait
+ * using wait_var_event_spinlock().
+ * The DCACHE_PAR_UPDATE bit will only be set in a denty if it is
+ * NOT set in the parent.  This avoids commensing a new operation in
+ * a directory that is being asynchronously deleted using ->mkdir_async.
+ * Instead of holding ->d_lock on the parent while testing the flag, we
+ * use memory ordering to ensure correctness.  Locking a child
+ * retests the parent *after* setting the bit, and deleting a directory
+ * requires testing all children *after* setting the bit in the parent.
+ */
+
+static bool check_dentry_locked(struct dentry *de)
+{
+	if (de->d_flags & DCACHE_PAR_UPDATE) {
+		de->d_flags |= DCACHE_PAR_WAITER;
+		return true;
+	}
+	return false;
+}
+
+bool d_update_lock(struct dentry *dentry,
+		   struct dentry *base, const struct qstr *last,
+		   unsigned int subclass)
+{
+	lock_acquire_exclusive(&dentry->d_update_map, subclass, 0, NULL, _THIS_IP_);
+again:
+	spin_lock(&dentry->d_lock);
+	wait_var_event_spinlock(&dentry->d_flags,
+				!check_dentry_locked(dentry),
+				&dentry->d_lock);
+	if (d_is_positive(dentry)) {
+		rcu_read_lock(); /* needed for d_same_name() */
+		if (
+			/* Was unlinked while we waited ?*/
+			d_unhashed(dentry) ||
+			/* Or was dentry renamed ?? */
+			dentry->d_parent != base ||
+			dentry->d_name.hash != last->hash ||
+			!d_same_name(dentry, base, last)
+		) {
+			rcu_read_unlock();
+			spin_unlock(&dentry->d_lock);
+			lock_map_release(&dentry->d_update_map);
+			return false;
+		}
+		rcu_read_unlock();
+	}
+	/* Must ensure DCACHE_PAR_UPDATE in child is visible before reading
+	 * from parent
+	 */
+	smp_store_mb(dentry->d_flags, dentry->d_flags | DCACHE_PAR_UPDATE);
+	if (base->d_flags & DCACHE_PAR_UPDATE) {
+		/* We cannot grant DCACHE_PAR_UPDATE on a dentry while
+		 * it is held on the parent
+		 */
+		dentry->d_flags &= ~DCACHE_PAR_UPDATE;
+		spin_unlock(&dentry->d_lock);
+		spin_lock(&base->d_lock);
+		wait_var_event_spinlock(&base->d_flags,
+					!check_dentry_locked(base),
+					&base->d_lock);
+		spin_unlock(&base->d_lock);
+		goto again;
+	}
+	spin_unlock(&dentry->d_lock);
+	return true;
+}
+
+bool d_update_trylock(struct dentry *dentry,
+		      struct dentry *base,
+		      const struct qstr *last)
+{
+	int ret = false;
+
+	spin_lock(&dentry->d_lock);
+	rcu_read_lock(); /* needed for d_same_name() */
+	if (!(smp_load_acquire(&dentry->d_flags) & DCACHE_PAR_UPDATE) &&
+	    !(dentry->d_parent->d_flags & DCACHE_PAR_UPDATE)) {
+		if (!base || !(
+			/* Was unlinked before we got spinlock ?*/
+			d_unhashed(dentry) ||
+			/* Or was dentry renamed ?? */
+			dentry->d_parent != base ||
+			dentry->d_name.hash != last->hash ||
+			!d_same_name(dentry, base, last)
+		)) {
+			lock_map_acquire_try(&dentry->d_update_map);
+			smp_store_mb(dentry->d_flags,
+				     dentry->d_flags | DCACHE_PAR_UPDATE);
+			if (dentry->d_parent->d_flags & DCACHE_PAR_UPDATE)
+				dentry->d_flags &= ~DCACHE_PAR_UPDATE;
+			else
+				ret = true;
+		}
+	}
+	rcu_read_unlock();
+	spin_unlock(&dentry->d_lock);
+	return ret;
+}
+
 static struct dentry *lookup_and_lock_nested(const struct qstr *last,
 					     struct dentry *base,
 					     unsigned int lookup_flags,
@@ -1759,8 +1863,9 @@ static struct dentry *lookup_and_lock_nested(const struct qstr *last,
 
 	if (!(lookup_flags & LOOKUP_PARENT_LOCKED))
 		inode_lock_nested(base->d_inode, subclass);
-
-	dentry = lookup_one_qstr(last, base, lookup_flags);
+	do {
+		dentry = lookup_one_qstr(last, base, lookup_flags);
+	} while (!IS_ERR(dentry) && !d_update_lock(dentry, base, last, subclass));
 	if (IS_ERR(dentry) && !(lookup_flags & LOOKUP_PARENT_LOCKED)) {
 			inode_unlock(base->d_inode);
 	}
@@ -1779,6 +1884,7 @@ void done_lookup_and_lock(struct dentry *base, struct dentry *dentry,
 			  unsigned int lookup_flags)
 {
 	d_lookup_done(dentry);
+	d_update_unlock(dentry);
 	dput(dentry);
 	if (!(lookup_flags & LOOKUP_PARENT_LOCKED))
 		inode_unlock(base->d_inode);
diff --git a/include/linux/dcache.h b/include/linux/dcache.h
index d5816cf19538..f891fb1be63b 100644
--- a/include/linux/dcache.h
+++ b/include/linux/dcache.h
@@ -111,6 +111,8 @@ struct dentry {
 					 * possible!
 					 */
 
+	/* lockdep tracking of DCACHE_PAR_UPDATE locks */
+	struct lockdep_map		d_update_map;
 	union {
 		struct list_head d_lru;		/* LRU list */
 		wait_queue_head_t *d_wait;	/* in-lookup ones only */
@@ -232,6 +234,8 @@ struct dentry_operations {
 #define DCACHE_DENTRY_CURSOR		BIT(25)
 #define DCACHE_NORCU			BIT(26) /* No RCU delay for freeing */
 
+#define DCACHE_PAR_UPDATE		BIT(27) /* Locked for update */
+#define DCACHE_PAR_WAITER		BIT(28) /* someone is waiting for PAR_UPDATE */
 extern seqlock_t rename_lock;
 
 /*
-- 
2.47.1


Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ