]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
ksmbd: negotiate and decode SMB2 compression
authorNamjae Jeon <linkinjeon@kernel.org>
Wed, 10 Jun 2026 09:46:00 +0000 (18:46 +0900)
committerSteve French <stfrench@microsoft.com>
Tue, 16 Jun 2026 23:57:22 +0000 (18:57 -0500)
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 <linkinjeon@kernel.org>
Signed-off-by: Steve French <stfrench@microsoft.com>
fs/smb/server/Makefile
fs/smb/server/compress.c [new file with mode: 0644]
fs/smb/server/compress.h [new file with mode: 0644]
fs/smb/server/connection.c
fs/smb/server/connection.h
fs/smb/server/smb2pdu.c
fs/smb/server/smb_common.c

index 6407ba6b9340128987a6e2db8163d1ca65c4c2e7..a3e9306055e8bd17460ac362d533d090c668a1af 100644 (file)
@@ -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 (file)
index 0000000..7c9f8a6
--- /dev/null
@@ -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 <linkinjeon@kernel.org>
+ */
+#include <linux/slab.h>
+
+#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 (file)
index 0000000..49b36d9
--- /dev/null
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * SMB2 compression support for ksmbd.
+ *
+ * Copyright (C) 2026 Namjae Jeon <linkinjeon@kernel.org>
+ */
+#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__ */
index 8347495dbc62842ffb3cb7a148b7316406bca71a..9e8fdb39e5a2a4f3eee1912deb09b0b3e9e8b574 100644 (file)
@@ -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;
 
index e074be9425823b70eeceb78f1410ba1a5bc24d62..ec75633b7da0fa2b3bb6206dd0800a3fe02a8712 100644 (file)
@@ -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;
index f9106b35e63cf2a0b5173d1e569456874ebd15e8..25742eb3f4833e8750daaa596f60b658e26c0ec1 100644 (file)
@@ -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)
index 82de4fdfe446258008af4b6aadc1d61313608400..7de73223189ae91f0c58b0ad87202b1e3fddd439 100644 (file)
@@ -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)