[PATCH] fs/ntfs3: add depth limit to indx_find_buffer to prevent stack overflow

Michael Bommarito <[email protected]> Mon, 13 Apr 2026 09:31:17 -0400
Newsgroups dev.linux.lists.ntfs3,org.kernel.vger.linux-fsdevel,org.kernel.vger.stable
Message-ID <[email protected]>
indx_find_buffer() recursively descends the B+ tree index with no depth
limit.  A crafted NTFS image with circular index node references causes
unbounded recursion, overflowing the kernel stack and panicking the
system.

This is reachable by mounting a malicious NTFS filesystem (e.g. from a
USB drive via desktop automount) and deleting a file whose index entry
triggers the rebalancing fallback path in indx_delete_entry().

Add a depth parameter and bail out with -EINVAL when it reaches the
fnd->nodes array bound, matching the constraint already enforced by
fnd_push() in indx_find().

The related function indx_find() was previously patched for a similar
infinite-loop issue (commit 1732053c8a6b), but indx_find_buffer() was
missed.

Fixes: 82cae269cfa9 ("fs/ntfs3: Add initialization of super block")
Cc: [email protected]
Assisted-by: Claude:claude-opus-4-6
Assisted-by: Codex:gpt-5-4
Signed-off-by: Michael Bommarito <[email protected]>
---
Found during a broader arch/um/ and filesystem security audit.
This is the same class of bug as the one fixed by commit
1732053c8a6b ("fs: ntfs3: check return value of indx_find to
avoid infinite loop"), which added a depth limit to indx_find()
but missed indx_find_buffer().

Reproduced on UML (ARCH=um) with a crafted NTFS image containing
a circular B+ tree directory index. Mounting the image and
deleting a specific file triggers indx_delete_entry() ->
indx_find_buffer() -> unbounded recursion -> stack overflow:

  Kernel panic - not syncing: Kernel tried to access user memory
    at addr 0x606128c4, ip 0x6012907e
  Call Trace:
   [<60611ec2>] ? indx_read_ra+0x0/0x677

At 168+ bytes per frame, ~97 recursions overflow the 16KB kernel
stack. Desktop automount (udisks2 + ntfs3) means a crafted USB
drive can trigger this without privilege.

Note: the pre-existing indx_node allocated by indx_read() during
the DFS is leaked when the new depth limit fires. This is a
pre-existing issue (the node was also leaked on any other error
return from indx_find_buffer); fixing it cleanly requires
restructuring the node ownership model and is left for a
follow-up patch.

Reproducer script and crafted image builder available on request.

 fs/ntfs3/index.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/fs/ntfs3/index.c b/fs/ntfs3/index.c
index 97f06c26fe1a..2c43e7c27861 100644
--- a/fs/ntfs3/index.c
+++ b/fs/ntfs3/index.c
@@ -2013,13 +2013,21 @@ int indx_insert_entry(struct ntfs_index *indx, struct ntfs_inode *ni,
 static struct indx_node *indx_find_buffer(struct ntfs_index *indx,
 					  struct ntfs_inode *ni,
 					  const struct INDEX_ROOT *root,
-					  __le64 vbn, struct indx_node *n)
+					  __le64 vbn, struct indx_node *n,
+					  int depth)
 {
 	int err;
 	const struct NTFS_DE *e;
 	struct indx_node *r;
 	const struct INDEX_HDR *hdr = n ? &n->index->ihdr : &root->ihdr;
 
+	/*
+	 * Limit recursion depth to prevent stack overflow from crafted
+	 * images.  Use the same bound as the fnd->nodes array (20).
+	 */
+	if (depth > ARRAY_SIZE(((struct ntfs_fnd *)NULL)->nodes))
+		return ERR_PTR(-EINVAL);
+
 	/* Step 1: Scan one level. */
 	for (e = hdr_first_de(hdr);; e = hdr_next_de(hdr, e)) {
 		if (!e)
@@ -2040,7 +2048,8 @@ static struct indx_node *indx_find_buffer(struct ntfs_index *indx,
 			if (err)
 				return ERR_PTR(err);
 
-			r = indx_find_buffer(indx, ni, root, vbn, n);
+			r = indx_find_buffer(indx, ni, root, vbn, n,
+					     depth + 1);
 			if (r)
 				return r;
 		}
@@ -2446,7 +2455,7 @@ int indx_delete_entry(struct ntfs_index *indx, struct ntfs_inode *ni,
 
 		fnd_clear(fnd);
 
-		in = indx_find_buffer(indx, ni, root, sub_vbn, NULL);
+		in = indx_find_buffer(indx, ni, root, sub_vbn, NULL, 0);
 		if (IS_ERR(in)) {
 			err = PTR_ERR(in);
 			goto out;
-- 
2.53.0