diff options
Diffstat (limited to 'fs/smb/server/compress.c')
| -rw-r--r-- | fs/smb/server/compress.c | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c new file mode 100644 index 000000000..5162fb84c --- /dev/null +++ b/fs/smb/server/compress.c @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SMB2 compression support for ksmbd. + * + * Receive and send SMB 3.1.1 compression transforms using the common helpers. + * + * Copyright (C) 2026 Namjae Jeon <linkinjeon@kernel.org> + */ +#include <linux/slab.h> + +#include "compress.h" +#include "smb_common.h" +#include "../common/compress/lz77.h" + +#define SMB_COMPRESS_MIN_LEN PAGE_SIZE + +static int __ksmbd_decompress_request(struct ksmbd_conn *conn, + void *request_buf, void **out_buf) +{ + struct smb2_compression_hdr *hdr; + unsigned int pdu_size = get_rfc1002_len(request_buf); + u32 orig_size, offset, out_size; + u32 max_allowed_pdu_size; + char *out; + int rc; + + if (pdu_size < sizeof(struct smb2_compression_hdr)) + return -EINVAL; + + if (conn->dialect != SMB311_PROT_ID || + conn->compress_algorithm == SMB3_COMPRESS_NONE) + return -EINVAL; + + hdr = smb_get_msg(request_buf); + if (hdr->ProtocolId != SMB2_COMPRESSION_TRANSFORM_ID) + return -EINVAL; + + orig_size = le32_to_cpu(hdr->OriginalCompressedSegmentSize); + /* + * For chained transforms the top-level header is only eight bytes; the + * Flags field overlays the first payload header. Reject unknown Flags + * and unnegotiated chained mode before allocating the output buffer. + */ + if (hdr->Flags == cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED)) { + if (!conn->compress_chained) + return -EINVAL; + out_size = orig_size; + } else if (hdr->Flags == cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE)) { + offset = le32_to_cpu(hdr->Offset); + if (offset > pdu_size - sizeof(*hdr) || + check_add_overflow(orig_size, offset, &out_size)) + return -EINVAL; + } else { + return -EINVAL; + } + + max_allowed_pdu_size = ksmbd_max_allowed_pdu_size(conn); + if (out_size < sizeof(struct smb2_pdu) || + out_size > max_allowed_pdu_size || + out_size > MAX_STREAM_PROT_LEN) + return -EINVAL; + + out = kvmalloc(out_size + 4 + 1, KSMBD_DEFAULT_GFP); + if (!out) + return -ENOMEM; + + *(__be32 *)out = cpu_to_be32(out_size); + rc = smb_compression_decompress(conn->compress_algorithm, + conn->compress_chained, + conn->compress_pattern, + (char *)hdr, pdu_size, out + 4, out_size); + if (rc) { + kvfree(out); + return rc; + } + + *out_buf = out; + return 0; +} + +/** + * ksmbd_decompress_request() - replace a compressed request with its SMB2 PDU + * @conn: connection which owns the current RFC1002 request buffer + * + * Derive the uncompressed size from the transform variant, enforce ksmbd's + * normal message limits, and ask the common decoder to validate every payload. + * On success, replace conn->request_buf with a regular RFC1002-framed SMB2 + * message so the rest of the request path needs no compression awareness. + * + * Return: 0 on success, otherwise a negative errno. + */ +int ksmbd_decompress_request(struct ksmbd_conn *conn) +{ + void *out_buf; + int rc; + + rc = __ksmbd_decompress_request(conn, conn->request_buf, &out_buf); + if (rc) + return rc; + + kvfree(conn->request_buf); + conn->request_buf = out_buf; + return 0; +} + +/** + * ksmbd_decompress_work_request() - decompress an encrypted work request + * @work: work item whose request buffer contains a compression transform + * + * SMB3 encrypts a compressed message by applying compression first and + * encryption second. The receive loop can therefore only decode the + * compression transform before work allocation for an unencrypted request; + * an encrypted request must be decompressed after its encryption layer has + * been removed. + * + * Return: 0 on success, otherwise a negative errno. + */ +int ksmbd_decompress_work_request(struct ksmbd_work *work) +{ + void *out_buf; + int rc; + + rc = __ksmbd_decompress_request(work->conn, work->request_buf, + &out_buf); + if (rc) + return rc; + + kvfree(work->request_buf); + work->request_buf = out_buf; + 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; +} |
