Skip to content

Commit ea07b86

Browse files
hnaztorvalds
authored andcommitted
mm: workingset: fix use-after-free in shadow node shrinker
Several people report seeing warnings about inconsistent radix tree nodes followed by crashes in the workingset code, which all looked like use-after-free access from the shadow node shrinker. Dave Jones managed to reproduce the issue with a debug patch applied, which confirmed that the radix tree shrinking indeed frees shadow nodes while they are still linked to the shadow LRU: WARNING: CPU: 2 PID: 53 at lib/radix-tree.c:643 delete_node+0x1e4/0x200 CPU: 2 PID: 53 Comm: kswapd0 Not tainted 4.10.0-rc2-think+ #3 Call Trace: delete_node+0x1e4/0x200 __radix_tree_delete_node+0xd/0x10 shadow_lru_isolate+0xe6/0x220 __list_lru_walk_one.isra.4+0x9b/0x190 list_lru_walk_one+0x23/0x30 scan_shadow_nodes+0x2e/0x40 shrink_slab.part.44+0x23d/0x5d0 shrink_node+0x22c/0x330 kswapd+0x392/0x8f0 This is the WARN_ON_ONCE(!list_empty(&node->private_list)) placed in the inlined radix_tree_shrink(). The problem is with 14b4687 ("mm: workingset: move shadow entry tracking to radix tree exceptional tracking"), which passes an update callback into the radix tree to link and unlink shadow leaf nodes when tree entries change, but forgot to pass the callback when reclaiming a shadow node. While the reclaimed shadow node itself is unlinked by the shrinker, its deletion from the tree can cause the left-most leaf node in the tree to be shrunk. If that happens to be a shadow node as well, we don't unlink it from the LRU as we should. Consider this tree, where the s are shadow entries: root->rnode | [0 n] | | [s ] [sssss] Now the shadow node shrinker reclaims the rightmost leaf node through the shadow node LRU: root->rnode | [0 ] | [s ] Because the parent of the deleted node is the first level below the root and has only one child in the left-most slot, the intermediate level is shrunk and the node containing the single shadow is put in its place: root->rnode | [s ] The shrinker again sees a single left-most slot in a first level node and thus decides to store the shadow in root->rnode directly and free the node - which is a leaf node on the shadow node LRU. root->rnode | s Without the update callback, the freed node remains on the shadow LRU, where it causes later shrinker runs to crash. Pass the node updater callback into __radix_tree_delete_node() in case the deletion causes the left-most branch in the tree to collapse too. Also add warnings when linked nodes are freed right away, rather than wait for the use-after-free when the list is scanned much later. Fixes: 14b4687 ("mm: workingset: move shadow entry tracking to radix tree exceptional tracking") Reported-by: Dave Chinner <[email protected]> Reported-by: Hugh Dickins <[email protected]> Reported-by: Andrea Arcangeli <[email protected]> Reported-and-tested-by: Dave Jones <[email protected]> Signed-off-by: Johannes Weiner <[email protected]> Cc: Christoph Hellwig <[email protected]> Cc: Chris Leech <[email protected]> Cc: Lee Duncan <[email protected]> Cc: Jan Kara <[email protected]> Cc: Kirill A. Shutemov <[email protected]> Cc: Matthew Wilcox <[email protected]> Cc: Andrew Morton <[email protected]> Signed-off-by: Linus Torvalds <[email protected]>
1 parent b0b9b3d commit ea07b86

File tree

3 files changed

+14
-4
lines changed

3 files changed

+14
-4
lines changed

include/linux/radix-tree.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,9 @@ void radix_tree_iter_replace(struct radix_tree_root *,
306306
void radix_tree_replace_slot(struct radix_tree_root *root,
307307
void **slot, void *item);
308308
void __radix_tree_delete_node(struct radix_tree_root *root,
309-
struct radix_tree_node *node);
309+
struct radix_tree_node *node,
310+
radix_tree_update_node_t update_node,
311+
void *private);
310312
void *radix_tree_delete_item(struct radix_tree_root *, unsigned long, void *);
311313
void *radix_tree_delete(struct radix_tree_root *, unsigned long);
312314
void radix_tree_clear_tags(struct radix_tree_root *root,

lib/radix-tree.c

+9-2
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,7 @@ static inline void radix_tree_shrink(struct radix_tree_root *root,
640640
update_node(node, private);
641641
}
642642

643+
WARN_ON_ONCE(!list_empty(&node->private_list));
643644
radix_tree_node_free(node);
644645
}
645646
}
@@ -666,6 +667,7 @@ static void delete_node(struct radix_tree_root *root,
666667
root->rnode = NULL;
667668
}
668669

670+
WARN_ON_ONCE(!list_empty(&node->private_list));
669671
radix_tree_node_free(node);
670672

671673
node = parent;
@@ -767,6 +769,7 @@ static void radix_tree_free_nodes(struct radix_tree_node *node)
767769
struct radix_tree_node *old = child;
768770
offset = child->offset + 1;
769771
child = child->parent;
772+
WARN_ON_ONCE(!list_empty(&node->private_list));
770773
radix_tree_node_free(old);
771774
if (old == entry_to_node(node))
772775
return;
@@ -1824,15 +1827,19 @@ EXPORT_SYMBOL(radix_tree_gang_lookup_tag_slot);
18241827
* __radix_tree_delete_node - try to free node after clearing a slot
18251828
* @root: radix tree root
18261829
* @node: node containing @index
1830+
* @update_node: callback for changing leaf nodes
1831+
* @private: private data to pass to @update_node
18271832
*
18281833
* After clearing the slot at @index in @node from radix tree
18291834
* rooted at @root, call this function to attempt freeing the
18301835
* node and shrinking the tree.
18311836
*/
18321837
void __radix_tree_delete_node(struct radix_tree_root *root,
1833-
struct radix_tree_node *node)
1838+
struct radix_tree_node *node,
1839+
radix_tree_update_node_t update_node,
1840+
void *private)
18341841
{
1835-
delete_node(root, node, NULL, NULL);
1842+
delete_node(root, node, update_node, private);
18361843
}
18371844

18381845
/**

mm/workingset.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,8 @@ static enum lru_status shadow_lru_isolate(struct list_head *item,
473473
if (WARN_ON_ONCE(node->exceptional))
474474
goto out_invalid;
475475
inc_node_state(page_pgdat(virt_to_page(node)), WORKINGSET_NODERECLAIM);
476-
__radix_tree_delete_node(&mapping->page_tree, node);
476+
__radix_tree_delete_node(&mapping->page_tree, node,
477+
workingset_update_node, mapping);
477478

478479
out_invalid:
479480
spin_unlock(&mapping->tree_lock);

0 commit comments

Comments
 (0)