--- /dev/null
+From 62c9827cbb996c2c04f615ecd783ce28bcea894b Mon Sep 17 00:00:00 2001
+From: Gang Li <ligang.bdlg@bytedance.com>
+Date: Fri, 14 Jan 2022 14:05:23 -0800
+Subject: shmem: fix a race between shmem_unused_huge_shrink and shmem_evict_inode
+
+From: Gang Li <ligang.bdlg@bytedance.com>
+
+commit 62c9827cbb996c2c04f615ecd783ce28bcea894b upstream.
+
+Fix a data race in commit 779750d20b93 ("shmem: split huge pages beyond
+i_size under memory pressure").
+
+Here are call traces causing race:
+
+ Call Trace 1:
+ shmem_unused_huge_shrink+0x3ae/0x410
+ ? __list_lru_walk_one.isra.5+0x33/0x160
+ super_cache_scan+0x17c/0x190
+ shrink_slab.part.55+0x1ef/0x3f0
+ shrink_node+0x10e/0x330
+ kswapd+0x380/0x740
+ kthread+0xfc/0x130
+ ? mem_cgroup_shrink_node+0x170/0x170
+ ? kthread_create_on_node+0x70/0x70
+ ret_from_fork+0x1f/0x30
+
+ Call Trace 2:
+ shmem_evict_inode+0xd8/0x190
+ evict+0xbe/0x1c0
+ do_unlinkat+0x137/0x330
+ do_syscall_64+0x76/0x120
+ entry_SYSCALL_64_after_hwframe+0x3d/0xa2
+
+A simple explanation:
+
+Image there are 3 items in the local list (@list). In the first
+traversal, A is not deleted from @list.
+
+ 1) A->B->C
+ ^
+ |
+ pos (leave)
+
+In the second traversal, B is deleted from @list. Concurrently, A is
+deleted from @list through shmem_evict_inode() since last reference
+counter of inode is dropped by other thread. Then the @list is corrupted.
+
+ 2) A->B->C
+ ^ ^
+ | |
+ evict pos (drop)
+
+We should make sure the inode is either on the global list or deleted from
+any local list before iput().
+
+Fixed by moving inodes back to global list before we put them.
+
+[akpm@linux-foundation.org: coding style fixes]
+
+Link: https://lkml.kernel.org/r/20211125064502.99983-1-ligang.bdlg@bytedance.com
+Fixes: 779750d20b93 ("shmem: split huge pages beyond i_size under memory pressure")
+Signed-off-by: Gang Li <ligang.bdlg@bytedance.com>
+Reviewed-by: Muchun Song <songmuchun@bytedance.com>
+Acked-by: Kirill A. Shutemov <kirill.shutemov@linux.intel.com>
+Cc: Hugh Dickins <hughd@google.com>
+Cc: <stable@vger.kernel.org>
+Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
+Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ mm/shmem.c | 37 +++++++++++++++++++++----------------
+ 1 file changed, 21 insertions(+), 16 deletions(-)
+
+--- a/mm/shmem.c
++++ b/mm/shmem.c
+@@ -436,7 +436,7 @@ static unsigned long shmem_unused_huge_s
+ struct shmem_inode_info *info;
+ struct page *page;
+ unsigned long batch = sc ? sc->nr_to_scan : 128;
+- int removed = 0, split = 0;
++ int split = 0;
+
+ if (list_empty(&sbinfo->shrinklist))
+ return SHRINK_STOP;
+@@ -451,7 +451,6 @@ static unsigned long shmem_unused_huge_s
+ /* inode is about to be evicted */
+ if (!inode) {
+ list_del_init(&info->shrinklist);
+- removed++;
+ goto next;
+ }
+
+@@ -459,12 +458,12 @@ static unsigned long shmem_unused_huge_s
+ if (round_up(inode->i_size, PAGE_SIZE) ==
+ round_up(inode->i_size, HPAGE_PMD_SIZE)) {
+ list_move(&info->shrinklist, &to_remove);
+- removed++;
+ goto next;
+ }
+
+ list_move(&info->shrinklist, &list);
+ next:
++ sbinfo->shrinklist_len--;
+ if (!--batch)
+ break;
+ }
+@@ -484,7 +483,7 @@ next:
+ inode = &info->vfs_inode;
+
+ if (nr_to_split && split >= nr_to_split)
+- goto leave;
++ goto move_back;
+
+ page = find_get_page(inode->i_mapping,
+ (inode->i_size & HPAGE_PMD_MASK) >> PAGE_SHIFT);
+@@ -498,38 +497,44 @@ next:
+ }
+
+ /*
+- * Leave the inode on the list if we failed to lock
+- * the page at this time.
++ * Move the inode on the list back to shrinklist if we failed
++ * to lock the page at this time.
+ *
+ * Waiting for the lock may lead to deadlock in the
+ * reclaim path.
+ */
+ if (!trylock_page(page)) {
+ put_page(page);
+- goto leave;
++ goto move_back;
+ }
+
+ ret = split_huge_page(page);
+ unlock_page(page);
+ put_page(page);
+
+- /* If split failed leave the inode on the list */
++ /* If split failed move the inode on the list back to shrinklist */
+ if (ret)
+- goto leave;
++ goto move_back;
+
+ split++;
+ drop:
+ list_del_init(&info->shrinklist);
+- removed++;
+-leave:
++ goto put;
++move_back:
++ /*
++ * Make sure the inode is either on the global list or deleted
++ * from any local list before iput() since it could be deleted
++ * in another thread once we put the inode (then the local list
++ * is corrupted).
++ */
++ spin_lock(&sbinfo->shrinklist_lock);
++ list_move(&info->shrinklist, &sbinfo->shrinklist);
++ sbinfo->shrinklist_len++;
++ spin_unlock(&sbinfo->shrinklist_lock);
++put:
+ iput(inode);
+ }
+
+- spin_lock(&sbinfo->shrinklist_lock);
+- list_splice_tail(&list, &sbinfo->shrinklist);
+- sbinfo->shrinklist_len -= removed;
+- spin_unlock(&sbinfo->shrinklist_lock);
+-
+ return split;
+ }
+