From 1ef06004ed4bd6d3ed8c840d9d1a376b66d4935b Mon Sep 17 00:00:00 2001 From: Hem Parekh Date: Tue, 2 Jun 2026 16:56:46 -0700 Subject: ksmbd: fix out-of-bounds read in smb_check_perm_dacl() The permission-check ACE walk in smb_check_perm_dacl() validates the ACE header size and caps sid.num_subauth at SID_MAX_SUB_AUTHORITIES, but it never checks that ace->size is actually large enough to contain num_subauth sub-authorities before compare_sids() dereferences them. CIFS_SID_BASE_SIZE covers the SID header up to but excluding the sub_auth[] array, and offsetof(struct smb_ace, sid) is the ACE header, so the existing guards only guarantee the 8-byte SID base, i.e. zero sub-authorities. compare_sids() then reads ace->sid.sub_auth[i] for i < min(local_sid->num_subauth, ace->sid.num_subauth). The local comparison SIDs (sid_everyone, sid_unix_NFS_mode, and the id_to_sid() result) always have at least one sub-authority, and an attacker controls the ACE revision and authority bytes (which lie within the in-bounds SID base), so they can match one of those SIDs and force the sub_auth read. A crafted ACE with size == 16 and num_subauth >= 1 placed at the tail of the security descriptor therefore causes a heap out-of-bounds read of up to SID_MAX_SUB_AUTHORITIES * sizeof(__le32) bytes past the pntsd allocation. The security descriptor is loaded by ksmbd_vfs_get_sd_xattr() into a buffer sized exactly to the on-disk data (kzalloc(sd_size) in ndr_decode_v4_ntacl()), so the read lands past the allocation. The malformed descriptor can be stored verbatim via SMB2_SET_INFO (the DACL is not normalised before being written to the security.NTACL xattr) and the read fires on a subsequent SMB2_CREATE access check, making this reachable by an authenticated client on a share that uses ACL xattrs. Add the missing num_subauth-versus-ace_size check, mirroring the identical guards already present in the sibling parsers parse_dacl() and smb_inherit_dacl(). Fixes: d07b26f39246 ("ksmbd: require minimum ACE size in smb_check_perm_dacl()") Cc: stable@vger.kernel.org Signed-off-by: Hem Parekh Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smbacl.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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) || -- cgit v1.2.3 From 65b655f65c3ca1ab5d598d3832bb0ff531725858 Mon Sep 17 00:00:00 2001 From: Guangshuo Li Date: Fri, 5 Jun 2026 12:30:16 +0800 Subject: ksmbd: fix use-after-free in same_client_has_lease() same_client_has_lease() returns an opinfo pointer from ci->m_op_list after dropping ci->m_lock without taking a reference. smb_grant_oplock() then dereferences that pointer in copy_lease() and when checking breaking_cnt. A concurrent close can remove the old lease from ci->m_op_list and drop the last reference before the caller uses the returned pointer, leading to a use-after-free. Take a reference when same_client_has_lease() selects an existing lease, drop any previous match while scanning, and release the returned reference in smb_grant_oplock() after copying the lease state. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Signed-off-by: Guangshuo Li Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/oplock.c | 6 ++++++ 1 file changed, 6 insertions(+) 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; } } -- cgit v1.2.3 From d20d1c8ba5765d1d12eefc0aee6385ab3f240e1e Mon Sep 17 00:00:00 2001 From: Davide Ornaghi Date: Sat, 6 Jun 2026 16:11:04 +0900 Subject: ksmbd: fix UAF of struct file_lock in SMB2_LOCK deferred-lock cancellation When a blocking byte-range lock request is deferred in the FILE_LOCK_DEFERRED path, ksmbd registers the asynchronous work into the connection's async_requests list via setup_async_work(). The cancel callback smb2_remove_blocked_lock() holds a reference to the flock. If the lock waiter is subsequently woken up but the work state is no longer KSMBD_WORK_ACTIVE (e.g., due to a concurrent cancellation), the cleanup path calls locks_free_lock(flock) without dequeuing the work from the async_requests list. Concurrently, smb2_cancel() walks the list under conn->request_lock and invokes the cancel callback, which then dereferences the already freed 'flock'. This leads to a slab-use-after-free inside __wake_up_common. Fix this by restructuring the cleanup logic after the worker returns from ksmbd_vfs_posix_lock_wait(). Move list_del(&smb_lock->llist) and release_async_work(work) to the top of the cleanup block. This guarantees that the async work is completely dequeued and serialized under conn->request_lock before locks_free_lock(flock) is called, rendering the flock unreachable for any concurrent smb2_cancel(). Cc: stable@vger.kernel.org Signed-off-by: Davide Ornaghi Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 00e63debcbe9..82f93b191c6c 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7755,29 +7755,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; + + locks_free_lock(flock); - rsp->hdr.Status = - STATUS_RANGE_NOT_LOCKED; + 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); -- cgit v1.2.3 From 54bab9ba5a9f156ffa9324fcbe5a356fd0242f95 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 7 Jun 2026 20:15:51 +0900 Subject: ksmbd: prevent path traversal bypass by restricting caseless retry ksmbd_vfs_path_lookup() enforces LOOKUP_BENEATH to restrict path resolution within the share root. When a crafted path attempts to escape the share boundary using parent-directory components ('..'), vfs_path_parent_lookup() detects this and immediately fails, returning -EXDEV. However, a bug exists in __ksmbd_vfs_kern_path() under caseless mode. The function fails to intercept the -EXDEV error and erroneously falls through to the caseless retry logic, which is intended only for genuinely missing files. During this retry process, the path is reconstructed, leading to an unintended LOOKUP_BENEATH bypass that allows write-capable users to create zero-length files or directories outside the exported share. Fix this by ensuring that the execution only proceeds to the caseless lookup retry when the error is specifically -ENOENT. Any other errors, such as -EXDEV from a path traversal attempt, must be returned immediately. Cc: stable@vger.kernel.org Reported-by: Y s65 Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/vfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index cd1dbca0cffb..18c0a7c6b41b 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1140,7 +1140,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); -- cgit v1.2.3 From 31c09ce67a8177c2660844b129013e2934b13091 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Mon, 15 Jun 2026 20:27:20 +0900 Subject: smb: remove duplicate server/smbfsctl.h Rename the following places: - FSCTL_COPYCHUNK -> FSCTL_SRV_COPYCHUNK - FSCTL_COPYCHUNK_WRITE -> FSCTL_SRV_COPYCHUNK_WRITE - FSCTL_REQUEST_RESUME_KEY -> FSCTL_SRV_REQUEST_RESUME_KEY server/smbfsctl.h contains the following additional definitions compared to common/smbfsctl.h: - IO_REPARSE_TAG_LX_SYMLINK_LE - IO_REPARSE_TAG_AF_UNIX_LE - IO_REPARSE_TAG_LX_FIFO_LE - IO_REPARSE_TAG_LX_CHR_LE - IO_REPARSE_TAG_LX_BLK_LE Signed-off-by: ChenXiaoSong Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/common/smbfsctl.h | 6 ++++ fs/smb/server/smb2pdu.c | 12 +++---- fs/smb/server/smbfsctl.h | 91 ------------------------------------------------ 3 files changed, 12 insertions(+), 97 deletions(-) delete mode 100644 fs/smb/server/smbfsctl.h diff --git a/fs/smb/common/smbfsctl.h b/fs/smb/common/smbfsctl.h index 3253a18ecb5c..74e78bcb4618 100644 --- a/fs/smb/common/smbfsctl.h +++ b/fs/smb/common/smbfsctl.h @@ -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/smb2pdu.c b/fs/smb/server/smb2pdu.c index 82f93b191c6c..e3c53f22f5ec 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -17,7 +17,7 @@ #include #include "glob.h" -#include "smbfsctl.h" +#include "../common/smbfsctl.h" #include "oplock.h" #include "smbacl.h" @@ -7932,9 +7932,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; @@ -8408,7 +8408,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; @@ -8422,8 +8422,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"); 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 */ -- cgit v1.2.3 From 9d357903ec9b0da60cccc4d311f99763ba0924a2 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Mon, 8 Jun 2026 14:01:23 +0000 Subject: smb: move compression definitions into common/fscc.h These definitions will also be used by ksmbd, move them into common header file. Signed-off-by: ChenXiaoSong Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/client/smb1pdu.h | 5 ----- fs/smb/client/smb2pdu.h | 4 ---- fs/smb/common/fscc.h | 18 ++++++++++++++++++ fs/smb/server/smb2pdu.h | 3 --- 4 files changed, 18 insertions(+), 12 deletions(-) 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.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/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/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; -- cgit v1.2.3 From e48b687221f4e5215c9b8d2ae2c319d88a65809c Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Mon, 15 Jun 2026 20:29:07 +0900 Subject: smb/server: get compression file attribute on open Example: 1. server: chattr +c /export/file 2. client: lsattr /mnt/file --------c------------- /mnt/file Signed-off-by: ChenXiaoSong Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 5 ++++- fs/smb/server/vfs.c | 18 ++++++++++++++++++ fs/smb/server/vfs.h | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index e3c53f22f5ec..4b8e1a4f9e6b 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3604,7 +3604,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); diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 18c0a7c6b41b..b2403db13558 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -20,6 +20,7 @@ #include #include #include +#include #include "glob.h" #include "oplock.h" @@ -1881,3 +1882,20 @@ 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; +} diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 16ca29ee16e5..ac6dda173b75 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -168,4 +168,5 @@ 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); #endif /* __KSMBD_VFS_H__ */ -- cgit v1.2.3 From c91f4543ddecc49c96c175a90f51c991957f72e3 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Mon, 8 Jun 2026 14:01:25 +0000 Subject: smb/server: implement FSCTL_GET_COMPRESSION ioctl handler Example: 1. server: chattr +c /export/file 2. client: smbinfo getcompression /mnt/file Compression: 2 (LZNT1) Signed-off-by: ChenXiaoSong Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/common/smbfsctl.h | 2 +- fs/smb/server/smb2pdu.c | 28 ++++++++++++++++++++++++++++ fs/smb/server/vfs.c | 23 +++++++++++++++++++++++ fs/smb/server/vfs.h | 1 + 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/fs/smb/common/smbfsctl.h b/fs/smb/common/smbfsctl.h index 74e78bcb4618..9debdd709d1c 100644 --- a/fs/smb/common/smbfsctl.h +++ b/fs/smb/common/smbfsctl.h @@ -61,7 +61,7 @@ #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_GET_COMPRESSION 0x0009003C #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 */ diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 4b8e1a4f9e6b..975ca2ed84df 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8354,6 +8354,34 @@ 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_CREATE_OR_GET_OBJECT_ID: { struct file_object_buf_type1_ioctl_rsp *obj_buf; diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index b2403db13558..82cfd0b2a4cb 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1899,3 +1899,26 @@ void ksmbd_vfs_update_compressed_fattr(struct dentry *dentry, __le32 *fattr) 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; +} diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index ac6dda173b75..f6121207dbda 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -169,4 +169,5 @@ 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); #endif /* __KSMBD_VFS_H__ */ -- cgit v1.2.3 From 7eeb7b6772f02772b92c39146603005d0a1cb324 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Mon, 8 Jun 2026 14:01:26 +0000 Subject: smb/server: implement FSCTL_SET_COMPRESSION ioctl handler Example: 1. client: smbinfo setcompression no /mnt/file 2. client: smbinfo getcompression /mnt/file Compression: 0 (NONE) 3. client: smbinfo setcompression lznt1 /mnt/file 4. client: smbinfo getcompression /mnt/file Compression: 2 (LZNT1) 5. client: smbinfo setcompression default /mnt/file 6. client: smbinfo getcompression /mnt/file Compression: 2 (LZNT1) Signed-off-by: ChenXiaoSong Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/common/smbfsctl.h | 2 +- fs/smb/server/smb2pdu.c | 28 +++++++++++++++++++ fs/smb/server/vfs.c | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ fs/smb/server/vfs.h | 1 + 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/fs/smb/common/smbfsctl.h b/fs/smb/common/smbfsctl.h index 9debdd709d1c..d1fcb46a7cde 100644 --- a/fs/smb/common/smbfsctl.h +++ b/fs/smb/common/smbfsctl.h @@ -62,7 +62,7 @@ #define FSCTL_UNLOCK_VOLUME 0x0009001C #define FSCTL_IS_PATHNAME_VALID 0x0009002C /* BB add struct */ #define FSCTL_GET_COMPRESSION 0x0009003C -#define FSCTL_SET_COMPRESSION 0x0009C040 /* BB add struct */ +#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 */ diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 975ca2ed84df..4aed23a0fc0a 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8382,6 +8382,34 @@ int smb2_ioctl(struct ksmbd_work *work) 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; diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 82cfd0b2a4cb..e865ae91650a 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1922,3 +1922,73 @@ int ksmbd_vfs_get_compression(struct ksmbd_file *fp, u16 *fmt) out: return rc; } + +int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u16 fmt) +{ + 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; + } + + 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: + return rc; +} diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index f6121207dbda..7b3d2f4fd985 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -170,4 +170,5 @@ int ksmbd_vfs_inherit_posix_acl(struct mnt_idmap *idmap, 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__ */ -- cgit v1.2.3 From 5560f971645ce357ac0d2859376cf4205d44a5bf Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Mon, 8 Jun 2026 14:01:27 +0000 Subject: smb/server: get compression format in get_file_compression_info() I have added `filecompressioninfo` subcommand to `smbinfo` in cifs-utils.git. Example: 1. client: smbinfo setcompression lznt1 /mnt/file 2. client: smbinfo filecompressioninfo /mnt/file Compressed File Size: 104857600 Compression Format: 2 (LZNT1) Compression Unit Shift: 0 Chunk Shift: 0 Cluster Shift: 0 Signed-off-by: ChenXiaoSong Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 4aed23a0fc0a..6258a8a92c56 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5279,6 +5279,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, @@ -5286,9 +5287,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->CompressionFormat = cpu_to_le16(fmt); file_info->CompressionUnitShift = 0; file_info->ChunkShift = 0; file_info->ClusterShift = 0; -- cgit v1.2.3 From f4d556008fd443c05dc599ae3b1bd56c56c93741 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Tue, 9 Jun 2026 06:10:28 +0000 Subject: smb/server: fix incorrect file size in get_file_compression_info() Before this patch, we got the wrong file size: - client: touch /mnt/file - client: smbinfo setcompression default /mnt/file - client: dd if=/dev/zero of=/mnt/file bs=1 count=1000 - client: smbinfo filecompressioninfo /mnt/file Compressed File Size: 4096 Compression Format: 2 (LZNT1) After this patch, we get the correct file size: - client: smbinfo filecompressioninfo /mnt/file Compressed File Size: 1000 Compression Format: 2 (LZNT1) Note that the actual compressed file size must be got by other methods. For Btrfs, use the following command to get actual compressed file size: - server: compsize /export/file Processed 1 file, 0 regular extents (0 refs), 1 inline. Type Perc Disk Usage Uncompressed Referenced TOTAL 4% 47B 1000B 1000B zlib 4% 47B 1000B 1000B Signed-off-by: ChenXiaoSong Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 6258a8a92c56..327ec625d42b 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5292,7 +5292,7 @@ static int get_file_compression_info(struct smb2_query_info_rsp *rsp, return ret; file_info = (struct smb2_file_comp_info *)rsp->Buffer; - file_info->CompressedFileSize = cpu_to_le64(stat.blocks << 9); + 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; -- cgit v1.2.3 From ae7db37582ab441c33be148b1415116efab1025e Mon Sep 17 00:00:00 2001 From: Ethan Nelson-Moore Date: Tue, 9 Jun 2026 18:44:27 -0700 Subject: smb: server: remove code guarded by nonexistent config option A small piece of code in fs/smb/server/smb_common.c depends on CONFIG_SMB_INSECURE_SERVER, which has never been defined in the mainline kernel, but was present in old out-of-tree versions of ksmbd. Remove this dead code. Discovered while searching for CONFIG_* symbols referenced in code but not defined in any Kconfig file. Signed-off-by: Ethan Nelson-Moore Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb_common.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/fs/smb/server/smb_common.c b/fs/smb/server/smb_common.c index 741aabdfcef5..82de4fdfe446 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}, -- cgit v1.2.3 From 10f293a07f9e10e988b0ae44e2e99c631f5a68e0 Mon Sep 17 00:00:00 2001 From: Gil Portnoy Date: Wed, 10 Jun 2026 19:53:14 +0900 Subject: ksmbd: fix use-after-free of a deferred file_lock on SMB2_CLOSE then SMB2_CANCEL Commit f580d27e8928 ("ksmbd: fix use-after-free of a deferred file_lock on double SMB2_CANCEL") made smb2_cancel() skip a work whose state is KSMBD_WORK_CANCELLED, so its cancel_fn cannot be fired a second time. But KSMBD_WORK has three states (ACTIVE, CANCELLED, CLOSED), and the same freeing producer path is reached for CLOSED too: SMB2_CLOSE on the locking handle -> set_close_state_blocked_works() sets the deferred work's state to KSMBD_WORK_CLOSED and wakes the smb2_lock() worker. The worker takes the non-ACTIVE early-exit, locks_free_lock()s the file_lock and, because the state is not KSMBD_WORK_CANCELLED, takes the STATUS_RANGE_NOT_LOCKED branch with "goto out2" -- which, like the cancelled branch, skips release_async_work(). The work stays on conn->async_requests with a live cancel_fn = smb2_remove_blocked_lock pointing at the freed file_lock. A subsequent SMB2_CANCEL for the same AsyncId then passes the KSMBD_WORK_CANCELLED-only guard (its state is KSMBD_WORK_CLOSED), so smb2_cancel() fires cancel_fn again over the freed file_lock -- the same use-after-free fixed, via SMB2_CLOSE instead of a first SMB2_CANCEL: BUG: KASAN: slab-use-after-free in __locks_delete_block __locks_delete_block locks_delete_block ksmbd_vfs_posix_lock_unblock smb2_remove_blocked_lock smb2_cancel <- 2nd SMB2_CANCEL fires cancel_fn handle_ksmbd_work Allocated by ...: locks_alloc_lock <- smb2_lock Freed by ...: locks_free_lock <- smb2_lock (non-ACTIVE early-exit) ... cache file_lock_cache of size 192 Reproduced on mainline 7.1-rc7 (which already contains f580d27e8928) with KASAN by an authenticated SMB client; the double-SMB2_CANCEL control is silent on that kernel, so the splat is attributable to the CLOSE trigger. Only an ACTIVE deferred work may have its cancel_fn fired: both terminal states (CANCELLED and CLOSED) reach the smb2_lock() early-exit that frees the file_lock and skips release_async_work(). Guard on KSMBD_WORK_ACTIVE so any non-active work is skipped. Fixes: f580d27e8928 ("ksmbd: fix use-after-free of a deferred file_lock on double SMB2_CANCEL") Cc: stable@vger.kernel.org Signed-off-by: Gil Portnoy Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 327ec625d42b..06c92af288fc 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7349,14 +7349,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, -- cgit v1.2.3 From 44df157a1183a7f746caa970c169255da5ac61f8 Mon Sep 17 00:00:00 2001 From: Gil Portnoy Date: Tue, 9 Jun 2026 00:00:00 +0000 Subject: ksmbd: add a WRITE_DAC/WRITE_OWNER check to SMB2 SET_INFO SECURITY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit cc57232cae23 ("ksmbd: fix FSCTL permission bypass by adding a permission check for FSCTL_SET_SPARSE") added a fp->daccess gate to fsctl_set_sparse and noted that "similar handle-level checks exist in other functions but are missing here." The SMB2 SET_INFO SECURITY arm is one of the missing ones, and the most security-relevant: smb2_set_info_sec() calls set_info_sec() with no per-handle access check. set_info_sec() (fs/smb/server/smbacl.c) re-permissions the file: it rewrites owner/group/mode via notify_change(), rewrites the POSIX ACL via set_posix_acl(), and on KSMBD_SHARE_FLAG_ACL_XATTR shares removes and rewrites the Windows security descriptor via ksmbd_vfs_set_sd_xattr(). Every other persistent-mutation arm of the sibling handler smb2_set_info_file() checks fp->daccess first (FILE_WRITE_DATA / FILE_DELETE / FILE_WRITE_EA / FILE_WRITE_ATTRIBUTES); the SECURITY arm — which mutates the access control itself — is the only one with no gate. A client can therefore open a handle with FILE_WRITE_ATTRIBUTES only (no FILE_WRITE_DAC / FILE_WRITE_OWNER) and use SMB2_SET_INFO with InfoType SMB2_O_INFO_SECURITY to rewrite the file's DACL and owner, granting itself access the handle's daccess never carried. Unlike the FSCTL data arms this is a metadata/xattr operation, so there is no FMODE_WRITE VFS backstop — the missing fp->daccess check is the entire gate. Setting a security descriptor is the WRITE_DAC / WRITE_OWNER operation, so require at least one of those on the handle before re-permissioning the file. -EACCES is mapped to STATUS_ACCESS_DENIED by smb2_set_info(). Cc: stable@vger.kernel.org Signed-off-by: Gil Portnoy Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 06c92af288fc..a2c6ff7d8b89 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -6632,6 +6632,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); } -- cgit v1.2.3 From 3320ba068198adc144c89d6661b805acce01735b Mon Sep 17 00:00:00 2001 From: Gil Portnoy Date: Wed, 10 Jun 2026 20:07:04 +0900 Subject: ksmbd: add a permission check for FSCTL_SET_ZERO_DATA FSCTL_SET_ZERO_DATA in smb2_ioctl() destroys file data via ksmbd_vfs_zero_data() -> vfs_fallocate(PUNCH_HOLE/ZERO_RANGE) after checking only the share-level KSMBD_TREE_CONN_FLAG_WRITABLE, with no per-handle access check. A handle opened with only FILE_WRITE_ATTRIBUTES still yields an FMODE_WRITE filp (FILE_WRITE_ATTRIBUTES is part of FILE_WRITE_DESIRE_ACCESS_LE, so smb2_create_open_flags() opens it O_WRONLY), so the vfs_fallocate FMODE_WRITE check does not stop it; only the missing fp->daccess gate would. Reproduced on mainline 7.1-rc7 with KASAN by an authenticated SMB client: a FILE_WRITE_ATTRIBUTES-only handle zeroed 4096 bytes of file data it had no FILE_WRITE_DATA right to (6/6; a FILE_READ_DATA-only handle was correctly denied). This is the unfixed sibling of commit cc57232cae23 ("ksmbd: fix FSCTL permission bypass by adding a permission check for FSCTL_SET_SPARSE"). Because SET_ZERO_DATA writes data (not an attribute), require FILE_WRITE_DATA. Cc: stable@vger.kernel.org Signed-off-by: Gil Portnoy Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a2c6ff7d8b89..42669402637d 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8565,6 +8565,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) -- cgit v1.2.3 From 13f3942f2bf45856bb751faed2f0c4618f41ca20 Mon Sep 17 00:00:00 2001 From: Gil Portnoy Date: Wed, 10 Jun 2026 20:13:51 +0900 Subject: ksmbd: add per-handle permission check to FILE_LINK_INFORMATION The FILE_LINK_INFORMATION arm of smb2_set_info_file() calls smb2_create_link() with no per-handle fp->daccess check. On the ReplaceIfExists path smb2_create_link() unlinks an existing file at the target name (ksmbd_vfs_remove_file) and creates a hardlink (ksmbd_vfs_link); neither helper checks daccess. A handle opened with FILE_READ_DATA only (no FILE_DELETE, no FILE_WRITE_DATA) can therefore delete an arbitrary file in the share and plant a hardlink over its name. The sibling delete/move arms in the same switch already gate: FILE_RENAME_INFORMATION and FILE_DISPOSITION_INFORMATION both require FILE_DELETE_LE; FILE_FULL_EA_INFORMATION requires FILE_WRITE_EA_LE. Gate the link arm the same way as its closest analogue (rename), since it mutates the namespace and, on replace, deletes an existing entry. This is a sibling of commit cc57232cae23 ("ksmbd: fix FSCTL permission bypass by adding a permission check for FSCTL_SET_SPARSE"). Cc: stable@vger.kernel.org Signed-off-by: Gil Portnoy Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 42669402637d..f9106b35e63c 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -6575,6 +6575,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; -- cgit v1.2.3 From 0121b154147f5affd1039ca4f8d9a0fc194142f6 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 10 Jun 2026 18:44:55 +0900 Subject: smb: move LZ77 compression into common code Move the LZ77 codec in cifs.ko to smb/common/ so both the SMB client and ksmbd can use it. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/client/Makefile | 2 +- fs/smb/client/compress.c | 12 +- fs/smb/client/compress.h | 28 +-- fs/smb/client/compress/lz77.c | 335 --------------------------- fs/smb/client/compress/lz77.h | 43 ---- fs/smb/common/Makefile | 3 + fs/smb/common/compress/compress.h | 22 ++ fs/smb/common/compress/lz77.c | 461 ++++++++++++++++++++++++++++++++++++++ fs/smb/common/compress/lz77.h | 46 ++++ 9 files changed, 542 insertions(+), 410 deletions(-) delete mode 100644 fs/smb/client/compress/lz77.c delete mode 100644 fs/smb/client/compress/lz77.h create mode 100644 fs/smb/common/compress/compress.h create mode 100644 fs/smb/common/compress/lz77.c create mode 100644 fs/smb/common/compress/lz77.h 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/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 #include #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/compress/lz77.c b/fs/smb/client/compress/lz77.c deleted file mode 100644 index 7365d0f97396..000000000000 --- a/fs/smb/client/compress/lz77.c +++ /dev/null @@ -1,335 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -/* - * Copyright (C) 2024-2026, SUSE LLC - * - * Authors: Enzo Matsumiya - * - * Implementation of the LZ77 "plain" compression algorithm, as per MS-XCA spec. - */ -#include -#include -#include -#include - -#include "lz77.h" - -/* - * Compression parameters. - * - * LZ77_MATCH_MAX_DIST: Farthest back a match can be from current position (can be 1 - 8K). - * LZ77_HASH_LOG: - * LZ77_HASH_SIZE: ilog2 hash size (recommended to be 13 - 18, default 15 (hash size - * 32k)). - * LZ77_RSTEP_SIZE: Number of bytes to read from input buffer for hashing and initial - * match check (default 4 bytes, this effectivelly makes this the min - * match len). - * LZ77_MSTEP_SIZE: Number of bytes to extend-compare a found match (default 8 bytes). - * LZ77_SKIP_TRIGGER: ilog2 value for adaptive skipping, i.e. to progressively skip input - * bytes when we can't find matches. Default is 4. - * Higher values (>0) will decrease compression time, but will result - * in worse compression ratio. Lower values will give better - * compression ratio (more matches found), but will increase time. - */ -#define LZ77_MATCH_MAX_DIST SZ_8K -#define LZ77_HASH_LOG 15 -#define LZ77_HASH_SIZE (1 << LZ77_HASH_LOG) -#define LZ77_RSTEP_SIZE sizeof(u32) -#define LZ77_MSTEP_SIZE sizeof(u64) -#define LZ77_SKIP_TRIGGER 4 - -#define LZ77_PREFETCH(ptr) __builtin_prefetch((ptr), 0, 3) -#define LZ77_FLAG_MAX 32 - -static __always_inline u8 lz77_read8(const u8 *ptr) -{ - return get_unaligned(ptr); -} - -static __always_inline u32 lz77_read32(const u32 *ptr) -{ - return get_unaligned(ptr); -} - -static __always_inline u64 lz77_read64(const u64 *ptr) -{ - return get_unaligned(ptr); -} - -static __always_inline void lz77_write8(u8 *ptr, u8 v) -{ - put_unaligned(v, ptr); -} - -static __always_inline void lz77_write16(u16 *ptr, u16 v) -{ - put_unaligned_le16(v, ptr); -} - -static __always_inline void lz77_write32(u32 *ptr, u32 v) -{ - put_unaligned_le32(v, ptr); -} - -static __always_inline u32 lz77_match_len(const void *match, const void *cur, const void *end) -{ - const void *start = cur; - - /* Safe for a do/while because otherwise we wouldn't reach here from the main loop. */ - do { - const u64 diff = lz77_read64(cur) ^ lz77_read64(match); - - if (!diff) { - cur += LZ77_MSTEP_SIZE; - match += LZ77_MSTEP_SIZE; - - continue; - } - - /* This computes the number of common bytes in @diff. */ - cur += count_trailing_zeros(diff) >> 3; - - return (cur - start); - } while (likely(cur + LZ77_MSTEP_SIZE <= end)); - - /* Fallback to byte-by-byte comparison for last <8 bytes. */ - while (cur < end && lz77_read8(cur) == lz77_read8(match)) { - cur++; - match++; - } - - return (cur - start); -} - -/** - * lz77_encode_match() - Match encoding. - * @dst: compressed buffer - * @nib: pointer to an address in @dst - * @dist: match distance - * @len: match length - * - * Assumes all args were previously checked. - * - * Return: @dst advanced to new position - * - * Ref: MS-XCA 2.3.4 "Plain LZ77 Compression Algorithm Details" - "Processing" - */ -static __always_inline void *lz77_encode_match(void *dst, void **nib, u16 dist, u32 len) -{ - len -= 3; - dist--; - dist <<= 3; - - if (len < 7) { - lz77_write16(dst, dist + len); - - return dst + sizeof(u16); - } - - dist |= 7; - lz77_write16(dst, dist); - dst += sizeof(u16); - len -= 7; - - if (!*nib) { - lz77_write8(dst, umin(len, 15)); - *nib = dst; - dst++; - } else { - u8 *b = *nib; - - lz77_write8(b, *b | umin(len, 15) << 4); - *nib = NULL; - } - - if (len < 15) - return dst; - - len -= 15; - if (len < 255) { - lz77_write8(dst, len); - - return dst + 1; - } - - lz77_write8(dst, 0xff); - dst++; - len += 7 + 15; - if (len <= 0xffff) { - lz77_write16(dst, len); - - return dst + sizeof(u16); - } - - lz77_write16(dst, 0); - dst += sizeof(u16); - lz77_write32(dst, len); - - return dst + sizeof(u32); -} - -/** - * lz77_encode_literals() - Literals encoding. - * @start: where to start copying literals (uncompressed buffer) - * @end: when to stop copying (uncompressed buffer) - * @dst: compressed buffer - * @f: pointer to current flag value - * @fc: pointer to current flag count - * @fp: pointer to current flag address - * - * Batch copy literals from @start to @dst, updating flag values accordingly. - * Assumes all args were previously checked. - * - * Return: @dst advanced to new position - * - * MS-XCA 2.3.4 "Plain LZ77 Compression Algorithm Details" - "Processing" - */ -static __always_inline void *lz77_encode_literals(const void *start, const void *end, void *dst, - long *f, u32 *fc, void **fp) -{ - if (start >= end) - return dst; - - do { - const u32 len = umin(end - start, LZ77_FLAG_MAX - *fc); - - memcpy(dst, start, len); - - dst += len; - start += len; - - *f <<= len; - *fc += len; - if (*fc == LZ77_FLAG_MAX) { - lz77_write32(*fp, *f); - *fc = 0; - *fp = dst; - dst += sizeof(u32); - } - } while (start < end); - - return dst; -} - -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) -{ - const void *srcp, *rlim, *end, *anchor; - u32 *htable, hash, flag_count = 0; - void *dstp, *nib, *flag_pos; - long flag = 0; - - /* This is probably a bug, so throw a warning. */ - if (WARN_ON_ONCE(*dlen < lz77_compressed_alloc_size(slen))) - return -EINVAL; - - srcp = anchor = src; - end = srcp + slen; /* absolute end */ - rlim = end - LZ77_MSTEP_SIZE; /* read limit (for lz77_match_len()) */ - dstp = dst; - flag_pos = dstp; - dstp += sizeof(u32); - nib = NULL; - - htable = kvcalloc(LZ77_HASH_SIZE, sizeof(*htable), GFP_KERNEL); - if (!htable) - return -ENOMEM; - - LZ77_PREFETCH(srcp + LZ77_RSTEP_SIZE); - - /* - * Adjust @srcp so we don't get a false positive match on first iteration. - * Then prepare hash for first loop iteration (don't advance @srcp again). - */ - hash = lz77_hash(lz77_read32(srcp++)); - htable[hash] = 0; - hash = lz77_hash(lz77_read32(srcp)); - - /* - * Main loop. - * - * @dlen is >= 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. - */ - do { - const void *match, *next = srcp; - u32 len, step = 1, skip = 1U << LZ77_SKIP_TRIGGER; - - /* Match finding (hot path -- don't change the read/check/write order). */ - do { - const u32 cur_hash = hash; - - srcp = next; - next += step; - - /* - * Adaptive skipping. - * - * Increment @step every (1 << LZ77_SKIP_TRIGGER, 16 in our case) bytes - * without a match. - * Reset to 1 when a match is found. - */ - step = (skip++ >> LZ77_SKIP_TRIGGER); - if (unlikely(next > rlim)) - goto out; - - hash = lz77_hash(lz77_read32(next)); - match = src + htable[cur_hash]; - htable[cur_hash] = srcp - src; - } while (likely(match + LZ77_MATCH_MAX_DIST < srcp) || - lz77_read32(match) != lz77_read32(srcp)); - - /* - * Match found. Warm/cold path; begin parsing @srcp and writing to @dstp: - * - flush literals - * - compute match length (*) - * - encode match - * - * (*) Current minimum match length is defined by the memory read size above, so - * here we already know that we have 4 matching bytes, but it's just faster to - * redundantly compute it again in lz77_match_len() than to adjust pointers/len. - */ - dstp = lz77_encode_literals(anchor, srcp, dstp, &flag, &flag_count, &flag_pos); - len = lz77_match_len(match, srcp, end); - dstp = lz77_encode_match(dstp, &nib, srcp - match, len); - srcp += len; - anchor = srcp; - - LZ77_PREFETCH(srcp); - - flag = (flag << 1) | 1; - flag_count++; - if (flag_count == LZ77_FLAG_MAX) { - lz77_write32(flag_pos, flag); - flag_count = 0; - flag_pos = dstp; - dstp += sizeof(u32); - } - - if (unlikely(srcp > rlim)) - break; - - /* Prepare for next loop. */ - hash = lz77_hash(lz77_read32(srcp)); - } while (srcp < end); -out: - dstp = lz77_encode_literals(anchor, end, dstp, &flag, &flag_count, &flag_pos); - - flag_count = LZ77_FLAG_MAX - flag_count; - flag <<= flag_count; - flag |= (1UL << flag_count) - 1; - lz77_write32(flag_pos, flag); - - *dlen = dstp - dst; - kvfree(htable); - - if (*dlen < slen) - return 0; - - return -EMSGSIZE; -} diff --git a/fs/smb/client/compress/lz77.h b/fs/smb/client/compress/lz77.h deleted file mode 100644 index 4e570846aefa..000000000000 --- a/fs/smb/client/compress/lz77.h +++ /dev/null @@ -1,43 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-only */ -/* - * Copyright (C) 2024-2026, SUSE LLC - * - * Authors: Enzo Matsumiya - * - * Implementation of the LZ77 "plain" compression algorithm, as per MS-XCA spec. - */ -#ifndef _SMB_COMPRESS_LZ77_H -#define _SMB_COMPRESS_LZ77_H - -#include - -/** - * lz77_compressed_alloc_size() - Compute compressed buffer size. - * @size: uncompressed (src) size - * - * Compute allocation size for the compressed buffer based on uncompressed size. - * Accounts for metadata and overprovision for the worst case scenario. - * - * LZ77 metadata is a 4-byte flag that is written: - * - on dst begin (pos 0) - * - every 32 literals or matches - * - on end-of-stream (possibly, if last write was another flag) - * - * 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. - * - * Return: required (*) allocation size for compressed buffer. - * - * (*) checked once in the beginning of lz77_compress() - */ -static __always_inline u32 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); -#endif /* _SMB_COMPRESS_LZ77_H */ diff --git a/fs/smb/common/Makefile b/fs/smb/common/Makefile index 9e0730a385fb..bd188d36fb6b 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/lz77.o diff --git a/fs/smb/common/compress/compress.h b/fs/smb/common/compress/compress.h new file mode 100644 index 000000000000..b504cd38b128 --- /dev/null +++ b/fs/smb/common/compress/compress.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (C) 2026 Namjae Jeon + */ +#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; +} + +#endif /* _COMMON_SMB_COMPRESS_H */ diff --git a/fs/smb/common/compress/lz77.c b/fs/smb/common/compress/lz77.c new file mode 100644 index 000000000000..9216d973d876 --- /dev/null +++ b/fs/smb/common/compress/lz77.c @@ -0,0 +1,461 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2024-2026, SUSE LLC + * Copyright (C) 2026 Namjae Jeon + * + * Authors: Enzo Matsumiya + * Namjae Jeon + * + * Implementation of the LZ77 "plain" compression algorithm, as per MS-XCA spec. + */ +#include +#include +#include +#include +#include +#include + +#include "lz77.h" + +/* + * Compression parameters. + * + * LZ77_MATCH_MAX_DIST: Farthest back a match can be from current position (can be 1 - 8K). + * LZ77_HASH_LOG: + * LZ77_HASH_SIZE: ilog2 hash size (recommended to be 13 - 18, default 15 (hash size + * 32k)). + * LZ77_RSTEP_SIZE: Number of bytes to read from input buffer for hashing and initial + * match check (default 4 bytes, this effectivelly makes this the min + * match len). + * LZ77_MSTEP_SIZE: Number of bytes to extend-compare a found match (default 8 bytes). + * LZ77_SKIP_TRIGGER: ilog2 value for adaptive skipping, i.e. to progressively skip input + * bytes when we can't find matches. Default is 4. + * Higher values (>0) will decrease compression time, but will result + * in worse compression ratio. Lower values will give better + * compression ratio (more matches found), but will increase time. + */ +#define LZ77_MATCH_MAX_DIST SZ_8K +#define LZ77_HASH_LOG 15 +#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 + +#define LZ77_PREFETCH(ptr) __builtin_prefetch((ptr), 0, 3) +#define LZ77_FLAG_MAX 32 + +static __always_inline u8 lz77_read8(const u8 *ptr) +{ + return get_unaligned(ptr); +} + +static __always_inline u32 lz77_read32(const u32 *ptr) +{ + return get_unaligned(ptr); +} + +static __always_inline u64 lz77_read64(const u64 *ptr) +{ + return get_unaligned(ptr); +} + +static __always_inline void lz77_write8(u8 *ptr, u8 v) +{ + put_unaligned(v, ptr); +} + +static __always_inline void lz77_write16(u16 *ptr, u16 v) +{ + put_unaligned_le16(v, ptr); +} + +static __always_inline void lz77_write32(u32 *ptr, u32 v) +{ + put_unaligned_le32(v, ptr); +} + +static __always_inline u32 lz77_match_len(const void *match, const void *cur, const void *end) +{ + const void *start = cur; + + /* Safe for a do/while because otherwise we wouldn't reach here from the main loop. */ + do { + const u64 diff = lz77_read64(cur) ^ lz77_read64(match); + + if (!diff) { + cur += LZ77_MSTEP_SIZE; + match += LZ77_MSTEP_SIZE; + + continue; + } + + /* This computes the number of common bytes in @diff. */ + cur += count_trailing_zeros(diff) >> 3; + + return (cur - start); + } while (likely(cur + LZ77_MSTEP_SIZE <= end)); + + /* Fallback to byte-by-byte comparison for last <8 bytes. */ + while (cur < end && lz77_read8(cur) == lz77_read8(match)) { + cur++; + match++; + } + + return (cur - start); +} + +/** + * lz77_encode_match() - Match encoding. + * @dst: compressed buffer + * @nib: pointer to an address in @dst + * @dist: match distance + * @len: match length + * + * Assumes all args were previously checked. + * + * Return: @dst advanced to new position + * + * Ref: MS-XCA 2.3.4 "Plain LZ77 Compression Algorithm Details" - "Processing" + */ +static __always_inline void *lz77_encode_match(void *dst, void **nib, u16 dist, u32 len) +{ + len -= 3; + dist--; + dist <<= 3; + + if (len < 7) { + lz77_write16(dst, dist + len); + + return dst + sizeof(u16); + } + + dist |= 7; + lz77_write16(dst, dist); + dst += sizeof(u16); + len -= 7; + + if (!*nib) { + lz77_write8(dst, umin(len, 15)); + *nib = dst; + dst++; + } else { + u8 *b = *nib; + + lz77_write8(b, *b | umin(len, 15) << 4); + *nib = NULL; + } + + if (len < 15) + return dst; + + len -= 15; + if (len < 255) { + lz77_write8(dst, len); + + return dst + 1; + } + + lz77_write8(dst, 0xff); + dst++; + len += 7 + 15; + if (len <= 0xffff) { + lz77_write16(dst, len); + + return dst + sizeof(u16); + } + + lz77_write16(dst, 0); + dst += sizeof(u16); + lz77_write32(dst, len); + + return dst + sizeof(u32); +} + +/** + * lz77_encode_literals() - Literals encoding. + * @start: where to start copying literals (uncompressed buffer) + * @end: when to stop copying (uncompressed buffer) + * @dst: compressed buffer + * @f: pointer to current flag value + * @fc: pointer to current flag count + * @fp: pointer to current flag address + * + * Batch copy literals from @start to @dst, updating flag values accordingly. + * Assumes all args were previously checked. + * + * Return: @dst advanced to new position + * + * MS-XCA 2.3.4 "Plain LZ77 Compression Algorithm Details" - "Processing" + */ +static __always_inline void *lz77_encode_literals(const void *start, const void *end, void *dst, + long *f, u32 *fc, void **fp) +{ + if (start >= end) + return dst; + + do { + const u32 len = umin(end - start, LZ77_FLAG_MAX - *fc); + + memcpy(dst, start, len); + + dst += len; + start += len; + + *f <<= len; + *fc += len; + if (*fc == LZ77_FLAG_MAX) { + lz77_write32(*fp, *f); + *fc = 0; + *fp = dst; + dst += sizeof(u32); + } + } while (start < end); + + return dst; +} + +static __always_inline u32 lz77_hash(const u32 v) +{ + return ((v ^ 0x9E3779B9) * 0x85EBCA6B) >> (32 - LZ77_HASH_LOG); +} + +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; + void *dstp, *nib, *flag_pos; + long flag = 0; + + /* This is probably a bug, so throw a warning. */ + if (WARN_ON_ONCE(*dlen < smb_lz77_compressed_alloc_size(slen))) + return -EINVAL; + + srcp = src; + anchor = src; + end = srcp + slen; /* absolute end */ + rlim = end - LZ77_MSTEP_SIZE; /* read limit (for lz77_match_len()) */ + dstp = dst; + flag_pos = dstp; + dstp += sizeof(u32); + nib = NULL; + + htable = kvcalloc(LZ77_HASH_SIZE, sizeof(*htable), GFP_KERNEL); + if (!htable) + return -ENOMEM; + + LZ77_PREFETCH(srcp + LZ77_RSTEP_SIZE); + + /* + * Adjust @srcp so we don't get a false positive match on first iteration. + * Then prepare hash for first loop iteration (don't advance @srcp again). + */ + hash = lz77_hash(lz77_read32(srcp++)); + htable[hash] = 0; + hash = lz77_hash(lz77_read32(srcp)); + + /* + * Main loop. + * + * @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. + */ + do { + const void *match, *next = srcp; + u32 len, step = 1, skip = 1U << LZ77_SKIP_TRIGGER; + + /* Match finding (hot path -- don't change the read/check/write order). */ + do { + const u32 cur_hash = hash; + + srcp = next; + next += step; + + /* + * Adaptive skipping. + * + * Increment @step every (1 << LZ77_SKIP_TRIGGER, 16 in our case) bytes + * without a match. + * Reset to 1 when a match is found. + */ + step = (skip++ >> LZ77_SKIP_TRIGGER); + if (unlikely(next > rlim)) + goto out; + + hash = lz77_hash(lz77_read32(next)); + match = src + htable[cur_hash]; + htable[cur_hash] = srcp - src; + } while (likely(match + LZ77_MATCH_MAX_DIST < srcp) || + lz77_read32(match) != lz77_read32(srcp)); + + /* + * Match found. Warm/cold path; begin parsing @srcp and writing to @dstp: + * - flush literals + * - compute match length (*) + * - encode match + * + * (*) Current minimum match length is defined by the memory read size above, so + * here we already know that we have 4 matching bytes, but it's just faster to + * redundantly compute it again in lz77_match_len() than to adjust pointers/len. + */ + dstp = lz77_encode_literals(anchor, srcp, dstp, &flag, &flag_count, &flag_pos); + len = lz77_match_len(match, srcp, end); + dstp = lz77_encode_match(dstp, &nib, srcp - match, len); + srcp += len; + anchor = srcp; + + LZ77_PREFETCH(srcp); + + flag = (flag << 1) | 1; + flag_count++; + if (flag_count == LZ77_FLAG_MAX) { + lz77_write32(flag_pos, flag); + flag_count = 0; + flag_pos = dstp; + dstp += sizeof(u32); + } + + if (unlikely(srcp > rlim)) + break; + + /* Prepare for next loop. */ + hash = lz77_hash(lz77_read32(srcp)); + } while (srcp < end); +out: + dstp = lz77_encode_literals(anchor, end, dstp, &flag, &flag_count, &flag_pos); + + flag_count = LZ77_FLAG_MAX - flag_count; + flag <<= flag_count; + flag |= (1UL << flag_count) - 1; + lz77_write32(flag_pos, flag); + + *dlen = dstp - dst; + kvfree(htable); + + if (*dlen < slen) + return 0; + + 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/common/compress/lz77.h b/fs/smb/common/compress/lz77.h new file mode 100644 index 000000000000..e032c0f1b48d --- /dev/null +++ b/fs/smb/common/compress/lz77.h @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (C) 2024-2026, SUSE LLC + * + * Authors: Enzo Matsumiya + * + * Implementation of the LZ77 "plain" compression algorithm, as per MS-XCA spec. + */ +#ifndef _SMB_COMPRESS_LZ77_H +#define _SMB_COMPRESS_LZ77_H + +#include + +/** + * smb_lz77_compressed_alloc_size() - Compute compressed buffer size. + * @size: uncompressed (src) size + * + * Compute allocation size for the compressed buffer based on uncompressed size. + * Accounts for metadata and overprovision for the worst case scenario. + * + * LZ77 metadata is a 4-byte flag that is written: + * - on dst begin (pos 0) + * - every 32 literals or matches + * - on end-of-stream (possibly, if last write was another flag) + * + * 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 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 smb_lz77_compress() + */ +static __always_inline u32 smb_lz77_compressed_alloc_size(const u32 size) +{ + return size + (size >> 3) + 8; +} + +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 */ -- cgit v1.2.3 From 6234f50105fbb0fffe878b6b493325d67822e43b Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 10 Jun 2026 18:45:10 +0900 Subject: smb: add common SMB2 compression transform helpers Implement common validation, compression and decompression for SMB2 compression transforms. Support unchained LZ77 and chained NONE, LZ77 and Pattern_V1 payloads. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/common/Makefile | 2 +- fs/smb/common/compress/compress.c | 399 ++++++++++++++++++++++++++++++++++++++ fs/smb/common/compress/compress.h | 6 + fs/smb/common/smb2pdu.h | 11 +- 4 files changed, 410 insertions(+), 8 deletions(-) create mode 100644 fs/smb/common/compress/compress.c diff --git a/fs/smb/common/Makefile b/fs/smb/common/Makefile index bd188d36fb6b..f2c6e09d4e77 100644 --- a/fs/smb/common/Makefile +++ b/fs/smb/common/Makefile @@ -6,4 +6,4 @@ obj-$(CONFIG_SMBFS) += cifs_md4.o obj-$(CONFIG_SMBFS) += smb_compress.o -smb_compress-y := compress/lz77.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 + */ +#include +#include +#include +#include + +#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 index b504cd38b128..7ace3bf4b664 100644 --- a/fs/smb/common/compress/compress.h +++ b/fs/smb/common/compress/compress.h @@ -19,4 +19,10 @@ static __always_inline bool smb_compress_alg_valid(__le16 alg, bool 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/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; /* -- cgit v1.2.3 From 17e12b1d14134d88d39a7a366391280e6775c2d6 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 10 Jun 2026 23:40:58 +0900 Subject: cifs: negotiate chained SMB2 compression capabilities Advertise LZ77 and Pattern_V1 with chained transform support in the SMB 3.1.1 compression negotiate context. Validate the server's returned algorithm list and flags, then retain the negotiated capabilities for a future compressed transform receive implementation. This patch only negotiates capabilities. It does not request compressed READ responses or add a compressed transform receive path. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/client/cifsglob.h | 2 ++ fs/smb/client/smb2pdu.c | 44 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/fs/smb/client/cifsglob.h b/fs/smb/client/cifsglob.h index 82e0adc1dabd..a3ac4aa4bbc6 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/smb2pdu.c b/fs/smb/client/smb2pdu.c index fbeb2156ddb6..a79d8e6091d0 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; } -- cgit v1.2.3 From a08de24c2b8568a26b560cda411284295decb3ba Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 10 Jun 2026 18:46:00 +0900 Subject: ksmbd: negotiate and decode SMB2 compression Parse the SMB 3.1.1 compression capabilities context and negotiate LZ77 with optional chained Pattern_V1 support. Advertise compression on tree connections and decode compressed requests before normal SMB dispatch. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/Makefile | 2 +- fs/smb/server/compress.c | 77 ++++++++++++++++++++++++++++++++ fs/smb/server/compress.h | 15 +++++++ fs/smb/server/connection.c | 12 +++++ fs/smb/server/connection.h | 3 ++ fs/smb/server/smb2pdu.c | 109 ++++++++++++++++++++++++++++++++++++++++++--- fs/smb/server/smb_common.c | 5 --- 7 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 fs/smb/server/compress.c create mode 100644 fs/smb/server/compress.h 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..7c9f8a6cceb8 --- /dev/null +++ b/fs/smb/server/compress.c @@ -0,0 +1,77 @@ +// 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 + */ +#include + +#include "compress.h" +#include "smb_common.h" + +/** + * 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; +} diff --git a/fs/smb/server/compress.h b/fs/smb/server/compress.h new file mode 100644 index 000000000000..49b36d931aac --- /dev/null +++ b/fs/smb/server/compress.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * SMB2 compression support for ksmbd. + * + * Copyright (C) 2026 Namjae Jeon + */ +#ifndef __KSMBD_COMPRESS_H__ +#define __KSMBD_COMPRESS_H__ + +#include "connection.h" +#include "../common/compress/compress.h" + +int ksmbd_decompress_request(struct ksmbd_conn *conn); + +#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/smb2pdu.c b/fs/smb/server/smb2pdu.c index f9106b35e63c..25742eb3f483 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -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) { @@ -804,6 +805,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 +890,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 +1006,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 +1106,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 +1152,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 +2152,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) diff --git a/fs/smb/server/smb_common.c b/fs/smb/server/smb_common.c index 82de4fdfe446..7de73223189a 100644 --- a/fs/smb/server/smb_common.c +++ b/fs/smb/server/smb_common.c @@ -185,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) -- cgit v1.2.3 From 08f641e2e2e092cbda5ce7f7b5280e327e46823d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 10 Jun 2026 18:46:10 +0900 Subject: ksmbd: compress SMB2 READ responses Handle SMB2_READFLAG_REQUEST_COMPRESSED for non-RDMA reads. Flatten the response iov, emit chained or unchained LZ77 transforms when compression is beneficial, and retain the generated buffer until the work item is released. Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/compress.c | 132 +++++++++++++++++++++++++++++++++++++++++++++ fs/smb/server/compress.h | 1 + fs/smb/server/ksmbd_work.c | 1 + fs/smb/server/ksmbd_work.h | 4 ++ fs/smb/server/server.c | 10 ++++ fs/smb/server/smb2pdu.c | 9 ++++ 6 files changed, 157 insertions(+) diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c index 7c9f8a6cceb8..f8cf515b9c30 100644 --- a/fs/smb/server/compress.c +++ b/fs/smb/server/compress.c @@ -10,6 +10,9 @@ #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 @@ -75,3 +78,132 @@ int ksmbd_decompress_request(struct ksmbd_conn *conn) 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 index 49b36d931aac..663c6f44f09b 100644 --- a/fs/smb/server/compress.h +++ b/fs/smb/server/compress.h @@ -11,5 +11,6 @@ #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/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/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 25742eb3f483..ae451e77689c 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7115,6 +7115,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; -- cgit v1.2.3 From 609ca17d869d04ba249e32cdcbf13c0b1c66f43c Mon Sep 17 00:00:00 2001 From: Gil Portnoy Date: Thu, 11 Jun 2026 22:59:19 +0900 Subject: ksmbd: reject non-VALID session in compound request branch smb2_check_user_session() takes a shortcut for any operation that is not the first in a COMPOUND request: it reuses work->sess (the session bound by the first operation) and validates only the SessionId, then returns "valid". It never re-checks work->sess->state == SMB2_SESSION_VALID, and a SessionId of 0xFFFFFFFFFFFFFFFF (ULLONG_MAX, the MS-SMB2 related-operation value) skips even the id comparison. The standalone path (ksmbd_session_lookup_all() plus the SESSION_SETUP state machine) does enforce the VALID state; the compound branch bypasses all of it. A SESSION_SETUP carrying only an NTLM Type-1 (NtLmNegotiate) blob publishes a fresh SMB2_SESSION_IN_PROGRESS session whose sess->user is still NULL (->user is assigned later, by ntlm_authenticate()). Used as operation 1 of a COMPOUND with operation 2 = TREE_CONNECT (related, SessionId=ULLONG_MAX, \\host\IPC$), the tree-connect then runs on that IN_PROGRESS session and reaches ksmbd_ipc_tree_connect_request(), which dereferences user_name(sess->user) with sess->user == NULL (transport_ipc.c:687/701/704) -> remote NULL-pointer dereference and a kernel Oops that wedges the ksmbd worker for all clients. Reject any non-first compound operation that lands on a session which is not SMB2_SESSION_VALID, mirroring the validity the standalone lookup path enforces. SESSION_SETUP itself legitimately runs on an IN_PROGRESS session, but it is never carried as a non-first compound operation, so multi-leg authentication is unaffected by this check. Fixes: 5005bcb42191 ("ksmbd: validate session id and tree id in the compound request") Cc: stable@vger.kernel.org Signed-off-by: Gil Portnoy Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index ae451e77689c..9efafa56c03e 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -615,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; } -- cgit v1.2.3 From 20c8442dc1003f9f7bb522d3dcd81d09ea59a79e Mon Sep 17 00:00:00 2001 From: Gil Portnoy Date: Thu, 11 Jun 2026 22:59:51 +0900 Subject: ksmbd: enforce FILE_READ_ATTRIBUTES on SMB_FIND_FILE_POSIX_INFORMATION find_file_posix_info() in smb2_query_info() returns file metadata (owner uid, group gid, mode, inode, size, allocation size, hard-link count and all four timestamps) but performs no per-handle access check. Every sibling query handler gates on the handle's granted access first -- get_file_basic_info(), get_file_all_info(), get_file_network_open_info() and get_file_attribute_tag_info() all reject a handle lacking FILE_READ_ATTRIBUTES_LE with -EACCES. The POSIX handler is gated only by the connection-scoped tcon->posix_extensions flag, which is not a per-handle authorization, so a handle opened with only FILE_WRITE_DATA is correctly denied FileBasicInformation yet is allowed the strict-superset POSIX info. Mirror the FILE_READ_ATTRIBUTES_LE gate the sibling info handlers already use. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Cc: stable@vger.kernel.org Signed-off-by: Gil Portnoy Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 9efafa56c03e..4e0087931795 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5437,6 +5437,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) -- cgit v1.2.3 From 388e4139db27a9e3612c9d356b826f5b1ff6a9e3 Mon Sep 17 00:00:00 2001 From: Gil Portnoy Date: Fri, 12 Jun 2026 07:15:38 +0900 Subject: ksmbd: add permission checks for FSCTL_DUPLICATE_EXTENTS_TO_FILE The FSCTL_DUPLICATE_EXTENTS_TO_FILE arm of smb2_ioctl() overwrites the destination file's data via vfs_clone_file_range() with neither the share-level KSMBD_TREE_CONN_FLAG_WRITABLE check nor a per-handle fp->daccess check that the other write-bearing arms carry. A client can overwrite destination data on a read-only share, or from a handle opened with only FILE_WRITE_ATTRIBUTES (which still yields an FMODE_WRITE filp). FILE_WRITE_ATTRIBUTES-only destination handle overwrote the file's data via the clone. Add both checks, matching the FSCTL_SET_SPARSE permission fix; require FILE_WRITE_DATA since this writes data. Cc: stable@vger.kernel.org Signed-off-by: Gil Portnoy Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 4e0087931795..32f568cea16a 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8766,6 +8766,17 @@ 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; + } + src_off = le64_to_cpu(dup_ext->SourceFileOffset); dst_off = le64_to_cpu(dup_ext->TargetFileOffset); length = le64_to_cpu(dup_ext->ByteCount); -- cgit v1.2.3 From be6d26bf27499977c746abc163659915082348d8 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 12 Jun 2026 08:00:00 +0900 Subject: ksmbd: serialize QUERY_DIRECTORY requests per file smb2_query_dir() stores a pointer to its stack-allocated private data in the ksmbd_file readdir_data. Concurrent QUERY_DIRECTORY requests using the same file handle can overwrite this pointer while an iterate_dir() callback is still using it, resulting in a stack use-after-free. Add a per-file mutex and hold it while accessing the shared directory enumeration state. The lock covers scan restart, dot entry state, readdir_data setup and iteration, and response construction. This prevents another request from replacing readdir_data.private before the current request has finished using it and also serializes the shared file position. Cc: stable@vger.kernel.org Reported-by: zdi-disclosures@trendmicro.com # ZDI-CAN-30527 Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 4 ++++ fs/smb/server/vfs_cache.c | 1 + fs/smb/server/vfs_cache.h | 2 ++ 3 files changed, 7 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 32f568cea16a..96dcb78cfb92 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4569,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); @@ -4673,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); @@ -4680,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: diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index ba3355a6057a..4daccc77f9ec 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -790,6 +790,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 #include +#include #include #include #include @@ -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; -- cgit v1.2.3 From 52e2f21911158ec961cd5aae19c56460db382af0 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 13 Jun 2026 22:00:00 +0900 Subject: ksmbd: use opener credentials for delete-on-close Delete-on-close can be completed by deferred or durable handle teardown, where no request work is available. Both the base-file unlink and the ADS xattr removal consequently run with the ksmbd worker credentials and can bypass filesystem permission checks. Run both operations with the credentials captured in struct file when the handle was opened. This preserves the authenticated user's fsuid, fsgid, supplementary groups and capability restrictions at final close. Cc: stable@vger.kernel.org Reported-by: Musaab Khan Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/vfs.c | 7 +++++-- fs/smb/server/vfs_cache.c | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index e865ae91650a..c002cc4b1c02 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1001,13 +1001,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); @@ -1026,7 +1028,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; } diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 4daccc77f9ec..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); -- cgit v1.2.3 From b383bcad3d2fe634b26efbce53e22bbb5753a520 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 13 Jun 2026 22:00:01 +0900 Subject: ksmbd: run set info with opener credentials SMB2 SET_INFO handlers call path-based VFS helpers after checking the access mask granted to the SMB handle. Those helpers perform their owner, inode permission and LSM checks using the current ksmbd worker credentials. Run the complete SET_INFO dispatch with the credentials captured when the handle was opened. This also removes the separate security information credential setup and keeps all SET_INFO classes under one credential scope. Direct override_creds() is used because it can nest with the request credential overrides already used by rename and link helpers. Cc: stable@vger.kernel.org Reported-by: Musaab Khan Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 96dcb78cfb92..6d3f975d582f 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -6762,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; @@ -6803,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"); @@ -6810,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; -- cgit v1.2.3 From cedff600f1642aa982178503552f0d007bc829c8 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 13 Jun 2026 22:00:02 +0900 Subject: ksmbd: require source read access for duplicate extents FSCTL_DUPLICATE_EXTENTS_TO_FILE passes the source file directly to vfs_clone_file_range() or vfs_copy_file_range() without checking the SMB access mask granted to the source handle. A handle opened with attribute access can consequently be used to copy file contents into an attacker-readable destination. Require FILE_READ_DATA on the source handle before either VFS operation, matching other ksmbd data-copy paths. Cc: stable@vger.kernel.org Reported-by: Musaab Khan Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 6d3f975d582f..fcb1bcd5de95 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8778,6 +8778,10 @@ int smb2_ioctl(struct ksmbd_work *work) 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); -- cgit v1.2.3 From baa5e094886fffa7e6272edcb5e08be5ce28262c Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 13 Jun 2026 22:00:03 +0900 Subject: ksmbd: use opener credentials for ADS I/O Alternate data streams are stored as xattrs. Unlike regular file I/O, their read and write paths therefore call VFS xattr helpers which recheck inode permissions and LSM policy using the current task credentials. Run ADS I/O with the credentials captured when the SMB handle was opened. Cc: stable@vger.kernel.org Reported-by: Musaab Khan Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/vfs.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index c002cc4b1c02..04a4bd0f492b 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -248,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; @@ -382,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; @@ -402,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, @@ -410,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) @@ -435,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 -- cgit v1.2.3 From c6394bcaf254c5baf9aff43376020be5db6d3316 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 13 Jun 2026 22:00:04 +0900 Subject: ksmbd: use opener credentials for FSCTL mutations SET_SPARSE, SET_ZERO_DATA and SET_COMPRESSION operate on an open SMB handle but call VFS xattr, fallocate or fileattr helpers with the current ksmbd worker credentials. Those helpers can revalidate inode permissions, ownership and LSM policy independently of the SMB handle access mask. Run each operation with the credentials captured in the target file when the handle was opened. Keep credential handling local to these single-file FSCTLs rather than applying session credentials to the complete IOCTL handler, which also contains handle-less and multi-handle operations. Cc: stable@vger.kernel.org Reported-by: Musaab Khan Signed-off-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/smb2pdu.c | 3 +++ fs/smb/server/vfs.c | 24 +++++++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index fcb1bcd5de95..c1c6c1a64f60 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8387,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, @@ -8395,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; } diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 04a4bd0f492b..fe376453a519 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -918,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, @@ -1935,6 +1941,7 @@ out: 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); @@ -1947,6 +1954,7 @@ int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u1 goto out; } + saved_cred = override_creds(fp->filp->f_cred); rc = vfs_fileattr_get(dentry, &fa); if (rc) goto out; @@ -2000,5 +2008,7 @@ int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u1 } out: + if (saved_cred) + revert_creds(saved_cred); return rc; } -- cgit v1.2.3 From 1c8951963d8ed357f70f59e0ad4ddce2199d2016 Mon Sep 17 00:00:00 2001 From: Davide Ornaghi Date: Mon, 15 Jun 2026 20:35:01 +0900 Subject: ksmbd: fix path resolution in ksmbd_vfs_kern_path_create The SMB2 open lookup is rooted at the share with LOOKUP_BENEATH, but the create/mkdir/hardlink sink is not: ksmbd_vfs_kern_path_create() builds an absolute path with convert_to_unix_name() and resolves it from AT_FDCWD via start_creating_path(), so a ".." component is walked from the real filesystem root and escapes the export. An authenticated client races a missing path component so the rooted open lookup returns -ENOENT (taking the create branch) while the same component is present (a directory) when the create walk runs; the create then resolves ".." out of the share. Root the create walk at the share like the lookup and rename paths already are: resolve the parent with vfs_path_parent_lookup(..., LOOKUP_BENEATH, &share_conf->vfs_path) and create the final component with start_creating_noperm(). convert_to_unix_name() then has no callers and is removed. Fixes: 265fd1991c1d ("ksmbd: use LOOKUP_BENEATH to prevent the out of share access") Cc: stable@vger.kernel.org Signed-off-by: Davide Ornaghi Acked-by: Namjae Jeon Signed-off-by: Steve French --- fs/smb/server/misc.c | 33 --------------------------------- fs/smb/server/misc.h | 1 - fs/smb/server/vfs.c | 27 +++++++++++++++++++++------ 3 files changed, 21 insertions(+), 40 deletions(-) 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/vfs.c b/fs/smb/server/vfs.c index fe376453a519..74b0307cb100 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1259,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; - abs_name = convert_to_unix_name(work->tcon->share_conf, name); - if (!abs_name) - return ERR_PTR(-ENOMEM); + /* resolve the name beneath the share root so ".." cannot escape */ + CLASS(filename_kernel, filename)(name); - dent = start_creating_path(AT_FDCWD, abs_name, path, flags); - kfree(abs_name); + err = vfs_path_parent_lookup(filename, flags | LOOKUP_BENEATH, + path, &last, &share_conf->vfs_path); + if (err) + return ERR_PTR(err); + + err = mnt_want_write(path->mnt); + if (err) { + path_put(path); + return ERR_PTR(err); + } + + dent = start_creating_noperm(path->dentry, &last); + if (IS_ERR(dent)) { + mnt_drop_write(path->mnt); + path_put(path); + } return dent; } -- cgit v1.2.3