public inbox for linux-cifs@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH 1/7] cifs: change_conf needs to be called for session setup
@ 2026-04-14 13:59 nspmangalore
  2026-04-14 13:59 ` [PATCH 2/7] cifs: abort open_cached_dir if we don't request leases nspmangalore
                   ` (5 more replies)
  0 siblings, 6 replies; 9+ messages in thread
From: nspmangalore @ 2026-04-14 13:59 UTC (permalink / raw)
  To: linux-cifs, smfrench, pc, bharathsm, dhowells, henrique.carvalho,
	ematsumiya
  Cc: Shyam Prasad N, stable

From: Shyam Prasad N <sprasad@microsoft.com>

Today we skip calling change_conf for negotiates and session setup
requests. This can be a problem for mchan as the immediate next call
after session setup could be due to an I/O that is made on the
mount point. For single channel, this is not a problem as
there will be several calls after setting up session.

This change enforces calling change_conf for the last session setup
response, so that echoes and oplocks are not disabled before the
first request to the server. So if that first request is an open,
it does not need to disable requesting leases.

Cc: <stable@vger.kernel.org>
Signed-off-by: Shyam Prasad N <sprasad@microsoft.com>
---
 fs/smb/client/smb2ops.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/fs/smb/client/smb2ops.c b/fs/smb/client/smb2ops.c
index 509fcea28a429..3625030d1912f 100644
--- a/fs/smb/client/smb2ops.c
+++ b/fs/smb/client/smb2ops.c
@@ -111,10 +111,19 @@ smb2_add_credits(struct TCP_Server_Info *server,
 				      cifs_trace_rw_credits_zero_in_flight);
 	}
 	server->in_flight--;
+
+	/*
+	 * Rebalance credits when an op drains in_flight. For session setup,
+	 * do this only when the server actually granted positive credits (>2) so a
+	 * newly established secondary channel can reserve echo/oplock credits.
+	 */
 	if (server->in_flight == 0 &&
 	   ((optype & CIFS_OP_MASK) != CIFS_NEG_OP) &&
 	   ((optype & CIFS_OP_MASK) != CIFS_SESS_OP))
 		rc = change_conf(server);
+	else if (server->in_flight == 0 &&
+		 ((optype & CIFS_OP_MASK) == CIFS_SESS_OP) && add > 2)
+		rc = change_conf(server);
 	/*
 	 * Sometimes server returns 0 credits on oplock break ack - we need to
 	 * rebalance credits in this case.
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 2/7] cifs: abort open_cached_dir if we don't request leases
  2026-04-14 13:59 [PATCH 1/7] cifs: change_conf needs to be called for session setup nspmangalore
@ 2026-04-14 13:59 ` nspmangalore
  2026-04-14 13:59 ` [PATCH 3/7] cifs: invalidate cfid on unlink/rename/rmdir nspmangalore
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 9+ messages in thread
From: nspmangalore @ 2026-04-14 13:59 UTC (permalink / raw)
  To: linux-cifs, smfrench, pc, bharathsm, dhowells, henrique.carvalho,
	ematsumiya
  Cc: Shyam Prasad N, stable

From: Shyam Prasad N <sprasad@microsoft.com>

It is possible that SMB2_open_init may not set lease context based
on the requested oplock level. This can happen when leases have been
temporarily or permanently disabled. When this happens, we will have
open_cached_dir making an open without lease context and the response
will anyway be rejected by open_cached_dir (thereby forcing a close to
discard this open). That's unnecessary two round-trips to the server.

This change adds a check before making the open request to the server
to make sure that SMB2_open_init did add the expected lease context
to the open in open_cached_dir.

Cc: <stable@vger.kernel.org>
Signed-off-by: Shyam Prasad N <sprasad@microsoft.com>
---
 fs/smb/client/cached_dir.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/fs/smb/client/cached_dir.c b/fs/smb/client/cached_dir.c
index 04bb95091f498..e9917e5204b00 100644
--- a/fs/smb/client/cached_dir.c
+++ b/fs/smb/client/cached_dir.c
@@ -286,6 +286,13 @@ int open_cached_dir(unsigned int xid, struct cifs_tcon *tcon,
 			    &rqst[0], &oplock, &oparms, utf16_path);
 	if (rc)
 		goto oshr_free;
+
+	if (oplock != SMB2_OPLOCK_LEVEL_II) {
+		rc = -EINVAL;
+		cifs_dbg(FYI, "unexpected oplock level %d for cached directory\n", oplock);
+		goto oshr_free;
+	}
+
 	smb2_set_next_command(tcon, &rqst[0]);
 
 	memset(&qi_iov, 0, sizeof(qi_iov));
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 3/7] cifs: invalidate cfid on unlink/rename/rmdir
  2026-04-14 13:59 [PATCH 1/7] cifs: change_conf needs to be called for session setup nspmangalore
  2026-04-14 13:59 ` [PATCH 2/7] cifs: abort open_cached_dir if we don't request leases nspmangalore
@ 2026-04-14 13:59 ` nspmangalore
  2026-04-14 13:59 ` [PATCH 4/7] cifs: define variable sized buffer for querydir responses nspmangalore
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 9+ messages in thread
From: nspmangalore @ 2026-04-14 13:59 UTC (permalink / raw)
  To: linux-cifs, smfrench, pc, bharathsm, dhowells, henrique.carvalho,
	ematsumiya
  Cc: Shyam Prasad N, stable

From: Shyam Prasad N <sprasad@microsoft.com>

Today we do not invalidate the cached_dirent or the entire
parent cfid when a dentry in a dir has been removed/moved.

This change invalidates the parent cfid so that we don't serve
directory contents from the cache.

Cc: <stable@vger.kernel.org>
Signed-off-by: Shyam Prasad N <sprasad@microsoft.com>
---
 fs/smb/client/inode.c | 40 ++++++++++++++++++++++++++++++++++++++--
 1 file changed, 38 insertions(+), 2 deletions(-)

diff --git a/fs/smb/client/inode.c b/fs/smb/client/inode.c
index 888f9e35f14b8..e077df844c819 100644
--- a/fs/smb/client/inode.c
+++ b/fs/smb/client/inode.c
@@ -28,6 +28,23 @@
 #include "cached_dir.h"
 #include "reparse.h"
 
+static void cifs_invalidate_cached_dir(struct cifs_tcon *tcon,
+				       struct dentry *parent)
+{
+	struct cached_fid *parent_cfid = NULL;
+
+	if (!tcon || !parent)
+		return;
+
+	if (!open_cached_dir_by_dentry(tcon, parent, &parent_cfid)) {
+		mutex_lock(&parent_cfid->dirents.de_mutex);
+		parent_cfid->dirents.is_valid = false;
+		parent_cfid->dirents.is_failed = true;
+		mutex_unlock(&parent_cfid->dirents.de_mutex);
+		close_cached_dir(parent_cfid);
+	}
+}
+
 /*
  * Set parameters for the netfs library
  */
@@ -322,7 +339,7 @@ cifs_unix_basic_to_fattr(struct cifs_fattr *fattr, FILE_UNIX_BASIC_INFO *info,
 				fattr->cf_uid = uid;
 		}
 	}
-	
+
 	fattr->cf_gid = cifs_sb->ctx->linux_gid;
 	if (!(sbflags & CIFS_MOUNT_OVERR_GID)) {
 		u64 id = le64_to_cpu(info->Gid);
@@ -2067,6 +2084,9 @@ static int __cifs_unlink(struct inode *dir, struct dentry *dentry, bool sillyren
 		cifs_set_file_info(inode, attrs, xid, full_path, origattr);
 
 out_reval:
+	if (!rc && dentry->d_parent)
+		cifs_invalidate_cached_dir(tcon, dentry->d_parent);
+
 	if (inode) {
 		cifs_inode = CIFS_I(inode);
 		cifs_inode->time = 0;	/* will force revalidate to get info
@@ -2378,7 +2398,6 @@ int cifs_rmdir(struct inode *inode, struct dentry *direntry)
 	}
 
 	rc = server->ops->rmdir(xid, tcon, full_path, cifs_sb);
-	cifs_put_tlink(tlink);
 
 	cifsInode = CIFS_I(d_inode(direntry));
 
@@ -2388,6 +2407,8 @@ int cifs_rmdir(struct inode *inode, struct dentry *direntry)
 		i_size_write(d_inode(direntry), 0);
 		clear_nlink(d_inode(direntry));
 		spin_unlock(&d_inode(direntry)->i_lock);
+		if (direntry->d_parent)
+			cifs_invalidate_cached_dir(tcon, direntry->d_parent);
 	}
 
 	/* force revalidate to go get info when needed */
@@ -2402,6 +2423,7 @@ int cifs_rmdir(struct inode *inode, struct dentry *direntry)
 
 	inode_set_ctime_current(d_inode(direntry));
 	inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode));
+	cifs_put_tlink(tlink);
 
 rmdir_exit:
 	free_dentry_path(page);
@@ -2501,6 +2523,8 @@ cifs_rename2(struct mnt_idmap *idmap, struct inode *source_dir,
 	struct cifs_sb_info *cifs_sb;
 	struct tcon_link *tlink;
 	struct cifs_tcon *tcon;
+	struct dentry *source_parent;
+	struct dentry *target_parent;
 	bool rehash = false;
 	unsigned int xid;
 	int rc, tmprc;
@@ -2532,6 +2556,8 @@ cifs_rename2(struct mnt_idmap *idmap, struct inode *source_dir,
 	if (IS_ERR(tlink))
 		return PTR_ERR(tlink);
 	tcon = tlink_tcon(tlink);
+	source_parent = source_dentry->d_parent ? dget(source_dentry->d_parent) : NULL;
+	target_parent = target_dentry->d_parent ? dget(target_dentry->d_parent) : NULL;
 	server = tcon->ses->server;
 
 	page1 = alloc_dentry_path();
@@ -2668,11 +2694,21 @@ cifs_rename2(struct mnt_idmap *idmap, struct inode *source_dir,
 	}
 
 	/* force revalidate to go get info when needed */
+	if (!rc) {
+		cifs_invalidate_cached_dir(tcon, source_parent);
+		if (target_parent != source_parent)
+			cifs_invalidate_cached_dir(tcon, target_parent);
+	}
+
 	CIFS_I(source_dir)->time = CIFS_I(target_dir)->time = 0;
 
 cifs_rename_exit:
 	if (rehash)
 		d_rehash(target_dentry);
+	if (target_parent)
+		dput(target_parent);
+	if (source_parent)
+		dput(source_parent);
 	kfree(info_buf_source);
 	free_dentry_path(page2);
 	free_dentry_path(page1);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 4/7] cifs: define variable sized buffer for querydir responses
  2026-04-14 13:59 [PATCH 1/7] cifs: change_conf needs to be called for session setup nspmangalore
  2026-04-14 13:59 ` [PATCH 2/7] cifs: abort open_cached_dir if we don't request leases nspmangalore
  2026-04-14 13:59 ` [PATCH 3/7] cifs: invalidate cfid on unlink/rename/rmdir nspmangalore
@ 2026-04-14 13:59 ` nspmangalore
  2026-04-14 13:59 ` [PATCH 5/7] cifs: optimize readdir for small directories nspmangalore
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 9+ messages in thread
From: nspmangalore @ 2026-04-14 13:59 UTC (permalink / raw)
  To: linux-cifs, smfrench, pc, bharathsm, dhowells, henrique.carvalho,
	ematsumiya
  Cc: Shyam Prasad N

From: Shyam Prasad N <sprasad@microsoft.com>

QueryDirectory responses today are stored in one of two fixed
sized buffers: smallbuf (448 bytes) or bigbuf (16KB). These are
borrowed from server struct and are not sufficient for large-sized
query dir operations.

With this change we will now define a new buffer type specifically
for cifs_search_info to hold variable sized responses. These will
be allocated by kmalloc and freed by kfree.

Signed-off-by: Shyam Prasad N <sprasad@microsoft.com>
---
 fs/smb/client/cifsglob.h |  2 ++
 fs/smb/client/file.c     |  2 ++
 fs/smb/client/readdir.c  |  2 ++
 fs/smb/client/smb2pdu.c  | 14 +++++++++++---
 4 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/fs/smb/client/cifsglob.h b/fs/smb/client/cifsglob.h
index 709e96e077916..8d089ba08e3e5 100644
--- a/fs/smb/client/cifsglob.h
+++ b/fs/smb/client/cifsglob.h
@@ -1394,6 +1394,7 @@ struct cifs_search_info {
 	bool emptyDir:1;
 	bool unicode:1;
 	bool smallBuf:1; /* so we know which buf_release function to call */
+	bool is_dynamic_buf:1; /* dynamically allocated buffer - can be variable size */
 };
 
 #define ACL_NO_MODE	((umode_t)(-1))
@@ -1907,6 +1908,7 @@ enum cifs_find_flags {
 #define   CIFS_NO_BUFFER        0    /* Response buffer not returned */
 #define   CIFS_SMALL_BUFFER     1
 #define   CIFS_LARGE_BUFFER     2
+#define   CIFS_DYNAMIC_BUFFER   3    /* Dynamically allocated buffer */
 #define   CIFS_IOVEC            4    /* array of response buffers */
 
 /* Type of Request to SendReceive2 */
diff --git a/fs/smb/client/file.c b/fs/smb/client/file.c
index a69e05f86d7e2..6a1419d59ed5a 100644
--- a/fs/smb/client/file.c
+++ b/fs/smb/client/file.c
@@ -1546,6 +1546,8 @@ int cifs_closedir(struct inode *inode, struct file *file)
 		cfile->srch_inf.ntwrk_buf_start = NULL;
 		if (cfile->srch_inf.smallBuf)
 			cifs_small_buf_release(buf);
+		else if (cfile->srch_inf.is_dynamic_buf)
+			kfree(buf);
 		else
 			cifs_buf_release(buf);
 	}
diff --git a/fs/smb/client/readdir.c b/fs/smb/client/readdir.c
index be22bbc4a65a0..b50efd9b9e1d2 100644
--- a/fs/smb/client/readdir.c
+++ b/fs/smb/client/readdir.c
@@ -732,6 +732,8 @@ find_cifs_entry(const unsigned int xid, struct cifs_tcon *tcon, loff_t pos,
 			if (cfile->srch_inf.smallBuf)
 				cifs_small_buf_release(cfile->srch_inf.
 						ntwrk_buf_start);
+			else if (cfile->srch_inf.is_dynamic_buf)
+				kfree(cfile->srch_inf.ntwrk_buf_start);
 			else
 				cifs_buf_release(cfile->srch_inf.
 						ntwrk_buf_start);
diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
index 5188218c25be4..49dca84b169e6 100644
--- a/fs/smb/client/smb2pdu.c
+++ b/fs/smb/client/smb2pdu.c
@@ -5625,6 +5625,8 @@ smb2_parse_query_directory(struct cifs_tcon *tcon,
 	if (srch_inf->ntwrk_buf_start) {
 		if (srch_inf->smallBuf)
 			cifs_small_buf_release(srch_inf->ntwrk_buf_start);
+		else if (srch_inf->is_dynamic_buf)
+			kfree(srch_inf->ntwrk_buf_start);
 		else
 			cifs_buf_release(srch_inf->ntwrk_buf_start);
 	}
@@ -5644,12 +5646,18 @@ smb2_parse_query_directory(struct cifs_tcon *tcon,
 	cifs_dbg(FYI, "num entries %d last_index %lld srch start %p srch end %p\n",
 		 srch_inf->entries_in_buffer, srch_inf->index_of_last_entry,
 		 srch_inf->srch_entries_start, srch_inf->last_entry);
-	if (resp_buftype == CIFS_LARGE_BUFFER)
+	if (resp_buftype == CIFS_LARGE_BUFFER) {
 		srch_inf->smallBuf = false;
-	else if (resp_buftype == CIFS_SMALL_BUFFER)
+		srch_inf->is_dynamic_buf = false;
+	} else if (resp_buftype == CIFS_SMALL_BUFFER) {
 		srch_inf->smallBuf = true;
-	else
+		srch_inf->is_dynamic_buf = false;
+	} else if (resp_buftype == CIFS_DYNAMIC_BUFFER) {
+		srch_inf->smallBuf = false;
+		srch_inf->is_dynamic_buf = true;
+	} else {
 		cifs_tcon_dbg(VFS, "Invalid search buffer type\n");
+	}
 
 	return 0;
 }
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 5/7] cifs: optimize readdir for small directories
  2026-04-14 13:59 [PATCH 1/7] cifs: change_conf needs to be called for session setup nspmangalore
                   ` (2 preceding siblings ...)
  2026-04-14 13:59 ` [PATCH 4/7] cifs: define variable sized buffer for querydir responses nspmangalore
@ 2026-04-14 13:59 ` nspmangalore
  2026-04-14 13:59 ` [PATCH 6/7] cifs: optimize readdir for larger directories nspmangalore
  2026-04-14 13:59 ` [PATCH 7/7] cifs: reorganize cached dir helpers nspmangalore
  5 siblings, 0 replies; 9+ messages in thread
From: nspmangalore @ 2026-04-14 13:59 UTC (permalink / raw)
  To: linux-cifs, smfrench, pc, bharathsm, dhowells, henrique.carvalho,
	ematsumiya
  Cc: Shyam Prasad N

From: Shyam Prasad N <sprasad@microsoft.com>

For small directories (where the entire directory contents could be
read in a single QueryDir request), we currently do an extra
round-trip just to get a STATUS_NO_MORE_FILES back from the server.

This change avoids doing that by adding another QueryDir to the first
compound to the server for readdir. i.e. first request to readdir
will correspond to a compound of (OPEN+QD1+QD2). QD2 will request
for a smaller size (in anticipation of STATUS_NO_MORE_FILES).

Signed-off-by: Shyam Prasad N <sprasad@microsoft.com>
---
 fs/smb/client/smb2ops.c   | 148 ++++++++++++++++++++++++++++++++++----
 fs/smb/client/smb2pdu.c   |  19 +++--
 fs/smb/client/smb2pdu.h   |  11 +++
 fs/smb/client/smb2proto.h |   3 +-
 4 files changed, 160 insertions(+), 21 deletions(-)

diff --git a/fs/smb/client/smb2ops.c b/fs/smb/client/smb2ops.c
index 3625030d1912f..5c6fe25204d26 100644
--- a/fs/smb/client/smb2ops.c
+++ b/fs/smb/client/smb2ops.c
@@ -2448,15 +2448,17 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
 		     struct cifs_search_info *srch_inf)
 {
 	__le16 *utf16_path;
-	struct smb_rqst rqst[2];
-	struct kvec rsp_iov[2];
-	int resp_buftype[2];
+	struct smb_rqst rqst[3];
+	struct kvec rsp_iov[3];
+	int resp_buftype[3];
 	struct kvec open_iov[SMB2_CREATE_IOV_SIZE];
-	struct kvec qd_iov[SMB2_QUERY_DIRECTORY_IOV_SIZE];
+	struct kvec qd_iov[SMB2_QUERY_DIRECTORY_IOV_SIZE + 1]; /* +1 for padding */
+	struct kvec qd2_iov[SMB2_QUERY_DIRECTORY_IOV_SIZE + 1]; /* +1 for padding */
 	int rc, flags = 0;
 	u8 oplock = SMB2_OPLOCK_LEVEL_NONE;
 	struct cifs_open_parms oparms;
 	struct smb2_query_directory_rsp *qd_rsp = NULL;
+	struct smb2_query_directory_rsp *qd2_rsp = NULL;
 	struct smb2_create_rsp *op_rsp = NULL;
 	struct TCP_Server_Info *server;
 	int retries = 0, cur_sleep = 0;
@@ -2475,7 +2477,7 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
 		flags |= CIFS_TRANSFORM_REQ;
 
 	memset(rqst, 0, sizeof(rqst));
-	resp_buftype[0] = resp_buftype[1] = CIFS_NO_BUFFER;
+	resp_buftype[0] = resp_buftype[1] = resp_buftype[2] = CIFS_NO_BUFFER;
 	memset(rsp_iov, 0, sizeof(rsp_iov));
 
 	/* Open */
@@ -2499,7 +2501,7 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
 		goto qdf_free;
 	smb2_set_next_command(tcon, &rqst[0]);
 
-	/* Query directory */
+	/* First Query directory */
 	srch_inf->entries_in_buffer = 0;
 	srch_inf->index_of_last_entry = 2;
 
@@ -2510,11 +2512,27 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
 	rc = SMB2_query_directory_init(xid, tcon, server,
 				       &rqst[1],
 				       COMPOUND_FID, COMPOUND_FID,
-				       0, srch_inf->info_level);
+				       0, srch_inf->info_level,
+				       SMB2_QD1_OUTPUT_SIZE(CIFSMaxBufSize));
 	if (rc)
 		goto qdf_free;
 
 	smb2_set_related(&rqst[1]);
+	smb2_set_next_command(tcon, &rqst[1]);
+
+	/* Second Query directory - minimal size to check if more data exists */
+	memset(&qd2_iov, 0, sizeof(qd2_iov));
+	rqst[2].rq_iov = qd2_iov;
+	rqst[2].rq_nvec = SMB2_QUERY_DIRECTORY_IOV_SIZE;
+
+	rc = SMB2_query_directory_init(xid, tcon, server,
+				       &rqst[2],
+				       COMPOUND_FID, COMPOUND_FID,
+				       0, srch_inf->info_level, SMB2_QD2_RESPONSE_SIZE);
+	if (rc)
+		goto qdf_free;
+
+	smb2_set_related(&rqst[2]);
 
 	if (retries) {
 		/* Back-off before retry */
@@ -2522,10 +2540,11 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
 			msleep(cur_sleep);
 		smb2_set_replay(server, &rqst[0]);
 		smb2_set_replay(server, &rqst[1]);
+		smb2_set_replay(server, &rqst[2]);
 	}
 
 	rc = compound_send_recv(xid, tcon->ses, server,
-				flags, 2, rqst,
+				flags, 3, rqst,
 				resp_buftype, rsp_iov);
 
 	/* If the open failed there is nothing to do */
@@ -2557,14 +2576,111 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
 		goto qdf_free;
 	}
 
-	rc = smb2_parse_query_directory(tcon, &rsp_iov[1], resp_buftype[1],
-					srch_inf);
-	if (rc) {
-		trace_smb3_query_dir_err(xid, fid->persistent_fid, tcon->tid,
-			tcon->ses->Suid, 0, 0, rc);
-		goto qdf_free;
+	qd2_rsp = (struct smb2_query_directory_rsp *)rsp_iov[2].iov_base;
+
+	/*
+	 * If QD2 has data, combine QD1 and QD2 responses before parsing.
+	 * The server cursor advances past both responses, so we can't discard QD2.
+	 */
+	if (qd2_rsp && qd2_rsp->hdr.Status == STATUS_SUCCESS &&
+	    le32_to_cpu(qd2_rsp->OutputBufferLength) > 0) {
+		char *combined_buf;
+		size_t qd1_data_len, qd2_data_len, combined_len;
+		u16 qd1_offset, qd2_offset;
+		struct smb2_query_directory_rsp *combined_rsp;
+		struct kvec combined_iov;
+		FILE_DIRECTORY_INFO *last_entry_in_qd1;
+		char *qd1_entries_start, *qd2_entries_start;
+		unsigned int next_offset;
+
+		qd1_offset = le16_to_cpu(qd_rsp->OutputBufferOffset);
+		qd2_offset = le16_to_cpu(qd2_rsp->OutputBufferOffset);
+		qd1_data_len = le32_to_cpu(qd_rsp->OutputBufferLength);
+		qd2_data_len = le32_to_cpu(qd2_rsp->OutputBufferLength);
+
+		/* Allocate buffer for: QD1 header + QD1 data + QD2 data */
+		combined_len = qd1_offset + qd1_data_len + qd2_data_len;
+		combined_buf = kmalloc(combined_len, GFP_KERNEL);
+		if (!combined_buf) {
+			rc = -ENOMEM;
+			goto qdf_free;
+		}
+
+		/* Copy QD1 header and data */
+		memcpy(combined_buf, qd_rsp, qd1_offset + qd1_data_len);
+
+		/* Append QD2 data (directory entries only, not the header) */
+		memcpy(combined_buf + qd1_offset + qd1_data_len,
+		       (char *)qd2_rsp + qd2_offset, qd2_data_len);
+
+		/* Update OutputBufferLength to reflect combined data */
+		combined_rsp = (struct smb2_query_directory_rsp *)combined_buf;
+		combined_rsp->OutputBufferLength = cpu_to_le32(qd1_data_len + qd2_data_len);
+
+		/*
+		 * Chain QD1 and QD2 entries: find the last entry in QD1 and update
+		 * its NextEntryOffset to point to the first entry in QD2.
+		 */
+		if (qd1_data_len > 0) {
+			qd1_entries_start = combined_buf + qd1_offset;
+			qd2_entries_start = combined_buf + qd1_offset + qd1_data_len;
+			last_entry_in_qd1 = (FILE_DIRECTORY_INFO *)qd1_entries_start;
+
+			/* Walk QD1 entries to find the last one with bounds checking */
+			while (1) {
+				char *end_of_qd1 = qd1_entries_start + qd1_data_len;
+
+				next_offset = le32_to_cpu(last_entry_in_qd1->NextEntryOffset);
+				if (next_offset == 0)
+					break;  /* Found last entry */
+
+				/* Bounds check before advancing */
+				if ((char *)last_entry_in_qd1 + next_offset >= end_of_qd1) {
+					cifs_dbg(VFS, "query_dir_first: invalid NextEntryOffset in QD1\n");
+					kfree(combined_buf);
+					rc = -EIO;
+					goto qdf_free;
+				}
+
+				last_entry_in_qd1 = (FILE_DIRECTORY_INFO *)((char *)last_entry_in_qd1 + next_offset);
+			}
+
+			/* Chain last QD1 entry to first QD2 entry */
+			last_entry_in_qd1->NextEntryOffset = cpu_to_le32(qd2_entries_start - (char *)last_entry_in_qd1);
+		}
+
+		/* Parse the combined buffer */
+		combined_iov.iov_base = combined_buf;
+		combined_iov.iov_len = combined_len;
+		rc = smb2_parse_query_directory(tcon, &combined_iov, CIFS_DYNAMIC_BUFFER,
+						srch_inf);
+		if (rc) {
+			kfree(combined_buf);
+			trace_smb3_query_dir_err(xid, fid->persistent_fid, tcon->tid,
+						 tcon->ses->Suid, 0, 0, rc);
+			goto qdf_free;
+		}
+		/* Ownership of combined_buf transferred to srch_inf->ntwrk_buf_start */
+		srch_inf->endOfSearch = false;
+		cifs_dbg(FYI, "query_dir_first: combined QD1 and QD2, %d entries\n",
+			 srch_inf->entries_in_buffer);
+	} else {
+		/* No data in QD2, just parse QD1 */
+		rc = smb2_parse_query_directory(tcon, &rsp_iov[1], resp_buftype[1],
+						srch_inf);
+		if (rc) {
+			trace_smb3_query_dir_err(xid, fid->persistent_fid, tcon->tid,
+						 tcon->ses->Suid, 0, 0, rc);
+			goto qdf_free;
+		}
+		resp_buftype[1] = CIFS_NO_BUFFER;
+
+		/* Check if QD2 indicates end of directory */
+		if (qd2_rsp && qd2_rsp->hdr.Status == STATUS_NO_MORE_FILES) {
+			srch_inf->endOfSearch = true;
+			cifs_dbg(FYI, "query_dir_first: small directory, all entries read\n");
+		}
 	}
-	resp_buftype[1] = CIFS_NO_BUFFER;
 
 	trace_smb3_query_dir_done(xid, fid->persistent_fid, tcon->tid,
 			tcon->ses->Suid, 0, srch_inf->entries_in_buffer);
@@ -2573,8 +2689,10 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
 	kfree(utf16_path);
 	SMB2_open_free(&rqst[0]);
 	SMB2_query_directory_free(&rqst[1]);
+	SMB2_query_directory_free(&rqst[2]);
 	free_rsp_buf(resp_buftype[0], rsp_iov[0].iov_base);
 	free_rsp_buf(resp_buftype[1], rsp_iov[1].iov_base);
+	free_rsp_buf(resp_buftype[2], rsp_iov[2].iov_base);
 
 	if (is_replayable_error(rc) &&
 	    smb2_should_replay(tcon, &retries, &cur_sleep))
diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
index 49dca84b169e6..2d55246d2851b 100644
--- a/fs/smb/client/smb2pdu.c
+++ b/fs/smb/client/smb2pdu.c
@@ -5504,18 +5504,27 @@ int SMB2_query_directory_init(const unsigned int xid,
 			      struct TCP_Server_Info *server,
 			      struct smb_rqst *rqst,
 			      u64 persistent_fid, u64 volatile_fid,
-			      int index, int info_level)
+			      int index, int info_level,
+			      unsigned int output_size)
 {
 	struct smb2_query_directory_req *req;
 	unsigned char *bufptr;
 	__le16 asteriks = cpu_to_le16('*');
-	unsigned int output_size = CIFSMaxBufSize -
-		MAX_SMB2_CREATE_RESPONSE_SIZE -
-		MAX_SMB2_CLOSE_RESPONSE_SIZE;
 	unsigned int total_len;
 	struct kvec *iov = rqst->rq_iov;
 	int len, rc;
 
+	/*
+	 * Use provided output_size, or default to CIFSMaxBufSize calculation.
+	 * The default is for standalone QueryDir (smb2_query_dir_next).
+	 * For compounds, the caller should pass explicit output_size.
+	 */
+	if (output_size == 0) {
+		output_size = CIFSMaxBufSize -
+			MAX_SMB2_CREATE_RESPONSE_SIZE -
+			MAX_SMB2_CLOSE_RESPONSE_SIZE;
+	}
+
 	rc = smb2_plain_req_init(SMB2_QUERY_DIRECTORY, tcon, server,
 				 (void **) &req, &total_len);
 	if (rc)
@@ -5697,7 +5706,7 @@ SMB2_query_directory(const unsigned int xid, struct cifs_tcon *tcon,
 	rc = SMB2_query_directory_init(xid, tcon, server,
 				       &rqst, persistent_fid,
 				       volatile_fid, index,
-				       srch_inf->info_level);
+				       srch_inf->info_level, 0);
 	if (rc)
 		goto qdir_exit;
 
diff --git a/fs/smb/client/smb2pdu.h b/fs/smb/client/smb2pdu.h
index 30d70097fe2fa..7b7a864520c68 100644
--- a/fs/smb/client/smb2pdu.h
+++ b/fs/smb/client/smb2pdu.h
@@ -129,6 +129,17 @@ struct share_redirect_error_context_rsp {
  */
 #define MAX_SMB2_CREATE_RESPONSE_SIZE 880
 
+/* Size of the minimal QueryDir response for checking if more data exists */
+#define SMB2_QD2_RESPONSE_SIZE 4096
+
+/*
+ * Output buffer size for first QueryDir in Create+QD1+QD2 compound.
+ * Accounts for shared buffer space needed for all three responses.
+ */
+#define SMB2_QD1_OUTPUT_SIZE(bufsize) \
+	((bufsize) - MAX_SMB2_CREATE_RESPONSE_SIZE - \
+	 sizeof(struct smb2_hdr) - SMB2_QD2_RESPONSE_SIZE)
+
 #define SMB2_LEASE_READ_CACHING_HE	0x01
 #define SMB2_LEASE_HANDLE_CACHING_HE	0x02
 #define SMB2_LEASE_WRITE_CACHING_HE	0x04
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index 230bb1e9f4e19..9de7d2fe8d466 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -194,7 +194,8 @@ int SMB2_query_directory(const unsigned int xid, struct cifs_tcon *tcon,
 int SMB2_query_directory_init(const unsigned int xid, struct cifs_tcon *tcon,
 			      struct TCP_Server_Info *server,
 			      struct smb_rqst *rqst, u64 persistent_fid,
-			      u64 volatile_fid, int index, int info_level);
+			      u64 volatile_fid, int index, int info_level,
+			      unsigned int output_size);
 void SMB2_query_directory_free(struct smb_rqst *rqst);
 int SMB2_set_eof(const unsigned int xid, struct cifs_tcon *tcon,
 		 u64 persistent_fid, u64 volatile_fid, u32 pid,
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 6/7] cifs: optimize readdir for larger directories
  2026-04-14 13:59 [PATCH 1/7] cifs: change_conf needs to be called for session setup nspmangalore
                   ` (3 preceding siblings ...)
  2026-04-14 13:59 ` [PATCH 5/7] cifs: optimize readdir for small directories nspmangalore
@ 2026-04-14 13:59 ` nspmangalore
  2026-04-15 23:08   ` Steve French
  2026-04-14 13:59 ` [PATCH 7/7] cifs: reorganize cached dir helpers nspmangalore
  5 siblings, 1 reply; 9+ messages in thread
From: nspmangalore @ 2026-04-14 13:59 UTC (permalink / raw)
  To: linux-cifs, smfrench, pc, bharathsm, dhowells, henrique.carvalho,
	ematsumiya
  Cc: Shyam Prasad N

From: Shyam Prasad N <sprasad@microsoft.com>

Today QueryDirectory uses the compound_send_recv infrastructure
which is limited to 16KB in size. As a result, readdir of large
directories generally take several round trips.

With this change, if the readdir needs a QueryDir after the first
round-trip (meaning that there are more dirents to read), then the
following QueryDirs will now switch to using larger buffers with
MTU credits.

Till now, the only command type that used this flow was SMB2_READ.
In case of encrypted response, it becomes challenging to decide if
the response is for SMB2_READ or SMB2_READDIR. This change reuses
receive_encrypted_read and after decrypting the response decides
the handling function based on the command in the resp header. That
way, care has been taken to ensure that the read code path
modifications on account of this change are kept to a minimum.

Cc: David Howells <dhowells@redhat.com>
Signed-off-by: Shyam Prasad N <sprasad@microsoft.com>
---
 fs/smb/client/cifsglob.h  |  23 +-
 fs/smb/client/cifsproto.h |   3 +
 fs/smb/client/readdir.c   |   2 +-
 fs/smb/client/smb1ops.c   |   4 +-
 fs/smb/client/smb2misc.c  |   7 +-
 fs/smb/client/smb2ops.c   | 452 ++++++++++++++++++++++++++++++++++++--
 fs/smb/client/smb2pdu.c   | 182 ++++++++++++++-
 fs/smb/client/smb2pdu.h   |   3 +
 fs/smb/client/smb2proto.h |   3 +
 fs/smb/client/trace.h     |   1 +
 fs/smb/client/transport.c | 184 ++++++++++++++++
 11 files changed, 829 insertions(+), 35 deletions(-)

diff --git a/fs/smb/client/cifsglob.h b/fs/smb/client/cifsglob.h
index 8d089ba08e3e5..45de35ecc0bd2 100644
--- a/fs/smb/client/cifsglob.h
+++ b/fs/smb/client/cifsglob.h
@@ -504,7 +504,7 @@ struct smb_version_operations {
 			       struct cifs_search_info *);
 	/* continue readdir */
 	int (*query_dir_next)(const unsigned int, struct cifs_tcon *,
-			      struct cifs_fid *,
+			      struct cifs_sb_info *, struct cifs_fid *,
 			      __u16, struct cifs_search_info *srch_inf);
 	/* close dir */
 	int (*close_dir)(const unsigned int, struct cifs_tcon *,
@@ -1397,6 +1397,27 @@ struct cifs_search_info {
 	bool is_dynamic_buf:1; /* dynamically allocated buffer - can be variable size */
 };
 
+/* Structure for QueryDirectory with multi-credit support */
+struct cifs_query_dir_io {
+	struct cifs_tcon *tcon;
+	struct TCP_Server_Info *server;
+	struct cifs_search_info *srch_inf;
+	unsigned int xid;
+	u64 persistent_fid;
+	u64 volatile_fid;
+	int index;
+	struct kvec combined_iov;	/* Pre-allocated combined header+data buffer;
+					 * iov_len holds total capacity until the receive
+					 * handler fills it in, then actual valid length */
+	struct completion done;
+	int result;
+	struct cifs_credits credits;
+	bool replay;
+	unsigned int retries;
+	unsigned int cur_sleep;
+	struct kvec iov[2];		/* For response handling */
+};
+
 #define ACL_NO_MODE	((umode_t)(-1))
 struct cifs_open_parms {
 	struct cifs_tcon *tcon;
diff --git a/fs/smb/client/cifsproto.h b/fs/smb/client/cifsproto.h
index 884bfa1cf0b42..bbbee0ef09443 100644
--- a/fs/smb/client/cifsproto.h
+++ b/fs/smb/client/cifsproto.h
@@ -336,6 +336,9 @@ struct cifs_ses *cifs_get_smb_ses(struct TCP_Server_Info *server,
 int cifs_readv_receive(struct TCP_Server_Info *server,
 		       struct mid_q_entry *mid);
 
+int cifs_query_dir_receive(struct TCP_Server_Info *server,
+			    struct mid_q_entry *mid);
+
 int cifs_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
 			  struct cifs_sb_info *cifs_sb,
 			  const unsigned char *path, char *pbuf,
diff --git a/fs/smb/client/readdir.c b/fs/smb/client/readdir.c
index b50efd9b9e1d2..8a444f97e0ae9 100644
--- a/fs/smb/client/readdir.c
+++ b/fs/smb/client/readdir.c
@@ -760,7 +760,7 @@ find_cifs_entry(const unsigned int xid, struct cifs_tcon *tcon, loff_t pos,
 	while ((index_to_find >= cfile->srch_inf.index_of_last_entry) &&
 	       (rc == 0) && !cfile->srch_inf.endOfSearch) {
 		cifs_dbg(FYI, "calling findnext2\n");
-		rc = server->ops->query_dir_next(xid, tcon, &cfile->fid,
+		rc = server->ops->query_dir_next(xid, tcon, cifs_sb, &cfile->fid,
 						 search_flags,
 						 &cfile->srch_inf);
 		if (rc)
diff --git a/fs/smb/client/smb1ops.c b/fs/smb/client/smb1ops.c
index 9694117050a6c..860a9b23a2f8d 100644
--- a/fs/smb/client/smb1ops.c
+++ b/fs/smb/client/smb1ops.c
@@ -1135,8 +1135,8 @@ cifs_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
 
 static int
 cifs_query_dir_next(const unsigned int xid, struct cifs_tcon *tcon,
-		    struct cifs_fid *fid, __u16 search_flags,
-		    struct cifs_search_info *srch_inf)
+		    struct cifs_sb_info *cifs_sb, struct cifs_fid *fid,
+		    __u16 search_flags, struct cifs_search_info *srch_inf)
 {
 	return CIFSFindNext(xid, tcon, fid->netfid, search_flags, srch_inf);
 }
diff --git a/fs/smb/client/smb2misc.c b/fs/smb/client/smb2misc.c
index 973fce3c959c4..b7b6ecd5fdaee 100644
--- a/fs/smb/client/smb2misc.c
+++ b/fs/smb/client/smb2misc.c
@@ -12,6 +12,7 @@
 #include "cifsglob.h"
 #include "cifsproto.h"
 #include "smb2proto.h"
+#include "smb2pdu.h"
 #include "cifs_debug.h"
 #include "cifs_unicode.h"
 #include "../common/smb2status.h"
@@ -316,7 +317,7 @@ char *
 smb2_get_data_area_len(int *off, int *len, struct smb2_hdr *shdr)
 {
 	const int max_off = 4096;
-	const int max_len = 128 * 1024;
+	int max_len = 128 * 1024;
 
 	*off = 0;
 	*len = 0;
@@ -367,6 +368,10 @@ smb2_get_data_area_len(int *off, int *len, struct smb2_hdr *shdr)
 		  ((struct smb2_query_directory_rsp *)shdr)->OutputBufferOffset);
 		*len = le32_to_cpu(
 		  ((struct smb2_query_directory_rsp *)shdr)->OutputBufferLength);
+		/* Allow larger buffers for query directory (up to 2MB).
+		 * The actual data is handled separately in cifs_query_dir_receive().
+		 */
+		max_len = SMB2_MAX_QD_DATABUF_SIZE;
 		break;
 	case SMB2_IOCTL:
 		*off = le32_to_cpu(
diff --git a/fs/smb/client/smb2ops.c b/fs/smb/client/smb2ops.c
index 5c6fe25204d26..ecbd717f5a5ea 100644
--- a/fs/smb/client/smb2ops.c
+++ b/fs/smb/client/smb2ops.c
@@ -2703,11 +2703,130 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
 
 static int
 smb2_query_dir_next(const unsigned int xid, struct cifs_tcon *tcon,
-		    struct cifs_fid *fid, __u16 search_flags,
-		    struct cifs_search_info *srch_inf)
+		    struct cifs_sb_info *cifs_sb, struct cifs_fid *fid,
+		    __u16 search_flags, struct cifs_search_info *srch_inf)
 {
-	return SMB2_query_directory(xid, tcon, fid->persistent_fid,
-				    fid->volatile_fid, 0, srch_inf);
+	struct cifs_query_dir_io qd_io;
+	struct TCP_Server_Info *server;
+	struct cifs_ses *ses = tcon->ses;
+	size_t buf_size;
+	int rc;
+	int resp_buftype = CIFS_DYNAMIC_BUFFER;
+
+	/* Pick server and determine buffer size based on negotiated rsize */
+	server = cifs_pick_channel(ses);
+	if (!server)
+		return smb_EIO(smb_eio_trace_null_pointers);
+
+	/* Negotiate rsize if not already set */
+	if (cifs_sb->ctx->rsize == 0)
+		cifs_negotiate_rsize(server, cifs_sb->ctx, tcon);
+
+	/* Use negotiated rsize for buffer size, with reasonable limits */
+	buf_size = cifs_sb->ctx->rsize;
+
+	cifs_dbg(FYI, "%s: using buffer size %zu (rsize=%u, encrypted=%d)\n",
+		 __func__, buf_size, cifs_sb->ctx->rsize, smb3_encryption_required(tcon));
+
+	/* Initialize qd_io structure */
+	memset(&qd_io, 0, sizeof(qd_io));
+	qd_io.tcon = tcon;
+	qd_io.server = server;
+	qd_io.srch_inf = srch_inf;
+	qd_io.xid = xid;
+	qd_io.persistent_fid = fid->persistent_fid;
+	qd_io.volatile_fid = fid->volatile_fid;
+	qd_io.index = 0;
+	qd_io.result = 0;
+	qd_io.replay = false;
+	qd_io.retries = 0;
+	qd_io.cur_sleep = 0;
+
+	/* Allocate credits for the buffer size */
+	rc = server->ops->wait_mtu_credits(server, buf_size, &buf_size,
+					   &qd_io.credits);
+	if (rc) {
+		cifs_dbg(VFS, "%s: failed to get credits: %d\n", __func__, rc);
+		return rc;
+	}
+
+	cifs_dbg(FYI, "%s: allocated %u credits for %zu bytes\n",
+		 __func__, qd_io.credits.value, buf_size);
+
+	/* Send query directory with large buffer and wait for completion */
+	rc = SMB2_query_directory_large(&qd_io, buf_size);
+	if (rc) {
+		if (rc == -ENODATA) {
+			const struct smb2_hdr *hdr = NULL;
+
+			if (qd_io.combined_iov.iov_base)
+				hdr = (const struct smb2_hdr *)qd_io.combined_iov.iov_base;
+			else if (qd_io.iov[0].iov_base)
+				hdr = (const struct smb2_hdr *)qd_io.iov[0].iov_base;
+
+			/*
+			 * ENODATA from QUERY_DIRECTORY generally means enumeration reached
+			 * the end. Treat it as end-of-search even if the header buffer is
+			 * unavailable in this async path.
+			 */
+			if (!hdr) {
+				cifs_dbg(FYI, "%s: ENODATA but hdr is NULL\n", __func__);
+			} else {
+				cifs_dbg(FYI, "%s: ENODATA with hdr->Status=0x%x (STATUS_NO_MORE_FILES=0x%x)\n",
+					 __func__, le32_to_cpu(hdr->Status), le32_to_cpu(STATUS_NO_MORE_FILES));
+			}
+
+			if (hdr && hdr->Status == STATUS_NO_MORE_FILES) {
+				trace_smb3_query_dir_done(xid, fid->persistent_fid,
+					tcon->tid, tcon->ses->Suid, 0, 0);
+				srch_inf->endOfSearch = true;
+				rc = 0;
+			} else {
+				cifs_dbg(FYI, "%s: ENODATA but Status mismatch - not treating as end-of-search\n", __func__);
+				trace_smb3_query_dir_err(xid, fid->persistent_fid,
+					tcon->tid, tcon->ses->Suid, 0, 0, rc);
+			}
+		} else {
+			trace_smb3_query_dir_err(xid, fid->persistent_fid,
+				tcon->tid, tcon->ses->Suid, 0, 0, rc);
+		}
+		goto qdir_next_exit;
+	}
+
+	/* Parse the response using the combined buffer built in receive handler */
+	if (qd_io.combined_iov.iov_len > 0) {
+		rc = smb2_parse_query_directory(tcon, &qd_io.combined_iov, resp_buftype,
+						srch_inf);
+		if (rc) {
+			trace_smb3_query_dir_err(xid, fid->persistent_fid,
+				tcon->tid, tcon->ses->Suid, 0, 0, rc);
+			kfree(qd_io.combined_iov.iov_base);
+			qd_io.combined_iov.iov_base = NULL;
+			goto qdir_next_exit;
+		}
+
+		/* combined_iov.iov_base ownership transferred to srch_inf->ntwrk_buf_start */
+		qd_io.combined_iov.iov_base = NULL;
+
+		trace_smb3_query_dir_done(xid, fid->persistent_fid,
+			tcon->tid, tcon->ses->Suid, 0,
+			srch_inf->entries_in_buffer);
+	}
+
+qdir_next_exit:
+	/* Free the data buffer if not transferred to srch_inf */
+	kfree(qd_io.combined_iov.iov_base);
+
+	/* Return credits if we still have them (they should have been cleared in callback) */
+	if (qd_io.credits.value != 0) {
+		trace_smb3_rw_credits(0, 0, 0,
+				      server->credits, server->in_flight,
+				      qd_io.credits.value,
+				      cifs_trace_rw_credits_query_dir_done);
+		add_credits(server, &qd_io.credits, 0);
+	}
+
+	return rc;
 }
 
 static int
@@ -4824,6 +4943,247 @@ cifs_copy_folioq_to_iter(struct folio_queue *folioq, size_t data_size,
 	return 0;
 }
 
+static int
+cifs_copy_folioq_to_buf(struct folio_queue *folioq, size_t total_size,
+			size_t skip, char *buf, size_t buf_len)
+{
+	size_t copied = 0;
+
+	if (buf_len > total_size - skip)
+		buf_len = total_size - skip;
+
+	for (; folioq; folioq = folioq->next) {
+		for (int s = 0; s < folioq_count(folioq); s++) {
+			struct folio *folio = folioq_folio(folioq, s);
+			size_t fsize = folio_size(folio);
+			size_t len = umin(fsize - skip, buf_len);
+
+			if (len == 0)
+				break;
+
+			memcpy_from_folio(buf + copied, folio, skip, len);
+			copied += len;
+			buf_len -= len;
+			skip = 0;
+
+			if (buf_len == 0)
+				return 0;
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * Handle encrypted QueryDirectory response data.
+ * Called only for encrypted responses where mid->decrypted == true.
+ * For unencrypted responses, cifs_query_dir_receive handles everything.
+ *
+ * This is written in such a way that handle_read_data does not need modification.
+ *
+ * buf: contains read_rsp_size bytes of decrypted response
+ * buffer: contains (total_len - read_rsp_size) bytes of decrypted data
+ *
+ * Since sizeof(struct smb2_query_directory_rsp) < read_rsp_size,
+ * the response header is always fully in buf. Data may be split between
+ * buf and buffer depending on data_offset.
+ */
+static int
+handle_query_dir_data(struct TCP_Server_Info *server, struct mid_q_entry *mid,
+		      char *buf, unsigned int buf_len, struct folio_queue *buffer,
+		      unsigned int buffer_len, bool is_offloaded)
+{
+	struct cifs_query_dir_io *qd_io = mid->callback_data;
+	struct smb2_query_directory_rsp *rsp;
+	struct smb2_hdr *shdr = (struct smb2_hdr *)buf;
+	unsigned int data_offset, data_len;
+	unsigned int hdr_len;
+
+	cifs_dbg(FYI, "%s: processing encrypted QueryDirectory response\n", __func__);
+
+	if (shdr->Command != SMB2_QUERY_DIRECTORY) {
+		cifs_server_dbg(VFS, "only QueryDirectory responses are supported\n");
+		return -EOPNOTSUPP;
+	}
+
+	if (server->ops->is_session_expired &&
+	    server->ops->is_session_expired(buf)) {
+		if (!is_offloaded)
+			cifs_reconnect(server, true);
+		return -1;
+	}
+
+	if (server->ops->is_status_pending &&
+			server->ops->is_status_pending(buf, server))
+		return -1;
+
+	rsp = (struct smb2_query_directory_rsp *)buf;
+	hdr_len = min_t(unsigned int, buf_len,
+			 sizeof(struct smb2_query_directory_rsp));
+
+	/* Map error code first */
+	qd_io->result = server->ops->map_error(buf, false);
+
+	/* Get data_offset early to set up iov properly */
+	data_offset = le16_to_cpu(rsp->OutputBufferOffset);
+	data_len = le32_to_cpu(rsp->OutputBufferLength);
+
+	/* Set up first iov to point to header portion (needed for credits/signature) */
+	qd_io->iov[0].iov_base = buf;
+	qd_io->iov[0].iov_len = qd_io->result ? hdr_len : data_offset;
+
+	if (qd_io->result != 0) {
+		cifs_dbg(FYI, "%s: server returned error %d (Status=0x%x)\n",
+			 __func__, qd_io->result, le32_to_cpu(rsp->hdr.Status));
+
+		/* Copy header to persistent combined_iov buffer so status
+		 * remains accessible after receive handler returns. buf is temporary
+		 * and will be freed/reused, so we can't leave iov[0] pointing to it. */
+		if (qd_io->combined_iov.iov_base && hdr_len > 0 &&
+		    hdr_len <= qd_io->combined_iov.iov_len) {
+			memcpy(qd_io->combined_iov.iov_base, buf, hdr_len);
+			qd_io->iov[0].iov_base = qd_io->combined_iov.iov_base;
+			qd_io->iov[0].iov_len = hdr_len;
+			cifs_dbg(FYI, "%s: copied error response header to combined_iov\n", __func__);
+		}
+
+		/* Normal error on query_directory response - response received successfully,
+		 * but the command failed. Store error in qd_io->result for callback. */
+		if (is_offloaded)
+			mid->mid_state = MID_RESPONSE_RECEIVED;
+		else
+			dequeue_mid(server, mid, false);
+		return 0;
+	}
+
+	/* Success - parse the response data */
+	cifs_dbg(FYI, "%s: data_offset=%u data_len=%u buf_len=%u buffer_len=%u\n",
+		 __func__, data_offset, data_len, buf_len, buffer_len);
+
+	/* Validate data_offset */
+	if (data_offset < sizeof(struct smb2_query_directory_rsp)) {
+		cifs_dbg(FYI, "%s: data offset (%u) inside response header\n",
+			 __func__, data_offset);
+		data_offset = sizeof(struct smb2_query_directory_rsp);
+	} else if (data_offset > MAX_CIFS_SMALL_BUFFER_SIZE) {
+		cifs_dbg(VFS, "%s: data offset (%u) beyond end of smallbuf\n",
+			 __func__, data_offset);
+		qd_io->result = -EIO;
+		dequeue_mid(server, mid, qd_io->result);
+		return qd_io->result;
+	}
+
+	/* Validate data_offset is within buf_len + buffer_len */
+	if (data_offset > buf_len + buffer_len) {
+		cifs_dbg(VFS, "%s: data offset (%u) beyond response length (%u)\n",
+			 __func__, data_offset, buf_len + buffer_len);
+		qd_io->result = -EIO;
+		dequeue_mid(server, mid, qd_io->result);
+		return qd_io->result;
+	}
+
+	/* Validate response fits in pre-allocated combined buffer */
+	if ((size_t)data_offset + data_len > qd_io->combined_iov.iov_len) {
+		cifs_dbg(VFS, "%s: response (%u + %u) exceeds buffer capacity (%zu)\n",
+			 __func__, data_offset, data_len, qd_io->combined_iov.iov_len);
+		qd_io->result = -EIO;
+		dequeue_mid(server, mid, qd_io->result);
+		return qd_io->result;
+	}
+
+	/* Copy the prefix present in buf into combined_iov, preserving wire layout */
+	memcpy(qd_io->combined_iov.iov_base, buf, min(data_offset, buf_len));
+
+	if (data_offset < buf_len) {
+		/* Data starts in buf, may continue into buffer */
+		unsigned int data_in_buf = buf_len - data_offset;
+		unsigned int data_in_buffer;
+
+		if (data_len <= data_in_buf) {
+			/* All data is in buf */
+			memcpy(qd_io->combined_iov.iov_base + data_offset,
+			       buf + data_offset, data_len);
+		} else {
+			/* Copy from buf first */
+			memcpy(qd_io->combined_iov.iov_base + data_offset,
+			       buf + data_offset, data_in_buf);
+
+			/* Copy remainder from buffer at offset 0 */
+			data_in_buffer = data_len - data_in_buf;
+			if (data_in_buffer > buffer_len) {
+				cifs_dbg(VFS, "%s: data_in_buffer (%u) > buffer_len (%u)\n",
+					 __func__, data_in_buffer, buffer_len);
+				qd_io->result = -EIO;
+				dequeue_mid(server, mid, qd_io->result);
+				return qd_io->result;
+			}
+
+			qd_io->result = cifs_copy_folioq_to_buf(buffer, buffer_len, 0,
+								qd_io->combined_iov.iov_base +
+								data_offset + data_in_buf,
+								data_in_buffer);
+			if (qd_io->result != 0) {
+				cifs_dbg(VFS, "%s: failed to copy from folio_queue: %d\n",
+					 __func__, qd_io->result);
+				dequeue_mid(server, mid, qd_io->result);
+				return qd_io->result;
+			}
+		}
+	} else {
+		/* Padding and data are in buffer starting at offset 0 */
+		unsigned int bytes_in_buffer = data_offset - buf_len + data_len;
+
+		if (bytes_in_buffer > buffer_len) {
+			cifs_dbg(VFS, "%s: data beyond buffer: prefix+len=%u buffer_len=%u\n",
+				 __func__, bytes_in_buffer, buffer_len);
+			qd_io->result = -EIO;
+			dequeue_mid(server, mid, qd_io->result);
+			return qd_io->result;
+		}
+
+		qd_io->result = cifs_copy_folioq_to_buf(buffer, buffer_len, 0,
+							qd_io->combined_iov.iov_base + buf_len,
+							bytes_in_buffer);
+		if (qd_io->result != 0) {
+			cifs_dbg(VFS, "%s: failed to copy from folio_queue: %d\n",
+				 __func__, qd_io->result);
+			dequeue_mid(server, mid, qd_io->result);
+			return qd_io->result;
+		}
+	}
+
+	/* Set up iov[1] pointing into combined buffer, finalize valid length */
+	qd_io->iov[1].iov_base = qd_io->combined_iov.iov_base + data_offset;
+	qd_io->iov[1].iov_len = data_len;
+	qd_io->combined_iov.iov_len = data_offset + data_len;
+
+	dequeue_mid(server, mid, false);
+	return 0;
+}
+
+/*
+ * Handle callback for async QueryDirectory with multi-credit support.
+ * For encrypted responses, extracts decrypted data.
+ * For unencrypted responses, cifs_query_dir_receive already processed everything.
+ */
+int
+smb2_query_dir_handle_data(struct TCP_Server_Info *server, struct mid_q_entry *mid)
+{
+	char *buf = server->large_buf ? server->bigbuf : server->smallbuf;
+
+	/* For unencrypted responses, data already processed in cifs_query_dir_receive */
+	if (!mid->decrypted)
+		return 0;
+
+	/*
+	 * For small encrypted responses (< CIFSMaxBufSize), all data is in buf.
+	 * For large encrypted responses, this callback is not used - instead,
+	 * receive_encrypted_read/smb2_decrypt_offload call handle_query_dir_data directly.
+	 */
+	return handle_query_dir_data(server, mid, buf, server->pdu_size,
+				      NULL, 0, false);
+}
+
 static int
 handle_read_data(struct TCP_Server_Info *server, struct mid_q_entry *mid,
 		 char *buf, unsigned int buf_len, struct folio_queue *buffer,
@@ -4987,25 +5347,46 @@ static void smb2_decrypt_offload(struct work_struct *work)
 	int rc;
 	struct mid_q_entry *mid;
 	struct iov_iter iter;
+	struct smb2_hdr *shdr;
+	unsigned int read_rsp_size = dw->server->vals->read_rsp_size;
 
+	/* decrypt read_rsp_size in buf + remainder in folio_queue */
 	iov_iter_folio_queue(&iter, ITER_DEST, dw->buffer, 0, 0, dw->len);
-	rc = decrypt_raw_data(dw->server, dw->buf, dw->server->vals->read_rsp_size,
-			      &iter, true);
+	rc = decrypt_raw_data(dw->server, dw->buf, read_rsp_size, &iter, true);
 	if (rc) {
 		cifs_dbg(VFS, "error decrypting rc=%d\n", rc);
 		goto free_pages;
 	}
 
 	dw->server->lstrp = jiffies;
+
+	shdr = (struct smb2_hdr *)(dw->buf + sizeof(struct smb2_transform_hdr));
+
+	/*
+	 * buf now contains read_rsp_size bytes after transform_hdr.
+	 * folio_queue contains (total_len - read_rsp_size) bytes.
+	 * The handle functions will determine where data actually starts based on data_offset.
+	 */
+
 	mid = smb2_find_dequeue_mid(dw->server, dw->buf);
 	if (mid == NULL)
 		cifs_dbg(FYI, "mid not found\n");
 	else {
 		mid->decrypted = true;
-		rc = handle_read_data(dw->server, mid, dw->buf,
-				      dw->server->vals->read_rsp_size,
-				      dw->buffer, dw->len,
-				      true);
+
+		/* Handle based on command type */
+		if (le16_to_cpu(shdr->Command) == SMB2_READ) {
+			rc = handle_read_data(dw->server, mid, dw->buf, read_rsp_size,
+					      dw->buffer, dw->len, true);
+		} else if (le16_to_cpu(shdr->Command) == SMB2_QUERY_DIRECTORY) {
+			rc = handle_query_dir_data(dw->server, mid, dw->buf, read_rsp_size,
+						   dw->buffer, dw->len, true);
+		} else {
+			cifs_dbg(VFS, "Unexpected command %u in decrypt offload\n",
+				 le16_to_cpu(shdr->Command));
+			rc = -EOPNOTSUPP;
+		}
+
 		if (rc >= 0) {
 #ifdef CONFIG_CIFS_STATS2
 			mid->when_received = jiffies;
@@ -5049,9 +5430,10 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
 {
 	char *buf = server->smallbuf;
 	struct smb2_transform_hdr *tr_hdr = (struct smb2_transform_hdr *)buf;
+	struct smb2_hdr *shdr;
 	struct iov_iter iter;
-	unsigned int len;
-	unsigned int buflen = server->pdu_size;
+	unsigned int len, total_len, buflen = server->pdu_size;
+	unsigned int read_rsp_size = server->vals->read_rsp_size;
 	int rc;
 	struct smb2_decrypt_work *dw;
 
@@ -5062,7 +5444,10 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
 	dw->server = server;
 
 	*num_mids = 1;
-	len = min_t(unsigned int, buflen, server->vals->read_rsp_size +
+	total_len = le32_to_cpu(tr_hdr->OriginalMessageSize);
+
+	/* Read transform_hdr + read_rsp_size into buf */
+	len = min_t(unsigned int, buflen, read_rsp_size +
 		sizeof(struct smb2_transform_hdr)) - HEADER_SIZE(server) + 1;
 
 	rc = cifs_read_from_socket(server, buf + HEADER_SIZE(server) - 1, len);
@@ -5070,9 +5455,8 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
 		goto free_dw;
 	server->total_read += rc;
 
-	len = le32_to_cpu(tr_hdr->OriginalMessageSize) -
-		server->vals->read_rsp_size;
-	dw->len = len;
+	/* Read remaining data into folio_queue */
+	dw->len = total_len - read_rsp_size;
 	len = round_up(dw->len, PAGE_SIZE);
 
 	size_t cur_size = 0;
@@ -5101,7 +5485,7 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
 		goto free_pages;
 
 	/*
-	 * For large reads, offload to different thread for better performance,
+	 * For large responses, offload to different thread for better performance,
 	 * use more cores decrypting which can be expensive
 	 */
 
@@ -5115,20 +5499,41 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
 		return -1;
 	}
 
-	rc = decrypt_raw_data(server, buf, server->vals->read_rsp_size,
-			      &iter, false);
+	/* Decrypt: read_rsp_size in buf + remainder in folio_queue */
+	rc = decrypt_raw_data(server, buf, read_rsp_size, &iter, false);
 	if (rc)
 		goto free_pages;
 
+	shdr = (struct smb2_hdr *) buf;
+
+	/*
+	 * buf now contains the complete response header (read_rsp_size bytes).
+	 * folio_queue contains (total_len - read_rsp_size) bytes.
+	 * The handle functions will determine where data actually starts based on data_offset.
+	 */
+
 	*mid = smb2_find_mid(server, buf);
 	if (*mid == NULL) {
 		cifs_dbg(FYI, "mid not found\n");
 	} else {
-		cifs_dbg(FYI, "mid found\n");
+		cifs_dbg(FYI, "mid found, command=%u\n", le16_to_cpu(shdr->Command));
 		(*mid)->decrypted = true;
-		rc = handle_read_data(server, *mid, buf,
-				      server->vals->read_rsp_size,
-				      dw->buffer, dw->len, false);
+
+		/* Handle based on command type */
+		if (le16_to_cpu(shdr->Command) == SMB2_READ) {
+			rc = handle_read_data(server, *mid, buf, read_rsp_size,
+					      dw->buffer, dw->len, false);
+		} else if (le16_to_cpu(shdr->Command) == SMB2_QUERY_DIRECTORY) {
+			rc = handle_query_dir_data(server, *mid, buf, read_rsp_size,
+						   dw->buffer, dw->len, false);
+		} else {
+			/* For now, other commands not supported in large encrypted path */
+			cifs_server_dbg(VFS,
+					"Large encrypted responses only supported for SMB2_READ and SMB2_QUERY_DIRECTORY (got %u)\n",
+					le16_to_cpu(shdr->Command));
+			rc = -EOPNOTSUPP;
+		}
+
 		if (rc >= 0) {
 			if (server->ops->is_network_name_deleted) {
 				server->ops->is_network_name_deleted(buf,
@@ -5269,7 +5674,6 @@ smb3_receive_transform(struct TCP_Server_Info *server,
 		return -ECONNABORTED;
 	}
 
-	/* TODO: add support for compounds containing READ. */
 	if (pdu_length > CIFSMaxBufSize + MAX_HEADER_SIZE(server)) {
 		return receive_encrypted_read(server, &mids[0], num_mids);
 	}
diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
index 2d55246d2851b..b59d99d5e193a 100644
--- a/fs/smb/client/smb2pdu.c
+++ b/fs/smb/client/smb2pdu.c
@@ -5496,6 +5496,182 @@ num_entries(int infotype, char *bufstart, char *end_of_buf, char **lastentry,
 	return entrycount;
 }
 
+/*
+ * Callback for async QueryDirectory with multi-credit support
+ */
+static void
+smb2_query_dir_callback(struct TCP_Server_Info *server, struct mid_q_entry *mid)
+{
+	struct cifs_query_dir_io *qd_io = mid->callback_data;
+	struct cifs_tcon *tcon = qd_io->tcon;
+	struct smb2_hdr *shdr = (struct smb2_hdr *)qd_io->iov[0].iov_base;
+	struct cifs_credits credits = {
+		.value = 0,
+		.instance = 0,
+	};
+
+	WARN_ONCE(qd_io->server != server,
+		  "qd_io server %p != mid server %p",
+		  qd_io->server, server);
+
+	cifs_dbg(FYI, "%s: mid=%llu state=%d result=%d\n",
+		 __func__, mid->mid, mid->mid_state, qd_io->result);
+
+	switch (mid->mid_state) {
+	case MID_RESPONSE_RECEIVED:
+		credits.value = le16_to_cpu(shdr->CreditRequest);
+		credits.instance = server->reconnect_instance;
+		/* result already set, check signature if needed */
+		if (server->sign && !mid->decrypted) {
+			int rc;
+			struct smb_rqst rqst = { .rq_iov = &qd_io->iov[0], .rq_nvec = 1 };
+
+			rc = smb2_verify_signature(&rqst, server);
+			if (rc) {
+				cifs_tcon_dbg(VFS, "QueryDir signature verification returned error = %d\n",
+					      rc);
+				qd_io->result = rc;
+			}
+		}
+		break;
+	case MID_REQUEST_SUBMITTED:
+	case MID_RETRY_NEEDED:
+		qd_io->result = -EAGAIN;
+		break;
+	case MID_RESPONSE_MALFORMED:
+		credits.value = le16_to_cpu(shdr->CreditRequest);
+		credits.instance = server->reconnect_instance;
+		qd_io->result = smb_EIO(smb_eio_trace_read_rsp_malformed);
+		break;
+	default:
+		qd_io->result = smb_EIO1(smb_eio_trace_read_mid_state_unknown,
+					 mid->mid_state);
+		break;
+	}
+
+	if (qd_io->result && qd_io->result != -ENODATA)
+		cifs_stats_fail_inc(tcon, SMB2_QUERY_DIRECTORY_HE);
+
+	trace_smb3_rw_credits(0, 0, qd_io->credits.value,
+			      server->credits, server->in_flight,
+			      0, cifs_trace_rw_credits_read_response_clear);
+	qd_io->credits.value = 0;
+	release_mid(server, mid);
+	trace_smb3_rw_credits(0, 0, 0,
+			      server->credits, server->in_flight,
+			      credits.value, cifs_trace_rw_credits_read_response_add);
+	add_credits(server, &credits, 0);
+
+	complete(&qd_io->done);
+}
+
+/*
+ * QueryDirectory with large buffer and multi-credit support.
+ * Uses async infrastructure but waits for completion synchronously.
+ */
+int
+SMB2_query_directory_large(struct cifs_query_dir_io *qd_io, unsigned int buf_size)
+{
+	int rc, flags = 0;
+	char *buf;
+	struct smb2_hdr *shdr;
+	struct smb_rqst rqst = { .rq_iov = &qd_io->iov[0],
+				 .rq_nvec = SMB2_QUERY_DIRECTORY_IOV_SIZE };
+	struct TCP_Server_Info *server = qd_io->server;
+	struct cifs_tcon *tcon = qd_io->tcon;
+	unsigned int total_len;
+	int credit_request;
+
+	cifs_dbg(FYI, "%s: buf_size=%u\n", __func__, buf_size);
+
+	/* Cap buffer size to avoid kmalloc failures for very large allocations.
+	 * SMB2_MAX_QD_DATABUF_SIZE is a safe limit that stays well below typical
+	 * kmalloc constraints while still allowing large directory listings.
+	 */
+	if (buf_size > SMB2_MAX_QD_DATABUF_SIZE)
+		buf_size = SMB2_MAX_QD_DATABUF_SIZE;
+
+	/* Allocate response buffer. Since we'll build a combined header+data buffer,
+	 * we need space for both. We'll request a slightly smaller OutputBufferLength
+	 * from the server to ensure the total response fits.
+	 */
+	qd_io->combined_iov.iov_base = kmalloc(buf_size, GFP_KERNEL);
+	if (!qd_io->combined_iov.iov_base)
+		return -ENOMEM;
+	/* Store total capacity in iov_len; updated to actual data length by receive handler */
+	qd_io->combined_iov.iov_len = buf_size;
+
+	/* Initialize completion */
+	init_completion(&qd_io->done);
+
+	/* Request less data from server to leave room for the response header.
+	 * Use MAX_CIFS_SMALL_BUFFER_SIZE as a safety margin.
+	 */
+	rc = SMB2_query_directory_init(qd_io->xid, tcon, server,
+				       &rqst, qd_io->persistent_fid,
+				       qd_io->volatile_fid, qd_io->index,
+				       qd_io->srch_inf->info_level,
+				       buf_size - MAX_CIFS_SMALL_BUFFER_SIZE);
+	if (rc) {
+		kfree(qd_io->combined_iov.iov_base);
+		qd_io->combined_iov.iov_base = NULL;
+		return rc;
+	}
+
+	if (smb3_encryption_required(tcon))
+		flags |= CIFS_TRANSFORM_REQ;
+
+	buf = rqst.rq_iov[0].iov_base;
+	total_len = rqst.rq_iov[0].iov_len;
+
+	shdr = (struct smb2_hdr *)buf;
+
+	if (qd_io->replay) {
+		/* Back-off before retry */
+		if (qd_io->cur_sleep)
+			msleep(qd_io->cur_sleep);
+		smb2_set_replay(server, &rqst);
+	}
+
+	/* Set credit charge based on buffer size */
+	if (qd_io->credits.value > 0) {
+		shdr->CreditCharge = cpu_to_le16(DIV_ROUND_UP(buf_size,
+						SMB2_MAX_BUFFER_SIZE));
+		credit_request = le16_to_cpu(shdr->CreditCharge) + 8;
+		if (server->credits >= server->max_credits)
+			shdr->CreditRequest = cpu_to_le16(0);
+		else
+			shdr->CreditRequest = cpu_to_le16(
+				min_t(int, server->max_credits -
+						server->credits, credit_request));
+
+		flags |= CIFS_HAS_CREDITS;
+	}
+
+	rc = cifs_call_async(server, &rqst,
+			     cifs_query_dir_receive, smb2_query_dir_callback,
+			     smb2_query_dir_handle_data, qd_io, flags,
+			     &qd_io->credits);
+	if (rc) {
+		cifs_stats_fail_inc(tcon, SMB2_QUERY_DIRECTORY_HE);
+		trace_smb3_query_dir_err(qd_io->xid, qd_io->persistent_fid,
+					 tcon->tid, tcon->ses->Suid,
+					 qd_io->index, 0, rc);
+		kfree(qd_io->combined_iov.iov_base);
+		qd_io->combined_iov.iov_base = NULL;
+	}
+
+	/* Free request buffer immediately after async call */
+	cifs_small_buf_release(buf);
+
+	if (rc)
+		return rc;
+
+	/* Wait for the async operation to complete */
+	wait_for_completion(&qd_io->done);
+	return qd_io->result;
+}
+
 /*
  * Readdir/FindFirst
  */
@@ -5560,12 +5736,6 @@ int SMB2_query_directory_init(const unsigned int xid,
 	req->FileNameOffset =
 		cpu_to_le16(sizeof(struct smb2_query_directory_req));
 	req->FileNameLength = cpu_to_le16(len);
-	/*
-	 * BB could be 30 bytes or so longer if we used SMB2 specific
-	 * buffer lengths, but this is safe and close enough.
-	 */
-	output_size = min_t(unsigned int, output_size, server->maxBuf);
-	output_size = min_t(unsigned int, output_size, 2 << 15);
 	req->OutputBufferLength = cpu_to_le32(output_size);
 
 	iov[0].iov_base = (char *)req;
diff --git a/fs/smb/client/smb2pdu.h b/fs/smb/client/smb2pdu.h
index 7b7a864520c68..843e30c1ecc61 100644
--- a/fs/smb/client/smb2pdu.h
+++ b/fs/smb/client/smb2pdu.h
@@ -132,6 +132,9 @@ struct share_redirect_error_context_rsp {
 /* Size of the minimal QueryDir response for checking if more data exists */
 #define SMB2_QD2_RESPONSE_SIZE 4096
 
+/* max query directory data buffer size */
+#define SMB2_MAX_QD_DATABUF_SIZE (2 * 1024 * 1024)
+
 /*
  * Output buffer size for first QueryDir in Create+QD1+QD2 compound.
  * Accounts for shared buffer space needed for all three responses.
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index 9de7d2fe8d466..9607e8899f7ff 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -46,6 +46,8 @@ __le32 smb2_get_lease_state(struct cifsInodeInfo *cinode, unsigned int oplock);
 bool smb2_is_valid_oplock_break(char *buffer, struct TCP_Server_Info *server);
 int smb3_handle_read_data(struct TCP_Server_Info *server,
 			  struct mid_q_entry *mid);
+int smb2_query_dir_handle_data(struct TCP_Server_Info *server,
+			       struct mid_q_entry *mid);
 struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
 					struct super_block *sb,
 					const unsigned int xid,
@@ -197,6 +199,7 @@ int SMB2_query_directory_init(const unsigned int xid, struct cifs_tcon *tcon,
 			      u64 volatile_fid, int index, int info_level,
 			      unsigned int output_size);
 void SMB2_query_directory_free(struct smb_rqst *rqst);
+int SMB2_query_directory_large(struct cifs_query_dir_io *qd_io, unsigned int buf_size);
 int SMB2_set_eof(const unsigned int xid, struct cifs_tcon *tcon,
 		 u64 persistent_fid, u64 volatile_fid, u32 pid,
 		 loff_t new_eof);
diff --git a/fs/smb/client/trace.h b/fs/smb/client/trace.h
index acfbb63086ea2..54ee1317c5b12 100644
--- a/fs/smb/client/trace.h
+++ b/fs/smb/client/trace.h
@@ -165,6 +165,7 @@
 	EM(cifs_trace_rw_credits_write_prepare,		"wr-prepare ") \
 	EM(cifs_trace_rw_credits_write_response_add,	"wr-resp-add") \
 	EM(cifs_trace_rw_credits_write_response_clear,	"wr-resp-clr") \
+	EM(cifs_trace_rw_credits_query_dir_done,	"qd-done    ") \
 	E_(cifs_trace_rw_credits_zero_in_flight,	"ZERO-IN-FLT")
 
 #define smb3_tcon_ref_traces					      \
diff --git a/fs/smb/client/transport.c b/fs/smb/client/transport.c
index 05f8099047e1a..4457b98b9365d 100644
--- a/fs/smb/client/transport.c
+++ b/fs/smb/client/transport.c
@@ -1154,6 +1154,190 @@ cifs_readv_discard(struct TCP_Server_Info *server, struct mid_q_entry *mid)
 	return  __cifs_readv_discard(server, mid, rdata->result);
 }
 
+static int
+cifs_query_dir_discard(struct TCP_Server_Info *server, struct mid_q_entry *mid)
+{
+	int length;
+	struct cifs_query_dir_io *qd_io = mid->callback_data;
+
+	length = cifs_discard_remaining_data(server);
+	dequeue_mid(server, mid, qd_io->result != 0);
+	mid->resp_buf = server->smallbuf;
+	server->smallbuf = NULL;
+	return length;
+}
+
+/*
+ * Receive handler for async QueryDirectory with multi-credit support
+ */
+int
+cifs_query_dir_receive(struct TCP_Server_Info *server, struct mid_q_entry *mid)
+{
+	int length, len;
+	unsigned int data_offset, data_len, resp_size;
+	struct cifs_query_dir_io *qd_io = mid->callback_data;
+	char *buf = server->smallbuf;
+	unsigned int buflen = server->pdu_size;
+	struct smb2_query_directory_rsp *rsp;
+
+	cifs_dbg(FYI, "%s: mid=%llu buf_capacity=%zu\n",
+		 __func__, mid->mid, qd_io->combined_iov.iov_len);
+
+	/*
+	 * Read the rest of QUERY_DIRECTORY_RSP header (sans Data array).
+	 * QueryDirectory response structure is 64 bytes (SMB2 header) + 8 bytes (fixed part).
+	 */
+	resp_size = sizeof(struct smb2_query_directory_rsp);
+	len = min_t(unsigned int, buflen, resp_size) - HEADER_SIZE(server) + 1;
+
+	length = cifs_read_from_socket(server,
+				       buf + HEADER_SIZE(server) - 1, len);
+	if (length < 0) {
+		qd_io->result = length;
+		goto discard_and_queue;
+	}
+	server->total_read += length;
+
+	if (server->ops->is_session_expired &&
+	    server->ops->is_session_expired(buf)) {
+		cifs_reconnect(server, true);
+		return -1;
+	}
+
+	if (server->ops->is_status_pending &&
+	    server->ops->is_status_pending(buf, server)) {
+		cifs_discard_remaining_data(server);
+		return -1;
+	}
+
+	/* Is there enough to get to the rest of the QUERY_DIRECTORY_RSP header? */
+	if (server->total_read < resp_size) {
+		cifs_dbg(FYI, "%s: server returned short header. got=%u expected=%u\n",
+			 __func__, server->total_read, resp_size);
+		qd_io->result = smb_EIO2(smb_eio_trace_read_rsp_short,
+					 server->total_read, resp_size);
+		return cifs_query_dir_discard(server, mid);
+	}
+
+	/* Set up first iov for signature check and to get credits */
+	qd_io->iov[0].iov_base = buf;
+	qd_io->iov[0].iov_len = server->total_read;
+	qd_io->iov[1].iov_base = NULL;
+	qd_io->iov[1].iov_len = 0;
+	cifs_dbg(FYI, "0: iov_base=%p iov_len=%zu\n",
+		 qd_io->iov[0].iov_base, qd_io->iov[0].iov_len);
+
+	/* Parse header early to access status before map_error converts it */
+	rsp = (struct smb2_query_directory_rsp *)buf;
+
+	/* Was the SMB query_directory successful? */
+	qd_io->result = server->ops->map_error(buf, false);
+	if (qd_io->result != 0) {
+		if (qd_io->combined_iov.iov_base && qd_io->iov[0].iov_len > 0 &&
+		    qd_io->iov[0].iov_len <= qd_io->combined_iov.iov_len) {
+			memcpy(qd_io->combined_iov.iov_base, qd_io->iov[0].iov_base,
+			       qd_io->iov[0].iov_len);
+			qd_io->iov[0].iov_base = qd_io->combined_iov.iov_base;
+		}
+		cifs_dbg(FYI, "%s: server returned error %d (Status was 0x%x)\n",
+			 __func__, qd_io->result, le32_to_cpu(rsp->hdr.Status));
+		return cifs_query_dir_discard(server, mid);
+	}
+
+	data_offset = le16_to_cpu(rsp->OutputBufferOffset);
+	data_len = le32_to_cpu(rsp->OutputBufferLength);
+
+	cifs_dbg(FYI, "%s: total_read=%u data_offset=%u data_len=%u\n",
+		 __func__, server->total_read, data_offset, data_len);
+
+	/* Validate data_offset and data_len */
+	if (data_offset < server->total_read) {
+		cifs_dbg(FYI, "%s: data offset (%u) inside response header\n",
+			 __func__, data_offset);
+		data_offset = server->total_read;
+		data_len = 0;
+	} else if (data_offset > MAX_CIFS_SMALL_BUFFER_SIZE) {
+		cifs_dbg(FYI, "%s: data offset (%u) beyond end of smallbuf\n",
+			 __func__, data_offset);
+		qd_io->result = smb_EIO1(smb_eio_trace_read_overlarge,
+					 data_offset);
+		return cifs_query_dir_discard(server, mid);
+	}
+
+	/* Read any padding between header and data */
+	len = data_offset - server->total_read;
+	if (len > 0) {
+		length = cifs_read_from_socket(server,
+					       buf + server->total_read, len);
+		if (length < 0) {
+			qd_io->result = length;
+			goto discard_and_queue;
+		}
+		server->total_read += length;
+		qd_io->iov[0].iov_len = server->total_read;
+	}
+
+	/* Check if data fits in the pre-allocated combined buffer */
+	if (qd_io->iov[0].iov_len + data_len > qd_io->combined_iov.iov_len) {
+		cifs_dbg(VFS, "%s: response (%zu + %u) exceeds buffer capacity (%zu)\n",
+			 __func__, qd_io->iov[0].iov_len, data_len, qd_io->combined_iov.iov_len);
+		qd_io->result = smb_EIO2(smb_eio_trace_read_rsp_malformed,
+					 data_len, (unsigned int)qd_io->combined_iov.iov_len);
+		return cifs_query_dir_discard(server, mid);
+	}
+
+	/*
+	 * Build combined header+data in qd_io->combined_iov.iov_base.
+	 * Layout: [header][data] with no padding between them.
+	 * We update OutputBufferOffset in the header to reflect the new layout.
+	 */
+	if (qd_io->iov[0].iov_len > 0 && qd_io->result == 0) {
+		size_t hdr_len = qd_io->iov[0].iov_len;
+		struct smb2_query_directory_rsp *combined_rsp;
+
+		/* Copy header to beginning of combined buffer */
+		memcpy(qd_io->combined_iov.iov_base, qd_io->iov[0].iov_base, hdr_len);
+
+		/* Read directory entries directly after the header, removing any padding */
+		if (data_len > 0) {
+			length = cifs_read_from_socket(server,
+						       qd_io->combined_iov.iov_base + hdr_len,
+						       data_len);
+			if (length < 0) {
+				qd_io->result = length;
+				goto discard_and_queue;
+			}
+			server->total_read += length;
+
+			/* Update OutputBufferOffset and OutputBufferLength to reflect
+			 * the new compact layout (header followed immediately by data).
+			 */
+			combined_rsp = (struct smb2_query_directory_rsp *)qd_io->combined_iov.iov_base;
+			combined_rsp->OutputBufferOffset = cpu_to_le16(hdr_len);
+			combined_rsp->OutputBufferLength = cpu_to_le32(length);
+
+			/* Set up second iov pointing to the directory data within combined buffer */
+			qd_io->iov[1].iov_base = qd_io->combined_iov.iov_base + hdr_len;
+			qd_io->iov[1].iov_len = length;
+		}
+
+		qd_io->combined_iov.iov_len = hdr_len + (data_len > 0 ? length : 0);
+
+		cifs_dbg(FYI, "total_read=%u buflen=%u data_len=%u hdr_len=%zu combined_len=%zu\n",
+			 server->total_read, buflen, data_len, hdr_len, qd_io->combined_iov.iov_len);
+	}
+
+discard_and_queue:
+	/* Discard any remaining data (shouldn't be any) */
+	if (server->total_read < buflen)
+		cifs_discard_remaining_data(server);
+
+	dequeue_mid(server, mid, false);
+	mid->resp_buf = server->smallbuf;
+	server->smallbuf = NULL;
+	return length;
+}
+
 int
 cifs_readv_receive(struct TCP_Server_Info *server, struct mid_q_entry *mid)
 {
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 7/7] cifs: reorganize cached dir helpers
  2026-04-14 13:59 [PATCH 1/7] cifs: change_conf needs to be called for session setup nspmangalore
                   ` (4 preceding siblings ...)
  2026-04-14 13:59 ` [PATCH 6/7] cifs: optimize readdir for larger directories nspmangalore
@ 2026-04-14 13:59 ` nspmangalore
  5 siblings, 0 replies; 9+ messages in thread
From: nspmangalore @ 2026-04-14 13:59 UTC (permalink / raw)
  To: linux-cifs, smfrench, pc, bharathsm, dhowells, henrique.carvalho,
	ematsumiya
  Cc: Shyam Prasad N

From: Shyam Prasad N <sprasad@microsoft.com>

Currently, we have helper functions for cfid and dirent caching spread
across cached_dir.c and readdir.c, with de_mutex locking done inside
the calling functions. This change neatly wraps them in helper functions
and keeps all such functions in cached_dir.c.

This code also splits the logic of dirent emit into cache and to VFS into
different functions.

Signed-off-by: Shyam Prasad N <sprasad@microsoft.com>
---
 fs/smb/client/cached_dir.c | 207 +++++++++++++++++++++++++++++++++++++
 fs/smb/client/cached_dir.h |  18 +++-
 fs/smb/client/readdir.c    | 167 ++----------------------------
 3 files changed, 231 insertions(+), 161 deletions(-)

diff --git a/fs/smb/client/cached_dir.c b/fs/smb/client/cached_dir.c
index e9917e5204b00..10fe703d353d1 100644
--- a/fs/smb/client/cached_dir.c
+++ b/fs/smb/client/cached_dir.c
@@ -23,6 +23,213 @@ struct cached_dir_dentry {
 	struct dentry *dentry;
 };
 
+static bool emit_cached_dirents(struct cached_dirents *cde,
+				struct dir_context *ctx)
+{
+	struct cached_dirent *dirent;
+	bool rc;
+
+	lockdep_assert_held(&cde->de_mutex);
+
+	list_for_each_entry(dirent, &cde->entries, entry) {
+		/*
+		 * Skip all early entries prior to the current lseek()
+		 * position.
+		 */
+		if (ctx->pos > dirent->pos)
+			continue;
+		/*
+		 * We recorded the current ->pos value for the dirent
+		 * when we stored it in the cache.
+		 * However, this sequence of ->pos values may have holes
+		 * in it, for example dot-dirs returned from the server
+		 * are suppressed.
+		 * Handle this by forcing ctx->pos to be the same as the
+		 * ->pos of the current dirent we emit from the cache.
+		 * This means that when we emit these entries from the cache
+		 * we now emit them with the same ->pos value as in the
+		 * initial scan.
+		 */
+		ctx->pos = dirent->pos;
+		rc = dir_emit(ctx, dirent->name, dirent->namelen,
+			      dirent->fattr.cf_uniqueid,
+			      dirent->fattr.cf_dtype);
+		if (!rc)
+			return rc;
+		ctx->pos++;
+	}
+	return true;
+}
+
+static bool add_cached_dirent(struct cached_dirents *cde,
+			      struct dir_context *ctx, const char *name,
+			      int namelen, struct cifs_fattr *fattr,
+			      struct file *file)
+{
+	struct cached_dirent *de;
+
+	lockdep_assert_held(&cde->de_mutex);
+
+	if (cde->file != file)
+		return false;
+	if (cde->is_valid || cde->is_failed)
+		return false;
+	if (ctx->pos != cde->pos) {
+		cde->is_failed = 1;
+		return false;
+	}
+	de = kzalloc_obj(*de, GFP_KERNEL);
+	if (de == NULL) {
+		cde->is_failed = 1;
+		return false;
+	}
+	de->namelen = namelen;
+	de->name = kstrndup(name, namelen, GFP_KERNEL);
+	if (de->name == NULL) {
+		kfree(de);
+		cde->is_failed = 1;
+		return false;
+	}
+	de->pos = ctx->pos;
+
+	memcpy(&de->fattr, fattr, sizeof(struct cifs_fattr));
+
+	list_add_tail(&de->entry, &cde->entries);
+	/* update accounting */
+	cde->entries_count++;
+	cde->bytes_used += sizeof(*de) + (size_t)namelen + 1;
+	return true;
+}
+
+bool emit_cached_dir_if_valid(struct cached_fid *cfid,
+			      struct file *file,
+			      struct dir_context *ctx)
+{
+	if (!cfid)
+		return false;
+
+	mutex_lock(&cfid->dirents.de_mutex);
+	/*
+	 * If this was reading from the start of the directory
+	 * we need to initialize scanning and storing the
+	 * directory content.
+	 */
+	if (ctx->pos == 0 && cfid->dirents.file == NULL) {
+		cfid->dirents.file = file;
+		cfid->dirents.pos = 2;
+	}
+
+	if (!cfid->dirents.is_valid) {
+		mutex_unlock(&cfid->dirents.de_mutex);
+		return false;
+	}
+
+	if (dir_emit_dots(file, ctx))
+		emit_cached_dirents(&cfid->dirents, ctx);
+
+	mutex_unlock(&cfid->dirents.de_mutex);
+	return true;
+}
+
+bool add_to_cached_dir(struct cached_fid *cfid,
+		       struct dir_context *ctx,
+		       const char *name,
+		       int namelen,
+		       struct cifs_fattr *fattr,
+		       struct file *file)
+{
+	size_t delta_bytes;
+	bool added = false;
+
+	if (!cfid)
+		return false;
+
+	/* Cost of this entry */
+	delta_bytes = sizeof(struct cached_dirent) + (size_t)namelen + 1;
+
+	mutex_lock(&cfid->dirents.de_mutex);
+	added = add_cached_dirent(&cfid->dirents, ctx, name, namelen,
+				  fattr, file);
+	mutex_unlock(&cfid->dirents.de_mutex);
+
+	if (added) {
+		/* per-tcon then global for consistency with free path */
+		atomic64_add((long long)delta_bytes, &cfid->cfids->total_dirents_bytes);
+		atomic_long_inc(&cfid->cfids->total_dirents_entries);
+		atomic64_add((long long)delta_bytes, &cifs_dircache_bytes_used);
+	}
+
+	return added;
+}
+
+static void update_cached_dirents_count(struct cached_dirents *cde,
+					struct file *file)
+{
+	if (cde->file != file)
+		return;
+	if (cde->is_valid || cde->is_failed)
+		return;
+
+	cde->pos++;
+}
+
+static void finished_cached_dirents_count(struct cached_dirents *cde,
+					  struct dir_context *ctx,
+					  struct file *file)
+{
+	if (cde->file != file)
+		return;
+	if (cde->is_valid || cde->is_failed)
+		return;
+	if (ctx->pos != cde->pos)
+		return;
+
+	cde->is_valid = 1;
+}
+
+void update_pos_cached_dir(struct cached_fid *cfid,
+				      struct file *file)
+{
+	if (!cfid)
+		return;
+
+	mutex_lock(&cfid->dirents.de_mutex);
+	update_cached_dirents_count(&cfid->dirents, file);
+	mutex_unlock(&cfid->dirents.de_mutex);
+}
+
+void complete_cached_dir(struct cached_fid *cfid,
+					struct dir_context *ctx,
+					struct file *file)
+{
+	if (!cfid)
+		return;
+
+	mutex_lock(&cfid->dirents.de_mutex);
+	finished_cached_dirents_count(&cfid->dirents, ctx, file);
+	mutex_unlock(&cfid->dirents.de_mutex);
+}
+
+struct cached_dirent *lookup_cached_dirent(struct cached_dirents *cde,
+				   const char *name,
+				   unsigned int namelen)
+{
+	struct cached_dirent *entry;
+
+	if (!cde)
+		return NULL;
+
+	lockdep_assert_held(&cde->de_mutex);
+
+	list_for_each_entry(entry, &cde->entries, entry) {
+		if (entry->namelen == namelen &&
+		    memcmp(entry->name, name, namelen) == 0)
+			return entry;
+	}
+
+	return NULL;
+}
+
 static struct cached_fid *find_or_create_cached_dir(struct cached_fids *cfids,
 						    const char *path,
 						    bool lookup_only,
diff --git a/fs/smb/client/cached_dir.h b/fs/smb/client/cached_dir.h
index 19d5592512e4b..09f1f488059c9 100644
--- a/fs/smb/client/cached_dir.h
+++ b/fs/smb/client/cached_dir.h
@@ -8,7 +8,6 @@
 #ifndef _CACHED_DIR_H
 #define _CACHED_DIR_H
 
-
 struct cached_dirent {
 	struct list_head entry;
 	char *name;
@@ -87,6 +86,23 @@ int open_cached_dir(unsigned int xid, struct cifs_tcon *tcon, const char *path,
 int open_cached_dir_by_dentry(struct cifs_tcon *tcon, struct dentry *dentry,
 			      struct cached_fid **ret_cfid);
 void close_cached_dir(struct cached_fid *cfid);
+bool emit_cached_dir_if_valid(struct cached_fid *cfid,
+			      struct file *file,
+			      struct dir_context *ctx);
+bool add_to_cached_dir(struct cached_fid *cfid,
+		       struct dir_context *ctx,
+		       const char *name,
+		       int namelen,
+		       struct cifs_fattr *fattr,
+		       struct file *file);
+void update_pos_cached_dir(struct cached_fid *cfid,
+				      struct file *file);
+void complete_cached_dir(struct cached_fid *cfid,
+					struct dir_context *ctx,
+					struct file *file);
+struct cached_dirent *lookup_cached_dirent(struct cached_dirents *cde,
+				   const char *name,
+				   unsigned int namelen);
 void drop_cached_dir_by_name(const unsigned int xid, struct cifs_tcon *tcon,
 			     const char *name, struct cifs_sb_info *cifs_sb);
 void close_all_cached_dirs(struct cifs_sb_info *cifs_sb);
diff --git a/fs/smb/client/readdir.c b/fs/smb/client/readdir.c
index 8a444f97e0ae9..907e235ad1b8f 100644
--- a/fs/smb/client/readdir.c
+++ b/fs/smb/client/readdir.c
@@ -817,136 +817,13 @@ find_cifs_entry(const unsigned int xid, struct cifs_tcon *tcon, loff_t pos,
 	return rc;
 }
 
-static bool emit_cached_dirents(struct cached_dirents *cde,
-				struct dir_context *ctx)
-{
-	struct cached_dirent *dirent;
-	bool rc;
-
-	list_for_each_entry(dirent, &cde->entries, entry) {
-		/*
-		 * Skip all early entries prior to the current lseek()
-		 * position.
-		 */
-		if (ctx->pos > dirent->pos)
-			continue;
-		/*
-		 * We recorded the current ->pos value for the dirent
-		 * when we stored it in the cache.
-		 * However, this sequence of ->pos values may have holes
-		 * in it, for example dot-dirs returned from the server
-		 * are suppressed.
-		 * Handle this by forcing ctx->pos to be the same as the
-		 * ->pos of the current dirent we emit from the cache.
-		 * This means that when we emit these entries from the cache
-		 * we now emit them with the same ->pos value as in the
-		 * initial scan.
-		 */
-		ctx->pos = dirent->pos;
-		rc = dir_emit(ctx, dirent->name, dirent->namelen,
-			      dirent->fattr.cf_uniqueid,
-			      dirent->fattr.cf_dtype);
-		if (!rc)
-			return rc;
-		ctx->pos++;
-	}
-	return true;
-}
-
-static void update_cached_dirents_count(struct cached_dirents *cde,
-					struct file *file)
-{
-	if (cde->file != file)
-		return;
-	if (cde->is_valid || cde->is_failed)
-		return;
-
-	cde->pos++;
-}
-
-static void finished_cached_dirents_count(struct cached_dirents *cde,
-					struct dir_context *ctx, struct file *file)
-{
-	if (cde->file != file)
-		return;
-	if (cde->is_valid || cde->is_failed)
-		return;
-	if (ctx->pos != cde->pos)
-		return;
-
-	cde->is_valid = 1;
-}
-
-static bool add_cached_dirent(struct cached_dirents *cde,
-			      struct dir_context *ctx, const char *name,
-			      int namelen, struct cifs_fattr *fattr,
-			      struct file *file)
-{
-	struct cached_dirent *de;
-
-	if (cde->file != file)
-		return false;
-	if (cde->is_valid || cde->is_failed)
-		return false;
-	if (ctx->pos != cde->pos) {
-		cde->is_failed = 1;
-		return false;
-	}
-	de = kzalloc_obj(*de, GFP_ATOMIC);
-	if (de == NULL) {
-		cde->is_failed = 1;
-		return false;
-	}
-	de->namelen = namelen;
-	de->name = kstrndup(name, namelen, GFP_ATOMIC);
-	if (de->name == NULL) {
-		kfree(de);
-		cde->is_failed = 1;
-		return false;
-	}
-	de->pos = ctx->pos;
-
-	memcpy(&de->fattr, fattr, sizeof(struct cifs_fattr));
-
-	list_add_tail(&de->entry, &cde->entries);
-	/* update accounting */
-	cde->entries_count++;
-	cde->bytes_used += sizeof(*de) + (size_t)namelen + 1;
-	return true;
-}
-
 static bool cifs_dir_emit(struct dir_context *ctx,
 			  const char *name, int namelen,
-			  struct cifs_fattr *fattr,
-			  struct cached_fid *cfid,
-			  struct file *file)
+			  struct cifs_fattr *fattr)
 {
-	size_t delta_bytes = 0;
-	bool rc, added = false;
 	ino_t ino = cifs_uniqueid_to_ino_t(fattr->cf_uniqueid);
 
-	rc = dir_emit(ctx, name, namelen, ino, fattr->cf_dtype);
-	if (!rc)
-		return rc;
-
-	if (cfid) {
-		/* Cost of this entry */
-		delta_bytes = sizeof(struct cached_dirent) + (size_t)namelen + 1;
-
-		mutex_lock(&cfid->dirents.de_mutex);
-		added = add_cached_dirent(&cfid->dirents, ctx, name, namelen,
-					  fattr, file);
-		mutex_unlock(&cfid->dirents.de_mutex);
-
-		if (added) {
-			/* per-tcon then global for consistency with free path */
-			atomic64_add((long long)delta_bytes, &cfid->cfids->total_dirents_bytes);
-			atomic_long_inc(&cfid->cfids->total_dirents_entries);
-			atomic64_add((long long)delta_bytes, &cifs_dircache_bytes_used);
-		}
-	}
-
-	return rc;
+	return dir_emit(ctx, name, namelen, ino, fattr->cf_dtype);
 }
 
 static int cifs_filldir(char *find_entry, struct file *file,
@@ -1040,10 +917,10 @@ static int cifs_filldir(char *find_entry, struct file *file,
 		 */
 		fattr.cf_flags |= CIFS_FATTR_NEED_REVAL;
 
+	add_to_cached_dir(cfid, ctx, name.name, name.len, &fattr, file);
 	cifs_prime_dcache(file_dentry(file), &name, &fattr);
 
-	return !cifs_dir_emit(ctx, name.name, name.len,
-			      &fattr, cfid, file);
+	return !cifs_dir_emit(ctx, name.name, name.len, &fattr);
 }
 
 
@@ -1088,30 +965,8 @@ int cifs_readdir(struct file *file, struct dir_context *ctx)
 	if (rc)
 		goto cache_not_found;
 
-	mutex_lock(&cfid->dirents.de_mutex);
-	/*
-	 * If this was reading from the start of the directory
-	 * we need to initialize scanning and storing the
-	 * directory content.
-	 */
-	if (ctx->pos == 0 && cfid->dirents.file == NULL) {
-		cfid->dirents.file = file;
-		cfid->dirents.pos = 2;
-	}
-	/*
-	 * If we already have the entire directory cached then
-	 * we can just serve the cache.
-	 */
-	if (cfid->dirents.is_valid) {
-		if (!dir_emit_dots(file, ctx)) {
-			mutex_unlock(&cfid->dirents.de_mutex);
-			goto rddir2_exit;
-		}
-		emit_cached_dirents(&cfid->dirents, ctx);
-		mutex_unlock(&cfid->dirents.de_mutex);
+	if (emit_cached_dir_if_valid(cfid, file, ctx))
 		goto rddir2_exit;
-	}
-	mutex_unlock(&cfid->dirents.de_mutex);
 
 	/* Drop the cache while calling initiate_cifs_search and
 	 * find_cifs_entry in case there will be reconnects during
@@ -1161,11 +1016,7 @@ int cifs_readdir(struct file *file, struct dir_context *ctx)
 	} else if (current_entry != NULL) {
 		cifs_dbg(FYI, "entry %lld found\n", ctx->pos);
 	} else {
-		if (cfid) {
-			mutex_lock(&cfid->dirents.de_mutex);
-			finished_cached_dirents_count(&cfid->dirents, ctx, file);
-			mutex_unlock(&cfid->dirents.de_mutex);
-		}
+		complete_cached_dir(cfid, ctx, file);
 		cifs_dbg(FYI, "Could not find entry\n");
 		goto rddir2_exit;
 	}
@@ -1202,11 +1053,7 @@ int cifs_readdir(struct file *file, struct dir_context *ctx)
 		}
 
 		ctx->pos++;
-		if (cfid) {
-			mutex_lock(&cfid->dirents.de_mutex);
-			update_cached_dirents_count(&cfid->dirents, file);
-			mutex_unlock(&cfid->dirents.de_mutex);
-		}
+		update_pos_cached_dir(cfid, file);
 
 		if (ctx->pos ==
 			cifsFile->srch_inf.index_of_last_entry) {
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH 6/7] cifs: optimize readdir for larger directories
  2026-04-14 13:59 ` [PATCH 6/7] cifs: optimize readdir for larger directories nspmangalore
@ 2026-04-15 23:08   ` Steve French
  2026-04-15 23:11     ` Steve French
  0 siblings, 1 reply; 9+ messages in thread
From: Steve French @ 2026-04-15 23:08 UTC (permalink / raw)
  To: nspmangalore
  Cc: linux-cifs, pc, bharathsm, dhowells, henrique.carvalho,
	ematsumiya, Shyam Prasad N

This (patch 6) hit a merge conflict with smb2ops.c when I tried to
apply it to current cifs-2.6.git for-next for testing

patching file fs/smb/client/smb2ops.c
Hunk #6 FAILED at 5455.
Hunk #7 succeeded at 5494 (offset 8 lines).
Hunk #8 succeeded at 5508 (offset 8 lines).
Hunk #9 succeeded at 5683 (offset 8 lines).
1 out of 9 hunks FAILED -- saving rejects to file fs/smb/client/smb2ops.c.rej

On Tue, Apr 14, 2026 at 8:59 AM <nspmangalore@gmail.com> wrote:
>
> From: Shyam Prasad N <sprasad@microsoft.com>
>
> Today QueryDirectory uses the compound_send_recv infrastructure
> which is limited to 16KB in size. As a result, readdir of large
> directories generally take several round trips.
>
> With this change, if the readdir needs a QueryDir after the first
> round-trip (meaning that there are more dirents to read), then the
> following QueryDirs will now switch to using larger buffers with
> MTU credits.
>
> Till now, the only command type that used this flow was SMB2_READ.
> In case of encrypted response, it becomes challenging to decide if
> the response is for SMB2_READ or SMB2_READDIR. This change reuses
> receive_encrypted_read and after decrypting the response decides
> the handling function based on the command in the resp header. That
> way, care has been taken to ensure that the read code path
> modifications on account of this change are kept to a minimum.
>
> Cc: David Howells <dhowells@redhat.com>
> Signed-off-by: Shyam Prasad N <sprasad@microsoft.com>
> ---
>  fs/smb/client/cifsglob.h  |  23 +-
>  fs/smb/client/cifsproto.h |   3 +
>  fs/smb/client/readdir.c   |   2 +-
>  fs/smb/client/smb1ops.c   |   4 +-
>  fs/smb/client/smb2misc.c  |   7 +-
>  fs/smb/client/smb2ops.c   | 452 ++++++++++++++++++++++++++++++++++++--
>  fs/smb/client/smb2pdu.c   | 182 ++++++++++++++-
>  fs/smb/client/smb2pdu.h   |   3 +
>  fs/smb/client/smb2proto.h |   3 +
>  fs/smb/client/trace.h     |   1 +
>  fs/smb/client/transport.c | 184 ++++++++++++++++
>  11 files changed, 829 insertions(+), 35 deletions(-)
>
> diff --git a/fs/smb/client/cifsglob.h b/fs/smb/client/cifsglob.h
> index 8d089ba08e3e5..45de35ecc0bd2 100644
> --- a/fs/smb/client/cifsglob.h
> +++ b/fs/smb/client/cifsglob.h
> @@ -504,7 +504,7 @@ struct smb_version_operations {
>                                struct cifs_search_info *);
>         /* continue readdir */
>         int (*query_dir_next)(const unsigned int, struct cifs_tcon *,
> -                             struct cifs_fid *,
> +                             struct cifs_sb_info *, struct cifs_fid *,
>                               __u16, struct cifs_search_info *srch_inf);
>         /* close dir */
>         int (*close_dir)(const unsigned int, struct cifs_tcon *,
> @@ -1397,6 +1397,27 @@ struct cifs_search_info {
>         bool is_dynamic_buf:1; /* dynamically allocated buffer - can be variable size */
>  };
>
> +/* Structure for QueryDirectory with multi-credit support */
> +struct cifs_query_dir_io {
> +       struct cifs_tcon *tcon;
> +       struct TCP_Server_Info *server;
> +       struct cifs_search_info *srch_inf;
> +       unsigned int xid;
> +       u64 persistent_fid;
> +       u64 volatile_fid;
> +       int index;
> +       struct kvec combined_iov;       /* Pre-allocated combined header+data buffer;
> +                                        * iov_len holds total capacity until the receive
> +                                        * handler fills it in, then actual valid length */
> +       struct completion done;
> +       int result;
> +       struct cifs_credits credits;
> +       bool replay;
> +       unsigned int retries;
> +       unsigned int cur_sleep;
> +       struct kvec iov[2];             /* For response handling */
> +};
> +
>  #define ACL_NO_MODE    ((umode_t)(-1))
>  struct cifs_open_parms {
>         struct cifs_tcon *tcon;
> diff --git a/fs/smb/client/cifsproto.h b/fs/smb/client/cifsproto.h
> index 884bfa1cf0b42..bbbee0ef09443 100644
> --- a/fs/smb/client/cifsproto.h
> +++ b/fs/smb/client/cifsproto.h
> @@ -336,6 +336,9 @@ struct cifs_ses *cifs_get_smb_ses(struct TCP_Server_Info *server,
>  int cifs_readv_receive(struct TCP_Server_Info *server,
>                        struct mid_q_entry *mid);
>
> +int cifs_query_dir_receive(struct TCP_Server_Info *server,
> +                           struct mid_q_entry *mid);
> +
>  int cifs_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
>                           struct cifs_sb_info *cifs_sb,
>                           const unsigned char *path, char *pbuf,
> diff --git a/fs/smb/client/readdir.c b/fs/smb/client/readdir.c
> index b50efd9b9e1d2..8a444f97e0ae9 100644
> --- a/fs/smb/client/readdir.c
> +++ b/fs/smb/client/readdir.c
> @@ -760,7 +760,7 @@ find_cifs_entry(const unsigned int xid, struct cifs_tcon *tcon, loff_t pos,
>         while ((index_to_find >= cfile->srch_inf.index_of_last_entry) &&
>                (rc == 0) && !cfile->srch_inf.endOfSearch) {
>                 cifs_dbg(FYI, "calling findnext2\n");
> -               rc = server->ops->query_dir_next(xid, tcon, &cfile->fid,
> +               rc = server->ops->query_dir_next(xid, tcon, cifs_sb, &cfile->fid,
>                                                  search_flags,
>                                                  &cfile->srch_inf);
>                 if (rc)
> diff --git a/fs/smb/client/smb1ops.c b/fs/smb/client/smb1ops.c
> index 9694117050a6c..860a9b23a2f8d 100644
> --- a/fs/smb/client/smb1ops.c
> +++ b/fs/smb/client/smb1ops.c
> @@ -1135,8 +1135,8 @@ cifs_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
>
>  static int
>  cifs_query_dir_next(const unsigned int xid, struct cifs_tcon *tcon,
> -                   struct cifs_fid *fid, __u16 search_flags,
> -                   struct cifs_search_info *srch_inf)
> +                   struct cifs_sb_info *cifs_sb, struct cifs_fid *fid,
> +                   __u16 search_flags, struct cifs_search_info *srch_inf)
>  {
>         return CIFSFindNext(xid, tcon, fid->netfid, search_flags, srch_inf);
>  }
> diff --git a/fs/smb/client/smb2misc.c b/fs/smb/client/smb2misc.c
> index 973fce3c959c4..b7b6ecd5fdaee 100644
> --- a/fs/smb/client/smb2misc.c
> +++ b/fs/smb/client/smb2misc.c
> @@ -12,6 +12,7 @@
>  #include "cifsglob.h"
>  #include "cifsproto.h"
>  #include "smb2proto.h"
> +#include "smb2pdu.h"
>  #include "cifs_debug.h"
>  #include "cifs_unicode.h"
>  #include "../common/smb2status.h"
> @@ -316,7 +317,7 @@ char *
>  smb2_get_data_area_len(int *off, int *len, struct smb2_hdr *shdr)
>  {
>         const int max_off = 4096;
> -       const int max_len = 128 * 1024;
> +       int max_len = 128 * 1024;
>
>         *off = 0;
>         *len = 0;
> @@ -367,6 +368,10 @@ smb2_get_data_area_len(int *off, int *len, struct smb2_hdr *shdr)
>                   ((struct smb2_query_directory_rsp *)shdr)->OutputBufferOffset);
>                 *len = le32_to_cpu(
>                   ((struct smb2_query_directory_rsp *)shdr)->OutputBufferLength);
> +               /* Allow larger buffers for query directory (up to 2MB).
> +                * The actual data is handled separately in cifs_query_dir_receive().
> +                */
> +               max_len = SMB2_MAX_QD_DATABUF_SIZE;
>                 break;
>         case SMB2_IOCTL:
>                 *off = le32_to_cpu(
> diff --git a/fs/smb/client/smb2ops.c b/fs/smb/client/smb2ops.c
> index 5c6fe25204d26..ecbd717f5a5ea 100644
> --- a/fs/smb/client/smb2ops.c
> +++ b/fs/smb/client/smb2ops.c
> @@ -2703,11 +2703,130 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
>
>  static int
>  smb2_query_dir_next(const unsigned int xid, struct cifs_tcon *tcon,
> -                   struct cifs_fid *fid, __u16 search_flags,
> -                   struct cifs_search_info *srch_inf)
> +                   struct cifs_sb_info *cifs_sb, struct cifs_fid *fid,
> +                   __u16 search_flags, struct cifs_search_info *srch_inf)
>  {
> -       return SMB2_query_directory(xid, tcon, fid->persistent_fid,
> -                                   fid->volatile_fid, 0, srch_inf);
> +       struct cifs_query_dir_io qd_io;
> +       struct TCP_Server_Info *server;
> +       struct cifs_ses *ses = tcon->ses;
> +       size_t buf_size;
> +       int rc;
> +       int resp_buftype = CIFS_DYNAMIC_BUFFER;
> +
> +       /* Pick server and determine buffer size based on negotiated rsize */
> +       server = cifs_pick_channel(ses);
> +       if (!server)
> +               return smb_EIO(smb_eio_trace_null_pointers);
> +
> +       /* Negotiate rsize if not already set */
> +       if (cifs_sb->ctx->rsize == 0)
> +               cifs_negotiate_rsize(server, cifs_sb->ctx, tcon);
> +
> +       /* Use negotiated rsize for buffer size, with reasonable limits */
> +       buf_size = cifs_sb->ctx->rsize;
> +
> +       cifs_dbg(FYI, "%s: using buffer size %zu (rsize=%u, encrypted=%d)\n",
> +                __func__, buf_size, cifs_sb->ctx->rsize, smb3_encryption_required(tcon));
> +
> +       /* Initialize qd_io structure */
> +       memset(&qd_io, 0, sizeof(qd_io));
> +       qd_io.tcon = tcon;
> +       qd_io.server = server;
> +       qd_io.srch_inf = srch_inf;
> +       qd_io.xid = xid;
> +       qd_io.persistent_fid = fid->persistent_fid;
> +       qd_io.volatile_fid = fid->volatile_fid;
> +       qd_io.index = 0;
> +       qd_io.result = 0;
> +       qd_io.replay = false;
> +       qd_io.retries = 0;
> +       qd_io.cur_sleep = 0;
> +
> +       /* Allocate credits for the buffer size */
> +       rc = server->ops->wait_mtu_credits(server, buf_size, &buf_size,
> +                                          &qd_io.credits);
> +       if (rc) {
> +               cifs_dbg(VFS, "%s: failed to get credits: %d\n", __func__, rc);
> +               return rc;
> +       }
> +
> +       cifs_dbg(FYI, "%s: allocated %u credits for %zu bytes\n",
> +                __func__, qd_io.credits.value, buf_size);
> +
> +       /* Send query directory with large buffer and wait for completion */
> +       rc = SMB2_query_directory_large(&qd_io, buf_size);
> +       if (rc) {
> +               if (rc == -ENODATA) {
> +                       const struct smb2_hdr *hdr = NULL;
> +
> +                       if (qd_io.combined_iov.iov_base)
> +                               hdr = (const struct smb2_hdr *)qd_io.combined_iov.iov_base;
> +                       else if (qd_io.iov[0].iov_base)
> +                               hdr = (const struct smb2_hdr *)qd_io.iov[0].iov_base;
> +
> +                       /*
> +                        * ENODATA from QUERY_DIRECTORY generally means enumeration reached
> +                        * the end. Treat it as end-of-search even if the header buffer is
> +                        * unavailable in this async path.
> +                        */
> +                       if (!hdr) {
> +                               cifs_dbg(FYI, "%s: ENODATA but hdr is NULL\n", __func__);
> +                       } else {
> +                               cifs_dbg(FYI, "%s: ENODATA with hdr->Status=0x%x (STATUS_NO_MORE_FILES=0x%x)\n",
> +                                        __func__, le32_to_cpu(hdr->Status), le32_to_cpu(STATUS_NO_MORE_FILES));
> +                       }
> +
> +                       if (hdr && hdr->Status == STATUS_NO_MORE_FILES) {
> +                               trace_smb3_query_dir_done(xid, fid->persistent_fid,
> +                                       tcon->tid, tcon->ses->Suid, 0, 0);
> +                               srch_inf->endOfSearch = true;
> +                               rc = 0;
> +                       } else {
> +                               cifs_dbg(FYI, "%s: ENODATA but Status mismatch - not treating as end-of-search\n", __func__);
> +                               trace_smb3_query_dir_err(xid, fid->persistent_fid,
> +                                       tcon->tid, tcon->ses->Suid, 0, 0, rc);
> +                       }
> +               } else {
> +                       trace_smb3_query_dir_err(xid, fid->persistent_fid,
> +                               tcon->tid, tcon->ses->Suid, 0, 0, rc);
> +               }
> +               goto qdir_next_exit;
> +       }
> +
> +       /* Parse the response using the combined buffer built in receive handler */
> +       if (qd_io.combined_iov.iov_len > 0) {
> +               rc = smb2_parse_query_directory(tcon, &qd_io.combined_iov, resp_buftype,
> +                                               srch_inf);
> +               if (rc) {
> +                       trace_smb3_query_dir_err(xid, fid->persistent_fid,
> +                               tcon->tid, tcon->ses->Suid, 0, 0, rc);
> +                       kfree(qd_io.combined_iov.iov_base);
> +                       qd_io.combined_iov.iov_base = NULL;
> +                       goto qdir_next_exit;
> +               }
> +
> +               /* combined_iov.iov_base ownership transferred to srch_inf->ntwrk_buf_start */
> +               qd_io.combined_iov.iov_base = NULL;
> +
> +               trace_smb3_query_dir_done(xid, fid->persistent_fid,
> +                       tcon->tid, tcon->ses->Suid, 0,
> +                       srch_inf->entries_in_buffer);
> +       }
> +
> +qdir_next_exit:
> +       /* Free the data buffer if not transferred to srch_inf */
> +       kfree(qd_io.combined_iov.iov_base);
> +
> +       /* Return credits if we still have them (they should have been cleared in callback) */
> +       if (qd_io.credits.value != 0) {
> +               trace_smb3_rw_credits(0, 0, 0,
> +                                     server->credits, server->in_flight,
> +                                     qd_io.credits.value,
> +                                     cifs_trace_rw_credits_query_dir_done);
> +               add_credits(server, &qd_io.credits, 0);
> +       }
> +
> +       return rc;
>  }
>
>  static int
> @@ -4824,6 +4943,247 @@ cifs_copy_folioq_to_iter(struct folio_queue *folioq, size_t data_size,
>         return 0;
>  }
>
> +static int
> +cifs_copy_folioq_to_buf(struct folio_queue *folioq, size_t total_size,
> +                       size_t skip, char *buf, size_t buf_len)
> +{
> +       size_t copied = 0;
> +
> +       if (buf_len > total_size - skip)
> +               buf_len = total_size - skip;
> +
> +       for (; folioq; folioq = folioq->next) {
> +               for (int s = 0; s < folioq_count(folioq); s++) {
> +                       struct folio *folio = folioq_folio(folioq, s);
> +                       size_t fsize = folio_size(folio);
> +                       size_t len = umin(fsize - skip, buf_len);
> +
> +                       if (len == 0)
> +                               break;
> +
> +                       memcpy_from_folio(buf + copied, folio, skip, len);
> +                       copied += len;
> +                       buf_len -= len;
> +                       skip = 0;
> +
> +                       if (buf_len == 0)
> +                               return 0;
> +               }
> +       }
> +
> +       return 0;
> +}
> +
> +/*
> + * Handle encrypted QueryDirectory response data.
> + * Called only for encrypted responses where mid->decrypted == true.
> + * For unencrypted responses, cifs_query_dir_receive handles everything.
> + *
> + * This is written in such a way that handle_read_data does not need modification.
> + *
> + * buf: contains read_rsp_size bytes of decrypted response
> + * buffer: contains (total_len - read_rsp_size) bytes of decrypted data
> + *
> + * Since sizeof(struct smb2_query_directory_rsp) < read_rsp_size,
> + * the response header is always fully in buf. Data may be split between
> + * buf and buffer depending on data_offset.
> + */
> +static int
> +handle_query_dir_data(struct TCP_Server_Info *server, struct mid_q_entry *mid,
> +                     char *buf, unsigned int buf_len, struct folio_queue *buffer,
> +                     unsigned int buffer_len, bool is_offloaded)
> +{
> +       struct cifs_query_dir_io *qd_io = mid->callback_data;
> +       struct smb2_query_directory_rsp *rsp;
> +       struct smb2_hdr *shdr = (struct smb2_hdr *)buf;
> +       unsigned int data_offset, data_len;
> +       unsigned int hdr_len;
> +
> +       cifs_dbg(FYI, "%s: processing encrypted QueryDirectory response\n", __func__);
> +
> +       if (shdr->Command != SMB2_QUERY_DIRECTORY) {
> +               cifs_server_dbg(VFS, "only QueryDirectory responses are supported\n");
> +               return -EOPNOTSUPP;
> +       }
> +
> +       if (server->ops->is_session_expired &&
> +           server->ops->is_session_expired(buf)) {
> +               if (!is_offloaded)
> +                       cifs_reconnect(server, true);
> +               return -1;
> +       }
> +
> +       if (server->ops->is_status_pending &&
> +                       server->ops->is_status_pending(buf, server))
> +               return -1;
> +
> +       rsp = (struct smb2_query_directory_rsp *)buf;
> +       hdr_len = min_t(unsigned int, buf_len,
> +                        sizeof(struct smb2_query_directory_rsp));
> +
> +       /* Map error code first */
> +       qd_io->result = server->ops->map_error(buf, false);
> +
> +       /* Get data_offset early to set up iov properly */
> +       data_offset = le16_to_cpu(rsp->OutputBufferOffset);
> +       data_len = le32_to_cpu(rsp->OutputBufferLength);
> +
> +       /* Set up first iov to point to header portion (needed for credits/signature) */
> +       qd_io->iov[0].iov_base = buf;
> +       qd_io->iov[0].iov_len = qd_io->result ? hdr_len : data_offset;
> +
> +       if (qd_io->result != 0) {
> +               cifs_dbg(FYI, "%s: server returned error %d (Status=0x%x)\n",
> +                        __func__, qd_io->result, le32_to_cpu(rsp->hdr.Status));
> +
> +               /* Copy header to persistent combined_iov buffer so status
> +                * remains accessible after receive handler returns. buf is temporary
> +                * and will be freed/reused, so we can't leave iov[0] pointing to it. */
> +               if (qd_io->combined_iov.iov_base && hdr_len > 0 &&
> +                   hdr_len <= qd_io->combined_iov.iov_len) {
> +                       memcpy(qd_io->combined_iov.iov_base, buf, hdr_len);
> +                       qd_io->iov[0].iov_base = qd_io->combined_iov.iov_base;
> +                       qd_io->iov[0].iov_len = hdr_len;
> +                       cifs_dbg(FYI, "%s: copied error response header to combined_iov\n", __func__);
> +               }
> +
> +               /* Normal error on query_directory response - response received successfully,
> +                * but the command failed. Store error in qd_io->result for callback. */
> +               if (is_offloaded)
> +                       mid->mid_state = MID_RESPONSE_RECEIVED;
> +               else
> +                       dequeue_mid(server, mid, false);
> +               return 0;
> +       }
> +
> +       /* Success - parse the response data */
> +       cifs_dbg(FYI, "%s: data_offset=%u data_len=%u buf_len=%u buffer_len=%u\n",
> +                __func__, data_offset, data_len, buf_len, buffer_len);
> +
> +       /* Validate data_offset */
> +       if (data_offset < sizeof(struct smb2_query_directory_rsp)) {
> +               cifs_dbg(FYI, "%s: data offset (%u) inside response header\n",
> +                        __func__, data_offset);
> +               data_offset = sizeof(struct smb2_query_directory_rsp);
> +       } else if (data_offset > MAX_CIFS_SMALL_BUFFER_SIZE) {
> +               cifs_dbg(VFS, "%s: data offset (%u) beyond end of smallbuf\n",
> +                        __func__, data_offset);
> +               qd_io->result = -EIO;
> +               dequeue_mid(server, mid, qd_io->result);
> +               return qd_io->result;
> +       }
> +
> +       /* Validate data_offset is within buf_len + buffer_len */
> +       if (data_offset > buf_len + buffer_len) {
> +               cifs_dbg(VFS, "%s: data offset (%u) beyond response length (%u)\n",
> +                        __func__, data_offset, buf_len + buffer_len);
> +               qd_io->result = -EIO;
> +               dequeue_mid(server, mid, qd_io->result);
> +               return qd_io->result;
> +       }
> +
> +       /* Validate response fits in pre-allocated combined buffer */
> +       if ((size_t)data_offset + data_len > qd_io->combined_iov.iov_len) {
> +               cifs_dbg(VFS, "%s: response (%u + %u) exceeds buffer capacity (%zu)\n",
> +                        __func__, data_offset, data_len, qd_io->combined_iov.iov_len);
> +               qd_io->result = -EIO;
> +               dequeue_mid(server, mid, qd_io->result);
> +               return qd_io->result;
> +       }
> +
> +       /* Copy the prefix present in buf into combined_iov, preserving wire layout */
> +       memcpy(qd_io->combined_iov.iov_base, buf, min(data_offset, buf_len));
> +
> +       if (data_offset < buf_len) {
> +               /* Data starts in buf, may continue into buffer */
> +               unsigned int data_in_buf = buf_len - data_offset;
> +               unsigned int data_in_buffer;
> +
> +               if (data_len <= data_in_buf) {
> +                       /* All data is in buf */
> +                       memcpy(qd_io->combined_iov.iov_base + data_offset,
> +                              buf + data_offset, data_len);
> +               } else {
> +                       /* Copy from buf first */
> +                       memcpy(qd_io->combined_iov.iov_base + data_offset,
> +                              buf + data_offset, data_in_buf);
> +
> +                       /* Copy remainder from buffer at offset 0 */
> +                       data_in_buffer = data_len - data_in_buf;
> +                       if (data_in_buffer > buffer_len) {
> +                               cifs_dbg(VFS, "%s: data_in_buffer (%u) > buffer_len (%u)\n",
> +                                        __func__, data_in_buffer, buffer_len);
> +                               qd_io->result = -EIO;
> +                               dequeue_mid(server, mid, qd_io->result);
> +                               return qd_io->result;
> +                       }
> +
> +                       qd_io->result = cifs_copy_folioq_to_buf(buffer, buffer_len, 0,
> +                                                               qd_io->combined_iov.iov_base +
> +                                                               data_offset + data_in_buf,
> +                                                               data_in_buffer);
> +                       if (qd_io->result != 0) {
> +                               cifs_dbg(VFS, "%s: failed to copy from folio_queue: %d\n",
> +                                        __func__, qd_io->result);
> +                               dequeue_mid(server, mid, qd_io->result);
> +                               return qd_io->result;
> +                       }
> +               }
> +       } else {
> +               /* Padding and data are in buffer starting at offset 0 */
> +               unsigned int bytes_in_buffer = data_offset - buf_len + data_len;
> +
> +               if (bytes_in_buffer > buffer_len) {
> +                       cifs_dbg(VFS, "%s: data beyond buffer: prefix+len=%u buffer_len=%u\n",
> +                                __func__, bytes_in_buffer, buffer_len);
> +                       qd_io->result = -EIO;
> +                       dequeue_mid(server, mid, qd_io->result);
> +                       return qd_io->result;
> +               }
> +
> +               qd_io->result = cifs_copy_folioq_to_buf(buffer, buffer_len, 0,
> +                                                       qd_io->combined_iov.iov_base + buf_len,
> +                                                       bytes_in_buffer);
> +               if (qd_io->result != 0) {
> +                       cifs_dbg(VFS, "%s: failed to copy from folio_queue: %d\n",
> +                                __func__, qd_io->result);
> +                       dequeue_mid(server, mid, qd_io->result);
> +                       return qd_io->result;
> +               }
> +       }
> +
> +       /* Set up iov[1] pointing into combined buffer, finalize valid length */
> +       qd_io->iov[1].iov_base = qd_io->combined_iov.iov_base + data_offset;
> +       qd_io->iov[1].iov_len = data_len;
> +       qd_io->combined_iov.iov_len = data_offset + data_len;
> +
> +       dequeue_mid(server, mid, false);
> +       return 0;
> +}
> +
> +/*
> + * Handle callback for async QueryDirectory with multi-credit support.
> + * For encrypted responses, extracts decrypted data.
> + * For unencrypted responses, cifs_query_dir_receive already processed everything.
> + */
> +int
> +smb2_query_dir_handle_data(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> +{
> +       char *buf = server->large_buf ? server->bigbuf : server->smallbuf;
> +
> +       /* For unencrypted responses, data already processed in cifs_query_dir_receive */
> +       if (!mid->decrypted)
> +               return 0;
> +
> +       /*
> +        * For small encrypted responses (< CIFSMaxBufSize), all data is in buf.
> +        * For large encrypted responses, this callback is not used - instead,
> +        * receive_encrypted_read/smb2_decrypt_offload call handle_query_dir_data directly.
> +        */
> +       return handle_query_dir_data(server, mid, buf, server->pdu_size,
> +                                     NULL, 0, false);
> +}
> +
>  static int
>  handle_read_data(struct TCP_Server_Info *server, struct mid_q_entry *mid,
>                  char *buf, unsigned int buf_len, struct folio_queue *buffer,
> @@ -4987,25 +5347,46 @@ static void smb2_decrypt_offload(struct work_struct *work)
>         int rc;
>         struct mid_q_entry *mid;
>         struct iov_iter iter;
> +       struct smb2_hdr *shdr;
> +       unsigned int read_rsp_size = dw->server->vals->read_rsp_size;
>
> +       /* decrypt read_rsp_size in buf + remainder in folio_queue */
>         iov_iter_folio_queue(&iter, ITER_DEST, dw->buffer, 0, 0, dw->len);
> -       rc = decrypt_raw_data(dw->server, dw->buf, dw->server->vals->read_rsp_size,
> -                             &iter, true);
> +       rc = decrypt_raw_data(dw->server, dw->buf, read_rsp_size, &iter, true);
>         if (rc) {
>                 cifs_dbg(VFS, "error decrypting rc=%d\n", rc);
>                 goto free_pages;
>         }
>
>         dw->server->lstrp = jiffies;
> +
> +       shdr = (struct smb2_hdr *)(dw->buf + sizeof(struct smb2_transform_hdr));
> +
> +       /*
> +        * buf now contains read_rsp_size bytes after transform_hdr.
> +        * folio_queue contains (total_len - read_rsp_size) bytes.
> +        * The handle functions will determine where data actually starts based on data_offset.
> +        */
> +
>         mid = smb2_find_dequeue_mid(dw->server, dw->buf);
>         if (mid == NULL)
>                 cifs_dbg(FYI, "mid not found\n");
>         else {
>                 mid->decrypted = true;
> -               rc = handle_read_data(dw->server, mid, dw->buf,
> -                                     dw->server->vals->read_rsp_size,
> -                                     dw->buffer, dw->len,
> -                                     true);
> +
> +               /* Handle based on command type */
> +               if (le16_to_cpu(shdr->Command) == SMB2_READ) {
> +                       rc = handle_read_data(dw->server, mid, dw->buf, read_rsp_size,
> +                                             dw->buffer, dw->len, true);
> +               } else if (le16_to_cpu(shdr->Command) == SMB2_QUERY_DIRECTORY) {
> +                       rc = handle_query_dir_data(dw->server, mid, dw->buf, read_rsp_size,
> +                                                  dw->buffer, dw->len, true);
> +               } else {
> +                       cifs_dbg(VFS, "Unexpected command %u in decrypt offload\n",
> +                                le16_to_cpu(shdr->Command));
> +                       rc = -EOPNOTSUPP;
> +               }
> +
>                 if (rc >= 0) {
>  #ifdef CONFIG_CIFS_STATS2
>                         mid->when_received = jiffies;
> @@ -5049,9 +5430,10 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
>  {
>         char *buf = server->smallbuf;
>         struct smb2_transform_hdr *tr_hdr = (struct smb2_transform_hdr *)buf;
> +       struct smb2_hdr *shdr;
>         struct iov_iter iter;
> -       unsigned int len;
> -       unsigned int buflen = server->pdu_size;
> +       unsigned int len, total_len, buflen = server->pdu_size;
> +       unsigned int read_rsp_size = server->vals->read_rsp_size;
>         int rc;
>         struct smb2_decrypt_work *dw;
>
> @@ -5062,7 +5444,10 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
>         dw->server = server;
>
>         *num_mids = 1;
> -       len = min_t(unsigned int, buflen, server->vals->read_rsp_size +
> +       total_len = le32_to_cpu(tr_hdr->OriginalMessageSize);
> +
> +       /* Read transform_hdr + read_rsp_size into buf */
> +       len = min_t(unsigned int, buflen, read_rsp_size +
>                 sizeof(struct smb2_transform_hdr)) - HEADER_SIZE(server) + 1;
>
>         rc = cifs_read_from_socket(server, buf + HEADER_SIZE(server) - 1, len);
> @@ -5070,9 +5455,8 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
>                 goto free_dw;
>         server->total_read += rc;
>
> -       len = le32_to_cpu(tr_hdr->OriginalMessageSize) -
> -               server->vals->read_rsp_size;
> -       dw->len = len;
> +       /* Read remaining data into folio_queue */
> +       dw->len = total_len - read_rsp_size;
>         len = round_up(dw->len, PAGE_SIZE);
>
>         size_t cur_size = 0;
> @@ -5101,7 +5485,7 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
>                 goto free_pages;
>
>         /*
> -        * For large reads, offload to different thread for better performance,
> +        * For large responses, offload to different thread for better performance,
>          * use more cores decrypting which can be expensive
>          */
>
> @@ -5115,20 +5499,41 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
>                 return -1;
>         }
>
> -       rc = decrypt_raw_data(server, buf, server->vals->read_rsp_size,
> -                             &iter, false);
> +       /* Decrypt: read_rsp_size in buf + remainder in folio_queue */
> +       rc = decrypt_raw_data(server, buf, read_rsp_size, &iter, false);
>         if (rc)
>                 goto free_pages;
>
> +       shdr = (struct smb2_hdr *) buf;
> +
> +       /*
> +        * buf now contains the complete response header (read_rsp_size bytes).
> +        * folio_queue contains (total_len - read_rsp_size) bytes.
> +        * The handle functions will determine where data actually starts based on data_offset.
> +        */
> +
>         *mid = smb2_find_mid(server, buf);
>         if (*mid == NULL) {
>                 cifs_dbg(FYI, "mid not found\n");
>         } else {
> -               cifs_dbg(FYI, "mid found\n");
> +               cifs_dbg(FYI, "mid found, command=%u\n", le16_to_cpu(shdr->Command));
>                 (*mid)->decrypted = true;
> -               rc = handle_read_data(server, *mid, buf,
> -                                     server->vals->read_rsp_size,
> -                                     dw->buffer, dw->len, false);
> +
> +               /* Handle based on command type */
> +               if (le16_to_cpu(shdr->Command) == SMB2_READ) {
> +                       rc = handle_read_data(server, *mid, buf, read_rsp_size,
> +                                             dw->buffer, dw->len, false);
> +               } else if (le16_to_cpu(shdr->Command) == SMB2_QUERY_DIRECTORY) {
> +                       rc = handle_query_dir_data(server, *mid, buf, read_rsp_size,
> +                                                  dw->buffer, dw->len, false);
> +               } else {
> +                       /* For now, other commands not supported in large encrypted path */
> +                       cifs_server_dbg(VFS,
> +                                       "Large encrypted responses only supported for SMB2_READ and SMB2_QUERY_DIRECTORY (got %u)\n",
> +                                       le16_to_cpu(shdr->Command));
> +                       rc = -EOPNOTSUPP;
> +               }
> +
>                 if (rc >= 0) {
>                         if (server->ops->is_network_name_deleted) {
>                                 server->ops->is_network_name_deleted(buf,
> @@ -5269,7 +5674,6 @@ smb3_receive_transform(struct TCP_Server_Info *server,
>                 return -ECONNABORTED;
>         }
>
> -       /* TODO: add support for compounds containing READ. */
>         if (pdu_length > CIFSMaxBufSize + MAX_HEADER_SIZE(server)) {
>                 return receive_encrypted_read(server, &mids[0], num_mids);
>         }
> diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
> index 2d55246d2851b..b59d99d5e193a 100644
> --- a/fs/smb/client/smb2pdu.c
> +++ b/fs/smb/client/smb2pdu.c
> @@ -5496,6 +5496,182 @@ num_entries(int infotype, char *bufstart, char *end_of_buf, char **lastentry,
>         return entrycount;
>  }
>
> +/*
> + * Callback for async QueryDirectory with multi-credit support
> + */
> +static void
> +smb2_query_dir_callback(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> +{
> +       struct cifs_query_dir_io *qd_io = mid->callback_data;
> +       struct cifs_tcon *tcon = qd_io->tcon;
> +       struct smb2_hdr *shdr = (struct smb2_hdr *)qd_io->iov[0].iov_base;
> +       struct cifs_credits credits = {
> +               .value = 0,
> +               .instance = 0,
> +       };
> +
> +       WARN_ONCE(qd_io->server != server,
> +                 "qd_io server %p != mid server %p",
> +                 qd_io->server, server);
> +
> +       cifs_dbg(FYI, "%s: mid=%llu state=%d result=%d\n",
> +                __func__, mid->mid, mid->mid_state, qd_io->result);
> +
> +       switch (mid->mid_state) {
> +       case MID_RESPONSE_RECEIVED:
> +               credits.value = le16_to_cpu(shdr->CreditRequest);
> +               credits.instance = server->reconnect_instance;
> +               /* result already set, check signature if needed */
> +               if (server->sign && !mid->decrypted) {
> +                       int rc;
> +                       struct smb_rqst rqst = { .rq_iov = &qd_io->iov[0], .rq_nvec = 1 };
> +
> +                       rc = smb2_verify_signature(&rqst, server);
> +                       if (rc) {
> +                               cifs_tcon_dbg(VFS, "QueryDir signature verification returned error = %d\n",
> +                                             rc);
> +                               qd_io->result = rc;
> +                       }
> +               }
> +               break;
> +       case MID_REQUEST_SUBMITTED:
> +       case MID_RETRY_NEEDED:
> +               qd_io->result = -EAGAIN;
> +               break;
> +       case MID_RESPONSE_MALFORMED:
> +               credits.value = le16_to_cpu(shdr->CreditRequest);
> +               credits.instance = server->reconnect_instance;
> +               qd_io->result = smb_EIO(smb_eio_trace_read_rsp_malformed);
> +               break;
> +       default:
> +               qd_io->result = smb_EIO1(smb_eio_trace_read_mid_state_unknown,
> +                                        mid->mid_state);
> +               break;
> +       }
> +
> +       if (qd_io->result && qd_io->result != -ENODATA)
> +               cifs_stats_fail_inc(tcon, SMB2_QUERY_DIRECTORY_HE);
> +
> +       trace_smb3_rw_credits(0, 0, qd_io->credits.value,
> +                             server->credits, server->in_flight,
> +                             0, cifs_trace_rw_credits_read_response_clear);
> +       qd_io->credits.value = 0;
> +       release_mid(server, mid);
> +       trace_smb3_rw_credits(0, 0, 0,
> +                             server->credits, server->in_flight,
> +                             credits.value, cifs_trace_rw_credits_read_response_add);
> +       add_credits(server, &credits, 0);
> +
> +       complete(&qd_io->done);
> +}
> +
> +/*
> + * QueryDirectory with large buffer and multi-credit support.
> + * Uses async infrastructure but waits for completion synchronously.
> + */
> +int
> +SMB2_query_directory_large(struct cifs_query_dir_io *qd_io, unsigned int buf_size)
> +{
> +       int rc, flags = 0;
> +       char *buf;
> +       struct smb2_hdr *shdr;
> +       struct smb_rqst rqst = { .rq_iov = &qd_io->iov[0],
> +                                .rq_nvec = SMB2_QUERY_DIRECTORY_IOV_SIZE };
> +       struct TCP_Server_Info *server = qd_io->server;
> +       struct cifs_tcon *tcon = qd_io->tcon;
> +       unsigned int total_len;
> +       int credit_request;
> +
> +       cifs_dbg(FYI, "%s: buf_size=%u\n", __func__, buf_size);
> +
> +       /* Cap buffer size to avoid kmalloc failures for very large allocations.
> +        * SMB2_MAX_QD_DATABUF_SIZE is a safe limit that stays well below typical
> +        * kmalloc constraints while still allowing large directory listings.
> +        */
> +       if (buf_size > SMB2_MAX_QD_DATABUF_SIZE)
> +               buf_size = SMB2_MAX_QD_DATABUF_SIZE;
> +
> +       /* Allocate response buffer. Since we'll build a combined header+data buffer,
> +        * we need space for both. We'll request a slightly smaller OutputBufferLength
> +        * from the server to ensure the total response fits.
> +        */
> +       qd_io->combined_iov.iov_base = kmalloc(buf_size, GFP_KERNEL);
> +       if (!qd_io->combined_iov.iov_base)
> +               return -ENOMEM;
> +       /* Store total capacity in iov_len; updated to actual data length by receive handler */
> +       qd_io->combined_iov.iov_len = buf_size;
> +
> +       /* Initialize completion */
> +       init_completion(&qd_io->done);
> +
> +       /* Request less data from server to leave room for the response header.
> +        * Use MAX_CIFS_SMALL_BUFFER_SIZE as a safety margin.
> +        */
> +       rc = SMB2_query_directory_init(qd_io->xid, tcon, server,
> +                                      &rqst, qd_io->persistent_fid,
> +                                      qd_io->volatile_fid, qd_io->index,
> +                                      qd_io->srch_inf->info_level,
> +                                      buf_size - MAX_CIFS_SMALL_BUFFER_SIZE);
> +       if (rc) {
> +               kfree(qd_io->combined_iov.iov_base);
> +               qd_io->combined_iov.iov_base = NULL;
> +               return rc;
> +       }
> +
> +       if (smb3_encryption_required(tcon))
> +               flags |= CIFS_TRANSFORM_REQ;
> +
> +       buf = rqst.rq_iov[0].iov_base;
> +       total_len = rqst.rq_iov[0].iov_len;
> +
> +       shdr = (struct smb2_hdr *)buf;
> +
> +       if (qd_io->replay) {
> +               /* Back-off before retry */
> +               if (qd_io->cur_sleep)
> +                       msleep(qd_io->cur_sleep);
> +               smb2_set_replay(server, &rqst);
> +       }
> +
> +       /* Set credit charge based on buffer size */
> +       if (qd_io->credits.value > 0) {
> +               shdr->CreditCharge = cpu_to_le16(DIV_ROUND_UP(buf_size,
> +                                               SMB2_MAX_BUFFER_SIZE));
> +               credit_request = le16_to_cpu(shdr->CreditCharge) + 8;
> +               if (server->credits >= server->max_credits)
> +                       shdr->CreditRequest = cpu_to_le16(0);
> +               else
> +                       shdr->CreditRequest = cpu_to_le16(
> +                               min_t(int, server->max_credits -
> +                                               server->credits, credit_request));
> +
> +               flags |= CIFS_HAS_CREDITS;
> +       }
> +
> +       rc = cifs_call_async(server, &rqst,
> +                            cifs_query_dir_receive, smb2_query_dir_callback,
> +                            smb2_query_dir_handle_data, qd_io, flags,
> +                            &qd_io->credits);
> +       if (rc) {
> +               cifs_stats_fail_inc(tcon, SMB2_QUERY_DIRECTORY_HE);
> +               trace_smb3_query_dir_err(qd_io->xid, qd_io->persistent_fid,
> +                                        tcon->tid, tcon->ses->Suid,
> +                                        qd_io->index, 0, rc);
> +               kfree(qd_io->combined_iov.iov_base);
> +               qd_io->combined_iov.iov_base = NULL;
> +       }
> +
> +       /* Free request buffer immediately after async call */
> +       cifs_small_buf_release(buf);
> +
> +       if (rc)
> +               return rc;
> +
> +       /* Wait for the async operation to complete */
> +       wait_for_completion(&qd_io->done);
> +       return qd_io->result;
> +}
> +
>  /*
>   * Readdir/FindFirst
>   */
> @@ -5560,12 +5736,6 @@ int SMB2_query_directory_init(const unsigned int xid,
>         req->FileNameOffset =
>                 cpu_to_le16(sizeof(struct smb2_query_directory_req));
>         req->FileNameLength = cpu_to_le16(len);
> -       /*
> -        * BB could be 30 bytes or so longer if we used SMB2 specific
> -        * buffer lengths, but this is safe and close enough.
> -        */
> -       output_size = min_t(unsigned int, output_size, server->maxBuf);
> -       output_size = min_t(unsigned int, output_size, 2 << 15);
>         req->OutputBufferLength = cpu_to_le32(output_size);
>
>         iov[0].iov_base = (char *)req;
> diff --git a/fs/smb/client/smb2pdu.h b/fs/smb/client/smb2pdu.h
> index 7b7a864520c68..843e30c1ecc61 100644
> --- a/fs/smb/client/smb2pdu.h
> +++ b/fs/smb/client/smb2pdu.h
> @@ -132,6 +132,9 @@ struct share_redirect_error_context_rsp {
>  /* Size of the minimal QueryDir response for checking if more data exists */
>  #define SMB2_QD2_RESPONSE_SIZE 4096
>
> +/* max query directory data buffer size */
> +#define SMB2_MAX_QD_DATABUF_SIZE (2 * 1024 * 1024)
> +
>  /*
>   * Output buffer size for first QueryDir in Create+QD1+QD2 compound.
>   * Accounts for shared buffer space needed for all three responses.
> diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> index 9de7d2fe8d466..9607e8899f7ff 100644
> --- a/fs/smb/client/smb2proto.h
> +++ b/fs/smb/client/smb2proto.h
> @@ -46,6 +46,8 @@ __le32 smb2_get_lease_state(struct cifsInodeInfo *cinode, unsigned int oplock);
>  bool smb2_is_valid_oplock_break(char *buffer, struct TCP_Server_Info *server);
>  int smb3_handle_read_data(struct TCP_Server_Info *server,
>                           struct mid_q_entry *mid);
> +int smb2_query_dir_handle_data(struct TCP_Server_Info *server,
> +                              struct mid_q_entry *mid);
>  struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
>                                         struct super_block *sb,
>                                         const unsigned int xid,
> @@ -197,6 +199,7 @@ int SMB2_query_directory_init(const unsigned int xid, struct cifs_tcon *tcon,
>                               u64 volatile_fid, int index, int info_level,
>                               unsigned int output_size);
>  void SMB2_query_directory_free(struct smb_rqst *rqst);
> +int SMB2_query_directory_large(struct cifs_query_dir_io *qd_io, unsigned int buf_size);
>  int SMB2_set_eof(const unsigned int xid, struct cifs_tcon *tcon,
>                  u64 persistent_fid, u64 volatile_fid, u32 pid,
>                  loff_t new_eof);
> diff --git a/fs/smb/client/trace.h b/fs/smb/client/trace.h
> index acfbb63086ea2..54ee1317c5b12 100644
> --- a/fs/smb/client/trace.h
> +++ b/fs/smb/client/trace.h
> @@ -165,6 +165,7 @@
>         EM(cifs_trace_rw_credits_write_prepare,         "wr-prepare ") \
>         EM(cifs_trace_rw_credits_write_response_add,    "wr-resp-add") \
>         EM(cifs_trace_rw_credits_write_response_clear,  "wr-resp-clr") \
> +       EM(cifs_trace_rw_credits_query_dir_done,        "qd-done    ") \
>         E_(cifs_trace_rw_credits_zero_in_flight,        "ZERO-IN-FLT")
>
>  #define smb3_tcon_ref_traces                                         \
> diff --git a/fs/smb/client/transport.c b/fs/smb/client/transport.c
> index 05f8099047e1a..4457b98b9365d 100644
> --- a/fs/smb/client/transport.c
> +++ b/fs/smb/client/transport.c
> @@ -1154,6 +1154,190 @@ cifs_readv_discard(struct TCP_Server_Info *server, struct mid_q_entry *mid)
>         return  __cifs_readv_discard(server, mid, rdata->result);
>  }
>
> +static int
> +cifs_query_dir_discard(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> +{
> +       int length;
> +       struct cifs_query_dir_io *qd_io = mid->callback_data;
> +
> +       length = cifs_discard_remaining_data(server);
> +       dequeue_mid(server, mid, qd_io->result != 0);
> +       mid->resp_buf = server->smallbuf;
> +       server->smallbuf = NULL;
> +       return length;
> +}
> +
> +/*
> + * Receive handler for async QueryDirectory with multi-credit support
> + */
> +int
> +cifs_query_dir_receive(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> +{
> +       int length, len;
> +       unsigned int data_offset, data_len, resp_size;
> +       struct cifs_query_dir_io *qd_io = mid->callback_data;
> +       char *buf = server->smallbuf;
> +       unsigned int buflen = server->pdu_size;
> +       struct smb2_query_directory_rsp *rsp;
> +
> +       cifs_dbg(FYI, "%s: mid=%llu buf_capacity=%zu\n",
> +                __func__, mid->mid, qd_io->combined_iov.iov_len);
> +
> +       /*
> +        * Read the rest of QUERY_DIRECTORY_RSP header (sans Data array).
> +        * QueryDirectory response structure is 64 bytes (SMB2 header) + 8 bytes (fixed part).
> +        */
> +       resp_size = sizeof(struct smb2_query_directory_rsp);
> +       len = min_t(unsigned int, buflen, resp_size) - HEADER_SIZE(server) + 1;
> +
> +       length = cifs_read_from_socket(server,
> +                                      buf + HEADER_SIZE(server) - 1, len);
> +       if (length < 0) {
> +               qd_io->result = length;
> +               goto discard_and_queue;
> +       }
> +       server->total_read += length;
> +
> +       if (server->ops->is_session_expired &&
> +           server->ops->is_session_expired(buf)) {
> +               cifs_reconnect(server, true);
> +               return -1;
> +       }
> +
> +       if (server->ops->is_status_pending &&
> +           server->ops->is_status_pending(buf, server)) {
> +               cifs_discard_remaining_data(server);
> +               return -1;
> +       }
> +
> +       /* Is there enough to get to the rest of the QUERY_DIRECTORY_RSP header? */
> +       if (server->total_read < resp_size) {
> +               cifs_dbg(FYI, "%s: server returned short header. got=%u expected=%u\n",
> +                        __func__, server->total_read, resp_size);
> +               qd_io->result = smb_EIO2(smb_eio_trace_read_rsp_short,
> +                                        server->total_read, resp_size);
> +               return cifs_query_dir_discard(server, mid);
> +       }
> +
> +       /* Set up first iov for signature check and to get credits */
> +       qd_io->iov[0].iov_base = buf;
> +       qd_io->iov[0].iov_len = server->total_read;
> +       qd_io->iov[1].iov_base = NULL;
> +       qd_io->iov[1].iov_len = 0;
> +       cifs_dbg(FYI, "0: iov_base=%p iov_len=%zu\n",
> +                qd_io->iov[0].iov_base, qd_io->iov[0].iov_len);
> +
> +       /* Parse header early to access status before map_error converts it */
> +       rsp = (struct smb2_query_directory_rsp *)buf;
> +
> +       /* Was the SMB query_directory successful? */
> +       qd_io->result = server->ops->map_error(buf, false);
> +       if (qd_io->result != 0) {
> +               if (qd_io->combined_iov.iov_base && qd_io->iov[0].iov_len > 0 &&
> +                   qd_io->iov[0].iov_len <= qd_io->combined_iov.iov_len) {
> +                       memcpy(qd_io->combined_iov.iov_base, qd_io->iov[0].iov_base,
> +                              qd_io->iov[0].iov_len);
> +                       qd_io->iov[0].iov_base = qd_io->combined_iov.iov_base;
> +               }
> +               cifs_dbg(FYI, "%s: server returned error %d (Status was 0x%x)\n",
> +                        __func__, qd_io->result, le32_to_cpu(rsp->hdr.Status));
> +               return cifs_query_dir_discard(server, mid);
> +       }
> +
> +       data_offset = le16_to_cpu(rsp->OutputBufferOffset);
> +       data_len = le32_to_cpu(rsp->OutputBufferLength);
> +
> +       cifs_dbg(FYI, "%s: total_read=%u data_offset=%u data_len=%u\n",
> +                __func__, server->total_read, data_offset, data_len);
> +
> +       /* Validate data_offset and data_len */
> +       if (data_offset < server->total_read) {
> +               cifs_dbg(FYI, "%s: data offset (%u) inside response header\n",
> +                        __func__, data_offset);
> +               data_offset = server->total_read;
> +               data_len = 0;
> +       } else if (data_offset > MAX_CIFS_SMALL_BUFFER_SIZE) {
> +               cifs_dbg(FYI, "%s: data offset (%u) beyond end of smallbuf\n",
> +                        __func__, data_offset);
> +               qd_io->result = smb_EIO1(smb_eio_trace_read_overlarge,
> +                                        data_offset);
> +               return cifs_query_dir_discard(server, mid);
> +       }
> +
> +       /* Read any padding between header and data */
> +       len = data_offset - server->total_read;
> +       if (len > 0) {
> +               length = cifs_read_from_socket(server,
> +                                              buf + server->total_read, len);
> +               if (length < 0) {
> +                       qd_io->result = length;
> +                       goto discard_and_queue;
> +               }
> +               server->total_read += length;
> +               qd_io->iov[0].iov_len = server->total_read;
> +       }
> +
> +       /* Check if data fits in the pre-allocated combined buffer */
> +       if (qd_io->iov[0].iov_len + data_len > qd_io->combined_iov.iov_len) {
> +               cifs_dbg(VFS, "%s: response (%zu + %u) exceeds buffer capacity (%zu)\n",
> +                        __func__, qd_io->iov[0].iov_len, data_len, qd_io->combined_iov.iov_len);
> +               qd_io->result = smb_EIO2(smb_eio_trace_read_rsp_malformed,
> +                                        data_len, (unsigned int)qd_io->combined_iov.iov_len);
> +               return cifs_query_dir_discard(server, mid);
> +       }
> +
> +       /*
> +        * Build combined header+data in qd_io->combined_iov.iov_base.
> +        * Layout: [header][data] with no padding between them.
> +        * We update OutputBufferOffset in the header to reflect the new layout.
> +        */
> +       if (qd_io->iov[0].iov_len > 0 && qd_io->result == 0) {
> +               size_t hdr_len = qd_io->iov[0].iov_len;
> +               struct smb2_query_directory_rsp *combined_rsp;
> +
> +               /* Copy header to beginning of combined buffer */
> +               memcpy(qd_io->combined_iov.iov_base, qd_io->iov[0].iov_base, hdr_len);
> +
> +               /* Read directory entries directly after the header, removing any padding */
> +               if (data_len > 0) {
> +                       length = cifs_read_from_socket(server,
> +                                                      qd_io->combined_iov.iov_base + hdr_len,
> +                                                      data_len);
> +                       if (length < 0) {
> +                               qd_io->result = length;
> +                               goto discard_and_queue;
> +                       }
> +                       server->total_read += length;
> +
> +                       /* Update OutputBufferOffset and OutputBufferLength to reflect
> +                        * the new compact layout (header followed immediately by data).
> +                        */
> +                       combined_rsp = (struct smb2_query_directory_rsp *)qd_io->combined_iov.iov_base;
> +                       combined_rsp->OutputBufferOffset = cpu_to_le16(hdr_len);
> +                       combined_rsp->OutputBufferLength = cpu_to_le32(length);
> +
> +                       /* Set up second iov pointing to the directory data within combined buffer */
> +                       qd_io->iov[1].iov_base = qd_io->combined_iov.iov_base + hdr_len;
> +                       qd_io->iov[1].iov_len = length;
> +               }
> +
> +               qd_io->combined_iov.iov_len = hdr_len + (data_len > 0 ? length : 0);
> +
> +               cifs_dbg(FYI, "total_read=%u buflen=%u data_len=%u hdr_len=%zu combined_len=%zu\n",
> +                        server->total_read, buflen, data_len, hdr_len, qd_io->combined_iov.iov_len);
> +       }
> +
> +discard_and_queue:
> +       /* Discard any remaining data (shouldn't be any) */
> +       if (server->total_read < buflen)
> +               cifs_discard_remaining_data(server);
> +
> +       dequeue_mid(server, mid, false);
> +       mid->resp_buf = server->smallbuf;
> +       server->smallbuf = NULL;
> +       return length;
> +}
> +
>  int
>  cifs_readv_receive(struct TCP_Server_Info *server, struct mid_q_entry *mid)
>  {
> --
> 2.43.0
>


-- 
Thanks,

Steve

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH 6/7] cifs: optimize readdir for larger directories
  2026-04-15 23:08   ` Steve French
@ 2026-04-15 23:11     ` Steve French
  0 siblings, 0 replies; 9+ messages in thread
From: Steve French @ 2026-04-15 23:11 UTC (permalink / raw)
  To: nspmangalore
  Cc: linux-cifs, pc, bharathsm, dhowells, henrique.carvalho,
	ematsumiya, Shyam Prasad N

it also looks like it (this patch 6) has build warnings

  CHECK   smb2ops.c
smb2ops.c:5378:51: warning: restricted __le16 degrades to integer
smb2ops.c:5381:58: warning: restricted __le16 degrades to integer
smb2ops.c:5532:51: warning: restricted __le16 degrades to integer
smb2ops.c:5535:58: warning: restricted __le16 degrades to integer

On Wed, Apr 15, 2026 at 6:08 PM Steve French <smfrench@gmail.com> wrote:
>
> This (patch 6) hit a merge conflict with smb2ops.c when I tried to
> apply it to current cifs-2.6.git for-next for testing
>
> patching file fs/smb/client/smb2ops.c
> Hunk #6 FAILED at 5455.
> Hunk #7 succeeded at 5494 (offset 8 lines).
> Hunk #8 succeeded at 5508 (offset 8 lines).
> Hunk #9 succeeded at 5683 (offset 8 lines).
> 1 out of 9 hunks FAILED -- saving rejects to file fs/smb/client/smb2ops.c.rej
>
> On Tue, Apr 14, 2026 at 8:59 AM <nspmangalore@gmail.com> wrote:
> >
> > From: Shyam Prasad N <sprasad@microsoft.com>
> >
> > Today QueryDirectory uses the compound_send_recv infrastructure
> > which is limited to 16KB in size. As a result, readdir of large
> > directories generally take several round trips.
> >
> > With this change, if the readdir needs a QueryDir after the first
> > round-trip (meaning that there are more dirents to read), then the
> > following QueryDirs will now switch to using larger buffers with
> > MTU credits.
> >
> > Till now, the only command type that used this flow was SMB2_READ.
> > In case of encrypted response, it becomes challenging to decide if
> > the response is for SMB2_READ or SMB2_READDIR. This change reuses
> > receive_encrypted_read and after decrypting the response decides
> > the handling function based on the command in the resp header. That
> > way, care has been taken to ensure that the read code path
> > modifications on account of this change are kept to a minimum.
> >
> > Cc: David Howells <dhowells@redhat.com>
> > Signed-off-by: Shyam Prasad N <sprasad@microsoft.com>
> > ---
> >  fs/smb/client/cifsglob.h  |  23 +-
> >  fs/smb/client/cifsproto.h |   3 +
> >  fs/smb/client/readdir.c   |   2 +-
> >  fs/smb/client/smb1ops.c   |   4 +-
> >  fs/smb/client/smb2misc.c  |   7 +-
> >  fs/smb/client/smb2ops.c   | 452 ++++++++++++++++++++++++++++++++++++--
> >  fs/smb/client/smb2pdu.c   | 182 ++++++++++++++-
> >  fs/smb/client/smb2pdu.h   |   3 +
> >  fs/smb/client/smb2proto.h |   3 +
> >  fs/smb/client/trace.h     |   1 +
> >  fs/smb/client/transport.c | 184 ++++++++++++++++
> >  11 files changed, 829 insertions(+), 35 deletions(-)
> >
> > diff --git a/fs/smb/client/cifsglob.h b/fs/smb/client/cifsglob.h
> > index 8d089ba08e3e5..45de35ecc0bd2 100644
> > --- a/fs/smb/client/cifsglob.h
> > +++ b/fs/smb/client/cifsglob.h
> > @@ -504,7 +504,7 @@ struct smb_version_operations {
> >                                struct cifs_search_info *);
> >         /* continue readdir */
> >         int (*query_dir_next)(const unsigned int, struct cifs_tcon *,
> > -                             struct cifs_fid *,
> > +                             struct cifs_sb_info *, struct cifs_fid *,
> >                               __u16, struct cifs_search_info *srch_inf);
> >         /* close dir */
> >         int (*close_dir)(const unsigned int, struct cifs_tcon *,
> > @@ -1397,6 +1397,27 @@ struct cifs_search_info {
> >         bool is_dynamic_buf:1; /* dynamically allocated buffer - can be variable size */
> >  };
> >
> > +/* Structure for QueryDirectory with multi-credit support */
> > +struct cifs_query_dir_io {
> > +       struct cifs_tcon *tcon;
> > +       struct TCP_Server_Info *server;
> > +       struct cifs_search_info *srch_inf;
> > +       unsigned int xid;
> > +       u64 persistent_fid;
> > +       u64 volatile_fid;
> > +       int index;
> > +       struct kvec combined_iov;       /* Pre-allocated combined header+data buffer;
> > +                                        * iov_len holds total capacity until the receive
> > +                                        * handler fills it in, then actual valid length */
> > +       struct completion done;
> > +       int result;
> > +       struct cifs_credits credits;
> > +       bool replay;
> > +       unsigned int retries;
> > +       unsigned int cur_sleep;
> > +       struct kvec iov[2];             /* For response handling */
> > +};
> > +
> >  #define ACL_NO_MODE    ((umode_t)(-1))
> >  struct cifs_open_parms {
> >         struct cifs_tcon *tcon;
> > diff --git a/fs/smb/client/cifsproto.h b/fs/smb/client/cifsproto.h
> > index 884bfa1cf0b42..bbbee0ef09443 100644
> > --- a/fs/smb/client/cifsproto.h
> > +++ b/fs/smb/client/cifsproto.h
> > @@ -336,6 +336,9 @@ struct cifs_ses *cifs_get_smb_ses(struct TCP_Server_Info *server,
> >  int cifs_readv_receive(struct TCP_Server_Info *server,
> >                        struct mid_q_entry *mid);
> >
> > +int cifs_query_dir_receive(struct TCP_Server_Info *server,
> > +                           struct mid_q_entry *mid);
> > +
> >  int cifs_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
> >                           struct cifs_sb_info *cifs_sb,
> >                           const unsigned char *path, char *pbuf,
> > diff --git a/fs/smb/client/readdir.c b/fs/smb/client/readdir.c
> > index b50efd9b9e1d2..8a444f97e0ae9 100644
> > --- a/fs/smb/client/readdir.c
> > +++ b/fs/smb/client/readdir.c
> > @@ -760,7 +760,7 @@ find_cifs_entry(const unsigned int xid, struct cifs_tcon *tcon, loff_t pos,
> >         while ((index_to_find >= cfile->srch_inf.index_of_last_entry) &&
> >                (rc == 0) && !cfile->srch_inf.endOfSearch) {
> >                 cifs_dbg(FYI, "calling findnext2\n");
> > -               rc = server->ops->query_dir_next(xid, tcon, &cfile->fid,
> > +               rc = server->ops->query_dir_next(xid, tcon, cifs_sb, &cfile->fid,
> >                                                  search_flags,
> >                                                  &cfile->srch_inf);
> >                 if (rc)
> > diff --git a/fs/smb/client/smb1ops.c b/fs/smb/client/smb1ops.c
> > index 9694117050a6c..860a9b23a2f8d 100644
> > --- a/fs/smb/client/smb1ops.c
> > +++ b/fs/smb/client/smb1ops.c
> > @@ -1135,8 +1135,8 @@ cifs_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
> >
> >  static int
> >  cifs_query_dir_next(const unsigned int xid, struct cifs_tcon *tcon,
> > -                   struct cifs_fid *fid, __u16 search_flags,
> > -                   struct cifs_search_info *srch_inf)
> > +                   struct cifs_sb_info *cifs_sb, struct cifs_fid *fid,
> > +                   __u16 search_flags, struct cifs_search_info *srch_inf)
> >  {
> >         return CIFSFindNext(xid, tcon, fid->netfid, search_flags, srch_inf);
> >  }
> > diff --git a/fs/smb/client/smb2misc.c b/fs/smb/client/smb2misc.c
> > index 973fce3c959c4..b7b6ecd5fdaee 100644
> > --- a/fs/smb/client/smb2misc.c
> > +++ b/fs/smb/client/smb2misc.c
> > @@ -12,6 +12,7 @@
> >  #include "cifsglob.h"
> >  #include "cifsproto.h"
> >  #include "smb2proto.h"
> > +#include "smb2pdu.h"
> >  #include "cifs_debug.h"
> >  #include "cifs_unicode.h"
> >  #include "../common/smb2status.h"
> > @@ -316,7 +317,7 @@ char *
> >  smb2_get_data_area_len(int *off, int *len, struct smb2_hdr *shdr)
> >  {
> >         const int max_off = 4096;
> > -       const int max_len = 128 * 1024;
> > +       int max_len = 128 * 1024;
> >
> >         *off = 0;
> >         *len = 0;
> > @@ -367,6 +368,10 @@ smb2_get_data_area_len(int *off, int *len, struct smb2_hdr *shdr)
> >                   ((struct smb2_query_directory_rsp *)shdr)->OutputBufferOffset);
> >                 *len = le32_to_cpu(
> >                   ((struct smb2_query_directory_rsp *)shdr)->OutputBufferLength);
> > +               /* Allow larger buffers for query directory (up to 2MB).
> > +                * The actual data is handled separately in cifs_query_dir_receive().
> > +                */
> > +               max_len = SMB2_MAX_QD_DATABUF_SIZE;
> >                 break;
> >         case SMB2_IOCTL:
> >                 *off = le32_to_cpu(
> > diff --git a/fs/smb/client/smb2ops.c b/fs/smb/client/smb2ops.c
> > index 5c6fe25204d26..ecbd717f5a5ea 100644
> > --- a/fs/smb/client/smb2ops.c
> > +++ b/fs/smb/client/smb2ops.c
> > @@ -2703,11 +2703,130 @@ smb2_query_dir_first(const unsigned int xid, struct cifs_tcon *tcon,
> >
> >  static int
> >  smb2_query_dir_next(const unsigned int xid, struct cifs_tcon *tcon,
> > -                   struct cifs_fid *fid, __u16 search_flags,
> > -                   struct cifs_search_info *srch_inf)
> > +                   struct cifs_sb_info *cifs_sb, struct cifs_fid *fid,
> > +                   __u16 search_flags, struct cifs_search_info *srch_inf)
> >  {
> > -       return SMB2_query_directory(xid, tcon, fid->persistent_fid,
> > -                                   fid->volatile_fid, 0, srch_inf);
> > +       struct cifs_query_dir_io qd_io;
> > +       struct TCP_Server_Info *server;
> > +       struct cifs_ses *ses = tcon->ses;
> > +       size_t buf_size;
> > +       int rc;
> > +       int resp_buftype = CIFS_DYNAMIC_BUFFER;
> > +
> > +       /* Pick server and determine buffer size based on negotiated rsize */
> > +       server = cifs_pick_channel(ses);
> > +       if (!server)
> > +               return smb_EIO(smb_eio_trace_null_pointers);
> > +
> > +       /* Negotiate rsize if not already set */
> > +       if (cifs_sb->ctx->rsize == 0)
> > +               cifs_negotiate_rsize(server, cifs_sb->ctx, tcon);
> > +
> > +       /* Use negotiated rsize for buffer size, with reasonable limits */
> > +       buf_size = cifs_sb->ctx->rsize;
> > +
> > +       cifs_dbg(FYI, "%s: using buffer size %zu (rsize=%u, encrypted=%d)\n",
> > +                __func__, buf_size, cifs_sb->ctx->rsize, smb3_encryption_required(tcon));
> > +
> > +       /* Initialize qd_io structure */
> > +       memset(&qd_io, 0, sizeof(qd_io));
> > +       qd_io.tcon = tcon;
> > +       qd_io.server = server;
> > +       qd_io.srch_inf = srch_inf;
> > +       qd_io.xid = xid;
> > +       qd_io.persistent_fid = fid->persistent_fid;
> > +       qd_io.volatile_fid = fid->volatile_fid;
> > +       qd_io.index = 0;
> > +       qd_io.result = 0;
> > +       qd_io.replay = false;
> > +       qd_io.retries = 0;
> > +       qd_io.cur_sleep = 0;
> > +
> > +       /* Allocate credits for the buffer size */
> > +       rc = server->ops->wait_mtu_credits(server, buf_size, &buf_size,
> > +                                          &qd_io.credits);
> > +       if (rc) {
> > +               cifs_dbg(VFS, "%s: failed to get credits: %d\n", __func__, rc);
> > +               return rc;
> > +       }
> > +
> > +       cifs_dbg(FYI, "%s: allocated %u credits for %zu bytes\n",
> > +                __func__, qd_io.credits.value, buf_size);
> > +
> > +       /* Send query directory with large buffer and wait for completion */
> > +       rc = SMB2_query_directory_large(&qd_io, buf_size);
> > +       if (rc) {
> > +               if (rc == -ENODATA) {
> > +                       const struct smb2_hdr *hdr = NULL;
> > +
> > +                       if (qd_io.combined_iov.iov_base)
> > +                               hdr = (const struct smb2_hdr *)qd_io.combined_iov.iov_base;
> > +                       else if (qd_io.iov[0].iov_base)
> > +                               hdr = (const struct smb2_hdr *)qd_io.iov[0].iov_base;
> > +
> > +                       /*
> > +                        * ENODATA from QUERY_DIRECTORY generally means enumeration reached
> > +                        * the end. Treat it as end-of-search even if the header buffer is
> > +                        * unavailable in this async path.
> > +                        */
> > +                       if (!hdr) {
> > +                               cifs_dbg(FYI, "%s: ENODATA but hdr is NULL\n", __func__);
> > +                       } else {
> > +                               cifs_dbg(FYI, "%s: ENODATA with hdr->Status=0x%x (STATUS_NO_MORE_FILES=0x%x)\n",
> > +                                        __func__, le32_to_cpu(hdr->Status), le32_to_cpu(STATUS_NO_MORE_FILES));
> > +                       }
> > +
> > +                       if (hdr && hdr->Status == STATUS_NO_MORE_FILES) {
> > +                               trace_smb3_query_dir_done(xid, fid->persistent_fid,
> > +                                       tcon->tid, tcon->ses->Suid, 0, 0);
> > +                               srch_inf->endOfSearch = true;
> > +                               rc = 0;
> > +                       } else {
> > +                               cifs_dbg(FYI, "%s: ENODATA but Status mismatch - not treating as end-of-search\n", __func__);
> > +                               trace_smb3_query_dir_err(xid, fid->persistent_fid,
> > +                                       tcon->tid, tcon->ses->Suid, 0, 0, rc);
> > +                       }
> > +               } else {
> > +                       trace_smb3_query_dir_err(xid, fid->persistent_fid,
> > +                               tcon->tid, tcon->ses->Suid, 0, 0, rc);
> > +               }
> > +               goto qdir_next_exit;
> > +       }
> > +
> > +       /* Parse the response using the combined buffer built in receive handler */
> > +       if (qd_io.combined_iov.iov_len > 0) {
> > +               rc = smb2_parse_query_directory(tcon, &qd_io.combined_iov, resp_buftype,
> > +                                               srch_inf);
> > +               if (rc) {
> > +                       trace_smb3_query_dir_err(xid, fid->persistent_fid,
> > +                               tcon->tid, tcon->ses->Suid, 0, 0, rc);
> > +                       kfree(qd_io.combined_iov.iov_base);
> > +                       qd_io.combined_iov.iov_base = NULL;
> > +                       goto qdir_next_exit;
> > +               }
> > +
> > +               /* combined_iov.iov_base ownership transferred to srch_inf->ntwrk_buf_start */
> > +               qd_io.combined_iov.iov_base = NULL;
> > +
> > +               trace_smb3_query_dir_done(xid, fid->persistent_fid,
> > +                       tcon->tid, tcon->ses->Suid, 0,
> > +                       srch_inf->entries_in_buffer);
> > +       }
> > +
> > +qdir_next_exit:
> > +       /* Free the data buffer if not transferred to srch_inf */
> > +       kfree(qd_io.combined_iov.iov_base);
> > +
> > +       /* Return credits if we still have them (they should have been cleared in callback) */
> > +       if (qd_io.credits.value != 0) {
> > +               trace_smb3_rw_credits(0, 0, 0,
> > +                                     server->credits, server->in_flight,
> > +                                     qd_io.credits.value,
> > +                                     cifs_trace_rw_credits_query_dir_done);
> > +               add_credits(server, &qd_io.credits, 0);
> > +       }
> > +
> > +       return rc;
> >  }
> >
> >  static int
> > @@ -4824,6 +4943,247 @@ cifs_copy_folioq_to_iter(struct folio_queue *folioq, size_t data_size,
> >         return 0;
> >  }
> >
> > +static int
> > +cifs_copy_folioq_to_buf(struct folio_queue *folioq, size_t total_size,
> > +                       size_t skip, char *buf, size_t buf_len)
> > +{
> > +       size_t copied = 0;
> > +
> > +       if (buf_len > total_size - skip)
> > +               buf_len = total_size - skip;
> > +
> > +       for (; folioq; folioq = folioq->next) {
> > +               for (int s = 0; s < folioq_count(folioq); s++) {
> > +                       struct folio *folio = folioq_folio(folioq, s);
> > +                       size_t fsize = folio_size(folio);
> > +                       size_t len = umin(fsize - skip, buf_len);
> > +
> > +                       if (len == 0)
> > +                               break;
> > +
> > +                       memcpy_from_folio(buf + copied, folio, skip, len);
> > +                       copied += len;
> > +                       buf_len -= len;
> > +                       skip = 0;
> > +
> > +                       if (buf_len == 0)
> > +                               return 0;
> > +               }
> > +       }
> > +
> > +       return 0;
> > +}
> > +
> > +/*
> > + * Handle encrypted QueryDirectory response data.
> > + * Called only for encrypted responses where mid->decrypted == true.
> > + * For unencrypted responses, cifs_query_dir_receive handles everything.
> > + *
> > + * This is written in such a way that handle_read_data does not need modification.
> > + *
> > + * buf: contains read_rsp_size bytes of decrypted response
> > + * buffer: contains (total_len - read_rsp_size) bytes of decrypted data
> > + *
> > + * Since sizeof(struct smb2_query_directory_rsp) < read_rsp_size,
> > + * the response header is always fully in buf. Data may be split between
> > + * buf and buffer depending on data_offset.
> > + */
> > +static int
> > +handle_query_dir_data(struct TCP_Server_Info *server, struct mid_q_entry *mid,
> > +                     char *buf, unsigned int buf_len, struct folio_queue *buffer,
> > +                     unsigned int buffer_len, bool is_offloaded)
> > +{
> > +       struct cifs_query_dir_io *qd_io = mid->callback_data;
> > +       struct smb2_query_directory_rsp *rsp;
> > +       struct smb2_hdr *shdr = (struct smb2_hdr *)buf;
> > +       unsigned int data_offset, data_len;
> > +       unsigned int hdr_len;
> > +
> > +       cifs_dbg(FYI, "%s: processing encrypted QueryDirectory response\n", __func__);
> > +
> > +       if (shdr->Command != SMB2_QUERY_DIRECTORY) {
> > +               cifs_server_dbg(VFS, "only QueryDirectory responses are supported\n");
> > +               return -EOPNOTSUPP;
> > +       }
> > +
> > +       if (server->ops->is_session_expired &&
> > +           server->ops->is_session_expired(buf)) {
> > +               if (!is_offloaded)
> > +                       cifs_reconnect(server, true);
> > +               return -1;
> > +       }
> > +
> > +       if (server->ops->is_status_pending &&
> > +                       server->ops->is_status_pending(buf, server))
> > +               return -1;
> > +
> > +       rsp = (struct smb2_query_directory_rsp *)buf;
> > +       hdr_len = min_t(unsigned int, buf_len,
> > +                        sizeof(struct smb2_query_directory_rsp));
> > +
> > +       /* Map error code first */
> > +       qd_io->result = server->ops->map_error(buf, false);
> > +
> > +       /* Get data_offset early to set up iov properly */
> > +       data_offset = le16_to_cpu(rsp->OutputBufferOffset);
> > +       data_len = le32_to_cpu(rsp->OutputBufferLength);
> > +
> > +       /* Set up first iov to point to header portion (needed for credits/signature) */
> > +       qd_io->iov[0].iov_base = buf;
> > +       qd_io->iov[0].iov_len = qd_io->result ? hdr_len : data_offset;
> > +
> > +       if (qd_io->result != 0) {
> > +               cifs_dbg(FYI, "%s: server returned error %d (Status=0x%x)\n",
> > +                        __func__, qd_io->result, le32_to_cpu(rsp->hdr.Status));
> > +
> > +               /* Copy header to persistent combined_iov buffer so status
> > +                * remains accessible after receive handler returns. buf is temporary
> > +                * and will be freed/reused, so we can't leave iov[0] pointing to it. */
> > +               if (qd_io->combined_iov.iov_base && hdr_len > 0 &&
> > +                   hdr_len <= qd_io->combined_iov.iov_len) {
> > +                       memcpy(qd_io->combined_iov.iov_base, buf, hdr_len);
> > +                       qd_io->iov[0].iov_base = qd_io->combined_iov.iov_base;
> > +                       qd_io->iov[0].iov_len = hdr_len;
> > +                       cifs_dbg(FYI, "%s: copied error response header to combined_iov\n", __func__);
> > +               }
> > +
> > +               /* Normal error on query_directory response - response received successfully,
> > +                * but the command failed. Store error in qd_io->result for callback. */
> > +               if (is_offloaded)
> > +                       mid->mid_state = MID_RESPONSE_RECEIVED;
> > +               else
> > +                       dequeue_mid(server, mid, false);
> > +               return 0;
> > +       }
> > +
> > +       /* Success - parse the response data */
> > +       cifs_dbg(FYI, "%s: data_offset=%u data_len=%u buf_len=%u buffer_len=%u\n",
> > +                __func__, data_offset, data_len, buf_len, buffer_len);
> > +
> > +       /* Validate data_offset */
> > +       if (data_offset < sizeof(struct smb2_query_directory_rsp)) {
> > +               cifs_dbg(FYI, "%s: data offset (%u) inside response header\n",
> > +                        __func__, data_offset);
> > +               data_offset = sizeof(struct smb2_query_directory_rsp);
> > +       } else if (data_offset > MAX_CIFS_SMALL_BUFFER_SIZE) {
> > +               cifs_dbg(VFS, "%s: data offset (%u) beyond end of smallbuf\n",
> > +                        __func__, data_offset);
> > +               qd_io->result = -EIO;
> > +               dequeue_mid(server, mid, qd_io->result);
> > +               return qd_io->result;
> > +       }
> > +
> > +       /* Validate data_offset is within buf_len + buffer_len */
> > +       if (data_offset > buf_len + buffer_len) {
> > +               cifs_dbg(VFS, "%s: data offset (%u) beyond response length (%u)\n",
> > +                        __func__, data_offset, buf_len + buffer_len);
> > +               qd_io->result = -EIO;
> > +               dequeue_mid(server, mid, qd_io->result);
> > +               return qd_io->result;
> > +       }
> > +
> > +       /* Validate response fits in pre-allocated combined buffer */
> > +       if ((size_t)data_offset + data_len > qd_io->combined_iov.iov_len) {
> > +               cifs_dbg(VFS, "%s: response (%u + %u) exceeds buffer capacity (%zu)\n",
> > +                        __func__, data_offset, data_len, qd_io->combined_iov.iov_len);
> > +               qd_io->result = -EIO;
> > +               dequeue_mid(server, mid, qd_io->result);
> > +               return qd_io->result;
> > +       }
> > +
> > +       /* Copy the prefix present in buf into combined_iov, preserving wire layout */
> > +       memcpy(qd_io->combined_iov.iov_base, buf, min(data_offset, buf_len));
> > +
> > +       if (data_offset < buf_len) {
> > +               /* Data starts in buf, may continue into buffer */
> > +               unsigned int data_in_buf = buf_len - data_offset;
> > +               unsigned int data_in_buffer;
> > +
> > +               if (data_len <= data_in_buf) {
> > +                       /* All data is in buf */
> > +                       memcpy(qd_io->combined_iov.iov_base + data_offset,
> > +                              buf + data_offset, data_len);
> > +               } else {
> > +                       /* Copy from buf first */
> > +                       memcpy(qd_io->combined_iov.iov_base + data_offset,
> > +                              buf + data_offset, data_in_buf);
> > +
> > +                       /* Copy remainder from buffer at offset 0 */
> > +                       data_in_buffer = data_len - data_in_buf;
> > +                       if (data_in_buffer > buffer_len) {
> > +                               cifs_dbg(VFS, "%s: data_in_buffer (%u) > buffer_len (%u)\n",
> > +                                        __func__, data_in_buffer, buffer_len);
> > +                               qd_io->result = -EIO;
> > +                               dequeue_mid(server, mid, qd_io->result);
> > +                               return qd_io->result;
> > +                       }
> > +
> > +                       qd_io->result = cifs_copy_folioq_to_buf(buffer, buffer_len, 0,
> > +                                                               qd_io->combined_iov.iov_base +
> > +                                                               data_offset + data_in_buf,
> > +                                                               data_in_buffer);
> > +                       if (qd_io->result != 0) {
> > +                               cifs_dbg(VFS, "%s: failed to copy from folio_queue: %d\n",
> > +                                        __func__, qd_io->result);
> > +                               dequeue_mid(server, mid, qd_io->result);
> > +                               return qd_io->result;
> > +                       }
> > +               }
> > +       } else {
> > +               /* Padding and data are in buffer starting at offset 0 */
> > +               unsigned int bytes_in_buffer = data_offset - buf_len + data_len;
> > +
> > +               if (bytes_in_buffer > buffer_len) {
> > +                       cifs_dbg(VFS, "%s: data beyond buffer: prefix+len=%u buffer_len=%u\n",
> > +                                __func__, bytes_in_buffer, buffer_len);
> > +                       qd_io->result = -EIO;
> > +                       dequeue_mid(server, mid, qd_io->result);
> > +                       return qd_io->result;
> > +               }
> > +
> > +               qd_io->result = cifs_copy_folioq_to_buf(buffer, buffer_len, 0,
> > +                                                       qd_io->combined_iov.iov_base + buf_len,
> > +                                                       bytes_in_buffer);
> > +               if (qd_io->result != 0) {
> > +                       cifs_dbg(VFS, "%s: failed to copy from folio_queue: %d\n",
> > +                                __func__, qd_io->result);
> > +                       dequeue_mid(server, mid, qd_io->result);
> > +                       return qd_io->result;
> > +               }
> > +       }
> > +
> > +       /* Set up iov[1] pointing into combined buffer, finalize valid length */
> > +       qd_io->iov[1].iov_base = qd_io->combined_iov.iov_base + data_offset;
> > +       qd_io->iov[1].iov_len = data_len;
> > +       qd_io->combined_iov.iov_len = data_offset + data_len;
> > +
> > +       dequeue_mid(server, mid, false);
> > +       return 0;
> > +}
> > +
> > +/*
> > + * Handle callback for async QueryDirectory with multi-credit support.
> > + * For encrypted responses, extracts decrypted data.
> > + * For unencrypted responses, cifs_query_dir_receive already processed everything.
> > + */
> > +int
> > +smb2_query_dir_handle_data(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> > +{
> > +       char *buf = server->large_buf ? server->bigbuf : server->smallbuf;
> > +
> > +       /* For unencrypted responses, data already processed in cifs_query_dir_receive */
> > +       if (!mid->decrypted)
> > +               return 0;
> > +
> > +       /*
> > +        * For small encrypted responses (< CIFSMaxBufSize), all data is in buf.
> > +        * For large encrypted responses, this callback is not used - instead,
> > +        * receive_encrypted_read/smb2_decrypt_offload call handle_query_dir_data directly.
> > +        */
> > +       return handle_query_dir_data(server, mid, buf, server->pdu_size,
> > +                                     NULL, 0, false);
> > +}
> > +
> >  static int
> >  handle_read_data(struct TCP_Server_Info *server, struct mid_q_entry *mid,
> >                  char *buf, unsigned int buf_len, struct folio_queue *buffer,
> > @@ -4987,25 +5347,46 @@ static void smb2_decrypt_offload(struct work_struct *work)
> >         int rc;
> >         struct mid_q_entry *mid;
> >         struct iov_iter iter;
> > +       struct smb2_hdr *shdr;
> > +       unsigned int read_rsp_size = dw->server->vals->read_rsp_size;
> >
> > +       /* decrypt read_rsp_size in buf + remainder in folio_queue */
> >         iov_iter_folio_queue(&iter, ITER_DEST, dw->buffer, 0, 0, dw->len);
> > -       rc = decrypt_raw_data(dw->server, dw->buf, dw->server->vals->read_rsp_size,
> > -                             &iter, true);
> > +       rc = decrypt_raw_data(dw->server, dw->buf, read_rsp_size, &iter, true);
> >         if (rc) {
> >                 cifs_dbg(VFS, "error decrypting rc=%d\n", rc);
> >                 goto free_pages;
> >         }
> >
> >         dw->server->lstrp = jiffies;
> > +
> > +       shdr = (struct smb2_hdr *)(dw->buf + sizeof(struct smb2_transform_hdr));
> > +
> > +       /*
> > +        * buf now contains read_rsp_size bytes after transform_hdr.
> > +        * folio_queue contains (total_len - read_rsp_size) bytes.
> > +        * The handle functions will determine where data actually starts based on data_offset.
> > +        */
> > +
> >         mid = smb2_find_dequeue_mid(dw->server, dw->buf);
> >         if (mid == NULL)
> >                 cifs_dbg(FYI, "mid not found\n");
> >         else {
> >                 mid->decrypted = true;
> > -               rc = handle_read_data(dw->server, mid, dw->buf,
> > -                                     dw->server->vals->read_rsp_size,
> > -                                     dw->buffer, dw->len,
> > -                                     true);
> > +
> > +               /* Handle based on command type */
> > +               if (le16_to_cpu(shdr->Command) == SMB2_READ) {
> > +                       rc = handle_read_data(dw->server, mid, dw->buf, read_rsp_size,
> > +                                             dw->buffer, dw->len, true);
> > +               } else if (le16_to_cpu(shdr->Command) == SMB2_QUERY_DIRECTORY) {
> > +                       rc = handle_query_dir_data(dw->server, mid, dw->buf, read_rsp_size,
> > +                                                  dw->buffer, dw->len, true);
> > +               } else {
> > +                       cifs_dbg(VFS, "Unexpected command %u in decrypt offload\n",
> > +                                le16_to_cpu(shdr->Command));
> > +                       rc = -EOPNOTSUPP;
> > +               }
> > +
> >                 if (rc >= 0) {
> >  #ifdef CONFIG_CIFS_STATS2
> >                         mid->when_received = jiffies;
> > @@ -5049,9 +5430,10 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
> >  {
> >         char *buf = server->smallbuf;
> >         struct smb2_transform_hdr *tr_hdr = (struct smb2_transform_hdr *)buf;
> > +       struct smb2_hdr *shdr;
> >         struct iov_iter iter;
> > -       unsigned int len;
> > -       unsigned int buflen = server->pdu_size;
> > +       unsigned int len, total_len, buflen = server->pdu_size;
> > +       unsigned int read_rsp_size = server->vals->read_rsp_size;
> >         int rc;
> >         struct smb2_decrypt_work *dw;
> >
> > @@ -5062,7 +5444,10 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
> >         dw->server = server;
> >
> >         *num_mids = 1;
> > -       len = min_t(unsigned int, buflen, server->vals->read_rsp_size +
> > +       total_len = le32_to_cpu(tr_hdr->OriginalMessageSize);
> > +
> > +       /* Read transform_hdr + read_rsp_size into buf */
> > +       len = min_t(unsigned int, buflen, read_rsp_size +
> >                 sizeof(struct smb2_transform_hdr)) - HEADER_SIZE(server) + 1;
> >
> >         rc = cifs_read_from_socket(server, buf + HEADER_SIZE(server) - 1, len);
> > @@ -5070,9 +5455,8 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
> >                 goto free_dw;
> >         server->total_read += rc;
> >
> > -       len = le32_to_cpu(tr_hdr->OriginalMessageSize) -
> > -               server->vals->read_rsp_size;
> > -       dw->len = len;
> > +       /* Read remaining data into folio_queue */
> > +       dw->len = total_len - read_rsp_size;
> >         len = round_up(dw->len, PAGE_SIZE);
> >
> >         size_t cur_size = 0;
> > @@ -5101,7 +5485,7 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
> >                 goto free_pages;
> >
> >         /*
> > -        * For large reads, offload to different thread for better performance,
> > +        * For large responses, offload to different thread for better performance,
> >          * use more cores decrypting which can be expensive
> >          */
> >
> > @@ -5115,20 +5499,41 @@ receive_encrypted_read(struct TCP_Server_Info *server, struct mid_q_entry **mid,
> >                 return -1;
> >         }
> >
> > -       rc = decrypt_raw_data(server, buf, server->vals->read_rsp_size,
> > -                             &iter, false);
> > +       /* Decrypt: read_rsp_size in buf + remainder in folio_queue */
> > +       rc = decrypt_raw_data(server, buf, read_rsp_size, &iter, false);
> >         if (rc)
> >                 goto free_pages;
> >
> > +       shdr = (struct smb2_hdr *) buf;
> > +
> > +       /*
> > +        * buf now contains the complete response header (read_rsp_size bytes).
> > +        * folio_queue contains (total_len - read_rsp_size) bytes.
> > +        * The handle functions will determine where data actually starts based on data_offset.
> > +        */
> > +
> >         *mid = smb2_find_mid(server, buf);
> >         if (*mid == NULL) {
> >                 cifs_dbg(FYI, "mid not found\n");
> >         } else {
> > -               cifs_dbg(FYI, "mid found\n");
> > +               cifs_dbg(FYI, "mid found, command=%u\n", le16_to_cpu(shdr->Command));
> >                 (*mid)->decrypted = true;
> > -               rc = handle_read_data(server, *mid, buf,
> > -                                     server->vals->read_rsp_size,
> > -                                     dw->buffer, dw->len, false);
> > +
> > +               /* Handle based on command type */
> > +               if (le16_to_cpu(shdr->Command) == SMB2_READ) {
> > +                       rc = handle_read_data(server, *mid, buf, read_rsp_size,
> > +                                             dw->buffer, dw->len, false);
> > +               } else if (le16_to_cpu(shdr->Command) == SMB2_QUERY_DIRECTORY) {
> > +                       rc = handle_query_dir_data(server, *mid, buf, read_rsp_size,
> > +                                                  dw->buffer, dw->len, false);
> > +               } else {
> > +                       /* For now, other commands not supported in large encrypted path */
> > +                       cifs_server_dbg(VFS,
> > +                                       "Large encrypted responses only supported for SMB2_READ and SMB2_QUERY_DIRECTORY (got %u)\n",
> > +                                       le16_to_cpu(shdr->Command));
> > +                       rc = -EOPNOTSUPP;
> > +               }
> > +
> >                 if (rc >= 0) {
> >                         if (server->ops->is_network_name_deleted) {
> >                                 server->ops->is_network_name_deleted(buf,
> > @@ -5269,7 +5674,6 @@ smb3_receive_transform(struct TCP_Server_Info *server,
> >                 return -ECONNABORTED;
> >         }
> >
> > -       /* TODO: add support for compounds containing READ. */
> >         if (pdu_length > CIFSMaxBufSize + MAX_HEADER_SIZE(server)) {
> >                 return receive_encrypted_read(server, &mids[0], num_mids);
> >         }
> > diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
> > index 2d55246d2851b..b59d99d5e193a 100644
> > --- a/fs/smb/client/smb2pdu.c
> > +++ b/fs/smb/client/smb2pdu.c
> > @@ -5496,6 +5496,182 @@ num_entries(int infotype, char *bufstart, char *end_of_buf, char **lastentry,
> >         return entrycount;
> >  }
> >
> > +/*
> > + * Callback for async QueryDirectory with multi-credit support
> > + */
> > +static void
> > +smb2_query_dir_callback(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> > +{
> > +       struct cifs_query_dir_io *qd_io = mid->callback_data;
> > +       struct cifs_tcon *tcon = qd_io->tcon;
> > +       struct smb2_hdr *shdr = (struct smb2_hdr *)qd_io->iov[0].iov_base;
> > +       struct cifs_credits credits = {
> > +               .value = 0,
> > +               .instance = 0,
> > +       };
> > +
> > +       WARN_ONCE(qd_io->server != server,
> > +                 "qd_io server %p != mid server %p",
> > +                 qd_io->server, server);
> > +
> > +       cifs_dbg(FYI, "%s: mid=%llu state=%d result=%d\n",
> > +                __func__, mid->mid, mid->mid_state, qd_io->result);
> > +
> > +       switch (mid->mid_state) {
> > +       case MID_RESPONSE_RECEIVED:
> > +               credits.value = le16_to_cpu(shdr->CreditRequest);
> > +               credits.instance = server->reconnect_instance;
> > +               /* result already set, check signature if needed */
> > +               if (server->sign && !mid->decrypted) {
> > +                       int rc;
> > +                       struct smb_rqst rqst = { .rq_iov = &qd_io->iov[0], .rq_nvec = 1 };
> > +
> > +                       rc = smb2_verify_signature(&rqst, server);
> > +                       if (rc) {
> > +                               cifs_tcon_dbg(VFS, "QueryDir signature verification returned error = %d\n",
> > +                                             rc);
> > +                               qd_io->result = rc;
> > +                       }
> > +               }
> > +               break;
> > +       case MID_REQUEST_SUBMITTED:
> > +       case MID_RETRY_NEEDED:
> > +               qd_io->result = -EAGAIN;
> > +               break;
> > +       case MID_RESPONSE_MALFORMED:
> > +               credits.value = le16_to_cpu(shdr->CreditRequest);
> > +               credits.instance = server->reconnect_instance;
> > +               qd_io->result = smb_EIO(smb_eio_trace_read_rsp_malformed);
> > +               break;
> > +       default:
> > +               qd_io->result = smb_EIO1(smb_eio_trace_read_mid_state_unknown,
> > +                                        mid->mid_state);
> > +               break;
> > +       }
> > +
> > +       if (qd_io->result && qd_io->result != -ENODATA)
> > +               cifs_stats_fail_inc(tcon, SMB2_QUERY_DIRECTORY_HE);
> > +
> > +       trace_smb3_rw_credits(0, 0, qd_io->credits.value,
> > +                             server->credits, server->in_flight,
> > +                             0, cifs_trace_rw_credits_read_response_clear);
> > +       qd_io->credits.value = 0;
> > +       release_mid(server, mid);
> > +       trace_smb3_rw_credits(0, 0, 0,
> > +                             server->credits, server->in_flight,
> > +                             credits.value, cifs_trace_rw_credits_read_response_add);
> > +       add_credits(server, &credits, 0);
> > +
> > +       complete(&qd_io->done);
> > +}
> > +
> > +/*
> > + * QueryDirectory with large buffer and multi-credit support.
> > + * Uses async infrastructure but waits for completion synchronously.
> > + */
> > +int
> > +SMB2_query_directory_large(struct cifs_query_dir_io *qd_io, unsigned int buf_size)
> > +{
> > +       int rc, flags = 0;
> > +       char *buf;
> > +       struct smb2_hdr *shdr;
> > +       struct smb_rqst rqst = { .rq_iov = &qd_io->iov[0],
> > +                                .rq_nvec = SMB2_QUERY_DIRECTORY_IOV_SIZE };
> > +       struct TCP_Server_Info *server = qd_io->server;
> > +       struct cifs_tcon *tcon = qd_io->tcon;
> > +       unsigned int total_len;
> > +       int credit_request;
> > +
> > +       cifs_dbg(FYI, "%s: buf_size=%u\n", __func__, buf_size);
> > +
> > +       /* Cap buffer size to avoid kmalloc failures for very large allocations.
> > +        * SMB2_MAX_QD_DATABUF_SIZE is a safe limit that stays well below typical
> > +        * kmalloc constraints while still allowing large directory listings.
> > +        */
> > +       if (buf_size > SMB2_MAX_QD_DATABUF_SIZE)
> > +               buf_size = SMB2_MAX_QD_DATABUF_SIZE;
> > +
> > +       /* Allocate response buffer. Since we'll build a combined header+data buffer,
> > +        * we need space for both. We'll request a slightly smaller OutputBufferLength
> > +        * from the server to ensure the total response fits.
> > +        */
> > +       qd_io->combined_iov.iov_base = kmalloc(buf_size, GFP_KERNEL);
> > +       if (!qd_io->combined_iov.iov_base)
> > +               return -ENOMEM;
> > +       /* Store total capacity in iov_len; updated to actual data length by receive handler */
> > +       qd_io->combined_iov.iov_len = buf_size;
> > +
> > +       /* Initialize completion */
> > +       init_completion(&qd_io->done);
> > +
> > +       /* Request less data from server to leave room for the response header.
> > +        * Use MAX_CIFS_SMALL_BUFFER_SIZE as a safety margin.
> > +        */
> > +       rc = SMB2_query_directory_init(qd_io->xid, tcon, server,
> > +                                      &rqst, qd_io->persistent_fid,
> > +                                      qd_io->volatile_fid, qd_io->index,
> > +                                      qd_io->srch_inf->info_level,
> > +                                      buf_size - MAX_CIFS_SMALL_BUFFER_SIZE);
> > +       if (rc) {
> > +               kfree(qd_io->combined_iov.iov_base);
> > +               qd_io->combined_iov.iov_base = NULL;
> > +               return rc;
> > +       }
> > +
> > +       if (smb3_encryption_required(tcon))
> > +               flags |= CIFS_TRANSFORM_REQ;
> > +
> > +       buf = rqst.rq_iov[0].iov_base;
> > +       total_len = rqst.rq_iov[0].iov_len;
> > +
> > +       shdr = (struct smb2_hdr *)buf;
> > +
> > +       if (qd_io->replay) {
> > +               /* Back-off before retry */
> > +               if (qd_io->cur_sleep)
> > +                       msleep(qd_io->cur_sleep);
> > +               smb2_set_replay(server, &rqst);
> > +       }
> > +
> > +       /* Set credit charge based on buffer size */
> > +       if (qd_io->credits.value > 0) {
> > +               shdr->CreditCharge = cpu_to_le16(DIV_ROUND_UP(buf_size,
> > +                                               SMB2_MAX_BUFFER_SIZE));
> > +               credit_request = le16_to_cpu(shdr->CreditCharge) + 8;
> > +               if (server->credits >= server->max_credits)
> > +                       shdr->CreditRequest = cpu_to_le16(0);
> > +               else
> > +                       shdr->CreditRequest = cpu_to_le16(
> > +                               min_t(int, server->max_credits -
> > +                                               server->credits, credit_request));
> > +
> > +               flags |= CIFS_HAS_CREDITS;
> > +       }
> > +
> > +       rc = cifs_call_async(server, &rqst,
> > +                            cifs_query_dir_receive, smb2_query_dir_callback,
> > +                            smb2_query_dir_handle_data, qd_io, flags,
> > +                            &qd_io->credits);
> > +       if (rc) {
> > +               cifs_stats_fail_inc(tcon, SMB2_QUERY_DIRECTORY_HE);
> > +               trace_smb3_query_dir_err(qd_io->xid, qd_io->persistent_fid,
> > +                                        tcon->tid, tcon->ses->Suid,
> > +                                        qd_io->index, 0, rc);
> > +               kfree(qd_io->combined_iov.iov_base);
> > +               qd_io->combined_iov.iov_base = NULL;
> > +       }
> > +
> > +       /* Free request buffer immediately after async call */
> > +       cifs_small_buf_release(buf);
> > +
> > +       if (rc)
> > +               return rc;
> > +
> > +       /* Wait for the async operation to complete */
> > +       wait_for_completion(&qd_io->done);
> > +       return qd_io->result;
> > +}
> > +
> >  /*
> >   * Readdir/FindFirst
> >   */
> > @@ -5560,12 +5736,6 @@ int SMB2_query_directory_init(const unsigned int xid,
> >         req->FileNameOffset =
> >                 cpu_to_le16(sizeof(struct smb2_query_directory_req));
> >         req->FileNameLength = cpu_to_le16(len);
> > -       /*
> > -        * BB could be 30 bytes or so longer if we used SMB2 specific
> > -        * buffer lengths, but this is safe and close enough.
> > -        */
> > -       output_size = min_t(unsigned int, output_size, server->maxBuf);
> > -       output_size = min_t(unsigned int, output_size, 2 << 15);
> >         req->OutputBufferLength = cpu_to_le32(output_size);
> >
> >         iov[0].iov_base = (char *)req;
> > diff --git a/fs/smb/client/smb2pdu.h b/fs/smb/client/smb2pdu.h
> > index 7b7a864520c68..843e30c1ecc61 100644
> > --- a/fs/smb/client/smb2pdu.h
> > +++ b/fs/smb/client/smb2pdu.h
> > @@ -132,6 +132,9 @@ struct share_redirect_error_context_rsp {
> >  /* Size of the minimal QueryDir response for checking if more data exists */
> >  #define SMB2_QD2_RESPONSE_SIZE 4096
> >
> > +/* max query directory data buffer size */
> > +#define SMB2_MAX_QD_DATABUF_SIZE (2 * 1024 * 1024)
> > +
> >  /*
> >   * Output buffer size for first QueryDir in Create+QD1+QD2 compound.
> >   * Accounts for shared buffer space needed for all three responses.
> > diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> > index 9de7d2fe8d466..9607e8899f7ff 100644
> > --- a/fs/smb/client/smb2proto.h
> > +++ b/fs/smb/client/smb2proto.h
> > @@ -46,6 +46,8 @@ __le32 smb2_get_lease_state(struct cifsInodeInfo *cinode, unsigned int oplock);
> >  bool smb2_is_valid_oplock_break(char *buffer, struct TCP_Server_Info *server);
> >  int smb3_handle_read_data(struct TCP_Server_Info *server,
> >                           struct mid_q_entry *mid);
> > +int smb2_query_dir_handle_data(struct TCP_Server_Info *server,
> > +                              struct mid_q_entry *mid);
> >  struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
> >                                         struct super_block *sb,
> >                                         const unsigned int xid,
> > @@ -197,6 +199,7 @@ int SMB2_query_directory_init(const unsigned int xid, struct cifs_tcon *tcon,
> >                               u64 volatile_fid, int index, int info_level,
> >                               unsigned int output_size);
> >  void SMB2_query_directory_free(struct smb_rqst *rqst);
> > +int SMB2_query_directory_large(struct cifs_query_dir_io *qd_io, unsigned int buf_size);
> >  int SMB2_set_eof(const unsigned int xid, struct cifs_tcon *tcon,
> >                  u64 persistent_fid, u64 volatile_fid, u32 pid,
> >                  loff_t new_eof);
> > diff --git a/fs/smb/client/trace.h b/fs/smb/client/trace.h
> > index acfbb63086ea2..54ee1317c5b12 100644
> > --- a/fs/smb/client/trace.h
> > +++ b/fs/smb/client/trace.h
> > @@ -165,6 +165,7 @@
> >         EM(cifs_trace_rw_credits_write_prepare,         "wr-prepare ") \
> >         EM(cifs_trace_rw_credits_write_response_add,    "wr-resp-add") \
> >         EM(cifs_trace_rw_credits_write_response_clear,  "wr-resp-clr") \
> > +       EM(cifs_trace_rw_credits_query_dir_done,        "qd-done    ") \
> >         E_(cifs_trace_rw_credits_zero_in_flight,        "ZERO-IN-FLT")
> >
> >  #define smb3_tcon_ref_traces                                         \
> > diff --git a/fs/smb/client/transport.c b/fs/smb/client/transport.c
> > index 05f8099047e1a..4457b98b9365d 100644
> > --- a/fs/smb/client/transport.c
> > +++ b/fs/smb/client/transport.c
> > @@ -1154,6 +1154,190 @@ cifs_readv_discard(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> >         return  __cifs_readv_discard(server, mid, rdata->result);
> >  }
> >
> > +static int
> > +cifs_query_dir_discard(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> > +{
> > +       int length;
> > +       struct cifs_query_dir_io *qd_io = mid->callback_data;
> > +
> > +       length = cifs_discard_remaining_data(server);
> > +       dequeue_mid(server, mid, qd_io->result != 0);
> > +       mid->resp_buf = server->smallbuf;
> > +       server->smallbuf = NULL;
> > +       return length;
> > +}
> > +
> > +/*
> > + * Receive handler for async QueryDirectory with multi-credit support
> > + */
> > +int
> > +cifs_query_dir_receive(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> > +{
> > +       int length, len;
> > +       unsigned int data_offset, data_len, resp_size;
> > +       struct cifs_query_dir_io *qd_io = mid->callback_data;
> > +       char *buf = server->smallbuf;
> > +       unsigned int buflen = server->pdu_size;
> > +       struct smb2_query_directory_rsp *rsp;
> > +
> > +       cifs_dbg(FYI, "%s: mid=%llu buf_capacity=%zu\n",
> > +                __func__, mid->mid, qd_io->combined_iov.iov_len);
> > +
> > +       /*
> > +        * Read the rest of QUERY_DIRECTORY_RSP header (sans Data array).
> > +        * QueryDirectory response structure is 64 bytes (SMB2 header) + 8 bytes (fixed part).
> > +        */
> > +       resp_size = sizeof(struct smb2_query_directory_rsp);
> > +       len = min_t(unsigned int, buflen, resp_size) - HEADER_SIZE(server) + 1;
> > +
> > +       length = cifs_read_from_socket(server,
> > +                                      buf + HEADER_SIZE(server) - 1, len);
> > +       if (length < 0) {
> > +               qd_io->result = length;
> > +               goto discard_and_queue;
> > +       }
> > +       server->total_read += length;
> > +
> > +       if (server->ops->is_session_expired &&
> > +           server->ops->is_session_expired(buf)) {
> > +               cifs_reconnect(server, true);
> > +               return -1;
> > +       }
> > +
> > +       if (server->ops->is_status_pending &&
> > +           server->ops->is_status_pending(buf, server)) {
> > +               cifs_discard_remaining_data(server);
> > +               return -1;
> > +       }
> > +
> > +       /* Is there enough to get to the rest of the QUERY_DIRECTORY_RSP header? */
> > +       if (server->total_read < resp_size) {
> > +               cifs_dbg(FYI, "%s: server returned short header. got=%u expected=%u\n",
> > +                        __func__, server->total_read, resp_size);
> > +               qd_io->result = smb_EIO2(smb_eio_trace_read_rsp_short,
> > +                                        server->total_read, resp_size);
> > +               return cifs_query_dir_discard(server, mid);
> > +       }
> > +
> > +       /* Set up first iov for signature check and to get credits */
> > +       qd_io->iov[0].iov_base = buf;
> > +       qd_io->iov[0].iov_len = server->total_read;
> > +       qd_io->iov[1].iov_base = NULL;
> > +       qd_io->iov[1].iov_len = 0;
> > +       cifs_dbg(FYI, "0: iov_base=%p iov_len=%zu\n",
> > +                qd_io->iov[0].iov_base, qd_io->iov[0].iov_len);
> > +
> > +       /* Parse header early to access status before map_error converts it */
> > +       rsp = (struct smb2_query_directory_rsp *)buf;
> > +
> > +       /* Was the SMB query_directory successful? */
> > +       qd_io->result = server->ops->map_error(buf, false);
> > +       if (qd_io->result != 0) {
> > +               if (qd_io->combined_iov.iov_base && qd_io->iov[0].iov_len > 0 &&
> > +                   qd_io->iov[0].iov_len <= qd_io->combined_iov.iov_len) {
> > +                       memcpy(qd_io->combined_iov.iov_base, qd_io->iov[0].iov_base,
> > +                              qd_io->iov[0].iov_len);
> > +                       qd_io->iov[0].iov_base = qd_io->combined_iov.iov_base;
> > +               }
> > +               cifs_dbg(FYI, "%s: server returned error %d (Status was 0x%x)\n",
> > +                        __func__, qd_io->result, le32_to_cpu(rsp->hdr.Status));
> > +               return cifs_query_dir_discard(server, mid);
> > +       }
> > +
> > +       data_offset = le16_to_cpu(rsp->OutputBufferOffset);
> > +       data_len = le32_to_cpu(rsp->OutputBufferLength);
> > +
> > +       cifs_dbg(FYI, "%s: total_read=%u data_offset=%u data_len=%u\n",
> > +                __func__, server->total_read, data_offset, data_len);
> > +
> > +       /* Validate data_offset and data_len */
> > +       if (data_offset < server->total_read) {
> > +               cifs_dbg(FYI, "%s: data offset (%u) inside response header\n",
> > +                        __func__, data_offset);
> > +               data_offset = server->total_read;
> > +               data_len = 0;
> > +       } else if (data_offset > MAX_CIFS_SMALL_BUFFER_SIZE) {
> > +               cifs_dbg(FYI, "%s: data offset (%u) beyond end of smallbuf\n",
> > +                        __func__, data_offset);
> > +               qd_io->result = smb_EIO1(smb_eio_trace_read_overlarge,
> > +                                        data_offset);
> > +               return cifs_query_dir_discard(server, mid);
> > +       }
> > +
> > +       /* Read any padding between header and data */
> > +       len = data_offset - server->total_read;
> > +       if (len > 0) {
> > +               length = cifs_read_from_socket(server,
> > +                                              buf + server->total_read, len);
> > +               if (length < 0) {
> > +                       qd_io->result = length;
> > +                       goto discard_and_queue;
> > +               }
> > +               server->total_read += length;
> > +               qd_io->iov[0].iov_len = server->total_read;
> > +       }
> > +
> > +       /* Check if data fits in the pre-allocated combined buffer */
> > +       if (qd_io->iov[0].iov_len + data_len > qd_io->combined_iov.iov_len) {
> > +               cifs_dbg(VFS, "%s: response (%zu + %u) exceeds buffer capacity (%zu)\n",
> > +                        __func__, qd_io->iov[0].iov_len, data_len, qd_io->combined_iov.iov_len);
> > +               qd_io->result = smb_EIO2(smb_eio_trace_read_rsp_malformed,
> > +                                        data_len, (unsigned int)qd_io->combined_iov.iov_len);
> > +               return cifs_query_dir_discard(server, mid);
> > +       }
> > +
> > +       /*
> > +        * Build combined header+data in qd_io->combined_iov.iov_base.
> > +        * Layout: [header][data] with no padding between them.
> > +        * We update OutputBufferOffset in the header to reflect the new layout.
> > +        */
> > +       if (qd_io->iov[0].iov_len > 0 && qd_io->result == 0) {
> > +               size_t hdr_len = qd_io->iov[0].iov_len;
> > +               struct smb2_query_directory_rsp *combined_rsp;
> > +
> > +               /* Copy header to beginning of combined buffer */
> > +               memcpy(qd_io->combined_iov.iov_base, qd_io->iov[0].iov_base, hdr_len);
> > +
> > +               /* Read directory entries directly after the header, removing any padding */
> > +               if (data_len > 0) {
> > +                       length = cifs_read_from_socket(server,
> > +                                                      qd_io->combined_iov.iov_base + hdr_len,
> > +                                                      data_len);
> > +                       if (length < 0) {
> > +                               qd_io->result = length;
> > +                               goto discard_and_queue;
> > +                       }
> > +                       server->total_read += length;
> > +
> > +                       /* Update OutputBufferOffset and OutputBufferLength to reflect
> > +                        * the new compact layout (header followed immediately by data).
> > +                        */
> > +                       combined_rsp = (struct smb2_query_directory_rsp *)qd_io->combined_iov.iov_base;
> > +                       combined_rsp->OutputBufferOffset = cpu_to_le16(hdr_len);
> > +                       combined_rsp->OutputBufferLength = cpu_to_le32(length);
> > +
> > +                       /* Set up second iov pointing to the directory data within combined buffer */
> > +                       qd_io->iov[1].iov_base = qd_io->combined_iov.iov_base + hdr_len;
> > +                       qd_io->iov[1].iov_len = length;
> > +               }
> > +
> > +               qd_io->combined_iov.iov_len = hdr_len + (data_len > 0 ? length : 0);
> > +
> > +               cifs_dbg(FYI, "total_read=%u buflen=%u data_len=%u hdr_len=%zu combined_len=%zu\n",
> > +                        server->total_read, buflen, data_len, hdr_len, qd_io->combined_iov.iov_len);
> > +       }
> > +
> > +discard_and_queue:
> > +       /* Discard any remaining data (shouldn't be any) */
> > +       if (server->total_read < buflen)
> > +               cifs_discard_remaining_data(server);
> > +
> > +       dequeue_mid(server, mid, false);
> > +       mid->resp_buf = server->smallbuf;
> > +       server->smallbuf = NULL;
> > +       return length;
> > +}
> > +
> >  int
> >  cifs_readv_receive(struct TCP_Server_Info *server, struct mid_q_entry *mid)
> >  {
> > --
> > 2.43.0
> >
>
>
> --
> Thanks,
>
> Steve



-- 
Thanks,

Steve

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-04-15 23:11 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-14 13:59 [PATCH 1/7] cifs: change_conf needs to be called for session setup nspmangalore
2026-04-14 13:59 ` [PATCH 2/7] cifs: abort open_cached_dir if we don't request leases nspmangalore
2026-04-14 13:59 ` [PATCH 3/7] cifs: invalidate cfid on unlink/rename/rmdir nspmangalore
2026-04-14 13:59 ` [PATCH 4/7] cifs: define variable sized buffer for querydir responses nspmangalore
2026-04-14 13:59 ` [PATCH 5/7] cifs: optimize readdir for small directories nspmangalore
2026-04-14 13:59 ` [PATCH 6/7] cifs: optimize readdir for larger directories nspmangalore
2026-04-15 23:08   ` Steve French
2026-04-15 23:11     ` Steve French
2026-04-14 13:59 ` [PATCH 7/7] cifs: reorganize cached dir helpers nspmangalore

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox