[PATCH v2 12/14] smb: client: add support for decompressing READs

Enzo Matsumiya <[email protected]> Mon, 20 Jul 2026 16:49:23 -0300
Newsgroups org.kernel.vger.linux-cifs
Message-ID <[email protected]>
Implement decompression support for SMB2 READ messages.

Changes:
- smb2ops.c: add receive_compressed() and check_decompressed_smb2()
  - check for compress header in smb3_is_transform_hdr() as well so we
    fall into the same path handling offloaded READ decryption
- common/compress/compress.h: add is_compress_hdr() and
  smb_decompress_alloc_size() helpers

Signed-off-by: Enzo Matsumiya <[email protected]>
---
 fs/smb/client/smb2ops.c           | 160 +++++++++++++++++++++++++++++-
 fs/smb/client/smb2pdu.c           |   6 ++
 fs/smb/common/compress/compress.h |  86 ++++++++++++++++
 3 files changed, 251 insertions(+), 1 deletion(-)

diff --git a/fs/smb/client/smb2ops.c b/fs/smb/client/smb2ops.c
index d60bc537ecf1..09cbc00e83b0 100644
--- a/fs/smb/client/smb2ops.c
+++ b/fs/smb/client/smb2ops.c
@@ -30,6 +30,8 @@
 #include "fs_context.h"
 #include "cached_dir.h"
 #include "reparse.h"
+#include "../common/compress/compress.h"
+#include "compress.h"
 
 /* Change credits for different ops and return the total number of credits */
 static int
@@ -4714,7 +4716,7 @@ smb3_is_transform_hdr(void *buf)
 {
 	struct smb2_transform_hdr *trhdr = buf;
 
-	return trhdr->ProtocolId == SMB2_TRANSFORM_PROTO_NUM;
+	return (trhdr->ProtocolId == SMB2_TRANSFORM_PROTO_NUM) || is_compress_hdr(buf);
 }
 
 static int
@@ -5246,6 +5248,153 @@ receive_encrypted_standard(struct TCP_Server_Info *server,
 	return ret;
 }
 
+static __always_inline bool check_decompressed_smb2(const void *buf, const u32 len)
+{
+	const struct smb2_read_rsp *rsp = buf;
+
+	if (unlikely(rsp->hdr.ProtocolId != SMB2_PROTO_NUMBER)) {
+		cifs_dbg(VFS, "decompressed message is not an SMB2 message (got ProtocolId 0x%x)\n",
+			 rsp->hdr.ProtocolId);
+		return false;
+	}
+
+	if (unlikely(rsp->hdr.Command != SMB2_READ)) {
+		cifs_dbg(VFS, "decompressed message is not an SMB2 READ (got %u)\n",
+			 le16_to_cpu(rsp->hdr.Command));
+		return false;
+	}
+
+	if (unlikely(le32_to_cpu(rsp->DataLength) != len)) {
+		cifs_dbg(VFS, "invalid decompressed length for SMB2 READ (got %u, expected %u)\n",
+			 le32_to_cpu(rsp->DataLength), len);
+		return false;
+	}
+
+	return true;
+}
+
+static int receive_compressed(struct TCP_Server_Info *server)
+{
+	struct mid_q_entry *mid;
+	void *src = NULL, *dst = NULL;
+	u32 slen, dlen;
+	int read, ret;
+
+	/* smb2_compression_pattern_v1 is the smallest compressed data we can have. */
+	slen = server->pdu_size;
+	if (unlikely(slen < sizeof(struct smb2_compression_hdr) +
+			    sizeof(struct smb2_compression_pattern_v1))) {
+		ret = -EINVAL;
+		goto err_free;
+	}
+
+	src = kvzalloc(slen, GFP_KERNEL);
+	if (unlikely(!src)) {
+		ret = -ENOMEM;
+		goto err_free;
+	}
+
+	read = server->total_read;
+	if (unlikely(read > slen)) {
+		ret = -EINVAL;
+		goto err_free;
+	}
+
+	memcpy(src, server->smallbuf, read);
+	ret = cifs_read_from_socket(server, src + read, slen - read);
+	if (ret < 0)
+		goto err_free;
+
+	server->total_read += ret;
+	if (unlikely(slen - ret != read)) {
+		ret = -EINVAL;
+		goto err_free;
+	}
+
+	ret = -EINVAL;
+	read = server->vals->read_rsp_size;
+	dlen = smb_decompress_alloc_size(src, slen, read, read + 256 +
+					 SMB2_COMPRESSION_PAYLOAD_BASE_LEN + server->max_read,
+					 server->compression.chained);
+	if (unlikely(dlen == 0))
+		goto err_free;
+
+	dst = kvzalloc(dlen, GFP_KERNEL);
+	if (unlikely(!dst)) {
+		ret = -ENOMEM;
+		goto err_free;
+	}
+
+	ret = smb_compression_decompress(server->compression.alg, server->compression.chained,
+					 src, slen, dst, dlen);
+	if (unlikely(ret))
+		goto err_free;
+
+	if (unlikely(!check_decompressed_smb2(dst, dlen - server->vals->read_rsp_size))) {
+		ret = -EINVAL;
+		goto err_free;
+	}
+
+	mid = smb2_find_dequeue_mid(server, dst);
+	if (!mid) {
+		ret = -EIO;
+		goto err_free;
+	}
+
+	ret = handle_read_data(server, mid, dst, dlen, NULL, 0, true);
+	if (!ret) {
+		struct cifs_io_subrequest *rdata = mid->callback_data;
+#ifdef CONFIG_CIFS_STATS2
+		mid->when_received = jiffies;
+#endif
+		if (server->ops->is_network_name_deleted)
+			server->ops->is_network_name_deleted(dst, server);
+
+		rdata->iov[0].iov_base = dst;
+		rdata->iov[0].iov_len = server->vals->read_rsp_size;
+		mid_execute_callback(server, mid);
+	} else {
+		spin_lock(&server->srv_lock);
+		if (server->tcpStatus == CifsNeedReconnect) {
+			spin_lock(&server->mid_queue_lock);
+			mid->mid_state = MID_RETRY_NEEDED;
+			spin_unlock(&server->mid_queue_lock);
+			spin_unlock(&server->srv_lock);
+
+			mid_execute_callback(server, mid);
+		} else {
+			spin_lock(&server->mid_queue_lock);
+			mid->mid_state = MID_REQUEST_SUBMITTED;
+			mid->deleted_from_q = false;
+			list_add_tail(&mid->qhead, &server->pending_mid_q);
+			spin_unlock(&server->mid_queue_lock);
+			spin_unlock(&server->srv_lock);
+		}
+	}
+
+	release_mid(server, mid);
+err_free:
+	kvfree(src);
+	kvfree(dst);
+
+	/*
+	 * We _must_ disconnect on any failed decompression.
+	 * Actually, there's not much we can do to handle different decompression errors here, so
+	 * forcing a reconnect (which will disable compression) is our best option as the request
+	 * should be retried.
+	 */
+	if (unlikely(ret)) {
+		/* Alias only if not -ENOMEM */
+		if (ret != -ENOMEM)
+			ret = -ECONNRESET;
+		spin_lock(&server->srv_lock);
+		server->tcpStatus = CifsNeedReconnect;
+		spin_unlock(&server->srv_lock);
+	}
+
+	return ret;
+}
+
 static int
 smb3_receive_transform(struct TCP_Server_Info *server,
 		       struct mid_q_entry **mids, char **bufs, int *num_mids)
@@ -5255,6 +5404,13 @@ smb3_receive_transform(struct TCP_Server_Info *server,
 	struct smb2_transform_hdr *tr_hdr = (struct smb2_transform_hdr *)buf;
 	unsigned int orig_len = le32_to_cpu(tr_hdr->OriginalMessageSize);
 
+	if (is_compress_hdr(buf))
+		return receive_compressed(server);
+
+	/*
+	 * @buf is either a compress or transform (encrypt) header, previously checked by cifsd
+	 * (or we wouldn't reach here), so no need to check again.
+	 */
 	if (pdu_length < sizeof(struct smb2_transform_hdr) +
 						sizeof(struct smb2_hdr)) {
 		cifs_server_dbg(VFS, "Transform message is too small (%u)\n",
@@ -5296,6 +5452,8 @@ static int smb2_next_header(struct TCP_Server_Info *server, char *buf,
 		*noff = le32_to_cpu(t_hdr->OriginalMessageSize);
 		if (unlikely(check_add_overflow(*noff, sizeof(*t_hdr), noff)))
 			return -EINVAL;
+	} else if (hdr->ProtocolId == SMB2_COMPRESSION_TRANSFORM_ID) {
+		*noff = 0;
 	} else {
 		*noff = le32_to_cpu(hdr->NextCommand);
 	}
diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
index e58ac7be30a3..bf2b3c7fa4b8 100644
--- a/fs/smb/client/smb2pdu.c
+++ b/fs/smb/client/smb2pdu.c
@@ -4933,6 +4933,12 @@ smb2_async_readv(struct cifs_io_subrequest *rdata)
 		flags |= CIFS_HAS_CREDITS;
 	}
 
+	if (should_compress(io_parms.tcon, &rqst)) {
+		struct smb2_read_req *req = (struct smb2_read_req *)buf;
+
+		req->Flags |= SMB2_READFLAG_REQUEST_COMPRESSED;
+	}
+
 	rc = cifs_call_async(server, &rqst,
 			     cifs_readv_receive, smb2_readv_callback,
 			     smb3_handle_read_data, rdata, flags,
diff --git a/fs/smb/common/compress/compress.h b/fs/smb/common/compress/compress.h
index 43dba4bb1176..e0ec41a21505 100644
--- a/fs/smb/common/compress/compress.h
+++ b/fs/smb/common/compress/compress.h
@@ -152,6 +152,92 @@ static __always_inline bool smb_compress_alg_valid(__le16 alg, bool valid_none)
 	return false;
 }
 
+static __always_inline bool is_compress_hdr(const void *buf)
+{
+	const struct smb2_compression_hdr *hdr = buf;
+
+	return likely(hdr) && hdr->ProtocolId == SMB2_COMPRESSION_TRANSFORM_ID;
+}
+
+/**
+ * smb_decompress_alloc_size() - Validate received SMB2 compression header + return allocation size
+ *				 for the decompressed buffer.
+ * @buf: received buffer
+ * @pdu_size: size of @buf
+ * @dmin: minimum decompressed size allowed (inclusive)
+ * @dmax: maximum decompressed size allowed (inclusive)
+ * @chained: if chaining was negotiated
+ *
+ * Return: decompressed size, or 0 if invalid.
+ */
+static __always_inline u32 smb_decompress_alloc_size(const void *buf, const u32 pdu_size,
+						     const u32 dmin, const u32 dmax, bool chained)
+{
+	const struct smb2_compression_hdr *hdr = buf;
+	__le16 alg, flags;
+	u32 len;
+
+	if (unlikely(!is_compress_hdr(buf) || pdu_size < sizeof(*hdr))) {
+		pr_warn("invalid SMB2 compressed buffer/header\n");
+		return 0;
+	}
+
+	len = le32_to_cpu(hdr->OriginalCompressedSegmentSize);
+	alg = le16_to_cpu(hdr->CompressionAlgorithm);
+	flags = le16_to_cpu(hdr->Flags);
+
+	if (flags == SMB2_COMPRESSION_FLAG_CHAINED) {
+		if (unlikely(!chained)) {
+			pr_warn("chained payload but chaining wasn't negotiated\n");
+			return 0;
+		}
+
+		if (unlikely(!smb_compress_alg_valid(alg, true))) {
+			pr_warn("invalid chained compression algorithm (%u)\n", alg);
+			return 0;
+		}
+
+		/* This is the first payload_hdr->Length field, so it can never be 0 */
+		if (unlikely(le32_to_cpu(hdr->Offset) == 0)) {
+			pr_warn("invalid chained compression payload length 0\n");
+			return 0;
+		}
+	} else if (flags == SMB2_COMPRESSION_FLAG_NONE) {
+		/*
+		 * When unchained, we must account for the offset (uncompressed) part as well
+		 * (usually SMB2 header, but can be anything).
+		 */
+		u32 res, offset = le32_to_cpu(hdr->Offset);
+
+		if (unlikely(chained)) {
+			pr_warn("unchained payload but chaining negotiated\n");
+			return 0;
+		}
+
+		if (unlikely(offset > pdu_size - sizeof(*hdr) ||
+			     check_add_overflow(len, offset, &res))) {
+			pr_warn("invalid unchained compression offset (%u, len=%u)\n", offset, len);
+			return 0;
+		}
+
+		len = res;
+		if (unlikely(!smb_compress_alg_valid(alg, false) || alg == SMB3_COMPRESS_PATTERN)) {
+			pr_warn("invalid unchained compression algorithm (%u)\n", alg);
+			return 0;
+		}
+	} else {
+		pr_warn("invalid SMB2 compression flags (0x%x)\n", flags);
+		return 0;
+	}
+
+	if (unlikely(len < dmin || len > dmax)) {
+		pr_warn("decompressed size %u out of range (min=%u, max=%u)\n", len, dmin, dmax);
+		return 0;
+	}
+
+	return len;
+}
+
 /**
  * smb_compress_alloc_size() - Compute total allocation size required for compressed (dst) buffer.
  * @size:		uncompressed size
-- 
2.54.0