aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-06-18 09:21:50 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-06-18 09:21:50 -0700
commitdac3b26eae7bee261fa05f20c3fcc24988a7c233 (patch)
tree7329c2f9a86be2db9e627293078ab643e01a8bde
parente753a63f2ac8599182a5b6899c158a745188551d (diff)
parent1c8951963d8ed357f70f59e0ad4ddce2199d2016 (diff)
Merge tag 'v7.2-rc-part1-ksmbd-fixes' of git://git.samba.org/ksmbd
Pull smb server updates from Steve French: - Use after free fixes - Out of bounds read fix - Add SMB compression support both at rest and over the wire: support decompression of compressed SMB2 requests, initially allow compressed SMB2 READ responses, and implement get/set compression operations for per-file compression state. - Credentials fixes: for various FSCTLs, setinfo, delete on close and for alternate data streams - Fix access checks and permission checks in DUPLICAT_EXTENTS and SET_ZERO_DATA fsctls, find_file_posix_info, FILE_LINK_INFORMATION and smb2_set_info_sec - Reject non valid session in compound request - Serialize QUERY_DIRECTORY - Prevent path traversal bypass by restricting caseless retry - Path lookup fix - Two minor cleanup fixes * tag 'v7.2-rc-part1-ksmbd-fixes' of git://git.samba.org/ksmbd: (31 commits) ksmbd: fix path resolution in ksmbd_vfs_kern_path_create ksmbd: use opener credentials for FSCTL mutations ksmbd: use opener credentials for ADS I/O ksmbd: require source read access for duplicate extents ksmbd: run set info with opener credentials ksmbd: use opener credentials for delete-on-close ksmbd: serialize QUERY_DIRECTORY requests per file ksmbd: add permission checks for FSCTL_DUPLICATE_EXTENTS_TO_FILE ksmbd: enforce FILE_READ_ATTRIBUTES on SMB_FIND_FILE_POSIX_INFORMATION ksmbd: reject non-VALID session in compound request branch ksmbd: compress SMB2 READ responses ksmbd: negotiate and decode SMB2 compression cifs: negotiate chained SMB2 compression capabilities smb: add common SMB2 compression transform helpers smb: move LZ77 compression into common code ksmbd: add per-handle permission check to FILE_LINK_INFORMATION ksmbd: add a permission check for FSCTL_SET_ZERO_DATA ksmbd: add a WRITE_DAC/WRITE_OWNER check to SMB2 SET_INFO SECURITY ksmbd: fix use-after-free of a deferred file_lock on SMB2_CLOSE then SMB2_CANCEL smb: server: remove code guarded by nonexistent config option ...
-rw-r--r--fs/smb/client/Makefile2
-rw-r--r--fs/smb/client/cifsglob.h2
-rw-r--r--fs/smb/client/compress.c12
-rw-r--r--fs/smb/client/compress.h28
-rw-r--r--fs/smb/client/smb1pdu.h5
-rw-r--r--fs/smb/client/smb2pdu.c44
-rw-r--r--fs/smb/client/smb2pdu.h4
-rw-r--r--fs/smb/common/Makefile3
-rw-r--r--fs/smb/common/compress/compress.c399
-rw-r--r--fs/smb/common/compress/compress.h28
-rw-r--r--fs/smb/common/compress/lz77.c (renamed from fs/smb/client/compress/lz77.c)136
-rw-r--r--fs/smb/common/compress/lz77.h (renamed from fs/smb/client/compress/lz77.h)17
-rw-r--r--fs/smb/common/fscc.h18
-rw-r--r--fs/smb/common/smb2pdu.h11
-rw-r--r--fs/smb/common/smbfsctl.h10
-rw-r--r--fs/smb/server/Makefile2
-rw-r--r--fs/smb/server/compress.c209
-rw-r--r--fs/smb/server/compress.h16
-rw-r--r--fs/smb/server/connection.c12
-rw-r--r--fs/smb/server/connection.h3
-rw-r--r--fs/smb/server/ksmbd_work.c1
-rw-r--r--fs/smb/server/ksmbd_work.h4
-rw-r--r--fs/smb/server/misc.c33
-rw-r--r--fs/smb/server/misc.h1
-rw-r--r--fs/smb/server/oplock.c6
-rw-r--r--fs/smb/server/server.c10
-rw-r--r--fs/smb/server/smb2pdu.c303
-rw-r--r--fs/smb/server/smb2pdu.h3
-rw-r--r--fs/smb/server/smb_common.c8
-rw-r--r--fs/smb/server/smbacl.c4
-rw-r--r--fs/smb/server/smbfsctl.h91
-rw-r--r--fs/smb/server/vfs.c182
-rw-r--r--fs/smb/server/vfs.h3
-rw-r--r--fs/smb/server/vfs_cache.c5
-rw-r--r--fs/smb/server/vfs_cache.h2
35 files changed, 1344 insertions, 273 deletions
diff --git a/fs/smb/client/Makefile b/fs/smb/client/Makefile
index 6e83b5204699..fc6b9d35c962 100644
--- a/fs/smb/client/Makefile
+++ b/fs/smb/client/Makefile
@@ -42,7 +42,7 @@ cifs-$(CONFIG_CIFS_ALLOW_INSECURE_LEGACY) += \
smb1session.o \
smb1transport.o
-cifs-$(CONFIG_CIFS_COMPRESSION) += compress.o compress/lz77.o
+cifs-$(CONFIG_CIFS_COMPRESSION) += compress.o
ifneq ($(CONFIG_CIFS_ALLOW_INSECURE_LEGACY),)
#
diff --git a/fs/smb/client/cifsglob.h b/fs/smb/client/cifsglob.h
index a462c1590a9e..befc5eecb55c 100644
--- a/fs/smb/client/cifsglob.h
+++ b/fs/smb/client/cifsglob.h
@@ -789,6 +789,8 @@ struct TCP_Server_Info {
struct {
bool requested; /* "compress" mount option set*/
bool enabled; /* actually negotiated with server */
+ bool chained; /* chained transforms were negotiated */
+ bool pattern; /* Pattern_V1 chained payloads were negotiated */
__le16 alg; /* preferred alg negotiated with server */
} compression;
__u16 signing_algorithm;
diff --git a/fs/smb/client/compress.c b/fs/smb/client/compress.c
index be9023f841e6..8f0860970741 100644
--- a/fs/smb/client/compress.c
+++ b/fs/smb/client/compress.c
@@ -22,7 +22,7 @@
#include "cifsproto.h"
#include "smb2proto.h"
-#include "compress/lz77.h"
+#include "../common/compress/lz77.h"
#include "compress.h"
/*
@@ -44,6 +44,11 @@ struct bucket {
unsigned int count;
};
+static inline size_t pow4(size_t n)
+{
+ return n * n * n * n;
+}
+
/*
* has_low_entropy() - Compute Shannon entropy of the sampled data.
* @bkt: Bytes counts of the sample.
@@ -65,7 +70,6 @@ static bool has_low_entropy(struct bucket *bkt, size_t slen)
const size_t threshold = 65, max_entropy = 8 * ilog2(16);
size_t i, p, p2, len, sum = 0;
-#define pow4(n) (n * n * n * n)
len = ilog2(pow4(slen));
for (i = 0; i < 256 && bkt[i].count > 0; i++) {
@@ -329,14 +333,14 @@ int smb_compress(struct TCP_Server_Info *server, struct smb_rqst *rq, compress_s
goto err_free;
}
- dlen = lz77_compressed_alloc_size(slen);
+ dlen = smb_lz77_compressed_alloc_size(slen);
dst = kvzalloc(dlen, GFP_KERNEL);
if (!dst) {
ret = -ENOMEM;
goto err_free;
}
- ret = lz77_compress(src, slen, dst, &dlen);
+ ret = smb_lz77_compress(src, slen, dst, &dlen);
if (!ret) {
struct smb2_compression_hdr hdr = { 0 };
struct smb_rqst comp_rq = { .rq_nvec = 3, };
diff --git a/fs/smb/client/compress.h b/fs/smb/client/compress.h
index 2679baca129b..e08e6d339d21 100644
--- a/fs/smb/client/compress.h
+++ b/fs/smb/client/compress.h
@@ -18,6 +18,7 @@
#include <linux/uio.h>
#include <linux/kernel.h>
#include "../common/smb2pdu.h"
+#include "../common/compress/compress.h"
#include "cifsglob.h"
/* sizeof(smb2_compression_hdr) - sizeof(OriginalPayloadSize) */
@@ -34,29 +35,6 @@ int smb_compress(struct TCP_Server_Info *server, struct smb_rqst *rq,
compress_send_fn send_fn);
bool should_compress(const struct cifs_tcon *tcon, const struct smb_rqst *rq);
-/*
- * smb_compress_alg_valid() - Validate a compression algorithm.
- * @alg: Compression algorithm to check.
- * @valid_none: Conditional check whether NONE algorithm should be
- * considered valid or not.
- *
- * If @alg is SMB3_COMPRESS_NONE, this function returns @valid_none.
- *
- * Note that 'NONE' (0) compressor type is considered invalid in protocol
- * negotiation, as it's never requested to/returned from the server.
- *
- * Return: true if @alg is valid/supported, false otherwise.
- */
-static __always_inline int smb_compress_alg_valid(__le16 alg, bool valid_none)
-{
- if (alg == SMB3_COMPRESS_NONE)
- return valid_none;
-
- if (alg == SMB3_COMPRESS_LZ77 || alg == SMB3_COMPRESS_PATTERN)
- return true;
-
- return false;
-}
#else /* !CONFIG_CIFS_COMPRESSION */
static inline int smb_compress(void *unused1, void *unused2, void *unused3)
{
@@ -68,9 +46,5 @@ static inline bool should_compress(void *unused1, void *unused2)
return false;
}
-static inline int smb_compress_alg_valid(__le16 unused1, bool unused2)
-{
- return -EOPNOTSUPP;
-}
#endif /* !CONFIG_CIFS_COMPRESSION */
#endif /* _SMB_COMPRESS_H */
diff --git a/fs/smb/client/smb1pdu.h b/fs/smb/client/smb1pdu.h
index 7584e94d9b2b..0870949144ab 100644
--- a/fs/smb/client/smb1pdu.h
+++ b/fs/smb/client/smb1pdu.h
@@ -1211,11 +1211,6 @@ typedef struct smb_com_transaction_compr_ioctl_req {
__le16 compression_state; /* See below for valid flags */
} __packed TRANSACT_COMPR_IOCTL_REQ;
-/* compression state flags */
-#define COMPRESSION_FORMAT_NONE 0x0000
-#define COMPRESSION_FORMAT_DEFAULT 0x0001
-#define COMPRESSION_FORMAT_LZNT1 0x0002
-
typedef struct smb_com_transaction_ioctl_rsp {
struct smb_hdr hdr; /* wct = 19 */
__u8 Reserved[3];
diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
index 4972cfe249f6..3c7691b39377 100644
--- a/fs/smb/client/smb2pdu.c
+++ b/fs/smb/client/smb2pdu.c
@@ -636,10 +636,16 @@ build_compression_ctxt(struct smb2_compression_capabilities_context *pneg_ctxt)
pneg_ctxt->DataLength =
cpu_to_le16(sizeof(struct smb2_compression_capabilities_context)
- sizeof(struct smb2_neg_context));
- pneg_ctxt->CompressionAlgorithmCount = cpu_to_le16(3);
+ /*
+ * Pattern_V1 is useful only as part of a chained transform. LZ77 remains
+ * the preferred general-purpose algorithm selected by this client.
+ */
+ pneg_ctxt->CompressionAlgorithmCount = cpu_to_le16(4);
+ pneg_ctxt->Flags = SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED;
pneg_ctxt->CompressionAlgorithms[0] = SMB3_COMPRESS_LZ77;
pneg_ctxt->CompressionAlgorithms[1] = SMB3_COMPRESS_LZ77_HUFF;
pneg_ctxt->CompressionAlgorithms[2] = SMB3_COMPRESS_LZNT1;
+ pneg_ctxt->CompressionAlgorithms[3] = SMB3_COMPRESS_PATTERN;
}
static unsigned int
@@ -827,9 +833,12 @@ static void decode_compress_ctx(struct TCP_Server_Info *server,
struct smb2_compression_capabilities_context *ctxt)
{
unsigned int len = le16_to_cpu(ctxt->DataLength);
- __le16 alg;
+ unsigned int count, i;
server->compression.enabled = false;
+ server->compression.chained = false;
+ server->compression.pattern = false;
+ server->compression.alg = SMB3_COMPRESS_NONE;
/*
* Caller checked that DataLength remains within SMB boundary. We still
@@ -841,20 +850,37 @@ static void decode_compress_ctx(struct TCP_Server_Info *server,
return;
}
- if (le16_to_cpu(ctxt->CompressionAlgorithmCount) != 1) {
+ count = le16_to_cpu(ctxt->CompressionAlgorithmCount);
+ if (!count || count > ARRAY_SIZE(ctxt->CompressionAlgorithms) ||
+ len < 8 + count * sizeof(__le16)) {
pr_warn_once("invalid SMB3 compress algorithm count\n");
return;
}
- alg = ctxt->CompressionAlgorithms[0];
-
- /* 'NONE' (0) compressor type is never negotiated */
- if (alg == 0 || le16_to_cpu(alg) > 3) {
- pr_warn_once("invalid compression algorithm '%u'\n", alg);
+ if (ctxt->Flags != SMB2_COMPRESSION_CAPABILITIES_FLAG_NONE &&
+ ctxt->Flags != SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED) {
+ pr_warn_once("invalid SMB3 compression flags\n");
return;
}
- server->compression.alg = alg;
+ for (i = 0; i < count; i++) {
+ /* Record the intersection supported by the shared SMB codec. */
+ if (ctxt->CompressionAlgorithms[i] == SMB3_COMPRESS_LZ77)
+ server->compression.alg = SMB3_COMPRESS_LZ77;
+ else if (ctxt->CompressionAlgorithms[i] == SMB3_COMPRESS_PATTERN)
+ server->compression.pattern = true;
+ }
+ if (server->compression.alg != SMB3_COMPRESS_LZ77)
+ return;
+
+ /*
+ * Pattern_V1 cannot appear in an unchained transform even if a broken
+ * peer lists it in the algorithm array.
+ */
+ server->compression.chained =
+ ctxt->Flags == SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED;
+ if (!server->compression.chained)
+ server->compression.pattern = false;
server->compression.enabled = true;
}
diff --git a/fs/smb/client/smb2pdu.h b/fs/smb/client/smb2pdu.h
index 30d70097fe2f..b9bf2fa989d5 100644
--- a/fs/smb/client/smb2pdu.h
+++ b/fs/smb/client/smb2pdu.h
@@ -195,10 +195,6 @@ struct network_resiliency_req {
#define NO_FILE_ID 0xFFFFFFFFFFFFFFFFULL /* general ioctls to srv not to file */
-struct compress_ioctl {
- __le16 CompressionState; /* See cifspdu.h for possible flag values */
-} __packed;
-
/*
* Maximum number of iovs we need for an ioctl request.
* [0] : struct smb2_ioctl_req
diff --git a/fs/smb/common/Makefile b/fs/smb/common/Makefile
index 9e0730a385fb..f2c6e09d4e77 100644
--- a/fs/smb/common/Makefile
+++ b/fs/smb/common/Makefile
@@ -4,3 +4,6 @@
#
obj-$(CONFIG_SMBFS) += cifs_md4.o
+obj-$(CONFIG_SMBFS) += smb_compress.o
+
+smb_compress-y := compress/compress.o compress/lz77.o
diff --git a/fs/smb/common/compress/compress.c b/fs/smb/common/compress/compress.c
new file mode 100644
index 000000000000..b07a317597a4
--- /dev/null
+++ b/fs/smb/common/compress/compress.c
@@ -0,0 +1,399 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * SMB2 compression transform helpers.
+ *
+ * Copyright (C) 2026 Namjae Jeon <linkinjeon@kernel.org>
+ */
+#include <linux/module.h>
+#include <linux/overflow.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+
+#include "compress.h"
+#include "lz77.h"
+
+#define SMB2_COMPRESSION_CHAINED_HDR_LEN \
+ offsetof(struct smb2_compression_hdr, CompressionAlgorithm)
+#define SMB2_COMPRESSION_PAYLOAD_BASE_LEN \
+ (sizeof(struct smb2_compression_payload_hdr) - sizeof(__le32))
+
+/*
+ * A NONE payload carries bytes verbatim. Keep both cursors and remaining
+ * lengths together so every chained payload handler applies identical bounds
+ * accounting.
+ */
+static int smb_decompress_none(const u8 **src, u32 *slen, u8 **dst, u32 *dlen,
+ u32 len)
+{
+ if (len > *slen || len > *dlen)
+ return -EINVAL;
+
+ memcpy(*dst, *src, len);
+ *src += len;
+ *slen -= len;
+ *dst += len;
+ *dlen -= len;
+ return 0;
+}
+
+/*
+ * Pattern_V1 represents a run of one byte. Its wire payload is always the
+ * fixed-size smb2_compression_pattern_v1 structure.
+ */
+static int smb_decompress_pattern(const u8 **src, u32 *slen, u8 **dst,
+ u32 *dlen, u32 len)
+{
+ const struct smb2_compression_pattern_v1 *pattern;
+ u32 repetitions;
+
+ if (len != sizeof(*pattern) || len > *slen)
+ return -EINVAL;
+
+ pattern = (const struct smb2_compression_pattern_v1 *)*src;
+ repetitions = le32_to_cpu(pattern->Repetitions);
+ if (repetitions > *dlen)
+ return -EINVAL;
+
+ memset(*dst, pattern->Pattern, repetitions);
+ *src += len;
+ *slen -= len;
+ *dst += repetitions;
+ *dlen -= repetitions;
+ return 0;
+}
+
+/*
+ * LZ77 payload Length includes the four-byte OriginalPayloadSize field.
+ * Consume that field before passing the compressed stream to the raw codec.
+ */
+static int smb_decompress_lz77_payload(const u8 **src, u32 *slen, u8 **dst,
+ u32 *dlen, u32 len)
+{
+ u32 orig_size;
+ int rc;
+
+ if (len < sizeof(__le32) || len > *slen)
+ return -EINVAL;
+
+ orig_size = get_unaligned_le32(*src);
+ if (orig_size > *dlen)
+ return -EINVAL;
+
+ *src += sizeof(__le32);
+ *slen -= sizeof(__le32);
+ len -= sizeof(__le32);
+
+ rc = smb_lz77_decompress(*src, len, *dst, orig_size);
+ if (rc)
+ return rc;
+
+ *src += len;
+ *slen -= len;
+ *dst += orig_size;
+ *dlen -= orig_size;
+ return 0;
+}
+
+static int smb_decompress_chained(__le16 alg, bool allow_chained,
+ const struct smb2_compression_hdr *hdr,
+ u32 slen, void *dst, u32 dlen)
+{
+ const struct smb2_compression_payload_hdr *payload;
+ const u8 *src = (const u8 *)hdr + SMB2_COMPRESSION_CHAINED_HDR_LEN;
+ u32 orig_size = le32_to_cpu(hdr->OriginalCompressedSegmentSize);
+ u32 remaining = slen - SMB2_COMPRESSION_CHAINED_HDR_LEN;
+ u8 *out = dst;
+ u32 out_remaining = dlen;
+ bool first = true;
+ int rc;
+
+ if (!allow_chained || orig_size != dlen)
+ return -EINVAL;
+
+ /*
+ * The chained transform has an eight-byte top-level header. The next
+ * bytes are a sequence of payload headers whose Length fields account
+ * for payload data, including OriginalPayloadSize where applicable.
+ */
+ while (remaining) {
+ __le16 payload_alg;
+ __le16 flags;
+ u32 len;
+
+ if (remaining < SMB2_COMPRESSION_PAYLOAD_BASE_LEN)
+ return -EINVAL;
+
+ payload = (const struct smb2_compression_payload_hdr *)src;
+ payload_alg = payload->CompressionAlgorithm;
+ flags = payload->Flags;
+ len = le32_to_cpu(payload->Length);
+
+ /*
+ * CHAINED marks only the first payload. Requiring NONE on every
+ * later payload rejects ambiguous or independently chained data.
+ */
+ if ((first && flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED)) ||
+ (!first && flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE)))
+ return -EINVAL;
+
+ src += SMB2_COMPRESSION_PAYLOAD_BASE_LEN;
+ remaining -= SMB2_COMPRESSION_PAYLOAD_BASE_LEN;
+
+ if (payload_alg == SMB3_COMPRESS_NONE) {
+ rc = smb_decompress_none(&src, &remaining, &out,
+ &out_remaining, len);
+ } else if (payload_alg == SMB3_COMPRESS_PATTERN) {
+ rc = smb_decompress_pattern(&src, &remaining, &out,
+ &out_remaining, len);
+ } else if (payload_alg == alg && alg == SMB3_COMPRESS_LZ77) {
+ rc = smb_decompress_lz77_payload(&src, &remaining, &out,
+ &out_remaining, len);
+ } else {
+ return -EINVAL;
+ }
+ if (rc)
+ return rc;
+ first = false;
+ }
+
+ return out_remaining ? -EINVAL : 0;
+}
+
+static int smb_decompress_unchained(__le16 alg,
+ const struct smb2_compression_hdr *hdr,
+ u32 slen, void *dst, u32 dlen)
+{
+ u32 orig_size, offset, comp_size;
+
+ if (hdr->CompressionAlgorithm != alg ||
+ !smb_compress_alg_valid(hdr->CompressionAlgorithm, false))
+ return -EINVAL;
+
+ orig_size = le32_to_cpu(hdr->OriginalCompressedSegmentSize);
+ offset = le32_to_cpu(hdr->Offset);
+ if (offset > slen - sizeof(*hdr) || offset > dlen ||
+ orig_size > dlen - offset || orig_size + offset != dlen)
+ return -EINVAL;
+
+ memcpy(dst, (const u8 *)hdr + sizeof(*hdr), offset);
+ comp_size = slen - sizeof(*hdr) - offset;
+ return smb_lz77_decompress((const u8 *)hdr + sizeof(*hdr) + offset,
+ comp_size, (u8 *)dst + offset, orig_size);
+}
+
+/**
+ * smb_compression_decompress() - decode an SMB2 compression transform
+ * @alg: negotiated general-purpose compression algorithm
+ * @allow_chained: whether chained transforms were negotiated
+ * @src: transform header followed by compressed payload data
+ * @slen: total number of bytes available at @src
+ * @dst: output buffer for the reconstructed SMB2 message
+ * @dlen: exact expected size of the reconstructed SMB2 message
+ *
+ * Validate the transform type and negotiated capabilities before dispatching
+ * to the chained or unchained decoder. The caller supplies the expected output
+ * size after applying its transport-specific message size limits.
+ *
+ * Return: 0 on success, otherwise a negative errno.
+ */
+int smb_compression_decompress(__le16 alg, bool allow_chained,
+ const void *src, u32 slen, void *dst, u32 dlen)
+{
+ const struct smb2_compression_hdr *hdr = src;
+
+ if (!src || !dst || slen < sizeof(*hdr) ||
+ hdr->ProtocolId != SMB2_COMPRESSION_TRANSFORM_ID ||
+ alg == SMB3_COMPRESS_NONE)
+ return -EINVAL;
+
+ if (hdr->Flags == cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED))
+ return smb_decompress_chained(alg, allow_chained, hdr, slen,
+ dst, dlen);
+
+ if (hdr->Flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE))
+ return -EINVAL;
+
+ return smb_decompress_unchained(alg, hdr, slen, dst, dlen);
+}
+EXPORT_SYMBOL_GPL(smb_compression_decompress);
+
+struct smb_compression_builder {
+ u8 *pos;
+ u32 remaining;
+ bool first;
+};
+
+/*
+ * Reserve one chained payload header and initialize its common fields.
+ * OriginalPayloadSize is present only for LZNT1/LZ77/LZ77+Huffman payloads.
+ */
+static struct smb2_compression_payload_hdr *
+smb_compression_add_payload(struct smb_compression_builder *builder,
+ __le16 alg, u32 payload_len, bool orig_size)
+{
+ struct smb2_compression_payload_hdr *payload;
+ u32 hdr_len = SMB2_COMPRESSION_PAYLOAD_BASE_LEN;
+ u32 total_len;
+
+ if (orig_size)
+ hdr_len += sizeof(payload->OriginalPayloadSize);
+ if (check_add_overflow(hdr_len, payload_len, &total_len) ||
+ total_len > builder->remaining)
+ return NULL;
+
+ payload = (struct smb2_compression_payload_hdr *)builder->pos;
+ payload->CompressionAlgorithm = alg;
+ payload->Flags = cpu_to_le16(builder->first ?
+ SMB2_COMPRESSION_FLAG_CHAINED : SMB2_COMPRESSION_FLAG_NONE);
+ payload->Length = cpu_to_le32(payload_len +
+ (orig_size ? sizeof(payload->OriginalPayloadSize) : 0));
+
+ builder->pos += hdr_len;
+ builder->remaining -= hdr_len;
+ builder->first = false;
+ return payload;
+}
+
+static int smb_compression_add_pattern(struct smb_compression_builder *builder,
+ u8 pattern, u32 repetitions)
+{
+ struct smb2_compression_pattern_v1 *payload;
+
+ if (!smb_compression_add_payload(builder, SMB3_COMPRESS_PATTERN,
+ sizeof(*payload), false))
+ return -ENOSPC;
+
+ payload = (struct smb2_compression_pattern_v1 *)builder->pos;
+ payload->Pattern = pattern;
+ payload->Reserved1 = 0;
+ payload->Reserved2 = 0;
+ payload->Repetitions = cpu_to_le32(repetitions);
+ builder->pos += sizeof(*payload);
+ builder->remaining -= sizeof(*payload);
+ return 0;
+}
+
+static int smb_compression_add_none(struct smb_compression_builder *builder,
+ const u8 *src, u32 len)
+{
+ if (!smb_compression_add_payload(builder, SMB3_COMPRESS_NONE, len, false))
+ return -ENOSPC;
+
+ memcpy(builder->pos, src, len);
+ builder->pos += len;
+ builder->remaining -= len;
+ return 0;
+}
+
+static int smb_compression_add_lz77(struct smb_compression_builder *builder,
+ const u8 *src, u32 len)
+{
+ struct smb2_compression_payload_hdr *payload;
+ u32 comp_len;
+ int rc;
+
+ if (builder->remaining <= sizeof(*payload))
+ return -ENOSPC;
+
+ comp_len = builder->remaining - sizeof(*payload);
+ payload = smb_compression_add_payload(builder, SMB3_COMPRESS_LZ77,
+ comp_len, true);
+ if (!payload)
+ return -ENOSPC;
+
+ rc = smb_lz77_compress(src, len, builder->pos, &comp_len);
+ if (rc)
+ return rc;
+
+ payload->Length = cpu_to_le32(comp_len +
+ sizeof(payload->OriginalPayloadSize));
+ payload->OriginalPayloadSize = cpu_to_le32(len);
+ builder->pos += comp_len;
+ builder->remaining -= comp_len;
+ return 0;
+}
+
+/**
+ * smb_compression_compress_chained() - build a chained SMB2 transform
+ * @alg: negotiated general-purpose compression algorithm
+ * @allow_pattern: whether Pattern_V1 was negotiated
+ * @src: complete uncompressed SMB2 message
+ * @slen: size of @src
+ * @dst: output buffer for the transform
+ * @dlen: input capacity of @dst and output transform size
+ *
+ * Following the algorithm in [MS-SMB2] 3.1.4.4, encode sufficiently long
+ * repeated runs at the front and back as Pattern_V1 payloads. Compress a
+ * middle region larger than 1 KiB with LZ77; smaller middle regions are
+ * represented by a chained NONE payload.
+ *
+ * This helper does not decide whether the final transform is smaller than the
+ * original message. The transport caller owns that policy decision.
+ *
+ * Return: 0 on success, otherwise a negative errno.
+ */
+int smb_compression_compress_chained(__le16 alg, bool allow_pattern,
+ const void *src, u32 slen,
+ void *dst, u32 *dlen)
+{
+ struct smb2_compression_hdr *hdr = dst;
+ struct smb_compression_builder builder;
+ const u8 *input = src;
+ u32 forward = 0, backward = 0, middle_len;
+ int rc;
+
+ if (!src || !dst || !dlen || alg != SMB3_COMPRESS_LZ77 ||
+ *dlen <= SMB2_COMPRESSION_CHAINED_HDR_LEN || !slen)
+ return -EINVAL;
+
+ hdr->ProtocolId = SMB2_COMPRESSION_TRANSFORM_ID;
+ hdr->OriginalCompressedSegmentSize = cpu_to_le32(slen);
+ builder.pos = (u8 *)dst + SMB2_COMPRESSION_CHAINED_HDR_LEN;
+ builder.remaining = *dlen - SMB2_COMPRESSION_CHAINED_HDR_LEN;
+ builder.first = true;
+
+ if (allow_pattern && slen > 32) {
+ for (forward = 1; forward < slen; forward++) {
+ if (input[forward] != input[0])
+ break;
+ }
+ if (forward <= 32)
+ forward = 0;
+
+ for (backward = 1; backward < slen - forward; backward++) {
+ if (input[slen - backward - 1] != input[slen - 1])
+ break;
+ }
+ if (backward <= 32)
+ backward = 0;
+ }
+
+ if (forward) {
+ rc = smb_compression_add_pattern(&builder, input[0], forward);
+ if (rc)
+ return rc;
+ }
+
+ middle_len = slen - forward - backward;
+ if (middle_len > 1024)
+ rc = smb_compression_add_lz77(&builder, input + forward,
+ middle_len);
+ else if (middle_len)
+ rc = smb_compression_add_none(&builder,
+ input + forward, middle_len);
+ else
+ rc = 0;
+ if (rc)
+ return rc;
+
+ if (backward) {
+ rc = smb_compression_add_pattern(&builder, input[slen - 1],
+ backward);
+ if (rc)
+ return rc;
+ }
+
+ *dlen = builder.pos - (u8 *)dst;
+ return 0;
+}
+EXPORT_SYMBOL_GPL(smb_compression_compress_chained);
diff --git a/fs/smb/common/compress/compress.h b/fs/smb/common/compress/compress.h
new file mode 100644
index 000000000000..7ace3bf4b664
--- /dev/null
+++ b/fs/smb/common/compress/compress.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2026 Namjae Jeon <linkinjeon@kernel.org>
+ */
+#ifndef _COMMON_SMB_COMPRESS_H
+#define _COMMON_SMB_COMPRESS_H
+
+#include "../smb2pdu.h"
+
+/*
+ * SMB3_COMPRESS_NONE is valid only in chained payload headers. It is never
+ * negotiated as a compression algorithm.
+ */
+static __always_inline bool smb_compress_alg_valid(__le16 alg, bool valid_none)
+{
+ if (alg == SMB3_COMPRESS_NONE)
+ return valid_none;
+
+ return alg == SMB3_COMPRESS_LZ77 || alg == SMB3_COMPRESS_PATTERN;
+}
+
+int smb_compression_decompress(__le16 alg, bool allow_chained,
+ const void *src, u32 slen, void *dst, u32 dlen);
+int smb_compression_compress_chained(__le16 alg, bool allow_pattern,
+ const void *src, u32 slen,
+ void *dst, u32 *dlen);
+
+#endif /* _COMMON_SMB_COMPRESS_H */
diff --git a/fs/smb/client/compress/lz77.c b/fs/smb/common/compress/lz77.c
index 7365d0f97396..9216d973d876 100644
--- a/fs/smb/client/compress/lz77.c
+++ b/fs/smb/common/compress/lz77.c
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 2024-2026, SUSE LLC
+ * Copyright (C) 2026 Namjae Jeon <linkinjeon@kernel.org>
*
* Authors: Enzo Matsumiya <ematsumiya@suse.de>
+ * Namjae Jeon <linkinjeon@kernel.org>
*
* Implementation of the LZ77 "plain" compression algorithm, as per MS-XCA spec.
*/
@@ -10,6 +12,8 @@
#include <linux/sizes.h>
#include <linux/count_zeros.h>
#include <linux/unaligned.h>
+#include <linux/module.h>
+#include <linux/overflow.h>
#include "lz77.h"
@@ -32,7 +36,7 @@
*/
#define LZ77_MATCH_MAX_DIST SZ_8K
#define LZ77_HASH_LOG 15
-#define LZ77_HASH_SIZE (1 << LZ77_HASH_LOG)
+#define LZ77_HASH_SIZE BIT(LZ77_HASH_LOG)
#define LZ77_RSTEP_SIZE sizeof(u32)
#define LZ77_MSTEP_SIZE sizeof(u64)
#define LZ77_SKIP_TRIGGER 4
@@ -215,7 +219,8 @@ static __always_inline u32 lz77_hash(const u32 v)
return ((v ^ 0x9E3779B9) * 0x85EBCA6B) >> (32 - LZ77_HASH_LOG);
}
-noinline int lz77_compress(const void *src, const u32 slen, void *dst, u32 *dlen)
+noinline int smb_lz77_compress(const void *src, const u32 slen,
+ void *dst, u32 *dlen)
{
const void *srcp, *rlim, *end, *anchor;
u32 *htable, hash, flag_count = 0;
@@ -223,10 +228,11 @@ noinline int lz77_compress(const void *src, const u32 slen, void *dst, u32 *dlen
long flag = 0;
/* This is probably a bug, so throw a warning. */
- if (WARN_ON_ONCE(*dlen < lz77_compressed_alloc_size(slen)))
+ if (WARN_ON_ONCE(*dlen < smb_lz77_compressed_alloc_size(slen)))
return -EINVAL;
- srcp = anchor = src;
+ srcp = src;
+ anchor = src;
end = srcp + slen; /* absolute end */
rlim = end - LZ77_MST