From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from shelob.surriel.com (shelob.surriel.com [96.67.55.147]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9D8653BFAEA for ; Tue, 16 Jun 2026 14:54:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=96.67.55.147 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781621677; cv=none; b=ic3vAKhcUtqhSjMqgAN4NoiWuTKAjzN0NBAb+Y0bvd8NkSmpLNDnd7zOx1fljqte76mjlnntSuJ/APIdmbnuTXhx3khTM2dKyPm/+ryoDEhmb36XJy1JOWNnvVYS0g20uCx69YL4k9c/gutGARg1RWSqYs6yxe62dOCKffE0Ogw= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781621677; c=relaxed/simple; bh=J1T3q/B8QHtMlfAmVlYfXlAC31aQIs7u1xgljif0qx0=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=uaTBNfYpeMZtyK6eSxC1l1koMszzBkx9O+Sh4bMu5eD/CQPqP81wsN9cJd4NBNclUnsTo/BTzB/Zw2AqraLDgHlK/vwoJR1UXkwfPi2WG4wKWl4ZKVqa6jvsFedA6hjTMpCJHRCXPAn71h9U6yMgZ8h0iAy2/Uj6KlLeoTWS8Zc= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=surriel.com; spf=pass smtp.mailfrom=surriel.com; dkim=pass (2048-bit key) header.d=surriel.com header.i=@surriel.com header.b=X4jn8n0U; arc=none smtp.client-ip=96.67.55.147 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=surriel.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=surriel.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=surriel.com header.i=@surriel.com header.b="X4jn8n0U" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=surriel.com ; s=mail; h=Content-Transfer-Encoding:MIME-Version:Message-ID:Date:Subject:Cc :To:From:Sender:Reply-To:Content-Type:Content-ID:Content-Description: Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID: In-Reply-To:References:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=xyyMgzL3dB9zRqf2mWub/khi9AUa+g6mx9a5bkCc6yY=; b=X4jn8n0UChybSdWbaA3p5vTXQ9 POf5b0yi0/B8W8WTmHwPzzKRALHRqJeKIpscn2Hd8ThX3gUT6csiQmRRRjiSJlrFS4EggNtX7vu53 n814jdE86cyFX83NHtB6FbOyWPgJVpQNkzHQspyXZPDqSh7TlY2kDv9cqEj9OdLxAcpxQvJc4EAI5 QTgP14bu/cz5cFwd0CvtNlW7ZMIcxXPmH3zaat242zpK4gE1ITbiCqHlft9rxEiwqudDjCRFq5EXl FgUL6zTZjeowEvI8b/HE0KHSpSJObx8kwzHIE2yhkB/sFQyqnlXyOOoiBGT2wY8Cwj6B0mf7vfrG6 d5y6fF8g==; Received: from fangorn.home.surriel.com ([10.0.13.7]) by shelob.surriel.com with esmtpsa (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.97.1) (envelope-from ) id 1wZVBC-000000002wP-3kfH; Tue, 16 Jun 2026 10:54:18 -0400 From: Rik van Riel To: linux-kernel@vger.kernel.org Cc: kernel-team@meta.com, Rik van Riel , Andrew Morton , David Hildenbrand , Lorenzo Stoakes , Matthew Wilcox , Hugh Dickins , Zi Yan , Baolin Wang , "Liam R. Howlett" , Nico Pache , Ryan Roberts , Dev Jain , Barry Song , Lance Yang , linux-mm@kvack.org Subject: [PATCH] mm: khugepaged: free xarray nodes left behind by failed collapse_file() Date: Tue, 16 Jun 2026 10:54:13 -0400 Message-ID: <20260616145413.1491961-1-riel@surriel.com> X-Mailer: git-send-email 2.54.0 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit collapse_file() calls xas_create_range() up front to populate the xarray with nodes spanning the entire collapse range, including empty slots for the holes (the nr_none entries). On the success path those nodes are either consumed by the multi-index store of new_folio or freed by the nr_none retry-entry dance. But every error path that branches straight to the rollback label -- e.g. SCAN_TRUNCATED / SCAN_PAGE_LOCK / SCAN_FAIL detected in the main scan loop (which then sets nr_none = 0 and jumps to rollback), or SCAN_COPY_MC during the copy -- does neither. The empty nodes are left dangling in mapping->i_pages and are leaked once the inode is finally evicted. This is exactly the "THP collapse_file() failed" case that clear_inode() documents and deliberately tolerates without warning. syzkaller reproduces it trivially with MADV_COLLAPSE on a sparse shmem mapping (collapse aborts with SCAN_TRUNCATED because the range is empty), and also via slab fault injection, which forces xas_create_range() down the xas_nomem() path before the same abort. kmemleak then reports the 576-byte struct xa_node objects allocated in xas_alloc()/xas_nomem(). The leaked objects are struct xa_node (576 bytes each), left dangling in mapping->i_pages and reclaimed only when the inode is finally evicted. Nodes leak when a collapse takes one of the rollback paths (SCAN_TRUNCATED / SCAN_PAGE_LOCK / SCAN_FAIL / SCAN_COPY_MC), and only for the empty hole slots (the nr_none entries); they are not leaked on the success path (the nodes are consumed by the multi-index store and the nr_none dance), and slots still holding folios are never touched. Prune the now-empty nodes on the rollback path. A node is only freed once its slot count reaches zero, and storing NULL into an already-empty slot is a no-op, so briefly store an XA_RETRY_ENTRY into each empty slot and immediately clear it: the clear drops the count back to zero and lets xas_store() -> xas_delete_node() free the node and its now-empty ancestors. Slots still holding the original folios are left untouched. Fixes: 77da9389b9d5 ("mm: Convert collapse_shmem to XArray") Assisted-by: Claude:claude-opus-4-8 syzkaller Signed-off-by: Rik van Riel Cc: Andrew Morton Cc: David Hildenbrand Cc: Lorenzo Stoakes Cc: Matthew Wilcox Cc: Hugh Dickins Cc: Zi Yan Cc: Baolin Wang Cc: Liam R. Howlett Cc: Nico Pache Cc: Ryan Roberts Cc: Dev Jain Cc: Barry Song Cc: Lance Yang Cc: linux-mm@kvack.org Cc: linux-kernel@vger.kernel.org --- mm/khugepaged.c | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/mm/khugepaged.c b/mm/khugepaged.c index b8452dbdb043..d11a4c9610e1 100644 --- a/mm/khugepaged.c +++ b/mm/khugepaged.c @@ -2273,6 +2273,39 @@ static enum scan_result collapse_file(struct mm_struct *mm, unsigned long addr, rollback: /* Something went wrong: roll back page cache changes */ + + /* + * xas_create_range() above populated the xarray with nodes spanning + * the whole collapse range, including empty slots for the holes + * (nr_none entries). On the success path these nodes are consumed by + * the multi-index store of new_folio, and the nr_none handling further + * up frees the ones covering the holes; but the error paths that branch + * straight here do neither. Prune the now-empty nodes explicitly, + * otherwise they are leaked until the mapping is torn down -- one of + * the two cases called out in the comment in clear_inode(). + * + * A node can only be deleted once its slot count drops to zero, so + * briefly store an XA_RETRY_ENTRY into each empty slot and then clear + * it again: clearing the retry entry drops the count back to zero and + * lets xas_store() -> xas_delete_node() free the node. Slots that + * still hold the original folios are left untouched. + */ + xas_lock_irq(&xas); + xas_set_order(&xas, start, 0); + for (index = start; index < end; index++) { + if (!xas_next(&xas)) { + xas_store(&xas, XA_RETRY_ENTRY); + if (xas_error(&xas)) + break; + } + } + xas_set_order(&xas, start, 0); + for (index = start; index < end; index++) { + if (xas_next(&xas) == XA_RETRY_ENTRY) + xas_store(&xas, NULL); + } + xas_unlock_irq(&xas); + if (nr_none) { xas_lock_irq(&xas); mapping->nrpages -= nr_none; -- 2.53.0-Meta