From: Namjae Jeon Date: Wed, 10 Jun 2026 09:46:10 +0000 (+0900) Subject: ksmbd: compress SMB2 READ responses X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=08f641e2e2e092cbda5ce7f7b5280e327e46823d;p=thirdparty%2Flinux.git ksmbd: compress SMB2 READ responses 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 Signed-off-by: Steve French --- diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c index 7c9f8a6cceb8e..f8cf515b9c30a 100644 --- a/fs/smb/server/compress.c +++ b/fs/smb/server/compress.c @@ -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; +} diff --git a/fs/smb/server/compress.h b/fs/smb/server/compress.h index 49b36d931aac4..663c6f44f09b0 100644 --- a/fs/smb/server/compress.h +++ b/fs/smb/server/compress.h @@ -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__ */ diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index ab4958dc3eb01..a5ab6799a65c5 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -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); diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index d36393ff8310c..0da8cc0972d6d 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -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; diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 5d799b2d4c62f..36feda7e09426 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -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); diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 25742eb3f4833..ae451e77689cb 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -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;