From 7019a11f79dc408f2b47b1027240e7f198784c9a Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 4 Jul 2026 18:21:33 +0900 Subject: [PATCH 001/142] ksmbd: reject SMB3.1.1 binding with mismatched cipher SMB3.1.1 multichannel connections belonging to the same session must use the same negotiated encryption cipher. ksmbd validates the dialect and client GUID during session binding, but does not compare the cipher negotiated by the new connection with the cipher used by the existing session channels. This allows a channel negotiated with AES-128-CCM to bind to a session using AES-128-GCM. Compare the new connection's cipher with an existing session channel and return STATUS_INVALID_PARAMETER when they differ. This fixes smb2.session.bind_negative_smb3encGtoCs. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 76f63f9adc72..b30bdeaa8eb7 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1964,6 +1964,21 @@ int smb2_sess_setup(struct ksmbd_work *work) goto out_err; } + if (conn->dialect == SMB311_PROT_ID) { + struct channel *chann; + unsigned long index; + + down_read(&sess->chann_lock); + xa_for_each(&sess->ksmbd_chann_list, index, chann) { + if (conn->cipher_type != chann->conn->cipher_type) + rc = -EINVAL; + break; + } + up_read(&sess->chann_lock); + if (rc) + goto out_err; + } + if (!(req->hdr.Flags & SMB2_FLAGS_SIGNED)) { rc = -EINVAL; goto out_err; From df35438ba9d5687335a405418772b6cd30383687 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 5 Jul 2026 00:13:18 +0900 Subject: [PATCH 002/142] ksmbd: validate SMB2 write offsets An SMB2 WRITE request with a negative offset returns -EINVAL directly from smb2_write(). This bypasses the common error response path, leaving the client waiting until the request times out. ksmbd also allows nonempty writes at or beyond MAXFILESIZE as defined by [MS-FSA]. Writes beyond the limit must fail with STATUS_INVALID_PARAMETER. Writes ending at the limit fail with STATUS_DISK_FULL, while a zero-length write remains valid. Route negative offsets through the common error path and validate the end offset of nonempty writes against MAXFILESIZE. This fixes smb2.rw.invalid. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index b30bdeaa8eb7..42660f7ab063 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -64,6 +64,9 @@ static void __wbuf(struct ksmbd_work *work, void **req, void **rsp) /* Windows reports automatic write-time updates at roughly 15 ms resolution. */ #define KSMBD_WRITE_TIME_RESOLUTION (15ULL * 10000) +/* MAXFILESIZE in [MS-FSA] 2.1.5.3 Server Requests a Write. */ +#define SMB2_MAX_FILE_SIZE 0xfffffff0000ULL + /** * check_session_id() - check for valid session id in smb header * @conn: connection instance @@ -7685,8 +7688,10 @@ int smb2_write(struct ksmbd_work *work) } offset = le64_to_cpu(req->Offset); - if (offset < 0) - return -EINVAL; + if (offset < 0) { + err = -EINVAL; + goto out; + } length = le32_to_cpu(req->Length); if (req->Channel == SMB2_CHANNEL_RDMA_V1 || @@ -7700,6 +7705,19 @@ int smb2_write(struct ksmbd_work *work) length = le32_to_cpu(req->RemainingBytes); } + if (length) { + u64 end = (u64)offset + length; + + if (end > SMB2_MAX_FILE_SIZE) { + err = -EINVAL; + goto out; + } + if (end == SMB2_MAX_FILE_SIZE) { + err = -EFBIG; + goto out; + } + } + if (is_rdma_channel == true) { unsigned int ch_offset = le16_to_cpu(req->WriteChannelInfoOffset); From cc2f133e80eb2c4a04bfa77a2f207749fe2f516a Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 5 Jul 2026 09:34:03 +0900 Subject: [PATCH 003/142] ksmbd: fix maximum allowed access checks The DACL permission check looks for an ACE matching the current user and falls back to the Everyone ACE. It does not consider an Authenticated Users ACE, even though an authenticated session is a member of that well-known group. As a result, opening a file whose access is granted through S-1-5-11 can incorrectly fail with STATUS_ACCESS_DENIED. Treat an Authenticated Users ACE as a fallback entry alongside Everyone. The maximal access calculation also combines access masks from every ACE, regardless of whether its SID applies to the current user. This can grant rights belonging to an unrelated principal. Process only ACEs applying to the user, Everyone, or Authenticated Users, and accumulate allowed and denied masks in ACL order. Preserve explicitly requested access bits so they are validated against the resulting maximal mask. When ACCESS_SYSTEM_SECURITY is denied, report STATUS_PRIVILEGE_NOT_HELD instead of the generic STATUS_ACCESS_DENIED. Access to the system ACL requires a security privilege that ksmbd does not grant. For regular files, include FILE_EXECUTE in maximal access when the client requested GENERIC_EXECUTE and the DACL grants the complete file-read set. Keep a direct FILE_EXECUTE request subject to the explicit DACL bit. This matches the POSIX file ACL mapping without broadening specific execute requests. Do not replace rights from an applicable NT ACE with a POSIX ACL entry. The POSIX ACL is only a fallback when no user, Everyone, or Authenticated Users ACE applies; otherwise it can incorrectly broaden the stored DACL. This fixes smb2.maximum_allowed.maximum_allowed. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 9 ++++- fs/smb/server/smbacl.c | 85 ++++++++++++++++++++++++++--------------- fs/smb/server/smbacl.h | 2 +- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 42660f7ab063..f965355fdf85 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3619,6 +3619,7 @@ int smb2_open(struct ksmbd_work *work) if (file_present && !(req->CreateOptions & FILE_DELETE_ON_CLOSE_LE)) { rc = smb_check_perm_dacl(conn, &path, &daccess, + req->DesiredAccess, sess->user->uid); if (rc) goto err_out; @@ -4191,8 +4192,12 @@ int smb2_open(struct ksmbd_work *work) rsp->hdr.Status = STATUS_INVALID_PARAMETER; else if (rc == -EOPNOTSUPP) rsp->hdr.Status = STATUS_NOT_SUPPORTED; - else if (rc == -EACCES || rc == -ESTALE || rc == -EXDEV) - rsp->hdr.Status = STATUS_ACCESS_DENIED; + else if (rc == -EACCES || rc == -ESTALE || rc == -EXDEV) { + if (req->DesiredAccess & FILE_ACCESS_SYSTEM_SECURITY_LE) + rsp->hdr.Status = STATUS_PRIVILEGE_NOT_HELD; + else + rsp->hdr.Status = STATUS_ACCESS_DENIED; + } else if (rc == -ENOENT) rsp->hdr.Status = STATUS_OBJECT_NAME_INVALID; else if (rc == -EPERM) diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index c13f07a09ab8..053763815332 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -1432,7 +1432,7 @@ bool smb_inherit_flags(int flags, bool is_dir) } int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, - __le32 *pdaccess, int uid) + __le32 *pdaccess, __le32 raw_daccess, int uid) { struct mnt_idmap *idmap = mnt_idmap(path->mnt); struct smb_ntsd *pntsd = NULL; @@ -1442,10 +1442,11 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, unsigned int dacl_offset; size_t dacl_struct_end; struct smb_sid sid; - int granted = le32_to_cpu(*pdaccess & ~FILE_MAXIMAL_ACCESS_LE); + int requested = le32_to_cpu(*pdaccess & ~FILE_MAXIMAL_ACCESS_LE); + int granted = requested; struct smb_ace *ace; int i, found = 0; - unsigned int access_bits = 0; + unsigned int access_bits = 0, denied = 0; struct smb_ace *others_ace = NULL; struct posix_acl_entry *pa_entry; unsigned int sid_type = SIDOWNER; @@ -1479,10 +1480,13 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, goto err_out; } - if (*pdaccess & FILE_MAXIMAL_ACCESS_LE) { - granted = READ_CONTROL | WRITE_DAC | FILE_READ_ATTRIBUTES | - DELETE; + if (!uid) + sid_type = SIDUNIX_USER; + id_to_sid(uid, sid_type, &sid); + if (*pdaccess & FILE_MAXIMAL_ACCESS_LE) { + access_bits = READ_CONTROL | WRITE_DAC | FILE_READ_ATTRIBUTES | + DELETE; ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); aces_size = acl_size - sizeof(struct smb_acl); for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) { @@ -1495,15 +1499,41 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, CIFS_SID_BASE_SIZE) break; aces_size -= ace_size; - granted |= le32_to_cpu(ace->access_req); + + 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 (ace->flags & INHERIT_ONLY_ACE || + (compare_sids(&sid, &ace->sid) && + compare_sids(&sid_unix_NFS_mode, &ace->sid) && + compare_sids(&sid_everyone, &ace->sid) && + compare_sids(&sid_authusers, &ace->sid))) + goto next_ace; + + switch (ace->type) { + case ACCESS_ALLOWED_ACE_TYPE: + access_bits |= le32_to_cpu(ace->access_req); + break; + case ACCESS_DENIED_ACE_TYPE: + case ACCESS_DENIED_CALLBACK_ACE_TYPE: + denied |= ~access_bits & + le32_to_cpu(ace->access_req); + break; + } +next_ace: ace = (struct smb_ace *)((char *)ace + le16_to_cpu(ace->size)); } + access_bits &= ~denied; + if ((raw_daccess & FILE_GENERIC_EXECUTE_LE) && + S_ISREG(d_inode(path->dentry)->i_mode) && + (access_bits & GENERIC_READ_FLAGS) == GENERIC_READ_FLAGS) + access_bits |= FILE_EXECUTE; + granted = requested | access_bits; } - if (!uid) - sid_type = SIDUNIX_USER; - id_to_sid(uid, sid_type, &sid); - ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); aces_size = acl_size - sizeof(struct smb_acl); for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) { @@ -1527,25 +1557,16 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, found = 1; break; } - if (!compare_sids(&sid_everyone, &ace->sid)) + if (!compare_sids(&sid_everyone, &ace->sid) || + !compare_sids(&sid_authusers, &ace->sid)) others_ace = ace; ace = (struct smb_ace *)((char *)ace + le16_to_cpu(ace->size)); } - if (*pdaccess & FILE_MAXIMAL_ACCESS_LE && found) { - granted = READ_CONTROL | WRITE_DAC | FILE_READ_ATTRIBUTES | - DELETE; - - granted |= le32_to_cpu(ace->access_req); - - if (!pdacl->num_aces) - granted = GENERIC_ALL_FLAGS; - } - if (IS_ENABLED(CONFIG_FS_POSIX_ACL)) { posix_acls = get_inode_acl(d_inode(path->dentry), ACL_TYPE_ACCESS); - if (!IS_ERR_OR_NULL(posix_acls) && !found) { + if (!IS_ERR_OR_NULL(posix_acls) && !found && !others_ace) { unsigned int id = -1; pa_entry = posix_acls->a_entries; @@ -1583,14 +1604,16 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, } } - switch (ace->type) { - case ACCESS_ALLOWED_ACE_TYPE: - access_bits = le32_to_cpu(ace->access_req); - break; - case ACCESS_DENIED_ACE_TYPE: - case ACCESS_DENIED_CALLBACK_ACE_TYPE: - access_bits = le32_to_cpu(~ace->access_req); - break; + if (!(*pdaccess & FILE_MAXIMAL_ACCESS_LE)) { + switch (ace->type) { + case ACCESS_ALLOWED_ACE_TYPE: + access_bits = le32_to_cpu(ace->access_req); + break; + case ACCESS_DENIED_ACE_TYPE: + case ACCESS_DENIED_CALLBACK_ACE_TYPE: + access_bits = le32_to_cpu(~ace->access_req); + break; + } } check_access_bits: diff --git a/fs/smb/server/smbacl.h b/fs/smb/server/smbacl.h index ab21ba2cd4df..bf0312bfa040 100644 --- a/fs/smb/server/smbacl.h +++ b/fs/smb/server/smbacl.h @@ -95,7 +95,7 @@ bool smb_inherit_flags(int flags, bool is_dir); int smb_inherit_dacl(struct ksmbd_conn *conn, const struct path *path, unsigned int uid, unsigned int gid); int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, - __le32 *pdaccess, int uid); + __le32 *pdaccess, __le32 raw_daccess, int uid); int set_info_sec(struct ksmbd_conn *conn, struct ksmbd_tree_connect *tcon, const struct path *path, struct smb_ntsd *pntsd, int ntsd_len, bool type_check, bool get_write); From e5f42cb7577221080e4db0498d71e6db7e67f1a5 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 5 Jul 2026 15:20:06 +0900 Subject: [PATCH 004/142] ksmbd: support access-based directory enumeration SMB shares can advertise access-based directory enumeration. ksmbd does not currently provide a share option or filter inaccessible directory entries. Add a hide-unreadable share flag and advertise SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM when it is enabled. During QUERY_DIRECTORY, omit entries unless the connected user has FILE_READ_DATA, FILE_READ_EA, and FILE_READ_ATTRIBUTES access according to the Windows ACL. Keep the existing implicit access allowances for normal CREATE permission checks while using strict access-mask matching for directory enumeration. Signed-off-by: Namjae Jeon --- fs/smb/server/ksmbd_netlink.h | 1 + fs/smb/server/smb2pdu.c | 24 +++++++++++++++++++++++- fs/smb/server/smbacl.c | 13 ++++++++++--- fs/smb/server/smbacl.h | 3 ++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/fs/smb/server/ksmbd_netlink.h b/fs/smb/server/ksmbd_netlink.h index 8ccd57fd904b..c9e1b0b689d7 100644 --- a/fs/smb/server/ksmbd_netlink.h +++ b/fs/smb/server/ksmbd_netlink.h @@ -377,6 +377,7 @@ enum KSMBD_TREE_CONN_STATUS { #define KSMBD_SHARE_FLAG_UPDATE BIT(14) #define KSMBD_SHARE_FLAG_CROSSMNT BIT(15) #define KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY BIT(16) +#define KSMBD_SHARE_FLAG_HIDE_UNREADABLE BIT(17) /* * Tree connect request flags. diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f965355fdf85..e79531e120a3 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2346,6 +2346,10 @@ int smb2_tree_connect(struct ksmbd_work *work) if (conn->dialect == SMB311_PROT_ID && conn->compress_algorithm != SMB3_COMPRESS_NONE) rsp->ShareFlags |= cpu_to_le32(SMB2_SHAREFLAG_COMPRESS_DATA); + if (share && test_share_config_flag(share, + KSMBD_SHARE_FLAG_HIDE_UNREADABLE)) + rsp->ShareFlags |= + cpu_to_le32(SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM); rc = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_tree_connect_rsp)); if (rc) @@ -3620,7 +3624,7 @@ int smb2_open(struct ksmbd_work *work) if (file_present && !(req->CreateOptions & FILE_DELETE_ON_CLOSE_LE)) { rc = smb_check_perm_dacl(conn, &path, &daccess, req->DesiredAccess, - sess->user->uid); + sess->user->uid, false); if (rc) goto err_out; } @@ -4595,6 +4599,7 @@ static int process_query_dir_entries(struct smb2_query_dir_private *priv) for (i = 0; i < priv->d_info->num_entry; i++) { struct dentry *dent; + struct path path; if (dentry_name(priv->d_info, priv->info_level)) return -EINVAL; @@ -4617,6 +4622,23 @@ static int process_query_dir_entries(struct smb2_query_dir_private *priv) continue; } + if (test_share_config_flag(priv->work->tcon->share_conf, + KSMBD_SHARE_FLAG_HIDE_UNREADABLE)) { + __le32 daccess = FILE_READ_DATA_LE | FILE_READ_EA_LE | + FILE_READ_ATTRIBUTES_LE; + + path.mnt = priv->dir_fp->filp->f_path.mnt; + path.dentry = dent; + rc = smb_check_perm_dacl(priv->work->conn, &path, + &daccess, daccess, + priv->work->sess->user->uid, + true); + if (rc) { + dput(dent); + continue; + } + } + ksmbd_kstat.kstat = &kstat; if (priv->info_level != FILE_NAMES_INFORMATION) { rc = ksmbd_vfs_fill_dentry_attrs(priv->work, diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index 053763815332..88bf7b97042f 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -1432,7 +1432,8 @@ bool smb_inherit_flags(int flags, bool is_dir) } int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, - __le32 *pdaccess, __le32 raw_daccess, int uid) + __le32 *pdaccess, __le32 raw_daccess, int uid, + bool strict) { struct mnt_idmap *idmap = mnt_idmap(path->mnt); struct smb_ntsd *pntsd = NULL; @@ -1617,8 +1618,14 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, } check_access_bits: - if (granted & - ~(access_bits | FILE_READ_ATTRIBUTES | READ_CONTROL | WRITE_DAC | DELETE)) { + if (strict) { + access_bits &= granted; + } else { + access_bits |= FILE_READ_ATTRIBUTES | READ_CONTROL | + WRITE_DAC | DELETE; + } + + if (granted & ~access_bits) { ksmbd_debug(SMB, "Access denied with winACL, granted : %x, access_req : %x\n", granted, le32_to_cpu(ace->access_req)); rc = -EACCES; diff --git a/fs/smb/server/smbacl.h b/fs/smb/server/smbacl.h index bf0312bfa040..01810c16cc04 100644 --- a/fs/smb/server/smbacl.h +++ b/fs/smb/server/smbacl.h @@ -95,7 +95,8 @@ bool smb_inherit_flags(int flags, bool is_dir); int smb_inherit_dacl(struct ksmbd_conn *conn, const struct path *path, unsigned int uid, unsigned int gid); int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, - __le32 *pdaccess, __le32 raw_daccess, int uid); + __le32 *pdaccess, __le32 raw_daccess, int uid, + bool strict); int set_info_sec(struct ksmbd_conn *conn, struct ksmbd_tree_connect *tcon, const struct path *path, struct smb_ntsd *pntsd, int ntsd_len, bool type_check, bool get_write); From 5d47ebb2795d0dab7bf718ae6665ffdaa7bb880c Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 5 Jul 2026 15:32:06 +0900 Subject: [PATCH 005/142] ksmbd: honor owner rights ACEs in maximal access The SMB2 create maximal-access context is currently calculated from POSIX mode bits when the client does not request MAXIMUM_ALLOWED. This overwrites the access granted by a stored Windows DACL. Calculate the create-context result with the DACL permission checker. Recognize the S-1-3-4 Owner Rights SID as applying to the object owner and process its allow and deny ACEs in ACL order. When an Owner Rights ACE is present, do not add the owner implicit READ_CONTROL and WRITE_DAC rights. The Owner Rights ACE replaces those implicit grants as required by Windows access-check semantics. Without an Owner Rights ACE, preserve the existing implicit owner grants, including FILE_READ_ATTRIBUTES and DELETE. This fixes smb2.acls.OWNER-RIGHTS and its deny variants without regressing smb2.acls.GENERIC. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 8 ++++++++ fs/smb/server/smbacl.c | 21 ++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index e79531e120a3..155a0b93ed58 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3627,6 +3627,14 @@ int smb2_open(struct ksmbd_work *work) sess->user->uid, false); if (rc) goto err_out; + + if (maximal_access_ctxt) { + maximal_access = FILE_MAXIMAL_ACCESS_LE; + rc = smb_check_perm_dacl(conn, &path, &maximal_access, + 0, sess->user->uid, false); + if (rc) + goto err_out; + } } if (daccess & FILE_MAXIMAL_ACCESS_LE) { diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index 88bf7b97042f..b5db6dcfbaa4 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -27,6 +27,9 @@ static const struct smb_sid creator_owner = { /* security id for everyone/world system group */ static const struct smb_sid creator_group = { 1, 1, {0, 0, 0, 0, 0, 3}, {cpu_to_le32(1)} }; +/* security id for owner rights */ +static const struct smb_sid sid_owner_rights = { + 1, 1, {0, 0, 0, 0, 0, 3}, {cpu_to_le32(4)} }; /* security id for everyone/world system group */ static const struct smb_sid sid_everyone = { @@ -1452,6 +1455,8 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, struct posix_acl_entry *pa_entry; unsigned int sid_type = SIDOWNER; unsigned short ace_size; + bool is_owner, owner_rights = false; + vfsuid_t vfsuid; ksmbd_debug(SMB, "check permission using windows acl\n"); pntsd_size = ksmbd_vfs_get_sd_xattr(conn, idmap, @@ -1484,10 +1489,10 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, if (!uid) sid_type = SIDUNIX_USER; id_to_sid(uid, sid_type, &sid); + vfsuid = i_uid_into_vfsuid(idmap, d_inode(path->dentry)); + is_owner = uid == from_kuid(&init_user_ns, vfsuid_into_kuid(vfsuid)); if (*pdaccess & FILE_MAXIMAL_ACCESS_LE) { - access_bits = READ_CONTROL | WRITE_DAC | FILE_READ_ATTRIBUTES | - DELETE; ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); aces_size = acl_size - sizeof(struct smb_acl); for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) { @@ -1507,11 +1512,18 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, sizeof(__le32) * ace->sid.num_subauth) break; + if (!compare_sids(&sid_owner_rights, &ace->sid)) { + owner_rights = true; + if (!is_owner) + goto next_ace; + } + if (ace->flags & INHERIT_ONLY_ACE || (compare_sids(&sid, &ace->sid) && compare_sids(&sid_unix_NFS_mode, &ace->sid) && compare_sids(&sid_everyone, &ace->sid) && - compare_sids(&sid_authusers, &ace->sid))) + compare_sids(&sid_authusers, &ace->sid) && + compare_sids(&sid_owner_rights, &ace->sid))) goto next_ace; switch (ace->type) { @@ -1527,6 +1539,9 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, next_ace: ace = (struct smb_ace *)((char *)ace + le16_to_cpu(ace->size)); } + if (is_owner && !owner_rights) + access_bits |= READ_CONTROL | WRITE_DAC | + FILE_READ_ATTRIBUTES | DELETE; access_bits &= ~denied; if ((raw_daccess & FILE_GENERIC_EXECUTE_LE) && S_ISREG(d_inode(path->dentry)->i_mode) && From 497dbc5999a52efd55e079589b166e5c18a20fe3 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 5 Jul 2026 15:43:29 +0900 Subject: [PATCH 006/142] ksmbd: reject delete-on-close for read-only files DELETE_ON_CLOSE is currently accepted for files carrying the read-only DOS attribute. The server consequently creates or opens the file and marks it for deletion instead of returning STATUS_CANNOT_DELETE. Reject creation of a new read-only file with DELETE_ON_CLOSE. For an existing file, load the stored DOS attributes before accepting the create option. Also reject FileDispositionInformation when the opened file has the read-only attribute. Preserve the explicit STATUS_CANNOT_DELETE value while unwinding the CREATE request. This fixes smb2.delete-on-close-perms.READONLY. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 155a0b93ed58..f16f3da4ee3d 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3534,6 +3534,8 @@ int smb2_open(struct ksmbd_work *work) file_present = true; if (req->CreateOptions & FILE_DELETE_ON_CLOSE_LE) { + struct xattr_dos_attrib da; + /* * If file exists with under flags, return access * denied error. @@ -3547,6 +3549,16 @@ int smb2_open(struct ksmbd_work *work) if (!test_tree_conn_flag(tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) { ksmbd_debug(SMB, "User does not have write permission\n"); + rc = -EACCES; + goto err_out; + } + + if (test_share_config_flag(tcon->share_conf, + KSMBD_SHARE_FLAG_STORE_DOS_ATTRS) && + ksmbd_vfs_get_dos_attrib_xattr(mnt_idmap(path.mnt), + path.dentry, &da) > 0 && + da.attr & FILE_ATTRIBUTE_READONLY) { + rsp->hdr.Status = STATUS_CANNOT_DELETE; rc = -EACCES; goto err_out; } @@ -3564,6 +3576,13 @@ int smb2_open(struct ksmbd_work *work) rc = 0; } + if (!file_present && req->CreateOptions & FILE_DELETE_ON_CLOSE_LE && + req->FileAttributes & FILE_ATTRIBUTE_READONLY_LE) { + rsp->hdr.Status = STATUS_CANNOT_DELETE; + rc = -EACCES; + goto err_out; + } + /* * An explicit ::$DATA suffix names the unnamed data stream and is * canonicalized to a NULL stream name (base file), but the request @@ -4204,7 +4223,8 @@ int smb2_open(struct ksmbd_work *work) rsp->hdr.Status = STATUS_INVALID_PARAMETER; else if (rc == -EOPNOTSUPP) rsp->hdr.Status = STATUS_NOT_SUPPORTED; - else if (rc == -EACCES || rc == -ESTALE || rc == -EXDEV) { + else if ((rc == -EACCES || rc == -ESTALE || rc == -EXDEV) && + !rsp->hdr.Status) { if (req->DesiredAccess & FILE_ACCESS_SYSTEM_SECURITY_LE) rsp->hdr.Status = STATUS_PRIVILEGE_NOT_HELD; else @@ -6953,6 +6973,9 @@ static int set_file_disposition_info(struct ksmbd_work *work, return -EACCES; } + if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_READONLY_LE) + return -EACCES; + inode = file_inode(fp->filp); if (file_info->DeletePending) { if (ksmbd_has_stream_without_delete_share(fp)) @@ -7223,8 +7246,14 @@ int smb2_set_info(struct ksmbd_work *work) return 0; err_out: - if (rc == -EACCES || rc == -EPERM || rc == -EXDEV) - rsp->hdr.Status = STATUS_ACCESS_DENIED; + if (rc == -EACCES || rc == -EPERM || rc == -EXDEV) { + if (fp && req->InfoType == SMB2_O_INFO_FILE && + req->FileInfoClass == FILE_DISPOSITION_INFORMATION && + fp->f_ci->m_fattr & FILE_ATTRIBUTE_READONLY_LE) + rsp->hdr.Status = STATUS_CANNOT_DELETE; + else + rsp->hdr.Status = STATUS_ACCESS_DENIED; + } else if (rc == -EINVAL) rsp->hdr.Status = STATUS_INVALID_PARAMETER; else if (rc == -EMSGSIZE) From 4ea46ea602fc7055eee4b5b0e84f90f22da7f7e7 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 5 Jul 2026 15:55:14 +0900 Subject: [PATCH 007/142] ksmbd: protect private extended attributes SMB clients can currently create an EA named NTACL because SMB EAs are mapped into the user namespace while the ksmbd security descriptor is stored as security.NTACL. Allowing the reserved logical name makes the server-private ACL metadata appear writable through the SMB EA API. Reject NTACL, DOSATTRIB, and DosStream-prefixed EA names without regard to case. Filter the same private names from EA query results so stale or externally-created user namespace attributes cannot be exposed. This fixes smb2.ea.acl_xattr when acl_xattr_name is configured as NTACL. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f16f3da4ee3d..31ca0872b0fb 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2658,6 +2658,22 @@ static noinline int create_smb2_pipe(struct ksmbd_work *work) return err; } +static bool smb2_is_private_ea(const char *name, size_t name_len) +{ + if (name_len == SD_PREFIX_LEN && + !strncasecmp(name, SD_PREFIX, SD_PREFIX_LEN)) + return true; + if (name_len == DOS_ATTRIBUTE_PREFIX_LEN && + !strncasecmp(name, DOS_ATTRIBUTE_PREFIX, + DOS_ATTRIBUTE_PREFIX_LEN)) + return true; + if (name_len >= STREAM_PREFIX_LEN && + !strncasecmp(name, STREAM_PREFIX, STREAM_PREFIX_LEN)) + return true; + + return false; +} + /** * smb2_set_ea() - handler for setting extended attributes using set * info command @@ -2699,6 +2715,10 @@ static int smb2_set_ea(struct smb2_ea_info *eabuf, unsigned int buf_len, rc = -EINVAL; break; } + if (smb2_is_private_ea(eabuf->name, eabuf->EaNameLength)) { + rc = -EACCES; + break; + } memcpy(attr_name, XATTR_USER_PREFIX, XATTR_USER_PREFIX_LEN); memcpy(&attr_name[XATTR_USER_PREFIX_LEN], eabuf->name, @@ -5274,17 +5294,13 @@ static int smb2_get_ea(struct ksmbd_work *work, struct ksmbd_file *fp, if (strncmp(name, XATTR_USER_PREFIX, XATTR_USER_PREFIX_LEN)) continue; - if (!strncmp(&name[XATTR_USER_PREFIX_LEN], STREAM_PREFIX, - STREAM_PREFIX_LEN)) - continue; - if (req->InputBufferLength && strncmp(&name[XATTR_USER_PREFIX_LEN], ea_req->name, ea_req->EaNameLength)) continue; - if (!strncmp(&name[XATTR_USER_PREFIX_LEN], - DOS_ATTRIBUTE_PREFIX, DOS_ATTRIBUTE_PREFIX_LEN)) + if (smb2_is_private_ea(&name[XATTR_USER_PREFIX_LEN], + name_len - XATTR_USER_PREFIX_LEN)) continue; if (!strncmp(name, XATTR_USER_PREFIX, XATTR_USER_PREFIX_LEN)) From 8184c425a19d44f138d42b44cc363917c08e5f1c Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 5 Jul 2026 16:01:27 +0900 Subject: [PATCH 008/142] ksmbd: allow I/O on directory named streams Named streams are stored as extended attributes on the base inode. The VFS read and write helpers reject directory inodes before or together with checking whether the handle represents a stream. Permit read and write operations when a directory-backed handle is a named stream. Continue rejecting direct I/O on ordinary directory handles. This fixes creation of the directory stream in smb2.getinfo.complex. Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index d324585c0566..86d9cda0ca9e 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -345,7 +345,7 @@ int ksmbd_vfs_read(struct ksmbd_work *work, struct ksmbd_file *fp, size_t count, ssize_t nbytes = 0; struct inode *inode = file_inode(filp); - if (S_ISDIR(inode->i_mode)) + if (S_ISDIR(inode->i_mode) && !ksmbd_stream_fd(fp)) return -EISDIR; if (unlikely(count == 0)) @@ -474,7 +474,8 @@ int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp, if (work->conn->connection_type) { if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_APPEND_DATA_LE)) || - S_ISDIR(file_inode(fp->filp)->i_mode)) { + (S_ISDIR(file_inode(fp->filp)->i_mode) && + !ksmbd_stream_fd(fp))) { pr_err("no right to write(%pD)\n", fp->filp); err = -EACCES; goto out; From 0ecd35fac4b4f2828490689b46039744d201dcb0 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 5 Jul 2026 22:43:46 +0900 Subject: [PATCH 009/142] ksmbd: return buffer overflow for partial filesystem info The query-info buffer check returns STATUS_INFO_LENGTH_MISMATCH for every output buffer smaller than the complete response. Variable-length filesystem information instead requires STATUS_BUFFER_OVERFLOW when the fixed portion fits but the complete data does not. Pass the fixed size for each filesystem information class to the buffer checker. Keep INFO_LENGTH_MISMATCH for buffers below that size, and return BUFFER_OVERFLOW with a response truncated to the requested length for larger partial buffers. This fixes smb2.getinfo.qfs_buffercheck. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 31ca0872b0fb..184501e08b29 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5115,21 +5115,30 @@ int smb2_query_dir(struct ksmbd_work *work) /** * buffer_check_err() - helper function to check buffer errors * @reqOutputBufferLength: max buffer length expected in command response + * @fixed_len: minimum fixed response length * @rsp: query info response buffer contains output buffer length * @rsp_org: base response buffer pointer in case of chained response * * Return: 0 on success, otherwise error */ static int buffer_check_err(int reqOutputBufferLength, + unsigned int fixed_len, struct smb2_query_info_rsp *rsp, void *rsp_org) { - if (reqOutputBufferLength < le32_to_cpu(rsp->OutputBufferLength)) { + unsigned int output_len = le32_to_cpu(rsp->OutputBufferLength); + + if (reqOutputBufferLength < fixed_len) { pr_err("Invalid Buffer Size Requested\n"); rsp->hdr.Status = STATUS_INFO_LENGTH_MISMATCH; *(__be32 *)rsp_org = cpu_to_be32(sizeof(struct smb2_hdr)); return -EINVAL; } + + if (reqOutputBufferLength < output_len) { + rsp->hdr.Status = STATUS_BUFFER_OVERFLOW; + rsp->OutputBufferLength = cpu_to_le32(reqOutputBufferLength); + } return 0; } @@ -5192,11 +5201,13 @@ static int smb2_get_info_file_pipe(struct ksmbd_session *sess, case FILE_STANDARD_INFORMATION: get_standard_info_pipe(rsp, rsp_org); rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), + le32_to_cpu(rsp->OutputBufferLength), rsp, rsp_org); break; case FILE_INTERNAL_INFORMATION: get_internal_info_pipe(rsp, id, rsp_org); rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), + le32_to_cpu(rsp->OutputBufferLength), rsp, rsp_org); break; default: @@ -6013,6 +6024,7 @@ static int smb2_get_info_file(struct ksmbd_work *work, } if (!rc) rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), + le32_to_cpu(rsp->OutputBufferLength), rsp, work->response_buf); ksmbd_fd_put(work, fp); @@ -6034,6 +6046,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, struct kstatfs stfs; struct path path; int rc = 0, len; + unsigned int fixed_len = 0; if (!share->path) return -EIO; @@ -6068,6 +6081,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->DeviceCharacteristics |= cpu_to_le32(FILE_READ_ONLY_DEVICE); rsp->OutputBufferLength = cpu_to_le32(8); + fixed_len = 8; break; } case FS_ATTRIBUTE_INFORMATION: @@ -6120,6 +6134,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->FileSystemNameLen = cpu_to_le32(len); sz = sizeof(FILE_SYSTEM_ATTRIBUTE_INFO) + len; rsp->OutputBufferLength = cpu_to_le32(sz); + fixed_len = 16; break; } case FS_VOLUME_INFORMATION: @@ -6147,6 +6162,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->SupportsObjects = 0; sz = sizeof(struct filesystem_vol_info) + len; rsp->OutputBufferLength = cpu_to_le32(sz); + fixed_len = 24; break; } case FS_SIZE_INFORMATION: @@ -6159,6 +6175,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->SectorsPerAllocationUnit = cpu_to_le32(1); info->BytesPerSector = cpu_to_le32(stfs.f_bsize); rsp->OutputBufferLength = cpu_to_le32(24); + fixed_len = 24; break; } case FS_FULL_SIZE_INFORMATION: @@ -6174,6 +6191,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->SectorsPerAllocationUnit = cpu_to_le32(1); info->BytesPerSector = cpu_to_le32(stfs.f_bsize); rsp->OutputBufferLength = cpu_to_le32(32); + fixed_len = 32; break; } case FS_OBJECT_ID_INFORMATION: @@ -6194,6 +6212,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->extended_info.rel_date = 0; memcpy(info->extended_info.version_string, "1.1.0", strlen("1.1.0")); rsp->OutputBufferLength = cpu_to_le32(64); + fixed_len = 64; break; } case FS_SECTOR_SIZE_INFORMATION: @@ -6215,6 +6234,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->ByteOffsetForSectorAlignment = 0; info->ByteOffsetForPartitionAlignment = 0; rsp->OutputBufferLength = cpu_to_le32(28); + fixed_len = 28; break; } case FS_CONTROL_INFORMATION: @@ -6235,6 +6255,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->DefaultQuotaLimit = cpu_to_le64(SMB2_NO_FID); info->Padding = 0; rsp->OutputBufferLength = cpu_to_le32(48); + fixed_len = 48; break; } case FS_POSIX_INFORMATION: @@ -6255,6 +6276,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->TotalFileNodes = cpu_to_le64(stfs.f_files); info->FreeFileNodes = cpu_to_le64(stfs.f_ffree); rsp->OutputBufferLength = cpu_to_le32(56); + fixed_len = 56; } break; } @@ -6263,6 +6285,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, return -EOPNOTSUPP; } rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), + fixed_len, rsp, work->response_buf); path_put(&path); @@ -6364,6 +6387,7 @@ static int smb2_get_info_sec(struct ksmbd_work *work, rsp->OutputBufferLength = cpu_to_le32(secdesclen); rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), + le32_to_cpu(rsp->OutputBufferLength), rsp, work->response_buf); if (rc) goto err_out; From d40c24634fe077a0dc91fd11fccf44ce12b454d5 Mon Sep 17 00:00:00 2001 From: Guangshuo Li Date: Sun, 5 Jul 2026 19:34:35 +0800 Subject: [PATCH 010/142] ksmbd: Do not skip lock checks for single-byte ranges check_lock_range() uses inclusive ranges. Its callers pass the end offset as start + length - 1, so start == end represents a valid single-byte range rather than an empty range. The start == end shortcut therefore skips mandatory byte-range lock checks for one-byte reads, writes, copychunk operations and one-byte truncate ranges. A conflicting lock covering that byte is not checked and the operation is allowed to proceed. Remove the shortcut. The truncate size == inode->i_size case is already handled by only calling check_lock_range() when the new size differs from the current file size. Fixes: 5d510ac31626 ("ksmbd: skip lock-range check on equal size to avoid size==0 underflow") Signed-off-by: Guangshuo Li Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 86d9cda0ca9e..6600c2f5a404 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -297,9 +297,6 @@ static int check_lock_range(struct file *filp, loff_t start, loff_t end, struct file_lock_context *ctx = locks_inode_context(file_inode(filp)); int error = 0; - if (start == end) - return 0; - if (!ctx || list_empty_careful(&ctx->flc_posix)) return 0; From 6b8b79226bc3e0ac3fdd4e91836241af712e8cd1 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 00:07:09 +0900 Subject: [PATCH 011/142] ksmbd: fix partial file information responses Variable-length file information handlers use the client output length while constructing the response. FILE_ALL_INFORMATION can consequently return -EINVAL before the common buffer check, while stream information can stop building the complete result too early. Build the complete response within the available server response buffer and apply the client output length only when selecting the final status and transmitted length. Use the protocol-defined fixed sizes for all, alternate-name, and stream information to distinguish STATUS_INFO_LENGTH_MISMATCH from STATUS_BUFFER_OVERFLOW. This fixes smb2.getinfo.qfile_buffercheck. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 184501e08b29..a9898e205f84 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5486,7 +5486,6 @@ static int get_file_all_info(struct ksmbd_work *work, char *filename; u64 time; int ret, buf_free_len, filename_len; - struct smb2_query_info_req *req = ksmbd_req_buf_next(work); if (!(fp->daccess & FILE_READ_ATTRIBUTES_LE)) { ksmbd_debug(SMB, "no right to read the attributes : 0x%x\n", @@ -5499,10 +5498,9 @@ static int get_file_all_info(struct ksmbd_work *work, return PTR_ERR(filename); filename_len = strlen(filename); - buf_free_len = smb2_calc_max_out_buf_len(work, + buf_free_len = smb2_resp_buf_len(work, offsetof(struct smb2_query_info_rsp, Buffer) + - offsetof(struct smb2_file_all_info, FileName), - le32_to_cpu(req->OutputBufferLength)); + offsetof(struct smb2_file_all_info, FileName)); if (buf_free_len < (filename_len + 1) * 2) { kfree(filename); return -EINVAL; @@ -5593,7 +5591,6 @@ static int get_file_stream_info(struct ksmbd_work *work, ssize_t xattr_list_len; int nbytes = 0, streamlen, stream_name_len, next, idx = 0; int buf_free_len; - struct smb2_query_info_req *req = ksmbd_req_buf_next(work); int ret; ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, @@ -5603,10 +5600,8 @@ static int get_file_stream_info(struct ksmbd_work *work, file_info = (struct smb2_file_stream_info *)rsp->Buffer; - buf_free_len = - smb2_calc_max_out_buf_len(work, - offsetof(struct smb2_query_info_rsp, Buffer), - le32_to_cpu(req->OutputBufferLength)); + buf_free_len = smb2_resp_buf_len(work, + offsetof(struct smb2_query_info_rsp, Buffer)); if (buf_free_len < 0) goto out; @@ -5919,6 +5914,7 @@ static int smb2_get_info_file(struct ksmbd_work *work, struct ksmbd_file *fp; int fileinfoclass = 0; int rc = 0; + unsigned int fixed_len; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; if (test_share_config_flag(work->tcon->share_conf, @@ -6022,10 +6018,23 @@ static int smb2_get_info_file(struct ksmbd_work *work, fileinfoclass); rc = -EOPNOTSUPP; } - if (!rc) + if (!rc) { + fixed_len = le32_to_cpu(rsp->OutputBufferLength); + switch (fileinfoclass) { + case FILE_ALL_INFORMATION: + fixed_len = FILE_ALL_INFORMATION_SIZE; + break; + case FILE_ALTERNATE_NAME_INFORMATION: + fixed_len = FILE_ALTERNATE_NAME_INFORMATION_SIZE; + break; + case FILE_STREAM_INFORMATION: + fixed_len = FILE_STREAM_INFORMATION_SIZE; + break; + } rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), - le32_to_cpu(rsp->OutputBufferLength), + fixed_len, rsp, work->response_buf); + } ksmbd_fd_put(work, fp); iov_pin_out: From 2103add92a02505bdd536ba6fce7f64a427222a2 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 00:19:39 +0900 Subject: [PATCH 012/142] ksmbd: return buffer too small for short security queries SMB2 QUERY_INFO security requests with an output buffer too small for the self-relative security descriptor header can fall through descriptor construction and be reported as STATUS_INVALID_INFO_CLASS. After validating the file handle, reject buffers shorter than struct smb_ntsd with STATUS_BUFFER_TOO_SMALL before building the descriptor. This fixes smb2.getinfo.qsec_buffercheck. Signed-off-by: Namjae Jeon --- 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 a9898e205f84..a477aaf4e940 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -6348,6 +6348,12 @@ static int smb2_get_info_sec(struct ksmbd_work *work, if (!fp) return -ENOENT; + if (le32_to_cpu(req->OutputBufferLength) < sizeof(struct smb_ntsd)) { + rsp->hdr.Status = STATUS_BUFFER_TOO_SMALL; + ksmbd_fd_put(work, fp); + return -ENOSPC; + } + idmap = file_mnt_idmap(fp->filp); inode = file_inode(fp->filp); ksmbd_acls_fattr(&fattr, idmap, inode); From 10aeff72ab82c264238dda270984cafb30175bad Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 00:19:55 +0900 Subject: [PATCH 013/142] ksmbd: support normalized name information FILE_NORMALIZED_NAME_INFORMATION is not handled and is returned as STATUS_INVALID_INFO_CLASS. SMB 3.1.1 clients use this information class to obtain the share-relative path with the on-disk name casing. Build the normalized path from the opened dentry, remove the leading share-relative separator, and recover the canonical named-stream casing from its backing xattr. Return an empty name for the share root and STATUS_NOT_SUPPORTED for dialects older than SMB 3.1.1. Also distinguish a named $DATA stream on a directory from the unnamed data stream so that directory:stream:$DATA can be opened normally. This fixes smb2.getinfo.normalized. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 81 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a477aaf4e940..1959fcd5ebdc 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3618,7 +3618,7 @@ int smb2_open(struct ksmbd_work *work) } } else { if (file_present && S_ISDIR(d_inode(path.dentry)->i_mode) && - s_type == DATA_STREAM) { + !stream_name && s_type == DATA_STREAM) { rc = -EIO; rsp->hdr.Status = STATUS_FILE_IS_A_DIRECTORY; } @@ -5578,6 +5578,80 @@ static void get_file_alternate_info(struct ksmbd_work *work, cpu_to_le32(struct_size(file_info, FileName, conv_len)); } +static char *smb2_get_normalized_stream_name(struct ksmbd_file *fp) +{ + char *name, *stream_name = NULL, *xattr_list = NULL; + ssize_t xattr_list_len; + + if (!ksmbd_stream_fd(fp)) + return NULL; + + xattr_list_len = ksmbd_vfs_listxattr(fp->filp->f_path.dentry, + &xattr_list); + if (xattr_list_len <= 0) + goto out; + + for (name = xattr_list; name - xattr_list < xattr_list_len; + name += strlen(name) + 1) { + char *type; + + if (strlen(name) + 1 != fp->stream.size || + strncasecmp(name, fp->stream.name, fp->stream.size - 1)) + continue; + + name += XATTR_NAME_STREAM_LEN; + type = strrchr(name, ':'); + if (type) + stream_name = kstrndup(name, type - name, + KSMBD_DEFAULT_GFP); + break; + } +out: + kvfree(xattr_list); + return stream_name; +} + +static int get_file_normalized_name_info(struct ksmbd_work *work, + struct smb2_query_info_rsp *rsp, + struct ksmbd_file *fp) +{ + struct smb2_file_alt_name_info *file_info; + char *filename, *normalized, *stream_name; + int conv_len, filename_len; + + if (work->conn->dialect < SMB311_PROT_ID) { + rsp->hdr.Status = STATUS_NOT_SUPPORTED; + return -EOPNOTSUPP; + } + + filename = convert_to_nt_pathname(work->tcon->share_conf, + &fp->filp->f_path); + if (IS_ERR(filename)) + return PTR_ERR(filename); + if (filename[0] == '\\') + memmove(filename, filename + 1, strlen(filename)); + + stream_name = smb2_get_normalized_stream_name(fp); + normalized = kasprintf(KSMBD_DEFAULT_GFP, "%s%s%s", filename, + stream_name ? ":" : "", + stream_name ? stream_name : ""); + kfree(stream_name); + kfree(filename); + if (!normalized) + return -ENOMEM; + + filename_len = strlen(normalized); + file_info = (struct smb2_file_alt_name_info *)rsp->Buffer; + conv_len = smbConvertToUTF16((__le16 *)file_info->FileName, + normalized, filename_len, + work->conn->local_nls, 0); + kfree(normalized); + conv_len *= 2; + file_info->FileNameLength = cpu_to_le32(conv_len); + rsp->OutputBufferLength = cpu_to_le32(sizeof(*file_info) + conv_len); + return 0; +} + static int get_file_stream_info(struct ksmbd_work *work, struct smb2_query_info_rsp *rsp, struct ksmbd_file *fp, @@ -5969,6 +6043,9 @@ static int smb2_get_info_file(struct ksmbd_work *work, case FILE_ALTERNATE_NAME_INFORMATION: get_file_alternate_info(work, rsp, fp, work->response_buf); break; + case FILE_NORMALIZED_NAME_INFORMATION: + rc = get_file_normalized_name_info(work, rsp, fp); + break; case FILE_STREAM_INFORMATION: rc = get_file_stream_info(work, rsp, fp, work->response_buf); @@ -6478,7 +6555,7 @@ int smb2_query_info(struct ksmbd_work *work) rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; else if (rc == -EINVAL && rsp->hdr.Status == 0) rsp->hdr.Status = STATUS_INVALID_PARAMETER; - else if (rc == -EOPNOTSUPP || rsp->hdr.Status == 0) + else if (rsp->hdr.Status == 0) rsp->hdr.Status = STATUS_INVALID_INFO_CLASS; smb2_set_err_rsp(work); From d112661f951c4b6d9eaca051c52c4828780da082 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 00:25:35 +0900 Subject: [PATCH 014/142] ksmbd: require read control for security information SMB2 QUERY_INFO security requests currently return owner, group, and DACL information without checking the access granted to the opened handle. A handle opened with only SYNCHRONIZE or READ_ATTRIBUTES can consequently read the security descriptor. Require READ_CONTROL when OWNER_SECINFO, GROUP_SECINFO, or DACL_SECINFO is requested and return STATUS_ACCESS_DENIED otherwise. This fixes smb2.getinfo.getinfo_access. Signed-off-by: Namjae Jeon --- 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 1959fcd5ebdc..7d1c1d2adc19 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -6425,6 +6425,12 @@ static int smb2_get_info_sec(struct ksmbd_work *work, if (!fp) return -ENOENT; + if (addition_info & (OWNER_SECINFO | GROUP_SECINFO | DACL_SECINFO) && + !(fp->daccess & FILE_READ_CONTROL_LE)) { + ksmbd_fd_put(work, fp); + return -EACCES; + } + if (le32_to_cpu(req->OutputBufferLength) < sizeof(struct smb_ntsd)) { rsp->hdr.Status = STATUS_BUFFER_TOO_SMALL; ksmbd_fd_put(work, fp); From d4ef8821fd5a61a67981daf79185a40b8e853137 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 00:31:54 +0900 Subject: [PATCH 015/142] ksmbd: support empty snapshot enumeration FSCTL_SRV_ENUM_SNAPS is currently unimplemented, causing clients to treat shadow-copy enumeration as unsupported even when the share simply has no snapshots. Handle the count-only SRV_SNAPSHOT_ARRAY request and return a valid empty snapshot list after validating the file handle and minimum output buffer size. Report a two-byte empty UTF-16 MULTI_SZ array and zero snapshot counts. This allows smb2.ioctl.shadow_copy to run without a snapshot backend. Signed-off-by: Namjae Jeon --- fs/smb/common/smbfsctl.h | 1 + fs/smb/server/smb2pdu.c | 24 ++++++++++++++++++++++++ fs/smb/server/smb2pdu.h | 7 +++++++ 3 files changed, 32 insertions(+) diff --git a/fs/smb/common/smbfsctl.h b/fs/smb/common/smbfsctl.h index d1fcb46a7cde..b1123b0f768d 100644 --- a/fs/smb/common/smbfsctl.h +++ b/fs/smb/common/smbfsctl.h @@ -119,6 +119,7 @@ #define FSCTL_SRV_ENUMERATE_SNAPSHOTS 0x00144064 /* Retrieve an opaque file reference for server-side data movement ie copy */ #define FSCTL_SRV_REQUEST_RESUME_KEY 0x00140078 +#define FSCTL_SRV_ENUM_SNAPS 0x00144064 #define FSCTL_LMR_REQUEST_RESILIENCY 0x001401D4 #define FSCTL_LMR_GET_LINK_TRACK_INF 0x001400E8 /* BB add struct */ #define FSCTL_LMR_SET_LINK_TRACK_INF 0x001400EC /* BB add struct */ diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 7d1c1d2adc19..9279c02bbdb0 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9154,6 +9154,30 @@ int smb2_ioctl(struct ksmbd_work *work) in_buf_len = le32_to_cpu(req->InputCount); switch (cnt_code) { + case FSCTL_SRV_ENUM_SNAPS: { + struct srv_snapshot_array *snap_rsp; + struct ksmbd_file *fp; + + if (out_buf_len < sizeof(*snap_rsp)) { + ret = -EINVAL; + goto out; + } + + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -ENOENT; + goto out; + } + ksmbd_fd_put(work, fp); + + snap_rsp = (struct srv_snapshot_array *)rsp->Buffer; + snap_rsp->NumberOfSnapShots = 0; + snap_rsp->NumberOfSnapShotsReturned = 0; + snap_rsp->SnapShotArraySize = cpu_to_le32(2); + snap_rsp->Reserved = 0; + nbytes = sizeof(*snap_rsp); + break; + } case FSCTL_DFS_GET_REFERRALS: case FSCTL_DFS_GET_REFERRALS_EX: /* Not support DFS yet */ diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index aa06c8c905f1..eadab043bc40 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -199,6 +199,13 @@ struct smb2_file_stream_info { char StreamName[]; } __packed; +struct srv_snapshot_array { + __le32 NumberOfSnapShots; + __le32 NumberOfSnapShotsReturned; + __le32 SnapShotArraySize; + __le32 Reserved; +} __packed; + struct smb2_file_standard_info { __le64 AllocationSize; __le64 EndOfFile; From c1c200924fd825b632ff8811c09c3d7a5dff3895 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 00:35:18 +0900 Subject: [PATCH 016/142] ksmbd: return complete resume key response The FSCTL_SRV_REQUEST_RESUME_KEY response contains a mandatory four-byte context field after ContextLength. Defining the context as a flexible array excludes it from sizeof(struct resume_key_ioctl_rsp), so ksmbd sends only 28 bytes instead of the required 32 bytes. The truncated response cannot be decoded and results in an NDR buffer size error. Define the reserved context as a fixed four-byte field. This makes the response size match the wire format and ensures the field is zeroed and included in OutputCount. Signed-off-by: Namjae Jeon --- fs/smb/common/smb2pdu.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/smb/common/smb2pdu.h b/fs/smb/common/smb2pdu.h index e7ff52b8aba5..653cb3579d40 100644 --- a/fs/smb/common/smb2pdu.h +++ b/fs/smb/common/smb2pdu.h @@ -1452,7 +1452,7 @@ struct resume_key_ioctl_rsp { __u64 ResumeKeyU64[3]; }; __le32 ContextLength; /* MBZ */ - char Context[]; /* ignored, Windows sets to 4 bytes of zero */ + char Context[4]; /* ignored, Windows sets to 4 bytes of zero */ } __packed; struct smb2_ioctl_rsp { From fd309860ef24558963b3d6461041373a7af2e41d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 00:46:32 +0900 Subject: [PATCH 017/142] ksmbd: preserve data during overlapping copy chunk Copying an overlapping range within the same file through do_splice_direct() can overwrite source data that has not yet been read. This corrupts the destination when the target range starts inside and after the source range. Handle overlapping ranges with a bounded temporary buffer. Copy from the end when the destination follows the source and from the beginning otherwise, providing memmove semantics without allocating the entire copy length. Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 76 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 6600c2f5a404..2e06bdd1ed41 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -16,10 +16,10 @@ #include #include #include +#include #include #include #include -#include #include #include "glob.h" @@ -1734,6 +1734,66 @@ int ksmbd_vfs_xattr_stream_name(char *stream_name, char **xattr_stream_name, return 0; } +static ssize_t ksmbd_vfs_copy_file_range_overlap(struct file *src_file, + struct file *dst_file, + loff_t src_off, loff_t dst_off, + size_t len) +{ + size_t buf_size = min_t(size_t, len, SZ_1M); + size_t copied = 0; + char *buf; + ssize_t ret = 0; + + if (src_off == dst_off) + return len; + + buf = kvmalloc(buf_size, KSMBD_DEFAULT_GFP); + if (!buf) + return -ENOMEM; + + while (copied < len) { + size_t chunk_size = min(buf_size, len - copied); + size_t done = 0; + loff_t src_pos, dst_pos; + + if (dst_off > src_off) { + src_pos = src_off + len - copied - chunk_size; + dst_pos = dst_off + len - copied - chunk_size; + } else { + src_pos = src_off + copied; + dst_pos = dst_off + copied; + } + + while (done < chunk_size) { + ret = kernel_read(src_file, buf + done, + chunk_size - done, &src_pos); + if (ret <= 0) { + if (!ret) + ret = -EIO; + goto out; + } + done += ret; + } + + done = 0; + while (done < chunk_size) { + ret = kernel_write(dst_file, buf + done, + chunk_size - done, &dst_pos); + if (ret <= 0) { + if (!ret) + ret = -EIO; + goto out; + } + done += ret; + } + copied += chunk_size; + } + ret = copied; +out: + kvfree(buf); + return ret; +} + int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, struct ksmbd_file *src_fp, struct ksmbd_file *dst_fp, @@ -1791,16 +1851,12 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, if (src_off + len > src_file_size) return -E2BIG; - /* - * vfs_copy_file_range does not allow overlapped copying - * within the same file. - */ + /* vfs_copy_file_range does not support overlapping ranges. */ if (file_inode(src_fp->filp) == file_inode(dst_fp->filp) && - dst_off + len > src_off && - dst_off < src_off + len) - ret = do_splice_direct(src_fp->filp, &src_off, - dst_fp->filp, &dst_off, - min_t(size_t, len, MAX_RW_COUNT), 0); + dst_off + len > src_off && dst_off < src_off + len) + ret = ksmbd_vfs_copy_file_range_overlap(src_fp->filp, + dst_fp->filp, src_off, + dst_off, len); else ret = vfs_copy_file_range(src_fp->filp, src_off, dst_fp->filp, dst_off, len, 0); From f7c0366e0a80bf8cd5eacc5479927abb50958b5e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 00:49:37 +0900 Subject: [PATCH 018/142] ksmbd: preserve access denied status for copychunk The copychunk error mapping handles -EACCES in an independent if statement. The following error chain therefore reaches its final else clause and overwrites STATUS_ACCESS_DENIED with STATUS_UNEXPECTED_IO_ERROR. Join the -EACCES check to the remaining error chain so an access failure is returned as STATUS_ACCESS_DENIED. Signed-off-by: Namjae Jeon --- 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 9279c02bbdb0..136435eb5588 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8752,7 +8752,7 @@ static int fsctl_copychunk(struct ksmbd_work *work, if (ret < 0) { if (ret == -EACCES) rsp->hdr.Status = STATUS_ACCESS_DENIED; - if (ret == -EAGAIN) + else if (ret == -EAGAIN) rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; else if (ret == -EBADF) rsp->hdr.Status = STATUS_INVALID_HANDLE; From 8482150a0743c47104a190ef507d5a0108668ffb Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 21:41:11 +0900 Subject: [PATCH 019/142] ksmbd: support copychunk for alternate data streams Copychunk rejects requests when either handle refers to an alternate data stream. These streams are stored in extended attributes and cannot be passed directly to vfs_copy_file_range(). Use the bounded buffered copy path when a source or destination is a stream. Obtain the source length from the stream extended attribute and perform I/O through the existing stream-aware read and write helpers. Keep vfs_copy_file_range() and its fallback for regular files only. Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 80 ++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 2e06bdd1ed41..7f5976624d7f 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1734,19 +1734,17 @@ int ksmbd_vfs_xattr_stream_name(char *stream_name, char **xattr_stream_name, return 0; } -static ssize_t ksmbd_vfs_copy_file_range_overlap(struct file *src_file, - struct file *dst_file, - loff_t src_off, loff_t dst_off, - size_t len) +static ssize_t ksmbd_vfs_copy_file_range_buffered(struct ksmbd_work *work, + struct ksmbd_file *src_fp, + struct ksmbd_file *dst_fp, + loff_t src_off, + loff_t dst_off, size_t len) { size_t buf_size = min_t(size_t, len, SZ_1M); size_t copied = 0; char *buf; ssize_t ret = 0; - if (src_off == dst_off) - return len; - buf = kvmalloc(buf_size, KSMBD_DEFAULT_GFP); if (!buf) return -ENOMEM; @@ -1765,8 +1763,10 @@ static ssize_t ksmbd_vfs_copy_file_range_overlap(struct file *src_file, } while (done < chunk_size) { - ret = kernel_read(src_file, buf + done, - chunk_size - done, &src_pos); + loff_t pos = src_pos + done; + + ret = ksmbd_vfs_read(work, src_fp, chunk_size - done, + &pos, buf + done); if (ret <= 0) { if (!ret) ret = -EIO; @@ -1777,14 +1777,19 @@ static ssize_t ksmbd_vfs_copy_file_range_overlap(struct file *src_file, done = 0; while (done < chunk_size) { - ret = kernel_write(dst_file, buf + done, - chunk_size - done, &dst_pos); - if (ret <= 0) { - if (!ret) - ret = -EIO; + loff_t pos = dst_pos + done; + ssize_t written = 0; + + ret = ksmbd_vfs_write(work, dst_fp, buf + done, + chunk_size - done, &pos, false, + &written); + if (ret < 0) + goto out; + if (!written) { + ret = -EIO; goto out; } - done += ret; + done += written; } copied += chunk_size; } @@ -1821,9 +1826,6 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, return -EACCES; } - if (ksmbd_stream_fd(src_fp) || ksmbd_stream_fd(dst_fp)) - return -EBADF; - smb_break_all_levII_oplock(work, dst_fp, 1); if (!work->tcon->posix_extensions) { @@ -1841,7 +1843,20 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, } } - src_file_size = i_size_read(file_inode(src_fp->filp)); + if (ksmbd_stream_fd(src_fp)) { + const struct cred *saved_cred; + + saved_cred = override_creds(src_fp->filp->f_cred); + src_file_size = ksmbd_vfs_casexattr_len( + file_mnt_idmap(src_fp->filp), + src_fp->filp->f_path.dentry, + src_fp->stream.name, src_fp->stream.size); + revert_creds(saved_cred); + if (src_file_size < 0) + return src_file_size; + } else { + src_file_size = i_size_read(file_inode(src_fp->filp)); + } for (i = 0; i < chunk_count; i++) { src_off = le64_to_cpu(chunks[i].SourceOffset); @@ -1851,19 +1866,24 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, if (src_off + len > src_file_size) return -E2BIG; - /* vfs_copy_file_range does not support overlapping ranges. */ - if (file_inode(src_fp->filp) == file_inode(dst_fp->filp) && - dst_off + len > src_off && dst_off < src_off + len) - ret = ksmbd_vfs_copy_file_range_overlap(src_fp->filp, - dst_fp->filp, src_off, - dst_off, len); - else + /* + * vfs_copy_file_range does not support streams or overlapping + * ranges within the same file. + */ + if (ksmbd_stream_fd(src_fp) || ksmbd_stream_fd(dst_fp) || + (file_inode(src_fp->filp) == file_inode(dst_fp->filp) && + dst_off + len > src_off && dst_off < src_off + len)) + ret = ksmbd_vfs_copy_file_range_buffered(work, src_fp, + dst_fp, src_off, + dst_off, len); + else { ret = vfs_copy_file_range(src_fp->filp, src_off, dst_fp->filp, dst_off, len, 0); - if (ret == -EOPNOTSUPP || ret == -EXDEV) - ret = vfs_copy_file_range(src_fp->filp, src_off, - dst_fp->filp, dst_off, len, - COPY_FILE_SPLICE); + if (ret == -EOPNOTSUPP || ret == -EXDEV) + ret = vfs_copy_file_range(src_fp->filp, src_off, + dst_fp->filp, dst_off, + len, COPY_FILE_SPLICE); + } if (ret < 0) return ret; From f4ce7da9b33011d71d66b4eb3b979fa754e5e035 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 22:02:18 +0900 Subject: [PATCH 020/142] ksmbd: handle AAPL stream copy length mismatch macOS can reuse the main file's chunk list when issuing a copychunk request for alternate data streams. The requested source range can therefore exceed the length of the xattr-backed stream and currently fails with STATUS_INVALID_VIEW_SIZE. For AAPL connections copying between two streams, limit the actual copy to the available source data while reporting the requested chunk length as written. Keep the source range validation unchanged for non-AAPL connections and requests involving a regular file. Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 7f5976624d7f..95933691b39b 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1859,33 +1859,63 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, } for (i = 0; i < chunk_count; i++) { + bool stream_len_mismatch = false; + size_t copy_len; + src_off = le64_to_cpu(chunks[i].SourceOffset); dst_off = le64_to_cpu(chunks[i].TargetOffset); len = le32_to_cpu(chunks[i].Length); + copy_len = len; - if (src_off + len > src_file_size) + if (src_off < 0) return -E2BIG; + if (src_off > src_file_size || len > src_file_size - src_off) { + /* + * macOS can reuse the main file's chunk list when copying + * streams, so the requested range can exceed the size of + * the xattr-backed stream. For an AAPL connection, copy the + * available stream data and report the requested length to + * avoid a copy length mismatch. + */ + if (!work->conn->is_aapl || + !ksmbd_stream_fd(src_fp) || + !ksmbd_stream_fd(dst_fp)) + return -E2BIG; + + stream_len_mismatch = true; + if (src_off < src_file_size) + copy_len = src_file_size - src_off; + else + copy_len = 0; + } + /* * vfs_copy_file_range does not support streams or overlapping * ranges within the same file. */ - if (ksmbd_stream_fd(src_fp) || ksmbd_stream_fd(dst_fp) || + if (!copy_len) { + ret = 0; + } else if (ksmbd_stream_fd(src_fp) || ksmbd_stream_fd(dst_fp) || (file_inode(src_fp->filp) == file_inode(dst_fp->filp) && - dst_off + len > src_off && dst_off < src_off + len)) + dst_off + copy_len > src_off && + dst_off < src_off + copy_len)) { ret = ksmbd_vfs_copy_file_range_buffered(work, src_fp, dst_fp, src_off, - dst_off, len); - else { + dst_off, copy_len); + } else { ret = vfs_copy_file_range(src_fp->filp, src_off, - dst_fp->filp, dst_off, len, 0); + dst_fp->filp, dst_off, copy_len, 0); if (ret == -EOPNOTSUPP || ret == -EXDEV) ret = vfs_copy_file_range(src_fp->filp, src_off, dst_fp->filp, dst_off, - len, COPY_FILE_SPLICE); + copy_len, + COPY_FILE_SPLICE); } if (ret < 0) return ret; + if (stream_len_mismatch) + ret = len; *chunk_count_written += 1; *total_size_written += ret; From d68d4b3293034f549d55f407a23b4c0a6c90e50a Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Tue, 7 Jul 2026 11:55:01 +0200 Subject: [PATCH 021/142] ksmbd: fix off-by-one rejecting minimal COPYCHUNK query-limits request The FSCTL_COPYCHUNK/FSCTL_COPYCHUNK_WRITE input length check uses in_buf_len <= sizeof(struct copychunk_ioctl_req), which rejects a buffer that is exactly sizeof(struct copychunk_ioctl_req) bytes -- the minimal, valid request containing only the fixed header with ChunkCount=0 and no chunk entries, used by clients to query the server's copy limits before issuing a real copychunk. Since copychunk_ioctl_req ends in a flexible array member, the correct minimum is that the buffer covers the fixed header, so use offsetof(..., Chunks) with '<' instead of '<=' against sizeof(): same value, but the boundary case is now correctly accepted. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- 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 136435eb5588..c690c7f0eb08 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9329,7 +9329,7 @@ int smb2_ioctl(struct ksmbd_work *work) goto out; } - if (in_buf_len <= sizeof(struct copychunk_ioctl_req)) { + if (in_buf_len < offsetof(struct copychunk_ioctl_req, Chunks)) { ret = -EINVAL; goto out; } From 495ade881b5c52e2a2b646d04b04a4429e5734e9 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Tue, 7 Jul 2026 11:55:02 +0200 Subject: [PATCH 022/142] ksmbd: route stream FileDispositionInformation through stream delete flag set_file_disposition_info() calls ksmbd_set_inode_pending_delete() / ksmbd_clear_inode_pending_delete() unconditionally, which always sets S_DEL_PENDING on the whole inode (ci->m_flags), regardless of whether the handle being closed is a regular file or an alternate data stream. Requesting delete-pending on a single stream handle (e.g. deleting just an alternate data stream some clients keep alongside a file) would therefore incorrectly schedule deletion of the entire file's data, not just the stream. Add ksmbd_fd_set_delete_pending()/ksmbd_fd_clear_delete_pending(), following the same stream-vs-whole-file routing pattern already used by ksmbd_fd_set_delete_on_close() for the CREATE-time DeleteOnClose option, and switch set_file_disposition_info() to use them. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 4 ++-- fs/smb/server/vfs_cache.c | 32 ++++++++++++++++++++++++++++++++ fs/smb/server/vfs_cache.h | 2 ++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index c690c7f0eb08..0d63752bb158 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7123,9 +7123,9 @@ static int set_file_disposition_info(struct ksmbd_work *work, ksmbd_vfs_empty_dir(fp) == -ENOTEMPTY) return -EBUSY; smb_break_all_levII_oplock_for_delete(work, fp); - ksmbd_set_inode_pending_delete(fp); + ksmbd_fd_set_delete_pending(fp); } else { - ksmbd_clear_inode_pending_delete(fp); + ksmbd_fd_clear_delete_pending(fp); } return 0; } diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index a141025581af..b53912ce5e2a 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -294,6 +294,38 @@ void ksmbd_fd_set_delete_on_close(struct ksmbd_file *fp, up_write(&ci->m_lock); } +/* + * FileDispositionInformation (SET_INFO) on a stream handle must only + * mark the stream for deletion, not the whole file -- otherwise + * deleting a single alternate data stream (e.g. AFP_AfpInfo) deletes + * the entire file's data along with it. + */ +void ksmbd_fd_set_delete_pending(struct ksmbd_file *fp) +{ + struct ksmbd_inode *ci = fp->f_ci; + + if (ksmbd_stream_fd(fp)) { + down_write(&ci->m_lock); + ci->m_flags |= S_DEL_ON_CLS_STREAM; + up_write(&ci->m_lock); + } else { + ksmbd_set_inode_pending_delete(fp); + } +} + +void ksmbd_fd_clear_delete_pending(struct ksmbd_file *fp) +{ + struct ksmbd_inode *ci = fp->f_ci; + + if (ksmbd_stream_fd(fp)) { + down_write(&ci->m_lock); + ci->m_flags &= ~S_DEL_ON_CLS_STREAM; + up_write(&ci->m_lock); + } else { + ksmbd_clear_inode_pending_delete(fp); + } +} + static void ksmbd_inode_hash(struct ksmbd_inode *ci) { struct hlist_head *b = inode_hashtable + diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index b9e27307a26c..111a4e315499 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -214,6 +214,8 @@ void ksmbd_set_inode_pending_delete(struct ksmbd_file *fp); void ksmbd_clear_inode_pending_delete(struct ksmbd_file *fp); void ksmbd_fd_set_delete_on_close(struct ksmbd_file *fp, int file_info); +void ksmbd_fd_set_delete_pending(struct ksmbd_file *fp); +void ksmbd_fd_clear_delete_pending(struct ksmbd_file *fp); int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp); int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share, struct ksmbd_file *fp, char *name); From a9417bb1889e3c869f4c059a67efa8899d8df3f5 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Tue, 7 Jul 2026 11:55:03 +0200 Subject: [PATCH 023/142] ksmbd: report actual xattr value length for stream EndOfFile/AllocationSize fp->stream.size holds the byte length of the mangled xattr *name* string (it's used as the attr_name_len argument when looking up the xattr), not the size of the stream's actual data. CREATE and every QUERY_INFO handler that reports EndOfFile/AllocationSize for a stream handle used fp->stream.size directly, so clients received a bogus size derived from the internal xattr key name length instead of the stream's real content length. Add ksmbd_stream_eof() to query the xattr's actual value length via ksmbd_vfs_casexattr_len(), and use it at every site that reports a stream handle's size: the CREATE response, get_file_standard_info(), get_file_all_info(), get_file_network_open_info(), and find_file_posix_info(). Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 55 ++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 0d63752bb158..f737ba5cd82c 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2822,6 +2822,22 @@ static noinline int smb2_set_stream_name_xattr(const struct path *path, return 0; } +/* + * fp->stream.size is the byte length of the mangled xattr *name* + * (used as attr_name_len when looking the xattr up), not the size of + * the xattr's value. Reporting it as EndOfFile/AllocationSize for a + * stream handle is wrong -- query the xattr's actual value length + * instead. + */ +static loff_t ksmbd_stream_eof(struct ksmbd_file *fp) +{ + ssize_t slen = ksmbd_vfs_casexattr_len(file_mnt_idmap(fp->filp), + fp->filp->f_path.dentry, + fp->stream.name, + fp->stream.size); + return slen < 0 ? 0 : (loff_t)slen; +} + static int smb2_remove_smb_xattrs(const struct path *path) { struct mnt_idmap *idmap = mnt_idmap(path->mnt); @@ -4106,10 +4122,17 @@ int smb2_open(struct ksmbd_work *work) * using the raw on-disk block count, which can include filesystem * preallocation and metadata rounding. */ - if (!S_ISDIR(stat.mode) && stat.size > fp->allocation_size) - fp->allocation_size = round_up(stat.size, stat.blksize); - rsp->AllocationSize = cpu_to_le64(fp->allocation_size); - rsp->EndofFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); + if (ksmbd_stream_fd(fp)) { + loff_t seof = ksmbd_stream_eof(fp); + + rsp->AllocationSize = cpu_to_le64((u64)seof); + rsp->EndofFile = cpu_to_le64((u64)seof); + } else { + if (!S_ISDIR(stat.mode) && stat.size > fp->allocation_size) + fp->allocation_size = round_up(stat.size, stat.blksize); + rsp->AllocationSize = cpu_to_le64(fp->allocation_size); + rsp->EndofFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); + } rsp->FileAttributes = fp->f_ci->m_fattr; rsp->Reserved2 = 0; @@ -5450,8 +5473,10 @@ static int get_file_standard_info(struct smb2_query_info_rsp *rsp, sinfo->AllocationSize = cpu_to_le64(fp->allocation_size); sinfo->EndOfFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); } else { - sinfo->AllocationSize = cpu_to_le64(fp->stream.size); - sinfo->EndOfFile = cpu_to_le64(fp->stream.size); + loff_t seof = ksmbd_stream_eof(fp); + + sinfo->AllocationSize = cpu_to_le64((u64)seof); + sinfo->EndOfFile = cpu_to_le64((u64)seof); } sinfo->NumberOfLinks = cpu_to_le32(get_nlink(&stat) - delete_pending); sinfo->DeletePending = delete_pending; @@ -5529,8 +5554,10 @@ static int get_file_all_info(struct ksmbd_work *work, file_info->AllocationSize = cpu_to_le64(fp->allocation_size); file_info->EndOfFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); } else { - file_info->AllocationSize = cpu_to_le64(fp->stream.size); - file_info->EndOfFile = cpu_to_le64(fp->stream.size); + loff_t seof = ksmbd_stream_eof(fp); + + file_info->AllocationSize = cpu_to_le64((u64)seof); + file_info->EndOfFile = cpu_to_le64((u64)seof); } file_info->NumberOfLinks = cpu_to_le32(get_nlink(&stat) - delete_pending); @@ -5807,8 +5834,10 @@ static int get_file_network_open_info(struct smb2_query_info_rsp *rsp, file_info->AllocationSize = cpu_to_le64(fp->allocation_size); file_info->EndOfFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); } else { - file_info->AllocationSize = cpu_to_le64(fp->stream.size); - file_info->EndOfFile = cpu_to_le64(fp->stream.size); + loff_t seof = ksmbd_stream_eof(fp); + + file_info->AllocationSize = cpu_to_le64((u64)seof); + file_info->EndOfFile = cpu_to_le64((u64)seof); } file_info->Reserved = cpu_to_le32(0); rsp->OutputBufferLength = @@ -5939,8 +5968,10 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, file_info->EndOfFile = cpu_to_le64(stat.size); file_info->AllocationSize = cpu_to_le64(fp->allocation_size); } else { - file_info->EndOfFile = cpu_to_le64(fp->stream.size); - file_info->AllocationSize = cpu_to_le64(fp->stream.size); + loff_t seof = ksmbd_stream_eof(fp); + + file_info->EndOfFile = cpu_to_le64((u64)seof); + file_info->AllocationSize = cpu_to_le64((u64)seof); } file_info->HardLinks = cpu_to_le32(stat.nlink); file_info->Mode = cpu_to_le32(stat.mode & 0777); From 9870bbb55a2fc70a3d0945f85c4ff8a4fbb99a8e Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Tue, 7 Jul 2026 11:55:04 +0200 Subject: [PATCH 024/142] ksmbd: return STATUS_OBJECT_NAME_NOT_FOUND for unknown IPC pipe names create_smb2_pipe() maps ksmbd_session_rpc_open() failing with -EINVAL (pipe name not recognized/supported) to STATUS_INVALID_PARAMETER. macOS Time Machine's backupd treats STATUS_INVALID_PARAMETER on a pipe open as a fatal error and aborts the backup immediately, whereas STATUS_OBJECT_NAME_NOT_FOUND is handled gracefully -- the client just treats that particular pipe as unavailable and continues. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f737ba5cd82c..06197275956d 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2640,7 +2640,13 @@ static noinline int create_smb2_pipe(struct ksmbd_work *work) out: switch (err) { case -EINVAL: - rsp->hdr.Status = STATUS_INVALID_PARAMETER; + /* + * Unknown pipe name: return STATUS_OBJECT_NAME_NOT_FOUND so + * macOS clients skip it gracefully. STATUS_INVALID_PARAMETER + * causes macOS Time Machine to abort the backup immediately + * (confirmed in ksmbd issue #502 / namjaejeon/ksmbd). + */ + rsp->hdr.Status = STATUS_OBJECT_NAME_NOT_FOUND; break; case -ENOSPC: case -ENOMEM: From 92db30225eba52d330eda2dd92311afac47bb7df Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Tue, 7 Jul 2026 11:55:05 +0200 Subject: [PATCH 025/142] ksmbd: quiet mdssvc RPC log spam macOS routinely probes the mdssvc RPC pipe to check for Spotlight search support. __rpc_method() already falls through to returning 0 (unsupported) for it via the default case, but that path also logs "Unsupported RPC: mdssvc" via pr_err on every single probe -- which happens often enough during normal macOS browsing/backup activity to spam the kernel log. Add an explicit case that returns the same value without the log line; behavior is unchanged, this only removes noise for an expected, routine client behavior. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index f99c86284ba3..eaa044da3280 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -308,6 +308,9 @@ static int __rpc_method(char *rpc_name) if (!strcmp(rpc_name, "\\lsarpc") || !strcmp(rpc_name, "lsarpc")) return KSMBD_RPC_LSARPC_METHOD_INVOKE; + if (!strcmp(rpc_name, "\\mdssvc") || !strcmp(rpc_name, "mdssvc")) + return 0; /* mdssvc unsupported: quiet, expected macOS probe */ + pr_err("Unsupported RPC: %s\n", rpc_name); return 0; } From 13e82e2cbd10490d8d6bb29714fb3ec884657913 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Tue, 7 Jul 2026 11:55:06 +0200 Subject: [PATCH 026/142] ksmbd: clear stale sparse attribute on non-sparse shares smb2_update_xattrs() copies the DOS SPARSE attribute bit verbatim from the stored xattr into the in-memory file attributes, without checking whether the share is currently advertising FILE_SUPPORTS_SPARSE_FILES. A file whose xattr has a stale SPARSE bit (set by a previous client, or from before the share was reconfigured) would keep reporting as sparse even after sparse-file support is turned off for the share. This matters for Time Machine: sparsebundle band files rely on accurate sparse-file status being reported, since macOS decides whether to issue FSCTL_SET_SPARSE based on it. Mask the bit out when the share doesn't currently advertise sparse-file support. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 06197275956d..283527337d0c 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2934,6 +2934,16 @@ static void smb2_update_xattrs(struct ksmbd_tree_connect *tcon, rc = ksmbd_vfs_get_dos_attrib_xattr(mnt_idmap(path->mnt), path->dentry, &da); if (rc > 0) { + /* + * Don't report a stale SPARSE bit (e.g. left over from a + * previous client, or from before the share was reconfigured) + * when the share isn't currently advertising sparse-file + * support. Time Machine sparsebundle band files rely on + * sparse status being accurate, since macOS decides whether + * to use FSCTL_SET_SPARSE based on it. + */ + if (!(server_conf.share_fake_fscaps & FILE_SUPPORTS_SPARSE_FILES)) + da.attr &= ~FILE_ATTRIBUTE_SPARSE_FILE; fp->f_ci->m_fattr = cpu_to_le32(da.attr); fp->create_time = da.create_time; fp->itime = da.itime; From 7c5d98b515f8cc30a4f5d7e1788b01c2bacea7ef Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 7 Jul 2026 22:22:04 +0900 Subject: [PATCH 027/142] ksmbd: distinguish unknown RPC pipe names Unknown RPC pipe names and malformed CREATE parameters both use -EINVAL. Mapping that errno to STATUS_OBJECT_NAME_NOT_FOUND therefore also hides invalid request parameters as a missing pipe. Return -ENOENT when RPC method lookup cannot find a supported pipe and map only that error to STATUS_OBJECT_NAME_NOT_FOUND. Preserve STATUS_INVALID_PARAMETER for -EINVAL returned by request validation. Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 8 ++++---- fs/smb/server/smb2pdu.c | 9 +++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index eaa044da3280..5b9bd46ff3a8 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -309,10 +309,10 @@ static int __rpc_method(char *rpc_name) return KSMBD_RPC_LSARPC_METHOD_INVOKE; if (!strcmp(rpc_name, "\\mdssvc") || !strcmp(rpc_name, "mdssvc")) - return 0; /* mdssvc unsupported: quiet, expected macOS probe */ + return -ENOENT; pr_err("Unsupported RPC: %s\n", rpc_name); - return 0; + return -ENOENT; } int ksmbd_session_rpc_open(struct ksmbd_session *sess, char *rpc_name) @@ -322,8 +322,8 @@ int ksmbd_session_rpc_open(struct ksmbd_session *sess, char *rpc_name) int method, id; method = __rpc_method(rpc_name); - if (!method) - return -EINVAL; + if (method < 0) + return method; entry = kzalloc_obj(struct ksmbd_session_rpc, KSMBD_DEFAULT_GFP); if (!entry) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 283527337d0c..5fdb8fec21cd 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2640,12 +2640,9 @@ static noinline int create_smb2_pipe(struct ksmbd_work *work) out: switch (err) { case -EINVAL: - /* - * Unknown pipe name: return STATUS_OBJECT_NAME_NOT_FOUND so - * macOS clients skip it gracefully. STATUS_INVALID_PARAMETER - * causes macOS Time Machine to abort the backup immediately - * (confirmed in ksmbd issue #502 / namjaejeon/ksmbd). - */ + rsp->hdr.Status = STATUS_INVALID_PARAMETER; + break; + case -ENOENT: rsp->hdr.Status = STATUS_OBJECT_NAME_NOT_FOUND; break; case -ENOSPC: From 906a62216339d7eccc336bfb4531ce3d7850b8ac Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Wed, 8 Jul 2026 02:56:13 +0000 Subject: [PATCH 028/142] smb/server: send compound prefix before async pending response When the last request in a compound request becomes async, ksmbd sends a STATUS_PENDING response for it. But the responses for previous requests in the same compound request are still kept in the same response buffer. Send these previous responses first. Clear NextCommand for the last response in this part, sign it again if needed, and reset the iov state. After that, the async request sends STATUS_PENDING first, and sends the real response later. Both are separate responses. Example: smbtorture //${server_ip}/export -U${username}%${password} smb2.compound_async.write_write Client request: Write Request Len:64 Off:0, File: compound_async_write_write; Write Request Len:64 Off:64 Before this patch, STATUS_PENDING Write Response is the first of several responses: Write Response, Error: STATUS_PENDING Write Response, File: compound_async_write_write; Write Response But STATUS_PENDING Write Response should be in the middle of several responses, after this patch: Write Response, File: compound_async_write_write Write Response SMB2, STATUS_PENDING, Write Response, MessageId 7 SMB2, Write Response, MessageId 7 Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 5fdb8fec21cd..f919d748c12c 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -839,6 +839,48 @@ void release_async_work(struct ksmbd_work *work) } } +static void smb2_send_interim_compound_prefix(struct ksmbd_work *work) +{ + struct smb2_hdr *req_hdr; + struct smb2_hdr *rsp_hdr; + int err; + + if (!work->next_smb2_rcv_hdr_off || + !work->next_smb2_rsp_hdr_off || + work->curr_smb2_rsp_hdr_off == work->next_smb2_rsp_hdr_off || + !work->iov_idx) + return; + + req_hdr = ksmbd_req_buf_next(work); + /* Detach only the final async command from the completed prefix. */ + if (req_hdr->NextCommand) + return; + + /* + * The responses before the async command are sent as a standalone + * compound response. The last response in this prefix must terminate + * the chain. + */ + rsp_hdr = ksmbd_resp_buf_curr(work); + rsp_hdr->NextCommand = 0; + if ((rsp_hdr->Flags & SMB2_FLAGS_SIGNED) && work->sess && + work->conn->ops->set_sign_rsp) + work->conn->ops->set_sign_rsp(work); + + err = ksmbd_conn_write(work); + if (err) + ksmbd_debug(SMB, "failed to send compound interim prefix: %d\n", + err); + + work->iov_idx = 0; + work->iov_cnt = 0; + work->curr_smb2_rsp_hdr_off = work->next_smb2_rsp_hdr_off; + *(__be32 *)work->response_buf = 0; + + rsp_hdr = ksmbd_resp_buf_next(work); + rsp_hdr->Flags &= ~SMB2_FLAGS_RELATED_OPERATIONS; +} + void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) { struct smb2_hdr *rsp_hdr; @@ -853,6 +895,9 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) return; } + if (status == STATUS_PENDING) + smb2_send_interim_compound_prefix(work); + in_work->conn = work->conn; memcpy(smb_get_msg(in_work->response_buf), ksmbd_resp_buf_next(work), __SMB2_HEADER_STRUCTURE_SIZE); From 0977715850ac3e16685e6ffdbb6760a1df9d41ea Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Wed, 8 Jul 2026 02:56:14 +0000 Subject: [PATCH 029/142] smb/server: introduce struct ksmbd_transport_write Put the arguments of ksmbd_transport_ops ->writev() into a struct. This makes the function call shorter and easier to read. Add __ksmbd_conn_write() for the common write code. A later patch will use it for another write helper. No functional change. Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 22 ++++++++++++++++------ fs/smb/server/connection.h | 13 ++++++++++--- fs/smb/server/transport_rdma.c | 9 +++++---- fs/smb/server/transport_tcp.c | 13 +++++++------ 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index ef6f202f4024..8d474501a4af 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -350,7 +350,8 @@ int ksmbd_conn_wait_idle_sess_id(struct ksmbd_conn *curr_conn, u64 sess_id) return 0; } -int ksmbd_conn_write(struct ksmbd_work *work) +static int __ksmbd_conn_write(struct ksmbd_work *work, + struct ksmbd_transport_write *tx) { struct ksmbd_conn *conn = work->conn; int sent; @@ -366,12 +367,14 @@ int ksmbd_conn_write(struct ksmbd_work *work) if (!work->iov_idx) return -EINVAL; + tx->iov = work->iov; + tx->iov_cnt = work->iov_cnt; + tx->size = get_rfc1002_len(work->iov[0].iov_base) + 4; + tx->need_invalidate_rkey = work->need_invalidate_rkey; + tx->remote_key = work->remote_key; + ksmbd_conn_lock(conn); - sent = conn->transport->ops->writev(conn->transport, work->iov, - work->iov_cnt, - get_rfc1002_len(work->iov[0].iov_base) + 4, - work->need_invalidate_rkey, - work->remote_key); + sent = conn->transport->ops->writev(conn->transport, tx); ksmbd_conn_unlock(conn); if (sent < 0) { @@ -382,6 +385,13 @@ int ksmbd_conn_write(struct ksmbd_work *work) return 0; } +int ksmbd_conn_write(struct ksmbd_work *work) +{ + struct ksmbd_transport_write tx = {}; + + return __ksmbd_conn_write(work, &tx); +} + int ksmbd_conn_rdma_read(struct ksmbd_conn *conn, void *buf, unsigned int buflen, struct smbdirect_buffer_descriptor_v1 *desc, diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 0e4ebfac5558..624ed55fa671 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -132,14 +132,21 @@ struct ksmbd_conn_ops { int (*terminate_fn)(struct ksmbd_conn *conn); }; +struct ksmbd_transport_write { + struct kvec *iov; + int iov_cnt; + int size; + bool need_invalidate_rkey; + unsigned int remote_key; +}; + struct ksmbd_transport_ops { void (*disconnect)(struct ksmbd_transport *t); void (*shutdown)(struct ksmbd_transport *t); int (*read)(struct ksmbd_transport *t, char *buf, unsigned int size, int max_retries); - int (*writev)(struct ksmbd_transport *t, struct kvec *iovs, int niov, - int size, bool need_invalidate_rkey, - unsigned int remote_key); + int (*writev)(struct ksmbd_transport *t, + const struct ksmbd_transport_write *tx); int (*rdma_read)(struct ksmbd_transport *t, void *buf, unsigned int len, struct smbdirect_buffer_descriptor_v1 *desc, diff --git a/fs/smb/server/transport_rdma.c b/fs/smb/server/transport_rdma.c index b6d63ff8a8a3..85d12c4c354c 100644 --- a/fs/smb/server/transport_rdma.c +++ b/fs/smb/server/transport_rdma.c @@ -239,17 +239,18 @@ static int smb_direct_read(struct ksmbd_transport *t, char *buf, } static int smb_direct_writev(struct ksmbd_transport *t, - struct kvec *iov, int niovs, int buflen, - bool need_invalidate, unsigned int remote_key) + const struct ksmbd_transport_write *tx) { struct smb_direct_transport *st = SMBD_TRANS(t); struct smbdirect_socket *sc = st->socket; struct iov_iter iter; - iov_iter_kvec(&iter, ITER_SOURCE, iov, niovs, buflen); + iov_iter_kvec(&iter, ITER_SOURCE, tx->iov, tx->iov_cnt, + tx->size); return smbdirect_connection_send_iter(sc, &iter, 0, - need_invalidate, remote_key); + tx->need_invalidate_rkey, + tx->remote_key); } static int smb_direct_rdma_write(struct ksmbd_transport *t, diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index 13b711ea575d..6d313cd8b345 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -417,14 +417,15 @@ static int ksmbd_tcp_read(struct ksmbd_transport *t, char *buf, return ksmbd_tcp_readv(TCP_TRANS(t), &iov, 1, to_read, max_retries); } -static int ksmbd_tcp_writev(struct ksmbd_transport *t, struct kvec *iov, - int nvecs, int size, bool need_invalidate, - unsigned int remote_key) - +static int ksmbd_tcp_writev(struct ksmbd_transport *t, + const struct ksmbd_transport_write *tx) { - struct msghdr smb_msg = {.msg_flags = MSG_NOSIGNAL}; + struct msghdr smb_msg = { + .msg_flags = MSG_NOSIGNAL, + }; - return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, iov, nvecs, size); + return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, tx->iov, + tx->iov_cnt, tx->size); } static void ksmbd_tcp_disconnect(struct ksmbd_transport *t) From 49f6a485868eff33fcd492ec60723aef3a43b017 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Wed, 8 Jul 2026 02:56:15 +0000 Subject: [PATCH 030/142] smb/server: use MSG_EOR for async interim response Two kernel_sendmsg() calls can still use the same TCP skb if the first skb can take more data. This can happen when ksmbd sends two SMB2 responses very close to each other. Without MSG_EOR, TCP can append the next sendmsg data to the previous skb. Then STATUS_PENDING and the later response can be put into the same TCP skb. MSG_EOR marks the skb as end of record, so TCP will not collapse the next sendmsg data into it. Example: smbtorture //${server_ip}/export -U${username}%${password} smb2.compound_async.write_write Client request: Write Request Len:64 Off:0, File: compound_async_write_write; Write Request Len:64 Off:64 Before this patch, server responses: Write Response, File: compound_async_write_write Write Response SMB2, STATUS_PENDING, Write Response, MessageId 7 SMB2, Write Response, MessageId 7 After this patch: Write Response, File: compound_async_write_write Write Response, Error: STATUS_PENDING Write Response Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 9 +++++++++ fs/smb/server/connection.h | 2 ++ fs/smb/server/smb2pdu.c | 4 ++-- fs/smb/server/transport_tcp.c | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 8d474501a4af..62e17883d5e2 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -392,6 +392,15 @@ int ksmbd_conn_write(struct ksmbd_work *work) return __ksmbd_conn_write(work, &tx); } +int ksmbd_conn_write_eor(struct ksmbd_work *work) +{ + struct ksmbd_transport_write tx = { + .msg_flags = MSG_EOR, + }; + + return __ksmbd_conn_write(work, &tx); +} + int ksmbd_conn_rdma_read(struct ksmbd_conn *conn, void *buf, unsigned int buflen, struct smbdirect_buffer_descriptor_v1 *desc, diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 624ed55fa671..11e18217258e 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -138,6 +138,7 @@ struct ksmbd_transport_write { int size; bool need_invalidate_rkey; unsigned int remote_key; + int msg_flags; }; struct ksmbd_transport_ops { @@ -182,6 +183,7 @@ int ksmbd_conn_wq_init(void); void ksmbd_conn_wq_destroy(void); bool ksmbd_conn_lookup_dialect(struct ksmbd_conn *c); int ksmbd_conn_write(struct ksmbd_work *work); +int ksmbd_conn_write_eor(struct ksmbd_work *work); int ksmbd_conn_rdma_read(struct ksmbd_conn *conn, void *buf, unsigned int buflen, struct smbdirect_buffer_descriptor_v1 *desc, diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f919d748c12c..513ed9696538 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -867,7 +867,7 @@ static void smb2_send_interim_compound_prefix(struct ksmbd_work *work) work->conn->ops->set_sign_rsp) work->conn->ops->set_sign_rsp(work); - err = ksmbd_conn_write(work); + err = ksmbd_conn_write_eor(work); if (err) ksmbd_debug(SMB, "failed to send compound interim prefix: %d\n", err); @@ -908,7 +908,7 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) smb2_set_err_rsp(in_work); rsp_hdr->Status = status; - ksmbd_conn_write(in_work); + ksmbd_conn_write_eor(in_work); ksmbd_free_work_struct(in_work); } diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index 6d313cd8b345..1045eca581c3 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -421,7 +421,7 @@ static int ksmbd_tcp_writev(struct ksmbd_transport *t, const struct ksmbd_transport_write *tx) { struct msghdr smb_msg = { - .msg_flags = MSG_NOSIGNAL, + .msg_flags = MSG_NOSIGNAL | tx->msg_flags, }; return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, tx->iov, From 0fb327626ad9cbe2551166b5b9cec13b7b221f36 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 8 Jul 2026 21:40:40 +0900 Subject: [PATCH 031/142] ksmbd: support file compression attributes Advertise file compression support and keep the compression state in the per-file DOS attributes when the backing filesystem cannot apply the compression flag directly. FSCTL_SET_COMPRESSION should still update the state returned by FSCTL_GET_COMPRESSION and file compression information in that case. When a new object is created under a compressed directory, inherit the compression attribute from the parent. If FILE_NO_COMPRESSION is specified, clear the compression state after creation and let it override inheritance for both files and directories. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 47 +++++++++++++++++++++++++++++++-- fs/smb/server/vfs.c | 57 +++++++++++++++++++++++++++++++---------- fs/smb/server/vfs.h | 2 ++ 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 513ed9696538..efd67d5c684d 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2960,6 +2960,34 @@ static void smb2_new_xattrs(struct ksmbd_tree_connect *tcon, const struct path * ksmbd_debug(SMB, "failed to store file attribute into xattr\n"); } +static bool smb2_parent_compressed(struct ksmbd_tree_connect *tcon, + const struct path *path) +{ + struct dentry *parent = dget_parent(path->dentry); + struct file_kattr fa = { .flags_valid = true }; + struct xattr_dos_attrib da; + bool compressed = false; + int rc; + + rc = vfs_fileattr_get(parent, &fa); + if (!rc && fa.flags & FS_COMPR_FL) { + compressed = true; + goto out; + } + + if (!test_share_config_flag(tcon->share_conf, + KSMBD_SHARE_FLAG_STORE_DOS_ATTRS)) + goto out; + + rc = ksmbd_vfs_get_dos_attrib_xattr(mnt_idmap(path->mnt), parent, &da); + if (rc > 0 && da.attr & FILE_ATTRIBUTE_COMPRESSED) + compressed = true; + +out: + dput(parent); + return compressed; +} + static void smb2_update_xattrs(struct ksmbd_tree_connect *tcon, const struct path *path, struct ksmbd_file *fp) { @@ -3520,8 +3548,6 @@ int smb2_open(struct ksmbd_work *work) if (req->CreateOptions & FILE_NON_DIRECTORY_FILE_LE) { rc = -EINVAL; goto err_out2; - } else if (req->CreateOptions & FILE_NO_COMPRESSION_LE) { - req->CreateOptions &= ~FILE_NO_COMPRESSION_LE; } } } @@ -4128,6 +4154,22 @@ int smb2_open(struct ksmbd_work *work) ksmbd_vfs_update_compressed_fattr(path.dentry, &fp->f_ci->m_fattr); + if (created) { + if (fp->coption & FILE_NO_COMPRESSION_LE) { + rc = ksmbd_vfs_set_compression_create(work, fp, + COMPRESSION_FORMAT_NONE); + if (rc) + fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; + rc = 0; + } else if (smb2_parent_compressed(tcon, &path)) { + rc = ksmbd_vfs_set_compression_create(work, fp, + COMPRESSION_FORMAT_LZNT1); + if (rc) + fp->f_ci->m_fattr |= FILE_ATTRIBUTE_COMPRESSED_LE; + rc = 0; + } + } + if (created) smb2_new_xattrs(tcon, &path, fp); @@ -6271,6 +6313,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, attrs = FILE_SUPPORTS_OBJECT_IDS | FILE_PERSISTENT_ACLS | FILE_UNICODE_ON_DISK | + FILE_FILE_COMPRESSION | FILE_SUPPORTS_BLOCK_REFCOUNTING; err = vfs_fileattr_get(path.dentry, &fa); diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 95933691b39b..29fa0e62e72e 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -2008,8 +2008,6 @@ void ksmbd_vfs_update_compressed_fattr(struct dentry *dentry, __le32 *fattr) struct file_kattr fa = { .flags_valid = true }; rc = vfs_fileattr_get(dentry, &fa); - if (rc == -ENOIOCTLCMD) - *fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; if (rc) return; @@ -2025,15 +2023,19 @@ int ksmbd_vfs_get_compression(struct ksmbd_file *fp, u16 *fmt) int rc; rc = vfs_fileattr_get(fp->filp->f_path.dentry, &fa); - if (rc == -ENOIOCTLCMD) { - *fmt = COMPRESSION_FORMAT_NONE; + if (rc == -ENOIOCTLCMD || rc == -ENOTTY || rc == -EINVAL || + rc == -EOPNOTSUPP) { + if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_COMPRESSED_LE) + *fmt = COMPRESSION_FORMAT_LZNT1; + else + *fmt = COMPRESSION_FORMAT_NONE; rc = 0; goto out; } if (rc) goto out; - if (fa.flags & FS_COMPR_FL) + if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_COMPRESSED_LE) *fmt = COMPRESSION_FORMAT_LZNT1; else *fmt = COMPRESSION_FORMAT_NONE; @@ -2042,7 +2044,9 @@ int ksmbd_vfs_get_compression(struct ksmbd_file *fp, u16 *fmt) return rc; } -int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u16 fmt) +static int __ksmbd_vfs_set_compression(struct ksmbd_work *work, + struct ksmbd_file *fp, u16 fmt, + bool check_access) { const struct cred *saved_cred = NULL; struct file_kattr fa; @@ -2052,13 +2056,23 @@ int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u1 __le32 old_fattr; int rc; - if (!(fp->daccess & FILE_WRITE_DATA_LE)) { + if (check_access && !(fp->daccess & FILE_WRITE_DATA_LE)) { rc = -EACCES; goto out; } + if (fmt != COMPRESSION_FORMAT_NONE && + fmt != COMPRESSION_FORMAT_DEFAULT && + fmt != COMPRESSION_FORMAT_LZNT1) { + rc = -EINVAL; + goto out; + } + saved_cred = override_creds(fp->filp->f_cred); rc = vfs_fileattr_get(dentry, &fa); + if (rc == -ENOIOCTLCMD || rc == -ENOTTY || rc == -EINVAL || + rc == -EOPNOTSUPP) + goto update_fattr; if (rc) goto out; @@ -2068,9 +2082,6 @@ int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u1 } else if (fmt == COMPRESSION_FORMAT_DEFAULT || fmt == COMPRESSION_FORMAT_LZNT1) { flags |= FS_COMPR_FL; - } else { - rc = -EINVAL; - goto out; } if (flags != fa.flags) { @@ -2081,10 +2092,14 @@ int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u1 rc = vfs_fileattr_set(idmap, dentry, &fa); mnt_drop_write_file(fp->filp); + if (rc == -ENOIOCTLCMD || rc == -ENOTTY || rc == -EINVAL || + rc == -EOPNOTSUPP) + goto update_fattr; if (rc) goto out; } +update_fattr: old_fattr = fp->f_ci->m_fattr; if (fmt == COMPRESSION_FORMAT_NONE) fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; @@ -2094,15 +2109,19 @@ int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u1 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; + struct xattr_dos_attrib da = {0}; rc = ksmbd_vfs_get_dos_attrib_xattr(idmap, dentry, &da); if (rc <= 0) { - rc = 0; - goto out; + da.version = 4; + da.itime = fp->itime; + da.create_time = fp->create_time; + da.flags = XATTR_DOSINFO_CREATE_TIME | + XATTR_DOSINFO_ITIME; } da.attr = le32_to_cpu(fp->f_ci->m_fattr); + da.flags |= XATTR_DOSINFO_ATTRIB; rc = ksmbd_vfs_set_dos_attrib_xattr(idmap, &fp->filp->f_path, &da, true); @@ -2115,3 +2134,15 @@ int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u1 revert_creds(saved_cred); return rc; } + +int ksmbd_vfs_set_compression(struct ksmbd_work *work, + struct ksmbd_file *fp, u16 fmt) +{ + return __ksmbd_vfs_set_compression(work, fp, fmt, true); +} + +int ksmbd_vfs_set_compression_create(struct ksmbd_work *work, + struct ksmbd_file *fp, u16 fmt) +{ + return __ksmbd_vfs_set_compression(work, fp, fmt, false); +} diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 7b3d2f4fd985..8eab9392ff89 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -171,4 +171,6 @@ int ksmbd_vfs_inherit_posix_acl(struct mnt_idmap *idmap, 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); +int ksmbd_vfs_set_compression_create(struct ksmbd_work *work, + struct ksmbd_file *fp, u16 fmt); #endif /* __KSMBD_VFS_H__ */ From bea20b65163500b2192fa10933fbf3a7edd5044a Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 8 Jul 2026 21:46:16 +0900 Subject: [PATCH 032/142] ksmbd: preserve compression state in set basic info FILE_ATTRIBUTE_COMPRESSED is controlled by FSCTL_SET_COMPRESSION and should not be set directly through FileBasicInformation. Keep the existing compression state when updating basic attributes and ignore the compressed bit supplied by the client in the basic information request. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index efd67d5c684d..46e492595e87 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7031,6 +7031,8 @@ static int set_file_basic_info(struct ksmbd_file *fp, struct file *filp; struct inode *inode; struct mnt_idmap *idmap; + __le32 attrs_mask = FILE_ATTRIBUTE_DIRECTORY_LE | + FILE_ATTRIBUTE_COMPRESSED_LE; int rc = 0; if (!(fp->daccess & FILE_WRITE_ATTRIBUTES_LE)) @@ -7068,8 +7070,9 @@ static int set_file_basic_info(struct ksmbd_file *fp, } if (!(S_ISDIR(inode->i_mode) && file_info->Attributes == FILE_ATTRIBUTE_NORMAL_LE)) - fp->f_ci->m_fattr = file_info->Attributes | - (fp->f_ci->m_fattr & FILE_ATTRIBUTE_DIRECTORY_LE); + fp->f_ci->m_fattr = + (file_info->Attributes & ~FILE_ATTRIBUTE_COMPRESSED_LE) | + (fp->f_ci->m_fattr & attrs_mask); } if (test_share_config_flag(share, KSMBD_SHARE_FLAG_STORE_DOS_ATTRS) && From 962b9df4720f468fc4577026687debba7d421a60 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 8 Jul 2026 21:55:15 +0900 Subject: [PATCH 033/142] ksmbd: preserve compression state across opens The compression state can be emulated with the DOS attribute xattr when the backing filesystem cannot store it directly. Do not limit that state to shares with store dos attributes enabled, otherwise a file reopened through another handle can lose FILE_ATTRIBUTE_COMPRESSED in file information responses. Load only the compressed bit from the DOS attribute xattr when store dos attributes is disabled. Keep the normal DOS attribute behavior unchanged when it is enabled. Also avoid clearing an already restored compressed bit just because the backing filesystem does not report FS_COMPR_FL. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 22 +++++++++++----------- fs/smb/server/vfs.c | 6 +----- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 46e492595e87..a2ce35ca42c1 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2975,10 +2975,6 @@ static bool smb2_parent_compressed(struct ksmbd_tree_connect *tcon, goto out; } - if (!test_share_config_flag(tcon->share_conf, - KSMBD_SHARE_FLAG_STORE_DOS_ATTRS)) - goto out; - rc = ksmbd_vfs_get_dos_attrib_xattr(mnt_idmap(path->mnt), parent, &da); if (rc > 0 && da.attr & FILE_ATTRIBUTE_COMPRESSED) compressed = true; @@ -2992,15 +2988,13 @@ static void smb2_update_xattrs(struct ksmbd_tree_connect *tcon, const struct path *path, struct ksmbd_file *fp) { struct xattr_dos_attrib da; + bool store_dos_attrs = test_share_config_flag(tcon->share_conf, + KSMBD_SHARE_FLAG_STORE_DOS_ATTRS); int rc; fp->f_ci->m_fattr &= ~(FILE_ATTRIBUTE_HIDDEN_LE | FILE_ATTRIBUTE_SYSTEM_LE); /* get FileAttributes from XATTR_NAME_DOS_ATTRIBUTE */ - if (!test_share_config_flag(tcon->share_conf, - KSMBD_SHARE_FLAG_STORE_DOS_ATTRS)) - return; - rc = ksmbd_vfs_get_dos_attrib_xattr(mnt_idmap(path->mnt), path->dentry, &da); if (rc > 0) { @@ -3014,9 +3008,15 @@ static void smb2_update_xattrs(struct ksmbd_tree_connect *tcon, */ if (!(server_conf.share_fake_fscaps & FILE_SUPPORTS_SPARSE_FILES)) da.attr &= ~FILE_ATTRIBUTE_SPARSE_FILE; - fp->f_ci->m_fattr = cpu_to_le32(da.attr); - fp->create_time = da.create_time; - fp->itime = da.itime; + if (store_dos_attrs) { + fp->f_ci->m_fattr = cpu_to_le32(da.attr); + fp->create_time = da.create_time; + fp->itime = da.itime; + } else if (da.attr & FILE_ATTRIBUTE_COMPRESSED) { + fp->f_ci->m_fattr |= FILE_ATTRIBUTE_COMPRESSED_LE; + } else { + fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; + } } } diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 29fa0e62e72e..df09d6cf5111 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -2013,8 +2013,6 @@ void ksmbd_vfs_update_compressed_fattr(struct dentry *dentry, __le32 *fattr) if (fa.flags & FS_COMPR_FL) *fattr |= FILE_ATTRIBUTE_COMPRESSED_LE; - else - *fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; } int ksmbd_vfs_get_compression(struct ksmbd_file *fp, u16 *fmt) @@ -2106,9 +2104,7 @@ static int __ksmbd_vfs_set_compression(struct ksmbd_work *work, 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)) { + if (fp->f_ci->m_fattr != old_fattr) { struct xattr_dos_attrib da = {0}; rc = ksmbd_vfs_get_dos_attrib_xattr(idmap, dentry, &da); From 159e727f763d21217ad9931be2c5599aad01f611 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 8 Jul 2026 22:12:20 +0900 Subject: [PATCH 034/142] ksmbd: persist FSCTL_SET_SPARSE state Advertise FILE_SUPPORTS_SPARSE_FILES so clients can use FSCTL_SET_SPARSE. Do not mark regular files sparse just because sparse support is advertised; FILE_ATTRIBUTE_SPARSE_FILE should reflect the state set by FSCTL_SET_SPARSE. Persist the sparse attribute in the DOS attribute xattr regardless of the store dos attributes setting. Restore the sparse and compressed bits from that xattr when only those emulated attributes need to be preserved. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a2ce35ca42c1..4dce10b83527 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -948,9 +948,6 @@ static int smb2_get_dos_mode(struct kstat *stat, int attribute) } else { attr = (attribute & 0x00005137) | FILE_ATTRIBUTE_ARCHIVE; attr &= ~(FILE_ATTRIBUTE_DIRECTORY); - if (S_ISREG(stat->mode) && (server_conf.share_fake_fscaps & - FILE_SUPPORTS_SPARSE_FILES)) - attr |= FILE_ATTRIBUTE_SPARSE_FILE; if (smb2_get_reparse_tag_special_file(stat->mode)) attr |= FILE_ATTRIBUTE_REPARSE_POINT; @@ -2998,24 +2995,18 @@ static void smb2_update_xattrs(struct ksmbd_tree_connect *tcon, rc = ksmbd_vfs_get_dos_attrib_xattr(mnt_idmap(path->mnt), path->dentry, &da); if (rc > 0) { - /* - * Don't report a stale SPARSE bit (e.g. left over from a - * previous client, or from before the share was reconfigured) - * when the share isn't currently advertising sparse-file - * support. Time Machine sparsebundle band files rely on - * sparse status being accurate, since macOS decides whether - * to use FSCTL_SET_SPARSE based on it. - */ - if (!(server_conf.share_fake_fscaps & FILE_SUPPORTS_SPARSE_FILES)) - da.attr &= ~FILE_ATTRIBUTE_SPARSE_FILE; if (store_dos_attrs) { fp->f_ci->m_fattr = cpu_to_le32(da.attr); fp->create_time = da.create_time; fp->itime = da.itime; - } else if (da.attr & FILE_ATTRIBUTE_COMPRESSED) { - fp->f_ci->m_fattr |= FILE_ATTRIBUTE_COMPRESSED_LE; } else { - fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; + fp->f_ci->m_fattr &= + ~(FILE_ATTRIBUTE_COMPRESSED_LE | + FILE_ATTRIBUTE_SPARSE_FILE_LE); + fp->f_ci->m_fattr |= + cpu_to_le32(da.attr & + (FILE_ATTRIBUTE_COMPRESSED | + FILE_ATTRIBUTE_SPARSE_FILE)); } } } @@ -6314,6 +6305,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, FILE_PERSISTENT_ACLS | FILE_UNICODE_ON_DISK | FILE_FILE_COMPRESSION | + FILE_SUPPORTS_SPARSE_FILES | FILE_SUPPORTS_BLOCK_REFCOUNTING; err = vfs_fileattr_get(path.dentry, &fa); @@ -9190,18 +9182,22 @@ static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id, else fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_SPARSE_FILE_LE; - if (fp->f_ci->m_fattr != old_fattr && - test_share_config_flag(work->tcon->share_conf, - KSMBD_SHARE_FLAG_STORE_DOS_ATTRS)) { + if (fp->f_ci->m_fattr != old_fattr) { const struct cred *saved_cred; - struct xattr_dos_attrib da; + struct xattr_dos_attrib da = {0}; ret = ksmbd_vfs_get_dos_attrib_xattr(idmap, fp->filp->f_path.dentry, &da); - if (ret <= 0) - goto out; + if (ret <= 0) { + da.version = 4; + da.itime = fp->itime; + da.create_time = fp->create_time; + da.flags = XATTR_DOSINFO_CREATE_TIME | + XATTR_DOSINFO_ITIME; + } da.attr = le32_to_cpu(fp->f_ci->m_fattr); + da.flags |= XATTR_DOSINFO_ATTRIB; saved_cred = override_creds(fp->filp->f_cred); ret = ksmbd_vfs_set_dos_attrib_xattr(idmap, &fp->filp->f_path, From 31169f41778a08407af332df6a32e9a1a4e5807e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 8 Jul 2026 22:17:44 +0900 Subject: [PATCH 035/142] ksmbd: reject FSCTL_SET_SPARSE on directories FSCTL_SET_SPARSE applies to files. Return STATUS_INVALID_PARAMETER when a client sends it for a directory handle instead of setting the sparse file attribute on the directory. Signed-off-by: Namjae Jeon --- 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 4dce10b83527..ab3ad2a358cc 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9169,6 +9169,11 @@ static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id, if (!fp) return -ENOENT; + if (S_ISDIR(file_inode(fp->filp)->i_mode)) { + ret = -EINVAL; + goto out; + } + if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_WRITE_ATTRIBUTES_LE))) { ret = -EACCES; goto out; From 86c84cc760080929b7be44bceebe5854957b99da Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 8 Jul 2026 22:21:25 +0900 Subject: [PATCH 036/142] ksmbd: allow FSCTL_SET_SPARSE without input buffer FSCTL_SET_SPARSE without an input buffer sets a file sparse. Treat a zero-length input buffer as SetSparse=true and keep rejecting truncated non-empty buffers. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index ab3ad2a358cc..dc49e41e1a14 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9487,15 +9487,21 @@ int smb2_ioctl(struct ksmbd_work *work) rsp); break; case FSCTL_SET_SPARSE: - if (in_buf_len < sizeof(struct file_sparse)) { + { + struct file_sparse sparse = {0}; + + if (in_buf_len && in_buf_len < sizeof(struct file_sparse)) { ret = -EINVAL; goto out; } - ret = fsctl_set_sparse(work, id, (struct file_sparse *)buffer); + *(u8 *)&sparse = 1; + ret = fsctl_set_sparse(work, id, in_buf_len ? + (struct file_sparse *)buffer : &sparse); if (ret < 0) goto out; break; + } case FSCTL_SET_ZERO_DATA: { struct file_zero_data_information *zero_data; From 7f07791522a251e2be087d11ae9cd51eb3408f1d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 9 Jul 2026 20:08:10 +0900 Subject: [PATCH 037/142] ksmbd: handle empty QUERY_ALLOCATED_RANGES output FSCTL_QUERY_ALLOCATED_RANGES can be issued with a valid input buffer but without room for an output range. Do not reject the request before looking at the file layout. If the query would produce a range, return STATUS_BUFFER_TOO_SMALL. If it produces no ranges, return success with an empty output. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index dc49e41e1a14..41d60c214a07 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9085,8 +9085,6 @@ static int fsctl_query_allocated_ranges(struct ksmbd_work *work, u64 id, int ret = 0; *out_count = 0; - if (in_count == 0) - return -EINVAL; start = le64_to_cpu(qar_req->file_offset); length = le64_to_cpu(qar_req->length); @@ -9098,8 +9096,18 @@ static int fsctl_query_allocated_ranges(struct ksmbd_work *work, u64 id, if (!fp) return -ENOENT; - ret = ksmbd_vfs_fqar_lseek(fp, start, length, - qar_rsp, in_count, out_count); + if (!in_count) { + struct file_allocated_range_buffer range; + + ret = ksmbd_vfs_fqar_lseek(fp, start, length, &range, 1, + out_count); + if (!ret && *out_count) + ret = -ENOSPC; + *out_count = 0; + } else { + ret = ksmbd_vfs_fqar_lseek(fp, start, length, + qar_rsp, in_count, out_count); + } if (ret && ret != -E2BIG) *out_count = 0; From 445b2244b6990c91655f9032b2bccad802711f99 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Fri, 10 Jul 2026 09:17:06 +0900 Subject: [PATCH 038/142] ksmbd: route stream FileDispositionInformation through stream delete flag ksmbd_fd_set_delete_pending()/ksmbd_fd_clear_delete_pending() to keep a stream's FileDispositionInformation from marking the whole file for deletion, but used the inode-wide S_DEL_ON_CLS_STREAM flag to do it -- the exact same problem class the commit was fixing, one level up. S_DEL_ON_CLS_STREAM lives on the shared ksmbd_inode, not on any specific stream handle. If a file has multiple stream handles open and one gets marked delete-pending via FileDispositionInformation, the flag can't record *which* stream should be deleted: whichever stream handle happens to close first (not necessarily the one that was actually marked) sees S_DEL_ON_CLS_STREAM set and has its xattr removed. Two clients (or two handles from the same client) touching different streams on the same file can end up deleting the wrong one. ksmbd_inode_pending_delete() has the same issue: it only checks S_DEL_PENDING, which is never set for a stream handle, so a client querying FileStandardInformation.DeletePending on a stream marked via this path would incorrectly see 0. Track this per-handle instead (stream_del_pending on struct ksmbd_file), matching the file itself rather than the shared inode. ksmbd_fd_set_delete_on_close() (the CREATE-time FILE_DELETE_ON_CLOSE option, a separate call path from FileDispositionInformation) still uses the inode-wide flag; __ksmbd_inode_close() now checks both, since either one should trigger removing the stream's xattr on close. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/vfs_cache.c | 51 +++++++++++++++++++++++++++++++-------- fs/smb/server/vfs_cache.h | 7 ++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index b53912ce5e2a..e014c880b88b 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -229,6 +229,22 @@ bool ksmbd_inode_pending_delete(struct ksmbd_file *fp) struct ksmbd_inode *ci = fp->f_ci; int ret; + /* + * Stream delete-pending is tracked per-handle (see + * ksmbd_fd_set_delete_pending()), not on the shared inode -- the + * whole-file flags checked below would never see it set, and would + * also incorrectly report a whole-file pending-delete as applying + * to an unrelated stream handle on the same inode. + */ + if (ksmbd_stream_fd(fp)) { + bool pending; + + spin_lock(&fp->f_lock); + pending = fp->stream_del_pending; + spin_unlock(&fp->f_lock); + return pending; + } + down_read(&ci->m_lock); ret = (ci->m_flags & S_DEL_PENDING); up_read(&ci->m_lock); @@ -299,15 +315,19 @@ void ksmbd_fd_set_delete_on_close(struct ksmbd_file *fp, * mark the stream for deletion, not the whole file -- otherwise * deleting a single alternate data stream (e.g. AFP_AfpInfo) deletes * the entire file's data along with it. + * + * This is tracked on fp itself (stream_del_pending), not the shared + * ksmbd_inode: the inode-wide S_DEL_ON_CLS_STREAM flag used by + * ksmbd_fd_set_delete_on_close() can't record *which* stream should be + * deleted, so if a different stream handle on the same file closed + * first, it would delete the wrong stream. */ void ksmbd_fd_set_delete_pending(struct ksmbd_file *fp) { - struct ksmbd_inode *ci = fp->f_ci; - if (ksmbd_stream_fd(fp)) { - down_write(&ci->m_lock); - ci->m_flags |= S_DEL_ON_CLS_STREAM; - up_write(&ci->m_lock); + spin_lock(&fp->f_lock); + fp->stream_del_pending = true; + spin_unlock(&fp->f_lock); } else { ksmbd_set_inode_pending_delete(fp); } @@ -315,12 +335,10 @@ void ksmbd_fd_set_delete_pending(struct ksmbd_file *fp) void ksmbd_fd_clear_delete_pending(struct ksmbd_file *fp) { - struct ksmbd_inode *ci = fp->f_ci; - if (ksmbd_stream_fd(fp)) { - down_write(&ci->m_lock); - ci->m_flags &= ~S_DEL_ON_CLS_STREAM; - up_write(&ci->m_lock); + spin_lock(&fp->f_lock); + fp->stream_del_pending = false; + spin_unlock(&fp->f_lock); } else { ksmbd_clear_inode_pending_delete(fp); } @@ -446,6 +464,19 @@ static void __ksmbd_inode_close(struct ksmbd_file *fp) } up_write(&ci->m_lock); + /* + * Per-handle delete-pending from ksmbd_fd_set_delete_pending() + * (FileDispositionInformation on this stream) -- separate from + * the inode-wide flag above, which only ever meant "some + * stream on this file" with no way to say which one. + */ + spin_lock(&fp->f_lock); + if (fp->stream_del_pending) { + fp->stream_del_pending = false; + remove_stream_xattr = true; + } + spin_unlock(&fp->f_lock); + if (remove_stream_xattr) { const struct cred *saved_cred; diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 111a4e315499..f796b6edc69b 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -118,6 +118,13 @@ struct ksmbd_file { struct list_head node; struct list_head blocked_works; struct list_head lock_list; + /* + * Per-handle FileDispositionInformation delete-pending state for a + * stream handle -- separate from ksmbd_inode's inode-wide m_flags, + * which have no way to record which stream on a multi-stream file + * was actually marked for deletion. See ksmbd_fd_set_delete_pending(). + */ + bool stream_del_pending; unsigned int durable_timeout; unsigned int durable_scavenger_timeout; From c1d7bbfc5e081875053723d1a69eb2cf341eaaf1 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 02:01:06 +0200 Subject: [PATCH 039/142] ksmbd: fix durable handle v2 default timeout units (60 -> 60000) When a client's Durable Handle Request V2 sets Timeout=0 ("let the server choose"), fp->durable_timeout was set to 60. Every other use of this field is in milliseconds: DURABLE_HANDLE_MAX_TIMEOUT (300000) in smb2pdu.h, the nonzero branch immediately above (min_t(unsigned int, dh_info.timeout, DURABLE_HANDLE_MAX_TIMEOUT), where dh_info.timeout is the wire value and already milliseconds per spec), and the scavenger in vfs_cache.c, which adds it directly to jiffies_to_msecs(jiffies). 60 is off by 1000x: the handle becomes scavenger-eligible 60 milliseconds after close instead of 60 seconds. A client requesting Timeout=0 is relying entirely on the server's default to cover the gap between a dropped connection and its reconnect -- 60ms is not enough time for even a fast network blip to be detected and reconnected, so any real disruption loses the race and a subsequent DH2C reconnect fails with a durable-handle lookup miss instead of succeeding. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 41d60c214a07..a05f2ccf0b3e 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4186,10 +4186,17 @@ int smb2_open(struct ksmbd_work *work) min_t(unsigned int, dh_info.timeout, DURABLE_HANDLE_MAX_TIMEOUT); else - fp->durable_timeout = 60; + fp->durable_timeout = 60000; } } + /* + * conn->is_aapl detection above (this function's create-context + * parsing) is skipped on the reconnect path below, since a + * reconnect always arrives on a fresh connection -- if the client + * cares, it sends its own AAPL context on this same CREATE, which + * this function's normal (non-reconnect) parsing already handles. + */ reconnected_fp: rsp->StructureSize = cpu_to_le16(89); opinfo = opinfo_get(fp); From 5838cfd6111ae5abe842babf194c54cd3b719f92 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 02:01:07 +0200 Subject: [PATCH 040/142] ksmbd: validate out_buf_len before FSCTL_CREATE_OR_GET_OBJECT_ID and FSCTL_GET_REPARSE_POINT writes Both cases write a fixed-size response structure into rsp->Buffer without first checking that out_buf_len (the space smb2_ioctl() actually has available, computed by smb2_calc_max_out_buf_len() from the client's OutputBufferLength minus space already consumed earlier in a compound request) is large enough. Every comparable case in this same switch (FSCTL_SRV_ENUMERATE_SNAPSHOTS, FSCTL_GET_COMPRESSION, FSCTL_VALIDATE_NEGOTIATE_INFO, FSCTL_SRV_REQUEST_RESUME_KEY, FSCTL_SRV_COPYCHUNK) validates this first; these two don't. A client can send a compound SMB2 request where an earlier command in the same compound chain consumes most of work->response_buf, leaving smb2_calc_max_out_buf_len() only a few bytes of out_buf_len for a trailing FSCTL_CREATE_OR_GET_OBJECT_ID or FSCTL_GET_REPARSE_POINT. Both then unconditionally write their full fixed-size structure (64 bytes and 8 bytes respectively) at rsp->Buffer[0] regardless, overflowing past the actual remaining space in the response buffer. Add the same out_buf_len check used by every other fixed-size-response case in this function, before the write. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a05f2ccf0b3e..f02520f54c6f 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9396,6 +9396,11 @@ int smb2_ioctl(struct ksmbd_work *work) struct file_object_buf_type1_ioctl_rsp *obj_buf; struct ksmbd_file *fp; + if (out_buf_len < sizeof(struct file_object_buf_type1_ioctl_rsp)) { + ret = -EINVAL; + goto out; + } + fp = ksmbd_lookup_fd_fast(work, id); if (!fp) { ret = -EBADF; @@ -9591,6 +9596,11 @@ int smb2_ioctl(struct ksmbd_work *work) struct reparse_data_buffer *reparse_ptr; struct ksmbd_file *fp; + if (out_buf_len < sizeof(struct reparse_data_buffer)) { + ret = -EINVAL; + goto out; + } + reparse_ptr = (struct reparse_data_buffer *)&rsp->Buffer[0]; fp = ksmbd_lookup_fd_fast(work, id); if (!fp) { From 439a472cb4fa5df77d595e37205115c01d887d95 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 02:01:09 +0200 Subject: [PATCH 041/142] ksmbd: zero-initialize xattr_dos_attrib in smb2_update_xattrs() ndr_decode_dos_attr() only populates da->itime for version-4 DOS attribute xattrs; for version 3 it's skipped entirely (only da->create_time is set). smb2_update_xattrs() declared da without initializing it, so fp->itime = da.itime unconditionally copies whatever was on the kernel stack for any file carrying a version-3 xattr (e.g. written by an older client or server) -- uninitialized stack memory that can later be exposed to a client via QUERY_INFO. Zero-initialize da at declaration, matching the pattern fsctl_set_sparse() already uses in this same file. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- 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 f02520f54c6f..1cb570bbde8a 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2984,7 +2984,7 @@ static bool smb2_parent_compressed(struct ksmbd_tree_connect *tcon, static void smb2_update_xattrs(struct ksmbd_tree_connect *tcon, const struct path *path, struct ksmbd_file *fp) { - struct xattr_dos_attrib da; + struct xattr_dos_attrib da = {}; bool store_dos_attrs = test_share_config_flag(tcon->share_conf, KSMBD_SHARE_FLAG_STORE_DOS_ATTRS); int rc; From 86f901803080056668cdaf23b01c8e81d2e77956 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 02:01:10 +0200 Subject: [PATCH 042/142] ksmbd: don't check directory emptiness when deleting a stream set_file_disposition_info() checks S_ISDIR(inode->i_mode) && ksmbd_vfs_empty_dir(fp) == -ENOTEMPTY to refuse deleting a non-empty directory. A stream handle's fp->filp refers to the same underlying inode as its base file or directory (streams are xattr-backed on that same inode), so this check also fires when the target is actually a stream attached to a directory, not the directory itself -- deleting the stream then incorrectly fails with -EBUSY whenever the directory happens to be non-empty, even though removing an xattr has nothing to do with the directory's contents. Skip the directory-emptiness check for stream handles, matching how ksmbd_stream_fd() is already used elsewhere in this function. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- 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 1cb570bbde8a..1a81391e95ef 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7253,7 +7253,7 @@ static int set_file_disposition_info(struct ksmbd_work *work, if (ksmbd_has_stream_without_delete_share(fp)) return -ESHARE; - if (S_ISDIR(inode->i_mode) && + if (S_ISDIR(inode->i_mode) && !ksmbd_stream_fd(fp) && ksmbd_vfs_empty_dir(fp) == -ENOTEMPTY) return -EBUSY; smb_break_all_levII_oplock_for_delete(work, fp); From bfdf81c62f4f52a3626ea62db0744def6778e83f Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 02:01:11 +0200 Subject: [PATCH 043/142] ksmbd: skip fallocate for SMB2_CREATE_ALLOCATION_SIZE on a stream handle smb2_open() calls vfs_fallocate(fp->filp, ...) unconditionally when a client's CREATE request includes an AllocationSize create context. For a stream handle, fp->filp refers to the base file's data fork (streams are xattr-backed on the same underlying file, not separate files), so this pre-allocates storage on the base file's actual data instead of doing anything meaningful for the stream -- fallocate has no applicability to an xattr-backed stream at all. Skip the fallocate call for stream handles. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 1a81391e95ef..587cc095b07b 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4097,13 +4097,22 @@ int smb2_open(struct ksmbd_work *work) ksmbd_debug(SMB, "request smb2 create allocate size : %llu\n", alloc_size); - smb_break_all_levII_oplock(work, fp, 1); - err = vfs_fallocate(fp->filp, FALLOC_FL_KEEP_SIZE, 0, - alloc_size); - if (err < 0) - ksmbd_debug(SMB, - "vfs_fallocate is failed : %d\n", - err); + /* + * fp->filp is the base file's data fork for a stream + * handle (streams are xattr-backed on the same + * underlying file) -- fallocate has no meaning for a + * stream and would otherwise pre-allocate storage on + * the base file's data instead. + */ + if (!ksmbd_stream_fd(fp)) { + smb_break_all_levII_oplock(work, fp, 1); + err = vfs_fallocate(fp->filp, FALLOC_FL_KEEP_SIZE, 0, + alloc_size); + if (err < 0) + ksmbd_debug(SMB, + "vfs_fallocate is failed : %d\n", + err); + } } context = smb2_find_context_vals(req, SMB2_CREATE_QUERY_ON_DISK_ID, 4); From 1c3ebf832d010c08afe1359215d4121cb1ed2de5 Mon Sep 17 00:00:00 2001 From: Enzo Matsumiya Date: Wed, 8 Jul 2026 09:59:12 -0300 Subject: [PATCH 044/142] smb: server: fix leak of ksmbd_ipc_login_request_ext() returned buffer Free it unconditionally after ksmbd_alloc_user() calls. kmemleak splat: unreferenced object 0xffff888103b83540 (size 192): comm "pool-0", pid 16970, jiffies 4377290937 hex dump (first 32 bytes): 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ backtrace (crc 408ccc66): __kvmalloc_node_noprof+0x730/0x920 handle_generic_event+0xec/0x1a0 [ksmbd] genl_family_rcv_msg_doit+0xe0/0x130 genl_rcv_msg+0x181/0x290 netlink_rcv_skb+0x4f/0x100 genl_rcv+0x28/0x40 netlink_unicast+0x1e6/0x2c0 netlink_sendmsg+0x20a/0x450 ____sys_sendmsg+0x2e8/0x310 ___sys_sendmsg+0x78/0xc0 __sys_sendmsg+0x63/0xc0 do_syscall_64+0xa1/0x670 entry_SYSCALL_64_after_hwframe+0x76/0x7e Fixes: a77e0e02af1c ("ksmbd: add support for supplementary groups") Signed-off-by: Enzo Matsumiya Signed-off-by: Namjae Jeon --- fs/smb/server/auth.c | 1 + fs/smb/server/mgmt/user_config.c | 1 + 2 files changed, 2 insertions(+) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index 4e7b6f0e6b8c..f2100e3ec54c 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -439,6 +439,7 @@ int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob, resp_ext = ksmbd_ipc_login_request_ext(resp->login_response.account); user = ksmbd_alloc_user(&resp->login_response, resp_ext); + kvfree(resp_ext); if (!user) { ksmbd_debug(AUTH, "login failure\n"); retval = -ENOMEM; diff --git a/fs/smb/server/mgmt/user_config.c b/fs/smb/server/mgmt/user_config.c index cf45841d9d1b..03184a3303b9 100644 --- a/fs/smb/server/mgmt/user_config.c +++ b/fs/smb/server/mgmt/user_config.c @@ -26,6 +26,7 @@ struct ksmbd_user *ksmbd_login_user(const char *account) resp_ext = ksmbd_ipc_login_request_ext(account); user = ksmbd_alloc_user(resp, resp_ext); + kvfree(resp_ext); out: kvfree(resp); return user; From 8f1b796ff1135f5660e1889973359331c61b78a0 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 14:59:56 +0900 Subject: [PATCH 045/142] ksmbd: add AAPL kAAPL_SERVER_QUERY create context support macOS clients (Finder, and specifically Time Machine's backupd) send an "AAPL" SMB2 create context on CREATE to negotiate AAPL-specific server capabilities (server_caps/vol_caps/model string). Without a response to this context, macOS Time Machine over SMB does not work at all. Add the AAPL create context structs (create_aapl_rsp, aapl_server_query_req) and create_aapl_rsp_buf(), which builds the kAAPL_SERVER_QUERY response mirroring the layout observed from macOS's own smbd, including the model string workaround: omitting the model string when the client requested it causes smbfs.kext to enter a broken disconnect path requiring a full macOS reboot to recover from. Command codes and bitmap values reuse the existing SMB2_CRTCTX_AAPL_* constants in fs/smb/common/smb2pdu.h. Wire format confirmed against AAPL's published public client kernel source (public client behavior reference) -- every field here and every SMB2_CRTCTX_AAPL_* constant matches exactly. Hook the request parsing and response into smb2_open()'s existing create-context handling, following the same DataOffset+DataLength bounds-checking convention already used by every other context parser in this file. The AAPL model string is configurable via the existing netlink startup path (server_conf.aapl_model, default "Xserve"). This is scoped to shares with the new KSMBD_SHARE_FLAG_TIME_MACHINE flag only, not enabled globally -- AAPL's AAPL extension is undocumented, so containing its blast radius to shares that explicitly opt in limits risk to ordinary SMB shares. conn->aapl_readdir_attr is set here when the client also advertises READDIR_ATTR support, but the actual inline-FinderInfo wire format (the feature that flag gates) is not implemented yet -- follow-up commit. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/common/smbglob.h | 1 + fs/smb/server/connection.h | 1 + fs/smb/server/ksmbd_netlink.h | 4 +- fs/smb/server/oplock.c | 77 +++++++++++++++++++++++++++++++++++ fs/smb/server/oplock.h | 1 + fs/smb/server/server.h | 2 + fs/smb/server/smb2ops.c | 4 ++ fs/smb/server/smb2pdu.c | 56 ++++++++++++++++++++++++- fs/smb/server/smb2pdu.h | 59 +++++++++++++++++++++++++++ fs/smb/server/transport_ipc.c | 9 ++++ 10 files changed, 212 insertions(+), 2 deletions(-) diff --git a/fs/smb/common/smbglob.h b/fs/smb/common/smbglob.h index 4e33d91cdc9d..d9c7e6e7af29 100644 --- a/fs/smb/common/smbglob.h +++ b/fs/smb/common/smbglob.h @@ -39,6 +39,7 @@ struct smb_version_values { size_t create_mxac_size; size_t create_disk_id_size; size_t create_posix_size; + size_t create_aapl_size; }; static inline unsigned int get_rfc1002_len(void *buf) diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 11e18217258e..05818a165e22 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -124,6 +124,7 @@ struct ksmbd_conn { bool binding; atomic_t refcnt; bool is_aapl; + bool aapl_readdir_attr; /* READDIR_ATTR negotiated */ struct work_struct release_work; }; diff --git a/fs/smb/server/ksmbd_netlink.h b/fs/smb/server/ksmbd_netlink.h index c9e1b0b689d7..1ea0a367b396 100644 --- a/fs/smb/server/ksmbd_netlink.h +++ b/fs/smb/server/ksmbd_netlink.h @@ -113,7 +113,8 @@ struct ksmbd_startup_request { __u32 max_connections; /* Number of maximum simultaneous connections */ __s8 bind_interfaces_only; __u32 max_ip_connections; /* Number of maximum connection per ip address */ - __s8 reserved[499]; /* Reserved room */ + __s8 aapl_model[32]; /* AAPL model string for Finder icon, e.g. "Xserve" */ + __s8 reserved[467]; /* Reserved room */ __u32 ifc_list_sz; /* interfaces list size */ __s8 ____payload[]; } __packed; @@ -378,6 +379,7 @@ enum KSMBD_TREE_CONN_STATUS { #define KSMBD_SHARE_FLAG_CROSSMNT BIT(15) #define KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY BIT(16) #define KSMBD_SHARE_FLAG_HIDE_UNREADABLE BIT(17) +#define KSMBD_SHARE_FLAG_TIME_MACHINE BIT(18) /* * Tree connect request flags. diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 79787099afdc..94fe464a85f3 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -16,6 +16,7 @@ #include "mgmt/user_session.h" #include "mgmt/share_config.h" #include "mgmt/tree_connect.h" +#include "server.h" static LIST_HEAD(lease_table_list); static DEFINE_RWLOCK(lease_list_lock); @@ -2138,6 +2139,82 @@ void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp) SIDUNIX_GROUP, (struct smb_sid *)&buf->SidBuffer[28]); } +/** + * create_aapl_rsp_buf() - build AAPL kAAPL_SERVER_QUERY response + * @cc: buffer to write the create context into (AAPL_RSP_MAX_SIZE bytes) + * @vol_caps: volume capability flags (SMB2_CRTCTX_AAPL_* volume bits) + * @req_bitmap: the client's request bitmap, echoed back in reply_bitmap + * + * Response format follows the layout observed from macOS's own smbd, and + * matches the client-side parsing in AAPL's published public client kernel + * source (public client behavior reference, kAAPL_SERVER_QUERY + * case): reply_bitmap, then server_caps/vol_caps/model-info fields present + * only when their reply_bitmap bit is set: + * reply_bitmap = req_bitmap masked to the fields we support + * server_caps = AAPL_SERVER_CAPS_KSMBD when requested + * vol_caps = caller-supplied + * model string = server_conf.aapl_model (default "Xserve") in UTF-16LE, + * when SMB2_CRTCTX_AAPL_MODEL_INFO requested + * + * Sending reply_bitmap with MODEL_INFO set but no model string causes + * smbfs.kext to enter a broken disconnect path requiring a macOS reboot. + */ +void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap) +{ + struct create_aapl_rsp *buf; + u64 reply_bitmap; + u32 data_len; + + buf = (struct create_aapl_rsp *)cc; + memset(buf, 0, AAPL_RSP_MAX_SIZE); + + reply_bitmap = req_bitmap & (SMB2_CRTCTX_AAPL_SERVER_CAPS | + SMB2_CRTCTX_AAPL_VOLUME_CAPS | + SMB2_CRTCTX_AAPL_MODEL_INFO); + + /* base data: cmd(4)+reserved(4)+reply_bitmap(8)+server_caps(8)+vol_caps(8) */ + data_len = 32; + if (reply_bitmap & SMB2_CRTCTX_AAPL_MODEL_INFO) + data_len += 4 + 4 + AAPL_MODEL_UTF16_BYTES; /* pad2+model_bytes+string */ + + buf->ccontext.DataOffset = cpu_to_le16(offsetof(struct create_aapl_rsp, cmd)); + buf->ccontext.DataLength = cpu_to_le32(data_len); + buf->ccontext.NameOffset = cpu_to_le16(offsetof(struct create_aapl_rsp, Name)); + buf->ccontext.NameLength = cpu_to_le16(SMB2_CREATE_AAPL_LEN); + buf->Name[0] = 'A'; + buf->Name[1] = 'A'; + buf->Name[2] = 'P'; + buf->Name[3] = 'L'; + + buf->cmd = cpu_to_le32(SMB2_CRTCTX_AAPL_SERVER_QUERY); + buf->reply_bitmap = cpu_to_le64(reply_bitmap); + buf->server_caps = (reply_bitmap & SMB2_CRTCTX_AAPL_SERVER_CAPS) ? + cpu_to_le64(AAPL_SERVER_CAPS_KSMBD) : 0; + buf->vol_caps = (reply_bitmap & SMB2_CRTCTX_AAPL_VOLUME_CAPS) ? + cpu_to_le64(vol_caps) : 0; + + if (reply_bitmap & SMB2_CRTCTX_AAPL_MODEL_INFO) { + __le32 *p = (__le32 *)((u8 *)buf + sizeof(*buf)); + __le16 *model_str = (__le16 *)(p + 2); + const char *src = server_conf.aapl_model[0] ? + server_conf.aapl_model : "Xserve"; + int i, model_bytes = 0; + + /* Convert ASCII model string to UTF-16LE in-place */ + for (i = 0; src[i] && i < AAPL_MODEL_MAX_CHARS; i++) { + model_str[i] = cpu_to_le16((unsigned char)src[i]); + model_bytes += 2; + } + + p[0] = 0; /* pad2 */ + p[1] = cpu_to_le32(model_bytes); + + /* Update DataLength to reflect actual model string size */ + buf->ccontext.DataLength = + cpu_to_le32(data_len - AAPL_MODEL_UTF16_BYTES + model_bytes); + } +} + /* * Find lease object(opinfo) for given lease key/fid from lease * break/file close path. diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index 3f581d22bb67..f7f6afcc5434 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -125,6 +125,7 @@ void create_durable_v2_rsp_buf(char *cc, struct ksmbd_file *fp); void create_mxac_rsp_buf(char *cc, int maximal_access); void create_disk_id_rsp_buf(char *cc, __u64 file_id, __u64 vol_id); void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp); +void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap); struct create_context *smb2_find_context_vals(void *open_req, const char *tag, int tag_len); struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, char *lease_key); diff --git a/fs/smb/server/server.h b/fs/smb/server/server.h index b8a7317be86b..4d4d268b59d5 100644 --- a/fs/smb/server/server.h +++ b/fs/smb/server/server.h @@ -48,6 +48,8 @@ struct ksmbd_server_config { char *conf[SERVER_CONF_WORK_GROUP + 1]; struct task_struct *dh_task; bool bind_interfaces_only; + /* AAPL model string for Finder icon, e.g. "Xserve" */ + char aapl_model[32]; }; extern struct ksmbd_server_config server_conf; diff --git a/fs/smb/server/smb2ops.c b/fs/smb/server/smb2ops.c index c9a32ee096b5..97938150d2d9 100644 --- a/fs/smb/server/smb2ops.c +++ b/fs/smb/server/smb2ops.c @@ -37,6 +37,7 @@ static struct smb_version_values smb21_server_values = { .create_mxac_size = sizeof(struct create_mxac_rsp), .create_disk_id_size = sizeof(struct create_disk_id_rsp), .create_posix_size = sizeof(struct create_posix_rsp), + .create_aapl_size = AAPL_RSP_MAX_SIZE, }; static struct smb_version_values smb30_server_values = { @@ -64,6 +65,7 @@ static struct smb_version_values smb30_server_values = { .create_mxac_size = sizeof(struct create_mxac_rsp), .create_disk_id_size = sizeof(struct create_disk_id_rsp), .create_posix_size = sizeof(struct create_posix_rsp), + .create_aapl_size = AAPL_RSP_MAX_SIZE, }; static struct smb_version_values smb302_server_values = { @@ -91,6 +93,7 @@ static struct smb_version_values smb302_server_values = { .create_mxac_size = sizeof(struct create_mxac_rsp), .create_disk_id_size = sizeof(struct create_disk_id_rsp), .create_posix_size = sizeof(struct create_posix_rsp), + .create_aapl_size = AAPL_RSP_MAX_SIZE, }; static struct smb_version_values smb311_server_values = { @@ -118,6 +121,7 @@ static struct smb_version_values smb311_server_values = { .create_mxac_size = sizeof(struct create_mxac_rsp), .create_disk_id_size = sizeof(struct create_disk_id_rsp), .create_posix_size = sizeof(struct create_posix_rsp), + .create_aapl_size = AAPL_RSP_MAX_SIZE, }; static struct smb_version_ops smb2_0_server_ops = { diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 587cc095b07b..423a70c4024e 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3351,6 +3351,8 @@ int smb2_open(struct ksmbd_work *work) int rc = 0; int contxt_cnt = 0, query_disk_id = 0; bool maximal_access_ctxt = false, posix_ctxt = false; + bool aapl_ctxt = false; + __u64 aapl_req_bitmap = 0, aapl_client_caps = 0; int s_type = 0; int next_off = 0; char *name = NULL; @@ -4124,7 +4126,32 @@ int smb2_open(struct ksmbd_work *work) query_disk_id = 1; } - if (conn->is_aapl == false) { + if (test_share_config_flag(share, KSMBD_SHARE_FLAG_TIME_MACHINE)) { + context = smb2_find_context_vals(req, SMB2_CREATE_AAPL, 4); + if (IS_ERR(context)) { + rc = PTR_ERR(context); + goto err_out1; + } else if (context) { + struct aapl_server_query_req *aapl_req; + + if (le32_to_cpu(context->DataLength) < + sizeof(struct aapl_server_query_req)) { + rc = -EINVAL; + goto err_out1; + } + + aapl_req = (struct aapl_server_query_req *) + ((char *)context + + le16_to_cpu(context->DataOffset)); + if (le32_to_cpu(aapl_req->cmd) == + SMB2_CRTCTX_AAPL_SERVER_QUERY) { + conn->is_aapl = true; + aapl_ctxt = true; + aapl_req_bitmap = le64_to_cpu(aapl_req->req_bitmap); + aapl_client_caps = le64_to_cpu(aapl_req->client_caps); + } + } + } else if (conn->is_aapl == false) { context = smb2_find_context_vals(req, SMB2_CREATE_AAPL, 4); if (IS_ERR(context)) { rc = PTR_ERR(context); @@ -4338,6 +4365,10 @@ int smb2_open(struct ksmbd_work *work) } if (posix_ctxt) { + struct create_context *posix_ccontext; + + posix_ccontext = (struct create_context *)(rsp->Buffer + + le32_to_cpu(rsp->CreateContextsLength)); contxt_cnt++; create_posix_rsp_buf(rsp->Buffer + le32_to_cpu(rsp->CreateContextsLength), @@ -4347,6 +4378,29 @@ int smb2_open(struct ksmbd_work *work) iov_len += conn->vals->create_posix_size; if (next_ptr) *next_ptr = cpu_to_le32(next_off); + next_ptr = &posix_ccontext->Next; + next_off = conn->vals->create_posix_size; + } + + /* + * AAPL create context response: see smb2pdu.h for the capability + * rationale. Scoped to TIME_MACHINE shares only. + */ + if (aapl_ctxt) { + if (aapl_client_caps & SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) + conn->aapl_readdir_attr = true; + + contxt_cnt++; + create_aapl_rsp_buf(rsp->Buffer + + le32_to_cpu(rsp->CreateContextsLength), + SMB2_CRTCTX_AAPL_FULL_SYNC, + aapl_req_bitmap); + le32_add_cpu(&rsp->CreateContextsLength, + conn->vals->create_aapl_size); + iov_len += conn->vals->create_aapl_size; + if (next_ptr) + *next_ptr = cpu_to_le32(next_off); + /* AAPL is last; next_ptr need not be updated */ } if (contxt_cnt > 0) { diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index eadab043bc40..b0daca928c44 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -66,6 +66,65 @@ struct preauth_integrity_info { /* Apple Defined Contexts */ #define SMB2_CREATE_AAPL "AAPL" +/* + * AAPL SMB2 extension -- kAAPL_SERVER_QUERY create context. + * + * Command code and bitmap values are the existing + * SMB2_CRTCTX_AAPL_* constants in fs/smb/common/smb2pdu.h. + * + * Omitting the model string when reply_bitmap includes + * SMB2_CRTCTX_AAPL_MODEL_INFO causes smbfs.kext to enter a broken + * disconnect path requiring a reboot. + * + * Layout: ccontext(16) + Name[4] + Pad[4] + cmd(4) + reserved(4) + + * reply_bitmap(8) + server_caps(8) + vol_caps(8) + * When MODEL_INFO requested, appended: pad2(4) + model_bytes(4) + UTF-16LE + */ +#define SMB2_CREATE_AAPL_LEN 4 + +/* + * Server capability flags (server_caps field) -- SMB2_CRTCTX_AAPL_UNIX_BASED: + * prevents macOS Windows-compat mode (question-mark icons). + * SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE: enables server-side file copy via + * FSCTL_SRV_COPYCHUNK. SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR: inline + * FinderInfo per FIND entry, set when client also advertises the bit; + * format: EaSize=max_access, ShortName[0..7]=rfork_size, + * ShortName[8..23]=FinderInfo(16B), Reserved2=unix_mode. + */ +#define AAPL_SERVER_CAPS_KSMBD (SMB2_CRTCTX_AAPL_UNIX_BASED | \ + SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE | \ + SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) + +/* Model string: up to 31 ASCII chars */ +#define AAPL_MODEL_MAX_CHARS 31 +#define AAPL_MODEL_UTF16_BYTES (AAPL_MODEL_MAX_CHARS * 2) + +/* + * Max AAPL response: header(24) + base data(32) + pad2(4) + model_bytes(4) + * + model(62), 8-byte aligned: ALIGN(126, 8) = 128 bytes. + */ +#define AAPL_RSP_MAX_SIZE 128 + +/* AAPL server query request (client->server) */ +struct aapl_server_query_req { + __le32 cmd; + __le32 reserved; + __le64 req_bitmap; + __le64 client_caps; +} __packed; + +struct create_aapl_rsp { + struct create_context_hdr ccontext; + __u8 Name[4]; + __u8 Pad[4]; + __le32 cmd; + __le32 reserved; + __le64 reply_bitmap; + __le64 server_caps; + __le64 vol_caps; + /* when MODEL_INFO requested: __le32 pad2; __le32 model_bytes; __le16 model[] */ +} __packed; + #define DURABLE_HANDLE_MAX_TIMEOUT 300000 struct create_alloc_size_req { diff --git a/fs/smb/server/transport_ipc.c b/fs/smb/server/transport_ipc.c index 0c581b9624d3..bd58d3d0bad5 100644 --- a/fs/smb/server/transport_ipc.c +++ b/fs/smb/server/transport_ipc.c @@ -322,6 +322,15 @@ static int ipc_server_config_on_startup(struct ksmbd_startup_request *req) goto out; } server_conf.share_fake_fscaps = req->share_fake_fscaps; + + /* AAPL model string for Finder icon */ + if (req->aapl_model[0]) + strscpy(server_conf.aapl_model, req->aapl_model, + sizeof(server_conf.aapl_model)); + else + strscpy(server_conf.aapl_model, "Xserve", + sizeof(server_conf.aapl_model)); + ksmbd_init_domain(req->sub_auth); if (req->smb2_max_read) From eaff8e924f6094bb53291982dc4131e9ccca2232 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 02:06:08 +0200 Subject: [PATCH 046/142] ksmbd: synthesize empty AFP_AfpInfo xattr on first probe Once a server advertises the AAPL COPYFILE capability, macOS requires an AFP_AfpInfo stream on every file it looks at for Finder type/ creator/icon resolution. smb2_set_stream_name_xattr() currently returns -EBADF (STATUS_OBJECT_NAME_NOT_FOUND) when a client opens AFP_AfpInfo with FILE_OPEN disposition and the xattr doesn't exist yet, which macOS treats as fatal for that file: Finder falls back to showing a generic icon, and file operations that depend on succeeding against this stream (e.g. Cmd+D duplication) fail. Synthesize a 60-byte zeroed AFP_AfpInfo xattr (magic 0x00051607, version 0x00020000, both big-endian per the AFP_AfpInfo wire format) on first FILE_OPEN probe instead. type=0/creator=0 tells macOS to fall back to extension-based type detection, which is correct for files with no explicit Finder metadata. The synthesized xattr persists on disk, so this only pays the extra write once per file; a later genuine write from macOS (e.g. after the user assigns a custom icon) overwrites it normally. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 423a70c4024e..3b739276f503 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2857,6 +2857,30 @@ static noinline int smb2_set_stream_name_xattr(const struct path *path, return 0; if (fp->cdoption == FILE_OPEN_LE) { + if (!strcmp(stream_name, "AFP_AfpInfo") && + test_share_config_flag(fp->tcon->share_conf, + KSMBD_SHARE_FLAG_TIME_MACHINE)) { + /* + * Synthesize an empty AFP_AfpInfo xattr on first access. + * type=0/creator=0 tells macOS to use the file extension + * for icon and type detection. + * + * Scoped to TIME_MACHINE shares, matching the rest of + * the AAPL series -- conn->is_aapl alone isn't a safe + * gate here, since the pre-existing narrow UniqueId=0 + * path can also set it on ordinary, non-Time-Machine + * shares whenever a Mac client happens to negotiate + * AAPL there too. + */ + static const u8 afpinfo_empty[60] = { + 0x00, 0x05, 0x16, 0x07, /* magic 0x00051607 BE */ + 0x00, 0x02, 0x00, 0x00, /* version 0x00020000 BE */ + }; + rc = ksmbd_vfs_setxattr(idmap, path, xattr_stream_name, + (void *)afpinfo_empty, + sizeof(afpinfo_empty), 0, false); + return rc < 0 ? rc : 0; + } ksmbd_debug(SMB, "XATTR stream name lookup failed: %d\n", rc); return -EBADF; } From 9dfb2813839d3c7458cf954f76f6a9cbe18007ce Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Fri, 10 Jul 2026 08:38:14 +0900 Subject: [PATCH 047/142] ksmbd: send inline FinderInfo in FIND responses when READDIR_ATTR negotiated Without READDIR_ATTR, macOS Finder resolves type/creator/icon for every file in a directory listing by opening its AFP_AfpInfo stream individually -- one extra CREATE+QUERY_INFO+CLOSE round trip per file, which is the dominant cost of browsing a large directory over SMB from a Mac. When the client negotiates READDIR_ATTR (conn->aapl_readdir_attr, set during CREATE's AAPL context exchange), inline the same information directly into each FILEID_BOTH_DIRECTORY_INFORMATION FIND entry: EaSize = max_access, expanded specific rights (GENERIC_ALL_FLAGS), not the raw FILE_GENERIC_ALL_LE "generic" meta-bit -- that bit has none of the specific FILE_* rights macOS's smbfs.kext checks bit-by-bit, so reporting it directly would fail every access check and show Finder's "no entry" badge on every file/folder. ShortNameLength = 24 (fixed; the spec says 0 when there's no short name; kept for wire parity with reference server, see below) ShortName[0..7] = resource fork size (0 -- no resource forks) ShortName[8..23] = compressed FinderInfo (all zero: type/creator unset, client falls back to extension-based icon/type detection, consistent with the AFP_AfpInfo synthesis this mirrors) Reserved2 = Unix mode bits Reparse-point status is still carried via ExtFileAttributes rather than EaSize once READDIR_ATTR is active, since EaSize is repurposed for max_access. Reverse-engineered from macOS smbfs.kext network behavior and cross-checked against reference implementation marshalling (reference implementation behavior). Also confirmed against AAPL's published public client behavior (public client behavior reference) -- every field here matches exactly, except ShortNameLength=24: real V1 clients read but never examine that field, so it's kept for wire parity with reference server, not because macOS requires it. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 58 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 3b739276f503..7c097ae64566 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4742,17 +4742,65 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, fibdinfo = (struct file_id_both_directory_info *)kstat; fibdinfo->FileNameLength = cpu_to_le32(conv_len); - fibdinfo->EaSize = - smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); - if (fibdinfo->EaSize) - fibdinfo->ExtFileAttributes = FILE_ATTRIBUTE_REPARSE_POINT_LE; if (conn->is_aapl) fibdinfo->UniqueId = 0; else fibdinfo->UniqueId = cpu_to_le64(ksmbd_kstat->kstat->ino); fibdinfo->ShortNameLength = 0; fibdinfo->Reserved = 0; - fibdinfo->Reserved2 = cpu_to_le16(0); + if (conn->aapl_readdir_attr) { + /* + * READDIR_ATTR wire format, confirmed against reference server's + * reference implementation marshalling (reference implementation behavior): + * EaSize = max_access (expanded specific + * rights, simplified to "grant all") + * ShortNameLength = 24 (fixed; not 0, despite the spec) + * ShortName[0..7] = resource fork size (uint64 LE, 0 = no rfork) + * ShortName[8..23] = compressed FinderInfo (type+creator+flags+ + * ext_flags+date_added, 16 bytes LE; all + * zeros means type=0/creator=0, i.e. use + * the file extension for icon lookup) + * Reserved2 = Unix mode bits (uint16 LE) + * Reparse-point tag is indicated via ExtFileAttributes, not EaSize. + */ + __le32 reparse_tag = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + + if (reparse_tag) + fibdinfo->ExtFileAttributes = FILE_ATTRIBUTE_REPARSE_POINT_LE; + /* + * FILE_GENERIC_ALL_LE (0x10000000) is the raw + * "generic all" meta-bit -- valid only in a + * client's requested access mask, for the server + * to expand. It has none of the specific FILE_* + * rights bits set (FILE_LIST_DIRECTORY, FILE_TRAVERSE, + * etc.), so reporting it here as max_access would make + * macOS's bit-by-bit access checks fail on every + * entry -> permanent "no entry" badges in Finder. + * Report the actual expanded rights instead, same + * as smb_map_generic_desired_access() does when + * translating a client's GENERIC_ALL request. + */ + fibdinfo->EaSize = cpu_to_le32(GENERIC_ALL_FLAGS); + /* + * The spec says ShortNameLength should be 0 when + * there's no short name; 24 here instead matches + * reference implementation marshalling (reference + * behavior) for server-to-server wire parity. + * V2 repurposes it as a flags field that is + * interpreted; V1 doesn't. Either value is safe + * here, so keep 24 for parity. + */ + fibdinfo->ShortNameLength = 24; + memset(fibdinfo->ShortName, 0, sizeof(fibdinfo->ShortName)); + fibdinfo->Reserved2 = cpu_to_le16(ksmbd_kstat->kstat->mode & 0xffff); + } else { + fibdinfo->EaSize = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + if (fibdinfo->EaSize) + fibdinfo->ExtFileAttributes = FILE_ATTRIBUTE_REPARSE_POINT_LE; + fibdinfo->Reserved2 = cpu_to_le16(0); + } if (d_info->hide_dot_file && d_info->name[0] == '.') fibdinfo->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE; memcpy(fibdinfo->FileName, conv_name, conv_len); From 152036e875e8cfe41dc70328f7e499ccc5142d6c Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 02:06:10 +0200 Subject: [PATCH 048/142] ksmbd: defer CHANGE_NOTIFY completion instead of STATUS_NOT_IMPLEMENTED smb2_notify() currently returns STATUS_NOT_IMPLEMENTED synchronously for every CHANGE_NOTIFY request. Genuine SMB2 servers never complete a CHANGE_NOTIFY spontaneously -- it's satisfied only by a real directory change or with STATUS_NOTIFY_CLEANUP when the watched handle is closed. macOS smbfs.kext depends on this deferred-completion contract: receiving STATUS_NOT_IMPLEMENTED instead makes it hard-freeze on unmount, since it never sees the cleanup it's waiting for. Add a notify_pendings list on struct ksmbd_file (protected by the existing f_lock) and a notify_entry list_head on struct ksmbd_work to link onto it. smb2_notify() now replies STATUS_PENDING immediately and queues a deferred STATUS_NOTIFY_CLEANUP response on the watched handle; __ksmbd_close_fd() drains and sends any pending notifications when the handle is actually closed. The drain splices the list out under fp->f_lock first, then processes the detached copy without the lock -- smb2_notify() on another connection can be adding to the same list at the same time a close happens on this one, and ksmbd_conn_write() can sleep (it takes the connection's write mutex), so it must not be called while the spinlock is held. Also handle the FileId=FFFF...FFFF share-root sentinel that macOS backupd sends to watch for changes without holding an open handle -- without an immediate STATUS_PENDING/STATUS_NOTIFY_CLEANUP reply here, backupd aborts Time Machine setup with STATUS_FILE_CLOSED. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/ksmbd_work.c | 1 + fs/smb/server/ksmbd_work.h | 2 + fs/smb/server/smb2pdu.c | 226 ++++++++++++++++++++++++++++++++++++- fs/smb/server/vfs_cache.c | 48 ++++++++ fs/smb/server/vfs_cache.h | 6 + 5 files changed, 280 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index e2c2f45264be..97502273a49c 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -56,6 +56,7 @@ struct ksmbd_work *ksmbd_alloc_work_struct(void) INIT_LIST_HEAD(&work->request_entry); INIT_LIST_HEAD(&work->async_request_entry); INIT_LIST_HEAD(&work->fp_entry); + INIT_LIST_HEAD(&work->notify_entry); INIT_LIST_HEAD(&work->aux_read_list); work->iov_alloc_cnt = ARRAY_SIZE(work->iov_inline); work->iov = work->iov_inline; diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index 88104f0cf363..50c4aa779647 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -104,6 +104,8 @@ struct ksmbd_work { /* List head at conn->async_requests */ struct list_head async_request_entry; struct list_head fp_entry; + /* List head at ksmbd_file->notify_pendings */ + struct list_head notify_entry; }; /** diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 7c097ae64566..b4b6f077e272 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10109,6 +10109,88 @@ int smb2_oplock_break(struct ksmbd_work *work) return 0; } +/* + * Cancel handler for a deferred CHANGE_NOTIFY. Races against + * __ksmbd_close_fd()'s notify_pendings drain (vfs_cache.c), which can run + * concurrently on a different connection closing the same handle -- only + * one of the two may claim and free in_work, so both sides check + * list_empty() under fp->f_lock before touching it (list_del_init() + * leaves a node empty, so whichever side removes it first is the owner; + * the loser must not touch in_work again, since the winner may already be + * freeing it). + * + * smb2_cancel() holds conn->request_lock (a spinlock) for the entire + * time it walks conn->async_requests and calls this function -- so this + * runs with preemption disabled and must not sleep or re-acquire that + * same lock. release_async_work() does both (it takes conn->request_lock + * itself, and frees things that can involve sleeping paths), so calling + * it from here would self-deadlock the very thread processing the + * client's CANCEL command. ksmbd_conn_write() can also sleep (it takes + * conn's write mutex). So: do only the non-sleeping, no-relock cleanup + * inline here (the async_requests removal itself is safe without + * re-locking, since the caller already holds that lock), and defer the + * actual response send + work-struct free to a workqueue, matching the + * minimal, non-blocking style of the existing smb2_remove_blocked_lock() + * cancel_fn (which only wakes a waiter, never sends network data itself). + */ +struct notify_cancel_ctx { + struct work_struct work; + struct ksmbd_work *in_work; +}; + +static void smb2_notify_cancel_deferred(struct work_struct *w) +{ + struct notify_cancel_ctx *ctx = + container_of(w, struct notify_cancel_ctx, work); + struct ksmbd_work *in_work = ctx->in_work; + struct smb2_hdr *in_hdr; + + in_hdr = smb_get_msg(in_work->response_buf); + in_hdr->Status = STATUS_CANCELLED; + ksmbd_conn_write(in_work); + ksmbd_free_work_struct(in_work); + kfree(ctx); +} + +static void smb2_notify_cancel_fn(void **argv) +{ + struct ksmbd_work *in_work = (struct ksmbd_work *)argv[0]; + struct ksmbd_file *fp = (struct ksmbd_file *)argv[1]; + struct ksmbd_conn *conn = in_work->conn; + struct notify_cancel_ctx *ctx; + bool claimed; + + spin_lock(&fp->f_lock); + claimed = !list_empty(&in_work->notify_entry); + if (claimed) + list_del_init(&in_work->notify_entry); + spin_unlock(&fp->f_lock); + + if (!claimed) + return; + + /* conn->request_lock is already held by the caller (smb2_cancel()). */ + list_del_init(&in_work->async_request_entry); + in_work->asynchronous = false; + in_work->cancel_fn = NULL; + kfree(in_work->cancel_argv); + in_work->cancel_argv = NULL; + if (in_work->async_id) { + ksmbd_release_id(&conn->async_ida, in_work->async_id); + in_work->async_id = 0; + } + + ctx = kmalloc(sizeof(*ctx), GFP_ATOMIC); + if (!ctx) { + /* Can't defer the response -- free without sending one. */ + ksmbd_free_work_struct(in_work); + return; + } + ctx->in_work = in_work; + INIT_WORK(&ctx->work, smb2_notify_cancel_deferred); + schedule_work(&ctx->work); +} + /** * smb2_notify() - handler for smb2 notify request * @work: smb work containing notify command buffer @@ -10119,6 +10201,9 @@ int smb2_notify(struct ksmbd_work *work) { struct smb2_change_notify_req *req; struct smb2_change_notify_rsp *rsp; + struct ksmbd_work *in_work; + struct smb2_hdr *in_hdr; + struct ksmbd_file *fp; ksmbd_debug(SMB, "Received smb2 notify\n"); @@ -10133,9 +10218,144 @@ int smb2_notify(struct ksmbd_work *work) return -EIO; } - smb2_set_err_rsp(work); - rsp->hdr.Status = STATUS_NOT_IMPLEMENTED; - return -EOPNOTSUPP; + /* + * macOS backupd sends CHANGE_NOTIFY with FileId=FFFF...FFFF (share-root + * sentinel) to watch for changes on the share root without holding an + * open handle. Respond STATUS_PENDING + STATUS_NOTIFY_CLEANUP immediately; + * without this, backupd aborts Time Machine setup on STATUS_FILE_CLOSED. + */ + if (req->VolatileFileId == SMB2_NO_FID && + req->PersistentFileId == SMB2_NO_FID) { + in_work = ksmbd_alloc_work_struct(); + if (!in_work || allocate_interim_rsp_buf(in_work)) { + if (in_work) + ksmbd_free_work_struct(in_work); + rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; + smb2_set_err_rsp(work); + return 0; + } + if (setup_async_work(work, NULL, NULL)) { + ksmbd_free_work_struct(in_work); + rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; + smb2_set_err_rsp(work); + return 0; + } + smb2_send_interim_resp(work, STATUS_PENDING); + in_work->conn = work->conn; + in_hdr = smb_get_msg(in_work->response_buf); + memcpy(in_hdr, ksmbd_resp_buf_next(work), + __SMB2_HEADER_STRUCTURE_SIZE); + in_hdr->Flags |= SMB2_FLAGS_ASYNC_COMMAND; + in_hdr->Id.AsyncId = cpu_to_le64(work->async_id); + smb2_set_err_rsp(in_work); + in_hdr->Status = STATUS_NOTIFY_CLEANUP; + in_work->async_id = work->async_id; + work->async_id = 0; + release_async_work(work); + ksmbd_conn_write(in_work); + ksmbd_free_work_struct(in_work); + work->send_no_response = 1; + return 0; + } + + /* + * KSMBD does not implement a real change-notification backend. + * Genuine SMB2 servers (and macOS smbfs) never complete a + * CHANGE_NOTIFY spontaneously: it is satisfied only by a real + * directory change, or with STATUS_NOTIFY_CLEANUP when the watched + * handle is closed. Completing it early (e.g. on a timer) makes + * Finder treat the cleanup as "directory changed" and re-enumerate + * the directory forever, leaving items unopenable. Returning + * STATUS_NOT_IMPLEMENTED here (like stock ksmbd) makes macOS smbfs + * hard-freeze on unmount, so this must stay deferred. + */ + fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId); + if (!fp) { + rsp->hdr.Status = STATUS_FILE_CLOSED; + smb2_set_err_rsp(work); + return 0; + } + + in_work = ksmbd_alloc_work_struct(); + if (!in_work || allocate_interim_rsp_buf(in_work)) { + if (in_work) + ksmbd_free_work_struct(in_work); + ksmbd_fd_put(work, fp); + rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; + smb2_set_err_rsp(work); + return 0; + } + /* + * in_work is synthetic (not from the normal request-receiving + * pipeline), so it has no request_buf of its own. It gets registered + * into conn->async_requests below, and smb2_cancel() unconditionally + * computes smb_get_msg(iter->request_buf) for every entry in that + * list while searching for a match -- give it its own small buffer + * (not an alias of response_buf: ksmbd_free_work_struct() kvfree()s + * both separately, so aliasing them would double-free) so that stays + * a harmless read instead of a near-NULL dereference. + */ + in_work->request_buf = kzalloc(MAX_CIFS_SMALL_BUFFER_SIZE, KSMBD_DEFAULT_GFP); + if (!in_work->request_buf) { + ksmbd_free_work_struct(in_work); + ksmbd_fd_put(work, fp); + rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; + smb2_set_err_rsp(work); + return 0; + } + + if (setup_async_work(work, NULL, NULL)) { + ksmbd_free_work_struct(in_work); + ksmbd_fd_put(work, fp); + rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; + smb2_set_err_rsp(work); + return 0; + } + + smb2_send_interim_resp(work, STATUS_PENDING); + + in_work->conn = work->conn; + in_hdr = smb_get_msg(in_work->response_buf); + memcpy(in_hdr, ksmbd_resp_buf_next(work), __SMB2_HEADER_STRUCTURE_SIZE); + in_hdr->Flags |= SMB2_FLAGS_ASYNC_COMMAND; + in_hdr->Id.AsyncId = cpu_to_le64(work->async_id); + smb2_set_err_rsp(in_work); + in_hdr->Status = STATUS_NOTIFY_CLEANUP; + + /* + * Transfer ownership of the async id to in_work; it stays reserved + * until in_work is freed after the deferred response is sent on + * close, so it can't be reused for an unrelated async response. + */ + in_work->async_id = work->async_id; + work->async_id = 0; + release_async_work(work); + + /* + * work itself is about to be recycled by the normal request-processing + * pipeline, so it can't stay the target of a future CANCEL -- register + * in_work instead, reusing the same async_id, so a client-sent CANCEL + * for this notify actually finds something to cancel instead of + * silently doing nothing until the handle eventually closes. + */ + in_work->asynchronous = true; + in_work->cancel_argv = kmalloc_array(2, sizeof(void *), KSMBD_DEFAULT_GFP); + if (in_work->cancel_argv) { + in_work->cancel_argv[0] = in_work; + in_work->cancel_argv[1] = fp; + in_work->cancel_fn = smb2_notify_cancel_fn; + } + spin_lock(&work->conn->request_lock); + list_add_tail(&in_work->async_request_entry, &work->conn->async_requests); + spin_unlock(&work->conn->request_lock); + + spin_lock(&fp->f_lock); + list_add_tail(&in_work->notify_entry, &fp->notify_pendings); + spin_unlock(&fp->f_lock); + + ksmbd_fd_put(work, fp); + work->send_no_response = 1; + return 0; } /** diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index e014c880b88b..05114d595b2a 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -560,6 +560,7 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) { struct file *filp; struct ksmbd_lock *smb_lock, *tmp_lock; + struct ksmbd_work *cn_work; fd_limit_close(); ksmbd_remove_durable_fd(fp); @@ -592,6 +593,52 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) kfree(smb_lock); } + /* + * Complete any CHANGE_NOTIFY left pending on this handle now that + * it is closed. KSMBD never completes CHANGE_NOTIFY spontaneously + * (no real change-notification backend), only on close -- matching + * genuine SMB2/macOS smbfs semantics and avoiding the Finder + * "directory changed, re-enumerate everything" loop. + * + * smb2_notify() on another connection can be adding to + * notify_pendings under fp->f_lock at the same time this handle is + * closed, and a client-sent CANCEL can concurrently be racing to + * claim the same entry via smb2_notify_cancel_fn() (smb2pdu.c). + * Pop one entry at a time under the lock via list_del_init() rather + * than a bulk list_splice_init(): list_del_init() leaves the node + * self-linked ("empty"), which is what the cancel path checks under + * the same lock to tell whether it lost the race -- a bulk splice + * would instead relink every entry into a shared local list, so an + * entry claimed here would still read as "not empty" to a racing + * cancel_fn, and both sides could end up freeing the same work. + * ksmbd_conn_write() can sleep (it takes conn's write mutex), so it + * must not be called while fp->f_lock is held -- release the lock + * before processing each popped entry, then reacquire it for the + * next. + */ + for (;;) { + spin_lock(&fp->f_lock); + if (list_empty(&fp->notify_pendings)) { + spin_unlock(&fp->f_lock); + break; + } + cn_work = list_first_entry(&fp->notify_pendings, + struct ksmbd_work, notify_entry); + list_del_init(&cn_work->notify_entry); + spin_unlock(&fp->f_lock); + + ksmbd_conn_write(cn_work); + /* + * release_async_work() removes cn_work from + * conn->async_requests, frees cancel_argv, and releases+zeroes + * async_id -- all needed before ksmbd_free_work_struct(), which + * only releases async_id itself if still nonzero (i.e. if this + * hadn't already been done). + */ + release_async_work(cn_work); + ksmbd_free_work_struct(cn_work); + } + /* * Drop fp's strong reference on conn (taken in ksmbd_open_fd() / * ksmbd_reopen_durable_fd()). Durable fps that reached the @@ -1113,6 +1160,7 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) INIT_LIST_HEAD(&fp->blocked_works); INIT_LIST_HEAD(&fp->node); INIT_LIST_HEAD(&fp->lock_list); + INIT_LIST_HEAD(&fp->notify_pendings); spin_lock_init(&fp->f_lock); mutex_init(&fp->readdir_lock); atomic_set(&fp->refcount, 1); diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index f796b6edc69b..1d9edc906b54 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -142,6 +142,12 @@ struct ksmbd_file { bool is_posix_ctxt; struct durable_owner owner; + + /* + * Pending CHANGE_NOTIFY completions for this handle, sent with + * STATUS_NOTIFY_CLEANUP when the handle is closed. + */ + struct list_head notify_pendings; }; static inline void set_ctx_actor(struct dir_context *ctx, From aa38147e4f07cd7af26b9458174e5fcb9e5b6dce Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 02:06:11 +0200 Subject: [PATCH 049/142] ksmbd: implement full-file copy for AAPL ChunkCount=0 COPYCHUNK fsctl_copychunk() treats FSCTL_SRV_COPYCHUNK with ChunkCount=0 as the standard SMB2 "query my copy limits, don't copy anything" request and returns success without ever looking up the file handles. That's correct for compliant SMB2 clients, but macOS Finder's Cmd+D duplicate sends ChunkCount=0 expecting the server to copy the whole file/stream -- so duplicated files are left at their just-created 0 bytes while the client reports success. Scope the full-copy fallback to AAPL-negotiated connections on a Time Machine share (conn->is_aapl && KSMBD_SHARE_FLAG_TIME_MACHINE) only, so standard non-AAPL SMB2 clients, and AAPL-negotiated clients on ordinary shares, keep the spec-correct query-limits behavior unchanged. both streams and regular files now share a single chunk_count == 0 fast path added right after src_file_size is computed, reusing the same buffered-copy helper and vfs_copy_file_range()/COPY_FILE_SPLICE fallback the existing per-chunk loop already uses, rather than the separate xattr-specific get/setxattr path this used before that rework. ChunksWritten/ChunkBytesWritten are 0 in the response: this is a synthesized whole-file copy, not a response to any chunk descriptor the client actually sent (it sent none), so there's no real chunk to report the count/size of. Only TotalBytesWritten is meaningful here. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 47 +++++++++++++++++++++++--------- fs/smb/server/vfs.c | 59 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index b4b6f077e272..65d367de43cd 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8963,23 +8963,44 @@ static int fsctl_copychunk(struct ksmbd_work *work, cpu_to_le32(ksmbd_server_side_copy_max_total_size()); chunk_count = le32_to_cpu(ci_req->ChunkCount); - if (chunk_count == 0) + /* + * ChunkCount=0 is the standard SMB2 "query my copy limits" request + * (no data copied) -- but macOS Finder's Cmd+D duplicate sends + * FSCTL_SRV_COPYCHUNK with ChunkCount=0 meaning "copy the whole + * file", relying on the AAPL-negotiated server to do a full copy + * instead. Keep the standard no-op behavior for everyone else. + * + * Gate on the TIME_MACHINE share flag, not just conn->is_aapl: + * that flag alone has ambiguous provenance -- the pre-existing + * narrow UniqueId=0 path can also set it on ordinary, + * non-Time-Machine shares, and this series' stated design keeps + * every AAPL-driven behavior opt-in per share. + */ + if (chunk_count == 0 && + !(work->conn->is_aapl && + test_share_config_flag(work->tcon->share_conf, + KSMBD_SHARE_FLAG_TIME_MACHINE))) goto out; total_size_written = 0; + i = 0; - /* verify the SRV_COPYCHUNK_COPY packet */ - if (chunk_count > ksmbd_server_side_copy_max_chunk_count() || - input_count < struct_size(ci_req, Chunks, chunk_count)) { - rsp->hdr.Status = STATUS_INVALID_PARAMETER; - return -EINVAL; - } + if (chunk_count) { + /* verify the SRV_COPYCHUNK_COPY packet */ + if (chunk_count > ksmbd_server_side_copy_max_chunk_count() || + input_count < struct_size(ci_req, Chunks, chunk_count)) { + rsp->hdr.Status = STATUS_INVALID_PARAMETER; + return -EINVAL; + } - chunks = &ci_req->Chunks[0]; - for (i = 0; i < chunk_count; i++) { - if (le32_to_cpu(chunks[i].Length) == 0 || - le32_to_cpu(chunks[i].Length) > ksmbd_server_side_copy_max_chunk_size()) - break; - total_size_written += le32_to_cpu(chunks[i].Length); + chunks = &ci_req->Chunks[0]; + for (i = 0; i < chunk_count; i++) { + if (le32_to_cpu(chunks[i].Length) == 0 || + le32_to_cpu(chunks[i].Length) > ksmbd_server_side_copy_max_chunk_size()) + break; + total_size_written += le32_to_cpu(chunks[i].Length); + } + } else { + chunks = &ci_req->Chunks[0]; } if (i < chunk_count || diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index df09d6cf5111..14a895685970 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1858,6 +1858,65 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, src_file_size = i_size_read(file_inode(src_fp->filp)); } + /* + * macOS Finder's Cmd+D duplicate sends FSCTL_SRV_COPYCHUNK with + * ChunkCount=0 meaning "copy the whole file/stream", not the + * standard SMB2 "query my copy limits, no data" semantics -- + * fsctl_copychunk() only reaches here with chunk_count == 0 for + * AAPL-negotiated connections, so this doesn't affect compliant + * non-AAPL clients. Without this, the destination stays at its + * just-created 0 bytes / empty stream: the for loop below is a + * no-op when chunk_count is 0, since it never has an iteration to + * treat as "copy everything". + */ + if (chunk_count == 0 && work->conn->is_aapl) { + loff_t off = 0; + + while (off < src_file_size) { + size_t remaining = src_file_size - off; + ssize_t copied; + + /* Same source/destination offset here: an in-place, + * same-inode copy at matching offsets is a degenerate + * no-op range, not a real overlap, but vfs_copy_file_range + * still doesn't support streams -- route those (and the + * same-inode case defensively) through the buffered path. + */ + if (ksmbd_stream_fd(src_fp) || ksmbd_stream_fd(dst_fp) || + file_inode(src_fp->filp) == file_inode(dst_fp->filp)) { + copied = ksmbd_vfs_copy_file_range_buffered(work, src_fp, dst_fp, + off, off, remaining); + } else { + copied = vfs_copy_file_range(src_fp->filp, off, + dst_fp->filp, off, + remaining, 0); + if (copied == -EOPNOTSUPP || copied == -EXDEV) + copied = vfs_copy_file_range(src_fp->filp, off, + dst_fp->filp, off, + remaining, + COPY_FILE_SPLICE); + } + if (copied < 0) + return copied; + if (copied == 0) + break; + off += copied; + } + + /* + * This is a synthesized whole-file copy, not a response to + * any chunk descriptor the client actually sent (it sent + * none -- chunk_count is 0). Report zero chunks/chunk-bytes + * rather than inventing a chunk that doesn't correspond to + * anything in the request; only total_size_written (bytes + * actually copied) is meaningful here. + */ + *chunk_count_written = 0; + *chunk_size_written = 0; + *total_size_written = off; + return 0; + } + for (i = 0; i < chunk_count; i++) { bool stream_len_mismatch = false; size_t copy_len; From 9907ed8457af0e867b0a0d6e3e92019ee6cedd83 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Fri, 10 Jul 2026 08:39:55 +0900 Subject: [PATCH 050/142] ksmbd: add AAPL READDIR_ATTR V2 support Extends the existing V1 inline-FinderInfo mechanism (SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) with the V2 variant: byte-identical layout otherwise, except the ShortNameLength+Reserved bytes (ignored outright by V1 clients) become a single flags field that V2 clients actually interpret. Negotiation: when a client's own client_caps requests V2 (SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2), advertise V2 instead of V1 in the server's own server_caps reply -- they're mutually exclusive on the wire, not both set together. Wire format: the only currently-defined V2 flag, AAPL_READDIR_ATTR_V2_NO_XATTR, signals that an item has no xattrs/streams so the client can skip a separate query. Compute this per-entry in ksmbd_vfs_fill_dentry_attrs() by checking for any xattr under the XATTR_NAME_STREAM ("user.DosStream.") prefix -- a reliable, distinct marker for genuine ADS/stream xattrs, unlike DOSATTRIB or ACL xattrs which live under different prefixes, so this can't false-positive into telling Finder a file has no extra data when it actually does. Only computed when a V2 connection is active, to avoid the extra listxattr() call otherwise. V1's fixed ShortNameLength=24 convention (real macOS clients ignore the value outright per the same client source, so it's cosmetic parity with other real servers, not a functional requirement) is kept V1-only rather than reused as a V2 base value -- V2 clients do interpret this field, so it needs a clean 0-or-flag value, not a leftover V1 constant that happens not to collide with the one defined flag bit today. Confirmed via live diagnostics that macOS actually negotiates and uses V2 (client_caps bit 0x10 set) rather than falling back to V1 or ignoring the capability. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/common/smb2pdu.h | 8 ++++++++ fs/smb/server/connection.h | 1 + fs/smb/server/oplock.c | 12 ++++++++++-- fs/smb/server/oplock.h | 3 ++- fs/smb/server/smb2pdu.c | 33 +++++++++++++++++++++++++++++++-- fs/smb/server/smb2pdu.h | 15 +++++++++++++++ fs/smb/server/vfs.c | 29 +++++++++++++++++++++++++++++ fs/smb/server/vfs.h | 1 + 8 files changed, 97 insertions(+), 5 deletions(-) diff --git a/fs/smb/common/smb2pdu.h b/fs/smb/common/smb2pdu.h index 653cb3579d40..6826f7bed1a4 100644 --- a/fs/smb/common/smb2pdu.h +++ b/fs/smb/common/smb2pdu.h @@ -1261,6 +1261,14 @@ struct create_mxac_req { #define SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE 2 #define SMB2_CRTCTX_AAPL_UNIX_BASED 4 #define SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE 8 +/* + * V2 extends the same inline-FinderInfo mechanism as + * SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR with an added flags field, + * confirmed byte-identical to V1 otherwise against AAPL's actual + * public client behavior. Mutually exclusive with the V1 bit on + * the wire, not both set together. + */ +#define SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2 16 /* "AAPL" Volume Capabilities bitmap */ #define SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID 1 diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 05818a165e22..ddfcddd3c09c 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -125,6 +125,7 @@ struct ksmbd_conn { atomic_t refcnt; bool is_aapl; bool aapl_readdir_attr; /* READDIR_ATTR negotiated */ + bool aapl_readdir_attr_v2; /* V2 specifically */ struct work_struct release_work; }; diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 94fe464a85f3..0fe82e740ffb 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -2158,11 +2158,15 @@ void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp) * * Sending reply_bitmap with MODEL_INFO set but no model string causes * smbfs.kext to enter a broken disconnect path requiring a macOS reboot. + * @readdir_attr_v2: advertise SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2 + * instead of the V1 bit */ -void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap) +void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap, + bool readdir_attr_v2) { struct create_aapl_rsp *buf; u64 reply_bitmap; + u64 server_caps; u32 data_len; buf = (struct create_aapl_rsp *)cc; @@ -2188,8 +2192,12 @@ void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap) buf->cmd = cpu_to_le32(SMB2_CRTCTX_AAPL_SERVER_QUERY); buf->reply_bitmap = cpu_to_le64(reply_bitmap); + server_caps = AAPL_SERVER_CAPS_KSMBD; + if (readdir_attr_v2) + server_caps = (server_caps & ~SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) | + SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2; buf->server_caps = (reply_bitmap & SMB2_CRTCTX_AAPL_SERVER_CAPS) ? - cpu_to_le64(AAPL_SERVER_CAPS_KSMBD) : 0; + cpu_to_le64(server_caps) : 0; buf->vol_caps = (reply_bitmap & SMB2_CRTCTX_AAPL_VOLUME_CAPS) ? cpu_to_le64(vol_caps) : 0; diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index f7f6afcc5434..aef296b21395 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -125,7 +125,8 @@ void create_durable_v2_rsp_buf(char *cc, struct ksmbd_file *fp); void create_mxac_rsp_buf(char *cc, int maximal_access); void create_disk_id_rsp_buf(char *cc, __u64 file_id, __u64 vol_id); void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp); -void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap); +void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap, + bool readdir_attr_v2); struct create_context *smb2_find_context_vals(void *open_req, const char *tag, int tag_len); struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, char *lease_key); diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 65d367de43cd..e3be1da76dc6 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4413,12 +4413,23 @@ int smb2_open(struct ksmbd_work *work) if (aapl_ctxt) { if (aapl_client_caps & SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) conn->aapl_readdir_attr = true; + /* + * V2 extends the same inline-FinderInfo mechanism (see + * smb2pdu.h), so a V2-requesting client also gets + * aapl_readdir_attr treatment -- the reply just advertises + * the V2 bit instead of the V1 one (create_aapl_rsp_buf). + */ + if (aapl_client_caps & SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2) { + conn->aapl_readdir_attr = true; + conn->aapl_readdir_attr_v2 = true; + } contxt_cnt++; create_aapl_rsp_buf(rsp->Buffer + le32_to_cpu(rsp->CreateContextsLength), SMB2_CRTCTX_AAPL_FULL_SYNC, - aapl_req_bitmap); + aapl_req_bitmap, + conn->aapl_readdir_attr_v2); le32_add_cpu(&rsp->CreateContextsLength, conn->vals->create_aapl_size); iov_len += conn->vals->create_aapl_size; @@ -4762,6 +4773,11 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, * the file extension for icon lookup) * Reserved2 = Unix mode bits (uint16 LE) * Reparse-point tag is indicated via ExtFileAttributes, not EaSize. + * + * V2 (conn->aapl_readdir_attr_v2): ShortNameLength+Reserved + * are read as a single flags field instead of being ignored + * -- see smb2pdu.h for the wire-format confirmation and + * AAPL_READDIR_ATTR_V2_NO_XATTR's meaning. */ __le32 reparse_tag = smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); @@ -4791,7 +4807,20 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, * interpreted; V1 doesn't. Either value is safe * here, so keep 24 for parity. */ - fibdinfo->ShortNameLength = 24; + if (conn->aapl_readdir_attr_v2) { + /* + * V2 repurposes this field as flags (see comment + * above) -- 24 is a V1-only convention that real + * macOS clients ignore outright, so don't reuse it + * here as a base value for a field V2 clients + * actually interpret. + */ + fibdinfo->ShortNameLength = 0; + if (!ksmbd_kstat->has_ads_stream) + fibdinfo->ShortNameLength = AAPL_READDIR_ATTR_V2_NO_XATTR; + } else { + fibdinfo->ShortNameLength = 24; + } memset(fibdinfo->ShortName, 0, sizeof(fibdinfo->ShortName)); fibdinfo->Reserved2 = cpu_to_le16(ksmbd_kstat->kstat->mode & 0xffff); } else { diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index b0daca928c44..9d77400a1670 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -95,6 +95,21 @@ struct preauth_integrity_info { SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE | \ SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) +/* + * READDIR_ATTR_V2 (SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2, see + * fs/smb/common/smb2pdu.h) extends the same inline-FinderInfo mechanism + * above with a flags field, confirmed byte-identical to V1 otherwise + * against AAPL's actual public client behavior. When a client's own + * client_caps requests V2, the server advertises V2 instead of V1 in + * its own server_caps reply; V1 and V2 are mutually exclusive on the + * wire, not both set together. The wire format's ShortNameLength+Reserved + * (ignored in V1) become a single flags field in V2 -- + * AAPL_READDIR_ATTR_V2_NO_XATTR is the only flag bit currently defined, + * signaling the item has no xattrs/streams so the client can skip a + * separate query. + */ +#define AAPL_READDIR_ATTR_V2_NO_XATTR 0x01 + /* Model string: up to 31 ASCII chars */ #define AAPL_MODEL_MAX_CHARS 31 #define AAPL_MODEL_UTF16_BYTES (AAPL_MODEL_MAX_CHARS * 2) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 14a895685970..0ec052e4a365 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -1684,6 +1684,35 @@ int ksmbd_vfs_fill_dentry_attrs(struct ksmbd_work *work, } } + /* + * Only pay for this when it'll actually be used: AAPL + * READDIR_ATTR_V2's flags field (AAPL_READDIR_ATTR_V2_NO_XATTR) is + * the only consumer. XATTR_NAME_STREAM ("user.DosStream.") is a + * reliable, distinct prefix for genuine ADS/stream xattrs -- unlike + * DOSATTRIB or ACL xattrs, which live under different prefixes, so + * this can't false-positive into telling Finder a file has no extra + * data when it actually does. + */ + ksmbd_kstat->has_ads_stream = false; + if (work->conn->aapl_readdir_attr_v2) { + char *xattr_list = NULL, *name; + ssize_t xattr_list_len; + + xattr_list_len = ksmbd_vfs_listxattr(dentry, &xattr_list); + if (xattr_list_len > 0) { + for (name = xattr_list; + name - xattr_list < xattr_list_len; + name += strlen(name) + 1) { + if (!strncmp(name, XATTR_NAME_STREAM, + XATTR_NAME_STREAM_LEN)) { + ksmbd_kstat->has_ads_stream = true; + break; + } + } + } + kvfree(xattr_list); + } + return 0; } diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 8eab9392ff89..2e8f9f2d95b0 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -70,6 +70,7 @@ struct ksmbd_kstat { struct kstat *kstat; unsigned long long create_time; __le32 file_attributes; + bool has_ads_stream; /* AAPL READDIR_ATTR V2 xattr-presence flag */ }; int ksmbd_vfs_lock_parent(struct dentry *parent, struct dentry *child); From 689f1eb3719d61700b58f984ac8602837f004f8c Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 18:50:46 +0200 Subject: [PATCH 051/142] ksmbd: report actual xattr value length in stream enumeration get_file_stream_info() (FileStreamInformation QUERY_INFO) reported each enumerated stream's StreamSize/StreamAllocationSize as stream_name_len -- the byte length of the stream's *name*, not its data. This is the same bug class already fixed for EndOfFile/ AllocationSize on an open stream handle (ksmbd_stream_eof()), just missed at this second site: a client enumerating streams sees a size derived from the name string length instead of the stream's actual content length, inconsistent with what querying the same stream by handle reports. Compute the real value length the same way ksmbd_stream_eof() does, via ksmbd_vfs_casexattr_len() on the already-known xattr key. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index e3be1da76dc6..70f8e780da33 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -5952,6 +5952,8 @@ static int get_file_stream_info(struct ksmbd_work *work, struct kstat stat; const struct path *path = &fp->filp->f_path; ssize_t xattr_list_len; + ssize_t slen; + loff_t ssize; int nbytes = 0, streamlen, stream_name_len, next, idx = 0; int buf_free_len; int ret; @@ -6013,8 +6015,20 @@ static int get_file_stream_info(struct ksmbd_work *work, streamlen *= 2; kfree(stream_buf); file_info->StreamNameLength = cpu_to_le32(streamlen); - file_info->StreamSize = cpu_to_le64(stream_name_len); - file_info->StreamAllocationSize = cpu_to_le64(stream_name_len); + /* + * stream_name_len is the byte length of the xattr's *name*, + * not its value -- same class of bug ksmbd_stream_eof() + * (smb2pdu.c) already fixes for EndOfFile/AllocationSize on + * a stream handle; this enumeration path needs the same + * real xattr value length, not the name length reused as a + * size. + */ + slen = ksmbd_vfs_casexattr_len(file_mnt_idmap(fp->filp), + path->dentry, stream_name, + strlen(stream_name) + 1); + ssize = slen < 0 ? 0 : (loff_t)slen; + file_info->StreamSize = cpu_to_le64(ssize); + file_info->StreamAllocationSize = cpu_to_le64(ssize); nbytes += next; buf_free_len -= next; From 6cbb144f8ed7ab7cb86f2f120740f578db9b62e0 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Thu, 9 Jul 2026 18:50:45 +0200 Subject: [PATCH 052/142] ksmbd: quiet mdssvc RPC log spam in create_smb2_pipe silenced __rpc_method()'s own "Unsupported RPC: mdssvc" log line but missed that ksmbd_session_rpc_open() failing for that same, now-still-rejected pipe also trips a second, separate pr_err() here in its caller. macOS's routine mdssvc (Spotlight) probes still spam the kernel log via this second site on every single probe, defeating the original commit's stated purpose. Suppress this specific case the same way the other site does; behavior is unchanged for every other RPC failure. __rpc_method() (mgmt/user_session.c) returns -ENOENT for mdssvc, not -EINVAL. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Gael Blivet Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 70f8e780da33..19d90a6eee51 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2649,7 +2649,16 @@ static noinline int create_smb2_pipe(struct ksmbd_work *work) id = ksmbd_session_rpc_open(work->sess, name); if (id < 0) { - pr_err("Unable to open RPC pipe: %d\n", id); + /* + * mdssvc (Spotlight) is a routine, expected probe from macOS + * that we deliberately don't support -- it's disabled at the + * __rpc_method() level (mgmt/user_session.c), but this + * generic failure log would otherwise still fire on every + * single probe regardless. + */ + if (!(id == -ENOENT && (!strcmp(name, "\\mdssvc") || + !strcmp(name, "mdssvc")))) + pr_err("Unable to open RPC pipe: %d\n", id); err = id; goto out; } From 350684e0498512d06963858b810101f58563810b Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 11 Jul 2026 23:20:41 +0900 Subject: [PATCH 053/142] ksmbd: handle allocated range queries on dense files FSCTL_QUERY_ALLOCATED_RANGES currently relies on SEEK_DATA and SEEK_HOLE for every file. That works for files with holes, but it is not a good match for dense files. A zeroed range in a dense file may be represented as an unwritten extent and skipped by SEEK_DATA. The server can then return no allocated ranges even though the file should still be treated as allocated from the protocol point of view. For dense files, report the requested range clipped to EOF as allocated instead of probing holes. Keep using SEEK_DATA and SEEK_HOLE for files marked with FILE_ATTRIBUTE_SPARSE_FILE, and wait for writeback before probing so punch-hole updates are visible to the filesystem seek implementation. This fixes the case where a query after zeroing data could return no ranges for a dense file. Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 0ec052e4a365..92b6def5a229 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -947,7 +947,8 @@ int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, { struct file *f = fp->filp; struct inode *inode = file_inode(fp->filp); - loff_t maxbytes = (u64)inode->i_sb->s_maxbytes, end; + loff_t maxbytes = (u64)inode->i_sb->s_maxbytes, end, query_start; + loff_t query_length, size; loff_t extent_start, extent_end; int ret = 0; @@ -963,11 +964,33 @@ int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, if (length > maxbytes || (maxbytes - length) < start) length = maxbytes - start; - if (start + length > inode->i_size) - length = inode->i_size - start; + size = i_size_read(inode); + if (start >= size) + return 0; + + if (!length) + return 0; + + if (start + length > size) + length = size - start; *out_count = 0; + query_start = start; + query_length = length; end = start + length; + if (!(fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE)) { + ranges[0].file_offset = cpu_to_le64(query_start); + ranges[0].length = cpu_to_le64(query_length); + *out_count = 1; + return 0; + } + + if (start < end) { + ret = file_write_and_wait_range(f, start, end - 1); + if (ret) + return ret; + } + while (start < end && *out_count < in_count) { extent_start = vfs_llseek(f, start, SEEK_DATA); if (extent_start < 0) { From 3c707d4b3f926fc9b7b13b5bfe71462cb0e0e5d8 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 11 Jul 2026 23:54:43 +0900 Subject: [PATCH 054/142] ksmbd: fix permission checks for file allocation ioctls FSCTL_SET_SPARSE should not require FILE_WRITE_ATTRIBUTES only. A handle with FILE_WRITE_DATA or FILE_APPEND_DATA is also allowed to set the file allocation state, while FILE_WRITE_EA alone must still be rejected. FSCTL_QUERY_ALLOCATED_RANGES needs FILE_READ_DATA access. Reject handles that only have metadata access such as FILE_READ_ATTRIBUTES. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 19d90a6eee51..883db2ceba3f 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9311,6 +9311,11 @@ static int fsctl_query_allocated_ranges(struct ksmbd_work *work, u64 id, if (!fp) return -ENOENT; + if (!(fp->daccess & FILE_READ_DATA_LE)) { + ret = -EACCES; + goto out; + } + if (!in_count) { struct file_allocated_range_buffer range; @@ -9326,6 +9331,7 @@ static int fsctl_query_allocated_ranges(struct ksmbd_work *work, u64 id, if (ret && ret != -E2BIG) *out_count = 0; +out: ksmbd_fd_put(work, fp); return ret; } @@ -9397,7 +9403,8 @@ static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id, goto out; } - if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_WRITE_ATTRIBUTES_LE))) { + if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_APPEND_DATA_LE | + FILE_WRITE_ATTRIBUTES_LE))) { ret = -EACCES; goto out; } From 8b57448a147d2378088e63cd473e7f2a967d6802 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 12 Jul 2026 00:03:34 +0900 Subject: [PATCH 055/142] ksmbd: honor byte-range locks for zero data FSCTL_SET_ZERO_DATA changes file allocation state and must respect byte-range locks over the affected part of the file. Check the requested range, clipped to EOF, before issuing the fallocate operation. Return STATUS_FILE_LOCK_CONFLICT when the range conflicts with an existing lock. Ranges starting past EOF are left untouched by the lock check so they continue to succeed. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 7 ++++++- fs/smb/server/vfs.c | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 883db2ceba3f..f57ba8baa63b 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9781,8 +9781,13 @@ int smb2_ioctl(struct ksmbd_work *work) ret = ksmbd_vfs_zero_data(work, fp, off, len); ksmbd_fd_put(work, fp); - if (ret < 0) + if (ret == -EAGAIN) { + rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; + ret = 0; goto out; + } else if (ret < 0) { + goto out; + } } break; } diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 92b6def5a229..12ab15b3bff9 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -928,6 +928,18 @@ int ksmbd_vfs_zero_data(struct ksmbd_work *work, struct ksmbd_file *fp, int err; smb_break_all_levII_oplock(work, fp, 1); + if (!work->tcon->posix_extensions) { + loff_t size = i_size_read(file_inode(fp->filp)); + + if (off < size) { + err = check_lock_range(fp->filp, off, + min(off + len, size) - 1, + WRITE); + if (err) + return -EAGAIN; + } + } + saved_cred = override_creds(fp->filp->f_cred); if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) err = vfs_fallocate(fp->filp, From a72692c5bcabb67d2320e1630192f78874a7c524 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 12 Jul 2026 00:11:39 +0900 Subject: [PATCH 056/142] ksmbd: support file level trim Advertise trim support through FS_SECTOR_SIZE_INFORMATION and handle FSCTL_FILE_LEVEL_TRIM requests. Process each trim range by punching a hole while keeping the file size unchanged, and report the number of ranges completed in the ioctl response. The trim operation uses the same byte-range lock handling as zero data for the affected part of the file. Signed-off-by: Namjae Jeon --- fs/smb/common/fscc.h | 15 ++++++++ fs/smb/server/smb2pdu.c | 82 ++++++++++++++++++++++++++++++++++++++++- fs/smb/server/vfs.c | 27 ++++++++++++++ fs/smb/server/vfs.h | 2 + 4 files changed, 125 insertions(+), 1 deletion(-) diff --git a/fs/smb/common/fscc.h b/fs/smb/common/fscc.h index 941db5a95564..e46d3379b779 100644 --- a/fs/smb/common/fscc.h +++ b/fs/smb/common/fscc.h @@ -202,6 +202,21 @@ struct file_zero_data_information { __le64 BeyondFinalZero; } __packed; +struct file_level_trim_range { + __le64 Offset; + __le64 Length; +} __packed; + +struct file_level_trim { + __le32 Key; + __le32 NumRanges; + struct file_level_trim_range Ranges[]; +} __packed; + +struct file_level_trim_output { + __le32 NumRangesProcessed; +} __packed; + /* * This level 18, although with struct with same name is different from cifs * level 0x107. Level 0x107 has an extra u64 between AccessFlags and diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index f57ba8baa63b..6c7f6e48b92b 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -6634,7 +6634,8 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->FSEffPhysicalBytesPerSectorForAtomicity = cpu_to_le32(sector_size); info->Flags = cpu_to_le32(SSINFO_FLAGS_ALIGNED_DEVICE | - SSINFO_FLAGS_PARTITION_ALIGNED_ON_DEVICE); + SSINFO_FLAGS_PARTITION_ALIGNED_ON_DEVICE | + SSINFO_FLAGS_TRIM_ENABLED); info->ByteOffsetForSectorAlignment = 0; info->ByteOffsetForPartitionAlignment = 0; rsp->OutputBufferLength = cpu_to_le32(28); @@ -9791,6 +9792,85 @@ int smb2_ioctl(struct ksmbd_work *work) } break; } + case FSCTL_FILE_LEVEL_TRIM: + { + struct file_level_trim *trim_req; + struct file_level_trim_output *trim_rsp; + struct ksmbd_file *fp; + u32 i, num_ranges; + + 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; + } + + if (in_buf_len < offsetof(struct file_level_trim, Ranges)) { + ret = -EINVAL; + goto out; + } + + if (out_buf_len < sizeof(struct file_level_trim_output)) { + ret = -EINVAL; + goto out; + } + + trim_req = (struct file_level_trim *)buffer; + num_ranges = le32_to_cpu(trim_req->NumRanges); + if (num_ranges > + (in_buf_len - offsetof(struct file_level_trim, Ranges)) / + sizeof(struct file_level_trim_range)) { + ret = -EINVAL; + goto out; + } + + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -ENOENT; + goto out; + } + + if (!(fp->daccess & FILE_WRITE_DATA_LE)) { + ksmbd_fd_put(work, fp); + ret = -EACCES; + goto out; + } + + trim_rsp = (struct file_level_trim_output *)&rsp->Buffer[0]; + trim_rsp->NumRangesProcessed = 0; + for (i = 0; i < num_ranges; i++) { + loff_t off = le64_to_cpu(trim_req->Ranges[i].Offset); + loff_t len = le64_to_cpu(trim_req->Ranges[i].Length); + + if (off < 0 || len < 0) { + ret = -EINVAL; + break; + } + + if (!len) { + trim_rsp->NumRangesProcessed = + cpu_to_le32(i + 1); + continue; + } + + ret = ksmbd_vfs_trim_data(work, fp, off, len); + if (ret) + break; + trim_rsp->NumRangesProcessed = cpu_to_le32(i + 1); + } + ksmbd_fd_put(work, fp); + if (ret == -EAGAIN) { + rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; + ret = 0; + goto out; + } else if (ret < 0) { + goto out; + } + + nbytes = sizeof(struct file_level_trim_output); + break; + } case FSCTL_QUERY_ALLOCATED_RANGES: if (in_buf_len < sizeof(struct file_allocated_range_buffer)) { ret = -EINVAL; diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 12ab15b3bff9..33d47f9f1d69 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -953,6 +953,33 @@ int ksmbd_vfs_zero_data(struct ksmbd_work *work, struct ksmbd_file *fp, return err; } +int ksmbd_vfs_trim_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); + if (!work->tcon->posix_extensions) { + loff_t size = i_size_read(file_inode(fp->filp)); + + if (off < size) { + err = check_lock_range(fp->filp, off, + min(off + len, size) - 1, + WRITE); + if (err) + return -EAGAIN; + } + } + + saved_cred = override_creds(fp->filp->f_cred); + err = vfs_fallocate(fp->filp, + FALLOC_FL_PUNCH_HOLE | 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, struct file_allocated_range_buffer *ranges, unsigned int in_count, unsigned int *out_count) diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 2e8f9f2d95b0..022b78268a7e 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -133,6 +133,8 @@ int ksmbd_vfs_empty_dir(struct ksmbd_file *fp); 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); +int ksmbd_vfs_trim_data(struct ksmbd_work *work, struct ksmbd_file *fp, + loff_t off, loff_t len); struct file_allocated_range_buffer; int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, struct file_allocated_range_buffer *ranges, From 96db370817d5f21a1dc10efb2210db05163f312e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 12 Jul 2026 00:16:24 +0900 Subject: [PATCH 057/142] ksmbd: fall back to copy for duplicate extents FSCTL_DUPLICATE_EXTENTS_TO_FILE currently returns STATUS_NOT_SUPPORTED when vfs_clone_file_range() cannot clone the requested range. That can happen on filesystems without reflink support even though the server can still satisfy the request by copying the bytes. Validate the requested source range before attempting the operation. If the destination range extends past EOF, leave the destination size unchanged and complete the request without copying, matching observed client expectations for this ioctl. Reject sparse source to non-sparse destination requests as unsupported. Keep sparse destination and sparse-to-sparse cases on the normal clone or copy path. Reject overlapping same-file ranges as unsupported before attempting the clone or copy operation. Return the expected handle status for invalid handles. A closed target handle fails with STATUS_FILE_CLOSED, while a bad source handle embedded in the request buffer fails with STATUS_INVALID_HANDLE. Fall back to vfs_copy_file_range() whenever the clone operation does not copy the full requested length, and keep reporting an error only if the fallback also fails or copies a partial range. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 48 ++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 6c7f6e48b92b..96fbf91a9cad 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9933,15 +9933,18 @@ int smb2_ioctl(struct ksmbd_work *work) dup_ext->PersistentFileHandle); if (!fp_in) { pr_err("not found file handle in duplicate extent to file\n"); - ret = -ENOENT; - goto out; + ret = -EBADF; + rsp->hdr.Status = STATUS_INVALID_HANDLE; + goto out2; } fp_out = ksmbd_lookup_fd_fast(work, id); if (!fp_out) { pr_err("not found fp\n"); - ret = -ENOENT; - goto dup_ext_out; + ret = -EBADF; + rsp->hdr.Status = STATUS_FILE_CLOSED; + ksmbd_fd_put(work, fp_in); + goto out2; } if (!test_tree_conn_flag(work->tcon, @@ -9962,21 +9965,32 @@ int smb2_ioctl(struct ksmbd_work *work) src_off = le64_to_cpu(dup_ext->SourceFileOffset); dst_off = le64_to_cpu(dup_ext->TargetFileOffset); length = le64_to_cpu(dup_ext->ByteCount); - /* - * XXX: It is not clear if FSCTL_DUPLICATE_EXTENTS_TO_FILE - * should fall back to vfs_copy_file_range(). This could be - * beneficial when re-exporting nfs/smb mount, but note that - * this can result in partial copy that returns an error status. - * If/when FSCTL_DUPLICATE_EXTENTS_TO_FILE_EX is implemented, - * fall back to vfs_copy_file_range(), should be avoided when - * the flag DUPLICATE_EXTENTS_DATA_EX_SOURCE_ATOMIC is set. - */ - cloned = vfs_clone_file_range(fp_in->filp, src_off, - fp_out->filp, dst_off, length, 0); - if (cloned == -EXDEV || cloned == -EOPNOTSUPP) { + if (src_off < 0 || dst_off < 0 || length < 0 || + src_off + length < src_off || dst_off + length < dst_off) { + ret = -EINVAL; + goto dup_ext_out; + } + if (src_off + length > i_size_read(file_inode(fp_in->filp))) { ret = -EOPNOTSUPP; goto dup_ext_out; - } else if (cloned != length) { + } + if (dst_off + length > i_size_read(file_inode(fp_out->filp))) + goto dup_ext_out; + if ((fp_in->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) && + !(fp_out->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE)) { + ret = -EOPNOTSUPP; + goto dup_ext_out; + } + if (file_inode(fp_in->filp) == file_inode(fp_out->filp) && + dst_off + length > src_off && + dst_off < src_off + length) { + ret = -EOPNOTSUPP; + goto dup_ext_out; + } + + cloned = vfs_clone_file_range(fp_in->filp, src_off, + fp_out->filp, dst_off, length, 0); + if (cloned != length) { cloned = vfs_copy_file_range(fp_in->filp, src_off, fp_out->filp, dst_off, length, 0); From cb946cd133f7958da4a62a102cf560de4857b2dc Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 12 Jul 2026 08:51:54 +0900 Subject: [PATCH 058/142] ksmbd: validate file ids for query network interface info FSCTL_QUERY_NETWORK_INTERFACE_INFO is not tied to an open file handle. Clients send SMB2_NO_FID for both file id fields when issuing this request. Reject requests that provide any other file id before checking the output buffer size. This returns STATUS_INVALID_PARAMETER for invalid file ids instead of treating the request as valid or reporting STATUS_BUFFER_TOO_SMALL for a small output buffer. Signed-off-by: Namjae Jeon --- 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 96fbf91a9cad..91abbb90f262 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9673,6 +9673,12 @@ int smb2_ioctl(struct ksmbd_work *work) rsp->VolatileFileId = SMB2_NO_FID; break; case FSCTL_QUERY_NETWORK_INTERFACE_INFO: + if (req->PersistentFileId != SMB2_NO_FID || + req->VolatileFileId != SMB2_NO_FID) { + ret = -EINVAL; + goto out; + } + ret = fsctl_query_iface_info_ioctl(conn, rsp, out_buf_len); if (ret < 0) goto out; From 99580a386220b8b0f156738c4bf195da53d3ba85 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 12 Jul 2026 09:02:02 +0900 Subject: [PATCH 059/142] ksmbd: send lease breaks for handle-caching share conflicts RH leases map to SMB2_OPLOCK_LEVEL_II because they do not include write caching. smb_grant_oplock() only sent break notifications for previous BATCH or EXCLUSIVE levels, so a conflicting open could skip the lease break when the existing lease was RH. That leaves the opener to fail or complete without the expected pending lease break sequence, instead of first asking the holder to drop handle caching. Treat share-mode conflicts against leases with HANDLE_CACHING as needing a break even when the mapped oplock level is LEVEL_II. This lets the server send the RH -> R lease break and wait for the normal break handling before continuing the conflicting open. Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 0fe82e740ffb..5b46af94cd82 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1454,6 +1454,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, bool prev_durable_detached = false; unsigned long long prev_fid = KSMBD_NO_FID; bool new_lease = false; + bool break_needed; __le32 prev_op_state = 0; /* Only v2 leases handle the directory */ @@ -1532,8 +1533,11 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, goto err_out; } - if (prev_opinfo->level != SMB2_OPLOCK_LEVEL_BATCH && - prev_opinfo->level != SMB2_OPLOCK_LEVEL_EXCLUSIVE) { + break_needed = prev_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH || + prev_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE || + (share_ret < 0 && prev_op_has_lease && + (prev_op_state & SMB2_LEASE_HANDLE_CACHING_LE)); + if (!break_needed) { opinfo_put(prev_opinfo); goto op_break_not_needed; } From 7f8029591bed054aceba18ca333e6ab20064441f Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 14 Jul 2026 21:41:06 +0900 Subject: [PATCH 060/142] ksmbd: synchronize lease breaks before renaming files Break read and handle caching leases before entering the VFS rename path. This keeps the destination name hidden until the lease holder acknowledges the break. Send the break synchronously before returning STATUS_PENDING for a rename. This avoids a race between the interim response and notification handling. Keep the existing asynchronous notification flow for all other lease break paths so chained breaks retain their ordering. Use the connection which owns the open for the notification. A lease table is shared by connections using the same client GUID. Its saved connection may belong to another active channel. Use it only when the owning channel is being released. Check directory sharing before issuing a break to avoid unnecessary lease breaks for a rename that must fail with a sharing violation. Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 73 +++++++++++++++++++++++++---------------- fs/smb/server/oplock.h | 3 +- fs/smb/server/smb2pdu.c | 7 ++-- fs/smb/server/vfs.c | 38 +++++++++++++-------- fs/smb/server/vfs.h | 4 ++- 5 files changed, 78 insertions(+), 47 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 5b46af94cd82..74bd2fadc757 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -994,22 +994,28 @@ static void __smb2_lease_break_noti(struct work_struct *wk) * smb2_lease_break_noti() - break lease when a new client request * write lease * @opinfo: contains lease state information - * @wait_ack: wait for lease break acknowledgment from the client + * @sync: send the lease break notification synchronously * @inc_epoch: increment the lease epoch before sending the break * * Return: 0 on success, otherwise error */ -static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack, +static int smb2_lease_break_noti(struct oplock_info *opinfo, bool sync, bool inc_epoch) { struct ksmbd_conn *conn; struct ksmbd_work *work; struct lease_break_info *br_info; struct lease *lease = opinfo->o_lease; - int ret = 0; conn = READ_ONCE(opinfo->conn); - if (lease->version == 2 && lease->l_lb && lease->l_lb->conn && + /* + * Keep a break on the channel which owns this open. A lease table is + * shared by connections with the same client GUID, so its connection + * can belong to another active channel. Only use it after the owning + * channel is being released. + */ + if ((!conn || ksmbd_conn_releasing(conn)) && lease->version == 2 && + lease->l_lb && lease->l_lb->conn && !ksmbd_conn_releasing(lease->l_lb->conn)) conn = lease->l_lb->conn; if (!conn) @@ -1042,11 +1048,11 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack, ksmbd_conn_r_count_inc(conn); if (opinfo->op_state == OPLOCK_ACK_WAIT) { - INIT_WORK(&work->work, __smb2_lease_break_noti); - ksmbd_queue_work(work); - if (wait_ack) { - if (wait_for_break_ack(opinfo)) - ret = ksmbd_invalidate_durable_fd(opinfo->fid); + if (sync) { + __smb2_lease_break_noti(&work->work); + } else { + INIT_WORK(&work->work, __smb2_lease_break_noti); + ksmbd_queue_work(work); } } else { __smb2_lease_break_noti(&work->work); @@ -1055,7 +1061,7 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo, bool wait_ack, lease_update_oplock_levels(opinfo->o_lease); } } - return ret; + return 0; } static void wait_lease_breaking(struct oplock_info *opinfo) @@ -1076,7 +1082,8 @@ static void wait_lease_breaking(struct oplock_info *opinfo) } static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, - struct ksmbd_work *in_work, bool share_break) + struct ksmbd_work *in_work, bool share_break, + bool sync_lease_break) { int err = 0; bool sent_interim = false; @@ -1137,13 +1144,6 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, } } - if (in_work && !sent_interim) { - setup_async_work(in_work, NULL, NULL); - smb2_send_interim_resp(in_work, STATUS_PENDING); - release_async_work(in_work); - sent_interim = true; - } - if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE)) { brk_opinfo->op_state = OPLOCK_ACK_WAIT; @@ -1157,8 +1157,16 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, inc_epoch = false; lease->reuse_epoch = false; } - err = smb2_lease_break_noti(brk_opinfo, wait_ack, inc_epoch); + err = smb2_lease_break_noti(brk_opinfo, sync_lease_break, inc_epoch); inc_epoch = false; + if (in_work && !sent_interim) { + setup_async_work(in_work, NULL, NULL); + smb2_send_interim_resp(in_work, STATUS_PENDING); + release_async_work(in_work); + sent_interim = true; + } + if (wait_ack && !err && wait_for_break_ack(brk_opinfo)) + err = ksmbd_invalidate_durable_fd(brk_opinfo->fid); ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level); if (brk_opinfo->op_state == OPLOCK_CLOSING) @@ -1230,7 +1238,8 @@ static void oplock_break_drain_none(struct list_head *head) struct oplock_break_entry *ent, *tmp; list_for_each_entry_safe(ent, tmp, head, list) { - oplock_break(ent->opinfo, SMB2_OPLOCK_LEVEL_NONE, NULL, false); + oplock_break(ent->opinfo, SMB2_OPLOCK_LEVEL_NONE, NULL, false, + false); list_del(&ent->list); opinfo_put(ent->opinfo); kfree(ent); @@ -1547,7 +1556,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, prev_fid = prev_op_snapshot.fid; err = oplock_break(prev_opinfo, break_level, work, - share_ret < 0 && prev_opinfo->is_lease); + share_ret < 0 && prev_opinfo->is_lease, false); if (prev_durable_detached || (prev_durable_open && err == -ENOENT)) ksmbd_invalidate_durable_fd(prev_fid); opinfo_put(prev_opinfo); @@ -1645,7 +1654,7 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work, } brk_opinfo->open_trunc = is_trunc; - oplock_break(brk_opinfo, SMB2_OPLOCK_LEVEL_II, work, false); + oplock_break(brk_opinfo, SMB2_OPLOCK_LEVEL_II, work, false, false); sent_break = true; opinfo_put(brk_opinfo); @@ -1660,10 +1669,12 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work, * @is_trunc: truncate on open * @send_interim: send interim response to the client * @send_oplock_break: send oplock break notification to the client + * @sync_lease_break: send the lease break notification synchronously */ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc, - bool send_interim, bool send_oplock_break) + bool send_interim, bool send_oplock_break, + bool sync_lease_break) { struct oplock_info *op, *brk_op; struct oplock_break_entry *ent, *tmp; @@ -1736,7 +1747,7 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, brk_op->is_lease && !is_trunc ? SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE, send_interim && !sent_interim ? work : NULL, - false); + false, sync_lease_break); } sent_interim = true; list_del(&ent->list); @@ -1751,19 +1762,24 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc) { - __smb_break_all_levII_oplock(work, fp, is_trunc, true, true); + __smb_break_all_levII_oplock(work, fp, is_trunc, true, true, false); +} + +void smb_break_all_levII_oplock_rename(struct ksmbd_work *work, struct ksmbd_file *fp) +{ + __smb_break_all_levII_oplock(work, fp, 0, true, true, true); } void smb_break_all_levII_oplock_no_interim(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc) { - __smb_break_all_levII_oplock(work, fp, is_trunc, false, true); + __smb_break_all_levII_oplock(work, fp, is_trunc, false, true, false); } void smb_break_all_levII_oplock_for_delete(struct ksmbd_work *work, struct ksmbd_file *fp) { - __smb_break_all_levII_oplock(work, fp, 0, false, false); + __smb_break_all_levII_oplock(work, fp, 0, false, false, false); } /** @@ -1780,7 +1796,7 @@ void smb_break_all_oplock(struct ksmbd_work *work, struct ksmbd_file *fp) return; sent_break = smb_break_all_write_oplock(work, fp, 1); - __smb_break_all_levII_oplock(work, fp, 1, !sent_break, true); + __smb_break_all_levII_oplock(work, fp, 1, !sent_break, true, false); } /** @@ -2271,7 +2287,6 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, if (!atomic_inc_not_zero(&opinfo->refcount)) continue; ret_op = opinfo; - break; } spin_unlock(&lease->lock); if (ret_op) { diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index aef296b21395..ee1550f5c177 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -98,7 +98,8 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, struct ksmbd_file *fp, __u16 tid, struct lease_ctx_info *lctx, int share_ret); void smb_break_all_levII_oplock(struct ksmbd_work *work, - struct ksmbd_file *fp, int is_trunc); + struct ksmbd_file *fp, int is_trunc); +void smb_break_all_levII_oplock_rename(struct ksmbd_work *work, struct ksmbd_file *fp); void smb_break_all_levII_oplock_no_interim(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc); void smb_break_all_levII_oplock_for_delete(struct ksmbd_work *work, diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 91abbb90f262..685746c2b501 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7135,9 +7135,12 @@ static int smb2_rename(struct ksmbd_work *work, if (!file_info->ReplaceIfExists) flags = RENAME_NOREPLACE; + rc = ksmbd_vfs_check_rename_share(work, &fp->filp->f_path); + if (rc) + goto out; + + smb_break_all_levII_oplock_rename(work, fp); rc = ksmbd_vfs_rename(work, &fp->filp->f_path, new_name, flags); - if (!rc) - smb_break_all_levII_oplock(work, fp, 0); out: kfree(new_name); return rc; diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 33d47f9f1d69..854a09020a97 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -658,15 +658,34 @@ int ksmbd_vfs_link(struct ksmbd_work *work, const char *oldname, return err; } +int ksmbd_vfs_check_rename_share(struct ksmbd_work *work, + const struct path *old_path) +{ + struct ksmbd_file *parent_fp; + int err = 0; + + parent_fp = ksmbd_lookup_fd_inode(old_path->dentry->d_parent); + if (!parent_fp) + return 0; + + if ((parent_fp->daccess & FILE_DELETE_LE) || + (!parent_fp->attrib_only && + !(parent_fp->saccess & FILE_SHARE_DELETE_LE))) { + ksmbd_debug(VFS, "parent dir blocks delete sharing\n"); + err = -ESHARE; + } + ksmbd_fd_put(work, parent_fp); + return err; +} + int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, - char *newname, int flags) + char *newname, int flags) { struct dentry *old_child = old_path->dentry; struct path new_path; struct qstr new_last; struct renamedata rd; struct ksmbd_share_config *share_conf = work->tcon->share_conf; - struct ksmbd_file *parent_fp; int err, lookup_flags = LOOKUP_NO_SYMLINKS; if (ksmbd_override_fsids(work)) @@ -704,18 +723,9 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, goto out3; } - parent_fp = ksmbd_lookup_fd_inode(old_child->d_parent); - if (parent_fp) { - if ((parent_fp->daccess & FILE_DELETE_LE) || - (!parent_fp->attrib_only && - !(parent_fp->saccess & FILE_SHARE_DELETE_LE))) { - pr_err("parent dir blocks delete sharing\n"); - err = -ESHARE; - ksmbd_fd_put(work, parent_fp); - goto out3; - } - ksmbd_fd_put(work, parent_fp); - } + err = ksmbd_vfs_check_rename_share(work, old_path); + if (err) + goto out3; if (d_is_symlink(rd.new_dentry)) { err = -EACCES; diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 022b78268a7e..10cb37a78c0d 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -89,7 +89,9 @@ int ksmbd_vfs_link(struct ksmbd_work *work, const char *oldname, const char *newname); int ksmbd_vfs_getattr(const struct path *path, struct kstat *stat); int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, - char *newname, int flags); + char *newname, int flags); +int ksmbd_vfs_check_rename_share(struct ksmbd_work *work, + const struct path *old_path); int ksmbd_vfs_truncate(struct ksmbd_work *work, struct ksmbd_file *fp, loff_t size); struct srv_copychunk; From 8dd5ca858d26f947c59297ba96a8446a11d7aebf Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 12 Jul 2026 14:03:23 +0900 Subject: [PATCH 061/142] ksmbd: check base file delete pending for stream opens A base file that has been marked for deletion remains present while stream handles are open. Name-based opens of either the base file or one of its streams must return STATUS_DELETE_PENDING during that interval. ksmbd_inode_pending_delete() returned only the per-handle stream state for stream handles. It therefore skipped the inode-wide S_DEL_PENDING state set by the base file delete-on-close path. As a result, a new stream open incorrectly succeeded. Check the inode-wide pending-delete state first for every handle. Only when the base file is not pending, check the per-handle stream state. Signed-off-by: Namjae Jeon --- fs/smb/server/vfs_cache.c | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 05114d595b2a..cb6b81d110e0 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -229,25 +229,15 @@ bool ksmbd_inode_pending_delete(struct ksmbd_file *fp) struct ksmbd_inode *ci = fp->f_ci; int ret; - /* - * Stream delete-pending is tracked per-handle (see - * ksmbd_fd_set_delete_pending()), not on the shared inode -- the - * whole-file flags checked below would never see it set, and would - * also incorrectly report a whole-file pending-delete as applying - * to an unrelated stream handle on the same inode. - */ - if (ksmbd_stream_fd(fp)) { - bool pending; - - spin_lock(&fp->f_lock); - pending = fp->stream_del_pending; - spin_unlock(&fp->f_lock); - return pending; - } - down_read(&ci->m_lock); ret = (ci->m_flags & S_DEL_PENDING); up_read(&ci->m_lock); + if (ret || !ksmbd_stream_fd(fp)) + return ret; + + spin_lock(&fp->f_lock); + ret = fp->stream_del_pending; + spin_unlock(&fp->f_lock); return ret; } From dd562212178ef7bcd24e17efff172143fe2b705e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 12 Jul 2026 14:31:02 +0900 Subject: [PATCH 062/142] ksmbd: validate object id handles before response buffers FSCTL_CREATE_OR_GET_OBJECT_ID requires a fixed-size output buffer, but an invalid file handle must take precedence over output buffer validation. Look up the handle before checking the available response buffer size. This returns STATUS_FILE_CLOSED for a closed handle while preserving the buffer size validation for valid handles. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 685746c2b501..39ed43214aa5 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9615,17 +9615,18 @@ int smb2_ioctl(struct ksmbd_work *work) struct file_object_buf_type1_ioctl_rsp *obj_buf; struct ksmbd_file *fp; - if (out_buf_len < sizeof(struct file_object_buf_type1_ioctl_rsp)) { - ret = -EINVAL; - goto out; - } - fp = ksmbd_lookup_fd_fast(work, id); if (!fp) { ret = -EBADF; rsp->hdr.Status = STATUS_FILE_CLOSED; goto out2; } + + if (out_buf_len < sizeof(struct file_object_buf_type1_ioctl_rsp)) { + ksmbd_fd_put(work, fp); + ret = -EINVAL; + goto out; + } ksmbd_fd_put(work, fp); nbytes = sizeof(struct file_object_buf_type1_ioctl_rsp); From 125c471ca9c954ab171cb02035f8a05baf04404a Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 12 Jul 2026 20:20:37 +0900 Subject: [PATCH 063/142] ksmbd: preserve DOS attributes across truncating opens An existing file can be opened with a truncating create request that supplies FileAttributes. Do not reset its cached DOS attributes while opening it. After a successful truncation, apply the requested attributes and store them in the DOS attribute xattr. This preserves READONLY when a truncating open requests that attribute. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 39ed43214aa5..d5fcb1108898 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3395,6 +3395,7 @@ int smb2_open(struct ksmbd_work *work) u64 time, alloc_size = 0; umode_t posix_mode = 0; __le32 daccess, maximal_access = 0; + u32 dos_attr; int iov_len = 0; ksmbd_debug(SMB, "Received smb2 create request\n"); @@ -4205,12 +4206,18 @@ int smb2_open(struct ksmbd_work *work) fp->change_time = ksmbd_UnixTimeToNT(stat.ctime); fp->allocation_size = S_ISDIR(stat.mode) ? 0 : (alloc_size ?: stat.blocks << 9); - if (req->FileAttributes || fp->f_ci->m_fattr == 0) + if (created || fp->f_ci->m_fattr == 0) fp->f_ci->m_fattr = cpu_to_le32(smb2_get_dos_mode(&stat, le32_to_cpu(req->FileAttributes))); if (!created) smb2_update_xattrs(tcon, &path, fp); + if (need_truncate && req->FileAttributes) { + dos_attr = le32_to_cpu(req->FileAttributes); + fp->f_ci->m_fattr = + cpu_to_le32(smb2_get_dos_mode(&stat, dos_attr)); + smb2_new_xattrs(tcon, &path, fp); + } ksmbd_vfs_update_compressed_fattr(path.dentry, &fp->f_ci->m_fattr); From f495154703cbcc0050cfd53469bd56965791616b Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 13 Jul 2026 10:09:27 +0900 Subject: [PATCH 064/142] ksmbd: retain connection for pending notify work Deferred CHANGE_NOTIFY work keeps an async message ID after the original request work is released. A durable handle can outlive its connection, so the connection teardown can destroy its async IDA before the handle close releases the pending notify work. Give the synthetic deferred work a connection reference. Release it after the async ID in ksmbd_free_work_struct(). This keeps the async IDA alive until the deferred work is released, even when the original connection has already left the connection list. During server shutdown there is no client to receive a cleanup response. Skip the write and only release the pending work. Signed-off-by: Namjae Jeon --- fs/smb/server/ksmbd_work.c | 2 ++ fs/smb/server/ksmbd_work.h | 2 ++ fs/smb/server/smb2pdu.c | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index 97502273a49c..c3f8915c3952 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -86,6 +86,8 @@ void ksmbd_free_work_struct(struct ksmbd_work *work) if (work->async_id) ksmbd_release_id(&work->conn->async_ida, work->async_id); + if (work->owns_conn_ref) + ksmbd_conn_put(work->conn); kmem_cache_free(work_cache, work); } diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index 50c4aa779647..e35a40d764d6 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -90,6 +90,8 @@ struct ksmbd_work { bool compress_response:1; /* Is this SYNC or ASYNC ksmbd_work */ bool asynchronous:1; + /* Work owns a reference to @conn. */ + bool owns_conn_ref:1; bool need_invalidate_rkey:1; unsigned int remote_key; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index d5fcb1108898..ca208a686f86 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10510,7 +10510,9 @@ int smb2_notify(struct ksmbd_work *work) smb2_send_interim_resp(work, STATUS_PENDING); - in_work->conn = work->conn; + /* Keep the async IDA alive until the deferred work is released. */ + in_work->conn = ksmbd_conn_get(work->conn); + in_work->owns_conn_ref = true; in_hdr = smb_get_msg(in_work->response_buf); memcpy(in_hdr, ksmbd_resp_buf_next(work), __SMB2_HEADER_STRUCTURE_SIZE); in_hdr->Flags |= SMB2_FLAGS_ASYNC_COMMAND; From bb8bf7eb13b518b379356a0c5dee8a23d6ee37ac Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 19 Jul 2026 00:36:32 +0900 Subject: [PATCH 065/142] ksmbd: add SMB3 request replay support SMB3 clients can replay selected requests after a channel disconnect by setting SMB2_FLAGS_REPLAY_OPERATION. The command sequence window rejects duplicate MessageIds on one connection, but it does not validate requests resent on another channel with a new MessageId. Add the state and validation required to replay durable CREATE and file-handle operations: - track each open ChannelSequence, outstanding request counts, and lock sequence entries. - retain a request-owned open reference until the common response path completes ChannelSequence accounting. - replay DurableHandleReqV2 CREATE requests by CreateGuid, validating the durable state, SecurityContext, session, lease key, and persistent flag. - publish CreateGuid and SecurityContext before an oplock or lease break can defer CREATE, rejecting replays of that pending CREATE with STATUS_FILE_NOT_AVAILABLE. - retain the original CREATE action and replay completed CreateGuid requests, including requests that did not receive a durable-handle grant, without modifying the existing open. - make replayed oplock and lease break acknowledgements idempotent. And - preserve SMB2_FLAGS_REPLAY_OPERATION in responses. Return STATUS_FILE_NOT_AVAILABLE when ChannelSequence validation rejects a replayed WRITE, IOCTL, or SET_INFO request. Signed-off-by: Namjae Jeon --- fs/smb/server/ksmbd_work.c | 2 + fs/smb/server/ksmbd_work.h | 9 + fs/smb/server/mgmt/user_session.c | 8 - fs/smb/server/oplock.c | 39 ++- fs/smb/server/oplock.h | 2 +- fs/smb/server/server.c | 9 +- fs/smb/server/smb2pdu.c | 525 ++++++++++++++++++++++++++++-- fs/smb/server/smb2pdu.h | 1 + fs/smb/server/vfs_cache.c | 13 +- fs/smb/server/vfs_cache.h | 18 + 10 files changed, 587 insertions(+), 39 deletions(-) diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index c3f8915c3952..f35335307670 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -11,6 +11,7 @@ #include "server.h" #include "connection.h" #include "ksmbd_work.h" +#include "vfs_cache.h" #include "mgmt/ksmbd_ida.h" static struct kmem_cache *work_cache; @@ -88,6 +89,7 @@ void ksmbd_free_work_struct(struct ksmbd_work *work) ksmbd_release_id(&work->conn->async_ida, work->async_id); if (work->owns_conn_ref) ksmbd_conn_put(work->conn); + ksmbd_fd_put(work, work->request_open); kmem_cache_free(work_cache, work); } diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index e35a40d764d6..52d0c4dee65c 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -12,6 +12,7 @@ struct ksmbd_conn; struct ksmbd_session; struct ksmbd_tree_connect; +struct ksmbd_file; #define KSMBD_WORK_INLINE_IOVS 4 @@ -93,6 +94,7 @@ struct ksmbd_work { /* Work owns a reference to @conn. */ bool owns_conn_ref:1; bool need_invalidate_rkey:1; + bool request_open_chseq_tracked:1; unsigned int remote_key; /* cancel works */ @@ -100,6 +102,13 @@ struct ksmbd_work { void **cancel_argv; void (*cancel_fn)(void **argv); + /* + * Refcounted open associated with the SMB2 command currently being + * processed. + */ + struct ksmbd_file *request_open; + __le16 request_open_chseq; + struct work_struct work; /* List head at conn->requests */ struct list_head request_entry; diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 5b9bd46ff3a8..cbe00f00f3f6 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -478,14 +478,6 @@ void ksmbd_sessions_deregister(struct ksmbd_conn *conn) down_write(&conn->session_lock); xa_for_each(&conn->sessions, id, sess) { - unsigned long chann_id; - struct channel *chann; - - xa_for_each(&sess->ksmbd_chann_list, chann_id, chann) { - if (chann->conn != conn) - ksmbd_conn_set_exiting(chann->conn); - } - ksmbd_chann_del(conn, sess); if (xa_empty(&sess->ksmbd_chann_list)) { xa_erase(&conn->sessions, sess->id); diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 74bd2fadc757..a534fe6c26b2 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1203,6 +1203,18 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, if (brk_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH || brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) brk_opinfo->op_state = OPLOCK_ACK_WAIT; + + /* + * Keep a conflicting CREATE asynchronous while waiting for an + * oplock-break acknowledgement. Besides avoiding a blocked client + * request, this lets a replay arrive while the original CREATE is + * still pending and be rejected with FILE_NOT_AVAILABLE. + */ + if (in_work) { + setup_async_work(in_work, NULL, NULL); + smb2_send_interim_resp(in_work, STATUS_PENDING); + release_async_work(in_work); + } } err = smb2_oplock_break_noti(brk_opinfo); @@ -1445,12 +1457,13 @@ void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp) * @tid: Tree id of connection * @lctx: lease context information on file open * @share_ret: share mode + * @replay: whether this is a replayed CREATE request * * Return: 0 on success, otherwise error */ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, struct ksmbd_file *fp, __u16 tid, - struct lease_ctx_info *lctx, int share_ret) + struct lease_ctx_info *lctx, int share_ret, bool replay) { int err = 0; int break_level = SMB2_OPLOCK_LEVEL_II; @@ -1535,6 +1548,21 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, prev_op_has_lease = prev_opinfo->is_lease; if (prev_op_has_lease) prev_op_state = prev_opinfo->o_lease->state; + /* + * A replay received while this open is waiting for an oplock or lease + * break must not observe an intermediate level and proceed as a new + * open. This check has to precede break_needed. an oplock may already + * have been downgraded from Batch to II while its acknowledgement is + * still pending. + */ + if (replay && + (test_bit(0, &prev_opinfo->pending_break) || + prev_opinfo->op_state == OPLOCK_ACK_WAIT)) { + err = -EINPROGRESS; + opinfo_put(prev_opinfo); + goto err_out; + } + if (share_ret < 0 && prev_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) { err = share_ret; @@ -1569,7 +1597,14 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, goto set_lev; } if (err == -ENOENT) { - if (req_op_level != SMB2_OPLOCK_LEVEL_NONE) + /* + * A pending durable CREATE can lose the previous oplock when + * its holder closes the file. In that case grant the original + * request its full caching state. Other opens still need the + * normal shared-open downgrade below. + */ + if (!prev_durable_open && + req_op_level != SMB2_OPLOCK_LEVEL_NONE) req_op_level = SMB2_OPLOCK_LEVEL_II; goto set_lev; } diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index ee1550f5c177..23274b645ede 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -96,7 +96,7 @@ struct oplock_break_info { int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, struct ksmbd_file *fp, __u16 tid, - struct lease_ctx_info *lctx, int share_ret); + struct lease_ctx_info *lctx, int share_ret, bool replay); void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc); void smb_break_all_levII_oplock_rename(struct ksmbd_work *work, struct ksmbd_file *fp); diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 960c4c897c11..95be17b79714 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -15,6 +15,7 @@ #include "server.h" #include "smb_common.h" +#include "smb2pdu.h" #include "../common/smb2status.h" #include "connection.h" #include "transport_ipc.h" @@ -229,8 +230,10 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, } rc = __process_request(work, conn, &command); - if (rc == SERVER_HANDLER_ABORT) + if (rc == SERVER_HANDLER_ABORT) { + smb2_complete_request_open(work); break; + } /* * Call smb2_set_rsp_credits() function to set number of credits @@ -243,10 +246,13 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, if (rc < 0) { conn->ops->set_rsp_status(work, STATUS_INVALID_PARAMETER); + smb2_complete_request_open(work); goto send; } } + smb2_complete_request_open(work); + is_chained = is_chained_smb2_message(work); if (work->sess && @@ -262,6 +268,7 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, } while (is_chained == true); send: + smb2_complete_request_open(work); /* * Release any credit charge still outstanding for this request. On * the normal path smb2_set_rsp_credits() already returned it, but the diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index ca208a686f86..e38d43378464 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -554,6 +554,8 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) */ rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR | SMB2_FLAGS_RELATED_OPERATIONS); + if (rcv_hdr->Flags & SMB2_FLAGS_REPLAY_OPERATION) + rsp_hdr->Flags |= SMB2_FLAGS_REPLAY_OPERATION; rsp_hdr->NextCommand = 0; rsp_hdr->MessageId = rcv_hdr->MessageId; rsp_hdr->Id.SyncId.ProcessId = rcv_hdr->Id.SyncId.ProcessId; @@ -646,6 +648,8 @@ int init_smb2_rsp_hdr(struct ksmbd_work *work) * Message is response. We don't grant oplock yet. */ rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR); + if (rcv_hdr->Flags & SMB2_FLAGS_REPLAY_OPERATION) + rsp_hdr->Flags |= SMB2_FLAGS_REPLAY_OPERATION; rsp_hdr->NextCommand = 0; rsp_hdr->MessageId = rcv_hdr->MessageId; rsp_hdr->Id.SyncId.ProcessId = rcv_hdr->Id.SyncId.ProcessId; @@ -656,6 +660,185 @@ int init_smb2_rsp_hdr(struct ksmbd_work *work) return 0; } +static __le16 smb3_hdr_channel_sequence(struct smb2_hdr *hdr) +{ + return ((struct smb3_hdr_req *)hdr)->ChannelSequence; +} + +static bool smb3_hdr_replay(struct smb2_hdr *hdr) +{ + return hdr->Flags & SMB2_FLAGS_REPLAY_OPERATION; +} + +static int smb3_verify_channel_sequence(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct smb2_hdr *hdr, + bool allow_stale) +{ + __le16 chseq_le; + u16 chseq, old_chseq; + int ret = 0; + + if (work->conn->dialect < SMB30_PROT_ID) + return 0; + + chseq_le = smb3_hdr_channel_sequence(hdr); + chseq = le16_to_cpu(chseq_le); + + spin_lock(&fp->f_lock); + old_chseq = le16_to_cpu(fp->channel_sequence); + if (smb3_hdr_replay(hdr)) { + if (chseq == old_chseq && fp->outstanding_pre_requests == 0) { + fp->outstanding_requests++; + } else if ((u16)(chseq - old_chseq) <= 0x7fff && + fp->outstanding_pre_requests == 0) { + fp->outstanding_pre_requests += fp->outstanding_requests; + fp->outstanding_requests = 1; + fp->channel_sequence = chseq_le; + } else if (allow_stale) { + fp->outstanding_pre_requests++; + } else { + ret = -EAGAIN; + } + } else { + if (chseq == old_chseq) { + fp->outstanding_requests++; + } else if ((u16)(chseq - old_chseq) <= 0x7fff) { + fp->outstanding_pre_requests += fp->outstanding_requests; + fp->outstanding_requests = 1; + fp->channel_sequence = chseq_le; + } else if (allow_stale) { + fp->outstanding_pre_requests++; + } else { + ret = -EAGAIN; + } + } + spin_unlock(&fp->f_lock); + + return ret; +} + +static void smb3_complete_channel_sequence(struct ksmbd_work *work, + struct ksmbd_file *fp, + __le16 chseq_le) +{ + u16 chseq; + + if (work->conn->dialect < SMB30_PROT_ID) + return; + + chseq = le16_to_cpu(chseq_le); + + spin_lock(&fp->f_lock); + if (chseq == le16_to_cpu(fp->channel_sequence)) { + if (fp->outstanding_requests) + fp->outstanding_requests--; + } else { + if (fp->outstanding_pre_requests) + fp->outstanding_pre_requests--; + } + spin_unlock(&fp->f_lock); +} + +static int smb2_set_request_open(struct ksmbd_work *work, struct ksmbd_file *fp, + struct smb2_hdr *hdr, bool verify_chseq, + bool allow_stale_chseq) +{ + struct ksmbd_file *open; + int ret; + + smb2_complete_request_open(work); + + open = ksmbd_file_get(fp); + if (!open) + return -ESTALE; + + if (verify_chseq) { + ret = smb3_verify_channel_sequence(work, fp, hdr, + allow_stale_chseq); + if (ret) { + ksmbd_fd_put(work, open); + return ret; + } + work->request_open_chseq_tracked = true; + } + + work->request_open = open; + work->request_open_chseq = smb3_hdr_channel_sequence(hdr); + return 0; +} + +void smb2_complete_request_open(struct ksmbd_work *work) +{ + struct ksmbd_file *open = work->request_open; + + if (!open) + return; + + if (work->request_open_chseq_tracked) + smb3_complete_channel_sequence(work, open, + work->request_open_chseq); + + work->request_open = NULL; + work->request_open_chseq_tracked = false; + ksmbd_fd_put(work, open); +} + +static bool smb2_lock_sequence_applicable(struct ksmbd_work *work, + struct ksmbd_file *fp) +{ + return fp->is_resilient || fp->is_durable || fp->is_persistent || + (work->conn->dialect >= SMB30_PROT_ID && + (work->conn->cli_cap & SMB2_GLOBAL_CAP_MULTI_CHANNEL)); +} + +static void smb2_verify_lock_sequence(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct smb2_lock_req *req) +{ + u32 val, index; + u8 sequence; + + if (work->conn->dialect == SMB20_PROT_ID || + !smb2_lock_sequence_applicable(work, fp)) + return; + + val = le32_to_cpu(req->LockSequenceNumber); + sequence = val & 0xf; + index = val >> 4; + if (!index || index > KSMBD_LOCK_SEQ_ARRAY_SIZE) + return; + + spin_lock(&fp->f_lock); + if (fp->lock_seq[index - 1].valid && + fp->lock_seq[index - 1].sequence != sequence) + fp->lock_seq[index - 1].valid = false; + spin_unlock(&fp->f_lock); +} + +static void smb2_update_lock_sequence(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct smb2_lock_req *req) +{ + u32 val, index; + u8 sequence; + + if (work->conn->dialect == SMB20_PROT_ID || + !smb2_lock_sequence_applicable(work, fp)) + return; + + val = le32_to_cpu(req->LockSequenceNumber); + sequence = val & 0xf; + index = val >> 4; + if (!index || index > KSMBD_LOCK_SEQ_ARRAY_SIZE) + return; + + spin_lock(&fp->f_lock); + fp->lock_seq[index - 1].valid = true; + fp->lock_seq[index - 1].sequence = sequence; + spin_unlock(&fp->f_lock); +} + /** * smb2_allocate_rsp_buf() - allocate smb2 response buffer * @work: smb work containing smb request buffer @@ -3142,12 +3325,86 @@ struct durable_info { unsigned short int type; bool persistent; bool reconnected; + bool replay; + bool replay_consumed; bool app_instance_id; unsigned int timeout; char *CreateGuid; char AppInstanceId[SMB2_CREATE_GUID_SIZE]; }; +static int smb2_check_durable_replay(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct lease_ctx_info *lc, + bool persistent) +{ + struct oplock_info *opinfo; + int ret = 0; + + if (!fp->is_durable && !fp->is_persistent) + return -EACCES; + + if (ksmbd_vfs_compare_durable_owner(fp, work->sess->user) == false) + return -EACCES; + + if (fp->is_persistent && !persistent) + return -EINVAL; + + opinfo = opinfo_get(fp); + if (!opinfo) + return 0; + + if (opinfo->sess && opinfo->sess->id != work->sess->id) { + ret = -ENOEXEC; + goto out; + } + + if (opinfo->is_lease) { + if (!lc || + memcmp(opinfo->o_lease->lease_key, lc->lease_key, + SMB2_LEASE_KEY_SIZE)) { + ret = -EACCES; + goto out; + } + } else { + if (lc) { + ret = -EACCES; + goto out; + } + + if (fp->is_durable && opinfo->level != SMB2_OPLOCK_LEVEL_BATCH) + ret = -EACCES; + } +out: + opinfo_put(opinfo); + return ret; +} + +static bool smb2_durable_replay_consumed(struct ksmbd_file *fp) +{ + bool consumed; + + spin_lock(&fp->f_lock); + consumed = fp->durable_replay_consumed; + spin_unlock(&fp->f_lock); + + return consumed; +} + +static void smb2_mark_durable_replay_consumed(struct ksmbd_file *fp) +{ + spin_lock(&fp->f_lock); + fp->durable_replay_consumed = true; + spin_unlock(&fp->f_lock); +} + +static bool smb2_durable_replay_differs(struct ksmbd_file *fp, + struct smb2_create_req *req) +{ + return fp->cdoption != req->CreateDisposition || + fp->create_file_attributes != req->FileAttributes; +} + static int parse_durable_handle_context(struct ksmbd_work *work, struct smb2_create_req *req, struct lease_ctx_info *lc, @@ -3275,6 +3532,10 @@ static int parse_durable_handle_context(struct ksmbd_work *work, durable_v2_blob = (struct create_durable_req_v2 *)context; ksmbd_debug(SMB, "Request for durable v2 open\n"); + dh_info->CreateGuid = durable_v2_blob->dcontext.CreateGuid; + dh_info->persistent = + le32_to_cpu(durable_v2_blob->dcontext.Flags) & + SMB2_DHANDLE_FLAG_PERSISTENT; dh_info->fp = ksmbd_lookup_fd_cguid(durable_v2_blob->dcontext.CreateGuid); if (dh_info->fp) { if (!memcmp(conn->ClientGUID, dh_info->fp->client_guid, @@ -3285,12 +3546,73 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - if (dh_info->fp->conn) { + if (dh_info->fp->f_state == FP_NEW) { + /* Original CREATE is still pending. */ ksmbd_put_durable_fd(dh_info->fp); - err = -EBADF; + err = -EAGAIN; goto out; } - dh_info->reconnected = true; + + if (!dh_info->fp->is_durable && + !dh_info->fp->is_persistent) { + /* + * A DurableHandleReqV2 CREATE can complete + * without granting durability (for example, if + * it requested no oplock). Its CreateGuid still + * identifies a completed CREATE for replay. + */ + if (dh_info->fp->conn && + ksmbd_vfs_compare_durable_owner( + dh_info->fp, work->sess->user)) { + if (smb2_durable_replay_consumed( + dh_info->fp)) { + ksmbd_put_durable_fd(dh_info->fp); + dh_info->fp = NULL; + dh_info->type = dh_idx; + dh_info->replay_consumed = true; + break; + } + if (smb2_durable_replay_differs( + dh_info->fp, req)) + smb2_mark_durable_replay_consumed( + dh_info->fp); + dh_info->replay = true; + dh_info->type = dh_idx; + goto out; + } + ksmbd_put_durable_fd(dh_info->fp); + err = -EACCES; + goto out; + } + + if (dh_info->fp->conn && + smb2_durable_replay_consumed(dh_info->fp)) { + ksmbd_put_durable_fd(dh_info->fp); + dh_info->fp = NULL; + dh_info->type = dh_idx; + dh_info->replay_consumed = true; + break; + } + + err = smb2_check_durable_replay(work, + dh_info->fp, + lc, + dh_info->persistent); + if (err) { + ksmbd_put_durable_fd(dh_info->fp); + goto out; + } + + if (dh_info->fp->conn) { + if (smb2_durable_replay_differs(dh_info->fp, + req)) + smb2_mark_durable_replay_consumed( + dh_info->fp); + dh_info->replay = true; + } else { + dh_info->reconnected = true; + } + dh_info->type = dh_idx; goto out; } ksmbd_put_durable_fd(dh_info->fp); @@ -3299,10 +3621,6 @@ static int parse_durable_handle_context(struct ksmbd_work *work, if ((lc && (lc->req_state & SMB2_LEASE_HANDLE_CACHING_LE)) || req_op_level == SMB2_OPLOCK_LEVEL_BATCH) { - dh_info->CreateGuid = - durable_v2_blob->dcontext.CreateGuid; - dh_info->persistent = - le32_to_cpu(durable_v2_blob->dcontext.Flags); dh_info->timeout = le32_to_cpu(durable_v2_blob->dcontext.Timeout); dh_info->type = dh_idx; @@ -3385,6 +3703,7 @@ int smb2_open(struct ksmbd_work *work) int contxt_cnt = 0, query_disk_id = 0; bool maximal_access_ctxt = false, posix_ctxt = false; bool aapl_ctxt = false; + bool durable_rsp = true; __u64 aapl_req_bitmap = 0, aapl_client_caps = 0; int s_type = 0; int next_off = 0; @@ -3504,6 +3823,21 @@ int smb2_open(struct ksmbd_work *work) if (rc) goto err_out2; + if (dh_info.replay == true) { + fp = dh_info.fp; + if (ksmbd_override_fsids(work)) { + rc = -ENOMEM; + goto err_out2; + } + + file_info = FILE_OPENED; + rc = ksmbd_vfs_getattr(&fp->filp->f_path, &stat); + if (rc) + goto err_out2; + + goto reconnected_fp; + } + if (dh_info.reconnected == true) { rc = smb2_check_durable_oplock(conn, share, dh_info.fp, lc, sess->user, name); @@ -3928,7 +4262,23 @@ int smb2_open(struct ksmbd_work *work) goto err_out; } + /* + * Publish the client and create GUID before an oplock/lease break can + * make this CREATE pending. A replay of that in-flight CREATE must find + * this FP_NEW handle and fail with STATUS_FILE_NOT_AVAILABLE instead of + * waiting on the same break again. + */ + memcpy(fp->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); + if (dh_info.CreateGuid) { + memcpy(fp->create_guid, dh_info.CreateGuid, SMB2_CREATE_GUID_SIZE); + fp->durable_replay_consumed = dh_info.replay_consumed; + rc = ksmbd_vfs_set_durable_owner(fp, sess->user); + if (rc) + goto err_out; + } + fp->cdoption = req->CreateDisposition; + fp->create_file_attributes = req->FileAttributes; fp->daccess = daccess; fp->saccess = req->ShareAccess; fp->coption = req->CreateOptions; @@ -4096,7 +4446,8 @@ int smb2_open(struct ksmbd_work *work) rc = smb_grant_oplock(work, req_op_level, fp->persistent_id, fp, le32_to_cpu(req->hdr.Id.SyncId.TreeId), - lc, share_ret); + lc, share_ret, + smb3_hdr_replay(&req->hdr)); if (rc < 0) goto err_out1; } @@ -4240,7 +4591,7 @@ int smb2_open(struct ksmbd_work *work) if (created) smb2_new_xattrs(tcon, &path, fp); - memcpy(fp->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); + fp->create_action = cpu_to_le32(file_info); if (dh_info.type == DURABLE_REQ_V2 || dh_info.type == DURABLE_REQ) { if (dh_info.type == DURABLE_REQ_V2 && dh_info.persistent && @@ -4249,10 +4600,7 @@ int smb2_open(struct ksmbd_work *work) fp->is_persistent = true; else fp->is_durable = true; - if (dh_info.type == DURABLE_REQ_V2) { - memcpy(fp->create_guid, dh_info.CreateGuid, - SMB2_CREATE_GUID_SIZE); if (dh_info.app_instance_id) memcpy(fp->app_instance_id, dh_info.AppInstanceId, @@ -4273,10 +4621,22 @@ int smb2_open(struct ksmbd_work *work) * cares, it sends its own AAPL context on this same CREATE, which * this function's normal (non-reconnect) parsing already handles. */ -reconnected_fp: + reconnected_fp: + if (dh_info.replay) + file_info = le32_to_cpu(fp->create_action); rsp->StructureSize = cpu_to_le16(89); opinfo = opinfo_get(fp); rsp->OplockLevel = opinfo != NULL ? opinfo->level : 0; + /* + * A durable CREATE replay does not modify the existing open. When + * replayed without an oplock, however, its response reflects that + * request and cannot include a new durable-handle response context. + */ + if (dh_info.replay && !lc && + req_op_level == SMB2_OPLOCK_LEVEL_NONE) { + rsp->OplockLevel = SMB2_OPLOCK_LEVEL_NONE; + durable_rsp = false; + } rsp->Flags = 0; rsp->CreateAction = cpu_to_le32(file_info); rsp->CreationTime = cpu_to_le64(fp->create_time); @@ -4377,7 +4737,8 @@ int smb2_open(struct ksmbd_work *work) next_off = conn->vals->create_disk_id_size; } - if (dh_info.type == DURABLE_REQ || dh_info.type == DURABLE_REQ_V2) { + if (durable_rsp && + (dh_info.type == DURABLE_REQ || dh_info.type == DURABLE_REQ_V2)) { struct create_context *durable_ccontext; durable_ccontext = (struct create_context *)(rsp->Buffer + @@ -4468,8 +4829,11 @@ int smb2_open(struct ksmbd_work *work) err_out2: if (!rc) { - rc = ksmbd_update_fstate(&work->sess->file_table, fp, - FP_INITED); + if (!dh_info.replay) + rc = ksmbd_update_fstate(&work->sess->file_table, fp, + FP_INITED); + if (!rc) + rc = smb2_set_request_open(work, fp, &req->hdr, false, false); if (!rc) rc = ksmbd_iov_pin_rsp(work, (void *)rsp, iov_len); } @@ -4501,15 +4865,22 @@ int smb2_open(struct ksmbd_work *work) rsp->hdr.Status = STATUS_OBJECT_NAME_COLLISION; else if (rc == -EMFILE) rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; + else if (rc == -EINPROGRESS) + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; + else if (rc == -EAGAIN) + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; if (!rsp->hdr.Status) rsp->hdr.Status = STATUS_UNEXPECTED_IO_ERROR; - if (fp) + if (fp && !dh_info.replay) ksmbd_fd_put(work, fp); smb2_set_err_rsp(work); ksmbd_debug(SMB, "Error response: %x\n", rsp->hdr.Status); } + if (dh_info.replay) + ksmbd_put_durable_fd(dh_info.fp); + if (dh_info.reconnected) { /* * If reconnect succeeded, fp was republished in the @@ -7647,6 +8018,7 @@ int smb2_set_info(struct ksmbd_work *work) struct smb2_set_info_rsp *rsp; struct ksmbd_file *fp = NULL; int rc = 0; + bool chseq_err = false; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; ksmbd_debug(SMB, "Received smb2 set info request\n"); @@ -7686,6 +8058,13 @@ int smb2_set_info(struct ksmbd_work *work) goto err_out; } + rc = smb2_set_request_open(work, fp, &req->hdr, true, false); + if (rc) { + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; + chseq_err = true; + goto err_out; + } + saved_cred = override_creds(fp->filp->f_cred); switch (req->InfoType) { case SMB2_O_INFO_FILE: @@ -7736,7 +8115,7 @@ int smb2_set_info(struct ksmbd_work *work) rsp->hdr.Status = STATUS_OBJECT_NAME_INVALID; else if (rc == -EBUSY || rc == -ENOTEMPTY) rsp->hdr.Status = STATUS_DIRECTORY_NOT_EMPTY; - else if (rc == -EAGAIN) + else if (rc == -EAGAIN && !chseq_err) rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; else if (rc == -EBADF || rc == -ESTALE) rsp->hdr.Status = STATUS_INVALID_HANDLE; @@ -7945,6 +8324,10 @@ int smb2_read(struct ksmbd_work *work) goto out; } + err = smb2_set_request_open(work, fp, &req->hdr, true, true); + if (err) + goto out; + if (!(fp->daccess & (FILE_READ_DATA_LE | FILE_READ_ATTRIBUTES_LE))) { pr_err("Not permitted to read : 0x%x\n", fp->daccess); err = -EACCES; @@ -8192,6 +8575,7 @@ int smb2_write(struct ksmbd_work *work) char *data_buf; bool writethrough = false, is_rdma_channel = false; bool async_interim = false; + bool chseq_err = false; int err = 0; unsigned int max_write_size = work->conn->vals->max_write_size; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; @@ -8281,6 +8665,13 @@ int smb2_write(struct ksmbd_work *work) goto out; } + err = smb2_set_request_open(work, fp, &req->hdr, true, false); + if (err) { + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; + chseq_err = true; + goto out; + } + if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_READ_ATTRIBUTES_LE))) { pr_err("Not permitted to write : 0x%x\n", fp->daccess); err = -EACCES; @@ -8352,7 +8743,7 @@ int smb2_write(struct ksmbd_work *work) if (async_interim) release_async_work(work); - if (err == -EAGAIN) + if (err == -EAGAIN && !chseq_err) rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; else if (err == -ENOSPC || err == -EFBIG) rsp->hdr.Status = STATUS_DISK_FULL; @@ -8364,7 +8755,7 @@ int smb2_write(struct ksmbd_work *work) rsp->hdr.Status = STATUS_SHARING_VIOLATION; else if (err == -EINVAL) rsp->hdr.Status = STATUS_INVALID_PARAMETER; - else + else if (rsp->hdr.Status == 0) rsp->hdr.Status = STATUS_INVALID_HANDLE; smb2_set_err_rsp(work); @@ -8653,6 +9044,12 @@ int smb2_lock(struct ksmbd_work *work) goto out2; } + err = smb2_set_request_open(work, fp, &req->hdr, false, false); + if (err) + goto out2; + + smb2_verify_lock_sequence(work, fp, req); + filp = fp->filp; lock_count = le16_to_cpu(req->LockCount); lock_ele = req->locks; @@ -8937,6 +9334,7 @@ int smb2_lock(struct ksmbd_work *work) err = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_lock_rsp)); if (err) goto out; + smb2_update_lock_sequence(work, fp, req); ksmbd_fd_put(work, fp); return 0; @@ -9487,10 +9885,12 @@ int smb2_ioctl(struct ksmbd_work *work) struct smb2_ioctl_req *req; struct smb2_ioctl_rsp *rsp; unsigned int cnt_code, nbytes = 0, out_buf_len, in_buf_len; - u64 id = KSMBD_NO_FID; + u64 id = KSMBD_NO_FID, pid = KSMBD_NO_FID; struct ksmbd_conn *conn = work->conn; int ret = 0; char *buffer; + bool no_fileid_ioctl = false; + bool chseq_err = false; ksmbd_debug(SMB, "Received smb2 ioctl request\n"); @@ -9503,14 +9903,17 @@ int smb2_ioctl(struct ksmbd_work *work) ksmbd_debug(SMB, "Compound request set FID = %llu\n", work->compound_fid); id = work->compound_fid; + pid = work->compound_pfid; } } else { req = smb_get_msg(work->request_buf); rsp = smb_get_msg(work->response_buf); } - if (!has_file_id(id)) + if (!has_file_id(id)) { id = req->VolatileFileId; + pid = req->PersistentFileId; + } if (req->Flags != cpu_to_le32(SMB2_0_IOCTL_IS_FSCTL)) { ret = -EOPNOTSUPP; @@ -9520,6 +9923,40 @@ int smb2_ioctl(struct ksmbd_work *work) buffer = (char *)req + le32_to_cpu(req->InputOffset); cnt_code = le32_to_cpu(req->CtlCode); + switch (cnt_code) { + case FSCTL_DFS_GET_REFERRALS: + case FSCTL_DFS_GET_REFERRALS_EX: + case FSCTL_QUERY_NETWORK_INTERFACE_INFO: + case FSCTL_VALIDATE_NEGOTIATE_INFO: + case FSCTL_PIPE_WAIT: + no_fileid_ioctl = true; + break; + default: + break; + } + + if (!no_fileid_ioctl && has_file_id(id)) { + struct ksmbd_file *fp; + + fp = ksmbd_lookup_fd_slow(work, id, pid); + if (!fp) { + if (cnt_code == FSCTL_DUPLICATE_EXTENTS_TO_FILE) { + rsp->hdr.Status = STATUS_FILE_CLOSED; + goto out2; + } + ret = -ENOENT; + goto out; + } + + ret = smb2_set_request_open(work, fp, &req->hdr, true, false); + ksmbd_fd_put(work, fp); + if (ret) { + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; + chseq_err = true; + goto out; + } + } + ret = smb2_calc_max_out_buf_len(work, offsetof(struct smb2_ioctl_rsp, Buffer), le32_to_cpu(req->MaxOutputResponse)); @@ -10055,7 +10492,7 @@ int smb2_ioctl(struct ksmbd_work *work) rsp->hdr.Status = STATUS_NOT_SUPPORTED; else if (ret == -ENOSPC) rsp->hdr.Status = STATUS_BUFFER_TOO_SMALL; - else if (ret < 0 || rsp->hdr.Status == 0) + else if (!chseq_err && (ret < 0 || rsp->hdr.Status == 0)) rsp->hdr.Status = STATUS_INVALID_PARAMETER; out2: @@ -10095,6 +10532,14 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) return; } + ret = smb2_set_request_open(work, fp, &req->hdr, false, false); + if (ret) { + rsp->hdr.Status = STATUS_FILE_CLOSED; + smb2_set_err_rsp(work); + ksmbd_fd_put(work, fp); + return; + } + opinfo = opinfo_get(fp); if (!opinfo) { pr_err("unexpected null oplock_info\n"); @@ -10107,6 +10552,22 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) if (opinfo->op_state != OPLOCK_ACK_WAIT) { ksmbd_debug(SMB, "unexpected oplock state 0x%x\n", opinfo->op_state); + if (smb3_hdr_replay(&req->hdr) && + opinfo->op_state == OPLOCK_STATE_NONE) { + rsp->StructureSize = cpu_to_le16(24); + rsp->OplockLevel = opinfo->level; + rsp->Reserved = 0; + rsp->Reserved2 = 0; + rsp->VolatileFid = volatile_id; + rsp->PersistentFid = persistent_id; + ret = ksmbd_iov_pin_rsp(work, rsp, + sizeof(struct smb2_oplock_break)); + if (ret) + ksmbd_debug(SMB, + "failed to pin replayed oplock break response: %d\n", + ret); + goto out_no_state_change; + } if (opinfo->level == SMB2_OPLOCK_LEVEL_NONE) status = STATUS_INVALID_OPLOCK_PROTOCOL; else @@ -10175,6 +10636,7 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) out: opinfo->op_state = OPLOCK_STATE_NONE; wake_up_interruptible_all(&opinfo->oplock_q); +out_no_state_change: opinfo_put(opinfo); ksmbd_fd_put(work, fp); } @@ -10227,11 +10689,15 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) if (opinfo->op_state == OPLOCK_STATE_NONE) { pr_err("unexpected lease break state 0x%x\n", opinfo->op_state); + if (smb3_hdr_replay(&req->hdr)) + goto replay_rsp; rsp->hdr.Status = STATUS_UNSUCCESSFUL; goto err_out; } if (!atomic_read(&opinfo->breaking_cnt)) { + if (smb3_hdr_replay(&req->hdr)) + goto replay_rsp; rsp->hdr.Status = STATUS_UNSUCCESSFUL; goto err_out; } @@ -10266,6 +10732,19 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) opinfo_put(opinfo); return; +replay_rsp: + rsp->StructureSize = cpu_to_le16(36); + rsp->Reserved = 0; + rsp->Flags = 0; + memcpy(rsp->LeaseKey, req->LeaseKey, 16); + rsp->LeaseState = lease->state; + rsp->LeaseDuration = 0; + ret = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_lease_ack)); + if (ret) + goto err_out; + opinfo_put(opinfo); + return; + err_out: smb2_set_err_rsp(work); opinfo_put(opinfo); diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index 9d77400a1670..3f08d1ca5a38 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -428,6 +428,7 @@ bool smb3_encryption_negotiated(struct ksmbd_conn *conn); /* smb2 misc functions */ int ksmbd_smb2_check_message(struct ksmbd_work *work); +void smb2_complete_request_open(struct ksmbd_work *work); /* smb2 command handlers */ int smb2_handle_negotiate(struct ksmbd_work *work); diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index cb6b81d110e0..2aa27f449692 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -729,6 +729,11 @@ static struct ksmbd_file *ksmbd_fp_get(struct ksmbd_file *fp) return fp; } +struct ksmbd_file *ksmbd_file_get(struct ksmbd_file *fp) +{ + return ksmbd_fp_get(fp); +} + static struct ksmbd_file *__ksmbd_lookup_fd(struct ksmbd_file_table *ft, u64 id) { @@ -1572,7 +1577,7 @@ void ksmbd_stop_durable_scavenger(void) } /* - * ksmbd_vfs_copy_durable_owner - Copy owner info for durable reconnect + * ksmbd_vfs_set_durable_owner - Store owner info for durable replay/reconnect * @fp: ksmbd file pointer to store owner info * @user: user pointer to copy from * @@ -1581,8 +1586,8 @@ void ksmbd_stop_durable_scavenger(void) * * Return: 0 on success, or negative error code on failure */ -static int ksmbd_vfs_copy_durable_owner(struct ksmbd_file *fp, - struct ksmbd_user *user) +int ksmbd_vfs_set_durable_owner(struct ksmbd_file *fp, + struct ksmbd_user *user) { char *name; @@ -1653,7 +1658,7 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, if (WARN_ON_ONCE(!fp->conn)) return false; - if (ksmbd_vfs_copy_durable_owner(fp, user)) + if (ksmbd_vfs_set_durable_owner(fp, user)) return false; /* diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 1d9edc906b54..5aff9bb556ec 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -82,6 +82,13 @@ struct durable_owner { char *name; }; +#define KSMBD_LOCK_SEQ_ARRAY_SIZE 64 + +struct ksmbd_lock_sequence { + bool valid; + u8 sequence; +}; + struct ksmbd_file { struct file *filp; u64 persistent_id; @@ -101,6 +108,7 @@ struct ksmbd_file { __le32 saccess; __le32 coption; __le32 cdoption; + __le32 create_file_attributes; __u64 create_time; __u64 change_time; __u64 allocation_size; @@ -128,6 +136,8 @@ struct ksmbd_file { unsigned int durable_timeout; unsigned int durable_scavenger_timeout; + /* CREATE action returned when this durable handle was established. */ + __le32 create_action; /* if ls is happening on directory, below is valid*/ struct ksmbd_readdir_data readdir_data; @@ -139,9 +149,14 @@ struct ksmbd_file { bool is_persistent; bool is_resilient; bool durable_reconnect_disabled; + bool durable_replay_consumed; bool is_posix_ctxt; struct durable_owner owner; + __le16 channel_sequence; + unsigned int outstanding_requests; + unsigned int outstanding_pre_requests; + struct ksmbd_lock_sequence lock_seq[KSMBD_LOCK_SEQ_ARRAY_SIZE]; /* * Pending CHANGE_NOTIFY completions for this handle, sent with @@ -180,6 +195,9 @@ struct ksmbd_file *ksmbd_lookup_fd_fast(struct ksmbd_work *work, u64 id); struct ksmbd_file *ksmbd_lookup_foreign_fd(struct ksmbd_work *work, u64 id); struct ksmbd_file *ksmbd_lookup_fd_slow(struct ksmbd_work *work, u64 id, u64 pid); +int ksmbd_vfs_set_durable_owner(struct ksmbd_file *fp, + struct ksmbd_user *user); +struct ksmbd_file *ksmbd_file_get(struct ksmbd_file *fp); void ksmbd_fd_put(struct ksmbd_work *work, struct ksmbd_file *fp); struct ksmbd_inode *ksmbd_inode_lookup_lock(struct dentry *d); void ksmbd_inode_put(struct ksmbd_inode *ci); From 1f7dd03a88a8405143aab195f6fa9ed2243494b1 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 28 Jun 2026 15:35:29 +0900 Subject: [PATCH 066/142] ksmbd: fix malformed procfs status output The ksmbd procfs monitoring files produce misleading or malformed output. The constant-name helper uses a bitwise test for enum values. This omits zero-valued constants and can print multiple names for one lease state. It also unconditionally emits a newline, splitting entries in the open-file table across two lines. Session capabilities are printed as numeric flag values even though a table of descriptive names is available. Use exact matching for enum values. Print flag names as a comma-separated list, preserving unknown bits as hexadecimal values. Let callers control line termination so each open-file entry remains on one line. Print common session properties once, and report signing and encryption independently. Adjust client and open-file column widths for IPv6 addresses and 64-bit file IDs, and fix the misspelled OPLOCK_EXCLUSIVE name. Also expose and maintain the total request count alongside the per-command counters. Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 12 ++--- fs/smb/server/mgmt/user_session.c | 75 +++++++++++++------------------ fs/smb/server/misc.h | 7 +-- fs/smb/server/proc.c | 38 ++++++++++++++++ fs/smb/server/stats.h | 4 +- fs/smb/server/vfs_cache.c | 16 ++++--- 6 files changed, 89 insertions(+), 63 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 62e17883d5e2..aa3c7a527959 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -33,9 +33,9 @@ static int proc_show_clients(struct seq_file *m, void *v) struct timespec64 now, t; int i; - seq_printf(m, "#%-20s %-10s %-10s %-10s %-10s %-10s\n", - "", "", "", "", - "", ""); + seq_printf(m, "#%-40s %-10s %-10s %-12s %-10s %s\n", + "", "", "", "", + "", ""); down_read(&conn_list_lock); hash_for_each(conn_list, i, conn, hlist) { @@ -44,11 +44,11 @@ static int proc_show_clients(struct seq_file *m, void *v) t = timespec64_sub(now, t); #if IS_ENABLED(CONFIG_IPV6) if (!conn->inet_addr) - seq_printf(m, "%-20pI6c", &conn->inet6_addr); + seq_printf(m, " %-40pI6c", &conn->inet6_addr); else #endif - seq_printf(m, "%-20pI4", &conn->inet_addr); - seq_printf(m, " 0x%-10x %-10u %-12d %-10d %ptT\n", + seq_printf(m, " %-40pI4", &conn->inet_addr); + seq_printf(m, " 0x%-8x %-10u %-12d %-10d %ptT\n", conn->dialect, conn->total_credits, atomic_read(&conn->stats.open_files_count), diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index cbe00f00f3f6..09c944a67141 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -90,9 +90,15 @@ static int show_proc_session(struct seq_file *m, void *v) sess = (struct ksmbd_session *)m->private; ksmbd_user_session_get(sess); + seq_printf(m, "%-20s\t%s\n", "user", session_user_name(sess)); + seq_printf(m, "%-20s\t%llu\n", "id", sess->id); + seq_printf(m, "%-20s\t%s\n", "state", session_state_string(sess)); + i = 0; down_read(&sess->chann_lock); xa_for_each(&sess->ksmbd_chann_list, id, chan) { + const char *name; + #if IS_ENABLED(CONFIG_IPV6) if (chan->conn->inet_addr) seq_printf(m, "%-20s\t%pI4\n", "client", @@ -104,29 +110,37 @@ static int show_proc_session(struct seq_file *m, void *v) seq_printf(m, "%-20s\t%pI4\n", "client", &chan->conn->inet_addr); #endif - seq_printf(m, "%-20s\t%s\n", "user", session_user_name(sess)); - seq_printf(m, "%-20s\t%llu\n", "id", sess->id); - seq_printf(m, "%-20s\t%s\n", "state", - session_state_string(sess)); - seq_printf(m, "%-20s\t", "capabilities"); ksmbd_proc_show_flag_names(m, ksmbd_sess_cap_const_names, ARRAY_SIZE(ksmbd_sess_cap_const_names), chan->conn->vals->req_capabilities); + seq_putc(m, '\n'); if (sess->sign) { - seq_printf(m, "%-20s\t", "signing"); - ksmbd_proc_show_const_name(m, "%s\t", - ksmbd_signing_const_names, - ARRAY_SIZE(ksmbd_signing_const_names), - le16_to_cpu(chan->conn->signing_algorithm)); - } else if (sess->enc) { - seq_printf(m, "%-20s\t", "encryption"); - ksmbd_proc_show_const_name(m, "%s\t", - ksmbd_cipher_const_names, - ARRAY_SIZE(ksmbd_cipher_const_names), - le16_to_cpu(chan->conn->cipher_type)); + unsigned int algorithm = + le16_to_cpu(chan->conn->signing_algorithm); + + name = ksmbd_proc_const_name(ksmbd_signing_const_names, + ARRAY_SIZE(ksmbd_signing_const_names), + algorithm); + if (name) + seq_printf(m, "%-20s\t%s\n", "signing", name); + else + seq_printf(m, "%-20s\t0x%04x\n", "signing", + algorithm); + } + if (sess->enc) { + unsigned int cipher = le16_to_cpu(chan->conn->cipher_type); + + name = ksmbd_proc_const_name(ksmbd_cipher_const_names, + ARRAY_SIZE(ksmbd_cipher_const_names), + cipher); + if (name) + seq_printf(m, "%-20s\t%s\n", "encryption", name); + else + seq_printf(m, "%-20s\t0x%04x\n", "encryption", + cipher); } i++; } @@ -152,35 +166,6 @@ static int show_proc_session(struct seq_file *m, void *v) return 0; } -void ksmbd_proc_show_flag_names(struct seq_file *m, - const struct ksmbd_const_name *table, - int count, - unsigned int flags) -{ - int i; - - for (i = 0; i < count; i++) { - if (table[i].const_value & flags) - seq_printf(m, "0x%08x\t", table[i].const_value); - } - seq_putc(m, '\n'); -} - -void ksmbd_proc_show_const_name(struct seq_file *m, - const char *format, - const struct ksmbd_const_name *table, - int count, - unsigned int const_value) -{ - int i; - - for (i = 0; i < count; i++) { - if (table[i].const_value & const_value) - seq_printf(m, format, table[i].name); - } - seq_putc(m, '\n'); -} - static int create_proc_session(struct ksmbd_session *sess) { char name[30]; diff --git a/fs/smb/server/misc.h b/fs/smb/server/misc.h index 3909104e18ad..680375a966c5 100644 --- a/fs/smb/server/misc.h +++ b/fs/smb/server/misc.h @@ -53,11 +53,8 @@ void ksmbd_proc_show_flag_names(struct seq_file *m, const struct ksmbd_const_name *table, int count, unsigned int flags); -void ksmbd_proc_show_const_name(struct seq_file *m, - const char *format, - const struct ksmbd_const_name *table, - int count, - unsigned int const_value); +const char *ksmbd_proc_const_name(const struct ksmbd_const_name *table, + int count, unsigned int const_value); #else static inline void ksmbd_proc_init(void) {} static inline void ksmbd_proc_cleanup(void) {} diff --git a/fs/smb/server/proc.c b/fs/smb/server/proc.c index 101a2cc45a44..b41490142480 100644 --- a/fs/smb/server/proc.c +++ b/fs/smb/server/proc.c @@ -27,6 +27,42 @@ struct proc_dir_entry *ksmbd_proc_create(const char *name, show, v); } +void ksmbd_proc_show_flag_names(struct seq_file *m, + const struct ksmbd_const_name *table, + int count, unsigned int flags) +{ + unsigned int remaining = flags; + bool separator = false; + int i; + + for (i = 0; i < count; i++) { + unsigned int flag = table[i].const_value; + + if (!flag || (remaining & flag) != flag) + continue; + seq_printf(m, "%s%s", separator ? "," : "", table[i].name); + separator = true; + remaining &= ~flag; + } + + if (remaining) + seq_printf(m, "%s0x%08x", separator ? "," : "", remaining); + else if (!separator) + seq_puts(m, "none"); +} + +const char *ksmbd_proc_const_name(const struct ksmbd_const_name *table, + int count, unsigned int const_value) +{ + int i; + + for (i = 0; i < count; i++) { + if (table[i].const_value == const_value) + return table[i].name; + } + return NULL; +} + struct ksmbd_const_smb2_process_req { unsigned int const_value; const char *name; @@ -71,6 +107,8 @@ static int proc_show_ksmbd_stats(struct seq_file *m, void *v) ksmbd_counter_sum(KSMBD_COUNTER_SESSIONS)); seq_printf(m, "tree connects: %lld\n", ksmbd_counter_sum(KSMBD_COUNTER_TREE_CONNS)); + seq_printf(m, "requests: %lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_REQUESTS)); seq_printf(m, "read bytes: %lld\n", ksmbd_counter_sum(KSMBD_COUNTER_READ_BYTES)); seq_printf(m, "written bytes: %lld\n", diff --git a/fs/smb/server/stats.h b/fs/smb/server/stats.h index b60c30c69077..08ee66f91eaa 100644 --- a/fs/smb/server/stats.h +++ b/fs/smb/server/stats.h @@ -52,8 +52,10 @@ static inline void ksmbd_counter_sub(int type, s64 value) static inline void ksmbd_counter_inc_reqs(unsigned int cmd) { - if (cmd < KSMBD_COUNTER_MAX_REQS) + if (cmd < KSMBD_COUNTER_MAX_REQS) { + percpu_counter_inc(&ksmbd_counters.counters[KSMBD_COUNTER_REQUESTS]); percpu_counter_inc(&ksmbd_counters.counters[KSMBD_COUNTER_FIRST_REQ + cmd]); + } } static inline s64 ksmbd_counter_sum(int type) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 2aa27f449692..989e086128a5 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -66,7 +66,7 @@ static const struct ksmbd_const_name ksmbd_lease_const_names[] = { static const struct ksmbd_const_name ksmbd_oplock_const_names[] = { {SMB2_OPLOCK_LEVEL_NONE, "OPLOCK_NONE"}, {SMB2_OPLOCK_LEVEL_II, "OPLOCK_II"}, - {SMB2_OPLOCK_LEVEL_EXCLUSIVE, "OPLOCK_EXECL"}, + {SMB2_OPLOCK_LEVEL_EXCLUSIVE, "OPLOCK_EXCLUSIVE"}, {SMB2_OPLOCK_LEVEL_BATCH, "OPLOCK_BATCH"}, }; @@ -76,14 +76,14 @@ static int proc_show_files(struct seq_file *m, void *v) unsigned int id; struct oplock_info *opinfo; - seq_printf(m, "#%-10s %-10s %-10s %-10s %-15s %-10s %-10s %s\n", + seq_printf(m, "#%-10s %-18s %-18s %-10s %-16s %-10s %-10s %s\n", "", "", "", "", "", "", "", ""); read_lock(&global_ft.lock); idr_for_each_entry(global_ft.idr, fp, id) { - seq_printf(m, "%#-10x %#-10llx %#-10llx %#-10x", + seq_printf(m, " %#-10x %#-18llx %#-18llx %#-10x", fp->tcon ? fp->tcon->id : 0, fp->persistent_id, fp->volatile_id, @@ -93,6 +93,7 @@ static int proc_show_files(struct seq_file *m, void *v) opinfo = rcu_dereference(fp->f_opinfo); if (opinfo) { const struct ksmbd_const_name *const_names; + const char *name; int count; unsigned int level; @@ -106,11 +107,14 @@ static int proc_show_files(struct seq_file *m, void *v) level = opinfo->level; } rcu_read_unlock(); - ksmbd_proc_show_const_name(m, " %-15s", - const_names, count, level); + name = ksmbd_proc_const_name(const_names, count, level); + if (name) + seq_printf(m, " %-16s", name); + else + seq_printf(m, " 0x%-14x", level); } else { rcu_read_unlock(); - seq_printf(m, " %-15s", " "); + seq_printf(m, " %-16s", " "); } seq_printf(m, " %#010x %#010x %s\n", From fe4dc5987d7daa87e2b61d29cb840f4bff3ac689 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 28 Jun 2026 15:37:31 +0900 Subject: [PATCH 067/142] ksmbd: expose connection runtime state in procfs The clients proc file currently shows only a small subset of the state needed to diagnose stalled or mis-negotiated connections. Report the transport, connection state, outstanding and total credits, session count, lifetime request count, and negotiated signing, encryption, compression, and POSIX features. Report each connection as a key/value record rather than a wide fixed-width table. Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 96 +++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index aa3c7a527959..616f6a1c8bc4 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -27,33 +27,105 @@ DECLARE_RWSEM(conn_list_lock); #ifdef CONFIG_PROC_FS static struct proc_dir_entry *proc_clients; +static const char *ksmbd_conn_state_string(struct ksmbd_conn *conn) +{ + switch (READ_ONCE(conn->status)) { + case KSMBD_SESS_NEW: + return "new"; + case KSMBD_SESS_GOOD: + return "good"; + case KSMBD_SESS_EXITING: + return "exiting"; + case KSMBD_SESS_NEED_RECONNECT: + return "reconnect"; + case KSMBD_SESS_NEED_NEGOTIATE: + return "negotiate"; + case KSMBD_SESS_NEED_SETUP: + return "setup"; + case KSMBD_SESS_RELEASING: + return "releasing"; + default: + return "unknown"; + } +} + +static const char *ksmbd_conn_transport_string(struct ksmbd_conn *conn) +{ + if (conn->transport->ops->rdma_read || conn->transport->ops->rdma_write) + return "smbdirect"; + return "tcp"; +} + +static void proc_show_conn_feature(struct seq_file *m, bool *separator, + bool enabled, const char *name) +{ + if (!enabled) + return; + seq_printf(m, "%s%s", *separator ? "," : "", name); + *separator = true; +} + +static void proc_show_conn_features(struct seq_file *m, + struct ksmbd_conn *conn) +{ + bool separator = false; + + proc_show_conn_feature(m, &separator, + conn->sign || conn->signing_negotiated, "sign"); + proc_show_conn_feature(m, &separator, conn->cipher_type, "encrypt"); + proc_show_conn_feature(m, &separator, + conn->compress_algorithm != SMB3_COMPRESS_NONE, + "compress"); + proc_show_conn_feature(m, &separator, conn->posix_ext_supported, "posix"); + if (!separator) + seq_puts(m, "none"); +} + static int proc_show_clients(struct seq_file *m, void *v) { struct ksmbd_conn *conn; struct timespec64 now, t; int i; - seq_printf(m, "#%-40s %-10s %-10s %-12s %-10s %s\n", - "", "", "", "", - "", ""); - down_read(&conn_list_lock); hash_for_each(conn_list, i, conn, hlist) { + unsigned int outstanding_credits, total_credits; + unsigned long id; + void *entry; + unsigned int sessions = 0; + jiffies_to_timespec64(jiffies - conn->last_active, &t); ktime_get_real_ts64(&now); t = timespec64_sub(now, t); + + spin_lock(&conn->credits_lock); + outstanding_credits = conn->outstanding_credits; + total_credits = conn->total_credits; + spin_unlock(&conn->credits_lock); + + rcu_read_lock(); + xa_for_each(&conn->sessions, id, entry) + sessions++; + rcu_read_unlock(); #if IS_ENABLED(CONFIG_IPV6) if (!conn->inet_addr) - seq_printf(m, " %-40pI6c", &conn->inet6_addr); + seq_printf(m, "client:\t%pI6c\n", &conn->inet6_addr); else #endif - seq_printf(m, " %-40pI4", &conn->inet_addr); - seq_printf(m, " 0x%-8x %-10u %-12d %-10d %ptT\n", - conn->dialect, - conn->total_credits, - atomic_read(&conn->stats.open_files_count), - atomic_read(&conn->req_running), - &t); + seq_printf(m, "client:\t%pI4\n", &conn->inet_addr); + seq_printf(m, "transport:\t%s\n", ksmbd_conn_transport_string(conn)); + seq_printf(m, "state:\t%s\n", ksmbd_conn_state_string(conn)); + seq_printf(m, "dialect:\t0x%04x\n", conn->dialect); + seq_printf(m, "credits:\t%u/%u\n", outstanding_credits, + total_credits); + seq_printf(m, "sessions:\t%u\n", sessions); + seq_printf(m, "open_files:\t%d\n", + atomic_read(&conn->stats.open_files_count)); + seq_printf(m, "requests:\t%lld\n", + atomic64_read(&conn->stats.request_served)); + seq_puts(m, "features:\t"); + proc_show_conn_features(m, conn); + seq_printf(m, "\nlast_active:\t%ptT\n\n", &t); } up_read(&conn_list_lock); return 0; From 7f9832651e6da5493f241438034376f5aff5e972 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 28 Jun 2026 15:39:39 +0900 Subject: [PATCH 068/142] ksmbd: report session and open file details in procfs Session and file proc entries lack the state needed to correlate inactive sessions with durable or delete-pending opens. Add the account type, dialect, idle time, open-file count, tree-connect count, and per-channel POSIX negotiation state to session entries. Extend the open-file table with the file state, durable timeout, create options, share access, and descriptive flags for durable, persistent, resilient, delete-on-close, stream, POSIX, and attribute-only opens. Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 82 ++++++++++++++++++++---------- fs/smb/server/vfs_cache.c | 84 +++++++++++++++++++++++++------ 2 files changed, 123 insertions(+), 43 deletions(-) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 09c944a67141..b2bc8119984f 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -78,6 +78,28 @@ static const char *session_user_name(struct ksmbd_session *session) return session->user->name; } +static const char *session_account_type(struct ksmbd_session *session) +{ + if (user_guest(session->user)) + return "guest"; + if (ksmbd_anonymous_user(session->user)) + return "anonymous"; + return "user"; +} + +static unsigned int session_open_file_count(struct ksmbd_session *session) +{ + struct ksmbd_file *fp; + unsigned int count = 0; + unsigned int id; + + read_lock(&session->file_table.lock); + idr_for_each_entry(session->file_table.idr, fp, id) + count++; + read_unlock(&session->file_table.lock); + return count; +} + static int show_proc_session(struct seq_file *m, void *v) { struct ksmbd_session *sess; @@ -90,9 +112,16 @@ static int show_proc_session(struct seq_file *m, void *v) sess = (struct ksmbd_session *)m->private; ksmbd_user_session_get(sess); - seq_printf(m, "%-20s\t%s\n", "user", session_user_name(sess)); - seq_printf(m, "%-20s\t%llu\n", "id", sess->id); - seq_printf(m, "%-20s\t%s\n", "state", session_state_string(sess)); + seq_printf(m, "user:\t%s\n", session_user_name(sess)); + seq_printf(m, "account_type:\t%s\n", + session_account_type(sess)); + seq_printf(m, "id:\t%llu\n", sess->id); + seq_printf(m, "state:\t%s\n", session_state_string(sess)); + seq_printf(m, "dialect:\t0x%04x\n", sess->dialect); + seq_printf(m, "last_active_seconds:\t%lu\n", + jiffies_to_msecs(jiffies - sess->last_active) / MSEC_PER_SEC); + seq_printf(m, "open_files:\t%u\n", + session_open_file_count(sess)); i = 0; down_read(&sess->chann_lock); @@ -101,21 +130,23 @@ static int show_proc_session(struct seq_file *m, void *v) #if IS_ENABLED(CONFIG_IPV6) if (chan->conn->inet_addr) - seq_printf(m, "%-20s\t%pI4\n", "client", + seq_printf(m, "client:\t%pI4\n", &chan->conn->inet_addr); else - seq_printf(m, "%-20s\t%pI6c\n", "client", + seq_printf(m, "client:\t%pI6c\n", &chan->conn->inet6_addr); #else - seq_printf(m, "%-20s\t%pI4\n", "client", + seq_printf(m, "client:\t%pI4\n", &chan->conn->inet_addr); #endif - seq_printf(m, "%-20s\t", "capabilities"); + seq_puts(m, "capabilities:\t"); ksmbd_proc_show_flag_names(m, ksmbd_sess_cap_const_names, ARRAY_SIZE(ksmbd_sess_cap_const_names), chan->conn->vals->req_capabilities); seq_putc(m, '\n'); + seq_printf(m, "posix_extensions:\t%s\n", + chan->conn->posix_ext_supported ? "yes" : "no"); if (sess->sign) { unsigned int algorithm = @@ -125,9 +156,9 @@ static int show_proc_session(struct seq_file *m, void *v) ARRAY_SIZE(ksmbd_signing_const_names), algorithm); if (name) - seq_printf(m, "%-20s\t%s\n", "signing", name); + seq_printf(m, "signing:\t%s\n", name); else - seq_printf(m, "%-20s\t0x%04x\n", "signing", + seq_printf(m, "signing:\t0x%04x\n", algorithm); } if (sess->enc) { @@ -137,30 +168,30 @@ static int show_proc_session(struct seq_file *m, void *v) ARRAY_SIZE(ksmbd_cipher_const_names), cipher); if (name) - seq_printf(m, "%-20s\t%s\n", "encryption", name); + seq_printf(m, "encryption:\t%s\n", name); else - seq_printf(m, "%-20s\t0x%04x\n", "encryption", + seq_printf(m, "encryption:\t0x%04x\n", cipher); } i++; } up_read(&sess->chann_lock); - seq_printf(m, "%-20s\t%d\n", "channels", i); + seq_printf(m, "channels:\t%d\n", i); i = 0; down_read(&sess->tree_conns_lock); xa_for_each(&sess->tree_conns, id, tree_conn) { share_conf = tree_conn->share_conf; - seq_printf(m, "%-20s\t%s\t%8d", "share", - share_conf->name, tree_conn->id); - if (test_share_config_flag(share_conf, KSMBD_SHARE_FLAG_PIPE)) - seq_printf(m, " %s ", "pipe"); - else - seq_printf(m, " %s ", "disk"); - seq_putc(m, '\n'); + seq_printf(m, "share:\t%s\n", share_conf->name); + seq_printf(m, "tree_id:\t%d\n", tree_conn->id); + seq_printf(m, "share_type:\t%s\n", + test_share_config_flag(share_conf, KSMBD_SHARE_FLAG_PIPE) ? + "pipe" : "disk"); + i++; } up_read(&sess->tree_conns_lock); + seq_printf(m, "tree_connects:\t%d\n", i); ksmbd_user_session_put(sess); return 0; @@ -189,9 +220,6 @@ static int show_proc_sessions(struct seq_file *m, void *v) int i; unsigned long id; - seq_printf(m, "#%-40s %-15s %-10s %-10s\n", - "", "", "", ""); - down_read(&sessions_table_lock); hash_for_each(sessions_table, i, session, hlist) { down_read(&session->chann_lock); @@ -201,13 +229,13 @@ static int show_proc_sessions(struct seq_file *m, void *v) #if IS_ENABLED(CONFIG_IPV6) if (!chan->conn->inet_addr) - seq_printf(m, " %-40pI6c", &chan->conn->inet6_addr); + seq_printf(m, "client:\t%pI6c\n", &chan->conn->inet6_addr); else #endif - seq_printf(m, " %-40pI4", &chan->conn->inet_addr); - seq_printf(m, " %-15s %-10llu %-10s\n", - session_user_name(session), - session->id, + seq_printf(m, "client:\t%pI4\n", &chan->conn->inet_addr); + seq_printf(m, "user:\t%s\n", session_user_name(session)); + seq_printf(m, "id:\t%llu\n", session->id); + seq_printf(m, "state:\t%s\n\n", session_state_string(session)); ksmbd_user_session_put(session); diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 989e086128a5..b66d21149859 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -70,24 +70,63 @@ static const struct ksmbd_const_name ksmbd_oplock_const_names[] = { {SMB2_OPLOCK_LEVEL_BATCH, "OPLOCK_BATCH"}, }; +static const struct ksmbd_const_name ksmbd_file_state_names[] = { + {FP_NEW, "new"}, + {FP_INITED, "open"}, + {FP_CLOSED, "closed"}, +}; + +#define KSMBD_PROC_FILE_DURABLE BIT(0) +#define KSMBD_PROC_FILE_PERSISTENT BIT(1) +#define KSMBD_PROC_FILE_RESILIENT BIT(2) +#define KSMBD_PROC_FILE_DELETE_ON_CLOSE BIT(3) +#define KSMBD_PROC_FILE_STREAM BIT(4) +#define KSMBD_PROC_FILE_POSIX BIT(5) +#define KSMBD_PROC_FILE_ATTRIB_ONLY BIT(6) + +static const struct ksmbd_const_name ksmbd_file_flag_names[] = { + {KSMBD_PROC_FILE_DURABLE, "durable"}, + {KSMBD_PROC_FILE_PERSISTENT, "persistent"}, + {KSMBD_PROC_FILE_RESILIENT, "resilient"}, + {KSMBD_PROC_FILE_DELETE_ON_CLOSE, "delete-on-close"}, + {KSMBD_PROC_FILE_STREAM, "stream"}, + {KSMBD_PROC_FILE_POSIX, "posix"}, + {KSMBD_PROC_FILE_ATTRIB_ONLY, "attrib-only"}, +}; + +static unsigned int ksmbd_proc_file_flags(struct ksmbd_file *fp) +{ + unsigned int flags = 0; + + if (fp->is_durable) + flags |= KSMBD_PROC_FILE_DURABLE; + if (fp->is_persistent) + flags |= KSMBD_PROC_FILE_PERSISTENT; + if (fp->is_resilient) + flags |= KSMBD_PROC_FILE_RESILIENT; + if (fp->coption & FILE_DELETE_ON_CLOSE_LE) + flags |= KSMBD_PROC_FILE_DELETE_ON_CLOSE; + if (fp->stream.name) + flags |= KSMBD_PROC_FILE_STREAM; + if (fp->is_posix_ctxt) + flags |= KSMBD_PROC_FILE_POSIX; + if (fp->attrib_only) + flags |= KSMBD_PROC_FILE_ATTRIB_ONLY; + return flags; +} + static int proc_show_files(struct seq_file *m, void *v) { struct ksmbd_file *fp = NULL; unsigned int id; struct oplock_info *opinfo; - seq_printf(m, "#%-10s %-18s %-18s %-10s %-16s %-10s %-10s %s\n", - "", "", "", "", - "", "", "", - ""); - read_lock(&global_ft.lock); idr_for_each_entry(global_ft.idr, fp, id) { - seq_printf(m, " %#-10x %#-18llx %#-18llx %#-10x", - fp->tcon ? fp->tcon->id : 0, - fp->persistent_id, - fp->volatile_id, - atomic_read(&fp->refcount)); + seq_printf(m, "tree_id:\t0x%x\n", fp->tcon ? fp->tcon->id : 0); + seq_printf(m, "persistent_id:\t0x%llx\n", fp->persistent_id); + seq_printf(m, "volatile_id:\t0x%llx\n", fp->volatile_id); + seq_printf(m, "refcount:\t%d\n", atomic_read(&fp->refcount)); rcu_read_lock(); opinfo = rcu_dereference(fp->f_opinfo); @@ -109,17 +148,30 @@ static int proc_show_files(struct seq_file *m, void *v) rcu_read_unlock(); name = ksmbd_proc_const_name(const_names, count, level); if (name) - seq_printf(m, " %-16s", name); + seq_printf(m, "oplock:\t%s\n", name); else - seq_printf(m, " 0x%-14x", level); + seq_printf(m, "oplock:\t0x%x\n", level); } else { rcu_read_unlock(); - seq_printf(m, " %-16s", " "); + seq_puts(m, "oplock:\tnone\n"); } - seq_printf(m, " %#010x %#010x %s\n", - le32_to_cpu(fp->daccess), - le32_to_cpu(fp->saccess), + seq_printf(m, "state:\t%s\n", + ksmbd_proc_const_name(ksmbd_file_state_names, + ARRAY_SIZE(ksmbd_file_state_names), + fp->f_state)); + seq_printf(m, "durable_timeout:\t%u\n", fp->durable_timeout); + seq_printf(m, "create_options:\t0x%08x\n", + le32_to_cpu(fp->coption)); + seq_printf(m, "desired_access:\t0x%08x\n", + le32_to_cpu(fp->daccess)); + seq_printf(m, "share_access:\t0x%08x\n", + le32_to_cpu(fp->saccess)); + seq_puts(m, "flags:\t"); + ksmbd_proc_show_flag_names(m, ksmbd_file_flag_names, + ARRAY_SIZE(ksmbd_file_flag_names), + ksmbd_proc_file_flags(fp)); + seq_printf(m, "\nname:\t%s\n\n", fp->filp->f_path.dentry->d_name.name); } read_unlock(&global_ft.lock); From 1248f400e0997b6e881454488baaf91f8e1828d5 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 28 Jun 2026 15:41:18 +0900 Subject: [PATCH 069/142] ksmbd: add procfs monitoring for active shares There is no kernel-side view of the share configurations currently cached by active tree connections. Add a shares proc entry that reports each active share name, type, tree-connection count, create masks, and descriptive configuration flags. Maintain a per-share tree-connection counter with the existing global counter so the value can be read without walking every session. Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/share_config.c | 57 +++++++++++++++++++++++++++++++ fs/smb/server/mgmt/share_config.h | 25 ++++++++++++++ fs/smb/server/mgmt/tree_connect.c | 2 ++ fs/smb/server/server.c | 1 + 4 files changed, 85 insertions(+) diff --git a/fs/smb/server/mgmt/share_config.c b/fs/smb/server/mgmt/share_config.c index 6f97f8d39657..1cb58bec0903 100644 --- a/fs/smb/server/mgmt/share_config.c +++ b/fs/smb/server/mgmt/share_config.c @@ -28,6 +28,62 @@ struct ksmbd_veto_pattern { struct list_head list; }; +#ifdef CONFIG_PROC_FS +static const struct ksmbd_const_name ksmbd_share_flag_names[] = { + {KSMBD_SHARE_FLAG_AVAILABLE, "available"}, + {KSMBD_SHARE_FLAG_BROWSEABLE, "browseable"}, + {KSMBD_SHARE_FLAG_WRITEABLE, "writeable"}, + {KSMBD_SHARE_FLAG_READONLY, "read-only"}, + {KSMBD_SHARE_FLAG_GUEST_OK, "guest-ok"}, + {KSMBD_SHARE_FLAG_GUEST_ONLY, "guest-only"}, + {KSMBD_SHARE_FLAG_STORE_DOS_ATTRS, "store-dos-attrs"}, + {KSMBD_SHARE_FLAG_OPLOCKS, "oplocks"}, + {KSMBD_SHARE_FLAG_PIPE, "pipe"}, + {KSMBD_SHARE_FLAG_HIDE_DOT_FILES, "hide-dot-files"}, + {KSMBD_SHARE_FLAG_INHERIT_OWNER, "inherit-owner"}, + {KSMBD_SHARE_FLAG_STREAMS, "streams"}, + {KSMBD_SHARE_FLAG_FOLLOW_SYMLINKS, "follow-symlinks"}, + {KSMBD_SHARE_FLAG_ACL_XATTR, "acl-xattr"}, + {KSMBD_SHARE_FLAG_UPDATE, "update"}, + {KSMBD_SHARE_FLAG_CROSSMNT, "crossmnt"}, + {KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY, "continuous-availability"}, +}; + +static int proc_show_shares(struct seq_file *m, void *v) +{ + struct ksmbd_share_config *share; + int i; + + down_read(&shares_table_lock); + hash_for_each(shares_table, i, share, hlist) { + seq_printf(m, "name:\t%s\n", share->name); + seq_printf(m, "type:\t%s\n", + test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE) ? + "pipe" : "disk"); + seq_printf(m, "tree_connects:\t%d\n", + atomic_read(&share->tree_connections)); + seq_printf(m, "file_mask:\t0%07o\n", share->create_mask); + seq_printf(m, "directory_mask:\t0%07o\n", share->directory_mask); + seq_puts(m, "flags:\t"); + ksmbd_proc_show_flag_names(m, ksmbd_share_flag_names, + ARRAY_SIZE(ksmbd_share_flag_names), + share->flags); + seq_puts(m, "\n\n"); + } + up_read(&shares_table_lock); + return 0; +} + +int create_proc_shares(void) +{ + if (!ksmbd_proc_create("shares", proc_show_shares, NULL)) + return -ENOMEM; + return 0; +} +#else +int create_proc_shares(void) { return 0; } +#endif + static unsigned int share_name_hash(const char *name) { return jhash(name, strlen(name), 0); @@ -156,6 +212,7 @@ static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work, share->flags = resp->flags; atomic_set(&share->refcount, 1); + ksmbd_share_tree_conn_init(share); INIT_LIST_HEAD(&share->veto_list); share->name = kstrdup(name, KSMBD_DEFAULT_GFP); diff --git a/fs/smb/server/mgmt/share_config.h b/fs/smb/server/mgmt/share_config.h index d4ac2dd4de20..d157545fe7d1 100644 --- a/fs/smb/server/mgmt/share_config.h +++ b/fs/smb/server/mgmt/share_config.h @@ -24,6 +24,9 @@ struct ksmbd_share_config { struct path vfs_path; atomic_t refcount; +#ifdef CONFIG_PROC_FS + atomic_t tree_connections; +#endif struct hlist_node hlist; unsigned short create_mask; unsigned short directory_mask; @@ -60,6 +63,27 @@ static inline int test_share_config_flag(struct ksmbd_share_config *share, return share->flags & flag; } +#ifdef CONFIG_PROC_FS +static inline void ksmbd_share_tree_conn_init(struct ksmbd_share_config *share) +{ + atomic_set(&share->tree_connections, 0); +} + +static inline void ksmbd_share_tree_conn_inc(struct ksmbd_share_config *share) +{ + atomic_inc(&share->tree_connections); +} + +static inline void ksmbd_share_tree_conn_dec(struct ksmbd_share_config *share) +{ + atomic_dec(&share->tree_connections); +} +#else +static inline void ksmbd_share_tree_conn_init(struct ksmbd_share_config *share) {} +static inline void ksmbd_share_tree_conn_inc(struct ksmbd_share_config *share) {} +static inline void ksmbd_share_tree_conn_dec(struct ksmbd_share_config *share) {} +#endif + void ksmbd_share_config_del(struct ksmbd_share_config *share); void __ksmbd_share_config_put(struct ksmbd_share_config *share); @@ -74,4 +98,5 @@ struct ksmbd_share_config *ksmbd_share_config_get(struct ksmbd_work *work, const char *name); bool ksmbd_share_veto_filename(struct ksmbd_share_config *share, const char *filename); +int create_proc_shares(void); #endif /* __SHARE_CONFIG_MANAGEMENT_H__ */ diff --git a/fs/smb/server/mgmt/tree_connect.c b/fs/smb/server/mgmt/tree_connect.c index 58e5b8592da4..5f63e236267a 100644 --- a/fs/smb/server/mgmt/tree_connect.c +++ b/fs/smb/server/mgmt/tree_connect.c @@ -88,6 +88,7 @@ ksmbd_tree_conn_connect(struct ksmbd_work *work, const char *share_name) goto out_error; } ksmbd_counter_inc(KSMBD_COUNTER_TREE_CONNS); + ksmbd_share_tree_conn_inc(sc); kvfree(resp); return status; @@ -116,6 +117,7 @@ static int __ksmbd_tree_conn_disconnect(struct ksmbd_session *sess, ret = ksmbd_ipc_tree_disconnect_request(sess->id, tree_conn->id); ksmbd_release_tree_conn_id(sess, tree_conn->id); ksmbd_counter_dec(KSMBD_COUNTER_TREE_CONNS); + ksmbd_share_tree_conn_dec(tree_conn->share_conf); if (atomic_dec_and_test(&tree_conn->refcount)) { ksmbd_share_config_put(tree_conn->share_conf); kfree(tree_conn); diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 95be17b79714..65f375a49d85 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -612,6 +612,7 @@ static int __init ksmbd_server_init(void) ksmbd_proc_init(); create_proc_sessions(); + create_proc_shares(); ksmbd_server_tcp_callbacks_init(); From 4c670ccd5790816fc0f5714d5ee3c67dd5a9c67a Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 28 Jun 2026 15:43:58 +0900 Subject: [PATCH 070/142] ksmbd: extend procfs server statistics The server proc entry does not expose configured limits or enough outcome data to distinguish protocol errors from transport stalls. Report the server state, listener and signing configuration, connection limits, timeout values, current client and open-file totals, IPC activity, and durable scavenger state. Classify processed SMB2 response statuses by NTSTATUS severity and provide counters for common error groups while retaining the per-command counters. Signed-off-by: Namjae Jeon --- fs/smb/server/proc.c | 121 +++++++++++++++++++++++++++++++++---- fs/smb/server/server.c | 7 ++- fs/smb/server/smb_common.h | 2 +- fs/smb/server/stats.h | 53 +++++++++++++++- fs/smb/server/vfs_cache.c | 10 +++ fs/smb/server/vfs_cache.h | 1 + 6 files changed, 176 insertions(+), 18 deletions(-) diff --git a/fs/smb/server/proc.c b/fs/smb/server/proc.c index b41490142480..1bf4e00dee34 100644 --- a/fs/smb/server/proc.c +++ b/fs/smb/server/proc.c @@ -11,10 +11,12 @@ #include #include "misc.h" +#include "connection.h" #include "server.h" #include "stats.h" #include "smb_common.h" #include "smb2pdu.h" +#include "vfs_cache.h" static struct proc_dir_entry *ksmbd_proc_fs; struct ksmbd_counters ksmbd_counters; @@ -90,34 +92,127 @@ static const struct ksmbd_const_smb2_process_req smb2_process_req[KSMBD_COUNTER_ {le16_to_cpu(SMB2_OPLOCK_BREAK), "SMB2_OPLOCK_BREAK"}, }; +static const char *ksmbd_server_state_string(void) +{ + switch (READ_ONCE(server_conf.state)) { + case SERVER_STATE_STARTING_UP: + return "starting"; + case SERVER_STATE_RUNNING: + return "running"; + case SERVER_STATE_RESETTING: + return "resetting"; + case SERVER_STATE_SHUTTING_DOWN: + return "shutdown"; + default: + return "unknown"; + } +} + +static const char *ksmbd_signing_mode_string(void) +{ + switch (server_conf.signing) { + case KSMBD_CONFIG_OPT_DISABLED: + return "disabled"; + case KSMBD_CONFIG_OPT_MANDATORY: + return "mandatory"; + case KSMBD_CONFIG_OPT_AUTO: + return "auto"; + default: + return "unknown"; + } +} + +static void proc_show_runtime_totals(struct seq_file *m) +{ + struct ksmbd_conn *conn; + unsigned int clients = 0; + unsigned int open_files = 0; + int i; + + down_read(&conn_list_lock); + hash_for_each(conn_list, i, conn, hlist) { + clients++; + open_files += atomic_read(&conn->stats.open_files_count); + } + up_read(&conn_list_lock); + + seq_printf(m, "clients:\t%u\n", clients); + seq_printf(m, "open_files:\t%u\n", open_files); +} + static int proc_show_ksmbd_stats(struct seq_file *m, void *v) { int i; seq_puts(m, "Server\n"); - seq_printf(m, "name: %s\n", ksmbd_server_string()); - seq_printf(m, "netbios: %s\n", ksmbd_netbios_name()); - seq_printf(m, "work group: %s\n", ksmbd_work_group()); - seq_printf(m, "min protocol: %s\n", ksmbd_get_protocol_string(server_conf.min_protocol)); - seq_printf(m, "max protocol: %s\n", ksmbd_get_protocol_string(server_conf.max_protocol)); - seq_printf(m, "flags: 0x%08x\n", server_conf.flags); - seq_printf(m, "share_fake_fscaps: 0x%08x\n", + seq_printf(m, "state:\t%s\n", ksmbd_server_state_string()); + seq_printf(m, "name:\t%s\n", ksmbd_server_string()); + seq_printf(m, "netbios:\t%s\n", ksmbd_netbios_name()); + seq_printf(m, "work_group:\t%s\n", ksmbd_work_group()); + seq_printf(m, "min_protocol:\t%s\n", ksmbd_get_protocol_string(server_conf.min_protocol)); + seq_printf(m, "max_protocol:\t%s\n", ksmbd_get_protocol_string(server_conf.max_protocol)); + seq_printf(m, "flags:\t0x%08x\n", server_conf.flags); + seq_printf(m, "tcp_port:\t%u\n", server_conf.tcp_port); + seq_printf(m, "signing:\t%s\n", ksmbd_signing_mode_string()); + seq_printf(m, "signing_enforced:\t%s\n", + server_conf.enforced_signing ? "yes" : "no"); + seq_printf(m, "bind_interfaces_only:\t%s\n", + server_conf.bind_interfaces_only ? "yes" : "no"); + seq_printf(m, "max_connections:\t%u\n", server_conf.max_connections); + seq_printf(m, "max_connections_per_ip:\t%u\n", + server_conf.max_ip_connections); + seq_printf(m, "max_inflight_requests:\t%u\n", + server_conf.max_inflight_req); + seq_printf(m, "deadtime_seconds:\t%lu\n", server_conf.deadtime / HZ); + seq_printf(m, "ipc_timeout_seconds:\t%u\n", server_conf.ipc_timeout / HZ); + if (server_conf.ipc_last_active) + seq_printf(m, "ipc_last_active_seconds:\t%lu\n", + jiffies_to_msecs(jiffies - server_conf.ipc_last_active) / + MSEC_PER_SEC); + else + seq_puts(m, "ipc_last_active_seconds:\tnever\n"); + seq_printf(m, "durable_scavenger:\t%s\n", + ksmbd_durable_scavenger_active() ? "running" : "stopped"); + seq_printf(m, "share_fake_fscaps:\t0x%08x\n", server_conf.share_fake_fscaps); - seq_printf(m, "sessions: %lld\n", + proc_show_runtime_totals(m); + seq_printf(m, "sessions:\t%lld\n", ksmbd_counter_sum(KSMBD_COUNTER_SESSIONS)); - seq_printf(m, "tree connects: %lld\n", + seq_printf(m, "tree_connects:\t%lld\n", ksmbd_counter_sum(KSMBD_COUNTER_TREE_CONNS)); - seq_printf(m, "requests: %lld\n", + seq_printf(m, "requests:\t%lld\n", ksmbd_counter_sum(KSMBD_COUNTER_REQUESTS)); - seq_printf(m, "read bytes: %lld\n", + seq_printf(m, "read_bytes:\t%lld\n", ksmbd_counter_sum(KSMBD_COUNTER_READ_BYTES)); - seq_printf(m, "written bytes: %lld\n", + seq_printf(m, "written_bytes:\t%lld\n", ksmbd_counter_sum(KSMBD_COUNTER_WRITE_BYTES)); seq_puts(m, "\nSMB2\n"); for (i = 0; i < KSMBD_COUNTER_MAX_REQS; i++) - seq_printf(m, "%-20s:\t%lld\n", smb2_process_req[i].name, + seq_printf(m, "%s:\t%lld\n", smb2_process_req[i].name, ksmbd_counter_sum(KSMBD_COUNTER_FIRST_REQ + i)); + + seq_puts(m, "\nSMB2 status\n"); + seq_printf(m, "success:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_STATUS_SUCCESS)); + seq_printf(m, "informational:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_STATUS_INFORMATIONAL)); + seq_printf(m, "warning:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_STATUS_WARNING)); + seq_printf(m, "error:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_STATUS_ERROR)); + seq_printf(m, "access_denied:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_ACCESS_DENIED)); + seq_printf(m, "not_found:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_NOT_FOUND)); + seq_printf(m, "invalid_parameter:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_INVALID_PARAMETER)); + seq_printf(m, "sharing_violation:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_SHARING_VIOLATION)); + seq_printf(m, "not_supported:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_NOT_SUPPORTED)); + seq_printf(m, "other:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_OTHER)); return 0; } diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 65f375a49d85..5b38406129b5 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -156,8 +156,11 @@ static int __process_request(struct ksmbd_work *work, struct ksmbd_conn *conn, } ret = cmds->proc(work); - if (conn->ops->inc_reqs) - conn->ops->inc_reqs(command); + if (conn->ops->inc_reqs) { + struct smb2_hdr *rsp = ksmbd_resp_buf_curr(work); + + conn->ops->inc_reqs(command, rsp->Status); + } if (ret < 0) ksmbd_debug(CONN, "Failed to process %u [%d]\n", command, ret); diff --git a/fs/smb/server/smb_common.h b/fs/smb/server/smb_common.h index b090b56743c4..7b9c5cfcb63b 100644 --- a/fs/smb/server/smb_common.h +++ b/fs/smb/server/smb_common.h @@ -135,7 +135,7 @@ struct file_id_both_directory_info { struct smb_version_ops { u16 (*get_cmd_val)(struct ksmbd_work *swork); - void (*inc_reqs)(unsigned int cmd); + void (*inc_reqs)(unsigned int cmd, __le32 status); int (*init_rsp_hdr)(struct ksmbd_work *swork); void (*set_rsp_status)(struct ksmbd_work *swork, __le32 err); int (*allocate_rsp_buf)(struct ksmbd_work *work); diff --git a/fs/smb/server/stats.h b/fs/smb/server/stats.h index 08ee66f91eaa..bc864efa0d46 100644 --- a/fs/smb/server/stats.h +++ b/fs/smb/server/stats.h @@ -9,12 +9,24 @@ #ifndef __KSMBD_STATS_H__ #define __KSMBD_STATS_H__ +#include "../common/smb2status.h" + #define KSMBD_COUNTER_MAX_REQS 19 enum { KSMBD_COUNTER_SESSIONS = 0, KSMBD_COUNTER_TREE_CONNS, KSMBD_COUNTER_REQUESTS, + KSMBD_COUNTER_STATUS_SUCCESS, + KSMBD_COUNTER_STATUS_INFORMATIONAL, + KSMBD_COUNTER_STATUS_WARNING, + KSMBD_COUNTER_STATUS_ERROR, + KSMBD_COUNTER_ERROR_ACCESS_DENIED, + KSMBD_COUNTER_ERROR_NOT_FOUND, + KSMBD_COUNTER_ERROR_INVALID_PARAMETER, + KSMBD_COUNTER_ERROR_SHARING_VIOLATION, + KSMBD_COUNTER_ERROR_NOT_SUPPORTED, + KSMBD_COUNTER_ERROR_OTHER, KSMBD_COUNTER_READ_BYTES, KSMBD_COUNTER_WRITE_BYTES, KSMBD_COUNTER_FIRST_REQ, @@ -50,8 +62,45 @@ static inline void ksmbd_counter_sub(int type, s64 value) percpu_counter_sub(&ksmbd_counters.counters[type], value); } -static inline void ksmbd_counter_inc_reqs(unsigned int cmd) +static inline void ksmbd_counter_inc_reqs(unsigned int cmd, __le32 status) { + unsigned int severity = le32_to_cpu(status) >> 30; + int type; + + switch (severity) { + case 0: + type = KSMBD_COUNTER_STATUS_SUCCESS; + break; + case 1: + type = KSMBD_COUNTER_STATUS_INFORMATIONAL; + break; + case 2: + type = KSMBD_COUNTER_STATUS_WARNING; + break; + default: + type = KSMBD_COUNTER_STATUS_ERROR; + break; + } + percpu_counter_inc(&ksmbd_counters.counters[type]); + + if (severity == 3) { + if (status == STATUS_ACCESS_DENIED) + type = KSMBD_COUNTER_ERROR_ACCESS_DENIED; + else if (status == STATUS_OBJECT_NAME_NOT_FOUND || + status == STATUS_NO_SUCH_FILE) + type = KSMBD_COUNTER_ERROR_NOT_FOUND; + else if (status == STATUS_INVALID_PARAMETER) + type = KSMBD_COUNTER_ERROR_INVALID_PARAMETER; + else if (status == STATUS_SHARING_VIOLATION) + type = KSMBD_COUNTER_ERROR_SHARING_VIOLATION; + else if (status == STATUS_NOT_SUPPORTED || + status == STATUS_NOT_IMPLEMENTED) + type = KSMBD_COUNTER_ERROR_NOT_SUPPORTED; + else + type = KSMBD_COUNTER_ERROR_OTHER; + percpu_counter_inc(&ksmbd_counters.counters[type]); + } + if (cmd < KSMBD_COUNTER_MAX_REQS) { percpu_counter_inc(&ksmbd_counters.counters[KSMBD_COUNTER_REQUESTS]); percpu_counter_inc(&ksmbd_counters.counters[KSMBD_COUNTER_FIRST_REQ + cmd]); @@ -68,7 +117,7 @@ static inline void ksmbd_counter_inc(int type) {} static inline void ksmbd_counter_dec(int type) {} static inline void ksmbd_counter_add(int type, s64 value) {} static inline void ksmbd_counter_sub(int type, s64 value) {} -static inline void ksmbd_counter_inc_reqs(unsigned int cmd) {} +static inline void ksmbd_counter_inc_reqs(unsigned int cmd, __le32 status) {} static inline s64 ksmbd_counter_sum(int type) { return 0; } #endif diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index b66d21149859..08b15f528d88 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -191,6 +191,16 @@ static bool durable_scavenger_running; static DEFINE_MUTEX(durable_scavenger_lock); static wait_queue_head_t dh_wq; +bool ksmbd_durable_scavenger_active(void) +{ + bool active; + + mutex_lock(&durable_scavenger_lock); + active = durable_scavenger_running; + mutex_unlock(&durable_scavenger_lock); + return active; +} + void ksmbd_set_fd_limit(unsigned long limit) { limit = min(limit, get_max_files()); diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 5aff9bb556ec..5fac4b0b419d 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -216,6 +216,7 @@ unsigned int ksmbd_open_durable_fd(struct ksmbd_file *fp); struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp); void ksmbd_launch_ksmbd_durable_scavenger(void); void ksmbd_stop_durable_scavenger(void); +bool ksmbd_durable_scavenger_active(void); void ksmbd_close_tree_conn_fds(struct ksmbd_work *work); void ksmbd_close_session_fds(struct ksmbd_work *work); int ksmbd_close_inode_fds(struct ksmbd_work *work, struct inode *inode); From bc2f3f3dd69424f76e2ed3dc53d29bfd6c9966d4 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 17 Jul 2026 12:05:00 +0900 Subject: [PATCH 071/142] ksmbd: honor client signing-required in all modes The SMB2 NEGOTIATE request's SMB2_NEGOTIATE_SIGNING_REQUIRED bit requires the server to set Connection.ShouldSign. KSMBD represents that state with conn->sign, but previously set it only when its signing configuration was auto or disabled. Set conn->sign whenever the client requires signing, independently of the server's signing mode. Keep the mandatory server-mode handling unchanged. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index e38d43378464..d1cc29ae95a5 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1704,11 +1704,9 @@ int smb2_handle_negotiate(struct ksmbd_work *work) rsp->SecurityMode = SMB2_NEGOTIATE_SIGNING_ENABLED_LE; conn->use_spnego = true; - if ((server_conf.signing == KSMBD_CONFIG_OPT_AUTO || - server_conf.signing == KSMBD_CONFIG_OPT_DISABLED) && - req->SecurityMode & SMB2_NEGOTIATE_SIGNING_REQUIRED_LE) + if (req->SecurityMode & SMB2_NEGOTIATE_SIGNING_REQUIRED_LE) conn->sign = true; - else if (server_conf.signing == KSMBD_CONFIG_OPT_MANDATORY) { + if (server_conf.signing == KSMBD_CONFIG_OPT_MANDATORY) { server_conf.enforced_signing = true; rsp->SecurityMode |= SMB2_NEGOTIATE_SIGNING_REQUIRED_LE; conn->sign = true; From d6bf101da7dd5d2c2fd6e21de67d4ac43900110e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 17 Jul 2026 12:30:00 +0900 Subject: [PATCH 072/142] ksmbd: fix durable V2 persistent handle handling Correct the durable-handle V2 response context layout and use the V2 context size when chaining a following CREATE response context. Validate the only defined DH2Q/DH2C flag, require the reconnect request type to match the saved open type, and process the application instance identifier before durable V2 state. Persistent opens are durable opens as required by MS-SMB2. Permit the durable reconnect path to rebind either type of disconnected open. Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 6 +++--- fs/smb/server/smb2pdu.c | 40 ++++++++++++++++++++++++++++++++------- fs/smb/server/vfs_cache.c | 2 +- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index a534fe6c26b2..591b2fca1d4e 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -2068,12 +2068,12 @@ void create_durable_v2_rsp_buf(char *cc, struct ksmbd_file *fp) struct create_durable_rsp_v2 *buf; buf = (struct create_durable_rsp_v2 *)cc; - memset(buf, 0, sizeof(struct create_durable_rsp)); + memset(buf, 0, sizeof(*buf)); buf->ccontext.DataOffset = cpu_to_le16(offsetof - (struct create_durable_rsp, Data)); + (struct create_durable_rsp_v2, dcontext)); buf->ccontext.DataLength = cpu_to_le32(8); buf->ccontext.NameOffset = cpu_to_le16(offsetof - (struct create_durable_rsp, Name)); + (struct create_durable_rsp_v2, Name)); buf->ccontext.NameLength = cpu_to_le16(4); /* SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2 is "DH2Q" */ buf->Name[0] = 'D'; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index d1cc29ae95a5..2c1418201708 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3430,6 +3430,7 @@ static int parse_durable_handle_context(struct ksmbd_work *work, case DURABLE_RECONN_V2: { struct create_durable_handle_reconnect_v2 *recon_v2; + u32 flags; if (dh_info->type == DURABLE_RECONN || dh_info->type == DURABLE_REQ_V2) { @@ -3444,6 +3445,12 @@ static int parse_durable_handle_context(struct ksmbd_work *work, } recon_v2 = (struct create_durable_handle_reconnect_v2 *)context; + flags = le32_to_cpu(recon_v2->dcontext.Flags); + if (flags & ~SMB2_DHANDLE_FLAG_PERSISTENT) { + err = -EINVAL; + goto out; + } + dh_info->persistent = flags & SMB2_DHANDLE_FLAG_PERSISTENT; persistent_id = recon_v2->dcontext.Fid.PersistentFileId; dh_info->fp = ksmbd_lookup_durable_fd(persistent_id); if (!dh_info->fp) { @@ -3466,6 +3473,13 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } + /* A persistent reconnect must match the original open type. */ + if (dh_info->fp->is_persistent != dh_info->persistent) { + err = dh_info->persistent ? -EINVAL : -EBADF; + ksmbd_put_durable_fd(dh_info->fp); + goto out; + } + dh_info->type = dh_idx; dh_info->reconnected = true; ksmbd_debug(SMB, @@ -3529,6 +3543,11 @@ static int parse_durable_handle_context(struct ksmbd_work *work, durable_v2_blob = (struct create_durable_req_v2 *)context; + if (le32_to_cpu(durable_v2_blob->dcontext.Flags) & + ~SMB2_DHANDLE_FLAG_PERSISTENT) { + err = -EINVAL; + goto out; + } ksmbd_debug(SMB, "Request for durable v2 open\n"); dh_info->CreateGuid = durable_v2_blob->dcontext.CreateGuid; dh_info->persistent = @@ -3812,14 +3831,14 @@ int smb2_open(struct ksmbd_work *work) if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) req_op_level = SMB2_OPLOCK_LEVEL_NONE; } + rc = parse_app_instance_id(req, &dh_info); + if (rc) + goto err_out2; rc = parse_durable_handle_context(work, req, lc, &dh_info); if (rc) { ksmbd_debug(SMB, "error parsing durable handle context\n"); goto err_out2; } - rc = parse_app_instance_id(req, &dh_info); - if (rc) - goto err_out2; if (dh_info.replay == true) { fp = dh_info.fp; @@ -4594,10 +4613,15 @@ int smb2_open(struct ksmbd_work *work) if (dh_info.type == DURABLE_REQ_V2 || dh_info.type == DURABLE_REQ) { if (dh_info.type == DURABLE_REQ_V2 && dh_info.persistent && test_share_config_flag(work->tcon->share_conf, - KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY)) - fp->is_persistent = true; - else + KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY) && + (conn->vals->req_capabilities & + SMB2_GLOBAL_CAP_PERSISTENT_HANDLES)) { + /* MS-SMB2 3.3.5.9.10: a persistent open is durable too. */ fp->is_durable = true; + fp->is_persistent = true; + } else { + fp->is_durable = true; + } if (dh_info.type == DURABLE_REQ_V2) { if (dh_info.app_instance_id) memcpy(fp->app_instance_id, @@ -4760,7 +4784,9 @@ int smb2_open(struct ksmbd_work *work) if (next_ptr) *next_ptr = cpu_to_le32(next_off); next_ptr = &durable_ccontext->Next; - next_off = conn->vals->create_durable_size; + next_off = dh_info.type == DURABLE_REQ ? + conn->vals->create_durable_size : + conn->vals->create_durable_v2_size; } if (posix_ctxt) { diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 08b15f528d88..0a8c3c6e1c81 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1845,7 +1845,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) unsigned int old_f_state; write_lock(&global_ft.lock); - if (!fp->is_durable || fp->conn || fp->tcon) { + if ((!fp->is_durable && !fp->is_persistent) || fp->conn || fp->tcon) { write_unlock(&global_ft.lock); pr_err("Invalid durable fd [%p:%p]\n", fp->conn, fp->tcon); return -EBADF; From eebdd3f1157e35a50df5ff2d3d9a305901df3254 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 17 Jul 2026 12:31:00 +0900 Subject: [PATCH 073/142] ksmbd: do not advertise unimplemented CA support ksmbd durable handles are currently in-memory state. There is no persistent open recovery, cluster ownership epoch, fencing, or failover implementation behind the continuous-availability share flag. Do not advertise SMB2 persistent-handle or continuous-availability capabilities until those guarantees exist. A client requesting DH2Q then falls back to the existing durable V2 behavior rather than being promised a persistent handle that cannot survive a server failure. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2ops.c | 9 +++++---- fs/smb/server/smb2pdu.c | 11 +++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/fs/smb/server/smb2ops.c b/fs/smb/server/smb2ops.c index 97938150d2d9..bb413a145332 100644 --- a/fs/smb/server/smb2ops.c +++ b/fs/smb/server/smb2ops.c @@ -270,8 +270,10 @@ void init_smb3_02_server(struct ksmbd_conn *conn) if (server_conf.flags & KSMBD_GLOBAL_FLAG_SMB3_MULTICHANNEL) conn->vals->req_capabilities |= SMB2_GLOBAL_CAP_MULTI_CHANNEL; - if (server_conf.flags & KSMBD_GLOBAL_FLAG_DURABLE_HANDLE) - conn->vals->req_capabilities |= SMB2_GLOBAL_CAP_PERSISTENT_HANDLES; + /* + * Durable handles are in-memory only. Do not advertise persistent + * handles until CA recovery and fencing are implemented. + */ } /** @@ -294,8 +296,7 @@ int init_smb3_11_server(struct ksmbd_conn *conn) if (server_conf.flags & KSMBD_GLOBAL_FLAG_SMB3_MULTICHANNEL) conn->vals->req_capabilities |= SMB2_GLOBAL_CAP_MULTI_CHANNEL; - if (server_conf.flags & KSMBD_GLOBAL_FLAG_DURABLE_HANDLE) - conn->vals->req_capabilities |= SMB2_GLOBAL_CAP_PERSISTENT_HANDLES; + /* See init_smb3_02_server(): persistent handles require CA recovery. */ INIT_LIST_HEAD(&conn->preauth_sess_table); return 0; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 2c1418201708..c84533d86e47 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2556,12 +2556,11 @@ int smb2_tree_connect(struct ksmbd_work *work) up_write(&sess->tree_conns_lock); rsp->StructureSize = cpu_to_le16(16); out_err1: - if (server_conf.flags & KSMBD_GLOBAL_FLAG_DURABLE_HANDLE && share && - test_share_config_flag(share, - KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY)) - rsp->Capabilities = SMB2_SHARE_CAP_CONTINUOUS_AVAILABILITY; - else - rsp->Capabilities = 0; + /* + * A configured CA share is not continuously available until persistent + * open recovery, ownership fencing, and failover are implemented. + */ + rsp->Capabilities = 0; rsp->Reserved = 0; /* default manual caching */ rsp->ShareFlags = SMB2_SHAREFLAG_MANUAL_CACHING; From 08f41323f549b1ad9ad2e67e7b5c5ac312c1cb1a Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Fri, 17 Jul 2026 09:15:04 +0200 Subject: [PATCH 074/142] ksmbd: fix maximal access leak when object has no NT ACL smb2_open()'s maximal-access handling sets maximal_access to the FILE_MAXIMAL_ACCESS_LE request sentinel, then calls smb_check_perm_dacl() to compute the real access mask from the object's DACL. smb_check_perm_dacl() returns success without touching *pdaccess when the object has no stored NT ACL xattr (ksmbd_vfs_get_sd_xattr() fails, taking an early goto err_out with rc still 0). This leaves maximal_access holding the raw FILE_MAXIMAL_ACCESS_LE sentinel instead of a real access mask. Observed live: a freshly-created share root shows macOS's "no entry" (prohibited-access) badge on connect, even though POSIX permissions clearly allow access -- macOS requests maximal access via the MxAc create context on every share-root open, not via DesiredAccess, so it trusts the leaked sentinel verbatim instead of falling through to the correct POSIX-based path. Fall back to ksmbd_vfs_query_maximal_access() -- the same POSIX-based computation already used for the DesiredAccess-requested-maximal-access case below -- whenever the sentinel comes back unmodified. Signed-off-by: Gael Blivet Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Namjae Jeon --- 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 c84533d86e47..8d8567b73c43 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4150,6 +4150,17 @@ int smb2_open(struct ksmbd_work *work) 0, sess->user->uid, false); if (rc) goto err_out; + + /* + * smb_check_perm_dacl() returns success without + * touching *pdaccess when the object has no stored + * NT ACL, leaving maximal_access as the + * FILE_MAXIMAL_ACCESS_LE request sentinel instead of + * a real access mask. + */ + if (maximal_access == FILE_MAXIMAL_ACCESS_LE) + ksmbd_vfs_query_maximal_access(idmap, path.dentry, + &maximal_access); } } From 7b461610882c8baa64d56dade57cdbfb686739fa Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Fri, 17 Jul 2026 09:37:20 +0200 Subject: [PATCH 075/142] ksmbd: fix AsyncId zeroed before use in smb2_lock() cancel response release_async_work() zeroes work->async_id before the CANCELLED path calls smb2_send_interim_resp(work, STATUS_CANCELLED), which reads work->async_id to build the response's AsyncId field. The cancellation response for a cancelled blocked-lock request is sent with AsyncId=0 instead of the id the client received in the original STATUS_PENDING response for this request. Checked against every other release_async_work() call site in this file: smb2_read()/smb2_write() don't send a further async response afterward (their status goes out on the synchronous path instead), and smb2_notify()'s two async paths already transfer the id to a separate struct before releasing, so this reordering is scoped to smb2_lock() only. Send the STATUS_CANCELLED response while work->async_id is still valid, then release the async work afterward. Signed-off-by: Gael Blivet Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 8d8567b73c43..18d1369b41f0 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9319,22 +9319,25 @@ int smb2_lock(struct ksmbd_work *work) spin_unlock(&fp->f_lock); list_del(&smb_lock->llist); - release_async_work(work); - - if (work->state == KSMBD_WORK_ACTIVE) - goto retry; - - locks_free_lock(flock); if (work->state == KSMBD_WORK_CANCELLED) { rsp->hdr.Status = STATUS_CANCELLED; kfree(smb_lock); smb2_send_interim_resp(work, STATUS_CANCELLED); + release_async_work(work); + locks_free_lock(flock); work->send_no_response = 1; goto out; } + release_async_work(work); + + if (work->state == KSMBD_WORK_ACTIVE) + goto retry; + + locks_free_lock(flock); + rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED; kfree(smb_lock); From fd8c97d7c1ccedb2321a3869e87f3211bbe21570 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 13 Jul 2026 21:55:58 +0900 Subject: [PATCH 076/142] ksmbd: implement the command sequence window ksmbd tracked only credit counts (total_credits/outstanding_credits) and never validated the MessageId of an incoming request. As a result a request carrying a MessageId outside the granted range was accepted, a MessageId could be replayed, and a 64-bit sequence wrap was not detected. Maintain a command sequence window per connection: - [seq_low, seq_high) is the range of granted sequence numbers and seq_bitmap records which of them have been granted but not yet consumed. The window starts as { 0 } at connection setup. - smb2_set_rsp_credits() extends seq_high by the number of credits it grants (setting the corresponding bits), capped so the window never spans more than KSMBD_CMD_SEQ_WINDOW (== SMB2_MAX_CREDITS) sequence numbers. This implements the "limit the range of acceptable sequence numbers" allowance and keeps seq_bitmap usable as a ring. - smb2_check_sequence_number(), run for every SMB2 request from ksmbd_smb2_check_message(), verifies that the CreditCharge consecutive sequence numbers starting at MessageId lie within the window and have not already been consumed, then removes them and slides seq_low forward. CANCEL consumes nothing. A violation (out of window, replay, or wrap) tears the connection down. The legacy SMB1 multi-protocol negotiate occupies sequence number 0 but does not pass through ksmbd_smb2_check_message(), so it consumes that sequence number explicitly; otherwise seq_low would stay pinned at 0 after the upgrade to SMB2 and eventually stall credit grants. For an in-order client seq_high - seq_low equals total_credits, so the window-room cap never reduces the number of credits granted. it only engages for a client that withholds low sequence numbers. init_smb2_max_credits() now clamps the configured maximum to SMB2_MAX_CREDITS so the window (and its bitmap) can always represent every outstanding sequence number. Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 8 ++++ fs/smb/server/connection.h | 20 ++++++++++ fs/smb/server/smb2misc.c | 79 ++++++++++++++++++++++++++++++++++++++ fs/smb/server/smb2ops.c | 7 ++++ fs/smb/server/smb2pdu.c | 16 ++++++++ fs/smb/server/smb_common.c | 17 +++++++- 6 files changed, 146 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 616f6a1c8bc4..b08ef8e49a24 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -291,6 +291,14 @@ struct ksmbd_conn *ksmbd_conn_alloc(void) conn->total_credits = 1; conn->outstanding_credits = 0; + /* + * The command sequence window starts as the set { 0 } when the + * connection is established. + */ + conn->seq_low = 0; + conn->seq_high = 1; + __set_bit(0, conn->seq_bitmap); + init_waitqueue_head(&conn->req_running_q); init_waitqueue_head(&conn->r_count_q); INIT_LIST_HEAD(&conn->requests); diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index ddfcddd3c09c..9ca03f9774d3 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -17,6 +17,7 @@ #include #include #include +#include #include "smb_common.h" #include "ksmbd_work.h" @@ -25,6 +26,15 @@ struct smbdirect_buffer_descriptor_v1; #define KSMBD_SOCKET_BACKLOG 16 +/* + * Size of the per-connection SMB2 command sequence window. This mirrors + * SMB2_MAX_CREDITS, the maximum number of credits (and therefore the + * maximum number of outstanding sequence numbers) that can be granted on + * a connection. It must be a power of two so the window can be indexed as + * a ring. + */ +#define KSMBD_CMD_SEQ_WINDOW 8192 + enum { KSMBD_SESS_NEW = 0, KSMBD_SESS_GOOD, @@ -74,6 +84,16 @@ struct ksmbd_conn { unsigned int total_credits; unsigned int outstanding_credits; spinlock_t credits_lock; + /* + * Connection command sequence window. [seq_low, seq_high) is the + * range of granted sequence numbers (message IDs). seq_bitmap marks + * the ones in that range that have been granted but + * not yet consumed by a received request. All three are protected by + * credits_lock. + */ + u64 seq_low; + u64 seq_high; + DECLARE_BITMAP(seq_bitmap, KSMBD_CMD_SEQ_WINDOW); wait_queue_head_t req_running_q; wait_queue_head_t r_count_q; /* Lock to protect requests list*/ diff --git a/fs/smb/server/smb2misc.c b/fs/smb/server/smb2misc.c index 9f3629c86291..532dea7be0b3 100644 --- a/fs/smb/server/smb2misc.c +++ b/fs/smb/server/smb2misc.c @@ -372,6 +372,75 @@ static int smb2_validate_credit_charge(struct ksmbd_work *work, return ret; } +/* + * Verify that the sequence number(s) consumed by an incoming request fall + * within the connection's command sequence window and are not a replay, then + * remove them from the window. Returns 0 if the request + * may proceed, or 1 if it is invalid and the connection must be torn down. + */ +static int smb2_check_sequence_number(struct ksmbd_work *work, + struct smb2_hdr *hdr) +{ + struct ksmbd_conn *conn = work->conn; + u64 mid = le64_to_cpu(hdr->MessageId); + unsigned short charge; + u64 i; + int ret = 0; + + /* An SMB2 CANCEL consumes no sequence number. */ + if (hdr->Command == SMB2_CANCEL) + return 0; + + /* + * A multi-credit request consumes CreditCharge consecutive sequence + * numbers; every other request consumes exactly one. + */ + charge = le16_to_cpu(hdr->CreditCharge); + if (!(conn->vals->req_capabilities & SMB2_GLOBAL_CAP_LARGE_MTU) || + charge == 0) + charge = 1; + + /* The 64-bit sequence number space must not wrap. */ + if (mid + charge < mid) { + pr_err("SMB2 sequence number wrapped (mid %llu charge %u)\n", + mid, charge); + return 1; + } + + spin_lock(&conn->credits_lock); + + /* The whole range must lie within the granted window... */ + if (mid < conn->seq_low || mid + charge > conn->seq_high) { + ksmbd_debug(SMB, + "MessageId %llu (charge %u) outside command window [%llu, %llu)\n", + mid, charge, conn->seq_low, conn->seq_high); + ret = 1; + goto out; + } + + /* ...and none of it may have been consumed already (replay). */ + for (i = mid; i < mid + charge; i++) { + if (!test_bit(i & (KSMBD_CMD_SEQ_WINDOW - 1), conn->seq_bitmap)) { + ksmbd_debug(SMB, + "replayed sequence number %llu (mid %llu charge %u)\n", + i, mid, charge); + ret = 1; + goto out; + } + } + + /* Consume the sequence numbers and slide the low edge forward. */ + for (i = mid; i < mid + charge; i++) + __clear_bit(i & (KSMBD_CMD_SEQ_WINDOW - 1), conn->seq_bitmap); + while (conn->seq_low < conn->seq_high && + !test_bit(conn->seq_low & (KSMBD_CMD_SEQ_WINDOW - 1), + conn->seq_bitmap)) + conn->seq_low++; +out: + spin_unlock(&conn->credits_lock); + return ret; +} + int ksmbd_smb2_check_message(struct ksmbd_work *work) { struct smb2_pdu *pdu = ksmbd_req_buf_next(work); @@ -476,6 +545,16 @@ int ksmbd_smb2_check_message(struct ksmbd_work *work) smb2_validate_credit_charge(work, hdr)) return 1; + /* + * A sequence number violation (out of window or a replay) is a + * protocol error. tear the connection down rather than + * keep accepting requests on it. + */ + if (smb2_check_sequence_number(work, hdr)) { + ksmbd_conn_set_exiting(work->conn); + return 1; + } + return 0; } diff --git a/fs/smb/server/smb2ops.c b/fs/smb/server/smb2ops.c index bb413a145332..c64a8c427d39 100644 --- a/fs/smb/server/smb2ops.c +++ b/fs/smb/server/smb2ops.c @@ -331,6 +331,13 @@ void init_smb2_max_trans_size(unsigned int sz) void init_smb2_max_credits(unsigned int sz) { + /* + * The command sequence window (and its backing bitmap) can track at + * most SMB2_MAX_CREDITS outstanding sequence numbers, so the number of + * credits granted on a connection must not exceed that. + */ + if (sz > SMB2_MAX_CREDITS) + sz = SMB2_MAX_CREDITS; smb21_server_values.max_credits = sz; smb30_server_values.max_credits = sz; smb302_server_values.max_credits = sz; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 18d1369b41f0..153bfe12ae80 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -388,6 +388,7 @@ int smb2_set_rsp_credits(struct ksmbd_work *work) struct ksmbd_conn *conn = work->conn; unsigned short credits_requested, aux_max; unsigned short credit_charge, credits_granted = 0; + u64 window_room, i; if (work->send_no_response) return 0; @@ -424,11 +425,26 @@ int smb2_set_rsp_credits(struct ksmbd_work *work) aux_max = 1; else aux_max = conn->vals->max_credits - conn->total_credits; + + /* + * The command sequence window must not grow beyond + * KSMBD_CMD_SEQ_WINDOW sequence numbers ahead of the oldest one still + * outstanding. Cap the grant by the room left in the window so that + * credits are withheld until the client consumes the low end (and so + * that seq_bitmap stays usable as a ring). + */ + window_room = conn->seq_low + KSMBD_CMD_SEQ_WINDOW - conn->seq_high; + aux_max = min_t(unsigned short, aux_max, window_room); credits_granted = min_t(unsigned short, credits_requested, aux_max); conn->total_credits += credits_granted; work->credits_granted += credits_granted; + /* Extend the sequence window to cover the newly granted credits. */ + for (i = conn->seq_high; i < conn->seq_high + credits_granted; i++) + __set_bit(i & (KSMBD_CMD_SEQ_WINDOW - 1), conn->seq_bitmap); + conn->seq_high += credits_granted; + if (!req_hdr->NextCommand) { /* Update CreditRequest in last request */ hdr->CreditRequest = cpu_to_le16(work->credits_granted); diff --git a/fs/smb/server/smb_common.c b/fs/smb/server/smb_common.c index 080fbc9eb470..4c2da65510bc 100644 --- a/fs/smb/server/smb_common.c +++ b/fs/smb/server/smb_common.c @@ -164,7 +164,22 @@ int ksmbd_verify_smb_message(struct ksmbd_work *work) hdr = smb_get_msg(work->request_buf); if (*(__le32 *)hdr->Protocol == SMB1_PROTO_NUMBER && hdr->Command == SMB_COM_NEGOTIATE) { - work->conn->outstanding_credits++; + struct ksmbd_conn *conn = work->conn; + + conn->outstanding_credits++; + /* + * A legacy SMB1 multi-protocol negotiate occupies sequence + * number 0 but does not pass through + * ksmbd_smb2_check_message(). Consume it here so that, after + * the connection is upgraded to SMB2, the command sequence + * window can advance instead of staying pinned at 0. + */ + spin_lock(&conn->credits_lock); + if (conn->seq_low == 0) { + __clear_bit(0, conn->seq_bitmap); + conn->seq_low = 1; + } + spin_unlock(&conn->credits_lock); return 0; } From 7405d0ba294306721843bc551611775e6edef516 Mon Sep 17 00:00:00 2001 From: Yunseong Kim Date: Tue, 21 Jul 2026 02:05:13 +0200 Subject: [PATCH 077/142] ksmbd: fix slab-out-of-bounds read in ksmbd_alloc_user() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ksmbd_alloc_user() copies resp->hash_sz bytes out of the mountd IPC login response with user->passkey_sz = resp->hash_sz; user->passkey = kmalloc(resp->hash_sz, KSMBD_DEFAULT_GFP); if (user->passkey) memcpy(user->passkey, resp->hash, resp->hash_sz); resp->hash_sz is a __u16 supplied by the response, but resp->hash[] is only KSMBD_REQ_MAX_HASH_SZ bytes. A malformed or malicious login response can set hash_sz well beyond that (up to 65535), so the memcpy() reads past the end of the response object. ipc_validate_msg() does not bound hash_sz, so reject any response whose hash_sz exceeds the on-stack hash[] buffer before allocating and copying. [ 2030.238706] BUG: KASAN: slab-out-of-bounds in ksmbd_alloc_user+0x278/0x680 [ 2030.240549] Read of size 65535 at addr ffff888121bb6680 by task kworker/4:1/18611 [ 2030.242296] [ 2030.242710] CPU: 4 UID: 0 PID: 18611 Comm: kworker/4:1 Not tainted 7.1.0-next-20260623-virtme #96 PREEMPT(lazy) [ 2030.242732] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014 [ 2030.242743] Workqueue: ksmbd-io handle_ksmbd_work [ 2030.242763] Call Trace: [ 2030.242769] [ 2030.242776] dump_stack_lvl+0xa2/0xd0 [ 2030.242794] print_address_description+0x77/0x200 [ 2030.242815] ? ksmbd_alloc_user+0x278/0x680 [ 2030.242831] print_report+0x58/0x70 [ 2030.242848] kasan_report+0x117/0x150 [ 2030.242869] ? ksmbd_alloc_user+0x278/0x680 [ 2030.242888] kasan_check_range+0x3c7/0x3f0 [ 2030.242908] ? ksmbd_alloc_user+0x278/0x680 [ 2030.242925] __asan_memcpy+0x29/0x70 [ 2030.242942] ksmbd_alloc_user+0x278/0x680 [ 2030.242960] ksmbd_login_user+0xc3/0x120 [ 2030.242978] ntlm_authenticate+0x5e6/0x1b00 [ 2030.243017] ? __pfx_ntlm_authenticate+0x10/0x10 [ 2030.243035] ? ksmbd_session_lookup+0x188/0x1d0 [ 2030.243054] ? __pfx_ksmbd_session_lookup+0x10/0x10 [ 2030.243090] ? __sanitizer_cov_trace_switch+0x7b/0x140 [ 2030.243108] smb2_sess_setup+0x1e4a/0x27b0 [ 2030.243126] ? copy_from_kernel_nofault+0x199/0x300 [ 2030.243156] ? __pfx_smb2_sess_setup+0x10/0x10 [ 2030.243173] ? get_smb2_cmd_val+0xe3/0x1c0 [ 2030.243208] handle_ksmbd_work+0x954/0x1280 [ 2030.243230] ? __pfx_handle_ksmbd_work+0x10/0x10 [ 2030.243249] ? process_scheduled_works+0xa07/0x1490 [ 2030.243270] ? process_scheduled_works+0xa07/0x1490 [ 2030.243291] process_scheduled_works+0xa70/0x1490 [ 2030.243320] ? __pfx_process_scheduled_works+0x10/0x10 [ 2030.243340] ? do_raw_spin_lock+0x130/0x300 [ 2030.243358] ? lock_is_held_type+0x7b/0x110 [ 2030.243388] worker_thread+0x932/0xe20 [ 2030.243415] kthread+0x38a/0x470 [ 2030.243431] ? __pfx_worker_thread+0x10/0x10 [ 2030.243451] ? __pfx_kthread+0x10/0x10 [ 2030.243467] ret_from_fork+0x484/0x910 [ 2030.243485] ? __pfx_ret_from_fork+0x10/0x10 [ 2030.243501] ? __switch_to+0xc77/0x12c0 [ 2030.243523] ? __pfx_kthread+0x10/0x10 [ 2030.243540] ret_from_fork_asm+0x1a/0x30 [ 2030.243564] [ 2030.243570] [ 2030.290164] Allocated by task 19279: [ 2030.290911] kasan_save_track+0x3e/0x80 [ 2030.292179] __kasan_kmalloc+0x72/0x90 [ 2030.293217] __kvmalloc_node_noprof+0x3ff/0x6b0 [ 2030.294467] handle_generic_event+0x59b/0x750 [ 2030.295345] genl_family_rcv_msg_doit+0x238/0x340 [ 2030.296553] genl_rcv_msg+0x606/0x7b0 [ 2030.297129] netlink_rcv_skb+0x22b/0x4a0 [ 2030.298500] genl_rcv+0x2d/0x40 [ 2030.299273] netlink_unicast+0x7ba/0x930 [ 2030.300019] netlink_sendmsg+0x8c3/0xb00 [ 2030.301073] __sock_sendmsg+0xec/0x140 [ 2030.301579] __sys_sendto+0x357/0x470 [ 2030.302255] __x64_sys_sendto+0xe3/0x100 [ 2030.303425] do_syscall_64+0x135/0x460 [ 2030.304763] entry_SYSCALL_64_after_hwframe+0x77/0x7f [ 2030.305594] [ 2030.305819] The buggy address belongs to the object at ffff888121bb6640 [ 2030.305819] which belongs to the cache kmalloc-192 of size 192 [ 2030.309595] The buggy address is located 64 bytes inside of [ 2030.309595] allocated 166-byte region [ffff888121bb6640, ffff888121bb66e6) [ 2030.312484] [ 2030.312719] The buggy address belongs to the physical page: [ 2030.314315] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x121bb6 [ 2030.316481] head: order:1 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0 [ 2030.318248] flags: 0x100000000000040(head|node=0|zone=2) [ 2030.319662] page_type: f5(slab) [ 2030.320242] raw: 0100000000000040 ffff8881000424c0 ffffea00047c1510 ffff888100040468 [ 2030.321911] raw: 0000000000000000 0000000000150015 00000000f5000000 0000000000000000 [ 2030.324413] head: 0100000000000040 ffff8881000424c0 ffffea00047c1510 ffff888100040468 [ 2030.326150] head: 0000000000000000 0000000000150015 00000000f5000000 0000000000000000 [ 2030.327960] head: 0100000000000001 ffffffffffffff81 00000000ffffffff 00000000ffffffff [ 2030.329615] head: ffff888121bb7ab0 0000000000000000 00000000ffffffff 0000000000000000 [ 2030.331861] page dumped because: kasan: bad access detected [ 2030.332946] [ 2030.333502] Memory state around the buggy address: [ 2030.334698] ffff888121bb6580: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 2030.336475] ffff888121bb6600: fc fc fc fc fc fc fc fc 00 00 00 00 00 00 00 00 [ 2030.338143] >ffff888121bb6680: 00 00 00 00 00 00 00 00 00 00 00 00 06 fc fc fc [ 2030.339116] ^ [ 2030.341315] ffff888121bb6700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 2030.342801] ffff888121bb6780: fc fc fc fc fc fc fc fc fa fb fb fb fb fb fb fb [ 2030.344643] ================================================================== Found with ksmbdzzer [2], a KSMBD fuzzer that drives libFuzzer with a kcov-dataflow [1] coverage vector: it folds each instrumented comparison/argument's runtime operand value together with its PC (the default arm mixes them as pc⊕val) so that a new operand value at a known site counts as new coverage. [1] https://lwn.net/Articles/1077606/ [2] https://github.com/yskzalloc/kcov-dataflow Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Signed-off-by: Yunseong Kim Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_config.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fs/smb/server/mgmt/user_config.c b/fs/smb/server/mgmt/user_config.c index 03184a3303b9..0be08cf1896c 100644 --- a/fs/smb/server/mgmt/user_config.c +++ b/fs/smb/server/mgmt/user_config.c @@ -37,6 +37,17 @@ struct ksmbd_user *ksmbd_alloc_user(struct ksmbd_login_response *resp, { struct ksmbd_user *user; + /* + * resp->hash_sz is a __u16 taken from the mountd IPC login response but + * resp->hash[] is only KSMBD_REQ_MAX_HASH_SZ bytes. A malformed or + * malicious response can set hash_sz far beyond that (up to 65535), + * making the memcpy() below read past the response object + * (slab-out-of-bounds in ksmbd_alloc_user()). Reject any oversized + * hash rather than trust the length. + */ + if (resp->hash_sz > sizeof(resp->hash)) + return NULL; + user = kmalloc_obj(struct ksmbd_user, KSMBD_DEFAULT_GFP); if (!user) return NULL; From fe2c0cacbcff9d56c03b296f68f22151c4223b04 Mon Sep 17 00:00:00 2001 From: Yunseong Kim Date: Tue, 21 Jul 2026 02:05:17 +0200 Subject: [PATCH 078/142] smb: smbdirect: free completion queues with ib_free_cq() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit smbdirect_connection_destroy_qp() creates the send and receive completion queues with ib_alloc_cq_any(), which for IB_POLL_WORKQUEUE arms an internal completion handler that runs ib_cq_poll_work() on a workqueue. Tearing those CQs down with ib_destroy_cq() frees them without first cancelling that poll work. If the provider posts a completion late -- for example Soft-RoCE (rxe) posting an RNR error from rxe_receiver() after rdma_destroy_qp() -- the handler re-queues ib_cq_poll_work() on the already-freed CQ, and a follow-on access faults in rxe_req_notify_cq(). Use ib_free_cq(), which cancel_work_sync()es the poll work before freeing the CQ, so no completion handler can run against a freed queue. [ 1236.599526] ================================================================== [ 1236.602142] BUG: KASAN: slab-use-after-free in ib_cq_poll_work+0xd0/0x1a0 [ 1236.605524] Read of size 8 at addr ffff888111865800 by task kworker/4:1H/82 [ 1236.609017] [ 1236.609270] CPU: 4 UID: 0 PID: 82 Comm: kworker/4:1H Not tainted 7.2.0-rc3-next-20260717-virtme #110 PREEMPT(lazy) [ 1236.609287] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014 [ 1236.609498] Workqueue: ib-comp-wq ib_cq_poll_work [ 1236.609525] Call Trace: [ 1236.609536] [ 1236.609545] __dump_stack+0x21/0x60 [ 1236.609562] dump_stack_lvl+0xc2/0x100 [ 1236.609573] print_address_description+0x77/0x200 [ 1236.609587] ? ib_cq_poll_work+0xd0/0x1a0 [ 1236.609597] print_report+0x58/0x70 [ 1236.609607] kasan_report+0x117/0x150 [ 1236.609623] ? ib_cq_poll_work+0xd0/0x1a0 [ 1236.609636] ? process_scheduled_works+0x954/0x1600 [ 1236.609650] ib_cq_poll_work+0xd0/0x1a0 [ 1236.609662] ? process_scheduled_works+0x954/0x1600 [ 1236.609674] process_scheduled_works+0xc22/0x1600 [ 1236.609698] ? __pfx_process_scheduled_works+0x10/0x10 [ 1236.609713] ? __pfx_assign_work+0x10/0x10 [ 1236.609726] ? lock_is_held_type+0x7b/0x110 [ 1236.609741] worker_thread+0x975/0xee0 [ 1236.609757] ? __pfx_do_raw_spin_lock+0x10/0x10 [ 1236.609775] ? __kthread_parkme+0x21e/0x260 [ 1236.609789] kthread+0x3a6/0x490 [ 1236.609800] ? __pfx_worker_thread+0x10/0x10 [ 1236.609809] ? __pfx_kthread+0x10/0x10 [ 1236.609820] ret_from_fork+0x55a/0xa20 [ 1236.609835] ? __pfx_ret_from_fork+0x10/0x10 [ 1236.609850] ? __pfx_kthread+0x10/0x10 [ 1236.609861] ret_from_fork_asm+0x1a/0x30 [ 1236.609880] [ 1236.609886] [ 1236.661292] Allocated by task 5076: [ 1236.662640] kasan_save_track+0x3e/0x80 [ 1236.663842] __kasan_kmalloc+0x72/0x90 [ 1236.664763] __kmalloc_noprof+0x2b0/0x5d0 [ 1236.665356] __ib_alloc_cq+0x284/0x1000 [ 1236.666573] __ib_alloc_cq_any+0x23e/0x340 [ 1236.668654] smbdirect_connection_create_qp+0x6f7/0x1070 [ 1236.669757] smbdirect_accept_connect_request+0x500/0x1ca0 [ 1236.672625] smbdirect_listen_rdma_event_handler+0x1655/0x1c50 [ 1236.673930] cma_listen_handler+0x1bf/0x260 [ 1236.674923] cma_cm_event_handler+0x128/0x380 [ 1236.676926] cma_ib_req_handler+0x2d3d/0x4de0 [ 1236.678368] cm_process_work+0xb0/0x530 [ 1236.680454] cm_queue_work_unlock+0xb1/0x230 [ 1236.681673] cm_work_handler+0x969f/0xdca0 [ 1236.682704] process_scheduled_works+0xc22/0x1600 [ 1236.683447] worker_thread+0x975/0xee0 [ 1236.685901] kthread+0x3a6/0x490 [ 1236.688164] ret_from_fork+0x55a/0xa20 [ 1236.689522] ret_from_fork_asm+0x1a/0x30 [ 1236.690073] [ 1236.690378] Freed by task 5137: [ 1236.692242] kasan_save_track+0x3e/0x80 [ 1236.694272] kasan_save_free_info+0x40/0x50 [ 1236.695514] __kasan_slab_free+0x3a/0x60 [ 1236.696773] kfree+0x14e/0x4e0 [ 1236.697216] ib_destroy_cq_user+0x18d/0x250 [ 1236.699817] smbdirect_connection_destroy_qp+0xf2/0x280 [ 1236.702115] smbdirect_socket_destroy_sync+0x1607/0x2720 [ 1236.704062] smbdirect_socket_release+0x140/0x280 [ 1236.705286] smb_direct_free_transport+0x3b/0x90 [ 1236.707241] __ksmbd_conn_release_work+0x99/0xf0 [ 1236.709287] process_scheduled_works+0xc22/0x1600 [ 1236.710763] worker_thread+0x975/0xee0 [ 1236.711262] kthread+0x3a6/0x490 [ 1236.711720] ret_from_fork+0x55a/0xa20 [ 1236.712232] ret_from_fork_asm+0x1a/0x30 [ 1236.712762] [ 1236.712992] Last potentially related work creation: [ 1236.715157] kasan_save_stack+0x3e/0x60 [ 1236.716993] kasan_record_aux_stack+0x99/0xb0 [ 1236.718864] insert_work+0xb2/0x4a0 [ 1236.720916] __queue_work+0xebb/0x1260 [ 1236.722397] queue_work_on+0x23b/0x350 [ 1236.723809] ib_cq_completion_workqueue+0xac/0x160 [ 1236.724895] rxe_cq_post+0x433/0x7c0 [ 1236.726273] rxe_receiver+0xa41/0xd0d0 [ 1236.727754] do_work+0x272/0x860 [ 1236.728896] process_scheduled_works+0xc22/0x1600 [ 1236.730026] worker_thread+0x975/0xee0 [ 1236.731499] kthread+0x3a6/0x490 [ 1236.732132] ret_from_fork+0x55a/0xa20 [ 1236.733171] ret_from_fork_asm+0x1a/0x30 [ 1236.734224] [ 1236.734871] Second to last potentially related work creation: [ 1236.736001] kasan_save_stack+0x3e/0x60 [ 1236.737161] kasan_record_aux_stack+0x99/0xb0 [ 1236.739074] insert_work+0xb2/0x4a0 [ 1236.740414] __queue_work+0xebb/0x1260 [ 1236.740932] queue_work_on+0x23b/0x350 [ 1236.741849] ib_cq_completion_workqueue+0xac/0x160 [ 1236.744099] rxe_cq_post+0x433/0x7c0 [ 1236.745514] rxe_receiver+0xa41/0xd0d0 [ 1236.746091] do_work+0x272/0x860 [ 1236.747187] process_scheduled_works+0xc22/0x1600 [ 1236.749060] worker_thread+0x975/0xee0 [ 1236.750224] kthread+0x3a6/0x490 [ 1236.751480] ret_from_fork+0x55a/0xa20 [ 1236.751989] ret_from_fork_asm+0x1a/0x30 [ 1236.752974] [ 1236.753627] The buggy address belongs to the object at ffff888111865800 [ 1236.753627] which belongs to the cache kmalloc-1k of size 1024 [ 1236.757729] The buggy address is located 0 bytes inside of [ 1236.757729] freed 1024-byte region [ffff888111865800, ffff888111865c00) [ 1236.760816] [ 1236.761373] The buggy address belongs to the physical page: [ 1236.762599] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x111860 [ 1236.764306] head: order:3 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0 [ 1236.765281] flags: 0x100000000000040(head|node=0|zone=2) [ 1236.765979] page_type: f5(slab) [ 1236.766410] raw: 0100000000000040 ffff8881000430c0 ffffea0004a7e210 ffffea0004586210 [ 1236.770418] raw: 0000000000000000 00000000000a000a 00000000f5000000 0000000000000000 [ 1236.775899] head: 0100000000000040 ffff8881000430c0 ffffea0004a7e210 ffffea0004586210 [ 1236.782823] head: 0000000000000000 00000000000a000a 00000000f5000000 0000000000000000 [ 1236.786239] head: 0100000000000003 fffffffffffffe01 00000000ffffffff 00000000ffffffff [ 1236.790658] head: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 [ 1236.794132] page dumped because: kasan: bad access detected [ 1236.798301] [ 1236.799640] Memory state around the buggy address: [ 1236.802028] ffff888111865700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 1236.806254] ffff888111865780: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 1236.809036] >ffff888111865800: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb [ 1236.813968] ^ [ 1236.816416] ffff888111865880: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb [ 1236.819454] ffff888111865900: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb [ 1236.823143] ================================================================== [ 1236.830365] Disabling lock debugging due to kernel taint [ 1236.831136] BUG: unable to handle page fault for address: ffffc90006dc8080 [ 1236.838157] #PF: supervisor read access in kernel mode [ 1236.843686] #PF: error_code(0x0000) - not-present page [ 1236.849393] PGD 100000067 P4D 100000067 PUD 100366067 PMD 12913e067 PTE 0 [ 1236.854156] Oops: Oops: 0000 [#1] SMP KASAN NOPTI [ 1236.857893] CPU: 4 UID: 0 PID: 82 Comm: kworker/4:1H Tainted: G B 7.2.0-rc3-next-20260717-virtme #110 PREEMPT(lazy) [ 1236.860893] ksmbd: smb_direct: smbdirect_connection_recv_io_refill() failed -ECONNRESET [ 1236.864209] Tainted: [B]=BAD_PAGE [ 1236.864220] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014 [ 1236.864233] Workqueue: ib-comp-wq ib_cq_poll_work [ 1236.878314] RIP: 0010:rxe_req_notify_cq+0x13a/0x350 [ 1236.881683] Code: 20 87 fd 4c 89 fe 48 ba 00 00 00 00 00 fc ff df 4c 8b 3e 49 83 ef 80 4c 89 f8 48 c1 e8 03 0f b6 04 10 84 c0 0f 85 9b 01 00 00 <45> 8b 2f 41 80 3c 16 00 74 18 49 89 f6 48 89 f7 e8 71 20 87 fd 4c [ 1236.886819] ksmbd: smb_direct: smbdirect_connection_recv_io_refill() failed -ECONNRESET [ 1236.890671] RSP: 0018:ffff88810222f920 EFLAGS: 00010046 [ 1236.890705] RAX: 0000000000000000 RBX: ffff88810222f920 RCX: ffffffff84c92863 [ 1236.901613] RDX: dffffc0000000000 RSI: ffff888120456d48 RDI: ffff888120456d48 [ 1236.903979] RBP: ffff88810222fa20 R08: 0000000000000003 R09: 0000000000000004 [ 1236.908958] R10: dffffc0000000000 R11: ffffed1020445f10 R12: ffff888120456d40 [ 1236.914473] R13: dffffc0000000000 R14: 1ffff1102408ada9 R15: ffffc90006dc8080 [ 1236.918946] FS: 0000000000000000(0000) GS:ffff88842600d000(0000) knlGS:0000000000000000 [ 1236.921600] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 1236.925779] CR2: ffffc90006dc8080 CR3: 000000012b35f003 CR4: 0000000000f72ef0 [ 1236.926809] ksmbd: smb_direct: smbdirect_connection_recv_io_refill() failed -ECONNRESET [ 1236.928302] PKRU: 55555554 [ 1236.928328] Call Trace: [ 1236.928338] [ 1236.928352] ? ib_cq_poll_work+0xd0/0x1a0 [ 1236.928374] ? __pfx_rxe_req_notify_cq+0x10/0x10 [ 1236.941601] ? ib_cq_poll_work+0xd0/0x1a0 [ 1236.943306] ib_cq_poll_work+0xfe/0x1a0 [ 1236.943961] ? process_scheduled_works+0x954/0x1600 [ 1236.947036] process_scheduled_works+0xc22/0x1600 [ 1236.951626] ? __pfx_process_scheduled_works+0x10/0x10 [ 1236.954316] ? __pfx_assign_work+0x10/0x10 [ 1236.958110] ? lock_is_held_type+0x7b/0x110 [ 1236.960042] worker_thread+0x975/0xee0 [ 1236.962668] ? __pfx_do_raw_spin_lock+0x10/0x10 [ 1236.965334] ? __kthread_parkme+0x21e/0x260 [ 1236.966058] kthread+0x3a6/0x490 [ 1236.968115] ? __pfx_worker_thread+0x10/0x10 [ 1236.971020] ? __pfx_kthread+0x10/0x10 [ 1236.974488] ret_from_fork+0x55a/0xa20 [ 1236.977419] ? __pfx_ret_from_fork+0x10/0x10 [ 1236.979846] ? __pfx_kthread+0x10/0x10 [ 1236.981238] ret_from_fork_asm+0x1a/0x30 [ 1236.984086] [ 1236.986181] Modules linked in: [ 1236.989048] CR2: ffffc90006dc8080 [ 1236.990412] ---[ end trace 0000000000000000 ]--- [ 1236.994119] RIP: 0010:rxe_req_notify_cq+0x13a/0x350 [ 1236.998482] Code: 20 87 fd 4c 89 fe 48 ba 00 00 00 00 00 fc ff df 4c 8b 3e 49 83 ef 80 4c 89 f8 48 c1 e8 03 0f b6 04 10 84 c0 0f 85 9b 01 00 00 <45> 8b 2f 41 80 3c 16 00 74 18 49 89 f6 48 89 f7 e8 71 20 87 fd 4c [ 1237.006635] RSP: 0018:ffff88810222f920 EFLAGS: 00010046 [ 1237.008513] RAX: 0000000000000000 RBX: ffff88810222f920 RCX: ffffffff84c92863 [ 1237.014100] RDX: dffffc0000000000 RSI: ffff888120456d48 RDI: ffff888120456d48 [ 1237.021058] RBP: ffff88810222fa20 R08: 0000000000000003 R09: 0000000000000004 [ 1237.024939] R10: dffffc0000000000 R11: ffffed1020445f10 R12: ffff888120456d40 [ 1237.030203] R13: dffffc0000000000 R14: 1ffff1102408ada9 R15: ffffc90006dc8080 [ 1237.034299] FS: 0000000000000000(0000) GS:ffff88842600d000(0000) knlGS:0000000000000000 [ 1237.037311] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 1237.042838] CR2: ffffc90006dc8080 CR3: 000000012b35f003 CR4: 0000000000f72ef0 Found with ksmbdzzer [2], a KSMBD fuzzer that drives libFuzzer with a kcov-dataflow [1] coverage vector: it folds each instrumented comparison/argument's runtime operand value together with its PC (the default arm mixes them as pc⊕val) so that a new operand value at a known site counts as new coverage. [1] https://lwn.net/Articles/1077606/ [2] https://github.com/yskzalloc/kcov-dataflow Fixes: 6073eb3e3175 ("smb: smbdirect: introduce smbdirect_connection_{create,destroy}_qp()") Signed-off-by: Yunseong Kim Acked-by: Stefan Metzmacher Signed-off-by: Namjae Jeon --- fs/smb/smbdirect/connection.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/fs/smb/smbdirect/connection.c b/fs/smb/smbdirect/connection.c index 8adf58097534..fe9953720717 100644 --- a/fs/smb/smbdirect/connection.c +++ b/fs/smb/smbdirect/connection.c @@ -403,12 +403,21 @@ void smbdirect_connection_destroy_qp(struct smbdirect_socket *sc) sc->ib.qp = NULL; rdma_destroy_qp(sc->rdma.cm_id); } + /* + * These CQs were created with ib_alloc_cq_any(), which arms an internal + * completion handler (ib_cq_poll_work for IB_POLL_WORKQUEUE). They MUST be + * torn down with ib_free_cq(), which cancel_work_sync()es that poll work + * before freeing the CQ. ib_destroy_cq() skips that step, so a completion + * posted late by the (software) provider — e.g. rxe posting an RNR error + * from rxe_receiver after rdma_destroy_qp() — re-queues ib_cq_poll_work on + * an already-freed CQ (KASAN slab-use-after-free in ib_cq_poll_work). + */ if (sc->ib.recv_cq) { - ib_destroy_cq(sc->ib.recv_cq); + ib_free_cq(sc->ib.recv_cq); sc->ib.recv_cq = NULL; } if (sc->ib.send_cq) { - ib_destroy_cq(sc->ib.send_cq); + ib_free_cq(sc->ib.send_cq); sc->ib.send_cq = NULL; } if (sc->ib.pd) { From 383a9480f5f40bc46454cce27ccfad532cedad9c Mon Sep 17 00:00:00 2001 From: Yunseong Kim Date: Tue, 21 Jul 2026 02:05:19 +0200 Subject: [PATCH 079/142] smb: smbdirect: destroy QP before mem pools on accept failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the rdma_accept_failed error path of smbdirect_accept_connect_request(), the receive io posted just above is owned by the QP (recv_io is set to NULL after a successful post). The error path fell through to smbdirect_connection_destroy_mem_pools() before smbdirect_connection_destroy_qp(), so the mem pools and the recv_io slab cache were destroyed while that recv_io was still outstanding on the QP. The drain in smbdirect_connection_destroy_qp() (ib_drain_qp()) is what runs the recv completion that returns the recv_io to the free list, so destroying the pools first leaves the object outstanding at kmem_cache_destroy() time ("Slab cache still has objects") and later frees it into an already-destroyed mempool (mempool_free_bulk NULL-pointer dereference). Give rdma_accept_failed its own teardown that drains the QP first, then destroys the mem pools, and returns. The remaining labels (post_recv_io_failed onward) run before the recv_io was ever posted, so they keep the mem-pools-then-qp order. The outstanding recv_io at kmem_cache_destroy() time: [ 3487.344647] ============================================================================= [ 3487.349942] BUG smbdirect_recv_io_cache_ffff88811ba99000 (Not tainted): Objects remaining on __kmem_cache_shutdown() [ 3487.356078] ----------------------------------------------------------------------------- [ 3487.356078] [ 3487.356738] Object 0xffff8881511c3440 @offset=13376 [ 3487.358464] Allocated in mempool_alloc_noprof+0x18c/0x290 age=1194 cpu=6 pid=22254 [ 3487.361197] mempool_alloc_noprof+0x18c/0x290 [ 3487.361542] smbdirect_connection_create_mem_pools+0x405/0x780 [ 3487.361972] smbdirect_accept_connect_request+0x5a8/0x1b80 [ 3487.362359] smbdirect_listen_rdma_event_handler+0x1579/0x1b90 [ 3487.362779] cma_cm_event_handler+0x9c/0x230 [ 3487.363096] cma_ib_req_handler+0x2682/0x45d0 [ 3487.363414] cm_process_work+0x56/0x3d0 [ 3487.363676] cm_work_handler+0x8a0e/0xd000 [ 3487.367496] process_scheduled_works+0xa07/0x13a0 [ 3487.367859] worker_thread+0x7c9/0xc80 [ 3487.368148] kthread+0x341/0x430 [ 3487.368407] ret_from_fork+0x3a8/0x7a0 [ 3487.368704] ret_from_fork_asm+0x1a/0x30 [ 3487.370307] Slab 0xffffea0005447000 objects=19 used=1 fp=0xffff8881511c0040 flags=0x100000000000240(workingset|head|node=0|zone=2) [ 3487.372840] ------------[ cut here ]------------ [ 3487.373195] WARNING: mm/slub.c:1244 at __slab_err+0x1a/0x30, CPU#6: kworker/6:84/22254 [ 3487.373759] Modules linked in: [ 3487.373993] CPU: 6 UID: 0 PID: 22254 Comm: kworker/6:84 Tainted: G B 7.1.0-next-20260623+ #88 PREEMPT(lazy) [ 3487.374778] Tainted: [B]=BAD_PAGE [ 3487.377830] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014 [ 3487.378515] Workqueue: ib_cm cm_work_handler [ 3487.378820] RIP: 0010:__slab_err+0x1a/0x30 [ 3487.379129] Code: 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 0f 1f 44 00 00 e8 36 00 00 00 bf 05 00 00 00 be 01 00 00 00 e8 f7 75 45 00 90 <0f> 0b 90 c3 cc cc cc cc cc 66 66 66 66 2e 0f 1f 84 00 00 00 00 00 [ 3487.383255] RSP: 0018:ffff888220fc7050 EFLAGS: 00010093 [ 3487.383643] RAX: ffffffff8168e60a RBX: ffff88810955e640 RCX: ffff88821c381d80 [ 3487.384158] RDX: 0000000000000000 RSI: 0000000000000008 RDI: ffffffff870fa080 [ 3487.384662] RBP: ffff888220fc7068 R08: ffffffff870fa087 R09: 1ffffffff0e1f410 [ 3487.385192] R10: dffffc0000000000 R11: fffffbfff0e1f411 R12: ffffea0005447210 [ 3487.385674] R13: ffffea0005447000 R14: ffff888220fc7068 R15: ffff88812a8ab300 [ 3487.388932] FS: 0000000000000000(0000) GS:ffff888427e76000(0000) knlGS:0000000000000000 [ 3487.389529] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 3487.389934] CR2: 00007ffcf2d84fd8 CR3: 0000000111d64006 CR4: 0000000000f72ef0 [ 3487.390440] PKRU: 55555554 [ 3487.390641] Call Trace: [ 3487.390826] [ 3487.391209] __kmem_cache_shutdown+0x1aa/0x2b0 [ 3487.392062] ? smbdirect_connection_destroy_mem_pools+0x239/0x300 [ 3487.393565] kmem_cache_destroy+0x9d/0x180 [ 3487.398663] smbdirect_connection_destroy_mem_pools+0x239/0x300 [ 3487.403534] ? __pfx_smb_direct_logging_needed+0x10/0x10 [ 3487.407562] smbdirect_accept_connect_request+0x95c/0x1b80 [ 3487.412391] ? __pfx_smbdirect_accept_connect_request+0x10/0x10 [ 3487.416753] ? do_raw_spin_lock+0x130/0x300 [ 3487.420623] ? smbdirect_socket_set_initial_parameters+0x28b/0x6a0 [ 3487.424322] ? lock_acquire+0x4c/0x270 [ 3487.424409] ksmbd: can't change a file to a directory [ 3487.426321] ? trace_irq_enable+0x36/0x120 [ 3487.429144] smbdirect_listen_rdma_event_handler+0x1579/0x1b90 [ 3487.432606] ? __pfx_smbdirect_listen_rdma_event_handler+0x10/0x10 [ 3487.433595] ? trace_cm_event_handler+0x51/0x170 [ 3487.435183] ? __pfx_smbdirect_listen_rdma_event_handler+0x10/0x10 [ 3487.435646] ? cma_listen_handler+0xf6/0x150 [ 3487.435975] cma_cm_event_handler+0x9c/0x230 [ 3487.436288] cma_ib_req_handler+0x2682/0x45d0 [ 3487.439039] ? __pfx_cma_ib_req_handler+0x10/0x10 [ 3487.439540] ? __pfx_roce_resolve_route_from_path+0x10/0x10 [ 3487.439972] ? stack_depot_save_flags+0x34/0x840 [ 3487.440374] ? __xas_nomem+0xa9/0x410 [ 3487.443356] ? xas_clear_mark+0x26c/0x4a0 [ 3487.443673] cm_process_work+0x56/0x3d0 [ 3487.443969] ? _raw_spin_unlock_irq+0x28/0x50 [ 3487.444317] cm_work_handler+0x8a0e/0xd000 [ 3487.444624] ? __pfx_cm_work_handler+0x10/0x10 [ 3487.444971] ? pwq_dec_nr_in_flight+0xa73/0xdf0 [ 3487.445344] ? __pfx_pwq_dec_nr_in_flight+0x10/0x10 [ 3487.445737] ? lock_acquire+0x4c/0x270 [ 3487.448833] ? process_scheduled_works+0x995/0x13a0 [ 3487.449230] ? process_scheduled_works+0x995/0x13a0 [ 3487.449588] process_scheduled_works+0xa07/0x13a0 [ 3487.449938] ? __pfx_process_scheduled_works+0x10/0x10 [ 3487.450334] ? do_raw_spin_lock+0x130/0x300 [ 3487.450639] ? assign_work+0x3bb/0x5c0 [ 3487.450916] worker_thread+0x7c9/0xc80 [ 3487.451211] kthread+0x341/0x430 [ 3487.451453] ? __pfx_worker_thread+0x10/0x10 [ 3487.451756] ? __pfx_kthread+0x10/0x10 [ 3487.454814] ret_from_fork+0x3a8/0x7a0 [ 3487.455114] ? __pfx_ret_from_fork+0x10/0x10 [ 3487.455450] ? __switch_to+0xb76/0x1110 [ 3487.455772] ? __pfx_kthread+0x10/0x10 [ 3487.456081] ret_from_fork_asm+0x1a/0x30 [ 3487.456384] [ 3487.456549] irq event stamp: 0 [ 3487.456767] hardirqs last enabled at (0): [<0000000000000000>] 0x0 [ 3487.460124] hardirqs last disabled at (0): [] copy_process+0xa08/0x3a10 [ 3487.460726] softirqs last enabled at (0): [] copy_process+0xa08/0x3a10 [ 3487.461328] softirqs last disabled at (0): [<0000000000000000>] 0x0 [ 3487.461778] ---[ end trace 0000000000000000 ]--- [ 3487.543875] ksmbd: can't change a file to a directory [ 3487.599675] ksmbd: can't change a file to a directory [ 3487.626694] ksmbd: can't change a file to a directory [ 3487.824687] ksmbd: can't change a file to a directory [ 3487.871840] ksmbd: can't change a file to a directory [ 3487.986207] ------------[ cut here ]------------ [ 3487.987157] kmem_cache_destroy smbdirect_recv_io_cache_ffff88811ba99000: Slab cache still has objects when called from smbdirect_connection_destroy_mem_pools+0x239/0x300 [ 3487.987183] WARNING: mm/slab_common.c:572 at kmem_cache_destroy+0x15c/0x180, CPU#6: kworker/6:84/22254 [ 3487.999821] Modules linked in: [ 3488.001902] CPU: 6 UID: 0 PID: 22254 Comm: kworker/6:84 Tainted: G B W 7.1.0-next-20260623+ #88 PREEMPT(lazy) [ 3488.008289] Tainted: [B]=BAD_PAGE, [W]=WARN [ 3488.010459] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014 [ 3488.014502] Workqueue: ib_cm cm_work_handler [ 3488.017790] RIP: 0010:kmem_cache_destroy+0x16a/0x180 [ 3488.020662] Code: fd ff 48 8b 3d 2f c0 9c 06 48 89 de 5b 41 5e 5d e9 5b a3 0e 00 48 8d 3d a4 07 12 04 48 8b 53 58 48 c7 c6 91 9d 3e 85 4c 89 f1 <67> 48 0f b9 3a e9 33 ff ff ff 66 66 66 2e 0f 1f 84 00 00 00 00 00 [ 3488.028077] RSP: 0018:ffff888220fc70b8 EFLAGS: 00010202 [ 3488.032038] RAX: 0000000000000001 RBX: ffff88810955e640 RCX: ffffffff822bc079 [ 3488.035742] RDX: ffff88812404ec40 RSI: ffffffff853e9d91 RDI: ffffffff85f7ac50 [ 3488.037830] RBP: 0000000000000001 R08: ffff8883aef3e843 R09: 1ffff11075de7d08 [ 3488.041076] R10: dffffc0000000000 R11: ffffed1075de7d09 R12: 1ffff11024fa6c3c [ 3488.045376] R13: ffff888127d361e8 R14: ffffffff822bc079 R15: ffff88811ba99538 [ 3488.049073] FS: 0000000000000000(0000) GS:ffff888427e76000(0000) knlGS:0000000000000000 [ 3488.052515] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 3488.054934] CR2: 00007ffcf2d84fd8 CR3: 0000000111d64006 CR4: 0000000000f72ef0 [ 3488.058529] PKRU: 55555554 [ 3488.060542] Call Trace: [ 3488.061595] [ 3488.062006] smbdirect_connection_destroy_mem_pools+0x239/0x300 [ 3488.066041] ? __pfx_smb_direct_logging_needed+0x10/0x10 [ 3488.068620] smbdirect_accept_connect_request+0x95c/0x1b80 [ 3488.071594] ? __pfx_smbdirect_accept_connect_request+0x10/0x10 [ 3488.073218] ksmbd: not allow base filename in rename [ 3488.074751] ? do_raw_spin_lock+0x130/0x300 [ 3488.076792] ksmbd: can't change a file to a directory [ 3488.077942] ? smbdirect_socket_set_initial_parameters+0x28b/0x6a0 [ 3488.080143] ? lock_acquire+0x4c/0x270 [ 3488.080747] ? trace_irq_enable+0x36/0x120 [ 3488.081400] smbdirect_listen_rdma_event_handler+0x1579/0x1b90 [ 3488.085089] ? __pfx_smbdirect_listen_rdma_event_handler+0x10/0x10 [ 3488.089333] ? trace_cm_event_handler+0x51/0x170 [ 3488.092637] ? __pfx_smbdirect_listen_rdma_event_handler+0x10/0x10 [ 3488.095741] ? cma_listen_handler+0xf6/0x150 [ 3488.099638] cma_cm_event_handler+0x9c/0x230 [ 3488.101552] cma_ib_req_handler+0x2682/0x45d0 [ 3488.104571] ? __pfx_cma_ib_req_handler+0x10/0x10 [ 3488.106790] ? __pfx_roce_resolve_route_from_path+0x10/0x10 [ 3488.109799] ? stack_depot_save_flags+0x34/0x840 [ 3488.112174] ? __xas_nomem+0xa9/0x410 [ 3488.114217] ? xas_clear_mark+0x26c/0x4a0 [ 3488.116854] cm_process_work+0x56/0x3d0 [ 3488.118179] ? _raw_spin_unlock_irq+0x28/0x50 [ 3488.120034] cm_work_handler+0x8a0e/0xd000 [ 3488.121677] ? __pfx_cm_work_handler+0x10/0x10 [ 3488.123682] ? pwq_dec_nr_in_flight+0xa73/0xdf0 [ 3488.126928] ? __pfx_pwq_dec_nr_in_flight+0x10/0x10 [ 3488.129390] ? lock_acquire+0x4c/0x270 [ 3488.131432] ? process_scheduled_works+0x995/0x13a0 [ 3488.132694] ksmbd: can't change a file to a directory [ 3488.136702] ? process_scheduled_works+0x995/0x13a0 [ 3488.140060] process_scheduled_works+0xa07/0x13a0 [ 3488.143379] ? __pfx_process_scheduled_works+0x10/0x10 [ 3488.147221] ? do_raw_spin_lock+0x130/0x300 [ 3488.150790] ? assign_work+0x3bb/0x5c0 [ 3488.154259] worker_thread+0x7c9/0xc80 [ 3488.155730] kthread+0x341/0x430 [ 3488.158309] ? __pfx_worker_thread+0x10/0x10 [ 3488.160865] ? __pfx_kthread+0x10/0x10 [ 3488.164384] ret_from_fork+0x3a8/0x7a0 [ 3488.167020] ? __pfx_ret_from_fork+0x10/0x10 [ 3488.170050] ? __switch_to+0xb76/0x1110 [ 3488.171809] ? __pfx_kthread+0x10/0x10 [ 3488.174955] ret_from_fork_asm+0x1a/0x30 [ 3488.175605] [ 3488.176206] irq event stamp: 0 [ 3488.179360] hardirqs last enabled at (0): [<0000000000000000>] 0x0 [ 3488.186521] hardirqs last disabled at (0): [] copy_process+0xa08/0x3a10 [ 3488.191889] softirqs last enabled at (0): [] copy_process+0xa08/0x3a10 [ 3488.196853] softirqs last disabled at (0): [<0000000000000000>] 0x0 [ 3488.200870] ---[ end trace 0000000000000000 ]--- Found with ksmbdzzer [2], a KSMBD fuzzer that drives libFuzzer with a kcov-dataflow [1] coverage vector: it folds each instrumented comparison/argument's runtime operand value together with its PC (the default arm mixes them as pc⊕val) so that a new operand value at a known site counts as new coverage. [1] https://lwn.net/Articles/1077606/ [2] https://github.com/yskzalloc/kcov-dataflow Fixes: eb3ed1e9048c ("smb: smbdirect: introduce smbdirect_accept_connect_request()") Signed-off-by: Yunseong Kim Acked-by: Stefan Metzmacher Signed-off-by: Namjae Jeon --- fs/smb/smbdirect/accept.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/fs/smb/smbdirect/accept.c b/fs/smb/smbdirect/accept.c index 529740005838..039c9bfbd8ac 100644 --- a/fs/smb/smbdirect/accept.c +++ b/fs/smb/smbdirect/accept.c @@ -145,11 +145,21 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc, rdma_accept_failed: /* - * smbdirect_connection_destroy_qp() calls ib_drain_qp(), - * so that smbdirect_accept_negotiate_recv_done() will - * call smbdirect_connection_put_recv_io() + * The recv_io posted above is now owned by the QP (recv_io was set to + * NULL after a successful post). smbdirect_connection_destroy_qp() + * calls ib_drain_qp(), whose completion + * (smbdirect_accept_negotiate_recv_done) returns the recv_io to the + * free list via smbdirect_connection_put_recv_io(). It therefore MUST + * run BEFORE smbdirect_connection_destroy_mem_pools(): otherwise the + * posted recv_io is still outstanding when kmem_cache_destroy() runs + * ("Slab cache still has objects") and is later freed into an + * already-destroyed mempool (mempool_free_bulk NULL-ptr-deref). */ + smbdirect_connection_destroy_qp(sc); + smbdirect_connection_destroy_mem_pools(sc); + return ret; post_recv_io_failed: + /* post failed: recv_io was not accepted by the QP, still in hand */ if (recv_io) smbdirect_connection_put_recv_io(recv_io); get_recv_io_failed: From 76fa42c004eb95a983bed8fd0e6e0e8428c751a5 Mon Sep 17 00:00:00 2001 From: Yunseong Kim Date: Wed, 5 Aug 2026 02:46:56 +0200 Subject: [PATCH 080/142] smb: smbdirect: avoid recursive listen.lock during cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit __smbdirect_socket_schedule_cleanup() and smbdirect_socket_cleanup_work() take sc->listen.lock and walk the listener's pending list, recursing into smbdirect_socket_schedule_cleanup() for each child, and every child takes its own listen.lock inside that walk. This cannot deadlock for real: the nesting is strictly listener into child, a child never listens, so the outer and the inner lock are always different instances. lockdep only tracks lock classes, though; it sees the same class acquired twice and reports a possible recursive locking deadlock. This change therefore works around a lockdep limitation rather than fixing a real hang, but the report is still worth avoiding: lockdep disables itself after the first splat and then hides real locking bugs for the rest of the run. Only a socket that was a listener owns a populated listen.ready/pending list; a child has empty lists and nothing to do in these blocks. Guard both of them with sc->listen.backlog != -1, the "was a listener" marker that smbdirect_socket_destroy() already uses: listen.backlog leaves its initial -1 exactly once, when smbdirect_socket_listen() succeeds. The alternative !sc->accept.listener test reads as "not a listener" while meaning the opposite, and it is also true for an accepted child, whose accept.listener has been cleared on hand-over. With the guard the walk only runs for a listener and never nests a child's listen.lock under it; a pending child stays on its listener's list for the free path (smbdirect_socket_destroy) to reap. [ 741.705044] WARNING: possible recursive locking detected [ 741.705403] 7.1.0-next-20260623+ #75 Not tainted [ 741.705695] -------------------------------------------- [ 741.706022] ksmbd.control/18502 is trying to acquire lock: [ 741.706379] ffff888108d612f8 (&sc->listen.lock){....}-{3:3}, at: __smbdirect_socket_schedule_cleanup+0x719/0xd70 [ 741.707008] [ 741.707008] but task is already holding lock: [ 741.707396] ffff8881087642f8 (&sc->listen.lock){....}-{3:3}, at: __smbdirect_socket_schedule_cleanup+0x719/0xd70 [ 741.708025] [ 741.708025] other info that might help us debug this: [ 741.708448] Possible unsafe locking scenario: [ 741.708448] [ 741.708845] CPU0 [ 741.709016] ---- [ 741.709186] lock(&sc->listen.lock); [ 741.709453] lock(&sc->listen.lock); [ 741.709705] [ 741.709705] *** DEADLOCK *** [ 741.709705] [ 741.710095] May be due to missing lock nesting notation [ 741.710095] [ 741.710663] 6 locks held by ksmbd.control/18502: [ 741.710975] #0: ffff888109e51420 (sb_writers#7){.+.+}-{0:0}, at: vfs_write+0x1e7/0xc70 [ 741.711561] #1: ffff888126ec3880 (&of->mutex){+.+.}-{4:4}, at: kernfs_fop_write_iter+0x1be/0x4d0 [ 741.712147] #2: ffff888102af17b0 (kn->active#45){.+.+}-{0:0}, at: kernfs_fop_write_iter+0x205/0x4d0 [ 741.712803] #3: ffffffff85ad1e00 (ctrl_lock){+.+.}-{4:4}, at: kill_server_store+0x1e0/0x2b0 [ 741.713381] #4: ffffffff85ad41a0 (init_lock){+.+.}-{4:4}, at: ksmbd_conn_transport_destroy+0x5b/0x3c0 [ 741.713995] #5: ffff8881087642f8 (&sc->listen.lock){....}-{3:3}, at: __smbdirect_socket_schedule_cleanup+0x719/0xd70 [ 741.714736] [ 741.714736] stack backtrace: [ 741.715038] CPU: 4 UID: 0 PID: 18502 Comm: ksmbd.control Not tainted 7.1.0-next-20260623+ #75 PREEMPT(lazy) [ 741.715043] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014 [ 741.715046] Call Trace: [ 741.715049] [ 741.715052] dump_stack_lvl+0x77/0xa0 [ 741.715058] print_deadlock_bug+0x279/0x290 [ 741.715065] __lock_acquire+0x272a/0x2e30 [ 741.715070] ? stack_trace_save+0xae/0x100 [ 741.715075] ? smb_direct_logging_vaprintf+0x1a0/0x230 [ 741.715079] ? __pfx_smb_direct_logging_vaprintf+0x10/0x10 [ 741.715082] ? __timer_delete+0x58/0x320 [ 741.715087] lock_acquire+0xd3/0x270 [ 741.715091] ? __smbdirect_socket_schedule_cleanup+0x719/0xd70 [ 741.715095] ? entry_SYSCALL_64_after_hwframe+0x77/0x7f [ 741.715099] _raw_spin_lock_irqsave+0x42/0x60 [ 741.715105] ? __smbdirect_socket_schedule_cleanup+0x719/0xd70 Note the two addresses above: ffff888108d612f8 is the child's lock, ffff8881087642f8 the listener's, always distinct objects. Found with ksmbdzzer [2], a KSMBD fuzzer that drives libFuzzer with a kcov-dataflow [1] coverage vector: it folds each instrumented comparison/argument's runtime operand value together with its PC (the default arm mixes them as pc⊕val) so that a new operand value at a known site counts as new coverage. [1] https://lwn.net/Articles/1077606/ [2] https://github.com/yskzalloc/kcov-dataflow Fixes: dc691b91ad16 ("smb: smbdirect: introduce smbdirect_socket_{listen,accept}()") Signed-off-by: Yunseong Kim Reviewed-by: Stefan Metzmacher Signed-off-by: Namjae Jeon --- fs/smb/smbdirect/socket.c | 42 +++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/fs/smb/smbdirect/socket.c b/fs/smb/smbdirect/socket.c index 39cca7219c4d..8dec47a6603c 100644 --- a/fs/smb/smbdirect/socket.c +++ b/fs/smb/smbdirect/socket.c @@ -305,12 +305,26 @@ void __smbdirect_socket_schedule_cleanup(struct smbdirect_socket *sc, * disconnect all pending and ready sockets * * First we move ready sockets to pending again. + * + * Only a socket that was a listener (listen.backlog != -1) owns a + * populated listen.ready/pending list. Guarding on that also keeps + * lockdep quiet: without it, the listener holds sc->listen.lock while + * the loop recurses into each child psc, which takes psc->listen.lock. + * Those are always different instances of the same lock class -- a + * child never listens, so the nesting is strictly listener -> child + * and cannot really deadlock -- but lockdep only sees the class and + * reports "possible recursive locking". A child has empty listen + * lists and nothing to do here, so skipping it loses nothing, and a + * pending child stays on its listener's list for the free path + * (smbdirect_socket_destroy) to reap. */ - spin_lock_irqsave(&sc->listen.lock, flags); - list_splice_init(&sc->listen.ready, &sc->listen.pending); - list_for_each_entry_safe(psc, tsc, &sc->listen.pending, accept.list) - smbdirect_socket_schedule_cleanup(psc, sc->first_error); - spin_unlock_irqrestore(&sc->listen.lock, flags); + if (sc->listen.backlog != -1) { /* was a listener */ + spin_lock_irqsave(&sc->listen.lock, flags); + list_splice_init(&sc->listen.ready, &sc->listen.pending); + list_for_each_entry_safe(psc, tsc, &sc->listen.pending, accept.list) + smbdirect_socket_schedule_cleanup(psc, sc->first_error); + spin_unlock_irqrestore(&sc->listen.lock, flags); + } switch (sc->status) { case SMBDIRECT_SOCKET_RESOLVE_ADDR_FAILED: @@ -405,12 +419,20 @@ static void smbdirect_socket_cleanup_work(struct work_struct *work) * disconnect all pending and ready sockets * * First we move ready sockets to pending again. + * + * Guarded on listen.backlog != -1 for the same reason as in + * __smbdirect_socket_schedule_cleanup(): only a listener owns a + * populated listen list, and skipping the block for a child avoids + * nesting psc->listen.lock under a listener's listen.lock (different + * instances of one class -- harmless, but lockdep cannot tell). */ - spin_lock_irqsave(&sc->listen.lock, flags); - list_splice_init(&sc->listen.ready, &sc->listen.pending); - list_for_each_entry_safe(psc, tsc, &sc->listen.pending, accept.list) - smbdirect_socket_schedule_cleanup(psc, sc->first_error); - spin_unlock_irqrestore(&sc->listen.lock, flags); + if (sc->listen.backlog != -1) { /* was a listener */ + spin_lock_irqsave(&sc->listen.lock, flags); + list_splice_init(&sc->listen.ready, &sc->listen.pending); + list_for_each_entry_safe(psc, tsc, &sc->listen.pending, accept.list) + smbdirect_socket_schedule_cleanup(psc, sc->first_error); + spin_unlock_irqrestore(&sc->listen.lock, flags); + } switch (sc->status) { case SMBDIRECT_SOCKET_NEGOTIATE_NEEDED: From db82fbe4bb68e68e4aef00ef5b79f92991d8ef8e Mon Sep 17 00:00:00 2001 From: Yunseong Kim Date: Wed, 5 Aug 2026 02:46:58 +0200 Subject: [PATCH 081/142] smb: smbdirect: release pending child sockets outside the handler lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit smbdirect_socket_destroy() releases the listener's pending/ready child sockets while still holding the listener's handler lock, the &id_priv->handler_mutex taken via rdma_lock_handler(), not sc->listen.lock, and before the listener's own rdma_destroy_id(). That ordering has one real consequence and one cosmetic one. The real one: smbdirect_socket_release() drops the child's last reference, which destroys the child's cm_id. Doing that before the listener's rdma_destroy_id() lets _cma_cancel_listens(), running from the listener's _destroy_id(), walk an already freed child id_priv, which KASAN catches as a slab-use-after-free during listener shutdown: [ 4758.909130] BUG: KASAN: slab-use-after-free in __mutex_lock+0x1469/0x1560 [ 4758.911450] Read of size 1 at addr ffff88821c381db4 by task ksmbd.control/1652 [ 4758.913262] Call Trace: [ 4758.913267] [ 4758.913299] __mutex_lock+0x1469/0x1560 [ 4758.913408] _cma_cancel_listens+0x312/0x3b0 [ 4758.913413] _destroy_id+0x363/0xee0 [ 4758.913417] smbdirect_socket_destroy_sync+0x17d5/0x2440 [ 4758.913443] smbdirect_socket_release+0x124/0x230 [ 4758.913451] ksmbd_rdma_stop_listening+0x9f/0x190 [ 4758.913457] ksmbd_conn_transport_destroy+0x65/0x3c0 [ 4758.913463] kill_server_store+0x1fb/0x2b0 [ 4758.913501] kernfs_fop_write_iter+0x349/0x4d0 [ 4758.913507] vfs_write+0x5e7/0xc70 [ 4758.913528] ksys_write+0x12a/0x210 [ 4758.913541] do_syscall_64+0x135/0x460 [ 4758.913555] entry_SYSCALL_64_after_hwframe+0x77/0x7f The cosmetic one: releasing a child recurses into smbdirect_socket_destroy(), which takes the child's own rdma_lock_handler() lock nested under the listener's. The listener's and the child's cm_id are always different instances, so this cannot deadlock for real; the CM core itself nests a new connection id's handler_mutex under the listening id's in cma_ib_req_handler(). But lockdep only sees one lock class, reports possible recursive locking, and then disables itself, hiding real locking bugs for the rest of the run: [ 2424.579653] WARNING: possible recursive locking detected [ 2424.581180] 7.1.0-next-20260623+ #89 Not tainted [ 2424.582548] -------------------------------------------- [ 2424.584500] ksmbd.control/8854 is trying to acquire lock: [ 2424.586817] ffff888102303c20 (&id_priv->handler_mutex){+.+.}-{4:4}, at: smbdirect_socket_destroy_sync+0xc39/0x2440 [ 2424.590590] [ 2424.590590] but task is already holding lock: [ 2424.591601] ffff888102046c20 (&id_priv->handler_mutex){+.+.}-{4:4}, at: smbdirect_socket_destroy_sync+0xc39/0x2440 [ 2424.594178] [ 2424.594178] other info that might help us debug this: [ 2424.596634] Possible unsafe locking scenario: [ 2424.596634] [ 2424.598841] CPU0 [ 2424.599765] ---- [ 2424.600695] lock(&id_priv->handler_mutex); [ 2424.601836] lock(&id_priv->handler_mutex); [ 2424.602590] [ 2424.602590] *** DEADLOCK *** [ 2424.602590] [ 2424.604512] May be due to missing lock nesting notation Splice the pending/ready children onto a local list under the listener's listen.lock, while the handler lock is held so a concurrent CM CONNECT_REQUEST cannot add more, but defer the actual smbdirect_socket_release() calls until after the listener's cm_id has been destroyed and its handler lock dropped. The children are independent sockets whose teardown needs neither the listener's handler lock nor its cm_id. Found with ksmbdzzer [2], a KSMBD fuzzer that drives libFuzzer with a kcov-dataflow [1] coverage vector: it folds each instrumented comparison/argument's runtime operand value together with its PC (the default arm mixes them as pc⊕val) so that a new operand value at a known site counts as new coverage. [1] https://lwn.net/Articles/1077606/ [2] https://github.com/yskzalloc/kcov-dataflow Fixes: dc691b91ad16 ("smb: smbdirect: introduce smbdirect_socket_{listen,accept}()") Signed-off-by: Yunseong Kim Reviewed-by: Stefan Metzmacher Signed-off-by: Namjae Jeon --- fs/smb/smbdirect/socket.c | 56 ++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/fs/smb/smbdirect/socket.c b/fs/smb/smbdirect/socket.c index 8dec47a6603c..bb02df6158b9 100644 --- a/fs/smb/smbdirect/socket.c +++ b/fs/smb/smbdirect/socket.c @@ -495,6 +495,7 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc) struct smbdirect_recv_io *recv_io; struct smbdirect_recv_io *recv_tmp; LIST_HEAD(all_list); + LIST_HEAD(pending_list); unsigned long flags; smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, @@ -552,24 +553,29 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc) * disconnect all pending and ready sockets * * We move ready sockets to pending again. + * + * Capture them here -- rdma_lock_handler(sc->rdma.cm_id) is held above, + * so a concurrent CM CONNECT_REQUEST cannot add more; sc->listen.lock + * below only protects the list splice itself -- but DEFER releasing + * them until the listener's cm_id is destroyed: + * + * - smbdirect_socket_release() -> smbdirect_socket_destroy() takes the + * child's own rdma_lock_handler() lock (&id_priv->handler_mutex). + * The listener's and the child's cm_id are always different + * instances, so the nesting cannot really deadlock, but lockdep only + * sees one lock class and reports "possible recursive locking". + * + * - rdma_destroy_id() of a child before the listener's own + * rdma_destroy_id() below lets _cma_cancel_listens() walk the freed + * child id_priv (KASAN slab-use-after-free in __mutex_lock()). + * + * The children are independent sockets whose teardown does not need + * the listener's handler lock. */ spin_lock_irqsave(&sc->listen.lock, flags); - list_splice_tail_init(&sc->listen.ready, &all_list); - list_splice_tail_init(&sc->listen.pending, &all_list); + list_splice_tail_init(&sc->listen.ready, &pending_list); + list_splice_tail_init(&sc->listen.pending, &pending_list); spin_unlock_irqrestore(&sc->listen.lock, flags); - psockets = list_count_nodes(&all_list); - if (sc->listen.backlog != -1) /* was a listener */ - smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, - "release %zu pending sockets\n", psockets); - list_for_each_entry_safe(psc, tsc, &all_list, accept.list) { - list_del_init(&psc->accept.list); - psc->accept.listener = NULL; - smbdirect_socket_release(psc); - } - if (sc->listen.backlog != -1) /* was a listener */ - smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, - "released %zu pending sockets\n", psockets); - INIT_LIST_HEAD(&all_list); /* It's not possible for upper layer to get to reassembly */ if (sc->listen.backlog == -1) /* was not a listener */ @@ -599,6 +605,26 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc) sc->rdma.cm_id = NULL; } + /* + * The listener's rdma_lock_handler() lock is dropped and its cm_id is + * destroyed, so it is safe to release the child sockets captured + * above: each release recurses into smbdirect_socket_destroy() and + * takes that child's own handler_mutex without nesting it under the + * listener's, and _cma_cancel_listens() can no longer reach them. + */ + psockets = list_count_nodes(&pending_list); + if (sc->listen.backlog != -1) /* was a listener */ + smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, + "release %zu pending sockets\n", psockets); + list_for_each_entry_safe(psc, tsc, &pending_list, accept.list) { + list_del_init(&psc->accept.list); + psc->accept.listener = NULL; + smbdirect_socket_release(psc); + } + if (sc->listen.backlog != -1) /* was a listener */ + smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, + "released %zu pending sockets\n", psockets); + if (sc->listen.backlog == -1) /* was not a listener */ smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO, "destroying mem pools\n"); From e9b33376bd07bca4175f7bcc2d6034ef250f8181 Mon Sep 17 00:00:00 2001 From: Yunseong Kim Date: Wed, 22 Jul 2026 22:31:06 +0200 Subject: [PATCH 082/142] ksmbd: validate ipc response length before dereferencing its fields ipc_validate_msg() computes the expected message size by reading length fields out of the response buffer supplied by the userspace ksmbd daemon (payload_sz, session_key_len, ngroups, ...). Those fields are read before the buffer is verified to be large enough to contain the struct they belong to, so a short response makes the read land past the end of the allocation. handle_response() sizes entry->response purely from the netlink attribute length (nla_len()) and only guards the leading handle read, so the daemon can install a response as small as the kmalloc-8 object seen below. When ipc_msg_send_request() then calls ipc_validate_msg() for a KSMBD_EVENT_RPC_REQUEST, the cast to struct ksmbd_rpc_command reads resp->payload_sz at offset 8 of an 8-byte allocation: [ 3697.841381] ================================================================== [ 3697.844099] BUG: KASAN: slab-out-of-bounds in ipc_msg_send_request+0x763/0x800 [ 3697.846604] Read of size 4 at addr ffff888105f95910 by task kworker/4:3/20682 [ 3697.849061] [ 3697.849801] CPU: 4 UID: 0 PID: 20682 Comm: kworker/4:3 Not tainted 7.2.0-rc3-next-20260717-virtme #117 PREEMPT(lazy) [ 3697.850077] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014 [ 3697.850303] Workqueue: ksmbd-io handle_ksmbd_work [ 3697.850592] Call Trace: [ 3697.850794] [ 3697.850952] __dump_stack+0x21/0x60 [ 3697.851239] dump_stack_lvl+0xc2/0x100 [ 3697.851528] print_address_description+0x77/0x200 [ 3697.851816] ? ipc_msg_send_request+0x763/0x800 [ 3697.852024] print_report+0x58/0x70 [ 3697.852316] kasan_report+0x117/0x150 [ 3697.852585] ? down_write+0x146/0x1f0 [ 3697.852809] ? ipc_msg_send_request+0x763/0x800 [ 3697.853082] ipc_msg_send_request+0x763/0x800 [ 3697.853385] ? __pfx_ipc_msg_send_request+0x10/0x10 [ 3697.853604] ? kasan_unpoison+0x48/0x70 [ 3697.853936] ? __pfx___up_read+0x10/0x10 [ 3697.854221] ksmbd_rpc_ioctl+0x380/0x520 [ 3697.854542] ? __pfx_ksmbd_rpc_ioctl+0x10/0x10 [ 3697.854757] ? kasan_unpoison+0x48/0x70 [ 3697.854962] ? copy_from_kernel_nofault+0x32c/0x4e0 [ 3697.855166] ? kasan_unpoison+0x48/0x70 [ 3697.855416] fsctl_pipe_transceive+0x139/0x7a0 [ 3697.855705] ? __pfx_copy_from_kernel_nofault+0x10/0x10 [ 3697.855937] ? __pfx_fsctl_pipe_transceive+0x10/0x10 [ 3697.856388] ? __sanitizer_cov_trace_switch+0x7b/0x140 [ 3697.856620] smb2_ioctl+0x1141/0x3420 [ 3697.856994] ? __pfx_smb2_ioctl+0x10/0x10 [ 3697.857182] ? get_smb2_cmd_val+0xe3/0x1c0 [ 3697.857655] handle_ksmbd_work+0x9ad/0x15e0 [ 3697.858034] ? __pfx_handle_ksmbd_work+0x10/0x10 [ 3697.858251] ? lock_release+0xf7/0x360 [ 3697.858466] ? process_scheduled_works+0x954/0x1600 [ 3697.858698] ? process_scheduled_works+0x954/0x1600 [ 3697.858905] process_scheduled_works+0xc22/0x1600 [ 3697.859368] ? __pfx_process_scheduled_works+0x10/0x10 [ 3697.859637] ? __pfx_assign_work+0x10/0x10 [ 3697.859896] ? lock_is_held_type+0x7b/0x110 [ 3697.860146] worker_thread+0x975/0xee0 [ 3697.860524] ? __pfx_do_raw_spin_lock+0x10/0x10 [ 3697.860830] ? __kthread_parkme+0x21e/0x260 [ 3697.861105] kthread+0x3a6/0x490 [ 3697.861423] ? __pfx_worker_thread+0x10/0x10 [ 3697.861643] ? __pfx_kthread+0x10/0x10 [ 3697.861878] ret_from_fork+0x55a/0xa20 [ 3697.862194] ? __pfx_ret_from_fork+0x10/0x10 [ 3697.862480] ? __pfx_kthread+0x10/0x10 [ 3697.862714] ret_from_fork_asm+0x1a/0x30 [ 3697.862965] [ 3697.863039] [ 3697.938882] Allocated by task 20761: [ 3697.940257] kasan_save_track+0x3e/0x80 [ 3697.941782] __kasan_kmalloc+0x72/0x90 [ 3697.943228] __kvmalloc_node_noprof+0x3e9/0x6a0 [ 3697.944948] handle_generic_event+0x59b/0x750 [ 3697.946592] genl_family_rcv_msg_doit+0x3d6/0x560 [ 3697.946977] genl_rcv_msg+0x67c/0x900 [ 3697.947224] netlink_rcv_skb+0x286/0x580 [ 3697.947488] genl_rcv+0x2d/0x80 [ 3697.947706] netlink_unicast+0x937/0xb70 [ 3697.947993] netlink_sendmsg+0x977/0xc10 [ 3697.948268] __sock_sendmsg+0x264/0x2d0 [ 3697.948536] __sys_sendto+0x4de/0x690 [ 3697.948789] __x64_sys_sendto+0x173/0x380 [ 3697.949069] do_syscall_64+0x13d/0x420 [ 3697.949328] entry_SYSCALL_64_after_hwframe+0x77/0x7f [ 3697.949662] [ 3697.949779] The buggy address belongs to the object at ffff888105f95908 [ 3697.949779] which belongs to the cache kmalloc-8 of size 8 [ 3697.950550] The buggy address is located 0 bytes to the right of [ 3697.950550] allocated 8-byte region [ffff888105f95908, ffff888105f95910) [ 3697.951455] [ 3697.951574] The buggy address belongs to the physical page: [ 3697.951958] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0xffff888105f951b8 pfn:0x105f95 [ 3697.952571] flags: 0x100000000000200(workingset|node=0|zone=2) [ 3697.952973] page_type: f5(slab) [ 3697.953198] raw: 0100000000000200 ffff888100042640 ffffea0004063610 ffff888100040588 [ 3697.953707] raw: ffff888105f951b8 00000000001c000e 00000000f5000000 0000000000000000 [ 3697.954240] page dumped because: kasan: bad access detected [ 3697.954616] [ 3697.954734] Memory state around the buggy address: [ 3697.955063] ffff888105f95800: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fa [ 3697.955534] ffff888105f95880: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 3697.956006] >ffff888105f95900: fc 00 fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 3697.956477] ^ [ 3697.956728] ffff888105f95980: fc fc fc fa fc fc fc fc fc fc fc fc fc fc fc fc [ 3697.957202] ffff888105f95a00: fc fc fc fc fc fa fc fc fc fc fc fc fc fc fc fc [ 3697.957671] ================================================================== The final "entry->msg_sz != msg_sz" comparison cannot help: the offending read has already happened by the time it runs. Every case in the switch shares this pattern. Floor entry->msg_sz against the base struct of each event type before dereferencing any of its length fields. On failure ipc_msg_send_request() already frees the response and returns NULL, so callers stay safe. The malformed message originates from the ksmbd.mountd daemon over genl netlink rather than a remote SMB client, so triggering it requires a buggy or compromised daemon; it is still an out-of-bounds read the validator is meant to prevent. Fixes: d6a6aa81eac2 ("ksmbd: validate response sizes in ipc_validate_msg()") Signed-off-by: Yunseong Kim Signed-off-by: Namjae Jeon --- fs/smb/server/transport_ipc.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/fs/smb/server/transport_ipc.c b/fs/smb/server/transport_ipc.c index bd58d3d0bad5..2584b162415b 100644 --- a/fs/smb/server/transport_ipc.c +++ b/fs/smb/server/transport_ipc.c @@ -506,6 +506,9 @@ static int ipc_validate_msg(struct ipc_msg_table_entry *entry) { struct ksmbd_rpc_command *resp = entry->response; + if (entry->msg_sz < sizeof(struct ksmbd_rpc_command)) + return -EINVAL; + if (check_add_overflow(sizeof(struct ksmbd_rpc_command), resp->payload_sz, &msg_sz)) return -EINVAL; @@ -515,6 +518,9 @@ static int ipc_validate_msg(struct ipc_msg_table_entry *entry) { struct ksmbd_spnego_authen_response *resp = entry->response; + if (entry->msg_sz < sizeof(struct ksmbd_spnego_authen_response)) + return -EINVAL; + msg_sz = sizeof(struct ksmbd_spnego_authen_response) + resp->session_key_len + resp->spnego_blob_len; break; @@ -523,6 +529,9 @@ static int ipc_validate_msg(struct ipc_msg_table_entry *entry) { struct ksmbd_share_config_response *resp = entry->response; + if (entry->msg_sz < sizeof(struct ksmbd_share_config_response)) + return -EINVAL; + if (resp->payload_sz) { if (resp->payload_sz < resp->veto_list_sz) return -EINVAL; @@ -537,6 +546,9 @@ static int ipc_validate_msg(struct ipc_msg_table_entry *entry) { struct ksmbd_login_response_ext *resp = entry->response; + if (entry->msg_sz < sizeof(struct ksmbd_login_response_ext)) + return -EINVAL; + if (resp->ngroups) { if (resp->ngroups < 0 || resp->ngroups > NGROUPS_MAX) { From df3e2150f3ea44256688ad1be9cbd7453fcf0ca2 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Thu, 23 Jul 2026 04:11:29 +0000 Subject: [PATCH 083/142] smb/server: fix signing when a response uses more than one iov Some SMB responses keep their data in another buffer. The SMB header and the data are then in different iovs. The old code only handled this for SMB2 READ. For other commands, it signed only the last iov. QUERY_INFO and CHANGE_NOTIFY can also use another iov for their data. Their SMB header was not signed, so Windows will client rejected the response. Find the iov that starts with the current SMB header. Sign this iov and all iovs after it. Suggested-by: Andy Shevchenko Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 51 ++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 153bfe12ae80..9c93a39f3588 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -11149,6 +11149,39 @@ int smb2_check_sign_req(struct ksmbd_work *work) return 1; } +/** + * smb2_get_sign_rsp_iov() - get the iovecs used to sign a response + * @work: work that has the response iovecs + * @hdr: SMB2 header of the response + * @n_vec: set to the number of iovecs to sign + * + * Response data may be in another buffer. In this case, the response uses + * more than one iovec. Find the iovec that starts with @hdr. Sign this + * iovec and all iovecs after it. + * + * Return: The first iovec to sign. + */ +static struct kvec *smb2_get_sign_rsp_iov(struct ksmbd_work *work, + struct smb2_hdr *hdr, int *n_vec) +{ + int i; + + /* + * iov[0] has the RFC1002 message length. It is not part of the SMB2 + * message, so do not sign it. + */ + for (i = 1; i <= work->iov_idx; i++) { + if (work->iov[i].iov_base == hdr) { + *n_vec = work->iov_idx - i + 1; + return &work->iov[i]; + } + } + + WARN_ON_ONCE(1); + *n_vec = 1; + return &work->iov[work->iov_idx]; +} + /** * smb2_set_sign_rsp() - handler for rsp packet sign processing * @work: smb work containing notify command buffer @@ -11159,18 +11192,13 @@ void smb2_set_sign_rsp(struct ksmbd_work *work) struct smb2_hdr *hdr; char signature[SMB2_HMACSHA256_SIZE]; struct kvec *iov; - int n_vec = 1; + int n_vec; hdr = ksmbd_resp_buf_curr(work); hdr->Flags |= SMB2_FLAGS_SIGNED; memset(hdr->Signature, 0, SMB2_SIGNATURE_SIZE); - if (hdr->Command == SMB2_READ) { - iov = &work->iov[work->iov_idx - 1]; - n_vec++; - } else { - iov = &work->iov[work->iov_idx]; - } + iov = smb2_get_sign_rsp_iov(work, hdr, &n_vec); ksmbd_sign_smb2_pdu(work->conn, work->sess->sess_key, iov, n_vec, signature); @@ -11253,7 +11281,7 @@ void smb3_set_sign_rsp(struct ksmbd_work *work) char signature[SMB2_CMACAES_SIZE]; struct kvec *iov; u16 command = conn->ops->get_cmd_val(work); - int n_vec = 1; + int n_vec; char *signing_key; hdr = ksmbd_resp_buf_curr(work); @@ -11275,12 +11303,7 @@ void smb3_set_sign_rsp(struct ksmbd_work *work) hdr->Flags |= SMB2_FLAGS_SIGNED; memset(hdr->Signature, 0, SMB2_SIGNATURE_SIZE); - if (hdr->Command == SMB2_READ) { - iov = &work->iov[work->iov_idx - 1]; - n_vec++; - } else { - iov = &work->iov[work->iov_idx]; - } + iov = smb2_get_sign_rsp_iov(work, hdr, &n_vec); ksmbd_sign_smb3_pdu(conn, signing_key, iov, n_vec, signature); memcpy(hdr->Signature, signature, SMB2_SIGNATURE_SIZE); From 4fd5bad647bfa45eb86bfd2f03ba1bef3fcd5851 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Thu, 23 Jul 2026 04:11:30 +0000 Subject: [PATCH 084/142] smb/server: cancel async requests when closing connection An async request may still be waiting when a connection is closed. This can stop the connection from closing. Cancel active async requests before waiting for them to finish. Suggested-by: Namjae Jeon Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index b08ef8e49a24..99ffb00e87bf 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -371,6 +371,26 @@ void ksmbd_conn_try_dequeue_request(struct ksmbd_work *work) wake_up_all(&conn->req_running_q); } +static void ksmbd_conn_cancel_async_requests(struct ksmbd_conn *conn) +{ + struct ksmbd_work *work, *tmp; + + ksmbd_debug(CONN, "Cancel pending async requests on releasing connection\n"); + spin_lock(&conn->request_lock); + list_for_each_entry_safe(work, tmp, &conn->async_requests, + async_request_entry) { + if (work->state != KSMBD_WORK_ACTIVE) + continue; + + ksmbd_debug(CONN, "Cancel async request id %d\n", + work->async_id); + work->state = KSMBD_WORK_CANCELLED; + if (work->cancel_fn) + work->cancel_fn(work->cancel_argv); + } + spin_unlock(&conn->request_lock); +} + void ksmbd_conn_lock(struct ksmbd_conn *conn) { mutex_lock(&conn->srv_mutex); @@ -665,6 +685,7 @@ int ksmbd_conn_handler_loop(void *p) } ksmbd_conn_set_releasing(conn); + ksmbd_conn_cancel_async_requests(conn); /* Wait till all reference dropped to the Server object*/ ksmbd_debug(CONN, "Wait for all pending requests(%d)\n", atomic_read(&conn->r_count)); wait_event(conn->r_count_q, atomic_read(&conn->r_count) == 0); From 528af7cf69f3e6f1d8899561441c75a1616ef779 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Thu, 23 Jul 2026 15:24:10 +0900 Subject: [PATCH 085/142] smb/server: avoid registering async requests during connection close A connection-close scan can miss the synthetic CHANGE_NOTIFY work item because smb2_notify() registers it directly after setup_async_work() has returned. Link both regular and synthetic async work through one helper that checks the connection state under request_lock. If the connection is already closing, release a newly allocated async ID or complete the synthetic notify work immediately. Signed-off-by: ChenXiaoSong Co-developed-by: Namjae Jeon Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 53 +++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 9c93a39f3588..2038de315fbc 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -991,6 +991,24 @@ smb2_get_name(const char *src, const int maxlen, struct nls_table *local_nls) return name; } +/* Link a fully initialized async work item unless the connection is closing. */ +static bool ksmbd_conn_link_async_request(struct ksmbd_conn *conn, + struct ksmbd_work *work) +{ + bool linked = false; + + spin_lock(&conn->request_lock); + if (!ksmbd_conn_exiting(conn) && !ksmbd_conn_releasing(conn)) { + if (list_empty(&work->async_request_entry)) + list_add_tail(&work->async_request_entry, + &conn->async_requests); + linked = true; + } + spin_unlock(&conn->request_lock); + + return linked; +} + int setup_async_work(struct ksmbd_work *work, void (*fn)(void **), void **arg) { struct ksmbd_conn *conn = work->conn; @@ -1003,20 +1021,22 @@ int setup_async_work(struct ksmbd_work *work, void (*fn)(void **), void **arg) } work->asynchronous = true; work->async_id = id; + work->cancel_fn = fn; + work->cancel_argv = arg; + + if (!ksmbd_conn_link_async_request(conn, work)) { + work->asynchronous = false; + work->async_id = 0; + work->cancel_fn = NULL; + work->cancel_argv = NULL; + ksmbd_release_id(&conn->async_ida, id); + return -ESHUTDOWN; + } ksmbd_debug(SMB, "Send interim Response to inform async request id : %d\n", work->async_id); - work->cancel_fn = fn; - work->cancel_argv = arg; - - if (list_empty(&work->async_request_entry)) { - spin_lock(&conn->request_lock); - list_add_tail(&work->async_request_entry, &conn->async_requests); - spin_unlock(&conn->request_lock); - } - return 0; } @@ -11075,9 +11095,18 @@ int smb2_notify(struct ksmbd_work *work) in_work->cancel_argv[1] = fp; in_work->cancel_fn = smb2_notify_cancel_fn; } - spin_lock(&work->conn->request_lock); - list_add_tail(&in_work->async_request_entry, &work->conn->async_requests); - spin_unlock(&work->conn->request_lock); + + if (!ksmbd_conn_link_async_request(work->conn, in_work)) { + kfree(in_work->cancel_argv); + in_work->cancel_argv = NULL; + in_work->cancel_fn = NULL; + in_work->asynchronous = false; + ksmbd_fd_put(work, fp); + ksmbd_conn_write(in_work); + ksmbd_free_work_struct(in_work); + work->send_no_response = 1; + return 0; + } spin_lock(&fp->f_lock); list_add_tail(&in_work->notify_entry, &fp->notify_pendings); From 06c7b1d731bc105a8644f1b70165ba8b9416cbab Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 24 Jul 2026 11:17:30 +0900 Subject: [PATCH 086/142] ksmbd: free preauth sessions on connection teardown SMB3.1.1 multichannel binding preserves the preauthentication hash in a preauth_session between the NTLM negotiate and authenticate requests. The binding NTLM negotiate allocates this object and returns STATUS_MORE_PROCESSING_REQUIRED. If the client disconnects before it sends the authenticate request, neither the authenticate nor error cleanup paths free the object. Release any remaining preauthentication sessions when tearing down the connection. Initialize the list when allocating the connection so that this cleanup is safe regardless of the negotiated dialect. Reported-by: Runa Takemoto Fixes: f5a544e3bab7 ("ksmbd: add support for SMB3 multichannel") Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 3 +++ fs/smb/server/mgmt/user_session.c | 11 +++++++++++ fs/smb/server/mgmt/user_session.h | 1 + fs/smb/server/smb2ops.c | 2 -- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 99ffb00e87bf..dfbbded896d4 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -11,6 +11,7 @@ #include "server.h" #include "smb_common.h" #include "mgmt/ksmbd_ida.h" +#include "mgmt/user_session.h" #include "connection.h" #include "compress.h" #include "transport_tcp.h" @@ -257,6 +258,7 @@ void ksmbd_conn_free(struct ksmbd_conn *conn) kvfree(conn->request_buf); kfree(conn->preauth_info); kfree(conn->mechToken); + ksmbd_preauth_session_destroy(conn); ksmbd_conn_put(conn); } @@ -303,6 +305,7 @@ struct ksmbd_conn *ksmbd_conn_alloc(void) init_waitqueue_head(&conn->r_count_q); INIT_LIST_HEAD(&conn->requests); INIT_LIST_HEAD(&conn->async_requests); + INIT_LIST_HEAD(&conn->preauth_sess_table); spin_lock_init(&conn->request_lock); spin_lock_init(&conn->credits_lock); ida_init(&conn->async_ida); diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index b2bc8119984f..65f3ce3c6e95 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -600,6 +600,17 @@ struct preauth_session *ksmbd_preauth_session_alloc(struct ksmbd_conn *conn, return sess; } +void ksmbd_preauth_session_destroy(struct ksmbd_conn *conn) +{ + struct preauth_session *sess, *tmp; + + list_for_each_entry_safe(sess, tmp, &conn->preauth_sess_table, + preauth_entry) { + list_del(&sess->preauth_entry); + kfree(sess); + } +} + void destroy_previous_session(struct ksmbd_conn *conn, struct ksmbd_user *user, u64 id) { diff --git a/fs/smb/server/mgmt/user_session.h b/fs/smb/server/mgmt/user_session.h index 4637a8c8436d..2a0168e08328 100644 --- a/fs/smb/server/mgmt/user_session.h +++ b/fs/smb/server/mgmt/user_session.h @@ -104,6 +104,7 @@ void destroy_previous_session(struct ksmbd_conn *conn, struct ksmbd_user *user, u64 id); struct preauth_session *ksmbd_preauth_session_alloc(struct ksmbd_conn *conn, u64 sess_id); +void ksmbd_preauth_session_destroy(struct ksmbd_conn *conn); struct preauth_session *ksmbd_preauth_session_lookup(struct ksmbd_conn *conn, unsigned long long id); diff --git a/fs/smb/server/smb2ops.c b/fs/smb/server/smb2ops.c index c64a8c427d39..4578291fb172 100644 --- a/fs/smb/server/smb2ops.c +++ b/fs/smb/server/smb2ops.c @@ -297,8 +297,6 @@ int init_smb3_11_server(struct ksmbd_conn *conn) conn->vals->req_capabilities |= SMB2_GLOBAL_CAP_MULTI_CHANNEL; /* See init_smb3_02_server(): persistent handles require CA recovery. */ - - INIT_LIST_HEAD(&conn->preauth_sess_table); return 0; } From 3f220a0a62e6b9b391c9d1f0e6580b05173cc7f7 Mon Sep 17 00:00:00 2001 From: Aldo Ariel Panzardo Date: Thu, 23 Jul 2026 20:00:53 -0300 Subject: [PATCH 087/142] ksmbd: only rebind the reopened file's own oplock on durable reconnect ksmbd_reopen_durable_fd() walks the inode's m_op_list and rebinds every detached oplock to the reconnecting session: list_for_each_entry_rcu(op, &ci->m_op_list, op_entry, lockdep_is_held(&ci->m_lock)) { if (op->conn) continue; op->conn = ksmbd_conn_get(fp->conn); op->sess = work->sess; } The only key is op->conn == NULL, which every detached durable handle on that inode matches, not just the one owned by fp. When two sessions hold durable handles on the same file and both disconnect, reconnecting one of them adopts the other session's oplock: op->sess is overwritten with the reconnecting session without taking a reference on it, while op->conn pins the connection. The sibling teardown path, session_fd_check(), keys on the identity of the connection being torn down (op->conn == conn) rather than on shared state, and so does not have this problem. Once the adopting session is destroyed, ksmbd_session_destroy() frees it while the foreign oplock still points at it. The reader in ksmbd_close_fd_app_instance_id() validates only opinfo->conn, which is still live thanks to the reference taken above, and then dereferences the stale session: if (!opinfo->conn) { up_read(&fp->f_ci->m_lock); goto out; } ft = &opinfo->sess->file_table; write_lock(&ft->lock); BUG: KASAN: slab-use-after-free in _raw_write_lock+0x74/0xd0 Write of size 4 at addr ffff88810a970528 by task kworker/0:0/9 Workqueue: ksmbd-io handle_ksmbd_work Call Trace: _raw_write_lock+0x74/0xd0 ksmbd_close_fd_app_instance_id+0x183/0x410 smb2_open+0x1346/0x4430 handle_ksmbd_work+0x2bb/0x7b0 Reached from an authenticated session against a share with the default durable-handle and oplock configuration: two sessions open the same file with a durable-v2 handle and an RH lease under distinct AppInstanceIds, both log off, one reconnects with DH2C, and a later durable-v2 create carrying the other AppInstanceId walks into the freed session. Constrain the loop to the oplock owned by the file being reopened. Fixes: f363a0fb134a ("ksmbd: fix app-instance durable supersede session UAF") Cc: stable@vger.kernel.org Signed-off-by: Aldo Ariel Panzardo Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/vfs_cache.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 0a8c3c6e1c81..c28e3d65d64b 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1894,7 +1894,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) down_write(&ci->m_lock); list_for_each_entry_rcu(op, &ci->m_op_list, op_entry, lockdep_is_held(&ci->m_lock)) { - if (op->conn) + if (op->conn || op->o_fp != fp) continue; op->conn = ksmbd_conn_get(fp->conn); op->sess = work->sess; From 25e414db703334954747cb1b3eedf914b093ff8c Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 26 Jul 2026 10:00:00 +0900 Subject: [PATCH 088/142] ksmbd: report holes in allocated range queries FSCTL_QUERY_ALLOCATED_RANGES treated every range in a file without the sparse attribute as allocated. Files can have holes after an ordinary write beyond EOF, so CIFS FIEMAP reported extents for those holes. SEEK_DATA and SEEK_HOLE are insufficient because unwritten extents look like holes. Use zero writes for FSCTL_SET_ZERO_DATA on dense files. Sparse files still use hole punching, and allocated-range queries can use SEEK_DATA and SEEK_HOLE for both file types. When clearing the sparse attribute, materialize holes with zero writes before updating the attribute. This keeps the file fully allocated without relying on unwritten extents that SEEK_DATA would still report as holes. Return STATUS_BUFFER_OVERFLOW when another allocated range does not fit in the SMB response so the client continues the query. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 18 +++-- fs/smb/server/vfs.c | 167 +++++++++++++++++++++++++++++++--------- fs/smb/server/vfs.h | 9 ++- 3 files changed, 148 insertions(+), 46 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 2038de315fbc..1ab0e37ec99c 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9801,14 +9801,15 @@ static int fsctl_query_allocated_ranges(struct ksmbd_work *work, u64 id, if (!in_count) { struct file_allocated_range_buffer range; - ret = ksmbd_vfs_fqar_lseek(fp, start, length, &range, 1, - out_count); - if (!ret && *out_count) + ret = ksmbd_vfs_query_allocated_ranges(fp, start, length, + &range, 1, out_count); + if ((!ret || ret == -E2BIG) && *out_count) ret = -ENOSPC; *out_count = 0; } else { - ret = ksmbd_vfs_fqar_lseek(fp, start, length, - qar_rsp, in_count, out_count); + ret = ksmbd_vfs_query_allocated_ranges(fp, start, length, + qar_rsp, in_count, + out_count); } if (ret && ret != -E2BIG) *out_count = 0; @@ -9894,6 +9895,13 @@ static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id, idmap = file_mnt_idmap(fp->filp); old_fattr = fp->f_ci->m_fattr; + if (!sparse->SetSparse && + (old_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE)) { + ret = ksmbd_vfs_zero_holes(fp); + if (ret) + goto out; + } + if (sparse->SetSparse) fp->f_ci->m_fattr |= FILE_ATTRIBUTE_SPARSE_FILE_LE; else diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 854a09020a97..ff86e0e88177 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -935,6 +935,8 @@ int ksmbd_vfs_zero_data(struct ksmbd_work *work, struct ksmbd_file *fp, loff_t off, loff_t len) { const struct cred *saved_cred; + loff_t pos = off, size; + char *zero_buf = NULL; int err; smb_break_all_levII_oplock(work, fp, 1); @@ -951,15 +953,117 @@ int ksmbd_vfs_zero_data(struct ksmbd_work *work, struct ksmbd_file *fp, } saved_cred = override_creds(fp->filp->f_cred); - if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) + if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) { 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); + } else { + size = i_size_read(file_inode(fp->filp)); + if (off >= size) { + err = 0; + goto out; + } + + len = min(len, size - off); + zero_buf = kvzalloc(SZ_64K, GFP_KERNEL); + if (!zero_buf) { + err = -ENOMEM; + goto out; + } + + while (len) { + ssize_t written; + size_t count = min_t(loff_t, len, SZ_64K); + + written = kernel_write(fp->filp, zero_buf, count, &pos); + if (written < 0) { + err = written; + goto out; + } + if (!written) { + err = -EIO; + goto out; + } + len -= written; + } + err = 0; + } +out: revert_creds(saved_cred); + kvfree(zero_buf); + return err; +} + +int ksmbd_vfs_zero_holes(struct ksmbd_file *fp) +{ + struct file *f = fp->filp; + const struct cred *saved_cred; + loff_t size, pos = 0; + char *zero_buf; + int err; + + err = file_write_and_wait(f); + if (err) + return err; + + size = i_size_read(file_inode(f)); + if (!size) + return 0; + + /* + * FALLOC_FL_ZERO_RANGE may leave unwritten extents, which SEEK_DATA + * reports as holes. Write zeroes into each hole so that clearing the + * sparse attribute leaves the file fully allocated. + */ + zero_buf = kvzalloc(SZ_64K, GFP_KERNEL); + if (!zero_buf) + return -ENOMEM; + + saved_cred = override_creds(f->f_cred); + while (pos < size) { + loff_t data, hole; + + hole = vfs_llseek(f, pos, SEEK_HOLE); + if (hole == -ENXIO || hole >= size) + break; + if (hole < 0) { + err = hole; + goto out; + } + + data = vfs_llseek(f, hole, SEEK_DATA); + if (data == -ENXIO) { + data = size; + } else if (data < 0) { + err = data; + goto out; + } + data = min(data, size); + if (data <= hole) { + err = -EIO; + goto out; + } + + pos = hole; + while (pos < data) { + ssize_t written; + size_t count = min_t(loff_t, data - pos, SZ_64K); + + written = kernel_write(f, zero_buf, count, &pos); + if (written < 0) { + err = written; + goto out; + } + if (!written) { + err = -EIO; + goto out; + } + } + } + err = file_write_and_wait(f); +out: + revert_creds(saved_cred); + kvfree(zero_buf); return err; } @@ -990,51 +1094,36 @@ int ksmbd_vfs_trim_data(struct ksmbd_work *work, struct ksmbd_file *fp, return err; } -int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, - struct file_allocated_range_buffer *ranges, - unsigned int in_count, unsigned int *out_count) +int ksmbd_vfs_query_allocated_ranges(struct ksmbd_file *fp, loff_t start, + loff_t length, + struct file_allocated_range_buffer *ranges, + unsigned int in_count, + unsigned int *out_count) { struct file *f = fp->filp; struct inode *inode = file_inode(fp->filp); - loff_t maxbytes = (u64)inode->i_sb->s_maxbytes, end, query_start; - loff_t query_length, size; - loff_t extent_start, extent_end; + loff_t maxbytes = inode->i_sb->s_maxbytes, size; + loff_t extent_start, extent_end, end; int ret = 0; + *out_count = 0; + if (start < 0 || length < 0) + return -EINVAL; if (start > maxbytes) return -EFBIG; - if (!in_count) return 0; - - /* - * Shrink request scope to what the fs can actually handle. - */ - if (length > maxbytes || (maxbytes - length) < start) + if (length > maxbytes || maxbytes - length < start) length = maxbytes - start; - size = i_size_read(inode); - if (start >= size) + if (!length || start >= size) return 0; - - if (!length) - return 0; - - if (start + length > size) + if (length > size - start) length = size - start; - *out_count = 0; - query_start = start; - query_length = length; end = start + length; - if (!(fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE)) { - ranges[0].file_offset = cpu_to_le64(query_start); - ranges[0].length = cpu_to_le64(query_length); - *out_count = 1; - return 0; - } - - if (start < end) { + if ((fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) && + start < end) { ret = file_write_and_wait_range(f, start, end - 1); if (ret) return ret; @@ -1044,7 +1133,7 @@ int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, extent_start = vfs_llseek(f, start, SEEK_DATA); if (extent_start < 0) { if (extent_start != -ENXIO) - ret = (int)extent_start; + ret = extent_start; break; } @@ -1054,7 +1143,7 @@ int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, extent_end = vfs_llseek(f, extent_start, SEEK_HOLE); if (extent_end < 0) { if (extent_end != -ENXIO) - ret = (int)extent_end; + ret = extent_end; break; } else if (extent_start >= extent_end) { break; @@ -1063,10 +1152,12 @@ int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, ranges[*out_count].file_offset = cpu_to_le64(extent_start); ranges[(*out_count)++].length = cpu_to_le64(min(extent_end, end) - extent_start); - start = extent_end; } + if (!ret && start < end && *out_count == in_count) + ret = -E2BIG; + return ret; } diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 10cb37a78c0d..1818b3f1971c 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -135,12 +135,15 @@ int ksmbd_vfs_empty_dir(struct ksmbd_file *fp); 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); +int ksmbd_vfs_zero_holes(struct ksmbd_file *fp); int ksmbd_vfs_trim_data(struct ksmbd_work *work, struct ksmbd_file *fp, loff_t off, loff_t len); struct file_allocated_range_buffer; -int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, - struct file_allocated_range_buffer *ranges, - unsigned int in_count, unsigned int *out_count); +int ksmbd_vfs_query_allocated_ranges(struct ksmbd_file *fp, loff_t start, + loff_t length, + struct file_allocated_range_buffer *ranges, + unsigned int in_count, + unsigned int *out_count); int ksmbd_vfs_unlink(struct file *filp); void *ksmbd_vfs_init_kstat(char **p, struct ksmbd_kstat *ksmbd_kstat); int ksmbd_vfs_fill_dentry_attrs(struct ksmbd_work *work, From 84c41b731b019f1e5a97c21ac1a4a4c63b6be38f Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 26 Jul 2026 11:01:42 +0900 Subject: [PATCH 089/142] ksmbd: stabilize allocation size after buffered writes Ordinary opens initialize their allocation size from stat.blocks. Buffered writes can leave delayed allocation pending, so separate handles can cache different block counts for the same file. This makes generic/568 fail when a zero write used for fallocate emulation is followed by an overwrite of the same range. The first query can report the pre-writeback block count, while the second query reports the block count after delayed allocation is completed. Complete writeback and refresh the cached block count before returning allocation information for ordinary opens. Track client-specified allocation sizes separately so CREATE allocation contexts and FILE_ALLOCATION_INFORMATION continue to return the requested value. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 38 ++++++++++++++++++++++++++++++-------- fs/smb/server/vfs_cache.h | 1 + 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 1ab0e37ec99c..ed6fb63d4a6d 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4544,6 +4544,7 @@ int smb2_open(struct ksmbd_work *work) goto err_out1; } alloc_size = le64_to_cpu(az_req->AllocationSize); + fp->allocation_size_set = true; ksmbd_debug(SMB, "request smb2 create allocate size : %llu\n", alloc_size); @@ -6172,6 +6173,30 @@ static int get_file_basic_info(struct smb2_query_info_rsp *rsp, return 0; } +static int get_file_allocation_stat(struct ksmbd_file *fp, struct kstat *stat) +{ + int ret; + + /* + * Buffered writes can leave delayed allocation in a state where two + * consecutive queries report different block counts even when the + * second write only overwrites the first one. Complete writeback before + * reporting the filesystem allocation for an ordinary open. + */ + if (!fp->allocation_size_set) { + ret = file_write_and_wait(fp->filp); + if (ret) + return ret; + } + + ret = vfs_getattr(&fp->filp->f_path, stat, STATX_BASIC_STATS, + AT_STATX_SYNC_AS_STAT); + if (!ret && !fp->allocation_size_set) + fp->allocation_size = S_ISDIR(stat->mode) ? 0 : stat->blocks << 9; + + return ret; +} + static int get_file_standard_info(struct smb2_query_info_rsp *rsp, struct ksmbd_file *fp, void *rsp_org) { @@ -6180,8 +6205,7 @@ static int get_file_standard_info(struct smb2_query_info_rsp *rsp, struct kstat stat; int ret; - ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, - AT_STATX_SYNC_AS_STAT); + ret = get_file_allocation_stat(fp, &stat); if (ret) return ret; @@ -6250,8 +6274,7 @@ static int get_file_all_info(struct ksmbd_work *work, return -EINVAL; } - ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, - AT_STATX_SYNC_AS_STAT); + ret = get_file_allocation_stat(fp, &stat); if (ret) { kfree(filename); return ret; @@ -6549,8 +6572,7 @@ static int get_file_network_open_info(struct smb2_query_info_rsp *rsp, return -EACCES; } - ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, - AT_STATX_SYNC_AS_STAT); + ret = get_file_allocation_stat(fp, &stat); if (ret) return ret; @@ -6683,8 +6705,7 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, return -EACCES; } - ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, - AT_STATX_SYNC_AS_STAT); + ret = get_file_allocation_stat(fp, &stat); if (ret) return ret; @@ -7817,6 +7838,7 @@ static int set_file_allocation_info(struct ksmbd_work *work, } fp->allocation_size = le64_to_cpu(file_alloc_info->AllocationSize); + fp->allocation_size_set = true; return 0; } diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 5fac4b0b419d..d80f379d4e12 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -117,6 +117,7 @@ struct ksmbd_file { bool is_nt_open; bool attrib_only; + bool allocation_size_set; char client_guid[16]; char create_guid[16]; From a47634cd729b42dfe372048b056d98ba4e128eaa Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 26 Jul 2026 22:49:45 +0900 Subject: [PATCH 090/142] ksmbd: expire SMB sessions when Kerberos tickets expire Store the expiry time from the Kerberos authentication response in the session and reject requests after that time with STATUS_NETWORK_SESSION_EXPIRED. Allow an expired Kerberos session to be reauthenticated. Keep the old SMB signing key until its SESSION_SETUP response has been signed, then install the new session key and regenerate the SMB3 keys. Signed-off-by: Namjae Jeon --- fs/smb/server/auth.c | 18 +- fs/smb/server/ksmbd_netlink.h | 1 + fs/smb/server/ksmbd_work.h | 1 + fs/smb/server/mgmt/user_session.c | 14 +- fs/smb/server/mgmt/user_session.h | 3 + fs/smb/server/server.c | 9 +- fs/smb/server/smb2pdu.c | 270 +++++++++++++++++++++--------- 7 files changed, 236 insertions(+), 80 deletions(-) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index f2100e3ec54c..d51f32095ada 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -463,6 +463,7 @@ int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob, memcpy(out_blob, resp->payload + resp->session_key_len, resp->spnego_blob_len); *out_len = resp->spnego_blob_len; + sess->kerberos_expiry = resp->session_expiry; retval = 0; out: kvfree(resp); @@ -717,8 +718,21 @@ static int ksmbd_get_encryption_key(struct ksmbd_work *work, __u64 ses_id, if (enc) sess = work->sess; - else - sess = ksmbd_session_lookup_all(work->conn, ses_id); + else { + /* + * An encrypted SESSION_SETUP request may reauthenticate an expired + * Kerberos session. Keep using the established decryption key so + * that the command can reach the session setup handler. Other + * commands are rejected there with STATUS_NETWORK_SESSION_EXPIRED. + */ + sess = ksmbd_session_lookup(work->conn, ses_id); + if (sess && sess->state != SMB2_SESSION_VALID && + (sess->state != SMB2_SESSION_EXPIRED || + !sess->kerberos_expiry)) { + ksmbd_user_session_put(sess); + sess = NULL; + } + } if (!sess) return -EINVAL; diff --git a/fs/smb/server/ksmbd_netlink.h b/fs/smb/server/ksmbd_netlink.h index 1ea0a367b396..af1e760453d9 100644 --- a/fs/smb/server/ksmbd_netlink.h +++ b/fs/smb/server/ksmbd_netlink.h @@ -286,6 +286,7 @@ struct ksmbd_spnego_authen_response { * stored in SecurityBuffer of SMB2 SESSION * SETUP response */ + __u64 session_expiry; /* Kerberos ticket expiry time */ __u8 payload[]; /* session key + AP_REP */ }; diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index 52d0c4dee65c..5f1d3ebab4fb 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -95,6 +95,7 @@ struct ksmbd_work { bool owns_conn_ref:1; bool need_invalidate_rkey:1; bool request_open_chseq_tracked:1; + bool session_setup_reauth:1; unsigned int remote_key; /* cancel works */ diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 65f3ce3c6e95..0a87a1378791 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -547,8 +547,8 @@ struct ksmbd_session *ksmbd_session_lookup_slowpath(unsigned long long id) return sess; } -struct ksmbd_session *ksmbd_session_lookup_all(struct ksmbd_conn *conn, - unsigned long long id) +struct ksmbd_session *ksmbd_session_lookup_all_states(struct ksmbd_conn *conn, + unsigned long long id) { struct ksmbd_session *sess; @@ -560,6 +560,15 @@ struct ksmbd_session *ksmbd_session_lookup_all(struct ksmbd_conn *conn, sess = NULL; } } + return sess; +} + +struct ksmbd_session *ksmbd_session_lookup_all(struct ksmbd_conn *conn, + unsigned long long id) +{ + struct ksmbd_session *sess; + + sess = ksmbd_session_lookup_all_states(conn, id); if (sess && sess->state != SMB2_SESSION_VALID) { ksmbd_user_session_put(sess); sess = NULL; @@ -639,6 +648,7 @@ void destroy_previous_session(struct ksmbd_conn *conn, } ksmbd_destroy_file_table(prev_sess); + prev_sess->kerberos_expiry = 0; prev_sess->state = SMB2_SESSION_EXPIRED; ksmbd_all_conn_set_status(id, KSMBD_SESS_NEED_SETUP); ksmbd_launch_ksmbd_durable_scavenger(); diff --git a/fs/smb/server/mgmt/user_session.h b/fs/smb/server/mgmt/user_session.h index 2a0168e08328..f8a24c33f7fe 100644 --- a/fs/smb/server/mgmt/user_session.h +++ b/fs/smb/server/mgmt/user_session.h @@ -47,6 +47,7 @@ struct ksmbd_session { __u8 *Preauth_HashValue; char sess_key[CIFS_KEY_SIZE]; + u64 kerberos_expiry; struct hlist_node hlist; struct rw_semaphore chann_lock; @@ -100,6 +101,8 @@ void ksmbd_sessions_deregister(struct ksmbd_conn *conn); struct ksmbd_session *__session_lookup(unsigned long long id); struct ksmbd_session *ksmbd_session_lookup_all(struct ksmbd_conn *conn, unsigned long long id); +struct ksmbd_session *ksmbd_session_lookup_all_states(struct ksmbd_conn *conn, + unsigned long long id); void destroy_previous_session(struct ksmbd_conn *conn, struct ksmbd_user *user, u64 id); struct preauth_session *ksmbd_preauth_session_alloc(struct ksmbd_conn *conn, diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 5b38406129b5..18c20a669307 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -208,6 +208,9 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, if (rc == -EINVAL) conn->ops->set_rsp_status(work, STATUS_INVALID_PARAMETER); + else if (rc == -EKEYEXPIRED) + conn->ops->set_rsp_status(work, + STATUS_NETWORK_SESSION_EXPIRED); else conn->ops->set_rsp_status(work, STATUS_USER_SESSION_DELETED); @@ -215,7 +218,11 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, struct smb2_hdr *rsp_hdr; rsp_hdr = ksmbd_resp_buf_curr(work); - rsp_hdr->Flags |= SMB2_FLAGS_SIGNED; + if (rc == -EKEYEXPIRED && work->sess && + conn->ops->set_sign_rsp) + conn->ops->set_sign_rsp(work); + else + rsp_hdr->Flags |= SMB2_FLAGS_SIGNED; } goto send; } else if (rc > 0) { diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index ed6fb63d4a6d..77c9e027d6aa 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -15,6 +15,7 @@ #include #include #include +#include #include "glob.h" #include "../common/smbfsctl.h" @@ -55,6 +56,10 @@ static void __wbuf(struct ksmbd_work *work, void **req, void **rsp) } } +static struct ksmbd_work *smb2_notify_cancel_claim(void **argv); +static void smb2_notify_cancel_fn(void **argv); +static void smb2_complete_notify_cancel(struct ksmbd_work *in_work); + #define WORK_BUFFERS(w, rq, rs) __wbuf((w), (void **)&(rq), (void **)&(rs)) #define SMB2_CREATE_FILE_ATTRIBUTE_MASK \ @@ -67,29 +72,6 @@ static void __wbuf(struct ksmbd_work *work, void **req, void **rsp) /* MAXFILESIZE in [MS-FSA] 2.1.5.3 Server Requests a Write. */ #define SMB2_MAX_FILE_SIZE 0xfffffff0000ULL -/** - * check_session_id() - check for valid session id in smb header - * @conn: connection instance - * @id: session id from smb header - * - * Return: 1 if valid session id, otherwise 0 - */ -static inline bool check_session_id(struct ksmbd_conn *conn, u64 id) -{ - struct ksmbd_session *sess; - - if (id == 0 || id == -1) - return false; - - sess = ksmbd_session_lookup_all(conn, id); - if (sess) { - ksmbd_user_session_put(sess); - return true; - } - pr_err("Invalid user session id: %llu\n", id); - return false; -} - struct channel *lookup_chann_list(struct ksmbd_session *sess, struct ksmbd_conn *conn) { struct channel *chann; @@ -899,6 +881,47 @@ int smb2_allocate_rsp_buf(struct ksmbd_work *work) return 0; } +static bool smb2_session_expired_cmd_allowed(struct ksmbd_work *work, + unsigned int cmd) +{ + struct smb2_lock_req *req; + unsigned int len, lock_count, i; + + if (cmd == SMB2_CANCEL_HE || cmd == SMB2_CLOSE_HE || + cmd == SMB2_LOGOFF_HE) + return true; + if (cmd != SMB2_LOCK_HE) + return false; + + req = ksmbd_req_buf_next(work); + if (req->hdr.NextCommand) + len = le32_to_cpu(req->hdr.NextCommand); + else { + len = get_rfc1002_len(work->request_buf); + if (len < work->next_smb2_rcv_hdr_off) + return false; + len -= work->next_smb2_rcv_hdr_off; + } + + lock_count = le16_to_cpu(req->LockCount); + if (!lock_count || len < offsetof(struct smb2_lock_req, locks) || + lock_count > (len - offsetof(struct smb2_lock_req, locks)) / + sizeof(struct smb2_lock_element)) + return false; + + for (i = 0; i < lock_count; i++) { + if (le32_to_cpu(req->locks[i].Flags) != SMB2_LOCKFLAG_UNLOCK) + return false; + } + return true; +} + +static bool smb2_session_kerberos_expired(struct ksmbd_session *sess) +{ + return sess->kerberos_expiry && + ktime_get_real_seconds() >= sess->kerberos_expiry; +} + /** * smb2_check_user_session() - check for valid session for a user * @work: smb work containing smb request buffer @@ -913,19 +936,37 @@ int smb2_check_user_session(struct ksmbd_work *work) unsigned long long sess_id; /* - * SMB2_ECHO, SMB2_NEGOTIATE, SMB2_SESSION_SETUP command do not - * require a session id, so no need to validate user session's for - * these commands. + * SMB2_NEGOTIATE and SMB2_SESSION_SETUP do not require a session id. + * SMB2_ECHO may omit it, but an echo carrying a session id still needs + * the session attached to work so that its signature can be checked and + * the response can be signed, including after Kerberos expiry. */ - if (cmd == SMB2_ECHO_HE || cmd == SMB2_NEGOTIATE_HE || - cmd == SMB2_SESSION_SETUP_HE) + if (cmd == SMB2_NEGOTIATE_HE || cmd == SMB2_SESSION_SETUP_HE) return 0; + sess_id = le64_to_cpu(req_hdr->SessionId); + if (cmd == SMB2_ECHO_HE) { + /* + * ECHO remains valid without a live session, including after + * LOGOFF. Attach an existing session only to authenticate a signed + * ECHO and sign its response; a stale SessionId is not an error. + */ + if (!work->next_smb2_rcv_hdr_off && sess_id) + work->sess = ksmbd_session_lookup_all_states(conn, sess_id); + if (work->sess) { + if (smb2_session_kerberos_expired(work->sess)) { + work->sess->state = SMB2_SESSION_EXPIRED; + } else if (work->sess->state != SMB2_SESSION_VALID) { + ksmbd_user_session_put(work->sess); + work->sess = NULL; + } + } + return 0; + } + if (!ksmbd_conn_good(conn)) return -EIO; - sess_id = le64_to_cpu(req_hdr->SessionId); - /* * If request is not the first in Compound request, * Just validate session id in header with work->sess->id. @@ -940,18 +981,35 @@ int smb2_check_user_session(struct ksmbd_work *work) sess_id, work->sess->id); return -EINVAL; } + if (smb2_session_kerberos_expired(work->sess)) + work->sess->state = SMB2_SESSION_EXPIRED; if (work->sess->state != SMB2_SESSION_VALID) { pr_err("compound request on a non-valid session (state %d)\n", work->sess->state); - return -EINVAL; + if (smb2_session_kerberos_expired(work->sess) && + smb2_session_expired_cmd_allowed(work, cmd)) + return 1; + return smb2_session_kerberos_expired(work->sess) ? + -EKEYEXPIRED : -EINVAL; } return 1; } /* Check for validity of user session */ - work->sess = ksmbd_session_lookup_all(conn, sess_id); - if (work->sess) + work->sess = ksmbd_session_lookup_all_states(conn, sess_id); + if (work->sess) { + if (smb2_session_kerberos_expired(work->sess)) { + work->sess->state = SMB2_SESSION_EXPIRED; + return smb2_session_expired_cmd_allowed(work, cmd) ? + 1 : -EKEYEXPIRED; + } + if (work->sess->state != SMB2_SESSION_VALID) { + ksmbd_user_session_put(work->sess); + work->sess = NULL; + return -ENOENT; + } return 1; + } ksmbd_debug(SMB, "Invalid user session, Uid %llu\n", sess_id); return -ENOENT; } @@ -2082,7 +2140,9 @@ static int krb5_authenticate(struct ksmbd_work *work, struct ksmbd_session *sess = work->sess; char *in_blob, *out_blob; char channel_key[CIFS_KEY_SIZE] = {}; - char *auth_key = conn->binding ? channel_key : sess->sess_key; + char reauth_key[CIFS_KEY_SIZE] = {}; + char *auth_key = conn->binding ? channel_key : + (work->session_setup_reauth ? reauth_key : sess->sess_key); u64 prev_sess_id; bool binding = conn->binding; int in_len, out_len; @@ -2101,7 +2161,7 @@ static int krb5_authenticate(struct ksmbd_work *work, if (retval) { ksmbd_debug(SMB, "krb5 authentication failed\n"); if (retval != -EKEYREJECTED) - retval = -EINVAL; + retval = -EPERM; goto out; } @@ -2117,12 +2177,21 @@ static int krb5_authenticate(struct ksmbd_work *work, * that it is reauthentication. And the user/password * has been verified, so return it here. */ - if (sess->state == SMB2_SESSION_VALID) { + if (sess->state == SMB2_SESSION_VALID && !work->session_setup_reauth) { if (conn->binding) goto binding_session; return 0; } + /* + * Reauthentication verifies the new Kerberos credentials but keeps + * the established SMB session keys. + */ + if (work->session_setup_reauth) { + retval = 0; + goto out; + } + if ((rsp->SessionFlags != SMB2_SESSION_FLAG_IS_GUEST_LE && (conn->sign || server_conf.enforced_signing)) || (req->SecurityMode & SMB2_NEGOTIATE_SIGNING_REQUIRED)) @@ -2160,6 +2229,7 @@ static int krb5_authenticate(struct ksmbd_work *work, } retval = 0; out: + memzero_explicit(reauth_key, sizeof(reauth_key)); if (binding) memzero_explicit(channel_key, sizeof(channel_key)); return retval; @@ -2318,8 +2388,13 @@ int smb2_sess_setup(struct ksmbd_work *work) } if (sess->state == SMB2_SESSION_EXPIRED) { - rc = -EFAULT; - goto out_err; + if (sess->kerberos_expiry && + ktime_get_real_seconds() >= sess->kerberos_expiry) { + work->session_setup_reauth = true; + } else { + rc = -EFAULT; + goto out_err; + } } if (ksmbd_conn_need_reconnect(conn)) { @@ -2363,10 +2438,8 @@ int smb2_sess_setup(struct ksmbd_work *work) if (conn->preferred_auth_mech & (KSMBD_AUTH_KRB5 | KSMBD_AUTH_MSKRB5)) { rc = krb5_authenticate(work, req, rsp); - if (rc) { - rc = -EINVAL; + if (rc) goto out_err; - } if (!ksmbd_conn_need_reconnect(conn)) { ksmbd_conn_set_good(conn); @@ -2477,6 +2550,7 @@ int smb2_sess_setup(struct ksmbd_work *work) */ if (!(req->Flags & SMB2_SESSION_REQ_FLAG_BINDING)) { sess->last_active = jiffies; + sess->kerberos_expiry = 0; sess->state = SMB2_SESSION_EXPIRED; } /* @@ -2824,6 +2898,7 @@ int smb2_session_logoff(struct ksmbd_work *work) } down_write(&conn->session_lock); + sess->kerberos_expiry = 0; sess->state = SMB2_SESSION_EXPIRED; up_write(&conn->session_lock); @@ -7404,7 +7479,6 @@ int smb2_close(struct ksmbd_work *work) u64 sess_id; struct smb2_close_req *req; struct smb2_close_rsp *rsp; - struct ksmbd_conn *conn = work->conn; struct ksmbd_file *fp; u64 time; int err = 0; @@ -7427,7 +7501,7 @@ int smb2_close(struct ksmbd_work *work) sess_id = work->compound_sid; work->compound_sid = 0; - if (check_session_id(conn, sess_id)) { + if (work->sess && work->sess->id == sess_id) { work->compound_sid = sess_id; } else { rsp->hdr.Status = STATUS_USER_SESSION_DELETED; @@ -8914,6 +8988,7 @@ int smb2_cancel(struct ksmbd_work *work) struct smb2_hdr *hdr = smb_get_msg(work->request_buf); struct smb2_hdr *chdr; struct ksmbd_work *iter; + struct ksmbd_work *cancelled_notify = NULL; struct list_head *command_list; if (work->next_smb2_rcv_hdr_off) @@ -8951,11 +9026,23 @@ int smb2_cancel(struct ksmbd_work *work) le64_to_cpu(hdr->Id.AsyncId), le16_to_cpu(chdr->Command)); iter->state = KSMBD_WORK_CANCELLED; - if (iter->cancel_fn) + if (iter->cancel_fn == smb2_notify_cancel_fn) + cancelled_notify = + smb2_notify_cancel_claim(iter->cancel_argv); + else if (iter->cancel_fn) iter->cancel_fn(iter->cancel_argv); break; } spin_unlock(&conn->request_lock); + + /* + * Complete a cancelled notify before this CANCEL handler returns. + * Deferring it to the system workqueue lets a following request and + * its response overtake STATUS_CANCELLED, leaving clients waiting + * for the original notify even though the cancellation was accepted. + */ + if (cancelled_notify) + smb2_complete_notify_cancel(cancelled_notify); } else { command_list = &conn->requests; @@ -10905,37 +10992,54 @@ int smb2_oplock_break(struct ksmbd_work *work) * it from here would self-deadlock the very thread processing the * client's CANCEL command. ksmbd_conn_write() can also sleep (it takes * conn's write mutex). So: do only the non-sleeping, no-relock cleanup - * inline here (the async_requests removal itself is safe without - * re-locking, since the caller already holds that lock), and defer the - * actual response send + work-struct free to a workqueue, matching the - * minimal, non-blocking style of the existing smb2_remove_blocked_lock() - * cancel_fn (which only wakes a waiter, never sends network data itself). + * inline here. smb2_cancel() sends and frees the claimed notify after it + * drops request_lock, preserving response order for a client CANCEL. The + * connection teardown caller has no such post-unlock path, so its wrapper + * defers the send and free to a workqueue. */ struct notify_cancel_ctx { struct work_struct work; struct ksmbd_work *in_work; }; +static void smb2_send_notify_cancelled(struct ksmbd_work *work) +{ + struct smb2_hdr *hdr = smb_get_msg(work->response_buf); + struct ksmbd_conn *conn = work->conn; + struct ksmbd_session *sess; + + sess = ksmbd_session_lookup(conn, le64_to_cpu(hdr->SessionId)); + if (sess) { + work->sess = sess; + if (work->encrypted && sess->enc && conn->ops->encrypt_resp) { + conn->ops->encrypt_resp(work); + } else if (conn->ops->is_sign_req && conn->ops->set_sign_rsp && + conn->ops->is_sign_req(work, + conn->ops->get_cmd_val(work))) { + conn->ops->set_sign_rsp(work); + } + } + + ksmbd_conn_write(work); + if (sess) { + ksmbd_user_session_put(sess); + work->sess = NULL; + } +} + static void smb2_notify_cancel_deferred(struct work_struct *w) { struct notify_cancel_ctx *ctx = container_of(w, struct notify_cancel_ctx, work); - struct ksmbd_work *in_work = ctx->in_work; - struct smb2_hdr *in_hdr; - in_hdr = smb_get_msg(in_work->response_buf); - in_hdr->Status = STATUS_CANCELLED; - ksmbd_conn_write(in_work); - ksmbd_free_work_struct(in_work); + smb2_complete_notify_cancel(ctx->in_work); kfree(ctx); } -static void smb2_notify_cancel_fn(void **argv) +static struct ksmbd_work *smb2_notify_cancel_claim(void **argv) { struct ksmbd_work *in_work = (struct ksmbd_work *)argv[0]; struct ksmbd_file *fp = (struct ksmbd_file *)argv[1]; - struct ksmbd_conn *conn = in_work->conn; - struct notify_cancel_ctx *ctx; bool claimed; spin_lock(&fp->f_lock); @@ -10945,22 +11049,44 @@ static void smb2_notify_cancel_fn(void **argv) spin_unlock(&fp->f_lock); if (!claimed) - return; + return NULL; - /* conn->request_lock is already held by the caller (smb2_cancel()). */ - list_del_init(&in_work->async_request_entry); - in_work->asynchronous = false; + /* conn->request_lock is held by smb2_cancel() or connection teardown. */ in_work->cancel_fn = NULL; kfree(in_work->cancel_argv); in_work->cancel_argv = NULL; - if (in_work->async_id) { - ksmbd_release_id(&conn->async_ida, in_work->async_id); - in_work->async_id = 0; - } + return in_work; +} + +static void smb2_complete_notify_cancel(struct ksmbd_work *in_work) +{ + struct smb2_hdr *in_hdr = smb_get_msg(in_work->response_buf); + + in_hdr->Status = STATUS_CANCELLED; + smb2_send_notify_cancelled(in_work); + release_async_work(in_work); + ksmbd_free_work_struct(in_work); +} + +static void smb2_notify_cancel_fn(void **argv) +{ + struct ksmbd_work *in_work = smb2_notify_cancel_claim(argv); + struct ksmbd_conn *conn; + struct notify_cancel_ctx *ctx; + + if (!in_work) + return; + conn = in_work->conn; ctx = kmalloc(sizeof(*ctx), GFP_ATOMIC); if (!ctx) { /* Can't defer the response -- free without sending one. */ + list_del_init(&in_work->async_request_entry); + in_work->asynchronous = false; + if (in_work->async_id) { + ksmbd_release_id(&conn->async_ida, in_work->async_id); + in_work->async_id = 0; + } ksmbd_free_work_struct(in_work); return; } @@ -11081,6 +11207,8 @@ int smb2_notify(struct ksmbd_work *work) smb2_set_err_rsp(work); return 0; } + memcpy(smb_get_msg(in_work->request_buf), req, + __SMB2_HEADER_STRUCTURE_SIZE); if (setup_async_work(work, NULL, NULL)) { ksmbd_free_work_struct(in_work); @@ -11095,6 +11223,7 @@ int smb2_notify(struct ksmbd_work *work) /* Keep the async IDA alive until the deferred work is released. */ in_work->conn = ksmbd_conn_get(work->conn); in_work->owns_conn_ref = true; + in_work->encrypted = work->encrypted; in_hdr = smb_get_msg(in_work->response_buf); memcpy(in_hdr, ksmbd_resp_buf_next(work), __SMB2_HEADER_STRUCTURE_SIZE); in_hdr->Flags |= SMB2_FLAGS_ASYNC_COMMAND; @@ -11460,7 +11589,6 @@ bool smb3_is_transform_hdr(void *buf) int smb3_decrypt_req(struct ksmbd_work *work) { - struct ksmbd_session *sess; char *buf = work->request_buf; unsigned int pdu_length = get_rfc1002_len(buf); struct kvec iov[2]; @@ -11480,14 +11608,6 @@ int smb3_decrypt_req(struct ksmbd_work *work) return -ECONNABORTED; } - sess = ksmbd_session_lookup_all(work->conn, le64_to_cpu(tr_hdr->SessionId)); - if (!sess) { - pr_err("invalid session id(%llx) in transform header\n", - le64_to_cpu(tr_hdr->SessionId)); - return -ECONNABORTED; - } - ksmbd_user_session_put(sess); - iov[0].iov_base = buf; iov[0].iov_len = sizeof(struct smb2_transform_hdr) + 4; iov[1].iov_base = buf + sizeof(struct smb2_transform_hdr) + 4; From f39ec312c3eb5ae1f6b4d1c5d8c556d844a20c0c Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Sun, 26 Jul 2026 09:01:08 +0000 Subject: [PATCH 091/142] smb/server: fix unbuffered file position alignment check FILE_NO_INTERMEDIATE_BUFFERING is a CreateOptions flag and can be combined with other flags, such as FILE_NON_DIRECTORY_FILE. Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- 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 77c9e027d6aa..bc7d4e62c330 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -8011,7 +8011,7 @@ static int set_file_position_info(struct ksmbd_file *fp, sector_size = inode->i_sb->s_blocksize; if (current_byte_offset < 0 || - (fp->coption == FILE_NO_INTERMEDIATE_BUFFERING_LE && + (fp->coption & FILE_NO_INTERMEDIATE_BUFFERING_LE && current_byte_offset & (sector_size - 1))) { pr_err("CurrentByteOffset is not valid : %llu\n", current_byte_offset); From b0148dc5625dbfd50596ac63c7487c12e8a8ab03 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 28 Jul 2026 22:17:14 +0900 Subject: [PATCH 092/142] ksmbd: serialize oplock close with pending break ownership close may abort an in-flight oplock break while another breaker already holds an opinfo reference. Releasing pending_break wakes that waiter, but without serializing the close transition with bit acquisition it can become a new break owner through the test_and_set_bit() fast path. It can then overwrite OPLOCK_CLOSING with OPLOCK_ACK_WAIT and continue a break for a dying opinfo. Make OPLOCK_CLOSING terminal once the opinfo is removed from the inode list. Serialize that transition, pending_break acquisition, and OPLOCK_ACK_WAIT setup with an opinfo state lock. A breaker which loses the race releases its ownership and returns -ENOENT. Explicitly wake pending_break waiters during close so they can observe the terminal state. Also prevent ACK and timeout paths from replacing OPLOCK_CLOSING with OPLOCK_STATE_NONE. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Co-developed-by: Yunseong Kim Signed-off-by: Yunseong Kim Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 86 ++++++++++++++++++++++++++++++++++------- fs/smb/server/oplock.h | 1 + fs/smb/server/smb2pdu.c | 12 ++++-- 3 files changed, 81 insertions(+), 18 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 591b2fca1d4e..b1cec9a4291b 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -90,6 +90,7 @@ static struct oplock_info *alloc_opinfo(struct ksmbd_work *work, opinfo->conn = ksmbd_conn_get(work->conn); opinfo->level = SMB2_OPLOCK_LEVEL_NONE; opinfo->op_state = OPLOCK_STATE_NONE; + spin_lock_init(&opinfo->state_lock); opinfo->pending_break = 0; opinfo->fid = id; opinfo->Tid = Tid; @@ -546,14 +547,23 @@ void close_id_del_oplock(struct ksmbd_file *fp) opinfo_del(opinfo); rcu_assign_pointer(fp->f_opinfo, NULL); - if (opinfo->op_state == OPLOCK_ACK_WAIT) { - opinfo->op_state = OPLOCK_CLOSING; - wake_up_interruptible_all(&opinfo->oplock_q); - if (opinfo->is_lease) { - atomic_set(&opinfo->breaking_cnt, 0); - wake_up_interruptible_all(&opinfo->oplock_brk); - } - } + spin_lock(&opinfo->state_lock); + if (opinfo->op_state == OPLOCK_ACK_WAIT && opinfo->is_lease) + atomic_set(&opinfo->breaking_cnt, 0); + /* + * An opinfo that has been removed from the inode list is terminal. Keep + * this transition and releasing pending_break under state_lock. a breaker + * takes the same lock before it acquires pending_break or sets ACK_WAIT. + */ + opinfo->op_state = OPLOCK_CLOSING; + clear_bit_unlock(0, &opinfo->pending_break); + spin_unlock(&opinfo->state_lock); + wake_up_interruptible_all(&opinfo->oplock_q); + if (opinfo->is_lease) + wake_up_interruptible_all(&opinfo->oplock_brk); + /* memory barrier is needed for wake_up_bit() */ + smp_mb__after_atomic(); + wake_up_bit(&opinfo->pending_break, 0); opinfo_count_dec(fp); atomic_dec(&opinfo->refcount); @@ -735,12 +745,18 @@ static bool wait_for_break_ack(struct oplock_info *opinfo) /* is this a timeout ? */ if (!rc) { + spin_lock(&opinfo->state_lock); + if (opinfo->op_state == OPLOCK_CLOSING) { + spin_unlock(&opinfo->state_lock); + return false; + } if (opinfo->is_lease) { opinfo->o_lease->state = SMB2_LEASE_NONE_LE; lease_update_oplock_levels(opinfo->o_lease); } opinfo->level = SMB2_OPLOCK_LEVEL_NONE; opinfo->op_state = OPLOCK_STATE_NONE; + spin_unlock(&opinfo->state_lock); return true; } @@ -755,9 +771,35 @@ static void wake_up_oplock_break(struct oplock_info *opinfo) wake_up_bit(&opinfo->pending_break, 0); } +static bool oplock_break_set_ack_wait(struct oplock_info *opinfo) +{ + bool ret = false; + + spin_lock(&opinfo->state_lock); + if (opinfo->op_state != OPLOCK_CLOSING) { + opinfo->op_state = OPLOCK_ACK_WAIT; + ret = true; + } + spin_unlock(&opinfo->state_lock); + + return ret; +} + static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level) { - while (test_and_set_bit(0, &opinfo->pending_break)) { + for (;;) { + bool closing; + + spin_lock(&opinfo->state_lock); + closing = opinfo->op_state == OPLOCK_CLOSING; + if (!closing && !test_and_set_bit(0, &opinfo->pending_break)) { + spin_unlock(&opinfo->state_lock); + break; + } + spin_unlock(&opinfo->state_lock); + if (closing) + return -ENOENT; + if (opinfo->is_lease) opinfo->o_lease->reuse_epoch = true; @@ -766,9 +808,12 @@ static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level) /* Not immediately break to none. */ opinfo->open_trunc = 0; - if (opinfo->op_state == OPLOCK_CLOSING) + spin_lock(&opinfo->state_lock); + closing = opinfo->op_state == OPLOCK_CLOSING; + spin_unlock(&opinfo->state_lock); + if (closing) return -ENOENT; - else if (opinfo->level <= req_op_level) { + if (opinfo->level <= req_op_level) { if (opinfo->is_lease == false) return 1; @@ -1146,7 +1191,11 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE)) { - brk_opinfo->op_state = OPLOCK_ACK_WAIT; + if (!oplock_break_set_ack_wait(brk_opinfo)) { + atomic_dec_if_positive(&brk_opinfo->breaking_cnt); + wake_up_oplock_break(brk_opinfo); + return -ENOENT; + } } else atomic_dec(&brk_opinfo->breaking_cnt); @@ -1201,8 +1250,12 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, return err < 0 ? err : 0; if (brk_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH || - brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) - brk_opinfo->op_state = OPLOCK_ACK_WAIT; + brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) { + if (!oplock_break_set_ack_wait(brk_opinfo)) { + wake_up_oplock_break(brk_opinfo); + return -ENOENT; + } + } /* * Keep a conflicting CREATE asynchronous while waiting for an @@ -1776,7 +1829,10 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work, if (!brk_op->is_lease && !send_oplock_break) { brk_op->level = SMB2_OPLOCK_LEVEL_NONE; - brk_op->op_state = OPLOCK_STATE_NONE; + spin_lock(&brk_op->state_lock); + if (brk_op->op_state != OPLOCK_CLOSING) + brk_op->op_state = OPLOCK_STATE_NONE; + spin_unlock(&brk_op->state_lock); } else { oplock_break(brk_op, brk_op->is_lease && !is_trunc ? diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index 23274b645ede..b08d21758e07 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -66,6 +66,7 @@ struct oplock_info { struct ksmbd_file *o_fp; int level; int op_state; + spinlock_t state_lock; unsigned long pending_break; u64 fid; atomic_t breaking_cnt; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index bc7d4e62c330..3174f179c3d4 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10824,7 +10824,10 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) smb2_set_err_rsp(work); out: - opinfo->op_state = OPLOCK_STATE_NONE; + spin_lock(&opinfo->state_lock); + if (opinfo->op_state != OPLOCK_CLOSING) + opinfo->op_state = OPLOCK_STATE_NONE; + spin_unlock(&opinfo->state_lock); wake_up_interruptible_all(&opinfo->oplock_q); out_no_state_change: opinfo_put(opinfo); @@ -10915,9 +10918,12 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) if (ret) goto err_out; - opinfo->op_state = OPLOCK_STATE_NONE; + spin_lock(&opinfo->state_lock); + if (opinfo->op_state != OPLOCK_CLOSING) + opinfo->op_state = OPLOCK_STATE_NONE; + spin_unlock(&opinfo->state_lock); wake_up_interruptible_all(&opinfo->oplock_q); - atomic_dec(&opinfo->breaking_cnt); + atomic_dec_if_positive(&opinfo->breaking_cnt); wake_up_interruptible_all(&opinfo->oplock_brk); opinfo_put(opinfo); return; From 54d90311f9b4eb23f3b0b62650b0cfddb11a12ef Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 29 Jul 2026 21:24:11 +0900 Subject: [PATCH 093/142] ksmbd: fix SMB2 byte-range lock end offset SMB2 describes a byte-range lock using an offset and a length, while Linux file_lock uses an inclusive end offset. smb2_lock() currently sets fl_end to start + length and consequently locks one extra byte for every nonzero-length request. Translate nonzero lengths to start + length - 1 and reject ranges that cannot be represented by loff_t instead of silently truncating them at OFFSET_MAX. Track zero-length locks from the request length so one-byte ranges are not mistaken for zero-length locks after endpoint conversion. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 44 +++++++++++++---------------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 3174f179c3d4..8dca4aab05c6 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9131,7 +9131,7 @@ static int smb2_set_flock_flags(struct file_lock *flock, int flags) } static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock, - unsigned int cmd, int flags, + unsigned int cmd, int flags, bool zero_len, struct list_head *lock_list) { struct ksmbd_lock *lock; @@ -9145,8 +9145,7 @@ static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock, lock->start = flock->fl_start; lock->end = flock->fl_end; lock->flags = flags; - if (lock->start == lock->end) - lock->zero_len = 1; + lock->zero_len = zero_len; INIT_LIST_HEAD(&lock->clist); INIT_LIST_HEAD(&lock->flist); INIT_LIST_HEAD(&lock->llist); @@ -9255,32 +9254,18 @@ int smb2_lock(struct ksmbd_work *work) lock_start = le64_to_cpu(lock_ele[i].Offset); lock_length = le64_to_cpu(lock_ele[i].Length); - if (lock_start > U64_MAX - lock_length) { + if (lock_start > OFFSET_MAX || + (lock_length && + lock_length - 1 > OFFSET_MAX - lock_start)) { pr_err("Invalid lock range requested\n"); rsp->hdr.Status = STATUS_INVALID_LOCK_RANGE; locks_free_lock(flock); goto out; } - if (lock_start > OFFSET_MAX) - flock->fl_start = OFFSET_MAX; - else - flock->fl_start = lock_start; - - lock_length = le64_to_cpu(lock_ele[i].Length); - if (lock_length > OFFSET_MAX - flock->fl_start) - lock_length = OFFSET_MAX - flock->fl_start; - - flock->fl_end = flock->fl_start + lock_length; - - if (flock->fl_end < flock->fl_start) { - ksmbd_debug(SMB, - "the end offset(%llx) is smaller than the start offset(%llx)\n", - flock->fl_end, flock->fl_start); - rsp->hdr.Status = STATUS_INVALID_LOCK_RANGE; - locks_free_lock(flock); - goto out; - } + flock->fl_start = lock_start; + flock->fl_end = lock_length ? + flock->fl_start + lock_length - 1 : flock->fl_start; /* Check conflict locks in one request */ list_for_each_entry(cmp_lock, &lock_list, llist) { @@ -9296,7 +9281,8 @@ int smb2_lock(struct ksmbd_work *work) } } - smb_lock = smb2_lock_init(flock, cmd, flags, &lock_list); + smb_lock = smb2_lock_init(flock, cmd, flags, !lock_length, + &lock_list); if (!smb_lock) { err = -EINVAL; locks_free_lock(flock); @@ -9370,7 +9356,7 @@ int smb2_lock(struct ksmbd_work *work) /* check zero byte lock range */ if (cmp_lock->zero_len && !smb_lock->zero_len && cmp_lock->start > smb_lock->start && - cmp_lock->start < smb_lock->end) { + cmp_lock->start <= smb_lock->end) { spin_unlock(&conn->llist_lock); up_read(&conn_list_lock); pr_err("previous lock conflict with zero byte lock range\n"); @@ -9379,17 +9365,15 @@ int smb2_lock(struct ksmbd_work *work) if (smb_lock->zero_len && !cmp_lock->zero_len && smb_lock->start > cmp_lock->start && - smb_lock->start < cmp_lock->end) { + smb_lock->start <= cmp_lock->end) { spin_unlock(&conn->llist_lock); up_read(&conn_list_lock); pr_err("current lock conflict with zero byte lock range\n"); goto out; } - if (((cmp_lock->start <= smb_lock->start && - cmp_lock->end > smb_lock->start) || - (cmp_lock->start < smb_lock->end && - cmp_lock->end >= smb_lock->end)) && + if (cmp_lock->start <= smb_lock->end && + smb_lock->start <= cmp_lock->end && !cmp_lock->zero_len && !smb_lock->zero_len) { spin_unlock(&conn->llist_lock); up_read(&conn_list_lock); From 5ce5227cd60b7213359e91837727ed1b6d457d49 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 29 Jul 2026 21:24:54 +0900 Subject: [PATCH 094/142] ksmbd: recognize replayed SMB2 lock sequences A server returns success without processing a lock request when a valid LockSequenceArray entry contains the same sequence number. The current verifier only invalidates mismatched entries, so matching requests are submitted to the VFS again and recorded as duplicate locks. Make the verifier report matching sequences and skip lock processing for those replays. Also correct the field comment to describe the sequence and index bit layout used by the implementation and the protocol. Use the capabilities advertised by the server when deciding whether lock sequence verification applies to a multichannel connection. Signed-off-by: Namjae Jeon --- fs/smb/common/smb2pdu.h | 4 ++-- fs/smb/server/smb2pdu.c | 34 +++++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/fs/smb/common/smb2pdu.h b/fs/smb/common/smb2pdu.h index 6826f7bed1a4..c1414a1ffe30 100644 --- a/fs/smb/common/smb2pdu.h +++ b/fs/smb/common/smb2pdu.h @@ -847,8 +847,8 @@ struct smb2_lock_req { __le16 StructureSize; /* Must be 48 */ __le16 LockCount; /* - * The least significant four bits are the index, the other 28 bits are - * the lock sequence number (0 to 64). See MS-SMB2 2.2.26 + * The least significant four bits are the lock sequence number. The + * other 28 bits are the index (0 to 64). See MS-SMB2 2.2.26. */ __le32 LockSequenceNumber; __u64 PersistentFileId; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 8dca4aab05c6..d334f8a8807a 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -787,31 +787,38 @@ static bool smb2_lock_sequence_applicable(struct ksmbd_work *work, { return fp->is_resilient || fp->is_durable || fp->is_persistent || (work->conn->dialect >= SMB30_PROT_ID && - (work->conn->cli_cap & SMB2_GLOBAL_CAP_MULTI_CHANNEL)); + (work->conn->vals->req_capabilities & + SMB2_GLOBAL_CAP_MULTI_CHANNEL)); } -static void smb2_verify_lock_sequence(struct ksmbd_work *work, - struct ksmbd_file *fp, - struct smb2_lock_req *req) +static bool smb2_verify_lock_sequence(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct smb2_lock_req *req) { u32 val, index; u8 sequence; + bool replay = false; if (work->conn->dialect == SMB20_PROT_ID || !smb2_lock_sequence_applicable(work, fp)) - return; + return false; val = le32_to_cpu(req->LockSequenceNumber); sequence = val & 0xf; index = val >> 4; if (!index || index > KSMBD_LOCK_SEQ_ARRAY_SIZE) - return; + return false; spin_lock(&fp->f_lock); - if (fp->lock_seq[index - 1].valid && - fp->lock_seq[index - 1].sequence != sequence) - fp->lock_seq[index - 1].valid = false; + if (fp->lock_seq[index - 1].valid) { + if (fp->lock_seq[index - 1].sequence == sequence) + replay = true; + else + fp->lock_seq[index - 1].valid = false; + } spin_unlock(&fp->f_lock); + + return replay; } static void smb2_update_lock_sequence(struct ksmbd_work *work, @@ -9194,6 +9201,7 @@ int smb2_lock(struct ksmbd_work *work) LIST_HEAD(rollback_list); int prior_lock = 0, bkt; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; + bool lock_replayed; WORK_BUFFERS(work, req, rsp); @@ -9226,7 +9234,9 @@ int smb2_lock(struct ksmbd_work *work) if (err) goto out2; - smb2_verify_lock_sequence(work, fp, req); + lock_replayed = smb2_verify_lock_sequence(work, fp, req); + if (lock_replayed) + goto lock_success; filp = fp->filp; lock_count = le16_to_cpu(req->LockCount); @@ -9493,6 +9503,7 @@ int smb2_lock(struct ksmbd_work *work) if (atomic_read(&fp->f_ci->op_count) > 1) smb_break_all_oplock(work, fp); +lock_success: rsp->StructureSize = cpu_to_le16(4); ksmbd_debug(SMB, "successful in taking lock\n"); rsp->hdr.Status = STATUS_SUCCESS; @@ -9500,7 +9511,8 @@ int smb2_lock(struct ksmbd_work *work) err = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_lock_rsp)); if (err) goto out; - smb2_update_lock_sequence(work, fp, req); + if (!lock_replayed) + smb2_update_lock_sequence(work, fp, req); ksmbd_fd_put(work, fp); return 0; From 054bcca4cd9f00719b01f7108b51a2168fb94f15 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 29 Jul 2026 21:25:35 +0900 Subject: [PATCH 095/142] ksmbd: safely discard unregistered deferred locks When vfs_lock_file() defers a lock, smb2_lock() puts its ksmbd_lock on rollback_list before allocating and registering the asynchronous work. If either operation fails, rollback assumes that smb_lock->conn is initialized and dereferences NULL. The deferred file_lock also remains linked into the VFS blocked-lock state while it is freed. Keep the lock off rollback_list until async setup succeeds. On setup failures, explicitly unblock and wake the deferred lock before freeing it and its ksmbd wrapper. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index d334f8a8807a..e3ab66dcb92b 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9169,6 +9169,13 @@ static void smb2_remove_blocked_lock(void **argv) locks_wake_up(flock); } +static void smb2_free_blocked_lock(struct file_lock *flock) +{ + ksmbd_vfs_posix_lock_unblock(flock); + locks_wake_up(flock); + locks_free_lock(flock); +} + static inline bool lock_defer_pending(struct file_lock *fl) { /* check pending lock waiters */ @@ -9428,11 +9435,12 @@ int smb2_lock(struct ksmbd_work *work) ksmbd_debug(SMB, "would have to wait for getting lock\n"); - list_add(&smb_lock->llist, &rollback_list); argv = kmalloc(sizeof(void *), KSMBD_DEFAULT_GFP); if (!argv) { err = -ENOMEM; + smb2_free_blocked_lock(flock); + kfree(smb_lock); goto out; } argv[0] = flock; @@ -9443,8 +9451,11 @@ int smb2_lock(struct ksmbd_work *work) if (rc) { kfree(argv); err = -ENOMEM; + smb2_free_blocked_lock(flock); + kfree(smb_lock); goto out; } + list_add(&smb_lock->llist, &rollback_list); spin_lock(&fp->f_lock); list_add(&work->fp_entry, &fp->blocked_works); spin_unlock(&fp->f_lock); From 16a7f7c2ecf3c893b65f0fb78fa7a7171ae0ba9e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 29 Jul 2026 21:26:26 +0900 Subject: [PATCH 096/142] ksmbd: reject blocking compound lock requests Clients set SMB2_LOCKFLAG_FAIL_IMMEDIATELY when a LOCK request contains multiple lock elements, and servers reject requests that omit it. Accepting such a request can leave earlier elements locked while a later element waits asynchronously, enabling prolonged partial lock ownership and avoidable deadlocks. Return STATUS_INVALID_PARAMETER before processing any element when a multi-element lock request contains a blocking lock. Unlock arrays remain unaffected. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index e3ab66dcb92b..4bde5ab2c881 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9308,6 +9308,13 @@ int smb2_lock(struct ksmbd_work *work) } list_for_each_entry_safe(smb_lock, tmp, &lock_list, llist) { + if (lock_count > 1 && + !(le32_to_cpu(lock_ele[0].Flags) & SMB2_LOCKFLAG_UNLOCK) && + !(smb_lock->flags & SMB2_LOCKFLAG_FAIL_IMMEDIATELY)) { + err = -EINVAL; + goto out; + } + if (smb_lock->cmd < 0) { err = -EINVAL; goto out; From 6eac877e0ea53b82fe726ba80bb1a349bd0b592b Mon Sep 17 00:00:00 2001 From: Rosen Penev Date: Tue, 28 Jul 2026 14:27:59 -0700 Subject: [PATCH 097/142] ksmbd: remove extra byte from ipc_msg_alloc() size calculations Three ipc_msg_alloc() calls in transport_ipc.c allocate sizeof(struct) + payload_len + 1, but the extra byte is unnecessary. The payload data is binary and copied with memcpy() to the exact size; no null terminator is needed. This was present in the original commit that introduced the file, where the structs already used [0] zero-length arrays, so the +1 was never correct. Assisted-by: Opencode:Big-Pickle Signed-off-by: Rosen Penev Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/transport_ipc.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/transport_ipc.c b/fs/smb/server/transport_ipc.c index 2584b162415b..4b0b572a3e1b 100644 --- a/fs/smb/server/transport_ipc.c +++ b/fs/smb/server/transport_ipc.c @@ -679,7 +679,7 @@ ksmbd_ipc_spnego_authen_request(const char *spnego_blob, int blob_len) return NULL; msg = ipc_msg_alloc(sizeof(struct ksmbd_spnego_authen_request) + - blob_len + 1); + blob_len); if (!msg) return NULL; @@ -860,7 +860,7 @@ struct ksmbd_rpc_command *ksmbd_rpc_write(struct ksmbd_session *sess, int handle if (payload_sz > KSMBD_IPC_MAX_PAYLOAD) return NULL; - msg = ipc_msg_alloc(sizeof(struct ksmbd_rpc_command) + payload_sz + 1); + msg = ipc_msg_alloc(sizeof(struct ksmbd_rpc_command) + payload_sz); if (!msg) return NULL; @@ -919,7 +919,7 @@ struct ksmbd_rpc_command *ksmbd_rpc_ioctl(struct ksmbd_session *sess, int handle if (payload_sz > KSMBD_IPC_MAX_PAYLOAD) return NULL; - msg = ipc_msg_alloc(sizeof(struct ksmbd_rpc_command) + payload_sz + 1); + msg = ipc_msg_alloc(sizeof(struct ksmbd_rpc_command) + payload_sz); if (!msg) return NULL; From d2ccf905f47d2344270749f4dfa905afcd3edeb0 Mon Sep 17 00:00:00 2001 From: ZhangGuoDong Date: Fri, 31 Jul 2026 11:50:03 +0000 Subject: [PATCH 098/142] smb/server: fix null-ptr-deref in ksmbd_ipc_tree_connect_request() See the procedure below: ksmbd_tree_conn_connect ksmbd_share_config_get share->name = kstrdup() // fail if (!test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) // false // do not check `share->name` ksmbd_ipc_tree_connect_request strlen(share->name) // null-ptr-deref Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Signed-off-by: ZhangGuoDong Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/share_config.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/mgmt/share_config.c b/fs/smb/server/mgmt/share_config.c index 1cb58bec0903..53d6f71dd871 100644 --- a/fs/smb/server/mgmt/share_config.c +++ b/fs/smb/server/mgmt/share_config.c @@ -215,6 +215,11 @@ static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work, ksmbd_share_tree_conn_init(share); INIT_LIST_HEAD(&share->veto_list); share->name = kstrdup(name, KSMBD_DEFAULT_GFP); + if (!share->name) { + kill_share(share); + share = NULL; + goto out; + } if (!test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) { int path_len = PATH_MAX; @@ -260,7 +265,7 @@ static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work, share->path = NULL; } } - if (ret || !share->name) { + if (ret) { kill_share(share); share = NULL; goto out; From 3ce2f9491963c9c9c02129deaf7e8a0809775e97 Mon Sep 17 00:00:00 2001 From: ZhangGuoDong Date: Fri, 31 Jul 2026 11:50:04 +0000 Subject: [PATCH 099/142] smb/server: fix memory leak in ksmbd_vfs_set_durable_owner() See the procedure below: smb2_open ksmbd_vfs_set_durable_owner fp->owner.name = name // When the connection goes away ksmbd_sessions_deregister ksmbd_session_destroy ksmbd_destroy_file_table __close_file_table_ids session_fd_check // skip() ksmbd_vfs_set_durable_owner fp->owner.name = name // memory leak Signed-off-by: ZhangGuoDong Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/vfs_cache.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index c28e3d65d64b..5acd06020d42 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1655,7 +1655,7 @@ void ksmbd_stop_durable_scavenger(void) int ksmbd_vfs_set_durable_owner(struct ksmbd_file *fp, struct ksmbd_user *user) { - char *name; + char *name, *old_name; if (!user) return -EINVAL; @@ -1666,10 +1666,12 @@ int ksmbd_vfs_set_durable_owner(struct ksmbd_file *fp, return -ENOMEM; spin_lock(&fp->f_lock); + old_name = fp->owner.name; fp->owner.uid = user->uid; fp->owner.gid = user->gid; fp->owner.name = name; spin_unlock(&fp->f_lock); + kfree(old_name); return 0; } From bef46b604732d83f8da29f782868de4d25bf972c Mon Sep 17 00:00:00 2001 From: ZhangGuoDong Date: Fri, 31 Jul 2026 11:50:05 +0000 Subject: [PATCH 100/142] smb/server: fix invalid pointer dereference in ksmbd_stop_durable_scavenger() See the procedure below: ksmbd_launch_ksmbd_durable_scavenger durable_scavenger_running = true server_conf.dh_task = kthread_run() // fail, dh_task is an ERR_PTR() server_ctrl_handle_reset ksmbd_stop_durable_scavenger kthread_stop(server_conf.dh_task) // invalid pointer Fixes: d484d621d40f ("ksmbd: add durable scavenger timer") Signed-off-by: ZhangGuoDong Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/vfs_cache.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 5acd06020d42..a35df2ab59c9 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1618,9 +1618,12 @@ void ksmbd_launch_ksmbd_durable_scavenger(void) server_conf.dh_task = kthread_run(ksmbd_durable_scavenger, (void *)NULL, "ksmbd-durable-scavenger"); - if (IS_ERR(server_conf.dh_task)) + if (IS_ERR(server_conf.dh_task)) { pr_err("cannot start conn thread, err : %ld\n", PTR_ERR(server_conf.dh_task)); + server_conf.dh_task = NULL; + durable_scavenger_running = false; + } mutex_unlock(&durable_scavenger_lock); } From db97f3763727d652112ae70038d4e17b3ce277bb Mon Sep 17 00:00:00 2001 From: ZhangGuoDong Date: Fri, 31 Jul 2026 11:50:06 +0000 Subject: [PATCH 101/142] smb/server: abort initialization when proc setup fails ksmbd_server_init() calls ksmbd_proc_init() before creating the remaining proc entries and server subsystems. ksmbd_proc_init() tears down partial state on a procfs or percpu_counter allocation failure, but returns void, so ksmbd_server_init() continues as if the counters were usable. Once userspace starts the server, server_ctrl_handle_init() calls ksmbd_proc_reset(), which reaches percpu_counter_set() with a NULL per-CPU counters pointer on SMP systems. The later ksmbd_proc_create() calls also receive a NULL parent and may create entries in the /proc root; ksmbd_proc_cleanup() cannot remove those entries because ksmbd_proc_fs is NULL. Fixes: b38f99c1217a ("ksmbd: add procfs interface for runtime monitoring and statistics") Signed-off-by: ZhangGuoDong Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/misc.h | 4 ++-- fs/smb/server/proc.c | 13 ++++++++----- fs/smb/server/server.c | 4 +++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/misc.h b/fs/smb/server/misc.h index 680375a966c5..1faaddd0f5f7 100644 --- a/fs/smb/server/misc.h +++ b/fs/smb/server/misc.h @@ -43,7 +43,7 @@ struct ksmbd_const_name { const char *name; }; -void ksmbd_proc_init(void); +int ksmbd_proc_init(void); void ksmbd_proc_cleanup(void); void ksmbd_proc_reset(void); struct proc_dir_entry *ksmbd_proc_create(const char *name, @@ -56,7 +56,7 @@ void ksmbd_proc_show_flag_names(struct seq_file *m, const char *ksmbd_proc_const_name(const struct ksmbd_const_name *table, int count, unsigned int const_value); #else -static inline void ksmbd_proc_init(void) {} +static inline int ksmbd_proc_init(void) { return 0; } static inline void ksmbd_proc_cleanup(void) {} static inline void ksmbd_proc_reset(void) {} #endif diff --git a/fs/smb/server/proc.c b/fs/smb/server/proc.c index 1bf4e00dee34..826353ed0553 100644 --- a/fs/smb/server/proc.c +++ b/fs/smb/server/proc.c @@ -239,14 +239,14 @@ void ksmbd_proc_reset(void) percpu_counter_set(&ksmbd_counters.counters[i], 0); } -void ksmbd_proc_init(void) +int ksmbd_proc_init(void) { int i; - int retval; + int retval = -ENOMEM; ksmbd_proc_fs = proc_mkdir("fs/ksmbd", NULL); if (!ksmbd_proc_fs) - return; + return retval; if (!proc_mkdir_mode("sessions", 0400, ksmbd_proc_fs)) goto err_out; @@ -257,11 +257,14 @@ void ksmbd_proc_init(void) goto err_out; } - if (!ksmbd_proc_create("server", proc_show_ksmbd_stats, NULL)) + if (!ksmbd_proc_create("server", proc_show_ksmbd_stats, NULL)) { + retval = -ENOMEM; goto err_out; + } ksmbd_proc_reset(); - return; + return 0; err_out: ksmbd_proc_cleanup(); + return retval; } diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 18c20a669307..19630ac53235 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -620,7 +620,9 @@ static int __init ksmbd_server_init(void) return ret; } - ksmbd_proc_init(); + ret = ksmbd_proc_init(); + if (ret) + goto err_unregister; create_proc_sessions(); create_proc_shares(); From f43cbe3b58ce989ce420c3bc875d48e7754c8aba Mon Sep 17 00:00:00 2001 From: ZhangGuoDong Date: Fri, 31 Jul 2026 11:50:07 +0000 Subject: [PATCH 102/142] smb/server: call ksmbd_proc_cleanup() on module init failure When a later initializer fails, the unwind chain releases resources created after procfs and then jumps directly to class_unregister(). Returning an error from module_init() leaves the proc tree and its per-CPU counters allocated. Fixes: b38f99c1217a ("ksmbd: add procfs interface for runtime monitoring and statistics") Signed-off-by: ZhangGuoDong Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/server.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 19630ac53235..0ccd123ba418 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -630,11 +630,11 @@ static int __init ksmbd_server_init(void) ret = server_conf_init(); if (ret) - goto err_unregister; + goto err_proc_cleanup; ret = ksmbd_work_pool_init(); if (ret) - goto err_unregister; + goto err_proc_cleanup; ret = ksmbd_init_file_cache(); if (ret) @@ -680,6 +680,8 @@ static int __init ksmbd_server_init(void) ksmbd_exit_file_cache(); err_destroy_work_pools: ksmbd_work_pool_destroy(); +err_proc_cleanup: + ksmbd_proc_cleanup(); err_unregister: class_unregister(&ksmbd_control_class); From 73541bd2bab77e7e8e89b1edb5d342f4190dd4d0 Mon Sep 17 00:00:00 2001 From: ZhangGuoDong Date: Fri, 31 Jul 2026 11:50:08 +0000 Subject: [PATCH 103/142] smb/server: preserve error status in smb2_handle_negotiate() smb2_handle_negotiate() records specific failures such as STATUS_INVALID_PARAMETER or STATUS_NOT_SUPPORTED. Fixes: e2b76ab8b5c9 ("ksmbd: add support for read compound") Signed-off-by: ZhangGuoDong Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 4bde5ab2c881..9b64e28b5d56 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1722,7 +1722,7 @@ int smb2_handle_negotiate(struct ksmbd_work *work) KSMBD_DEFAULT_GFP); if (!conn->preauth_info) { rc = -ENOMEM; - rsp->hdr.Status = STATUS_INVALID_PARAMETER; + rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; goto err_out; } @@ -1817,7 +1817,7 @@ int smb2_handle_negotiate(struct ksmbd_work *work) ksmbd_conn_set_need_setup(conn); err_out: - if (rc) + if (rc && rsp->hdr.Status == STATUS_SUCCESS) rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; if (!rc) From 3a98de41b0a4d80e0aa57f677f7592e5f5321613 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 1 Aug 2026 23:48:09 +0900 Subject: [PATCH 104/142] ksmbd: fix use-after-free in lease break notification smb2_lease_break_noti() selects a connection from a shared lease table, but reads lease->l_lb without lease_list_lock. Connection teardown can free the table before the notification takes a reference to the selected connection. Select and pin the connection while holding the lock protecting its lifetime, before the allocations that may sleep. Also protect the owner connection lookup with ci->m_lock, since session reconnect can clear opinfo->conn under that lock. Transfer the reference to the notification work and release it on allocation failures or in the existing work cleanup path. Fixes: 2145945feb2c ("ksmbd: route v2 lease breaks on the client lease channel") Reported-by: Jinpyo Lee Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 54 ++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index b1cec9a4291b..5fc2c79881d1 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1035,6 +1035,40 @@ static void __smb2_lease_break_noti(struct work_struct *wk) ksmbd_conn_put(conn); } +/* + * Select and pin the connection used for a lease break before doing any + * allocations which may sleep. opinfo->conn is cleared under ci->m_lock, + * while lease->l_lb and the lease table lifetime are protected by + * lease_list_lock. + */ +static struct ksmbd_conn *smb2_lease_break_conn_get(struct oplock_info *opinfo) +{ + struct lease *lease = opinfo->o_lease; + struct lease_table *lb; + struct ksmbd_conn *conn; + + /* Keep the connection which owns the open, when it is still active. */ + down_read(&lease->ci->m_lock); + conn = READ_ONCE(opinfo->conn); + if (conn && !ksmbd_conn_releasing(conn)) + conn = ksmbd_conn_get(conn); + else + conn = NULL; + up_read(&lease->ci->m_lock); + + if (conn || lease->version != 2) + return conn; + + /* Otherwise route v2 lease breaks through the shared lease channel. */ + read_lock(&lease_list_lock); + lb = lease->l_lb; + if (lb && lb->conn && !ksmbd_conn_releasing(lb->conn)) + conn = ksmbd_conn_get(lb->conn); + read_unlock(&lease_list_lock); + + return conn; +} + /** * smb2_lease_break_noti() - break lease when a new client request * write lease @@ -1052,27 +1086,20 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo, bool sync, struct lease_break_info *br_info; struct lease *lease = opinfo->o_lease; - conn = READ_ONCE(opinfo->conn); - /* - * Keep a break on the channel which owns this open. A lease table is - * shared by connections with the same client GUID, so its connection - * can belong to another active channel. Only use it after the owning - * channel is being released. - */ - if ((!conn || ksmbd_conn_releasing(conn)) && lease->version == 2 && - lease->l_lb && lease->l_lb->conn && - !ksmbd_conn_releasing(lease->l_lb->conn)) - conn = lease->l_lb->conn; + conn = smb2_lease_break_conn_get(opinfo); if (!conn) return ksmbd_invalidate_durable_fd(opinfo->fid); work = ksmbd_alloc_work_struct(); - if (!work) + if (!work) { + ksmbd_conn_put(conn); return -ENOMEM; + } br_info = kmalloc_obj(struct lease_break_info, KSMBD_DEFAULT_GFP); if (!br_info) { ksmbd_free_work_struct(work); + ksmbd_conn_put(conn); return -ENOMEM; } @@ -1088,7 +1115,8 @@ static int smb2_lease_break_noti(struct oplock_info *opinfo, bool sync, memcpy(br_info->lease_key, lease->lease_key, SMB2_LEASE_KEY_SIZE); work->request_buf = (char *)br_info; - work->conn = ksmbd_conn_get(conn); + /* Transfer the reference acquired by smb2_lease_break_conn_get(). */ + work->conn = conn; work->sess = opinfo->sess; ksmbd_conn_r_count_inc(conn); From 29f74f0f2e6df3b393b7b66e810136d0c64e3c59 Mon Sep 17 00:00:00 2001 From: Ilan Dudnik Date: Sat, 1 Aug 2026 20:18:48 +0300 Subject: [PATCH 105/142] ksmbd: defer publishing granted locks to prevent UAF/double-free race In smb2_lock(), mid-batch granted locks are published to connection-wide (conn->lock_list) and file-wide (fp->lock_list) lists immediately upon vfs_lock_file() success, while also remaining tracked on the stack-local rollback_list. If a subsequent element in the same SMB2_LOCK request array fails validation or execution, the thread jumps to out: and walks rollback_list to undo previously granted locks. However, because the granted lock was already published to conn->lock_list, a concurrent UNLOCK request on the same connection can find the lock object and kfree() it before the rollback loop executes. When the granting thread subsequently walks rollback_list, it dereferences and frees the already-freed ksmbd_lock structure, resulting in a Use-After-Free and Double-Free (on both ksmbd_lock and struct file_lock). Fix this by deferring the publication of granted locks to conn->lock_list and fp->lock_list until after the entire array of lock elements has been processed without error. Mid-batch grants remain tracked exclusively on the request-local rollback_list until the whole batch succeeds, eliminating the race window. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Signed-off-by: Ilan Dudnik Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 9b64e28b5d56..2039a44d4b17 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9498,16 +9498,10 @@ int smb2_lock(struct ksmbd_work *work) rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED; kfree(smb_lock); - goto out2; + /* rollback_list may still hold earlier grants */ + goto out; } else if (!rc) { list_add(&smb_lock->llist, &rollback_list); - smb_lock->conn = ksmbd_conn_get(work->conn); - spin_lock(&work->conn->llist_lock); - list_add_tail(&smb_lock->clist, - &work->conn->lock_list); - list_add_tail(&smb_lock->flist, - &fp->lock_list); - spin_unlock(&work->conn->llist_lock); ksmbd_debug(SMB, "successful in taking lock\n"); } else { locks_free_lock(flock); @@ -9529,6 +9523,20 @@ int smb2_lock(struct ksmbd_work *work) err = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_lock_rsp)); if (err) goto out; + + /* publish only once the whole batch has committed */ + if (!list_empty(&rollback_list)) { + spin_lock(&work->conn->llist_lock); + list_for_each_entry(smb_lock, &rollback_list, llist) { + smb_lock->conn = ksmbd_conn_get(work->conn); + list_add_tail(&smb_lock->clist, + &work->conn->lock_list); + list_add_tail(&smb_lock->flist, + &fp->lock_list); + } + spin_unlock(&work->conn->llist_lock); + } + if (!lock_replayed) smb2_update_lock_sequence(work, fp, req); @@ -9559,15 +9567,6 @@ int smb2_lock(struct ksmbd_work *work) } list_del(&smb_lock->llist); - conn = smb_lock->conn; - spin_lock(&conn->llist_lock); - if (!list_empty(&smb_lock->flist)) - list_del(&smb_lock->flist); - list_del(&smb_lock->clist); - smb_lock->conn = NULL; - spin_unlock(&conn->llist_lock); - ksmbd_conn_put(conn); - locks_free_lock(smb_lock->fl); if (rlock) locks_free_lock(rlock); From a3bcea7c819a6c69ca937a68631311711bf20e66 Mon Sep 17 00:00:00 2001 From: Gael Blivet Date: Fri, 31 Jul 2026 13:54:20 +0200 Subject: [PATCH 106/142] ksmbd: exempt FSCTL_PIPE_TRANSCEIVE from the generic file-id lookup smb2_ioctl() rejects FSCTL_PIPE_TRANSCEIVE with STATUS_OBJECT_NAME_NOT_FOUND before fsctl_pipe_transceive() runs. RPC pipe IDs live in sess->rpc_handle_list, a separate namespace from the ksmbd_file table the generic ksmbd_lookup_fd_slow() gate searches, so the lookup always misses. Found while testing generic SMB browsing (Finder's "Connect to Server"): every DCE/RPC bind over a named pipe (SRVSVC, WKSSVC, SAMR, LSARPC) failed right after CREATE. Adding FSCTL_PIPE_TRANSCEIVE to the same no_fileid_ioctl exemption as FSCTL_PIPE_WAIT fixes it, confirmed by testing a build with and without the change. Signed-off-by: Gael Blivet Assisted-by: Claude:claude-sonnet-5 Tested-by: ChenXiaoSong Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 2039a44d4b17..e214d27e4d37 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -10132,6 +10132,7 @@ int smb2_ioctl(struct ksmbd_work *work) case FSCTL_QUERY_NETWORK_INTERFACE_INFO: case FSCTL_VALIDATE_NEGOTIATE_INFO: case FSCTL_PIPE_WAIT: + case FSCTL_PIPE_TRANSCEIVE: no_fileid_ioctl = true; break; default: From 2d99fbd7ad36ef288700f00102aaee11bcae04b4 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Mon, 3 Aug 2026 17:21:17 +0900 Subject: [PATCH 107/142] smb/server: fix posix state check for directory rename Pass the source ksmbd_file to the rename helpers and use the per-handle POSIX create-context state when deciding whether open children block a directory rename. work->tcon->posix_extensions only records whether POSIX extensions were negotiated on the connection. It does not indicate that the handles were opened with POSIX create contexts. Reproducer: 1. server: systemctl start ksmbd 2. client: mount -t cifs //${server_ip}/export /mnt # without posix option 3. client: mkdir /mnt/dir1/; touch /mnt/dir1/file 4. client: tail -f /mnt/dir1/file # open file 5. client: mv /mnt/dir1 /mnt/dir2 Without this fix, the rename can succeed when it should fail with "Permission denied". Fixes: c841bd3d8dec ("ksmbd: deny renaming directory with open children") Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 2 +- fs/smb/server/vfs.c | 6 +++--- fs/smb/server/vfs.h | 4 ++-- fs/smb/server/vfs_cache.c | 5 ++++- fs/smb/server/vfs_cache.h | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index e214d27e4d37..80f5791c687b 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -7690,7 +7690,7 @@ static int smb2_rename(struct ksmbd_work *work, goto out; smb_break_all_levII_oplock_rename(work, fp); - rc = ksmbd_vfs_rename(work, &fp->filp->f_path, new_name, flags); + rc = ksmbd_vfs_rename(work, fp, new_name, flags); out: kfree(new_name); return rc; diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index ff86e0e88177..0e7d66b0e899 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -678,9 +678,10 @@ int ksmbd_vfs_check_rename_share(struct ksmbd_work *work, return err; } -int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, +int ksmbd_vfs_rename(struct ksmbd_work *work, struct ksmbd_file *old_fp, char *newname, int flags) { + const struct path *old_path = &old_fp->filp->f_path; struct dentry *old_child = old_path->dentry; struct path new_path; struct qstr new_last; @@ -717,8 +718,7 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, if (err) goto out_drop_write; - if (!work->tcon->posix_extensions && d_is_dir(old_child) && - ksmbd_has_open_files(old_child)) { + if (d_is_dir(old_child) && ksmbd_has_open_files(old_fp)) { err = -EACCES; goto out3; } diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 1818b3f1971c..55d099de71f5 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -88,8 +88,8 @@ int ksmbd_vfs_remove_file(struct ksmbd_work *work, const struct path *path); int ksmbd_vfs_link(struct ksmbd_work *work, const char *oldname, const char *newname); int ksmbd_vfs_getattr(const struct path *path, struct kstat *stat); -int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, - char *newname, int flags); +int ksmbd_vfs_rename(struct ksmbd_work *work, struct ksmbd_file *old_fp, + char *newname, int flags); int ksmbd_vfs_check_rename_share(struct ksmbd_work *work, const struct path *old_path); int ksmbd_vfs_truncate(struct ksmbd_work *work, diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index a35df2ab59c9..eac9886eb6e3 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1137,8 +1137,9 @@ struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry) return NULL; } -bool ksmbd_has_open_files(struct dentry *dentry) +bool ksmbd_has_open_files(struct ksmbd_file *old_fp) { + struct dentry *dentry = old_fp->filp->f_path.dentry; struct ksmbd_file *fp; unsigned int id; bool ret = false; @@ -1151,6 +1152,8 @@ bool ksmbd_has_open_files(struct dentry *dentry) continue; if (fp_dentry == dentry) continue; + if (old_fp->is_posix_ctxt && fp->is_posix_ctxt) + continue; if (is_subdir(fp_dentry, dentry)) { ret = true; break; diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index d80f379d4e12..127ea4987e3f 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -212,7 +212,7 @@ bool ksmbd_has_stream_without_delete_share(struct ksmbd_file *fp); int ksmbd_close_fd_app_instance_id(char *app_instance_id); struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid); struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry); -bool ksmbd_has_open_files(struct dentry *dentry); +bool ksmbd_has_open_files(struct ksmbd_file *old_fp); unsigned int ksmbd_open_durable_fd(struct ksmbd_file *fp); struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp); void ksmbd_launch_ksmbd_durable_scavenger(void); From 1e3b4b4f7768126e365d9b74246f2f26a3b6fda3 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Wed, 5 Aug 2026 05:11:10 +0000 Subject: [PATCH 108/142] smb/server: rename to ksmbd_has_nonposix_open_child() The original function name `ksmbd_has_open_files()` could be confused with the function name introduced in the next patch, and it does not accurately describe what this function does. Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 2 +- fs/smb/server/vfs_cache.c | 2 +- fs/smb/server/vfs_cache.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 0e7d66b0e899..4c3cf376a756 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -718,7 +718,7 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, struct ksmbd_file *old_fp, if (err) goto out_drop_write; - if (d_is_dir(old_child) && ksmbd_has_open_files(old_fp)) { + if (d_is_dir(old_child) && ksmbd_has_nonposix_open_child(old_fp)) { err = -EACCES; goto out3; } diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index eac9886eb6e3..028bc1b0f652 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1137,7 +1137,7 @@ struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry) return NULL; } -bool ksmbd_has_open_files(struct ksmbd_file *old_fp) +bool ksmbd_has_nonposix_open_child(struct ksmbd_file *old_fp) { struct dentry *dentry = old_fp->filp->f_path.dentry; struct ksmbd_file *fp; diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 127ea4987e3f..9dca617bc429 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -212,7 +212,7 @@ bool ksmbd_has_stream_without_delete_share(struct ksmbd_file *fp); int ksmbd_close_fd_app_instance_id(char *app_instance_id); struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid); struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry); -bool ksmbd_has_open_files(struct ksmbd_file *old_fp); +bool ksmbd_has_nonposix_open_child(struct ksmbd_file *old_fp); unsigned int ksmbd_open_durable_fd(struct ksmbd_file *fp); struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp); void ksmbd_launch_ksmbd_durable_scavenger(void); From 6a8292d379d640ad2cfba9caa9c74da5390b498f Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Wed, 5 Aug 2026 05:11:11 +0000 Subject: [PATCH 109/142] smb/server: deny overwriting targets with non-POSIX opens Reproducer: 1. server: systemctl start ksmbd 2. client: mount without `posix` option mount -t cifs //${server_ip}/export /mnt 3. client: touch /mnt/file1 /mnt/file2 4. client: C program: int fd = open("/mnt/file2", O_RDONLY); 5. client: C program: rename("/mnt/file1", "/mnt/file2"); 6. client: C program: struct stat stbuf; fstat(fd, &stbuf); stbuf.st_nlink is 1, should be 0 This patch fixes xfstests generic/035 when mounted without `posix` option. Suggested-by: Namjae Jeon Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/vfs.c | 13 +++++++++++++ fs/smb/server/vfs_cache.c | 27 +++++++++++++++++++++++++++ fs/smb/server/vfs_cache.h | 1 + 3 files changed, 41 insertions(+) diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index 4c3cf376a756..286536f75144 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -723,6 +723,19 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, struct ksmbd_file *old_fp, goto out3; } + /* + * See MS-FSA 2.1.5.15.12. + * An overwrite rename must fail with STATUS_ACCESS_DENIED if the + * existing target still has a non-POSIX open. + */ + if (!(flags & (RENAME_NOREPLACE | RENAME_EXCHANGE)) && + d_inode(rd.new_dentry) && + d_inode(rd.new_dentry) != d_inode(old_child) && + ksmbd_has_other_nonposix_open(rd.new_dentry)) { + err = -EACCES; + goto out3; + } + err = ksmbd_vfs_check_rename_share(work, old_path); if (err) goto out3; diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 028bc1b0f652..90348a409162 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -1137,6 +1137,33 @@ struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry) return NULL; } +bool ksmbd_has_other_nonposix_open(struct dentry *dentry) +{ + struct ksmbd_file *fp; + struct inode *inode = d_inode(dentry); + unsigned int id; + bool ret = false; + + if (!inode) + return false; + + read_lock(&global_ft.lock); + idr_for_each_entry(global_ft.idr, fp, id) { + if (READ_ONCE(fp->f_state) != FP_INITED) + continue; + if (inode != file_inode(fp->filp)) + continue; + if (fp->is_posix_ctxt) + continue; + + ret = true; + break; + } + read_unlock(&global_ft.lock); + + return ret; +} + bool ksmbd_has_nonposix_open_child(struct ksmbd_file *old_fp) { struct dentry *dentry = old_fp->filp->f_path.dentry; diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 9dca617bc429..5cac022b540b 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -212,6 +212,7 @@ bool ksmbd_has_stream_without_delete_share(struct ksmbd_file *fp); int ksmbd_close_fd_app_instance_id(char *app_instance_id); struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid); struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry); +bool ksmbd_has_other_nonposix_open(struct dentry *dentry); bool ksmbd_has_nonposix_open_child(struct ksmbd_file *old_fp); unsigned int ksmbd_open_durable_fd(struct ksmbd_file *fp); struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp); From 23a0be6a84a38ac169eeab49017934e8e27c65f0 Mon Sep 17 00:00:00 2001 From: Ilan Dudnik Date: Fri, 7 Aug 2026 12:03:31 +0300 Subject: [PATCH 110/142] ksmbd: fix heap out-of-bounds write in krb5_authenticate() In krb5_authenticate(), out_len is calculated to determine available headroom in response_buf for the incoming Kerberos AP-REP token. However, when SMB2_SESSION_SETUP is processed as a non-first element of a compounded SMB2 request, the calculation omits work->next_smb2_rsp_hdr_off. This causes out_len to overstate remaining buffer headroom by the cumulative size of prior responses in the compound chain. Consequently, the length check in ksmbd_krb5_authenticate() (*out_len <= resp->spnego_blob_len) passes erroneously, allowing memcpy() to write the AP-REP blob past the end of response_buf into adjacent kernel heap memory. Fix this by subtracting work->next_smb2_rsp_hdr_off when computing out_len, ensuring it accurately reflects physical remaining buffer space. Signed-off-by: Ilan Dudnik Signed-off-by: Namjae Jeon --- 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 80f5791c687b..a7bfa6dbcfd5 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2160,7 +2160,7 @@ static int krb5_authenticate(struct ksmbd_work *work, in_len = le16_to_cpu(req->SecurityBufferLength); out_blob = (char *)&rsp->hdr.ProtocolId + le16_to_cpu(rsp->SecurityBufferOffset); - out_len = work->response_sz - + out_len = work->response_sz - work->next_smb2_rsp_hdr_off - (le16_to_cpu(rsp->SecurityBufferOffset) + 4); retval = ksmbd_krb5_authenticate(sess, in_blob, in_len, From 9a9f1342daaa211fdd68688ac2b16acfb4df06b6 Mon Sep 17 00:00:00 2001 From: Thomas Huth Date: Wed, 12 Aug 2026 21:35:23 +0900 Subject: [PATCH 111/142] smb: server: Clear sensitive stack and heap data in auth.c Sensitive data like keys that are stored in stack-local arrays could be leaked via the stack to the calling functions, or via the heap when using only normal kfree() functions. There is no known vulnaribility for this right now, but it's good security style to explicitly zeroize this sensitive matieral as soon as possible to avoid that it could be exploited together with other bugs later. In calc_ntlmv2_hash(), the struct hmac_md5_ctx is normally cleared during hmac_md5_final() already, but in case of errors, this function is skipped and ctx is never zeroized, so add a memzero_explicit(&ctx, sizeof(ctx)) there to fix the problem. In ksmbd_krb5_authenticate(), the ksmbd_spnego_authen_response contains the session key in the payload. It's currently freed with plain kvfree(). Let's better use kvfree_sensitive() instead. In generate_key(), the prfhash[] array is used to calculate the key, but it's never cleared, so it leaks on the stack. Thus clear this with a memzero_explicit(), too. In ksmbd_crypt_message(), the sign[] and key[] arrays are leaked via the stack, too. Make sure to clear them via memzero_explicit() at the end. Signed-off-by: Thomas Huth Signed-off-by: Namjae Jeon --- fs/smb/server/auth.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index d51f32095ada..2f89af029247 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -122,6 +122,8 @@ static int calc_ntlmv2_hash(struct ksmbd_conn *conn, struct ksmbd_session *sess, out: kfree(uniname); kfree(domain); + if (ret) /* Done by hmac_md5_final() already if ret == 0 */ + memzero_explicit(&ctx, sizeof(ctx)); return ret; } @@ -466,7 +468,8 @@ int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob, sess->kerberos_expiry = resp->session_expiry; retval = 0; out: - kvfree(resp); + kvfree_sensitive(resp, sizeof(*resp) + resp->session_key_len + + resp->spnego_blob_len); return retval; } #else @@ -558,6 +561,7 @@ static void generate_key(struct ksmbd_conn *conn, const char *sess_key, hmac_sha256_final(&ctx, prfhash); memcpy(key, prfhash, key_size); + memzero_explicit(prfhash, sizeof(prfhash)); } static int generate_smb3signingkey(struct ksmbd_session *sess, @@ -863,7 +867,8 @@ int ksmbd_crypt_message(struct ksmbd_work *work, struct kvec *iov, ctx = ksmbd_crypto_ctx_find_ccm(); if (!ctx) { pr_err("crypto alloc failed\n"); - return -ENOMEM; + rc = -ENOMEM; + goto zeroize_key; } if (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || @@ -943,5 +948,8 @@ int ksmbd_crypt_message(struct ksmbd_work *work, struct kvec *iov, aead_request_free(req); free_ctx: ksmbd_release_crypto_ctx(ctx); +zeroize_key: + memzero_explicit(key, sizeof(key)); + memzero_explicit(sign, sizeof(sign)); return rc; } From 2c5a176882695f73b52482df03b0017b006ef2d1 Mon Sep 17 00:00:00 2001 From: Thomas Huth Date: Wed, 12 Aug 2026 21:36:32 +0900 Subject: [PATCH 112/142] smb: server: Make sure that passkey is not leaked on the heap in user_config.c Use kfree_sensitive() to free the user->passkey (and the struct ksmbd_login_response in ksmbd_login_user() that contains the same information) to avoid that this information could leak somewhere else via the heap. Signed-off-by: Thomas Huth Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_config.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/mgmt/user_config.c b/fs/smb/server/mgmt/user_config.c index 0be08cf1896c..5efc3d7455b9 100644 --- a/fs/smb/server/mgmt/user_config.c +++ b/fs/smb/server/mgmt/user_config.c @@ -28,7 +28,7 @@ struct ksmbd_user *ksmbd_login_user(const char *account) user = ksmbd_alloc_user(resp, resp_ext); kvfree(resp_ext); out: - kvfree(resp); + kvfree_sensitive(resp, sizeof(*resp)); return user; } @@ -82,7 +82,7 @@ struct ksmbd_user *ksmbd_alloc_user(struct ksmbd_login_response *resp, err_free: kfree(user->name); - kfree(user->passkey); + kfree_sensitive(user->passkey); kfree(user); return NULL; } @@ -92,7 +92,7 @@ void ksmbd_free_user(struct ksmbd_user *user) ksmbd_ipc_logout_request(user->name, user->flags); kfree(user->sgid); kfree(user->name); - kfree(user->passkey); + kfree_sensitive(user->passkey); kfree(user); } From 9a4bd6507c21f66e84dc021705a4b9d84773111e Mon Sep 17 00:00:00 2001 From: Thomas Huth Date: Tue, 11 Aug 2026 17:46:18 +0200 Subject: [PATCH 113/142] smb: server: Free session data in user_session.c with kfree_sensitive() struct ksmbd_session contains some arrays with sensitive information, like sess_key, smb3encryptionkey, smb3decryptionkey and smb3signingkey. Thus let's make sure that this information cannot leak via the heap and use kfree_sensitive() to free it. Signed-off-by: Thomas Huth Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 0a87a1378791..f4675c457714 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -405,10 +405,10 @@ void ksmbd_session_destroy(struct ksmbd_session *sess) ksmbd_launch_ksmbd_durable_scavenger(); ksmbd_session_rpc_clear_list(sess); free_channel_list(sess); - kfree(sess->Preauth_HashValue); + kfree_sensitive(sess->Preauth_HashValue); ksmbd_release_id(&session_ida, sess->id); ida_destroy(&sess->tree_conn_ida); - kfree(sess); + kfree_sensitive(sess); } struct ksmbd_session *__session_lookup(unsigned long long id) From 0cb81ed8b55b91f5358cb5dad7dd63dfa19178eb Mon Sep 17 00:00:00 2001 From: Thomas Huth Date: Wed, 12 Aug 2026 21:38:04 +0900 Subject: [PATCH 114/142] smb: server: Free sensitive data in connection.c with kfree_sensitive() struct ksmbd_conn contains an embedded struct ntlmssp_auth with the ciphertext[] and cryptkey[] arrays, so to avoid leaking this information via the heap, it should be freed with kfree_sensitive(). While we're at it, also use kfree_sensitive() for freeing preauth_info in ksmbd_conn_free() to avoid that the Preauth_HashValue[] could leak via the heap here, too. Signed-off-by: Thomas Huth Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index dfbbded896d4..e225aca67686 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -190,7 +190,7 @@ static void __ksmbd_conn_release_work(struct work_struct *work) ida_destroy(&conn->async_ida); conn->transport->ops->free_transport(conn->transport); - kfree(conn); + kfree_sensitive(conn); } /** @@ -256,7 +256,7 @@ void ksmbd_conn_free(struct ksmbd_conn *conn) */ xa_destroy(&conn->sessions); kvfree(conn->request_buf); - kfree(conn->preauth_info); + kfree_sensitive(conn->preauth_info); kfree(conn->mechToken); ksmbd_preauth_session_destroy(conn); ksmbd_conn_put(conn); @@ -794,7 +794,7 @@ static void stop_sessions(void) if (atomic_dec_and_test(&target->refcnt)) { ida_destroy(&target->async_ida); t->ops->free_transport(t); - kfree(target); + kfree_sensitive(target); } goto again; } From 5b90f78dc22a3ee2f9dabbb41100f029bb799727 Mon Sep 17 00:00:00 2001 From: Thomas Huth Date: Tue, 11 Aug 2026 17:46:20 +0200 Subject: [PATCH 115/142] smb: server: Clear Preauth_HashValue in smb2pdu.c with kfree_sensitive() struct preauth_session contains the Preauth_HashValue[] array that might contain sensitive data. Use kfree_sensitive() to clear it before returning the memory to the heap. Signed-off-by: Thomas Huth Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index a7bfa6dbcfd5..835d9e2aacc4 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2475,7 +2475,7 @@ int smb2_sess_setup(struct ksmbd_work *work) ksmbd_preauth_session_lookup(conn, sess->id); if (preauth_sess) { list_del(&preauth_sess->preauth_entry); - kfree(preauth_sess); + kfree_sensitive(preauth_sess); } } } else { @@ -2529,7 +2529,7 @@ int smb2_sess_setup(struct ksmbd_work *work) preauth_sess = ksmbd_preauth_session_lookup(conn, sess->id); if (preauth_sess) { list_del(&preauth_sess->preauth_entry); - kfree(preauth_sess); + kfree_sensitive(preauth_sess); } } From 215e8816b1ac25176d911abb8704390413ccee4b Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 14 Aug 2026 11:18:38 +0900 Subject: [PATCH 116/142] ksmbd: detach blocked lock requests before freeing A file_lock retained by ksmbd for byte-range lock bookkeeping can still be part of the VFS blocked-request graph. In particular, the VFS can chain a new waiter below an already blocked request through flc_blocked_requests. The ksmbd_file reference count does not cover that graph. Both __ksmbd_close_fd() and the cross-request unlock path free these retained file_lock objects directly. If a dependent waiter is still attached, locks_release_private() hits BUG_ON(!list_empty(&flc->flc_blocked_requests)). The same lifetime mismatch can leave a freed ksmbd_lock reachable through its request-local llist. Detach the file_lock from the blocked-request graph before freeing it in the close, cross-request unlock, and rollback paths. locks_delete_block() also wakes requests chained below the object. Remove llist when a completed lock is published so a globally visible ksmbd_lock no longer points into the submitting worker's stack. Fixes: d63528eb0d43 ("ksmbd: free ksmbd_lock when file is closed") Reported-by: Kyenghwan Hwang Tested-by: Kyenghwan Hwang Tested-by: ChenXiaoSong Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 17 ++++++++++++----- fs/smb/server/vfs_cache.c | 8 +++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 835d9e2aacc4..37cf1deed6d0 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9169,6 +9169,12 @@ static void smb2_remove_blocked_lock(void **argv) locks_wake_up(flock); } +static void smb2_free_lock(struct file_lock *flock) +{ + ksmbd_vfs_posix_lock_unblock(flock); + locks_free_lock(flock); +} + static void smb2_free_blocked_lock(struct file_lock *flock) { ksmbd_vfs_posix_lock_unblock(flock); @@ -9355,14 +9361,14 @@ int smb2_lock(struct ksmbd_work *work) cmp_lock->end == smb_lock->end && !lock_defer_pending(cmp_lock->fl)) { nolock = 0; - list_del(&cmp_lock->flist); - list_del(&cmp_lock->clist); + list_del_init(&cmp_lock->flist); + list_del_init(&cmp_lock->clist); cmp_lock->conn = NULL; spin_unlock(&conn->llist_lock); up_read(&conn_list_lock); ksmbd_conn_put(conn); - locks_free_lock(cmp_lock->fl); + smb2_free_lock(cmp_lock->fl); kfree(cmp_lock); goto out_check_cl; } @@ -9527,7 +9533,8 @@ int smb2_lock(struct ksmbd_work *work) /* publish only once the whole batch has committed */ if (!list_empty(&rollback_list)) { spin_lock(&work->conn->llist_lock); - list_for_each_entry(smb_lock, &rollback_list, llist) { + list_for_each_entry_safe(smb_lock, tmp, &rollback_list, llist) { + list_del_init(&smb_lock->llist); smb_lock->conn = ksmbd_conn_get(work->conn); list_add_tail(&smb_lock->clist, &work->conn->lock_list); @@ -9567,7 +9574,7 @@ int smb2_lock(struct ksmbd_work *work) } list_del(&smb_lock->llist); - locks_free_lock(smb_lock->fl); + smb2_free_lock(smb_lock->fl); if (rlock) locks_free_lock(rlock); kfree(smb_lock); diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 90348a409162..972e8985a503 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -630,8 +630,9 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) if (!IS_ERR_OR_NULL(filp)) fput(filp); - /* because the reference count of fp is 0, it is guaranteed that - * there are not accesses to fp->lock_list. + /* + * The zero fp reference count serializes access to fp->lock_list, but + * the VFS may still have blocked requests chained below these locks. */ list_for_each_entry_safe(smb_lock, tmp_lock, &fp->lock_list, flist) { struct ksmbd_conn *conn = smb_lock->conn; @@ -644,7 +645,8 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) ksmbd_conn_put(conn); } - list_del(&smb_lock->flist); + list_del_init(&smb_lock->flist); + ksmbd_vfs_posix_lock_unblock(smb_lock->fl); locks_free_lock(smb_lock->fl); kfree(smb_lock); } From 93a3cda16124fe0a7d80666e6dcb56c75cbedd1c Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Thu, 13 Aug 2026 10:40:42 +0000 Subject: [PATCH 117/142] smb/server: fix use-after-free in ksmbd_conn_transport_destroy() Reproducer (Link[1]): 1. Build kernel with CONFIG_KASAN=y 2. server: systemctl start ksmbd 3. client: mount -t cifs //localhost/export /mnt 4. client: umount /mnt 5. server: modprobe -r ksmbd The error message is as follows: ================================================================== BUG: KASAN: slab-use-after-free in proc_remove+0x3e/0x80 Read of size 8 at addr ffff88810654e098 by task modprobe/785 ... Call Trace: __dump_stack+0x19/0x30 dump_stack_lvl+0x49/0x60 print_address_description+0x7b/0x200 print_report+0x5b/0x70 kasan_report+0xed/0x130 __asan_report_load8_noabort+0x18/0x20 proc_remove+0x3e/0x80 ksmbd_conn_transport_destroy+0x2b/0x320 [ksmbd] cleanup_module+0x33/0xe00 [ksmbd] __se_sys_delete_module+0x276/0x400 __x64_sys_delete_module+0x5f/0x70 x64_sys_call+0x2675/0x3030 do_syscall_64+0xf0/0x3b0 entry_SYSCALL_64_after_hwframe+0x76/0x7e RIP: 0033:0x7f5b56d2b02b ... Allocated by task 159: kasan_save_track+0x2f/0x70 kasan_save_alloc_info+0x40/0x50 __kasan_slab_alloc+0x52/0x70 kmem_cache_alloc_noprof+0x168/0x3e0 __proc_create+0x20b/0x710 proc_create_single_data+0x78/0x150 ksmbd_proc_create+0x24/0x30 [ksmbd] ksmbd_conn_transport_init+0x4f/0x80 [ksmbd] server_ctrl_handle_work+0x64/0x2c0 [ksmbd] process_scheduled_works+0x788/0xec0 worker_thread+0x894/0xc10 kthread+0x2e5/0x3c0 ret_from_fork+0x168/0x4f0 ret_from_fork_asm+0x1a/0x30 Freed by task 785: kasan_save_track+0x2f/0x70 kasan_save_free_info+0x4a/0x60 __kasan_slab_free+0x47/0x70 kmem_cache_free+0x122/0x410 pde_put+0xfd/0x160 remove_proc_subtree+0x365/0x540 proc_remove+0x6a/0x80 ksmbd_proc_cleanup+0x1f/0x60 [ksmbd] cleanup_module+0x18/0xe00 [ksmbd] __se_sys_delete_module+0x276/0x400 __x64_sys_delete_module+0x5f/0x70 x64_sys_call+0x2675/0x3030 do_syscall_64+0xf0/0x3b0 entry_SYSCALL_64_after_hwframe+0x76/0x7e ================================================================== Reported-by: Kyenghwan Hwang Link[1]: https://lore.kernel.org/linux-cifs/8ea028f5-90f4-4d21-b1ac-a343f0f04d88@chenxiaosong.com/ Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/server.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 0ccd123ba418..d619d1f22601 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -596,11 +596,16 @@ static int ksmbd_server_shutdown(void) { WRITE_ONCE(server_conf.state, SERVER_STATE_SHUTTING_DOWN); - ksmbd_proc_cleanup(); class_unregister(&ksmbd_control_class); ksmbd_workqueue_destroy(); ksmbd_ipc_release(); ksmbd_conn_transport_destroy(); + /* + * ksmbd_conn_transport_destroy() calls delete_proc_clients() and destroys + * sessions. ksmbd_session_destroy() removes each session's proc entry. + * Keep the procfs tree alive until these entries have been removed. + */ + ksmbd_proc_cleanup(); ksmbd_crypto_destroy(); ksmbd_free_global_file_table(); destroy_lease_table(NULL); From 4818df5aaac04f83c7fc196384783033275c8041 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Fri, 14 Aug 2026 13:51:00 +0900 Subject: [PATCH 118/142] ksmbd: wait for deferred notify cancellation A cancelled SMB2 CHANGE_NOTIFY request is completed from system_wq. The deferred work keeps a reference to the connection, but it is not included in the connection's r_count. During connection teardown, ksmbd_conn_transport_destroy() can therefore finish the connection handler and destroy session proc entries before the deferred response runs. Account for the deferred cancellation work in r_count. The connection handler now waits for the deferred response to finish before it deregisters sessions and removes their proc entries. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 37cf1deed6d0..365439abbebe 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -11058,9 +11058,15 @@ static void smb2_notify_cancel_deferred(struct work_struct *w) { struct notify_cancel_ctx *ctx = container_of(w, struct notify_cancel_ctx, work); + struct ksmbd_conn *conn = ctx->in_work->conn; smb2_complete_notify_cancel(ctx->in_work); kfree(ctx); + /* + * The connection teardown waits for r_count before destroying + * connection sessions and their proc entries. + */ + ksmbd_conn_r_count_dec(conn); } static struct ksmbd_work *smb2_notify_cancel_claim(void **argv) @@ -11119,6 +11125,12 @@ static void smb2_notify_cancel_fn(void **argv) } ctx->in_work = in_work; INIT_WORK(&ctx->work, smb2_notify_cancel_deferred); + /* + * This deferred work can outlive the connection handler's receive loop. + * Keep teardown from destroying the connection's sessions until the + * deferred response has finished using them. + */ + ksmbd_conn_r_count_inc(conn); schedule_work(&ctx->work); } From 39f2032096715daae5f6fd0f587ca7a474b019df Mon Sep 17 00:00:00 2001 From: Ze Tan Date: Fri, 14 Aug 2026 10:36:29 +0800 Subject: [PATCH 119/142] smb/server: fix tree connection leak in smb2_tree_connect() See the procedure below: smb2_tree_connect ksmbd_tree_conn_connect xa_store(&sess->tree_conns, tree_conn->id, tree_conn) ksmbd_counter_inc(KSMBD_COUNTER_TREE_CONNS) ksmbd_share_tree_conn_inc(sc) ksmbd_iov_pin_rsp // fail status.ret = KSMBD_TREE_CONN_STATUS_NOMEM // do not disconnect tree_conn Disconnect the new tree connection if ksmbd_iov_pin_rsp() fails. Fixes: e2b76ab8b5c9 ("ksmbd: add support for read compound") Signed-off-by: Ze Tan Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 365439abbebe..bfa895414fd4 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2691,8 +2691,16 @@ int smb2_tree_connect(struct ksmbd_work *work) cpu_to_le32(SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM); rc = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_tree_connect_rsp)); - if (rc) + if (rc) { + if (status.ret == KSMBD_TREE_CONN_STATUS_OK) { + down_write(&sess->tree_conns_lock); + status.tree_conn->t_state = TREE_DISCONNECTED; + up_write(&sess->tree_conns_lock); + ksmbd_tree_conn_disconnect(sess, status.tree_conn); + status.tree_conn = NULL; + } status.ret = KSMBD_TREE_CONN_STATUS_NOMEM; + } if (!IS_ERR(treename)) kfree(treename); From 68f3508e4c36be026f7526e753a9e1bc3ff6bfbb Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 15 Aug 2026 22:17:35 +0900 Subject: [PATCH 120/142] ksmbd: accept unspecified volatile ID on durable reconnect BVT_DurableHandleV1_Reconnect_WithBatchOplock, BVT_DurableHandleV1_Reconnect_WithLeaseV1, BVT_DurableHandleV2_Reconnect_WithBatchOplock, and BVT_DurableHandleV2_Reconnect_WithLeaseV1 fail to reconnect a durable handle when the request leaves VolatileFileId unset. A durable reconnect request may omit VolatileFileId by setting it to zero. Treating zero as an ID makes ksmbd reject the request whenever the saved volatile ID is nonzero. Only compare the saved and requested volatile IDs when the request contains a nonzero value. Explicit mismatches continue to be rejected. This allows SMB2 durable handle V1 and V2 reconnects that identify the handle through the persistent ID and reconnect context. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index bfa895414fd4..bd806dca9dcd 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -3584,7 +3584,9 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - if (dh_info->fp->durable_volatile_id != + /* A zero VolatileFileId means that the client did not specify it. */ + if (recon_v2->dcontext.Fid.VolatileFileId && + dh_info->fp->durable_volatile_id != recon_v2->dcontext.Fid.VolatileFileId) { err = -EBADF; ksmbd_put_durable_fd(dh_info->fp); @@ -3637,7 +3639,9 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - if (dh_info->fp->durable_volatile_id != + /* A zero VolatileFileId means that the client did not specify it. */ + if (recon->Data.Fid.VolatileFileId && + dh_info->fp->durable_volatile_id != recon->Data.Fid.VolatileFileId) { err = -EBADF; ksmbd_put_durable_fd(dh_info->fp); From 5213c368631291eef99f09ae5607b130b2a3d6ac Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 15 Aug 2026 22:18:59 +0900 Subject: [PATCH 121/142] ksmbd: implement SMB2 AppInstanceVersion takeover BVT_AppInstanceVersion_SMB311_GreaterVersion, BVT_AppInstanceVersion_SMB311_SameVersion, BVT_AppInstanceVersion_SMB311_LowerAppInstanceVersionHigh, and BVT_AppInstanceVersion_SMB311_LowerAppInstanceVersionLow exercise ordered opens using the same AppInstanceId. ksmbd tracked the AppInstanceId, but did not parse the version context or enforce the version ordering, so versioned opens returned incorrect sharing violations. Parse and retain the 24-byte AppInstanceVersion context with each open. Reject a version that is lower than or equal to the active version with STATUS_FILE_FORCED_CLOSED, reject an unversioned open against a versioned handle, and close the previous handle for a newer takeover. Do not apply the takeover check to durable reconnect or replay requests. Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 98 +++++++++++++++++++++++++++++++++++++-- fs/smb/server/vfs_cache.c | 7 ++- fs/smb/server/vfs_cache.h | 5 ++ 3 files changed, 101 insertions(+), 9 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index bd806dca9dcd..da461535229a 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -16,6 +16,7 @@ #include #include #include +#include #include "glob.h" #include "../common/smbfsctl.h" @@ -3451,9 +3452,12 @@ struct durable_info { bool replay; bool replay_consumed; bool app_instance_id; + bool app_instance_version_valid; unsigned int timeout; char *CreateGuid; char AppInstanceId[SMB2_CREATE_GUID_SIZE]; + u64 app_instance_version_high; + u64 app_instance_version_low; }; static int smb2_check_durable_replay(struct ksmbd_work *work, @@ -3819,6 +3823,68 @@ static int parse_app_instance_id(struct smb2_create_req *req, return 0; } +static int parse_app_instance_version(struct smb2_create_req *req, + struct durable_info *dh_info) +{ + struct create_context *context; + char *data; + + context = smb2_find_context_vals(req, SMB2_CREATE_APP_INSTANCE_VERSION, + SMB2_CREATE_GUID_SIZE); + if (IS_ERR(context)) + return PTR_ERR(context); + if (!context) + return 0; + + if (le32_to_cpu(context->DataLength) < 24) + return -EINVAL; + + data = (char *)context + le16_to_cpu(context->DataOffset); + if (get_unaligned_le16(data) != 24 || + get_unaligned_le16(data + 2) != 0) + return -EINVAL; + + dh_info->app_instance_version_high = get_unaligned_le64(data + 8); + dh_info->app_instance_version_low = get_unaligned_le64(data + 16); + dh_info->app_instance_version_valid = true; + return 0; +} + +static int smb2_handle_app_instance_id(struct smb2_create_rsp *rsp, + struct durable_info *dh_info) +{ + struct ksmbd_file *old_fp; + bool reject = false; + + if (!dh_info->app_instance_id) + return 0; + + old_fp = ksmbd_lookup_fd_app_instance_id(dh_info->AppInstanceId); + if (!old_fp) + return 0; + + if (dh_info->app_instance_version_valid) { + if (old_fp->app_instance_version_valid && + (dh_info->app_instance_version_high < + old_fp->app_instance_version_high || + (dh_info->app_instance_version_high == + old_fp->app_instance_version_high && + dh_info->app_instance_version_low <= + old_fp->app_instance_version_low))) + reject = true; + } else if (old_fp->app_instance_version_valid) { + reject = true; + } + + ksmbd_put_durable_fd(old_fp); + if (reject) { + rsp->hdr.Status = STATUS_FILE_FORCED_CLOSED; + return -EIO; + } + + return ksmbd_close_fd_app_instance_id(dh_info->AppInstanceId); +} + /** * smb2_open() - handler for smb file open request * @work: smb work containing request buffer @@ -3946,6 +4012,15 @@ int smb2_open(struct ksmbd_work *work) req_op_level = req->RequestedOplockLevel; + if (req->CreateContextsOffset) { + rc = parse_app_instance_id(req, &dh_info); + if (rc) + goto err_out2; + rc = parse_app_instance_version(req, &dh_info); + if (rc) + goto err_out2; + } + if (server_conf.flags & KSMBD_GLOBAL_FLAG_DURABLE_HANDLE && req->CreateContextsOffset) { lc = parse_lease_state(req); @@ -3960,9 +4035,6 @@ int smb2_open(struct ksmbd_work *work) if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) req_op_level = SMB2_OPLOCK_LEVEL_NONE; } - rc = parse_app_instance_id(req, &dh_info); - if (rc) - goto err_out2; rc = parse_durable_handle_context(work, req, lc, &dh_info); if (rc) { ksmbd_debug(SMB, "error parsing durable handle context\n"); @@ -4010,8 +4082,6 @@ int smb2_open(struct ksmbd_work *work) goto reconnected_fp; } - if (dh_info.type == DURABLE_REQ_V2 && dh_info.app_instance_id) - ksmbd_close_fd_app_instance_id(dh_info.AppInstanceId); } else if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) { lc = parse_lease_state(req); if (IS_ERR(lc)) { @@ -4026,6 +4096,13 @@ int smb2_open(struct ksmbd_work *work) } } + if (dh_info.app_instance_id && !dh_info.reconnected && + !dh_info.replay) { + rc = smb2_handle_app_instance_id(rsp, &dh_info); + if (rc) + goto err_out2; + } + if (le32_to_cpu(req->ImpersonationLevel) > le32_to_cpu(IL_DELEGATE)) { pr_err("Invalid impersonationlevel : 0x%x\n", le32_to_cpu(req->ImpersonationLevel)); @@ -4426,6 +4503,17 @@ int smb2_open(struct ksmbd_work *work) * waiting on the same break again. */ memcpy(fp->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); + if (dh_info.app_instance_id) { + memcpy(fp->app_instance_id, dh_info.AppInstanceId, + SMB2_CREATE_GUID_SIZE); + fp->has_app_instance_id = true; + } + if (dh_info.app_instance_version_valid) { + fp->app_instance_version_high = + dh_info.app_instance_version_high; + fp->app_instance_version_low = dh_info.app_instance_version_low; + fp->app_instance_version_valid = true; + } if (dh_info.CreateGuid) { memcpy(fp->create_guid, dh_info.CreateGuid, SMB2_CREATE_GUID_SIZE); fp->durable_replay_consumed = dh_info.replay_consumed; diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 972e8985a503..413997f393f0 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -995,16 +995,15 @@ bool ksmbd_has_other_active_fd(struct ksmbd_file *fp) return ret; } -static struct ksmbd_file *ksmbd_lookup_fd_app_instance_id(char *app_instance_id) +struct ksmbd_file *ksmbd_lookup_fd_app_instance_id(char *app_instance_id) { struct ksmbd_file *fp = NULL; unsigned int id; - if (!memchr_inv(app_instance_id, 0, SMB2_CREATE_GUID_SIZE)) - return NULL; - read_lock(&global_ft.lock); idr_for_each_entry(global_ft.idr, fp, id) { + if (!fp->has_app_instance_id) + continue; if (!memcmp(fp->app_instance_id, app_instance_id, SMB2_CREATE_GUID_SIZE)) { fp = ksmbd_fp_get(fp); diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 5cac022b540b..502efb16f05f 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -149,6 +149,10 @@ struct ksmbd_file { bool is_durable; bool is_persistent; bool is_resilient; + bool has_app_instance_id; + bool app_instance_version_valid; + u64 app_instance_version_high; + u64 app_instance_version_low; bool durable_reconnect_disabled; bool durable_replay_consumed; @@ -209,6 +213,7 @@ void ksmbd_put_durable_fd(struct ksmbd_file *fp); int ksmbd_invalidate_durable_fd(unsigned long long id); bool ksmbd_has_other_active_fd(struct ksmbd_file *fp); bool ksmbd_has_stream_without_delete_share(struct ksmbd_file *fp); +struct ksmbd_file *ksmbd_lookup_fd_app_instance_id(char *app_instance_id); int ksmbd_close_fd_app_instance_id(char *app_instance_id); struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid); struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry); From abe5acb34282206c2a6303d0e09b3165b1addf0e Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sat, 15 Aug 2026 22:19:38 +0900 Subject: [PATCH 122/142] ksmbd: notify parent directory leases on child create BVT_DirectoryLeasing_ReadWriteHandleCaching requires a parent directory lease break notification when another client creates a child in the leased directory. A child CREATE without a lease context did not notify the parent lease holders because the notification path expected a non-NULL lease context. Allow the parent lease notification helper to handle a NULL child lease context and notify matching parent leases. Invoke it after a child is created without a lease context while preserving the existing lease-key filtering for requests that provide one. Signed-off-by: Namjae Jeon --- fs/smb/server/oplock.c | 9 +++++---- fs/smb/server/smb2pdu.c | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 5fc2c79881d1..58af0fddf39f 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -1450,7 +1450,7 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp, struct ksmbd_inode *p_ci = NULL; LIST_HEAD(brk_list); - if (lctx->version != 2) + if (lctx && lctx->version != 2) return; p_ci = ksmbd_inode_lookup_lock(fp->filp->f_path.dentry->d_parent); @@ -1463,9 +1463,10 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp, continue; if (opinfo->o_lease->state != SMB2_OPLOCK_LEVEL_NONE && - (!(lctx->flags & SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE) || - !compare_guid_key(opinfo, fp->conn->ClientGUID, - lctx->parent_lease_key))) { + (!lctx || + (!(lctx->flags & SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE) || + !compare_guid_key(opinfo, fp->conn->ClientGUID, + lctx->parent_lease_key)))) { if (!atomic_inc_not_zero(&opinfo->refcount)) continue; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index da461535229a..bcf4e8e1ca22 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -4663,6 +4663,9 @@ int smb2_open(struct ksmbd_work *work) goto err_out1; } } else { + if (created && !lc) + smb_send_parent_lease_break_noti(fp, NULL); + if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE && lc) { if (S_ISDIR(file_inode(filp)->i_mode)) { lc->req_state &= ~SMB2_LEASE_WRITE_CACHING_LE; From 50a400cff59f534254ace2828f2eb9d844517fbb Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 16 Aug 2026 09:33:41 +0900 Subject: [PATCH 123/142] ksmbd: add per-share SMB3 encryption enforcement Add a share flag for requiring SMB3 encryption on an individual share. Advertise SMB2_SHAREFLAG_ENCRYPT_DATA in TREE_CONNECT responses and reject both unencrypted TREE_CONNECT attempts and plaintext requests for shares carrying the flag. Keep BIT(19) reserved for the existing ksmbd-tools WIDE_LINKS flag and use BIT(20) for the new netlink ABI flag. Signed-off-by: Namjae Jeon --- fs/smb/common/smb2pdu.h | 3 ++- fs/smb/server/ksmbd_netlink.h | 2 ++ fs/smb/server/mgmt/share_config.c | 1 + fs/smb/server/server.c | 11 +++++++++++ fs/smb/server/smb2pdu.c | 22 ++++++++++++++++++---- 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/fs/smb/common/smb2pdu.h b/fs/smb/common/smb2pdu.h index c1414a1ffe30..d9650aff0d3c 100644 --- a/fs/smb/common/smb2pdu.h +++ b/fs/smb/common/smb2pdu.h @@ -370,7 +370,8 @@ struct smb2_tree_connect_req { #define SMB2_SHAREFLAG_FORCE_LEVELII_OPLOCK 0x00001000 #define SMB2_SHAREFLAG_ENABLE_HASH_V1 0x00002000 #define SMB2_SHAREFLAG_ENABLE_HASH_V2 0x00004000 -#define SHI1005_FLAGS_ENCRYPT_DATA 0x00008000 +#define SMB2_SHAREFLAG_ENCRYPT_DATA 0x00008000 +#define SHI1005_FLAGS_ENCRYPT_DATA SMB2_SHAREFLAG_ENCRYPT_DATA #define SMB2_SHAREFLAG_IDENTITY_REMOTING 0x00040000 /* 3.1.1 */ #define SMB2_SHAREFLAG_COMPRESS_DATA 0x00100000 /* 3.1.1 */ #define SMB2_SHAREFLAG_ISOLATED_TRANSPORT 0x00200000 diff --git a/fs/smb/server/ksmbd_netlink.h b/fs/smb/server/ksmbd_netlink.h index af1e760453d9..2673522c76bc 100644 --- a/fs/smb/server/ksmbd_netlink.h +++ b/fs/smb/server/ksmbd_netlink.h @@ -381,6 +381,8 @@ enum KSMBD_TREE_CONN_STATUS { #define KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY BIT(16) #define KSMBD_SHARE_FLAG_HIDE_UNREADABLE BIT(17) #define KSMBD_SHARE_FLAG_TIME_MACHINE BIT(18) +/* Keep BIT(19) reserved for the existing ksmbd-tools WIDE_LINKS flag. */ +#define KSMBD_SHARE_FLAG_ENCRYPT_DATA BIT(20) /* * Tree connect request flags. diff --git a/fs/smb/server/mgmt/share_config.c b/fs/smb/server/mgmt/share_config.c index 53d6f71dd871..9edb2fe08812 100644 --- a/fs/smb/server/mgmt/share_config.c +++ b/fs/smb/server/mgmt/share_config.c @@ -47,6 +47,7 @@ static const struct ksmbd_const_name ksmbd_share_flag_names[] = { {KSMBD_SHARE_FLAG_UPDATE, "update"}, {KSMBD_SHARE_FLAG_CROSSMNT, "crossmnt"}, {KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY, "continuous-availability"}, + {KSMBD_SHARE_FLAG_ENCRYPT_DATA, "encrypt-data"}, }; static int proc_show_shares(struct seq_file *m, void *v) diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index d619d1f22601..7881fc7bb8cd 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -24,6 +24,8 @@ #include "auth.h" #include "stats.h" #include "compress.h" +#include "mgmt/share_config.h" +#include "mgmt/tree_connect.h" int ksmbd_debug_types; @@ -236,6 +238,15 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, STATUS_NETWORK_NAME_DELETED); goto send; } + + if (work->tcon && + test_share_config_flag(work->tcon->share_conf, + KSMBD_SHARE_FLAG_ENCRYPT_DATA) && + !work->encrypted) { + conn->ops->set_rsp_status(work, + STATUS_ACCESS_DENIED); + goto send; + } } } diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index bcf4e8e1ca22..aa662adaf63d 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2636,12 +2636,22 @@ int smb2_tree_connect(struct ksmbd_work *work) name, treename); status = ksmbd_tree_conn_connect(work, name); - if (status.ret == KSMBD_TREE_CONN_STATUS_OK) + if (status.ret == KSMBD_TREE_CONN_STATUS_OK) { rsp->hdr.Id.SyncId.TreeId = cpu_to_le32(status.tree_conn->id); - else + share = status.tree_conn->share_conf; + + /* A share that requires encryption needs a negotiated SMB3 cipher. */ + if (test_share_config_flag(share, KSMBD_SHARE_FLAG_ENCRYPT_DATA) && + !smb3_encryption_negotiated(conn)) { + ksmbd_tree_conn_disconnect(sess, status.tree_conn); + status.tree_conn = NULL; + share = NULL; + status.ret = KSMBD_TREE_CONN_STATUS_ERROR; + goto out_err1; + } + } else goto out_err1; - share = status.tree_conn->share_conf; if (test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) { ksmbd_debug(SMB, "IPC share path request\n"); rsp->ShareType = SMB2_SHARE_TYPE_PIPE; @@ -2687,9 +2697,13 @@ int smb2_tree_connect(struct ksmbd_work *work) conn->compress_algorithm != SMB3_COMPRESS_NONE) rsp->ShareFlags |= cpu_to_le32(SMB2_SHAREFLAG_COMPRESS_DATA); if (share && test_share_config_flag(share, - KSMBD_SHARE_FLAG_HIDE_UNREADABLE)) + KSMBD_SHARE_FLAG_HIDE_UNREADABLE)) rsp->ShareFlags |= cpu_to_le32(SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM); + if (share && test_share_config_flag(share, + KSMBD_SHARE_FLAG_ENCRYPT_DATA)) + rsp->ShareFlags |= + cpu_to_le32(SMB2_SHAREFLAG_ENCRYPT_DATA); rc = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_tree_connect_rsp)); if (rc) { From 2cbd4a8bf460cdf414a2d7e4912c5bcfe3d0fdc2 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 13 Aug 2026 09:00:00 +0900 Subject: [PATCH 124/142] ksmbd: fix encrypted request lookup on bound channels An SMB3 multichannel binding registers the secondary connection in the session channel list, but does not insert the session into the secondary connection's session xarray. The decryption path only searches the connection-local xarray. As a result, every encrypted request received on a bound channel fails with "Could not get decryption key". Use the channel-aware session lookup for decryption. Also stop using the temporary conn->binding flag to decide whether the global lookup is allowed. Validate the permanent channel association under chann_lock instead. Fixes: f5a544e3bab7 ("ksmbd: add support for SMB3 multichannel") Signed-off-by: Namjae Jeon --- fs/smb/server/auth.c | 2 +- fs/smb/server/mgmt/user_session.c | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index 2f89af029247..bcd371f5550d 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -729,7 +729,7 @@ static int ksmbd_get_encryption_key(struct ksmbd_work *work, __u64 ses_id, * that the command can reach the session setup handler. Other * commands are rejected there with STATUS_NETWORK_SESSION_EXPIRED. */ - sess = ksmbd_session_lookup(work->conn, ses_id); + sess = ksmbd_session_lookup_all_states(work->conn, ses_id); if (sess && sess->state != SMB2_SESSION_VALID && (sess->state != SMB2_SESSION_EXPIRED || !sess->kerberos_expiry)) { diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index f4675c457714..31eccad5d732 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -551,11 +551,18 @@ struct ksmbd_session *ksmbd_session_lookup_all_states(struct ksmbd_conn *conn, unsigned long long id) { struct ksmbd_session *sess; + bool channel_found; sess = ksmbd_session_lookup(conn, id); - if (!sess && conn->binding) { + if (!sess) { sess = ksmbd_session_lookup_slowpath(id); - if (sess && !xa_load(&sess->ksmbd_chann_list, (long)conn)) { + if (!sess) + return NULL; + + down_read(&sess->chann_lock); + channel_found = xa_load(&sess->ksmbd_chann_list, (long)conn); + up_read(&sess->chann_lock); + if (!channel_found) { ksmbd_user_session_put(sess); sess = NULL; } From c50e628122aed077695669e25b842e778511a43d Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 13 Aug 2026 09:01:00 +0900 Subject: [PATCH 125/142] ksmbd: scope session state changes to bound connections ksmbd_all_conn_set_status() treats every connection whose transient binding flag is set as belonging to the target SessionId. A logoff or session replacement can consequently move an unrelated connection to NEED_RECONNECT or NEED_SETUP. Pass the target session itself and select connections using either the connection-local session xarray or the session's permanent channel list. Use the same association test while waiting for requests to drain. Serialize session-wide status changes under request_lock and do not overwrite EXITING or RELEASING. Protect the shutdown transition with the same lock so a concurrent session update cannot revive a closing connection. Fixes: f5a544e3bab7 ("ksmbd: add support for SMB3 multichannel") Fixes: abcc506a9a71 ("ksmbd: fix racy issue from smb2 close and logoff with multichannel") Fixes: c444139cb747 ("ksmbd: rewrite stop_sessions() with restartable iteration") Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 37 ++++++++++++++++++++++++++----- fs/smb/server/connection.h | 6 +++-- fs/smb/server/mgmt/user_session.c | 8 +++---- fs/smb/server/smb2pdu.c | 6 ++--- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index e225aca67686..71d55d903f6f 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -404,15 +404,37 @@ void ksmbd_conn_unlock(struct ksmbd_conn *conn) mutex_unlock(&conn->srv_mutex); } -void ksmbd_all_conn_set_status(u64 sess_id, u32 status) +static bool ksmbd_session_is_bound_to_conn(struct ksmbd_session *sess, + struct ksmbd_conn *conn) +{ + bool found; + + rcu_read_lock(); + found = xa_load(&conn->sessions, sess->id) == sess; + rcu_read_unlock(); + if (found) + return true; + + down_read(&sess->chann_lock); + found = xa_load(&sess->ksmbd_chann_list, (long)conn); + up_read(&sess->chann_lock); + return found; +} + +void ksmbd_all_conn_set_status(struct ksmbd_session *sess, u32 status) { struct ksmbd_conn *conn; int bkt; down_read(&conn_list_lock); hash_for_each(conn_list, bkt, conn, hlist) { - if (conn->binding || xa_load(&conn->sessions, sess_id)) - WRITE_ONCE(conn->status, status); + if (ksmbd_session_is_bound_to_conn(sess, conn)) { + spin_lock(&conn->request_lock); + if (!ksmbd_conn_exiting(conn) && + !ksmbd_conn_releasing(conn)) + WRITE_ONCE(conn->status, status); + spin_unlock(&conn->request_lock); + } } up_read(&conn_list_lock); } @@ -422,7 +444,8 @@ void ksmbd_conn_wait_idle(struct ksmbd_conn *conn) wait_event(conn->req_running_q, atomic_read(&conn->req_running) < 2); } -int ksmbd_conn_wait_idle_sess_id(struct ksmbd_conn *curr_conn, u64 sess_id) +int ksmbd_conn_wait_idle_sess(struct ksmbd_conn *curr_conn, + struct ksmbd_session *sess) { struct ksmbd_conn *conn; int rc, retry_count = 0, max_timeout = 120; @@ -434,7 +457,7 @@ int ksmbd_conn_wait_idle_sess_id(struct ksmbd_conn *curr_conn, u64 sess_id) down_read(&conn_list_lock); hash_for_each(conn_list, bkt, conn, hlist) { - if (conn->binding || xa_load(&conn->sessions, sess_id)) { + if (ksmbd_session_is_bound_to_conn(sess, conn)) { rcount = (conn == curr_conn) ? 2 : 1; if (atomic_read(&conn->req_running) >= rcount) { rc = wait_event_timeout(conn->req_running_q, @@ -780,8 +803,10 @@ static void stop_sessions(void) * handler exited its receive loop for an unrelated * reason). */ - if (READ_ONCE(conn->status) != KSMBD_SESS_RELEASING) + spin_lock(&conn->request_lock); + if (!ksmbd_conn_releasing(conn)) ksmbd_conn_set_exiting(conn); + spin_unlock(&conn->request_lock); target = conn; break; } diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 9ca03f9774d3..c01ccbe8b97c 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -23,6 +23,7 @@ #include "ksmbd_work.h" struct smbdirect_buffer_descriptor_v1; +struct ksmbd_session; #define KSMBD_SOCKET_BACKLOG 16 @@ -196,7 +197,8 @@ extern struct rw_semaphore conn_list_lock; bool ksmbd_conn_alive(struct ksmbd_conn *conn); void ksmbd_conn_wait_idle(struct ksmbd_conn *conn); -int ksmbd_conn_wait_idle_sess_id(struct ksmbd_conn *curr_conn, u64 sess_id); +int ksmbd_conn_wait_idle_sess(struct ksmbd_conn *curr_conn, + struct ksmbd_session *sess); struct ksmbd_conn *ksmbd_conn_alloc(void); void ksmbd_conn_free(struct ksmbd_conn *conn); struct ksmbd_conn *ksmbd_conn_get(struct ksmbd_conn *conn); @@ -310,5 +312,5 @@ static inline void ksmbd_conn_set_releasing(struct ksmbd_conn *conn) WRITE_ONCE(conn->status, KSMBD_SESS_RELEASING); } -void ksmbd_all_conn_set_status(u64 sess_id, u32 status); +void ksmbd_all_conn_set_status(struct ksmbd_session *sess, u32 status); #endif /* __CONNECTION_H__ */ diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 31eccad5d732..7e187d20828b 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -647,17 +647,17 @@ void destroy_previous_session(struct ksmbd_conn *conn, memcmp(user->passkey, prev_user->passkey, user->passkey_sz)) goto out; - ksmbd_all_conn_set_status(id, KSMBD_SESS_NEED_RECONNECT); - err = ksmbd_conn_wait_idle_sess_id(conn, id); + ksmbd_all_conn_set_status(prev_sess, KSMBD_SESS_NEED_RECONNECT); + err = ksmbd_conn_wait_idle_sess(conn, prev_sess); if (err) { - ksmbd_all_conn_set_status(id, KSMBD_SESS_NEED_SETUP); + ksmbd_all_conn_set_status(prev_sess, KSMBD_SESS_NEED_SETUP); goto out; } ksmbd_destroy_file_table(prev_sess); prev_sess->kerberos_expiry = 0; prev_sess->state = SMB2_SESSION_EXPIRED; - ksmbd_all_conn_set_status(id, KSMBD_SESS_NEED_SETUP); + ksmbd_all_conn_set_status(prev_sess, KSMBD_SESS_NEED_SETUP); ksmbd_launch_ksmbd_durable_scavenger(); out: up_write(&conn->session_lock); diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index aa662adaf63d..6581c79635fa 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -2899,7 +2899,6 @@ int smb2_session_logoff(struct ksmbd_work *work) struct ksmbd_session *sess = work->sess; struct smb2_logoff_req *req; struct smb2_logoff_rsp *rsp; - u64 sess_id; int err; WORK_BUFFERS(work, req, rsp); @@ -2913,8 +2912,7 @@ int smb2_session_logoff(struct ksmbd_work *work) smb2_set_err_rsp(work); return -ENOENT; } - sess_id = le64_to_cpu(req->hdr.SessionId); - ksmbd_all_conn_set_status(sess_id, KSMBD_SESS_NEED_RECONNECT); + ksmbd_all_conn_set_status(sess, KSMBD_SESS_NEED_RECONNECT); ksmbd_conn_unlock(conn); ksmbd_close_session_fds(work); @@ -2932,7 +2930,7 @@ int smb2_session_logoff(struct ksmbd_work *work) sess->state = SMB2_SESSION_EXPIRED; up_write(&conn->session_lock); - ksmbd_all_conn_set_status(sess_id, KSMBD_SESS_NEED_SETUP); + ksmbd_all_conn_set_status(sess, KSMBD_SESS_NEED_SETUP); rsp->StructureSize = cpu_to_le16(4); err = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_logoff_rsp)); From 2a0e037648da5695ab56dace74bda28eb6402b5f Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 13 Aug 2026 09:03:00 +0900 Subject: [PATCH 126/142] ksmbd: encrypt interim responses to encrypted requests The normal response path applies an SMB3 transform when the request was encrypted. Async interim responses, completed compound prefixes and two CHANGE_NOTIFY cleanup paths write their synthetic response work directly, bypassing that encryption step. A packet capture shows FE SMB2 STATUS_PENDING, CREATE and CHANGE_NOTIFY responses following FD SMB3 requests. The client resets the connection immediately after receiving those plaintext responses. Send synthetic interim work through a common helper that applies the session encryption transform first. A compound prefix shares the original work's response iov, which encryption would replace in place, so flatten it into an independently owned work before encrypting and sending it. Fixes: 64bfa9d49026 ("smb/server: use MSG_EOR for async interim response") Signed-off-by: Namjae Jeon --- fs/smb/server/smb2pdu.c | 71 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 6581c79635fa..ade16532a8c1 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1124,6 +1124,66 @@ void release_async_work(struct ksmbd_work *work) } } +static int smb2_send_interim_work(struct ksmbd_work *in_work, + struct ksmbd_work *work, bool eor) +{ + int err = 0; + + in_work->encrypted = work->encrypted; + if (work->encrypted && work->sess && work->sess->enc && + work->conn->ops->encrypt_resp) { + in_work->sess = work->sess; + err = work->conn->ops->encrypt_resp(in_work); + in_work->sess = NULL; + } + if (err) + return err; + + return eor ? ksmbd_conn_write_eor(in_work) : + ksmbd_conn_write(in_work); +} + +static int smb2_send_interim_prefix_work(struct ksmbd_work *work) +{ + struct ksmbd_work *in_work; + unsigned int len, copied = 0; + char *dst; + int err = -ENOMEM; + int i; + + len = get_rfc1002_len(work->iov[0].iov_base); + in_work = ksmbd_alloc_work_struct(); + if (!in_work) + return err; + + in_work->response_buf = kvzalloc(len + 4, KSMBD_DEFAULT_GFP); + if (!in_work->response_buf) + goto out; + in_work->response_sz = len + 4; + in_work->conn = work->conn; + dst = in_work->response_buf + 4; + for (i = 1; i <= work->iov_idx; i++) { + if (work->iov[i].iov_len > len - copied) { + err = -EINVAL; + goto out; + } + memcpy(dst + copied, work->iov[i].iov_base, + work->iov[i].iov_len); + copied += work->iov[i].iov_len; + } + if (copied != len) { + err = -EINVAL; + goto out; + } + + err = ksmbd_iov_pin_rsp(in_work, dst, len); + if (!err) + err = smb2_send_interim_work(in_work, work, true); +out: + ksmbd_free_work_struct(in_work); + return err; +} + static void smb2_send_interim_compound_prefix(struct ksmbd_work *work) { struct smb2_hdr *req_hdr; @@ -1152,7 +1212,7 @@ static void smb2_send_interim_compound_prefix(struct ksmbd_work *work) work->conn->ops->set_sign_rsp) work->conn->ops->set_sign_rsp(work); - err = ksmbd_conn_write_eor(work); + err = smb2_send_interim_prefix_work(work); if (err) ksmbd_debug(SMB, "failed to send compound interim prefix: %d\n", err); @@ -1193,7 +1253,8 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) smb2_set_err_rsp(in_work); rsp_hdr->Status = status; - ksmbd_conn_write_eor(in_work); + if (smb2_send_interim_work(in_work, work, true)) + ksmbd_debug(SMB, "failed to send interim response\n"); ksmbd_free_work_struct(in_work); } @@ -11310,7 +11371,8 @@ int smb2_notify(struct ksmbd_work *work) in_work->async_id = work->async_id; work->async_id = 0; release_async_work(work); - ksmbd_conn_write(in_work); + if (smb2_send_interim_work(in_work, work, false)) + ksmbd_debug(SMB, "failed to send notify cleanup\n"); ksmbd_free_work_struct(in_work); work->send_no_response = 1; return 0; @@ -11415,7 +11477,8 @@ int smb2_notify(struct ksmbd_work *work) in_work->cancel_fn = NULL; in_work->asynchronous = false; ksmbd_fd_put(work, fp); - ksmbd_conn_write(in_work); + if (smb2_send_interim_work(in_work, work, false)) + ksmbd_debug(SMB, "failed to send notify cleanup\n"); ksmbd_free_work_struct(in_work); work->send_no_response = 1; return 0; From 12a6680ce59bcd431730c9f049caddb017964c64 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 13 Aug 2026 17:05:49 +0900 Subject: [PATCH 127/142] ksmbd: disconnect on SMB3 decryption failure MS-SMB2 requires the server to disconnect a connection when an encrypted transform cannot be associated with a session or fails authenticated decryption. This includes an encrypted request that still carries a SessionId invalidated through PreviousSessionId. Move the connection to EXITING and shut down its transport when decrypt_req() fails. Add the missing TCP shutdown callback so a receive blocked in kernel_recvmsg() is released; SMB Direct already provides the corresponding callback. Plaintext requests using an invalidated SessionId do not take this path and continue to receive STATUS_USER_SESSION_DELETED. Fixes: e2f34481b24d ("cifsd: add server-side procedures for SMB3") Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 16 ++++++++++++++++ fs/smb/server/connection.h | 1 + fs/smb/server/server.c | 4 +++- fs/smb/server/transport_tcp.c | 6 ++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 71d55d903f6f..5d729473dd18 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -439,6 +439,22 @@ void ksmbd_all_conn_set_status(struct ksmbd_session *sess, u32 status) up_read(&conn_list_lock); } +void ksmbd_conn_abort(struct ksmbd_conn *conn) +{ + bool shutdown = false; + + spin_lock(&conn->request_lock); + if (!ksmbd_conn_exiting(conn) && !ksmbd_conn_releasing(conn)) { + ksmbd_conn_set_exiting(conn); + shutdown = true; + } + spin_unlock(&conn->request_lock); + wake_up_all(&conn->req_running_q); + + if (shutdown && conn->transport->ops->shutdown) + conn->transport->ops->shutdown(conn->transport); +} + void ksmbd_conn_wait_idle(struct ksmbd_conn *conn) { wait_event(conn->req_running_q, atomic_read(&conn->req_running) < 2); diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index c01ccbe8b97c..421907aed473 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -203,6 +203,7 @@ struct ksmbd_conn *ksmbd_conn_alloc(void); void ksmbd_conn_free(struct ksmbd_conn *conn); struct ksmbd_conn *ksmbd_conn_get(struct ksmbd_conn *conn); void ksmbd_conn_put(struct ksmbd_conn *conn); +void ksmbd_conn_abort(struct ksmbd_conn *conn); int ksmbd_conn_wq_init(void); void ksmbd_conn_wq_destroy(void); bool ksmbd_conn_lookup_dialect(struct ksmbd_conn *c); diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 7881fc7bb8cd..ba44bea9ddc3 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -188,8 +188,10 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, if (conn->ops->is_transform_hdr && conn->ops->is_transform_hdr(work->request_buf)) { rc = conn->ops->decrypt_req(work); - if (rc < 0) + if (rc < 0) { + ksmbd_conn_abort(conn); return; + } work->encrypted = true; } diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index 1045eca581c3..0ae5f145a332 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -435,6 +435,11 @@ static void ksmbd_tcp_disconnect(struct ksmbd_transport *t) atomic_dec(&active_num_conn); } +static void ksmbd_tcp_shutdown(struct ksmbd_transport *t) +{ + kernel_sock_shutdown(TCP_TRANS(t)->sock, SHUT_RDWR); +} + static void tcp_destroy_socket(struct socket *ksmbd_socket) { int ret; @@ -681,5 +686,6 @@ static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops = { .read = ksmbd_tcp_read, .writev = ksmbd_tcp_writev, .disconnect = ksmbd_tcp_disconnect, + .shutdown = ksmbd_tcp_shutdown, .free_transport = ksmbd_tcp_free_transport, }; From d8fc4fc7e57fc2bfd45699a10ee0df8ab9dccfa5 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 16 Aug 2026 13:46:36 +0900 Subject: [PATCH 128/142] ksmbd: decrypt requests from expired encrypted sessions Previous-session replacement marks the old session expired but retains its SMB3 encryption key. An in-flight encrypted request can still arrive on that connection. Rejecting the expired session before decryption made ksmbd treat the request as a key failure and abort the transport, causing reconnect failures. Allow key lookup for expired sessions that have encryption enabled. Keep the session reference during validation so the normal STATUS_USER_SESSION_DELETED response is encrypted with the old key. The session remains expired and no command is executed. Fixes: fa9415d4024f ("ksmbd: mark SMB2_SESSION_EXPIRED to session when destroying previous session") Signed-off-by: Namjae Jeon --- fs/smb/server/auth.c | 12 ++++++------ fs/smb/server/smb2pdu.c | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index bcd371f5550d..78491b20897e 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -724,15 +724,15 @@ static int ksmbd_get_encryption_key(struct ksmbd_work *work, __u64 ses_id, sess = work->sess; else { /* - * An encrypted SESSION_SETUP request may reauthenticate an expired - * Kerberos session. Keep using the established decryption key so - * that the command can reach the session setup handler. Other - * commands are rejected there with STATUS_NETWORK_SESSION_EXPIRED. + * A previous-session replacement leaves the old encryption key in + * place. Use it to authenticate an encrypted request, then let + * session validation reject the expired session. This preserves the + * encrypted STATUS_USER_SESSION_DELETED response without reviving + * the session. */ sess = ksmbd_session_lookup_all_states(work->conn, ses_id); if (sess && sess->state != SMB2_SESSION_VALID && - (sess->state != SMB2_SESSION_EXPIRED || - !sess->kerberos_expiry)) { + (sess->state != SMB2_SESSION_EXPIRED || !sess->enc)) { ksmbd_user_session_put(sess); sess = NULL; } diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index ade16532a8c1..8d06c934f24f 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1012,6 +1012,14 @@ int smb2_check_user_session(struct ksmbd_work *work) 1 : -EKEYEXPIRED; } if (work->sess->state != SMB2_SESSION_VALID) { + /* + * Keep the reference for an encrypted request so the caller can + * return STATUS_USER_SESSION_DELETED encrypted with the old key. + */ + if (work->encrypted && + work->sess->state == SMB2_SESSION_EXPIRED && + work->sess->enc) + return -ENOENT; ksmbd_user_session_put(work->sess); work->sess = NULL; return -ENOENT; From 6639b6928ba22dd6750ff3c6f6de77715cf46d50 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 16 Aug 2026 15:33:04 +0900 Subject: [PATCH 129/142] ksmbd: handle encrypted compressed requests SMB3 permits a message to be compressed before it is encrypted. After decrypting such a request, ksmbd must trim the AEAD tag using OriginalMessageSize, decompress the nested compression transform, and validate the resulting SMB2 PDU. Share the decompression helper between the connection receive path and the post-decryption work path so unencrypted and encrypted compressed requests follow the same validation. Fixes: a08de24c2b85 ("ksmbd: negotiate and decode SMB2 compression") Signed-off-by: Namjae Jeon --- fs/smb/server/compress.c | 76 ++++++++++++++++++++++++++++++---------- fs/smb/server/compress.h | 1 + fs/smb/server/server.c | 22 ++++++++++++ fs/smb/server/smb2pdu.c | 22 ++++++++---- 4 files changed, 97 insertions(+), 24 deletions(-) diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c index 01d1771ff663..5162fb84c755 100644 --- a/fs/smb/server/compress.c +++ b/fs/smb/server/compress.c @@ -14,24 +14,14 @@ #define SMB_COMPRESS_MIN_LEN PAGE_SIZE -/** - * ksmbd_decompress_request() - replace a compressed request with its SMB2 PDU - * @conn: connection which owns the current RFC1002 request buffer - * - * Derive the uncompressed size from the transform variant, enforce ksmbd's - * normal message limits, and ask the common decoder to validate every payload. - * On success, replace conn->request_buf with a regular RFC1002-framed SMB2 - * message so the rest of the request path needs no compression awareness. - * - * Return: 0 on success, otherwise a negative errno. - */ -int ksmbd_decompress_request(struct ksmbd_conn *conn) +static int __ksmbd_decompress_request(struct ksmbd_conn *conn, + void *request_buf, void **out_buf) { struct smb2_compression_hdr *hdr; - unsigned int pdu_size = get_rfc1002_len(conn->request_buf); + unsigned int pdu_size = get_rfc1002_len(request_buf); u32 orig_size, offset, out_size; u32 max_allowed_pdu_size; - char *buf, *out; + char *out; int rc; if (pdu_size < sizeof(struct smb2_compression_hdr)) @@ -41,7 +31,7 @@ int ksmbd_decompress_request(struct ksmbd_conn *conn) conn->compress_algorithm == SMB3_COMPRESS_NONE) return -EINVAL; - hdr = smb_get_msg(conn->request_buf); + hdr = smb_get_msg(request_buf); if (hdr->ProtocolId != SMB2_COMPRESSION_TRANSFORM_ID) return -EINVAL; @@ -74,19 +64,69 @@ int ksmbd_decompress_request(struct ksmbd_conn *conn) if (!out) return -ENOMEM; - buf = (char *)hdr; *(__be32 *)out = cpu_to_be32(out_size); rc = smb_compression_decompress(conn->compress_algorithm, conn->compress_chained, conn->compress_pattern, - buf, pdu_size, out + 4, out_size); + (char *)hdr, pdu_size, out + 4, out_size); if (rc) { kvfree(out); return rc; } + *out_buf = out; + return 0; +} + +/** + * 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) +{ + void *out_buf; + int rc; + + rc = __ksmbd_decompress_request(conn, conn->request_buf, &out_buf); + if (rc) + return rc; + kvfree(conn->request_buf); - conn->request_buf = out; + conn->request_buf = out_buf; + return 0; +} + +/** + * ksmbd_decompress_work_request() - decompress an encrypted work request + * @work: work item whose request buffer contains a compression transform + * + * SMB3 encrypts a compressed message by applying compression first and + * encryption second. The receive loop can therefore only decode the + * compression transform before work allocation for an unencrypted request; + * an encrypted request must be decompressed after its encryption layer has + * been removed. + * + * Return: 0 on success, otherwise a negative errno. + */ +int ksmbd_decompress_work_request(struct ksmbd_work *work) +{ + void *out_buf; + int rc; + + rc = __ksmbd_decompress_request(work->conn, work->request_buf, + &out_buf); + if (rc) + return rc; + + kvfree(work->request_buf); + work->request_buf = out_buf; return 0; } diff --git a/fs/smb/server/compress.h b/fs/smb/server/compress.h index 663c6f44f09b..13df2eb221e8 100644 --- a/fs/smb/server/compress.h +++ b/fs/smb/server/compress.h @@ -11,6 +11,7 @@ #include "../common/compress/compress.h" int ksmbd_decompress_request(struct ksmbd_conn *conn); +int ksmbd_decompress_work_request(struct ksmbd_work *work); int ksmbd_compress_response(struct ksmbd_work *work); #endif /* __KSMBD_COMPRESS_H__ */ diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index ba44bea9ddc3..6cfe8148da85 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -193,6 +193,28 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, return; } work->encrypted = true; + + /* + * SMB3 applies compression before encryption. The receive loop + * handles a plain compression transform before allocating work, but + * an encrypted request exposes that transform only after decryption. + */ + if (((struct smb2_hdr *)smb_get_msg(work->request_buf))->ProtocolId == + SMB2_COMPRESSION_TRANSFORM_ID) { + rc = ksmbd_decompress_work_request(work); + if (rc < 0) { + ksmbd_conn_abort(conn); + return; + } + } + + /* The decrypted payload must now be a complete SMB2 request. */ + if (((struct smb2_hdr *)smb_get_msg(work->request_buf))->ProtocolId != + SMB2_PROTO_NUMBER || + get_rfc1002_len(work->request_buf) < sizeof(struct smb2_pdu)) { + ksmbd_conn_abort(conn); + return; + } } if (conn->ops->allocate_rsp_buf(work)) diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 8d06c934f24f..a564535132e5 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -11817,18 +11817,27 @@ int smb3_decrypt_req(struct ksmbd_work *work) char *buf = work->request_buf; unsigned int pdu_length = get_rfc1002_len(buf); struct kvec iov[2]; - int buf_data_size = pdu_length - sizeof(struct smb2_transform_hdr); + unsigned int buf_data_size; struct smb2_transform_hdr *tr_hdr = smb_get_msg(buf); + unsigned int original_msg_size; int rc = 0; - if (pdu_length < sizeof(struct smb2_transform_hdr) || - buf_data_size < sizeof(struct smb2_hdr)) { + if (pdu_length < sizeof(struct smb2_transform_hdr)) { pr_err("Transform message is too small (%u)\n", pdu_length); return -ECONNABORTED; } - if (buf_data_size < le32_to_cpu(tr_hdr->OriginalMessageSize)) { + buf_data_size = pdu_length - sizeof(struct smb2_transform_hdr); + original_msg_size = le32_to_cpu(tr_hdr->OriginalMessageSize); + if (buf_data_size < sizeof(struct smb2_compression_hdr) || + original_msg_size < sizeof(struct smb2_compression_hdr)) { + pr_err("Transform message is too small (%u)\n", + pdu_length); + return -ECONNABORTED; + } + + if (buf_data_size < original_msg_size) { pr_err("Transform message is broken\n"); return -ECONNABORTED; } @@ -11841,8 +11850,9 @@ int smb3_decrypt_req(struct ksmbd_work *work) if (rc) return rc; - memmove(buf + 4, iov[1].iov_base, buf_data_size); - *(__be32 *)buf = cpu_to_be32(buf_data_size); + /* Drop the AEAD authentication tag from the inner RFC1002 frame. */ + memmove(buf + 4, iov[1].iov_base, original_msg_size); + *(__be32 *)buf = cpu_to_be32(original_msg_size); return rc; } From 05c978948ea55715c17785c4e81ff64bedc88429 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Sun, 16 Aug 2026 17:27:28 +0900 Subject: [PATCH 130/142] ksmbd: add SMB Direct RDMA encryption transform Port SMB Direct RDMA payload encryption support to the current ksmbd tree. The current tree already supports all-state lookup for encrypted expired sessions, so the overlapping lookup hunk from the original patch is intentionally omitted. Signed-off-by: Namjae Jeon --- fs/smb/client/smb2pdu.h | 24 -- fs/smb/common/smb2pdu.h | 22 ++ fs/smb/common/smb2status.h | 1 + fs/smb/server/auth.c | 158 +++++++++++ fs/smb/server/auth.h | 4 + fs/smb/server/connection.c | 2 + fs/smb/server/connection.h | 3 + fs/smb/server/smb2pdu.c | 500 ++++++++++++++++++++++++++++++--- fs/smb/server/transport_rdma.c | 10 + fs/smb/server/transport_rdma.h | 2 + 10 files changed, 663 insertions(+), 63 deletions(-) diff --git a/fs/smb/client/smb2pdu.h b/fs/smb/client/smb2pdu.h index b9bf2fa989d5..ab6c667bebc0 100644 --- a/fs/smb/client/smb2pdu.h +++ b/fs/smb/client/smb2pdu.h @@ -21,30 +21,6 @@ /* The total header size for SMB2 read and write */ #define SMB2_READWRITE_PDU_HEADER_SIZE (48 + sizeof(struct smb2_hdr)) -/* See MS-SMB2 2.2.43 */ -struct smb2_rdma_transform { - __le16 RdmaDescriptorOffset; - __le16 RdmaDescriptorLength; - __le32 Channel; /* for values see channel description in smb2 read above */ - __le16 TransformCount; - __le16 Reserved1; - __le32 Reserved2; -} __packed; - -/* TransformType */ -#define SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION 0x0001 -#define SMB2_RDMA_TRANSFORM_TYPE_SIGNING 0x0002 - -struct smb2_rdma_crypto_transform { - __le16 TransformType; - __le16 SignatureLength; - __le16 NonceLength; - __u16 Reserved; - __u8 Signature[]; /* variable length */ - /* u8 Nonce[] */ - /* followed by padding */ -} __packed; - /* * Definitions for SMB2 Protocol Data Units (network frames) * diff --git a/fs/smb/common/smb2pdu.h b/fs/smb/common/smb2pdu.h index d9650aff0d3c..f9a8862cb3d4 100644 --- a/fs/smb/common/smb2pdu.h +++ b/fs/smb/common/smb2pdu.h @@ -743,6 +743,28 @@ struct smb2_close_rsp { #define SMB2_CHANNEL_RDMA_V1_INVALIDATE cpu_to_le32(0x00000002) #define SMB2_CHANNEL_RDMA_TRANSFORM cpu_to_le32(0x00000003) +/* See MS-SMB2 2.2.43. */ +struct smb2_rdma_transform { + __le16 RdmaDescriptorOffset; + __le16 RdmaDescriptorLength; + __le32 Channel; + __le16 TransformCount; + __le16 Reserved1; + __le32 Reserved2; +} __packed; + +#define SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION 0x0001 +#define SMB2_RDMA_TRANSFORM_TYPE_SIGNING 0x0002 + +struct smb2_rdma_crypto_transform { + __le16 TransformType; + __le16 SignatureLength; + __le16 NonceLength; + __le16 Reserved; + __u8 Signature[]; + /* Followed by Nonce[] and optional alignment padding. */ +} __packed; + /* SMB2 read request without RFC1001 length at the beginning */ struct smb2_read_req { struct smb2_hdr hdr; diff --git a/fs/smb/common/smb2status.h b/fs/smb/common/smb2status.h index b6421bc5113c..2989c3a5cb67 100644 --- a/fs/smb/common/smb2status.h +++ b/fs/smb/common/smb2status.h @@ -1049,6 +1049,7 @@ struct ntstatus { #define STATUS_WOW_ASSERTION cpu_to_le32(0xC0009898) // -EIO #define STATUS_INVALID_SIGNATURE cpu_to_le32(0xC000A000) // -EIO #define STATUS_HMAC_NOT_SUPPORTED cpu_to_le32(0xC000A001) // -EIO +#define STATUS_AUTH_TAG_MISMATCH cpu_to_le32(0xC000A002) // -EBADMSG #define STATUS_IPSEC_QUEUE_OVERFLOW cpu_to_le32(0xC000A010) // -EIO #define STATUS_ND_QUEUE_OVERFLOW cpu_to_le32(0xC000A011) // -EIO #define STATUS_HOPLIMIT_EXCEEDED cpu_to_le32(0xC000A012) // -EIO diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index 78491b20897e..db362c64af8d 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -833,6 +833,164 @@ static struct scatterlist *ksmbd_init_sg(struct kvec *iov, unsigned int nvec, return sg; } +/** + * ksmbd_init_rdma_sg() - build an AEAD scatterlist for an RDMA payload + * @buf: payload buffer + * @buflen: payload length + * @tag: authentication tag buffer + * @taglen: authentication tag length + * + * Split vmalloc-backed payloads at page boundaries and append the detached + * authentication tag as the final scatterlist entry. + * + * Return: allocated scatterlist, or NULL on allocation failure + */ +static struct scatterlist *ksmbd_init_rdma_sg(void *buf, + unsigned int buflen, + u8 *tag, + unsigned int taglen) +{ + struct scatterlist *sg; + unsigned int nr_data = 1, nr_entries, i = 0; + void *data = buf; + int len = buflen; + + if (is_vmalloc_addr(buf)) + nr_data = DIV_ROUND_UP(offset_in_page(buf) + buflen, PAGE_SIZE); + nr_entries = nr_data + 1; + + sg = kmalloc_objs(struct scatterlist, nr_entries, KSMBD_DEFAULT_GFP); + if (!sg) + return NULL; + + sg_init_table(sg, nr_entries); + if (!is_vmalloc_addr(buf)) { + smb2_sg_set_buf(&sg[i++], buf, buflen); + } else { + while (len) { + unsigned int bytes = min_t(unsigned int, + PAGE_SIZE - offset_in_page(data), len); + + sg_set_page(&sg[i++], vmalloc_to_page(data), bytes, + offset_in_page(data)); + data += bytes; + len -= bytes; + } + } + smb2_sg_set_buf(&sg[i], tag, taglen); + return sg; +} + +/** + * ksmbd_crypt_rdma() - encrypt or decrypt an SMB Direct data buffer + * @conn: connection containing the negotiated cipher + * @key: session encryption or decryption key + * @buf: RDMA payload, transformed in place + * @buflen: payload length (the authentication tag is carried out of band) + * @nonce: transform nonce + * @nonce_len: nonce length + * @tag: authentication tag output for encryption, input for decryption + * @tag_len: authentication tag length + * @enc: true to encrypt, false to decrypt + * + * SMB2_RDMA_CRYPTO_TRANSFORM carries the nonce and authentication tag in the + * SMB2 message while only the payload is transferred through RDMA. Therefore + * this uses AEAD without the normal SMB3 transform header as associated data. + * + * Return: 0 on success, otherwise a negative errno + */ +int ksmbd_crypt_rdma(struct ksmbd_conn *conn, const u8 *key, + void *buf, unsigned int buflen, const u8 *nonce, + unsigned int nonce_len, u8 *tag, unsigned int tag_len, + bool enc) +{ + struct ksmbd_crypto_ctx *ctx; + struct crypto_aead *tfm; + struct aead_request *req = NULL; + struct scatterlist *sg = NULL; + unsigned int iv_len, crypt_len; + u8 auth_tag[SMB2_SIGNATURE_SIZE] = {}; + u8 *iv = NULL; + int rc; + DECLARE_CRYPTO_WAIT(wait); + + if (!buflen || !tag_len || tag_len > SMB2_SIGNATURE_SIZE) + return -EINVAL; + if (!enc) + memcpy(auth_tag, tag, tag_len); + + if (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) { + if (nonce_len != SMB3_AES_GCM_NONCE) + return -EINVAL; + ctx = ksmbd_crypto_ctx_find_gcm(); + } else { + if (nonce_len != SMB3_AES_CCM_NONCE) + return -EINVAL; + ctx = ksmbd_crypto_ctx_find_ccm(); + } + if (!ctx) + return -ENOMEM; + + tfm = (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) ? + CRYPTO_GCM(ctx) : CRYPTO_CCM(ctx); + if (conn->cipher_type == SMB2_ENCRYPTION_AES256_CCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) + rc = crypto_aead_setkey(tfm, key, SMB3_GCM256_CRYPTKEY_SIZE); + else + rc = crypto_aead_setkey(tfm, key, SMB3_GCM128_CRYPTKEY_SIZE); + if (rc) + goto out; + + rc = crypto_aead_setauthsize(tfm, tag_len); + if (rc) + goto out; + + req = aead_request_alloc(tfm, KSMBD_DEFAULT_GFP); + if (!req) { + rc = -ENOMEM; + goto out; + } + + sg = ksmbd_init_rdma_sg(buf, buflen, auth_tag, tag_len); + if (!sg) { + rc = -ENOMEM; + goto out; + } + + iv_len = crypto_aead_ivsize(tfm); + iv = kzalloc(iv_len, KSMBD_DEFAULT_GFP); + if (!iv) { + rc = -ENOMEM; + goto out; + } + if (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) { + memcpy(iv, nonce, nonce_len); + } else { + iv[0] = 3; + memcpy(iv + 1, nonce, nonce_len); + } + + crypt_len = buflen + (enc ? 0 : tag_len); + aead_request_set_crypt(req, sg, sg, crypt_len, iv); + aead_request_set_ad(req, 0); + aead_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG | + CRYPTO_TFM_REQ_MAY_SLEEP, + crypto_req_done, &wait); + rc = crypto_wait_req(enc ? crypto_aead_encrypt(req) : + crypto_aead_decrypt(req), &wait); + if (!rc && enc) + memcpy(tag, auth_tag, tag_len); +out: + kfree(iv); + kfree(sg); + aead_request_free(req); + ksmbd_release_crypto_ctx(ctx); + return rc; +} + int ksmbd_crypt_message(struct ksmbd_work *work, struct kvec *iov, unsigned int nvec, int enc) { diff --git a/fs/smb/server/auth.h b/fs/smb/server/auth.h index f14b7c033264..7ce9c42d58f1 100644 --- a/fs/smb/server/auth.h +++ b/fs/smb/server/auth.h @@ -38,6 +38,10 @@ struct kvec; int ksmbd_crypt_message(struct ksmbd_work *work, struct kvec *iov, unsigned int nvec, int enc); +int ksmbd_crypt_rdma(struct ksmbd_conn *conn, const u8 *key, + void *buf, unsigned int buflen, const u8 *nonce, + unsigned int nonce_len, u8 *tag, unsigned int tag_len, + bool enc); void ksmbd_copy_gss_neg_header(void *buf); int ksmbd_auth_ntlmv2(struct ksmbd_conn *conn, struct ksmbd_session *sess, struct ntlmv2_resp *ntlmv2, int blen, char *domain_name, diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 5d729473dd18..d32f4f3cef93 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -77,6 +77,8 @@ static void proc_show_conn_features(struct seq_file *m, proc_show_conn_feature(m, &separator, conn->compress_algorithm != SMB3_COMPRESS_NONE, "compress"); + proc_show_conn_feature(m, &separator, conn->rdma_transform_ids, + "rdma-transform"); proc_show_conn_feature(m, &separator, conn->posix_ext_supported, "posix"); if (!separator) seq_puts(m, "none"); diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 421907aed473..63484c8efbbd 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -139,6 +139,9 @@ struct ksmbd_conn { /* Negotiated SMB 3.1.1 compression capabilities. */ bool compress_chained; bool compress_pattern; + /* Bitmap indexed by SMB2_RDMA_TRANSFORM_* IDs. */ + unsigned long rdma_transform_ids; + bool rdma_transform_negotiated; 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 a564535132e5..b48eff02dbf8 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1356,6 +1356,37 @@ static void build_compress_ctxt(struct smb2_compression_capabilities_context *pn pneg_ctxt->CompressionAlgorithms[3] = 0; } +/** + * build_rdma_ctx() - build an RDMA transform negotiate response context + * @ctxt: response context header to populate + * @transform_ids: bitmap of transforms common to the client and server + * + * Return: encoded negotiate context length + */ +static int build_rdma_ctx(struct smb2_neg_context *ctxt, + unsigned long transform_ids) +{ + struct smb2_rdma_transform_capabilities_context *pneg_ctxt; + int count = 0; + + pneg_ctxt = (void *)ctxt; + pneg_ctxt->ContextType = SMB2_RDMA_TRANSFORM_CAPABILITIES; + pneg_ctxt->Reserved = 0; + pneg_ctxt->Reserved1 = 0; + pneg_ctxt->Reserved2 = 0; + if (transform_ids & BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION)) + pneg_ctxt->RDMATransformIds[count++] = + cpu_to_le16(SMB2_RDMA_TRANSFORM_ENCRYPTION); + if (!count) + pneg_ctxt->RDMATransformIds[count++] = + cpu_to_le16(SMB2_RDMA_TRANSFORM_NONE); + + pneg_ctxt->TransformCount = cpu_to_le16(count); + pneg_ctxt->DataLength = cpu_to_le16(8 + count * sizeof(__le16)); + return sizeof(struct smb2_neg_context) + + le16_to_cpu(pneg_ctxt->DataLength); +} + static void build_sign_cap_ctxt(struct smb2_signing_capabilities *pneg_ctxt, __le16 sign_algo) { @@ -1431,6 +1462,18 @@ static unsigned int assemble_neg_contexts(struct ksmbd_conn *conn, (conn->compress_pattern ? 12 : 10); } + if (conn->rdma_transform_negotiated) { + struct smb2_neg_context *rdma_ctxt; + + ctxt_size = round_up(ctxt_size, 8); + ksmbd_debug(SMB, + "assemble SMB2_RDMA_TRANSFORM_CAPABILITIES context\n"); + rdma_ctxt = (void *)(pneg_ctxt + ctxt_size); + ctxt_size += build_rdma_ctx(rdma_ctxt, + conn->rdma_transform_ids); + neg_ctxt_cnt++; + } + if (conn->posix_ext_supported) { ctxt_size = round_up(ctxt_size, 8); ksmbd_debug(SMB, @@ -1631,6 +1674,46 @@ static void decode_sign_cap_ctxt(struct ksmbd_conn *conn, } } +/** + * decode_rdma_ctx() - decode an RDMA transform negotiate request context + * @conn: connection being negotiated + * @ctxt: request context header to decode + * @ctxt_len: total context length, including the negotiate context header + * + * Record transforms supported by both peers only for SMB Direct connections. + * + * Return: NT status describing the decode result + */ +static __le32 decode_rdma_ctx(struct ksmbd_conn *conn, + struct smb2_neg_context *ctxt, int ctxt_len) +{ + struct smb2_rdma_transform_capabilities_context *pneg_ctxt; + unsigned int count, i; + + pneg_ctxt = (void *)ctxt; + /* RDMA transforms are a node capability, not just a transport capability. */ + if (!ksmbd_rdma_enabled()) + return STATUS_SUCCESS; + + if (ctxt_len < sizeof(*pneg_ctxt)) + return STATUS_INVALID_PARAMETER; + + count = le16_to_cpu(pneg_ctxt->TransformCount); + if (!count || count > + (ctxt_len - sizeof(*pneg_ctxt)) / sizeof(__le16)) + return STATUS_INVALID_PARAMETER; + + conn->rdma_transform_negotiated = true; + conn->rdma_transform_ids = 0; + for (i = 0; i < count; i++) { + u16 id = le16_to_cpu(pneg_ctxt->RDMATransformIds[i]); + + if (id == SMB2_RDMA_TRANSFORM_ENCRYPTION) + conn->rdma_transform_ids |= BIT(id); + } + return STATUS_SUCCESS; +} + static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, struct smb2_negotiate_req *req, unsigned int len_of_smb) @@ -1641,7 +1724,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; + int compress_ctxt_cnt = 0, rdma_transform_ctxt_cnt = 0; ksmbd_debug(SMB, "decoding %d negotiate contexts\n", neg_ctxt_cnt); if (len_of_smb <= offset) { @@ -1700,6 +1783,17 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, } else if (pctx->ContextType == SMB2_NETNAME_NEGOTIATE_CONTEXT_ID) { ksmbd_debug(SMB, "deassemble SMB2_NETNAME_NEGOTIATE_CONTEXT_ID context\n"); + } else if (pctx->ContextType == SMB2_RDMA_TRANSFORM_CAPABILITIES) { + ksmbd_debug(SMB, + "deassemble SMB2_RDMA_TRANSFORM_CAPABILITIES context\n"); + if (ksmbd_rdma_enabled() && + rdma_transform_ctxt_cnt++) { + status = STATUS_INVALID_PARAMETER; + break; + } + status = decode_rdma_ctx(conn, pctx, ctxt_len); + if (status != STATUS_SUCCESS) + break; } else if (pctx->ContextType == SMB2_POSIX_EXTENSIONS_AVAILABLE) { ksmbd_debug(SMB, "deassemble SMB2_POSIX_EXTENSIONS_AVAILABLE context\n"); @@ -1807,6 +1901,9 @@ int smb2_handle_negotiate(struct ksmbd_work *work) conn->preauth_info = NULL; goto err_out; } + if (!conn->cipher_type) + conn->rdma_transform_ids &= + ~BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION); rc = init_smb3_11_server(conn); if (rc < 0) { @@ -8553,18 +8650,31 @@ static noinline int smb2_read_pipe(struct ksmbd_work *work) return err; } -static int smb2_set_remote_key_for_rdma(struct ksmbd_work *work, - struct smbdirect_buffer_descriptor_v1 *desc, - __le32 Channel, - __le16 ChannelInfoLength) +/** + * smb2_set_rdma_key() - validate descriptors and save invalidation state + * @work: request work item + * @desc: first RDMA buffer descriptor + * @Channel: nested RDMA channel type + * @channel_info_len: descriptor array length + * + * Return: 0 on success, otherwise -EINVAL + */ +static int smb2_set_rdma_key(struct ksmbd_work *work, + struct smbdirect_buffer_descriptor_v1 *desc, + __le32 Channel, __le16 channel_info_len) { unsigned int i, ch_count; + if (Channel != SMB2_CHANNEL_RDMA_V1 && + Channel != SMB2_CHANNEL_RDMA_V1_INVALIDATE) + return -EINVAL; if (work->conn->dialect == SMB30_PROT_ID && Channel != SMB2_CHANNEL_RDMA_V1) return -EINVAL; + if (le16_to_cpu(channel_info_len) % sizeof(*desc)) + return -EINVAL; - ch_count = le16_to_cpu(ChannelInfoLength) / sizeof(*desc); + ch_count = le16_to_cpu(channel_info_len) / sizeof(*desc); if (ksmbd_debug_types & KSMBD_DEBUG_RDMA) { for (i = 0; i < ch_count; i++) { pr_info("RDMA r/w request %#x: token %#x, length %#x\n", @@ -8583,9 +8693,223 @@ static int smb2_set_remote_key_for_rdma(struct ksmbd_work *work, return 0; } -static ssize_t smb2_read_rdma_channel(struct ksmbd_work *work, - struct smb2_read_req *req, void *data_buf, - size_t length) +/** + * smb2_prep_rdma_read() - transform an RDMA READ payload + * @work: request work item + * @req: READ request controlling encryption or signing + * @rsp: READ response receiving transform metadata + * @data: data that will be transferred through RDMA + * @datalen: data length + * + * Encrypt the payload in place and encode the detached crypto metadata in + * the response buffer. + * + * Return: metadata length, zero when no transform applies, or negative errno + */ +static int smb2_prep_rdma_read(struct ksmbd_work *work, + struct smb2_read_req *req, + struct smb2_read_rsp *rsp, + void *data, unsigned int datalen) +{ + struct ksmbd_conn *conn = work->conn; + struct smb2_rdma_transform *transform; + struct smb2_rdma_crypto_transform *crypto; + u8 *nonce; + unsigned int nonce_len = 0, transform_len; + u16 transform_type; + int err; + + if (work->encrypted && + (conn->rdma_transform_ids & BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION))) { + transform_type = SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION; + nonce_len = (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) ? + SMB3_AES_GCM_NONCE : SMB3_AES_CCM_NONCE; + } else { + return 0; + } + + transform = (struct smb2_rdma_transform *)rsp->Buffer; + crypto = (struct smb2_rdma_crypto_transform *)(transform + 1); + memset(transform, 0, sizeof(*transform) + sizeof(*crypto) + + SMB2_SIGNATURE_SIZE + nonce_len); + transform->Channel = SMB2_CHANNEL_NONE; + transform->TransformCount = cpu_to_le16(1); + + crypto->TransformType = cpu_to_le16(transform_type); + crypto->SignatureLength = cpu_to_le16(SMB2_SIGNATURE_SIZE); + crypto->NonceLength = cpu_to_le16(nonce_len); + nonce = crypto->Signature + SMB2_SIGNATURE_SIZE; + + get_random_bytes(nonce, nonce_len); + err = ksmbd_crypt_rdma(conn, + work->sess->smb3encryptionkey, + data, datalen, nonce, nonce_len, + crypto->Signature, + SMB2_SIGNATURE_SIZE, true); + if (err) + return err; + + transform_len = sizeof(*transform) + sizeof(*crypto) + + SMB2_SIGNATURE_SIZE + nonce_len; + rsp->Flags = SMB2_READFLAG_RESPONSE_RDMA_TRANSFORM; + rsp->DataLength = cpu_to_le32(transform_len); + return transform_len; +} + +struct smb2_rdma_write_transform { + struct smbdirect_buffer_descriptor_v1 *desc; + struct smb2_rdma_crypto_transform *crypto; + u8 *nonce; + unsigned int desc_len; + unsigned int nonce_len; + unsigned int signature_len; + u16 type; + __le32 channel; +}; + +/** + * smb2_current_req_len() - return the current compound request element size + * @work: request work item + * @hdr: current SMB2 header + * + * Return: current request element length measured from the SMB2 header + */ +static unsigned int smb2_current_req_len(struct ksmbd_work *work, + struct smb2_hdr *hdr) +{ + if (hdr->NextCommand) + return le32_to_cpu(hdr->NextCommand); + return get_rfc1002_len(work->request_buf) - + work->next_smb2_rcv_hdr_off; +} + +/** + * check_rdma_desc() - validate an RDMA descriptor array + * @desc: descriptor array + * @desc_len: descriptor array length + * @required_len: minimum aggregate buffer length + * + * Return: 0 when the descriptors cover the transfer, otherwise -EINVAL + */ +static int check_rdma_desc(struct smbdirect_buffer_descriptor_v1 *desc, + unsigned int desc_len, + unsigned int required_len) +{ + unsigned int i, count; + u64 described_len = 0; + + if (!desc_len || desc_len % sizeof(*desc)) + return -EINVAL; + count = desc_len / sizeof(*desc); + if (!le32_to_cpu(desc[0].length)) + return -EINVAL; + for (i = 0; i < count; i++) + described_len += le32_to_cpu(desc[i].length); + return described_len < required_len ? -EINVAL : 0; +} + +/** + * smb2_parse_rdma_write_transform() - validate RDMA WRITE transform metadata + * @work: request work item + * @req: WRITE request containing the transform + * @info: parsed transform information + * + * Validate transform counts, crypto fields, descriptor alignment and bounds, + * negotiated algorithms, and the nested RDMA channel. + * + * Return: 0 on success, otherwise a negative errno + */ +static int smb2_parse_rdma_write_transform(struct ksmbd_work *work, + struct smb2_write_req *req, + struct smb2_rdma_write_transform *info) +{ + struct smb2_rdma_transform *transform; + struct smb2_rdma_crypto_transform *crypto; + unsigned int req_len = smb2_current_req_len(work, &req->hdr); + unsigned int offset = le16_to_cpu(req->WriteChannelInfoOffset); + unsigned int length = le16_to_cpu(req->WriteChannelInfoLength); + unsigned int desc_offset, desc_len, crypto_len, expected_desc_offset; + + if (!work->conn->rdma_transform_ids || + offset < offsetof(struct smb2_write_req, Buffer) || + length < sizeof(*transform) || offset > req_len || + length > req_len - offset) + return -EINVAL; + + transform = (struct smb2_rdma_transform *)((char *)req + offset); + if (le16_to_cpu(transform->TransformCount) != 1 || + (transform->Channel != SMB2_CHANNEL_RDMA_V1 && + transform->Channel != SMB2_CHANNEL_RDMA_V1_INVALIDATE)) + return -EINVAL; + + desc_offset = le16_to_cpu(transform->RdmaDescriptorOffset); + desc_len = le16_to_cpu(transform->RdmaDescriptorLength); + if (!desc_len || desc_len % sizeof(*info->desc) || + desc_offset < sizeof(*transform) || desc_offset > length || + desc_len > length - desc_offset) + return -EINVAL; + + crypto = (struct smb2_rdma_crypto_transform *)(transform + 1); + if (length - sizeof(*transform) < sizeof(*crypto)) + return -EINVAL; + info->type = le16_to_cpu(crypto->TransformType); + info->signature_len = le16_to_cpu(crypto->SignatureLength); + info->nonce_len = le16_to_cpu(crypto->NonceLength); + if (!info->signature_len) + return info->type == SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION ? + -EBADMSG : -EINVAL; + if (info->signature_len > SMB2_SIGNATURE_SIZE) + return info->type == SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION ? + -EBADMSG : -EINVAL; + if (info->signature_len > length - sizeof(*transform) - sizeof(*crypto) || + info->nonce_len > length - sizeof(*transform) - sizeof(*crypto) - + info->signature_len) + return -EINVAL; + + crypto_len = sizeof(*crypto) + info->signature_len + info->nonce_len; + expected_desc_offset = ALIGN(sizeof(*transform) + crypto_len, 8); + if (desc_offset != expected_desc_offset) + return -EINVAL; + + if (info->type == SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION) { + unsigned int expected_nonce_len; + + if (!(work->conn->rdma_transform_ids & + BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION)) || !work->encrypted) + return -EINVAL; + expected_nonce_len = + (work->conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + work->conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) ? + SMB3_AES_GCM_NONCE : SMB3_AES_CCM_NONCE; + if (info->nonce_len != expected_nonce_len) + return -EBADMSG; + } else { + return -EINVAL; + } + + info->desc = (struct smbdirect_buffer_descriptor_v1 *) + ((char *)transform + desc_offset); + info->desc_len = desc_len; + info->crypto = crypto; + info->nonce = crypto->Signature + info->signature_len; + info->channel = transform->Channel; + return check_rdma_desc(info->desc, info->desc_len, + le32_to_cpu(req->RemainingBytes)); +} + +/** + * smb2_read_rdma() - transfer READ data to client RDMA buffers + * @work: request work item + * @req: READ request containing client descriptors + * @data_buf: data to transfer + * @length: data length + * + * Return: transferred length on success, otherwise a negative errno + */ +static ssize_t smb2_read_rdma(struct ksmbd_work *work, + struct smb2_read_req *req, void *data_buf, + size_t length) { int err; @@ -8615,6 +8939,7 @@ int smb2_read(struct ksmbd_work *work) size_t length, mincount; ssize_t nbytes = 0, remain_bytes = 0; int err = 0; + int rdma_transform_len = 0; bool is_rdma_channel = false, async_interim = false; unsigned int max_read_size = conn->vals->max_read_size; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; @@ -8649,6 +8974,12 @@ int smb2_read(struct ksmbd_work *work) pid = req->PersistentFileId; } + if (req->Channel != SMB2_CHANNEL_NONE && + req->Channel != SMB2_CHANNEL_RDMA_V1 && + req->Channel != SMB2_CHANNEL_RDMA_V1_INVALIDATE) { + err = -EINVAL; + goto out; + } if (req->Channel == SMB2_CHANNEL_RDMA_V1_INVALIDATE || req->Channel == SMB2_CHANNEL_RDMA_V1) { is_rdma_channel = true; @@ -8661,16 +8992,24 @@ int smb2_read(struct ksmbd_work *work) if (is_rdma_channel == true) { unsigned int ch_offset = le16_to_cpu(req->ReadChannelInfoOffset); + unsigned int ch_len = le16_to_cpu(req->ReadChannelInfoLength); + unsigned int req_len = smb2_current_req_len(work, &req->hdr); + struct smbdirect_buffer_descriptor_v1 *desc; - if (ch_offset < offsetof(struct smb2_read_req, Buffer)) { + if (!le32_to_cpu(req->Length) || + ch_offset < offsetof(struct smb2_read_req, Buffer) || + ch_offset > req_len || ch_len > req_len - ch_offset) { err = -EINVAL; goto out; } - err = smb2_set_remote_key_for_rdma(work, - (struct smbdirect_buffer_descriptor_v1 *) - ((char *)req + ch_offset), - req->Channel, - req->ReadChannelInfoLength); + desc = (struct smbdirect_buffer_descriptor_v1 *) + ((char *)req + ch_offset); + err = check_rdma_desc(desc, ch_len, le32_to_cpu(req->Length)); + if (err) + goto out; + err = smb2_set_rdma_key(work, desc, + req->Channel, + req->ReadChannelInfoLength); if (err) goto out; } @@ -8753,10 +9092,19 @@ int smb2_read(struct ksmbd_work *work) nbytes, offset, mincount); if (is_rdma_channel == true) { + rdma_transform_len = smb2_prep_rdma_read(work, req, + rsp, + aux_payload_buf, + nbytes); + if (rdma_transform_len < 0) { + kvfree(aux_payload_buf); + err = rdma_transform_len; + goto out; + } /* write data to the client using rdma channel */ - remain_bytes = smb2_read_rdma_channel(work, req, - aux_payload_buf, - nbytes); + remain_bytes = smb2_read_rdma(work, req, + aux_payload_buf, + nbytes); kvfree(aux_payload_buf); aux_payload_buf = NULL; nbytes = 0; @@ -8769,11 +9117,13 @@ int smb2_read(struct ksmbd_work *work) rsp->StructureSize = cpu_to_le16(17); rsp->DataOffset = 80; rsp->Reserved = 0; - rsp->DataLength = cpu_to_le32(nbytes); + rsp->DataLength = cpu_to_le32(rdma_transform_len ?: nbytes); rsp->DataRemaining = cpu_to_le32(remain_bytes); - rsp->Flags = 0; + rsp->Flags = rdma_transform_len ? + SMB2_READFLAG_RESPONSE_RDMA_TRANSFORM : 0; err = ksmbd_iov_pin_rsp_read(work, (void *)rsp, - offsetof(struct smb2_read_rsp, Buffer), + offsetof(struct smb2_read_rsp, Buffer) + + rdma_transform_len, aux_payload_buf, nbytes); if (err) { kvfree(aux_payload_buf); @@ -8885,10 +9235,28 @@ static noinline int smb2_write_pipe(struct ksmbd_work *work) return err; } -static ssize_t smb2_write_rdma_channel(struct ksmbd_work *work, - struct smb2_write_req *req, - struct ksmbd_file *fp, - loff_t offset, size_t length, bool sync) +/** + * smb2_write_rdma() - receive and store an RDMA WRITE payload + * @work: request work item + * @desc: client RDMA buffer descriptors + * @desc_len: descriptor array length + * @transform: parsed transform, or NULL for an untransformed transfer + * @fp: target open file + * @offset: target file offset + * @length: transfer length + * @sync: request synchronous storage completion + * + * Receive the payload, authenticate or decrypt it when required, and write it + * to the target file. + * + * Return: written byte count on success, otherwise a negative errno + */ +static ssize_t smb2_write_rdma(struct ksmbd_work *work, + struct smbdirect_buffer_descriptor_v1 *desc, + unsigned int desc_len, + struct smb2_rdma_write_transform *transform, + struct ksmbd_file *fp, loff_t offset, + size_t length, bool sync) { char *data_buf; int ret; @@ -8898,15 +9266,27 @@ static ssize_t smb2_write_rdma_channel(struct ksmbd_work *work, if (!data_buf) return -ENOMEM; - ret = ksmbd_conn_rdma_read(work->conn, data_buf, length, - (struct smbdirect_buffer_descriptor_v1 *) - ((char *)req + le16_to_cpu(req->WriteChannelInfoOffset)), - le16_to_cpu(req->WriteChannelInfoLength)); + ret = ksmbd_conn_rdma_read(work->conn, data_buf, length, desc, + desc_len); if (ret < 0) { kvfree(data_buf); return ret; } + if (transform && + transform->type == SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION) { + ret = ksmbd_crypt_rdma(work->conn, + work->sess->smb3decryptionkey, + data_buf, length, transform->nonce, + transform->nonce_len, + transform->crypto->Signature, + transform->signature_len, false); + if (ret) { + kvfree(data_buf); + return ret == -ENOMEM ? ret : -EBADMSG; + } + } + ret = ksmbd_vfs_write(work, fp, data_buf, length, &offset, sync, &nbytes); kvfree(data_buf); if (ret < 0) @@ -8925,6 +9305,10 @@ int smb2_write(struct ksmbd_work *work) { struct smb2_write_req *req; struct smb2_write_rsp *rsp; + struct smb2_rdma_write_transform rdma_transform = {}; + struct smb2_rdma_write_transform *rdma_info = NULL; + struct smbdirect_buffer_descriptor_v1 *rdma_desc = NULL; + unsigned int rdma_desc_len = 0; struct ksmbd_file *fp = NULL; loff_t offset; size_t length; @@ -8969,8 +9353,21 @@ int smb2_write(struct ksmbd_work *work) } length = le32_to_cpu(req->Length); + if (req->Channel != SMB2_CHANNEL_NONE && + req->Channel != SMB2_CHANNEL_RDMA_V1 && + req->Channel != SMB2_CHANNEL_RDMA_V1_INVALIDATE && + req->Channel != SMB2_CHANNEL_RDMA_TRANSFORM) { + err = -EINVAL; + goto out; + } + if (req->Channel == SMB2_CHANNEL_RDMA_TRANSFORM && + work->conn->dialect != SMB311_PROT_ID) { + err = -EINVAL; + goto out; + } if (req->Channel == SMB2_CHANNEL_RDMA_V1 || - req->Channel == SMB2_CHANNEL_RDMA_V1_INVALIDATE) { + req->Channel == SMB2_CHANNEL_RDMA_V1_INVALIDATE || + req->Channel == SMB2_CHANNEL_RDMA_TRANSFORM) { is_rdma_channel = true; max_write_size = get_smbd_max_read_write_size(work->conn->transport); if (max_write_size == 0) { @@ -8995,17 +9392,37 @@ int smb2_write(struct ksmbd_work *work) if (is_rdma_channel == true) { unsigned int ch_offset = le16_to_cpu(req->WriteChannelInfoOffset); + unsigned int ch_len = le16_to_cpu(req->WriteChannelInfoLength); + unsigned int req_len = smb2_current_req_len(work, &req->hdr); - if (req->Length != 0 || req->DataOffset != 0 || - ch_offset < offsetof(struct smb2_write_req, Buffer)) { + if (!length || req->Length != 0 || req->DataOffset != 0 || + ch_offset < offsetof(struct smb2_write_req, Buffer) || + ch_offset > req_len || ch_len > req_len - ch_offset) { err = -EINVAL; goto out; } - err = smb2_set_remote_key_for_rdma(work, - (struct smbdirect_buffer_descriptor_v1 *) - ((char *)req + ch_offset), - req->Channel, - req->WriteChannelInfoLength); + if (req->Channel == SMB2_CHANNEL_RDMA_TRANSFORM) { + err = smb2_parse_rdma_write_transform(work, req, + &rdma_transform); + if (err) + goto out; + rdma_desc = rdma_transform.desc; + rdma_desc_len = rdma_transform.desc_len; + rdma_info = &rdma_transform; + err = smb2_set_rdma_key(work, rdma_desc, + rdma_transform.channel, + cpu_to_le16(rdma_desc_len)); + } else { + rdma_desc = (struct smbdirect_buffer_descriptor_v1 *) + ((char *)req + ch_offset); + rdma_desc_len = ch_len; + err = check_rdma_desc(rdma_desc, rdma_desc_len, length); + if (err) + goto out; + err = smb2_set_rdma_key(work, rdma_desc, + req->Channel, + req->WriteChannelInfoLength); + } if (err) goto out; } @@ -9074,8 +9491,9 @@ int smb2_write(struct ksmbd_work *work) /* read data from the client using rdma channel, and * write the data. */ - nbytes = smb2_write_rdma_channel(work, req, fp, offset, length, - writethrough); + nbytes = smb2_write_rdma(work, rdma_desc, rdma_desc_len, + rdma_info, fp, offset, length, + writethrough); if (nbytes < 0) { err = (int)nbytes; goto out; @@ -9112,6 +9530,10 @@ int smb2_write(struct ksmbd_work *work) rsp->hdr.Status = STATUS_SHARING_VIOLATION; else if (err == -EINVAL) rsp->hdr.Status = STATUS_INVALID_PARAMETER; + else if (err == -EBADMSG) + rsp->hdr.Status = STATUS_AUTH_TAG_MISMATCH; + else if (err == -EKEYREJECTED) + rsp->hdr.Status = STATUS_INVALID_SIGNATURE; else if (rsp->hdr.Status == 0) rsp->hdr.Status = STATUS_INVALID_HANDLE; diff --git a/fs/smb/server/transport_rdma.c b/fs/smb/server/transport_rdma.c index 85d12c4c354c..ee28a4d1cc86 100644 --- a/fs/smb/server/transport_rdma.c +++ b/fs/smb/server/transport_rdma.c @@ -76,6 +76,8 @@ static int smb_direct_max_receive_size = 1364; static int smb_direct_max_read_write_size = SMBD_DEFAULT_IOSIZE; +static bool smb_direct_enabled; + static struct smb_direct_listener { int port; @@ -512,18 +514,26 @@ int ksmbd_rdma_init(void) ksmbd_debug(RDMA, "iWarp RDMA listener. socket=%p\n", smb_direct_iw_listener.socket); + WRITE_ONCE(smb_direct_enabled, true); return 0; err: + WRITE_ONCE(smb_direct_enabled, false); ksmbd_rdma_stop_listening(); return ret; } void ksmbd_rdma_stop_listening(void) { + WRITE_ONCE(smb_direct_enabled, false); smb_direct_listener_destroy(&smb_direct_ib_listener); smb_direct_listener_destroy(&smb_direct_iw_listener); } +bool ksmbd_rdma_enabled(void) +{ + return READ_ONCE(smb_direct_enabled); +} + bool ksmbd_rdma_capable_netdev(struct net_device *netdev) { u8 node_type = smbdirect_netdev_rdma_capable_node_type(netdev); diff --git a/fs/smb/server/transport_rdma.h b/fs/smb/server/transport_rdma.h index 8b78917a1795..23247713b5c3 100644 --- a/fs/smb/server/transport_rdma.h +++ b/fs/smb/server/transport_rdma.h @@ -14,12 +14,14 @@ #ifdef CONFIG_SMB_SERVER_SMBDIRECT int ksmbd_rdma_init(void); void ksmbd_rdma_stop_listening(void); +bool ksmbd_rdma_enabled(void); bool ksmbd_rdma_capable_netdev(struct net_device *netdev); void init_smbd_max_io_size(unsigned int sz); unsigned int get_smbd_max_read_write_size(struct ksmbd_transport *kt); #else static inline int ksmbd_rdma_init(void) { return 0; } static inline void ksmbd_rdma_stop_listening(void) { } +static inline bool ksmbd_rdma_enabled(void) { return false; } static inline bool ksmbd_rdma_capable_netdev(struct net_device *netdev) { return false; } static inline void init_smbd_max_io_size(unsigned int sz) { } static inline unsigned int get_smbd_max_read_write_size(struct ksmbd_transport *kt) { return 0; } From f7157d148d05a800a9a24028266f8f821f8b2b98 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 17 Aug 2026 09:32:06 +0900 Subject: [PATCH 131/142] ksmbd: make RDMA encryption diagnostics conditional The temporary RDMA encryption diagnostics logged every SMB3 request and successful payload operation with pr_err(), which made normal traffic too noisy. Keep only negotiation, RDMA READ preparation, RDMA WRITE transform metadata, crypto completion, and final transfer completion messages as KSMBD_DEBUG_RDMA diagnostics. Keep error reports for malformed metadata, crypto, RDMA transfer, and file write failures at error level. This preserves the diagnostics needed to verify RDMA transform operation without flooding the kernel error log during normal I/O. Signed-off-by: Namjae Jeon --- fs/smb/server/auth.c | 33 +++++++++++++++--- fs/smb/server/smb2pdu.c | 75 +++++++++++++++++++++++++++++++++-------- 2 files changed, 90 insertions(+), 18 deletions(-) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index db362c64af8d..9f3151a9f379 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -911,26 +911,42 @@ int ksmbd_crypt_rdma(struct ksmbd_conn *conn, const u8 *key, unsigned int iv_len, crypt_len; u8 auth_tag[SMB2_SIGNATURE_SIZE] = {}; u8 *iv = NULL; + u16 cipher = le16_to_cpu(conn->cipher_type); int rc; DECLARE_CRYPTO_WAIT(wait); - if (!buflen || !tag_len || tag_len > SMB2_SIGNATURE_SIZE) + if (!buflen || !tag_len || tag_len > SMB2_SIGNATURE_SIZE) { + pr_err("RDMA %s rejected: cipher=0x%04x payload=%u nonce=%u tag=%u\n", + enc ? "encryption" : "decryption", cipher, buflen, + nonce_len, tag_len); return -EINVAL; + } if (!enc) memcpy(auth_tag, tag, tag_len); if (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) { - if (nonce_len != SMB3_AES_GCM_NONCE) + if (nonce_len != SMB3_AES_GCM_NONCE) { + pr_err("RDMA %s rejected: cipher=0x%04x invalid nonce=%u expected=%u\n", + enc ? "encryption" : "decryption", cipher, + nonce_len, SMB3_AES_GCM_NONCE); return -EINVAL; + } ctx = ksmbd_crypto_ctx_find_gcm(); } else { - if (nonce_len != SMB3_AES_CCM_NONCE) + if (nonce_len != SMB3_AES_CCM_NONCE) { + pr_err("RDMA %s rejected: cipher=0x%04x invalid nonce=%u expected=%u\n", + enc ? "encryption" : "decryption", cipher, + nonce_len, SMB3_AES_CCM_NONCE); return -EINVAL; + } ctx = ksmbd_crypto_ctx_find_ccm(); } - if (!ctx) + if (!ctx) { + pr_err("RDMA %s failed: cipher=0x%04x crypto context unavailable\n", + enc ? "encryption" : "decryption", cipher); return -ENOMEM; + } tfm = (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) ? @@ -988,6 +1004,15 @@ int ksmbd_crypt_rdma(struct ksmbd_conn *conn, const u8 *key, kfree(sg); aead_request_free(req); ksmbd_release_crypto_ctx(ctx); + if (rc) + pr_err("RDMA %s failed: cipher=0x%04x payload=%u nonce=%u tag=%u rc=%d\n", + enc ? "encryption" : "decryption", cipher, buflen, + nonce_len, tag_len, rc); + else + ksmbd_debug(RDMA, + "RDMA %s completed: cipher=0x%04x payload=%u nonce=%u tag=%u\n", + enc ? "encryption" : "decryption", cipher, buflen, + nonce_len, tag_len); return rc; } diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index b48eff02dbf8..bd74b45ce0e7 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -1904,6 +1904,13 @@ int smb2_handle_negotiate(struct ksmbd_work *work) if (!conn->cipher_type) conn->rdma_transform_ids &= ~BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION); + ksmbd_debug(RDMA, + "RDMA transform negotiation: transport=%s context=%s encryption=%s cipher=0x%04x\n", + conn->transport->ops->rdma_read ? "rdma" : "tcp", + conn->rdma_transform_negotiated ? "present" : "absent", + conn->rdma_transform_ids & + BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION) ? "enabled" : "disabled", + le16_to_cpu(conn->cipher_type)); rc = init_smb3_11_server(conn); if (rc < 0) { @@ -8719,15 +8726,14 @@ static int smb2_prep_rdma_read(struct ksmbd_work *work, u16 transform_type; int err; - if (work->encrypted && - (conn->rdma_transform_ids & BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION))) { - transform_type = SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION; - nonce_len = (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || - conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) ? - SMB3_AES_GCM_NONCE : SMB3_AES_CCM_NONCE; - } else { + if (!work->encrypted || + !(conn->rdma_transform_ids & BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION))) return 0; - } + + transform_type = SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION; + nonce_len = (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) ? + SMB3_AES_GCM_NONCE : SMB3_AES_CCM_NONCE; transform = (struct smb2_rdma_transform *)rsp->Buffer; crypto = (struct smb2_rdma_crypto_transform *)(transform + 1); @@ -8747,13 +8753,20 @@ static int smb2_prep_rdma_read(struct ksmbd_work *work, data, datalen, nonce, nonce_len, crypto->Signature, SMB2_SIGNATURE_SIZE, true); - if (err) + if (err) { + pr_err("RDMA READ encryption failed: session=%llu payload=%u rc=%d\n", + work->sess->id, datalen, err); return err; + } transform_len = sizeof(*transform) + sizeof(*crypto) + SMB2_SIGNATURE_SIZE + nonce_len; rsp->Flags = SMB2_READFLAG_RESPONSE_RDMA_TRANSFORM; rsp->DataLength = cpu_to_le32(transform_len); + ksmbd_debug(RDMA, + "RDMA READ encryption prepared: session=%llu cipher=0x%04x payload=%u transform=%u nonce=%u tag=%u\n", + work->sess->id, le16_to_cpu(conn->cipher_type), datalen, + transform_len, nonce_len, SMB2_SIGNATURE_SIZE); return transform_len; } @@ -8830,6 +8843,7 @@ static int smb2_parse_rdma_write_transform(struct ksmbd_work *work, unsigned int offset = le16_to_cpu(req->WriteChannelInfoOffset); unsigned int length = le16_to_cpu(req->WriteChannelInfoLength); unsigned int desc_offset, desc_len, crypto_len, expected_desc_offset; + int err; if (!work->conn->rdma_transform_ids || offset < offsetof(struct smb2_write_req, Buffer) || @@ -8894,8 +8908,18 @@ static int smb2_parse_rdma_write_transform(struct ksmbd_work *work, info->crypto = crypto; info->nonce = crypto->Signature + info->signature_len; info->channel = transform->Channel; - return check_rdma_desc(info->desc, info->desc_len, - le32_to_cpu(req->RemainingBytes)); + err = check_rdma_desc(info->desc, info->desc_len, + le32_to_cpu(req->RemainingBytes)); + if (err) + return err; + + ksmbd_debug(RDMA, + "RDMA WRITE encryption metadata: session=%llu cipher=0x%04x payload=%u channel=0x%x descriptors=%zu nonce=%u tag=%u\n", + work->sess->id, le16_to_cpu(work->conn->cipher_type), + le32_to_cpu(req->RemainingBytes), le32_to_cpu(info->channel), + info->desc_len / sizeof(*info->desc), info->nonce_len, + info->signature_len); + return 0; } /** @@ -9105,6 +9129,15 @@ int smb2_read(struct ksmbd_work *work) remain_bytes = smb2_read_rdma(work, req, aux_payload_buf, nbytes); + if (remain_bytes < 0) + pr_err("RDMA READ transfer failed: session=%llu payload=%zu transform=%d rc=%zd\n", + work->sess ? work->sess->id : 0, nbytes, + rdma_transform_len, remain_bytes); + else + ksmbd_debug(RDMA, + "RDMA READ transfer completed: session=%llu payload=%zu transform=%d\n", + work->sess ? work->sess->id : 0, nbytes, + rdma_transform_len); kvfree(aux_payload_buf); aux_payload_buf = NULL; nbytes = 0; @@ -9269,10 +9302,12 @@ static ssize_t smb2_write_rdma(struct ksmbd_work *work, ret = ksmbd_conn_rdma_read(work->conn, data_buf, length, desc, desc_len); if (ret < 0) { + if (transform) + pr_err("RDMA WRITE encrypted transfer failed: session=%llu payload=%zu rdma_read_rc=%d\n", + work->sess->id, length, ret); kvfree(data_buf); return ret; } - if (transform && transform->type == SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION) { ret = ksmbd_crypt_rdma(work->conn, @@ -9282,6 +9317,8 @@ static ssize_t smb2_write_rdma(struct ksmbd_work *work, transform->crypto->Signature, transform->signature_len, false); if (ret) { + pr_err("RDMA WRITE decryption failed: session=%llu payload=%zu rc=%d\n", + work->sess->id, length, ret); kvfree(data_buf); return ret == -ENOMEM ? ret : -EBADMSG; } @@ -9289,8 +9326,15 @@ static ssize_t smb2_write_rdma(struct ksmbd_work *work, ret = ksmbd_vfs_write(work, fp, data_buf, length, &offset, sync, &nbytes); kvfree(data_buf); - if (ret < 0) + if (ret < 0) { + if (transform) + pr_err("RDMA WRITE encrypted file write failed: session=%llu payload=%zu rc=%d\n", + work->sess->id, length, ret); return ret; + } + ksmbd_debug(RDMA, + "RDMA WRITE transfer completed: session=%llu payload=%zu transformed=%u written=%zd\n", + work->sess ? work->sess->id : 0, length, !!transform, nbytes); return nbytes; } @@ -9404,8 +9448,11 @@ int smb2_write(struct ksmbd_work *work) if (req->Channel == SMB2_CHANNEL_RDMA_TRANSFORM) { err = smb2_parse_rdma_write_transform(work, req, &rdma_transform); - if (err) + if (err) { + pr_err("RDMA WRITE encryption metadata rejected: session=%llu rc=%d\n", + work->sess ? work->sess->id : 0, err); goto out; + } rdma_desc = rdma_transform.desc; rdma_desc_len = rdma_transform.desc_len; rdma_info = &rdma_transform; From 79decd88dd3f0d42e7fb0689b1a7853bfa302459 Mon Sep 17 00:00:00 2001 From: Hang Nan <2122295973@qq.com> Date: Mon, 17 Aug 2026 09:52:45 +0900 Subject: [PATCH 132/142] ksmbd: bound smb_check_perm_dacl() ACE walks by DACL size smb_check_perm_dacl() validates that the DACL fits inside the NT security descriptor, but then bounds its two ACE walks by the remaining NTSD length (acl_size) rather than the DACL's declared size (pdacl_size). When pdacl->size is smaller than the trailing NTSD buffer, bytes after the declared DACL boundary - still inside the stored security descriptor - are parsed as ACEs during access checks. A crafted DACL can place an access-granting ACE beyond pdacl->size, and the current code accepts it during SMB2_CREATE access validation, while parse_dacl() and smb_inherit_dacl() stop at pdacl_size. Bound both ACE walks by pdacl_size to match the DACL boundary semantics used elsewhere in the server. Validation: - semantic KUnit harness shows the post-boundary ACE is selected before the fix and rejected (EACCES) after it - linux master (7.2-rc6), x86_64 Fixes: 8f0541186e9a ("ksmbd: fix heap-based overflow in set_ntacl_dacl()") Signed-off-by: Hang Nan <2122295973@qq.com> Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/smbacl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index b5db6dcfbaa4..8ad2e5a5cca8 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -1494,7 +1494,7 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, if (*pdaccess & FILE_MAXIMAL_ACCESS_LE) { ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); - aces_size = acl_size - sizeof(struct smb_acl); + aces_size = pdacl_size - sizeof(struct smb_acl); for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) { if (aces_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE) @@ -1551,7 +1551,7 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, } ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); - aces_size = acl_size - sizeof(struct smb_acl); + aces_size = pdacl_size - sizeof(struct smb_acl); for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) { if (aces_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE) From 9a74739026fb71d1197183a067eef657bb5fba77 Mon Sep 17 00:00:00 2001 From: Ze Tan Date: Fri, 14 Aug 2026 13:51:41 +0000 Subject: [PATCH 133/142] smb/server: warn if ksmbd_proc_create() fails Print a warning if the sessions procfs entry cannot be created. Signed-off-by: Ze Tan Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/connection.c | 3 ++- fs/smb/server/mgmt/user_session.c | 5 ++++- fs/smb/server/server.c | 8 ++++++-- fs/smb/server/vfs_cache.c | 6 ++++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index d32f4f3cef93..91fdd1ddc61f 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -787,7 +787,8 @@ int ksmbd_conn_transport_init(void) } out: mutex_unlock(&init_lock); - create_proc_clients(); + if (create_proc_clients()) + pr_warn("Unable to create clients procfs entry\n"); return ret; } diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 7e187d20828b..e22c07ea28bd 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -204,6 +204,8 @@ static int create_proc_session(struct ksmbd_session *sess) snprintf(name, sizeof(name), "sessions/%llu", sess->id); sess->proc_entry = ksmbd_proc_create(name, show_proc_session, sess); + if (!sess->proc_entry) + return -ENOMEM; return 0; } @@ -729,7 +731,8 @@ static struct ksmbd_session *__session_create(int protocol) hash_add(sessions_table, &sess->hlist, sess->id); up_write(&sessions_table_lock); - create_proc_session(sess); + if (create_proc_session(sess)) + pr_warn_ratelimited("Unable to create session %llu procfs entry\n", sess->id); ksmbd_counter_inc(KSMBD_COUNTER_SESSIONS); return sess; diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 6cfe8148da85..0069d4e6a60a 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -663,8 +663,12 @@ static int __init ksmbd_server_init(void) ret = ksmbd_proc_init(); if (ret) goto err_unregister; - create_proc_sessions(); - create_proc_shares(); + + if (create_proc_sessions()) + pr_warn("Unable to create sessions procfs entry\n"); + + if (create_proc_shares()) + pr_warn("Unable to create shares procfs entry\n"); ksmbd_server_tcp_callbacks_init(); diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index 413997f393f0..81626d204249 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -180,7 +180,8 @@ static int proc_show_files(struct seq_file *m, void *v) static int create_proc_files(void) { - ksmbd_proc_create("files", proc_show_files, NULL); + if (!ksmbd_proc_create("files", proc_show_files, NULL)) + return -ENOMEM; return 0; } #else @@ -1828,7 +1829,8 @@ void ksmbd_close_session_fds(struct ksmbd_work *work) int ksmbd_init_global_file_table(void) { - create_proc_files(); + if (create_proc_files()) + pr_warn("Unable to create files procfs entry\n"); return ksmbd_init_file_table(&global_ft); } From 99b25b046e47e4904373cfeb445c5483f1633d88 Mon Sep 17 00:00:00 2001 From: Ze Tan Date: Fri, 14 Aug 2026 13:51:42 +0000 Subject: [PATCH 134/142] smb/server: fix session leak in ksmbd_session_register() See the procedure below: smb2_sess_setup ksmbd_smb2_session_create __session_create atomic_set(&sess->refcnt, 2) hash_add(sessions_table, &sess->hlist, sess->id) ksmbd_session_register xa_store(&conn->sessions, sess->id, sess) // fail ksmbd_user_session_put atomic_dec(&sess->refcnt) // refcnt is 1, session is not freed Remove the session from sessions_table and drop its table reference if xa_store() fails. Fixes: f5c779b7ddbd ("ksmbd: fix racy issue from session setup and logoff") Signed-off-by: Ze Tan Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index e22c07ea28bd..6d1292243378 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -451,10 +451,21 @@ static void ksmbd_expire_session(struct ksmbd_conn *conn) int ksmbd_session_register(struct ksmbd_conn *conn, struct ksmbd_session *sess) { + int ret; + sess->dialect = conn->dialect; memcpy(sess->ClientGUID, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); ksmbd_expire_session(conn); - return xa_err(xa_store(&conn->sessions, sess->id, sess, KSMBD_DEFAULT_GFP)); + ret = xa_err(xa_store(&conn->sessions, sess->id, sess, + KSMBD_DEFAULT_GFP)); + if (ret) { + down_write(&sessions_table_lock); + hash_del(&sess->hlist); + up_write(&sessions_table_lock); + ksmbd_user_session_put(sess); + } + + return ret; } static int ksmbd_chann_del(struct ksmbd_conn *conn, struct ksmbd_session *sess) From 492b24b5b651a72ee83a8d481f70246b36192832 Mon Sep 17 00:00:00 2001 From: Ze Tan Date: Fri, 14 Aug 2026 13:51:43 +0000 Subject: [PATCH 135/142] smb/server: update session counter under sessions table lock KSMBD_COUNTER_SESSIONS tracks sessions published in sessions_table. Increment it while holding sessions_table_lock so publishing a session and updating the counter happen together. Fixes: b38f99c1217a ("ksmbd: add procfs interface for runtime monitoring and statistics") Signed-off-by: Ze Tan Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 6d1292243378..087cf968a320 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -740,11 +740,11 @@ static struct ksmbd_session *__session_create(int protocol) down_write(&sessions_table_lock); hash_add(sessions_table, &sess->hlist, sess->id); + ksmbd_counter_inc(KSMBD_COUNTER_SESSIONS); up_write(&sessions_table_lock); if (create_proc_session(sess)) pr_warn_ratelimited("Unable to create session %llu procfs entry\n", sess->id); - ksmbd_counter_inc(KSMBD_COUNTER_SESSIONS); return sess; error: From 7de5cf9bcf96bf2ee2ea2ad1d35a7b950f71161a Mon Sep 17 00:00:00 2001 From: Ze Tan Date: Fri, 14 Aug 2026 13:51:44 +0000 Subject: [PATCH 136/142] smb/server: fix session counter on session removal See the procedure below: smb2_sess_setup ksmbd_smb2_session_create __session_create hash_add(sessions_table, &sess->hlist, sess->id) ksmbd_counter_inc(KSMBD_COUNTER_SESSIONS) ksmbd_conn_handler_loop ksmbd_server_terminate_conn ksmbd_sessions_deregister hash_del(&sess->hlist) // do not decrement KSMBD_COUNTER_SESSIONS KSMBD_COUNTER_SESSIONS tracks sessions published in sessions_table, but session removal does not decrement it. The value therefore keeps growing after sessions are expired, rejected during registration, or removed on the last channel disconnect. Fixes: b38f99c1217a ("ksmbd: add procfs interface for runtime monitoring and statistics") Signed-off-by: Ze Tan Reviewed-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- fs/smb/server/mgmt/user_session.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 087cf968a320..7022d5d656b4 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -413,6 +413,12 @@ void ksmbd_session_destroy(struct ksmbd_session *sess) kfree_sensitive(sess); } +static void ksmbd_session_remove_from_table(struct ksmbd_session *sess) +{ + hash_del(&sess->hlist); + ksmbd_counter_dec(KSMBD_COUNTER_SESSIONS); +} + struct ksmbd_session *__session_lookup(unsigned long long id) { struct ksmbd_session *sess; @@ -439,7 +445,7 @@ static void ksmbd_expire_session(struct ksmbd_conn *conn) time_after(jiffies, sess->last_active + SMB2_SESSION_TIMEOUT))) { xa_erase(&conn->sessions, sess->id); - hash_del(&sess->hlist); + ksmbd_session_remove_from_table(sess); ksmbd_session_destroy(sess); continue; } @@ -460,7 +466,7 @@ int ksmbd_session_register(struct ksmbd_conn *conn, KSMBD_DEFAULT_GFP)); if (ret) { down_write(&sessions_table_lock); - hash_del(&sess->hlist); + ksmbd_session_remove_from_table(sess); up_write(&sessions_table_lock); ksmbd_user_session_put(sess); } @@ -493,7 +499,7 @@ void ksmbd_sessions_deregister(struct ksmbd_conn *conn) hash_for_each_safe(sessions_table, bkt, tmp, sess, hlist) { if (!ksmbd_chann_del(conn, sess) && xa_empty(&sess->ksmbd_chann_list)) { - hash_del(&sess->hlist); + ksmbd_session_remove_from_table(sess); down_write(&conn->session_lock); xa_erase(&conn->sessions, sess->id); up_write(&conn->session_lock); @@ -507,7 +513,7 @@ void ksmbd_sessions_deregister(struct ksmbd_conn *conn) ksmbd_chann_del(conn, sess); if (xa_empty(&sess->ksmbd_chann_list)) { xa_erase(&conn->sessions, sess->id); - hash_del(&sess->hlist); + ksmbd_session_remove_from_table(sess); if (atomic_dec_and_test(&sess->refcnt)) ksmbd_session_destroy(sess); } From 56549c18a4f75309161ca780feaa4d17904e3ee9 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Wed, 19 Aug 2026 10:01:11 +0900 Subject: [PATCH 137/142] ksmbd: enable TCP keepalive for accepted connections A client that disappears without sending a FIN or RST can leave its ksmbd connection in ESTABLISHED indefinitely. ksmbd sets a socket receive timeout, but the connection receive loop retries timeout errors without a limit, so the connection remains in conn_list and consumes the per-IP connection quota. Enable SO_KEEPALIVE on accepted TCP sockets so the TCP stack can detect a silent peer failure. The keepalive idle time, interval, and probe count remain controlled by the existing TCP sysctl settings. Signed-off-by: Namjae Jeon --- fs/smb/server/transport_tcp.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index 0ae5f145a332..921e4e86d01c 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -292,6 +292,12 @@ static int ksmbd_kthread_fn(void *p) ksmbd_debug(CONN, "connect success: accepted new connection\n"); client_sk->sk->sk_rcvtimeo = KSMBD_TCP_RECV_TIMEOUT; client_sk->sk->sk_sndtimeo = KSMBD_TCP_SEND_TIMEOUT; + /* + * Detect peers that disappear without sending a FIN or RST. + * Otherwise the connection handler can retry receive timeouts + * indefinitely and keep the connection in conn_list. + */ + sock_set_keepalive(client_sk->sk); ksmbd_tcp_new_connection(client_sk); } From 80b6367ec37faf61fbd11aae6fae64c51faa5bba Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 20 Aug 2026 08:26:03 +0900 Subject: [PATCH 138/142] ksmbd: keep TCP timers alive for kernel sockets ksmbd creates its listening socket with sock_create_kern(). Kernel sockets do not hold a network namespace reference by default. Accepted sockets inherit this state. When an accepted socket is released, tcp_close() clears its pending TCP timers for a kernel socket after the socket enters an orphaned state. If the peer is unreachable while ksmbd sends a FIN, this can leave a FIN-WAIT-1 orphan without a retransmission timer. Upgrade the listening socket's network namespace reference before kernel_listen(). Accepted sockets inherit the reference, so the TCP stack can keep the retransmission timer active and apply its normal orphan retry policy. Preserve the existing graceful shutdown behavior. Link: https://github.com/openwrt/openwrt/issues/24744 Signed-off-by: Namjae Jeon --- fs/smb/server/transport_tcp.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index 921e4e86d01c..832e93084605 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -523,6 +523,12 @@ static int create_socket(struct interface *iface) goto out_error; } + /* + * Accepted sockets inherit the listener's net reference. Keep TCP + * timers alive after a kernel socket is released. + */ + sk_net_refcnt_upgrade(ksmbd_socket->sk); + ret = kernel_listen(ksmbd_socket, KSMBD_SOCKET_BACKLOG); if (ret) { pr_err("Port listen() error: %d\n", ret); From ed91d80242358ffdf127a34e3b7c9fc445c9e5d1 Mon Sep 17 00:00:00 2001 From: Thomas Huth Date: Thu, 20 Aug 2026 08:58:15 +0200 Subject: [PATCH 139/142] smb: server: Remove obsolete "select CRYPTO_LIB_DES" from Kconfig file The DES encryption in the smb server code has been removed in 2021 with the removal of the insecure NTLMv1 authentication code. Thus we don't need this "select" statement here anymore. Fixes: ce812992f239f ("ksmbd: remove NTLMv1 authentication") Signed-off-by: Thomas Huth Signed-off-by: Namjae Jeon --- fs/smb/server/Kconfig | 1 - 1 file changed, 1 deletion(-) diff --git a/fs/smb/server/Kconfig b/fs/smb/server/Kconfig index 08d8b7a965a6..221ec9717a83 100644 --- a/fs/smb/server/Kconfig +++ b/fs/smb/server/Kconfig @@ -9,7 +9,6 @@ config SMB_SERVER select CRYPTO select CRYPTO_LIB_AES_CBC_MACS select CRYPTO_LIB_ARC4 - select CRYPTO_LIB_DES select CRYPTO_LIB_MD5 select CRYPTO_LIB_SHA256 select CRYPTO_LIB_SHA512 From c5e640fe346177372ff4a51a45b01fcd48c29207 Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 20 Aug 2026 16:43:15 +0900 Subject: [PATCH 140/142] smb: server: remove unused DES crypto header The DES crypto header is no longer used after the removal of NTLMv1 authentication. Remove it now that the server no longer selects CRYPTO_LIB_DES. Fixes: ce812992f239 ("ksmbd: remove NTLMv1 authentication") Signed-off-by: Namjae Jeon --- fs/smb/server/auth.c | 1 - 1 file changed, 1 deletion(-) diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index 9f3151a9f379..5ec43e32de3e 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -24,7 +24,6 @@ #include #include -#include #include "server.h" #include "smb_common.h" From 13b1d8a99687122faa13f246cbd6fa9abe2ac562 Mon Sep 17 00:00:00 2001 From: ChenXiaoSong Date: Wed, 19 Aug 2026 07:12:24 +0000 Subject: [PATCH 141/142] MAINTAINERS: add myself as KSMBD reviewer I and my team have been working on KSMBD development, and I am also interested in helping maintain this code. Signed-off-by: ChenXiaoSong Signed-off-by: Namjae Jeon --- MAINTAINERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 8014b9f8253e..a0e04c0ba453 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14138,6 +14138,8 @@ M: Steve French M: Steve French R: Sergey Senozhatsky R: Tom Talpey +R: ChenXiaoSong +R: ChenXiaoSong L: linux-cifs@vger.kernel.org S: Maintained T: git https://git.samba.org/ksmbd.git ksmbd-for-next From 4c6320e0ad400d4ee41cfde614c05a0d87f54e1b Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Thu, 20 Aug 2026 22:40:22 +0900 Subject: [PATCH 142/142] MAINTAINERS: update ksmbd repository URL Update the ksmbd repository URL to the kernel.org ksmbd-for-next branch. Signed-off-by: Namjae Jeon --- MAINTAINERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index a0e04c0ba453..a2d6c6e92314 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14142,7 +14142,7 @@ R: ChenXiaoSong R: ChenXiaoSong L: linux-cifs@vger.kernel.org S: Maintained -T: git https://git.samba.org/ksmbd.git ksmbd-for-next +T: git git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/smb.git ksmbd-for-next F: Documentation/filesystems/smb/ksmbd.rst F: fs/smb/common/ F: fs/smb/server/