diff options
| author | Linus Torvalds <torvalds@linux-foundation.org> | 2026-06-18 09:21:50 -0700 |
|---|---|---|
| committer | Linus Torvalds <torvalds@linux-foundation.org> | 2026-06-18 09:21:50 -0700 |
| commit | dac3b26eae7bee261fa05f20c3fcc24988a7c233 (patch) | |
| tree | 7329c2f9a86be2db9e627293078ab643e01a8bde | |
| parent | e753a63f2ac8599182a5b6899c158a745188551d (diff) | |
| parent | 1c8951963d8ed357f70f59e0ad4ddce2199d2016 (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
...
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_MSTEP_SIZE; /* read limit (for lz77_match_len()) */ dstp = dst; @@ -251,7 +257,8 @@ noinline int lz77_compress(const void *src, const u32 slen, void *dst, u32 *dlen /* * Main loop. * - * @dlen is >= lz77_compressed_alloc_size(), so run without bound-checking @dstp. + * @dlen is >= smb_lz77_compressed_alloc_size(), so run without + * bound-checking @dstp. * * This code was crafted in a way to best utilise fetch-decode-execute CPU flow. * Any attempt to optimize it, or even organize it, can lead to huge performance loss. @@ -333,3 +340,122 @@ out: return -EMSGSIZE; } +EXPORT_SYMBOL_GPL(smb_lz77_compress); + +static int lz77_decode_match_len(const u8 **src, const u8 *end, u16 token, + u8 *nibble, bool *have_nibble, u32 *len) +{ + u8 extra; + + *len = (token & 0x7) + 3; + if ((token & 0x7) != 0x7) + return 0; + + if (!*have_nibble) { + if (*src >= end) + return -EINVAL; + *nibble = *(*src)++; + extra = *nibble & 0xf; + *have_nibble = true; + } else { + extra = *nibble >> 4; + *have_nibble = false; + } + + *len += extra; + if (extra == 0xf) { + u8 b; + + if (*src >= end) + return -EINVAL; + b = *(*src)++; + if (b != 0xff) { + *len += b; + } else { + u16 w; + + if (end - *src < 2) + return -EINVAL; + w = get_unaligned_le16(*src); + *src += 2; + if (w) { + *len = w + 3; + } else { + u32 long_len; + + if (end - *src < 4) + return -EINVAL; + long_len = get_unaligned_le32(*src); + *src += 4; + if (check_add_overflow(long_len, 3, len)) + return -EINVAL; + } + } + } + + return 0; +} + +int smb_lz77_decompress(const void *src, const u32 slen, void *dst, + const u32 dlen) +{ + const u8 *sp = src, *send = sp + slen; + u8 *dp = dst, *dend = dp + dlen; + u32 flags = 0; + int flag_count = 0; + u8 nibble = 0; + bool have_nibble = false; + + while (dp < dend) { + u32 len, dist; + u16 token; + + if (!flag_count) { + if (send - sp < 4) + return -EINVAL; + flags = get_unaligned_le32(sp); + sp += 4; + flag_count = 32; + } + + if (!(flags & 0x80000000)) { + if (sp >= send) + return -EINVAL; + *dp++ = *sp++; + flags <<= 1; + flag_count--; + continue; + } + + flags <<= 1; + flag_count--; + + if (send - sp < 2) + return -EINVAL; + + token = get_unaligned_le16(sp); + sp += 2; + + dist = (token >> 3) + 1; + if (dist > dp - (u8 *)dst) + return -EINVAL; + + if (lz77_decode_match_len(&sp, send, token, &nibble, + &have_nibble, &len)) + return -EINVAL; + + if (len > dend - dp) + return -EINVAL; + + while (len--) { + *dp = *(dp - dist); + dp++; + } + } + + return 0; +} +EXPORT_SYMBOL_GPL(smb_lz77_decompress); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("SMB plain LZ77 compression"); diff --git a/fs/smb/client/compress/lz77.h b/fs/smb/common/compress/lz77.h index 4e570846aefa..e032c0f1b48d 100644 --- a/fs/smb/client/compress/lz77.h +++ b/fs/smb/common/compress/lz77.h @@ -12,7 +12,7 @@ #include <linux/kernel.h> /** - * lz77_compressed_alloc_size() - Compute compressed buffer size. + * smb_lz77_compressed_alloc_size() - Compute compressed buffer size. * @size: uncompressed (src) size * * Compute allocation size for the compressed buffer based on uncompressed size. @@ -26,18 +26,21 @@ * Worst case scenario is an all-literal compression, which means: * metadata bytes = 4 + ((@size / 32) * 4) + 4, or, simplified, (@size >> 3) + 8 * - * The worst case scenario rarely happens, but such overprovisioning also allows lz77_compress() - * main loop to run without ever bound checking dst, which is a huge perf improvement, while also - * being safe when compression goes bad. + * The worst case scenario rarely happens, but such overprovisioning also + * allows smb_lz77_compress() main loop to run without ever bound checking dst, + * which is a huge perf improvement, while also being safe when compression goes + * bad. * * Return: required (*) allocation size for compressed buffer. * - * (*) checked once in the beginning of lz77_compress() + * (*) checked once in the beginning of smb_lz77_compress() */ -static __always_inline u32 lz77_compressed_alloc_size(const u32 size) +static __always_inline u32 smb_lz77_compressed_alloc_size(const u32 size) { return size + (size >> 3) + 8; } -int lz77_compress(const void *src, const u32 slen, void *dst, u32 *dlen); +int smb_lz77_compress(const void *src, const u32 slen, void *dst, u32 *dlen); +int smb_lz77_decompress(const void *src, const u32 slen, void *dst, + const u32 dlen); #endif /* _SMB_COMPRESS_LZ77_H */ diff --git a/fs/smb/common/fscc.h b/fs/smb/common/fscc.h index bc3012cc295d..859849a42fec 100644 --- a/fs/smb/common/fscc.h +++ b/fs/smb/common/fscc.h @@ -100,6 +100,24 @@ struct duplicate_extents_to_file_ex { __le32 Reserved; } __packed; +/* + * compression state flags + * See MS-FSCC 2.3.18 + * MS-FSCC 2.3.67 + * MS-FSCC 2.4.9 + */ +#define COMPRESSION_FORMAT_NONE 0x0000 +#define COMPRESSION_FORMAT_DEFAULT 0x0001 +#define COMPRESSION_FORMAT_LZNT1 0x0002 + +/* + * See MS-FSCC 2.3.18 + * MS-FSCC 2.3.67 + */ +struct compress_ioctl { + __le16 CompressionState; +} __packed; + /* See MS-FSCC 2.3.20 */ struct fsctl_get_integrity_information_rsp { __le16 ChecksumAlgorithm; diff --git a/fs/smb/common/smb2pdu.h b/fs/smb/common/smb2pdu.h index aeb0a245c532..325ff83b12fe 100644 --- a/fs/smb/common/smb2pdu.h +++ b/fs/smb/common/smb2pdu.h @@ -218,10 +218,9 @@ struct smb2_transform_hdr { * These are simplified versions from the spec, as we don't need a fully fledged * form of both unchained and chained structs. * - * Moreover, even in chained compressed payloads, the initial compression header - * has the form of the unchained one -- i.e. it never has the - * OriginalPayloadSize field and ::Offset field always represent an offset - * (instead of a length, as it is in the chained header). + * For chained payloads, only the first 8 bytes belong to the transform header. + * CompressionAlgorithm, Flags and Offset below overlay the first chained + * payload header, where Offset represents Length. * * See MS-SMB2 2.2.42 for more details. */ @@ -524,9 +523,7 @@ struct smb2_compression_capabilities_context { __le16 CompressionAlgorithmCount; __le16 Padding; __le32 Flags; - __le16 CompressionAlgorithms[3]; - __u16 Pad; /* Some servers require pad to DataLen multiple of 8 */ - /* Check if pad needed */ + __le16 CompressionAlgorithms[4]; } __packed; /* diff --git a/fs/smb/common/smbfsctl.h b/fs/smb/common/smbfsctl.h index 3253a18ecb5c..d1fcb46a7cde 100644 --- a/fs/smb/common/smbfsctl.h +++ b/fs/smb/common/smbfsctl.h @@ -61,8 +61,8 @@ #define FSCTL_LOCK_VOLUME 0x00090018 #define FSCTL_UNLOCK_VOLUME 0x0009001C #define FSCTL_IS_PATHNAME_VALID 0x0009002C /* BB add struct */ -#define FSCTL_GET_COMPRESSION 0x0009003C /* BB add struct */ -#define FSCTL_SET_COMPRESSION 0x0009C040 /* BB add struct */ +#define FSCTL_GET_COMPRESSION 0x0009003C +#define FSCTL_SET_COMPRESSION 0x0009C040 #define FSCTL_QUERY_FAT_BPB 0x00090058 /* BB add struct */ /* Verify the next FSCTL number, we had it as 0x00090090 before */ #define FSCTL_FILESYSTEM_GET_STATS 0x00090060 /* BB add struct */ @@ -159,6 +159,12 @@ #define IO_REPARSE_TAG_LX_CHR 0x80000025 #define IO_REPARSE_TAG_LX_BLK 0x80000026 +#define IO_REPARSE_TAG_LX_SYMLINK_LE cpu_to_le32(IO_REPARSE_TAG_LX_SYMLINK) +#define IO_REPARSE_TAG_AF_UNIX_LE cpu_to_le32(IO_REPARSE_TAG_AF_UNIX) +#define IO_REPARSE_TAG_LX_FIFO_LE cpu_to_le32(IO_REPARSE_TAG_LX_FIFO) +#define IO_REPARSE_TAG_LX_CHR_LE cpu_to_le32(IO_REPARSE_TAG_LX_CHR) +#define IO_REPARSE_TAG_LX_BLK_LE cpu_to_le32(IO_REPARSE_TAG_LX_BLK) + /* If Name Surrogate Bit is set, the file or directory represents another named entity in the system. */ #define IS_REPARSE_TAG_NAME_SURROGATE(tag) (!!((tag) & 0x20000000)) diff --git a/fs/smb/server/Makefile b/fs/smb/server/Makefile index 6407ba6b9340..a3e9306055e8 100644 --- a/fs/smb/server/Makefile +++ b/fs/smb/server/Makefile @@ -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 index 000000000000..f8cf515b9c30 --- /dev/null +++ b/fs/smb/server/compress.c @@ -0,0 +1,209 @@ +// 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" +#include "../common/compress/lz77.h" + +#define SMB_COMPRESS_MIN_LEN PAGE_SIZE + +/** + * 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; +} + +/** + * ksmbd_compress_response() - compress an eligible ksmbd response + * @work: request work item containing the response iov + * + * Compression transforms describe one contiguous SMB2 message, while ksmbd + * builds responses from multiple iov entries. Flatten the response first, + * produce the negotiated transform, and replace the response iov only when the + * result is smaller than the original message. + * + * Encrypted and compound responses are intentionally left unchanged. The + * caller may still continue sending the original response when this function + * returns zero. + * + * Return: 1 if the response was replaced, 0 if compression was skipped, or a + * negative errno on failure. + */ +int ksmbd_compress_response(struct ksmbd_work *work) +{ + struct smb2_compression_hdr *chdr; + struct smb2_hdr *req_hdr; + u32 src_len, dst_len, compressed_pdu_len, max_dst_len; + u8 *src = NULL, *out = NULL, *p; + int i, rc; + + if (!work->compress_response || work->encrypted || + work->conn->compress_algorithm != SMB3_COMPRESS_LZ77) + return 0; + + req_hdr = smb_get_msg(work->request_buf); + if (req_hdr->NextCommand || work->next_smb2_rcv_hdr_off || + work->next_smb2_rsp_hdr_off) + return 0; + + src_len = get_rfc1002_len(work->iov[0].iov_base); + if (src_len < SMB_COMPRESS_MIN_LEN) + return 0; + + src = kvmalloc(src_len, KSMBD_DEFAULT_GFP); + if (!src) + return -ENOMEM; + + p = src; + /* iov[0] contains only the RFC1002 length; the SMB2 PDU starts at iov[1]. */ + for (i = 1; i < work->iov_cnt; i++) { + if (work->iov[i].iov_len > src + src_len - p) { + rc = -EINVAL; + goto out; + } + memcpy(p, work->iov[i].iov_base, work->iov[i].iov_len); + p += work->iov[i].iov_len; + } + if (p != src + src_len) { + rc = -EINVAL; + goto out; + } + + max_dst_len = smb_lz77_compressed_alloc_size(src_len) + + sizeof(struct smb2_compression_hdr) + + 3 * sizeof(struct smb2_compression_payload_hdr) + + 2 * sizeof(struct smb2_compression_pattern_v1); + out = kvzalloc(sizeof(__be32) + max_dst_len, + KSMBD_DEFAULT_GFP); + if (!out) { + rc = -ENOMEM; + goto out; + } + + if (work->conn->compress_chained) { + dst_len = max_dst_len; + rc = smb_compression_compress_chained(SMB3_COMPRESS_LZ77, + work->conn->compress_pattern, + src, src_len, + out + sizeof(__be32), + &dst_len); + if (rc == -EMSGSIZE || dst_len >= src_len) { + rc = 0; + goto out; + } + if (rc) + goto out; + compressed_pdu_len = dst_len; + } else { + /* + * Peers which did not negotiate chained compression still use + * the original 16-byte unchained transform format. + */ + dst_len = smb_lz77_compressed_alloc_size(src_len); + rc = smb_lz77_compress(src, src_len, + out + sizeof(__be32) + sizeof(*chdr), + &dst_len); + if (rc == -EMSGSIZE || + dst_len + sizeof(*chdr) >= src_len) { + rc = 0; + goto out; + } + if (rc) + goto out; + + compressed_pdu_len = sizeof(*chdr) + dst_len; + chdr = (struct smb2_compression_hdr *)(out + sizeof(__be32)); + chdr->ProtocolId = SMB2_COMPRESSION_TRANSFORM_ID; + chdr->OriginalCompressedSegmentSize = cpu_to_le32(src_len); + chdr->CompressionAlgorithm = SMB3_COMPRESS_LZ77; + chdr->Flags = cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE); + chdr->Offset = 0; + } + + *(__be32 *)out = cpu_to_be32(compressed_pdu_len); + + /* + * Keep the transform in work->compress_buf until send completion. + * Existing response iovs can then be replaced without changing their + * individual ownership rules. + */ + work->compress_buf = out; + work->iov[0].iov_base = out; + work->iov[0].iov_len = sizeof(__be32); + work->iov[1].iov_base = out + sizeof(__be32); + work->iov[1].iov_len = compressed_pdu_len; + work->iov_cnt = 2; + work->iov_idx = 1; + out = NULL; + rc = 1; +out: + kvfree(out); + kvfree(src); + return rc; +} diff --git a/fs/smb/server/compress.h b/fs/smb/server/compress.h new file mode 100644 index 000000000000..663c6f44f09b --- /dev/null +++ b/fs/smb/server/compress.h @@ -0,0 +1,16 @@ +/* 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); +int ksmbd_compress_response(struct ksmbd_work *work); + +#endif /* __KSMBD_COMPRESS_H__ */ diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 8347495dbc62..9e8fdb39e5a2 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -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; diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index e074be942582..ec75633b7da0 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -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; diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index ab4958dc3eb0..a5ab6799a65c 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -53,6 +53,7 @@ void ksmbd_free_work_struct(struct ksmbd_work *work) } kfree(work->tr_buf); + kvfree(work->compress_buf); kvfree(work->request_buf); kfree(work->iov); diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index d36393ff8310..0da8cc0972d6 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -67,12 +67,16 @@ struct ksmbd_work { unsigned int response_sz; void *tr_buf; + /* Contiguous SMB2 compression transform owned by this work item. */ + void *compress_buf; unsigned char state; /* No response for cancelled request */ bool send_no_response:1; /* Request is encrypted */ bool encrypted:1; + /* READ response should be wrapped in a compression transform. */ + bool compress_response:1; /* Is this SYNC or ASYNC ksmbd_work */ bool asynchronous:1; bool need_invalidate_rkey:1; diff --git a/fs/smb/server/misc.c b/fs/smb/server/misc.c index a543ec9d3581..966004c414a8 100644 --- a/fs/smb/server/misc.c +++ b/fs/smb/server/misc.c @@ -283,39 +283,6 @@ char *ksmbd_extract_sharename(struct unicode_map *um, const char *treename) return ksmbd_casefold_sharename(um, name); } -/** - * convert_to_unix_name() - convert windows name to unix format - * @share: ksmbd_share_config pointer - * @name: file name that is relative to share - * - * Return: converted name on success, otherwise NULL - */ -char *convert_to_unix_name(struct ksmbd_share_config *share, const char *name) -{ - int no_slash = 0, name_len, path_len; - char *new_name; - - if (name[0] == '/') - name++; - - path_len = share->path_sz; - name_len = strlen(name); - new_name = kmalloc(path_len + name_len + 2, KSMBD_DEFAULT_GFP); - if (!new_name) - return new_name; - - memcpy(new_name, share->path, path_len); - if (new_name[path_len - 1] != '/') { - new_name[path_len] = '/'; - no_slash = 1; - } - - memcpy(new_name + path_len + no_slash, name, name_len); - path_len += name_len + no_slash; - new_name[path_len] = 0x00; - return new_name; -} - char *ksmbd_convert_dir_info_name(struct ksmbd_dir_info *d_info, const struct nls_table *local_nls, int *conv_len) diff --git a/fs/smb/server/misc.h b/fs/smb/server/misc.h index 13423696ae8c..3909104e18ad 100644 --- a/fs/smb/server/misc.h +++ b/fs/smb/server/misc.h @@ -25,7 +25,6 @@ void ksmbd_strip_last_slash(char *path); void ksmbd_conv_path_to_windows(char *path); char *ksmbd_casefold_sharename(struct unicode_map *um, const char *name); char *ksmbd_extract_sharename(struct unicode_map *um, const char *treename); -char *convert_to_unix_name(struct ksmbd_share_config *share, const char *name); #define KSMBD_DIR_INFO_ALIGNMENT 8 struct ksmbd_dir_info; diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index b193dde4810d..60e7e821c245 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -528,7 +528,12 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, ret = compare_guid_key(opinfo, client_guid, lctx->lease_key); if (ret) { + if (!atomic_inc_not_zero(&opinfo->refcount)) + continue; + if (m_opinfo) + opinfo_put(m_opinfo); m_opinfo = opinfo; + /* skip upgrading lease about breaking lease */ if (atomic_read(&opinfo->breaking_cnt)) continue; @@ -1246,6 +1251,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, if (atomic_read(&m_opinfo->breaking_cnt)) opinfo->o_lease->flags = SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; + opinfo_put(m_opinfo); goto out; } } diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 5d799b2d4c62..36feda7e0942 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -22,6 +22,7 @@ #include "crypto_ctx.h" #include "auth.h" #include "stats.h" +#include "compress.h" int ksmbd_debug_types; @@ -244,6 +245,15 @@ send: if (work->tcon) ksmbd_tree_connect_put(work->tcon); smb3_preauth_hash_rsp(work); + /* + * Preauthentication hashes cover the original SMB2 response. Apply the + * transport compression wrapper only after updating the hash. + */ + if (work->compress_response) { + rc = ksmbd_compress_response(work); + if (rc < 0) + ksmbd_debug(CONN, "Failed to compress response: %d\n", rc); + } if (work->sess && work->sess->enc && work->encrypted && conn->ops->encrypt_resp) { rc = conn->ops->encrypt_resp(work); diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 00e63debcbe9..c1c6c1a64f60 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -17,7 +17,7 @@ #include <linux/fileattr.h> #include "glob.h" -#include "smbfsctl.h" +#include "../common/smbfsctl.h" #include "oplock.h" #include "smbacl.h" @@ -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) { @@ -614,6 +615,11 @@ int smb2_check_user_session(struct ksmbd_work *work) sess_id, work->sess->id); return -EINVAL; } + if (work->sess->state != SMB2_SESSION_VALID) { + pr_err("compound request on a non-valid session (state %d)\n", + work->sess->state); + return -EINVAL; + } return 1; } @@ -804,6 +810,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 +895,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 +1011,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 +1111,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 +1157,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 +2157,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) @@ -3604,7 +3704,10 @@ int smb2_open(struct ksmbd_work *work) if (!created) smb2_update_xattrs(tcon, &path, fp); - else + + ksmbd_vfs_update_compressed_fattr(path.dentry, &fp->f_ci->m_fattr); + + if (created) smb2_new_xattrs(tcon, &path, fp); memcpy(fp->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); @@ -4466,6 +4569,8 @@ int smb2_query_dir(struct ksmbd_work *work) ksmbd_debug(SMB, "Search pattern is %s\n", srch_ptr); } + mutex_lock(&dir_fp->readdir_lock); + if (srch_flag & SMB2_REOPEN || srch_flag & SMB2_RESTART_SCANS) { ksmbd_debug(SMB, "Restart directory scan\n"); generic_file_llseek(dir_fp->filp, 0, SEEK_SET); @@ -4570,6 +4675,7 @@ no_buf_len: goto err_out; } + mutex_unlock(&dir_fp->readdir_lock); kfree(srch_ptr); ksmbd_fd_put(work, dir_fp); ksmbd_revert_fsids(work); @@ -4577,6 +4683,7 @@ no_buf_len: err_out: pr_err("error while processing smb2 query dir rc = %d\n", rc); + mutex_unlock(&dir_fp->readdir_lock); kfree(srch_ptr); err_out2: @@ -5276,6 +5383,7 @@ static int get_file_compression_info(struct smb2_query_info_rsp *rsp, { struct smb2_file_comp_info *file_info; struct kstat stat; + u16 fmt; int ret; ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, @@ -5283,9 +5391,13 @@ static int get_file_compression_info(struct smb2_query_info_rsp *rsp, if (ret) return ret; + ret = ksmbd_vfs_get_compression(fp, &fmt); + if (ret) + return ret; + file_info = (struct smb2_file_comp_info *)rsp->Buffer; - file_info->CompressedFileSize = cpu_to_le64(stat.blocks << 9); - file_info->CompressionFormat = COMPRESSION_FORMAT_NONE; + file_info->CompressedFileSize = cpu_to_le64(min_t(u64, stat.blocks << 9, stat.size)); + file_info->CompressionFormat = cpu_to_le16(fmt); file_info->CompressionUnitShift = 0; file_info->ChunkShift = 0; file_info->ClusterShift = 0; @@ -5329,6 +5441,12 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, int out_buf_len = sizeof(struct smb311_posix_qinfo) + 32; int ret; + if (!(fp->daccess & FILE_READ_ATTRIBUTES_LE)) { + pr_err("no right to read the attributes : 0x%x\n", + fp->daccess); + return -EACCES; + } + ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, AT_STATX_SYNC_AS_STAT); if (ret) @@ -6567,6 +6685,11 @@ static int smb2_set_info_file(struct ksmbd_work *work, struct ksmbd_file *fp, } case FILE_LINK_INFORMATION: { + if (!(fp->daccess & FILE_DELETE_LE)) { + pr_err("no right to delete : 0x%x\n", fp->daccess); + return -EACCES; + } + if (buf_len < sizeof(struct smb2_file_link_info)) return -EMSGSIZE; @@ -6624,6 +6747,9 @@ static int smb2_set_info_sec(struct ksmbd_file *fp, int addition_info, fp->saccess |= FILE_SHARE_DELETE_LE; + if (!(fp->daccess & (FILE_WRITE_DAC_LE | FILE_WRITE_OWNER_LE))) + return -EACCES; + return set_info_sec(fp->conn, fp->tcon, &fp->filp->f_path, pntsd, buf_len, false, true); } @@ -6636,6 +6762,7 @@ static int smb2_set_info_sec(struct ksmbd_file *fp, int addition_info, */ int smb2_set_info(struct ksmbd_work *work) { + const struct cred *saved_cred; struct smb2_set_info_req *req; struct smb2_set_info_rsp *rsp; struct ksmbd_file *fp = NULL; @@ -6677,6 +6804,7 @@ int smb2_set_info(struct ksmbd_work *work) goto err_out; } + saved_cred = override_creds(fp->filp->f_cred); switch (req->InfoType) { case SMB2_O_INFO_FILE: ksmbd_debug(SMB, "GOT SMB2_O_INFO_FILE\n"); @@ -6684,19 +6812,15 @@ int smb2_set_info(struct ksmbd_work *work) break; case SMB2_O_INFO_SECURITY: ksmbd_debug(SMB, "GOT SMB2_O_INFO_SECURITY\n"); - if (ksmbd_override_fsids(work)) { - rc = -ENOMEM; - goto err_out; - } rc = smb2_set_info_sec(fp, le32_to_cpu(req->AdditionalInformation), (char *)req + le16_to_cpu(req->BufferOffset), le32_to_cpu(req->BufferLength)); - ksmbd_revert_fsids(work); break; default: rc = -EOPNOTSUPP; } + revert_creds(saved_cred); if (rc < 0) goto err_out; @@ -7004,6 +7128,15 @@ int smb2_read(struct ksmbd_work *work) kvfree(aux_payload_buf); goto out; } + /* + * RDMA responses are transferred through channel buffers and encrypted + * responses use the encryption transform, so only normal SMB transport + * responses are candidates for compression. + */ + if (!is_rdma_channel && nbytes && + (req->Flags & SMB2_READFLAG_REQUEST_COMPRESSED) && + conn->compress_algorithm != SMB3_COMPRESS_NONE) + work->compress_response = true; ksmbd_fd_put(work, fp); return 0; @@ -7341,14 +7474,14 @@ int smb2_cancel(struct ksmbd_work *work) continue; /* - * A cancelled deferred byte-range lock frees its - * file_lock and takes the smb2_lock() early-exit that - * skips release_async_work(), so the work stays on - * conn->async_requests with a live cancel_fn pointing - * at the freed file_lock. Re-firing it on a second - * SMB2_CANCEL is a use-after-free. + * Only an ACTIVE deferred work may have its cancel_fn + * fired. A CANCELLED or CLOSED work already took the + * smb2_lock() non-ACTIVE early-exit that frees the + * file_lock and skips release_async_work(), so it is + * still on conn->async_requests with a live cancel_fn + * pointing at the freed file_lock. */ - if (iter->state == KSMBD_WORK_CANCELLED) + if (iter->state != KSMBD_WORK_ACTIVE) break; ksmbd_debug(SMB, @@ -7755,29 +7888,27 @@ skip: list_del(&work->fp_entry); spin_unlock(&fp->f_lock); - if (work->state != KSMBD_WORK_ACTIVE) { - list_del(&smb_lock->llist); - locks_free_lock(flock); + list_del(&smb_lock->llist); + release_async_work(work); - if (work->state == KSMBD_WORK_CANCELLED) { - rsp->hdr.Status = - STATUS_CANCELLED; - kfree(smb_lock); - smb2_send_interim_resp(work, - STATUS_CANCELLED); - work->send_no_response = 1; - goto out; - } + if (work->state == KSMBD_WORK_ACTIVE) + goto retry; - rsp->hdr.Status = - STATUS_RANGE_NOT_LOCKED; + locks_free_lock(flock); + + if (work->state == KSMBD_WORK_CANCELLED) { + rsp->hdr.Status = STATUS_CANCELLED; kfree(smb_lock); - goto out2; + smb2_send_interim_resp(work, + STATUS_CANCELLED); + work->send_no_response = 1; + goto out; } - list_del(&smb_lock->llist); - release_async_work(work); - goto retry; + rsp->hdr.Status = + STATUS_RANGE_NOT_LOCKED; + kfree(smb_lock); + goto out2; } else if (!rc) { list_add(&smb_lock->llist, &rollback_list); spin_lock(&work->conn->llist_lock); @@ -7934,9 +8065,9 @@ static int fsctl_copychunk(struct ksmbd_work *work, /* * FILE_READ_DATA should only be included in - * the FSCTL_COPYCHUNK case + * the FSCTL_SRV_COPYCHUNK case */ - if (cnt_code == FSCTL_COPYCHUNK && + if (cnt_code == FSCTL_SRV_COPYCHUNK && !(dst_fp->daccess & (FILE_READ_DATA_LE | FILE_GENERIC_READ_LE))) { rsp->hdr.Status = STATUS_ACCESS_DENIED; goto out; @@ -8256,6 +8387,7 @@ static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id, if (fp->f_ci->m_fattr != old_fattr && test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_STORE_DOS_ATTRS)) { + const struct cred *saved_cred; struct xattr_dos_attrib da; ret = ksmbd_vfs_get_dos_attrib_xattr(idmap, @@ -8264,9 +8396,11 @@ static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id, goto out; da.attr = le32_to_cpu(fp->f_ci->m_fattr); + saved_cred = override_creds(fp->filp->f_cred); ret = ksmbd_vfs_set_dos_attrib_xattr(idmap, &fp->filp->f_path, &da, true); + revert_creds(saved_cred); if (ret) fp->f_ci->m_fattr = old_fattr; } @@ -8353,6 +8487,62 @@ int smb2_ioctl(struct ksmbd_work *work) ret = -EOPNOTSUPP; rsp->hdr.Status = STATUS_FS_DRIVER_REQUIRED; goto out2; + case FSCTL_GET_COMPRESSION: { + struct compress_ioctl *cmpr_rsp; + struct ksmbd_file *fp; + u16 fmt; + + if (out_buf_len < sizeof(struct compress_ioctl)) { + ret = -EINVAL; + goto out; + } + + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -ENOENT; + goto out; + } + + ret = ksmbd_vfs_get_compression(fp, &fmt); + ksmbd_fd_put(work, fp); + if (ret < 0) + goto out; + + cmpr_rsp = (struct compress_ioctl *)&rsp->Buffer[0]; + cmpr_rsp->CompressionState = cpu_to_le16(fmt); + nbytes = sizeof(struct compress_ioctl); + rsp->PersistentFileId = req->PersistentFileId; + rsp->VolatileFileId = req->VolatileFileId; + break; + } + case FSCTL_SET_COMPRESSION: { + struct compress_ioctl *cmpr_req; + struct ksmbd_file *fp; + + if (in_buf_len < sizeof(struct compress_ioctl)) { + ret = -EINVAL; + goto out; + } + + if (!test_tree_conn_flag(work->tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) { + ksmbd_debug(SMB, "User does not have write permission\n"); + ret = -EACCES; + goto out; + } + + cmpr_req = (struct compress_ioctl *)buffer; + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -ENOENT; + goto out; + } + + ret = ksmbd_vfs_set_compression(work, fp, le16_to_cpu(cmpr_req->CompressionState)); + ksmbd_fd_put(work, fp); + if (ret) + goto out; + break; + } case FSCTL_CREATE_OR_GET_OBJECT_ID: { struct file_object_buf_type1_ioctl_rsp *obj_buf; @@ -8410,7 +8600,7 @@ int smb2_ioctl(struct ksmbd_work *work) goto out; nbytes = ret; break; - case FSCTL_REQUEST_RESUME_KEY: + case FSCTL_SRV_REQUEST_RESUME_KEY: if (out_buf_len < sizeof(struct resume_key_ioctl_rsp)) { ret = -EINVAL; goto out; @@ -8424,8 +8614,8 @@ int smb2_ioctl(struct ksmbd_work *work) rsp->VolatileFileId = req->VolatileFileId; nbytes = sizeof(struct resume_key_ioctl_rsp); break; - case FSCTL_COPYCHUNK: - case FSCTL_COPYCHUNK_WRITE: + case FSCTL_SRV_COPYCHUNK: + case FSCTL_SRV_COPYCHUNK_WRITE: if (!test_tree_conn_flag(work->tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) { ksmbd_debug(SMB, "User does not have write permission\n"); @@ -8500,6 +8690,12 @@ int smb2_ioctl(struct ksmbd_work *work) goto out; } + if (!(fp->daccess & FILE_WRITE_DATA_LE)) { + ksmbd_fd_put(work, fp); + ret = -EACCES; + goto out; + } + ret = ksmbd_vfs_zero_data(work, fp, off, len); ksmbd_fd_put(work, fp); if (ret < 0) @@ -8575,6 +8771,21 @@ int smb2_ioctl(struct ksmbd_work *work) goto dup_ext_out; } + if (!test_tree_conn_flag(work->tcon, + KSMBD_TREE_CONN_FLAG_WRITABLE)) { + ret = -EACCES; + goto dup_ext_out; + } + + if (!(fp_out->daccess & FILE_WRITE_DATA_LE)) { + ret = -EACCES; + goto dup_ext_out; + } + if (!(fp_in->daccess & FILE_READ_DATA_LE)) { + ret = -EACCES; + goto dup_ext_out; + } + src_off = le64_to_cpu(dup_ext->SourceFileOffset); dst_off = le64_to_cpu(dup_ext->TargetFileOffset); length = le64_to_cpu(dup_ext->ByteCount); diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index e7cf573e59f0..3bed676bb5ad 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -230,9 +230,6 @@ struct smb2_file_mode_info { __le32 Mode; } __packed; -#define COMPRESSION_FORMAT_NONE 0x0000 -#define COMPRESSION_FORMAT_LZNT1 0x0002 - struct smb2_file_comp_info { __le64 CompressedFileSize; __le16 CompressionFormat; diff --git a/fs/smb/server/smb_common.c b/fs/smb/server/smb_common.c index 741aabdfcef5..7de73223189a 100644 --- a/fs/smb/server/smb_common.c +++ b/fs/smb/server/smb_common.c @@ -102,9 +102,6 @@ static const struct { int version; const char *string; } version_strings[] = { -#ifdef CONFIG_SMB_INSECURE_SERVER - {SMB1_PROT, SMB1_VERSION_STRING}, -#endif {SMB2_PROT, SMB20_VERSION_STRING}, {SMB21_PROT, SMB21_VERSION_STRING}, {SMB30_PROT, SMB30_VERSION_STRING}, @@ -188,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) diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index 664b1b4a3233..340ea98fa494 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -1477,7 +1477,9 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, break; aces_size -= ace_size; - if (ace->sid.num_subauth > SID_MAX_SUB_AUTHORITIES) + if (ace->sid.num_subauth > SID_MAX_SUB_AUTHORITIES || + ace_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE + + sizeof(__le32) * ace->sid.num_subauth) break; if (!compare_sids(&sid, &ace->sid) || diff --git a/fs/smb/server/smbfsctl.h b/fs/smb/server/smbfsctl.h deleted file mode 100644 index ecdf8f6e0df4..000000000000 --- a/fs/smb/server/smbfsctl.h +++ /dev/null @@ -1,91 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1+ */ -/* - * fs/smb/server/smbfsctl.h: SMB, CIFS, SMB2 FSCTL definitions - * - * Copyright (c) International Business Machines Corp., 2002,2009 - * Author(s): Steve French (sfrench@us.ibm.com) - */ - -/* IOCTL information */ -/* - * List of ioctl/fsctl function codes that are or could be useful in the - * future to remote clients like cifs or SMB2 client. There is probably - * a slightly larger set of fsctls that NTFS local filesystem could handle, - * including the seven below that we do not have struct definitions for. - * Even with protocol definitions for most of these now available, we still - * need to do some experimentation to identify which are practical to do - * remotely. Some of the following, such as the encryption/compression ones - * could be invoked from tools via a specialized hook into the VFS rather - * than via the standard vfs entry points - */ - -#ifndef __KSMBD_SMBFSCTL_H -#define __KSMBD_SMBFSCTL_H - -#define FSCTL_DFS_GET_REFERRALS 0x00060194 -#define FSCTL_DFS_GET_REFERRALS_EX 0x000601B0 -#define FSCTL_REQUEST_OPLOCK_LEVEL_1 0x00090000 -#define FSCTL_REQUEST_OPLOCK_LEVEL_2 0x00090004 -#define FSCTL_REQUEST_BATCH_OPLOCK 0x00090008 -#define FSCTL_LOCK_VOLUME 0x00090018 -#define FSCTL_UNLOCK_VOLUME 0x0009001C -#define FSCTL_IS_PATHNAME_VALID 0x0009002C /* BB add struct */ -#define FSCTL_GET_COMPRESSION 0x0009003C /* BB add struct */ -#define FSCTL_SET_COMPRESSION 0x0009C040 /* BB add struct */ -#define FSCTL_QUERY_FAT_BPB 0x00090058 /* BB add struct */ -/* Verify the next FSCTL number, we had it as 0x00090090 before */ -#define FSCTL_FILESYSTEM_GET_STATS 0x00090060 /* BB add struct */ -#define FSCTL_GET_NTFS_VOLUME_DATA 0x00090064 /* BB add struct */ -#define FSCTL_GET_RETRIEVAL_POINTERS 0x00090073 /* BB add struct */ -#define FSCTL_IS_VOLUME_DIRTY 0x00090078 /* BB add struct */ -#define FSCTL_ALLOW_EXTENDED_DASD_IO 0x00090083 /* BB add struct */ -#define FSCTL_REQUEST_FILTER_OPLOCK 0x0009008C -#define FSCTL_FIND_FILES_BY_SID 0x0009008F /* BB add struct */ -#define FSCTL_SET_OBJECT_ID 0x00090098 /* BB add struct */ -#define FSCTL_GET_OBJECT_ID 0x0009009C /* BB add struct */ -#define FSCTL_DELETE_OBJECT_ID 0x000900A0 /* BB add struct */ -#define FSCTL_SET_REPARSE_POINT 0x000900A4 /* BB add struct */ -#define FSCTL_GET_REPARSE_POINT 0x000900A8 /* BB add struct */ -#define FSCTL_DELETE_REPARSE_POINT 0x000900AC /* BB add struct */ -#define FSCTL_SET_OBJECT_ID_EXTENDED 0x000900BC /* BB add struct */ -#define FSCTL_CREATE_OR_GET_OBJECT_ID 0x000900C0 /* BB add struct */ -#define FSCTL_SET_SPARSE 0x000900C4 /* BB add struct */ -#define FSCTL_SET_ZERO_DATA 0x000980C8 /* BB add struct */ -#define FSCTL_SET_ENCRYPTION 0x000900D7 /* BB add struct */ -#define FSCTL_ENCRYPTION_FSCTL_IO 0x000900DB /* BB add struct */ -#define FSCTL_WRITE_RAW_ENCRYPTED 0x000900DF /* BB add struct */ -#define FSCTL_READ_RAW_ENCRYPTED 0x000900E3 /* BB add struct */ -#define FSCTL_READ_FILE_USN_DATA 0x000900EB /* BB add struct */ -#define FSCTL_WRITE_USN_CLOSE_RECORD 0x000900EF /* BB add struct */ -#define FSCTL_SIS_COPYFILE 0x00090100 /* BB add struct */ -#define FSCTL_RECALL_FILE 0x00090117 /* BB add struct */ -#define FSCTL_QUERY_SPARING_INFO 0x00090138 /* BB add struct */ -#define FSCTL_SET_ZERO_ON_DEALLOC 0x00090194 /* BB add struct */ -#define FSCTL_SET_SHORT_NAME_BEHAVIOR 0x000901B4 /* BB add struct */ -#define FSCTL_QUERY_ALLOCATED_RANGES 0x000940CF /* BB add struct */ -#define FSCTL_SET_DEFECT_MANAGEMENT 0x00098134 /* BB add struct */ -#define FSCTL_DUPLICATE_EXTENTS_TO_FILE 0x00098344 -#define FSCTL_SIS_LINK_FILES 0x0009C104 -#define FSCTL_PIPE_PEEK 0x0011400C /* BB add struct */ -#define FSCTL_PIPE_TRANSCEIVE 0x0011C017 /* BB add struct */ -/* strange that the number for this op is not sequential with previous op */ -#define FSCTL_PIPE_WAIT 0x00110018 /* BB add struct */ -#define FSCTL_REQUEST_RESUME_KEY 0x00140078 -#define FSCTL_LMR_GET_LINK_TRACK_INF 0x001400E8 /* BB add struct */ -#define FSCTL_LMR_SET_LINK_TRACK_INF 0x001400EC /* BB add struct */ -#define FSCTL_VALIDATE_NEGOTIATE_INFO 0x00140204 -#define FSCTL_QUERY_NETWORK_INTERFACE_INFO 0x001401FC -#define FSCTL_COPYCHUNK 0x001440F2 -#define FSCTL_COPYCHUNK_WRITE 0x001480F2 - -#define IO_REPARSE_TAG_MOUNT_POINT 0xA0000003 -#define IO_REPARSE_TAG_HSM 0xC0000004 -#define IO_REPARSE_TAG_SIS 0x80000007 - -/* WSL reparse tags */ -#define IO_REPARSE_TAG_LX_SYMLINK_LE cpu_to_le32(0xA000001D) -#define IO_REPARSE_TAG_AF_UNIX_LE cpu_to_le32(0x80000023) -#define IO_REPARSE_TAG_LX_FIFO_LE cpu_to_le32(0x80000024) -#define IO_REPARSE_TAG_LX_CHR_LE cpu_to_le32(0x80000025) -#define IO_REPARSE_TAG_LX_BLK_LE cpu_to_le32(0x80000026) -#endif /* __KSMBD_SMBFSCTL_H */ diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index cd1dbca0cffb..74b0307cb100 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -20,6 +20,7 @@ #include <linux/sched/xacct.h> #include <linux/crc32c.h> #include <linux/splice.h> +#include <linux/fileattr.h> #include "glob.h" #include "oplock.h" @@ -247,17 +248,20 @@ out: static int ksmbd_vfs_stream_read(struct ksmbd_file *fp, char *buf, loff_t *pos, size_t count) { + const struct cred *saved_cred; ssize_t v_len; char *stream_buf = NULL; ksmbd_debug(VFS, "read stream data pos : %llu, count : %zd\n", *pos, count); + saved_cred = override_creds(fp->filp->f_cred); v_len = ksmbd_vfs_getcasexattr(file_mnt_idmap(fp->filp), fp->filp->f_path.dentry, fp->stream.name, fp->stream.size, &stream_buf); + revert_creds(saved_cred); if ((int)v_len <= 0) return (int)v_len; @@ -381,6 +385,7 @@ int ksmbd_vfs_read(struct ksmbd_work *work, struct ksmbd_file *fp, size_t count, static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, size_t count) { + const struct cred *saved_cred; char *stream_buf = NULL, *wbuf; struct mnt_idmap *idmap = file_mnt_idmap(fp->filp); size_t size; @@ -401,6 +406,7 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, count = XATTR_SIZE_MAX - *pos; } + saved_cred = override_creds(fp->filp->f_cred); v_len = ksmbd_vfs_getcasexattr(idmap, fp->filp->f_path.dentry, fp->stream.name, @@ -409,14 +415,14 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, if (v_len < 0) { pr_err("not found stream in xattr : %zd\n", v_len); err = v_len; - goto out; + goto out_revert; } if (v_len < size) { wbuf = kvzalloc(size, KSMBD_DEFAULT_GFP); if (!wbuf) { err = -ENOMEM; - goto out; + goto out_revert; } if (v_len > 0) @@ -434,6 +440,8 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, size, 0, true); +out_revert: + revert_creds(saved_cred); if (err < 0) goto out; else @@ -910,15 +918,21 @@ void ksmbd_vfs_set_fadvise(struct file *filp, __le32 option) int ksmbd_vfs_zero_data(struct ksmbd_work *work, struct ksmbd_file *fp, loff_t off, loff_t len) { + const struct cred *saved_cred; + int err; + smb_break_all_levII_oplock(work, fp, 1); + saved_cred = override_creds(fp->filp->f_cred); if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) - return vfs_fallocate(fp->filp, - FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, - off, len); - - return vfs_fallocate(fp->filp, - FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE, - off, len); + err = vfs_fallocate(fp->filp, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + off, len); + else + err = vfs_fallocate(fp->filp, + FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE, + off, len); + revert_creds(saved_cred); + return err; } int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, @@ -1000,13 +1014,15 @@ int ksmbd_vfs_remove_xattr(struct mnt_idmap *idmap, int ksmbd_vfs_unlink(struct file *filp) { + const struct cred *saved_cred; int err = 0; struct dentry *dir, *dentry = filp->f_path.dentry; struct mnt_idmap *idmap = file_mnt_idmap(filp); + saved_cred = override_creds(filp->f_cred); err = mnt_want_write(filp->f_path.mnt); if (err) - return err; + goto out_revert; dir = dget_parent(dentry); dentry = start_removing_dentry(dir, dentry); @@ -1025,7 +1041,8 @@ int ksmbd_vfs_unlink(struct file *filp) out: dput(dir); mnt_drop_write(filp->f_path.mnt); - +out_revert: + revert_creds(saved_cred); return err; } @@ -1140,7 +1157,7 @@ int __ksmbd_vfs_kern_path(struct ksmbd_work *work, char *filepath, retry: err = ksmbd_vfs_path_lookup(share_conf, filepath, flags, path, for_remove); - if (!err || !caseless) + if (!err || err != -ENOENT || !caseless) return err; path_len = strlen(filepath); @@ -1242,15 +1259,30 @@ struct dentry *ksmbd_vfs_kern_path_create(struct ksmbd_work *work, unsigned int flags, struct path *path) { - char *abs_name; + struct ksmbd_share_config *share_conf = work->tcon->share_conf; + struct qstr last; struct dentry *dent; + int err; + + /* resolve the name beneath the share root so ".." cannot escape */ + CLASS(filename_kernel, filename)(name); + + err = vfs_path_parent_lookup(filename, flags | LOOKUP_BENEATH, + path, &last, &share_conf->vfs_path); + if (err) + return ERR_PTR(err); - abs_name = convert_to_unix_name(work->tcon->share_conf, name); - if (!abs_name) - return ERR_PTR(-ENOMEM); + err = mnt_want_write(path->mnt); + if (err) { + path_put(path); + return ERR_PTR(err); + } - dent = start_creating_path(AT_FDCWD, abs_name, path, flags); - kfree(abs_name); + dent = start_creating_noperm(path->dentry, &last); + if (IS_ERR(dent)) { + mnt_drop_write(path->mnt); + path_put(path); + } return dent; } @@ -1881,3 +1913,117 @@ int ksmbd_vfs_inherit_posix_acl(struct mnt_idmap *idmap, posix_acl_release(acls); return rc; } + +void ksmbd_vfs_update_compressed_fattr(struct dentry *dentry, __le32 *fattr) +{ + int rc; + struct file_kattr fa = { .flags_valid = true }; + + rc = vfs_fileattr_get(dentry, &fa); + if (rc == -ENOIOCTLCMD) + *fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; + if (rc) + return; + + if (fa.flags & FS_COMPR_FL) + *fattr |= FILE_ATTRIBUTE_COMPRESSED_LE; + else + *fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; +} + +int ksmbd_vfs_get_compression(struct ksmbd_file *fp, u16 *fmt) +{ + struct file_kattr fa = { .flags_valid = true }; + int rc; + + rc = vfs_fileattr_get(fp->filp->f_path.dentry, &fa); + if (rc == -ENOIOCTLCMD) { + *fmt = COMPRESSION_FORMAT_NONE; + rc = 0; + goto out; + } + if (rc) + goto out; + + if (fa.flags & FS_COMPR_FL) + *fmt = COMPRESSION_FORMAT_LZNT1; + else + *fmt = COMPRESSION_FORMAT_NONE; + +out: + return rc; +} + +int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u16 fmt) +{ + const struct cred *saved_cred = NULL; + struct file_kattr fa; + struct dentry *dentry = fp->filp->f_path.dentry; + struct mnt_idmap *idmap = file_mnt_idmap(fp->filp); + u32 flags; + __le32 old_fattr; + int rc; + + if (!(fp->daccess & FILE_WRITE_DATA_LE)) { + rc = -EACCES; + goto out; + } + + saved_cred = override_creds(fp->filp->f_cred); + rc = vfs_fileattr_get(dentry, &fa); + if (rc) + goto out; + + flags = fa.flags; + if (fmt == COMPRESSION_FORMAT_NONE) { + flags &= ~FS_COMPR_FL; + } else if (fmt == COMPRESSION_FORMAT_DEFAULT || + fmt == COMPRESSION_FORMAT_LZNT1) { + flags |= FS_COMPR_FL; + } else { + rc = -EINVAL; + goto out; + } + + if (flags != fa.flags) { + fileattr_fill_flags(&fa, flags); + rc = mnt_want_write_file(fp->filp); + if (rc) + goto out; + + rc = vfs_fileattr_set(idmap, dentry, &fa); + mnt_drop_write_file(fp->filp); + if (rc) + goto out; + } + + old_fattr = fp->f_ci->m_fattr; + if (fmt == COMPRESSION_FORMAT_NONE) + fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; + else + fp->f_ci->m_fattr |= FILE_ATTRIBUTE_COMPRESSED_LE; + + if (fp->f_ci->m_fattr != old_fattr && + test_share_config_flag(work->tcon->share_conf, + KSMBD_SHARE_FLAG_STORE_DOS_ATTRS)) { + struct xattr_dos_attrib da; + + rc = ksmbd_vfs_get_dos_attrib_xattr(idmap, dentry, &da); + if (rc <= 0) { + rc = 0; + goto out; + } + + da.attr = le32_to_cpu(fp->f_ci->m_fattr); + rc = ksmbd_vfs_set_dos_attrib_xattr(idmap, + &fp->filp->f_path, + &da, true); + if (rc) + rc = 0; + } + +out: + if (saved_cred) + revert_creds(saved_cred); + return rc; +} diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 16ca29ee16e5..7b3d2f4fd985 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -168,4 +168,7 @@ int ksmbd_vfs_set_init_posix_acl(struct mnt_idmap *idmap, int ksmbd_vfs_inherit_posix_acl(struct mnt_idmap *idmap, const struct path *path, struct inode *parent_inode); +void ksmbd_vfs_update_compressed_fattr(struct dentry *dentry, __le32 *fattr); +int ksmbd_vfs_get_compression(struct ksmbd_file *fp, u16 *fmt); +int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u16 fmt); #endif /* __KSMBD_VFS_H__ */ diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index ba3355a6057a..8c556e46cc10 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -385,10 +385,14 @@ static void __ksmbd_inode_close(struct ksmbd_file *fp) up_write(&ci->m_lock); if (remove_stream_xattr) { + const struct cred *saved_cred; + + saved_cred = override_creds(filp->f_cred); err = ksmbd_vfs_remove_xattr(file_mnt_idmap(filp), &filp->f_path, fp->stream.name, true); + revert_creds(saved_cred); if (err) pr_err("remove xattr failed : %s\n", fp->stream.name); @@ -790,6 +794,7 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) INIT_LIST_HEAD(&fp->node); INIT_LIST_HEAD(&fp->lock_list); spin_lock_init(&fp->f_lock); + mutex_init(&fp->readdir_lock); atomic_set(&fp->refcount, 1); fp->filp = filp; diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index e6871266a94b..7d547e1a74f7 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -8,6 +8,7 @@ #include <linux/file.h> #include <linux/fs.h> +#include <linux/mutex.h> #include <linux/rwsem.h> #include <linux/spinlock.h> #include <linux/idr.h> @@ -113,6 +114,7 @@ struct ksmbd_file { /* if ls is happening on directory, below is valid*/ struct ksmbd_readdir_data readdir_data; + struct mutex readdir_lock; int dot_dotdot[2]; unsigned int f_state; bool reserve_lease_break; |
