[PATCH v2 12/14] smb: client: add support for decompressing READs
Enzo Matsumiya <[email protected]> Mon, 20 Jul 2026 16:49:23 -0300
| Newsgroups | org.kernel.vger.linux-cifs |
|---|---|
| Message-ID | <[email protected]> |
Implement decompression support for SMB2 READ messages.
Changes:
- smb2ops.c: add receive_compressed() and check_decompressed_smb2()
- check for compress header in smb3_is_transform_hdr() as well so we
fall into the same path handling offloaded READ decryption
- common/compress/compress.h: add is_compress_hdr() and
smb_decompress_alloc_size() helpers
Signed-off-by: Enzo Matsumiya <[email protected]>
---
fs/smb/client/smb2ops.c | 160 +++++++++++++++++++++++++++++-
fs/smb/client/smb2pdu.c | 6 ++
fs/smb/common/compress/compress.h | 86 ++++++++++++++++
3 files changed, 251 insertions(+), 1 deletion(-)
diff --git a/fs/smb/client/smb2ops.c b/fs/smb/client/smb2ops.c
index d60bc537ecf1..09cbc00e83b0 100644
--- a/fs/smb/client/smb2ops.c
+++ b/fs/smb/client/smb2ops.c
@@ -30,6 +30,8 @@
#include "fs_context.h"
#include "cached_dir.h"
#include "reparse.h"
+#include "../common/compress/compress.h"
+#include "compress.h"
/* Change credits for different ops and return the total number of credits */
static int
@@ -4714,7 +4716,7 @@ smb3_is_transform_hdr(void *buf)
{
struct smb2_transform_hdr *trhdr = buf;
- return trhdr->ProtocolId == SMB2_TRANSFORM_PROTO_NUM;
+ return (trhdr->ProtocolId == SMB2_TRANSFORM_PROTO_NUM) || is_compress_hdr(buf);
}
static int
@@ -5246,6 +5248,153 @@ receive_encrypted_standard(struct TCP_Server_Info *server,
return ret;
}
+static __always_inline bool check_decompressed_smb2(const void *buf, const u32 len)
+{
+ const struct smb2_read_rsp *rsp = buf;
+
+ if (unlikely(rsp->hdr.ProtocolId != SMB2_PROTO_NUMBER)) {
+ cifs_dbg(VFS, "decompressed message is not an SMB2 message (got ProtocolId 0x%x)\n",
+ rsp->hdr.ProtocolId);
+ return false;
+ }
+
+ if (unlikely(rsp->hdr.Command != SMB2_READ)) {
+ cifs_dbg(VFS, "decompressed message is not an SMB2 READ (got %u)\n",
+ le16_to_cpu(rsp->hdr.Command));
+ return false;
+ }
+
+ if (unlikely(le32_to_cpu(rsp->DataLength) != len)) {
+ cifs_dbg(VFS, "invalid decompressed length for SMB2 READ (got %u, expected %u)\n",
+ le32_to_cpu(rsp->DataLength), len);
+ return false;
+ }
+
+ return true;
+}
+
+static int receive_compressed(struct TCP_Server_Info *server)
+{
+ struct mid_q_entry *mid;
+ void *src = NULL, *dst = NULL;
+ u32 slen, dlen;
+ int read, ret;
+
+ /* smb2_compression_pattern_v1 is the smallest compressed data we can have. */
+ slen = server->pdu_size;
+ if (unlikely(slen < sizeof(struct smb2_compression_hdr) +
+ sizeof(struct smb2_compression_pattern_v1))) {
+ ret = -EINVAL;
+ goto err_free;
+ }
+
+ src = kvzalloc(slen, GFP_KERNEL);
+ if (unlikely(!src)) {
+ ret = -ENOMEM;
+ goto err_free;
+ }
+
+ read = server->total_read;
+ if (unlikely(read > slen)) {
+ ret = -EINVAL;
+ goto err_free;
+ }
+
+ memcpy(src, server->smallbuf, read);
+ ret = cifs_read_from_socket(server, src + read, slen - read);
+ if (ret < 0)
+ goto err_free;
+
+ server->total_read += ret;
+ if (unlikely(slen - ret != read)) {
+ ret = -EINVAL;
+ goto err_free;
+ }
+
+ ret = -EINVAL;
+ read = server->vals->read_rsp_size;
+ dlen = smb_decompress_alloc_size(src, slen, read, read + 256 +
+ SMB2_COMPRESSION_PAYLOAD_BASE_LEN + server->max_read,
+ server->compression.chained);
+ if (unlikely(dlen == 0))
+ goto err_free;
+
+ dst = kvzalloc(dlen, GFP_KERNEL);
+ if (unlikely(!dst)) {
+ ret = -ENOMEM;
+ goto err_free;
+ }
+
+ ret = smb_compression_decompress(server->compression.alg, server->compression.chained,
+ src, slen, dst, dlen);
+ if (unlikely(ret))
+ goto err_free;
+
+ if (unlikely(!check_decompressed_smb2(dst, dlen - server->vals->read_rsp_size))) {
+ ret = -EINVAL;
+ goto err_free;
+ }
+
+ mid = smb2_find_dequeue_mid(server, dst);
+ if (!mid) {
+ ret = -EIO;
+ goto err_free;
+ }
+
+ ret = handle_read_data(server, mid, dst, dlen, NULL, 0, true);
+ if (!ret) {
+ struct cifs_io_subrequest *rdata = mid->callback_data;
+#ifdef CONFIG_CIFS_STATS2
+ mid->when_received = jiffies;
+#endif
+ if (server->ops->is_network_name_deleted)
+ server->ops->is_network_name_deleted(dst, server);
+
+ rdata->iov[0].iov_base = dst;
+ rdata->iov[0].iov_len = server->vals->read_rsp_size;
+ mid_execute_callback(server, mid);
+ } else {
+ spin_lock(&server->srv_lock);
+ if (server->tcpStatus == CifsNeedReconnect) {
+ spin_lock(&server->mid_queue_lock);
+ mid->mid_state = MID_RETRY_NEEDED;
+ spin_unlock(&server->mid_queue_lock);
+ spin_unlock(&server->srv_lock);
+
+ mid_execute_callback(server, mid);
+ } else {
+ spin_lock(&server->mid_queue_lock);
+ mid->mid_state = MID_REQUEST_SUBMITTED;
+ mid->deleted_from_q = false;
+ list_add_tail(&mid->qhead, &server->pending_mid_q);
+ spin_unlock(&server->mid_queue_lock);
+ spin_unlock(&server->srv_lock);
+ }
+ }
+
+ release_mid(server, mid);
+err_free:
+ kvfree(src);
+ kvfree(dst);
+
+ /*
+ * We _must_ disconnect on any failed decompression.
+ * Actually, there's not much we can do to handle different decompression errors here, so
+ * forcing a reconnect (which will disable compression) is our best option as the request
+ * should be retried.
+ */
+ if (unlikely(ret)) {
+ /* Alias only if not -ENOMEM */
+ if (ret != -ENOMEM)
+ ret = -ECONNRESET;
+ spin_lock(&server->srv_lock);
+ server->tcpStatus = CifsNeedReconnect;
+ spin_unlock(&server->srv_lock);
+ }
+
+ return ret;
+}
+
static int
smb3_receive_transform(struct TCP_Server_Info *server,
struct mid_q_entry **mids, char **bufs, int *num_mids)
@@ -5255,6 +5404,13 @@ smb3_receive_transform(struct TCP_Server_Info *server,
struct smb2_transform_hdr *tr_hdr = (struct smb2_transform_hdr *)buf;
unsigned int orig_len = le32_to_cpu(tr_hdr->OriginalMessageSize);
+ if (is_compress_hdr(buf))
+ return receive_compressed(server);
+
+ /*
+ * @buf is either a compress or transform (encrypt) header, previously checked by cifsd
+ * (or we wouldn't reach here), so no need to check again.
+ */
if (pdu_length < sizeof(struct smb2_transform_hdr) +
sizeof(struct smb2_hdr)) {
cifs_server_dbg(VFS, "Transform message is too small (%u)\n",
@@ -5296,6 +5452,8 @@ static int smb2_next_header(struct TCP_Server_Info *server, char *buf,
*noff = le32_to_cpu(t_hdr->OriginalMessageSize);
if (unlikely(check_add_overflow(*noff, sizeof(*t_hdr), noff)))
return -EINVAL;
+ } else if (hdr->ProtocolId == SMB2_COMPRESSION_TRANSFORM_ID) {
+ *noff = 0;
} else {
*noff = le32_to_cpu(hdr->NextCommand);
}
diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
index e58ac7be30a3..bf2b3c7fa4b8 100644
--- a/fs/smb/client/smb2pdu.c
+++ b/fs/smb/client/smb2pdu.c
@@ -4933,6 +4933,12 @@ smb2_async_readv(struct cifs_io_subrequest *rdata)
flags |= CIFS_HAS_CREDITS;
}
+ if (should_compress(io_parms.tcon, &rqst)) {
+ struct smb2_read_req *req = (struct smb2_read_req *)buf;
+
+ req->Flags |= SMB2_READFLAG_REQUEST_COMPRESSED;
+ }
+
rc = cifs_call_async(server, &rqst,
cifs_readv_receive, smb2_readv_callback,
smb3_handle_read_data, rdata, flags,
diff --git a/fs/smb/common/compress/compress.h b/fs/smb/common/compress/compress.h
index 43dba4bb1176..e0ec41a21505 100644
--- a/fs/smb/common/compress/compress.h
+++ b/fs/smb/common/compress/compress.h
@@ -152,6 +152,92 @@ static __always_inline bool smb_compress_alg_valid(__le16 alg, bool valid_none)
return false;
}
+static __always_inline bool is_compress_hdr(const void *buf)
+{
+ const struct smb2_compression_hdr *hdr = buf;
+
+ return likely(hdr) && hdr->ProtocolId == SMB2_COMPRESSION_TRANSFORM_ID;
+}
+
+/**
+ * smb_decompress_alloc_size() - Validate received SMB2 compression header + return allocation size
+ * for the decompressed buffer.
+ * @buf: received buffer
+ * @pdu_size: size of @buf
+ * @dmin: minimum decompressed size allowed (inclusive)
+ * @dmax: maximum decompressed size allowed (inclusive)
+ * @chained: if chaining was negotiated
+ *
+ * Return: decompressed size, or 0 if invalid.
+ */
+static __always_inline u32 smb_decompress_alloc_size(const void *buf, const u32 pdu_size,
+ const u32 dmin, const u32 dmax, bool chained)
+{
+ const struct smb2_compression_hdr *hdr = buf;
+ __le16 alg, flags;
+ u32 len;
+
+ if (unlikely(!is_compress_hdr(buf) || pdu_size < sizeof(*hdr))) {
+ pr_warn("invalid SMB2 compressed buffer/header\n");
+ return 0;
+ }
+
+ len = le32_to_cpu(hdr->OriginalCompressedSegmentSize);
+ alg = le16_to_cpu(hdr->CompressionAlgorithm);
+ flags = le16_to_cpu(hdr->Flags);
+
+ if (flags == SMB2_COMPRESSION_FLAG_CHAINED) {
+ if (unlikely(!chained)) {
+ pr_warn("chained payload but chaining wasn't negotiated\n");
+ return 0;
+ }
+
+ if (unlikely(!smb_compress_alg_valid(alg, true))) {
+ pr_warn("invalid chained compression algorithm (%u)\n", alg);
+ return 0;
+ }
+
+ /* This is the first payload_hdr->Length field, so it can never be 0 */
+ if (unlikely(le32_to_cpu(hdr->Offset) == 0)) {
+ pr_warn("invalid chained compression payload length 0\n");
+ return 0;
+ }
+ } else if (flags == SMB2_COMPRESSION_FLAG_NONE) {
+ /*
+ * When unchained, we must account for the offset (uncompressed) part as well
+ * (usually SMB2 header, but can be anything).
+ */
+ u32 res, offset = le32_to_cpu(hdr->Offset);
+
+ if (unlikely(chained)) {
+ pr_warn("unchained payload but chaining negotiated\n");
+ return 0;
+ }
+
+ if (unlikely(offset > pdu_size - sizeof(*hdr) ||
+ check_add_overflow(len, offset, &res))) {
+ pr_warn("invalid unchained compression offset (%u, len=%u)\n", offset, len);
+ return 0;
+ }
+
+ len = res;
+ if (unlikely(!smb_compress_alg_valid(alg, false) || alg == SMB3_COMPRESS_PATTERN)) {
+ pr_warn("invalid unchained compression algorithm (%u)\n", alg);
+ return 0;
+ }
+ } else {
+ pr_warn("invalid SMB2 compression flags (0x%x)\n", flags);
+ return 0;
+ }
+
+ if (unlikely(len < dmin || len > dmax)) {
+ pr_warn("decompressed size %u out of range (min=%u, max=%u)\n", len, dmin, dmax);
+ return 0;
+ }
+
+ return len;
+}
+
/**
* smb_compress_alloc_size() - Compute total allocation size required for compressed (dst) buffer.
* @size: uncompressed size
--
2.54.0