[PATCH v2 11/14] smb: common: compress: implement LZ77-Huffman

Enzo Matsumiya <[email protected]> Mon, 20 Jul 2026 16:49:22 -0300
Newsgroups org.kernel.vger.linux-cifs
Message-ID <[email protected]>
Implement LZ77-Huffman compression algorithm as per MS-XCA.

Huffman encoding adds a few extra passes on top of LZ77-style encoding
in order to provide better compression ratio (at bit level rather than
byte level).

Refer to MS-XCA spec or code/comments in huffman.* for details.

Changes:
- add huffman.{h,c} with implementation
- adjust server/ and client/ code to support new alg
- rename smb_compression_add_lz77() to smb_compression_add_lz() and
  smb_decompress_lz77_payload() to smb_decompress_lz_payload(), and
  add an @alg arg to both, so they handle LZ77 and LZ77-Huffman
  (which are handled the same way in that regard)
- add @lzalg arg to smb_compress_alloc_size() to account for
  LZ77-Huffman allocation requirements
- add LZ77-Huffman to smb_compress_alg_valid()

Signed-off-by: Enzo Matsumiya <[email protected]>
---
 fs/smb/client/compress.c          |  21 +-
 fs/smb/client/smb2pdu.c           |  20 +-
 fs/smb/common/Makefile            |   2 +-
 fs/smb/common/compress/compress.c |  46 +-
 fs/smb/common/compress/compress.h |  22 +-
 fs/smb/common/compress/huffman.c  | 983 ++++++++++++++++++++++++++++++
 fs/smb/common/compress/huffman.h  |  31 +
 fs/smb/server/compress.c          |   9 +-
 fs/smb/server/smb2pdu.c           |  27 +-
 9 files changed, 1117 insertions(+), 44 deletions(-)
 create mode 100644 fs/smb/common/compress/huffman.c
 create mode 100644 fs/smb/common/compress/huffman.h

diff --git a/fs/smb/client/compress.c b/fs/smb/client/compress.c
index 363ad58ad798..bc8f67dc2e95 100644
--- a/fs/smb/client/compress.c
+++ b/fs/smb/client/compress.c
@@ -225,14 +225,16 @@ static int check_compressible_chunks(const u8 *buf, const u32 len, u32 *freqs)
 /*
  * Check @buf heuristics (entropy/distribution) to determine its compressibility level.
  *
+ * If @maybe_ok is false, alias MAYBE_COMPRESSIBLE to UNCOMPRESSIBLE.
+ *
  * Tests shows that this function is quite reliable in predicting data compressibility, matching
- * very close with the behaviour of LZ77 compression success and failures.
+ * very close with the behaviour of LZ* compression success and failures.
  *
  * This function allocates memory, callers must check for -ENOMEM.
  *
  * Return: one of the *COMPRESSIBLE values on success, -errno otherwise.
  */
-static __must_check int check_compressible(const u8 *buf, u32 len)
+static __must_check int check_compressible(const u8 *buf, u32 len, bool maybe_ok)
 {
 	u32 entropy, *freqs, rle = 0, rle_boost = 0;
 	const u32 min_reps = (len / 100); /* ~1% of @len */
@@ -310,6 +312,9 @@ static __must_check int check_compressible(const u8 *buf, u32 len)
 
 	kfree(freqs);
 
+	if (!maybe_ok && ret == MAYBE_COMPRESSIBLE)
+		ret = UNCOMPRESSIBLE;
+
 	return ret;
 }
 
@@ -355,6 +360,7 @@ int smb_compress(struct TCP_Server_Info *server, struct smb_rqst *rq, compress_s
 	bool chained, use_pattern;
 	void *src, *dst = NULL;
 	u32 slen, dlen;
+	__le16 lzalg;
 	int ret;
 
 	if (!server || !rq || !rq->rq_iov || !rq->rq_iov->iov_base)
@@ -363,6 +369,10 @@ int smb_compress(struct TCP_Server_Info *server, struct smb_rqst *rq, compress_s
 	if (rq->rq_iov->iov_len != sizeof(struct smb2_write_req))
 		return -EINVAL;
 
+	lzalg = server->compression.alg;
+	if (unlikely(!smb_compress_alg_valid(lzalg, false)))
+		return -EINVAL;
+
 	slen = iov_iter_count(&rq->rq_iter);
 	src = kvzalloc(slen, GFP_KERNEL);
 	if (!src) {
@@ -386,7 +396,7 @@ int smb_compress(struct TCP_Server_Info *server, struct smb_rqst *rq, compress_s
 	 * uncompressible low-hanging fruits here and let smb_lz77_compress() handle the
 	 * exceptions/rare cases.
 	 */
-	ret = check_compressible(src, slen);
+	ret = check_compressible(src, slen, lzalg == SMB3_COMPRESS_LZ77_HUFF);
 
 	/* XXX: do something with MAYBE_COMPRESSIBLE */
 	if (ret != COMPRESSIBLE) {
@@ -397,15 +407,14 @@ int smb_compress(struct TCP_Server_Info *server, struct smb_rqst *rq, compress_s
 
 	chained = server->compression.chained;
 	use_pattern = server->compression.pattern;
-	dlen = smb_compress_alloc_size(slen, chained, use_pattern);
+	dlen = smb_compress_alloc_size(slen, chained, use_pattern, lzalg);
 	dst = kvzalloc(dlen, GFP_KERNEL);
 	if (!dst) {
 		ret = -ENOMEM;
 		goto err_free;
 	}
 
-	ret = smb_compression_compress(SMB3_COMPRESS_LZ77, chained, use_pattern,
-				       src, slen, dst, &dlen);
+	ret = smb_compression_compress(lzalg, chained, use_pattern, src, slen, dst, &dlen);
 	if (!ret && dlen < slen) {
 		struct smb2_compression_hdr *hdrp = dst, hdr = {};
 		struct smb_rqst comp_rq = { .rq_nvec = 3, };
diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
index cc20d9b6638f..e58ac7be30a3 100644
--- a/fs/smb/client/smb2pdu.c
+++ b/fs/smb/client/smb2pdu.c
@@ -44,6 +44,7 @@
 #include "cached_dir.h"
 #include "compress.h"
 #include "fs_context.h"
+#include "../common/compress/compress.h"
 
 /*
  *  The following table defines the expected "StructureSize" of SMB2 requests
@@ -877,14 +878,23 @@ static void decode_compress_ctx(struct TCP_Server_Info *server,
 	pattern = false;
 
 	for (i = 0; i < count; i++) {
-		/* Record the intersection supported by the shared SMB codec. */
-		if (ctxt->CompressionAlgorithms[i] == SMB3_COMPRESS_LZ77)
-			alg = SMB3_COMPRESS_LZ77;
-		else if (ctxt->CompressionAlgorithms[i] == SMB3_COMPRESS_PATTERN)
+		__le16 rsp_alg = ctxt->CompressionAlgorithms[i];
+
+		/*
+		 * Servers only return 1 LZ* algorithm, or + Pattern_V1 if chained.
+		 * server->compression.alg only tracks LZ* algs.
+		 */
+		if (rsp_alg == SMB3_COMPRESS_PATTERN)
 			pattern = true;
+		else if (smb_compress_alg_valid(rsp_alg, false))
+			alg = rsp_alg;
+		else
+			pr_warn_once("invalid compression algorithm '0x%04x'\n",
+				     le16_to_cpu(rsp_alg));
 	}
 
-	if (unlikely(alg != SMB3_COMPRESS_LZ77)) {
+	/* We explicitly checked for SMB3_COMPRESS_PATTERN above, so this works fine */
+	if (unlikely(!smb_compress_alg_valid(alg, false))) {
 		cifs_dbg(VFS, "invalid LZ algorithm negotiated 0x%x\n", alg);
 		goto out;
 	}
diff --git a/fs/smb/common/Makefile b/fs/smb/common/Makefile
index f2c6e09d4e77..35368807da76 100644
--- a/fs/smb/common/Makefile
+++ b/fs/smb/common/Makefile
@@ -6,4 +6,4 @@
 obj-$(CONFIG_SMBFS) += cifs_md4.o
 obj-$(CONFIG_SMBFS) += smb_compress.o
 
-smb_compress-y := compress/compress.o compress/lz77.o
+smb_compress-y := compress/compress.o compress/lz77.o compress/huffman.o
diff --git a/fs/smb/common/compress/compress.c b/fs/smb/common/compress/compress.c
index 64c33af95877..3b2690acb93a 100644
--- a/fs/smb/common/compress/compress.c
+++ b/fs/smb/common/compress/compress.c
@@ -56,11 +56,11 @@ static int smb_decompress_pattern(const u8 **src, u32 *slen, u8 **dst,
 }
 
 /*
- * LZ77 payload Length includes the four-byte OriginalPayloadSize field.
+ * LZ* 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)
+static int smb_decompress_lz_payload(const u8 **src, u32 *slen, u8 **dst,
+				     u32 *dlen, u32 len, __le16 alg)
 {
 	u32 orig_size;
 	int rc;
@@ -76,7 +76,11 @@ static int smb_decompress_lz77_payload(const u8 **src, u32 *slen, u8 **dst,
 	*slen -= sizeof(__le32);
 	len -= sizeof(__le32);
 
-	rc = smb_lz77_decompress(*src, len, *dst, orig_size);
+	if (alg == SMB3_COMPRESS_LZ77)
+		rc = smb_lz77_decompress(*src, len, *dst, orig_size);
+	else /* SMB3_COMPRESS_LZ77_HUFF, assumes others were checked by caller */
+		rc = smb_huff_decompress(*src, len, *dst, orig_size);
+
 	if (rc)
 		return rc;
 
@@ -135,8 +139,9 @@ static int smb_decompress_chained(__le16 alg, const struct smb2_compression_hdr
 			rc = smb_decompress_none(&src, &remaining, &out, &out_remaining, len);
 			break;
 		case SMB3_COMPRESS_LZ77:
-			rc = smb_decompress_lz77_payload(&src, &remaining, &out,
-							 &out_remaining, len);
+		case SMB3_COMPRESS_LZ77_HUFF:
+			rc = smb_decompress_lz_payload(&src, &remaining, &out,
+						       &out_remaining, len, payload_alg);
 			break;
 		case SMB3_COMPRESS_PATTERN:
 			rc = smb_decompress_pattern(&src, &remaining, &out, &out_remaining, len);
@@ -173,7 +178,15 @@ static int smb_decompress_unchained(__le16 alg,
 
 	memcpy(dst, (const u8 *)hdr + sizeof(*hdr), offset);
 	comp_size = slen - sizeof(*hdr) - offset;
-	return smb_lz77_decompress((const u8 *)hdr + sizeof(*hdr) + offset,
+
+	if (alg == SMB3_COMPRESS_LZ77)
+		return smb_lz77_decompress((const u8 *)hdr + sizeof(*hdr) + offset,
+					   comp_size, (u8 *)dst + offset, orig_size);
+
+	if (WARN_ON_ONCE(alg != SMB3_COMPRESS_LZ77_HUFF))
+		return -EINVAL;
+
+	return smb_huff_decompress((const u8 *)hdr + sizeof(*hdr) + offset,
 				   comp_size, (u8 *)dst + offset, orig_size);
 }
 
@@ -280,8 +293,8 @@ static int smb_compression_add_none(struct smb_compression_builder *builder,
 	return 0;
 }
 
-static int smb_compression_add_lz77(struct smb_compression_builder *builder,
-				    const u8 *src, u32 len, bool chained)
+static int smb_compression_add_lz(struct smb_compression_builder *builder, const u8 *src, u32 len,
+				  __le16 alg, bool chained)
 {
 	struct smb2_compression_payload_hdr *payload;
 	u32 comp_len, offset = chained ? sizeof(payload->OriginalPayloadSize) : 0;
@@ -296,14 +309,19 @@ static int smb_compression_add_lz77(struct smb_compression_builder *builder,
 	 * accounted for.
 	 * Also, use @payload_len == 0 as we don't know compressed size yet.
 	 */
-	payload = smb_compression_add_payload(builder, SMB3_COMPRESS_LZ77, 0);
+	payload = smb_compression_add_payload(builder, alg, 0);
 	if (!payload)
 		return -ENOSPC;
 
 	builder->pos += offset;
 	builder->remaining -= offset;
 	comp_len = builder->remaining;
-	rc = smb_lz77_compress(src, len, builder->pos, &comp_len);
+
+	rc = -EIO;
+	if (alg == SMB3_COMPRESS_LZ77)
+		rc = smb_lz77_compress(src, len, builder->pos, &comp_len);
+	else /* SMB3_COMPRESS_LZ77_HUFF, assumes others were checked by caller */
+		rc = smb_huff_compress(src, len, builder->pos, &comp_len);
 	if (rc)
 		return rc;
 
@@ -348,8 +366,8 @@ int smb_compression_compress(__le16 alg, bool chained, bool allow_pattern,
 	u32 forward = 0, backward = 0, middle_len;
 	int rc;
 
-	if (!src || !dst || !dlen || alg != SMB3_COMPRESS_LZ77 ||
-	    *dlen <= SMB2_COMPRESSION_CHAINED_HDR_LEN || !slen)
+	if (!src || !dst || !dlen || *dlen <= SMB2_COMPRESSION_CHAINED_HDR_LEN || !slen ||
+	    !smb_compress_alg_valid(alg, false))
 		return -EINVAL;
 
 	/* Note that the below is a bug, but (chained && !allow_pattern) is a valid combination */
@@ -391,7 +409,7 @@ int smb_compression_compress(__le16 alg, bool chained, bool allow_pattern,
 	rc = -ENODATA;
 	middle_len = slen - forward - backward;
 	if (middle_len > 1024 || !chained)
-		rc = smb_compression_add_lz77(&builder, input + forward, middle_len, chained);
+		rc = smb_compression_add_lz(&builder, input + forward, middle_len, alg, chained);
 	else if (middle_len && allow_pattern)
 		rc = smb_compression_add_none(&builder, input + forward, middle_len);
 
diff --git a/fs/smb/common/compress/compress.h b/fs/smb/common/compress/compress.h
index 601ccb789fa4..43dba4bb1176 100644
--- a/fs/smb/common/compress/compress.h
+++ b/fs/smb/common/compress/compress.h
@@ -15,6 +15,7 @@
 #include <linux/slab.h>
 
 #include "../smb2pdu.h"
+#include "huffman.h"
 #include "lz77.h"
 
 #define SMB2_COMPRESSION_CHAINED_HDR_LEN \
@@ -139,10 +140,16 @@ static __always_inline u32 smb_compress_hash_ptr(const void *ptr)
  */
 static __always_inline bool smb_compress_alg_valid(__le16 alg, bool valid_none)
 {
-	if (alg == SMB3_COMPRESS_NONE)
+	switch (alg) {
+	case SMB3_COMPRESS_NONE:
 		return valid_none;
+	case SMB3_COMPRESS_LZ77:
+	case SMB3_COMPRESS_LZ77_HUFF:
+	case SMB3_COMPRESS_PATTERN:
+		return true;
+	}
 
-	return alg == SMB3_COMPRESS_LZ77 || alg == SMB3_COMPRESS_PATTERN;
+	return false;
 }
 
 /**
@@ -150,6 +157,7 @@ static __always_inline bool smb_compress_alg_valid(__le16 alg, bool valid_none)
  * @size:		uncompressed size
  * @chained:		if chained compression is enabled
  * @use_pattern:	if Pattern_V1 is enabled
+ * @lzalg:		LZ* algorithm that will be used
  *
  * For any case:
  * - SMB2 compression hdr
@@ -168,11 +176,15 @@ static __always_inline bool smb_compress_alg_valid(__le16 alg, bool valid_none)
  * checked by the caller.
  */
 static __always_inline u32 smb_compress_alloc_size(const u32 size, const bool chained,
-						   const bool use_pattern)
+						   const bool use_pattern, const __le16 lzalg)
 {
-	u32 alloc_size;
+	u32 alloc_size = sizeof(struct smb2_compression_hdr);
+
+	if (lzalg == SMB3_COMPRESS_LZ77)
+		alloc_size += smb_lz77_compressed_alloc_size(size);
+	else if (lzalg == SMB3_COMPRESS_LZ77_HUFF)
+		alloc_size += smb_huff_compressed_alloc_size(size);
 
-	alloc_size = sizeof(struct smb2_compression_hdr) + smb_lz77_compressed_alloc_size(size);
 	if (chained) {
 		alloc_size += SMB2_COMPRESSION_PAYLOAD_BASE_LEN;
 		if (use_pattern)
diff --git a/fs/smb/common/compress/huffman.c b/fs/smb/common/compress/huffman.c
new file mode 100644
index 000000000000..654f42a5a6b9
--- /dev/null
+++ b/fs/smb/common/compress/huffman.c
@@ -0,0 +1,983 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026, SUSE LLC
+ *
+ * Authors: Enzo Matsumiya <[email protected]>
+ *
+ * Implementation of the LZ77+Huffman compression algorithm, as per MS-XCA spec.
+ */
+#include <linux/slab.h>
+#include <linux/count_zeros.h>
+#include <linux/sort.h>
+#include <linux/list_sort.h>
+#include <linux/uio.h>
+
+#include "compress.h"
+
+#define HUFF_MIN_MATCH_LEN	3
+#define HUFF_MAX_SEQUENCES	((HUFF_BLOCK_SIZE / HUFF_MIN_MATCH_LEN) + 1)
+#define HUFF_MAX_SYMS		512
+#define HUFF_SYM_MARKER		256
+
+struct huff_sym {
+	union {
+		u16 freq;
+		u16 len;
+	};
+
+	union {
+		u16 sym;
+		u16 code;
+	};
+} __packed;
+
+struct huff_sequence {
+	const u8 *lits_start;
+	const u8 *lits_end;
+
+	u16 match_sym;
+	u16 match_dist;
+	u16 match_len;
+};
+
+struct bitstream {
+	u32 bits;
+	s32 extra;
+
+	union {
+		const void *src;
+		void *dst;
+	};
+	const void *end;
+	void *cur;
+	void *next;
+};
+
+#define huff_sym_ptr(ptr)	((struct huff_sym *)(ptr))
+
+static int cmp_freq(const void *a, const void *b)
+{
+	const u16 freq_a = huff_sym_ptr(a)->freq;
+	const u16 freq_b = huff_sym_ptr(b)->freq;
+
+	if (freq_a <= freq_b)
+		return -1;
+
+	/*
+	 * MS-XCA says we should compare symbol values in case of equal frequencies, but we never
+	 * have duplicate symbols in an array, at any time.
+	 */
+	return 1;
+}
+
+static __always_inline int cmp_sym(const void *a, const void *b)
+{
+	const u16 sym_a = huff_sym_ptr(a)->sym;
+	const u16 sym_b = huff_sym_ptr(b)->sym;
+
+	if (sym_a <= sym_b)
+		return -1;
+
+	/* There are no duplicate symbols ever, no need to check. */
+	return 1;
+}
+
+static __always_inline int cmp_len(const void *a, const void *b)
+{
+	const u16 len_a = huff_sym_ptr(a)->len;
+	const u16 len_b = huff_sym_ptr(b)->len;
+
+	if (len_a < len_b)
+		return -1;
+
+	if (len_a > len_b)
+		return 1;
+
+	/* Same depth, choose smallest symbol. */
+	return cmp_sym(a, b);
+}
+
+/*
+ * Bitstream ops.
+ */
+
+/*
+ * Read 1, 2, or 4 bytes directly from @st->src and advance it.
+ * Store read value in *@varp.
+ * Don't update bitstream state!
+ */
+#define bitstream_read_bytes(st, t, varp)			\
+({								\
+	const size_t __ts = sizeof(t);				\
+	bool __ret = false;					\
+	BUILD_BUG_ON(__ts == 3 || __ts > 4);			\
+	if (likely((st)->src + __ts <= (st)->end)) {		\
+		const t __v = MEM_UNALIGNED_READ((st)->src, t);	\
+		(st)->src += sizeof(t);				\
+		MEM_UNALIGNED_WRITE((varp), __v, t);		\
+		__ret = true;					\
+	}							\
+	(__ret);						\
+})
+
+static __always_inline bool bitstream_decompress_init(struct bitstream *stream, const void *src,
+						      const void *end)
+{
+	u16 bits;
+
+	stream->src = src;
+	stream->end = end;
+
+	/* There must be at least 32 bits of data available at start */
+	if (unlikely(!bitstream_read_bytes(stream, u16, &bits)))
+		return false;
+
+	stream->bits = bits << 16;
+	if (unlikely(!bitstream_read_bytes(stream, u16, &bits)))
+		return false;
+
+	stream->bits |= bits;
+	stream->extra = 16;
+
+	/* Unused on decompress. */
+	stream->cur = NULL;
+	stream->next = NULL;
+
+	return true;
+}
+
+static __always_inline bool bitstream_read_advance(struct bitstream *stream, const u8 bits)
+{
+	stream->bits <<= bits;
+	stream->extra -= bits;
+	if (stream->extra < 0) {
+		u16 bits;
+
+		if (unlikely(!bitstream_read_bytes(stream, u16, &bits)))
+			return false;
+
+		stream->bits |= bits << (-stream->extra);
+		stream->extra += 16;
+	}
+
+	return true;
+}
+
+static __always_inline void bitstream_compress_init(struct bitstream *stream, void *dst,
+						    const void *end)
+{
+	stream->bits = 0;
+	stream->extra = 16;
+	stream->dst = dst;
+	stream->end = end;
+	stream->cur = dst + 2;
+	stream->next = dst + 4;
+}
+
+static __always_inline int bitstream_write(struct bitstream *stream, const u16 bits, const u8 n)
+{
+	if (n <= stream->extra) {
+		stream->extra -= n;
+		stream->bits <<= n;
+		stream->bits |= bits;
+	} else {
+		stream->bits <<= stream->extra;
+		stream->bits |= (bits >> (n - stream->extra));
+		stream->extra -= n;
+
+		if (unlikely(stream->dst + 1 >= stream->end))
+			return false;
+
+		mem_write8(stream->dst, stream->bits & 0xff);
+		mem_write8(stream->dst + 1, (stream->bits >> 8) & 0xff);
+
+		stream->dst = stream->cur;
+		stream->cur = stream->next;
+		stream->next += 2;
+		stream->extra += 16;
+		stream->bits = bits;
+	}
+
+	return true;
+}
+
+static __always_inline bool bitstream_write_byte(struct bitstream *stream, const u8 bits)
+{
+	if (unlikely(stream->next >= stream->end))
+		return false;
+
+	mem_write8(stream->next, bits);
+	stream->next++;
+	return true;
+}
+
+static __always_inline bool bitstream_write_2bytes(struct bitstream *stream, const u16 bits)
+{
+	if (unlikely(stream->next + sizeof(u16) > stream->end))
+		return false;
+
+	mem_write16(stream->next, bits);
+	stream->next += sizeof(u16);
+	return true;
+}
+
+static __always_inline bool bitstream_flush(struct bitstream *stream)
+{
+	stream->bits <<= stream->extra;
+
+	if (unlikely(stream->dst + 1 >= stream->end || stream->cur + sizeof(u16) > stream->end))
+		return false;
+
+	mem_write8(stream->dst, (stream->bits & 0xff));
+	mem_write8(stream->dst + 1, ((stream->bits >> 8) & 0xff));
+	mem_write16(stream->cur, 0);
+
+	return true;
+}
+
+static int huff_build_histogram(struct huff_sequence *seqs, const int nseqs, struct huff_sym *syms)
+{
+	u16 nsyms = 0;
+	int i;
+
+	for (i = 0; i < nseqs; i++) {
+		struct huff_sequence *seq = &seqs[i];
+
+		if (seq->lits_start && seq->lits_end) {
+			const u8 *p = seq->lits_start;
+
+			while (p < seq->lits_end)
+				if (!syms[mem_read8(p++)].freq++)
+					nsyms++;
+		}
+
+		if (likely(seq->match_sym >= HUFF_SYM_MARKER))
+			if (!syms[seq->match_sym].freq++)
+				nsyms++;
+	}
+
+	/* Bug in huff_scan_sequences() */
+	if (unlikely(nsyms == 0 || nsyms > HUFF_MAX_SYMS))
+		return -EIO;
+
+	return nsyms;
+}
+
+static __always_inline void init_nodes(struct huff_sym *syms, const int nsyms,
+				       struct huff_sym *nodes, const int rebalances)
+{
+	u16 i, n = 0;
+
+	for (i = 0; i < HUFF_MAX_SYMS; i++) {
+		u16 freq = syms[i].freq;
+
+		if (!freq)
+			continue;
+		/*
+		 * When rebalancing, half symbols frequencies on each retry, which will generate a
+		 * shallower "tree".
+		 * Rebalancing doesn't affect original symbol count.
+		 */
+		if (unlikely(rebalances)) {
+			freq /= (2 * rebalances);
+			freq++;
+		}
+
+		nodes[n].sym = i;
+		nodes[n].freq = freq;
+		n++;
+	}
+
+	sort(nodes, nsyms, sizeof(*nodes), cmp_freq, NULL);
+}
+
+static __always_inline int select_node(int *ap, const int amax, int *bp, const int bmax,
+				       const struct huff_sym *nodes)
+{
+	const int a = *ap;
+	const int b = *bp;
+
+	if (a < amax && b >= bmax)
+		return (*ap)++;
+
+	if (a >= amax && b < bmax)
+		return (*bp)++;
+
+	if (nodes[a].freq <= nodes[b].freq)
+		return (*ap)++;
+
+	return (*bp)++;
+}
+
+/*
+ * Compute the depth (code length) of each Huffman symbol in @nodes.
+ *
+ * A full-fledged Huffman tree would be built something like:
+ *	qa = syms
+ *	qb = nodes
+ *	while (qa not empty || qb not singular) {
+ *		left = select_node(qa, qb)
+ *		right = select_node(qa, qb)
+ *		new_node->freq = left->freq + right->freq
+ *		queue_node(new_node, qb)
+ *	}
+ *	root = queue_first(qb)
+ *
+ * Then traverse the tree from root to compute each leaf (i.e. syms elements) depth.
+ *
+ * This implementation instead simulates those merges, and store frequencies and parents in a
+ * separate array, and traverse the "tree" by chasing parents in a more linear manner.
+ *
+ * Also, the Huffman tree is a full binary tree, so, given its properties, we know exactly how many
+ * merges should be done, and not rely on more complex checks.
+ *
+ * This saves memory and computing resources.
+ */
+static bool compute_code_lengths(struct huff_sym *nodes, const int nsyms, int *parents)
+{
+	int root, l = 0, n = nsyms, next = nsyms, merges = nsyms - 1;
+
+	do {
+		const int a = select_node(&l, nsyms, &n, next, nodes);
+		const int b = select_node(&l, nsyms, &n, next, nodes);
+
+		nodes[next].freq = nodes[a].freq + nodes[b].freq;
+		parents[a] = next;
+		parents[b] = next;
+
+		next++;
+	} while (--merges);
+
+	root = next - 1;
+
+	for (l = 0; l < nsyms; l++) {
+		int d = 0, p = l;
+
+		/* Chase parent's indices up until root to compute this symbol code length. */
+		while (p != root) {
+			p = parents[p];
+			d++;
+		}
+
+		if (unlikely(d > 14))
+			return false;
+
+		nodes[l].len = d;
+	}
+
+	return true;
+}
+
+static __always_inline int write_codes(struct huff_sym *nodes, struct huff_sym *syms,
+				       const int nsyms, u8 *dst)
+{
+	u16 nextcode = 0, clen = 0;
+	int i;
+
+	MEM_PREFETCH(nodes);
+
+	memset(dst, 0, HUFF_TABLE_SIZE);
+	sort(nodes, nsyms, sizeof(*nodes), cmp_len, NULL);
+
+	for (i = 0; i < nsyms; i++) {
+		struct huff_sym *node = &nodes[i];
+		const u16 len = node->len;
+		const u16 s = node->sym;
+		const u16 pos = (s >> 1);
+		const u8 shift = (s & 1) ? 4 : 0;
+
+		/* Generate symbol code. */
+		nextcode <<= len - clen;
+		clen = len;
+
+		/* Make @syms indexed by symbol value, so we can access it directly later. */
+		syms[s].code = nextcode++;
+		syms[s].len = len;
+
+		/*
+		 * Write symbol depth/code length of Huffman symbols to the table header.
+		 * There can be up to 512 Huffman symbols, but code lengths are stored on nibs
+		 * (4 bits) so the final table will be 256 bytes long.
+		 * (odd symbols go in the upper 4 bits, even symbols on lower 4 bits)
+		 *
+		 * @pos >= HUFF_TABLE_SIZE means there's a bug in huff_encode_syms().
+		 */
+		if (WARN_ON_ONCE(pos >= HUFF_TABLE_SIZE))
+			return -EFAULT;
+
+		dst[pos] |= (len << shift);
+	}
+
+	return 0;
+}
+
+static __always_inline int write_single_code(struct huff_sym *syms, u8 *dst)
+{
+	u16 i;
+
+	memset(dst, 0, HUFF_TABLE_SIZE);
+
+	/* @syms is unsorted here, so @i == symbol */
+	for (i = 0; i < HUFF_MAX_SYMS; i++) {
+		u16 pos;
+		u8 shift;
+
+		if (!syms[i].freq)
+			continue;
+
+		pos = (i >> 1);
+		shift = (i & 1) ? 4 : 0;
+		if (WARN_ON_ONCE(pos >= HUFF_TABLE_SIZE))
+			return -EFAULT;
+
+		syms[i].code = 0;
+		/* len is 1 as it would be the only child in the tree */
+		syms[i].len = 1;
+		dst[pos] |= (1 << shift);
+
+		return 0;
+	}
+
+	return -EFAULT;
+}
+
+static noinline void *huff_encode_syms(struct huff_sym *syms, const int nsyms, void *dst)
+{
+	const int max_nodes = 2 * nsyms - 1;
+	int ret, rebalances = 0, *parents = NULL;
+	struct huff_sym *nodes = NULL;
+
+	/*
+	 * Skip the whole allocations and computations if @nsyms == 1 (very unlikely case).
+	 * (assumes @nsyms == 0 was previously discarded (cf. huff_build_histogram()))
+	 */
+	if (unlikely(nsyms == 1)) {
+		ret = write_single_code(syms, dst);
+		goto out;
+	}
+
+	nodes = kzalloc_objs(*nodes, max_nodes);
+	parents = kzalloc_objs(*parents, max_nodes);
+	if (unlikely(!nodes || !parents)) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	ret = -EOVERFLOW;
+	do {
+		MEM_PREFETCH(nodes);
+
+		init_nodes(syms, nsyms, nodes, rebalances++);
+
+		if (likely(compute_code_lengths(nodes, nsyms, parents))) {
+			ret = write_codes(nodes, syms, nsyms, dst);
+			break;
+		}
+
+		/* Max code length is too large ("tree" too deep); reset, rebalance, and retry. */
+		memset(parents, 0, max_nodes * sizeof(parents[0]));
+		memset(nodes, 0, max_nodes * sizeof(*nodes));
+
+		 /* XXX: should this be increased/decreased? */
+	} while (rebalances < 5);
+out:
+	kfree(nodes);
+	kfree(parents);
+
+	return (!ret ? dst + HUFF_TABLE_SIZE : ERR_PTR(ret));
+}
+
+static noinline void *huff_encode_final(const void *src, void *dst, const void *dst_end,
+					struct huff_sequence *seqs, const int max_seqs,
+					struct huff_sym *syms)
+{
+	const struct huff_sequence *seq;
+	struct bitstream stream;
+	int i = 0;
+
+	bitstream_compress_init(&stream, dst, dst_end);
+
+	do {
+		u16 len, sym;
+		u8 distbit;
+
+		/* Assumes @max_seqs >= 1 was checked by caller. */
+		seq = &seqs[i++];
+		if (seq->lits_start && seq->lits_end) {
+			const u8 *p = seq->lits_start;
+
+			while (p < seq->lits_end) {
+				sym = *p++;
+				if (unlikely(!bitstream_write(&stream, syms[sym].code,
+							      syms[sym].len)))
+					return ERR_PTR(-EFAULT);
+			}
+		}
+
+		/* Done, we just wrote leftover literals */
+		if (unlikely(!seq->match_len))
+			break;
+
+		sym = seq->match_sym;
+		len = seq->match_len;
+		distbit = (sym - HUFF_SYM_MARKER) / 16;
+
+		if (unlikely(!bitstream_write(&stream, syms[sym].code, syms[sym].len)))
+			return ERR_PTR(-EFAULT);
+
+		len -= 3;
+		if (len >= 15) {
+			if (unlikely(!bitstream_write_byte(&stream, (u8)umin(len - 15, 255))))
+				return ERR_PTR(-EFAULT);
+
+			if (len - 15 >= 255)
+				/*
+				 * Match length is < 64k.
+				 * No current support for longer matches.
+				 */
+				if (unlikely(!bitstream_write_2bytes(&stream, (u16)len)))
+					return ERR_PTR(-EFAULT);
+		}
+
+		if (unlikely(!bitstream_write(&stream, seq->match_dist - (1U << distbit),
+					      (u8)distbit)))
+			return ERR_PTR(-EFAULT);
+	} while (i < max_seqs);
+
+	if (unlikely(!bitstream_flush(&stream)))
+		return ERR_PTR(-EFAULT);
+
+	return stream.next;
+}
+
+static __always_inline u16 huff_encode_match(const u16 dist, const u16 len)
+{
+	const u8 distbit = dist < 256 ? __fls(dist) : 8 + __fls(dist >> 8);
+
+	return (u16)(HUFF_SYM_MARKER + umin(len - 3, 15) + (16 * distbit));
+}
+
+static __always_inline u32 hash3(const void *ptr)
+{
+	return smb_compress_hash(mem_read32(ptr) & 0xffffff);
+}
+
+static const void *store_seq(struct huff_sequence *seq, const void *literals,
+			     const void *match, const void *cur, const void *end)
+{
+	if (cur > literals) {
+		seq->lits_start = literals;
+		seq->lits_end = cur;
+	}
+
+	if (likely(match)) {
+		seq->match_len = mem_match_len(match, cur, end);
+		seq->match_dist = cur - match;
+		seq->match_sym = huff_encode_match(seq->match_dist, seq->match_len);
+
+		cur += seq->match_len;
+	}
+
+	return cur;
+}
+
+/*
+ * Scan sequences on @src.
+ *
+ * Sequences are defined as:
+ * - literals chunk (start and end)
+ * - match data (symbol, distance, and length)
+ *
+ * A sequence is stored when a match is found, or at the end, if there are literal leftovers.
+ *
+ * Aside from that, the whole function structure and match finding algorithm are the same as the
+ * one found in lz77.c::smb_lz77_compress(), the only difference is that here we don't do adaptive
+ * skipping, but actually parse every @src byte (for better compression).
+ */
+static noinline int huff_scan_sequences(const void *src, const u32 slen,
+					struct huff_sequence *seqs, int *nseqs)
+{
+	const void *srcp, *rlim, *end, *anchor;
+	const int max_seqs = *nseqs;
+	u32 *htable, hash, s = 0;
+	int ret = 0;
+
+	*nseqs = 0;
+
+	srcp = anchor = src;
+	end = src + slen;
+	rlim = end - SMB_COMPRESS_MSTEP_SIZE; /* read limit for match finding */
+
+	htable = kvcalloc(SMB_COMPRESS_HASH_SIZE, sizeof(*htable), GFP_KERNEL);
+	if (!htable)
+		return -ENOMEM;
+
+	MEM_PREFETCH(srcp + SMB_COMPRESS_RSTEP_SIZE);
+
+	hash = hash3(srcp++);
+	htable[hash] = 0;
+	hash = hash3(srcp);
+
+	do {
+		const void *match, *next = srcp;
+
+		do {
+			const u32 cur_hash = hash;
+
+			srcp = next;
+			next++;
+			if (unlikely(next >= rlim))
+				goto out;
+
+			hash = hash3(next);
+			match = src + htable[cur_hash];
+			htable[cur_hash] = srcp - src;
+
+			/*
+			 * Scans are done in blocks up to HUFF_BLOCK_SIZE (64k) bytes long.
+			 * Due to encoding limitations, Huffman can only find matches that are
+			 * (64k - 1) bytes back, which is impossible, because:
+			 * - we're only reading up to 'rlim' (i.e. end - 8)
+			 * - even if going further, that would mean a match len of 1 (min is 3)
+			 *
+			 * So, IOW, with our window < block size and window < max distance, we're
+			 * always within our window, so all we need to check is 'match' == 'srcp'
+			 * (i.e. htable entry was not filled yet).
+			 */
+		} while (match == srcp || memcmp(match, srcp, HUFF_MIN_MATCH_LEN));
+
+		if (unlikely(s >= max_seqs)) {
+			ret = -EIO;
+			break;
+		}
+
+		srcp = store_seq(&seqs[s++], anchor, match, srcp, end);
+		anchor = srcp;
+		MEM_PREFETCH(srcp);
+
+		if (unlikely(srcp >= rlim))
+			break;
+
+		hash = hash3(srcp);
+	} while (srcp < end);
+out:
+	kvfree(htable);
+
+	if (!ret) {
+		if (unlikely(s >= max_seqs))
+			return -EIO;
+
+		/* Add sequence for leftover literals */
+		end = store_seq(&seqs[s++], anchor, NULL, end, NULL);
+		if (IS_ERR(end))
+			ret = PTR_ERR(end);
+	}
+
+	*nseqs = s;
+
+	return ret;
+}
+
+/*
+ * Huffman encoding performs extra steps on top of a LZ77-style encoded buffer so it can further
+ * compress the symbols at bit level, offering a much better compression (vs. e.g. LZ77 plain).
+ *
+ * The uncompressed buffer @src is parsed in 64k blocks, and each block goes through 4 main steps
+ * as described below.
+ *
+ * Expectations (compared to LZ77 plain):
+ * - compression ratio should be about 10-20% better
+ * - performance should be around 2-3x worse
+ */
+int smb_huff_compress(const void *src, const u32 slen, void *dst, u32 *dlen)
+{
+	struct huff_sequence *seqs;
+	ssize_t ret, remaining = slen;
+	struct huff_sym *syms;
+	const void *srcp = src, *dst_end = dst + *dlen;
+	int nseqs, nsyms;
+	void *dstp = dst;
+
+	seqs = kvzalloc_objs(*seqs, HUFF_MAX_SEQUENCES);
+	if (unlikely(!seqs))
+		return -ENOMEM;
+
+	syms = kzalloc_objs(*syms, HUFF_MAX_SYMS);
+	if (unlikely(!syms)) {
+		kvfree(seqs);
+		return -ENOMEM;
+	}
+
+	do {
+		const u32 block_slen = umin(HUFF_BLOCK_SIZE, remaining);
+
+		/*
+		 * Step 1. LZ77 encoding
+		 *
+		 * As per the spec, we should first compress @src with smb_lz77_compress() and then
+		 * use @dst as our input for step 2.
+		 * This implementation decided to NOT do the full LZ77 encoding in order to save
+		 * processing time, as it would be required to implement a smb_lz77_decompress-like
+		 * function to proceed.
+		 * Instead it stores "sequence" tokens to aggregate literals and matches data
+		 * (which are then used on step 2).
+		 *
+		 * This step is implementation specific anyway, as it only builds up intermediate
+		 * data that won't affect the final format.
+		 *
+		 * Even though the bit- vs byte-level compression does an amazing job, match
+		 * finding here still counts a lot.
+		 */
+		nseqs = HUFF_MAX_SEQUENCES;
+		ret = huff_scan_sequences(srcp, block_slen, seqs, &nseqs);
+		if (unlikely(ret))
+			break;
+
+		/*
+		 * Step 2. Symbol histogram
+		 *
+		 * Build a histogram of Huffman symbols; each literal (individual byte) is a symbol
+		 * (i.e. 0 - 255), and each match is encoded as a symbol too (256 - 511).
+		 *
+		 * Count the frequencies of each symbol occurrence, along with the number of unique
+		 * symbols in the block.
+		 */
+		nsyms = huff_build_histogram(seqs, nseqs, syms);
+		if (unlikely(nsyms < 0)) {
+			ret = nsyms;
+			break;
+		}
+
+		/*
+		 * Step 3. Encode symbols
+		 *
+		 * Canonically, this step builds up the Huffman tree in order to compute the code
+		 * and depth (or length) of each symbol.
+		 * This implementation also decided to not do it this way, but instead use a faster
+		 * approach (cf. compute_code_lengths()).
+		 *
+		 * After computing those, code lengths are written to the Huffman table (the first
+		 * 256 bytes of the compressed block).
+		 */
+		dstp = huff_encode_syms(syms, nsyms, dstp);
+		if (IS_ERR(dstp)) {
+			ret = PTR_ERR(dstp);
+			break;
+		}
+
+		/*
+		 * Step 4. Final encoding
+		 *
+		 * Now we have all symbols' codes and lengths, write those out to @dstp as a
+		 * bitstream (for bit-level compression).
+		 */
+		dstp = huff_encode_final(srcp, dstp, dst_end, seqs, nseqs, syms);
+		if (IS_ERR(dstp)) {
+			ret = PTR_ERR(dstp);
+			break;
+		}
+
+		srcp += block_slen;
+		remaining -= block_slen;
+
+		if (likely(remaining > 0)) {
+			int i;
+
+			memset(syms, 0, sizeof(*syms) * HUFF_MAX_SYMS);
+
+			for (i = 0; i < HUFF_MAX_SEQUENCES; i++)
+				memset(&seqs[i], 0, sizeof(*seqs));
+		}
+	} while (remaining > 0);
+
+	kvfree(seqs);
+	kfree(syms);
+
+	if (!ret)
+		*dlen = dstp - dst;
+
+	return ret;
+}
+
+static __always_inline const void *fill_table(const u8 *header, u16 *table, u8 *clens)
+{
+	const u8 *end = header + HUFF_TABLE_SIZE;
+	int len, i = 0;
+	u8 *plens = clens;
+	u16 sym;
+
+	/* read code lenghts from Huffman table on compressed buffer */
+	while (header < end) {
+		const u8 b = mem_read8(header++);
+
+		mem_write8(plens++, (b & 0x0f));
+		mem_write8(plens++, (b & 0xf0) >> 4);
+	}
+
+	for (len = 1; len < 16; len++) {
+		for (sym = 0; sym < HUFF_MAX_SYMS; sym++) {
+			if (clens[sym] == len) {
+				int n = (1U << (SMB_COMPRESS_HASH_LOG - len));
+
+				while (n-- > 0) {
+					if (unlikely(i >= SMB_COMPRESS_HASH_SIZE))
+						return ERR_PTR(-EIO);
+
+					table[i++] = sym;
+				}
+			}
+		}
+	}
+
+	if (unlikely(i != SMB_COMPRESS_HASH_SIZE))
+		return ERR_PTR(-EIO);
+
+	return header;
+}
+
+static __always_inline int huff_decode_match(struct bitstream *stream, u32 *len)
+{
+	u32 mlen = *len & 15;
+
+	if (mlen == 15) {
+		if (unlikely(!bitstream_read_bytes(stream, u8, &mlen)))
+			return -EFAULT;
+
+		if (mlen == 255) {
+			if (unlikely(!bitstream_read_bytes(stream, u16, &mlen)))
+				return -EFAULT;
+
+			if (mlen == 0) {
+				if (unlikely(!bitstream_read_bytes(stream, u32, &mlen)))
+					return -EFAULT;
+
+				if (unlikely(mlen + 15 < HUFF_BLOCK_SIZE))
+					return -EIO;
+			} else if (unlikely(mlen < 15)) {
+				return -EIO;
+			}
+			mlen -= 15;
+		}
+		mlen += 15;
+	}
+	mlen += 3;
+	*len = mlen;
+
+	return 0;
+}
+
+int smb_huff_decompress(const void *src, const u32 slen, void *dst, const u32 dlen)
+{
+	const void *srcp = src, *end = src + slen;
+	void *dstp = dst, *dst_end = dst + dlen;
+	int ret = 0;
+
+	do {
+		struct bitstream stream;
+		const void *block_end = dstp + umin(SZ_64K, dst_end - dstp);
+		u16 *table = NULL;
+		u8 *clens = NULL;
+
+		ret = -ENOMEM;
+		table = kvzalloc_objs(*table, SMB_COMPRESS_HASH_SIZE);
+		if (unlikely(!table))
+			goto err_free;
+
+		clens = kzalloc_objs(*clens, HUFF_MAX_SYMS);
+		if (unlikely(!clens))
+			goto err_free;
+
+		if (unlikely(srcp + HUFF_TABLE_SIZE > end)) {
+			ret = -EFAULT;
+			goto err_free;
+		}
+
+		srcp = fill_table(srcp, table, clens);
+		if (IS_ERR(srcp)) {
+			ret = PTR_ERR(srcp);
+			goto err_free;
+		}
+
+		if (unlikely(!bitstream_decompress_init(&stream, srcp, end))) {
+			ret = -EFAULT;
+			goto err_free;
+		}
+
+		do {
+			const u16 sym = table[stream.bits >> (32 - 15)];
+			const u32 clen = clens[sym];
+			u16 dist, distbit;
+			const void *match;
+			u32 len;
+
+			ret = -EIO;
+			if (unlikely(sym >= HUFF_MAX_SYMS))
+				break;
+
+			if (unlikely(clen >= 16))
+				break;
+
+			/* Advance bitstream before checking sym */
+			if (unlikely(!bitstream_read_advance(&stream, clen))) {
+				ret = -EFAULT;
+				break;
+			}
+
+			if (sym < HUFF_SYM_MARKER) {
+				mem_write8(dstp++, sym);
+				ret = 0;
+				continue;
+			}
+
+			if (unlikely(sym == HUFF_SYM_MARKER && stream.src >= end)) {
+				ret = 0;
+				break;
+			}
+
+			len = sym - HUFF_SYM_MARKER;
+			distbit = len >> 4;
+			if (unlikely(distbit >= 16))
+				break;
+
+			ret = huff_decode_match(&stream, &len);
+			if (unlikely(ret))
+				break;
+
+			ret = -EFAULT;
+			dist = (u32)((u64)stream.bits >> (32 - distbit));
+			dist += (1U << distbit);
+			if (unlikely(dist > dstp - dst))
+				break;
+
+			if (unlikely(len > dst_end - dstp))
+				break;
+
+			/* Advance bitstream only after checking match len */
+			if (unlikely(!bitstream_read_advance(&stream, distbit)))
+				break;
+
+			match = dstp - dist;
+			if (len < dist) {
+				memcpy(dstp, match, len);
+				dstp += len;
+			} else {
+				const void *match_end = dstp + len;
+
+				while (dstp < match_end)
+					mem_write8(dstp++, mem_read8(match++));
+			}
+
+			ret = 0;
+		} while (dstp < block_end);
+err_free:
+		kvfree(table);
+		kfree(clens);
+
+		if (unlikely(ret))
+			break;
+
+		srcp = stream.src;
+	} while (srcp < end && dstp < dst_end);
+
+	return ret;
+}
diff --git a/fs/smb/common/compress/huffman.h b/fs/smb/common/compress/huffman.h
new file mode 100644
index 000000000000..0106e596f9e1
--- /dev/null
+++ b/fs/smb/common/compress/huffman.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2026, SUSE LLC
+ *
+ * Authors: Enzo Matsumiya <[email protected]>
+ *
+ * Implementation of the LZ77+Huffman compression algorithm, as per MS-XCA spec.
+ */
+#ifndef _SMB_COMPRESS_LZ77_HUFF_H
+#define _SMB_COMPRESS_LZ77_HUFF_H
+
+#include <linux/kernel.h>
+#include <linux/sizes.h>
+
+#define HUFF_TABLE_SIZE		256
+#define HUFF_BLOCK_SIZE		SZ_64K
+
+/*
+ * LZ77-Huffman metadata is Huffman table (256 bytes) at the beginning of every 64k block, so
+ * compute allocation size considering the worst-case scenarios (fully uncompressible blocks).
+ */
+static __always_inline u32 smb_huff_compressed_alloc_size(const u32 size)
+{
+	const u32 nblocks = DIV_ROUND_UP(size, HUFF_BLOCK_SIZE);
+
+	return nblocks * (HUFF_TABLE_SIZE + HUFF_BLOCK_SIZE);
+}
+
+int smb_huff_compress(const void *src, const u32 slen, void *dst, u32 *dlen);
+int smb_huff_decompress(const void *src, const u32 slen, void *dst, const u32 dlen);
+#endif /* _SMB_COMPRESS_LZ77_HUFF_H */
diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c
index 59e78c3b042c..46974499dddf 100644
--- a/fs/smb/server/compress.c
+++ b/fs/smb/server/compress.c
@@ -35,7 +35,7 @@ int ksmbd_decompress_request(struct ksmbd_conn *conn)
 		return -EINVAL;
 
 	if (conn->dialect != SMB311_PROT_ID ||
-	    conn->compress_algorithm == SMB3_COMPRESS_NONE)
+	    !smb_compress_alg_valid(conn->compress_algorithm, false))
 		return -EINVAL;
 
 	hdr = smb_get_msg(conn->request_buf);
@@ -98,10 +98,11 @@ int ksmbd_compress_response(struct ksmbd_work *work)
 	u8 *src = NULL, *out = NULL, *p;
 	bool chained, pattern;
 	u32 src_len, dst_len;
+	__le16 alg = work->conn->compress_algorithm;
 	int i, rc;
 
 	if (!work->compress_response || work->encrypted ||
-	    work->conn->compress_algorithm != SMB3_COMPRESS_LZ77)
+	    !smb_compress_alg_valid(alg, false))
 		return 0;
 
 	req_hdr = smb_get_msg(work->request_buf);
@@ -139,7 +140,7 @@ int ksmbd_compress_response(struct ksmbd_work *work)
 		goto out;
 	}
 
-	dst_len = smb_compress_alloc_size(src_len, chained, pattern);
+	dst_len = smb_compress_alloc_size(src_len, chained, pattern, alg);
 	out = kvzalloc(sizeof(__be32) + dst_len,
 		       KSMBD_DEFAULT_GFP);
 	if (!out) {
@@ -147,7 +148,7 @@ int ksmbd_compress_response(struct ksmbd_work *work)
 		goto out;
 	}
 
-	rc = smb_compression_compress(SMB3_COMPRESS_LZ77, chained, pattern,
+	rc = smb_compression_compress(alg, chained, pattern,
 				      src, src_len,
 				      out + sizeof(__be32),
 				      &dst_len);
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 5859fa68bb84..3bd62c16ae49 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -1130,18 +1130,27 @@ static __le32 decode_compress_ctxt(struct ksmbd_conn *conn,
 		__le16 alg = algs[i];
 
 		/*
-		 * LZ77 is the required general-purpose codec. Pattern_V1 is an
+		 * LZ* are the required general-purpose codecs. Pattern_V1 is an
 		 * optional chained payload type and cannot stand alone.
+		 *
+		 * Use the first LZ algorithm found in the request array.  It's sorted by
+		 * client-preferred order, so don't overwrite it if already set.
 		 */
-		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) {
+		switch (alg) {
+		case SMB3_COMPRESS_LZ77:
+		case SMB3_COMPRESS_LZ77_HUFF:
+			if (conn->compress_algorithm == SMB3_COMPRESS_NONE) {
+				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));
+			}
+			break;
+		case SMB3_COMPRESS_PATTERN:
 			conn->compress_pattern = true;
+			break;
 		}
 	}
 
-- 
2.54.0