ALSA: core: Add async signal helpers
authorTakashi Iwai <tiwai@suse.de>
Thu, 28 Jul 2022 12:59:42 +0000 (14:59 +0200)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Thu, 25 Aug 2022 09:40:44 +0000 (11:40 +0200)
[ Upstream commit ef34a0ae7a2654bc9e58675e36898217fb2799d8 ]

Currently the call of kill_fasync() from an interrupt handler might
lead to potential spin deadlocks, as spotted by syzkaller.
Unfortunately, it's not so trivial to fix this lock chain as it's
involved with the tasklist_lock that is touched in allover places.

As a temporary workaround, this patch provides the way to defer the
async signal notification in a work.  The new helper functions,
snd_fasync_helper() and snd_kill_faync() are replacements for
fasync_helper() and kill_fasync(), respectively.  In addition,
snd_fasync_free() needs to be called at the destructor of the relevant
file object.

Link: https://lore.kernel.org/r/20220728125945.29533-2-tiwai@suse.de
Signed-off-by: Takashi Iwai <tiwai@suse.de>
Signed-off-by: Sasha Levin <sashal@kernel.org>
include/sound/core.h
sound/core/misc.c

index 6d4cc49..39cee40 100644 (file)
@@ -501,4 +501,12 @@ snd_pci_quirk_lookup_id(u16 vendor, u16 device,
 }
 #endif
 
+/* async signal helpers */
+struct snd_fasync;
+
+int snd_fasync_helper(int fd, struct file *file, int on,
+                     struct snd_fasync **fasyncp);
+void snd_kill_fasync(struct snd_fasync *fasync, int signal, int poll);
+void snd_fasync_free(struct snd_fasync *fasync);
+
 #endif /* __SOUND_CORE_H */
index 50e4aaa..d32a199 100644 (file)
@@ -10,6 +10,7 @@
 #include <linux/time.h>
 #include <linux/slab.h>
 #include <linux/ioport.h>
+#include <linux/fs.h>
 #include <sound/core.h>
 
 #ifdef CONFIG_SND_DEBUG
@@ -145,3 +146,96 @@ snd_pci_quirk_lookup(struct pci_dev *pci, const struct snd_pci_quirk *list)
 }
 EXPORT_SYMBOL(snd_pci_quirk_lookup);
 #endif
+
+/*
+ * Deferred async signal helpers
+ *
+ * Below are a few helper functions to wrap the async signal handling
+ * in the deferred work.  The main purpose is to avoid the messy deadlock
+ * around tasklist_lock and co at the kill_fasync() invocation.
+ * fasync_helper() and kill_fasync() are replaced with snd_fasync_helper()
+ * and snd_kill_fasync(), respectively.  In addition, snd_fasync_free() has
+ * to be called at releasing the relevant file object.
+ */
+struct snd_fasync {
+       struct fasync_struct *fasync;
+       int signal;
+       int poll;
+       int on;
+       struct list_head list;
+};
+
+static DEFINE_SPINLOCK(snd_fasync_lock);
+static LIST_HEAD(snd_fasync_list);
+
+static void snd_fasync_work_fn(struct work_struct *work)
+{
+       struct snd_fasync *fasync;
+
+       spin_lock_irq(&snd_fasync_lock);
+       while (!list_empty(&snd_fasync_list)) {
+               fasync = list_first_entry(&snd_fasync_list, struct snd_fasync, list);
+               list_del_init(&fasync->list);
+               spin_unlock_irq(&snd_fasync_lock);
+               if (fasync->on)
+                       kill_fasync(&fasync->fasync, fasync->signal, fasync->poll);
+               spin_lock_irq(&snd_fasync_lock);
+       }
+       spin_unlock_irq(&snd_fasync_lock);
+}
+
+static DECLARE_WORK(snd_fasync_work, snd_fasync_work_fn);
+
+int snd_fasync_helper(int fd, struct file *file, int on,
+                     struct snd_fasync **fasyncp)
+{
+       struct snd_fasync *fasync = NULL;
+
+       if (on) {
+               fasync = kzalloc(sizeof(*fasync), GFP_KERNEL);
+               if (!fasync)
+                       return -ENOMEM;
+               INIT_LIST_HEAD(&fasync->list);
+       }
+
+       spin_lock_irq(&snd_fasync_lock);
+       if (*fasyncp) {
+               kfree(fasync);
+               fasync = *fasyncp;
+       } else {
+               if (!fasync) {
+                       spin_unlock_irq(&snd_fasync_lock);
+                       return 0;
+               }
+               *fasyncp = fasync;
+       }
+       fasync->on = on;
+       spin_unlock_irq(&snd_fasync_lock);
+       return fasync_helper(fd, file, on, &fasync->fasync);
+}
+EXPORT_SYMBOL_GPL(snd_fasync_helper);
+
+void snd_kill_fasync(struct snd_fasync *fasync, int signal, int poll)
+{
+       unsigned long flags;
+
+       if (!fasync || !fasync->on)
+               return;
+       spin_lock_irqsave(&snd_fasync_lock, flags);
+       fasync->signal = signal;
+       fasync->poll = poll;
+       list_move(&fasync->list, &snd_fasync_list);
+       schedule_work(&snd_fasync_work);
+       spin_unlock_irqrestore(&snd_fasync_lock, flags);
+}
+EXPORT_SYMBOL_GPL(snd_kill_fasync);
+
+void snd_fasync_free(struct snd_fasync *fasync)
+{
+       if (!fasync)
+               return;
+       fasync->on = 0;
+       flush_work(&snd_fasync_work);
+       kfree(fasync);
+}
+EXPORT_SYMBOL_GPL(snd_fasync_free);