]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
ksmbd: compress SMB2 READ responses
authorNamjae Jeon <linkinjeon@kernel.org>
Wed, 10 Jun 2026 09:46:10 +0000 (18:46 +0900)
committerSteve French <stfrench@microsoft.com>
Tue, 16 Jun 2026 23:57:22 +0000 (18:57 -0500)
Handle SMB2_READFLAG_REQUEST_COMPRESSED for non-RDMA reads.

Flatten the response iov, emit chained or unchained LZ77 transforms when
compression is beneficial, and retain the generated buffer until the work
item is released.

Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
Signed-off-by: Steve French <stfrench@microsoft.com>
fs/smb/server/compress.c
fs/smb/server/compress.h
fs/smb/server/ksmbd_work.c
fs/smb/server/ksmbd_work.h
fs/smb/server/server.c
fs/smb/server/smb2pdu.c

index 7c9f8a6cceb8e2d09b3a88fdea3e007531d526d6..f8cf515b9c30a6e6f0a4a6a7b1b50bf72055e0a6 100644 (file)
@@ -10,6 +10,9 @@
 
 #include "compress.h"
 #include "smb_common.h"
+#include "../common/compress/lz77.h"
+
+#define SMB_COMPRESS_MIN_LEN   PAGE_SIZE
 
 /**
  * ksmbd_decompress_request() - replace a compressed request with its SMB2 PDU
@@ -75,3 +78,132 @@ int ksmbd_decompress_request(struct ksmbd_conn *conn)
        conn->request_buf = out;
        return 0;
 }
+
+/**
+ * ksmbd_compress_response() - compress an eligible ksmbd response
+ * @work: request work item containing the response iov
+ *
+ * Compression transforms describe one contiguous SMB2 message, while ksmbd
+ * builds responses from multiple iov entries. Flatten the response first,
+ * produce the negotiated transform, and replace the response iov only when the
+ * result is smaller than the original message.
+ *
+ * Encrypted and compound responses are intentionally left unchanged. The
+ * caller may still continue sending the original response when this function
+ * returns zero.
+ *
+ * Return: 1 if the response was replaced, 0 if compression was skipped, or a
+ * negative errno on failure.
+ */
+int ksmbd_compress_response(struct ksmbd_work *work)
+{
+       struct smb2_compression_hdr *chdr;
+       struct smb2_hdr *req_hdr;
+       u32 src_len, dst_len, compressed_pdu_len, max_dst_len;
+       u8 *src = NULL, *out = NULL, *p;
+       int i, rc;
+
+       if (!work->compress_response || work->encrypted ||
+           work->conn->compress_algorithm != SMB3_COMPRESS_LZ77)
+               return 0;
+
+       req_hdr = smb_get_msg(work->request_buf);
+       if (req_hdr->NextCommand || work->next_smb2_rcv_hdr_off ||
+           work->next_smb2_rsp_hdr_off)
+               return 0;
+
+       src_len = get_rfc1002_len(work->iov[0].iov_base);
+       if (src_len < SMB_COMPRESS_MIN_LEN)
+               return 0;
+
+       src = kvmalloc(src_len, KSMBD_DEFAULT_GFP);
+       if (!src)
+               return -ENOMEM;
+
+       p = src;
+       /* iov[0] contains only the RFC1002 length; the SMB2 PDU starts at iov[1]. */
+       for (i = 1; i < work->iov_cnt; i++) {
+               if (work->iov[i].iov_len > src + src_len - p) {
+                       rc = -EINVAL;
+                       goto out;
+               }
+               memcpy(p, work->iov[i].iov_base, work->iov[i].iov_len);
+               p += work->iov[i].iov_len;
+       }
+       if (p != src + src_len) {
+               rc = -EINVAL;
+               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);
+       out = kvzalloc(sizeof(__be32) + max_dst_len,
+                      KSMBD_DEFAULT_GFP);
+       if (!out) {
+               rc = -ENOMEM;
+               goto out;
+       }
+
+       if (work->conn->compress_chained) {
+               dst_len = max_dst_len;
+               rc = smb_compression_compress_chained(SMB3_COMPRESS_LZ77,
+                                                     work->conn->compress_pattern,
+                                                     src, src_len,
+                                                     out + sizeof(__be32),
+                                                     &dst_len);
+               if (rc == -EMSGSIZE || dst_len >= src_len) {
+                       rc = 0;
+                       goto out;
+               }
+               if (rc)
+                       goto out;
+               compressed_pdu_len = dst_len;
+       } else {
+               /*
+                * Peers which did not negotiate chained compression still use
+                * the original 16-byte unchained transform format.
+                */
+               dst_len = smb_lz77_compressed_alloc_size(src_len);
+               rc = smb_lz77_compress(src, src_len,
+                                      out + sizeof(__be32) + sizeof(*chdr),
+                                      &dst_len);
+               if (rc == -EMSGSIZE ||
+                   dst_len + sizeof(*chdr) >= src_len) {
+                       rc = 0;
+                       goto out;
+               }
+               if (rc)
+                       goto out;
+
+               compressed_pdu_len = sizeof(*chdr) + dst_len;
+               chdr = (struct smb2_compression_hdr *)(out + sizeof(__be32));
+               chdr->ProtocolId = SMB2_COMPRESSION_TRANSFORM_ID;
+               chdr->OriginalCompressedSegmentSize = cpu_to_le32(src_len);
+               chdr->CompressionAlgorithm = SMB3_COMPRESS_LZ77;
+               chdr->Flags = cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE);
+               chdr->Offset = 0;
+       }
+
+       *(__be32 *)out = cpu_to_be32(compressed_pdu_len);
+
+       /*
+        * Keep the transform in work->compress_buf until send completion.
+        * Existing response iovs can then be replaced without changing their
+        * individual ownership rules.
+        */
+       work->compress_buf = out;
+       work->iov[0].iov_base = out;
+       work->iov[0].iov_len = sizeof(__be32);
+       work->iov[1].iov_base = out + sizeof(__be32);
+       work->iov[1].iov_len = compressed_pdu_len;
+       work->iov_cnt = 2;
+       work->iov_idx = 1;
+       out = NULL;
+       rc = 1;
+out:
+       kvfree(out);
+       kvfree(src);
+       return rc;
+}
index 49b36d931aac4ad280c542f14e4f1962b04f3035..663c6f44f09b01e92d224010adcc95a7c45c7a3b 100644 (file)
@@ -11,5 +11,6 @@
 #include "../common/compress/compress.h"
 
 int ksmbd_decompress_request(struct ksmbd_conn *conn);
+int ksmbd_compress_response(struct ksmbd_work *work);
 
 #endif /* __KSMBD_COMPRESS_H__ */
index ab4958dc3eb0115e477fabc2b31f14106c403084..a5ab6799a65c50840d622f7fcc02f1a0aec53bcc 100644 (file)
@@ -53,6 +53,7 @@ void ksmbd_free_work_struct(struct ksmbd_work *work)
        }
 
        kfree(work->tr_buf);
+       kvfree(work->compress_buf);
        kvfree(work->request_buf);
        kfree(work->iov);
 
index d36393ff8310cd7e9eb6ea6e18314a51ab15e76f..0da8cc0972d6d5c494fcd212de9d34382d266498 100644 (file)
@@ -67,12 +67,16 @@ struct ksmbd_work {
        unsigned int                    response_sz;
 
        void                            *tr_buf;
+       /* Contiguous SMB2 compression transform owned by this work item. */
+       void                            *compress_buf;
 
        unsigned char                   state;
        /* No response for cancelled request */
        bool                            send_no_response:1;
        /* Request is encrypted */
        bool                            encrypted:1;
+       /* READ response should be wrapped in a compression transform. */
+       bool                            compress_response:1;
        /* Is this SYNC or ASYNC ksmbd_work */
        bool                            asynchronous:1;
        bool                            need_invalidate_rkey:1;
index 5d799b2d4c62f4c36dd025e162e1984fe606775c..36feda7e094269381f8cbefa3df485258e06a5bb 100644 (file)
@@ -22,6 +22,7 @@
 #include "crypto_ctx.h"
 #include "auth.h"
 #include "stats.h"
+#include "compress.h"
 
 int ksmbd_debug_types;
 
@@ -244,6 +245,15 @@ send:
        if (work->tcon)
                ksmbd_tree_connect_put(work->tcon);
        smb3_preauth_hash_rsp(work);
+       /*
+        * Preauthentication hashes cover the original SMB2 response. Apply the
+        * transport compression wrapper only after updating the hash.
+        */
+       if (work->compress_response) {
+               rc = ksmbd_compress_response(work);
+               if (rc < 0)
+                       ksmbd_debug(CONN, "Failed to compress response: %d\n", rc);
+       }
        if (work->sess && work->sess->enc && work->encrypted &&
            conn->ops->encrypt_resp) {
                rc = conn->ops->encrypt_resp(work);
index 25742eb3f4833e8750daaa596f60b658e26c0ec1..ae451e77689cb686bce0a854b8162965a226f200 100644 (file)
@@ -7115,6 +7115,15 @@ int smb2_read(struct ksmbd_work *work)
                kvfree(aux_payload_buf);
                goto out;
        }
+       /*
+        * RDMA responses are transferred through channel buffers and encrypted
+        * responses use the encryption transform, so only normal SMB transport
+        * responses are candidates for compression.
+        */
+       if (!is_rdma_channel && nbytes &&
+           (req->Flags & SMB2_READFLAG_REQUEST_COMPRESSED) &&
+           conn->compress_algorithm != SMB3_COMPRESS_NONE)
+               work->compress_response = true;
        ksmbd_fd_put(work, fp);
        return 0;