mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Bo Zhang <zhangbo0325@gmail.com>
To: aliceryhl@google.com, gregkh@linuxfoundation.org, cmllamas@google.com
Cc: arve@android.com, tkjos@android.com, christian@brauner.io,
	surenb@google.com, baohua@kernel.org, zhanghongru06@gmail.com,
	linux-kernel@vger.kernel.org, Bo Zhang <zhangbo56@xiaomi.com>
Subject: [RFC PATCH v3 2/2] binder: add install_mutex to serialize page install and shrinker zap
Date: Fri,  4 Sep 2026 19:04:48 +0800	[thread overview]
Message-ID: <20260904110448.23086-3-zhangbo0325@gmail.com> (raw)
In-Reply-To: <20260904110448.23086-1-zhangbo0325@gmail.com>

From: Bo Zhang <zhangbo56@xiaomi.com>

The previous patch converted alloc->mutex to a spinlock for the hot
path (buffer alloc/free). However, this leaves page installation and
shrinker's zap_vma_range() unserialized, which can cause use-after-free
as identified by Alice Ryhl.

Add a separate install_mutex to serialize page installation against
the shrinker's page reclaim (pages[index]=NULL + zap_vma_range). This
mutex is only contended on the cold path when pages need to be installed
or reclaimed, not on the hot path.

Key changes:

- binder_install_single_page() holds install_mutex across the entire
  install sequence, eliminating the need for binder_page_lookup() (GUP)
  since concurrent installers are now serialized.

- The shrinker holds install_mutex across pages[index]=NULL and
  zap_vma_range(), making them atomic to the install side.

- binder_lru_freelist_del() returns -EAGAIN when list_lru_del() fails
  (shrinker already isolated the page). Any pages already removed from
  the LRU are rolled back, and if the free buffer was split, the split
  is undone (rb_erase + list_del). The caller in binder_alloc_new_buf()
  reallocates the preallocated buffer on each attempt, so a freed
  pointer is never reused.

- To avoid an ABBA deadlock (the shrinker takes mmap_lock before
  install_mutex, while the install side takes install_mutex first),
  binder_page_insert() uses mmap_read_trylock() in its fallback path
  and returns -EAGAIN on contention. The caller drops install_mutex and
  waits for mmap_lock without holding any binder lock before retrying,
  so the install side never blocks on mmap_lock under install_mutex.

- Under install_mutex the PTE cannot already be populated, so an
  unexpected -EBUSY from vm_insert_page() is treated as an error rather
  than retried, avoiding any risk of looping.

Performance (binderThroughputTest, Qualcomm SM8850, 2 workers, 10 runs)
under concurrent drop_caches shows no regression from the install_mutex:

                    mutex (baseline)    spinlock + install_mutex
  throughput:    27k-59k iter/s       85k-89k iter/s
  average:       0.031-0.068ms        0.021-0.022ms
  P99:           0.088-0.148ms        0.046-0.056ms

Signed-off-by: Bo Zhang <zhangbo56@xiaomi.com>
---
 drivers/android/binder_alloc.c | 108 ++++++++++++++++++++++++++-------
 drivers/android/binder_alloc.h |   3 +
 2 files changed, 90 insertions(+), 21 deletions(-)

diff --git a/drivers/android/binder_alloc.c b/drivers/android/binder_alloc.c
index 9775df3616aa..bbef44cc30d4 100644
--- a/drivers/android/binder_alloc.c
+++ b/drivers/android/binder_alloc.c
@@ -268,8 +268,13 @@ static int binder_page_insert(struct binder_alloc *alloc,
 		return ret;
 	}
 
-	/* fall back to mmap_lock */
-	mmap_read_lock(mm);
+	/*
+	 * Fall back to mmap_lock. Use trylock to avoid blocking under
+	 * install_mutex, which could deadlock against the shrinker (it
+	 * takes mmap_lock before install_mutex). Retry on contention.
+	 */
+	if (!mmap_read_trylock(mm))
+		return -EAGAIN;
 	vma = vma_lookup(mm, addr);
 	if (vma && binder_alloc_is_mapped(alloc))
 		ret = vm_insert_page(vma, addr, page);
@@ -325,24 +330,36 @@ static int binder_install_single_page(struct binder_alloc *alloc,
 		goto out;
 	}
 
+	mutex_lock(&alloc->install_mutex);
+
+	/* Check again under install_mutex */
+	if (binder_get_installed_page(alloc, index)) {
+		mutex_unlock(&alloc->install_mutex);
+		binder_free_page(page);
+		ret = 0;
+		goto out;
+	}
+
 	ret = binder_page_insert(alloc, addr, page);
 	switch (ret) {
+	case -EAGAIN:
+		/* mmap_lock contended; drop install_mutex and retry */
+		binder_free_page(page);
+		mutex_unlock(&alloc->install_mutex);
+		goto out;
 	case -EBUSY:
 		/*
-		 * EBUSY is ok. Someone installed the pte first but the
-		 * alloc->pages[index] has not been updated yet. Discard
-		 * our page and look up the one already installed.
+		 * install_mutex serializes page installation against the
+		 * shrinker's zap, so the PTE should never be already
+		 * populated here. If it somehow is (e.g. populated
+		 * externally), fail rather than retry to avoid looping.
 		 */
-		ret = 0;
 		binder_free_page(page);
-		page = binder_page_lookup(alloc, addr);
-		if (!page) {
-			pr_err("%d: failed to find page at offset %lx\n",
-			       alloc->pid, addr - alloc->vm_start);
-			ret = -ESRCH;
-			break;
-		}
-		fallthrough;
+		mutex_unlock(&alloc->install_mutex);
+		pr_err("%d: %s unexpected EBUSY at offset %lx\n",
+		       alloc->pid, __func__, addr - alloc->vm_start);
+		ret = -ENOMEM;
+		goto out;
 	case 0:
 		/* Mark page installation complete and safe to use */
 		binder_set_installed_page(alloc, index, page);
@@ -353,6 +370,8 @@ static int binder_install_single_page(struct binder_alloc *alloc,
 		       alloc->pid, __func__, addr - alloc->vm_start, ret);
 		break;
 	}
+
+	mutex_unlock(&alloc->install_mutex);
 out:
 	mmput_async(alloc->mm);
 	return ret;
@@ -377,8 +396,21 @@ static int binder_install_buffer_pages(struct binder_alloc *alloc,
 			continue;
 
 		trace_binder_alloc_page_start(alloc, index);
-
+retry:
 		ret = binder_install_single_page(alloc, index, page_addr);
+		if (ret == -EAGAIN) {
+			/*
+			 * Wait for mmap_lock to become free before retrying,
+			 * to avoid busy-looping. Safe here as no binder lock
+			 * is held.
+			 */
+			if (mmget_not_zero(alloc->mm)) {
+				mmap_read_lock(alloc->mm);
+				mmap_read_unlock(alloc->mm);
+				mmput_async(alloc->mm);
+			}
+			goto retry;
+		}
 		if (ret)
 			return ret;
 
@@ -389,7 +421,7 @@ static int binder_install_buffer_pages(struct binder_alloc *alloc,
 }
 
 /* The range of pages should exclude those shared with other buffers */
-static void binder_lru_freelist_del(struct binder_alloc *alloc,
+static int binder_lru_freelist_del(struct binder_alloc *alloc,
 				    unsigned long start, unsigned long end)
 {
 	unsigned long page_addr;
@@ -411,7 +443,16 @@ static void binder_lru_freelist_del(struct binder_alloc *alloc,
 					      page_to_lru(page),
 					      page_to_nid(page),
 					      NULL);
-			WARN_ON(!on_lru);
+			/*
+			 * If !on_lru, the shrinker has already isolated this
+			 * page and will reclaim it. Abort so the caller can
+			 * retry after the shrinker finishes.
+			 */
+			if (!on_lru) {
+				/* Rollback pages already removed from LRU */
+				binder_lru_freelist_add(alloc, start, page_addr);
+				return -EAGAIN;
+			}
 
 			trace_binder_alloc_lru_end(alloc, index);
 			continue;
@@ -420,6 +461,8 @@ static void binder_lru_freelist_del(struct binder_alloc *alloc,
 		if (index + 1 > alloc->pages_high)
 			alloc->pages_high = index + 1;
 	}
+
+	return 0;
 }
 
 static void debug_no_space_locked(struct binder_alloc *alloc)
@@ -521,6 +564,7 @@ static struct binder_buffer *binder_alloc_new_buf_locked(
 	struct rb_node *n = alloc->free_buffers.rb_node;
 	struct rb_node *best_fit = NULL;
 	struct binder_buffer *buffer;
+	struct binder_buffer *split_buffer = NULL;
 	unsigned long next_used_page;
 	unsigned long curr_last_page;
 	size_t buffer_size;
@@ -568,6 +612,7 @@ static struct binder_buffer *binder_alloc_new_buf_locked(
 		list_add(&new_buffer->entry, &buffer->entry);
 		new_buffer->free = 1;
 		binder_insert_free_buffer(alloc, new_buffer);
+		split_buffer = new_buffer;
 		new_buffer = NULL;
 	}
 
@@ -583,8 +628,17 @@ static struct binder_buffer *binder_alloc_new_buf_locked(
 	 */
 	next_used_page = (buffer->user_data + buffer_size) & PAGE_MASK;
 	curr_last_page = PAGE_ALIGN(buffer->user_data + size);
-	binder_lru_freelist_del(alloc, PAGE_ALIGN(buffer->user_data),
-				min(next_used_page, curr_last_page));
+	if (binder_lru_freelist_del(alloc, PAGE_ALIGN(buffer->user_data),
+				min(next_used_page, curr_last_page))) {
+		/* Shrinker is reclaiming a page; undo the split and retry */
+		if (split_buffer) {
+			rb_erase(&split_buffer->rb_node, &alloc->free_buffers);
+			list_del(&split_buffer->entry);
+			new_buffer = split_buffer;
+		}
+		buffer = ERR_PTR(-EAGAIN);
+		goto out;
+	}
 
 	rb_erase(&buffer->rb_node, &alloc->free_buffers);
 	buffer->free = 0;
@@ -671,7 +725,8 @@ struct binder_buffer *binder_alloc_new_buf(struct binder_alloc *alloc,
 		return ERR_PTR(-EINVAL);
 	}
 
-	/* Preallocate the next buffer */
+	/* Preallocate the next buffer; (re)allocate on each attempt */
+retry:
 	next = kzalloc_obj(*next);
 	if (!next)
 		return ERR_PTR(-ENOMEM);
@@ -680,6 +735,12 @@ struct binder_buffer *binder_alloc_new_buf(struct binder_alloc *alloc,
 	buffer = binder_alloc_new_buf_locked(alloc, next, size, is_async);
 	if (IS_ERR(buffer)) {
 		spin_unlock(&alloc->lock);
+		if (PTR_ERR(buffer) == -EAGAIN) {
+			/* wait for the shrinker to finish, then retry */
+			mutex_lock(&alloc->install_mutex);
+			mutex_unlock(&alloc->install_mutex);
+			goto retry;
+		}
 		goto out;
 	}
 
@@ -1175,7 +1236,6 @@ enum lru_status binder_alloc_free_page(struct list_head *item,
 	trace_binder_unmap_kernel_start(alloc, index);
 
 	page_to_free = alloc->pages[index];
-	binder_set_installed_page(alloc, index, NULL);
 
 	trace_binder_unmap_kernel_end(alloc, index);
 
@@ -1183,6 +1243,9 @@ enum lru_status binder_alloc_free_page(struct list_head *item,
 	spin_unlock(&alloc->lock);
 	spin_unlock(&lru->lock);
 
+	mutex_lock(&alloc->install_mutex);
+	binder_set_installed_page(alloc, index, NULL);
+
 	if (vma) {
 		trace_binder_unmap_user_start(alloc, index);
 
@@ -1191,6 +1254,8 @@ enum lru_status binder_alloc_free_page(struct list_head *item,
 		trace_binder_unmap_user_end(alloc, index);
 	}
 
+	mutex_unlock(&alloc->install_mutex);
+
 	if (mm_locked)
 		mmap_read_unlock(mm);
 	else
@@ -1236,6 +1301,7 @@ VISIBLE_IF_KUNIT void __binder_alloc_init(struct binder_alloc *alloc,
 	alloc->mm = current->mm;
 	mmgrab(alloc->mm);
 	spin_lock_init(&alloc->lock);
+	mutex_init(&alloc->install_mutex);
 	INIT_LIST_HEAD(&alloc->buffers);
 	alloc->freelist = freelist;
 }
diff --git a/drivers/android/binder_alloc.h b/drivers/android/binder_alloc.h
index bea5a77bb6da..85817efdbef6 100644
--- a/drivers/android/binder_alloc.h
+++ b/drivers/android/binder_alloc.h
@@ -9,6 +9,7 @@
 #include <linux/rbtree.h>
 #include <linux/list.h>
 #include <linux/mm.h>
+#include <linux/mutex.h>
 #include <linux/spinlock.h>
 #include <linux/vmalloc.h>
 #include <linux/slab.h>
@@ -81,6 +82,7 @@ static inline struct list_head *page_to_lru(struct page *p)
 /**
  * struct binder_alloc - per-binder proc state for binder allocator
  * @lock:               protects binder_alloc fields
+ * @install_mutex:      serializes page installation and shrinker zap
  * @mm:                 copy of task->mm (invariant after open)
  * @vm_start:           base of per-proc address space mapped via mmap
  * @buffers:            list of all buffers for this proc
@@ -106,6 +108,7 @@ static inline struct list_head *page_to_lru(struct page *p)
  */
 struct binder_alloc {
 	spinlock_t lock;
+	struct mutex install_mutex;
 	struct mm_struct *mm;
 	unsigned long vm_start;
 	struct list_head buffers;
-- 
2.34.1


  parent reply	other threads:[~2026-09-04 11:05 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-04 11:04 [RFC PATCH v3 0/2] binder: split alloc->mutex to improve performance Bo Zhang
2026-09-04 11:04 ` [RFC PATCH v3 1/2] binder: switch alloc->mutex to spinlock for buffer metadata Bo Zhang
2026-09-04 11:04 ` Bo Zhang [this message]
2026-09-04 16:19   ` [RFC PATCH v3 2/2] binder: add install_mutex to serialize page install and shrinker zap Bo Zhang

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260904110448.23086-3-zhangbo0325@gmail.com \
    --to=zhangbo0325@gmail.com \
    --cc=aliceryhl@google.com \
    --cc=arve@android.com \
    --cc=baohua@kernel.org \
    --cc=christian@brauner.io \
    --cc=cmllamas@google.com \
    --cc=gregkh@linuxfoundation.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=surenb@google.com \
    --cc=tkjos@android.com \
    --cc=zhangbo56@xiaomi.com \
    --cc=zhanghongru06@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®