| // 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, |
| bool allow_pattern, |
| 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) { |
| if (!allow_pattern) |
| return -EINVAL; |
| 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 |
| * @allow_pattern: whether Pattern_V1 payloads 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, |
| bool allow_pattern, 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, allow_pattern, |
| 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); |