[PATCH v2 07/14] smb: client: compress: enable Pattern_V1 + chained compression

Enzo Matsumiya <[email protected]> Mon, 20 Jul 2026 16:49:18 -0300
Newsgroups org.kernel.vger.linux-cifs
Message-ID <[email protected]>
Negotiate Pattern_V1 and LZ77 compression algorithms by default (if
'compress' mount option is set).

Changes:
- add smb_compress_alloc_size() helper to compute compressed buffer
  size, to account for Pattern_V1 payloads as well
- adjust server/compress.c to use smb_compress_alloc_size()
- adjust client/compress.c to use smb_compression_compress(), and fixup
  the compression header (as client sends the SMB2 header uncompressed)

Signed-off-by: Enzo Matsumiya <[email protected]>
---
 fs/smb/client/cifs_debug.c        | 18 ++++++---
 fs/smb/client/compress.c          | 61 +++++++++++++++++++++++--------
 fs/smb/client/smb2pdu.c           | 17 +++++++++
 fs/smb/common/compress/compress.h | 38 +++++++++++++++++++
 fs/smb/server/compress.c          | 19 ++++++----
 5 files changed, 124 insertions(+), 29 deletions(-)

diff --git a/fs/smb/client/cifs_debug.c b/fs/smb/client/cifs_debug.c
index 4ed4f55a0bb7..165fc4b25f8f 100644
--- a/fs/smb/client/cifs_debug.c
+++ b/fs/smb/client/cifs_debug.c
@@ -552,14 +552,20 @@ static int cifs_debug_data_proc_show(struct seq_file *m, void *v)
 		}
 
 		seq_puts(m, "\nCompression: ");
-		if (!IS_ENABLED(CONFIG_CIFS_COMPRESSION))
+		if (!IS_ENABLED(CONFIG_CIFS_COMPRESSION)) {
 			seq_puts(m, "no built-in support");
-		else if (!server->compression.requested)
+		} else if (!server->compression.requested) {
 			seq_puts(m, "disabled on mount");
-		else if (server->compression.enabled)
-			seq_printf(m, "enabled (%s)", compression_alg_str(server->compression.alg));
-		else
-			seq_puts(m, "disabled (not supported by this server)");
+		} else if (!server->compression.enabled) {
+			seq_puts(m, "disabled"); /* XXX: reason? */
+		} else {
+			seq_printf(m, "enabled, chained: %s, algs: ",
+				   str_yes_no(server->compression.chained));
+
+			seq_printf(m, "%s ", compression_alg_str(server->compression.alg));
+			if (server->compression.pattern)
+				seq_printf(m, "%s ", compression_alg_str(SMB3_COMPRESS_PATTERN));
+		}
 
 		/* Show negotiated encryption cipher, even if not required */
 		seq_puts(m, "\nEncryption: ");
diff --git a/fs/smb/client/compress.c b/fs/smb/client/compress.c
index 925b01a4d0a5..37e92e2bc816 100644
--- a/fs/smb/client/compress.c
+++ b/fs/smb/client/compress.c
@@ -352,8 +352,9 @@ bool should_compress(const struct cifs_tcon *tcon, const struct smb_rqst *rq)
 int smb_compress(struct TCP_Server_Info *server, struct smb_rqst *rq, compress_send_fn send_fn)
 {
 	struct iov_iter iter;
-	u32 slen, dlen;
+	bool chained, use_pattern;
 	void *src, *dst = NULL;
+	u32 slen, dlen;
 	int ret;
 
 	if (!server || !rq || !rq->rq_iov || !rq->rq_iov->iov_base)
@@ -394,30 +395,60 @@ int smb_compress(struct TCP_Server_Info *server, struct smb_rqst *rq, compress_s
 		goto err_free;
 	}
 
-	dlen = smb_lz77_compressed_alloc_size(slen);
+	chained = server->compression.chained;
+	use_pattern = server->compression.pattern;
+	dlen = smb_compress_alloc_size(slen, chained, use_pattern);
 	dst = kvzalloc(dlen, GFP_KERNEL);
 	if (!dst) {
 		ret = -ENOMEM;
 		goto err_free;
 	}
 
-	ret = smb_lz77_compress(src, slen, dst, &dlen);
+	ret = smb_compression_compress(SMB3_COMPRESS_LZ77, chained, use_pattern,
+				       src, slen, dst, &dlen);
 	if (!ret) {
-		struct smb2_compression_hdr hdr = { 0 };
+		struct smb2_compression_hdr *hdrp = dst, hdr = {};
 		struct smb_rqst comp_rq = { .rq_nvec = 3, };
 		struct kvec iov[3];
+		u32 payload_offset = sizeof(hdr);
+		u32 shdr_len = rq->rq_iov[0].iov_len;
+
+		/*
+		 * smb_compression_compress() already setup a compression header in @dst, but we're
+		 * sending the SMB2 header uncompressed, so we need to adjust our iovs layout to
+		 * accommodate that when @chained (iov[2] always points to the start of compressed
+		 * data, regardless of @chained).
+		 *
+		 * Sneak in a "NONE" payload header (that corresponds to our uncompressed SMB2
+		 * header) as the first one, and point iov[2] to the original first payload header
+		 * created (which now becomes the second payload header).
+		 *
+		 * If unchained, we can just use the created header.
+		 */
+		hdr = *hdrp;
+		if (chained) {
+			struct smb2_compression_payload_hdr *phdr;
+
+			payload_offset = SMB2_COMPRESSION_CHAINED_HDR_LEN;
+			le32_add_cpu(&hdr.OriginalCompressedSegmentSize, shdr_len);
+			hdr.CompressionAlgorithm = SMB3_COMPRESS_NONE;
+			hdrp = &hdr;
+
+			/*
+			 * Only the first payload header must have the CHAINED flag set, so strip
+			 * the original one.
+			 */
+			phdr = dst + payload_offset;
+			phdr->Flags = cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE);
+		}
+
+		hdrp->Offset = cpu_to_le32(shdr_len);
 
-		hdr.ProtocolId = SMB2_COMPRESSION_TRANSFORM_ID;
-		hdr.OriginalCompressedSegmentSize = cpu_to_le32(slen);
-		hdr.CompressionAlgorithm = SMB3_COMPRESS_LZ77;
-		hdr.Flags = SMB2_COMPRESSION_FLAG_NONE;
-		hdr.Offset = cpu_to_le32(rq->rq_iov[0].iov_len);
-
-		iov[0].iov_base = &hdr;
-		iov[0].iov_len = sizeof(hdr);
-		iov[1] = rq->rq_iov[0];
-		iov[2].iov_base = dst;
-		iov[2].iov_len = dlen;
+		iov[0].iov_base = hdrp;
+		iov[0].iov_len = sizeof(*hdrp);
+		iov[1] = rq->rq_iov[0]; /* this is the SMB2 header */
+		iov[2].iov_base = dst + payload_offset;
+		iov[2].iov_len = dlen - payload_offset;
 
 		comp_rq.rq_iov = iov;
 
diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
index e1ed23b3dd2c..cc20d9b6638f 100644
--- a/fs/smb/client/smb2pdu.c
+++ b/fs/smb/client/smb2pdu.c
@@ -892,6 +892,12 @@ static void decode_compress_ctx(struct TCP_Server_Info *server,
 	/*
 	 * Pattern_V1 cannot appear in an unchained transform even if a broken
 	 * peer lists it in the algorithm array.
+	 *
+	 * OTOH, the chained flag might have been negotiated without either/both peers having not
+	 * advertised Pattern_V1 algorithm support.
+	 *
+	 * (which doesn't make much sense, but we'll still have to send compressed payloads with
+	 * the chained flag set in "chained && !pattern_v1" cases)
 	 */
 	chained = (ctxt->Flags == SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED);
 	if (!chained)
@@ -1382,6 +1388,12 @@ SMB2_negotiate(const unsigned int xid,
 			rc = smb_EIO1(smb_eio_trace_neg_decode_token, rc);
 	}
 
+	/*
+	 * Disable compression until we decode negcontext -- keep other settings as they are, in
+	 * case there are channels doing compression concurrently.
+	 */
+	server->compression.enabled = false;
+
 	if (server->dialect == SMB311_PROT_ID) {
 		if (rsp->NegotiateContextCount)
 			rc = smb311_decode_neg_context(rsp, server,
@@ -1390,6 +1402,11 @@ SMB2_negotiate(const unsigned int xid,
 			cifs_server_dbg(VFS, "Missing expected negotiate contexts\n");
 	}
 
+	if (server->compression.requested && !server->compression.enabled) {
+		cifs_server_dbg(VFS, "Server doesn't support SMB2 compression\n");
+		disable_compression(server);
+	}
+
 	if (server->cipher_type && !rc)
 		rc = smb3_crypto_aead_allocate(server);
 neg_exit:
diff --git a/fs/smb/common/compress/compress.h b/fs/smb/common/compress/compress.h
index ade37d552067..601ccb789fa4 100644
--- a/fs/smb/common/compress/compress.h
+++ b/fs/smb/common/compress/compress.h
@@ -145,6 +145,44 @@ static __always_inline bool smb_compress_alg_valid(__le16 alg, bool valid_none)
 	return alg == SMB3_COMPRESS_LZ77 || alg == SMB3_COMPRESS_PATTERN;
 }
 
+/**
+ * smb_compress_alloc_size() - Compute total allocation size required for compressed (dst) buffer.
+ * @size:		uncompressed size
+ * @chained:		if chained compression is enabled
+ * @use_pattern:	if Pattern_V1 is enabled
+ *
+ * For any case:
+ * - SMB2 compression hdr
+ * - LZ* payload
+ *
+ * If @chained, account for:
+ * - 1x payload hdr
+ *
+ * If @use_pattern, also account for:
+ * - 2x payload hdr for Pattern_V1
+ * - 2x Pattern_V1 payloads
+ *
+ * (possible uncompressed leftovers are included in LZ alloc size)
+ *
+ * This helper assumes that the invalid combination (!@chained && @use_pattern) was previously
+ * checked by the caller.
+ */
+static __always_inline u32 smb_compress_alloc_size(const u32 size, const bool chained,
+						   const bool use_pattern)
+{
+	u32 alloc_size;
+
+	alloc_size = sizeof(struct smb2_compression_hdr) + smb_lz77_compressed_alloc_size(size);
+	if (chained) {
+		alloc_size += SMB2_COMPRESSION_PAYLOAD_BASE_LEN;
+		if (use_pattern)
+			alloc_size += (SMB2_COMPRESSION_PAYLOAD_BASE_LEN * 2) +
+				(sizeof(struct smb2_compression_pattern_v1) * 2);
+	}
+
+	return alloc_size;
+}
+
 int smb_compression_decompress(__le16 alg, bool allow_chained,
 			       const void *src, u32 slen, void *dst, u32 dlen);
 int smb_compression_compress(__le16 alg, bool chained, bool allow_pattern,
diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c
index da7dea58c5a7..a64fbc278ad3 100644
--- a/fs/smb/server/compress.c
+++ b/fs/smb/server/compress.c
@@ -98,6 +98,7 @@ int ksmbd_compress_response(struct ksmbd_work *work)
 	struct smb2_hdr *req_hdr;
 	u32 src_len, dst_len, compressed_pdu_len, max_dst_len;
 	u8 *src = NULL, *out = NULL, *p;
+	bool chained, pattern;
 	int i, rc;
 
 	if (!work->compress_response || work->encrypted ||
@@ -132,10 +133,14 @@ int ksmbd_compress_response(struct ksmbd_work *work)
 		goto out;
 	}
 
-	max_dst_len = smb_lz77_compressed_alloc_size(src_len) +
-		sizeof(struct smb2_compression_hdr) +
-		3 * sizeof(struct smb2_compression_payload_hdr) +
-		2 * sizeof(struct smb2_compression_pattern_v1);
+	chained = work->conn->compress_chained;
+	pattern = work->conn->compress_pattern;
+	if (unlikely(!chained && pattern)) {
+		rc = -EINVAL;
+		goto out;
+	}
+
+	max_dst_len = smb_compress_alloc_size(src_len, chained, pattern);
 	out = kvzalloc(sizeof(__be32) + max_dst_len,
 		       KSMBD_DEFAULT_GFP);
 	if (!out) {
@@ -143,11 +148,9 @@ int ksmbd_compress_response(struct ksmbd_work *work)
 		goto out;
 	}
 
-	if (work->conn->compress_chained) {
+	if (chained) {
 		dst_len = max_dst_len;
-		rc = smb_compression_compress(SMB3_COMPRESS_LZ77,
-					      work->conn->compress_chained,
-					      work->conn->compress_pattern,
+		rc = smb_compression_compress(SMB3_COMPRESS_LZ77, chained, pattern,
 					      src, src_len,
 					      out + sizeof(__be32),
 					      &dst_len);
-- 
2.54.0