[PATCH] gfs2: avoid sysfs freeze and unmount lock inversion

"Jiacheng Xu" <[email protected]>
Newsgroups dev.linux.lists.gfs2,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
The gfs2 freeze sysfs callback runs with kernfs active protection held.
freeze_super() and thaw_super() may acquire sb->s_umount.

At the same time, the unmount path holds s_umount and calls
gfs2_sys_fs_del(), which drains the same kernfs node. This creates an
ABBA dependency:

freeze_store()       kernfs active -> s_umount
gfs2_put_super()     s_umount -> kernfs active

Break the active protection before accessing s_umount. Since this allows
unmount to proceed concurrently, acquire an active superblock reference
while holding s_umount for reading. Release that reference from
gfs2_freeze_wq so a final superblock reference cannot enter
gfs2_put_super() from the sysfs callback itself.

Return -EAGAIN if the superblock is being mounted, unmounted, or otherwise
has s_umount held. Reject invalid freeze values before taking any
references or changing sysfs active protection.

Fixes: b3b94faa5fe5 ("[GFS2] The core of GFS2")
Signed-off-by: Jiacheng Xu <[email protected]>

---
fs/gfs2/sys.c | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 76 insertions(+), 8 deletions(-)

diff --git a/fs/gfs2/sys.c b/fs/gfs2/sys.c
index ea2c7b9e4a77..78bc310be1fb 100644
--- a/fs/gfs2/sys.c
+++ b/fs/gfs2/sys.c
@@ -11,11 +11,14 @@
#include <linux/spinlock.h>
#include <linux/completion.h>
#include <linux/buffer_head.h>
+#include <linux/slab.h>
#include <linux/module.h>
#include <linux/kobject.h>
+#include <linux/sysfs.h>
#include <linux/uaccess.h>
#include <linux/gfs2_ondisk.h>
#include <linux/blkdev.h>
+#include <linux/workqueue.h>

#include "gfs2.h"
#include "incore.h"
@@ -33,6 +36,24 @@ struct gfs2_attr {
      ssize_t (*store)(struct gfs2_sbd *, const char *, size_t);
};

+/* Forward declaration for freeze_store(), defined with the other attributes. */
+static struct gfs2_attr gfs2_attr_freeze;
+
+struct gfs2_sysfs_sb_ref {
+     struct work_struct work;
+     struct super_block *sb;
+};
+
+static void gfs2_sysfs_sb_ref_put(struct work_struct *work)
+{
+     struct gfs2_sysfs_sb_ref *ref =
+             container_of(work, struct gfs2_sysfs_sb_ref, work);
+     struct super_block *sb = ref->sb;
+
+     deactivate_super(sb);
+     kfree(ref);
+}
+
static ssize_t gfs2_attr_show(struct kobject *kobj, struct attribute *attr,
                        char *buf)
{
@@ -153,6 +174,9 @@ static ssize_t freeze_show(struct gfs2_sbd *sdp, char *buf)

static ssize_t freeze_store(struct gfs2_sbd *sdp, const char *buf, size_t len)
{
+     struct super_block *sb = sdp->sd_vfs;
+     struct gfs2_sysfs_sb_ref *ref = NULL;
+     struct kernfs_node *kn;
      int error, n;

      error = kstrtoint(buf, 0, &n);
@@ -162,23 +186,67 @@ static ssize_t freeze_store(struct gfs2_sbd *sdp, const char *buf, size_t
len)
      if (!capable(CAP_SYS_ADMIN))
            return -EPERM;

+     if (n != 0 && n != 1)
+             return -EINVAL;
+
+     /*
+      * The sysfs callback normally holds kn->active.  Break that
+      * dependency before taking s_umount, otherwise unmount can wait for
+      * this callback while the callback waits for s_umount.
+      */
+     kn = sysfs_break_active_protection(&sdp->sd_kobj,
+                                        &gfs2_attr_freeze.attr);
+     if (!kn)
+             return -ENODEV;
+
+     ref = kmalloc_obj(*ref);
+     if (!ref) {
+             error = -ENOMEM;
+             goto out_unbreak;
+     }
+     INIT_WORK(&ref->work, gfs2_sysfs_sb_ref_put);
+     ref->sb = sb;
+
+     /*
+      * Pin the superblock before running an operation which may wait for
+      * s_umount.  Release the pin from a separate work item so a last
+      * reference cannot enter gfs2_put_super() on the sysfs call path.
+      */
+     if (!down_read_trylock(&sb->s_umount)) {
+             error = -EAGAIN;
+             goto out_unbreak;
+     }
+     if (!(sb->s_flags & SB_ACTIVE)) {
+             up_read(&sb->s_umount);
+             error = -EAGAIN;
+             goto out_unbreak;
+     }
+     atomic_inc(&sb->s_active);
+     up_read(&sb->s_umount);
+
      switch (n) {
      case 0:
-             error = thaw_super(sdp->sd_vfs, FREEZE_HOLDER_USERSPACE, NULL);
+             error = thaw_super(sb, FREEZE_HOLDER_USERSPACE, NULL);
            break;
      case 1:
-             error = freeze_super(sdp->sd_vfs, FREEZE_HOLDER_USERSPACE, NULL);
+             error = freeze_super(sb, FREEZE_HOLDER_USERSPACE, NULL);
            break;
-     default:
-             return -EINVAL;
      }

-     if (error) {
+     if (error)
            fs_warn(sdp, "freeze %d error %d\n", n, error);
-             return error;
-     }

-     return len;
+     /* No sdp access is allowed after active protection is restored. */
+     sysfs_unbreak_active_protection(kn);
+     /* gfs2 module teardown drains this queue before unloading the module. */
+     queue_work(gfs2_freeze_wq, &ref->work);
+
+     return error ? error : len;
+
+out_unbreak:
+     sysfs_unbreak_active_protection(kn);
+     kfree(ref);
+     return error;
}

static ssize_t withdraw_show(struct gfs2_sbd *sdp, char *buf)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.