From: Namjae Jeon Date: Wed, 10 Jun 2026 09:46:00 +0000 (+0900) Subject: ksmbd: negotiate and decode SMB2 compression X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a08de24c2b8568a26b560cda411284295decb3ba;p=thirdparty%2Flinux.git ksmbd: negotiate and decode SMB2 compression Parse the SMB 3.1.1 compression capabilities context and negotiate LZ77 with optional chained Pattern_V1 support. Advertise compression on tree connections and decode compressed requests before normal SMB dispatch. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- diff --git a/fs/smb/server/Makefile b/fs/smb/server/Makefile index 6407ba6b93401..a3e9306055e8b 100644 --- a/fs/smb/server/Makefile +++ b/fs/smb/server/Makefile @@ -10,7 +10,7 @@ ksmbd-y := unicode.o auth.o vfs.o vfs_cache.o server.o ndr.o \ mgmt/tree_connect.o mgmt/user_session.o smb_common.o \ transport_tcp.o transport_ipc.o smbacl.o smb2pdu.o \ smb2ops.o smb2misc.o ksmbd_spnego_negtokeninit.asn1.o \ - ksmbd_spnego_negtokentarg.asn1.o asn1.o + ksmbd_spnego_negtokentarg.asn1.o asn1.o compress.o $(obj)/asn1.o: $(obj)/ksmbd_spnego_negtokeninit.asn1.h $(obj)/ksmbd_spnego_negtokentarg.asn1.h diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c new file mode 100644 index 0000000000000..7c9f8a6cceb8e --- /dev/null +++ b/fs/smb/server/compress.c @@ -0,0 +1,77 @@ +// 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 + */ +#include + +#include "compress.h" +#include "smb_common.h" + +/** + * 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) +{ + struct smb2_compression_hdr *hdr; + unsigned int pdu_size = get_rfc1002_len(conn->request_buf); + u32 orig_size, offset, out_size; + u32 max_allowed_pdu_size; + char *buf, *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(conn->request_buf); + if (hdr->ProtocolId != SMB2_COMPRESSION_TRANSFORM_ID) + return -EINVAL; + + orig_size = le32_to_cpu(hdr->OriginalCompressedSegmentSize); + if (hdr->Flags == cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED)) { + out_size = orig_size; + } else { + offset = le32_to_cpu(hdr->Offset); + if (offset > pdu_size - sizeof(*hdr) || + check_add_overflow(orig_size, offset, &out_size)) + return -EINVAL; + } + + max_allowed_pdu_size = SMB3_MAX_MSGSIZE + conn->vals->max_write_size; + if (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; + + buf = (char *)hdr; + *(__be32 *)out = cpu_to_be32(out_size); + rc = smb_compression_decompress(conn->compress_algorithm, + conn->compress_chained, + buf, pdu_size, out + 4, out_size); + if (rc) { + kvfree(out); + return rc; + } + + kvfree(conn->request_buf); + conn->request_buf = out; + return 0; +} diff --git a/fs/smb/server/compress.h b/fs/smb/server/compress.h new file mode 100644 index 0000000000000..49b36d931aac4 --- /dev/null +++ b/fs/smb/server/compress.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * SMB2 compression support for ksmbd. + * + * Copyright (C) 2026 Namjae Jeon + */ +#ifndef __KSMBD_COMPRESS_H__ +#define __KSMBD_COMPRESS_H__ + +#include "connection.h" +#include "../common/compress/compress.h" + +int ksmbd_decompress_request(struct ksmbd_conn *conn); + +#endif /* __KSMBD_COMPRESS_H__ */ diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 8347495dbc628..9e8fdb39e5a2a 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -12,6 +12,7 @@ #include "smb_common.h" #include "mgmt/ksmbd_ida.h" #include "connection.h" +#include "compress.h" #include "transport_tcp.h" #include "transport_rdma.h" #include "misc.h" @@ -531,6 +532,17 @@ recheck: continue; } + if (((struct smb2_hdr *)smb_get_msg(conn->request_buf))->ProtocolId == + SMB2_COMPRESSION_TRANSFORM_ID) { + /* + * Convert the transform into a normal RFC1002-framed SMB2 + * request before protocol validation and work allocation. + */ + if (ksmbd_decompress_request(conn)) + break; + pdu_size = get_rfc1002_len(conn->request_buf); + } + if (!ksmbd_smb_request(conn)) break; diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index e074be9425823..ec75633b7da0f 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -115,6 +115,9 @@ struct ksmbd_conn { __le16 cipher_type; __le16 compress_algorithm; + /* Negotiated SMB 3.1.1 compression capabilities. */ + bool compress_chained; + bool compress_pattern; bool posix_ext_supported; bool signing_negotiated; __le16 signing_algorithm; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f9106b35e63cf..25742eb3f4833 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -42,6 +42,7 @@ #include "ndr.h" #include "stats.h" #include "transport_tcp.h" +#include "compress.h" static void __wbuf(struct ksmbd_work *work, void **req, void **rsp) { @@ -804,6 +805,30 @@ static void build_encrypt_ctxt(struct smb2_encryption_neg_context *pneg_ctxt, pneg_ctxt->Ciphers[0] = cipher_type; } +static void build_compress_ctxt(struct smb2_compression_capabilities_context *pneg_ctxt, + __le16 compress_algorithm, bool compress_chained, + bool compress_pattern) +{ + /* + * Return only algorithms implemented by ksmbd. Pattern_V1 is advertised + * as a second ID when the client also enabled chained transforms. + */ + pneg_ctxt->ContextType = SMB2_COMPRESSION_CAPABILITIES; + pneg_ctxt->DataLength = cpu_to_le16(compress_pattern ? 12 : 10); + pneg_ctxt->Reserved = cpu_to_le32(0); + pneg_ctxt->CompressionAlgorithmCount = + cpu_to_le16(compress_pattern ? 2 : 1); + pneg_ctxt->Padding = cpu_to_le16(0); + pneg_ctxt->Flags = compress_chained ? + SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED : + SMB2_COMPRESSION_CAPABILITIES_FLAG_NONE; + pneg_ctxt->CompressionAlgorithms[0] = compress_algorithm; + pneg_ctxt->CompressionAlgorithms[1] = compress_pattern ? + SMB3_COMPRESS_PATTERN : 0; + pneg_ctxt->CompressionAlgorithms[2] = 0; + pneg_ctxt->CompressionAlgorithms[3] = 0; +} + static void build_sign_cap_ctxt(struct smb2_signing_capabilities *pneg_ctxt, __le16 sign_algo) { @@ -865,8 +890,19 @@ static unsigned int assemble_neg_contexts(struct ksmbd_conn *conn, ctxt_size += sizeof(struct smb2_encryption_neg_context) + 2; } - /* compression context not yet supported */ - WARN_ON(conn->compress_algorithm != SMB3_COMPRESS_NONE); + if (conn->compress_algorithm != SMB3_COMPRESS_NONE) { + ctxt_size = round_up(ctxt_size, 8); + ksmbd_debug(SMB, + "assemble SMB2_COMPRESSION_CAPABILITIES context\n"); + build_compress_ctxt((struct smb2_compression_capabilities_context *) + (pneg_ctxt + ctxt_size), + conn->compress_algorithm, + conn->compress_chained, + conn->compress_pattern); + neg_ctxt_cnt++; + ctxt_size += sizeof(struct smb2_neg_context) + + (conn->compress_pattern ? 12 : 10); + } if (conn->posix_ext_supported) { ctxt_size = round_up(ctxt_size, 8); @@ -970,10 +1006,59 @@ bool smb3_encryption_negotiated(struct ksmbd_conn *conn) conn->cipher_type; } -static void decode_compress_ctxt(struct ksmbd_conn *conn, - struct smb2_compression_capabilities_context *pneg_ctxt) +static __le32 decode_compress_ctxt(struct ksmbd_conn *conn, + struct smb2_compression_capabilities_context *pneg_ctxt, + int ctxt_len) { + int alg_cnt, algs_size, i; + + if (sizeof(struct smb2_neg_context) + 10 > ctxt_len) { + pr_err("Invalid SMB2_COMPRESSION_CAPABILITIES context length\n"); + return STATUS_INVALID_PARAMETER; + } + conn->compress_algorithm = SMB3_COMPRESS_NONE; + conn->compress_chained = false; + conn->compress_pattern = false; + + alg_cnt = le16_to_cpu(pneg_ctxt->CompressionAlgorithmCount); + if (!alg_cnt) + return STATUS_INVALID_PARAMETER; + + if (pneg_ctxt->Flags != SMB2_COMPRESSION_CAPABILITIES_FLAG_NONE && + pneg_ctxt->Flags != SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED) + return STATUS_INVALID_PARAMETER; + + algs_size = alg_cnt * sizeof(__le16); + if (sizeof(struct smb2_neg_context) + 8 + algs_size > ctxt_len) { + pr_err("Invalid compression algorithm count(%d)\n", alg_cnt); + return STATUS_INVALID_PARAMETER; + } + + for (i = 0; i < alg_cnt; i++) { + __le16 alg = pneg_ctxt->CompressionAlgorithms[i]; + + /* + * LZ77 is the required general-purpose codec. Pattern_V1 is an + * optional chained payload type and cannot stand alone. + */ + if (alg == SMB3_COMPRESS_LZ77) { + conn->compress_algorithm = alg; + conn->compress_chained = + pneg_ctxt->Flags == + SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED; + ksmbd_debug(SMB, "Compression Algorithm ID = 0x%x\n", + le16_to_cpu(alg)); + } else if (alg == SMB3_COMPRESS_PATTERN) { + conn->compress_pattern = true; + } + } + + if (conn->compress_algorithm == SMB3_COMPRESS_NONE || + !conn->compress_chained) + conn->compress_pattern = false; + + return STATUS_SUCCESS; } static void decode_sign_cap_ctxt(struct ksmbd_conn *conn, @@ -1021,6 +1106,7 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, unsigned int offset = le32_to_cpu(req->NegotiateContextOffset); unsigned int neg_ctxt_cnt = le16_to_cpu(req->NegotiateContextCount); __le32 status = STATUS_INVALID_PARAMETER; + int compress_ctxt_cnt = 0; ksmbd_debug(SMB, "decoding %d negotiate contexts\n", neg_ctxt_cnt); if (len_of_smb <= offset) { @@ -1066,11 +1152,16 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, } else if (pctx->ContextType == SMB2_COMPRESSION_CAPABILITIES) { ksmbd_debug(SMB, "deassemble SMB2_COMPRESSION_CAPABILITIES context\n"); - if (conn->compress_algorithm) + if (compress_ctxt_cnt++) { + status = STATUS_INVALID_PARAMETER; break; + } - decode_compress_ctxt(conn, - (struct smb2_compression_capabilities_context *)pctx); + status = decode_compress_ctxt(conn, + (struct smb2_compression_capabilities_context *) + pctx, ctxt_len); + if (status != STATUS_SUCCESS) + break; } else if (pctx->ContextType == SMB2_NETNAME_NEGOTIATE_CONTEXT_ID) { ksmbd_debug(SMB, "deassemble SMB2_NETNAME_NEGOTIATE_CONTEXT_ID context\n"); @@ -2061,6 +2152,10 @@ out_err1: rsp->Reserved = 0; /* default manual caching */ rsp->ShareFlags = SMB2_SHAREFLAG_MANUAL_CACHING; + /* Tell the client that READ requests may request compressed responses. */ + if (conn->dialect == SMB311_PROT_ID && + conn->compress_algorithm != SMB3_COMPRESS_NONE) + rsp->ShareFlags |= cpu_to_le32(SMB2_SHAREFLAG_COMPRESS_DATA); rc = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_tree_connect_rsp)); if (rc) diff --git a/fs/smb/server/smb_common.c b/fs/smb/server/smb_common.c index 82de4fdfe4462..7de73223189ae 100644 --- a/fs/smb/server/smb_common.c +++ b/fs/smb/server/smb_common.c @@ -185,11 +185,6 @@ bool ksmbd_smb_request(struct ksmbd_conn *conn) return false; proto = (__le32 *)smb_get_msg(conn->request_buf); - if (*proto == SMB2_COMPRESSION_TRANSFORM_ID) { - pr_err_ratelimited("smb2 compression not support yet"); - return false; - } - if (*proto != SMB1_PROTO_NUMBER && *proto != SMB2_PROTO_NUMBER && *proto != SMB2_TRANSFORM_PROTO_NUM)